-
Notifications
You must be signed in to change notification settings - Fork 1
Simple Walker
In this example we will put together the control system for the simple 4-legged walking robot described here. This robot has only 2 servos for output and we'll control it with only 2 inputs from an R/C radio.
I built one of these from acrylic cut on a laser cutter. It's a pretty clever design with the servos tilted in towards each other to allow for a crude but effective lifting of the feet.
Here is a diagram of the simple walker. The left image is the neutral position, the right represents the range of motion. The leg pairs move 30 degrees to each side.
###Simple Walker Diagram

We'll start coding with some imports:
import kabuki
from kabuki.operators import Cycler
from kabuki.pyboard.inputs import PpmIn
from kabuki.pyboard.outputs import ServoOutWe're going to have an input to control the speed, both forward and reverse. This comes from a standard R/C radio system.
ppm = PpmIn("X1")
speed = (ppm
.channel(1)
.map(1000, 2000, -2, 2)
.filter_between(-0.5, 0.5)
.throttle(10)
)What we've done here is create an input, PpmIn which reads a PPM frame on the X1 pin. This input depends on the PPM frame decoder found here
. The PPM frame contains the signals of all channels from the receiver. We'll read channel 1 here.
A given channel has values in the range of 1000 to 2000 for milliseconds of duty cycle. We want to translate this to the range of negative two to positive two. This range was determined by experimentation once the whole program was completed and relates to the length of time of the full walk cycle. We chain a map operator which will "stretch" or "compress" values from one range to another. By default the map operator also "constrains" the value such that it is always within the output range (so both 1000 and 900 in this case would map to -2).
The input signal has a little noise in it which is to say the duty cycle varies even when your hands are off the stick. We don't want the walker to creep along ever so slowly. So we chain on a filter_between operation to create a "dead band" around the neutral position.
Finally, we don't need super high resolution readings from the PPM input. So we add a throttle operation that says "don't sample less than 10 milliseconds apart", or cache the last read for 10ms.
The right side image above shows the legs "in sync" so to speak, both are at the extreme position. This symmetric motion actually is not productive. Which way is forward? There isn't any difference between forward and reverse motion. We need to offset the front from the back a bit.
Here is a graph of the key frames we will use for the "animation" of the walking motion:

The red line is the front, the blue the rear (it doesn't really matter, I don't know which end of this thing in "the front"). So the front leg reaches its extreme just before the rear.
The top and bottom dotted lines represent 30 and -30 which are the extreme positions of the servos in degrees. The four circles are the key frame positions.
cycler = Cycler(100, speed)Here we introduce the Cycler. The Cycler is a key frame animation system. We declare the length of the animation to be 100 units. This value is arbitrary and related to the speed scaling value of +2 to -2 in the speed mapping above. The speed is, as you might guess, the speed at which the animation plays, which is the amount the position changes each loop. To walk backwards we will simply play the animation backwards.
front = cycler.channel([
(5, 0),
(30, 30),
(55, 0),
(80, -30)
])
kabuki.wire_output(front, ServoOut(2))Next we create a "channel" off the cycler we can add key frames to, then we wire the animated channel to servo #2. The position in time and the values of the keys are fixed (literal values). Later we'll make the position values dynamic.
rear = cycler.channel([
(15, 0),
(40, -30),
(65, 0),
(90, 30)
])
kabuki.wire_output(rear, ServoOut(3))Finally we do the same for the rear legs. That's all we need for forward and backward walking. Next, we'd like to make it turn.
###Turning

The strategy for turning is to compress the swing of the legs to one side or the other. In the image above, we change the front leg range from (30 to -30) to (0 to -30). If you look back up at the key frame setup, you see 4 lines where the second argument is the literal position value (0, -30, 0, and 30). The zero is the neutral position, 30 and -30 the extremes. We need to change these literal values to nodes so we can influence them. We'll do the influencing from another radio channel:
turn = (ppm
.channel(0)
.map(1000, 2000, -0.5, 0.5)
.filter_between(-0.05, 0.05)
.throttle(10)
)We create a new input off the PPM for channel 0. This is just like forward/backward, just with different numbers.
The end point of the clockwise swing of the front leg will vary from 30 for straight to 0 for a left turn. This same end point will be at 30 for a right turn as well so we're not going to map the full input range of (-0.5 to 0.5) to (0 to 30). We want to map (-0.5 to 0) to (0 to 30). Again, map() will constrain the output to the given range, therefore all values from 0 to 0.5 will map to 30.
At the same time the neutral point will shift from -15 to 15.
front_neutral = turn.map(-0.5, 0.5, -15, 15)
front_swing_right = turn.map(-0.5, 0.0, 0.0, 30)
front_swing_left = turn.map(0.0, 0.5, -30, 0.0)
rear_neutral = front_neutral.neg()
rear_swing_right = front_swing_left.neg()
rear_swing_left = front_swing_right.neg()Since the rear legs just do the opposite of the front all we have to do is negate them.
front = cycler.channel([
(5, front_neutral),
(30, front_swing_right),
(55, front_neutral),
(80, front_swing_left)
])
kabuki.wire_output(front, ServoOut(2))
rear = cycler.channel([
(15, rear_neutral),
(40, rear_swing_left),
(65, rear_neutral),
(90, rear_swing_right)
])
kabuki.wire_output(rear, ServoOut(3))Finally, we replace the literal values in the Cycler channels with the new dynamic nodes. Here's the full program. My IDE shows a total of 46 lines of code. I've spread some statements over multiple lines for readability. Other than the imports there are really only 14 statements!
import kabuki
from kabuki.operators import Cycler
from kabuki.pyboard.inputs import PpmIn
from kabuki.pyboard.outputs import ServoOut
ppm = PpmIn("X1")
speed = (ppm
.channel(1)
.map(1000, 2000, -2, 2)
.filter_between(-0.5, 0.5)
.throttle(10)
)
turn = (ppm
.channel(0)
.map(1000, 2000, 0.5, -0.5)
.filter_between(0.05, -0.05)
.throttle(11)
)
cycler = Cycler(100, speed)
front_neutral = turn.map(-0.5, 0.5, -15, 15)
front_swing_right = turn.map(-0.5, 0.0, 0.0, 30)
front_swing_left = turn.map(0.0, 0.5, -30, 0.0)
rear_neutral = front_neutral.neg()
rear_swing_right = front_swing_left.neg()
rear_swing_left = front_swing_right.neg()
front = cycler.channel([
(5, front_neutral),
(30, front_swing_right),
(55, front_neutral),
(80, front_swing_left)
])
kabuki.wire_output(front, ServoOut(2))
rear = cycler.channel([
(15, rear_neutral),
(40, rear_swing_left),
(65, rear_neutral),
(90, rear_swing_right)
])
kabuki.wire_output(rear, ServoOut(3))One thing you might notice is that when turning, the range of motion is compressed but the time is not. Therefore it moves slower when turning. Can you figure out how to influence the speed node with the turn node to speed up the swing and thus maintain a constant speed?