Internetowy termometr

Jak już kiedyś pisałem, w domu mam kilkanaście termometrów, podłączonych do systemu autmatyki domowej. Połączone są one równolegle przez magistralę dwuprzewodową do konwertera 1-wire, który z kolei współpracuje z routerem. A raczej tak było. Ponieważ teraz zmieniam nieco architekturę systemu, a dane zbierać ma Raspberry Pi, znajdujący się w innym niż router miejscu, postanowiłem zmienić też sposób zbierania temperatur. W ten sposób powstało urządzenie, oparte o Arduino, które odczytuje dane z termometrów DS28B20 i regularnie raportuje odczyty przez ethernet do serwera www. Jest to przy okazji test długodystansowy urządzenia, które praktycznie można zastosować w wielu małych kotłowniach.

Urządzenie do pomiaru temperatur przez Internet

Urządzenie mieści się w niewielkiej obudowie hermetycznej, która bardzo dobrze nadaje się do takich projektów. Sercem układu jest, opisywany już, Arduino Pro Mini w wersji 5V. Za komunikację IP odpowiada zaś układ ENC28J60, o którym także już wspominałem. Arduino zasilane jest przez wbudowany stabilizator z zasilacza o napięciu 7,5V (jako, że taki miałem pod ręką), natomiast dla układu ethernetowego zastosowany jest zewnętrzny stabilizator 3,3V, który dodatkowo wymaga użycia dwóch kondensatorów (wszystkie 3 elementy umieszczone są na zielonej płytce).

Odczyty z urządzeń 1-wire w przypadku Arduino są bardzo proste. Ponieważ w domu jest już instalacja dwuprzewodowa (zamiast 3 żył) i odpowiednio podłączone termometry, to zastosowane zostało tzw. zasilanie pasożytnicze (parasite power). Ponieważ mój stary konwerter serial-1wire, który był podłączony do routera, miał wyjście typu RCA (cich), taki sam standard zachowałem przy nowym urządzeniu. Wszystkie połączenia są na tzw. goldpiny, bo uważam, że to bardzo praktyczne i wygodne rozwiązanie dla urządzeń, które potencjalnie mogą się nieco zmienić (np. zmiana podłączonego pinu Arduino jest bardzo prosta). Dodatkowo na zielonej płytce widać wyprowadzenie dla czujnika wilgotności z rodziny DHT, który być może w przyszłości będę chciał zastosować.

Jedyną rzeczą, którą trzeba było poprawić w tym co widać na zdjęciu powyżej, to zmiana opornika na magistrali 1wire. Standardowe 4700Ohm okazało się zbyt dużą wartością dla tak długiej magistrali, dlatego rezystor został wymieniony ostatecznie na posiadający mniejszą rezystancję. Po zmianie wszystkie 15 termometrów znajdowane i odczytywane jest bez problemu.

Pomiar temperatur - Arduino

 

Co od strony programowej? Prosta pętle, w której odczytywane są po kolei temperatury z kolejnych termometrów. Co jakiś czas robiony jest test, czy jakiś nowy termometr nie został dodany. Cyklicznie zaś pomiary wysyłane są, wraz z numerami seryjnymi termometrów, do serwera www (Raspberry Pi z Node.js).

#include <UIPEthernet.h>
#include <OneWire.h>

#define OW_PIN 7
#define MAX_ONEWIRE 20
#define OW_DELAY 900

#define HOST "192.168.10.27"
#define URL "/sensor.php?p="
IPAddress servip(192,168,10,1);
IPAddress myIP(192,168,10,111);
byte gateway[] = { 192, 168, 10, 1 };
byte subnet[] = { 255, 255, 255, 0 };
IPAddress mydns(8,8,8,8);

#define SEND_INTERVAL 30000
#define DISCOVER_INTERVAL 3000000

EthernetClient aclient;
OneWire owbus(OW_PIN);

byte onewAddr[MAX_ONEWIRE][8]; // adresy termometrów
float onewTemp[MAX_ONEWIRE]; // temperatury
int onewFoundNum=0; // liczba znalezionych termometrów

