Mini Robot Cart β Part Three
This part will detail the software used.
Software:
I use PuTTY to SSH to the Pi and FileZilla to transfer files. Sublime Text to edit program script. My development platform is Windows 10.
The Raspberry Pi sets itself up as a WiFi Hot Spot. This can then be connected to via a tablet or phone (I prefer the tablet as the screen is larger). Open a web brower on the tablet and select the IP address of the Pi and the port address for the web server, 192.168.4.1:5000. (PuTTY and FileZilla use 192.168.4.1:22)
The running program is Python 3 script which spawns the Python Flask Web Server as a seperate thread.
Raspberry Pi setup:
-
SD card initial setup Current version of Raspbian loaded on SD card as a Headless install. Static IP address assigned in router for the Pi. raspi-config used to set password, host name, enable interfaces, etc Update the Pi. Install libraries.
-
Pi as a WiFi Hot Spot This can be a little tricky to set up if you are using WiFi to access the Pi. A point will come in the set up when the Pi is no longer accessiable via the static IP address assigned in the router. After a number of tries I have found it better to connect a keyboard and screen for the setup. This makes it easy to see what is going on.
-
Once the Pi is accessiable via WiFi as a Hot Spot, program development can begin. There is one Python 3 script, two camera scripts and two HTML scripts. minicart.py camerapi.py base_camera.py index.html pics.html
-
Add the following lines to the end of /etc/rc.local before exit 0 to start the program upon Pi power up or reboot. cd /home/pi python3 minicart.py
Note: If the web server is started with IP address 0.0.0.0 it will use whatever IP address has been assigned to the Pi. The following is the Python 3 code to start the web server; it is also where the port is assigned.
def server_Run():
app.run(host='0.0.0.0', port=5000, threaded=True, debug=False)
return
The following are the current versions and are provided βas isβ.
Python 3 β minicart.py:
#########################################################################
#########################################################################
# Mini Robat Cart V2.0
#
# 01 Feb 2021
# Remove play random noise output - hogs processor time
# Leave play routines for startup and shutdown and future mods
#
# Count pulses on both motors when forward or backward
# Compare and adjust motor run time to compensate
#
# Remove record of movement, not really effective
#
#
###################################################################
#########################################################################
#########################################################################
#########################################################################
#!/usr/bin/env python
import os
import time
import datetime
import base64
import RPi.GPIO as GPIO
import multiprocessing
from random import seed
from random import random
from random import randint
from subprocess import call
from importlib import import_module
from flask import Flask, render_template, request, Response
from camera_pi import Camera
#########################################################################
# Set up web application
app = Flask(__name__)
app.config["TEMPLATES_AUTO_RELOAD"] = True
#########################################################################
# Seed random number generator, uses time as seed value
seed()
#########################################################################
# Setup GPIO input pins
GPIO.setmode(GPIO.BCM)
# Setup Run LED and Light LED pins
GPIO.setup(6, GPIO.OUT) # Run LED
GPIO.setup(13, GPIO.OUT) # Light LED
# Setup Motor GPIO pins
# Right Hand side motor turns slower and grinds more than Left Hand side
GPIO.setup(19, GPIO.OUT) # Motor 1 + Right Hand side
GPIO.setup(20, GPIO.OUT) # Motor 1 -
GPIO.setup(21, GPIO.OUT) # Motor 2 + Left Hand side
GPIO.setup(26, GPIO.OUT) # Motor 2 -
pwm19 = GPIO.PWM(19,100000) # PWM frequency 100000uS, 100mS on GPIO 19
pwm20 = GPIO.PWM(20,100000) # PWM frequency 100000uS, 100mS on GPIO 21
pwm21 = GPIO.PWM(21,100000) # PWM frequency 100000uS, 100mS on GPIO 19
pwm26 = GPIO.PWM(26,100000) # PWM frequency 100000uS, 100mS on GPIO 21
#pwm19.start(100) # Duty cycle 100%
#pwm19.ChangeDutyCycle(int(50)) # Duty cycle 50%
#pwm21.start(100) # Duty cycle 100%
#pwm21.ChangeDutyCycle(int(50)) # Duty cycle 50%
#pwm19.stop()
#pwm21.stop()
# Setup Encoder GPIO pins
GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input 2
GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input 1
GPIO.setup(24, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input 3
GPIO.setup(25, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input 4
# Set run LED on, shows program is running
GPIO.output(6, GPIO.HIGH)
#########################################################################
# select sound file from list
def play_file(arg):
switcher = {
1: "Tada-sound.wav", # play on startup
2: "'Heart Rate Monitor Flatline short'.wav", # play on shutdown
3: "Birds-chirping.wav",
4: "'Cash Register Cha Ching.wav'",
5: "Cat_Meow_2.wav",
6: "church-chime.wav",
7: "'Class Bell.wav'",
8: "Clinking_Teaspoon.wav",
9: "clock-chimes.wav",
10: "cartoon-telephone.wav",
11: "ECG.wav",
12: "Falcon.wav",
13: "glass_ping.wav",
14: " Horn.wav",
15: "i-cant-wait.wav",
16: "i-dont-think-so-1.wav",
17: "i-dont-understand.wav",
18: "old-fashioned-door-bell.wav",
18: "old-fashioned-school-bell.wav",
20: "'Ship_Bell.wav'",
20: "sos-morse-code.wav",
21: "typewriter.wav",
22: "'Wake Up Call.wav'",
23: "YesFemale.wav",
24: "YesMale.wav",
25: "yes-indeed.wav",
26: "yes-please.wav",
27: "youre-almost-there.wav",
28: "youre-on-the-right-track.wav",
29: "you-wanna-go.wav"
}
file = switcher.get(arg, "YesMale.wav")
file = "aplay Music/" + file
call(file, shell=True)
return
#########################################################################
# Called when encoder interrupts, counts pulses so as to determine distance
def encoder_count1(channel):
global count1
count1 = count1 + 1
return
def encoder_count3(channel):
global count3
count3 = count3 + 1
return
#########################################################################
# Gets camera frames
def gen(camera):
global frameSave
while True:
frame = camera.get_frame()
frameSave.value = frame
yield (b'--frame\r\n'b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
#########################################################################
# Called when page is accessed
@app.route("/")
def index():
return render_template("index.html")
@app.route('/video_feed')
def video_feed():
return Response(gen(Camera()),mimetype='multipart/x-mixed-replace; boundary=frame')
#########################################################################
@app.route("/Shutdown")
def Shutdown():
global button
button.value = 'Shutdown'
return "nothing"
@app.route("/Light")
def Light():
GPIO.output(13, not GPIO.input(13))
return "nothing"
@app.route("/TakePic")
def TakePic():
button.value = 'TakePic'
return "nothing"
@app.route("/StopRec")
def Pause():
global button
button.value = 'StopRec'
return "nothing"
#########################################################################
@app.route("/<actionName>", methods=['GET', 'POST'])
def action(actionName):
global move
global diff_motor1
global diff_motor2
if actionName == 'Stop':
pwm19.stop()
pwm20.stop()
pwm21.stop()
pwm26.stop()
GPIO.output(19,GPIO.LOW)
GPIO.output(20,GPIO.LOW)
GPIO.output(21,GPIO.LOW)
GPIO.output(26,GPIO.LOW)
move.value = 'Stop'
if actionName == 'Forward':
if diff_motor1.value > 0:
pwm19.start(0)
pwm20.start(100 - diff_motor1.value)
pwm21.start(100)
pwm26.start(0)
else:
pwm19.start(0)
pwm20.start(100)
pwm21.start(100 - diff_motor2.value)
pwm26.start(0)
move.value = 'Forward'
if actionName == 'Back':
if diff_motor1.value > 0:
pwm19.start(100 - diff_motor1.value)
pwm20.start(0)
pwm21.start(0)
pwm26.start(100)
else:
pwm19.start(100)
pwm20.start(0)
pwm21.start(0)
pwm26.start(100 - diff_motor2.value)
move.value = 'Back'
if actionName == 'Right':
pwm19.start(100)
pwm20.start(0)
GPIO.output((21,26),(GPIO.LOW,GPIO.LOW))
time.sleep(0.2)
pwm19.stop()
pwm20.stop()
GPIO.output(19, GPIO.LOW)
GPIO.output(20, GPIO.LOW)
move.value = 'Stop'
if actionName == 'Left':
pwm21.start(0)
pwm26.start(100)
GPIO.output((19,20),(GPIO.LOW,GPIO.LOW))
time.sleep(0.2)
pwm21.stop()
pwm26.stop()
GPIO.output(21, GPIO.LOW)
GPIO.output(26, GPIO.LOW)
move.value = 'Stop'
if actionName == 'pics.html':
return render_template("pics.html")
return "nothing"
#########################################################################
# Run as seperate process
def server_Run():
app.run(host='0.0.0.0', port=5000, threaded=True, debug=False)
return
#########################################################################
#########################################################################
if __name__ == "__main__":
loop = 0
count1 = 0
count3 = 0
previous_move = "Stop"
PicTaken = False
picNum = 1
delay = 2
startTimeLoop = int(time.time())
nowTimeLoop = int(time.time())
# Setup event to activate when encoder pulse present, count1 = Right side, count3 = Left side
GPIO.add_event_detect(23, GPIO.RISING, callback=encoder_count1) # input 1
GPIO.add_event_detect(24, GPIO.RISING, callback=encoder_count3) # input 3
# play startup sound
play_file(1)
# Setup Global variables to indicate state of cart, Changed in web process, accessed in main
manager = multiprocessing.Manager()
move = manager.Value('c_char_p','Stop') # c_char_p is type code for string
button = manager.Value('c_char_p','StopRec')
frameSave = manager.Value('c_char_p','takepic')
diff_motor1 = manager.Value('c_int',0) # c_int is type code for integer
diff_motor2 = manager.Value('c_int',0) # c_int is type code for integer
# Start web server and audio play as seperate processes
p = multiprocessing.Process(target=server_Run)
p.start()
#########################################################################
# Main Run Loop
try:
while True:
if button.value == "StopRec":
PicTaken = False
# Saves whatever is the current image to a file. Only saves 9 pics before overwrite.
if button.value == "TakePic":
if not PicTaken:
file = "static/pic" + str(picNum) + ".jpg"
picNum = picNum + 1
if picNum > 9:
picNum = 1
print (file)
with open(file, "wb") as fh:
fh.write(frameSave.value)
PicTaken = True
button.value = "StopRec"
# Exit and power down of cart. Best way to ensure no corruption of SD card.
if button.value == "Shutdown":
p.terminate()
GPIO.cleanup() # reset GPIO pins at end of program
play_file(2) # play shutdown sound
time.sleep(0.5)
os.system("sudo poweroff")
# Determines the pulse count on each motor and adjusts the speed if necessary.
# This is an attmept to overcome the small difference in speed between the two motors.
# Meaning the cart moves in a straight line rather than veering to left or right.
nowTimeLoop = int(time.time())
if nowTimeLoop > startTimeLoop + delay: # checking is done every 2 seconds
if count1 > 500 and count3 > 500: # only check if the cart has moved some distance
if (count1 > count3) :
diff_motor1.value = int(((count1 - count3) * 100) / count3)
diff_motor2.value = 0
else:
diff_motor1.value = 0
diff_motor2.value = int(((count3 - count1) * 100) / count1)
# print (count1,count3,diff_motor1.value,diff_motor2.value) # for debugging
count1 = 0
count3 = 0
startTimeLoop = nowTimeLoop
time.sleep(0.05)
#########################################################################
# End run Loop
except KeyboardInterrupt: #crtl-C pressed on stdin
p.terminate()
GPIO.cleanup() # reset GPIO pins at end of program
play_file(2) # play shutdown sound
#########################################################################
#########################################################################
#########################################################################
#########################################################################
#########################################################################
#########################################################################
#########################################################################
#########################################################################
#########################################################################
#########################################################################
#########################################################################
#########################################################################
HTML β index.html:
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Robot Mini Cart V1.0</title>
<script src="static/jquery-3.5.1.min.js"></script>
<style>
.image_left {
position: absolute;
top: 45%;
left: 0px;
opacity: 0.7;
}
.image_right {
position: absolute;
top: 45%;
right: 0px;
opacity: 0.7;
}
.image_forward {
position: absolute;
top: 0px;
left: 45%;
opacity: 0.7;
}
.image_back {
position: absolute;
bottom: 0px;
left: 45%;
opacity: 0.7;
}
.image_light {
position: absolute;
bottom: 5px;
left: 0px;
opacity: 1;
}
.image_camera {
position: absolute;
bottom: 5px;
right: 0px;
opacity: 1;
}
.image_X {
position: absolute;
top: 0px;
right: 0px;
opacity: 1;
}
.video {
position: relative;
}
.imgVid {
max-width: 100%;
max-height: auto;
width: 100%;
height: auto;
}
.poweroff {
position: absolute;
top:47%;
left:35%;
color: white;
font-weight: bold;
font-size: 40px;
text-align: center;
text-shadow: 2px 2px 4px #000000;
}
.piccapture {
position: absolute;
top:47%;
left:35%;
color: white;
font-weight: bold;
font-size: 40px;
text-align: center;
text-shadow: 2px 2px 4px #000000;
}
.showPicsButton {
position: absolute;
left: 42%;
bottom: -80px;
color: white;
font-weight: bold;
font-size: 30px;
text-align: center;
text-shadow: 2px 2px 4px #000000;
}
</style>
</head>
<body>
<div class="video">
<img src="{{url_for('video_feed') }}" class="imgVid" onmouseover="MoveStop()" onclick="MoveStop()">
<img src="/static/Arrow_Left1.png" class="image_left" onclick="MoveLeft()">
<img src="/static/Arrow_Right1.png" class="image_right" onclick="MoveRight()">
<img src="/static/Arrow_Up1.png" class="image_forward" onclick="MoveForward()">
<img src="/static/Arrow_Down1.png" class="image_back" onclick="MoveBack()">
<img src="/static/Light.png" class="image_light" onclick="Light()">
<img src="/static/Camera.png" class="image_camera" onclick="TakePic()">
<img src="/static/X.png" class="image_X" onclick="Shutdown()">
<center>
<a href="/pics.html" id=showPics class="showPicsButton">Show Pics</a>
</center>
<div class="poweroff" ><a id=poweroff></a></div>
<div class="piccapture" ><a id=piccapture></a></div>
</div>
<script type="text/javascript" language = "javascript">
function MoveForward() { $(function() { $.getJSON('/Forward', function(data) { }); return false; }); }
function MoveBack() { $(function() { $.getJSON('/Back', function(data) { }); return false; }); }
function MoveLeft() { $(function() { $.getJSON('/Left', function(data) { }); return false; }); }
function MoveRight() { $(function() { $.getJSON('/Right', function(data) { }); return false; }); }
function MoveStop() { $(function() { $.getJSON('/Stop', function(data) { }); return false; }); }
function TakePic() {
document.getElementById("piccapture").innerHTML = "Pic Capture";
setTimeout(() => document.getElementById("piccapture").innerHTML = "", 1000);
$(function() { $.getJSON('/TakePic', function(data) { }); return false; });
}
function Light() { $(function() { $.getJSON('/Light', function(data) { }); return false; }); }
function Shutdown() {
document.getElementById("poweroff").innerHTML = "POWEROFF";
$(function() { $.getJSON('/Shutdown', function(data) { }); return false; });
}
</script>
</body>
</html>
HTML β pics.html:
<!DOCTYPE html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Robot Mini Cart V1.0</title>
<style>
.pic {
max-width: 30%;
max-height: auto;
width: 30%;
height: auto;
}
</style>
</head>
<html>
<body>
<img src="static/pic1.jpg" class="pic">
<img src="static/pic2.jpg" class="pic">
<img src="static/pic3.jpg" class="pic">
<br></br>
<img src="static/pic4.jpg" class="pic">
<img src="static/pic5.jpg" class="pic">
<img src="static/pic6.jpg" class="pic">
<br></br>
<img src="static/pic7.jpg" class="pic">
<img src="static/pic8.jpg" class="pic">
<img src="static/pic9.jpg" class="pic">
</body>
</html>
Conclusion:
To get all this working has been quite a complex process and has evolved over a number of different projects. It has been fun to develop and more so seeing the grandkids play it.