Saturday, October 31, 2015

Raspberry Pi Wifi Rover on Dagu Rover 5 Chassis




Overview

This is the second revision of my Wifi controlled rover. The first used an old Android phone and an IOIO board as a way to learn some Android programming and figure out how to control a vehicle over the network. It worked pretty well - a friend drove it across the internet from 40 miles away, and I learned a lot.

I did figure out that I didn't like Android for a robotics platform, since it is so highly optimized for GUI use. Keeping a program running at high priority in the background on an Android device isn't trivial, because it's not designed for that. You have to assume your program can be interrupted and resumed at any time. I decided that even though the phone came with all sorts of cool sensors and was extremely compact, I wanted the control over what was happening that comes with Linux.

My intent is to use this rover as a testbed for systems I will eventually install in an underwater remotely operated vehicle. It's also a lot of fun to drive. I wanted to document the overall design here - it has been done lots and lots of times, but a detailed writeup might be useful to someone.

So here we go. This is intended to show you one possible way to do it, and the thought processes that went into the design. It can all be improved.

Hardware





Dagu Rover 5 chassis. This thing is pretty awesome, but I have a problem with mine shedding tracks occasionally that I have not been able to fix. I understand this was fixed in later versions than mine.

A motor controller capable of handling the current of all four motors. The stall current on each motor is 2.5A, and there are four of them.

A Raspberry Pi B+ and camera module and wifi dongle. Probably going to upgrade the dongle to something with a real antenna soon, as range is limited.

A 2200 mah LiPO battery from my quadcopter, providing 12.6V.  LiPo batteries are a signifcant fire hazard if not handled properly. The rover currently has no method to automatically kill power when pack voltage drops - consider a safer battery chemistry, like NiMH, if you are unfamiliar with the risks inherent in big unprotected LiPo packs.  An excellent guide is here.

At the very least, a low voltage alarm like those commonly used in RC aircraft is a must.

A battery eliminator circuit like those used in RC aircraft to generate a nice steady 5 volts from the 12.6V LiPo pack to power the Raspberry Pi and motor board.

Network Design


The rover starts up an access point and also starts two servers on the Raspberry Pi. Each listens on a different port. Full details on the software configuration are below.

An Android device connects to the access point and is issued an IP address. The IP address of the rover is fixed - it's acting just like a router for your home internet. This makes it very easy to take the router somewhere and run it with no additional infrastructure. However, it makes it harder to run over the internet. If that's your goal, it's better to just connect your rover to an existing Wifi network so that traffic can be routed to it from anywhere. I may add a switch later that allows me to flip between these modes.

My ROV will be designed to take to places with no infrastructure, and I wanted to be able to easily take the rover to show friends, so I chose to make the rover the access point.

I currently have the rover configured to act as an access point and hand out IP addresses in the 192.168.42.x range with no DNS or default gateway. The rover itself is on 192.168.42.1.

Software Design

A traditional robotics paradigm is the "Sense, Think, Act" cycle. A robot takes input from it's sensors, performs processing on them to try to identify the best course of action, and then commands the robot's actuators to do something. The process then repeats.



We're not building a robot in the typical sense. That's because a human is in the loop, making the decisions based on sensor input. I wanted to make sure that the platform could be used as a robot, just by changing the software on the server, but right now I'm interested in building a reasonably robust remotely operated vehicle rather than something autonomous.

On reflection, I decided that a remotely operated vehicle can follow the same sense-think-act cycle. The primary difference is that the thinking is done off-vehicle, by the human operator.




I wanted to be able to send back sensor data from the rover, such as video, voltage levels, accelerometers, GPS data, etc. and display them on a simple console. So on the network, the command traffic would look like:

rover sends current sensor data to console
console sends back commands (turn on motors, etc)
repeat

Video would be handled on a separate connection, on it's own thread.



Currently, I'm not sending back any data from sensors. I will detail plans for that in the "Next Steps" below.

The server sets the appropriate IO pins, which drives the motor controller board. My rover has 4 motors, each controlled by a direction line and an enable line.

If the timeout value is exceeded, the server shuts down the motors, resets and waits for another connection,

The Python program at the end of this post implements this. Sending the full string defining the direction is horrendously inefficient - in the next revision of the client program I'll reduce that to, say, a single character. I originally did it this way to aid in debugging the client, and never got around to fixing it.

Client Program

The client I wrote is fairly simple. It rapidly makes HTTP requests to get an updated JPEG image from the rover, and updates the screen. A separate thread sends commands and gets a fake sensor value back. It attempts to reconnect when the connection is lost.

Doing the video this was is crude and eats a lot of network bandwidth compared to something like H.264, but it's easy to implement and actually works pretty well at 320x240 and 640x480.



Low(er) Lag Video Streaming on the Raspberry Pi

There are a number of tutorials for using the raspi-still command to grab a still frame and shove it across the network via a couple methods. These work well for a stream that can tolerate a lag, but it results in a delay of up to a second and the framerates are low. This is due to an inherent delay in the raspi-still program - it's not designed for that.

