Skip to main content

Rough around the hedges


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, take over the world, and stop.  And guess what?  She did exactly that.  To the letter.  That's why when we told her to turn left, when she was already travelling forwards towards the ice cream stand, she abruptly stopped and caused a traffic pile-up, before veering off, dangerously.  Cue the dramatic police chase.

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:

  1. I’ve got Pi(3) brain
  2. Why-Fi-ght the Wi-Fi?
  3. Hurrah-ndom inventions
  4. Don't reinvent the eel
  5. Achoo! Crash-choo! Episode I
  6. Achoo! Crash-choo! Episode II
  7. Web of Pies
  8. Hello supervision, goodbye supervision
  9. 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 mad bespoke ways of controlling them individually, we will create a new low-level class for our motor (which we will instantiate twice for our actual left and right motors).  And while we're at it, let's magic up a higher-level class for our very own motor controller.  This way, we can still send simple instructions to the motor controller to make Rosie move, and it will work out what exactly to tell the two motors.

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
...represents what our motor is, and does.  Please could we instantiate two of these so that we can operate our left and right motors at different speeds, in different directions, as and when required by our motor controller?  It's your lucky day.  Permission granted!

  • MotorController class
...represents what our motor controller is, and does (with above motors).  We'll code the motor controller with some methods so that we can still tell the robot to go forward, reverse, turn left, turn right and stop.  And it can work out exactly what it needs to do with the two motors to make these things happen.  And what about the smoothing we keep promising you?  Yes, let's do that here too.

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

MOST VISITED (APPARENTLY)

LoRa-Wan Kenobi

In the regurgitated words of Michael BublĂ©: It's a new dawn .  It's a new day .  It's a new Star Wars film .  For me .  And I'm (George Lucas, and I'm) feeling good .  Unfortunately for Canadian Mike, the Grammy that year was won by the novelty disco classic with the famous refrain: We love IoT, even in Planet Tatooine * . *Not true. Clearly, the Star Wars producers didn't sincerely mean the last Jedi the previous time around.  Return of the Jedi, released during the decade that spearheaded cultural renaissance 2.0 with the mullet and hair-metal , was less economic with the truth.  Either way, we're going to take inspiration from the impressive longevity of the money-spinning space-opera and reboot our franchise with some Jedi mind tricks.  Except this particular flick doesn't require an ever-growing cast of unrecognisable characters, unless ASCII or UTF counts.  In place of an ensemble gathering of Hollywood stars and starlet...

Battle of BLEtain

The trolling . The doxing . An army of perplexing emojis. And endless links to the same - supposedly funny - viral video of a cat confusing a reflection from a dangling key for a golden hamster, while taking part in the mice bucket challenge. Has social media really been this immense force for good? Has it actually contributed significantly to the continued enlightenment of the human (or feline) race? In order to answer these poignant existential questions about the role of prominent platforms such as Critter, StinkedIn and Binterest, employing exceptional scientific rigour equal to that demonstrated by Theranos , we're going to set up a ground-breaking experiment using the Bluetooth Low Energy feature of MicroPython v1.12, and two ESP32 development boards with inexplicable hatred for one another.  And let them hurl quintessentially British expressions (others call them abuse) at each other like two Wiltshire residents who have had their internet access curbed by the co...

Hard grapht

You would all be forgiven for assuming that bar , pie and queue line are favourite pastimes of the British .  Yet, in fact – yes, we did learn this back in GCSE maths – they are also mechanisms through which meaningless, mundane data of suspect origin can be given a Gok Wan -grade makeover, with the prime objective of padding out biblical 187-page PowerPoint presentations and 871-page Word reports (*other Microsoft productivity tools are available).  In other words, documents that nobody has the intention of ever reading.  But it becomes apparent over the years; this is perhaps the one skill which serves you well for a lifetime in certain careers.  In sales.  Consultancy.  Politics.  Or any other profession in which the only known entry requirement is the ability to chat loudly over a whizzy graph of dubious quality and value, preferably while frantically waving your arms around. Nevertheless, we are acutely conscious of the fact that w...