char current=-1;
unsigned long time;
unsigned long nextSendTime=10000;
unsigned long nextDiscoverTime=100000;

void setup()
{
  Serial.begin(9600);
  uint8_t mac[6] = {0x00,0x01,0x02,0x03,0x04,0x11};
  
  //inicjalizacja tablicy temperatur
  for( int w=0; w<MAX_ONEWIRE; w++ )
  {
    onewTemp[w]=-100;
  }
  
  // wyszukiwanie termometrów
  onewDiscoverDevices();
  onewDiscoverDevices();

  Ethernet.begin(mac,myIP, mydns, gateway, subnet);  
}

void loop()
{
  time=millis();
  current++;
  if( current>=onewFoundNum ) current=0;

  getTempFrom(current);
    
  if( isAfter(nextSendTime) )
  {
    nextSendTime=time+SEND_INTERVAL;
    sendTempTo();
    if( isAfter(nextSendTime) ) nextSendTime=time;
  }

  if( isAfter(nextDiscoverTime) )
  {
    nextDiscoverTime=time+DISCOVER_INTERVAL;
    onewDiscoverDevices();
  }
}


// czy czujnik jest już znany, jeżeli tak, zwraca indeks w tablicy
int onewFoundInArray(byte addr[])
{
  int w,q;
  boolean found;
  
  for( w=0; w<onewFoundNum; w++ )
  {
    found=true;
    for(q=0;q<8;q++) if(onewAddr[w][q]!=addr[q]) found=false;
    if(found) return(w);
  }
  
  return(-1);
}

void onewDiscoverDevices(void) 
{
  byte addr[8];
  int q;

  while(owbus.search(addr)) 
  {
    if( onewFoundInArray(addr)==-1 )
    {
      if ( OneWire::crc8( addr, 7) == addr[7] ) //crc ok
      {        
        for(q=0;q<8;q++) onewAddr[onewFoundNum][q]=addr[q];
        onewFoundNum++;
      }      
    }
  }
  owbus.reset_search();
  return;
}

boolean isAfter( unsigned long timer )
{
  if(timer<=time && !(time>3000000000L && timer<1000000000L) ) return(true);
  if(timer>3000000000L && time<1000000000L) return(true);
  return(false);
}

//ilość wolnej pamięci
int freeMemory() {
  int size = 1024;
  byte *buf;
  while ((buf = (byte *) malloc(--size)) == NULL);
  free(buf);
  return size;
}

// odczytuje temperature z wybranego termometru i wpisuje to tablicy
void getTempFrom(char index)
{
  byte present=0;
  byte data[12];
  
  owbus.reset();
  owbus.select(onewAddr[index]);
  owbus.write(0x44,1);

  delay(OW_DELAY);

  present = owbus.reset();
  owbus.select(onewAddr[index]);
  owbus.write(0xBE);

  for ( int i = 0; i<9; i++) {
    data[i] = owbus.read();
   }
  
  // dane poprawne
  if( OneWire::crc8( data, 8) == data[8] )
  {
    unsigned int raw = (data[1] << 8) | data[0];

    byte cfg = (data[4] & 0x60);
    if (cfg == 0x00) raw = raw << 3;
    else if (cfg == 0x20) raw = raw << 2;
    else if (cfg == 0x40) raw = raw << 1;
  
    float celsius = (float)raw / 16.0;
    float fahrenheit = celsius * 1.8 + 32.0;
    if( celsius>-80.0 && celsius<150 )
    {
      onewTemp[index]=celsius;
    }
  }
}

