Raspberry Pi and Gamepad Programming Part 3: Adding a Gun Turret

Subtitling this one “Adding a Gun Turret” seems almost like click bait, doesn’t it? But yes, that’s exactly what we’re doing. One of Dexter Industries’ sample projects is attaching the Dream Cheeky Thunder Cannon to the GoPiGo. I’m going to show you how to control it with the same gamepad that also controls the robot’s movement.

In part two of this series we connected the gamepad events to the GoPiGo movement commands. We used the evdev module to identify the button that was pressed and then moved (or stopped) the GoPiGo accordingly.

But the gamepad has more controls than the seven we have used so far. What about the joysticks? We’ll use the directional pad (which sends joystick events) to control the cannon.

Dream-Cheeky-Ltd-908-desktop-gadgets-computer-accessories-PACK

Interfacing with the Cannon

The Thunder Cannon does not ship with any kind of API. However the “Office Cannon” project does have Python code in it for controlling the thunder cannon.  I modified the script in the project and made a small module for controlling the cannon. I have to give credit where credit is due: the Dexter Industries crew’s work made this possible. I just moved the code to a module and shifted a few things around.

We can’t use evdev to control the cannon : evdev is designed for reading from devices, not writing to them. Even if we did try to use it, the messages in evdev are not what the Cannon expects. The pyusb module allows us to see devices on USB and write messages to them.

The Raspbian Linux run on most Raspberry Pis is, of course, based on Debian Linux. Debian is very conservative when it comes to packages, and if you use apt-get to install pyusb you will get an older version that does include all of the features we need. I used Dexter’s modified image which already has pyusb installed. If you wish to use your own, follow the instructions for installing from Github here. (All things considered, taking the time to setup an SD Card with Dexter’s image is worth the effort.)

The office cannon script from the GoPiGo samples has some commands that can be sent to the cannon. I reused them, plus added a few constants of my own. I’ll explain my additions in a bit.

# Protocol command bytes
DOWN = 0x01
UP = 0x02
LEFT = 0x04
RIGHT = 0x08
FIRE = 0x10
STOP = 0x20
PARK = 0x30
LED = 0x31

But before we can send a command we need to “find” the device:

    DEVICE = usb.core.find(idVendor=0x2123, idProduct=0x1010)

    if DEVICE is None:
        DEVICE = usb.core.find(idVendor=0x0a81, idProduct=0x0701)
        if DEVICE is None:
            raise ValueError('Missile device not found')
        else:
            DEVICE_TYPE = "Original"
    else:
        DEVICE_TYPE = "Thunder"

Find() finds USB devices by probing for the vendor and product ids it is passed and returns an object (or None). In setup() we search for either a Thunder Cannon or an older model. We set DEVICE_TYPE as a flag to tell us which one we ended up with.

Once we have a device, we can send commands to it:

