Showing posts with label robot. Show all posts
Showing posts with label robot. Show all posts

Friday, March 17, 2017

HC-SR04 Ultrasonic Sensor Python Class for Raspberry Pi

The HC-SR04




The HC - SR04 ultrasonic ranging module provides 2cm - 400cm non-contact
measurement, with ranging accuracy up to 3mm. The module includes ultrasonic transmitters, receiver and control circuitry. The time difference between transmission and reception of ultrasonic signals is calculated. Using the speed of sound and ‘Speed = Distance/Time‘ equation, the distance between the source and target can be easily calculated.

Credit to Vivek and his article on the same subject for the diagrams.




Wiring the HC-SR04 to a Raspberry Pi


The module has 4 pins:

  • VCC - 5V Supply
  • TRIG - Trigger Pulse Input
  • ECHO - Echo Pulse Output
  • GND - 0V Ground 

Wiring is straight forward with one exception, note that the sensor operates at 5V not the 3.3V of the Raspberry Pi. Connecting the ECHO pulse pin directly to the Raspberry Pi would be a BAD idea and could damage the Pi. We need to use a voltage divider or a logic level converter module to drop the logic level from the HC-SR04 to a maximum of 3.3V. Current draw for the sensor is 15 mA.

As we have a spare logic level converter, we will use that. Connections for the logic converter are shown below.


For the voltage divider option: Vout = Vin x R2/(R1+R2) = 5 x 10000/(4700 + 10000) = 3.4V






Python Class for the HC-SR04 Ultrasonic Sensor



To utilise the HC-SR04:

  1. Provide a trigger signal to TRIG input, it requires a HIGH signal of at least 10μS duration.
  2. This enables the module to transmit eight 40KHz ultrasonic bursts.
  3. If there is an obstacle in-front of the module, it will reflect those ultrasonic waves
  4. If the signal comes back, the ECHO output of the module will be HIGH for a duration of time taken for sending and receiving ultrasonic signals. The pulse width ranges from 150μS to 25mS depending upon the distance of the obstacle from the sensor and it will be about 38ms if there is no obstacle.
  5. Obstacle distance = (high level time × velocity of sound (343.21 m/s at sea level and 20°C) / 2
  6. Allow at least 60 ms between measurements.





Time taken by the pulse is actually for return travel of the ultrasonic signals. Therefore Time is taken as Time/2.

Distance = Speed * Time/2

Speed of sound at sea level = 343.21 m/s or 34321 cm/s

Thus, Distance = 17160.5 * Time (unit cm).

As we are using the ultrasonic sensor with our Raspberry Pi robot, we have created a python class that can be easily imported and used. Note the calibration function which can be used to help correct for things like altitude and temperature.

We have included a simple low pass filter function which is equivalent to an exponentially weighted moving average. This is useful for smoothing the distance values returned from the sensor. The higher the value of beta, the greater the smoothing.

#!/usr/bin/python
# RS_UltraSonic.py - Ultrasonic Distance Sensor Class for the Raspberry Pi 
#
# 15 March 2017 - 1.0 Original Issue
#
# Reefwing Software
# Simplified BSD Licence - see bottom of file.

import RPi.GPIO as GPIO
import os, signal

from time import sleep, time

# Private Attributes
__CALIBRATE      = "1"
__TEST           = "2"
__FILTER         = "3"
__QUIT           = "q"

class UltraSonic():
    # Ultrasonic sensor class 
    
    def __init__(self, TRIG, ECHO, offset = 0.5):
        # Create a new sensor instance
        self.TRIG = TRIG
        self.ECHO = ECHO
        self.offset = offset                             # Sensor calibration factor
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.TRIG, GPIO.OUT)                  # Set pin as GPIO output
        GPIO.setup(self.ECHO, GPIO.IN)                   # Set pin as GPIO input

    def __str__(self):
        # Return string representation of sensor
        return "Ultrasonic Sensor: TRIG - {0}, ECHO - {1}, Offset: {2} cm".format(self.TRIG, self.ECHO, self.offset)

    def ping(self):
        # Get distance measurement
        GPIO.output(self.TRIG, GPIO.LOW)                 # Set TRIG LOW
        sleep(0.1)                                       # Min gap between measurements        
        # Create 10 us pulse on TRIG
        GPIO.output(self.TRIG, GPIO.HIGH)                # Set TRIG HIGH
        sleep(0.00001)                                   # Delay 10 us
        GPIO.output(self.TRIG, GPIO.LOW)                 # Set TRIG LOW
        # Measure return echo pulse duration
        while GPIO.input(self.ECHO) == GPIO.LOW:         # Wait until ECHO is LOW
            pulse_start = time()                         # Save pulse start time

        while GPIO.input(self.ECHO) == GPIO.HIGH:        # Wait until ECHO is HIGH
            pulse_end = time()                           # Save pulse end time

        pulse_duration = pulse_end - pulse_start 
        # Distance = 17160.5 * Time (unit cm) at sea level and 20C
        distance = pulse_duration * 17160.5              # Calculate distance
        distance = round(distance, 2)                    # Round to two decimal points

        if distance > 2 and distance < 400:              # Check distance is in sensor range
            distance = distance + self.offset
            print("Distance: ", distance," cm")
        else:
            distance = 0
            print("No obstacle")                         # Nothing detected by sensor
        return distance

    def calibrate(self):
        # Calibrate sensor distance measurement
        while True:
            self.ping()
            response = input("Enter Offset (q = quit): ")
            if response == __QUIT:
                break;
            sensor.offset = float(response)
            print(sensor)
            
    @staticmethod
    def low_pass_filter(value, previous_value, beta):
        # Simple infinite-impulse-response (IIR) single-pole low-pass filter.
        # ß = discrete-time smoothing parameter (determines smoothness). 0 < ß < 1
        # LPF: Y(n) = (1-ß)*Y(n-1) + (ß*X(n))) = Y(n-1) - (ß*(Y(n-1)-X(n)))
        smooth_value = previous_value - (beta * (previous_value - value))
        return smooth_value
        

