I want to write code to communicate with encoders of different positioning styles (absolute vs. relative). The idea I have is to create a base class that has things common to all encoders. Then there will be two child classes, one for absolute and one for relative. There will then be child classes of that for each brand. Each class will have unique functions to communicate with.
Sketch of hierarchy:
Supposing I had a way to identify each motor by brand and type, how would I programmatically decide what class object to create?
This sounds like an excellent situation for inheritance, as you already hinted, and can be solved pretty quickly that way. I have put together some sample code for you that shows how you could accomplish this.
There is one point you make that I would like to contend:
You mention that each class will have unique functions. I actually recommend exactly the opposite, given every class the same core functions, and then after they are initialized, you never need to worry about which one you actually got, because they all adhere to the same interface. This can greatly simplify your future code as you will not need to ever check which class you are using, and your code will be reusable between different motors without any rewriting, just by changing the motor brand and name.
To answer this question how would I programmatically decide what class object to create? : I recommend a dictionary with brand name and model name tuple as the key, and the appropriately built class definition as the value. If two separate models use the same interface, you can simply map both to the same class definition. I have an example implementation of this below named MOTOR_ENCODER_LOOKUP
.
import enum
import typing
class PositioningType(enum.Enum):
relative = 'relative'
absolute = 'absolute'
class EncoderBase:
def __init__(
self,
pos_type: PositioningType,
motor_brand: str,
motor_model: str,
address: str
):
self.pos_type = pos_type
self.motor_brand = motor_brand
self.motor_model = motor_model
self.address = address
self._setup_hardware_interface()
self._last_commanded_state = None
self._last_commanded_state = self.get_position()
def move_relative(self, relative_change: float) -> bool:
raise NotImplementedError
def move_absolute(self, new_position: float) -> bool:
raise NotImplementedError
def get_position(self) -> float:
# Do something to get the position from the hardware
# May want to override this in the motor-specific classes
return self._last_commanded_state or 0.0
def _setup_hardware_interface(self) -> bool:
# Whatever specific setup is necessary for each encoder can be
# performed in the derived classes
# i.e., open a COM port, setup UDP, etc
raise NotImplementedError
def _send_command(self, commandvalue: float) -> bool:
raise NotImplementedError
def __repr__(self):
outstr = ''
outstr += f'<{type(self).__name__}\n'
outstr += f' positioning type: {self.pos_type}\n'
outstr += f' motor brand: {self.motor_brand}\n'
outstr += f' motor model: {self.motor_model}\n'
outstr += f' hardware address: {self.address}\n'
outstr += f' last position: {self._last_commanded_state}\n'
outstr += f'/>'
return outstr
@property
def pos_type(self) -> PositioningType:
return self._pos_type
@pos_type.setter
def pos_type(self, invalue: PositioningType):
self._pos_type = invalue
@property
def motor_brand(self) -> str:
return self._motor_brand
@motor_brand.setter
def motor_brand(self, invalue: str):
self._motor_brand = invalue
@property
def motor_model(self) -> str:
return self._motor_model
@motor_model.setter
def motor_model(self, invalue: str):
self._motor_model = invalue
@property
def address(self) -> str:
return self._address
@address.setter
def address(self, invalue: str):
self._address = invalue
class AbsoluteEncoder(EncoderBase):
def __init__(self, motor_brand: str, motor_model: str, address: str):
super().__init__(PositioningType.absolute, motor_brand, motor_model, address)
def move_relative(self, relative_change: float) -> bool:
# This is inherently an absolute encoder, so calculate the new absolute
# position and send that
new_position = self.get_position() + relative_change
return self.move_absolute(new_position)
def move_absolute(self, new_position: float) -> bool:
# This is already an absolute encoder, so send the command as-is
success = self._send_command(new_position)
if success:
self._last_commanded_state = new_position
return success
class RelativeEncoder(EncoderBase):
def __init__(self, motor_brand: str, motor_model: str, address: str):
super().__init__(PositioningType.relative, motor_brand, motor_model, address)
def move_relative(self, relative_change: float) -> bool:
# This is already a relative encoder, so send the command as-is
success = self._send_command(relative_change)
if success:
self._last_commanded_state += relative_change
return success
def move_absolute(self, new_position: float) -> bool:
# This is inherently a relative encoder, so calculate the relative change
# and send the relative command
relative_change = new_position - self.get_position()
return self.move_relative(relative_change)
class EncoderAlphaOne(AbsoluteEncoder):
def _send_command(self, commandvalue: float) -> bool:
# do something to send the command
return True
def _setup_hardware_interface(self) -> bool:
return True
def get_position(self) -> float:
# Ask the hardware for its current position since AbsoluteEncoders probably
# have that feature
return self._last_commanded_state or 0.0
class EncoderAlphaTwo(RelativeEncoder):
def _send_command(self, commandvalue: float) -> bool:
# do something to send the command
return True
def _setup_hardware_interface(self) -> bool:
return True
class EncoderBetaOne(AbsoluteEncoder):
def _send_command(self, commandvalue: float) -> bool:
# do something to send the command
return True
def _setup_hardware_interface(self) -> bool:
return True
def get_position(self) -> float:
# Ask the hardware for its current position since AbsoluteEncoders probably
# have that feature
return self._last_commanded_state or 0.0
# Add all your various make/model of encoders here with appropriate classes
# and encoder_factory will automatically grab it
# Each encoder needs to have a class definition written, as shown above
# but most of the work is done in one of the parent classes, so it should be quick
MOTOR_ENCODER_LOOKUP = {
('AlphaCompany', 'Model1'): EncoderAlphaOne,
('AlphaCompany', 'Model2'): EncoderAlphaTwo,
('BetaCompany', 'FirstModel'): EncoderBetaOne
}
# A factory function to go grab the correct class definition and initialize it
def encoder_factory(motor_brand: str, motor_model: str, address: str):
return MOTOR_ENCODER_LOOKUP[(motor_brand, motor_model)](
motor_brand,
motor_model,
address
)
def _main():
# Demonstrate that the functionality basically works
# Use three separate types of encoder without fussing about which one you have
e1 = encoder_factory('AlphaCompany', 'Model1', 'COM1')
e2 = encoder_factory('AlphaCompany', 'Model2', 'COM3')
e3 = encoder_factory('BetaCompany', 'FirstModel', 'COM4')
e1.move_relative(25.0)
e1.move_relative(-5.0)
e2.move_relative(10.0)
e2.move_absolute(45.0)
e3.move_absolute(60.0)
e3.move_relative(10.0)
print(e1)
print(e2)
print(e3)
if __name__ == '__main__':
_main()
Running the above yields:
<EncoderAlphaOne
positioning type: PositioningType.absolute
motor brand: AlphaCompany
motor model: Model1
hardware address: COM1
last position: 20.0
/>
<EncoderAlphaTwo
positioning type: PositioningType.relative
motor brand: AlphaCompany
motor model: Model2
hardware address: COM3
last position: 45.0
/>
<EncoderBetaOne
positioning type: PositioningType.absolute
motor brand: BetaCompany
motor model: FirstModel
hardware address: COM4
last position: 70.0
/>
Let me know if you have any follow up questions.