Drive your YetiBorg v2 from a smart phone or web browser
One of the best things about building your own robot is being able to control it remotely. Using YetiBorg v2 with a Raspberry Pi camera you can drive around and see what the robot sees from a smart phone, laptop, or even via the web!
In this example we will show how you can control YetiBorg v2 using a web page accessed via WiFi. This can then be controlled from any device with a web browser on the same WiFi network :)
Parts
In order to run this script you will not need much:
- A YetiBorg v2
- A Raspberry Pi camera so the robot can see
- Either a Pi Zero W or a USB WiFi dongle
- Connection to a WiFi router or phone hotspot
IP address
To talk to the YetiBorg v2 you will need to know what address to use. You can find out what your IP address is using the ifconfig
command in a terminal. It should be four numbers separated by dots, e.g. 192.168.0.198
We will need this to access the controls, so make a note of it.
Controls
The controls are fairly straight forward, one slider and some buttons:
Each of the buttons will start the robot moving in a specific direction:
- Forward → Drive straight ahead
- Reverse → Drive backwards
- Spin Left → Rotate anti-clockwise on the spot
- Spin Right → Rotate clockwise on the spot
- Turn Left → Steer towards the left
- Turn Right → Steer towards the right
- Stop → Stop moving
The slider controls what speed the YetiBorg v2 moves at, fully right is full speed. The new speed will take effect when you press a button.
Get the example
The example is part of the standard set of YetiBorg v2 examples installed during the getting started instructions: bash <(curl https://www.piborg.org/installer/install-yetiborg-v2.txt)
Run once
Go to the YetiBorg v2 code directory: cd ~/yetiborgv2
and run the script using sudo: sudo ./yeti2Web.py
Run at startup
Open /etc/rc.local to make an addition using: sudo nano /etc/rc.local
Then add this line just above the exit 0
line: sudo /home/pi/yetiborgv2/yeti2Web.py &
Finally press CTRL+O, ENTER to save the file followed by CTRL+X to exit nano. Next time you power up the Raspberry Pi it should start the script for you :)
Accessing the page
Once the script is running you can access the controls from a web browser on any other device on the same network. Replace 192.168.0.198
in the below addresses with the IP address from your YetiBorg v2:
http://192.168.0.198
- Standard controls, click to change speed or directionhttp://192.168.0.198/hold
- Press and hold controls to keep moving, may not work on all deviceshttp://192.168.0.198/stream
- Gets the video stream without any controlshttp://192.168.0.198/cam.jpg
- Single frame from the camera, you may need to force-refresh to get a new image
Full code listing - yeti2Web.py
#!/usr/bin/env python # coding: Latin-1 # Creates a web-page interface for YetiBorg v2 # Import library functions we need import ZeroBorg import time import sys import threading import SocketServer import picamera import picamera.array import cv2 import datetime # Settings for the web-page webPort = 80 # Port number for the web-page, 80 is what web-pages normally use imageWidth = 240 # Width of the captured image in pixels imageHeight = 180 # Height of the captured image in pixels frameRate = 10 # Number of images to capture per second displayRate = 2 # Number of images to request per second photoDirectory = '/home/pi' # Directory to save photos to flippedImage = True # True if the camera needs to be rotated # Global values global ZB global lastFrame global lockFrame global camera global processor global running global watchdog running = True # Setup the ZeroBorg ZB = ZeroBorg.ZeroBorg() #ZB.i2cAddress = 0x44 # Uncomment and change the value if you have changed the board address ZB.Init() if not ZB.foundChip: boards = ZeroBorg.ScanForZeroBorg() if len(boards) == 0: print 'No ZeroBorg found, check you are attached :)' else: print 'No ZeroBorg at address %02X, but we did find boards:' % (ZB.i2cAddress) for board in boards: print ' %02X (%d)' % (board, board) print 'If you need to change the I²C address change the setup line so it is correct, e.g.' print 'ZB.i2cAddress = 0x%02X' % (boards[0]) sys.exit() #ZB.SetEpoIgnore(True) # Uncomment to disable EPO latch, needed if you do not have a switch / jumper ZB.SetCommsFailsafe(False) ZB.ResetEpo() ZB.SetLed(False) # Power settings voltageIn = 8.4 # Total battery voltage to the ZeroBorg (change to 9V if using a non-rechargeable battery) voltageOut = 6.0 # Maximum motor voltage # Setup the power limits if voltageOut > voltageIn: maxPower = 1.0 else: maxPower = voltageOut / float(voltageIn) # Timeout thread class Watchdog(threading.Thread): def __init__(self): super(Watchdog, self).__init__() self.event = threading.Event() self.terminated = False self.start() self.timestamp = time.time() def run(self): timedOut = True # This method runs in a separate thread while not self.terminated: # Wait for a network event to be flagged for up to one second if timedOut: if self.event.wait(1): # Connection print 'Reconnected...' timedOut = False self.event.clear() else: if self.event.wait(1): self.event.clear() else: # Timed out print 'Timed out...' timedOut = True ZB.MotorsOff() # Image stream processing thread class StreamProcessor(threading.Thread): def __init__(self): super(StreamProcessor, self).__init__() self.stream = picamera.array.PiRGBArray(camera) self.event = threading.Event() self.terminated = False self.start() self.begin = 0 def run(self): global lastFrame global lockFrame # This method runs in a separate thread while not self.terminated: # Wait for an image to be written to the stream if self.event.wait(1): try: # Read the image and save globally self.stream.seek(0) if flippedImage: flippedArray = cv2.flip(self.stream.array, -1) # Flips X and Y retval, thisFrame = cv2.imencode('.jpg', flippedArray) del flippedArray else: retval, thisFrame = cv2.imencode('.jpg', self.stream.array) lockFrame.acquire() lastFrame = thisFrame lockFrame.release() finally: # Reset the stream and event self.stream.seek(0) self.stream.truncate() self.event.clear() # Image capture thread class ImageCapture(threading.Thread): def __init__(self): super(ImageCapture, self).__init__() self.start() def run(self): global camera global processor print 'Start the stream using the video port' camera.capture_sequence(self.TriggerStream(), format='bgr', use_video_port=True) print 'Terminating camera processing...' processor.terminated = True processor.join() print 'Processing terminated.' # Stream delegation loop def TriggerStream(self): global running while running: if processor.event.is_set(): time.sleep(0.01) else: yield processor.stream processor.event.set() # Class used to implement the web server class WebServer(SocketServer.BaseRequestHandler): def handle(self): global ZB global lastFrame global watchdog # Get the HTTP request data reqData = self.request.recv(1024).strip() reqData = reqData.split(' ') # Get the URL requested getPath = '' for line in reqData: if line.startswith('GET'): parts = line.split(' ') getPath = parts[1] break watchdog.event.set() if getPath.startswith('/cam.jpg'): # Camera snapshot lockFrame.acquire() sendFrame = lastFrame lockFrame.release() if sendFrame != None: self.send(sendFrame.tostring()) elif getPath.startswith('/off'): # Turn the drives off httpText = '<html><body><center>' httpText += 'Speeds: 0 %, 0 %' httpText += '</center></body></html>' self.send(httpText) ZB.MotorsOff() elif getPath.startswith('/set/'): # Motor power setting: /set/driveLeft/driveRight parts = getPath.split('/') # Get the power levels if len(parts) >= 4: try: driveLeft = float(parts[2]) driveRight = float(parts[3]) except: # Bad values driveRight = 0.0 driveLeft = 0.0 else: # Bad request driveRight = 0.0 driveLeft = 0.0 # Ensure settings are within limits if driveRight < -1: driveRight = -1 elif driveRight > 1: driveRight = 1 if driveLeft < -1: driveLeft = -1 elif driveLeft > 1: driveLeft = 1 # Report the current settings percentLeft = driveLeft * 100.0; percentRight = driveRight * 100.0; httpText = '<html><body><center>' httpText += 'Speeds: %.0f %%, %.0f %%' % (percentLeft, percentRight) httpText += '</center></body></html>' self.send(httpText) # Set the outputs driveLeft *= maxPower driveRight *= maxPower ZB.SetMotor1(-driveRight * maxPower) # Rear right ZB.SetMotor2(-driveRight * maxPower) # Front right ZB.SetMotor3(-driveLeft * maxPower) # Front left ZB.SetMotor4(-driveLeft * maxPower) # Rear left elif getPath.startswith('/photo'): # Save camera photo lockFrame.acquire() captureFrame = lastFrame lockFrame.release() httpText = '<html><body><center>' if captureFrame != None: photoName = '%s/Photo %s.jpg' % (photoDirectory, datetime.datetime.utcnow()) try: photoFile = open(photoName, 'wb') photoFile.write(captureFrame) photoFile.close() httpText += 'Photo saved to %s' % (photoName) except: httpText += 'Failed to take photo!' else: httpText += 'Failed to take photo!' httpText += '</center></body></html>' self.send(httpText) elif getPath == '/': # Main page, click buttons to move and to stop httpText = '<html> ' httpText += '<head> ' httpText += '<script language="JavaScript"><!-- ' httpText += 'function Drive(left, right) { ' httpText += ' var iframe = document.getElementById("setDrive"); ' httpText += ' var slider = document.getElementById("speed"); ' httpText += ' left *= speed.value / 100.0;' httpText += ' right *= speed.value / 100.0;' httpText += ' iframe.src = "/set/" + left + "/" + right; ' httpText += '} ' httpText += 'function Off() { ' httpText += ' var iframe = document.getElementById("setDrive"); ' httpText += ' iframe.src = "/off"; ' httpText += '} ' httpText += 'function Photo() { ' httpText += ' var iframe = document.getElementById("setDrive"); ' httpText += ' iframe.src = "/photo"; ' httpText += '} ' httpText += '//--></script> ' httpText += '</head> ' httpText += '<body> ' httpText += '<iframe src="/stream" width="100%" height="500" frameborder="0"></iframe> ' httpText += '<iframe id="setDrive" src="/off" width="100%" height="50" frameborder="0"></iframe> ' httpText += '<center> ' httpText += '<button onclick="Drive(-1,1)" style="width:200px;height:100px;"><b>Spin Left</b></button> ' httpText += '<button onclick="Drive(1,1)" style="width:200px;height:100px;"><b>Forward</b></button> ' httpText += '<button onclick="Drive(1,-1)" style="width:200px;height:100px;"><b>Spin Right</b></button> ' httpText += '<br /><br /> ' httpText += '<button onclick="Drive(0,1)" style="width:200px;height:100px;"><b>Turn Left</b></button> ' httpText += '<button onclick="Drive(-1,-1)" style="width:200px;height:100px;"><b>Reverse</b></button> ' httpText += '<button onclick="Drive(1,0)" style="width:200px;height:100px;"><b>Turn Right</b></button> ' httpText += '<br /><br /> ' httpText += '<button onclick="Off()" style="width:200px;height:100px;"><b>Stop</b></button> ' httpText += '<br /><br /> ' httpText += '<button onclick="Photo()" style="width:200px;height:100px;"><b>Save Photo</b></button> ' httpText += '<br /><br /> ' httpText += '<input id="speed" type="range" min="0" max="100" value="100" style="width:600px" /> ' httpText += '</center> ' httpText += '</body> ' httpText += '</html> ' self.send(httpText) elif getPath == '/hold': # Alternate page, hold buttons to move (does not work with all devices) httpText = '<html> ' httpText += '<head> ' httpText += '<script language="JavaScript"><!-- ' httpText += 'function Drive(left, right) { ' httpText += ' var iframe = document.getElementById("setDrive"); ' httpText += ' var slider = document.getElementById("speed"); ' httpText += ' left *= speed.value / 100.0;' httpText += ' right *= speed.value / 100.0;' httpText += ' iframe.src = "/set/" + left + "/" + right; ' httpText += '} ' httpText += 'function Off() { ' httpText += ' var iframe = document.getElementById("setDrive"); ' httpText += ' iframe.src = "/off"; ' httpText += '} ' httpText += 'function Photo() { ' httpText += ' var iframe = document.getElementById("setDrive"); ' httpText += ' iframe.src = "/photo"; ' httpText += '} ' httpText += '//--></script> ' httpText += '</head> ' httpText += '<body> ' httpText += '<iframe src="/stream" width="100%" height="500" frameborder="0"></iframe> ' httpText += '<iframe id="setDrive" src="/off" width="100%" height="50" frameborder="0"></iframe> ' httpText += '<center> ' httpText += '<button onmousedown="Drive(-1,1)" onmouseup="Off()" style="width:200px;height:100px;"><b>Spin Left</b></button> ' httpText += '<button onmousedown="Drive(1,1)" onmouseup="Off()" style="width:200px;height:100px;"><b>Forward</b></button> ' httpText += '<button onmousedown="Drive(1,-1)" onmouseup="Off()" style="width:200px;height:100px;"><b>Spin Right</b></button> ' httpText += '<br /><br /> ' httpText += '<button onmousedown="Drive(0,1)" onmouseup="Off()" style="width:200px;height:100px;"><b>Turn Left</b></button> ' httpText += '<button onmousedown="Drive(-1,-1)" onmouseup="Off()" style="width:200px;height:100px;"><b>Reverse</b></button> ' httpText += '<button onmousedown="Drive(1,0)" onmouseup="Off()" style="width:200px;height:100px;"><b>Turn Right</b></button> ' httpText += '<br /><br /> ' httpText += '<button onclick="Photo()" style="width:200px;height:100px;"><b>Save Photo</b></button> ' httpText += '<br /><br /> ' httpText += '<input id="speed" type="range" min="0" max="100" value="100" style="width:600px" /> ' httpText += '</center> ' httpText += '</body> ' httpText += '</html> ' self.send(httpText) elif getPath == '/stream': # Streaming frame, set a delayed refresh displayDelay = int(1000 / displayRate) httpText = '<html> ' httpText += '<head> ' httpText += '<script language="JavaScript"><!-- ' httpText += 'function refreshImage() { ' httpText += ' if (!document.images) return; ' httpText += ' document.images["rpicam"].src = "cam.jpg?" + Math.random(); ' httpText += ' setTimeout("refreshImage()", %d); ' % (displayDelay) httpText += '} ' httpText += '//--></script> ' httpText += '</head> ' httpText += '<body onLoad="setTimeout(\'refreshImage()\', %d)"> ' % (displayDelay) httpText += '<center><img src="/cam.jpg" style="width:640;height:480;" name="rpicam" /></center> ' httpText += '</body> ' httpText += '</html> ' self.send(httpText) else: # Unexpected page self.send('Path : "%s"' % (getPath)) def send(self, content): self.request.sendall('HTTP/1.0 200 OK %s' % (content)) # Create the image buffer frame lastFrame = None lockFrame = threading.Lock() # Startup sequence print 'Setup camera' camera = picamera.PiCamera() camera.resolution = (imageWidth, imageHeight) camera.framerate = frameRate print 'Setup the stream processing thread' processor = StreamProcessor() print 'Wait ...' time.sleep(2) captureThread = ImageCapture() print 'Setup the watchdog' watchdog = Watchdog() # Run the web server until we are told to close httpServer = SocketServer.TCPServer(("0.0.0.0", webPort), WebServer) try: print 'Press CTRL+C to terminate the web-server' while running: httpServer.handle_request() except KeyboardInterrupt: # CTRL+C exit print ' User shutdown' finally: # Turn the motors off under all scenarios ZB.MotorsOff() print 'Motors off' # Tell each thread to stop, and wait for them to end running = False captureThread.join() processor.terminated = True watchdog.terminated = True processor.join() watchdog.join() del camera ZB.SetLed(True) print 'Web-server terminated.'
Comments
Leave a Comment
Leave a Reply