Artikel top billede

Gør det selv-IOT del 4: Byg en dims, der kan tænde og slukke for andre dimser over internettet: Softwaren og test af det endelige produkt

Klumme: Internet of Things er tidens store buzzword, men kan man selv ved hjælp af lidt fingersnilde strikke en IoT-løsning sammen, som kan tilgås fra en åben platform? Her er fjerde og sidste del af det store selvbyggerprojekt.

I de forløbne tre klummer har vi specificeret, hvad vi vil have, at enheden skal kunne gøre:

Gør det selv-IoT, del 1: Sådan bygger du en dims, der kan aktivere andre dimser over internettet

Gør det selv-IOT del 2: Byg en dims, der kan tænde og slukke for andre dimser over internettet - installation og forberedelse af Raspberry Pi

Gør det selv-IOT del 3: Byg en dims, der kan tænde og slukke for andre dimser over internettet - hardwaren

Vi har foretaget nogle valg, og vi har forberedt vores Raspberry Pi til at virke i den kontekst, som vores produkt skal.

Senest har vi loddet noget hardware sammen.

Og vigtigst af alt: Jeg er overlevet. Ingen stød og software-frustrationerne har ikke været af en så alvorlig karakter, at det har kunnet få mig til at hoppe ud af vinduet. Success!

Denne fjerde og sidste del af serien vil handle om selve den software, vi skal skrive for at implementere vores API og for at kunne manipulere med GPIO-pindene.

Jeg afslutter artiklen med en lille demo, hvor jeg med dødsforagt brygger en kop kaffe på en kaffemaskine, som bliver tændt og trigget via min dims.

Først skal vi lige have afklaret cliff-hangeren fra sidste del og se det færdige produkt.

Her er dimsen i al sin gloværdighed:

Ikke det mest sexede design, men som ingeniør er jeg fokuseret på funktion frem for design, og det væsentligste funktionelle krav - at minimere risikoen for, at jeg dør af elektrisk stød - er opfyldt.

Men hvad er dog det med de to underlige huller i frontpladen?

Det er et klassisk eksempel på konsekvensen af, at man ikke tænker sig om.

Jeg skulle montere Raspberry Pi'en i kassen og sad og prøvede alle mulige kombinationer. Det var på den forkerte side af kl. 23 en hverdagsaften, og jeg havde sat mig som mål, at kassen skulle være færdigmonteret, før jeg gik i seng.

Jeg fandt den optimale placering, tænkte 'Yes!', greb boremaskinen og borede fluks to huller til skruerne.

Så vendte jeg boksen om ... og sprang direkte til sidste del af punkt 5 i hardwareopskriften i seneste klumme.

Nå, heldigvis er skaden ikke så stor, da jeg alligevel har tænkt mig at sætte en lille rekvalme på fronten af boksen, som kommer til at dække over det.

Nu skal der kodes!

Først laver jeg et lille script, som skal styre status-LED'en.

Som I måske husker, så fandt jeg en RGB-LED i skuffen, og kan dermed lave masser af farver.

Det giver mulighed for at signalere en masse forskellige ting (system ok, men wifi ikke forbundet, forkert wpa-key, intern temperatur for høj, etc.), men jeg vælger at være lidt primitiv og blot implementere rød (= fejl/ikke forbundet til internettet) og grøn (=hvad tror du selv?).

Mere fancy funktionalitet må skydes til version 2.

Scriptet skriver jeg i python (Den komplette kildekode finder du her: http://www.xn--nrdoteket-l8a.dk/index.php/gds-iot-kildekode-status-py/ )

Virkemåden er, at jeg bestemmer netværkets default-gateway, som er routerens IP-adresse.

I et uendeligt loop pinger jeg den IP-adresse, og hvis der er svar, sætter jeg LED til grøn, og hvis der ikke er svar, sætter jeg LED til rød.

Koden er ganske simpel, og python er dejligt nemt at bruge.

I python bruger jeg RPi.GPIO biblioteket til at kontrollere GPIO-pins. LED'en er forbundet til GPIO#9, 10 og 11 (Grøn, Blå, Rød). GPIO skal initialiseres som enten input- eller output-pins:

GPIO.setmode(GPIO.BCM) # Use Broadcom pin numbers
GPIO.setup(9, GPIO.OUT) # Green LED, set as output
Og man tænder for en pin ved at sætte den til HIGH:
GPIO.output(9, GPIO.HIGH) # LED on
GPIO.output(9, GPIO.LOW) # LED off
Det primære loop, hvor jeg pinger ser således ud:
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)
Dette program skal køre automatisk ved boot, og derfor skal det tilføjes rc.local:
rpi_admin@skarpline-iot:~ $ sudo nano /etc/rc.local
Tilføj " /home/rpi_admin/status.py > /dev/null" til filen (husk at tilpasse home dir navn til din bruger, eller hvor du nu måtte lægge status.py filen), så det endelige resultat ser således ud:
#!/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

Med denne metode vil lampen lyse grønt, så længe routeren svarer, også selv hvis der ikke er forbindelse til internettet.

Hvis man udskifter default-gateway med noget ude på internettet, f.eks. Googles DNS-server 8.8.8.8, er testen mere retvisende.

Det undlader jeg personligt at gøre, da det ikke er god stil at lave noget, der kontinuerligt står og genererer trafik mod andre menneskers servere.

En oplagt tilføjelse kunne være at tilføje advarsel i tilfælde af for høj inten temperature.

Man kunne bruge onchip-sensoren i Raspberry Pi:

rpi_admin@skarpline-iot:~ $ echo $(($(cat /sys/class/thermal/thermal_zone0/temp) / 1000))
46
Og hvis man vil have decimaler med:
rpi_admin@skarpline-iot:~ $ echo $(cat /sys/class/thermal/thermal_zone0/temp) 1000 | awk '{ print $1/$2 }'
45.464

Nu til selve koden der implementerer API'et.

Den komplette kildekode finder du her: http://www.xn--nrdoteket-l8a.dk/index.php/gds-iot-kildekode-control-php/.

Det foregår som en webservice, og derfor er det første, vi skal gøre, at validere brugeren.

Det gøres med HTTP Digest (RFC-2617), og derfor starter vi med at afvise alle requests til vores server, der ikke har authentication information med i headeren.

Vi afviser med en "http 401 / unauthorized"-header (https://httpstatuses.com/401), og tilføjer en unik værdi kaldet et "nonce" (Løst oversat: en engangs-term).

Kigger vi i TCP-pakkerne (med wireshark - hvis du ikke har prøvet det endnu, så er det på høje tide. Fantastisk og helt uundværligt værktøj!) ser det således ud (figur 2):

Og i koden ser det således ud:

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 er php's reserverede variabel, der indeholder al den information, som er med i det request, der er modtaget (http://php.net/manual/en/reserved.variables.server.php).

Nonce genereres med php's uniqid( )-funktion, der genererer en unik streng baseret på tidspunktet for requestet (hvis man sender det samme hver gang, er man sårbar for playback-angreb).

Hvis vi nu sørger for at sende en request-header med korrekt authorization information, kan vi se, at vi får det ønskede svar (og en http 200 / ok-header).

Der er en del information i det svar, som vi lige skal gennemgå.

Først skal vi sikre, at klienten har sendt alle de nødvendige data. Det gøres ved at parse PHP_AUTH_DIGEST variablen.

// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
!isset($users[$data['username']]))
die('WRONG_CREDENTIALS');
Dernæst beregnes det response, som vi forventer at få, hvis ellers afsenderen har brugt korrekt brugernavn og password.

Det foregår i HTTP Digest ved at tage et MD5 hash af informationerne (som beskrevet i 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);
Og sluttelig tjekker vi, om det modtagne response svarer til det, vi selv har beregnet.
if ($data['response'] != $valid_response)
die('WRONG_CREDENTIALS');
Nu er brugeren valideret, og vi skal se, hvad der bliver bedt om i requestet.

Når man skal levere information i et "http/get" gøres det i URL'en med "parameter=værdi" par. I PHP læses parametrene med $_GET['parameter']:

$action = $_GET['action'];
$port = $_GET['device'];
Device skal mappes til den korrekte GPIO-port, og vi skal sikre, at der ikke spørges efter et ugyldigt device. Det gøres med en 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;
}
Endelig, efter så meget skriveri, lodderi og koderi, er vi nået til kernen i det hele: Bliver vi bedt om at tænde eller slukke for et strømudtag. Det tjekker vi også med en switch statement:
switch($action)
{
case 'ON': // Vi vil tænde
..tænd for device
case 'OFF': // Vi vil slukke
..sluk for device
case 'STATUS':
..returner om der er tændt eller slukket
}
Selve interaktionen med GPIO-pins foretager vi med WiringPi, som blev installeret i klumme nr 2.

WiringPi er et lib til shell-kommandoer, hvilket vil sige, at vi fra PHP skal lave et systemkald.

Det kan gøres med shell_exec kommandoen (hvor $portreal= 17 eller 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
Hermed er projektet nået til sin ende.

Egentlig imponerende, hvor meget jeg kan skrive om så lidt og stadig have fornemmelsen af kun lige have ridset i overfladen!

Men vi skal da lige have demonstreret dimsen - ellers tror I bare ikke på, at den virker. Det er lidt svært at gøre på skrift, så jeg har bikset en lille video sammen til jer.

Med reference til den første klumme i serien, så er det elektriske apparat, som skal testes, selvfølgelig den obligatoriske kaffemaskine.

Problemet er blot, at min kaffemaskine er en fancy sag, som kræver tryk på en knap for at brygge en kop kaffe.

Hvordan klarer jeg nu det problem?

Et helt nyt projekt begynder at forme sig i mit hovede: Et legotårn, en servo, en afhugget finger.

Argh, stop! Jeg bliver aldrig færdig med at skrive, og Computerworld løber tør for anslag, så nu holder jeg mund.

Sig goddag til GDS-IOT-dimsen og få ikke mindst et kig på, hvordan jeg løste problemet med at trykke på kaffemaskinens knap:

Håber, I har nydt at læse serien.

Jeg har lavet og laver mange af denne slags småprojekter, men det er første gang jeg har prøvet at beskrive det i detaljer på denne måde.

Er der interesse for lignende beskrivelser i fremtiden, så skriv det gerne i kommentarfeltet nedenunder.

Kildekoden står til fri afbenyttelse i henhold til revision 42 af Beer-ware licensen (en af mange geniale ting fra phk's hånd).

Hvis jeg havde mere tid

Hvis nu man ville lege videre med udgangspunkt i denne dims:

Hardware:
Flere udtag
Det kommer an på dit behov. Det er særdeles nemt at udvide med flere, det koster blot en stikkontakt og et relæ samt driverkredsløb pr. stk.

Måling af effektforbrug pr. udtag
Det kunne være sjovt at lege med. Kræver en strømsensor pr. udtag. Output fra strømsensorer er typisk analoge, så en ADC (analog til digital konverter) skal bruges pr. strømudtag.

Eller man kunne overveje at skifte Raspberry Pi ud med en anden platform der allerede har ADC-kanaler.

Mulighed for at lysdæmpe en lampe tilsluttet et udtag
Måske ikke så relevant ved en strømskinne?

Skrumpning af design, så det kan skohornes ind i en strømskinne.

Læg print ud og lav det hele fra bunden, bedre, billigere og mindre.

Andre forslag?

Software:

Skriv en app til kontrol. Det er jo så moderne.

Timerfunktion
Udvid API, så man kan sige "tænd device X, og sluk det automatisk om Y minutter"

'Feriemode'
Lav funktion, som tænder og slukker for de forskellige udtag på tilfældige tidspunkter

WPS, så det er nemt at installere

UPnP, så det er nemt at installere

Andre forslag?

Husk, I kan følge mig på Facebook, hvor jeg skriver løst og fast om teknologi, og hvor I kan komme med forslag til emner som jeg kan tage op i klummen: https://www.facebook.com/en.noerds.bekendelser/

Min lille private teknologiske losseplads finder I på http://www.nørdoteket.dk/ hvor jeg dumper de artikler jeg har skrevet og diverse andet snask - alt sammen med primær fokus på teknologi