niedziela, 29 czerwca 2014

OpenHab, arduino, inteligentny dom, sterowanie za pośrednictwem protokołu TCP

Jakiś czas temu obiecałem opisanie sterowania urządzeniami za pośrednictwem Arduino i OpenHab przy pomocy protokołu TCP.
Niestety nie jest to tak bezproblemowe jak mogłoby się wydawać, więc nie zdecydowałem się na wykorzystanie takiego rozwiązania w produkcyjnych warunkach, ale opiszę to co udało mi się ustalić. Być może komuś się przyda.

Zacznijmy zatem od początku.
W pierwszym kroku należy pobrać OpenHab RuntimeCore.
Znajdziemy je pod adresem http://www.openhab.org/downloads.html
Po rozpakowaniu archiwum zip, należy zajrzeć do katalogu configurations.
Konieczne będzie utworzenie tzw. sitemap'y. Jest to struktura głównego menu, w którym możemy grupować sobie urządzenia na przykład wg pomieszczeń w których się znajdują, czyli salon, łazienka, piętro itd. 
W moim przykładzie będę chciał sterować tylko jednym urządzeniem, załóżmy że będzie to taśma led w salonie.
Zatem moja sitemap'a będzie znajdowała się w pliku house.sitemap w podkatalogu sitemaps i będzie zawierała następujące wpisy:

sitemap Dom label="Menu główne"
{
Frame {
Group item=Salon label="Salon" icon="firstfloor"
}

}

Następnie musimy utworzyć plik house.items w podkatalogu items, gdzie określamy kontrolery naszych urządzeń. Dla naszego przypadku testowego będzie on wyglądał następująco:
Group All
Group Salon "Salon" (All)
/*Lights */
Switch Light_Salon_LED_Tape "Taśmy LED - salon" (All, Salon) {tcp=">[ON:192.168.0.5:8888:MAP(salonled.map)], >[OFF:192.168.0.5:8888:MAP(salonled.map)], <[192.168.0.5:*:JS(salonled.js)]"}
view raw house.items hosted with ❤ by GitHub

Znalazł się w nim jeden element typu Switch, który może przyjmować wartości ON oraz OFF.
Następnie jest informacja iż obsługa przełącznika ma być realizowana za pośrednictwem protokołu TCP, oraz konfiguracja wyjściowa (elementy ze znakiem ">") i wejściowa (elementy ze znakiem "<").
Określone zostało również mapowanie komend które zostanie wykorzystane przy zmianie stanu przycisku. Mapowanie znajduje się w pliku salonled.map. Pliki mapowań umieszczamy w podkatalogu transform. Plik salonled.map w moim przypadku wygląda następująco:

ON=salonled#
OFF=salonled#

Oznacza to że zarówno przy komendzie "ON" (włączenie przełącznika), jak i przy komencie "OFF" (wyłączenie przełącznika) na adres 192.168.0.5:8888 (adres IP arduino) zostanie wysłany ciąg znaków "salonled#".

W ten sposób załatwione mamy sterowanie taśmą LED po stronie OpenHab. Co jednak w przypadku gdy stan oświetlenia zostanie zmieniony bez udziału OpenHab? Na przykład za pomocą przełącznika na ścianie? Konieczne jest aby informacja o zmianie stanu dotarła do OpenHab, aby mógł on wyświetlić użytkownikowi który może znajdować się daleko od domu aktualny stan oświetlenia.
Do tego wykorzystałem komunikację zwrotną, czyli OpenHab również będzie nasłuchiwał na wybranym porcie, a Arduino przy każdej zmianie stanu oświetlenia będzie wysyłało informację na podany numer IP.
W pliku house.items widać że mapowanie w konfiguracji wejściowej OpenHab zrobione jest za pośrednictwem JavaScriptu. Jest to spowodowane problemami na które w międzyczasie się natknąłem.
Założyłem że w przypadku załączenia oświetlenia Arduino wyśle komunikat salonled#1 a w przypadku wyłączenia salonled#0. Pomimo tego że obsłużyłem takie komendy w mapowaniu OpenHab konfiguracja nie działała poprawnie. Dopiero po analizie kodu źródłowego OpenHab'a i podłączeniu JavaScriptu który umożliwił wyświetlenie w log'u komunikatów które docierają do OpenHab okazało się że treść polecenia jest dużo dłuższa i uzupełniona pustymi znakami. Prawdopodobnie przesyłany był pełny bufor TCP - nie badałem tego tematu dokładniej.
Rozwiązaniem okazało się trim'owanie komunikatu i ręczne obsłużenie mapowania na odpowiedni stan przycisku. Udało się to osiągnąć następującym skryptem JS (OpenHab wspiera skrypty RHINO), który umieściłem w pliku salonled.js w podkatalogu transform:
function transform(val) {
var trimmed = new java.lang.String(val);
trimmed = "" + trimmed.trim();
if (trimmed.equals("salonled#1")) {
return "ON";
}
if (trimmed.equals("salonled#0")) {
return "OFF";
}
return "";
}
transform(input);
view raw salonled.js hosted with ❤ by GitHub


Należy jeszcze włączyć obsługę TCP w OpenHab.
W tym celu należy pobrać dodatkowe plugin'y (OpenHab AddOns), a następnie plik org.openhab.binding.tcp-1.4.0.jar (lub inna wersja) skopiować do katalogu addons z serwera OpenHab.
W pliku configurations/openhab_default.cfg należy odkomentować linię

tcp:port=25001

Dzięki temu OpenHab będzie nasłuchiwał na porcie 25001
Teraz można uruchomić OpenHab skryptem startup.sh (lub startup.bat w przypadku Windows) upewniając się wcześniej że mamy zainstalowaną odpowiednią wersję javy.
Po chwili pod adresem;

http://localhost:8080/openhab.app?sitemap=house

dostępne powinno być menu główne naszej konfiguracji.

To już cała konfiguracja po stronie OpenHab. Pozostaje kod źródłowy po stronie Arduino.

Odsyłam tutaj do wpisu http://technika-laika.blogspot.com/2014/04/arduino-sterowanie-przekaznikiem-za.html gdzie jest pierwotna wersja kodu źródłowego, dokładniejszy opis i wyjaśnienie nazewnictwa plików.
Jedyne zmiany które się pojawiają to zmiany komunikacji zwrotnej w pliku network.ino:

#include <SPI.h> // needed for Arduino versions later than 0018
#include <Ethernet.h>
#include <EthernetUdp.h> // UDP library from: bjoern@cs.stanford.edu 12/30/2008
byte mac[] = {
0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED
};
IPAddress ip(192, 168, 0, 5);
IPAddress responseIp(192, 168, 0, 12); //IP number to send response (Ip of OpenHab Server)
unsigned int responsePort = 25001; // port to send response -set in OpenHab configuration
unsigned int localPort = 8888; // local port to listen on
EthernetServer server(localPort);
EthernetClient client;
char packetBuffer[UDP_TX_PACKET_MAX_SIZE]; //buffer to hold incoming packet,
char ReplyBuffer[] = "acknowledged"; // a string to send back
void initializeNetwork() {
Ethernet.begin(mac, ip);
}
void checkNetwork() {
EthernetClient client = server.available();
if (client) {
String clientMsg ="";
while (client.connected()) {
if (client.available()) {
char c = client.read();
clientMsg+=c;//store the recieved chracters in a string
if (c == '#') {
Serial.println("Message from Client:"+clientMsg);//print it to the serial
executeRemoteCommand(clientMsg);
client.stop();
}
}
}
}
delay(10);
}
void executeRemoteCommand(String command) {
if (command == SALON_LED_REMOTE_COMMAND) {
toogleSalonLed();
}
}
void sendTcpState(String command) {
if (client.connect(responseIp, responsePort)) {
Serial.println("connected");//report it to the Serial
Serial.println("sending Message:"+command);//log to serial
client.print(command);//send the message
client.stop();
}
else {
// if you didn't get a connection to the server:
Serial.println("connection failed");
}
}
view raw _40_network.ino hosted with ❤ by GitHub
oraz dodanie wysłania komunikatu zwrotnego w pliku salon_led.ino:
String SALON_LED_REMOTE_COMMAND("salonled#");
const int SALON_LED_BUTTON_PIN = 22;
const int SALON_LED_RELAY_PIN = 23;
const int SALON_LED_AFTER_CHANGE_DELAY = 500;
int salonLedButtonState;
int salonLedState = LOW;
void initializeSalonLed() {
pinMode(SALON_LED_BUTTON_PIN, INPUT);
pinMode(SALON_LED_RELAY_PIN, OUTPUT);
setSalonLedState(HIGH);
}
void checkSalonLed() {
salonLedButtonState = digitalRead(SALON_LED_BUTTON_PIN);
if (salonLedButtonState == HIGH) {
toogleSalonLed();
delay(SALON_LED_AFTER_CHANGE_DELAY);
}
}
void setSalonLedState(int state) {
digitalWrite(SALON_LED_RELAY_PIN, state);
salonLedState = state;
sendTcpState(SALON_LED_REMOTE_COMMAND + state);
}
void toogleSalonLed() {
if (salonLedState == LOW) {
setSalonLedState(HIGH);
} else {
setSalonLedState(LOW);
}
}
Działanie całej kongfiguracji można zobaczyć na poniższym filmie:



 Generalnie zamierzony cel został osiągnięty, natomiast problemy na które się natknąłem, konieczność zrywania połączenia po obsłudze komunikatu, opóźnienia przy wznawianiu połączenia przez OpenHab powodują że nie polecam takiego rozwiązania w warunkach produkcyjnych. Być może ktoś z Was poradził sobie z tymi problemami. Jeśli tak, proszę o informację w komentarzach. Dużo ciekawiej zapowiada się komunikacja za pomocą mqtt którą postaram się zademonstrować w kolejnym wpisie.