Source code for panoptes.pocs.focuser.astromechanics
"""Astromechanics Canon EF/EF-S electronic focuser driver.
Controls Canon EF/EF-S lenses via the Astromechanics adapter over a serial
connection, implementing the AbstractSerialFocuser interface.
"""
from panoptes.utils.serial.device import find_serial_port
from panoptes.pocs.focuser.serial import AbstractSerialFocuser
[docs]
class Focuser(AbstractSerialFocuser):
"""
Focuser class for control of a Canon DSLR lens via an Astromechanics
Engineering Canon EF/EF-S adapter.
Min/max commands do not exist for the astromechanics controller, as well as
other commands to get serial numbers and library/hardware versions. However,
as they are marked with the decorator @abstractmethod, we have to override them.
Astromechanics focuser are currently very slow to respond to position queries. When they do
respond, they give the exact position that was requested by the last move_to command (i.e.
there is no reported position error). We can therefore avoid such queries by storing the
current position in memory.
"""
def __init__(
self,
name="Astromechanics Focuser",
model="Canon EF-232",
vendor_id=0x0403,
product_id=0x6001,
zero_position=-25000,
baudrate=38400,
*args,
**kwargs,
):
"""
Args:
name (str, optional): default 'Astromechanics Focuser'
model (str, optional): default 'Canon EF/EF-S'
vendor_id (str, optional): idVendor of device, can be retrieved with lsusb -v from
terminal.
product_id (str, optional): idProduct of device, can be retrieved with lsusb -v from
terminal.
zero_position (int, optional): Position to use to calibrate the zero position of the
focuser. This should be a negative number larger than the possible range of encoder
values. Default: -25000.
baudrate (int, optional): The baudrate of the serial device. Default: 38400.
"""
self._position = None
if vendor_id and product_id:
port = find_serial_port(vendor_id, product_id)
self._vendor_id = vendor_id
self._product_id = product_id
self._zero_position = zero_position
super().__init__(name=name, model=model, baudrate=baudrate, port=port, *args, **kwargs)
# Properties
@AbstractSerialFocuser.position.getter
def position(self):
"""Current encoder position of the focuser (cached).
Returns:
int | None: The last commanded position, or None if unknown.
"""
return self._position
@property
def min_position(self):
"""
Returns position of close limit of focus travel, in encoder units.
"""
return 0
@property
def max_position(self):
"""
Returns position of far limit of focus travel, in encoder units.
"""
return None
# Public Methods
[docs]
def move_to(self, position):
"""Moves focuser to a new position.
Does not do any checking of the requested position but will warn if the lens reports
hitting a stop.
Args:
position (int): new focuser position, in encoder units.
Returns:
int: focuser position following the move, in encoder units.
"""
self._is_moving = True
try:
self._send_command(f"M{int(position):d}#")
self._position = position
finally:
# Focuser move commands block until the move is finished, so if the command has
# returned then the focuser is no longer moving.
self._is_moving = False
self.logger.debug(f"Moved to encoder position {self.position}")
return self.position
[docs]
def move_by(self, increment):
"""Move focuser by a given amount.
Does not do any checking of the requested increment but will warn if the lens reports
hitting a stop.
Args:
increment (int): distance to move the focuser, in encoder units.
Returns:
int: focuser position following the move, in encoder units.
"""
new_pos = self.position + increment
return self.move_to(new_pos)
# Private Methods
def _initialize(self):
# Initialise the aperture motor. This also has the side effect of fully opening the iris.
self._initialise_aperture()
# Calibrate near stop of the astromechanics focuser.
self._move_zero()
def _send_command(self, command, pre_cmd="", post_cmd="#"):
"""
Sends a command to the focuser adaptor and retrieves the response.
Args:
command (string): command string to send (without newline), e.g. 'P#'.
pre_cmd (string): Prefix for the command, default empty string.
post_cmd (string): Post for the command, default '#' for astromechs.
Returns:
string: containing the '\r' terminated lines of the response from the adaptor.
"""
if not self.is_connected:
self.logger.critical(f"Attempt to send command to {self} when not connected!")
return
# Clear the input buffer in case there's anything left over in there.
self._serial.reset_input_buffer()
# Send command
self._serial.write(f"{pre_cmd}{command}{post_cmd}\r")
return self._serial.read()
def _initialise_aperture(self):
"""Initialise the aperture motor."""
self.logger.debug("Initialising aperture motor")
self._is_moving = True
try:
self._send_command("A00")
self.logger.debug("Aperture initialised")
finally:
self._is_moving = False
def _move_zero(self):
"""Move the focuser to its zero position and set the current position to zero."""
self.logger.debug(f"Setting focus encoder zero point at position={self._zero_position}")
self._is_moving = True
try:
# Move to a negative position that is larger than the movement range of the lens.
self.move_to(self._zero_position)
# Set the current position as 0
self._position = 0
self.logger.debug(f"Zero point of focuser has been calibrated at {self._zero_position}")
finally:
self._is_moving = False