What's the very definition of beauty? A grand MirĂł painting sat in an art gallery? A snow-covered mountain range sparkling before your very eyes? Your favourite Coldplay song sang by a choir of Yetis, perhaps?
The answer is actually none of the above. It's because the most beautiful spectacle on earth is... a robot moving around smoothly (of course it is!). Without a care in the world. Gliding around. Effortlessly.
It's true: we did have Rosie moving around... but (if we're honest about it) not very elegantly. We sent her simple instructions like: forward, reverse, left, right,
Not very responsible of her.
What we would actually like is a gradual change in direction, like when we drive a car gently around a not-so-taxing bend. Not - we repeat, not - a 90 degree turn into the awaiting hedges.
Yes, let's make Rosie the fabulous dancer she always longed to be. A masterful, class-y ballerina.
You will need to have these:
- Raspberry Pi 3 (kitted out with wheels and sensors and stuff), and Raspbian running on SDHC card
- Computer from which you are connecting to the Raspberry Pi remotely
You will need to have done these things:
- I’ve got Pi(3) brain
- Why-Fi-ght the Wi-Fi?
- Hurrah-ndom inventions
- Don't reinvent the eel
- Achoo! Crash-choo! Episode I
- Achoo! Crash-choo! Episode II
- Web of Pies
- Hello supervision, goodbye supervision
- Abject, disorientated programming
You will need to do this:
- Modify the Python code to create our own classes for a motor controller, and motor
- Implement an algorithm to smoothly change motor speeds between different directions of travel
- Put on a robot show, of the utmost elegance and grace
What do you get after all this?
Unfortunately, there is a dash more of our brain power required for this task. And a bit more of that Object Orientated Programming we came across earlier. We made our distance sensors class-y in our previous post using... erm... classes. Did we not have enough? Clearly not, as we now want to make our motors and motor controller classes too.Because we want to have total control over each of our motors, and we want to come up with more
But what about the other cool stuff we said we'll do? The graceful, exquisite transition from one direction, to another? Remember?
Oh yes. For that, we'll create an internal method within our motor controller class to gradually change the speed of each motor as Rosie moves from one state to another. And we'll use an Algorithm (ooh, that's a grand word for just some logic and maths!) to work out how much we need to change the motor speeds by, in each step, to complete the overall change.
Do you feel a headache coming along? Get your aspirin ready...
Don't worry - it'll all be worth it. Because when we're all done, we'll wonder why we didn't do this in the first place.
This is simply too much detail:
So do you remember that RRB3 library we were using before? It was great for simple movements. Like for making Rosie (suddenly) move in a direction. But we want to go a bit more NASA. Like we did with our distance sensors, we want to create two new classes. Why? Because we want to, and we can.It's time... for some (rather unexpected) bullet points:
- Motor class
- MotorController class
All set. Let's go!
We begin our journey with the very low-level Motor class. Low-level, because it's the only bit of code that we'll allow to interact with the GPIO pins directly connected to the RasPiRobot V3 motor controller board. It's also not terribly sophisticated. In fact, it won't do much else other than to initialise the GPIO pins (during instantiation of our two motor objects), and send the necessary electrical signals to the controller board when instructed to do so by the MotorController class.
Interestingly, on its own, a motor won't know how to turn left or right. It can only turn in one direction, or the opposite, at a certain speed (which is actually what the Pulse Width Modulation (PWM) GPIO pin and signal is used to control). The logic to turn the robot will therefore be owned by the MotorController class, where it will have access to both motor objects.
As with all classes, our Motor class will have its special __init__ method. You'll recognise bits from the RRB3 library, but in short, during instantiation of our motors, we assign them GPIO pins, a description, and initialise a few variables.
Notice the self.current_speed instance variable at the end. This is what we will use to track individual motor's speed. We desperately need this to calculate our gradual speed changes for each motor (hence we can't track it in the MotorController class).
class Motor: def __init__(self, battery_voltage = 9.0, motor_voltage = 6.0, PWM_PIN = None, GPIO_PIN_1 = None, GPIO_PIN_2 = None, motor_description = None): self.pwm_scale = float(motor_voltage) / float(battery_voltage) self.PWM_PIN = PWM_PIN self.GPIO_PIN_1 = GPIO_PIN_1 self.GPIO_PIN_2 = GPIO_PIN_2 self.motor_description = motor_description if self.pwm_scale > 1: raise ValueError("Motor voltage is higher than battery voltage") gpio.setmode(gpio.BCM) gpio.setwarnings(False) gpio.setup(self.PWM_PIN, gpio.OUT) self.pwm = gpio.PWM(self.PWM_PIN, 500) self.pwm.start(0) gpio.setup(self.GPIO_PIN_1, gpio.OUT) gpio.setup(self.GPIO_PIN_2, gpio.OUT) self.current_speed = 0
The actual 'do-ing' of the Motor class is done in further two methods.
The first - _set_motors() - is used to send the required signals to the GPIO pins connected to the motor controller board. This is where we control the direction of the motor, and its speed. The method name starts with an underscore (_), to remind people that it's not to be called directly from outside this class. In other words, this is purely an internal method. There is actually an official style guide for Python (called PEP 8), which serious coders are supposed to be following*, specifically for advice like this.
*Yes, we've ignored much of it to date. Yes, we'll go and stand in the naughty corner.
def _set_motors(self, set_speed, motor_direction): if set_speed > 1: set_speed = 1 elif set_speed < 0: set_speed = 0 self.pwm.ChangeDutyCycle(set_speed * 100 * self.pwm_scale) gpio.output(self.GPIO_PIN_1, motor_direction) gpio.output(self.GPIO_PIN_2, not motor_direction) if motor_direction == 0: self.current_speed = set_speed elif motor_direction == 1: self.current_speed = -set_speed else: self.current_speed = 0
There are two things here that might interest you (if you haven't got much else to interest you, generally). We have a bit of code to make sure no miscreant can send a speed of more than 1, or less than 0, to the motor. These values are effectively capped. Having this fail-safe at this level makes sure that whatever mistakes happen at the motor controller level and above (like an erroneous speed of 1,987,657), it won't impact the motors, at least physically.
You'll also see that after the GPIO pins are set - and motors begin doing their thing - we set the instance variable self.current_speed. We can now track this value from outside this class, for example from the MotorController class, and keep tabs on what speed (and direction) a specific motor is. Here, you'll notice that we use a scale of -1 (reverse) to 0 (stopped) to 1 (forward), as this allows us to nicely calculate speed differences in the MotorController class. We can also tell the direction, from a single variable.
We then have our actual method that we can call externally from the MotorController class. We've been very creative, and called it... move().
def move(self, requested_speed = 0): if requested_speed > 0: self._set_motors(requested_speed, 0) elif requested_speed < 0: self._set_motors(-requested_speed, 1) else: self._set_motors(0, 1)
What does it do? Not much actually. It just receives the call to move the motor between -1 (reverse), 0 (stopped) and 1 (forward) and converts this into a language that the motor understands in _set_motors(). This is necessary, because rather than just use values between -1 and 1, the motor controller board requires values in two forms: speed between 0 and 1, and motor direction as a 0 or a 1.
We can now move onto our grand reveal: the MotorController class. During its initialisation, it actually instantiates the two motor classes, as self.m1 and self.m2. It also configures two instance variables, to track its current speed (for the robot itself, not the individual motors) and current action: self.current_speed and self.current_action.
def __init__(self): self.m1 = Motor(9, 6, 14, 10, 25, "Left front motor") self.m2 = Motor(9, 6, 24, 17, 4, "Right front motor") self.current_speed = 0 self.current_action = None
Rest of the class is a little crowded. It contains two distinct sets of methods. First batch of methods, allows the motor controller to receive instructions from other parts of the application, and move each motor appropriately. Like forward, reverse, left, right and stop. The usual. You get the idea. Here's an example for the forward method, forward():
def forward(self, next_speed = 0, gradual = False, transition_duration_s = 1, transition_steps = 10): if gradual == False: self.m1.move(next_speed) self.m2.move(next_speed) elif gradual == True: self._transition(self.ACTION_FORWARD, next_speed, transition_duration_s, transition_steps) self.current_speed = next_speed self.current_action = self.ACTION_FORWARD
If the gradual flag is False, it instructs the two Motor objects (self.m1 and self.m2) to immediately move the motors at the intended speed. This was the only behaviour up until now. You know, the not very smooth one. And it's also still quite useful, as sometimes, we just want to apply the emergency breaks, or speed through our day, dangerously.
The bulk of the new code, however, is invoked when the gradual flag is set to True. This is when it steps through our internal _transition() method, where the real magic happens.
Are you prepared for some more bullet points? Good, you're going to get them.
The overall purpose of _transition() is to use the following information (inputs):
- Current action (or direction) - self.current_action
- Current speeds of each motor - self.m1.current_speed, self.m2.current_speed
- Next action (or direction) - next_action
- Next speed - next_speed
- Duration (in seconds) to move from current action to next action - transition_duration_s
- Number of steps (changes in speed) we'd like to make - transition_steps
...And from this, work out (outputs):
- How long each step should last for, stored as _step_time
- Planned sequence of the changes in speed for each motor, to get us from current action / speed, to next action / speed. We'll store this sequence in a Python List variable (_control_sequence), so that we can iterate through it, line by line, to instruct the motors.
So for example, if both motors are moving forwards at a speed of 0.5, and we want Rosie to stop in 10 steps, our _control_sequence list for this transition will probably look like this. As you can see, both motors are losing speed, together.
(0.45, 0.45) (0.4, 0.4) (0.35, 0.35) (0.3, 0.3) (0.25, 0.25) (0.2, 0.2) (0.15, 0.15) (0.1, 0.1) (0.05, 0.05) (0.0, 0.0)
So over a total of 1 second, we will reduce each motor's speed by 0.05 at 0.1 second intervals.
Things get a lot more interesting when we need to reverse one motor, or both. This is Rosie changing from turning right at a speed of 0.5, to moving forwards. You can see the right motor gaining speed at increments of 0.1 which brings it on par with the left motor (which doesn't change speed).
(0.5, -0.4) (0.5, -0.3) (0.5, -0.2) (0.5, -0.1) (0.5, 0.0) (0.5, 0.1) (0.5, 0.2) (0.5, 0.3) (0.5, 0.4) (0.5, 0.5)
And all we're doing to work this out is simple maths. Using current and next speeds for each motor, we can work out the total change in speed required, then divide that by the amount of steps (transition_steps). We then know, for each step, what change in speed is required by each motor, incrementally. This is the example when the next action is a 'stop'.
if next_action == None: _m1_change_speed = float(0 - self.m1.current_speed) / transition_steps _m2_change_speed = float(0 - self.m2.current_speed) / transition_steps while _count < transition_steps: _control_sequence.append((self.m1.current_speed + _m1_change_speed * (_count + 1), self.m2.current_speed + _m2_change_speed * (_count + 1))) _count += 1
Here, we simply work out what the incremental changes in speed need to be to bring both motors to a standstill, from their current speeds. Then we populate our list _control_sequence, using the append() function of a Python list and a while loop, with a list of planned motor speeds, starting with step 0, to 9.
Finally, after all of this, we simply instruct our motor controller to read our list, line by line using a while loop, and instruct each motor to travel at the determined speed at each step. Notice that len() is used to establish the size of the list (which should actually be the same as transition_steps), and brackets - [ ] - are used to access actual values in the list. _step, in this instance, is being used as the index (or step number in our case), with the second [0 or 1] used to indicate the 1st or 2nd values at that index (m1 speed, or m2 speed).
while _step < len(_control_sequence): self.m1.move(_control_sequence[_step][0]) self.m2.move(_control_sequence[_step][1]) _step += 1 time.sleep(_step_time)
The entirety of the code, which we bundled up in a file named rosie-web-gentle.py, is presented here:
And don't forget, where we now call the motor controller into action, such as in our Flask 'control' HTTP function for our web page, we do so using the flag. And we all know why now. We are happy with the default transition duration (1s), and steps (10), so we don't provide them as arguments. You could experiment with these values to see what happens.
rr.left(0.5, True)
Of course, don't forget the steps in Supervisor that we configured before when you are deploying this thing, so that this application auto-starts.
First, stop the current rosie application in Supervisor.
sudo supervisorctl stop rosie cd rosie/rosie-web/ nano rosie-start.sh...stops the current supervisor-managed 'rosie' application. Then let's edit the rosie-start.sh shell script, using nano, to point it at our new rosie-web-gentle.py Python application with all the goodies.
Then, let's start the 'rosie' application using Supervisor (now pointing at rosie-web-gentle.py), again.
sudo supervisorctl start rosie...starts rosie application using Supervisor
Now, navigate to the web page and enjoy the gentler results. And as this Python application is now registered in Supervisor (via the shell script), it will start-up automatically when the Pi is powered on.
This is by no means all that's possible with our brand new motor controller class. You could work on an algorithm to overcome the initial resistance encountered when the motor initially starts moving (and not make the speed changes so linear). Or you could make your robot go through a long list of pre-planned motions (like... a dance routine!)
With your own motor controller class, these things are now all very much in your reach. But for now, enjoy a much improved robot gracing your dance floor. And a group of Yetis singing Viva la Vida is very much optional.
Even more reading:
Official Python documentation on lists:Official style guide for Python (PEP8) which describes various conventions:
Simon Monk's RRB3 library:
The motor controller board in use is the RasPiRobot Board V3 by Monk Makes:
Comments
Post a Comment