Created
June 21, 2020 21:22
-
-
Save kopp/2642945fb9988fe21fcf0e68da2925a6 to your computer and use it in GitHub Desktop.
A python class that allows to control a two-wheel robot with two AC motors on the Calliope Mini.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from calliope_mini import pin28, pin29, pin30 | |
from utime import ticks_us, ticks_add | |
# Note: as the file is large and has long comments, use pyminifier before uploading to Calliope mini. | |
# Note: Some abstration is missing here and code is duplicated. | |
# This happens with a reason: | |
# MicroPython has only a very limited stack size and thus number of nested | |
# function calls is low. | |
# Because of that, code is repeated in different functions | |
# instead of factoring it out, because calling the factored out function | |
# reduces the available number of nested function calls for the "client" using | |
# this code. | |
class TwoAcMotors: | |
""" | |
This class assumes, that the motors are wired as follows: | |
- left motor is connected to Motor A pin and GND | |
- right motor is connected to Motor B pin and GND | |
This means, that both motors can only go forwards. | |
Implementation: | |
When the robot should go straight forwards, each period is split evenly | |
between the left and the right motor. | |
|--------------- one period ------------------------| | |
|---- left on ------------|---- left off -----------| | |
|---- right off ----------|---- right on -----------| | |
When the robot should turn, the time is not divided evenly any more but | |
with a given factor, e.g. turn left with factor 0.7 means that the right | |
motor is on for 70% of the period and the left for only 30%. | |
|--------------- one period ------------------------| | |
|-- left on ---|----- left off ---------------------| | |
|-- right off -|----- right on ---------------------| | |
The speed is determined by what "on" means. | |
The pin controlling each motor will be written with an analog value which | |
is proportional to the speed (1.0 -> maximum analog value, 0.0 -> off). | |
""" | |
MOTOR1_ON = 1 | |
MOTOR2_ON = 2 | |
OBSOLETE = 3 # mark the current state as obsolete i.e. needing change | |
def __init__(self, default_speed=1): | |
if not 0.0 <= default_speed <= 1.0: | |
raise ValueError("Speed values must be in [0, 1].") | |
self.default_speed = default_speed | |
self.period_duration_us = 16384 # multiple of 256 (analog period) | |
self._is_active = False | |
self._motors_analog_value = None | |
self._timestamp_us_period_begin = None | |
self._duration_us_first_motor = None | |
self._timestamp_us_period_end = None | |
pin29.set_analog_period_microseconds(256) | |
pin30.set_analog_period_microseconds(256) | |
def start_to_move_forwards(self, speed=None): | |
""" | |
Move forward with given speed. | |
Note: if speed is too low, it may happen that the robot does not move at all. | |
:param speed float: Speed with which to move -- see ``set_default_speed``. | |
""" | |
if speed is None: | |
speed = self.default_speed | |
if not 0.0 <= speed <= 1.0: | |
raise ValueError("Speed values must be in [0, 1].") | |
now = ticks_us() | |
self._timestamp_us_period_begin = now | |
self._duration_us_first_motor = int(self.period_duration_us / 2) | |
self._timestamp_us_period_end = ticks_add(self._timestamp_us_period_begin, self.period_duration_us) | |
self._motors_analog_value = int(1024 * speed) | |
self._motors_current_state = TwoAcMotors.OBSOLETE | |
self._is_active = True | |
def stop(self): | |
""" | |
Stop the motors directly -- no further call to ``act()`` necessary. | |
""" | |
self._is_active = False | |
pin28.write_digital(0) | |
pin29.write_digital(0) | |
pin30.write_digital(0) | |
def start_curve(self, direction, turn_factor=1.0, speed=None): | |
""" | |
Start to turn the robot. | |
Note: if speeds are low, it may happen that for a small turn factor, | |
the wheel in turning direction is not turned at all. | |
:param direction str: either 'left' or 'right' | |
:param turn_factor float: Ratio of the robot's ability to do the turn (1 -> maximal, 0 -> no turn at all) | |
:param speed float: Speed with which to move -- see ``set_default_speed``. | |
""" | |
if not 0.0 <= turn_factor <= 1.0: | |
raise ValueError("Please use a value in [0, 1] as turn factor.") | |
if speed is None: | |
speed = self.default_speed | |
else: | |
if not 0.0 <= speed <= 1.0: | |
raise ValueError("Speed values must be in [0, 1].") | |
if direction == "left": | |
self._duration_us_first_motor = int(self.period_duration_us * ((1.0 - turn_factor) / (2.0 - turn_factor))) | |
elif direction == "right": | |
self._duration_us_first_motor = int(self.period_duration_us / (2.0 - turn_factor)) | |
else: | |
raise ValueError("Please use only 'left' or 'right' as direction.") | |
now = ticks_us() | |
self._timestamp_us_period_begin = now | |
self._timestamp_us_period_end = ticks_add(self._timestamp_us_period_begin, self.period_duration_us) | |
self._motors_analog_value = int(1024 * speed) | |
self._motors_current_state = TwoAcMotors.OBSOLETE | |
self._is_active = True | |
def act(self): | |
""" | |
This method must be called in a fast loop. | |
Only when it is called, actual commands are executed and sent to the motors. | |
""" | |
if self._is_active: | |
now = ticks_us() | |
pin28.write_digital(1) | |
# handle end of period | |
if now >= self._timestamp_us_period_end: | |
self._timestamp_us_period_begin = self._timestamp_us_period_end | |
self._timestamp_us_period_end = ticks_add(self._timestamp_us_period_begin, self.period_duration_us) | |
# check, in which part of the period (motor 1 or 2) | |
timestamp_switch_motors = ticks_add(self._timestamp_us_period_begin, self._duration_us_first_motor) | |
if now < timestamp_switch_motors: # TODO: comparison is mostly wrong in case of overflow | |
motors_desired_state = TwoAcMotors.MOTOR1_ON | |
else: | |
motors_desired_state = TwoAcMotors.MOTOR2_ON | |
# set the pins if they need change | |
if motors_desired_state != self._motors_current_state: | |
if motors_desired_state == TwoAcMotors.MOTOR1_ON: | |
self._motors_current_state = TwoAcMotors.MOTOR1_ON | |
if self._motors_analog_value == 1024: | |
pin29.write_digital(1) | |
else: | |
pin29.write_analog(self._motors_analog_value) | |
pin30.write_digital(0) | |
elif motors_desired_state == TwoAcMotors.MOTOR2_ON: | |
self._motors_current_state = TwoAcMotors.MOTOR2_ON | |
pin29.write_digital(0) | |
if self._motors_analog_value == 1024: | |
pin30.write_digital(1) | |
else: | |
pin30.write_analog(self._motors_analog_value) | |
else: | |
raise ValueError("Unknown desired motor state {}".format(motors_desired_state)) | |
def set_period_duration(self, duration_in_us): | |
""" | |
This class implements a software PWM. | |
Use this function to set the period duration. | |
A larger period means, that it is possible to use/define the speed commands more precisely. | |
The commands can only be executed as fractions of the period duration | |
-- if the period duration is 20, only speeds 0/20, 1/20, 2/20, ..., | |
20/20 can be actually actuated. | |
On the other hand, a longer period means, that the robot drives | |
longer with only one wheel on, so the small S-curves that it drives | |
during the two parts of a period are more visible. | |
If the values are too small, the software simply breaks, because it | |
has no time to call multiple ``act()`` calls within a period. | |
See the documentation of the class for more info on the period. | |
Only use this if you know, what you do. | |
""" | |
self.period_duration_us = duration_in_us | |
def set_default_speed(self, speed): | |
""" | |
Set the default speed -- this speed is used if you use one of the | |
moovement-functions without parameter for speed. | |
The speed is a floating point value in the range 0..1. | |
0.0 means to not move at all and 1.0 means to move as fast as possible. | |
""" | |
if 0.0 <= speed <= 1.0: | |
self.default_speed = speed | |
else: | |
raise ValueError("Speed values must be in [0, 1].") | |
# the following is for demo only. | |
class Timer: | |
def __init__(self, duration_ms, autostart=True): | |
self.duration_us = 1000 * duration_ms | |
self._is_expired = False | |
if autostart: | |
self._timestamp_us_end = ticks_add(ticks_us(), self.duration_us) | |
def start(self): | |
self._is_expired = False | |
self._timestamp_us_end = ticks_add(ticks_us(), self.duration_us) | |
def rewind(self, new_duration_ms, autostart=True): | |
self.duration_us = 1000 * new_duration_ms | |
self._is_expired = False | |
if autostart: | |
self._timestamp_us_end = ticks_add(ticks_us(), self.duration_us) | |
def is_expired(self): | |
now = ticks_us() | |
if now >= self._timestamp_us_end: # TODO: breaks in case of tick overflow | |
self._is_expired = True | |
return self._is_expired | |
if __name__ == "__main__": | |
motors = TwoAcMotors() | |
timer = Timer(1000) | |
print("go straight") | |
timer.start() | |
motors.start_to_move_forwards() | |
while not timer.is_expired(): | |
motors.act() | |
print("rotate left") | |
timer.start() | |
motors.start_curve("left") | |
while not timer.is_expired(): | |
motors.act() | |
print("pause") | |
motors.stop() | |
timer.rewind(500) | |
while not timer.is_expired(): | |
pass | |
print("rotate right slower") | |
timer.rewind(1500) | |
motors.start_curve("right", speed=0.7) | |
while not timer.is_expired(): | |
motors.act() | |
print("pause") | |
motors.stop() | |
timer.rewind(500) | |
while not timer.is_expired(): | |
pass | |
print("curve left") | |
timer.rewind(1500) | |
motors.start_curve("left", turn_factor=0.6) | |
while not timer.is_expired(): | |
motors.act() | |
print("straight ...") | |
timer.rewind(1000) | |
motors.start_to_move_forwards() | |
while not timer.is_expired(): | |
motors.act() | |
print(" ... small curve right ...") | |
timer.rewind(300) | |
motors.start_curve("right", turn_factor=0.3) | |
while not timer.is_expired(): | |
motors.act() | |
print(" ... and straight again") | |
timer.rewind(1000) | |
motors.start_to_move_forwards() | |
while not timer.is_expired(): | |
motors.act() | |
motors.stop() | |
from calliope_mini import display, Image | |
display.show(Image.HEART) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment