Core Electronics Forum

Mini Robot Cart - Part Three

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:

  1. 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.

  2. 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.

  3. 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

  4. 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.

2 Likes

Hey Jim,

This is an awesome project! The detail on this is brilliant and hopefully some other makers can take advantage of this to get started with their projects, if you’d like you can submit it to be added to the site for a store credit reward according to the procedure listed at the link below by hitting the share your project link. Thanks for sharing this with the community! :grin: