Raspberry Pi & Python Powered Tank Part III: More Tank

Python Powered Tank Part III More Tank

This was only meant to be a two-parter, but I couldn’t leave well enough alone.

Raspberry Pi & Python Powered Tank Part III is an update with better Python code and an improved design.

I’ve gone back to fix the biggest issues that were left after Part II. I’ve omitted a bit of the mucking about in this article to keep it focused on what worked.

How Did We Get Here?

If you haven’t yet checked out the full build process, here are the previous entries on LinuxScrew.com:

More projects:

The Gun

The completed tank in all its glory!

 

Tank BBs

The Tank’s BBs – they get everywhere!

First things first, the gun now works!

It shoots these little guys. They get everywhere.

Circuitry

Tank control circuit diagram

Tank control circuit diagram. You can see how the stripboard is cut, inset

Tank wired into Raspberry Pi GPIO

Tank wired into Raspberry Pi GPIO

Here’s an improved look at what controls the motors: The pin numbers are listed in the code below.

Camera

Now using a piCamera!

The Pi Camera is lighter, so the tank can move better

The Pi Camera is lighter, so the tank can move better

Enabling the PiCamera

The PiCamera needs to be enabled before it can be used. Done so by running:

sudo raspi-config

…and navigating to:

Interface Options -> Camera

…and enabling it.

Testing the PiCamera

To avoid much frustration, here’s how to test the camera BEFORE trying to write code to access it:

raspistill -o test.jpg

If you get a test.jpg file, your camera is working. If not, the ribbon cable probably isn’t attached correctly.

Note, if you set up your Raspberry Pi OS system on a Pi without a PiCamera slot and then move to one that has one, it won’t be detected.

Code Update

The L293D library I was using was kind of not great.

  • It would throw errors when telling the motors to stop if already stopped
  • It would only sometimes be able to run both motors simultaneously, even when threaded

So, I’ve rewritten the code to control the GPIO pins directly. It turns out it’s almost less code to do it this way than using the L293D library. Who knew.

New Dependencies

There are some new dependencies for the following code, installed below:

sudo apt install python3 python3-pip python3-opencv  python3-rpi.gpio python3-flask python3-picamera

This includes the libraries for using either the PiCamera or a USB web camera with OpenCV.

The Code

# This script tested for use with Python 3
# This script starts a web server, so needs to be run as root or using sudo
# This application will be available by accessing http://<your raspberry pi ip address>:5000

from flask import Flask, Response, render_template
import cv2
import sys
import RPi.GPIO as GPIO
import io
import time
import picamera
import logging 
import os

# Set up logging
logPath = os.path.dirname(os.path.abspath(__file__)) + 'https://cd.linuxscrew.com/pitank.log' # Makes sure the log goes in the directory the script is located in
print('Logging to ' + logPath)
logging.basicConfig(filename=logPath, level=logging.DEBUG)

# Initialise Flask, the Python web server package which serves all of this up to a browser
app = Flask(__name__)

# Define global variables for future use
# Some are initialized to None so that we can confirm they have been set up later

usbCamera = None
piCam = None

# GPIO Pins connected to LD293D chips
en1 = 22 # GPIO 25
in1 = 18 # GPIO 24
in2 = 16 # GPIO 23
# out1 to left motor
# out2 to left motor

en2 = 23 # GPIO 11
in3 = 21 # GPIO 9
in4 = 19 # GPIO 10
#out3 to right motor
#out4 to right motor

#Second l293d for GUN
g_en1 = 33 # GPIO 13
g_in1 = 35 # GPIO 19
g_in2 = 37 # GPIO 26
# out1 to gun motor
# out2 to gun motor

# Use BOARD pin numbering - ie the position of the pin, not the GPIO number. This is a matter of preference.
GPIO.setmode(GPIO.BOARD)

# Link to Pi Zero pinout
# https://i.stack.imgur.com/yHddo.png

# Setup to set up camera, motors

GPIO.setup(in1, GPIO.OUT)  
GPIO.setup(in2, GPIO.OUT)
GPIO.setup(en1, GPIO.OUT)

GPIO.setup(in3, GPIO.OUT)  
GPIO.setup(in4, GPIO.OUT)
GPIO.setup(en2, GPIO.OUT)

GPIO.setup(g_en1, GPIO.OUT)  
GPIO.setup(g_in1, GPIO.OUT)
GPIO.setup(g_in2, GPIO.OUT)

# Initialise CV2 with the first available USB camera, if one is being used
# No need to comment out if not in use, it doesn't throw an error if no camera is present
usbCamera = cv2.VideoCapture(0)

# PiCamera configuration
# Values chosen to maximise refresh rate on the network
framerate = 10
res = (1024, 568)
rotation = 180
quality = 80

# Initialise PiCamera
# Be sure to comment this line out if no PiCamera is present or an error is thrown
piCam = picamera.PiCamera(framerate=framerate, resolution=res)

time.sleep(2)  # Let the camera warm up

# If camera is mounted upside down or sideways, rotate the video feed
if piCam:
    piCam.rotation = rotation

