Source code for pitop.pma.encoder_motor

import atexit
import time
from math import floor, pi

from pitop.core.mixins import Recreatable, Stateful

from .encoder_motor_controller import EncoderMotorController
from .parameters import BrakingType, Direction, ForwardDirection


[docs] class EncoderMotor(Stateful, Recreatable): """Represents a pi-top motor encoder component. Note that pi-top motor encoders use a built-in closed-loop control system, that feeds the readings from an encoder sensor to an PID controller. This controller will actively modify the motor's current to move at the desired speed or position, even if a load is applied to the shaft. This internal controller is used when moving the motor through :class:`set_target_rpm` or :class:`set_target_speed` methods, while using the :class:`set_power` method will make the motor work in open-loop, not using the controller. .. note:: Note that some methods allow to use distance and speed settings in meters and meters per second. These will only make sense when using a wheel attached to the shaft of the motor. The conversions between angle, rotations and RPM used by the motor to meters and meters/second are performed considering the :data:`wheel_diameter` parameter. This parameter defaults to the diameter of the wheel included with MMK. If a wheel of different dimmensions is attached to the motor, you'll need to measure it's diameter, in order for these methods to work properly. :type port_name: str :param port_name: The ID for the port to which this component is connected. :type forward_direction: ForwardDirection :param forward_direction: The type of rotation of the motor shaft that corresponds to forward motion. :type braking_type: BrakingType :param braking_type: The braking type of the motor. Defaults to coast. :type wheel_diameter: int or float :param wheel_diameter: The diameter of the wheel attached to the motor. :type name: str :param name: Component name, defaults to `encoder_motor`. Used to access this component when added to a :class:`pitop.Pitop` object. """ MMK_STANDARD_GEAR_RATIO = 41.8 MAX_DC_MOTOR_RPM = 4800 def __init__( self, port_name, forward_direction, braking_type=BrakingType.BRAKE, wheel_diameter=0.075, name="encoder_motor", ): self.name = name self._pma_port = port_name if isinstance(braking_type, BrakingType): braking_type = braking_type.value self.__motor_core = EncoderMotorController(self._pma_port, braking_type) self.__forward_direction = forward_direction self.__wheel_diameter = wheel_diameter self.__prev_time_dist_cnt = time.time() self.__previous_reading_odometer = 0 atexit.register(self.stop) Stateful.__init__(self) Recreatable.__init__( self, config_dict={ "port_name": port_name, "name": name, "forward_direction": lambda: self.forward_direction, "braking_type": lambda: self.braking_type, "wheel_diameter": lambda: self.wheel_diameter, }, ) @property def own_state(self): return { "current_rpm": lambda: self.current_rpm, "current_speed": lambda: self.current_speed, "distance": lambda: self.distance, "wheel_diameter": lambda: self.wheel_diameter, "forward_direction": lambda: self.forward_direction, "braking_type": lambda: self.braking_type, } @property def forward_direction(self): """Represents the forward direction setting used by the motor. Setting this property will determine on which direction the motor will turn whenever a movement in a particular direction is requested. :type forward_direction: ForwardDirection :param forward_direction: The direction that corresponds to forward motion. """ return ForwardDirection(self.__forward_direction) @forward_direction.setter def forward_direction(self, forward_direction): self.__forward_direction = forward_direction @property def braking_type(self): """Returns the type of braking used by the motor when it's stopping after a movement. Setting this property will change the way the motor stops a movement: - :data:`BrakingType.COAST` will make the motor coast to a halt when stopped. - :data:`BrakingType.BRAKE` will cause the motor to actively brake when stopped. :type braking_type: BrakingType :param braking_type: The braking type of the motor. """ return BrakingType(self.__motor_core.braking_type()) @braking_type.setter def braking_type(self, braking_type): self.__motor_core.set_braking_type(braking_type.value)
[docs] def set_power(self, power, direction=Direction.FORWARD): """Turn the motor on at the power level provided, in the range -1.0 to. +1.0, where: - 1.0: motor will turn with full power in the :data:`direction` provided as argument. - 0.0: motor will not move. - -1.0: motor will turn with full power in the direction contrary to :data:`direction`. .. warning:: Setting a :data:`.power` value out of range will cause the method to raise an exception. :type power: int or float :param power: Motor power, in the range -1.0 to +1.0 :type direction: Direction :param direction: Direction to rotate the motor """ if not (-1.0 <= power <= 1.0): raise ValueError("Power value must be between -1.0 and +1.0 (inclusive)") power_mapping = int(round(power * 1000) * self.__forward_direction * direction) self.__motor_core.set_power(power_mapping)
[docs] def power(self): """Get the current power of the motor. Returns a value from -1.0 to +1.0, assuming the user is controlling the motor using the :class:`set_power` method (motor is in control mode 0). If this is not the case, returns None. """ power = self.__motor_core.power() if power: return power / 1000.0 return None
[docs] def set_target_rpm( self, target_rpm, direction=Direction.FORWARD, total_rotations=0.0 ): """Run the motor at the specified :data:`.target_rpm` RPM. If desired, a number of full or partial rotations can also be set through the :data:`total_rotations` parameter. Once reached, the motor will stop. Setting :data:`total_rotations` to 0 will set the motor to run indefinitely until stopped. If the desired RPM setting cannot be achieved, :class:`torque_limited` will be set to :data:`True` and the motor will run at the maximum possible RPM it is capable of for the instantaneous torque. This means that if the torque lowers, then the RPM will continue to rise until it meets the desired level. Care needs to be taken here if you want to drive a vehicle forward in a straight line, as the motors are not guaranteed to spin at the same rate if they are torque-limited. .. warning:: Setting a :data:`.target_rpm` higher than the maximum allowed will cause the method to throw an exception. To determine what the maximum possible target RPM for the motor is, use the :class:`max_rpm` method. :type target_rpm: int or float :param target_rpm: Desired RPM of output shaft :type direction: Direction :param direction: Direction to rotate the motor. Defaults to forward. :type total_rotations: int or float :param total_rotations: Total number of rotations to be execute. Set to 0 to run indefinitely. """ if not (-self.max_rpm <= abs(target_rpm) <= self.max_rpm): raise ValueError( f"Target RPM value must be between {-self.max_rpm} and {self.max_rpm} (inclusive)" ) dc_motor_rpm = int( round(target_rpm * self.MMK_STANDARD_GEAR_RATIO) * self.__forward_direction * direction ) dc_motor_rotations = int( round(total_rotations * self.MMK_STANDARD_GEAR_RATIO) * self.__forward_direction * direction ) if not (-self.MAX_DC_MOTOR_RPM <= dc_motor_rpm <= self.MAX_DC_MOTOR_RPM): raise ValueError( f"DC motor RPM value must be between {-self.MAX_DC_MOTOR_RPM} and {self.MAX_DC_MOTOR_RPM} (inclusive)" ) if dc_motor_rotations == 0: self.__motor_core.set_rpm_control(dc_motor_rpm) else: dc_motor_rotation_counter = self.__motor_core.odometer() rotations_offset_to_send = int( dc_motor_rotations + dc_motor_rotation_counter ) self.__motor_core.set_rpm_with_rotations( dc_motor_rpm, rotations_offset_to_send )
[docs] def target_rpm(self): """Get the desired RPM of the motor output shaft, assuming the user is controlling the motor using :class:`set_target_rpm` (motor is in control mode 1). If this is not the case, returns None. """ rpm = self.__motor_core.rpm_control() if rpm: return rpm / self.MMK_STANDARD_GEAR_RATIO * self.__forward_direction return None
[docs] def stop(self): """Stop the motor in all circumstances.""" self.__motor_core.stop()
@property def current_rpm(self): """Returns the actual RPM currently being achieved at the output shaft, measured by the encoder sensor. This value might differ from the target RPM set through :class:`set_target_rpm`. """ dc_motor_rpm_actual = self.__motor_core.tachometer() * self.__forward_direction output_shaft_rpm_actual = dc_motor_rpm_actual / self.MMK_STANDARD_GEAR_RATIO return output_shaft_rpm_actual @property def rotation_counter(self): """Returns the total or partial number of rotations performed by the motor shaft. Rotations will increment when moving forward, and decrement when moving backward. This value is a float with many decimal points of accuracy, so can be used to monitor even very small turns of the output shaft. """ dc_motor_rotation_counter = ( self.__motor_core.odometer() * self.__forward_direction ) output_shaft_rotation_counter = round( dc_motor_rotation_counter / self.MMK_STANDARD_GEAR_RATIO, 1 ) return output_shaft_rotation_counter @property def torque_limited(self): """Check if the actual motor speed or RPM does not match the target speed or RPM. Returns a boolean value, :data:`True` if the motor is torque- limited and :data:`False` if it is not. """ return False @property def max_rpm(self): """Returns the approximate maximum RPM capable given the motor and gear ratio.""" return floor(self.MAX_DC_MOTOR_RPM / self.MMK_STANDARD_GEAR_RATIO) @property def wheel_diameter(self): """Represents the diameter of the wheel attached to the motor in meters. This parameter is important if using library functions to measure speed or distance, as these rely on knowing the diameter of the wheel in order to function correctly. Use one of the predefined pi-top wheel and tyre types, or define your own wheel size. .. note:: Note the following diameters: - pi-top MMK Standard Wheel: 0.060.0m - pi-top MMK Standard Wheel with Rubber Tyre: 0.065m - pi-top MMK Standard Wheel with tank track: 0.070m :type wheel_diameter: int or float :param wheel_diameter: Wheel diameter in meters. """ return self.__wheel_diameter @wheel_diameter.setter def wheel_diameter(self, wheel_diameter): if wheel_diameter <= 0.0: raise ValueError("Wheel diameter must be higher than 0") self.__wheel_diameter = wheel_diameter @property def wheel_circumference(self): return self.wheel_diameter * pi
[docs] def set_target_speed(self, target_speed, direction=Direction.FORWARD, distance=0.0): """Run the wheel at the specified target speed in meters per second. If desired, a :data:`distance` to travel can also be specified in meters, after which the motor will stop. Setting :data:`distance` to 0 will set the motor to run indefinitely until stopped. .. warning:: Setting a :data:`.target_speed` higher than the maximum allowed will cause the method to throw an exception. To determine what the maximum possible target speed for the motor is, use the :class:`max_speed` method. .. note:: Note that for this method to move the wheel the expected :data:`distance`, the correct :data:`wheel_diameter` value needs to be used. :type target_speed: int or float :param target_speed: Desired speed in m/s :type direction: Direction :param direction: Direction to rotate the motor. Defaults to forward. :type distance: int or float :param distance: Total distance to travel in m. Set to 0 to run indefinitely. """ if not (-self.max_speed <= target_speed <= self.max_speed): raise ValueError( f"Wheel speed value must be between {-self.max_speed} and {self.max_speed} (inclusive)" ) rpm = 60.0 * (target_speed / self.wheel_circumference) total_rotations = distance / self.wheel_circumference self.set_target_rpm(rpm, direction, total_rotations)
[docs] def forward(self, target_speed, distance=0.0): """Run the wheel forward at the desired speed in meters per second. This method is a simple interface to move the motor that wraps a call to :data:`set_target_speed`, specifying the forward direction. If desired, a :data:`distance` to travel can also be specified in meters, after which the motor will stop. Setting :data:`distance` to 0 will set the motor to run indefinitely until stopped. .. note:: Note that for this method to move the wheel the expected :data:`distance`, the correct :data:`wheel_circumference` value needs to be used. :type target_speed: int or float :param target_speed: Desired speed in m/s :type distance: int or float :param distance: Total distance to travel in m. Set to 0 to run indefinitely. """ self.set_target_speed(target_speed, Direction.FORWARD, distance)
[docs] def backward(self, target_speed, distance=0.0): """Run the wheel backwards at the desired speed in meters per second. This method is a simple interface to move the wheel that wraps a call to :data:`set_target_speed`, specifying the back direction. If desired, a :data:`distance` to travel can also be specified in meters, after which the motor will stop. Setting :data:`distance` to 0 will set the motor to run indefinitely until stopped. .. note:: Note that for this method to move the wheel the expected :data:`distance`, the correct :data:`wheel_circumference` value needs to be used. :type target_speed: int or float :param target_speed: Desired speed in m/s :type distance: int or float :param distance: Total distance to travel in m. Set to 0 to run indefinitely. """ self.set_target_speed(target_speed, Direction.BACK, distance)
@property def current_speed(self): """Returns the speed currently being achieved by the motor in meters per second. This value may differ from the target speed set through :class:`set_target_speed`. """ return (self.current_rpm / 60.0) * self.wheel_circumference @property def distance(self): """Returns the distance the wheel has travelled in meters. This value depends on the correct :data:`wheel_circumference` value being set. """ return self.wheel_circumference * self.rotation_counter @property def max_speed(self): """The approximate maximum speed possible for the wheel attached to the motor shaft, given the motor specs, gear ratio and wheel circumference. This value depends on the correct :data:`wheel_circumference` value being set. """ return self.max_rpm / 60.0 * self.wheel_circumference