61LUDL_ERRORS = { -1:
'Unknown command',
62 -2:
'Illegal point type or axis, or module not installed',
63 -3:
'Not enough parameters (e.g. move r=)',
64 -4:
'Parameter out of range',
65 -21:
'Process aborted by HALT command',
68 -10:
'No slides selected',
69 -11:
'End of list reached',
71 -16:
'Motor move error (move not completed successfully due to stall, end limit, etc.',
72 -17:
'Initialization error',
75AXIS_MAPPER = { 1:
'X' ,
81 """Connection to a Ludl Controller and wrapper to its commands.
83 Tested with MC2000 controller
and xy stage.
86 This
class also implements the logic to parse and validate
87 commands so it can be shared between multiple devices.
89 This
class has only been tested on a MAC2000 controller from the
90 1990
's however newer controllers should be compatible.
94 def __init__(self, port: str, baudrate: int, timeout: float) ->
None:
102 bytesize=serial.EIGHTBITS,
103 stopbits=serial.STOPBITS_TWO,
104 parity=serial.PARITY_NONE,
109 self.
_lock = threading.RLock()
119 print(
"Unable to read configuration. Is Ludl connected?")
126 for line
in answer[4:-1]:
129 devinfo=re.split(
r"\s{2,}",line.decode(
'ascii'))
131 self.
_devlist[devinfo[0]]=devinfo[1:]
139 def get_number_axes(self):
143 """Send command to device."""
145 self.
_serial.write(command + b
"\r")
148 """Read a line from the device connection until '\n'."""
150 return self.
_serial.read_until(b
"\n")
152 def read_multiline(self):
157 output.append(line.strip())
158 if line==b
'N' or line[0:2] == b
':A' :
162 elif line[0] == b
'N':
164 error=line[2:].strip()
165 raise(
'Ludl controller error: %s,%s' % (error,LUDL_ERRORS[error]))
169 """Read until timeout; used to clean buffer if in an unknown state."""
176 """Keep sending the 'STATUS' comand until the respnce
180 def _command_and_validate(self, command: bytes, expected: bytes) ->
None:
183 if answer == b
':A \n' :
190 """Send get command and return the answer."""
196 """Send a move command and check return value."""
207 """Send a realtive movement command to stated axis"""
208 axisname=AXIS_MAPPER[axis]
209 self.
move_command(bytes(
'MOVREL {0}={1}'.format(axisname,
210 str(delta)),
'ascii'))
213 """Send a realtive movement command to stated axis"""
214 axisname=AXIS_MAPPER[axis]
217 def move_to_limit(self,axis: bytes,speed: int):
218 axisname=AXIS_MAPPER[axis]
219 self.
get_command(bytes(
'SPIN {0}={1}'.format(axisname,speed),
'ascii'))
221 def motor_moving(self,axis: bytes) -> int:
222 axisname=AXIS_MAPPER[axis]
223 reply=self.
get_command(bytes(
'RDSTAT {0}'.format(axisname),
'ascii'))
224 flags=int(reply.strip()[3:])
227 def set_speed(self, axis: bytes, speed: int) ->
None:
228 axisname=AXIS_MAPPER[axis]
229 self.
get_command(bytes(
'SPEED {0}={1}'.format(axisname,speed),
'ascii'))
232 def wait_for_motor_stop(self,axis: bytes):
237 def reset_position(self, axis: bytes):
238 axisname=AXIS_MAPPER[axis]
239 self.
get_command(bytes(
'HERE {0}=0'.format(axisname),
'ascii'))
241 def get_absolute_position(self, axis: bytes) -> float:
242 axisname=AXIS_MAPPER[axis]
243 position=self.
get_command(bytes(
'WHERE {0}'.format(axisname),
'ascii'))
244 if position[3:4]==b
'N':
245 print(
"Error: {0} : {1}".format(position,
246 LUDL_ERRORS[int(position[4:6])]))
248 return float(position.strip()[2:])
251 """Send a set command and check return value."""
258 """Send a get description command and return it."""
261 return self.
_serial.read_until(b
"\rEND\r")
263 @contextlib.contextmanager
264 def changed_timeout(self, new_timeout: float):
265 previous = self.
_serial.timeout
267 self.
_serial.timeout = new_timeout
270 self.
_serial.timeout = previous
274 def __init__(self, dev_conn: _LudlController, axis: str) ->
None:
293 _logger.warning(
"querying stage axis position but device is busy")
298 def limits(self) -> microscope.AxisLimits:
305 def home(self) -> None:
309 def set_speed(self, speed: int) ->
None:
313 def find_limits(self,speed = 100000):
334 self, conn: _LudlController, **kwargs
336 super().__init__(**kwargs)
344 def _do_shutdown(self) -> None:
347 def _do_enable(self) -> bool:
354 self.axes[axis].home()
360 return not self.
homed
364 def axes(self) -> typing.Mapping[str, microscope.abc.StageAxis]:
367 def move_by(self, delta: typing.Mapping[str, float]) ->
None:
368 """Move specified axes by the specified distance. """
369 for axis_name, axis_delta
in delta.items():
370 self.
_dev_conn.move_by_relative_position(
371 int(axis_name), int(axis_delta),
375 def move_to(self, position: typing.Mapping[str, float]) ->
None:
376 """Move specified axes by the specified distance. """
378 for axis_name, axis_position
in position.items():
379 self.
_dev_conn.move_to_absolute_position(
380 int(axis_name), int(axis_position),
430 """Ludl MC 2000 controller.
434 The Ludl MC5000 can control a stage, filter wheels and shutters.
441 self, port: str, baudrate: int = 9600, timeout: float = 0.5, **kwargs
443 super().__init__(**kwargs)
456 def devices(self) -> typing.Mapping[str, microscope.abc.Device]:
None move_to(self, float pos)
microscope.AxisLimits limits(self)
None read_until_timeout(self)
None _command_and_validate(self, bytes command, bytes expected)
bytes get_description(self, bytes command)
None wait_until_idle(self)
int motor_moving(self, bytes axis)
None move_by_relative_position(self, bytes axis, float delta)
bytes get_command(self, bytes command)
None set_command(self, bytes command)
None move_command(self, bytes command)
None move_to_absolute_position(self, bytes axis, float pos)
None command(self, bytes command)
None set_speed(self, int speed)
None move_to(self, float pos)
None move_by(self, float delta)
microscope.AxisLimits limits(self)
def find_limits(self, speed=100000)
None move_to(self, typing.Mapping[str, float] position)
typing.Mapping[str, microscope.abc.StageAxis] axes(self)
None move_by(self, typing.Mapping[str, float] delta)
bool may_move_on_enable(self)