boolean sendTempTo()
{   
  int size;
  
  Ethernet.maintain();  
  
  if (aclient.connected()) { aclient.stop(); }
  if (aclient.connect(HOST, 80)) 
  {
    aclient.print("GET ");
    aclient.print(URL);
    aclient.print(onewFoundNum);
    aclient.print(",");
    for( int w=0; w<onewFoundNum; w++ )
    {
      aclient.print( onewAddr[w][0], HEX );
      aclient.print( onewAddr[w][1], HEX );
      aclient.print( onewAddr[w][2], HEX );
      aclient.print( onewAddr[w][3], HEX );
      aclient.print( onewAddr[w][4], HEX );
      aclient.print( onewAddr[w][5], HEX );
      aclient.print( onewAddr[w][6], HEX );
      aclient.print( onewAddr[w][7], HEX );
      aclient.print(",");
      aclient.print(onewTemp[w]);
      aclient.print(",");
    }
    aclient.print(freeMemory());    
    aclient.println(" HTTP/1.0");
    aclient.print( "Host: " );
    aclient.println( HOST );
    aclient.println();
    unsigned long tout=millis()+5000;
    while(aclient.available()==0)
    {
      time=millis();
      if ( isAfter(tout) )
      {
        aclient.stop();
        return(true);
      }      
    }
    aclient.stop();
    return(true);
  } 
  else 
  {
    return(false);
  }  
  
}

Jak widać, żadnej wiedzy tajemnej nie ma, ale działa to sprawnie (na razie kilka tygodni bezproblemowo). Fragmenty dotyczące odczytu 1-wire zostały zagarnięte z przykładów dostępnych w Internecie. Do obsługi ENC28J60 została użyta biblioteka UIPEthernet.

Trochę ciekawej jest po drugiej stronie. Informacje od urządzenia odbiera Raspberry Pi z działającym na nim serwerem w node.js. Tak, to javascript po stronie serwera. Póki co dość niszowo, ale bardzo podoba mi się ta technologia, mam już produkcyjne doświadczenia z programem działającym od pół roku – jest stabilnie i wydajnie. Dlaczego nie skrypty w php? Tak, sprawdziłyby się do tego idealnie, a node.js to może trochę sztuka dla sztuki. Tak jest, o ile nie weźmiemy pod uwagę dalszego rozwoju systemu. W przyszłości na node.js ma być oparty główny program inteligentnego domu. Przypomnę, że do tej pory sercem systemu były routery z OpenWRT oraz skrypty shellowe i php. Teraz natomiast chcę pójść w kierunku jak najpełniejszego i najszybszego przetwarzania informacji pochodzących z wielu czujników. Program działający na stałe i trzymający w pamięci stan wszystkiego jest porządany. W szczególności planuję bezpośrednią integrację z układem radiowym nRF24L01+ oraz z centralą alarmową Satel Integra (przez port szeregowy).

Póki co uruchomiłem Raspberry Pi w dużej obudowie, zasilane z zasilacza buforowego ze sporym akumulatorem – tak by wyłączenie prądu na kilkanaście godzin nie było dla niego problemem – to dość naturalne, skoro przetwarza dane m.in. z centrali alarmowej.

Jak wygląda w tej chwili program w node.js? Podstawowy plik:

var temperatury={};
var config=require('./config');
var sys = require('sys');
var exec = require('child_process').exec;
var app = require('http').createServer(handler);
var lastSensor=0; // czas ostatniego otrzymania danych z sensora

app.listen(config.listenPort,config.listenHost);
console.log("listening...");

logRRD();

function handler (req, res) {

  var reqok=false;
  var stats;

  if( config.debug ) console.log("http request: "+req.url );

  if( req.url.substring(0,14)=="/sensor.php?p=" )
  {
    if( req.url.length>14 )
    {
      lastSensor=getTimestamp();
      res.writeHead(200, {'Content-Type': 'text/plain'} );

      var reqval = req.url.substring(14).split(",");
      if( config.debug ) console.log("termometry: "+reqval[0]);

      for( var w=0; w<reqval[0]; w++ )
      {
        if( config.termId[reqval[1+2*w]]==undefined ) console.log("Nieznany termometr "+reqval[1+2*w]);
        else
        {
          temperatury[config.termId[reqval[1+2*w]]]=reqval[2+2*w];
          if( config.debug ) console.log(config.termId[reqval[1+2*w]]+": "+reqval[2+2*w]);
        }
      }
      reqok=true;
      res.end();
    }
  }

  if( !reqok )
  {
    res.writeHead(404);
    res.end();
    console.log("Url not found: "+req.url);
  }

}

