Skip to content

Commit e50caf0

Browse files
Typehints and numpydoc...
1 parent b8a799c commit e50caf0

File tree

3 files changed

+136
-65
lines changed

3 files changed

+136
-65
lines changed

LICENSE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2021-2023 The University of Texas Southwestern Medical Center.
1+
Copyright (c) 2021-2024 The University of Texas Southwestern Medical Center.
22

33
All rights reserved.
44

src/navigate/model/devices/APIs/sutter/MP285.py

+122-55
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import serial
3636
import threading
3737
import logging
38+
from typing import Optional, Tuple, Union
3839

3940
# Third-party imports
4041
import numpy as np
@@ -69,8 +70,22 @@ class MP285:
6970
If a command returns data, the last byte returned is the task-completed indicator.
7071
"""
7172

72-
def __init__(self, com_port, baud_rate, timeout=0.25):
73+
def __init__(self, com_port: str, baud_rate: int, timeout=0.25) -> None:
74+
"""Initialize the MP-285 stage.
75+
76+
Parameters
77+
----------
78+
com_port : str
79+
COM port of the MP-285 stage.
80+
baud_rate : int
81+
Baud rate of the MP-285 stage.
82+
timeout : float
83+
Timeout for the serial connection.
84+
"""
85+
86+
#: serial.Serial: Serial connection to the MP-285 stage
7387
self.serial = serial.Serial()
88+
7489
self.serial.port = com_port
7590
self.serial.baudrate = baud_rate
7691
self.serial.timeout = timeout
@@ -80,32 +95,72 @@ def __init__(self, com_port, baud_rate, timeout=0.25):
8095
self.serial.xonxoff = False
8196
self.serial.rtscts = True
8297

98+
#: int: Speed of the stage in microns/sec
8399
self.speed = 1000 # None
100+
101+
#: str: Resolution of the stage. High or Low.
84102
self.resolution = "high" # None
103+
104+
#: bool: Wait until the stage is done moving before returning
85105
self.wait_until_done = True
86106

107+
#: float: Time to wait between checking if the stage is done moving
87108
self.wait_time = 0.002
109+
110+
#: int: Number of times to check if the stage is done moving
88111
self.n_waits = max(int(timeout / self.wait_time), 1)
89112

90113
# Thread blocking here to prevent calls to get_current_position()
91114
# while move_to_specified_position is waiting for a response. Serial
92115
# commands must complete or the MP-285A completely locks up and has
93116
# to be power cycled.
117+
118+
#: threading.Event: Event to prevent writing to the serial port
94119
self.safe_to_write = threading.Event()
95120
self.safe_to_write.set()
121+
122+
#: threading.Lock: Lock to prevent writing to the serial port
96123
self.write_done_flag = threading.Lock()
124+
125+
#: bool: Flag to indicate if the stage is moving
97126
self.is_moving = False
127+
128+
#: time.time: Time of the last write to the serial port
98129
self.last_write_time = time.time()
99130

131+
#: int: Number of commands to buffer
100132
self.commands_num = 10
133+
134+
#: int: Index of the top command in the buffer
101135
self.top_command_idx = 0
136+
137+
#: int: Index of the last command in the buffer
102138
self.last_command_idx = 0
139+
140+
#: bool: Flag to indicate if the stage is interrupted
103141
self.is_interrupted = False
142+
143+
#: bool: Flat to indicate of the stage is moving.
104144
self.is_moving = False
145+
146+
#: list: Buffer to store the number of bytes to read for each command
105147
self.commands_buffer = [1] * self.commands_num
106148

149+
def send_command(self, command: bytes, response_num=1) -> int:
150+
"""Send a command to the MP-285 stage.
107151
108-
def send_command(self, command, response_num=1):
152+
Parameters
153+
----------
154+
command : bytes
155+
Command to send to the MP-285 stage.
156+
response_num : int
157+
Number of bytes to read for the response.
158+
159+
Returns
160+
-------
161+
idx : int
162+
Index of the command in the buffer.
163+
"""
109164
self.safe_to_write.wait()
110165
self.safe_to_write.clear()
111166
self.write_done_flag.acquire()
@@ -119,11 +174,23 @@ def send_command(self, command, response_num=1):
119174
self.last_command_idx = (self.last_command_idx + 1) % self.commands_num
120175
self.write_done_flag.release()
121176
return idx
122-
123-
def read_response(self, idx):
177+
178+
def read_response(self, idx: int) -> Union[bytes, str, None]:
179+
"""Read the response from the MP-285 stage.
180+
181+
Parameters
182+
----------
183+
idx : int
184+
Index of the command in the buffer.
185+
186+
Returns
187+
-------
188+
response : bytes, str, None
189+
Response from the MP-285 stage.
190+
"""
124191
if idx != self.top_command_idx:
125192
return None
126-
193+
127194
for _ in range(self.n_waits):
128195
if self.serial.in_waiting >= self.commands_buffer[self.top_command_idx]:
129196
r = self.serial.read(self.commands_buffer[self.top_command_idx])
@@ -132,26 +199,38 @@ def read_response(self, idx):
132199
self.safe_to_write.set()
133200
return r
134201
time.sleep(self.wait_time)
135-
136-
logger.error("Haven't received any responses from MP285! Please check the stage device!")
202+
203+
logger.error(
204+
"Haven't received any responses from MP285! "
205+
"Please check the stage device!"
206+
)
137207
self.top_command_idx = (self.top_command_idx + 1) % self.commands_num
138208
self.safe_to_write.set()
139209
return ""
140-
# raise TimeoutError("Haven't received any responses from MP285! Please check the stage device!")
210+
# raise TimeoutError("Haven't received any responses
211+
# from MP285! Please check the stage device!")
212+
213+
def connect_to_serial(self) -> None:
214+
"""Connect to the serial port of the MP-285 stage.
141215
142-
def connect_to_serial(self):
216+
Raises
217+
------
218+
serial.SerialException
219+
If the serial connection fails.
220+
"""
143221
try:
144222
self.serial.open()
145223
except serial.SerialException as e:
146224
print("MP285 serial connection failed.")
147225
logger.error(f"{str(self)}, Could not open port {self.serial.port}")
148226
raise e
149227

150-
def disconnect_from_serial(self):
228+
def disconnect_from_serial(self) -> None:
229+
"""Disconnect from the serial port of the MP-285 stage."""
151230
self.serial.close()
152231

153232
@staticmethod
154-
def convert_microsteps_to_microns(microsteps):
233+
def convert_microsteps_to_microns(microsteps: float) -> float:
155234
"""Converts microsteps to microns
156235
157236
Parameters
@@ -169,7 +248,7 @@ def convert_microsteps_to_microns(microsteps):
169248
return microns
170249