# Cleanup function to release the camera and GPIO pins ready for the next time
def cleanup():

    print('Done, cleaning up video and GPIO')
    GPIO.cleanup()
    usbCamera.release()
    cv2.destroyAllWindows()
    sys.exit("Cleaned up") # Make sure the application exits after a cleanup in case it was called due to an error

# Function to generate frames from the USB camera using the cv2 library
def generateUsbCameraFrames():  # generate frame by frame from camera

    # Ensure the global camera variable is used in this scope
    global usbCamera

    # Only try to generate frames if the camera variable has been populated
    if usbCamera:
        while True:
            # Capture frame
            success, frame = usbCamera.read()  # Reads the camera frame
            if not success:
                break
            else:
                ret, buffer = cv2.imencode('.jpg', frame)
                frame = buffer.tobytes()
                yield (b'--frame\r\n'
                    b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')  # Concat frame one by one and show result

# Function to generate frames from an attached PiCamera
def generatePiCameraFrames():

    global piCam
    global quality

    if piCam:
        while True:
            try:
                image = io.BytesIO()
                piCam.capture(image, 'jpeg', quality=quality, use_video_port=True)
                frame = image.getvalue()
            
                yield (b'--frame\r\n'
                    b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')  # Concat frame one by one and show result
            except:
                 break

# @app.route decorators tell flask which functions return HTML pages

# Video streaming route - should output the live video stream
@app.route('/video_feed')
def video_feed():
    # Comment out the video capture method you're not using
    #return Response(generateUsbCameraFrames(), mimetype='multipart/x-mixed-replace; boundary=frame')
    return Response(generatePiCameraFrames(), mimetype='multipart/x-mixed-replace; boundary=frame')

# Main/index route - the page that loads when you access the app via a browser
@app.route('/')
def index():
    return render_template('index.html') # Ensure that index.html exists in the templates subdirectory for this to work

# Control routes - these routes will be hit when the related link is pressed from index.html

@app.route('/control_stop')
def control_stop():

    # Ensure the global GPIO variables are used in this scope
    global en1
    global in1
    global in2
    global en2
    global in3
    global in4

    # Stop
    GPIO.output(en1,GPIO.LOW)
    GPIO.output(en2,GPIO.LOW)
    
    # Return an empty (successful) response regardless of what happened above
    return Response("", mimetype='text/plain')

@app.route('/control_forward')
def control_forward():

    # Ensure the global GPIO variables are used in this scope
    global en1
    global in1
    global in2
    global en2
    global in3
    global in4

    # Left Motor Forward
    GPIO.output(in1, GPIO.HIGH)
    GPIO.output(in2, GPIO.LOW)
    GPIO.output(en1, GPIO.HIGH)

    # Right Motor Forward
    GPIO.output(in3, GPIO.HIGH)
    GPIO.output(in4, GPIO.LOW)
    GPIO.output(en2, GPIO.HIGH)

    return Response("", mimetype='text/plain')

@app.route('/control_back')
def control_back():

    # Ensure the global GPIO variables are used in this scope
    global en1
    global in1
    global in2
    global en2
    global in3
    global in4

    # Left Motor Back
    GPIO.output(in1,GPIO.LOW)
    GPIO.output(in2,GPIO.HIGH)
    GPIO.output(en1,GPIO.HIGH)

    # Right Motor Back
    GPIO.output(in3,GPIO.LOW)
    GPIO.output(in4,GPIO.HIGH)
    GPIO.output(en2,GPIO.HIGH)

    return Response("", mimetype='text/plain')

@app.route('/control_turn_right')
def control_turn_right():

    # Ensure the global GPIO variables are used in this scope
    global en1
    global in1
    global in2
    global en2
    global in3
    global in4
    
    # Left Motor Forward
    GPIO.output(in1, GPIO.HIGH)
    GPIO.output(in2, GPIO.LOW)
    GPIO.output(en1, GPIO.HIGH)
    
    # Right Motor Back
    GPIO.output(in3,GPIO.LOW)
    GPIO.output(in4,GPIO.HIGH)
    GPIO.output(en2,GPIO.HIGH)

    return Response("", mimetype='text/plain')

@app.route('/control_turn_left')
def control_turn_left():

    # Ensure the global GPIO variables are used in this scope
    global en1
    global in1
    global in2
    global en2
    global in3
    global in4
    
    # Left Motor Back
    GPIO.output(in1,GPIO.LOW)
    GPIO.output(in2,GPIO.HIGH)
    GPIO.output(en1,GPIO.HIGH)
    
    # Right Motor Forward
    GPIO.output(in3, GPIO.HIGH)
    GPIO.output(in4, GPIO.LOW)
    GPIO.output(en2, GPIO.HIGH)

    return Response("", mimetype='text/plain')

@app.route('/control_fire')
def control_fire():

    global g_en1
    global g_in1
    global g_in2

    # Gun Motor Forward
    GPIO.output(g_in1, GPIO.HIGH)
    GPIO.output(g_in2, GPIO.LOW)
    GPIO.output(g_en1, GPIO.HIGH)

    # Fire the gun for 2 seconds to get some shots off
    time.sleep(2)

    # Stop
    GPIO.output(g_en1,GPIO.LOW)

    return Response("", mimetype='text/plain')

# Launch the Flask web server when this script is executed
# Catch KeyboardInterrupt so that if the application is quitting, cleanup can be run
try:

    if __name__ == '__main__': # If the script is being run directly rather than called by another script

        # Start the flask app!
        app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False)
        # App is run WITHOUT threading to reduce the chance of camera/GPIO conflict - only one concurrent user is expected, so this is fine
        # Debug is enabled so we can see what's happening in the console
        # However, the app automatically reloads when debug=True, which causes camera/GPIO conflicts, so this is disabled with use_reloader=false
        # This application will be available by accessing http://<your raspberry pi ip address>:5000

except KeyboardInterrupt:

    pass

finally:

    # Ensure cleanup on exit
    cleanup()

# End of file!

You’ll also need the following in the file index.html in a folder called templates:

<!doctype html>
<html lang="en">
<head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>PyTank!</title>


    <style>
        
         html, body{
             width: 100%;
             height: 100%;
             margin: 0;
             padding: 0;
             background-color: #000;
         }
         .feed {
             width: 100%;
             height: 100%;
           object-fit: contain;
         }
        
         .nav {
             opacity: 0.75;
             font-size: 5em;
             max-width: 100%;
             position: absolute;
             bottom: 5px;
             right: 5px;
         }
         a {
         	text-decoration: none;
         }
         
    </style>
    
</head>
<body>

    <img class="feed" src="{{ url_for('video_feed') }}">
    
    <div class="nav">
        
        <a href="#" onclick="fetch('{{ url_for('control_forward') }}')">F</a>
        <a href="#" onclick="fetch('{{ url_for('control_back') }}')">B</a>
        <a href="#" onclick="fetch('{{ url_for('control_turn_left') }}')">L</a>
        <a href="#" onclick="fetch('{{ url_for('control_turn_right') }}')">R</a>
        <a href="#" onclick="fetch('{{ url_for('control_stop') }}')">S</a>
        <a href="#" onclick="fetch('{{ url_for('control_fire') }}')">G</a>

    </div>

</body>
</html>

The code editor here can’t handle emoji – replace the link text for those last HTML links with emoji of your choice for pretty control buttons.

Starting the Remote Control Server on Boot

To start up the remote control interface on boot rather than having to SSH into the pi, add it to the root crontab by running:

sudo crontab -e

And adding this line to the end:

@reboot python3 /home/pi/pitank/tank_remote.py

This will start the server every time the Pi boots – just make sure the path at the end of the line points to your python file.

WiFi

I was going to set up the Pi as a wifi hot spot so that a tablet or phone could connect to it for remote control, but it would cause more trouble than it’s worth as it means the Pi would no longer have internet access for updating the software (Or running an IRC server from a tank because why not).

Instead, I’ve enabled the WiFi hotspot on my phone and set up the Pi to connect to that. Road warrior!

New Interface

Here’s what the new interface looks like:

The new remote control interface

The new remote control interface – Yes, it’s a mockup, I forgot to screenshot it, but it does look like this.

In the Field

Trophy skull

Trophy skull

Misc Notes

  • The PiCamera is laggier than using a webcam but much lighter. Choose one based on your preference (or if you have a better RC vehicle that can tow more weight)
  • Due to this and the general slowness of the PiZero (encoding and transmitting the image takes time), video responsiveness wasn’t a priority – code simplicity was.
    • If you were building something really fancy, you could send the video feed over its own radio channel with faster hardware or something.
  • With this in mind, you could always modify the code so the tank only moves a set distance on each command so it can’t run away.
  • If you have a better, faster (but still simple! no big includes – people need to understand what they’re coding when following this!) way to stream the vid, let me know!
  • This tank is still the ULTIMATE DESTROYER OF AA BATTERIES.
  • If another client visits the remote control web page, the feed on the first stops. This is OK; I only expect one user at a time.
  • I’ve left the Flask server running with the video feed open in a browser for a while, and it doesn’t seem to crash, which is nice.
Tank close up

Tank close up

More

Here are some of my previous projects for LinuxScrew:

I’ve got more junk, more ideas (Roboduck…), and lots more solder to burn through, so follow LinuxScrew and myself on Twitter if you’re keen for more attempts to put computers inside of things.

Probably the last you'll see of the Python/Pi tank

Probably the last you’ll see of the Python/Pi tank.

SHARE:
nv-author-image

Brad Morton

I'm Brad, and I'm nearing 20 years of experience with Linux. I've worked in just about every IT role there is before taking the leap into software development. Currently, I'm building desktop and web-based solutions with NodeJS and PHP hosted on Linux infrastructure. Visit my blog or find me on Twitter to see what I'm up to.

Leave a Reply

Your email address will not be published. Required fields are marked *