function getTimestamp()
{
  return( Math.round(+new Date()) );
}

function execcmd(cmd)
{
  var child = exec(cmd, function (error, stdout, stderr) {
  if( config.deug ) console.log(stdout);
  if( config.deug ) console.log(stderr);
  if (error !== null) {
    console.log('exec error: ' + error);
  }
});

}

//sprawdza czy temperatura jest poprawnie zdefiniowana i jest prawid~Bowo odczytana
function isTempOk(index)
{
  if( temperatury[index]==undefined ) return(false);
  if( temperatury[index]120 || temperatury[index]==85 ) return(false);
  return(true);
}

// loguje temperatury przed rrdtool
function logRRD()
{
  var cmd;
  if( config.debug ) console.log("logRRD();");

  if( isTempOk('taras') && getTimestamp()-lastSensor<5*60*1000 )
  {
    cmd="/usr/bin/rrdtool update /dom/rrd/zewn.rrd "+Math.floor(getTimestamp()/1000)+":"+temperatury['taras'];
    if( config.debug ) console.log(cmd);
    execcmd(cmd);
  }
  if( isTempOk('zewn') && getTimestamp()-lastSensor<5*60*1000 )
  {
    cmd="/usr/bin/rrdtool update /dom/rrd/zewn2.rrd "+Math.floor(getTimestamp()/1000)+":"+temperatury['zewn'];
    if( config.debug ) console.log(cmd);
    execcmd(cmd);
  }


  if(isTempOk('szafka') && isTempOk('garaz') && isTempOk('kotlownia') && isTempOk('kuchnia') && isTempOk('piwnica') && isTempOk('pokoj2') && isTempOk('pokoj1') && isTempOk('salon') && getTimestamp()-lastSensor<5*60*1000 )
  {
    cmd="/usr/bin/rrdtool update /dom/rrd/pomiary.rrd "+Math.floor(getTimestamp()/1000)+":"+temperatury['szafka']+":"+temperatury['garaz']+":"+temperatury['kotlownia']+":"+temperatury['kuchnia']+":"+temperatury['piwnica']+":"+temperatury['pokoj2']+":"+temperatury['pokoj1']+":"+temperatury['salon'];
    if( config.debug ) console.log(cmd);
    execcmd(cmd);
  }

  if(isTempOk('cwu zasilanie') && isTempOk('kotlownia') && isTempOk('cwu wyjscie') && isTempOk('kociol zasilanie') && isTempOk('kociol powrot') && isTempOk('co zasilanie') && getTimestamp()-lastSensor<5*60*1000 )
  {
    cmd="/usr/bin/rrdtool update /dom/rrd/kotlownia.rrd "+Math.floor(getTimestamp()/1000)+":"+temperatury['cwu zasilanie']+":"+temperatury['kotlownia']+":"+temperatury['cwu wyjscie']+":"+temperatury['kociol zasilanie']+":"+temperatury['kociol powrot']+":"+temperatury['co zasilanie'];
    if( config.debug ) console.log(cmd);
    execcmd(cmd);
  }

  setTimeout( logRRD, config.logTempInterval );
}

Do tego config.js z konfiguracją – m.in. numerami seryjnymi termometrów:

var listenPort=8080;
var listenHost="192.168.10.27";
var logTempInterval=59000;

var termId={};

termId['28A0598A200A9']='szafka';
termId['28F0486740033']='kociol zasilanie';
termId['28F83B8A200B4']='co zasilanie';
termId['2884578A20061']='kuchnia';
termId['2844628A200A8']='cwu wyjscie';
termId['28345B8A20047']='piwnica';
termId['286C418A2009']='pokoj1';
termId['2824A8A200A9']='pokoj2';
termId['28EA408A2009C']='cwu zasilanie';
termId['282E568A2003F']='salon';
termId['28715A8A20014']='garaz';
termId['2819308A200DA']='taras';
termId['2819350100EE']='kotlownia';
termId['28A5758A2009B']='kociol powrot';
termId['28342E840067']='zewn';

