├── .gitignore ├── COPYING ├── INSTALL.rst ├── MANIFEST.in ├── NEWS.rst ├── README.rst ├── doc ├── _static │ └── dummy ├── architecture │ ├── abc.rst │ ├── device-server.rst │ ├── gui.rst │ ├── index.rst │ ├── supported-devices.rst │ └── triggers.rst ├── authors.rst ├── conf.py ├── examples │ ├── index.rst │ └── time-lapse-example-code.py ├── get-involved │ ├── dev-install.rst │ ├── hacking.rst │ ├── index.rst │ ├── maintaining.rst │ └── new-device.rst ├── getting-started.rst ├── index.rst ├── install.rst └── news.rst ├── microscope ├── __init__.py ├── _utils.py ├── _wrappers │ ├── BMC.py │ ├── __init__.py │ ├── asdk.py │ ├── dcamapi4.py │ └── mirao52e.py ├── abc.py ├── cameras │ ├── _SDK3.py │ ├── _SDK3Cam.py │ ├── __init__.py │ ├── andorsdk3.py │ ├── atmcd.py │ ├── hamamatsu.py │ ├── picamera.py │ ├── pvcam.py │ └── ximea.py ├── clients.py ├── controllers │ ├── __init__.py │ ├── asi.py │ ├── coolled.py │ ├── ludl.py │ ├── lumencor.py │ ├── prior.py │ ├── toptica.py │ └── zaber.py ├── device_server.py ├── devices.py ├── deviceserver.py ├── digitalio │ ├── __init__.py │ └── raspberrypi.py ├── filterwheels │ ├── __init__.py │ ├── aurox.py │ └── thorlabs.py ├── gui.py ├── lasers │ ├── __init__.py │ ├── cobolt.py │ ├── deepstar.py │ ├── obis.py │ ├── sapphire.py │ └── toptica.py ├── lights │ ├── __init__.py │ ├── cobolt.py │ ├── deepstar.py │ ├── obis.py │ ├── sapphire.py │ └── toptica.py ├── mirror │ ├── __init__.py │ ├── alpao.py │ ├── bmc.py │ └── mirao52e.py ├── simulators │ ├── __init__.py │ └── stage_aware_camera.py ├── stages │ ├── __init__.py │ └── linkam.py ├── testsuite │ ├── __init__.py │ ├── devices.py │ ├── hardware.py │ ├── mock_devices.py │ ├── test_client.py │ ├── test_device_server.py │ ├── test_devices.py │ └── test_settings.py ├── valuelogger │ ├── __init__.py │ └── raspberrypi.py └── win32.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | ## Some people use MacOS 4 | .DS_Store 5 | 6 | ## Some text editors often create these 7 | ~.* 8 | *~ 9 | .#* 10 | 11 | ## Files generated by setuptools and friends 12 | /.eggs 13 | /microscope.egg-info 14 | /build 15 | /dist 16 | /.tox 17 | /.mypy_cache 18 | /.pytest_cache 19 | 20 | ## Files generated by sphinx-apidoc during documentation build 21 | /doc/api/ 22 | -------------------------------------------------------------------------------- /INSTALL.rst: -------------------------------------------------------------------------------- 1 | Microscope is available on the Python Package Index (PyPI) and can be 2 | `installed like any other Python package 3 | `_. The 4 | short version of it is to "use pip":: 5 | 6 | pip install microscope 7 | 8 | You need to have Python and pip already installed on your system. 9 | 10 | For details on system requirements, dependencies, and system specific 11 | instructions see `doc/install.rst`. For details on installing from 12 | development sources see `doc/get-involved/doc-install.rst`. 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # We list here all extra non-standard files to be included in source 2 | # distributions. We list *all*, even those that setuptools picks up 3 | # automatically --- at the moment setuptools picks up COPYING and 4 | # README.rst and ignores NEWS.rst and INSTALL.rst. We list them all 5 | # because 1) we never know when setuptools changes their mind on what 6 | # files to include by default; 2) we may accidentally use an older 7 | # version of setuptools; or 3) we may move away from setuptools to 8 | # another build tool. 9 | # 10 | # Note we do not use package_data from setuptools. That is for files 11 | # that will be included in the binary distribution, i.e., needed at 12 | # runtime. We only want these files in the source distribution, they 13 | # are for user information. 14 | 15 | include COPYING 16 | include NEWS.rst 17 | include README.rst 18 | include INSTALL.rst 19 | 20 | graft doc 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python-Microscope 2 | ***************** 3 | 4 | .. image:: https://github.com/python-microscope/python-microscope.org/raw/main/_static/microscope-logo-96-dpi.png 5 | :align: center 6 | :alt: Python-Microscope logo 7 | 8 | Python's ``microscope`` package is a free and open source library for: 9 | 10 | * control of local and remote microscope devices; 11 | * aggregation of microscope devices into complex microscopes; 12 | * automate microscope experiments with hardware triggers. 13 | 14 | It is aimed at those that are building their own microscopes or want 15 | programmatic control for microscope experiments. More details can be 16 | found in the paper `Python-Microscope: High performance control of 17 | arbitrarily complex and scalable bespoke microscopes 18 | `__ and 19 | in the `online documentation `__. 20 | 21 | Python Microscope source distribution are available in `PyPI 22 | `__ and can be easily 23 | installed with ``pip``:: 24 | 25 | pip install microscope 26 | 27 | Alternatively, the development sources are available on `github 28 | `__. 29 | 30 | This package does *not* provide a graphical user interface that a 31 | typical microscope user would expect. Instead, it provides the 32 | foundation over which such interfaces can be built. For a microscope 33 | graphical user interface in Python consider using `Microscope-Cockpit 34 | `__. 35 | -------------------------------------------------------------------------------- /doc/_static/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/doc/_static/dummy -------------------------------------------------------------------------------- /doc/architecture/abc.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | .. _ABCs: 8 | 9 | ABCs 10 | **** 11 | 12 | At the core of Microscope are the Abstract Base Classes (ABC) for the 13 | different device types. ABCs are a way to enforce a defined interface 14 | by requiring those subclassing from them, the concrete classes, to 15 | implement the declared abstract methods. For example, 16 | :class:`TopticaiBeam ` is a 17 | concrete implementation of the :class:`LightSource 18 | ` ABC and so is forced to implement a 19 | series of methods, marked as abstract on the ABC, thus "promising" 20 | that it works like all other devices that implement the 21 | ``LightSource`` ABC. 22 | 23 | Microscope has the following ABCs that map to a specific device type: 24 | 25 | * :class:`microscope.abc.Camera` 26 | * :class:`microscope.abc.Controller` 27 | * :class:`microscope.abc.DeformableMirror` 28 | * :class:`microscope.abc.FilterWheel` 29 | * :class:`microscope.abc.LightSource` 30 | * :class:`microscope.abc.Stage` 31 | * :class:`microscope.abc.DigitalIO` 32 | 33 | In addition, they all subclass from :class:`microscope.abc.Device` 34 | which defines a base interface to all devices such as the 35 | :meth:`shutdown ` method. 36 | 37 | There is an additional special device class 38 | :class:`microscope.abc.DataDevice` which defines a class that has the 39 | ability to asynchronously send data back to the calling 40 | connection. This is used for situations like cameras and asynchronous 41 | communication such as digital input signals. 42 | 43 | The actual concrete classes, those which provide actual control over 44 | the devices, are listed on the section :ref:`supported-devices`. 45 | 46 | In addition to the different device ABC, there is 47 | :class:`microscope.abc.StageAxis` which is not a device on its own but 48 | are device specific and returned by ``Stage`` instances to control the 49 | individual axis. 50 | 51 | Finally, :class:`microscope.abc.TriggerTargetMixin` is an ABC that is 52 | mixed in other classes to add support for hardware triggers. 53 | 54 | .. once we write the section on hardware triggers we should link it 55 | here. 56 | 57 | Settings 58 | ======== 59 | 60 | Many microscope devices have specialised features which, when not 61 | unique, are very specific to the hardware. For example, some cameras 62 | have the option of applying noise filters during acquisition or 63 | provide control over amplifiers in the different stages of the 64 | readout. Being so specialised, such features do not fit in the ABC of 65 | their device type. To supported these features, Microscope has the 66 | concept of "Settings". 67 | 68 | Settings map a name, such as `"TemperatureSetPoint"` or 69 | `"PREAMP_DELAY"`, to their setters and getters which act at the lowest 70 | level available. Those getter/setter are not exposed and only 71 | available via the ``get_setting`` and ``set_setting`` methods, like 72 | so: 73 | 74 | .. code-block:: python 75 | 76 | camera.get_setting("TemperatureSetPoint") 77 | # Some settings are readonly, so check first 78 | if not camera.describe_setting("TemperatureSetPoint")["readonly"]: 79 | camera.set_setting("TemperatureSetPoint", -5) 80 | 81 | Settings often overlap with the defined interface. For example, 82 | ``PVCamera`` instances have the ``binning`` property as defined on the 83 | ``Camera`` ABC, but if supported by the hardware they will also have 84 | the `"BINNING_PAR"` and `"BINNING_SER"` settings which effectively do 85 | the same. 86 | 87 | The use of settings is a powerful feature that provides a more direct 88 | access to the device but this comes at the cost of reduced 89 | interoperability, i.e., code written using settings becomes tied to 90 | that specific hardware which makes it hard to later replace the device 91 | with a different one. In addition, settings also bypass the rest of 92 | the device code and it is possible for settings to lead a device into 93 | an unknown state. Once settings are used, there is no more promise on 94 | the behaviour of the device interface. If possible, avoid use of 95 | settings and prefer methods defined by the ABC. 96 | 97 | An alternative to the current settings scheme would be to declare a 98 | method for each setting on the concrete device classes. There are a 99 | few reasons not to. First, many classes support a wide range of 100 | models, for example, ``AndorSDK3`` supports all of Andor CMOS cameras, 101 | and different models have different settings which would lead to 102 | multiple classes with different sets of methods. Second, some of 103 | those settings would clash with the ABC, for example, ``AndorAtmcd`` 104 | devices might have a ``"Binning"`` setting which could clash with the 105 | ``binning`` property. Finally, using ``get_setting`` and 106 | ``set_setting`` clearly declares the use of methods that are not part 107 | of the interface and reminds the implications that come with it. 108 | -------------------------------------------------------------------------------- /doc/architecture/device-server.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | .. _device-server: 8 | 9 | Device Server 10 | ************* 11 | 12 | Microscope has been designed from the start to support remote devices 13 | where each device is on its own separate server. These separate 14 | servers may be in different computers or they can be different daemons 15 | (or services) in the same computer. In this architecture, a program 16 | that wants to control the device becomes a client and connects to the 17 | device server. A program that controls multiple devices, such as 18 | `Cockpit `_, connects to multiple servers one per 19 | device. This client-server architecture to control a microscope has a 20 | series of advantages: 21 | 22 | - having each device on its own separate daemon means that each runs 23 | on its own Python process and so are not blocked by `Python GIL 24 | `_ 25 | 26 | - enables distribution of devices with hard requirements over multiple 27 | computers. This is typically done when there are too many cameras 28 | acquiring images at high speed and IO becomes a bottleneck. 29 | 30 | - possible to have devices with incompatible requirements, e.g., a 31 | camera that only works in Linux with a deformable mirror that only 32 | works in Windows. 33 | 34 | .. todo:: 35 | 36 | add figures to explain device server (figure from the paper). 37 | 38 | The ``device-server`` program 39 | ============================= 40 | 41 | The ``device-server`` program is part of the Microscope installation. 42 | It can started from the command line with a configuration file 43 | defining the devices to be served, like so: 44 | 45 | .. code-block:: bash 46 | 47 | device-server PATH-TO-CONFIGURATION-FILE 48 | # alternatively, if scripts were not installed: 49 | python3 -m microscope.device_server PATH-TO-CONFIGURATION-FILE 50 | 51 | where the configuration file is a Python script that declares the 52 | devices to be constructed and served on its ``DEVICES`` attribute via 53 | device definitions. A device definition is created with the 54 | :func:`microscope.device_server.device` function. For example: 55 | 56 | .. code-block:: python 57 | 58 | # Serve two test cameras, each on their own process. 59 | from microscope.device_server import device 60 | from microscope.simulators import SimulatedCamera 61 | 62 | DEVICES = [ 63 | device(SimulatedCamera, host="127.0.0.1", port=8000), 64 | device(SimulatedCamera, host="127.0.0.1", port=8001) 65 | ] 66 | 67 | The example above creates two device servers, each on their own python 68 | process and listening on different ports. If the class requires 69 | arguments to construct the device, these must be passed as separate 70 | keyword arguments, like so: 71 | 72 | .. code-block:: python 73 | 74 | from microscope.device_server import device 75 | from microscope.simulators import SimulatedFilterWheel 76 | 77 | DEVICES = [ 78 | # The device will be constructed with `SimulatedFilterWheel(**conf)` 79 | # i.e., `SimulatedFilterWheel(positions=6)` 80 | device( 81 | SimulatedFilterWheel, 82 | host="127.0.0.1", 83 | port=8001, 84 | conf={"positions": 6}, 85 | ), 86 | ] 87 | 88 | Instead of a device type, a function can be passed to the device 89 | definition. Reasons to do so are: configure the device before serving 90 | it; specify their URI; force a group of devices in the same process 91 | (see :ref:`composite-devices`); and readability of the configuration 92 | when `conf` gets too complex. For example: 93 | 94 | .. code-block:: python 95 | 96 | # Serve a cameras and a filter wheel 97 | from microscope.device_server import device 98 | from microscope.simulators import SimulatedCamera 99 | 100 | def construct_camera() -> typing.Dict[str, Device]: 101 | camera = SimulatedCamera() 102 | camera.set_setting("display image number", False) 103 | return {"DummyCamera": camera} 104 | 105 | # Will serve PYRO:DummyCamera@127.0.0.1:8000 106 | DEVICES = [ 107 | device(construct_camera, host="127.0.0.1", port=8000), 108 | ] 109 | 110 | 111 | Connect to remote devices 112 | ========================= 113 | 114 | The Microscope device server makes use of `Pyro4 115 | `_, a Python package for 116 | remote method invocation of Python objects. One can use the Pyro 117 | proxy, the remote object, as if it was a local instance of the device 118 | itself and Pyro takes care of locating the right object on the right 119 | computer and execute the method. Creating the proxy is simply a 120 | matter of knowing the device server URI: 121 | 122 | .. code-block:: python 123 | 124 | import Pyro4 125 | 126 | proxy = Pyro4.Proxy("PYRO:SomeLaser@127.0.0.1:8000") 127 | # use proxy as if it was an instance of the SomeLaser class 128 | proxy._pyroRelease() 129 | 130 | The device server will take care of anything special. If the remote 131 | device is a :class:`Controller`, the device 132 | server will use automatically create proxies for the individual 133 | devices it controls. 134 | 135 | Pyro configuration 136 | ------------------ 137 | 138 | Pyro4 configuration is the singleton ``Pyro4.config``. If there's any 139 | special configuration wanted, this can be done on the 140 | ``device-server`` configuration file: 141 | 142 | .. code-block:: python 143 | 144 | import Pyro4 145 | import microscope.device_server 146 | # ... 147 | 148 | # Pyro4.config is a singleton, these changes to config will be 149 | # used for all the device servers. This needs to be done after 150 | # importing microscope.device_server 151 | Pyro4.config.COMPRESSION = True 152 | Pyro4.config.PICKLE_PROTOCOL_VERSION = 2 153 | 154 | DEVICES = [ 155 | #... 156 | ] 157 | 158 | Importing ``microscope.device_server`` will already change the Pyro 159 | configuration, namely it sets the `SERIALIZER` to use the pickle 160 | protocol. Despite the security implications associated with it, 161 | pickle is the fastest of the protocols and one of the few capable of 162 | serialise numpy arrays which are camera images. 163 | 164 | 165 | Floating Devices 166 | ================ 167 | 168 | A :class:`floating device` is a 169 | device that can't be specified during object construction, and only 170 | after initialisation can it be identified. This happens in some 171 | cameras and is an issue when more than one such device is present. 172 | For example, if there are two Andor CMOS cameras present, it is not 173 | possible to specify which one to use when constructing the 174 | ``AndorSDK3`` instance. Only after the device has been initialised 175 | can we query its ID, typically the device serial number, and check if 176 | we obtained the one we want. Like so: 177 | 178 | .. code-block:: python 179 | 180 | wanted = "20200910" # serial number of the wanted camera 181 | camera = AndorSDK3() 182 | camera.initialize() 183 | if camera.get_id() != wanted: 184 | # We got the other camera, so try again 185 | next_camera = AndorSDK3() 186 | # Only shutdown the first camera after getting the next or we 187 | # might get the same wrong camera again. 188 | camera.shutdown() 189 | camera = next_camera 190 | 191 | In the interest of keeping each camera on their own separate process, 192 | the above can't be used. To address this, the device definition must 193 | specify ``uid`` if the device class is a floating device. Like so:: 194 | 195 | DEVICES = [ 196 | device(AndorSDK3, "127.0.0.1", 8000, uid="20200910"), 197 | device(AndorSDK3, "127.0.0.1", 8001, uid="20130802"), 198 | ] 199 | 200 | The device server will then construct each device on its own process, 201 | and then serve them on the named port. Two implication come out of 202 | this. The first is that ``uid`` *must* be specified, even if there is 203 | only such device present on the system. The second is that all 204 | devices of that class *must* be present. 205 | 206 | .. _composite-devices: 207 | 208 | Composite Devices 209 | ================= 210 | 211 | A composite device is a device that internally makes use of another 212 | device to function. These are typically not real hardware, they are 213 | an abstraction that merges multiple devices to provide something 214 | augmented. For example, ``ClarityCamera`` is a camera that returns a 215 | processed image based on the settings of ``AuroxClarity``. Another 216 | example is the ``StageAwareCamera`` which is a dummy camera that 217 | returns a subsection of an image file based on the stage coordinates 218 | in order to mimic navigating a real sample. 219 | 220 | If the multiple devices are on the same computer, it might be worth 221 | have them share the same process to avoid the inter process 222 | communication. This is achieved by returning multiple devices on the 223 | function that constructs. Like so: 224 | 225 | .. code-block:: python 226 | 227 | def construct_composite_device( 228 | device1 = SomeDevice() 229 | composite_device = DeviceThatNeedsOther(device1) 230 | return { 231 | "Device1" : device1, 232 | "CompositeDevice": composite_device, 233 | } 234 | 235 | # Will serve both: 236 | # PYRO:Device1@127.0.0.1:8000 237 | # PYRO:CompositeDevice@127.0.0.1:8000 238 | DEVICES = [ 239 | device(construct_composite_device, "127.0.0.1", 8000) 240 | ] 241 | -------------------------------------------------------------------------------- /doc/architecture/gui.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | .. _gui: 8 | 9 | GUI 10 | *** 11 | 12 | Microscope is a library for the control of microscope devices. 13 | Provision of a graphical user interface for the control of a 14 | microscope itself is outside the scope of the project (we recommend 15 | the use of `Cockpit `_ or `PYME 16 | `_). 17 | 18 | Still, during development, both of the microscope and support for new 19 | modules, a GUI can be useful. For example, check what a camera is 20 | acquiring, emitting light, checking if all methods are working as 21 | expected. For this purpose, there is the ``microscope-gui`` program 22 | as well as a :mod:`microscope.gui` module with Qt widgets. 23 | 24 | The ``microscope-gui`` program provides a minimal GUI for each device 25 | type. It requires the device server. For example: 26 | 27 | .. code-block:: shell 28 | 29 | microscope-gui FilterWheel PYRO:SomeFilterWheel@localhost:8001 30 | 31 | Start a program to control the filter wheel of the device being served 32 | with Pyro at ``SomeFilterWheel@localhost:8001``. 33 | 34 | The widgets are purposedly kept simple. This is because the aim of 35 | these widgets is to support development and we want to minimise issues 36 | in the widgets code which could be interpreted as issues on the 37 | hardware or on the device control code. 38 | 39 | .. todo:: add screenshoots (from the paper) 40 | -------------------------------------------------------------------------------- /doc/architecture/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | Architecture 8 | ************ 9 | 10 | .. toctree:: 11 | :hidden: 12 | 13 | abc 14 | supported-devices 15 | device-server 16 | triggers 17 | gui 18 | 19 | At its core, Microscope is a Python library that provides a defined 20 | interface to microscope devices. Technically, Microscope aims to be: 21 | 22 | * Easy to use 23 | * Flexible 24 | * Fast 25 | 26 | .. 27 | The main reason for Microscope is to enable faster development of new 28 | microscope. The people doing this systems are not computer 29 | scientists, and code is what they use to get the system and not their 30 | aim. Also, biologists who do new experiments, we want them to be able 31 | to script. As such, Microscope needs to be easy to use. 32 | 33 | .. 34 | We don't know what will be be the microscopes of the future. The 35 | devices are independent of each other, we want to keep supporting 36 | that. 37 | 38 | Microscopes require the interaction of multiple devices with tight 39 | timing constraints, specially when imaging at high speeds. 40 | 41 | Python is a great choice for that aim, and since most researchers 42 | During development, a series of 43 | choices have been made with these aims in mind, namely the choice of 44 | Python as the programming language 45 | 46 | Python has become the *de facto* language for scientific computing. 47 | If most researchers are already familiar with it, it makes it easier 48 | to adopt since they don't need to learn a new language. Python is 49 | also well know for being easy to learn. 50 | 51 | In addition, Python has a terrific ecosystem for scientific computing, 52 | e.g., NumPy, SciPy and SciKits, or TensorFlow. One of the 53 | flexibility, is the ability to expand the control of individual to 54 | merge with the analysis. Having the whole Python scientific stack is 55 | great, makes it more flexible. 56 | 57 | Flexibility also means ability to distribute devices. For this, 58 | Microscope was developed to support :ref:`remote devices 59 | `. This enables distribution of devices over multiple 60 | computers, an arbitrary number of devices, to provide any flexibility 61 | required. 62 | 63 | Finally, despite common ideas that performance requires a compiled 64 | language such as C++, Python has been shown to be fast enough. 65 | Anyway, when push comes to shove, new microscopes have tight timing 66 | requirements, synchronization between multiple devices that can only 67 | be satisfied by real time software. Most devices have a mode of 68 | operation where they act on receive of a hardware trigger and many 69 | devices can act as source of triggers. 70 | -------------------------------------------------------------------------------- /doc/architecture/supported-devices.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | .. _supported-devices: 8 | 9 | Supported Devices 10 | ***************** 11 | 12 | The following group of devices is currently supported. To request 13 | support for more devices, open an issue on the issue tracker. 14 | 15 | Cameras 16 | ======= 17 | 18 | - Andor (:class:`microscope.cameras.andorsdk3.AndorSDK3` and 19 | :class:`microscope.cameras.atmcd.AndorAtmcd`) 20 | - Hamamatsu (:class:`microscope.cameras.hamamatsu.HamamatsuCamera`) 21 | - Photometrics (:class:`microscope.cameras.pvcam.PVCamera`) 22 | - QImaging (:class:`microscope.cameras.pvcam.PVCamera`) 23 | - Raspberry Pi camera (:class:`microscope.cameras.picamera.PiCamera`) 24 | - Ximea (:class:`microscope.cameras.ximea.XimeaCamera`) 25 | 26 | Controllers 27 | =========== 28 | 29 | - ASI MS2000 (:class:`microscope.controllers.asi.ASIMS2000`) 30 | - CoolLED (:class:`microscope.controllers.coolled.CoolLED`) 31 | - Ludl MC 2000 (:class:`microscope.controllers.ludl.LudlMC2000`) 32 | - Lumencor Spectra III light engine 33 | (:class:`microscope.controllers.lumencor.SpectraIIILightEngine`) 34 | - Prior ProScan III (:class:`microscope.controllers.prior.ProScanIII`) 35 | - Toptica iChrome MLE (:class:`microscope.controllers.toptica.iChromeMLE`) 36 | - Zaber daisy chain devices 37 | (:class:`microscope.controllers.zaber.ZaberDaisyChain`) 38 | - Zaber LED controller (:class:`microscope.controllers.zaber.ZaberDaisyChain`) 39 | 40 | Deformable Mirrors 41 | ================== 42 | 43 | - Alpao (:class:`microscope.mirror.alpao.AlpaoDeformableMirror`) 44 | - Boston Micromachines Corporation 45 | (:class:`microscope.mirror.bmc.BMCDeformableMirror`) 46 | - Imagine Optic Mirao 52e (:class:`microscope.mirror.mirao52e.Mirao52e`) 47 | 48 | Filter Wheels 49 | ============= 50 | 51 | - Prior (:mod:`microscope.controllers.prior`) 52 | - Thorlabs (:mod:`microscope.filterwheels.thorlabs`) 53 | - Zaber (:class:`microscope.controllers.zaber.ZaberDaisyChain`) 54 | 55 | Light Sources 56 | ============= 57 | 58 | - Cobolt (:class:`microscope.lights.cobolt.CoboltLaser`) 59 | - Coherent Obis (:class:`microscope.lights.obis.ObisLaser`) 60 | - Coherent Sapphire (:class:`microscope.lights.sapphire.SapphireLaser`) 61 | - Omicron Deepstar (:class:`microscope.lights.deepstar.DeepstarLaser`) 62 | - Toptica iBeam (:class:`microscope.lights.toptica.TopticaiBeam`) 63 | 64 | Stages 65 | ====== 66 | 67 | - Linkam CMS196 (:class:`microscope.stages.linkam.LinkamCMS`) 68 | - Ludl (:class:`microscope.controllers.ludl.LudlMC2000`) 69 | - Zaber (:class:`microscope.controllers.zaber.ZaberDaisyChain`) 70 | 71 | DigitalIO 72 | ========= 73 | 74 | - Raspberry Pi (:class:`microscope.digitalio.raspberrypi.RPiDIO`) 75 | 76 | 77 | ValueLogger 78 | =========== 79 | 80 | - Raspberry Pi 81 | (:class:`microscope.valuelogger.raspberrypi.RPiValueLogger`) 82 | includes support for the MCP9808 and TSYS01 I2C temperature sensors 83 | 84 | 85 | Other 86 | ===== 87 | 88 | - Aurox Clarity (:class:`microscope.filterwheels.aurox.Clarity`) 89 | -------------------------------------------------------------------------------- /doc/architecture/triggers.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | .. _hardware-triggers: 8 | 9 | Hardware triggers 10 | ***************** 11 | 12 | Yeah, Python is slow but all software is slow for microscope control. 13 | We really need some real-time, and rely on hardware triggers for 14 | performance. 15 | 16 | .. todo:: 17 | 18 | Write this section. 19 | -------------------------------------------------------------------------------- /doc/authors.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | Contributors 8 | ************ 9 | 10 | The following people have contributed in the development of 11 | Microscope: 12 | 13 | - Danail Stoychev 14 | - David Miguel Susano Pinto 15 | - Ian Dobbie 16 | - Julio Mateos-Langerak 17 | - Mick Phillips 18 | - Nicholas Hall 19 | - Thomas Fish 20 | - Tiago Susano Pinto 21 | - 久保俊貴 (Toshiki Kubo) 22 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | ## Copyright (C) 2020 David Miguel Susano Pinto 5 | ## 6 | ## Copying and distribution of this file, with or without modification, 7 | ## are permitted in any medium without royalty provided the copyright 8 | ## notice and this notice are preserved. This file is offered as-is, 9 | ## without any warranty. 10 | 11 | import ctypes 12 | import datetime 13 | import sys 14 | import unittest.mock 15 | 16 | 17 | sys.path.insert(0, "..") 18 | 19 | 20 | # autodoc imports the modules to be documented. Modules that wrap the 21 | # device C libraries will load that library. This would require all 22 | # device libraries to be available to build the documentation. 23 | # autodoc can mocks different modules (see `autodoc_mock_imports`) but 24 | # that's not enough for all of our cases. 25 | 26 | 27 | def patch_cdll(): 28 | real_c_dll = ctypes.CDLL 29 | 30 | mocked_c_libs = [ 31 | # Andor's SDK for (EM)CCD cameras. Loading of this libary 32 | # should be in a separate microcope._wrappers.atmcd module and 33 | # then autodoc could just mock it. 34 | "atmcd32d", 35 | "atmcd32d.so", 36 | "atmcd64d", 37 | "atmcd64d.so", 38 | # pvcam's SDK. Loading of this shared library should be in a 39 | # separate microcope._wrappers.pvcam module and then autodoc 40 | # could just mock it. 41 | "pvcam.so", 42 | "pvcam32", 43 | "pvcam64", 44 | ] 45 | 46 | def cdll_diversion(name, *args, **kwargs): 47 | if name in mocked_c_libs: 48 | return unittest.mock.MagicMock() 49 | else: 50 | return real_c_dll(name, *args, **kwargs) 51 | 52 | ctypes.WinDLL = cdll_diversion 53 | ctypes.CDLL = cdll_diversion 54 | 55 | 56 | patch_cdll() 57 | 58 | 59 | def patch_sizeof(): 60 | # autodoc is mocking microscope._wrappers.dcamapi4 but we create 61 | # one of these structs for a singleton. 62 | real_sizeof = ctypes.sizeof 63 | 64 | def sizeof_diversion(struct): 65 | if ( 66 | hasattr(struct, "___sphinx_mock__") 67 | and str(struct) == "microscope._wrappers.dcamapi4.API_INIT" 68 | ): 69 | return 40 # doesn't really matter 70 | else: 71 | return real_sizeof(struct) 72 | 73 | ctypes.sizeof = sizeof_diversion 74 | 75 | 76 | patch_sizeof() 77 | 78 | 79 | # This should ve read from setup.py. Maybe we should use 80 | # pkg_resources to avoid duplication? 81 | author = "" 82 | project = "Microscope" 83 | 84 | copyright = "%s, %s" % (datetime.datetime.now().year, author) 85 | 86 | master_doc = "index" 87 | # nitpicky = True 88 | 89 | 90 | extensions = [ 91 | "sphinx.ext.autodoc", 92 | "sphinx.ext.intersphinx", 93 | "sphinx.ext.napoleon", 94 | "sphinx.ext.todo", 95 | "sphinx.ext.viewcode", 96 | "sphinxcontrib.apidoc", 97 | ] 98 | 99 | # Configuration for sphinx.ext.autodoc 100 | autodoc_mock_imports = [ 101 | "microscope._wrappers", 102 | "microscope.cameras._SDK3", # should go into microscope._wrappers 103 | "picamera", 104 | "picamera.array", 105 | "qtpy", 106 | "RPi", 107 | "servicemanager", 108 | "win32service", 109 | "win32serviceutil", 110 | "ximea", 111 | ] 112 | 113 | # Configuration for sphinx.ext.intersphinx 114 | intersphinx_mapping = { 115 | "numpy": ("https://numpy.org/doc/stable/", None), 116 | "pyro4": ("https://pyro4.readthedocs.io/en/stable/", None), 117 | "pyserial": ("https://pyserial.readthedocs.io/en/latest/", None), 118 | "python": ("https://docs.python.org/3", None), 119 | } 120 | 121 | # Configuration for sphinx.ext.napoleon 122 | napoleon_google_docstring = True 123 | napoleon_include_private_with_doc = True 124 | napoleon_include_special_with_doc = False 125 | 126 | # Configuration for sphinx.ext.todo 127 | todo_include_todos = True 128 | 129 | # Configuration for sphinxcontrib.apidoc 130 | apidoc_module_dir = '../microscope' 131 | apidoc_output_dir = 'api' 132 | apidoc_excluded_paths = [ 133 | # Exclude the testsuite 134 | "microscope/testsuite/", 135 | # Exclude the wrappers to shared libraries 136 | "microscope/_wrappers/", 137 | # Exclude the deprecated devices and deviceserver that are kept 138 | # for backwards compatibility only 139 | "microscope/devices.py", 140 | "microscope/deviceserver.py", 141 | "microscope/lasers/", 142 | # Exclude these that should be moved to microscope/_wrappers 143 | "microscope/cameras/_SDK3.py", 144 | "microscope/cameras/_SDK3Cam.py", 145 | ] 146 | apidoc_separate_modules = True 147 | apidoc_toc_file = "index" 148 | apidoc_module_first = True 149 | apidoc_exra_args = ["--private"] 150 | 151 | # 152 | # Options for HTML output 153 | # 154 | 155 | # html_theme = "agogo" 156 | html_static_path = ["_static"] 157 | html_title = "Python Microscope documentation" 158 | html_short_title = "import microscope" 159 | html_show_copyright = False 160 | html_show_sphinx = False 161 | html_copy_source = False 162 | html_show_sourcelink = False 163 | 164 | rst_prolog = """ 165 | .. _repo-browse: https://github.com/python-microscope/microscope 166 | .. _repo-vcs: https://github.com/python-microscope/microscope.git 167 | .. _gpl-licence: https://www.gnu.org/licenses/gpl-3.0.html 168 | .. _cockpit-link: https://github.com/MicronOxford/cockpit/ 169 | """ 170 | 171 | 172 | # 173 | # Options for LaTeX/PDF output 174 | # 175 | # Currently this has issues because of Japanese characters in 176 | # authors.rst. But maybe it would make sense to change authors.rst or 177 | # maybe there's something smart we can do on LaTeX customisation with 178 | # latex_elements https://www.sphinx-doc.org/en/master/latex.html 179 | 180 | latex_show_urls = "footnote" 181 | -------------------------------------------------------------------------------- /doc/examples/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | .. _examples: 8 | 9 | Examples 10 | ******** 11 | 12 | Example scripts of the things that can be done, typically when 13 | interacting with multiple devices. 14 | 15 | .. For now, we just "insert" the example code but later we might have 16 | one per file and a discussion of what is happening. 17 | 18 | Experiments 19 | =========== 20 | 21 | Simple experiments can be ran with Python only. For a simple time 22 | series experiment: 23 | 24 | .. code:: python 25 | 26 | import queue 27 | import time 28 | 29 | from microscope.lights.cobolt import CoboltLaser 30 | from microscope.cameras.atmcd import AndorAtmcd 31 | 32 | laser = CoboltLaser(com='/dev/ttyS0') 33 | laser.power = 0.3 34 | 35 | camera = AndorAtmcd(uid='9146') 36 | camera.set_exposure_time(0.15) 37 | 38 | buffer = queue.Queue() 39 | 40 | camera.set_client(buffer) 41 | laser.enable() 42 | camera.enable() 43 | 44 | for i in range(10): 45 | camera.trigger() # images go to buffer 46 | time.sleep(1) 47 | 48 | laser.disable() 49 | camera.disable() 50 | 51 | 52 | Remote devices 53 | ============== 54 | 55 | Microscope was designed around the idea that the multiple devices are 56 | distributed over the network. To accomplish this, the device objects 57 | can be replaced with ``Client`` and ``DataClient`` instances. For 58 | example, to run the previous experiment with remote devices: 59 | 60 | .. code:: python 61 | 62 | import microscope.clients 63 | 64 | camera_uri = 'PYRO:SomeCamera@127.0.0.1:8005' 65 | laser_uri = 'PYRO:SomeLaser@127.0.0.1:8006' 66 | 67 | camera = microscope.clients.DataClient(camera_uri) 68 | laser = microscope.clients.Client(laser_uri) 69 | 70 | 71 | Device server 72 | ============= 73 | 74 | The device server requires a configuration file defining the different 75 | devices to be initialised. It can be started with: 76 | 77 | .. code-block:: shell 78 | 79 | python3 -m microscope.device_server PATH-TO-CONFIG-FILE 80 | 81 | The device server configuration file is a Python script that defines a 82 | ``DEVICES`` list. Each element in the list corresponds to one 83 | device. For example:: 84 | 85 | """Configuration file for deviceserver. 86 | """ 87 | # The 'device' function creates device definitions. 88 | from microscope.device_server import device 89 | 90 | # Import required device classes 91 | from microscope.lights.cobolt import CoboltLaser 92 | from microscope.cameras.atmcd import AndorAtmcd 93 | 94 | # host is the IP address (or hostname) from where the device will be 95 | # accessible. If everything is on the same computer, then host will 96 | # be '127.0.0.1'. If devices are to be available on the network, 97 | # then it will be the IP address on that network. 98 | host = "127.0.0.1" 99 | 100 | # Each element in the DEVICES list identifies a device that will be 101 | # served on the network. Each device is defined like so: 102 | # 103 | # device(cls, host, port, conf) 104 | # cls: class of the device that will be served 105 | # host: ip or hostname where the device will be accessible. 106 | # This will be the same value for all devices. 107 | # port: port number where the device will be accessible. 108 | # Each device must have its own port. 109 | # conf: a dict with the arguments to construct the device 110 | # instance. See the individual class documentation. 111 | # 112 | # This list, initialises two cobolt lasers and one Andor camera. 113 | DEVICES = [ 114 | device(CoboltLaser, host, 7701, 115 | {"com": "/dev/ttyS0"}), 116 | device(CoboltLaser, host, 7702, 117 | {"com": "/dev/ttyS1"}), 118 | device(AndorAtmcd, host, 7703, 119 | {"uid": "9146"}), 120 | ] 121 | 122 | 123 | Test devices 124 | ------------ 125 | 126 | Microscope includes multiple simulated devices. These are meant to 127 | support development by providing a fake device for testing purposes. 128 | 129 | .. code:: python 130 | 131 | from microscope.device_server import device 132 | 133 | from microscope.simulators import ( 134 | SimulatedCamera, 135 | SimulatedFilterWheel, 136 | SimulatedLightSource, 137 | ) 138 | 139 | DEVICES = [ 140 | device(SimulatedCamera, "127.0.0.1", 8005, 141 | {"sensor_shape": (512, 512)}), 142 | device(SimulatedLightSource, "127.0.0.1", 8006), 143 | device(SimulatedFilterWheel, "127.0.0.1", 8007, 144 | {"positions": 6}), 145 | ] 146 | -------------------------------------------------------------------------------- /doc/examples/time-lapse-example-code.py: -------------------------------------------------------------------------------- 1 | # Simple python script to use Pyth0n-Microscope 2 | # https://github.com/python-microscope/microscope 3 | # to produce a time laspe image series. 4 | 5 | # Example code for a time-series experiment with hardware 6 | # triggers. In the hardware, this experiment requires the camera 7 | # digital output line to be connected to the laser digital input 8 | # line, so that the camera emits a high TTL 9 | # signal while its sensor is being exposed. In the code, the laser 10 | # is configured as to emit light only while receiving a high TTL 11 | # input signal. The example triggers the camera a specific number 12 | # times with a time interval between exposures. The acquired 13 | # images are put in the buffer asynchronously. The images are taken 14 | # from the queue at the end of the experiment and saved to a file. 15 | 16 | import time 17 | from queue import Queue 18 | 19 | from tifffile import TiffWriter 20 | 21 | from microscope import TriggerMode, TriggerType 22 | from microscope.cameras.pvcam import PVCamera 23 | from microscope.lights.toptica import TopticaiBeam 24 | 25 | 26 | # set parameters 27 | n_repeats = 10 28 | interval_seconds = 15 29 | exposure_seconds = 0.5 30 | power_level = 0.5 31 | 32 | # create devices 33 | camera = PVCamera() 34 | laser = TopticaiBeam(port="COM1") 35 | 36 | # initialise buffer as a queue 37 | image_buffer = Queue() 38 | 39 | # configure camera, pass the buffer queue and enable. 40 | camera.set_client(image_buffer) 41 | camera.exposure_time = exposure_seconds 42 | camera.set_trigger(TriggerType.SOFTWARE, TriggerMode.ONCE) 43 | camera.enable() 44 | 45 | # configure laser 46 | laser.power = power_level 47 | laser.set_trigger(TriggerType.HIGH, TriggerMode.BULB) 48 | laser.enable() 49 | 50 | # main loop to collect images. 51 | for i in range(n_repeats): 52 | camera.trigger() 53 | time.sleep(interval_seconds) 54 | 55 | # shutdown hardware devices 56 | laser.shutdown() 57 | camera.shutdown() 58 | 59 | # write out image data to a file. 60 | writer = TiffWriter("data.tif") 61 | for i in range(n_repeats): 62 | writer.save(image_buffer.get()) 63 | writer.close() 64 | -------------------------------------------------------------------------------- /doc/get-involved/dev-install.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | Development Installation 8 | ************************ 9 | 10 | Development sources 11 | =================== 12 | 13 | Microscope development sources are available on GitHub. To install 14 | the current development version of Microscope: 15 | 16 | .. code-block:: shell 17 | 18 | git clone https://github.com/python-microscope/microscope.git 19 | python3 -m pip install microscope/ 20 | 21 | Consider using editable mode if you plan to make changes to the 22 | project: 23 | 24 | .. code-block:: shell 25 | 26 | python3 -m pip install --editable microscope/ 27 | 28 | Multiple Microscope versions 29 | ---------------------------- 30 | 31 | The Python package system does not, by default, handle multiple 32 | versions of a package. If installing from development sources beware 33 | to not overwrite a previous installation. A typical approach to 34 | address this issue is with the use of `virtual environments 35 | `_. 36 | 37 | Un-merged features 38 | ------------------ 39 | 40 | Some features are still in development and have not been merged in the 41 | main branch. To test such features you will need to know the branch 42 | name and the repository where such feature is being developed. For 43 | example, to try Toshiki Kubo's implementation of the Mirao52e 44 | deformable mirror:: 45 | 46 | git remote add toshiki https://github.com/toshikikubo/microscope.git 47 | git fetch toshiki 48 | git checkout toshiki/mirao52e 49 | python3 -m pip install ./ 50 | -------------------------------------------------------------------------------- /doc/get-involved/hacking.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | Contributing 8 | ************ 9 | 10 | This documentation is for people who want to contribute code to the 11 | project, whether fixing a small bug, adding support for a new device, 12 | or even discussing a completely new device type. 13 | 14 | In short 15 | ======== 16 | 17 | - Open new issues (do not create pull requests). 18 | - Open a new issue even if you already have a commit made. Even if it 19 | is about adding support for a new device. 20 | - Coding style is `Black `_ 21 | - Development sources are on `Github `_ 22 | - Bug tracker is also on `Github 23 | `_ 24 | 25 | 26 | Reporting issues 27 | ================ 28 | 29 | We use the github issue tracker at 30 | ``_. When 31 | reporting an issue please include as much information as you can 32 | from: 33 | 34 | - Steps to reproduce issue 35 | Include information so that we can try it ourselves. Don't just 36 | say "camera fails when setting exposure time". Instead, include 37 | the code to reproduce the issue and the error message. 38 | 39 | - Operating system 40 | MacOS 10.15, Ubuntu 18.04, Windows 7, Windows 10, etc... 41 | 42 | - Python version 43 | Also include the python minor version number, i.e, Python 3.7.3 or 44 | 3.6.2. On command line, this is the output of ``python --version``. 45 | 46 | - Device, SDK, and firmware information 47 | Include the device model, revision number, and serial number. 48 | Also include the firmware and the device SDK version number. 49 | 50 | - Pyro version 51 | If the issue only happens in the network, please also include the 52 | version of the Pyro package. 53 | 54 | 55 | Requesting support for new device 56 | ================================= 57 | 58 | To request support for a new device, or even to support for a feature 59 | of an already supported device, open a new issue on the `issue tracker 60 | `_. 61 | 62 | If there's already an open issue requesting the same, please leave a 63 | comment so that we know more people want it. 64 | 65 | 66 | Fixing an issue 67 | =============== 68 | 69 | To fix an issue, including adding support for a new device, please do 70 | the following: 71 | 72 | - Open a new issue (if not already opened) 73 | - On the commit message, refer to the issue number 74 | - Comment on the issue what branch or commit to pull to fix it. 75 | 76 | Why the non-standard procedure? 77 | ------------------------------- 78 | 79 | This procedure to fix an issue is not very standard on github 80 | projects. However, it prevents spreading the discussion of the issue 81 | over multiple pages and enables one to find that discussion from the 82 | git history. 83 | 84 | 85 | Coding standards 86 | ================ 87 | 88 | Code format style 89 | ----------------- 90 | 91 | Let us not discuss over code formatting style. Code formatting is 92 | handled by `Black `_. Simply run 93 | ``black`` at the root of the project after making changes and before 94 | making a commit: 95 | 96 | .. code-block:: shell 97 | 98 | black . 99 | 100 | 101 | Docstrings 102 | ---------- 103 | 104 | The API section of the documentation is generated from the inlined 105 | docstrings. It requires the `Google Python docstrings format 106 | `_ 107 | minus the type description on the list of arguments since those are 108 | defined in the type annotations. It looks like this:: 109 | 110 | 111 | def func(arg1: str, arg2: int) -> bool: 112 | """One-line summary. 113 | 114 | Extended description of function. 115 | 116 | Args: 117 | arg1: Description of arg1. This can be a multi-line 118 | description, no problem. 119 | arg2: Description of arg2. 120 | 121 | Returns: 122 | Description of return value 123 | """ 124 | return True 125 | 126 | 127 | Commit messages 128 | --------------- 129 | 130 | The first line of the commit message have a one-line summary of the 131 | change. This needs to mention the class, function, or module where 132 | relevant changes happen. If there's an associated issue number, make 133 | reference to it. If it is a "fixup" commit, make reference to it. 134 | Some examples for changes: 135 | 136 | - limited to a method function: 137 | 138 | ``TheClassName.enable: fix for firmware older than Y (#98)`` 139 | 140 | - effecting multiple methods in a class: 141 | 142 | ``TheClassName: add support for very fancy feature (#99)`` 143 | 144 | - fixing a typo or obvious mistake on a previous commit: 145 | 146 | ``AndorAtmcd: pass index to super (fixup a16bef042a41)`` 147 | 148 | - documentation only: 149 | 150 | ``doc: add example for multiple cameras with hardware triggering`` 151 | 152 | 153 | Test suite 154 | ---------- 155 | 156 | Most of Python Microscope is about controlling very specific hardware 157 | and there are no realist mocks of such hardware. Still, there are 158 | some tests written. They can be run with `tox 159 | `_. The repository has the required 160 | configuration, so simply run ``tox`` at the root of the repository. 161 | 162 | All test units, as well as other tools for testing purposes, are part 163 | of the ``microscope.testsuite`` package. 164 | 165 | If your changes do not actually change a specific device, please 166 | include a test unit. 167 | 168 | 169 | Copyright 170 | ========= 171 | 172 | We do not request that copyright is assigned to us, you can remain the 173 | copyright holder of any contribution made. However, please ensure 174 | that you are the copyright holder. Depending on your contract, even 175 | if you are a student, the copyright holder is likely to be your 176 | employer or university. Ask your employer or PhD supervisor if you 177 | are not sure. 178 | -------------------------------------------------------------------------------- /doc/get-involved/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | Get Involved 8 | ************ 9 | 10 | Microscope is free and open source software released under the terms 11 | of the `GNU GPL `_. Development is open and all 12 | conversations about development happen on the public `GitHub 13 | repository `_. 14 | 15 | We welcome all contributions, be it in form of comments, ideas, and 16 | code. Anyone with an interest in the project can chime in the 17 | development. 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | dev-install 23 | hacking 24 | maintaining 25 | new-device 26 | -------------------------------------------------------------------------------- /doc/get-involved/maintaining.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | Permission is granted to copy, distribute and/or modify this 4 | document under the terms of the GNU Free Documentation License, 5 | Version 1.3 or any later version published by the Free Software 6 | Foundation; with no Invariant Sections, no Front-Cover Texts, and 7 | no Back-Cover Texts. A copy of the license is included in the 8 | section entitled "GNU Free Documentation License". 9 | 10 | Maintainer's guide 11 | ****************** 12 | 13 | This document includes information for those deeper in the project. 14 | 15 | 16 | The NEWS file 17 | ============= 18 | 19 | The file should only include user visible changes worth mentioning. 20 | For example, adding support for a new device should be listed but 21 | removing multiple lines to address code duplication should not. 22 | 23 | It's easier to keep the ``NEWS`` file up to date as changes are made. 24 | This prevents having to check all the changes since the last release 25 | for such relevant changes. Ideally, the same commit that makes the 26 | relevant change should also add the entry to the NEWS file. 27 | 28 | 29 | Steps to make a release 30 | ======================= 31 | 32 | #. Check the ``NEWS`` is up to date for the next release. Because the 33 | ``NEWS`` file is supposed to be kept up to date with each commit 34 | that introduces changes worth mentioning, there should be no need 35 | to add entries now. But if so, then:: 36 | 37 | git commit -m "maint: update NEWS for upcoming release" NEWS.rst 38 | 39 | #. Manually add date and version for next release on ``NEWS.rst``. 40 | Then change the version on ``pyproject.toml``, commit it, and tag 41 | it:: 42 | 43 | NEW_VERSION="X.Y.Z" # replace this with new version number 44 | OLD_VERSION=`grep '^version ' pyproject.toml | sed 's,^version = "\([0-9.]*+dev\)"$,\1,'` 45 | python3 -c "from packaging.version import parse; assert parse('$NEW_VERSION') > parse('$OLD_VERSION');" 46 | 47 | sed -i 's,^version = "'$OLD_VERSION'"$,version = "'$NEW_VERSION'",' pyproject.toml 48 | git commit -m "maint: release $NEW_VERSION" pyproject.toml NEWS.rst 49 | COMMIT=$(git rev-parse HEAD | cut -c1-12) 50 | git tag -a -m "Added tag release-$NEW_VERSION for commit $COMMIT" release-$NEW_VERSION 51 | 52 | Note that we use ``release-N`` for tag and not ``v.N``. This will 53 | enable us to one day perform snapshot releases with tags such as 54 | ``snapshot-N``. 55 | 56 | #. Build a source and wheel distribution from a git archive export:: 57 | 58 | rm -rf target 59 | git archive --format=tar --prefix="target/" release-$NEW_VERSION | tar -x 60 | (cd target/ ; python3 -m build) 61 | 62 | Performing a release from a git archive ensures that the release 63 | does not accidentally include modified or untracked files. The 64 | wheel distribution is not for actual distribution, we only build it 65 | to ensure that a binary distribution can be built from the source 66 | distribution. 67 | 68 | We should probably do this from a git clone and not an archive to 69 | ensure that we are not using a commit that has not been pushed yet. 70 | 71 | #. Upload source distribution to PyPI:: 72 | 73 | twine upload -r pypi target/dist/microscope-$NEW_VERSION.tar.gz 74 | 75 | #. Add ``+dev`` to version string and manually add a new entry on the 76 | ``NEWS`` file for the upcoming version:: 77 | 78 | sed -i 's,^version = "'$NEW_VERSION'"$,version = "'$NEW_VERSION'+dev",' pyproject.toml 79 | # manually add new version line on NEWS.rst file 80 | git commit -m "maint: set version to $NEW_VERSION+dev after $NEW_VERSION release." pyproject.toml NEWS.rst 81 | git push upstream master 82 | git push upstream release-$NEW_VERSION 83 | 84 | 85 | Documentation 86 | ============= 87 | 88 | The documentation is generated with `Sphinx 89 | `__, like so:: 90 | 91 | sphinx-build -b html doc/ dist/sphinx/html 92 | sphinx-build -M pdflatex doc/ dist/sphinx/pdf 93 | 94 | The API section is generated from the inline docstrings and makes use 95 | of Sphinx's `Napoleon 96 | `__ and `apidoc 97 | `__ extensions. 98 | 99 | 100 | Versioning 101 | ========== 102 | 103 | We use the style ``major.minor.patch`` for releases and haven't yet 104 | had to deal with rc. 105 | 106 | In between releases and snapshots, we use the ``dev`` as a local 107 | version identifiers as per `PEP 440 108 | `_ so a version string 109 | ``0.0.1+dev`` is the release ``0.0.1`` plus development changes on top 110 | of it (and not development release for an upcoming ``0.0.1``). With 111 | examples: 112 | 113 | * ``0.0.1`` - major version 0, minor version 0, patch version 1 114 | 115 | * ``0.0.1+dev`` - not a public release. A development build, probably 116 | from VCS sources, sometime *after* release ``0.0.1``. Note the use 117 | of ``+`` which marks ``dev`` as a local version identifier. 118 | 119 | * ``0.0.1.dev1`` - we do not do this. PEP 440 states this would be 120 | the first development public release *before* ``0.0.1``. We use 121 | ``+dev`` which are local version and not public releases. This is 122 | only mentioned here to avoid confusion for people used to that 123 | style. 124 | 125 | 126 | Website 127 | ======= 128 | 129 | The sources for the https://python-microscope.org is on the repository 130 | https://github.com/python-microscope/python-microscope.org 131 | -------------------------------------------------------------------------------- /doc/getting-started.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | Getting Started 8 | *************** 9 | 10 | To control a device with Microscope one needs to find the Python class 11 | that supports it. These are listed on :ref:`supported-devices`. Each 12 | device has its own class which typically have their own module. Note 13 | that many devices have the same class, for example, all Ximea cameras 14 | use :class:`XimeaCamera ` and 15 | all Andor CMOS cameras use :class:`AndorSDK3 16 | `. 17 | 18 | Connecting to the Device 19 | ======================== 20 | 21 | Once the class is known its documentation will state the arguments 22 | required to construct the device instance and connect to it. For 23 | devices controlled over a serial channel the argument is typically the 24 | port name of its address, e.g., ``/dev/ttyS0`` on GNU/Linux or 25 | ``COM1`` on Windows. For other devices the argument is typically the 26 | serial number (this is typically printed on a label on the physical 27 | device). 28 | 29 | .. code-block:: python 30 | 31 | from microscope.lights.sapphire import SapphireLaser 32 | laser = SapphireLaser(com="/dev/ttyS0") 33 | 34 | from microscope.mirror.alpao import AlpaoDeformableMirror 35 | dm = AlpaoDeformableMirror(serial_number="BIL103") 36 | 37 | Controlling the Device 38 | ====================== 39 | 40 | The construction of the device is the only device specific code. 41 | Beyond this :ref:`ABCs` force a defined interface on the device 42 | classes ensuring that all devices have the same methods and 43 | properties. The following ABCs, one per device type, are currently 44 | supported: 45 | 46 | * :class:`microscope.abc.Camera` 47 | * :class:`microscope.abc.Controller` 48 | * :class:`microscope.abc.DeformableMirror` 49 | * :class:`microscope.abc.FilterWheel` 50 | * :class:`microscope.abc.LightSource` 51 | * :class:`microscope.abc.Stage` 52 | 53 | 54 | LightSource 55 | ----------- 56 | 57 | A light source emits light when enabled and its power can be read and 58 | set via the ``power`` property:: 59 | 60 | laser.power = .7 # set power to 70% 61 | laser.enable() # only start emitting light now 62 | laser.power = laser.power /3 # set power to 1/3 63 | laser.disable() # stop emitting light 64 | 65 | 66 | Filter Wheel 67 | ------------ 68 | 69 | A filter wheel changes its position by setting the ``position`` 70 | property:: 71 | 72 | print("Number of positions is %d" % filterwheel.n_positions) 73 | print("Current position is %d" % filterwheel.position) 74 | filterwheel.position = 3 # move in filter at position 3 75 | 76 | 77 | Stage 78 | ----- 79 | 80 | A stage device can have any number of axes and dimensions. For a 81 | single ``StageDevice`` instance each axis has a name that uniquely 82 | identifies it. The names of the individual axes are hardware 83 | dependent and will be part of the concrete class documentation. They 84 | are typically strings such as `"x"` or `"y"`. 85 | 86 | .. code-block:: python 87 | 88 | stage.enable() # may trigger a stage move 89 | 90 | # move operations 91 | stage.move_to({"x": 42.0, "y": -5.1}) 92 | stage.move_by({"x": -5.3, "y": 14.6}) 93 | 94 | # Individual StageAxis can be controlled directly. 95 | x_axis = stage.axes["x"] 96 | y_axis = stage.axes["y"] 97 | x_axis.move_to(42.0) 98 | y_axis.move_by(-5.3) 99 | 100 | 101 | Camera 102 | ------ 103 | 104 | Cameras when triggered will put an image on their client which is a 105 | queue-like object. These queue-like objects must first be created and 106 | set on the camera:: 107 | 108 | buffer = queue.Queue() 109 | camera.set_client(buffer) 110 | camera.enable() 111 | camera.trigger() # acquire image 112 | 113 | img = buffer.get() # retrieve image 114 | 115 | 116 | Deformable Mirror 117 | ----------------- 118 | 119 | A deformable mirror applies a NumpPy array with the values for each of 120 | its actuators in the range of [0 1]:: 121 | 122 | # Set all actuators to their mid-point 123 | dm.apply_pattern(np.full(dm.n_actuators, 0.5)) 124 | 125 | Alternatively, a series of patterns can be first queued and applied 126 | when a trigger is received:: 127 | 128 | # `patterns` is a NumPy array of shape (K, N) where K is the number of 129 | # patterns and N is the number of actuators. 130 | dm.queue_patterns(patterns) 131 | for i in range(patterns.shape[0]): 132 | dm.trigger() 133 | 134 | 135 | Controller 136 | ---------- 137 | 138 | A controller is a device that controls a series of other devices. For 139 | example, a multi laser engine is a controller of many light sources. 140 | A controller instance only has as ``devices`` property which is a map 141 | of names to instances of other device classes. 142 | 143 | .. code-block:: python 144 | 145 | cyan_led = controller.devices["CYAN"] 146 | red_laser = controller.devices["RED"] 147 | 148 | The class documentation will include details on the names of the 149 | controller device. 150 | 151 | 152 | Shutdown the Device 153 | =================== 154 | 155 | When all is done, it's a good idea to disable and cleanly shutdown the 156 | device:: 157 | 158 | camera.disable() 159 | camera.shutdown() 160 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | Microscope Documentation 8 | ************************ 9 | 10 | .. toctree:: 11 | :hidden: 12 | 13 | getting-started 14 | install 15 | examples/index 16 | architecture/index 17 | api/index 18 | news 19 | get-involved/index 20 | authors 21 | 22 | Microscope is fundamentally a Python package for the control of 23 | microscope devices. It provides an easy to use interface for 24 | different device types. For example: 25 | 26 | .. code-block:: python 27 | 28 | # Connect to a Coherent Sapphire laser, set its power while 29 | # emitting light. 30 | from microscope.lights.sapphire import SapphireLaser 31 | laser = SapphireLaser(com="/dev/ttyS1") 32 | laser.power = .7 # initial laser power at 70% 33 | laser.enable() # start emitting light 34 | laser.power = laser.power / .3 # set laser power to 1/3 35 | 36 | 37 | # Connect to a Thorlabs filterwheel, query filter position, then 38 | # change filter. 39 | from microscope.filterwheels.thorlabs import ThorlabsFilterWheel 40 | filterwheel = ThorlabsFilterWheel(com="/dev/ttyS0") 41 | print("Number of positions is %d" % filterwheel.n_positions) 42 | print("Current position is %d" % filterwheel.position) 43 | filterwheel.position = 3 # move in filter at position 3 44 | 45 | 46 | At the core of Microscope is the idea that all devices of the same 47 | type should have the same interface (see :ref:`ABCs`). 48 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | .. _install: 8 | 9 | Installation 10 | ************ 11 | 12 | Microscope is available on the Python Package Index (PyPI) and can be 13 | `installed like any other Python package 14 | `_. The 15 | short version of it is "use pip" (you will need to have Python and pip 16 | already installed on your system):: 17 | 18 | python3 -m pip install microscope 19 | 20 | 21 | Requirements 22 | ============ 23 | 24 | Microscope can run in any operating system with Python installed but 25 | individual devices and the intended usage may add specific 26 | requirements: 27 | 28 | - **hardware performance**: control of devices at high speed, namely 29 | image acquisition at very high rates, may require a fast connection 30 | to the camera, high amount of memory, or a disk with fast write 31 | speeds. 32 | 33 | - **external libraries**: control of many devices is performed via 34 | vendor provided external libraries which are often limited to 35 | specific operating systems. For example, Alpao does not provide a 36 | library for Linux systems so control of Alpao deformable mirrors is 37 | limited to Windows. See the :ref:`Dependencies 38 | ` sections below. 39 | 40 | If there are multiple devices with high resources or incompatible 41 | requirements they can be distributed over multiple computers. For 42 | example, two cameras acquiring images at a high frame rate or a camera 43 | that requires Windows paired with a deformable mirror that requires 44 | Linux. See the :ref:`device-server` section for details. 45 | 46 | 47 | .. _install-dependencies: 48 | 49 | Dependencies 50 | ============ 51 | 52 | Microscope has very few dependencies and all are Python packages 53 | available on PyPI that will be automatically resolved if Microscope is 54 | installed with pip. However, the interface to many devices is done 55 | via an external library (sometimes named SDK or driver) that is only 56 | provided by the device vendor. 57 | 58 | To identify if an external library is required check the device module 59 | documentation. If an external library is required, contact the device 60 | vendor for install instructions. Cameras and deformable mirrors all 61 | require an external library. Filter wheels, lasers, and stages 62 | typically do not but there are exceptions. 63 | -------------------------------------------------------------------------------- /doc/news.rst: -------------------------------------------------------------------------------- 1 | .. Copyright (C) 2020 David Miguel Susano Pinto 2 | 3 | This work is licensed under the Creative Commons 4 | Attribution-ShareAlike 4.0 International License. To view a copy of 5 | this license, visit http://creativecommons.org/licenses/by-sa/4.0/. 6 | 7 | Release Notes 8 | ************* 9 | 10 | .. include:: ../NEWS.rst 11 | -------------------------------------------------------------------------------- /microscope/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | import enum 21 | from typing import NamedTuple 22 | 23 | 24 | class MicroscopeError(Exception): 25 | """Base class for Python Microscope exceptions.""" 26 | 27 | pass 28 | 29 | 30 | class DeviceError(MicroscopeError): 31 | """Raised when there is an issue controlling a device. 32 | 33 | This exception is raised when there is an issue with controlling 34 | the device, be it with its programming interface or with the 35 | physical hardware. It is most common when commands to the device 36 | fail or return something unexpected. 37 | 38 | .. note:: 39 | 40 | The subclasses `DisabledDeviceError`, `IncompatibleStateError`, 41 | `InitialiseError`, and `UnsupportedFeatureError` provide more 42 | fine grained exceptions. 43 | 44 | """ 45 | 46 | pass 47 | 48 | 49 | class IncompatibleStateError(DeviceError): 50 | """Raised when an operation is incompatible with the current device 51 | state. 52 | 53 | This exception is raised when the device is in a state 54 | incompatible with an attempted operation, e.g., calling 55 | :mod:`trigger ` on a 56 | device that is set for hardware triggers. The subclass 57 | `DisabledDeviceError` provides an exception specific to the case 58 | where the issue is the device being disabled. 59 | 60 | .. note:: 61 | 62 | This exception is for attempting to perform some action but 63 | device is wrong state. If the issue is about a setting that is 64 | incompatible with some other setting that this specific device 65 | does not support, then `UnsupportedFeatureError` should be 66 | raised. 67 | 68 | """ 69 | 70 | pass 71 | 72 | 73 | class DisabledDeviceError(IncompatibleStateError): 74 | """Raised when an operation requires an enabled device but the device is 75 | disabled. 76 | """ 77 | 78 | pass 79 | 80 | 81 | class InitialiseError(DeviceError): 82 | """Raised when a device fails to initialise. 83 | 84 | This exception is raised when there is a failure connecting to a 85 | device, typically because the device is not connected, or the serial 86 | number or port address is incorrect. 87 | 88 | """ 89 | 90 | pass 91 | 92 | 93 | class UnsupportedFeatureError(DeviceError): 94 | """Raised when some operation requires a feature that is not supported. 95 | 96 | This exception is raised when an operation requires some feature 97 | that is not supported, either because the physical device does not 98 | provide it, or Python Microscope has not yet implemented it. For 99 | example, most devices do not support all trigger modes. 100 | 101 | """ 102 | 103 | pass 104 | 105 | 106 | class LibraryLoadError(MicroscopeError): 107 | """Raised when the loading and initialisation of a device library fails. 108 | 109 | This exception is raised when a shared library or DLL fails to load, 110 | typically because the required library is not found or is missing 111 | some required symbol. 112 | 113 | If there is a module that is a straight wrapper to the C library 114 | (there should be one on the `microscope._wrappers` package) then 115 | this exception can easily be used chained with the exception that 116 | originated it like so:: 117 | 118 | .. code-block:: python 119 | 120 | try: 121 | import microscope._wrappers.libname 122 | except Exception as e: 123 | raise microscope.LibraryLoadError(e) from e 124 | 125 | """ 126 | 127 | pass 128 | 129 | 130 | class AxisLimits(NamedTuple): 131 | """Limits of a :class:`microscope.abc.StageAxis`.""" 132 | 133 | lower: float 134 | upper: float 135 | 136 | 137 | class Binning(NamedTuple): 138 | """A tuple containing parameters for horizontal and vertical binning.""" 139 | 140 | h: int 141 | v: int 142 | 143 | 144 | class ROI(NamedTuple): 145 | """A tuple that defines a region of interest. 146 | 147 | This rectangle format completely defines the ROI without reference 148 | to the sensor geometry. 149 | """ 150 | 151 | left: int 152 | top: int 153 | width: int 154 | height: int 155 | 156 | 157 | class TriggerType(enum.Enum): 158 | """Type of a trigger for a :class:`microscope.abc.TriggerTargetMixin`. 159 | 160 | The trigger type defines what constitutes a trigger, as opposed to 161 | the trigger mode which defines a type of action when the trigger 162 | is received. 163 | 164 | :const:`TriggerType.SOFTWARE` 165 | when :meth:`microscope.abc.TriggerTargetMixin.trigger` is called 166 | :const:`TriggerType.RISING_EDGE` 167 | when level changes to high 168 | :const:`TriggerType.FALLING_EDGE` 169 | when level changes to low 170 | """ 171 | 172 | SOFTWARE = 0 173 | RISING_EDGE = 1 174 | HIGH = 1 175 | FALLING_EDGE = 2 176 | LOW = 2 177 | PULSE = 3 178 | 179 | 180 | class TriggerMode(enum.Enum): 181 | """Mode of a trigger for a :class:`microscope.abc.TriggerTargetMixin`. 182 | 183 | The trigger mode defines what type of action when a trigger is 184 | received, as opposed to the trigger type which defines what 185 | constitutes a trigger. The exact type of action is highly 186 | dependent on device type, so check their documentation. 187 | 188 | :const:`TriggerMode.ONCE` 189 | Act once. For example, acquire a single image when a camera 190 | is triggered. 191 | :const:`TriggerMode.BULB` 192 | Act while device is being triggered. For example, a laser 193 | keeps emitting emit light or a camera keeps exposing while the 194 | trigger line is high. This trigger mode is incompatible with 195 | :attr:`TriggerType.SOFTWARE`. 196 | :const:`TriggerMode.STROBE` 197 | Act repeatably while device is being triggered. For example, 198 | a camera keep acquiring multiple images while the trigger line 199 | is high. 200 | """ 201 | 202 | ONCE = 1 203 | BULB = 2 204 | STROBE = 3 205 | START = 4 206 | -------------------------------------------------------------------------------- /microscope/_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | import ctypes 21 | import os 22 | import sys 23 | import threading 24 | from typing import List, Optional, Type 25 | 26 | import serial 27 | 28 | import microscope 29 | import microscope.abc 30 | 31 | # Both pySerial and serial distribution packages install an import 32 | # package named serial. If both are installed we may have imported 33 | # the wrong one. Check it to provide a better error message. See 34 | # issue #232. 35 | if not hasattr(serial, "Serial"): 36 | if hasattr(serial, "marshall"): 37 | raise microscope.MicroscopeError( 38 | "incorrect imported package serial. It appears that the serial" 39 | " package from the distribution serial, instead of pyserial, was" 40 | " imported. Consider uninstalling serial and installing pySerial." 41 | ) 42 | else: 43 | raise microscope.MicroscopeError( 44 | "imported package serial does not have Serial class" 45 | ) 46 | 47 | 48 | def library_loader( 49 | libname: str, dlltype: Type[ctypes.CDLL] = ctypes.CDLL, **kwargs 50 | ) -> ctypes.CDLL: 51 | """Load shared library. 52 | 53 | This exists mainly to search for DLL in Windows using a standard 54 | search path, i.e, search for dlls in ``PATH``. 55 | 56 | Args: 57 | libname: file name or path of the library to be loaded as 58 | required by `dlltype` 59 | dlltype: the class of shared library to load. Typically, 60 | `ctypes.CDLL` but sometimes `ctypes.WinDLL` in windows. 61 | kwargs: other arguments passed on to `dlltype`. 62 | """ 63 | # Python 3.8 in Windows uses an altered search path. Their 64 | # reasons is security and I guess it would make sense if we 65 | # installed the DLLs we need ourselves but we don't. `winmode=0` 66 | # restores the use of the standard search path. See issue #235. 67 | if ( 68 | os.name == "nt" 69 | and sys.version_info >= (3, 8) 70 | and "winmode" not in kwargs 71 | ): 72 | winmode_kwargs = {"winmode": 0} 73 | else: 74 | winmode_kwargs = {} 75 | return dlltype(libname, **winmode_kwargs, **kwargs) 76 | 77 | 78 | class OnlyTriggersOnceOnSoftwareMixin(microscope.abc.TriggerTargetMixin): 79 | """Utility mixin for devices that only trigger "once" with software. 80 | 81 | This mixin avoids code duplication for the many devices whose only 82 | supported trigger type and trigger mode are `TriggerType.SOFTWARE` 83 | and `TriggerMode.ONCE`. 84 | 85 | """ 86 | 87 | @property 88 | def trigger_type(self) -> microscope.TriggerType: 89 | return microscope.TriggerType.SOFTWARE 90 | 91 | @property 92 | def trigger_mode(self) -> microscope.TriggerMode: 93 | return microscope.TriggerMode.ONCE 94 | 95 | def set_trigger( 96 | self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode 97 | ) -> None: 98 | if ttype is not microscope.TriggerType.SOFTWARE: 99 | raise microscope.UnsupportedFeatureError( 100 | "the only trigger type supported is software" 101 | ) 102 | if tmode is not microscope.TriggerMode.ONCE: 103 | raise microscope.UnsupportedFeatureError( 104 | "the only trigger mode supported is 'once'" 105 | ) 106 | 107 | 108 | class OnlyTriggersBulbOnSoftwareMixin(microscope.abc.TriggerTargetMixin): 109 | """Utility mixin for devices that only trigger "bulb" with software. 110 | 111 | This mixin avoids code duplication for the many devices whose only 112 | supported trigger type and trigger mode are `TriggerType.SOFTWARE` 113 | and `TriggerMode.BULB`. 114 | 115 | """ 116 | 117 | @property 118 | def trigger_type(self) -> microscope.TriggerType: 119 | return microscope.TriggerType.SOFTWARE 120 | 121 | @property 122 | def trigger_mode(self) -> microscope.TriggerMode: 123 | return microscope.TriggerMode.BULB 124 | 125 | def set_trigger( 126 | self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode 127 | ) -> None: 128 | if ttype is not microscope.TriggerType.SOFTWARE: 129 | raise microscope.UnsupportedFeatureError( 130 | "the only trigger type supported is software" 131 | ) 132 | if tmode is not microscope.TriggerMode.BULB: 133 | raise microscope.UnsupportedFeatureError( 134 | "the only trigger mode supported is 'bulb'" 135 | ) 136 | 137 | def _do_trigger(self) -> None: 138 | raise microscope.IncompatibleStateError( 139 | "trigger does not make sense in trigger mode bulb, only enable" 140 | ) 141 | 142 | 143 | class SharedSerial: 144 | """Wraps a `Serial` instance with a lock for synchronization.""" 145 | 146 | def __init__(self, serial: serial.Serial) -> None: 147 | self._serial = serial 148 | self._lock = threading.RLock() 149 | 150 | @property 151 | def lock(self) -> threading.RLock: 152 | return self._lock 153 | 154 | def readline(self) -> bytes: 155 | with self._lock: 156 | return self._serial.readline() 157 | 158 | def readlines(self, hint: int = -1) -> List[bytes]: 159 | with self._lock: 160 | return self._serial.readlines(hint) 161 | 162 | # Beware: pySerial 3.5 changed the named of its first argument 163 | # from terminator to expected. See issue #233. 164 | def read_until( 165 | self, terminator: bytes = b"\n", size: Optional[int] = None 166 | ) -> bytes: 167 | with self._lock: 168 | return self._serial.read_until(terminator, size=size) 169 | 170 | def write(self, data: bytes) -> int: 171 | with self._lock: 172 | return self._serial.write(data) 173 | -------------------------------------------------------------------------------- /microscope/_wrappers/BMC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Boston Micromachines deformable mirrors SDK. 21 | """ 22 | 23 | import ctypes 24 | import os 25 | from ctypes import c_char, c_char_p, c_double, c_int, c_uint, c_uint32 26 | 27 | import microscope._utils 28 | 29 | if os.name == "nt": # is windows 30 | # Not actually tested yet 31 | SDK = microscope._utils.library_loader("BMC2", ctypes.WinDLL) 32 | else: 33 | SDK = microscope._utils.library_loader("libBMC.so.3") 34 | 35 | 36 | # Definitions from BMCDefs.h 37 | MAX_PATH = 260 38 | SERIAL_NUMBER_LEN = 11 39 | MAX_DM_SIZE = 4096 40 | 41 | 42 | class DM_PRIV(ctypes.Structure): 43 | pass 44 | 45 | 46 | class DM_DRIVER(ctypes.Structure): 47 | _fields_ = [ 48 | ("channel_count", c_uint), 49 | ("serial_number", c_char * (SERIAL_NUMBER_LEN + 1)), 50 | ("reserved", c_uint * 7), 51 | ] 52 | 53 | 54 | class DM(ctypes.Structure): 55 | _fields_ = [ 56 | ("Driver_Type", c_uint), 57 | ("DevId", c_uint), 58 | ("HVA_Type", c_uint), 59 | ("use_fiber", c_uint), 60 | ("use_CL", c_uint), 61 | ("burst_mode", c_uint), 62 | ("fiber_mode", c_uint), 63 | ("ActCount", c_uint), 64 | ("MaxVoltage", c_uint), 65 | ("VoltageLimit", c_uint), 66 | ("mapping", c_char * MAX_PATH), 67 | ("inactive", c_uint * MAX_DM_SIZE), 68 | ("profiles_path", c_char * MAX_PATH), 69 | ("maps_path", c_char * MAX_PATH), 70 | ("cals_path", c_char * MAX_PATH), 71 | ("cal", c_char * MAX_PATH), 72 | ("serial_number", c_char * (SERIAL_NUMBER_LEN + 1)), 73 | ("driver", DM_DRIVER), 74 | ("priv", ctypes.POINTER(DM_PRIV)), 75 | ] 76 | 77 | 78 | DMHANDLE = ctypes.POINTER(DM) 79 | 80 | RC = c_int # enum for error codes 81 | 82 | LOGLEVEL = c_int # enum for log-levels 83 | LOG_ALL = 0 84 | LOG_TRACE = LOG_ALL 85 | LOG_DEBUG = 1 86 | LOG_INFO = 2 87 | LOG_WARN = 3 88 | LOG_ERROR = 4 89 | LOG_FATAL = 5 90 | LOG_OFF = 6 91 | 92 | 93 | def make_prototype(name, argtypes, restype=RC): 94 | func = getattr(SDK, name) 95 | func.argtypes = argtypes 96 | func.restype = restype 97 | return func 98 | 99 | 100 | Open = make_prototype("BMCOpen", [DMHANDLE, c_char_p]) 101 | 102 | SetArray = make_prototype( 103 | "BMCSetArray", 104 | [DMHANDLE, ctypes.POINTER(c_double), ctypes.POINTER(c_uint32)], 105 | ) 106 | 107 | GetArray = make_prototype( 108 | "BMCGetArray", [DMHANDLE, ctypes.POINTER(c_double), c_uint32] 109 | ) 110 | 111 | Close = make_prototype("BMCClose", [DMHANDLE]) 112 | 113 | ErrorString = make_prototype("BMCErrorString", [RC], c_char_p) 114 | 115 | ConfigureLog = make_prototype("BMCConfigureLog", [c_char_p, LOGLEVEL]) 116 | 117 | VersionString = make_prototype("BMCVersionString", [], c_char_p) 118 | -------------------------------------------------------------------------------- /microscope/_wrappers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/_wrappers/__init__.py -------------------------------------------------------------------------------- /microscope/_wrappers/asdk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Alpao deformable mirrors SDK. 21 | """ 22 | 23 | import ctypes 24 | import os 25 | from ctypes import c_char_p, c_double, c_int, c_size_t, c_uint32 26 | 27 | import microscope._utils 28 | 29 | if os.name == "nt": # is windows 30 | _libname = "ASDK" 31 | else: 32 | _libname = "libasdk.so" # Not actually tested yet 33 | SDK = microscope._utils.library_loader(_libname) 34 | 35 | 36 | class DM(ctypes.Structure): 37 | pass 38 | 39 | 40 | pDM = ctypes.POINTER(DM) 41 | 42 | # We have this "typedefs" to ease matching with alpao's headers. 43 | CStr = c_char_p 44 | Scalar = c_double 45 | Scalar_p = ctypes.POINTER(Scalar) 46 | UInt = c_uint32 47 | Size_T = c_size_t 48 | 49 | COMPL_STAT = c_int # enum for function completion status 50 | SUCCESS = 0 51 | FAILURE = -1 52 | 53 | 54 | def make_prototype(name, argtypes, restype=COMPL_STAT): 55 | func = getattr(SDK, name) 56 | func.argtypes = argtypes 57 | func.restype = restype 58 | return func 59 | 60 | 61 | Get = make_prototype("asdkGet", [pDM, CStr, Scalar_p]) 62 | 63 | GetLastError = make_prototype( 64 | "asdkGetLastError", [ctypes.POINTER(UInt), CStr, Size_T] 65 | ) 66 | 67 | Init = make_prototype("asdkInit", [CStr], pDM) 68 | 69 | Release = make_prototype("asdkRelease", [pDM]) 70 | 71 | Send = make_prototype("asdkSend", [pDM, Scalar_p]) 72 | 73 | SendPattern = make_prototype("asdkSendPattern", [pDM, Scalar_p, UInt, UInt]) 74 | 75 | Set = make_prototype("asdkSet", [pDM, CStr, Scalar]) 76 | 77 | Stop = make_prototype("asdkStop", [pDM]) 78 | -------------------------------------------------------------------------------- /microscope/_wrappers/mirao52e.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Wrapper to the Imagine Optic Mirao 52-e API. 21 | """ 22 | 23 | import ctypes 24 | 25 | import microscope._utils 26 | 27 | # Vendor only supports Windows 28 | SDK = microscope._utils.library_loader("mirao52e", ctypes.WinDLL) 29 | 30 | 31 | TRUE = 1 # TRUE MroBoolean value 32 | FALSE = 0 # FALSE MroBoolean value 33 | 34 | # Number of values of a mirao 52-e command (the number of actuators 35 | # is a define on the library header) 36 | NB_COMMAND_VALUES = 52 37 | 38 | # Error code defines 39 | OK = 0 40 | 41 | 42 | Boolean = ctypes.c_char 43 | Command = ctypes.POINTER(ctypes.c_double) 44 | 45 | 46 | def prototype(name, argtypes, restype=Boolean): 47 | func = getattr(SDK, name) 48 | # All functions have 'int *' as the last argument for status. 49 | func.argtypes = argtypes + [ctypes.POINTER(ctypes.c_int)] 50 | func.restype = restype 51 | return func 52 | 53 | 54 | open = prototype("mro_open", []) 55 | close = prototype("mro_close", []) 56 | 57 | applyCommand = prototype("mro_applyCommand", [Command, Boolean]) 58 | -------------------------------------------------------------------------------- /microscope/cameras/_SDK3Cam.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2012 David Baddeley 4 | ## Copyright (C) 2020 David Miguel Susano Pinto 5 | ## Copyright (C) 2020 Mick Phillips 6 | ## 7 | ## This file is part of Microscope. 8 | ## 9 | ## Microscope is free software: you can redistribute it and/or modify 10 | ## it under the terms of the GNU General Public License as published by 11 | ## the Free Software Foundation, either version 3 of the License, or 12 | ## (at your option) any later version. 13 | ## 14 | ## Microscope is distributed in the hope that it will be useful, 15 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ## GNU General Public License for more details. 18 | ## 19 | ## You should have received a copy of the GNU General Public License 20 | ## along with Microscope. If not, see . 21 | 22 | from microscope.cameras import _SDK3 as SDK3 23 | 24 | 25 | class ATProperty: 26 | def connect(self, handle, propertyName): 27 | self.handle = handle 28 | self.propertyName = propertyName 29 | 30 | def isImplemented(self): 31 | return SDK3.IsImplemented(self.handle, self.propertyName).value 32 | 33 | def isReadable(self): 34 | return SDK3.IsReadable(self.handle, self.propertyName).value 35 | 36 | def isWritable(self): 37 | return SDK3.IsWritable(self.handle, self.propertyName).value 38 | 39 | def isReadOnly(self): 40 | return SDK3.IsReadOnly(self.handle, self.propertyName).value 41 | 42 | 43 | class ATInt(ATProperty): 44 | def getValue(self): 45 | return SDK3.GetInt(self.handle, self.propertyName).value 46 | 47 | def setValue(self, val): 48 | SDK3.SetInt(self.handle, self.propertyName, val) 49 | 50 | def max(self): 51 | return SDK3.GetIntMax(self.handle, self.propertyName).value 52 | 53 | def min(self): 54 | return SDK3.GetIntMin(self.handle, self.propertyName).value 55 | 56 | 57 | class ATBool(ATProperty): 58 | def getValue(self): 59 | return SDK3.GetBool(self.handle, self.propertyName).value > 0 60 | 61 | def setValue(self, val): 62 | SDK3.SetBool(self.handle, self.propertyName, val) 63 | 64 | 65 | class ATFloat(ATProperty): 66 | def getValue(self): 67 | return SDK3.GetFloat(self.handle, self.propertyName).value 68 | 69 | def setValue(self, val): 70 | SDK3.SetFloat(self.handle, self.propertyName, val) 71 | 72 | def max(self): 73 | return SDK3.GetFloatMax(self.handle, self.propertyName).value 74 | 75 | def min(self): 76 | return SDK3.GetFloatMin(self.handle, self.propertyName).value 77 | 78 | 79 | class ATString(ATProperty): 80 | def getValue(self): 81 | return SDK3.GetString(self.handle, self.propertyName, 255).value 82 | 83 | def setValue(self, val): 84 | SDK3.SetString(self.handle, self.propertyName, val) 85 | 86 | def maxLength(self): 87 | return SDK3.GetStringMaxLength(self.handle, self.propertyName).value 88 | 89 | 90 | class ATEnum(ATProperty): 91 | def getIndex(self): 92 | return SDK3.GetEnumIndex(self.handle, self.propertyName).value 93 | 94 | def setIndex(self, val): 95 | SDK3.SetEnumIndex(self.handle, self.propertyName, val) 96 | 97 | def getString(self): 98 | return self.__getitem__(self.getIndex()) 99 | 100 | def setString(self, val): 101 | SDK3.SetEnumString(self.handle, self.propertyName, val) 102 | 103 | def __len__(self): 104 | return SDK3.GetEnumCount(self.handle, self.propertyName).value 105 | 106 | def __getitem__(self, key): 107 | return SDK3.GetEnumStringByIndex( 108 | self.handle, self.propertyName, key, 255 109 | ).value 110 | 111 | def getAvailableValues(self): 112 | n = SDK3.GetEnumCount(self.handle, self.propertyName).value 113 | return [ 114 | SDK3.GetEnumStringByIndex( 115 | self.handle, self.propertyName, i, 255 116 | ).value 117 | for i in range(n) 118 | if SDK3.IsEnumIndexAvailable( 119 | self.handle, self.propertyName, i 120 | ).value 121 | ] 122 | 123 | def getAvailableValueMap(self): 124 | n = SDK3.GetEnumCount(self.handle, self.propertyName).value 125 | return { 126 | i: SDK3.GetEnumStringByIndex( 127 | self.handle, self.propertyName, i, 255 128 | ).value 129 | for i in range(n) 130 | if SDK3.IsEnumIndexAvailable( 131 | self.handle, self.propertyName, i 132 | ).value 133 | } 134 | 135 | 136 | class ATCommand(ATProperty): 137 | def __call__(self): 138 | return SDK3.Command(self.handle, self.propertyName) 139 | 140 | 141 | class camReg: 142 | # keep track of the number of cameras initialised so we can initialise and finalise the library 143 | numCameras = 0 144 | 145 | @classmethod 146 | def regCamera(cls): 147 | if cls.numCameras == 0: 148 | SDK3.InitialiseLibrary() 149 | 150 | cls.numCameras += 1 151 | 152 | @classmethod 153 | def unregCamera(cls): 154 | cls.numCameras -= 1 155 | if cls.numCameras == 0: 156 | SDK3.FinaliseLibrary() 157 | 158 | 159 | # make sure the library is intitalised 160 | camReg.regCamera() 161 | 162 | 163 | def GetNumCameras(): 164 | return SDK3.GetInt(SDK3.AT_HANDLE_SYSTEM, "DeviceCount").value 165 | 166 | 167 | def GetSoftwareVersion(): 168 | return SDK3.GetString(SDK3.AT_HANDLE_SYSTEM, "SoftwareVersion", 255) 169 | 170 | 171 | class SDK3Camera: 172 | def __init__(self, camNum): 173 | """camera initialisation - note that this should be called from derived classes 174 | *AFTER* the properties have been defined""" 175 | # camReg.regCamera() #initialise the library if needed 176 | self.camNum = camNum 177 | 178 | def Init(self): 179 | self.handle = SDK3.Open(self.camNum) 180 | self.connectProperties() 181 | 182 | def connectProperties(self): 183 | for name, var in self.__dict__.items(): 184 | if isinstance(var, ATProperty): 185 | var.connect(self.handle, name) 186 | 187 | def shutdown(self): 188 | SDK3.Close(self.handle) 189 | # camReg.unregCamera() 190 | -------------------------------------------------------------------------------- /microscope/cameras/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/cameras/__init__.py -------------------------------------------------------------------------------- /microscope/clients.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 Mick Phillips 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """TODO: complete this docstring 21 | """ 22 | 23 | import inspect 24 | import itertools 25 | import queue 26 | import socket 27 | import threading 28 | 29 | import Pyro4 30 | 31 | # Pyro configuration. Use pickle because it can serialize numpy ndarrays. 32 | Pyro4.config.SERIALIZERS_ACCEPTED.add("pickle") 33 | Pyro4.config.SERIALIZER = "pickle" 34 | 35 | LISTENERS = {} 36 | 37 | 38 | class Client: 39 | """Base Client object that makes methods on proxy available locally.""" 40 | 41 | def __init__(self, url): 42 | self._url = url 43 | self._proxy = None 44 | self._connect() 45 | 46 | def _connect(self): 47 | """Connect to a proxy and set up self passthrough to proxy methods.""" 48 | self._proxy = Pyro4.Proxy(self._url) 49 | self._proxy._pyroGetMetadata() 50 | 51 | # Derived classes may over-ride some methods. Leave these alone. 52 | my_methods = [ 53 | m[0] for m in inspect.getmembers(self, predicate=inspect.ismethod) 54 | ] 55 | methods = set(self._proxy._pyroMethods).difference(my_methods) 56 | # But in the case of propertyes, we need to inspect the class. 57 | my_properties = [ 58 | m[0] 59 | for m in inspect.getmembers( 60 | self.__class__, predicate=inspect.isdatadescriptor 61 | ) 62 | ] 63 | properties = set(self._proxy._pyroAttrs).difference(my_properties) 64 | 65 | for attr in itertools.chain(methods, properties): 66 | setattr(self, attr, getattr(self._proxy, attr)) 67 | 68 | 69 | class DataClient(Client): 70 | """A client that can receive and buffer data.""" 71 | 72 | def __init__(self, url): 73 | super().__init__(url) 74 | self._buffer = queue.Queue() 75 | # Register self with a listener. 76 | if self._url.split("@")[1].split(":")[0] in ["127.0.0.1", "localhost"]: 77 | iface = "127.0.0.1" 78 | else: 79 | # TODO: support multiple interfaces. Could use ifaddr.get_adapters() to 80 | # query ip addresses then pick first interface on the same subnet. 81 | iface = socket.gethostbyname(socket.gethostname()) 82 | if iface not in LISTENERS: 83 | LISTENERS[iface] = Pyro4.Daemon(host=iface) 84 | lthread = threading.Thread(target=LISTENERS[iface].requestLoop) 85 | lthread.daemon = True 86 | lthread.start() 87 | self._client_uri = LISTENERS[iface].register(self) 88 | 89 | def enable(self): 90 | """Set the client on the remote and enable it.""" 91 | self.set_client(self._client_uri) 92 | self._proxy.enable() 93 | 94 | @Pyro4.expose 95 | @Pyro4.oneway 96 | # noinspection PyPep8Naming 97 | # Legacy naming convention. 98 | def receiveData(self, data, timestamp, *args): 99 | del args 100 | self._buffer.put((data, timestamp)) 101 | 102 | def trigger_and_wait(self): 103 | if not hasattr(self, "trigger"): 104 | raise Exception("Device has no trigger method.") 105 | self.trigger() 106 | return self._buffer.get(block=True) 107 | -------------------------------------------------------------------------------- /microscope/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/controllers/__init__.py -------------------------------------------------------------------------------- /microscope/controllers/lumencor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Lumencor Spectra Light Engine. 21 | 22 | The implementation here is limited to the Lumencor Spectra III but 23 | should be trivial to make it work for other Lumencor light engines. 24 | We only need access to other such devices. 25 | 26 | .. note:: 27 | 28 | The engine is expected to be on the standard mode communications 29 | (not legacy). This can be changed via the device web interface. 30 | """ 31 | 32 | from typing import List, Mapping, Tuple 33 | 34 | import serial 35 | 36 | import microscope 37 | import microscope._utils 38 | import microscope.abc 39 | 40 | 41 | class _SpectraIIIConnection: 42 | """Connection to a Spectra III Light Engine. 43 | 44 | This module makes checks for Spectra III light engine and it was 45 | only tested for it. But it should work with other lumencor light 46 | engines with little work though, if only we got access to them. 47 | 48 | """ 49 | 50 | def __init__(self, serial: microscope._utils.SharedSerial) -> None: 51 | self._serial = serial 52 | # If the Spectra has just been powered up the first command 53 | # will fail with UNKNOWNCMD. So just send an empty command 54 | # and ignore the result. 55 | self._serial.write(b"\n") 56 | self._serial.readline() 57 | 58 | # We use command() and readline() instead of get_command() in 59 | # case this is not a Lumencor and won't even give a standard 60 | # answer and raises an exception during the answer validation. 61 | self._serial.write(b"GET MODEL\n") 62 | answer = self._serial.readline() 63 | if not answer.startswith(b"A MODEL Spectra III"): 64 | raise microscope.InitialiseError( 65 | "Not a Lumencor Spectra III Light Engine" 66 | ) 67 | 68 | def command_and_answer(self, *TX_tokens: bytes) -> bytes: 69 | # Command contains two or more tokens. The first token for a 70 | # TX (transmitted) command string is one of the two keywords 71 | # GET, SET (to query or to set). The second token is the 72 | # command name. 73 | assert len(TX_tokens) >= 2, "invalid command with less than two tokens" 74 | assert TX_tokens[0] in ( 75 | b"GET", 76 | b"SET", 77 | ), "invalid command (not SET/GET)" 78 | 79 | TX_command = b" ".join(TX_tokens) + b"\n" 80 | with self._serial.lock: 81 | self._serial.write(TX_command) 82 | answer = self._serial.readline() 83 | RX_tokens = answer.split(maxsplit=2) 84 | # A received answer has at least two tokens. The first token 85 | # is A or E (for success or failure). The second token is the 86 | # command name (second token of the transmitted command). 87 | if ( 88 | len(RX_tokens) < 2 89 | or RX_tokens[0] != b"A" 90 | or RX_tokens[1] != TX_tokens[1] 91 | ): 92 | raise microscope.DeviceError( 93 | "command %s failed: %s" % (TX_command, answer) 94 | ) 95 | return answer 96 | 97 | def get_command(self, command: bytes, *args: bytes) -> bytes: 98 | answer = self.command_and_answer(b"GET", command, *args) 99 | # The three bytes we remove at the start are the 'A ' before 100 | # the command, and the space after the command. The last two 101 | # bytes are '\r\n'. 102 | return answer[3 + len(command) : -2] 103 | 104 | def set_command(self, command: bytes, *args: bytes) -> None: 105 | self.command_and_answer(b"SET", command, *args) 106 | 107 | def get_channel_map(self) -> List[Tuple[int, str]]: 108 | answer = self.get_command(b"CHMAP") 109 | return list(enumerate(answer.decode().split())) 110 | 111 | 112 | class _LightChannelConnection: 113 | """Commands for a channel in a Lumencor light engine.""" 114 | 115 | def __init__(self, connection: _SpectraIIIConnection, index: int) -> None: 116 | self._conn = connection 117 | self._index_bytes = b"%d" % index 118 | 119 | def get_light_state(self) -> bool: 120 | """On (True) or off (False) state""" 121 | # We use CHACT (actual light state) instead of CH (light 122 | # state) because CH checks both the TTL inputs and channel 123 | # state switches. 124 | state = self._conn.get_command(b"CHACT", self._index_bytes) 125 | if state == b"1": 126 | return True 127 | elif state == b"0": 128 | return False 129 | else: 130 | raise microscope.DeviceError("unexpected answer") 131 | 132 | def set_light_state(self, state: bool) -> None: 133 | """Turn light on (True) or off (False).""" 134 | state_arg = b"1" if state else b"0" 135 | self._conn.set_command(b"CH", self._index_bytes, state_arg) 136 | 137 | def get_max_intensity(self) -> int: 138 | """Maximum valid intensity that can be applied to a light channel.""" 139 | return int(self._conn.get_command(b"MAXINT", self._index_bytes)) 140 | 141 | def get_intensity(self) -> int: 142 | """Current intensity setting between 0 and maximum intensity.""" 143 | return int(self._conn.get_command(b"CHINT", self._index_bytes)) 144 | 145 | def set_intensity(self, intensity: int) -> None: 146 | """Set light intensity between 0 and maximum intensity.""" 147 | self._conn.set_command(b"CHINT", self._index_bytes, b"%d" % intensity) 148 | 149 | 150 | class SpectraIIILightEngine(microscope.abc.Controller): 151 | """Spectra III Light Engine. 152 | 153 | Args: 154 | port: port name (Windows) or path to port (everything else) to 155 | connect to. For example, `/dev/ttyS1`, `COM1`, or 156 | `/dev/cuad1`. 157 | 158 | The names used on the devices dict are the ones provided by the 159 | Spectra engine. These are the colour names in capitals such as 160 | `'BLUE'`, `'NIR'`, or `'VIOLET'`. 161 | 162 | Not all sources may be turned on simultaneously. To prevent 163 | exceeding the capacity of the DC power supply, power consumption 164 | is tracked by the Spectra onboard computer. If a set limit is 165 | exceeded, either by increasing intensity settings for sources that 166 | are already on, or by turning on additional sources, commands will 167 | be rejected. To clear the error condition, reduce intensities of 168 | sources that are on or turn off additional sources. 169 | 170 | """ 171 | 172 | def __init__(self, port: str, **kwargs) -> None: 173 | super().__init__(**kwargs) 174 | self._lights: Mapping[str, microscope.abc.Device] = {} 175 | 176 | # We use standard (not legacy) mode communication so 115200,8,N,1 177 | serial_conn = serial.Serial( 178 | port=port, 179 | baudrate=115200, 180 | timeout=1, 181 | bytesize=serial.EIGHTBITS, 182 | stopbits=serial.STOPBITS_ONE, 183 | parity=serial.PARITY_NONE, 184 | xonxoff=False, 185 | rtscts=False, 186 | dsrdtr=False, 187 | ) 188 | shared_serial = microscope._utils.SharedSerial(serial_conn) 189 | connection = _SpectraIIIConnection(shared_serial) 190 | 191 | for index, name in connection.get_channel_map(): 192 | assert ( 193 | name not in self._lights 194 | ), "light with name '%s' already mapped" 195 | self._lights[name] = _SpectraIIILightChannel(connection, index) 196 | 197 | @property 198 | def devices(self) -> Mapping[str, microscope.abc.Device]: 199 | return self._lights 200 | 201 | 202 | class _SpectraIIILightChannel( 203 | microscope._utils.OnlyTriggersBulbOnSoftwareMixin, 204 | microscope.abc.LightSource, 205 | ): 206 | """A single light channel from a light engine. 207 | 208 | A channel may be an LED, luminescent light pipe, or a laser. 209 | """ 210 | 211 | def __init__(self, connection: _SpectraIIIConnection, index: int) -> None: 212 | super().__init__() 213 | self._conn = _LightChannelConnection(connection, index) 214 | # The lumencor only allows to set the power via intensity 215 | # levels (values between 0 and MAXINT). We keep the max 216 | # intensity internal as float for the multiply/divide 217 | # operations. 218 | self._max_intensity = float(self._conn.get_max_intensity()) 219 | 220 | def _do_shutdown(self) -> None: 221 | # There is a shutdown command but this actually powers off the 222 | # device which is not what LightSource.shutdown() is meant to 223 | # do. So do nothing. 224 | pass 225 | 226 | def get_status(self) -> List[str]: 227 | status: List[str] = [] 228 | return status 229 | 230 | def enable(self) -> None: 231 | self._conn.set_light_state(True) 232 | 233 | def disable(self) -> None: 234 | self._conn.set_light_state(False) 235 | 236 | def get_is_on(self) -> bool: 237 | return self._conn.get_light_state() 238 | 239 | def _do_set_power(self, power: float) -> None: 240 | self._conn.set_intensity(int(power * self._max_intensity)) 241 | 242 | def _do_get_power(self) -> float: 243 | return self._conn.get_intensity() / self._max_intensity 244 | -------------------------------------------------------------------------------- /microscope/controllers/prior.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Prior controller. 21 | """ 22 | 23 | import contextlib 24 | import threading 25 | from typing import Mapping 26 | 27 | import serial 28 | 29 | import microscope.abc 30 | 31 | 32 | class _ProScanIIIConnection: 33 | """Connection to a Prior ProScanIII and wrapper to its commands. 34 | 35 | Devices that are controlled by the same controller should share 36 | the same connection instance to ensure correct synchronization of 37 | communications from different threads. This ensures that commands 38 | for different devices, or replies from different devices, don't 39 | get entangled. 40 | 41 | This class also implements the logic to parse and validate 42 | commands so it can be shared between multiple devices. 43 | 44 | """ 45 | 46 | def __init__(self, port: str, baudrate: int, timeout: float) -> None: 47 | # From the technical datasheet: 8 bit word 1 stop bit, no 48 | # parity no handshake, baudrate options of 9600, 19200, 38400, 49 | # 57600 and 115200. 50 | self._serial = serial.Serial( 51 | port=port, 52 | baudrate=baudrate, 53 | timeout=timeout, 54 | bytesize=serial.EIGHTBITS, 55 | stopbits=serial.STOPBITS_ONE, 56 | parity=serial.PARITY_NONE, 57 | xonxoff=False, 58 | rtscts=False, 59 | dsrdtr=False, 60 | ) 61 | self._lock = threading.RLock() 62 | 63 | with self._lock: 64 | # We do not use the general get_description() here because 65 | # if this is not a ProScan device it would never reach the 66 | # '\rEND\r' that signals the end of the description. 67 | self.command(b"?") 68 | answer = self.readline() 69 | if answer != b"PROSCAN INFORMATION\r": 70 | self.read_until_timeout() 71 | raise RuntimeError( 72 | "Not a ProScanIII device: '?' returned '%s'" 73 | % answer.decode() 74 | ) 75 | # A description ends with END on its own line. 76 | line = self._serial.read_until(b"\rEND\r") 77 | if not line.endswith(b"\rEND\r"): 78 | raise RuntimeError("Failed to clear description") 79 | 80 | def command(self, command: bytes) -> None: 81 | """Send command to device.""" 82 | with self._lock: 83 | self._serial.write(command + b"\r") 84 | 85 | def readline(self) -> bytes: 86 | """Read a line from the device connection.""" 87 | with self._lock: 88 | return self._serial.read_until(b"\r") 89 | 90 | def read_until_timeout(self) -> None: 91 | """Read until timeout; used to clean buffer if in an unknown state.""" 92 | with self._lock: 93 | self._serial.flushInput() 94 | while self._serial.readline(): 95 | continue 96 | 97 | def _command_and_validate(self, command: bytes, expected: bytes) -> None: 98 | """Send command and raise exception if answer is unexpected""" 99 | with self._lock: 100 | answer = self.get_command(command) 101 | if answer != expected: 102 | self.read_until_timeout() 103 | raise RuntimeError( 104 | "command '%s' failed (got '%s')" 105 | % (command.decode(), answer.decode()) 106 | ) 107 | 108 | def get_command(self, command: bytes) -> bytes: 109 | """Send get command and return the answer.""" 110 | with self._lock: 111 | self.command(command) 112 | return self.readline() 113 | 114 | def move_command(self, command: bytes) -> None: 115 | """Send a move command and check return value.""" 116 | # Movement commands respond with an R at the end of move. 117 | # Once a movement command is issued the application should 118 | # wait until the end of move R response is received before 119 | # sending any further commands. 120 | # TODO: this times 10 for timeout is a bit arbitrary. 121 | with self.changed_timeout(10 * self._serial.timeout): 122 | self._command_and_validate(command, b"R\r") 123 | 124 | def set_command(self, command: bytes) -> None: 125 | """Send a set command and check return value.""" 126 | # Property type commands that set certain status respond with 127 | # zero. They respond with a zero even if there are invalid 128 | # arguments in the command. 129 | self._command_and_validate(command, b"0\r") 130 | 131 | def get_description(self, command: bytes) -> bytes: 132 | """Send a get description command and return it.""" 133 | with self._lock: 134 | self.command(command) 135 | return self._serial.read_until(b"\rEND\r") 136 | 137 | @contextlib.contextmanager 138 | def changed_timeout(self, new_timeout: float): 139 | previous = self._serial.timeout 140 | try: 141 | self._serial.timeout = new_timeout 142 | yield 143 | finally: 144 | self._serial.timeout = previous 145 | 146 | def assert_filterwheel_number(self, number: int) -> None: 147 | assert number > 0 and number < 4 148 | 149 | def _has_thing(self, command: bytes, expected_start: bytes) -> bool: 150 | # Use the commands that returns a description string to find 151 | # whether a specific device is connected. 152 | with self._lock: 153 | description = self.get_description(command) 154 | if not description.startswith(expected_start): 155 | self.read_until_timeout() 156 | raise RuntimeError( 157 | "Failed to get description '%s' (got '%s')" 158 | % (command.decode(), description.decode()) 159 | ) 160 | return not description.startswith(expected_start + b"NONE\r") 161 | 162 | def has_filterwheel(self, number: int) -> bool: 163 | self.assert_filterwheel_number(number) 164 | # We use the 'FILTER w' command to check if there's a filter 165 | # wheel instead of the '?' command. The reason is that the 166 | # third filter wheel, named "A AXIS" on the controller box and 167 | # "FOURTH" on the output of the '?' command, can be used for 168 | # non filter wheels. We hope that 'FILTER 3' will fail 169 | # properly if what is connected to "A AXIS" is not a filter 170 | # wheel. 171 | return self._has_thing(b"FILTER %d" % number, b"FILTER_%d = " % number) 172 | 173 | def get_n_filter_positions(self, number: int) -> int: 174 | self.assert_filterwheel_number(number) 175 | answer = self.get_command(b"FPW %d" % number) 176 | return int(answer) 177 | 178 | def get_filter_position(self, number: int) -> int: 179 | self.assert_filterwheel_number(number) 180 | answer = self.get_command(b"7 %d F" % number) 181 | return int(answer) 182 | 183 | def set_filter_position(self, number: int, pos: int) -> None: 184 | self.assert_filterwheel_number(number) 185 | self.move_command(b"7 %d %d" % (number, pos)) 186 | 187 | 188 | class ProScanIII(microscope.abc.Controller): 189 | """Prior ProScanIII controller. 190 | 191 | The controlled devices have the following labels: 192 | 193 | `filter 1` 194 | Filter wheel connected to connector labelled "FILTER 1". 195 | `filter 2` 196 | Filter wheel connected to connector labelled "FILTER 1". 197 | `filter 3` 198 | Filter wheel connected to connector labelled "A AXIS". 199 | 200 | .. note:: 201 | 202 | The Prior ProScanIII can control up to three filter wheels. 203 | However, a filter position may have a different number 204 | dependening on which connector it is. For example, using an 8 205 | position filter wheel, what is position 1 on the "filter 1" and 206 | "filter 2" connectors, is position 4 when on the "A axis" (or 207 | "filter 3") connector. 208 | 209 | """ 210 | 211 | def __init__( 212 | self, port: str, baudrate: int = 9600, timeout: float = 0.5, **kwargs 213 | ) -> None: 214 | super().__init__(**kwargs) 215 | self._conn = _ProScanIIIConnection(port, baudrate, timeout) 216 | self._devices: Mapping[str, microscope.abc.Device] = {} 217 | 218 | # Can have up to three filter wheels, numbered 1 to 3. 219 | for number in range(1, 4): 220 | if self._conn.has_filterwheel(number): 221 | key = "filter %d" % number 222 | self._devices[key] = _ProScanIIIFilterWheel(self._conn, number) 223 | 224 | @property 225 | def devices(self) -> Mapping[str, microscope.abc.Device]: 226 | return self._devices 227 | 228 | 229 | class _ProScanIIIFilterWheel(microscope.abc.FilterWheel): 230 | def __init__(self, connection: _ProScanIIIConnection, number: int) -> None: 231 | super().__init__(positions=connection.get_n_filter_positions(number)) 232 | self._conn = connection 233 | self._number = number 234 | 235 | def _do_get_position(self) -> int: 236 | return self._conn.get_filter_position(self._number) 237 | 238 | def _do_set_position(self, position: int) -> None: 239 | self._conn.set_filter_position(self._number, position) 240 | 241 | def _do_shutdown(self) -> None: 242 | pass 243 | -------------------------------------------------------------------------------- /microscope/devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """This module is deprecated and only kept for backwards compatibility. 21 | """ 22 | 23 | from microscope import ROI, AxisLimits, Binning, TriggerMode, TriggerType 24 | from microscope.abc import Camera as CameraDevice 25 | from microscope.abc import Controller as ControllerDevice 26 | from microscope.abc import DataDevice, DeformableMirror, Device 27 | from microscope.abc import FilterWheel as FilterWheelBase 28 | from microscope.abc import FloatingDeviceMixin 29 | from microscope.abc import LightSource as LaserDevice 30 | from microscope.abc import SerialDeviceMixin as SerialDeviceMixIn 31 | from microscope.abc import Stage as StageDevice 32 | from microscope.abc import StageAxis 33 | from microscope.abc import TriggerTargetMixin as TriggerTargetMixIn 34 | from microscope.abc import keep_acquiring 35 | from microscope.device_server import device 36 | -------------------------------------------------------------------------------- /microscope/deviceserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """This module is deprecated and only kept for backwards compatibility. 21 | """ 22 | 23 | from microscope.device_server import * 24 | -------------------------------------------------------------------------------- /microscope/digitalio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/digitalio/__init__.py -------------------------------------------------------------------------------- /microscope/digitalio/raspberrypi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2023 Ian Dobbie 5 | ## 6 | ## 7 | ## This file is part of Microscope. 8 | ## 9 | ## Microscope is free software: you can redistribute it and/or modify 10 | ## it under the terms of the GNU General Public License as published by 11 | ## the Free Software Foundation, either version 3 of the License, or 12 | ## (at your option) any later version. 13 | ## 14 | ## Microscope is distributed in the hope that it will be useful, 15 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ## GNU General Public License for more details. 18 | ## 19 | ## You should have received a copy of the GNU General Public License 20 | ## along with Microscope. If not, see . 21 | 22 | """Raspberry Pi Digital IO module. 23 | """ 24 | 25 | import logging 26 | import queue 27 | import threading 28 | import time 29 | 30 | import RPi.GPIO as GPIO 31 | 32 | import microscope.abc 33 | 34 | # Support for async digital IO control on the Raspberryy Pi. 35 | # Currently supports digital input and output via GPIO lines 36 | 37 | 38 | # Use BCM GPIO references (naming convention for GPIO pins from Broadcom) 39 | # instead of physical pin numbers on the Raspberry Pi board 40 | GPIO.setmode(GPIO.BCM) 41 | _logger = logging.getLogger(__name__) 42 | 43 | 44 | class RPiDIO(microscope.abc.DigitalIO): 45 | """Digital IO device implementation for a Raspberry Pi 46 | 47 | Requires the raspberry pi RPi.GPIO library and the user must be 48 | in the gpio group to allow access to the io pins. 49 | 50 | gpioMap input array maps line numbers to specific GPIO pins 51 | [GPIO pin, GPIO pin] 52 | [27,25,29,...] line 0 in pin 27, 1 is pin 25 etc.... 53 | 54 | gpioState input array maps output and input lines. 55 | True maps to output 56 | False maps to input 57 | 58 | with the gpioMap above [True,False,True,..] would map: 59 | 27 to out, 60 | 25 to in 61 | 29 to out""" 62 | 63 | def __init__(self, gpioMap=[], gpioState=[], **kwargs): 64 | super().__init__(numLines=len(gpioMap), **kwargs) 65 | # setup io lines 1-n mapped to GPIO lines 66 | self._gpioMap = gpioMap 67 | self._IOMap = gpioState 68 | self._numLines = len(self._gpioMap) 69 | self.inputQ = queue.Queue() 70 | self._outputCache = [False] * self._numLines 71 | self.set_all_IO_state(self._IOMap) 72 | 73 | # functions needed 74 | 75 | def set_IO_state(self, line: int, state: bool) -> None: 76 | _logger.debug("Line %d set IO state %s" % (line, str(state))) 77 | if state: 78 | # true maps to output 79 | GPIO.setup(self._gpioMap[line], GPIO.OUT) 80 | self._IOMap[line] = True 81 | # restore state from cache. 82 | state = self._outputCache[line] 83 | GPIO.output(self._gpioMap[line], state) 84 | else: 85 | GPIO.setup(self._gpioMap[line], GPIO.IN) 86 | 87 | self._IOMap[line] = False 88 | self.register_HW_interupt(line) 89 | 90 | def register_HW_interupt(self, line): 91 | GPIO.remove_event_detect(self._gpioMap[line]) 92 | GPIO.add_event_detect( 93 | self._gpioMap[line], 94 | GPIO.BOTH, 95 | callback=self.HW_trigger, 96 | ) 97 | 98 | def HW_trigger(self, pin): 99 | state = GPIO.input(pin) 100 | line = self._gpioMap.index(pin) 101 | self.inputQ.put((line, state)) 102 | 103 | def get_IO_state(self, line: int) -> bool: 104 | # returns 105 | # True if the line is Output 106 | # Flase if Input 107 | # None in other cases (i2c, spi etc) 108 | pinmode = GPIO.gpio_function(self._gpioMap[line]) 109 | if pinmode == GPIO.OUT: 110 | return True 111 | elif pinmode == GPIO.IN: 112 | return False 113 | return None 114 | 115 | def write_line(self, line: int, state: bool): 116 | # Do we need to check if the line can be written? 117 | _logger.debug("Line %d set IO state %s" % (line, str(state))) 118 | self._outputCache[line] = state 119 | GPIO.output(self._gpioMap[line], state) 120 | 121 | def read_line(self, line: int) -> bool: 122 | # Should we check if the line is set to input first? 123 | # If input read the real state 124 | if not self._IOMap[line]: 125 | state = GPIO.input(self._gpioMap[line]) 126 | _logger.debug("Line %d returns %s" % (line, str(state))) 127 | return state 128 | else: 129 | # line is an outout so returned cached state 130 | return self._outputCache[line] 131 | 132 | def _do_shutdown(self) -> None: 133 | self.abort() 134 | 135 | def debug_ret_Q(self): 136 | if not self.inputQ.empty(): 137 | return self.inputQ.get() 138 | 139 | # functions required for a data device. 140 | def _fetch_data(self): 141 | # need to return data fetched from interupt driven state chnages. 142 | if self.inputQ.empty(): 143 | return None 144 | (line, state) = self.inputQ.get() 145 | _logger.info("Line %d chnaged to %s" % (line, str(state))) 146 | return (line, state) 147 | 148 | def _do_enable(self): 149 | for i in range(self._numLines): 150 | if not self._gpioMap[i]: 151 | # this is an input line so remove its subscription 152 | self.register_HW_interupt(self._gpioMap[i]) 153 | return True 154 | 155 | def _do_disable(self): 156 | self.abort() 157 | 158 | def abort(self): 159 | _logger.info("Disabling DIO module.") 160 | # remove interupt subscriptions 161 | for i in range(self._numLines): 162 | if not self._gpioMap[i]: 163 | # this is an input line so remove its subscription 164 | GPIO.remove_event_detect(self._gpioMap[i]) 165 | -------------------------------------------------------------------------------- /microscope/filterwheels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/filterwheels/__init__.py -------------------------------------------------------------------------------- /microscope/filterwheels/thorlabs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 Mick Phillips 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | import io 21 | import string 22 | import threading 23 | import warnings 24 | 25 | import serial 26 | 27 | import microscope 28 | import microscope.abc 29 | 30 | 31 | class ThorlabsFilterWheel(microscope.abc.FilterWheel): 32 | """Implements FilterServer wheel interface for Thorlabs FW102C. 33 | 34 | Note that the FW102C also has manual controls on the device, so clients 35 | should periodically query the current wheel position.""" 36 | 37 | def __init__(self, com, baud=115200, timeout=2.0, **kwargs): 38 | """Create ThorlabsFilterWheel 39 | 40 | :param com: COM port 41 | :param baud: baud rate 42 | :param timeout: serial timeout 43 | """ 44 | self.eol = "\r" 45 | rawSerial = serial.Serial( 46 | port=com, 47 | baudrate=baud, 48 | timeout=timeout, 49 | stopbits=serial.STOPBITS_ONE, 50 | bytesize=serial.EIGHTBITS, 51 | parity=serial.PARITY_NONE, 52 | xonxoff=0, 53 | ) 54 | # The Thorlabs controller serial implementation is strange. 55 | # Generally, it uses \r as EOL, but error messages use \n. 56 | # A readline after sending a 'pos?\r' command always times out, 57 | # but returns a string terminated by a newline. 58 | # We use TextIOWrapper with newline=None to perform EOL translation 59 | # inbound, but must explicitly append \r to outgoing commands. 60 | # The TextIOWrapper also deals with conversion between unicode 61 | # and bytes. 62 | self.connection = io.TextIOWrapper( 63 | rawSerial, 64 | newline=None, 65 | line_buffering=True, # flush on write 66 | write_through=True, # write out immediately 67 | ) 68 | # A lock for the connection. We should probably be using 69 | # SharedSerial (maybe change it to SharedIO, and have it 70 | # accept any IOBase implementation). 71 | self._lock = threading.RLock() 72 | position_count = int(self._send_command("pcount?")) 73 | super().__init__(positions=position_count, **kwargs) 74 | 75 | def _do_shutdown(self) -> None: 76 | pass 77 | 78 | def _do_set_position(self, new_position: int) -> None: 79 | # Thorlabs positions start at 1, hence the +1 80 | self._send_command("pos=%d" % (new_position + 1)) 81 | 82 | def _do_get_position(self): 83 | # Thorlabs positions start at 1, hence the -1 84 | try: 85 | return int(self._send_command("pos?")) - 1 86 | except TypeError: 87 | raise microscope.DeviceError( 88 | "Unable to get position of %s", self.__class__.__name__ 89 | ) 90 | 91 | def _readline(self): 92 | """Custom _readline to overcome limitations of the serial implementation.""" 93 | result = [] 94 | with self._lock: 95 | while not result or result[-1] not in ("\n", ""): 96 | char = self.connection.read() 97 | # Do not allow lines to be empty. 98 | if result or (char not in string.whitespace): 99 | result.append(char) 100 | return "".join(result) 101 | 102 | def _send_command(self, command): 103 | """Send a command and return any result.""" 104 | with self._lock: 105 | self.connection.write(command + self.eol) 106 | response = "dummy" 107 | while command not in response and ">" not in response: 108 | # Read until we receive the command echo. 109 | response = self._readline().strip() 110 | if command.endswith("?"): 111 | # Last response was the command. Next is result. 112 | return self._readline().strip() 113 | return None 114 | 115 | 116 | class ThorlabsFW102C(ThorlabsFilterWheel): 117 | """Deprecated, use ThorlabsFilterWheel. 118 | 119 | This class is from when ThorlabsFilterWheel did not automatically 120 | found its own number of positions and there was a separate class 121 | for each thorlabs filterwheel model. 122 | """ 123 | 124 | def __init__(self, *args, **kwargs): 125 | warnings.warn( 126 | "Use ThorlabsFilterWheel instead of ThorlabsFW102C", 127 | DeprecationWarning, 128 | stacklevel=2, 129 | ) 130 | super().__init__(*args, **kwargs) 131 | if self.n_positions != 6: 132 | raise microscope.InitialiseError( 133 | "Does not look like a FW102C, it has %d positions instead of 6" 134 | ) 135 | 136 | 137 | class ThorlabsFW212C(ThorlabsFilterWheel): 138 | """Deprecated, use ThorlabsFilterWheel. 139 | 140 | This class is from when ThorlabsFilterWheel did not automatically 141 | found its own number of positions and there was a separate class 142 | for each thorlabs filterwheel model. 143 | """ 144 | 145 | def __init__(self, *args, **kwargs): 146 | warnings.warn( 147 | "Use ThorlabsFilterWheel instead of ThorlabsFW212C", 148 | DeprecationWarning, 149 | stacklevel=2, 150 | ) 151 | super().__init__(*args, **kwargs) 152 | if self.n_positions != 12: 153 | raise microscope.InitialiseError( 154 | "Does not look like a FW212C, it has %d positions instead of 12" 155 | ) 156 | -------------------------------------------------------------------------------- /microscope/lasers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/lasers/__init__.py -------------------------------------------------------------------------------- /microscope/lasers/cobolt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2021 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Module kept for backwards compatibility. Look into microscope.lights.""" 21 | 22 | from microscope.lights.cobolt import CoboltLaser 23 | -------------------------------------------------------------------------------- /microscope/lasers/deepstar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2021 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Module kept for backwards compatibility. Look into microscope.lights.""" 21 | 22 | from microscope.lights.deepstar import DeepstarLaser 23 | -------------------------------------------------------------------------------- /microscope/lasers/obis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2021 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Module kept for backwards compatibility. Look into microscope.lights.""" 21 | 22 | from microscope.lights.obis import ObisLaser 23 | -------------------------------------------------------------------------------- /microscope/lasers/sapphire.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2021 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Module kept for backwards compatibility. Look into microscope.lights.""" 21 | 22 | from microscope.lights.sapphire import SapphireLaser 23 | -------------------------------------------------------------------------------- /microscope/lasers/toptica.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2021 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Module kept for backwards compatibility. Look into microscope.lights.""" 21 | 22 | from microscope.lights.toptica import TopticaiBeam 23 | -------------------------------------------------------------------------------- /microscope/lights/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/lights/__init__.py -------------------------------------------------------------------------------- /microscope/lights/cobolt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2020 Mick Phillips 5 | ## 6 | ## This file is part of Microscope. 7 | ## 8 | ## Microscope is free software: you can redistribute it and/or modify 9 | ## it under the terms of the GNU General Public License as published by 10 | ## the Free Software Foundation, either version 3 of the License, or 11 | ## (at your option) any later version. 12 | ## 13 | ## Microscope is distributed in the hope that it will be useful, 14 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ## GNU General Public License for more details. 17 | ## 18 | ## You should have received a copy of the GNU General Public License 19 | ## along with Microscope. If not, see . 20 | 21 | import logging 22 | 23 | import serial 24 | 25 | import microscope._utils 26 | import microscope.abc 27 | 28 | _logger = logging.getLogger(__name__) 29 | 30 | 31 | class CoboltLaser( 32 | microscope._utils.OnlyTriggersBulbOnSoftwareMixin, 33 | microscope.abc.SerialDeviceMixin, 34 | microscope.abc.LightSource, 35 | ): 36 | """Cobolt lasers. 37 | 38 | The cobolt lasers are diode pumped lasers and only supports 39 | `TriggerMode.SOFTWARE` (this is probably not completely true, some 40 | cobolt lasers are probably not diode pumped and those should be 41 | able to support other trigger modes, but we only got access to the 42 | 04 series). 43 | 44 | """ 45 | 46 | def __init__(self, com=None, baud=115200, timeout=0.1, **kwargs): 47 | super().__init__(**kwargs) 48 | self.connection = serial.Serial( 49 | port=com, 50 | baudrate=baud, 51 | timeout=timeout, 52 | stopbits=serial.STOPBITS_ONE, 53 | bytesize=serial.EIGHTBITS, 54 | parity=serial.PARITY_NONE, 55 | ) 56 | # Start a logger. 57 | response = self.send(b"sn?") 58 | _logger.info("Cobolt laser serial number: [%s]", response.decode()) 59 | # We need to ensure that autostart is disabled so that we can switch emission 60 | # on/off remotely. 61 | response = self.send(b"@cobas 0") 62 | _logger.info("Response to @cobas 0 [%s]", response.decode()) 63 | 64 | self._max_power_mw = float(self.send(b"gmlp?")) 65 | 66 | self.initialize() 67 | 68 | def send(self, command): 69 | """Send command and retrieve response.""" 70 | success = False 71 | while not success: 72 | self._write(command) 73 | response = self._readline() 74 | # Catch zero-length responses to queries and retry. 75 | if not command.endswith(b"?"): 76 | success = True 77 | elif len(response) > 0: 78 | success = True 79 | return response 80 | 81 | @microscope.abc.SerialDeviceMixin.lock_comms 82 | def clearFault(self): 83 | self.send(b"cf") 84 | return self.get_status() 85 | 86 | @microscope.abc.SerialDeviceMixin.lock_comms 87 | def get_status(self): 88 | result = [] 89 | for cmd, stat in [ 90 | (b"l?", "Emission on?"), 91 | (b"p?", "Target power:"), 92 | (b"pa?", "Measured power:"), 93 | (b"f?", "Fault?"), 94 | (b"hrs?", "Head operating hours:"), 95 | ]: 96 | response = self.send(cmd) 97 | result.append(stat + " " + response.decode()) 98 | return result 99 | 100 | @microscope.abc.SerialDeviceMixin.lock_comms 101 | def _do_shutdown(self) -> None: 102 | # Disable laser. 103 | self.disable() 104 | self.send(b"@cob0") 105 | self.connection.flushInput() 106 | 107 | # Initialization to do when cockpit connects. 108 | @microscope.abc.SerialDeviceMixin.lock_comms 109 | def initialize(self): 110 | self.connection.flushInput() 111 | # We don't want 'direct control' mode. 112 | self.send(b"@cobasdr 0") 113 | # Force laser into autostart mode. 114 | self.send(b"@cob1") 115 | 116 | # Turn the laser ON. Return True if we succeeded, False otherwise. 117 | @microscope.abc.SerialDeviceMixin.lock_comms 118 | def _do_enable(self): 119 | _logger.info("Turning laser ON.") 120 | # Turn on emission. 121 | response = self.send(b"l1") 122 | _logger.info("l1: [%s]", response.decode()) 123 | 124 | if not self.get_is_on(): 125 | # Something went wrong. 126 | _logger.error("Failed to turn on. Current status:\r\n") 127 | _logger.error(self.get_status()) 128 | return False 129 | return True 130 | 131 | # Turn the laser OFF. 132 | @microscope.abc.SerialDeviceMixin.lock_comms 133 | def disable(self): 134 | _logger.info("Turning laser OFF.") 135 | return self.send(b"l0").decode() 136 | 137 | # Return True if the laser is currently able to produce light. 138 | @microscope.abc.SerialDeviceMixin.lock_comms 139 | def get_is_on(self): 140 | response = self.send(b"l?") 141 | return response == b"1" 142 | 143 | @microscope.abc.SerialDeviceMixin.lock_comms 144 | def _get_power_mw(self) -> float: 145 | if not self.get_is_on(): 146 | return 0.0 147 | success = False 148 | # Sometimes the controller returns b'1' rather than the power. 149 | while not success: 150 | response = self.send(b"pa?") 151 | if response != b"1": 152 | success = True 153 | return 1000 * float(response) 154 | 155 | @microscope.abc.SerialDeviceMixin.lock_comms 156 | def _set_power_mw(self, mW: float) -> None: 157 | # There is no minimum power in cobolt lasers. Any 158 | # non-negative number is accepted. 159 | W_str = "%.4f" % (mW / 1000.0) 160 | _logger.info("Setting laser power to %s W.", W_str) 161 | return self.send(b"@cobasp " + W_str.encode()) 162 | 163 | def _do_set_power(self, power: float) -> None: 164 | self._set_power_mw(power * self._max_power_mw) 165 | 166 | def _do_get_power(self) -> float: 167 | return self._get_power_mw() / self._max_power_mw 168 | -------------------------------------------------------------------------------- /microscope/lights/deepstar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2020 Mick Phillips 5 | ## 6 | ## This file is part of Microscope. 7 | ## 8 | ## Microscope is free software: you can redistribute it and/or modify 9 | ## it under the terms of the GNU General Public License as published by 10 | ## the Free Software Foundation, either version 3 of the License, or 11 | ## (at your option) any later version. 12 | ## 13 | ## Microscope is distributed in the hope that it will be useful, 14 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ## GNU General Public License for more details. 17 | ## 18 | ## You should have received a copy of the GNU General Public License 19 | ## along with Microscope. If not, see . 20 | 21 | import logging 22 | 23 | import serial 24 | 25 | import microscope 26 | import microscope.abc 27 | 28 | _logger = logging.getLogger(__name__) 29 | 30 | 31 | class DeepstarLaser( 32 | microscope.abc.SerialDeviceMixin, microscope.abc.LightSource 33 | ): 34 | """Omicron DeepStar laser. 35 | 36 | Omicron LDM lasers can be bought with and without the LDM.APC 37 | power monitoring option (light pick-off). If this option is not 38 | available, the `power` attribute will return the set power value 39 | instead of the actual power value. 40 | 41 | """ 42 | 43 | def __init__(self, com, baud=9600, timeout=2.0, **kwargs): 44 | super().__init__(**kwargs) 45 | self.connection = serial.Serial( 46 | port=com, 47 | baudrate=baud, 48 | timeout=timeout, 49 | stopbits=serial.STOPBITS_ONE, 50 | bytesize=serial.EIGHTBITS, 51 | parity=serial.PARITY_NONE, 52 | ) 53 | # If the laser is currently on, then we need to use 7-byte mode; otherwise we need to 54 | # use 16-byte mode. 55 | self._write(b"S?") 56 | response = self._readline() 57 | _logger.info("Current laser state: [%s]", response.decode()) 58 | 59 | self._write(b"STAT3") 60 | option_codes = self._readline() 61 | if not option_codes.startswith(b"OC "): 62 | raise microscope.DeviceError( 63 | "Failed to get option codes '%s'" % option_codes.decode() 64 | ) 65 | if option_codes[9:12] == b"AP1": 66 | self._has_apc = True 67 | else: 68 | _logger.warning( 69 | "Laser is missing APC option. Will return set" 70 | " power instead of actual power" 71 | ) 72 | self._has_apc = False 73 | 74 | def _write(self, command): 75 | """Send a command.""" 76 | # We'll need to pad the command out to 16 bytes. There's also 77 | # a 7-byte mode but we never need to use it. CR/LF counts 78 | # towards the byte limit, hence 14 (16-2) 79 | command = command.ljust(14) + b"\r\n" 80 | response = self.connection.write(command) 81 | return response 82 | 83 | # Get the status of the laser, by sending the 84 | # STAT0, STAT1, STAT2, and STAT3 commands. 85 | @microscope.abc.SerialDeviceMixin.lock_comms 86 | def get_status(self): 87 | result = [] 88 | for i in range(4): 89 | self._write(("STAT%d" % i).encode()) 90 | result.append(self._readline().decode()) 91 | return result 92 | 93 | # Turn the laser ON. Return True if we succeeded, False otherwise. 94 | @microscope.abc.SerialDeviceMixin.lock_comms 95 | def _do_enable(self): 96 | _logger.info("Turning laser ON.") 97 | # Turn on deepstar mode with internal voltage ref 98 | # Enable internal peak power 99 | # Set MF turns off internal digital and bias modulation 100 | # Disable analog modulation to digital modulation 101 | for cmd, msg in [ 102 | (b"LON", "Enable response: [%s]"), 103 | (b"L2", "L2 response: [%s]"), 104 | (b"IPO", "Enable-internal peak power response: [%s]"), 105 | (b"MF", "MF response [%s]"), 106 | (b"A2DF", "A2DF response [%s]"), 107 | ]: 108 | self._write(cmd) 109 | response = self._readline() 110 | _logger.debug(msg, response.decode()) 111 | 112 | if not self.get_is_on(): 113 | # Something went wrong. 114 | self._write(b"S?") 115 | response = self._readline() 116 | _logger.error( 117 | "Failed to turn on. Current status: [%s]", response.decode() 118 | ) 119 | return False 120 | return True 121 | 122 | def _do_shutdown(self) -> None: 123 | self.disable() 124 | 125 | # Turn the laser OFF. 126 | @microscope.abc.SerialDeviceMixin.lock_comms 127 | def _do_disable(self): 128 | _logger.info("Turning laser OFF.") 129 | self._write(b"LF") 130 | return self._readline().decode() 131 | 132 | # Return True if the laser is currently able to produce light. We assume this is equivalent 133 | # to the laser being in S2 mode. 134 | @microscope.abc.SerialDeviceMixin.lock_comms 135 | def get_is_on(self): 136 | self._write(b"S?") 137 | response = self._readline() 138 | _logger.debug("Are we on? [%s]", response.decode()) 139 | return response == b"S2" 140 | 141 | @microscope.abc.SerialDeviceMixin.lock_comms 142 | def _do_set_power(self, power: float) -> None: 143 | _logger.debug("level=%d", power) 144 | power_int = int(power * 0xFFF) 145 | _logger.debug("power=%d", power_int) 146 | strPower = "PP%03X" % power_int 147 | _logger.debug("power level=%s", strPower) 148 | self._write(strPower.encode()) 149 | response = self._readline() 150 | _logger.debug("Power response [%s]", response.decode()) 151 | 152 | def _do_get_power(self) -> float: 153 | if not self.get_is_on(): 154 | return 0.0 155 | if self._has_apc: 156 | query = b"P" 157 | scale = 0xCCC 158 | else: 159 | query = b"PP" 160 | scale = 0xFFF 161 | 162 | self._write(query + b"?") 163 | answer = self._readline() 164 | if not answer.startswith(query): 165 | raise microscope.DeviceError( 166 | "failed to read power from '%s'" % answer.decode() 167 | ) 168 | 169 | level = int(answer[len(query) :], 16) 170 | return float(level) / float(scale) 171 | 172 | @property 173 | def trigger_type(self) -> microscope.TriggerType: 174 | return microscope.TriggerType.HIGH 175 | 176 | @property 177 | def trigger_mode(self) -> microscope.TriggerMode: 178 | return microscope.TriggerMode.BULB 179 | 180 | def set_trigger( 181 | self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode 182 | ) -> None: 183 | if ttype is not microscope.TriggerType.HIGH: 184 | raise microscope.UnsupportedFeatureError( 185 | "the only trigger type supported is 'high'" 186 | ) 187 | if tmode is not microscope.TriggerMode.BULB: 188 | raise microscope.UnsupportedFeatureError( 189 | "the only trigger mode supported is 'bulb'" 190 | ) 191 | 192 | def _do_trigger(self) -> None: 193 | raise microscope.IncompatibleStateError( 194 | "trigger does not make sense in trigger mode bulb, only enable" 195 | ) 196 | -------------------------------------------------------------------------------- /microscope/lights/obis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2020 Julio Mateos Langerak 5 | ## Copyright (C) 2020 Mick Phillips 6 | ## 7 | ## This file is part of Microscope. 8 | ## 9 | ## Microscope is free software: you can redistribute it and/or modify 10 | ## it under the terms of the GNU General Public License as published by 11 | ## the Free Software Foundation, either version 3 of the License, or 12 | ## (at your option) any later version. 13 | ## 14 | ## Microscope is distributed in the hope that it will be useful, 15 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ## GNU General Public License for more details. 18 | ## 19 | ## You should have received a copy of the GNU General Public License 20 | ## along with Microscope. If not, see . 21 | 22 | import logging 23 | 24 | import serial 25 | 26 | import microscope 27 | import microscope.abc 28 | 29 | _logger = logging.getLogger(__name__) 30 | 31 | 32 | class ObisLaser(microscope.abc.SerialDeviceMixin, microscope.abc.LightSource): 33 | def __init__(self, com, baud=115200, timeout=0.5, **kwargs) -> None: 34 | super().__init__(**kwargs) 35 | self.connection = serial.Serial( 36 | port=com, 37 | baudrate=baud, 38 | timeout=timeout, 39 | stopbits=serial.STOPBITS_ONE, 40 | bytesize=serial.EIGHTBITS, 41 | parity=serial.PARITY_NONE, 42 | ) 43 | # Start a logger. 44 | self._write(b"SYSTem:INFormation:MODel?") 45 | response = self._readline() 46 | _logger.info("OBIS laser model: [%s]", response.decode()) 47 | self._write(b"SYSTem:INFormation:SNUMber?") 48 | response = self._readline() 49 | _logger.info("OBIS laser serial number: [%s]", response.decode()) 50 | self._write(b"SYSTem:CDRH?") 51 | response = self._readline() 52 | _logger.info("CDRH safety: [%s]", response.decode()) 53 | self._write(b"SOURce:TEMPerature:APRobe?") 54 | response = self._readline() 55 | _logger.info("TEC temperature control: [%s]", response.decode()) 56 | self._write(b"*TST?") 57 | response = self._readline() 58 | _logger.info("Self test procedure: [%s]", response.decode()) 59 | 60 | # We need to ensure that autostart is disabled so that we can 61 | # switch emission on/off remotely. 62 | self._write(b"SYSTem:AUTostart?") 63 | response = self._readline() 64 | _logger.info("Response to Autostart: [%s]", response.decode()) 65 | 66 | self._write(b"SOURce:POWer:LIMit:HIGH?") 67 | response = self._readline() 68 | _logger.info("Max intensity in watts: [%s]", response.decode()) 69 | self._max_power_mw = float(response) * 1000.0 70 | 71 | self.initialize() 72 | 73 | def _write(self, command): 74 | """Send a command.""" 75 | response = self.connection.write(command + b"\r\n") 76 | return response 77 | 78 | def _readline(self): 79 | """Read a line from connection without leading and trailing whitespace. 80 | We override from SerialDeviceMixin 81 | """ 82 | response = self.connection.readline().strip() 83 | if self.connection.readline().strip() != b"OK": 84 | raise microscope.DeviceError( 85 | "Did not get a proper answer from the laser serial comm." 86 | ) 87 | return response 88 | 89 | def _flush_handshake(self): 90 | self.connection.readline() 91 | 92 | @microscope.abc.SerialDeviceMixin.lock_comms 93 | def get_status(self): 94 | result = [] 95 | for cmd, stat in [ 96 | (b"SOURce:AM:STATe?", "Emission on?"), 97 | (b"SOURce:POWer:LEVel:IMMediate:AMPLitude?", "Target power:"), 98 | (b"SOURce:POWer:LEVel?", "Measured power:"), 99 | (b"SYSTem:STATus?", "Status code?"), 100 | (b"SYSTem:FAULt?", "Fault code?"), 101 | (b"SYSTem:HOURs?", "Head operating hours:"), 102 | ]: 103 | self._write(cmd) 104 | result.append(stat + " " + self._readline().decode()) 105 | return result 106 | 107 | @microscope.abc.SerialDeviceMixin.lock_comms 108 | def _do_enable(self): 109 | """Turn the laser ON. Return True if we succeeded, False otherwise.""" 110 | _logger.info("Turning laser ON.") 111 | # Exiting Sleep Mode. 112 | self._write(b"SOURce:TEMPerature:APRobe ON") 113 | self._flush_handshake() 114 | # Turn on emission. 115 | self._write(b"SOURce:AM:STATe ON") 116 | self._flush_handshake() 117 | self._write(b"SOURce:AM:STATe?") 118 | response = self._readline() 119 | _logger.info("SOURce:AM:STATe? [%s]", response.decode()) 120 | 121 | if not self.get_is_on(): 122 | # Something went wrong. 123 | _logger.error("Failed to turn ON. Current status:\r\n") 124 | _logger.error(self.get_status()) 125 | return False 126 | return True 127 | 128 | def _do_shutdown(self) -> None: 129 | self.disable() 130 | # We set the power to a safe level 131 | self._set_power_mw(2) 132 | # We want it back into direct control mode. 133 | self._write(b"SOURce:AM:INTernal CWP") 134 | self._flush_handshake() 135 | 136 | # Going into Sleep mode 137 | self._write(b"SOURce:TEMPerature:APRobe OFF") 138 | self._flush_handshake() 139 | 140 | def initialize(self): 141 | # self.flush_buffer() 142 | # We ensure that handshaking is off. 143 | self._write(b"SYSTem:COMMunicate:HANDshaking ON") 144 | self._flush_handshake() 145 | # We don't want 'direct control' mode. 146 | # TODO: Change to MIXED when analogue output is available 147 | self._write(b"SOURce:AM:EXTernal DIGital") 148 | self._flush_handshake() 149 | 150 | @microscope.abc.SerialDeviceMixin.lock_comms 151 | def _do_disable(self): 152 | """Turn the laser OFF. Return True if we succeeded, False otherwise.""" 153 | _logger.info("Turning laser OFF.") 154 | # Turning LASER OFF 155 | self._write(b"SOURce:AM:STATe OFF") 156 | self._flush_handshake() 157 | 158 | if self.get_is_on(): 159 | _logger.error("Failed to turn OFF. Current status:\r\n") 160 | _logger.error(self.get_status()) 161 | return False 162 | return True 163 | 164 | @microscope.abc.SerialDeviceMixin.lock_comms 165 | def get_is_on(self): 166 | """Return True if the laser is currently able to produce light.""" 167 | self._write(b"SOURce:AM:STATe?") 168 | response = self._readline() 169 | _logger.info("Are we on? [%s]", response.decode()) 170 | return response == b"ON" 171 | 172 | @microscope.abc.SerialDeviceMixin.lock_comms 173 | def _get_power_mw(self): 174 | if not self.get_is_on(): 175 | return 0.0 176 | self._write(b"SOURce:POWer:LEVel?") 177 | response = self._readline() 178 | return float(response.decode()) * 1000.0 179 | 180 | @microscope.abc.SerialDeviceMixin.lock_comms 181 | def _set_power_mw(self, mw): 182 | power_w = mw / 1000.0 183 | _logger.info("Setting laser power to %.7sW", power_w) 184 | self._write(b"SOURce:POWer:LEVel:IMMediate:AMPLitude %.5f" % power_w) 185 | self._flush_handshake() 186 | 187 | def _do_set_power(self, power: float) -> None: 188 | self._set_power_mw(power * self._max_power_mw) 189 | 190 | def _do_get_power(self) -> float: 191 | return self._get_power_mw() / self._max_power_mw 192 | 193 | @property 194 | def trigger_type(self) -> microscope.TriggerType: 195 | return microscope.TriggerType.HIGH 196 | 197 | @property 198 | def trigger_mode(self) -> microscope.TriggerMode: 199 | return microscope.TriggerMode.BULB 200 | 201 | def set_trigger( 202 | self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode 203 | ) -> None: 204 | if ttype is not microscope.TriggerType.HIGH: 205 | raise microscope.UnsupportedFeatureError( 206 | "the only trigger type supported is 'high'" 207 | ) 208 | if tmode is not microscope.TriggerMode.BULB: 209 | raise microscope.UnsupportedFeatureError( 210 | "the only trigger mode supported is 'bulb'" 211 | ) 212 | 213 | def _do_trigger(self) -> None: 214 | raise microscope.IncompatibleStateError( 215 | "trigger does not make sense in trigger mode bulb, only enable" 216 | ) 217 | -------------------------------------------------------------------------------- /microscope/lights/sapphire.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2020 Ian Dobbie 5 | ## Copyright (C) 2020 Mick Phillips 6 | ## Copyright (C) 2020 Tiago Susano Pinto 7 | ## 8 | ## This file is part of Microscope. 9 | ## 10 | ## Microscope is free software: you can redistribute it and/or modify 11 | ## it under the terms of the GNU General Public License as published by 12 | ## the Free Software Foundation, either version 3 of the License, or 13 | ## (at your option) any later version. 14 | ## 15 | ## Microscope is distributed in the hope that it will be useful, 16 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ## GNU General Public License for more details. 19 | ## 20 | ## You should have received a copy of the GNU General Public License 21 | ## along with Microscope. If not, see . 22 | 23 | import logging 24 | 25 | import serial 26 | 27 | import microscope._utils 28 | import microscope.abc 29 | 30 | _logger = logging.getLogger(__name__) 31 | 32 | 33 | class SapphireLaser( 34 | microscope._utils.OnlyTriggersBulbOnSoftwareMixin, 35 | microscope.abc.SerialDeviceMixin, 36 | microscope.abc.LightSource, 37 | ): 38 | """Coherent Sapphire laser. 39 | 40 | The Sapphire is a diode-pumped solid-state laser and only supports 41 | `TriggerMode.SOFTWARE`. 42 | 43 | """ 44 | 45 | laser_status = { 46 | b"1": "Start up", 47 | b"2": "Warmup", 48 | b"3": "Standby", 49 | b"4": "Laser on", 50 | b"5": "Laser ready", 51 | b"6": "Error", 52 | } 53 | 54 | def __init__(self, com=None, baud=19200, timeout=0.5, **kwargs): 55 | # laser controller must run at 19200 baud, 8+1 bits, 56 | # no parity or flow control 57 | # timeout is recomended to be over 0.5 58 | super().__init__(**kwargs) 59 | self.connection = serial.Serial( 60 | port=com, 61 | baudrate=baud, 62 | timeout=timeout, 63 | stopbits=serial.STOPBITS_ONE, 64 | bytesize=serial.EIGHTBITS, 65 | parity=serial.PARITY_NONE, 66 | ) 67 | # Turning off command prompt 68 | self.send(b">=0") 69 | 70 | # The sapphire laser turns on as soon as the key is switched 71 | # on. So turn radiation off before we start. 72 | self.send(b"L=0") 73 | 74 | # Head ID value is a float point value, 75 | # but only the integer part is significant 76 | headID = int(float(self.send(b"?hid"))) 77 | _logger.info("Sapphire laser serial number: [%s]", headID) 78 | 79 | self._max_power_mw = float(self.send(b"?maxlp")) 80 | self._min_power = float(self.send(b"?minlp")) / self._max_power_mw 81 | 82 | self.initialize() 83 | 84 | def _write(self, command): 85 | count = super()._write(command) 86 | # This device always writes backs something. If echo is on, 87 | # it's the whole command, otherwise just an empty line. Read 88 | # it and throw it away. 89 | self._readline() 90 | return count 91 | 92 | def send(self, command): 93 | """Send command and retrieve response.""" 94 | self._write(command) 95 | return self._readline() 96 | 97 | @microscope.abc.SerialDeviceMixin.lock_comms 98 | def clearFault(self): 99 | self.flush_buffer() 100 | return self.get_status() 101 | 102 | def flush_buffer(self): 103 | line = b" " 104 | while len(line) > 0: 105 | line = self._readline() 106 | 107 | @microscope.abc.SerialDeviceMixin.lock_comms 108 | def get_status(self): 109 | result = [] 110 | 111 | status_code = self.send(b"?sta") 112 | result.append( 113 | ( 114 | "Laser status: " 115 | + self.laser_status.get(status_code, "Undefined") 116 | ) 117 | ) 118 | 119 | for cmd, stat in [ 120 | (b"?l", "Ligh Emission on?"), 121 | (b"?t", "TEC Servo on?"), 122 | (b"?k", "Key Switch on?"), 123 | (b"?sp", "Target power:"), 124 | (b"?p", "Measured power:"), 125 | (b"?hh", "Head operating hours:"), 126 | ]: 127 | result.append(stat + " " + self.send(cmd).decode()) 128 | 129 | self._write(b"?fl") 130 | faults = self._readline() 131 | response = self._readline() 132 | while response: 133 | faults += b" " + response 134 | response = self._readline() 135 | 136 | result.append(faults.decode()) 137 | return result 138 | 139 | @microscope.abc.SerialDeviceMixin.lock_comms 140 | def _do_shutdown(self) -> None: 141 | # Disable laser. 142 | self._write(b"l=0") 143 | self.flush_buffer() 144 | 145 | # Initialization to do when cockpit connects. 146 | @microscope.abc.SerialDeviceMixin.lock_comms 147 | def initialize(self): 148 | self.flush_buffer() 149 | 150 | # Turn the laser ON. Return True if we succeeded, False otherwise. 151 | @microscope.abc.SerialDeviceMixin.lock_comms 152 | def _do_enable(self): 153 | _logger.info("Turning laser ON.") 154 | # Turn on emission. 155 | response = self.send(b"l=1") 156 | _logger.info("l=1: [%s]", response.decode()) 157 | 158 | # Enabling laser might take more than 500ms (default timeout) 159 | prevTimeout = self.connection.timeout 160 | self.connection.timeout = max(1, prevTimeout) 161 | isON = self.get_is_on() 162 | self.connection.timeout = prevTimeout 163 | 164 | if not isON: 165 | # Something went wrong. 166 | _logger.error("Failed to turn on. Current status:\r\n") 167 | _logger.error(self.get_status()) 168 | return isON 169 | 170 | # Turn the laser OFF. 171 | @microscope.abc.SerialDeviceMixin.lock_comms 172 | def disable(self): 173 | _logger.info("Turning laser OFF.") 174 | return self._write(b"l=0") 175 | 176 | # Return True if the laser is currently able to produce light. 177 | @microscope.abc.SerialDeviceMixin.lock_comms 178 | def get_is_on(self): 179 | return self.send(b"?l") == b"1" 180 | 181 | @microscope.abc.SerialDeviceMixin.lock_comms 182 | def _get_power_mw(self): 183 | return float(self.send(b"?p")) 184 | 185 | @microscope.abc.SerialDeviceMixin.lock_comms 186 | def _set_power_mw(self, mW): 187 | mW_str = "%.3f" % mW 188 | _logger.info("Setting laser power to %s mW.", mW_str) 189 | # using send instead of _write, because 190 | # if laser is not on, warning is returned 191 | return self.send(b"p=%s" % mW_str.encode()) 192 | 193 | def _do_set_power(self, power: float) -> None: 194 | # power is already clipped to the [0 1] range but we need to 195 | # clip it again since the min power we actually can do is 0.2 196 | # and we get an error from the laser if we set it to lower. 197 | power = max(self._min_power, power) 198 | self._set_power_mw(power * self._max_power_mw) 199 | 200 | def _do_get_power(self) -> float: 201 | return self._get_power_mw() / self._max_power_mw 202 | -------------------------------------------------------------------------------- /microscope/mirror/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/mirror/__init__.py -------------------------------------------------------------------------------- /microscope/mirror/alpao.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | import ctypes 21 | import warnings 22 | 23 | import numpy as np 24 | 25 | import microscope 26 | import microscope.abc 27 | 28 | try: 29 | import microscope._wrappers.asdk as asdk 30 | except Exception as e: 31 | raise microscope.LibraryLoadError(e) from e 32 | 33 | 34 | class AlpaoDeformableMirror(microscope.abc.DeformableMirror): 35 | """Alpao deformable mirror. 36 | 37 | The Alpao mirrors support hardware triggers modes 38 | `TriggerMode.ONCE` and `TriggerMode.START`. By default, they will 39 | be set for software triggering, and trigger once. 40 | 41 | Args: 42 | serial_number: the serial number of the deformable mirror, 43 | something like `"BIL103"`. 44 | """ 45 | 46 | _TriggerType_to_asdkTriggerIn = { 47 | microscope.TriggerType.SOFTWARE: 0, 48 | microscope.TriggerType.RISING_EDGE: 1, 49 | microscope.TriggerType.FALLING_EDGE: 2, 50 | } 51 | 52 | _supported_TriggerModes = [ 53 | microscope.TriggerMode.ONCE, 54 | microscope.TriggerMode.START, 55 | ] 56 | 57 | @staticmethod 58 | def _normalize_patterns(patterns: np.ndarray) -> np.ndarray: 59 | """ 60 | Alpao SDK expects values in the [-1 1] range, so we normalize 61 | them from the [0 1] range we expect in our interface. 62 | """ 63 | patterns = (patterns * 2) - 1 64 | return patterns 65 | 66 | def _find_error_str(self) -> str: 67 | """Get an error string from the Alpao SDK error stack. 68 | 69 | Returns: 70 | A string with error message. An empty string if there was 71 | no error on the stack. 72 | """ 73 | err_msg_buffer_len = 64 74 | err_msg_buffer = ctypes.create_string_buffer(err_msg_buffer_len) 75 | 76 | err = ctypes.pointer(asdk.UInt(0)) 77 | status = asdk.GetLastError(err, err_msg_buffer, err_msg_buffer_len) 78 | if status == asdk.SUCCESS: 79 | msg = err_msg_buffer.value 80 | if len(msg) > err_msg_buffer_len: 81 | msg = msg + b"..." 82 | msg += b" (error code %i)" % (err.contents.value) 83 | return msg.decode() 84 | else: 85 | return "" 86 | 87 | def _raise_if_error( 88 | self, status: int, exception_cls=microscope.DeviceError 89 | ) -> None: 90 | if status != asdk.SUCCESS: 91 | msg = self._find_error_str() 92 | if msg: 93 | raise exception_cls(msg) 94 | 95 | def __init__(self, serial_number: str, **kwargs) -> None: 96 | super().__init__(**kwargs) 97 | self._dm = asdk.Init(serial_number.encode()) 98 | if not self._dm: 99 | raise microscope.InitialiseError( 100 | "Failed to initialise connection: don't know why" 101 | ) 102 | # In theory, asdkInit should return a NULL pointer in case of 103 | # failure and that should be enough to check. However, at least 104 | # in the case of a missing configuration file it still returns a 105 | # DM pointer so we still need to check for errors on the stack. 106 | self._raise_if_error(asdk.FAILURE) 107 | 108 | value = asdk.Scalar_p(asdk.Scalar()) 109 | status = asdk.Get(self._dm, b"NbOfActuator", value) 110 | self._raise_if_error(status) 111 | self._n_actuators = int(value.contents.value) 112 | self._trigger_type = microscope.TriggerType.SOFTWARE 113 | self._trigger_mode = microscope.TriggerMode.ONCE 114 | 115 | @property 116 | def n_actuators(self) -> int: 117 | return self._n_actuators 118 | 119 | @property 120 | def trigger_mode(self) -> microscope.TriggerMode: 121 | return self._trigger_mode 122 | 123 | @property 124 | def trigger_type(self) -> microscope.TriggerType: 125 | return self._trigger_type 126 | 127 | def _do_apply_pattern(self, pattern: np.ndarray) -> None: 128 | pattern = self._normalize_patterns(pattern) 129 | data_pointer = pattern.ctypes.data_as(asdk.Scalar_p) 130 | status = asdk.Send(self._dm, data_pointer) 131 | self._raise_if_error(status) 132 | 133 | def set_trigger(self, ttype, tmode): 134 | if tmode not in self._supported_TriggerModes: 135 | raise microscope.UnsupportedFeatureError( 136 | "unsupported trigger of mode '%s' for Alpao Mirrors" 137 | % tmode.name 138 | ) 139 | elif ( 140 | ttype == microscope.TriggerType.SOFTWARE 141 | and tmode != microscope.TriggerMode.ONCE 142 | ): 143 | raise microscope.UnsupportedFeatureError( 144 | "trigger mode '%s' only supports trigger type ONCE" 145 | % tmode.name 146 | ) 147 | self._trigger_mode = tmode 148 | 149 | try: 150 | value = self._TriggerType_to_asdkTriggerIn[ttype] 151 | except KeyError: 152 | raise microscope.UnsupportedFeatureError( 153 | "unsupported trigger of type '%s' for Alpao Mirrors" 154 | % ttype.name 155 | ) 156 | status = asdk.Set(self._dm, b"TriggerIn", value) 157 | self._raise_if_error(status) 158 | self._trigger_type = ttype 159 | 160 | def queue_patterns(self, patterns: np.ndarray) -> None: 161 | if self._trigger_type == microscope.TriggerType.SOFTWARE: 162 | super().queue_patterns(patterns) 163 | return 164 | 165 | self._validate_patterns(patterns) 166 | patterns = self._normalize_patterns(patterns) 167 | patterns = np.atleast_2d(patterns) 168 | n_patterns: int = patterns.shape[0] 169 | 170 | # The Alpao SDK seems to only support the trigger mode start. It 171 | # still has option called nRepeats that we can't really figure 172 | # what is meant to do. When set to 1, the mode is start. What 173 | # we want it is to have trigger mode once which was not 174 | # supported. We have received a modified version where if 175 | # nRepeats is set to same number of patterns, does trigger mode 176 | # once (not documented on Alpao SDK). 177 | if self._trigger_mode == microscope.TriggerMode.ONCE: 178 | n_repeats = n_patterns 179 | elif self._trigger_mode == microscope.TriggerMode.START: 180 | n_repeats = 1 181 | else: 182 | # We should not get here in the first place since 183 | # set_trigger filters unsupported modes. 184 | raise microscope.UnsupportedFeatureError( 185 | "trigger type '%s' and trigger mode '%s' is not supported" 186 | % (self._trigger_type.name, self._trigger_mode.name) 187 | ) 188 | 189 | data_pointer = patterns.ctypes.data_as(asdk.Scalar_p) 190 | 191 | # We don't know if the previous queue of pattern ran until the 192 | # end, so we need to clear it before sending (see issue #50) 193 | status = asdk.Stop(self._dm) 194 | self._raise_if_error(status) 195 | 196 | status = asdk.SendPattern( 197 | self._dm, data_pointer, n_patterns, n_repeats 198 | ) 199 | self._raise_if_error(status) 200 | 201 | def _do_shutdown(self) -> None: 202 | status = asdk.Release(self._dm) 203 | if status != asdk.SUCCESS: 204 | msg = self._find_error_str() 205 | warnings.warn(msg) 206 | -------------------------------------------------------------------------------- /microscope/mirror/bmc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Boston MicroMachines Corporation deformable mirrors. 21 | """ 22 | 23 | import ctypes 24 | import os 25 | import warnings 26 | 27 | import numpy as np 28 | 29 | import microscope 30 | import microscope._utils 31 | import microscope.abc 32 | 33 | try: 34 | import microscope._wrappers.BMC as BMC 35 | except Exception as e: 36 | raise microscope.LibraryLoadError(e) from e 37 | 38 | 39 | class BMCDeformableMirror( 40 | microscope._utils.OnlyTriggersOnceOnSoftwareMixin, 41 | microscope.abc.DeformableMirror, 42 | ): 43 | """Boston MicroMachines (BMC) deformable mirror. 44 | 45 | BMC deformable mirrors only support software trigger. 46 | """ 47 | 48 | def __init__(self, serial_number: str, **kwargs) -> None: 49 | super().__init__(**kwargs) 50 | self._dm = BMC.DM() 51 | 52 | if __debug__: 53 | BMC.ConfigureLog(os.devnull.encode(), BMC.LOG_ALL) 54 | else: 55 | BMC.ConfigureLog(os.devnull.encode(), BMC.LOG_OFF) 56 | 57 | status = BMC.Open(self._dm, serial_number.encode()) 58 | if status: 59 | raise microscope.InitialiseError(BMC.ErrorString(status)) 60 | 61 | @property 62 | def n_actuators(self) -> int: 63 | return self._dm.ActCount 64 | 65 | def _do_apply_pattern(self, pattern: np.ndarray) -> None: 66 | data_pointer = pattern.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) 67 | status = BMC.SetArray(self._dm, data_pointer, None) 68 | if status: 69 | raise microscope.DeviceError(BMC.ErrorString(status)) 70 | 71 | def _do_shutdown(self) -> None: 72 | status = BMC.Close(self._dm) 73 | if status: 74 | warnings.warn(BMC.ErrorString(status), RuntimeWarning) 75 | -------------------------------------------------------------------------------- /microscope/mirror/mirao52e.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2020 久保俊貴 5 | ## 6 | ## This file is part of Microscope. 7 | ## 8 | ## Microscope is free software: you can redistribute it and/or modify 9 | ## it under the terms of the GNU General Public License as published by 10 | ## the Free Software Foundation, either version 3 of the License, or 11 | ## (at your option) any later version. 12 | ## 13 | ## Microscope is distributed in the hope that it will be useful, 14 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ## GNU General Public License for more details. 17 | ## 18 | ## You should have received a copy of the GNU General Public License 19 | ## along with Microscope. If not, see . 20 | 21 | """Imagine Optic Mirao 52-e deformable mirror. 22 | 23 | The Mirao 52-e deformable mirror is not capable of receiving hardware 24 | triggers. It is only capable of sending hardware triggers. That 25 | sending of hardware triggers is not implemented on this module because 26 | it's pointless. 27 | 28 | The Mirao 52-e deformable mirror has a limitation on valid patterns. 29 | From the vendor documentation (the command is the pattern to be 30 | applied): 31 | 32 | [...] the sum of the absolute values defining the command must be 33 | lower than or equal to 24 and each value must be comprised between 34 | -1.0 and 1.0. 35 | 36 | In microscope, a pattern must be specified in the [0 1] range. 37 | However, the limit of 24, after rescaling to [-1 1] range, still 38 | applies. 39 | 40 | """ 41 | 42 | import ctypes 43 | from typing import Callable 44 | 45 | import numpy as np 46 | 47 | import microscope 48 | import microscope._utils 49 | import microscope.abc 50 | 51 | try: 52 | import microscope._wrappers.mirao52e as mro 53 | except Exception as e: 54 | raise microscope.LibraryLoadError(e) from e 55 | 56 | 57 | class Mirao52e( 58 | microscope._utils.OnlyTriggersOnceOnSoftwareMixin, 59 | microscope.abc.DeformableMirror, 60 | ): 61 | """Imagine Optic Mirao 52e deformable mirror. 62 | 63 | The Mirao 52e deformable mirrors only support software trigger. 64 | 65 | """ 66 | 67 | def __init__(self, **kwargs) -> None: 68 | super().__init__(**kwargs) 69 | # Status is not the return code of the function calls. 70 | # Status is where we can find the error code in case a 71 | # function call returns false. This _status variable will be 72 | # an argument in all function calls. 73 | self._status = ctypes.pointer(ctypes.c_int(mro.OK)) 74 | if not mro.open(self._status): 75 | raise microscope.InitialiseError( 76 | "failed to open mirao mirror (error code %d)" 77 | % self._status.contents.value 78 | ) 79 | 80 | @property 81 | def n_actuators(self) -> int: 82 | return mro.NB_COMMAND_VALUES 83 | 84 | @staticmethod 85 | def _normalize_patterns(patterns: np.ndarray) -> np.ndarray: 86 | """ 87 | mirao52e SDK expects values in the [-1 1] range, so we normalize 88 | them from the [0 1] range we expect in our interface. 89 | """ 90 | patterns = (patterns * 2) - 1 91 | return patterns 92 | 93 | def _do_apply_pattern(self, pattern: np.ndarray) -> None: 94 | pattern = self._normalize_patterns(pattern) 95 | command = pattern.ctypes.data_as(mro.Command) 96 | if not mro.applyCommand(command, mro.FALSE, self._status): 97 | self._raise_status(mro.applyCommand) 98 | 99 | def _raise_status(self, func: Callable) -> None: 100 | error_code = self._status.contents.value 101 | raise microscope.DeviceError( 102 | "mro_%s() failed (error code %d)" % (func.__name__, error_code) 103 | ) 104 | 105 | def _do_shutdown(self) -> None: 106 | if not mro.close(self._status): 107 | self._raise_status(mro.close) 108 | -------------------------------------------------------------------------------- /microscope/simulators/stage_aware_camera.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2020 Ian Dobbie 5 | ## 6 | ## This file is part of Microscope. 7 | ## 8 | ## Microscope is free software: you can redistribute it and/or modify 9 | ## it under the terms of the GNU General Public License as published by 10 | ## the Free Software Foundation, either version 3 of the License, or 11 | ## (at your option) any later version. 12 | ## 13 | ## Microscope is distributed in the hope that it will be useful, 14 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ## GNU General Public License for more details. 17 | ## 18 | ## You should have received a copy of the GNU General Public License 19 | ## along with Microscope. If not, see . 20 | 21 | """Simulation of a full setup based on a given image file. 22 | """ 23 | 24 | import logging 25 | import time 26 | from typing import Dict, Optional 27 | 28 | import numpy as np 29 | import PIL.Image 30 | import scipy.ndimage 31 | 32 | import microscope 33 | import microscope.abc 34 | from microscope.simulators import ( 35 | SimulatedCamera, 36 | SimulatedFilterWheel, 37 | SimulatedStage, 38 | ) 39 | 40 | _logger = logging.getLogger(__name__) 41 | 42 | 43 | class StageAwareCamera(SimulatedCamera): 44 | """Simulated camera that returns subregions of image based on stage 45 | position. 46 | 47 | Instead of using this class directly, consider using the 48 | :func:`simulated_setup_from_image` function which will generate 49 | all the required simulated devices for a given image file. 50 | 51 | Args: 52 | image: the image from which regions will be cropped based on 53 | the stage and filter wheel positions. 54 | stage: stage to read coordinates from. Must have an "x", 55 | "y", and "z" axis. 56 | filterwheel: filter wheel to read position. 57 | 58 | """ 59 | 60 | def __init__( 61 | self, 62 | image: np.ndarray, 63 | stage: microscope.abc.Stage, 64 | filterwheel: microscope.abc.FilterWheel, 65 | **kwargs, 66 | ) -> None: 67 | super().__init__(**kwargs) 68 | self._image = image 69 | self._stage = stage 70 | self._filterwheel = filterwheel 71 | self._pixel_size = 1.0 72 | 73 | if not all([name in stage.axes.keys() for name in ["x", "y", "z"]]): 74 | raise microscope.InitialiseError( 75 | "stage for StageAwareCamera requires x, y, and z axis" 76 | ) 77 | if image.shape[2] != self._filterwheel.n_positions: 78 | raise ValueError( 79 | "image has %d channels but filterwheel has %d positions" 80 | ) 81 | 82 | # Empty the settings dict, most of them are for testing 83 | # settings, and the rest is specific to the image generator 84 | # which we don't need. We probably should have a simpler 85 | # SimulatedCamera that we could subclass. 86 | self._settings = {} 87 | 88 | self.add_setting( 89 | "pixel size", 90 | "float", 91 | lambda: self._pixel_size, 92 | lambda pxsz: setattr(self, "_pixel_size", pxsz), 93 | # technically should be: (nextafter(0.0, inf), nextafter(inf, 0.0)) 94 | values=(0.0, float("inf")), 95 | ) 96 | 97 | def _fetch_data(self) -> Optional[np.ndarray]: 98 | if not self._acquiring or self._triggered == 0: 99 | return None 100 | 101 | time.sleep(self._exposure_time) 102 | self._triggered -= 1 103 | _logger.info("Creating image") 104 | 105 | # Use filter wheel position to select the image channel. 106 | channel = self._filterwheel.position 107 | 108 | width = self._roi.width // self._binning.h 109 | height = self._roi.height // self._binning.v 110 | 111 | # Use stage position to compute bounding box. 112 | xstart = int( 113 | (self._stage.position["x"] / self._pixel_size) - (width / 2) 114 | ) 115 | ystart = int( 116 | (self._stage.position["y"] / self._pixel_size) - (height / 2) 117 | ) 118 | xend = xstart + width 119 | yend = ystart + height 120 | 121 | # Need to check that the bounding box in entirely within the 122 | # source image (see #231). 123 | if ( 124 | xstart < 0 125 | or ystart < 0 126 | or xend > self._image.shape[1] 127 | or yend > self._image.shape[0] 128 | ): 129 | # If part of image is out of bounds, pad with zeros, ... 130 | subsection = np.zeros((height, width), dtype=self._image.dtype) 131 | # work out the relevant parts of input image ... 132 | img_x0 = max(0, xstart) 133 | img_x1 = min(xend, self._image.shape[1]) 134 | img_y0 = max(0, ystart) 135 | img_y1 = min(yend, self._image.shape[0]) 136 | # and work out where to place it in output image. 137 | sub_x0 = max(-xstart, 0) 138 | sub_y0 = max(-ystart, 0) 139 | sub_x1 = sub_x0 + (img_x1 - img_x0) 140 | sub_y1 = sub_y0 + (img_y1 - img_y0) 141 | 142 | subsection[sub_y0:sub_y1, sub_x0:sub_x1] = self._image[ 143 | img_y0:img_y1, img_x0:img_x1, channel 144 | ] 145 | else: 146 | subsection = self._image[ystart:yend, xstart:xend, channel] 147 | 148 | # Gaussian filter on abs Z position to simulate being out of 149 | # focus (Z position zero is in focus). 150 | blur = abs((self._stage.position["z"]) / 10.0) 151 | image = scipy.ndimage.gaussian_filter(subsection, blur) 152 | 153 | self._sent += 1 154 | # Not sure this flipping is correct but it's required to make 155 | # cockpit mosaic work. This is probably related to not having 156 | # defined what the image origin should be (see issue #89). 157 | return np.fliplr(np.flipud(image)) 158 | 159 | 160 | def simulated_setup_from_image( 161 | filepath: str, **kwargs 162 | ) -> Dict[str, microscope.abc.Device]: 163 | """Create simulated devices given an image file. 164 | 165 | To use with the `device-server`:: 166 | 167 | DEVICES = [ 168 | device(simulated_setup_from_image, 'localhost', 8000, 169 | conf={'filepath': path_to_image_file}), 170 | ] 171 | """ 172 | # PIL will error if trying to open very large images to avoid 173 | # decompression bomb DOS attack. However, this is used to fake a 174 | # stage and will really have very very large images, so remove 175 | # remove the PIL limit temporarily. 176 | original_pil_max_image_pixels = PIL.Image.MAX_IMAGE_PIXELS 177 | try: 178 | PIL.Image.MAX_IMAGE_PIXELS = None 179 | image = np.array(PIL.Image.open(filepath)) 180 | finally: 181 | PIL.Image.MAX_IMAGE_PIXELS = original_pil_max_image_pixels 182 | 183 | if len(image.shape) < 3: 184 | raise ValueError("not an RGB image") 185 | 186 | stage = SimulatedStage( 187 | { 188 | "x": microscope.AxisLimits(0, image.shape[1]), 189 | "y": microscope.AxisLimits(0, image.shape[0]), 190 | "z": microscope.AxisLimits(-50, 50), 191 | } 192 | ) 193 | filterwheel = SimulatedFilterWheel(positions=image.shape[2]) 194 | camera = StageAwareCamera(image, stage, filterwheel) 195 | 196 | return { 197 | "camera": camera, 198 | "filterwheel": filterwheel, 199 | "stage": stage, 200 | } 201 | -------------------------------------------------------------------------------- /microscope/stages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/stages/__init__.py -------------------------------------------------------------------------------- /microscope/testsuite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/testsuite/__init__.py -------------------------------------------------------------------------------- /microscope/testsuite/devices.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2020 Mick Phillips 5 | ## 6 | ## This file is part of Microscope. 7 | ## 8 | ## Microscope is free software: you can redistribute it and/or modify 9 | ## it under the terms of the GNU General Public License as published by 10 | ## the Free Software Foundation, either version 3 of the License, or 11 | ## (at your option) any later version. 12 | ## 13 | ## Microscope is distributed in the hope that it will be useful, 14 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ## GNU General Public License for more details. 17 | ## 18 | ## You should have received a copy of the GNU General Public License 19 | ## along with Microscope. If not, see . 20 | 21 | import logging 22 | import time 23 | from enum import IntEnum 24 | 25 | import microscope.abc 26 | 27 | # These classes were originally in testsuite but have been moved to 28 | # their own subpackage, these imports are for backwards compatibility. 29 | from microscope.simulators import SimulatedCamera 30 | from microscope.simulators import SimulatedController as TestController 31 | from microscope.simulators import ( 32 | SimulatedDeformableMirror as TestDeformableMirror, 33 | ) 34 | from microscope.simulators import SimulatedFilterWheel as TestFilterWheel 35 | from microscope.simulators import SimulatedLightSource 36 | from microscope.simulators import SimulatedStage as TestStage 37 | 38 | _logger = logging.getLogger(__name__) 39 | 40 | 41 | class CamEnum(IntEnum): 42 | A = 1 43 | B = 2 44 | C = 3 45 | D = 4 46 | 47 | 48 | class TestCamera(SimulatedCamera): 49 | # This adds a series of weird settings to the base simulated 50 | # camera which are only useful to test settings in cockpit. 51 | def __init__(self, **kwargs) -> None: 52 | super().__init__(**kwargs) 53 | # Enum-setting tests 54 | self._intEnum = CamEnum.A 55 | self.add_setting( 56 | "intEnum", 57 | "enum", 58 | lambda: self._intEnum, 59 | lambda val: setattr(self, "_intEnum", val), 60 | CamEnum, 61 | ) 62 | self._dictEnum = 0 63 | self.add_setting( 64 | "dictEnum", 65 | "enum", 66 | lambda: self._dictEnum, 67 | lambda val: setattr(self, "_dictEnum", val), 68 | {0: "A", 8: "B", 13: "C", 22: "D"}, 69 | ) 70 | self._listEnum = 0 71 | self.add_setting( 72 | "listEnum", 73 | "enum", 74 | lambda: self._listEnum, 75 | lambda val: setattr(self, "_listEnum", val), 76 | ["A", "B", "C", "D"], 77 | ) 78 | self._tupleEnum = 0 79 | self.add_setting( 80 | "tupleEnum", 81 | "enum", 82 | lambda: self._tupleEnum, 83 | lambda val: setattr(self, "_tupleEnum", val), 84 | ("A", "B", "C", "D"), 85 | ) 86 | 87 | 88 | class TestLaser(SimulatedLightSource): 89 | # Deprecated, kept for backwards compatibility. 90 | pass 91 | 92 | 93 | class DummySLM(microscope.abc.Device): 94 | # This only exists to test cockpit. There is no corresponding 95 | # device type in microscope yet. 96 | def __init__(self, **kwargs): 97 | super().__init__(**kwargs) 98 | self.sim_diffraction_angle = 0.0 99 | self.sequence_params = [] 100 | self.sequence_index = 0 101 | 102 | def _do_shutdown(self) -> None: 103 | pass 104 | 105 | def set_sim_diffraction_angle(self, theta): 106 | _logger.info("set_sim_diffraction_angle %f", theta) 107 | self.sim_diffraction_angle = theta 108 | 109 | def get_sim_diffraction_angle(self): 110 | return self.sim_diffraction_angle 111 | 112 | def run(self): 113 | self.enabled = True 114 | _logger.info("run") 115 | return 116 | 117 | def stop(self): 118 | self.enabled = False 119 | _logger.info("stop") 120 | return 121 | 122 | def get_sim_sequence(self): 123 | return self.sequence_params 124 | 125 | def set_sim_sequence(self, seq): 126 | _logger.info("set_sim_sequence") 127 | self.sequence_params = seq 128 | return 129 | 130 | def get_sequence_index(self): 131 | return self.sequence_index 132 | 133 | 134 | class DummyDSP(microscope.abc.Device): 135 | # This only exists to test cockpit. There is no corresponding 136 | # device type in microscope yet. 137 | def __init__(self, **kwargs): 138 | super().__init__(**kwargs) 139 | self._digi = 0 140 | self._ana = [0, 0, 0, 0] 141 | self._client = None 142 | self._actions = [] 143 | 144 | def _do_shutdown(self) -> None: 145 | pass 146 | 147 | def Abort(self): 148 | _logger.info("Abort") 149 | 150 | def WriteDigital(self, value): 151 | _logger.info("WriteDigital: %s", bin(value)) 152 | self._digi = value 153 | 154 | def MoveAbsolute(self, aline, pos): 155 | _logger.info("MoveAbsoluteADU: line %d, value %d", aline, pos) 156 | self._ana[aline] = pos 157 | 158 | def arcl(self, mask, pairs): 159 | _logger.info("arcl: %s, %s", mask, pairs) 160 | 161 | def profileSet(self, pstr, digitals, *analogs): 162 | _logger.info("profileSet ...") 163 | _logger.info("... ", pstr) 164 | _logger.info("... ", digitals) 165 | _logger.info("... ", analogs) 166 | 167 | def DownloadProfile(self): 168 | _logger.info("DownloadProfile") 169 | 170 | def InitProfile(self, numReps): 171 | _logger.info("InitProfile") 172 | 173 | def trigCollect(self, *args, **kwargs): 174 | _logger.info("trigCollect: ... ") 175 | _logger.info(args) 176 | _logger.info(kwargs) 177 | 178 | def ReadPosition(self, aline): 179 | _logger.info( 180 | "ReadPosition : line %d, value %d", aline, self._ana[aline] 181 | ) 182 | return self._ana[aline] 183 | 184 | def ReadDigital(self): 185 | _logger.info("ReadDigital: %s", bin(self._digi)) 186 | return self._digi 187 | 188 | def PrepareActions(self, actions, numReps=1): 189 | _logger.info("PrepareActions") 190 | self._actions = actions 191 | self._repeats = numReps 192 | 193 | def RunActions(self): 194 | _logger.info("RunActions ...") 195 | for i in range(self._repeats): 196 | for a in self._actions: 197 | _logger.info(a) 198 | time.sleep(a[0] / 1000.0) 199 | if self._client: 200 | self._client.receiveData("DSP done") 201 | _logger.info("... RunActions done.") 202 | 203 | def receiveClient(self, *args, **kwargs): 204 | # XXX: maybe this should be on its own mixin instead of on DataDevice 205 | return microscope.abc.DataDevice.receiveClient(self, *args, **kwargs) 206 | 207 | def set_client(self, *args, **kwargs): 208 | # XXX: maybe this should be on its own mixin instead of on DataDevice 209 | return microscope.abc.DataDevice.set_client(self, *args, **kwargs) 210 | 211 | 212 | class TestFloatingDevice( 213 | microscope.abc.FloatingDeviceMixin, microscope.abc.Device 214 | ): 215 | """Simple device with a UID after having been initialized. 216 | 217 | Floating devices are devices where we can't specify which one to 218 | get, we can only construct it and after initialisation check its 219 | UID. In this class for test units we can check which UID to get. 220 | 221 | """ 222 | 223 | def __init__(self, uid: str, **kwargs) -> None: 224 | super().__init__(**kwargs) 225 | self._initialized = False 226 | self._uid = uid 227 | self.initialize() 228 | 229 | def initialize(self) -> None: 230 | super().initialize() 231 | self._initialized = True 232 | 233 | def get_index(self) -> int: 234 | """Expose private _index for testing purposes.""" 235 | return self._index 236 | 237 | def get_id(self) -> str: 238 | if self._initialized: 239 | return self._uid 240 | else: 241 | raise microscope.IncompatibleStateError( 242 | "uid is not available until after initialisation" 243 | ) 244 | 245 | def _do_shutdown(self) -> None: 246 | pass 247 | -------------------------------------------------------------------------------- /microscope/testsuite/hardware.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Interactive tests for hardware. 21 | """ 22 | 23 | import time 24 | 25 | import numpy as np 26 | 27 | 28 | def test_mirror_actuators(dm, time_interval=0.5): 29 | """Iterate over all actuators of a deformable mirror. 30 | 31 | Args: 32 | dm (microscope.abc.DeformableMirror): The mirror to test. 33 | time_interval (float): Number of seconds between trying each 34 | actuator. 35 | """ 36 | base_value = 0.5 37 | data = np.full((dm.n_actuators), base_value) 38 | dm.apply_pattern(data) 39 | 40 | time.sleep(time_interval) 41 | for new_value in [1.0, 0.0]: 42 | for i in range(dm.n_actuators): 43 | data[i] = new_value 44 | dm.apply_pattern(data) 45 | time.sleep(time_interval) 46 | data[i] = base_value 47 | 48 | dm.apply_pattern(data) 49 | -------------------------------------------------------------------------------- /microscope/testsuite/test_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | import threading 21 | import unittest 22 | 23 | import Pyro4 24 | 25 | import microscope.clients 26 | import microscope.testsuite.devices as dummies 27 | 28 | 29 | @Pyro4.expose 30 | class PyroService: 31 | """Simple class to test serving via Pyro. 32 | 33 | We can use one of our own test devices but the idea is to have 34 | this tests independent from the devices. We should be able to 35 | test the Client with any Python object and weird cases, even if we 36 | don't yet make use of them in the devices. 37 | """ 38 | 39 | def __init__(self): 40 | self._value = 42 # not exposed 41 | 42 | @property 43 | def attr(self): # exposed as 'proxy.attr' remote attribute 44 | return self._value 45 | 46 | @attr.setter 47 | def attr(self, value): # exposed as 'proxy.attr' writable 48 | self._value = value 49 | 50 | 51 | @Pyro4.expose 52 | class ExposedDeformableMirror(dummies.TestDeformableMirror): 53 | """ 54 | Microscope device server is configure to not require @expose but 55 | this is to test our client with Pyro4's own Daemon. We need to 56 | subclass and have the passthrough because the property comes from 57 | the Abstract Base class, not the TestDeformableMirror class. 58 | """ 59 | 60 | @property 61 | def n_actuators(self): 62 | return super().n_actuators 63 | 64 | 65 | class TestClient(unittest.TestCase): 66 | def setUp(self): 67 | self.daemon = Pyro4.Daemon() 68 | self.thread = threading.Thread(target=self.daemon.requestLoop) 69 | 70 | def tearDown(self): 71 | self.daemon.shutdown() 72 | self.thread.join() 73 | 74 | def _serve_objs(self, objs): 75 | uris = [self.daemon.register(obj) for obj in objs] 76 | self.thread.start() 77 | clients = [microscope.clients.Client(uri) for uri in uris] 78 | return clients 79 | 80 | def test_property_access(self): 81 | """Test we can read properties via the Client""" 82 | # list of (object-to-serve, property-name-to-test) 83 | objs2prop = [ 84 | (PyroService(), "attr"), 85 | (ExposedDeformableMirror(10), "n_actuators"), 86 | ] 87 | clients = self._serve_objs([x[0] for x in objs2prop]) 88 | for client, obj_prop in zip(clients, objs2prop): 89 | obj = obj_prop[0] 90 | name = obj_prop[1] 91 | self.assertTrue(getattr(client, name), getattr(obj, name)) 92 | 93 | def test_property_writing(self): 94 | """Test we can write properties via the Client""" 95 | obj = PyroService() 96 | client = (self._serve_objs([obj]))[0] 97 | self.assertTrue(client.attr, 42) 98 | client.attr = 10 99 | self.assertTrue(client.attr, 10) 100 | self.assertTrue(obj.attr, 10) 101 | 102 | 103 | if __name__ == "__main__": 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /microscope/testsuite/test_settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Tests for the microscope devices settings. 21 | """ 22 | 23 | import enum 24 | import unittest 25 | 26 | import microscope.abc 27 | 28 | 29 | class EnumSetting(enum.Enum): 30 | A = 0 31 | B = 1 32 | C = 2 33 | 34 | 35 | class ThingWithSomething: 36 | """Very simple container with setter and getter methods""" 37 | 38 | def __init__(self, val): 39 | self.val = val 40 | 41 | def set_val(self, val): 42 | self.val = val 43 | 44 | def get_val(self): 45 | return self.val 46 | 47 | 48 | def create_enum_setting(default, with_getter=True, with_setter=True): 49 | thing = ThingWithSomething(EnumSetting(default)) 50 | getter = thing.get_val if with_getter else None 51 | setter = thing.set_val if with_setter else None 52 | setting = microscope.abc._Setting( 53 | "foobar", "enum", get_func=getter, set_func=setter, values=EnumSetting 54 | ) 55 | return setting, thing 56 | 57 | 58 | class TestEnumSetting(unittest.TestCase): 59 | def test_get_returns_enum_value(self): 60 | """For enums, get() returns the enum value not the enum instance""" 61 | setting, thing = create_enum_setting(1) 62 | self.assertIsInstance(setting.get(), int) 63 | 64 | def test_set_creates_enum(self): 65 | """For enums, set() sets an enum instance, not the enum value""" 66 | setting, thing = create_enum_setting(1) 67 | setting.set(2) 68 | self.assertIsInstance(thing.val, EnumSetting) 69 | self.assertEqual(thing.val, EnumSetting(2)) 70 | 71 | def test_set_and_get_write_only(self): 72 | """get() works for write-only enum settings""" 73 | setting, thing = create_enum_setting(1, with_getter=False) 74 | self.assertEqual(EnumSetting(1), thing.val) 75 | setting.set(2) 76 | self.assertEqual(setting.get(), 2) 77 | self.assertEqual(EnumSetting(2), thing.val) 78 | 79 | 80 | if __name__ == "__main__": 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /microscope/valuelogger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-microscope/microscope/2c282da1f2676fdf327699e46a167d38697c49b6/microscope/valuelogger/__init__.py -------------------------------------------------------------------------------- /microscope/valuelogger/raspberrypi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 David Miguel Susano Pinto 4 | ## Copyright (C) 2023 Ian Dobbie 5 | ## 6 | ## 7 | ## This file is part of Microscope. 8 | ## 9 | ## Microscope is free software: you can redistribute it and/or modify 10 | ## it under the terms of the GNU General Public License as published by 11 | ## the Free Software Foundation, either version 3 of the License, or 12 | ## (at your option) any later version. 13 | ## 14 | ## Microscope is distributed in the hope that it will be useful, 15 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | ## GNU General Public License for more details. 18 | ## 19 | ## You should have received a copy of the GNU General Public License 20 | ## along with Microscope. If not, see . 21 | 22 | """Raspberry Pi Value Logger module. 23 | """ 24 | 25 | import logging 26 | import queue 27 | import threading 28 | import time 29 | 30 | try: 31 | from Adafruit_MCP9808 import MCP9808 32 | 33 | has_MCP9808 = True 34 | except ModuleNotFoundError: 35 | has_MCP9808 = False 36 | 37 | try: 38 | from TSYS01 import TSYS01 39 | 40 | has_TSYS01 = True 41 | except ModuleNotFoundError: 42 | has_TSYS01 = False 43 | 44 | 45 | import microscope.abc 46 | 47 | # Support for async digital IO control on the Raspberryy Pi. 48 | # Currently supports digital input and output via GPIO lines 49 | 50 | 51 | # Use BCM GPIO references (naming convention for GPIO pins from Broadcom) 52 | # instead of physical pin numbers on the Raspberry Pi board 53 | 54 | _logger = logging.getLogger(__name__) 55 | 56 | 57 | class RPiValueLogger(microscope.abc.ValueLogger): 58 | """ValueLogger device for a Raspberry Pi with support for 59 | MCP9808 and TSYS01 I2C thermometer chips.""" 60 | 61 | def __init__(self, sensors=[], **kwargs): 62 | super().__init__(**kwargs) 63 | # setup Q for fetching data. 64 | self.inputQ = queue.Queue() 65 | self._sensors = [] 66 | for sensor in sensors: 67 | sensor_type, i2c_address = sensor 68 | print( 69 | "adding sensor: " + sensor_type + " Adress: %d " % i2c_address 70 | ) 71 | if sensor_type == "MCP9808": 72 | if not has_MCP9808: 73 | raise microscope.LibraryLoadError( 74 | "Adafruit_MCP9808 Python package not found" 75 | ) 76 | self._sensors.append(MCP9808.MCP9808(address=i2c_address)) 77 | # starts the last one added 78 | self._sensors[-1].begin() 79 | print(self._sensors[-1].readTempC()) 80 | elif sensor_type == "TSYS01": 81 | if not has_TSYS01: 82 | raise microscope.LibraryLoadError( 83 | "TSYS01 Python package not found" 84 | ) 85 | self._sensors.append(TSYS01.TSYS01(address=i2c_address)) 86 | print(self._sensors[-1].readTempC()) 87 | self.initialize() 88 | 89 | def initialize(self): 90 | self.updatePeriod = 1.0 91 | self.readsPerUpdate = 10 92 | # Open and start all temp sensors 93 | # A thread to record periodic temperature readings 94 | # This reads temperatures and logs them 95 | if self._sensors: 96 | # only strart thread if we have a sensor 97 | self.statusThread = threading.Thread(target=self.updateTemps) 98 | self.stopEvent = threading.Event() 99 | self.statusThread.Daemon = True 100 | self.statusThread.start() 101 | 102 | def debug_ret_Q(self): 103 | if not self.inputQ.empty(): 104 | return self.inputQ.get() 105 | 106 | # functions required for a data device. 107 | def _fetch_data(self): 108 | # need to return data fetched from interupt driven state chnages. 109 | if self.inputQ.empty(): 110 | return None 111 | temps = self.inputQ.get() 112 | if len(temps) == 1: 113 | outtemps = temps[0] 114 | else: 115 | outtemps = temps 116 | # print(self.inputQ.get()) 117 | _logger.debug("Temp readings are %s" % str(outtemps)) 118 | return outtemps 119 | 120 | def abort(self): 121 | pass 122 | 123 | def _do_enable(self): 124 | return True 125 | 126 | def _do_shutdown(self) -> None: 127 | # need to kill threads. 128 | self.stopEvent.set() 129 | 130 | # return the list of current temperatures. 131 | 132 | def get_temperature(self): 133 | return self.temperature 134 | 135 | # function to change updatePeriod 136 | def tempUpdatePeriod(self, period): 137 | self.updatePeriod = period 138 | 139 | # function to change readsPerUpdate 140 | def tempReadsPerUpdate(self, reads): 141 | self.readsPerUpdate = reads 142 | 143 | # needs to be re-written to push data into a queue which _fetch_data can 144 | # then send out to the server. 145 | 146 | # function to read temperature at set update frequency. 147 | # runs in a separate thread. 148 | def updateTemps(self): 149 | """Runs in a separate thread publish status updates.""" 150 | self.temperature = [None] * len(self._sensors) 151 | tempave = [None] * len(self._sensors) 152 | 153 | # self.create_rotating_log() 154 | 155 | if len(self._sensors) == 0: 156 | return () 157 | 158 | while True: 159 | if self.stopEvent.is_set(): 160 | break 161 | # zero the new averages. 162 | for i in range(len(self._sensors)): 163 | tempave[i] = 0.0 164 | # take readsPerUpdate measurements and average to reduce digitisation 165 | # errors and give better accuracy. 166 | for i in range(int(self.readsPerUpdate)): 167 | for i in range(len(self._sensors)): 168 | try: 169 | tempave[i] += self._sensors[i].readTempC() 170 | except: 171 | localTemperature = None 172 | time.sleep(self.updatePeriod / self.readsPerUpdate) 173 | for i in range(len(self._sensors)): 174 | self.temperature[i] = tempave[i] / self.readsPerUpdate 175 | _logger.debug( 176 | "Temperature-%s = %s" % (i, self.temperature[i]) 177 | ) 178 | self.inputQ.put(self.temperature) 179 | 180 | def getValues(self): 181 | """Reads all sensor values for running the value logger in remote 182 | pull mode""" 183 | 184 | if len(self._sensors) == 0: 185 | return () 186 | 187 | self.temperature = [None] * len(self._sensors) 188 | 189 | for i in range(len(self._sensors)): 190 | try: 191 | self.temprature[i] = self._sensors[i].readTempC() 192 | except: 193 | raise Exception("Unable to read temparture value") 194 | return self.temprature 195 | -------------------------------------------------------------------------------- /microscope/win32.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ## Copyright (C) 2020 Mick Phillips 4 | ## 5 | ## This file is part of Microscope. 6 | ## 7 | ## Microscope is free software: you can redistribute it and/or modify 8 | ## it under the terms of the GNU General Public License as published by 9 | ## the Free Software Foundation, either version 3 of the License, or 10 | ## (at your option) any later version. 11 | ## 12 | ## Microscope is distributed in the hope that it will be useful, 13 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | ## GNU General Public License for more details. 16 | ## 17 | ## You should have received a copy of the GNU General Public License 18 | ## along with Microscope. If not, see . 19 | 20 | """Win32 specific microscope classes. 21 | 22 | If called as a program, it will configure and control a Windows 23 | service to serve devices, similar to the device-server program. 24 | 25 | To configure and run as a Windows service use:: 26 | 27 | python -m microscope.win32 \ 28 | [install,remove,update,start,stop,restart,status] \ 29 | CONFIG-FILE 30 | 31 | """ 32 | 33 | 34 | import logging 35 | import multiprocessing 36 | import os 37 | import sys 38 | 39 | import servicemanager 40 | 41 | # These win32* modules both import win32api which is a pyd file. 42 | # Importing win32api can be problematic because of Windows things 43 | # specially when running as a Windows. So if it fails, add the 44 | # executable path to the DLL search PATH. 45 | try: 46 | import win32service 47 | import win32serviceutil 48 | except: 49 | os.environ["PATH"] += ";" + os.path.split(sys.executable)[0] 50 | import win32service 51 | import win32serviceutil 52 | 53 | 54 | class MicroscopeWindowsService(win32serviceutil.ServiceFramework): 55 | """Serves microscope devices via a Windows service. 56 | 57 | Win32 service manipulation relies on fetching _svc_name_ without 58 | instantiating any object, so _svc_name_ must be a class 59 | attribute. This means that only one MicroscopeService may be 60 | installed on any one system, and will be responsible for serving 61 | all microscope devices on that system. 62 | """ 63 | 64 | _svc_name_ = "MicroscopeService" 65 | _svc_display_name_ = "Microscope device servers" 66 | _svc_description_ = "Serves microscope devices." 67 | 68 | @classmethod 69 | def set_config_file(cls, path): 70 | win32serviceutil.SetServiceCustomOption( 71 | cls._svc_name_, "config", os.path.abspath(path) 72 | ) 73 | 74 | @classmethod 75 | def get_config_file(cls): 76 | return win32serviceutil.GetServiceCustomOption( 77 | cls._svc_name_, "config" 78 | ) 79 | 80 | def log(self, message, error=False): 81 | if error: 82 | logFunc = servicemanager.LogErrorMsg 83 | else: 84 | logFunc = servicemanager.LogInfoMsg 85 | logFunc("%s: %s" % (self._svc_name_, message)) 86 | 87 | def __init__(self, args): 88 | # Initialise service framework. 89 | win32serviceutil.ServiceFramework.__init__(self, args) 90 | self.stop_event = multiprocessing.Event() 91 | 92 | def SvcDoRun(self): 93 | configfile = win32serviceutil.GetServiceCustomOption( 94 | self._svc_name_, "config" 95 | ) 96 | os.chdir(os.path.dirname(configfile)) 97 | self.log("Using config file %s." % configfile) 98 | self.log("Logging at %s." % os.getcwd()) 99 | self.ReportServiceStatus(win32service.SERVICE_RUNNING) 100 | 101 | from microscope.device_server import ( 102 | DeviceServerOptions, 103 | serve_devices, 104 | validate_devices, 105 | ) 106 | 107 | options = DeviceServerOptions( 108 | config_fpath=configfile, 109 | logging_level=logging.INFO, 110 | ) 111 | 112 | try: 113 | devices = validate_devices(configfile) 114 | serve_devices(devices, options, self.stop_event) 115 | except Exception as e: 116 | servicemanager.LogErrorMsg(str(e)) 117 | # Exit with non-zero error code so Windows will attempt to restart. 118 | sys.exit(-1) 119 | self.log("Service shutdown complete.") 120 | self.ReportServiceStatus(win32service.SERVICE_STOPPED) 121 | 122 | def SvcStop(self): 123 | self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 124 | self.stop_event.set() 125 | 126 | 127 | def handle_command_line(): 128 | if len(sys.argv) == 1: 129 | print("\nNo action specified.\n", file=sys.stderr) 130 | sys.exit(1) 131 | if sys.argv[1].lower() in ["install", "update"]: 132 | if len(sys.argv) == 2: 133 | print("\nNo config file specified.\n") 134 | sys.exit(1) 135 | configfile = sys.argv.pop() 136 | win32serviceutil.HandleCommandLine(MicroscopeWindowsService) 137 | # Set persistent data on service 138 | MicroscopeWindowsService.set_config_file(configfile) 139 | else: 140 | win32serviceutil.HandleCommandLine(MicroscopeWindowsService) 141 | 142 | 143 | if __name__ == "__main__": 144 | handle_command_line() 145 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "microscope" 3 | version = "0.7.0+dev" 4 | description = "An interface for control of microscope devices" 5 | readme = "README.rst" 6 | license = {file = "COPYING"} 7 | 8 | # Names are in alphabetical order. This is the list of active 9 | # maintainers. For the full list of people that have contributed to 10 | # the development of the project, see `doc/authors.rst`. 11 | maintainers = [ 12 | {name = "David Miguel Susano Pinto"}, 13 | {name = "Ian Dobbie"}, 14 | {name = "Julio Mateos-Langerak"}, 15 | ] 16 | 17 | # https://pypi.org/classifiers 18 | classifiers = [ 19 | "Intended Audience :: Science/Research", 20 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 21 | "Topic :: Scientific/Engineering", 22 | ] 23 | 24 | requires-python = ">=3.7" 25 | dependencies = [ 26 | "Pillow", 27 | "Pyro4", 28 | "hidapi", 29 | "numpy", 30 | "pyserial", 31 | "scipy", 32 | ] 33 | 34 | [project.optional-dependencies] 35 | GUI = ["PyQt"] 36 | 37 | [project.scripts] 38 | device-server = "microscope.device_server:_setuptools_entry_point" 39 | deviceserver = "microscope.device_server:_setuptools_entry_point" 40 | 41 | [project.gui-scripts] 42 | microscope-gui = "microscope.gui:_setuptools_entry_point [GUI]" 43 | 44 | [project.urls] 45 | Homepage = "https://www.python-microscope.org" 46 | Download = "https://pypi.org/project/microscope/" 47 | Documentation = "https://www.python-microscope.org/doc/" 48 | News = "https://www.python-microscope.org/doc/news.html" 49 | Source = "https://github.com/python-microscope/microscope" 50 | Tracker = "https://github.com/python-microscope/microscope" 51 | 52 | 53 | [build-system] 54 | requires = ["setuptools >= 61.0"] 55 | build-backend = "setuptools.build_meta" 56 | 57 | [tool.setuptools.package-dir] 58 | microscope = "microscope" 59 | 60 | 61 | [tool.isort] 62 | profile = "black" 63 | line_length = 79 64 | 65 | 66 | [tool.black] 67 | line-length = 79 68 | 69 | 70 | [tool.pylint.FORMAT] 71 | max-line-length = 79 72 | 73 | 74 | [tool.pytest.ini_options] 75 | testpaths = ["microscope/testsuite",] 76 | # python_classes must be an empty string otherwise it defaults to all 77 | # Test* classes which then include the TestDevices imported in the 78 | # test_* modules. By using an empty value, it defaults to only 79 | # picking classes that subclass from unittest.TestCase. If we ever 80 | # move away from the unittest framework, an alternative is to import 81 | # the TestDevice classes under a different name. 82 | python_classes = "" 83 | 84 | 85 | [tool.tox] 86 | legacy_tox_ini = """ 87 | [tox] 88 | # We need to set isolated_build because: 'pyproject.toml file found. 89 | # To use a PEP 517 build-backend you are required to configure tox to 90 | # use an isolated_build" 91 | isolated_build = True 92 | envlist = py 93 | 94 | [testenv] 95 | description = run whole test suite 96 | commands = python -m unittest discover \ 97 | --start-directory microscope/testsuite \ 98 | --verbose 99 | """ 100 | --------------------------------------------------------------------------------