171250
@staticmethod
172-
def convert_microns_to_microsteps(microns):
251+
def convert_microns_to_microsteps(microns: float) -> float:
173252
"""Converts microsteps to microns.
174253
175254
Parameters
@@ -186,7 +265,9 @@ def convert_microns_to_microsteps(microns):
186265
microsteps = np.divide(microns, 0.04)
187266
return microsteps
188267

189-
def get_current_position(self):
268+
def get_current_position(
269+
self,
270+
) -> Tuple[Optional[float], Optional[float], Optional[float]]:
190271
"""Get the current stage position.
191272
192273
Gets the stage position. The data returned consists of 13 bytes:
@@ -234,20 +315,29 @@ def get_current_position(self):
234315

235316
# print(f"received: {position_information}")
236317
self.is_interrupted = False
237-
l = self.commands_buffer[idx]
318+
l = self.commands_buffer[idx] # noqa
238319
if len(position_information) < l:
239320
return None, None, None
240-
xs = int.from_bytes(position_information[l-13:l-9], byteorder="little", signed=True)
241-
ys = int.from_bytes(position_information[l-9:l-5], byteorder="little", signed=True)
242-
zs = int.from_bytes(position_information[l-5:-1], byteorder="little", signed=True)
321+
xs = int.from_bytes(
322+
position_information[l - 13 : l - 9], byteorder="little", signed=True
323+
)
324+
ys = int.from_bytes(
325+
position_information[l - 9 : l - 5], byteorder="little", signed=True
326+
)
327+
zs = int.from_bytes(
328+
position_information[l - 5 : -1], byteorder="little", signed=True
329+
)
330+
243331
# print(f"converted to microsteps: {xs} {ys} {zs}")
244332
x_pos = self.convert_microsteps_to_microns(xs)
245333
y_pos = self.convert_microsteps_to_microns(ys)
246334
z_pos = self.convert_microsteps_to_microns(zs)
247335
# print(f"converted to position: {x_pos} {y_pos} {z_pos}")
248336
return x_pos, y_pos, z_pos
249337

