BioImager  3.9.1
A .NET microscopy imaging library. Supports various microscopes by using imported libraries & GUI automation. Supported libraries include PriorĀ® & ZeissĀ® & all devices supported by Micromanager 2.0 and python-microscope.
Loading...
Searching...
No Matches
coolled.py
1#!/usr/bin/env python3
2
3## Copyright (C) 2020 David Miguel Susano Pinto <carandraug@gmail.com>
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 <http://www.gnu.org/licenses/>.
19
20"""CoolLED illumination systems.
21"""
22
23import logging
24import typing
25
26import serial
27
28import microscope
30import microscope.abc
31
32
33_logger = logging.getLogger(__name__)
34
35
37 """Connection to the CoolLED controller, wraps base commands."""
38
39 def __init__(self, serial: microscope._utils.SharedSerial) -> None:
40 self._serial = serial
41
42 # When we connect for the first time, we will get back a
43 # greeting message like 'CoolLED precisExcite, Hello, pleased
44 # to meet you'. Discard it by reading until timeout.
45 self._serial.readlines()
46
47 # Check that this behaves like a CoolLED device.
48 try:
49 self.get_css()
50 except Exception:
52 "Not a CoolLED device, unable to get CSS"
53 )
54
55 def get_css(self) -> bytes:
56 """Get the global channel status map."""
57 with self._serial.lock:
58 self._serial.write(b"CSS?\n")
59 answer = self._serial.readline()
60 if not answer.startswith(b"CSS"):
62 "answer to 'CSS?' should start with 'CSS'"
63 " but got '%s' instead" % answer.decode
64 )
65 return answer[3:-2] # remove initial b'CSS' and final b'\r\n'
66
67 def set_css(self, css: bytes) -> None:
68 """Set status for any number of channels."""
69 assert len(css) % 6 == 0, "css must be multiple of 6 (6 per channel)"
70 with self._serial.lock:
71 self._serial.write(b"CSS" + css + b"\n")
72 answer = self._serial.readline()
73 if not answer.startswith(b"CSS"):
75 "answer to 'CSS?' should start with 'CSS'"
76 " but got '%s' instead" % answer.decode
77 )
78
79 def get_channels(self) -> typing.List[str]:
80 """Return list of channel names (names are one character string)."""
81 # answer has the form: [xsnNNN] per channel. The letter 'x'
82 # defines the channel (A to H), 's' refers to S (Selected) or
83 # X (Not selected), 'n' refers to N (On) or F (Off) and 'NNN'
84 # is the intensity in integer percent.
85 return list(self.get_css()[::6].decode())
86
87
89 """Wraps the CoolLED connection to control a single channel."""
90
91 def __init__(self, connection: _CoolLEDConnection, name: str) -> None:
92 if len(name) != 1:
93 raise ValueError("name should be a one character string")
94 self._conn = connection
95 self._css_offset = self._conn.get_css()[::6].index(name.encode()) * 6
96
97 def _get_css(self) -> bytes:
98 global_css = self._conn.get_css()
99 return global_css[self._css_offset : self._css_offset + 6]
100
101 def get_intensity(self) -> int:
102 """Intensity in integer percent [0 100]"""
103 return int(self._get_css()[3:])
104
105 def set_intensity(self, intensity: int) -> None:
106 """Intensity in integer percent [0 100]"""
107 percent = str(intensity).zfill(3)
108 self._conn.set_css(self._get_css()[0:3] + percent.encode())
109
110 def get_switch_state(self) -> str:
111 """N (On) or F (Off)"""
112 return self._get_css()[2:3].decode()
113
114 def set_switch_state(self, state: str) -> None:
115 """N (On) or F (Off)"""
116 if state not in ["N", "F"]:
117 raise ValueError("state needs to be N (on) or F (off)")
118 css = self._get_css()
119 self._conn.set_css(css[0:2] + state.encode() + css[3:])
120
121 def get_selected_state(self) -> str:
122 "S (Selected) or X (Unselected)" ""
123 return self._get_css()[1:2].decode()
124
125 def set_selected_state(self, state: str) -> None:
126 """X (Unselected) or S (Selected)"""
127 if state not in ["X", "S"]:
128 raise ValueError("state must be X (Unselected) or S (Selected)")
129 css = self._get_css()
130 self._conn.set_css(css[0:1] + state.encode() + css[2:])
131
132
134 """Individual light devices that compose a CoolLED controller."""
135
136 def __init__(
137 self, connection: _CoolLEDConnection, name: str, **kwargs
138 ) -> None:
139 super().__init__(**kwargs)
140 self._conn = _CoolLEDChannelConnection(connection, name)
141 # If a channel is disabled ("unselected"), setting the trigger
142 # type to software ("on") is not recorded and reverts back to
143 # high ("off"). Because of this, we keep track of what
144 # trigger type we want, i.e., should be "on" or "off", and
145 # apply it when the channel is enabled.
146 self._should_be_on = False
147
148 # The channel may be "selected" (enabled) and "off" (trigger
149 # type HIGH). When we set the trigger type to software,
150 # that's the same as setting it "on" which will make the
151 # channel emit light. Constructing this channel should not
152 # accidentally mae it emit light so disable it firt.
153 self.disabledisable()
154
155 # Default to software trigger type.
157 microscope.TriggerType.SOFTWARE, microscope.TriggerMode.BULB
158 )
159
160 def _do_shutdown(self) -> None:
161 pass
162
163 def get_status(self) -> typing.List[str]:
164 return []
165
166 def enable(self) -> None:
167 self._conn.set_selected_state("S")
168 if self._should_be_on:
169 # TriggerType.SOFTWARE
170 self._conn.set_switch_state("N")
171 else:
172 # TriggerType.HIGH
173 self._conn.set_switch_state("F")
174
175 def disable(self) -> None:
176 self._conn.set_selected_state("X")
177
178 def get_is_on(self) -> bool:
179 selected = self._conn.get_selected_state()
180 assert selected in ["S", "X"]
181 return selected == "S"
182
183 def _do_get_power(self) -> float:
184 return self._conn.get_intensity() / 100.0
185
186 def _do_set_power(self, power: float) -> None:
187 self._conn.set_intensity(int(power * 100.0))
188
189 @property
190 def trigger_type(self) -> microscope.TriggerType:
191 if self._conn.get_selected_state() == "S":
192 # Channel is "selected" (enabled): get the answer from
193 # switch state ("on" or "off").
194 if self._conn.get_switch_state() == "N":
195 return microscope.TriggerType.SOFTWARE
196 else:
197 return microscope.TriggerType.HIGH
198 else:
199 # Channel is "unselected" (disabled): trigger type will be
200 # whatever we set it to when we enable it.
201 if self._should_be_on:
202 return microscope.TriggerType.SOFTWARE
203 else:
204 return microscope.TriggerType.HIGH
205
206 @property
207 def trigger_mode(self) -> microscope.TriggerMode:
208 return microscope.TriggerMode.BULB
209
211 self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode
212 ) -> None:
213 if tmode is not microscope.TriggerMode.BULB:
215 "the only trigger mode supported is 'bulb'"
216 )
217 if ttype is microscope.TriggerType.SOFTWARE:
218 self._conn.set_switch_state("N")
219 self._should_be_on = True
220 elif ttype is microscope.TriggerType.HIGH:
221 self._conn.set_switch_state("F")
222 self._should_be_on = False
223 else:
225 "trigger type supported must be 'SOFTWARE' or 'HIGH'"
226 )
227
228 def _do_trigger(self) -> None:
230 "trigger does not make sense in trigger mode bulb, only enable"
231 )
232
233
235 """CoolLED controller for the individual light devices.
236
237 Args:
238 port: port name (Windows) or path to port (everything else) to
239 connect to. For example, `/dev/ttyACM0`, `COM1`, or
240 `/dev/cuad1`.
241
242 The individual channels are named A to H and depend on the actual
243 device. The pE-300 have three channels named A, B, and C by
244 increasing order of wavelength of their spectral region. The
245 pE-4000 have four selectable channels named A, B, C, and D with
246 channels E-H for peripheral devices via a pE expansion box.
247
248 .. code-block:: python
249
250 # Connect to a pE-300 ultra and get the individual lights.
251 controller = CoolLED('/dev/ttyACM0')
252 violet = controller.devices['A']
253 blue = controller.devices['B']
254 red = controller.devices['C']
255
256 # Turn on the violet channel.
257 violet.enable()
258
259 CoolLED controllers are often used with a control pod which can
260 select/unselect and turn on/off individual channels. The meaning
261 of these two states are:
262
263 * "selected" and "on": channel is always emitting light. This is
264 equivalent to being enabled with `SOFTWARE` trigger type.
265
266 * "selected" and "off": channel will emit light in receipt of a
267 TTL signal. This is equivalent to being enabled with `HIGH`
268 trigger type.
269
270 * "unselected" and "off": channel nevers emit light. This is
271 equivalent to being disabled.
272
273 * "unselected" and "on": this is not possible. If an "unselected"
274 channel is turned "on" it reverts back to "off".
275
276 .. note::
277
278 If a channel is set with `TriggerType.SOFTWARE` ("on") it will
279 start emitting light once enabled ("selected"). Once enabled,
280 even though trigger type is set to software and not hardware,
281 if the channel receives a TTL signal it will switch to
282 `TriggerType.HIGH` and continue to report being set to
283 software. This seems to be an issue with the CoolLED
284 https://github.com/python-microscope/vendor-issues/issues/9
285
286 This was developed with a CoolLED pE-300 ultra but should work
287 with the whole pE-300 series. It should also work with the
288 pE-4000 and the pE expansion box with the exception of loading
289 different sources.
290
291 """
292
293 def __init__(self, port: str, **kwargs) -> None:
294 super().__init__(**kwargs)
295 self._channels: typing.Dict[str, microscope.abc.LightSource] = {}
296
297 # CoolLED manual only has the baudrate, we guessed the rest.
298 serial_conn = serial.Serial(
299 port=port,
300 baudrate=57600,
301 timeout=1,
302 bytesize=serial.EIGHTBITS,
303 stopbits=serial.STOPBITS_ONE,
304 parity=serial.PARITY_NONE,
305 xonxoff=False,
306 rtscts=False,
307 dsrdtr=False,
308 )
309 shared_serial = microscope._utils.SharedSerial(serial_conn)
310 connection = _CoolLEDConnection(shared_serial)
311 for name in connection.get_channels():
312 self._channels[name] = _CoolLEDChannel(connection, name)
313
314 @property
315 def devices(self) -> typing.Dict[str, microscope.abc.Device]:
316 return self._channels
None disable(self)
Definition: abc.py:307
None set_trigger(self, microscope.TriggerType ttype, microscope.TriggerMode tmode)
Definition: abc.py:249
None set_trigger(self, microscope.TriggerType ttype, microscope.TriggerMode tmode)
Definition: coolled.py:212