DIY-IOT: How to construct a device, that turns on/off other devices via the Internet – Part 4/4: The software

IMPORTANT: READ THIS, before deciding to build something like this device. This device is connected directly to the mains power, which is potentially lethal to mess around with! I am not kidding you – the best scenario when subjected to electrical shock is that it hurts like h*ll, worst case scenario is that you DIE. You should not mess with mains power without a minimum of knowledge.

In the past three articles we have specified the requirements for the device. We have made some choices and prepared the Raspberry Pi for the context that we will need, and last article was about creating the hardware. Most important of all, I survived so far! No electrocutions, and software frustrations have not been severe enough to make me jump out the window. Success!

This forth and final article will be on creating the software that implement the IOT functionality, and the demonstration of the finalized ‘product’.

But the first thing to do is deal with the cliff-hanger from last article, and show the final product. This is the gizmo in all it’s glory:

dimsen

Not the most sexy design, but hey! I am an engineer, and as such much more interested in functionality rather than design. The most important requirement on the physical design is to prevent me from dying from electrical shock, and I’m alive, so job well done!

But how about those weird holes in the front plate? Well. Cough-cough… Those holes are a perfect textbook example of the consequences on being in a hurry and not thinking your actions through. It was on the wrong side of 11pm a Wednesday night, and I had set up a goal that I had to be done with mounting the Raspberry Pi in the box before going to bed. While trying a lot of different combinations, I found the optimal position, and thought “Yes!”, grabbed the power drill and drilled a couple of holes for the mounting screws. Then I realised what I’d done, and jumped directly to let last part of step#4 in the hardware article:

Luckily the damage is manageable, since I anyway planned to cover the lower part of the device with a small ‘commercial’:

final_product

Now let’s do some coding! The first thing is to create the script that controls the status LED. The LED is a RGB type, so we can signal all sorts of different events (system ok, but no WIFI, wrong WPA key, internal temperature too high, etc.), but I choose in this first version to make do with green = network ok and red = no network connection. The script is written in python, and the complete source code can be found here: http://www.geekoholic.net/index.php/157-2/

The script determines the default gateway of the network, which typically correspond to the IP address of the router, and attempt to ping this IP address. This happens in an infinite loop, and depending on the reply to the ping, the LED is set as green/red. Python is a easy language to code, and I use the RPi.GPIO library to control the GPIO pins. The LED is connected to GPIO#9, 10 and 11(green, blue, red), and the GPIO pins must first be configured as either input or output:

GPIO.setmode(GPIO.BCM) # Use Broadcom pin numbers
GPIO.setup(9, GPIO.OUT) # Green LED, set as output

And you ”turn on” a pin, by setting it HIGH:

GPIO.output(9, GPIO.HIGH) # LED on
GPIO.output(9, GPIO.LOW) # LED off

The primary loop, where the ICMP echo requests are sent looks like this:

while True: # infinite loop
    response = os.system("ping -c 1 " + hostname) # send one ICMP packet to host
    if response == 0: # Reply
        # Network is reachable, LED=Green
        GPIO.output(11, GPIO.LOW)
        GPIO.output(9, GPIO.HIGH)
    else: # No reply
        # Network is NOT reachable, LED=Red
        GPIO.output(11, GPIO.HIGH)
        GPIO.output(9, GPIO.LOW)

This script must run on system boot, so we need to modify rc.local:

rpi_admin@skarpline-iot:~ $ sudo nano /etc/rc.local

Add ” /home/rpi_admin/status.py > /dev/null” to the file, and remember to change the directory to fit where you put the script on your system. The new rc.local file should look like this:

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

/home/rpi_admin/status.py > /dev/null

# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi

exit 0
 

There is a limitation in this method: Even if there is no Internet connection the LED would be green, since I am only pinging the default gateway. If an address on the Internet itself were chosen as target (e.g. Google DNS 8.8.8.8), the test would show actual Internet connectivity. Personally I refrain from doing this, since in my opinion it is not polite to create something that continuously generate traffic load on other peoples servers. You could consider using traceroute to find a network device inside the network of your ISP, and ping this device instead. Then the connectivity of your DSL/Cable/whatever router is part of the test.

An obvious next step for the LED status script would be to add a warning for too high temperature. You could use the on-chip sensor on the Raspberry Pi:

rpi_admin@skarpline-iot:~ $ echo $(($(cat /sys/class/thermal/thermal_zone0/temp) / 1000))
46 

Or if you want decimals

rpi_admin@skarpline-iot:~ $ echo $(cat /sys/class/thermal/thermal_zone0/temp) 1000 | awk '{ print $1/$2 }'
45.464

Now we move to the code that implements the API. The complete source code is available here:

http://www.geekoholic.net/index.php/diy-iot-control-php-source-code/

and the API here:

http://www.geekoholic.net/index.php/diy-iot-the-api/

The API is implemented as a webservice, and the first thing to do is to validate the user. This is done with HTTP Digest (RFC-2617), and therefore we begin by rejecting all requests our webserver that do not contain authentication information in the header. We reject by returning a “HTTP 401 / UNAUTHORIZED” header (https://httpstatuses.com/401), and add a unique value to the reply , called a nonce (a one-time term).

digest_process

If we inspect the TCP traffic (Wireshark, if you haven’t tried it yet, please do. Fantastic tool!) the request/response look like this:

http_digest_mangler_i_header

And in the code like this:

if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
  header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Digest realm="'.$realm.
     '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
 
  die('UNAUTHORIZED');
  }

$_SERVER is a reserved variable in php containing all the information in the request that is received (http://php.net/manual/en/reserved.variables.server.php).

Nonce is generated with php’s uniqid() function which generates a unique string, based on the time of the request (if you’d send the same nonce every time, you’d be vulnerable to playback attacks).

So how does it look, when a header with correct authorization information is received? We get the desired response, and a HTTP 200 / OK header:

http_digest_med_i_header

This reply contain some information that I will need to explain. First we must make sure, that the client delivers all the necessary data in the header. This is done by parsing the PHP_AUTH_DIGEST variable:

// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
    !isset($users[$data['username']]))
      die('WRONG_CREDENTIALS');

Next we calculate the response that we expect to receive, provided the client used correct username and password. HTTP Digest dictates, that this should be done by calculating a MD5 hash from the information, as described in RFC-2617 §3.2.2

// generate the valid response
$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

And finally, we compare the received response to the one we just calculated:

 
if ($data['response'] != $valid_response)
  die('WRONG_CREDENTIALS');
 

Now the user is validated, so let’s see what he asks us to do, according to the API. When you want to parse information in a “http/get”, you append “parameter=value” pairs to the URL. In php, these parameters are read by using the $_GET[‘parameter’]:

$action = $_GET['action'];
$port = $_GET['device'];

Device must be mapped to the correct GPIO port, and we must ensure that the request is for a valid device. This is done using a switch() statement:

switch ($port)
{
case 1:
    $portreal="27"; // Device#1 maps to GPIO#27
    break;
case 2:
    $portreal="17"; // Device#2 maps to GPIO#17
    break;
default:
    die('INVALID_DEVICE');
    break;
}

Finally! After all this soldering and coding, we have reached the point where we will perform the requested action. Again we use a switch() statement:

switch($action)
{
case ’ON’: // We want to turn on the device
  ..GPIO=HIGH
case ’OFF’: // We want to turn off the device
  ..GPIO = LOW
case ’STATUS’:
  ..Tell if the GPIO is HIGH or LOW
}

To manipulate the GPIO pins we use the library WiringPi, that we installed in part 2 of the series (link to part 2). WiringPi is a library for shell commands, so we need to breakout from PHP and make a system call. This is done with the shell_exec() command (where $portreal= 17 or 27):

    $gpio_on = shell_exec("/usr/local/bin/gpio -g write " . $portreal . " 1"); // Turn on the desired GPIO
    $gpio_stat = shell_exec("/usr/local/bin/gpio -g read " . $portreal); // Read status of port

This concludes the project. If you made this far, well done! I’m amazed how much I can write about so little, and still feel I only scratched the surface.

But we need a demonstration of the final product, or you’d just think it didn’t work!

A demo is not easy in writing, so I have created a small video for you.

As discussed in the first part of the series, the electrical appliance of choice is a coffeemaker, but alas, my coffeemaker is a fancy machine that will need the press of a button after powering up, to make a cup of coffee.

How to deal with that problem? I don’t want to mess up my coffeemaker, but wait… An new project is forming inside my head… If I borrowed some of my daughters lego, and I know I have a finger lying around somewhere. Argh, stop! Enough writing:

See a demo of the DIY-IOT gizmo, and how the issue with the button is dealt with here: https://www.youtube.com/watch?v=HPYywrao5dk

Al sourcecode is released under revision 42 of the Beer-ware license.

 

Leave a Reply

Your email address will not be published. Required fields are marked *