def main():
    sensor = UltraSonic(8, 7)       # create a new sensor instance on GPIO pins 7 & 8
    print(sensor)

    def endProcess(signum = None, frame = None):
        # Called on process termination. 
        if signum is not None:
            SIGNAL_NAMES_DICT = dict((getattr(signal, n), n) for n in dir(signal) if n.startswith('SIG') and '_' not in n )
            print("signal {} received by process with PID {}".format(SIGNAL_NAMES_DICT[signum], os.getpid()))
        print("\n-- Terminating program --")
        print("Cleaning up GPIO...")
        GPIO.cleanup()
        print("Done.")
        exit(0)

    # Assign handler for process exit
    signal.signal(signal.SIGTERM, endProcess)
    signal.signal(signal.SIGINT, endProcess)
    signal.signal(signal.SIGHUP, endProcess)
    signal.signal(signal.SIGQUIT, endProcess)

    while True:
        action = input("\nSelect Action - (1) Calibrate, (2) Test, or (3) Filter: ")

        if action == __CALIBRATE:
            sensor.calibrate()
        elif action == __FILTER:
            beta = input("Enter Beta 0 < ß < 1 (q = quit): ")
            filtered_value = 0
            if beta == __QUIT:
                break;
            while True:
                filtered_value = sensor.low_pass_filter(sensor.ping(), filtered_value, float(beta))
                filtered_value = round(filtered_value, 2)
                print("Filtered: ", filtered_value, " cm")
        else:
            sensor.ping()

if __name__ == "__main__":
    # execute only if run as a script
    main()

## Copyright (c) 2017, Reefwing Software
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are met:
##
## 1. Redistributions of source code must retain the above copyright notice, this
##   list of conditions and the following disclaimer.
## 2. Redistributions in binary form must reproduce the above copyright notice,
##   this list of conditions and the following disclaimer in the documentation
##   and/or other materials provided with the distribution.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
## ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
## DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
## ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
## LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
## SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.



Wednesday, March 15, 2017

Controlling the Raspberry Pi via a web browser

Web Controlled Robot





Now that we can stream video to a web page it would be nice to be able to remotely control our robot. To do this we will us the Raspberry Pi to run a web server that serves the page used to control the robot. Once we have this up and running you will be able to drive your robot around using a browser on your laptop via WiFi on your LAN.

