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
lumencor.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"""Lumencor Spectra Light Engine.
21
22The implementation here is limited to the Lumencor Spectra III but
23should be trivial to make it work for other Lumencor light engines.
24We 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
32import typing
33
34import serial
35
36import microscope
37import microscope._utils
38import microscope.abc
39
40
41class _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"):
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 ):
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) -> typing.List[typing.Tuple[int, str]]:
108 answer = self.get_command(b"CHMAP")
109 return list(enumerate(answer.decode().split()))
110
111
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
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: typing.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) -> typing.Mapping[str, microscope.abc.Device]:
199 return self._lights
200
201
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) -> typing.List[str]:
227 status: typing.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
bytes command_and_answer(self, *bytes TX_tokens)
Definition: lumencor.py:68
bytes get_command(self, bytes command, *bytes args)
Definition: lumencor.py:97