250-
def move_to_specified_position(self, x_pos, y_pos, z_pos):
338+
def move_to_specified_position(
339+
self, x_pos: float, y_pos: float, z_pos: float
340+
) -> bool:
251341
"""Move to Specified Position (‘m’) Command
252342
253343
This command instructs the controller to move all three axes to the position
@@ -297,7 +387,7 @@ def move_to_specified_position(self, x_pos, y_pos, z_pos):
297387

298388
return r == bytes.fromhex("0d")
299389

300-
def set_resolution_and_velocity(self, speed, resolution):
390+
def set_resolution_and_velocity(self, speed: int, resolution: str) -> bool:
301391
"""Sets the MP-285 stage speed and resolution.
302392
303393
This command instructs the controller to move all three axes to the position
@@ -325,7 +415,7 @@ def set_resolution_and_velocity(self, speed, resolution):
325415
resolution_bit = 1
326416
if speed > 1310:
327417
speed = 1310
328-
logger.error(f"Speed for the high-resolution mode is too fast.")
418+
logger.error("Speed for the high-resolution mode is too fast.")
329419
raise UserWarning(
330420
"High resolution mode of Sutter MP285 speed too "
331421
"high. Setting to 1310 microns/sec."
@@ -334,13 +424,13 @@ def set_resolution_and_velocity(self, speed, resolution):
334424
resolution_bit = 0
335425
if speed > 3000:
336426
speed = 3000
337-
logger.error(f"Speed for the low-resolution mode is too fast.")
427+
logger.error("Speed for the low-resolution mode is too fast.")
338428
raise UserWarning(
339429
"Low resolution mode of Sutter MP285 speed too "
340430
"high. Setting to 3000 microns/sec."
341431
)
342432
else:
343-
logger.error(f"MP-285 resolution must be 'high' or 'low'")
433+
logger.error("MP-285 resolution must be 'high' or 'low'")
344434
raise UserWarning("MP-285 resolution must be 'high' or 'low'")
345435

346436
speed_and_res = int(resolution_bit * 32768 + speed)
@@ -367,7 +457,7 @@ def set_resolution_and_velocity(self, speed, resolution):
367457
self.safe_to_write.set()
368458
return command_complete
369459

370-
def interrupt_move(self):
460+
def interrupt_move(self) -> Union[bool, None]:
371461
"""Interrupt stage movement.
372462
373463
This command interrupts and stops a move in progress that originally
@@ -417,7 +507,7 @@ def interrupt_move(self):
417507
self.is_interrupted = False
418508
return False
419509

420-
def set_absolute_mode(self):
510+
def set_absolute_mode(self) -> bool:
421511
"""Set MP285 to Absolute Position Mode.
422512
423513
This command sets the nature of the positional values specified with the Move
@@ -442,32 +532,7 @@ def set_absolute_mode(self):
442532
time.sleep(self.wait_time)
443533
return False
444534

445-
# def set_relative_mode(self):
446-
# """Set MP285 to Relative Position Mode.
447-
#
448-
# This command sets the nature of the positional values specified with the Move
449-
# (‘m’) command as relative positions as measured from the current position
450-
# (absolute position returned by the Get Current Position (‘c’) command).
451-
# The command sequence consists of 2 bytes: Command byte, followed by the
452-
# terminator. Return data consists of 1 byte (task-complete indicator).
453-
#
454-
# Returns
455-
# -------
456-
# command_complete : bool
457-
# True if command was successful, False if not.
458-
# """
459-
# # print("calling set_relative_mode")
460-
# self.flush_buffers()
461-
# self.safe_write(bytes.fromhex("62") + bytes.fromhex("0d"))
462-
# response = self.serial.read(1)
463-
# if response == bytes.fromhex("0d"):
464-
# command_complete = True
465-
# else:
466-
# command_complete = False
467-
# self.safe_to_write.set()
468-
# return command_complete
469-
470-
def refresh_display(self):
535+
def refresh_display(self) -> bool:
471536
"""Refresh the display on the MP-285 controller.
472537
473538
This command refreshes the VFD (Vacuum Fluorescent Display) of the controller.
@@ -490,7 +555,7 @@ def refresh_display(self):
490555

491556
return response == bytes.fromhex("0d")
492557

493-
def reset_controller(self):
558+
def reset_controller(self) -> bool:
494559
"""Reset the MP-285 controller.
495560
496561
This command resets the controller. The command sequence consists of 2 bytes:
@@ -503,15 +568,15 @@ def reset_controller(self):
503568
True if command was successful, False if not.
504569
"""
505570
idx = self.send_command(bytes.fromhex("72") + bytes.fromhex("0d"))
506-
571+
507572
response = self.read_response(idx)
508573
if response == bytes.fromhex("0d"):
509574
command_complete = True
510575
else:
511576
command_complete = False
512577
return command_complete
513578

514-
def get_controller_status(self):
579+
def get_controller_status(self) -> bool:
515580
"""Get the status of the MP-285 controller.
516581
517582
This command gets status information from the controller and returns it in
@@ -526,7 +591,9 @@ def get_controller_status(self):
526591
"""
527592
# print("calling get_controller_status")
528593
# self.flush_buffers()
529-
idx = self.send_command(bytes.fromhex("73") + bytes.fromhex("0d"), response_num=33)
594+
idx = self.send_command(
595+
bytes.fromhex("73") + bytes.fromhex("0d"), response_num=33
596+
)
530597
response = self.read_response(idx)
531598
if len(response) == 33 and response[-1] == bytes.fromhex("0d"):
532599
command_complete = True
@@ -536,6 +603,6 @@ def get_controller_status(self):
536603
# not implemented yet. See page 74 of documentation.
537604
return command_complete
538605

539-
def close(self):
606+
def close(self) -> None:
540607
"""Close the serial connection to the stage"""
541608
self.serial.close()

0 commit comments

Comments
 (0)