As shown in the previous post, you can use the python command print(server) to see what URL you need to point your browser at to see the video and control your robot. The way the controls work is as follows:
  1. Typing the address of your Pi served page (e.g. http://192.168.0.9:8082) into your browser will send a web request to the python program running the server, in our case RS_Server.py.
  2. RS_Server responds with the contents of index.html. Your browser renders this HTML and it appears in your browser.
  3. The broadcasting of video data is handled by the broadcast thread object in RS_Sever. The BroadcastThread class implements a background thread which continually reads encoded MPEG1 data from the background FFmpeg process started by the BroadcastOutput class and broadcasts it to all connected websockets. More detail on this can be found at pistreaming if you are interested. Basically the camera is continually taking photos, converting them to MPEG's and sending them at the frame rate to a canvas in your browser.
  4. You will see below that we have modified the index.html file to display a number of buttons to control our robot. Pressing one of these buttons will send a GET request to the server running on your Pi with a parameter of "command" and the value of the button pressed. We then handle the request by passing on the appropriate command to our MotorControl class. To do this we will need to bring together RS_Server and RS_MotorControl in our new RS_Robot class.

Modifying index.html



The index.html file provided by pistreaming just creates a canvas in which to display our streaming video. To this we will add a table with 9 command control buttons for our robot. You could get away with only 5 (Forward, Back, Left, Right and Stop) but looking ahead we know we will also need 4 more (speed increase, speed decrease, auto and manual). Auto and Manual will toggle between autonomous control and remote control (i.e. via the browser). Associated with each button is a JavaScript script that will send the appropriate command when the button is clicked.

In addition to controlling your robot via the on screen buttons you can use the keyboard. We have mapped the following functionality:

Up Arrow      = Forward
Down Arrow = Back
Left Arrow    = Left
Right Arrow  = Right
Space             = Stop
-                     = Decrease Speed
+                    = Increase Speed
m                   = Manual
a                    = Autonomous

You can modify the index.html to map whatever keybindings you want. Be aware that the keycode returned by different browsers isn't always consistent. You can use the JavaScript Event KeyCode Test Page to find out what key code your browser returns for different keys.

The manual and auto modes don't do anything at this stage. 

The modified index.html file is shown below.

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=${WIDTH}, initial-scale=1"/>
    <title>Alexa M</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js" type="text/javascript" charset="utf-8"></script>

    <style>
        .controls {
            width: 150px;
            font-size: 22pt;
            text-align: center;
            padding: 15px;
            background-color: green;
            color: white;
        }
    </style>

    <style type="text/css">
            body {
                background: ${BGCOLOR};
                text-align: center;
                margin-top: 2%;
            }
            #videoCanvas {
                // Always stretch the canvas to 640x480, regardless of its internal size.
                width: ${WIDTH}px;
                height: ${HEIGHT}px;
            }
    </style>

    <script>
    function sendCommand(command)
    {
        $.get('/', {command: command});
    }
    
    function keyPress(event)
    {
        keyCode = event.keyCode;
        
        switch (keyCode) {
            case 38:                // up arrow
                sendCommand('f');
                break;
            case 37:                // left arrow
                sendCommand('l');
                break;
            case 32:                // space
                sendCommand('s');
                break;
            case 39:                // right arrow
                sendCommand('r');
                break;
            case 40:                // down arrow
                sendCommand('b');
                break;
            case 109:               // - = decrease speed
            case 189:
                sendCommand('-');
                break;
            case 107:
            case 187:
                sendCommand('+');   // + = increase speed
                break;
            case 77: 
                sendCommand('m');   // m = manual (remote control)
                break;
            case 65:
                sendCommand('a');   // a = autonomous
                break;
            default: return;        // allow other keys to be handled
        }
        
        // prevent default action (eg. page moving up/down with arrow keys)
        event.preventDefault();
    }
    $(document).keydown(keyPress);
    </script>
</head>