I got much better results using the Video for Linux (V4L) driver and MJPG-Streamer.It took some doing - you first have to compile the V4L driver. Good instructions are available here and here.

I ran into a problem getting mine to compile. I got an error, "undefined reference to symbol 'clock_gettime'". The solution was found here.

A great tutorial for compiling mjpg-streamer is here.

While compiling mjpg-streamer, I ran into a kernel specific problem with kernel version 3.18 . The solution was found here.

I use this command to launch the video server on port 6001.


/usr/local/bin/mjpg_streamer -i "/usr/local/lib/input_uvc.so -n -f 15 -q 80 -r 320x240" -o "/usr/local/lib/output_http.so -p 6001 -w /usr/local/www"


Access Point Configuration

One way to turn your Raspberry Pi into an access point is to use hostapd and dhcpd.

The Edimax WiFi dongle is not supported by the stock hostapd binary that you get with apt-get install. Dave Conroy has figured out how to make it work - he has a great document describing the process here (starts at the Prerequisites section). I used that to get it working, and some of the configuration options described on Adafruit's tutorial. My dhcpd.conf  file and hostapd.conf file are below.

dhcpd.conf:

#
# Sample configuration file for ISC dhcpd for Debian
#
#

# The ddns-updates-style parameter controls whether or not the server will
# attempt to do a DNS update when a lease is confirmed. We default to the
# behavior of the version 2 packages ('none', since DHCP v2 didn't
# have support for DDNS.)
ddns-update-style none;

# option definitions common to all supported networks...
#option domain-name "example.org";
#option domain-name-servers ns1.example.org, ns2.example.org;

default-lease-time 600;
max-lease-time 7200;

# If this DHCP server is the official DHCP server for the local
# network, the authoritative directive should be uncommented.
authoritative;

# Use this to send dhcp log messages to a different log file (you also
# have to hack syslog.conf to complete the redirection).
log-facility local7;

# No service will be given on this subnet, but declaring it helps the 
# DHCP server to understand the network topology.

#subnet 10.152.187.0 netmask 255.255.255.0 {
#}

# This is a very basic subnet declaration.

#subnet 10.254.239.0 netmask 255.255.255.224 {
#  range 10.254.239.10 10.254.239.20;
#  option routers rtr-239-0-1.example.org, rtr-239-0-2.example.org;
#}

# This declaration allows BOOTP clients to get dynamic addresses,
# which we don't really recommend.

#subnet 10.254.239.32 netmask 255.255.255.224 {
#  range dynamic-bootp 10.254.239.40 10.254.239.60;
#  option broadcast-address 10.254.239.31;
#  option routers rtr-239-32-1.example.org;
#}

# A slightly different configuration for an internal subnet.
#subnet 10.5.5.0 netmask 255.255.255.224 {
#  range 10.5.5.26 10.5.5.30;
#  option domain-name-servers ns1.internal.example.org;
#  option domain-name "internal.example.org";
#  option routers 10.5.5.1;
#  option broadcast-address 10.5.5.31;
#  default-lease-time 600;
#  max-lease-time 7200;
#}

# Hosts which require special configuration options can be listed in
# host statements.   If no address is specified, the address will be
# allocated dynamically (if possible), but the host-specific information
# will still come from the host declaration.

#host passacaglia {
#  hardware ethernet 0:0:c0:5d:bd:95;
#  filename "vmunix.passacaglia";
#  server-name "toccata.fugue.com";
#}

# Fixed IP addresses can also be specified for hosts.   These addresses
# should not also be listed as being available for dynamic assignment.
# Hosts for which fixed IP addresses have been specified can boot using
# BOOTP or DHCP.   Hosts for which no fixed address is specified can only
# be booted with DHCP, unless there is an address range on the subnet
# to which a BOOTP client is connected which has the dynamic-bootp flag
# set.
#host fantasia {
#  hardware ethernet 08:00:07:26:c0:a5;
#  fixed-address fantasia.fugue.com;
#}

# You can declare a class of clients and then do address allocation
# based on that.   The example below shows a case where all clients
# in a certain class get addresses on the 10.17.224/24 subnet, and all
# other clients get addresses on the 10.0.29/24 subnet.

#class "foo" {
#  match if substring (option vendor-class-identifier, 0, 4) = "SUNW";
#}

#shared-network 224-29 {
#  subnet 10.17.224.0 netmask 255.255.255.0 {
#    option routers rtr-224.example.org;
#  }
#  subnet 10.0.29.0 netmask 255.255.255.0 {
#    option routers rtr-29.example.org;
#  }
#  pool {
#    allow members of "foo";
#    range 10.17.224.10 10.17.224.250;
#  }
#  pool {
#    deny members of "foo";
#    range 10.0.29.10 10.0.29.230;
#  }
#}

