15 Feb 2014

Raspberry Pi control from mobile device or desktop web browser

In my disco ball hack project, I have been using my mobile phone browser to send commands to my Raspberry Pi. In this post, I will give you a quick overview on how to do the same thing and we will take a look at the code involved. I hooked up a LED and a button to my RPi so we can read the state of the button and toggle the LED. I'm using a self flashing RGB LED that I bought on Ebay, it looks cool :)

There are multiple ways to connect to the Raspberry Pi from another device. It could have been a usb-serial link, bluetooth or even raw socket programming. Also, you could have used other methods  to create the user interface by using native programming on the device :  Android or iPhone app, Visual Studio on the PC... The biggest advantage of using HTTP with a web server on the Pi is that it will work with all devices that have a browser. Also, you only need to know a bit of HTML and JavaScript, you don't need to learn all the other SDK and languages. I think that it's much easier to design a user interface using HTML and adding a bit of CSS for improving the look of the page than learning all the GUI toolkits out there. Using HTML5 and JavaScript is trending right now with all the web development. In Windows 8, apps can be created natively in HTML5 and JavaScript. Some of the new operating systems like Firefox OS and Tizen use HTML for everything. Anyway, let's get started with our application.

For the server side software part, I'm using Python and the web framework Flask. There is a ton of these Python web frameworks (Django is the most well known) but Flask is great because it is really simple and does not need any configuration. For the HTML part, I'm using a bit of JQuery Mobile because it helps making a great looking page in a few line of HTML. There is also another reason to use JQuery: Ajax. (Asynchronous JavaScript and XML)

In a typical webpage without Ajax in which you want to send data to the server, you have to make an HTML form with a submit button. Once you press submit, a POST or GET request is done, your data is transmitted to the server and the page is completely reloaded with new data or you are redirected. With Ajax, you can easily send data to the web server without disrupting the page displayed. It is also possible to request JSON data from the server and modify the DOM (update only certain element of the page).

The simple circuit used

My circuit is an LED connected on P1 (WiringPi pin numbers) in series with a 470 ohm resistor. I also have a button between P0 and GND. I don't need a pull-up on the button because I'm using the internal pull-up of the Raspberry Pi GPIO. The circuit is even simpler like that.

Python code to access the button and LED

This code is fairly simple, I used the standard Raspberry Pi Python module to read/write to the GPIO. I made small utility functions so that later it is easier to read the code in the server Python file. The file name is Pins.py and I will use this as a module in the web server code.

import RPi.GPIO as GPIO

# BCM to WiringPi pin numbers
P0 = 17 # LED pin
P1 = 18 # Button pin

def Init():
    GPIO.setwarnings(False) # suppress GPIO used message
    GPIO.setmode(GPIO.BCM) # use BCM pin numbers
    GPIO.setup(P0, GPIO.OUT) # set LED pin as output
    # set button pin as input
    # also use internal pull-up so we don't need external resistor
    GPIO.setup(P1, GPIO.IN, pull_up_down=GPIO.PUD_UP)

def LEDon():
    GPIO.output(P0, GPIO.HIGH)

def LEDoff():
    GPIO.output(P0, GPIO.LOW)

def SetLED(state):
    if state:
        LEDon()
    else:
        LEDoff()

# the "not" is used to reverse the state of the input
# when pull-up is used, 1 is returned when the button is not pressed
def ReadButton():
    if not GPIO.input(P1):
        return True
    else:
        return False

# if not used as a module (standalone), run this test program 
if __name__ == "__main__":
    Init()
    try:
        while(True):
            SetLED(ReadButton())
    except:
        #clean exit on CTRL-C
        GPIO.cleanup()
        quit()

Web server code

With Flask you can create a working web server in half a page of code. The Index function send the page to the web browser. Notice that the page is rendered only once, the rest is handled with Ajax. The function names that starts with the underscore are the Ajax request. They are called with the GetJSON function with JQuery (see the web page JavaScript in the next section). The _led function does not return JSON, but extract the state parameter from the GET url from the Ajax request to set the state of the LED. The _button function does not have parameter but instead return a JSONified text string corresponding to the button state. As a bonus, when the page is rendered first, I send the uptime of the Raspberry Pi. You can try to type "uptime" in a Unix terminal, you will see a status string displayed. It gives you the time, uptime, # of connected users and load average. I only wanted the uptime so I parsed it.

By the way, to get the Flask module on your Raspbian Python folder, enter the following commands:
sudo apt-get install python-pip
sudo pip install flask

The first command is to install pip (a tool for installing and managing Python packages).

from flask import Flask, render_template, request, jsonify
import Pins

app = Flask(__name__)

# return index page when IP address of RPi is typed in the browser
@app.route("/")
def Index():
    return render_template("index.html", uptime=GetUptime())

# ajax GET call this function to set led state
# depeding on the GET parameter sent
@app.route("/_led")
def _led():
    state = request.args.get('state')
    if state=="on":
        Pins.LEDon()
    else:
        Pins.LEDoff()
    return ""

# ajax GET call this function periodically to read button state
# the state is sent back as json data
@app.route("/_button")
def _button():
    if Pins.ReadButton():
        state = "pressed"
    else:
        state = "not pressed"
    return jsonify(buttonState=state)

def GetUptime():
    # get uptime from the linux terminal command
    from subprocess import check_output
    output = check_output(["uptime"])
    # return only uptime info
    uptime = output[output.find("up"):output.find("user")-5]
    return uptime
    
# run the webserver on standard port 80, requires sudo
if __name__ == "__main__":
    Pins.Init()
    app.run(host='0.0.0.0', port=80, debug=True)

Web page HTML code

In the web page, I import everything needed to get JQuery Mobile working. In the script section, I have two JavaScript functions : the first one detect a change on the slider-switch and send the state of this switch with an Ajax GET to the webserver, the second one is called once each 500 ms to get the button state. The rest of the HTML is the minimal structure of the page.

<!doctype html>
<head>
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.0/jquery.mobile-1.4.0.min.css" />
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.0/jquery.mobile-1.4.0.min.js"></script>
</head>

<style>
h3, h4 {text-align: center;}
span {font-weight: bold;}
</style>

<script type=text/javascript>
    $(
    // When the LED button is pressed (change)
    // do an ajax request to server to change LED state
    function() 
    {
        $('#flip-1').change(function() 
        {
        $.getJSON('/_led', {state: $('#flip-1').val()});
        });     
    }
    );

    $(
    // periodically (500ms) do an ajax request to get the button state
    // modify the span tag to reflect the state (pressed or not)
    // the state text comes from the JSON string returned by the server
    function button() 
    {
        $.getJSON('/_button', function(data)
            {
                $("#buttonState").text(data.buttonState);
                setTimeout(function(){button();},500);
            });
    }
    );
    

</script>
<!-- Simple JQuery Mobile page that display the button state on the breadoard -->
<!-- You can also change the LED state with the slider switch -->
<!-- The Raspberry Pi uptime is displayed in the footer (Jinja2 expands the template tag) -->

<div data-role="page" data-theme="b">
  <div data-role="header">
    <div><h3>Raspberry Pi Web Control</h3></div>
  </div>

  <div data-role="content">
    <form>
    <p>The button is <span id="buttonState"></span></p>
    <br>
    <select name="flip-1" id="flip-1" data-role="slider" style="float: left;">
        <option value="off">LED off</option>
        <option value="on">LED on</option>
    </select>
    </form>
  </div>
 <div data-role="footer">
    <div><h4>This Raspberry Pi has been {{uptime}}</h4></div>
  </div>
</div>

In action : demo video on Youtube





Here is a demo of the server running and being controlled by my mobile phone. On my main computer, I connect to the Raspberry Pi with SSH. I then start the python application. If you look closely on the computer screen around 0:15, you will see a lot of requests being made to the server. This is due to the 500ms timer sending the Ajax request to read the status of the button.



I really hope that this code can help you in your projects. When I started doing electronics stuff in 2007, I was dreaming about doing something like this. At that time, we did not have smart phones and low cost Linux single board computer. Things changed a lot since!

11 Feb 2014

Hacking a Disco Ball - Part 2

In my last blog post, I gave you an overview of the Disco Ball project. A couple weeks ago I received the printed circuit boards from OSH Park. So here are a couple of pictures of the boards and assembly :





After soldering the circuit, I was really relieved that everything worked perfectly. The first thing I did was to test if I was able to identify the Propeller chip with the programming software and load a program in the EEPROM memory. Once done, I soldered the 3 MOSFET LED drivers and fired a 3.3V signal on their gates. The LEDs of the disco ball turned on as normal. I was a bit stressed with the last part, driving the 120V motor with the Triac. I tested the connections on the breadboard before doing the PCB, but there is always a chance that something is not correct. I closed my eyes when I plugged the 120V in... no explosion or blue fume, phew :)

Software

Originally, I wrote a command interpreter in Python that compile a sequence file and send the compiled sequence to the microcontroller on the PCB. The compiled sequence was stored in the unused part of the boot EEPROM and the main program purpose was to run the loaded sequence. The Python code is shown below :

# -*- coding: utf-8 -*-
"""
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
import sys, re
#Pyserial needed to connect to the printed circuit board
try:
    import serial
except:
    sys.exit(COM_LIBRARY_UNDEFINED)

#This constant value needs to fit with the one in the microcontroller, it won't work otherwise    
MAX_COMMANDS = 1024 
    
#Error codes, ease the debugging process and script interpreting troubleshooting
OK = 0                      #Everything is OK
COM_UNDEFINED = 1           #COM Port undefined on the first line
COM_ERROR = 2               #Unable to connect to the board
SYNTAX_ERROR = 3            #Incorrect command syntax
COLOR_ERROR = 4             #bad color range (0-100)
DELAY_ERROR = 5             #bad delay range
LABEL_NOT_FOUND_ERROR = 6   #unable to find loop label
LABEL_DUPLICATE_ERROR = 10  #loop lable duplicate
SCRIPT_NOT_FOUND = 7        #wrong script path
COM_LIBRARY_UNDEFINED = 8   #pyserial not installed on the machine
ARGS_ERROR = 9              #need path as command line argument

#we need the disco light sequence script path as first argument
try:
    script_path = sys.argv[1]
except:
    sys.exit(ARGS_ERROR)

#read the script and put it in a list    
try:
    script_file = open(script_path)
    raw_script = script_file.readlines()
    script_file.close()
except:
    sys.exit(SCRIPT_NOT_FOUND)

#only keep the important stuff (remove comments)    
def strip(line):
    if line[0] == '[':
        index = line.find(']')
        if index==-1:
            return
        else:
            return line[1:index].lower()

#list comprehension to execute the above function on all the lines
cleaned_script = [strip(line) for line in raw_script if strip(line)]
print "Clean script:\n"+str(cleaned_script)

#find all the labels
labels = {}
index = -1 #Strip the COM Port line
for command in cleaned_script:
    index += 1
    if command[0] == '.':
        labels[command[1:]] = index

#look for duplicates        
unique_labels = tuple(set(labels))
if tuple(labels) != unique_labels:
    sys.exit(LABEL_DUPLICATE_ERROR)

print "Labels:\n"+str(labels)

#regex patterns to parse commands
pat_com = re.compile("(com\d+)")
pat_col = re.compile("([rgb]):(\d+)")
pat_del = re.compile("\$(\d+)")
pat_jmp = re.compile("jmp\.(.+)?(\d+)")
pat_jmpINF = re.compile("jmp\.(.+)")
pat_lbl = re.compile("\.(.+)")

#get the serial COM Port and set the link speed
m = pat_com.match(cleaned_script[0])
if not m:
    sys.exit(COM_UNDEFINED)
else:
    com_port = m.group(1)
    print "Port: "+str(com_port)
try:
    BAUDRATE = 115200
    ser = serial.Serial(com_port,BAUDRATE)
except:
    sys.exit(COM_ERROR)



#The next lines parse all the commands and generate
#   a list of parsed command in an easy to use way
commands = []
for command in cleaned_script[1:]:
    m = pat_col.match(command)
    if m:
        color = m.group(1)
        brightness = m.group(2)
        if int(brightness) > 100:
            print brightness
            sys.exit(COLOR_ERROR)
        commands.append(("COLOR",color,brightness))
        continue
    m = pat_del.match(command)
    if m:
        delay = m.group(1)
        if int(delay) > 100000:
            sys.exit(DELAY_ERROR)
        delay = int(delay)
        b0 = delay >>  0 & 0b11111111
        b1 = delay >>  8 & 0b11111111
        b2 = delay >> 16 & 0b11111111
        b3 = delay >> 24 & 0b11111111
        commands.append(("DELAY",b0,b1,b2,b3))
        continue
    m = pat_jmp.match(command)
    if m:
        label = m.group(1)
        label = label[:-1] # Strip the question mark
        times = m.group(2)
        if not label in labels:
            sys.exit(LABEL_NOT_FOUND_ERROR)
        commands.append(("REPEAT",labels[label],times))
        continue
    m = pat_jmpINF.match(command)
    if m:
        label = m.group(1)
        if not label in labels:
            sys.exit(LABEL_NOT_FOUND_ERROR)
        commands.append(("LOOP FOREVER",labels[label]))
        continue
    m = pat_lbl.match(command)
    if m:
        label = m.group(1)
        commands.append(("LABEL",labels[label]))
        continue
    if command == "stop":
        commands.append(("MOTOR",False))
        continue
    if command == "start":
        commands.append(("MOTOR",True))
        continue
    sys.exit(SYNTAX_ERROR)

print "Commands:\n"+str(commands)

#Patch for the ser.write : we want to send 
#   the decimal number as a byte
commands_sent = 0
def write(num):
    global commands_sent
    commands_sent += 1
    if num != 0:
        pass#print num
    ser.write(chr(int(num)))

#Special read : read the bytes as string and when there is a newline, it marks the end of string
#Return all the strings in a array (tuple) stop reading when null character received chr(0)
def readLineFeed():
    data = []
    buffer = []
    while(True):
        value = ser.read()
        if value == "\n": #LineFeed
            data.append("".join(buffer))
            buffer = [] #Flush the buffer
        elif value == chr(0):
            break
        else:
            buffer.append(value)
    return tuple(data)

def readINF():
    while(True):
        value = ser.read()
        print value
#After the commands are parsed, we send 
#   serial commands to the controller PCB
for command in commands:    
    if command[0] == "COLOR":
        if command[1] == 'r':
            write(1)
            write(command[2]) #brightness
        elif command[1] == 'g':
            write(2)
            write(command[2]) #brightness
        elif command[1] == 'b':
            write(3)
            write(command[2]) #brightness
            
    elif command[0] == "MOTOR":
        if command[1]:
            write(4) #Start
        else:
            write(5) #Stop
            
    elif command[0] == "DELAY":
        write(6)
        write(command[1]) #delay b0
        write(command[2]) #delay b1
        write(command[3]) #delay b2
        write(command[4]) #delay b3
        
    elif command[0] == "LABEL":
        write(7)
        write(command[1])
        
    elif command[0] == "REPEAT":
        write(8)
        write(command[1]) #label
        write(command[2]) #times

    elif command[0] == "LOOP FOREVER":
        write(9)
        write(command[1]) #label

if commands_sent != MAX_COMMANDS:
    print "Before looping: "+str(commands_sent)
    to_max = MAX_COMMANDS-commands_sent
    for index in range(to_max):
        write('0')
    print "After looping: "+str(commands_sent)

   
#Print the data echoed (debug)
for data in readLineFeed():
    print(data)
    
#Everything should be fine if we get there   
ser.close()
print "Script OK!\n"
sys.exit(OK)

In the end, my friend and I decided to control the board with a Raspberry Pi, by using a serial port on each side. The interface is really simple : you have commands to dim the 3 LEDs from 0 to 100% and a command to turn the motor on and off. The reason for the Raspberry Pi is that with an internet connected device, we can easily make a simple webpage to control the board with our mobile phones.

I love Python, so for the web framework on the Pi I decided to use Flask. For the HTML code, I used jQuery Mobile for a good page rendering on mobile devices. There is an excellent jQuery Mobile tutorial on the w3schools website. To make the page interactive, I used a bit of AJAX with jQuery. I really should do a little tutorial about all that in another article. It is really cool to be able to control the IO of the Pi with a webpage! Pictures of the webpage:



Here is a YouTube video showing the Disco Ball in action




Finally, I did not gave too much explanation about the software side of the project (webpage source and microcontroller programs)... I might just be a little bit lazy :) If someone is interested, I can send my source code. Thanks for reading!