<body>

    <h1><FONT color=white>Alexa M</h1>

    <!-- The Canvas size specified here is the "initial" internal resolution. jsmpeg will
        change this internal resolution to whatever the source provides. The size the
        canvas is displayed on the website is dictated by the CSS style.
    -->
    <canvas id="videoCanvas" width="${WIDTH}" height="${HEIGHT}">
        <p>
            Please use a browser that supports the Canvas Element, like
            <a href="http://www.google.com/chrome">Chrome</a>,
            <a href="http://www.mozilla.com/firefox/">Firefox</a>,
            <a href="http://www.apple.com/safari/">Safari</a> or Internet Explorer 10
        </p>
    </canvas>
    <script type="text/javascript" src="jsmpg.js"></script>
    <script type="text/javascript">
        // Show loading notice
        var canvas = document.getElementById('videoCanvas');
        var ctx = canvas.getContext('2d');
        ctx.fillStyle = '${COLOR}';
        ctx.fillText('Loading...', canvas.width/2-30, canvas.height/3);
        // Setup the WebSocket connection and start the player
        var client = new WebSocket('ws://${ADDRESS}/');
        var player = new jsmpeg(client, {canvas:canvas});
    </script>

    <table align="center">
    <tr><td  class="controls" onClick="sendCommand('-');">-</td>
        <td  class="controls" onClick="sendCommand('f');">Forward</td>
        <td  class="controls" onClick="sendCommand('+');">+</td>
    </tr>
    <tr><td  class="controls" onClick="sendCommand('l');">Left</td>
        <td  class="controls" onClick="sendCommand('s');">Stop</td>
        <td  class="controls" onClick="sendCommand('r');">Right</td>
    </tr>
    <tr><td  class="controls" onClick="sendCommand('m');">Manual</td>
        <td  class="controls" onClick="sendCommand('b');">Back</td>
        <td  class="controls" onClick="sendCommand('a');">Auto</td>
    </tr>
    </table>

</body>
</html>

Python Robot Class


As Alexa M continues to evolve, so too will this robot class. For now we can keep things pretty simple. In addition to creating a robot class we have updated the motor control, servo and server classes. Rather than reproduce all the code, we will provide links to our Gist Repository where you can download the latest versions. For completeness, I will also provide links to the HTML and JavaScript library that you will need. All these files need to be in the same directory.

  1. RS_Robot.py version 1.0 - Run this script on your Pi to create a telepresence rover.
  2. RS_Server.py version 1.1 - Updated to include command parsing.
  3. RS_MotorControl.py version 1.1 - New motor control methods.
  4. RS_Servo.py version version 1.2 - License added.
  5. index.html version 1.0 - The file shown in the previous section.
  6. jsmpg.js - Dominic Szablewski's Javascript-based MPEG1 decoder.
That completes the remote control and video streaming portion of the design. We hope you have as much fun driving around your robot as we do. Next up we will look at battery monitoring and autonomous control of the robot.

Saturday, March 4, 2017

Raspberry Pi Motor Board Python Class


Overview


We have made some additional changes to the Seeed Raspberry Pi Motor Board class. An obvious missing method is a way to change the speed of the motors. You can of course just change the duty attribute but this will only take effect the next time you change direction. So we have added a speed(duty) method. This will assign the new duty cycle and change the duty cycle of any motors which are already moving.

Note that in the Motor() class provided in the previous post:

def Stop():

should be:

def Stop(self):

Motor Control Class


Here is the updated Motor Control Class for the Seeed Motor Board. You many need to change the names of the direction methods as this will be determined by how you have wired your motors to the motor control board.

The MotorState enum class is used to record a history list of commands received. This may be useful when debugging the Robot in autonomous mode.

#!/usr/bin/python
# RS_MotorControl.py - Motor Control Class for the Seeed Raspberry Pi Motor Driver 
# Board v1.0 which uses the Freescale MC33932 dual H-Bridge Power IC.
#
# Based on Seeed Motor() Class 
# ref: http://wiki.seeed.cc/Raspberry_Pi_Motor_Driver_Board_v1.0/
#
# 1 March 2017 - 1.0 Original Issue
#
# Reefwing Software
# Simplified BSD Licence - see bottom of file.

import RPi.GPIO as GPIO
import os, signal

from time import sleep
from enum import Enum, unique
from PiSoftPwm import *

@unique
class MotorState(Enum):
    INIT         = 1
    STOPPED      = 2
    LEFT_FWD     = 3
    RIGHT_FWD    = 4
    BOTH_FWD     = 5
    LEFT_BACK    = 6
    RIGHT_BACK   = 7
    BOTH_BACK    = 8
    CHANGE_SPEED = 9

