Domofon z powiadomieniem e-mailowym

Uruchomiłem już “internetowy domofon”. Za każdym razem gdy ktoś przy furtce wciśnie przycisk domofonu, dostaję powiadomienie e-mailowe z linkiem do zdjęcia drogiego gościa. Ponieważ w telefonie używam K-9 mail z obsługą IMAP IDLE, jako programu pocztowego, więc mail dociera do mnie w ciągu kilku sekund. Dzięki temu wiem, kiedy ktoś mnie odwiedza a nie ma mnie w domu. Gdy przyjdzie ktoś znajomy, mogę zadzwonić i powiedzieć, żeby poczekał, bo właśnie wracam do domu. Wkrótce będę mógł też otworzyć z telefonu furtkę i wpuścić gościa na posesję. Podobnie w przypadku śmieciarzy, gdy przyjadą odebrać śmieci, a furtka będzie zamknięta. Gdyby natmiast ktoś chciał się włamać, to istnieje pewne prawdopodobieństwo, że zadzwoni wpierw domofonem, żeby upewnić się, że nikogo nie ma w domu - wtedy jego zdjęcia trafią bezpiecznie na serwer internetowy poza domem.

Jak to działa? Opisywałem ostatnio integrację domofonu Eura ADP-05A7 z Arduino od strony sprzętowej. Teraz pora zająć się częścią programistyczną. Przy okazji powstaną zaczątki głównego modułu oprogramowania inteligentnego domu . Zgodnie z tym, co przedstawiałem wcześniej, Arduino Mega kontroluje różne zdarzenia (w tym wciśnięcie przycisku domofonu) i przez protokół http przekazuje informacje o nich do systemu nadrzędnego - routera z OpenWRT. Protokół komunikacyjny jest oczywiście już zdefiniowany . Jeżeli wydaje się to skomplikowane, to proponuję zajrzeć do wpisu o programowaniu systemu podlewania . Warto go przeczytać, choćby ze względu na to, że część oprogramowania po stronie Arduino będzie wspólna i jest tam szczegółowo opisana. Nie będziemy tworzyć wszystkiego od nowa, tylko dopiszemy dodatkowe funkcje.

Programowanie Arduino

Jak już wspomniałem, ta część będzie się opierać w dużej mierze na wcześniejszych pracach nad systemem nawadniania . Dla obsługi domofonu dołożyć trzeba odczytywanie i interpretację stanów wejść analogowych. Nie potrzebujemy dokładnych pomiarów napięcia, ineresuje nas tylko to, w jakim stanie jest domofon, czyli w jakim zakresie utrzymuje się napięcie na linii zasilającej bramofon. Dla przypomnienia - 0V oznacza wciśnięty przycisk bramofonu, trochę ponad 1V to stan oczekiwania (nic się nie dzieje), a około 12V oznacza podniesioną słuchawkę i rozmowę. Po drugiej stronie transoptora przekłada się to na odczyty wejścia analogowego Arduino na poziomie około 0, 300 i 1018. Jak widać wartości są odległe, więc jakiekolwiek zakłócenia nie powinny wpływać na prawidłową interpretację stanu (może poza momentami podnoszenia czy odkładania słuchawki, ale z tym łatwo sobie poradzić).

Wprowadźmy więc tablice dla wejść analogowych, analogicznie jak wcześniej było to z wejściami i wyjściami cyfrowymi. Dodatkowo, dla czytelności kodu, stała INTERCOM będzie oznaczać indeks tablicy odpowiadający domofonowi, a INTLEVEL1 i 2 będą określać granice mędzy stanami.

// tablica wejść analogowych
int analog_state[]={0,1,0,0,0,0,0};
int analog_pins[]={2,3,4,5,6,7,8,9};
unsigned long analog_lastchange[]={0,0,0,0,0,0,0};
#define ANALOG_SIZE 7 // liczba elementów w tablicy

#define INTERCOM 1
// numer wejścia analogowego (w tablicy) dla domofonu

#define INTLEVEL1 10
#define INTLEVEL2 700
#define INTDEBOUNCETIME 1000

Skąd wiedziałem, jakie są wartości odczytów dla różnych stanów domofonu? Przez zdefiniowanie komendy a (dla wyjaśnienia - protokół komunikacyjny ) w funkcji runCommand (opisywanej we wpisie o programowaniu systemu nawadniania ) odczytującej aktualny stan wejścia analogowego sprawiłem, że dało się to sprawdzić przez przeglądarkę internetową.

    case 'a': // pytanie o stan wejścia analogowego
      w=param.toInt();
      if( w<0 || w>ANALOG_SIZE ) return("Error");
      return( String(analogRead(analog_pins[w])) );
      break;

Mając te dane, pozostało napisanie prostej funkcji (i dodanie jej do głównej pętli - loop).

// sprawdza stan domofonu
void checkIntercom()
{
  int inp, state;

  inp=analogRead(analog_pins[INTERCOM]);

  state=1;
  if( inp<INTLEVEL1 ) state=0;
  if( inp>INTLEVEL2 ) state=2;

  if(state!=analog_state[INTERCOM] && analog_lastchange[INTERCOM]+INTDEBOUNCETIME>time )
  {
    analog_lastchange[INTERCOM]=time;
    analog_state[INTERCOM]=state;
    sendMessage('a', String(String(INTERCOM)+state) );
  }
}

Wyjaśnienia wymaga jeszcze zastosowanie stałej INTDEBOUNCE. Jest to zabezpiecznie przed odczytami “na granicy” - częstymi zmianami związanymi np. z niedoskonałością przycisków. Często przy przyciskaniu lub zwalnianiu przycisku następuje zjawisko bardzo krótkiego (na ułamki sekund) załączania i wyłączania styku. Ponieważ nas nie interesuje czy ktoś zwolnił przycisk sekundę wcześniej, czy później, czy wcisnął przycisk raz czy 5 razy w ciągu sekundy, więc dajemy sobie duży margines tolerancji, żeby wyeliminować tę niedoskonałość.

Gdy zmienia się stan, wysyłamy komunikat a (nie mylić ze znaczeniem a wysyłanego w drugą stronę) z nowym stanem wejścia. Np. gdy ktoś zadzwoni, to wyślemy xa10 (gdzie x to identyfikator Arduino), natomiast gdy rozmowa zostanie odebrana - xa12.

Jakie jest tu zagrożenie, które łatwo przeoczyć? Przekroczenie zakresu wewnętrznego zegara, ale tym chwilowo nie będziemy się przejmować, a wyeliminujemy to przy opisie sposobu radzenia sobie z tym problemem (wkrótce). Ponieważ po stronie Arduino to już wszystko, więc można przejść do OpenWRT.

Router z OpenWRT

Nie będę oczywiście opisywał jak wgrać OpenWRT do routera, bo temat jest zbyt szeroki i dobrze opisany w sieci. Poza tym tak naprawdę, czy ktoś używa routera z OpenWRT, Raspberry Pi czy komputera z Linuksem, nie ma tu znaczenia - wszystko będzie działać tak samo. Ograniczę się tylko do kilku słów dotyczących konfiguracji i programów wymaganych do działania (oczywiście, jak to w Linuksie, można spokojnie zastosować inne). OpenWRT działa na TP-Linku WR-1043ND z podłączonym zewnętrznym dyskiem USB (i exrootem). Zainstalowane jest też php na serwerze uhttpd. Do wysyłania poczty służy mstmp.

Zacznijmy od php. Skrypt cmd.php ma za zadanie odbierać polecenia z Arduino, zgodnie z protokołem komunikacyjnym . Jak na razie skrypt jedynie zapisuje wszystko do logu, a dodatkowo reaguje na komendę oznaczającą wciśnięcie przycisku dzwonienia w bramofonie.

<?php

$c=$_GET['c'];

exec("echo `date` ".$_SERVER['REQUEST_URI']." >> /tmp/arduino.log");

if( $c=="xa10" ) passthru( "/dom/domofon-event > /dev/null &" );

?>

Jak widać. w przypadku wykrycia dzwonienia (xa10 odebrane od Arduino), wywołujemy skrypt domofon-event. Jego wyjście przekierowujemy do /dev/null, bo chcemy od razu zakończyć połączenie z Arduino, a w przeciwnym wypadku skrypt php zakończyłby się dopiero po skończeniu działania przez domofon-event. I to na razie tyle działki php. Reszta to proste skrypty shellowe.

Za robienie zdjęć gościom odpowiada, opisywana wcześniej,  kamera Edimax IC-3030 . Skonfigurowana jest w ten sposób, że po wywołaniu URLa /snapshot.jpg można pobrać aktualny widok z kamery. Skrypt powinien więc pobrać obraz, wysłać go na serwer www, powiadomić mnie mailem o fakcie odwiedzin z podaniem odnośnika do wykonanego zdjęcia. Potem jeszcze dla pewności zostaną zapisane 2 zdjęcia i również wysłane na serwer (już bez powiadomienia). Dodatkowo zagwarantujemy sobie, że gdy nie zakończyło się jeszcze robienie i wysyłanie zdjęć - nie robimy następnych (żeby zabezpieczyć się przed nadpobudliwym wciskaczem przycisku i wielokrotnym uruchamianiem skryptu). Program w ash shellu (prostszy bash) jest krótki:

#!/bin/ash

cd /mnt/kamera1_storage_path/

EVENT=/dom/event/domofon.txt
SURFIX=`date +%Y-%m-%d-%H-%M-%S`
URL=192.168.15.123/snapshot.jpg

if [ -e $EVENT ]
then
  exit
fi

touch $EVENT

wget $URL -O intercom-$SURFIX.jpg
/usr/skrypty/mailsend "Dzwonek do domofonu http://www.mojserwer.www/sciezka/intercom-$SURFIX.jpg"
/dom/sendwebcam
sleep 1
SURFIX=`date +%Y-%m-%d-%H-%M-%S`
wget $URL -O intercom-$SURFIX.jpg
sleep 5
SURFIX=`date +%Y-%m-%d-%H-%M-%S`
wget $URL -O intercom-$SURFIX.jpg
/dom/sendwebcam

rm $EVENT

W pierwszej kolejności sprawdzamy, czy plik zapisany jako $EVENT istnieje (czyli inna instancja skryptu się wykonuje). Jeżeli tak, kończymy działanie. Jeżeli nie - tworzymy go. Potem ściągamy z kamery obraz i zapisuemy pod określoną nazwą. Następnie wysyłamy powiadomienie z linkiem do zdjęcia (nie ma go jeszcze na serwerze) i uruchamiamy skrypt wysyłający oczekujące zdjęcia (w tym wypadku jedno) na zewnętrzny serwer www. Po odczekaniu sekundy, a potem pięciu robimy kolejne zdjęcia i znowu wysyłamy (bez powiadomienia). Na koniec pozostaje uzunąć plik blokujące wielokrotne uruchomienie. Co jeżeli wyłączymy prąd podczas wykonywania skryptu (szansa minimalna, ale zawsze)? Zostanie plik z lockiem i całość przestanie działać. Ale bez strachu - przy starcie i co jakiś czas uruchamiam skrypt czyszczący stare blokady - na wszelki wypadek.

Co zawiera sendwebcam?

#!/bin/sh
DIR=/mnt/kamera1_storage_path
mv $DIR/*.jpg $DIR/m/
scp -i /root/.ssh/id_rsa $DIR/m/*.jpg webcam@mojswerwerwww:~
[ $? -eq 0 ] && {
rm $DIR/m/*.jpg
}

Nic skomplikowanego - przenosi wszystkie pliki zdjęć do podkatalogu i wysyła je przez scp. Gdy operacja się uda - usuwa dane z podkatalogu. Gdybyśmy chcieli wykonywać skrypty wysyłające równolegle (a nie chcemy, bo zabezpieczyliśmy się wcześniej), to należałby generować nazwę podkatalogu i go tworzyć przy każdym wykonaniu.

Do wyjaśnienia pozostał jeszcze banalny skrypt mailsend, wysyłający przez program mstmp e-maila z treścią przyjętą jako parametry.

NOW=`date -u +%s`
PLIK=/tmp/mail$NOW.txt

echo "From: d@mojserwer.pocztowy" > $PLIK
echo "To: ja@mojserwer.pocztowy" >> $PLIK
echo "Subject: Dom" >> $PLIK
echo "" >> $PLIK
echo $1 >> $PLIK

/usr/bin/msmtp -t < $PLIK
rm $PLIK

I to już wszystko. Działa jak zaprojektowano. Jak widać, nie było ciężko. To świetnie obrazuje, że posiadanie już podstaw instalacji inteligentnego domu sprawia, że dokładanie kolejnych funkcji jest bardzo proste.