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
toptica.py
1#!/usr/bin/env python3
2
3## Copyright (C) 2021 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
20import logging
21import typing
22
23import serial
24
25import microscope
27import microscope.abc
28
29
30_LOGGER = logging.getLogger(__name__)
31
32_QUOTATION_CODE = ord(b'"')
33
34
35def _parse_string(answer: bytes) -> str:
36 assert answer[0] == _QUOTATION_CODE and answer[-1] == _QUOTATION_CODE
37 return answer[1:-1].decode()
38
39
40def _parse_bool(answer: bytes) -> bool:
41 assert answer in [b"#f", b"#t"]
42 return answer == b"#t"
43
44
46 """Connection to the iChrome MLE.
47
48 This is a simple wrapper to the iChrome MLE interface. It only
49 supports the parameter commands which reply with a single line
50 which is all we need to support this on Python-Microscope.
51
52 """
53
54 def __init__(self, shared_serial: microscope._utils.SharedSerial) -> None:
55 self._serial = shared_serial
56
57 self._serial.readlines() # discard anything that may be on the line
58
59 if self.get_system_type() != "iChrome-MLE":
60 raise microscope.DeviceError("not an iChrome MLE device")
61
62 def _param_command(self, command: bytes) -> bytes:
63 """Run command and return raw answer (minus prompt and echo)."""
64 command = command + b"\r\n"
65 with self._serial.lock:
66 self._serial.write(command)
67 answer = self._serial.read_until(b"\r\n> ")
68
69 # When we read, we are reading the whole command console
70 # including the prompt and even the command is echoed back.
71 assert answer[: len(command)] == command and answer[-4:] == b"\r\n> "
72
73 # Errors are indicated by the string "Error: " at the
74 # beginning of a new line.
75 if answer[len(command) : len(command) + 7] == b"Error: ":
76 base_command = command[:-2]
77 error_msg = answer[len(command) + 8 : -4]
79 "error on command '%s': %s"
80 % (base_command.decode(), error_msg.decode())
81 )
82
83 # Return the answer minus the "echoed" command and the prompt
84 # for the next command.
85 return answer[len(command) : -4]
86
87 def param_ref(self, name: bytes) -> bytes:
88 """Get parameter value (`param-ref` operator)."""
89 return self._param_command(b"(param-ref '%s)" % name)
90
91 def param_set(self, name: bytes, value: bytes) -> None:
92 """Change parameter (`param-set!` operator)."""
93 answer = self._param_command(b"(param-set! '%s %s)" % (name, value))
94 status = int(answer)
95 if status < 0:
97 "Failed to set parameter %s (return value %d)"
98 % (name.decode(), status)
99 )
100
101 def get_serial_number(self) -> str:
102 return _parse_string(self.param_ref(b"serial-number"))
103
104 def get_system_type(self) -> str:
105 return _parse_string(self.param_ref(b"system-type"))
106
107
109 def __init__(self, conn: _iChromeConnection, laser_number: int) -> None:
110 self._conn = conn
111 self._param_prefix = b"laser%d:" % laser_number
112
113 # We Need to confirm that indeed there is a laser at this
114 # position. There is no command to check this, we just try to
115 # read a parameter and check if it works.
116 try:
117 self.get_label()
118 except microscope.DeviceError as ex:
120 "failed to get label, probably no laser %d" % laser_number
121 ) from ex
122
123 def _param_ref(self, name: bytes) -> bytes:
124 return self._conn.param_ref(self._param_prefix + name)
125
126 def _param_set(self, name: bytes, value: bytes) -> None:
127 self._conn.param_set(self._param_prefix + name, value)
128
129 def get_label(self) -> str:
130 return _parse_string(self._param_ref(b"label"))
131
132 def get_type(self) -> str:
133 return _parse_string(self._param_ref(b"type"))
134
135 def get_delay(self) -> int:
136 return int(self._param_ref(b"delay"))
137
138 def get_enable(self) -> bool:
139 return _parse_bool(self._param_ref(b"enable"))
140
141 def set_enable(self, state: bool) -> None:
142 value = b"#t" if state else b"#f"
143 self._param_set(b"enable", value)
144
145 def get_cw(self) -> bool:
146 return _parse_bool(self._param_ref(b"cw"))
147
148 def set_cw(self, state: bool) -> None:
149 value = b"#t" if state else b"#f"
150 self._param_set(b"cw", value)
151
152 def get_use_ttl(self) -> bool:
153 return _parse_bool(self._param_ref(b"use-ttl"))
154
155 def set_use_ttl(self, state: bool) -> None:
156 value = b"#t" if state else b"#f"
157 self._param_set(b"use-ttl", value)
158
159 def get_level(self) -> float:
160 return float(self._param_ref(b"level"))
161
162 def set_level(self, level: float) -> None:
163 value = b"%.1f" % level
164 self._param_set(b"level", value)
165
166 def get_status_txt(self) -> str:
167 return _parse_string(self._param_ref(b"status-txt"))
168
169
171 def __init__(self, conn: _iChromeConnection, laser_number: int) -> None:
172 super().__init__()
173 self._conn = _iChromeLaserConnection(conn, laser_number)
174
175 # FIXME: set values to '0' because we need to pass an int as
176 # values for settings of type str. Probably a bug on
177 # Device.set_setting.
178 self.add_setting("label", "str", self._conn.get_label, None, values=0)
179 self.add_setting("type", "str", self._conn.get_type, None, values=0)
180
181 self.add_setting(
182 "delay", "int", self._conn.get_delay, None, values=tuple()
183 )
184
185 def get_status(self) -> typing.List[str]:
186 return self._conn.get_status_txt().split()
187
188 def get_is_on(self) -> bool:
189 if self._conn.get_enable():
190 if self._conn.get_cw():
191 return True
192 else:
193 # There doesn't seem to be command to check whether
194 # the TTL line is currently high, so just return True
195 # if set that way.
196 return self._conn.get_use_ttl()
197 else:
198 return False
199
200 def _do_get_power(self) -> float:
201 return self._conn.get_level() / 100.0
202
203 def _do_set_power(self, power: float) -> None:
204 self._conn.set_level(power * 100.0)
205
206 def _do_enable(self) -> None:
207 self._conn.set_enable(True)
208
209 def _do_disable(self) -> None:
210 self._conn.set_enable(False)
211
212 def _do_shutdown(self) -> None:
213 pass # Nothing to do
214
215 @property
216 def trigger_mode(self) -> microscope.TriggerMode:
217 return microscope.TriggerMode.BULB
218
219 @property
220 def trigger_type(self) -> microscope.TriggerType:
221 if self._conn.get_use_ttl():
222 return microscope.TriggerType.HIGH
223 else:
224 return microscope.TriggerType.SOFTWARE
225
227 self, ttype: microscope.TriggerType, tmode: microscope.TriggerMode
228 ) -> None:
229 if tmode is not microscope.TriggerMode.BULB:
231 "only TriggerMode.BULB mode is supported"
232 )
233
234 # From the manual it seems that cw and ttl parameters are
235 # mutually exclusive but also still need to be set separately.
236 if ttype is microscope.TriggerType.HIGH:
237 self._conn.set_cw(False)
238 self._conn.set_use_ttl(True)
239 elif ttype is microscope.TriggerType.SOFTWARE:
240 self._conn.set_use_ttl(False)
241 self._conn.set_cw(True)
242 else:
244 "only trigger type HIGH and SOFTWARE are supported"
245 )
246
247 def _do_trigger(self) -> None:
249 "trigger does not make sense in trigger mode bulb, only enable"
250 )
251
252
254 """Toptica iChrome MLE (multi-laser engine).
255
256 The names of the light devices are `laser1`, `laser2`, `laser3`,
257 ...
258
259 """
260
261 def __init__(self, port: str, **kwargs) -> None:
262 super().__init__(**kwargs)
263 self._lasers: typing.Dict[str, _iChromeLaser] = {}
264
265 # Setting specified on the manual (M-051 version 03)
266 serial_conn = serial.Serial(
267 port=port,
268 baudrate=115200,
269 timeout=1,
270 bytesize=serial.EIGHTBITS,
271 stopbits=serial.STOPBITS_ONE,
272 parity=serial.PARITY_NONE,
273 xonxoff=False,
274 rtscts=False,
275 dsrdtr=False,
276 )
277 shared_serial = microscope._utils.SharedSerial(serial_conn)
278 ichrome_connection = _iChromeConnection(shared_serial)
279
280 _LOGGER.info("Connected to %s", ichrome_connection.get_serial_number())
281
282 # According to the manual the iChrome can have between 3 and 5
283 # lasers. There doesn't seem to be a simple command to check
284 # what's installed, we'd have to parse the whole summary
285 # table. So we try/except to each laser line.
286 for i in range(1, 6):
287 name = "laser%d" % i
288 try:
289 laser = _iChromeLaser(ichrome_connection, i)
291 _LOGGER.info("no %s available", name)
292 continue
293 else:
294 _LOGGER.info("found %s on iChrome MLE", name)
295 self._lasers[name] = laser
296
297 @property
298 def devices(self) -> typing.Dict[str, _iChromeLaser]:
299 return self._lasers
None add_setting(self, name, dtype, get_func, set_func, values, typing.Optional[typing.Callable[[], bool]] readonly=None)
Definition: abc.py:407
None param_set(self, bytes name, bytes value)
Definition: toptica.py:91
bytes _param_command(self, bytes command)
Definition: toptica.py:62
None _param_set(self, bytes name, bytes value)
Definition: toptica.py:126
None set_trigger(self, microscope.TriggerType ttype, microscope.TriggerMode tmode)
Definition: toptica.py:228