class MotorControl():
    def __init__(self, base_time=0.01, duty=50):
        # MC33932 pins connected to GPIO
        self.PWMA = 25  
        self.PWMB = 22
        self._IN1 = 23  
        self._IN2 = 24 
        self._IN3 = 17
        self._IN4 = 27

        self.base_time = base_time
        self.duty = duty
        self.history = [MotorState.INIT]

        # Initialize PWMA & PWMB 
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.PWMA, GPIO.OUT)
        GPIO.setup(self.PWMB, GPIO.OUT)
        GPIO.output(self.PWMA, True)
        GPIO.output(self.PWMB, True)

        # Initialize Software PWM outputs
        # Left Motor  = OUT_1 and OUT_2
        # Right Motor = OUT_3 and OUT_4
        self.OUT_1  = PiSoftPwm(self.base_time, 100, self._IN1, GPIO.BCM)
        self.OUT_2  = PiSoftPwm(self.base_time, 100, self._IN2, GPIO.BCM)
        self.OUT_3  = PiSoftPwm(self.base_time, 100, self._IN3, GPIO.BCM)
        self.OUT_4  = PiSoftPwm(self.base_time, 100, self._IN4, GPIO.BCM)

        # Start PWM for outputs - nbSlicesOn = 0, i.e. duty cycle = 0
        self.OUT_1.start(0)
        self.OUT_2.start(0)
        self.OUT_3.start(0)
        self.OUT_4.start(0)

    def __str__(self):
        # Return string representation of motor control
        return "Motor Control: base time - {0} seconds, duty - {1}%".format(self.base_time, self.duty)

    def left_back(self):
        self.OUT_1.changeBaseTime(self.base_time)
        self.OUT_2.changeBaseTime(self.base_time)
        self.OUT_1.changeNbSlicesOn(self.duty)
        self.OUT_2.changeNbSlicesOn(0)
        self.history.append(MotorState.LEFT_BACK)

    def left_forward(self):
        self.OUT_1.changeBaseTime(self.base_time)
        self.OUT_2.changeBaseTime(self.base_time)
        self.OUT_1.changeNbSlicesOn(0)
        self.OUT_2.changeNbSlicesOn(self.duty)
        self.history.append(MotorState.LEFT_FWD)

    def right_back(self):
        self.OUT_3.changeBaseTime(self.base_time)
        self.OUT_4.changeBaseTime(self.base_time)
        self.OUT_3.changeNbSlicesOn(0)
        self.OUT_4.changeNbSlicesOn(self.duty)
        self.history.append(MotorState.RIGHT_BACK)

    def right_forward(self):
        self.OUT_3.changeBaseTime(self.base_time)
        self.OUT_4.changeBaseTime(self.base_time)
        self.OUT_3.changeNbSlicesOn(self.duty)
        self.OUT_4.changeNbSlicesOn(0)
        self.history.append(MotorState.RIGHT_FWD)

    def speed(self, duty):
        # Change motor speed to duty (0-100) if not stopped (0)
        self.duty = duty
        self.OUT_1.nbSlicesOn = duty if self.OUT_1.nbSlicesOn else 0
        self.OUT_2.nbSlicesOn = duty if self.OUT_2.nbSlicesOn else 0
        self.OUT_3.nbSlicesOn = duty if self.OUT_3.nbSlicesOn else 0
        self.OUT_4.nbSlicesOn = duty if self.OUT_4.nbSlicesOn else 0
        self.history.append(MotorState.CHANGE_SPEED)

    def stop(self):
        self.OUT_1.changeNbSlicesOn(0)
        self.OUT_2.changeNbSlicesOn(0)
        self.OUT_3.changeNbSlicesOn(0)
        self.OUT_4.changeNbSlicesOn(0)
        self.history.append(MotorState.STOPPED)
        
    def cleanup(self):
        # Stop PWM on all outputs
        self.OUT_1.stop()
        self.OUT_2.stop()
        self.OUT_3.stop()
        self.OUT_4.stop()