subnet 192.168.42.0 netmask 255.255.255.0 {
 range 192.168.42.10 192.168.42.50;
 option broadcast-address 192.168.42.255;
 option routers 192.168.42.1;
 default-lease-time 600;
 max-lease-time 7200;
 option domain-name "local";
 option domain-name-servers 8.8.8.8, 8.8.4.4;
}


hostapd.conf:

interface=wlan0
driver=rtl871xdrv
ssid=Rover002
hw_mode=g
#  this enables the 802.11n speeds and capabilities
ieee80211n=1
wmm_enabled=1
country_code=US
ieee80211d=1
channel=4
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_passphrase=SECRETPASSWORD
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
The following commands are in a small script, /home/pi/startap, to start the dhcp server and hostapd. sudo service isc-dhcp-server start sudo hostapd /etc/hostapd/hostapd.conf &

Automatic startup

There are a number of ways to do this, but I decided the simplest way was to make small scripts to start each subsystem and then launch the from /etc/rc.local. I appended these commands to /etc/rc.local:

/home/pi/startAP &
/home/pi/controlServer.py &
/home/pi/startVidServer &

Next Steps

I intend to add an Arduino that can communicate via USB to gather sensor data such as pack voltage. Ideally, the Arduino could control power to the Raspberry Pi to allow a complete shutdown. Even if you shut down the Raspberry Pi via a shutdown -h, it will still draw significant power while halted. That's not good - you need to be able to kill power when the pack is dead. I intend to design this and test it prior to using it in the ROV.

It needs some big honkin' bright lights. Just because.

A Sharp IR sensor or ultrasonic range finder would be cool to have and would allow for simple autonomous behavior, as well as being useful to a human operator.

Control server code

#!/usr/bin/env python

##example command set: true,stop,75
import socket
import sys
import traceback
import time
import syslog
import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.OUT)
GPIO.setup(23, GPIO.OUT)
GPIO.setup(24, GPIO.OUT)
GPIO.setup(22, GPIO.OUT)
GPIO.setup(27, GPIO.OUT)

pwm = GPIO.PWM(18, 1000)
pwm.start(0)

##pin 22 = back left, true = reverse
##pin 23 = front left, true = reverse
##pin 24 = back right, true = reverse
##pin 27 = front right, true = reverse

value = 0

syslog.syslog('Rover: Server starting....')

host = ''
port = 6000
backlog = 1
size = 4096
count = 0

while 1:
 syslog.syslog("Rover: Waiting for connection...")
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        s.bind((host,port))
        s.listen(backlog)

        client, address = s.accept()
 s.settimeout(1.0)
 client.settimeout(1.0)

        syslog.syslog("Rover: Got client connection...")
 count = 0
 while (1):
  try:
   clientReq = client.recv(size)
   except:
   syslog.syslog("Rover: Socket error")
   pwm.ChangeDutyCycle(0)
   break

  if (clientReq == ""):
   syslog.syslog("Rover: Connection broken.")
   pwm.ChangeDutyCycle(0)
   break

  parsedCommands = clientReq.split(',')
  ##parsedCommands[0] is motorEnabled
  ##parsedCommands[1] is direction
  ##stop
  ##forward
  ##rotateRight
  ##rotateLeft
  ##reverse
  ##parsedCommands[2] is integer 0-100 representing throttle

  pwm.ChangeDutyCycle(0)
  GPIO.output(22,GPIO.LOW)
  GPIO.output(23,GPIO.LOW)
  GPIO.output(24,GPIO.LOW)
  GPIO.output(27,GPIO.LOW)

  if (parsedCommands[1] == "forward"):
   value = int(parsedCommands[2])
   pwm.ChangeDutyCycle(value)
   GPIO.output(22,GPIO.LOW)
   GPIO.output(23,GPIO.LOW)
   GPIO.output(24,GPIO.LOW)
   GPIO.output(27,GPIO.LOW)

  if (parsedCommands[1] == "reverse"):
   value = int(parsedCommands[2])
   pwm.ChangeDutyCycle(value)
   GPIO.output(22,GPIO.HIGH)
   GPIO.output(23,GPIO.HIGH)
   GPIO.output(24,GPIO.HIGH)
   GPIO.output(27,GPIO.HIGH)

  if (parsedCommands[1] == "rotateRight"):
   value = int(parsedCommands[2])
   pwm.ChangeDutyCycle(value)
   GPIO.output(22,GPIO.LOW)
   GPIO.output(23,GPIO.LOW)
   GPIO.output(24,GPIO.HIGH)
   GPIO.output(27,GPIO.HIGH)

  if (parsedCommands[1] == "rotateLeft"):
   value = int(parsedCommands[2])
   pwm.ChangeDutyCycle(value)
   GPIO.output(22,GPIO.HIGH)
   GPIO.output(23,GPIO.HIGH)
   GPIO.output(24,GPIO.LOW)
   GPIO.output(27,GPIO.LOW)

  response = str(5) + "\n"
         client.send(response)
  count = count + 1
        syslog.syslog("Rover: Shutting down server socket.")
 s.shutdown(1)
        s.close()
 pwm.ChangeDutyCycle(0)