# Send command to the office cannon
def __cmd(cmd):
    if "Thunder" == dev_type:
        dev.ctrl_transfer(0x21, 0x09, 0, 0, [0x02, cmd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
    elif "Original" == dev_type:
        dev.ctrl_transfer(0x21, 0x09, 0x0200, 0, [cmd])


# Send command to control the LED on the office cannon
def __led(cmd):
    if "Thunder" == dev_type:
        dev.ctrl_transfer(0x21, 0x09, 0, 0, [0x03, cmd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
    elif "Original" == dev_type:
        print("There is no LED on this device")

These 2 “hidden” methods send messages, or “transfers” to use USB lingo, to the cannon.  We see where the DEVICE_TYPE flag is used here. The older model cannon didn’t have an LED on it and also expects a different command syntax.

I hid these methods from other modules by prefixing them with two underscores.The reason for this is to simplify the interface for the user, as we’ll see below.

The public interface to the module is here:

def run_command(command, value):
    if command == RIGHT or command == LEFT or command == UP or command == DOWN:
        __move(command, value)
    elif command == PARK:
        # Move to bottom-left
        __move(DOWN, 2000)
        __move(LEFT, 8000)
    elif command == LED:
        if value == 0:
            __led(0x00)
        else:
            __led(0x01)
    elif command == FIRE:
        # Stabilize prior to the shot, then allow for reload time after.
        time.sleep(0.5)
        __cmd(FIRE)
        time.sleep(4.5)
    else:
        print "Error: Unknown command: '%s'" % command

This function accepts one of the constants defined above, along with an integer value. The function using the constant to figure out which internal (“hidden”) function to call. And how the integer value is used.

  • At the top we see that the directional constants are simply passed on to __move().
  • PARK is expanded into a __move DOWN and then LEFT.
  • LED turns the LED on or off.
  • FIRE pauses for half a second and the fires the cannon.

__move adds duration to the cannon movements:

# Send command to move the office cannon
def __move(cmd, duration_ms):
    __cmd(cmd)
    time.sleep(duration_ms / 1000.0)
    __cmd(STOP)

So when a direction is sent, the integer argument is how long to move in that direction.

With an LED we expect 0 or 1 to turn it on or off.

With PARK and FIRE the integer is ignored. Not the most elegant API, but it’s good enough for this quick exercise.

Connecting the Gamepad to the Cannon

Joystick events in evdev are quite different from buttons. Buttons send us a simple up or down event. Joystick controllers send events that contain coordinates corresponding to a position on a screen.  If we wanted to use the actual joystick controllers we would need to simplify these into UP-DOWN-RIGHT-LEFT commands. However the directional pad does that for us. So here are our controls, including controlling to GoPiGo.

Turrent controls

We need to process the directional pad events in our evdev read loop:

    for event in gamepad.read_loop():
        if event.type == ecodes.EV_ABS:
            absevent = categorize(event)
            print ecodes.bytype[absevent.event.type][absevent.event.code], absevent.event.value
            if ecodes.bytype[absevent.event.type][absevent.event.code] == 'ABS_HAT0X':
                if absevent.event.value == -1:
                    next_move = thunder.LEFT
                    event_time = absevent.event.timestamp()
                elif absevent.event.value == 1:
                    next_move = thunder.RIGHT
                    event_time = absevent.event.timestamp()
            if ecodes.bytype[absevent.event.type][absevent.event.code] == 'ABS_HAT0Y':
                if absevent.event.value == -1:
                    next_move = thunder.UP
                    event_time = absevent.event.timestamp()
                elif absevent.event.value == 1:
                    next_move = thunder.DOWN
                    event_time = absevent.event.timestamp()
        elif event.type == ecodes.EV_SYN:
            synevent = categorize(event)
            print synevent
            syntime = synevent.event.timestamp()
            move_duration = (syntime - event_time) * 1000
            print move_duration
            thunder.run_command(next_move, move_duration)

There’s a lot packed in here.

When we press the directional pad we receive an ABS event. These events, similar to button events, have a timestamp. They also have a code (similar to the button codes we’re seen before) and a value. With an actual joystick this value corresponds to a position on an axis. With the pad we simply get 1 or -1.

So when we receive the event we check for horizontal (ABS_HAT0X) or vertical (ABS_HAT0Y) and then we save the direction and the time.

We save the time because when the directional pad is released we then receive a SYN event. This event is similar to the button UP events we saw in part 2.  If we subtract our saved time from this we have how may milliseconds the button was held.  We use this as our duration for the command.

With this code we have very simple control over the cannon. How long we hold a directional button roughly governs how far the turret moves, and we can fire with the left hand joystick button. The script sends a “Park” command on startup.

Here’s the whole thing. (Of course you can get it on Github too!)

#!/usr/bin/python

import thunder_control as thunder
from evdev import InputDevice, categorize, ecodes, KeyEvent
import os
from gopigo import *

model_b_plus = True
next_move = "down"
event_time = 0
speed = 100
gamepad = InputDevice('/dev/input/event0')

thunder.setup()

# Enable USB to supply up to 1.2A on model B+
if model_b_plus:
    os.system("gpio -g write 38 0")
    os.system("gpio -g mode 38 out")
    os.system("gpio -g write 38 1")

try:
    thunder.run_command(thunder.PARK, 1000)
    print "Parked"
    for event in gamepad.read_loop():
        if event.type == ecodes.EV_ABS:
            absevent = categorize(event)
            print ecodes.bytype[absevent.event.type][absevent.event.code], absevent.event.value
            if ecodes.bytype[absevent.event.type][absevent.event.code] == 'ABS_HAT0X':
                if absevent.event.value == -1:
                    next_move = thunder.LEFT
                    event_time = absevent.event.timestamp()
                elif absevent.event.value == 1:
                    next_move = thunder.RIGHT
                    event_time = absevent.event.timestamp()
            if ecodes.bytype[absevent.event.type][absevent.event.code] == 'ABS_HAT0Y':
                if absevent.event.value == -1:
                    next_move = thunder.UP
                    event_time = absevent.event.timestamp()
                elif absevent.event.value == 1:
                    next_move = thunder.DOWN
                    event_time = absevent.event.timestamp()
        elif event.type == ecodes.EV_SYN:
            synevent = categorize(event)
            print synevent
            syntime = synevent.event.timestamp()
            move_duration = (syntime - event_time) * 1000
            print move_duration
            thunder.run_command(next_move, move_duration)
        elif event.type == ecodes.EV_KEY:
            keyevent = categorize(event)
            if keyevent.keystate == KeyEvent.key_down:
                if keyevent.keycode[0] == 'BTN_A':
                    print "Back"
                    bwd()
                elif keyevent.keycode == 'BTN_Y':
                    print "Forward"
                    fwd()
                elif keyevent.keycode == 'BTN_B':
                    print "Right"
                    right()
                elif keyevent.keycode == 'BTN_X':
                    print "Left"
                    left()
                elif keyevent.keycode == 'BTN_THUMBR':
                    print "Stop"
                    stop()
                elif keyevent.keycode == 'BTN_THUMBL':
                    thunder.run_command(thunder.FIRE, 250)
                elif keyevent.keycode == 'BTN_TR':
                    print "Faster"
                    speed += 20
                    if speed > 255:
                        speed = 255
                    set_speed(speed)
                elif keyevent.keycode == 'BTN_TL':
                    print "Slower"
                    speed -= 20
                    if speed < 50:
                        speed = 50
                    set_speed(speed)

except KeyboardInterrupt:
    # Disable high current mode on USB before exiting
    if model_b_plus:
        os.system("gpio -g write 38 0")

At the top of the script you see some extra code (also from the original Dexter Industries script) where, if we are running on a Pi B+, we enable high current mode for the Cannon. In my experience the Cannon uses a lot of power.

The main loops is also wrapped in a try/except block that makes sure we disable high current if the script is interrupted.

That’s it for now. I apologize for the delay in posting this. There’s more coming soon, but before I return to the GoPiGo they’ll be a brief interlude with Raspberry Pi, MAME, and analog controls.

Leave a Reply