def main():
    motor_control = MotorControl()    # create a new motor control instance
    print(motor_control)

    def endProcess(signum = None, frame = None):
        # Called on process termination. Stop motor control PWM
        if signum is not None:
            SIGNALS_NAMES_DICT = dict((getattr(signal, n), n) for n in dir(signal) if n.startswith('SIG') and '_' not in n )
            print("signal {} received by process with PID {}".format(SIGNALS_NAMES_DICT[signum], os.getpid()))
        print("\n-- Terminating program --")
        print("Cleaning up motor control PWM and GPIO...")
        motor_control.cleanup()
        GPIO.cleanup()
        print("Done.")
        exit(0)

    # Assign handler for process exit
    signal.signal(signal.SIGTERM, endProcess)
    signal.signal(signal.SIGINT, endProcess)
    signal.signal(signal.SIGHUP, endProcess)
    signal.signal(signal.SIGQUIT, endProcess)

    while True:
        print('Testing motors...')
        motor_control.left_forward()
        sleep(1)
        motor_control.left_back()
        sleep(1)
        motor_control.right_forward()
        sleep(1)
        motor_control.right_back()
        sleep(1)
        # speed = int(input("Enter Speed (0-100, CTRL c to quit): "))
        # motor_control.speed(speed)
        
if __name__ == "__main__":
    # execute only if run as a script
    main()

## Copyright (c) 2017, Reefwing Software
## All rights reserved.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are met:
##
## 1. Redistributions of source code must retain the above copyright notice, this
##   list of conditions and the following disclaimer.
## 2. Redistributions in binary form must reproduce the above copyright notice,
##   this list of conditions and the following disclaimer in the documentation
##   and/or other materials provided with the distribution.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
## ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
## DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
## ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
## (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
## LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
## SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.



Sunday, February 12, 2017

Raspberry Pi Robot - Alexa M

Raspberry Pi Robot


As mentioned in an earlier post we recently acquired an Amazon Echo Dot. We have also been playing around with a 2WD robot chassis and the Raspberry Pi. Put all these together and you have the makings of an interesting robot. The folks over at Dexter Industries obviously had the same idea.


We are going to have a crack at making our own version - called Alexa M (to differentiate it from our Echo Dot). We will do this in stages to ensure each part works!

In part 1, we will construct a pan/tilt bracket which could hold an ultrasonic sensor or a camera.

Wednesday, January 20, 2016

Bluetooth (BLE) Robot Remote Control using an iPhone - Part 2

Introduction


If you want to use an iPhone as your controller then Bluetooth is one of the better communication options for directing your robot / drone / whatever.

In Part 2 we will cover the iPhone app required to control your BLE device. The AVA BLE Remote is available as a free download from iTunes. We will first cover how the app works and then how it was coded. For those who don't want to write their own controller, feel free to use ours. We have included the ability to customise the characters sent in response to a command.

We suggest you read Part 1 to understand the overall design but here is a quick recap.

This app has been designed to work with a Bluno board from DFRobot. The Bluno is a combination of an Arduino and a Bluetooth shield. It uses Bluetooth Low Energy (BLE), which is compatible with iOS 7.0+ devices: iPhone 5+,iPad 3+,iPad Mini,and iPod 5th Gen (note that Bluetooth LE capability is required).




Getting Started: The Connection Screen


When the app starts, it will open the connection tab and display any compatible BLE devices which are within range. To connect to a device just tap on its name. If the connection is successful, the word connected will be displayed and the communication indicator "LEDs" will turn from flashing blue to green. You can start a manual search for BLE devices by tapping the search (magnifying glass) icon in the top right of the connection screen. You can stop the search by tapping the disconnect device button (a cross) in the top left.

If you are already connected to a BLE device, tapping the disconnect device button will disconnect you. You can only attach to one BLE device at a time.



Setting a Task


Tasks are a behavioural robotics concept, but of course you can use them as you wish. You don't need to use tasks at all, if you want you can just use the next tab (Control) to direct your robot. Tasks are useful if you want the robot to do more than one thing. In our case, the robot only responds to remote control commands when the remote task has been set.

The idea is that each task is made up of subset of core behaviours. By combining different behaviours you can create a robot task. For example, the default tasks provided with the app are mapped to the following core behaviours in AVA.

Task ID        Name        Behaviours

     0               Status        BID_STOP
     1               Remote     BID_MANUAL
     2               Patrol        BID_AVOID, BID_ESCAPE, BID_POWER
     3               Follow IR BID_PIR_ATTRACT, BID_AVOID, BID_ESCAPE, BID_POWER
     4               Avoid IR   BID_PIR_REPEL, BID_AVOID, BID_ESCAPE, BID_POWER

By combining simple core behaviours you can get emergent complex task following behaviours which degrade gracefully when the robot faces unexpected situations. In our robot, each of the behaviours are set a priority. This priority decides which behaviour takes precedence. If the priorities are the same then the code will deal with them in the order presented (which gives an implied priority). You can refresh your knowledge on behavioural robotics by reading our earlier post on different approaches to robot AI.

Behaviour                        Priority                      Inputs                               Outputs 

BID_STOP                            1                            cliff, collision sensors       left & right motor stop
BID_MANUAL                    2                            BLE remote                       left & right motor controls
BID_ESCAPE                       3                            distance sensors, heading  left & right motor controls
BID_AVOID                         4                            distance sensors                 left & right motor controls
BID_POWER                        5                            batt voltage, homing          left & right motor controls
BID_PIR_ATTRACT           6                            PIR                                     left & right motor controls
BID_PIR_REPEL                 6                            PIR                                     left & right motor controls

The Task tab in the iPhone app allows you to send a tasking message to your Bluno. It is the responsibility of the robot code to assign current behaviours based on the assigned task.

Tapping a task will transmit the tasking ID (e.g. 1) via Bluetooth. Tap the add button (+) to add your own tasks. Tap Edit to delete or rearrange the tasks. Tapping "Default Tasks" will reload the task list that comes with the app.




Controlling your BLE Device


The Control tab acts as a virtual gamepad for your BLE device. The currently selected task is displayed at the top of the screen. Tapping the function keys (F1 to F4) will send the designated character via Bluetooth to your Bluno. Similarly, tapping the directional dPad or stop button will send the character assigned to those buttons. You can change the characters which are transmitted from the key mapping option in the console tab. The default mappings are:

F1    w
F2    x
F3    y
F4    z

Up Arrow         f
Down Arrow    b
Left Arrow       l
Right Arrow     r
Stop                  s

The speed slider will send the selected speed encapsulated with less than and greater than brackets.

Messages received back from the Bluno are displayed at the bottom of the screen.




The Console


The console tab allows you to send any character strings that you wish to the Bluno. Just type the message in the text field and tap Send. Any received messages will be displayed below.

The console tab also contains the system log which records various events, such as device discovery, connection, data transmission and disconnection. You can email the system log by tapping the mail button in the top right of the screen.

Tap the settings (gear icon) button if you wish to change the keys mapped to the Control buttons.




iOS Code


The iOS code is pretty straight forward and made much easier by the Bluno frameworks provided by DFRobot. You will need to include the following classes in your code.

- BLEDevice
- BLEUtility
- DFBlunoDevice
- DFBlunoManager

The download link for the entire Xcode project is provided below, but here are the highlights. Most of the heavy lifting is done by the DFBlunoManager class. This is a shared instance (singleton), so you just access it in your classes using:

 blunoManager = [DFBlunoManager sharedInstance];

The first step is scanning for and connecting with available Bluetooth LE (BLE) devices. Scanning is as simple as:

[blunoManager scan];

The results of the scan are handled by the DFBlunoDelegate, so you will need to have one of your classes conform to this protocol. We used the TabBarViewController as this is nice and central. Within our TabBarViewController, there are a couple of delegate methods which get notified when a scan is performed.

#pragma mark- DFBlunoDelegate

- (void)bleDidUpdateState: (BOOL)bleSupported
{
    NSString *logString;
    NSString *timeStamp = [formatter stringFromDate: [NSDate date]];
    
    if (bleSupported) {
        logString = [NSString stringWithFormat: @"%@: Scanning for BLE devices\n", timeStamp];
        [blunoManager scan];
    } else {
        logString = [NSString stringWithFormat: @"%@: BLE not supported\n", timeStamp];
    }
    
    [self log: logString];
}

- (void)didDiscoverDevice: (DFBlunoDevice *)device
{
    NSString *timeStamp = [formatter stringFromDate: [NSDate date]];
    NSString *logString = [NSString stringWithFormat: @"%@: New device discovered - %@\n", timeStamp, device.identifier];
    
    [self log: logString];
    
    BOOL bRepeat = NO;
    
    for (DFBlunoDevice *bleDevice in appDelegate.deviceArray) {
        if ([bleDevice isEqual: device]) {
            bRepeat = YES;
            break;
        }
    }
    
    if (!bRepeat) {
        [appDelegate.deviceArray addObject: device];
        
        NSString *logString = [NSString stringWithFormat: @"%@: Device added to BLE list\n", timeStamp];
        
        [self log: logString];
    }
    
    [connectionViewController.tbDevices reloadData];
}

We use these to update the table tbDevices in the ConnectionViewController. Once you have found an eligible device, you connect to it using:

[blunoManager connectToDevice: device];

In particular, since we use a table view to list the devices in range we allow the user to connect to a specific device by tapping on the row in the table that corresponds to that device. The device data is stored in deviceArray (which is a property of the application delegate). We also have a property in the application delegate which points to the currently active device (blunoDevice). The relevant code from the ConnectionViewController is:

#pragma mark- Table View Delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    DFBlunoDevice *device = [appDelegate.deviceArray objectAtIndex:indexPath.row];
    
    if (appDelegate.blunoDevice == nil) {
        appDelegate.blunoDevice = device;
        [blunoManager connectToDevice: appDelegate.blunoDevice];
    } else if ([device isEqual: appDelegate.blunoDevice]) {
        if (!appDelegate.blunoDevice.bReadyToWrite) {
            [blunoManager connectToDevice: appDelegate.blunoDevice];
        }
    } else {
        if (appDelegate.blunoDevice.bReadyToWrite) {
            [blunoManager disconnectToDevice: appDelegate.blunoDevice];
            appDelegate.blunoDevice = nil;
        }
        
        [blunoManager connectToDevice: device];
    }
    
    [self.activityIndicator stopAnimating];
    self.activityIndicator.hidden = YES;
    [tableView deselectRowAtIndexPath: indexPath animated: YES];
}

Once you are connected to the remote device, sending data to it is performed by the following method (found in the application delegate).

#pragma mark - Bluno Communications

- (void)sendString: (NSString *)msg {
    if (self.blunoDevice.bReadyToWrite) {
        NSData *data = [msg dataUsingEncoding: NSUTF8StringEncoding];
        
        [blunoManager writeDataToDevice: data Device: self.blunoDevice];
    }
}

The DFBlunoDelegate will let you know what happens via two methods.

- (void)didWriteData: (DFBlunoDevice*)device
{
    //  NSLog(@"%s", __func__);
    
    NSString *timeStamp = [formatter stringFromDate: [NSDate date]];
    NSString *logString = [NSString stringWithFormat: @"%@: Data written\n", timeStamp];
    
    appDelegate.state = Transmitting;
    [self log: logString];
}

- (void)didReceiveData: (NSData *)data Device: (DFBlunoDevice *)device
{
    //  NSLog(@"%s", __func__);
    
    NSString *timeStamp = [formatter stringFromDate: [NSDate date]];
    NSString *receivedTextString = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
    
    //  NSLog(@"ASCII Rx: %d",[receivedTextString characterAtIndex: 0]);
    
    if (receivedTextString && receivedTextString.length > 0 && ![receivedTextString isEqualToString: @"\n"]) {
        NSString *logString = [NSString stringWithFormat: @"%@: Rx - %@\n", timeStamp, receivedTextString];
        manualViewController.txtReceivedMsg.text = receivedTextString;
        consoleViewController.txtReceivedMsg.text = receivedTextString;
        [self log: logString];
    }
    
    appDelegate.state = Receiving;
    consoleViewController.txtSendMsg.text = @"";
}


In our app we use these to update the log file in the console and print out any data received from the remote device.

Finally, to disconnect a device, just use:

[blunoManager disconnectToDevice: appDelegate.blunoDevice];

You will need to replace appDelegate.blunoDevice with whatever you have called your Bluno Device object.

The complete source code is available at the Reefwing Code Repository.

Bluno Code


The Bluno code was covered in Part 1 of this series. Sample code to allow you to test the app is available from the Reefwing Gist Repository (https://gist.github.com/reefwing/eab05c12b615070732c3).