exports.debug=true;

exports.termId=termId;
exports.listenPort = listenPort;
exports.listenHost = listenHost;
exports.logTempInterval=logTempInterval;

Co robi program? Na razie nic wielkiego – to samo co robiły porzednio skrypty. Przede wszystkim jest gotowy do rozbudowy. Podstawą nadal jest rrdtool i dane gromadzone w tym narzędziu – i dalej skrypty rysują wykresy, itp. To odróżnia ten program od poprzedniego rozwiązania, to uruchomienie serwera www i zapamiętywanie przesyłanych danych – na razie z jednego urządzenia, ale w przyszłości z wielu. Serwer dodatkowo cyklicznie zapisuje ostatnie znane wartości do bazy rrd. Oczywiście przy pierwszym kontakcie może się to wydawać bardziej skomplikowane niż php, które w tym wypadku byłoby prostsze i dla większej liczby osób czytelne, ale tutaj chodzi przede wszystkim o elastyczność. Ten sam serwer będzie mógł także prezentować dane, np. przez webservice w czasie rzeczywistym.

Oczywiście nic nie stoi na przeszkodzie, żeby ktoś zbudował sobie analogiczne urządzenie i napisał proste skrypty w php do gromadzenia danych. Będzie działać tak samo dobrze.

Wprawne oko pewnie wychwyci, że program w node.js nie otwiera portu 80, do którego odwołuje się Arduino. Dzieje się tak dlatego, że serwer działa na prawach zwykłego użytkownika, a przekierowanie portu jest wykonane przez iptables. To jednak tylko techniczny szczegół nie mający związku z urządzeniem do pomiaru temperatury.

Ten wpis został opublikowany w kategorii Inteligentny Dom i oznaczony tagami , , , , , . Dodaj zakładkę do bezpośredniego odnośnika.

0 odpowiedzi na Internetowy termometr

  1. Andrzej pisze:

    Jakiego użyłeś stabilizatora do zasilenia enc28j60?

  2. Pio pisze:

    Można prosić więcej o integracji z SATEL’em ??

  3. rs pisze:

    A jak jest z utrzymaniem ciągłej pracy serwera opartego na node.js.
    Mam na malince postawiony serwer oparty na LAMPPie. Pozwala to na obsługę plików php. Wadą takiego rozwiązania jest to, że odpalona aplikacja działa tak długo jak żyje sesja. Przeglądarki tekstowe pracujące w trybie terminala nie nadają się do tego celu. Są nieprzydatne i za słabe. Trzeba używać pełnosprawnych przeglądarek w trybie graficznym. W przypadku pracy tylko ssh nie ma praktycznie rozwiązania na uruchomienie strony z pełno sprawnymi przeglądarkami.
    Czy na serwerze z node.js mozna wysłać maila po otrzymaniu alertu (gdy zostanie przekroczona wartość progowa).

  4. Pluto pisze:

    Fajny jest ten moduł ethernetowy, też się nim bawię, tylko z inną biblioteką. W swoim zestawie też będę go używał do łączności z Raspberry Pi.
    W swoich zastosowaniach używam Pro Mini na 3,3V Zasilanie przez PoE, ale trzeba przed wtyczką RJ45 wyprowadzić odpowiednie żyły z zasilaniem i podłączać osobną wtyczką. Dalej przetwornica step-down, np. https://sklep.avt.pl/modul-przetwornica-lm2596-impulsowa-zasilania-dc-dc.html
    U siebie też postawiłem zasilacz buforowy, mój jest na dwa akumulatory i daje 24V (27V). Większe napięcie to mniejsze straty na przesyle przy PoE a step-down i tak sprawnie zejdzie z napięciem.

Skomentuj techniczny Anuluj pisanie odpowiedzi

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *