Bezprzewodowe sterowanie urządzeniami przez WiFi

Ktoś mi ostatnio zarzucił zbytnie przywiązanie do technologii przewodowych. Postanowiłem więc wyjść naprzeciw oczekiwaniom i przygotować bezprzewodową wersję układu sterującego urządzeniami elektrycznymi na 433MHz. Choć tak naprawdę skłoniła mnie do tego konkretna potrzeba. Niezależnie od motywacji powstała nowa wersja bramki IP-433.92MHz, tym razem działająca przez WiFi. Dzięki temu można ją powiesić w miejscu, gdzie jest zasięg Wifi, a jednocześnie odbiorniki 433MHz (gniazdka, urządzenia w puszkach, radiowe włączniki, itp.). Do urządzenia dorzuciłem jeden mały „gratis”, ale o tym za chwilę.

Sterowanie urządzeniami elektrycznymi przez Wifi

Możliwości jest wiele

Na wstępie warto podsumować zrealizowane już sposoby sterowania urządzeniami elektrycznymi, ponieważ parę pomysłów już wdrożyliśmy z życie. Zaczęło się od sterowania modułami przekaźnikowymi przez Arduino. Dzięki temu możemy sterować dowolnymi urządzeniami na niskie napięcie, bądź 230V. Wykorzystuję to szeroko w domu do sterowania oświetleniem, ogrzewaniem, bramą garażową, cyrkulacją ciepłej wody, nawadnianiem ogrodu i pewnie w jakichś innych miejscach. Te systemy sterowane są przez Ethernet, czyli sieć przewodową.

Kolejnym pomysłem, by nie przeciągać wszędzie przewodów było użycie popularnego i taniego systemu na 433MHz. Zbudowałem centralny sterownik, podłączany przez Ethernet, który jest w stanie nadawać i odbierać sygnały na popularnej częstotliwości, współpracujący z urządzeniami różnych producentów – u mnie Conrad, Kemot i Clarus. Sterownik jest więc bramką IP-433MHz.

Ostatnim podejściem było przygotowanie sterownika WiFi do urządzeń elektrycznych z użyciem modułów przekaźnikowych, których używałem wcześniej z Arduino. To rozwiązanie powstało na potrzeby sterowania ogrzewaniem w jednym z domów zamieszkiwanych okazjonalnie.

Koncepcja i komponenty

Dzisiaj natomiast, przygotujemy połączenie dwóch ostatnich rozwiązań – bramkę IP-433MHz sterowaną przez WiFi. Do budowy użyty zostanie ponownie NodeMCU w wersji LoLin (wersja ma znaczenie tylko dla osób chcących użyć gotowej obudowy z tego wpisu) oraz tanie moduły do nadawania i odbierania danych na 433.92MHz. NodeMCU wybrałem ponownie, bo dla prostych urządzeń wszystko jest dostępne „na pokładzie”. Nie trzeba przygotowywać płytki drukowanej, bo potrzebujemy tylko kilka połączeń między modułami, które spokojnie można zrobić przewodami. Całość będzie stabilna, bo przygotujemy dedykowaną obudowę. NodeMCU daje nam stabilizację napięcia, możliwość zasilenia przez micro USB, a co za tym idzie dwa napięcia, które mogą być potrzebne – 5V i 3,3V. Jest to o tyle istotne, bo daje nam możliwość użycia właściwie dowolnego z popularnych układów do 433MHz – zarówno na 5V, jak i na 3,3V.

Ponieważ rozmiar urządzenia nie był priorytetem, a NodeMCU v3 daje sporo wolnych wejść, postanowiłem zostawić trochę miejsca w obudowie na dodatkowe moduły. Ponieważ w ostatnim czasie zainteresowałem się pomiarami ciśnienia atmosferycznego, użyty zostanie czujnik BMP180 firmy Bosch. W pierwszej chwili chciałem użyć starszego BMP085, jednak mój egzemplarz okazał się wadliwy – dobrze reagował na zmiany ciśnienia, ale źle wskazywał ciśnienie rzeczywiste. Obecnie bardziej naturalnym wyborem byłby nowszy BMP280, ale BMP180 znalazłem w swoich zasobach. Tak naprawdę wersja nie ma znaczenia, bo czujniki dysponują dokładnością istotną tylko przy pomiarze zmian wysokości. Dla obserwacji meteorologicznych nie ma to znaczenia.

Moduły radiowe 433.92MHz

Wróćmy jednak na razie do głównej funkcji – sterowania urządzeniami elektrycznymi. Użyjemy do tego dwóch modułów. Wybór jest spory, natomiast zwykle różnią się akceptowanymi napięciami i zasięgiem/czułością. Ja jako odbiornik używam zwykle WL101-341, zasilanego napięciem 3,3V, który po dolutowaniu anteny (choćby przewodu o długości 17,3cm) ma niezłą czułość, w przeciwieństwie do najpopularniejszego modułu MX-05V. Jako nadajnik do tej pory najczęściej używałem FS1000A (MX-FS-03V) zasilany 5V (w tym z NodeMCU), jednak wyraźnie lepsze efekty daje WL102-341 przy zasilaniu 3,3V (także z anteną).

NodeMCU i moduły radiowe

Mamy więc komplet NodeMCU v3, dwa moduły radiowe i jeden czujnik ciśnienia. Nadajnik potrzebuje napięcia 3,3V, masy i wyjścia do przekazywania sygnałów. Odbiornik podobnie, tylko zamiast wyjścia potrzebne jest wejście. Natomiast BMP180 poza zasilaniem i masą wymaga komunikacji przez I2C, więc dwóch dodatkowych przewodów. Tak się składa, że NodeMCU ma wyprowadzone napięcie 3,3V na trzech pinach, a masę nawet na pięciu. Wejścia i wyjścia także nie są problemem. Potrzeba jedynie krótkich przewodów na złącza gold pin (dupont).

Przewody dupont

Połączenie jest tak proste, że trudno mówić o projekcie. Po prostu łączymy przewodami i zamykamy w obudowie, żeby było stabilne.

Obudowa

Obudowę tradycyjnie projektujemy w OpenSCAD, wzorując się na wcześniejszym projekcie z NodeMCU i przekaźnikami. Miejsca nie oszczędzamy, zostawiając przegrody na moduły. W RepetierHost, z użyciem slicera przygotowujemy plik do wydruku i umieszczamy na karcie pamięci, aby drukarka 3d mogła drukować obudowę bez połączenia z komputerem. Chętni mogą pobrać gotowe pliki obudowy.

Sterownik NodeMCU

Wydruk w jednej wersji wykonamy z mojego ulubionego, przezroczystego filmentu PETG od polskiej firmy DevilDesign. Dzięki temu widać światło diody i wiadomo, że urządzenie działa. Oczywiście to także może być wada, dlatego wydrukowałem też wersję z filamentu PLA tego samego producenta.

Komunikacja

Podobnie jak w ostatnim urządzeniu użyjemy pakietów UDP zwierających 3 liczby oddzielone kropkami. Są to numer protokołu radiowego (zwykle 1 lub 2, zgodnie z biblioteką rcswitch), długość komunikatu w bitach i sam komunikat. Urządzenie ma przyjmować pakiety w tym formacie i wysyłać je radiowo, a w przypadku odebrania sygnału radiowego odsyłać kod do urządzenia nadrzędnego (u mnie Raspberry Pi z nodejs, o czym już pisałem). Ma także potwierdzać otrzymanie pakietu UDP. Nie różni się to niczym w stosunku do opisywanej już bramki przewodowej.

Dodatkowo będziemy obsługiwać czujnik ciśnienia i co zadany czas (pół minuty) wysyłać pakiet UDP z informacją o aktualnym ciśnieniu w Pascalach.

Ciśnienie atmosferyczne

Zanim przejdziemy do części programistycznej, warto poświęcić chwilę na rozważania o ciśnieniu atmosferycznym. Cóż to jest jest to ciśnienie? To siła z jaką powietrze naciska na powierzchnię Ziemi (ściśle stosunek tej siły do powierzchni). Zależy więc od wysokości (bo im wyżej jesteśmy, tym niższy słup jest nad nami). Pierwszym zaskoczeniem, gdy zacząłem się przyglądać ciśnieniu było dla mnie to, jak szybko ciśnienie zmienia się z wysokością. Na piętrze w domu było wyraźnie różne niż na parterze.

BMP085

Czujniki ciśnienia używane są w lotnictwie do określenia wysokości względnej nad poziomem Ziemi (i morza oczywiście). Z tego powodu lotnisko zawsze podaje pilotom dokładne informacje o aktualnym ciśnieniu na poziomie pasa startowego. Czujniki BMP, są stosowane w dronach, także do określania wysokości. Rozdzielczość pomiaru w nowych czujnikach Boscha dochodzi powinna (przynajmniej w teorii) wykrywać zmiany wysokości na poziomie kilkunastu centymetrów.

I tu dochodzimy do istotnego problemu w pomiarze ciśnienia – skoro 8 metrów wysokości (2 wysokie piętra) powoduje, że ciśnienie jest niższe o 1 hPa, to w jaki sposób w prognozach podaje się ciśnienia dla danego obszaru, skoro różne miejsca mogą się różnić wysokością o choćby kilkadziesiąt metrów? Ano ciśnienie podawane w prognozach, to trochę ściema. Nie jest podawane ciśnienie rzeczywiste, tylko przeliczone do poziomu morza. W związku z tym między plażą nad morzem, a polskimi szczytami górskimi różnica w rzeczywistym ciśnieniu wynosi ponad 300hPa, co jest wartością dużo większą, niż wahania związane ze zmianami pogody…

Żeby policzyć ciśnienie nad poziomem morza, potrzebna jest znajomość temperatury. Układy BMP są wyposażone w czujniki temperatury dla łatwego przeliczenia. Zakładam jednak, że dla przeliczenia istotna jest temperatura na zewnątrz i to ją trzeba brać pod uwagę w obliczeniach. Natomiast oczywiście to, co jest najistotniejsze, to wysokość nad poziomem morza naszego urządzenia pomiarowego.

W każdym razie przyjmiemy, że wysłać należy rzeczywiste ciśnienia, a przeliczenia dokona urządzenie nadrzędne.

ESP8266 a ArduinoIDE

Ponieważ do NodeMCU nie ma w tej chwili dobrej biblioteki do wysyłania i odbierania kodów na 433MHz, a pod Arduino istnieje rcswich, przyjąłem, że pora przyjrzeć się obsłudze ESP8266 pod ArduinoIDE. Doskonały pretekst do czegoś, co chciałem zrobić od dawna.

Samo dodanie obsługi NodeMCU jest banalnie proste, natomiast pisząc program trzeba zdawać sobie sprawę z różnic. Przede wszystkim ESP8266 jest układem jednordzeniowym i poza programem musi wykonywać zadania związane z obsługą WiFi. Kiedy te zadania są wykonywane? Po każdym zakończeniu pętli loop. To jednak może być zbyt żadko, szczególnie gdy pętla trwa powyżej 50ms. Dlatego ESP zajmuje się także obsługą WiFi podczas wywołań funkcji delay (uwaga, nie dotyczy to delayMicroseconds), a także yield (odpowiednik delay(0)). Najlepiej więc pamiętać, żeby w czasochłonnych miejscach dodawać funkcję yield().

Sterownik WiFi - 433MHz

Programowanie

Użycie ArduinoIDE ma jeszcze jedną dużą zaletę – większość kodu mamy już gotową, bo przewodowa wersja bramki opierała się o Arduino. To co trzeba zmienić, to minimalne zmiany w inicjalizacji, obsłudze pakietów UDP i dołożyć część odpowiedzialną za czujnik ciśnienia. Dodatkowo, ponieważ używamy tylko pakietów UDP to nie do końca mamy gwarancję, że sieć działa poprawnie. Założymy więc, że co jakiś czas powinien przyjść pakiet z systemu nadrzędnego. Jeżeli nie przyjdzie, to na wszelki wypadek, zrestartujemy urządzenie.

#include <Wire.h>
#include <Adafruit_BMP085.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <RCSwitch.h>

const char *ssid = "mojasiec";      
const char *pass = "tajnehaslo";

IPAddress master(192,168,200,2); //tam wysyłamy
int masterport=8000; //na ten port
int localport = 8000; //na tym porcie nasłuchujemy
int lastwifistatus = -1;
int statuschanges=0;

//czujnik ciśnienia
Adafruit_BMP085 bmp;

String str;
char cstr[19];

WiFiUDP UDP;
char packetBuffer[UDP_TX_PACKET_MAX_SIZE];  //dla pakietów przychodzących

RCSwitch mySwitch = RCSwitch();

#define D5 14
#define D4 2
#define D1 5
#define D2 4

#define SEND_INTERVAL 30000 // wysyłanie ciśnienia co 30 sekund
#define NO_COMM_TIMEOUT 1205000 // po jakim czasie uznajemy, że warto zrobić restart 
unsigned long stime;
unsigned long nextSendTime=10000;
unsigned long commTimeout=NO_COMM_TIMEOUT;

// Update these with values suitable for your network.
IPAddress ip(192,168,200,200);  //static IP
IPAddress gateway(192,168,200,1);
IPAddress subnet(255,255,255,0);

void setup()
{
  Serial.begin(9600);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);
  WiFi.config(ip, gateway, subnet);

  stime=millis();
  commTimeout=stime+NO_COMM_TIMEOUT;

  //Wifi connection
  lastwifistatus=WiFi.status();
  
  bmp.begin(3);

  mySwitch.enableReceive(D5); // pin modułu odbiorczego
  mySwitch.enableTransmit(D4); // pin modułu nadawczego
  mySwitch.setPulseLength(221); // ważne dla gniazdek Clarus
  UDP.begin(localport); //nasłuchujemy

  //wysyłamy info o (re)starcie
  UDP.beginPacket(master, masterport);
  UDP.write("Restart433");
  UDP.endPacket();
}

// wysyła ciśnienie w Pa jako UDP
int sendPressure(int pressure)
{
  char str[10];
  sprintf(str, "%d", pressure);
  
  UDP.beginPacket(master, masterport);
  Serial.println(pressure);
  UDP.write("Pressure:");
  UDP.write(str);
  return UDP.endPacket();
}

// czy timer już nadszedł lub minął (jest mniejszy od time); zakładamy, że czasy nie są dłuższe niż kilka dni i sprawdzamy czy nie miało szansy przekroczenie zakresów
boolean isAfter( unsigned long timer )
{
  if(timer<=stime && !(stime>3000000000L && timer<1000000000L) ) return(true);
  if(timer>3000000000L && stime<1000000000L) return(true);
  return(false);
}

void loop()
{
  int mProtocol;
  int mSize;
  unsigned long mValue;
  char strint[10];

  //powiadamiamy o zmianach statusu połączenia wifi
  if(lastwifistatus!=WiFi.status()) 
  {
    Serial.print("WiFi new status - ");
    Serial.print(WiFi.status());
    Serial.print(", IP address: ");
    Serial.println(WiFi.localIP());
    lastwifistatus=WiFi.status();  
    if( lastwifistatus== WL_CONNECTED )
    {
      statuschanges++;
      UDP.beginPacket(master, masterport);
      UDP.write("Connected ");
      sprintf(strint, "%d", statuschanges);
      UDP.write(strint);
      UDP.endPacket(); 
    }
  }
  
  stime=millis();

  //wysyłanie ciśnienia
  if( isAfter(nextSendTime) )
  {
    nextSendTime=stime+SEND_INTERVAL;
    int temperature = bmp.readTemperature();
    int pressure = bmp.readPressure();
    //pressure = bmp.readSealevelPressure(66);
    sendPressure(pressure); 
    if( isAfter(nextSendTime) ) nextSendTime=stime;
  }

  // odbieranie kodów do wysłania przez 433.92Mhz
  
  int packetSize = UDP.parsePacket();

  //przyszły jakieś dane przez sieć
  if (packetSize) 
  {
    Serial.println("UDP Received");
    commTimeout=stime+NO_COMM_TIMEOUT;
    IPAddress remote = UDP.remoteIP();
    if( remote[0]!=192 ) { 
      //tu można dodać autoryzację po adresie źródłowym
    }
    
    UDP.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);

    //pobieramy wartości z ciągu znaków
    mProtocol=packetBuffer[0]-48;
    mSize=packetBuffer[3]-48+10*(packetBuffer[2]-48);
    mValue=String(packetBuffer).substring(5, packetSize).toInt(); 

    // wyłaczamy odbieranie kodów, żeby nie odczytywać wysłaego przez siebie
    mySwitch.disableReceive();

    // wysyłamy polecenie radiowo
    mySwitch.setProtocol(mProtocol);
    mySwitch.setPulseLength(221);
    mySwitch.send(mValue, mSize);

    // włączamy odbieranie ponownie
    mySwitch.enableReceive(D5);

    // odsyłamy potwierdzenie wysłania
    UDP.beginPacket(master, masterport);
    str = String("P:"+String(mProtocol)+"."+String(mSize)+"."+String(mValue)+"...");
    for(int w=0; w<19; w++) cstr[w]=0;
    String(str).toCharArray(cstr,19);
    UDP.write(cstr, 19);
    UDP.endPacket();
  }

  // restartujemy gdy dawno nie przyszedł żaden pakiet
  if( isAfter(commTimeout) ) ESP.restart();

  // przyszły dane radiowo
  if (mySwitch.available()) {
    
    int value = mySwitch.getReceivedValue();
    
    if (value == 0) {
      Serial.print("Unknown encoding");
    } else {

       str = String(String(mySwitch.getReceivedProtocol())+"."+mySwitch.getReceivedBitlength()+"."+
       String(mySwitch.getReceivedValue()));
       for(int w=0; w<19; w++) cstr[w]=0;
       str.toCharArray(cstr,19);

       // wysyłamy pakiet udp
       UDP.beginPacket(master, masterport);
       UDP.write(cstr, 19);
       UDP.endPacket();
    }
    mySwitch.resetAvailable();
  }
}

Podsumowanie

Urządzenie działa niezawodnie od dłuższego czasu. Wbrew obawom ESP8266 także pod ArduinoIDE jest bardzo stabilne. Jedyne problemy, które napotkałem wynikały z błędu w jednej z bibliotek (starszej wersji), natomiast analiza zrzutów przekazywanych przez port szeregowy, naprowadziły mnie na ślad. Na pewno bardzo wygodne jest to, że NodeMCU zasilane jest przez microUSB oraz duża liczba wejść. Przy prostych urządzeniach można łatwo uniknąć tworzenia dedykowanej płytki drukowanej.

Advertisements

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

%d bloggers like this: