Ott 172012
 

Oggi sviluppiamo ulteriormente il precedente progetto e vediamo come inviare le letture delle temperature dei sensori DS18B20 su un server remoto delegando alle tecnologie web la rappresentazione dei dati.Si noti che non andiamo a sostituire le funzionalità già implementate ma le andiamo ad integrare. Il funzionamento finale prevede che Arduino a tempi regolari rileva le temperature su tutti i sensori, le invia su server remoto e, se interrogato sulla rete locale, invia una pagina web con le letture memorizzate.

La prima cosa da fare è utilizzare l’EthernetClient che permette di collegarsi al server remoto per inviare i dati che ci interessano. Nell’esempio qui sotto ho usato come server il portatile da cui scrivo questo articolo sul quale è installato un web server. L’indirizzo IP è perciò un indirizzo locale (192.168.1.8) ma su server remoto la cosa è del tutto identica. L’altra grossa differenza rispetto il precedente articolo è nella gestione della “tempistica” dei dati. Prima infatti inviavamo le temperatura quando giungeva una specifica richiesta, ma ora le cose sono diverse e stabiliamo noi quando inviare i dati. Nel nostro caso, trattandosi di temperature, è logico inviarle ogni tot tempo, ma le cose potrebbero essere diverse in altri casi, ad esempio potremmo avere un sistema che segnala via web quando piove, quando viene aperta una porta, quando qualcuno preme un pulsante o quant’altro. Vediamo come fare il tutto.

1) inizializziamo il client

byte remote[]={192,168,1,8};
EthernetClient ArduinoClient;

2) all’interno del loop eseguire la connessione al server remoto e, in caso di successo, inviare i dati:

if (ArduinoClient.connect(remote, 80))
{
Serial.println("connected"); // scrive sul monitor seriale, utile per il debug dell'applicazione
ArduinoClient.print( "HEAD /test/myscript.php?c1=10");
ArduinoClient.println(" HTTP/1.1"); // attenzione allo spazio iniziale
ArduinoClient.println(Host: 192.168.1.25");
ArduinoClient.println("User-Agent:Arduino");
ArduinoClient.println("Accept: text/html");
ArduinoClient.println("Connection: close");
ArduinoClient.println();
ArduinoClient.stop();
}
else Serial.println("connection failed");

Nella prima riga viene effettuata la connessione al server remoto sulla porta 80, se l’esito è positivo è ritornato true e si entra nell’if.Tramite le istruzioni print e pintln del client vengono inviati i dati al server remoto. Nell’esempio, in particolare, viene richiamato lo script myscript.php che si trova su /test, nel server remoto 192.168.1.8. Vediamo che come dati viene inviato unicamente c1=10, cosa che ho fatto unicamente per semplificare il listato in questa fase della descrizione. Nel listato definitivo i dati inviati saranno diversi in base al numero di sensori e ai valori di temperatura rilevati.

Ovviamente dovrà essere scritto anche lo script php e qui si potrebbe aprire un capitolo di proporzioni sconfinate in quanto vi sono infinite possibilità sulla gestione dei dati in arrivo. Questi potrebbero essere infatti inviati ad un database, a uno o più files di testo o utilizati per creare pagine web dinamiche o altro ancora.Nel listato qui riportato viene semplicemente scritto il dato su un file di testo tempdata.txt


<?php

if ($_SERVER['REQUEST_METHOD'] == 'HEAD') // se ricevi una richiesta di tipo "HEAD"
{

$fields = explode("&", $_SERVER['QUERY_STRING']);
$values = array();

foreach ($fields as $field) {
$keyval = explode("=", $field);
$values[$keyval[0]] = $keyval[1];
}

$file = fopen('tempdata.txt','a+');
if ($file)
{
fwrite($file, time() . ':' . $values['c1']); // marcatura temporale in secondi e temperatura "c1"
fwrite($file, "\n");
fclose($file);
}
}

?>

Fin qui dovrebbe essere tutto abbastanza semplice. Ora dobbiamo decidere esattamente come stutturare i dati e come visualizzarli. Ad esempio dobbiamo prevedere la possibilità di aggiungere o rimuovere sensori nel tempo senza sballare tutto il sistema, pensare all’opportunità di tracciare grafici “real-time”, giornalieri, settimanali, mensili o annuali, calcolare il valore medio o quanto ci venga in mente. Una volta stabilito cosa e come memorizzare i dati sorge il problema della visualizzazione. Non voglio però rendere le cose complicate per cui al fine di non perdere di vista l’obiettivo prefissato preferisco mantenere le cose semplici motivo limitandoci ad inviare le letture solo del primo sensore, le memorizziamo sul file testuale e le visualizziamo in una pagina dedicata. Un approccio di questo tipo ha l’inconveniente di accumulare una gran quantità di dati sullo stesso file, cosa che col tempo diventa scarsamente gestibile, motivo per il quale è un sistema applicabile come esercizio ma non certo come monitoraggio per lunghi periodi.

La prima modifica riguarda l’invio del dato di temperatura:


Serial.println("connected");
ArduinoClient.print("HEAD /test/myscript.php?c1=");
ArduinoClient.print(temps[1]);

Lo script è invece già a posto per cui c’è solo da scrivere il software per visualizzare il grafico con le temperature. A dirla tutta, l’ispirazione per questo progetto l’ho presa da qui che a sua volta è una rielaborazione di quanto troviamo a questo articolo. Purtoppo in entrambi i casi lo script di visualizzazione dei dati è molto riduttivo e presenta alcuni errori di programmazione per cui sono intervenuto con pesanti modifiche che portano allo script qui sotto:


<?php

define('WIDTH', 1600); //>=320
define('HEIGHT', 1000); //>=200
define('FONT', 1);
define('FONT2',2);

define('UP',20);//>=20
define('DOWN',20);//>=20
define('DX',20);//>=20
define('SN',20);//>=20

// Prelvo i dati dal file
$data = array();
$file = fopen("tempdata.txt","r");
if ($file) {
while (!feof($file)) {
$line = trim(fgets($file));
if (strlen($line)) {
$fields = explode(":", $line);
$keyval = array();
$keyval['time']     = $fields[0];
$keyval['c1']       = $fields[1];
$data[] = $keyval;
}
}
fclose($file);
}

$datapoints = count($data); // conto il numero di valori

// Lines are chronological
$mintime = $data[0]['time'];
$maxtime = $data[$datapoints - 1]['time'];

$mintemp = 250;
$maxtemp = -250;
$avgtemp=0;

foreach ($data as $datapoint) // trovo il valore minimo e massimo di temperatura
{
if($datapoint['c1']<$mintemp) $mintemp=$datapoint['c1'];
if($datapoint['c1']>$maxtemp) $maxtemp=$datapoint['c1'];
$avgtemp += $datapoint['c1'];
$curtemp = $datapoint['c1'];
}
$avgtemp = round(($avgtemp / $datapoints),2);

$lowtime  = intval($mintime / 3600) * 3600;
$hightime = (intval($maxtime / 3600) + 1) * 3600;
$difftime = $hightime - $lowtime; // ore

$lowtemp  = intval($mintemp)-1;
$hightemp = intval($maxtemp)+1;
$difftemp = $hightemp - $lowtemp;

// Create the image
$image = imagecreate(WIDTH, HEIGHT);
if ($image) {
$background = imagecolorallocate($image, 255, 255, 255);
$black      = imagecolorallocate($image, 0, 0, 0);
$red        = imagecolorallocate($image, 255, 0, 0);
$blue       = imagecolorallocate($image, 0, 0, 255);
$green      = imagecolorallocate($image, 0, 195, 5);
$grey       = imagecolorallocate($image, 210,210,210);

imageline($image, SN, UP-5, SN, HEIGHT-DOWN+5, $black); // asse Y
imageline($image, SN-5, HEIGHT-DOWN, WIDTH-DX, HEIGHT-DOWN, $black); // asse X
imageline($image, SN+1, UP, WIDTH-DX, UP, $grey); // linea di chiusura superiore
imageline($image, WIDTH-DX, UP, WIDTH-DX, HEIGHT-DOWN-1, $grey); // linea di chiusura destra

for ($i = 3600; $i < $difftime; $i += 3600) // va da un'ora all'altra
{
$x = SN + (($i * WIDTH-(SN+DX)) / $difftime);
imageline($image, $x, HEIGHT-DOWN, $x, HEIGHT-DOWN+3, $black); // Tacche verticali
imageline($image, $x, HEIGHT-DOWN-1, $x, UP, $grey); // Linee Verticali
}
$temp = intval($lowtemp);

$dx=(WIDTH-(SN+DX)) / $difftime; // coefficienti per il disegno
$dy=(HEIGHT-(UP+DOWN)) / $difftemp;

for ($i = 1; $i < $difftemp; $i++)
{
$y = HEIGHT-DOWN - ($i * $dy );
imageline($image, SN-3, $y, SN, $y, $black); // Tacche verticali
imageline($image, SN+1, $y, WIDTH-DX, $y, $grey);  // Linee verticali
$temp = intval($temp) + 1;
$ytemp = intval($y) - 5;
if((HEIGHT-DOWN-UP)/$difftemp<17) // se le scritte sono troppo ravvicinate visualizza solo i gradi pari
{
if($temp % 2 == 0) imagestring($image, FONT, SN-7-(imagefontwidth(FONT) * strlen($temp)), $ytemp, $temp , $black);
}
else imagestring($image, FONT, SN-7-(imagefontwidth(FONT) * strlen($temp)), $ytemp, $temp , $black);
}

imagestring($image, FONT, SN-12, HEIGHT-DOWN+10, date("H:i", $lowtime), $black); // ora minore
imagestring($image, FONT, WIDTH-DX-15, HEIGHT-DOWN+10, date("H:i", $hightime), $black); // ora maggiore
imagestring($image, FONT, SN-7-(imagefontwidth(FONT) * strlen($lowtemp)), HEIGHT-DOWN-5, $lowtemp, $blue); // temperatura minima
imagestring($image, FONT, SN-7-(imagefontwidth(FONT) * strlen($hightemp)), UP-5, $hightemp, $red); // temperatura massima

// temperature minima, corrente, massima e media
imagestring($image, FONT2, WIDTH-290, HEIGHT-DOWN-35, "Cur. temp : " . $curtemp . " °C",$black);
imagestring($image, FONT2, WIDTH-290, HEIGHT-DOWN-20, "Avg. temp : " . $avgtemp . " °C",$green);
imagestring($image, FONT2, WIDTH-150, HEIGHT-DOWN-35, "Min. temp : " . $mintemp . " °C",$blue);
imagestring($image, FONT2, WIDTH-150, HEIGHT-DOWN-20, "Max. temp : " . $maxtemp . " °C",$red);

$avgtemp = HEIGHT-DOWN - (($avgtemp - $lowtemp) * $dy); // linea media
imageline($image,SN+1, $avgtemp, WIDTH-DX-1, $avgtemp, $green);

// posizionamento al primo punto
$prevx = ($data[0]['time'] - $lowtime) * $dx;
$prevy = ($data[0]['c1'] - $lowtemp) * $dy;

$pp=intval(0.5*(HEIGHT-DOWN-UP)/$difftemp);
//$pp=1; // togli commento per visualizzare i dati grezzi
imagestring($image, FONT2, SN+20, 8, "Risoluzione: " . $pp,$grey);

$prevym=$prevy;
if($i<$datapoints-$pp) $prevxm=($data[intval($pp/2)]['time'] - $lowtime) * $dx;
// Draw line from previous point to current point
for ($i = 1; $i < $datapoints; $i++) {

$x = ($data[$i]['time'] - $lowtime) * $dx;
$y = ($data[$i]['c1'] - $lowtemp) * $dy;
//imageline($image, $prevx + SN, HEIGHT-DOWN - $prevy, $x + SN, HEIGHT-DOWN - $y, $red); // dato grezzo

if($i<$datapoints-$pp)
{
$ym=0;
$xm= ($data[$i+$pp/2]['time'] - $lowtime) * $dx;
for($k=0;$k<$pp;$k++) $ym+=$data[$i+$k]['c1']; // sommo i dati
$ym=$ym/$pp; // e divido per fare la media

$ym=($ym - $lowtemp) * $dy;
imageline($image, $prevxm + SN, HEIGHT-DOWN - $prevym, $xm + SN, HEIGHT-DOWN - $ym, $blue); // dato medio

$prevym=$ym;
$prevxm=$xm;
}

$prevx = $x;
$prevy = $y;
}

// ora e data del grafico
$generated = 'Data e Ora: ' . date("r");
imagestring($image,
FONT,
WIDTH/2  - (imagefontwidth(FONT) * strlen($generated)/2),
HEIGHT -DOWN+8,
$generated,
$blue);

// Output the image

header('Content-Type: image/png');
imagepng($image);

// Destroy it
imagedestroy($image);
}

?>

Purtoppo non posso descriverlo nei dettagli, ci vorebbe un intero articolo solo per questo. Ho comunque commentato estesamente il listato e qui vi sottolineo alcune delle più importanti caratteristiche. Innanzitutto nelle prime righe c’è la definizione della grandezza dell’immagine (larghezza e altezza) che a differenza dello script originale modifica anche la grandezza del grafico contenuto e non solo l’area bianca sullo sfondo. Inoltre sono impostabili i 4 bordi in modo indipendente, in questo modo è possibile ad esempio modificare lo script per inserire un logo, un titolo o quanto vogliate. L’intero script è un’ottima partenza per una funzione che disegna grafici generici per cui spero possiate apprezzare lo sforzo che ho fatto per darvi un listato di buon livello. L’altra modifica importante che ho voluto apportare riguarda la visualizzazione. Qui sotto vi mostro la visualizzazione originale di un campione di dati raccolto:

Come potete vedere se l’escursione termica è piccola si nota vistosamente il rumore di fondo delle misurazioni il che è decisamente brutto da vedersi. Lo stesso comportamento lo potete vedere negli articoli da cui ho preso ispirazione. A tal proposito ho creato un semplice algoritmo che visualizza non il dato grezzo com’è acquisito ma mostra la media di tot punti che lo succedono, fatto che però impedisce di visualizzare gli ultimi dati acquisiti in quanto non hanno un campione dati sufficiente per creare la media. Quanti punti dobbiamo usare per la media? Più ne usiamo e più il grafico è lineare ed omogeneo ma meno sensibile alle variazioni termiche e in più si riduce il campione dati utilizzabile. Ho fatto perciò delle prove ed ho automatizzato il processo facendo in modo che il numero di punti varia al variare dell’escursione termica da visualizzare in modo da ottenere sempre il miglior bilanciamento. Il risultato è il seguente:

E per finire vi mostro la sovrapposizione dei due grafici. Per fare ciò non dovete far altro che intervenire sulle linee di listato che sono commentate ed autoesplicative. Vi faccio notare che in alto a sinistra, sotto la voce “risoluzione” è scritto il numero di punti interpolati.

Purtoppo nel grafico a bassa risoluzione la linea blu viene sovrastata e si vede male, ma vi assicuro che su un grafico 1400×1000 fà decisamente un’altro effetto. Ultima modifica fatta riguarda la visualizzazione dei gradi celsius sull’asse Y, infatti se ci sono grosse escursioni termiche i numeri si avvicinano al punto da non essere più leggibili per la loro sovrapposizione, motivo per cui nel caso ci sia un’eccessiva vicinanza vengono visualizzati solo i numeri pari, olte alla temperatura minima in blu e massima in rosso. Come sempre vi faccio notare che anche se non capite nulla del listato, potete semplicemente metterlo così com’è su un server, nella stessa cartella in cui ci sono i dati di temperatura raccolti, e lo richiamate via web per avere il grafico bello che pronto.