├── .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 |
--------------------------------------------------------------------------------