Stacja pogodowa – omówienie kodu

Witajcie! Tak jak wspominałem we wcześniejszym artykule, skupiłem się obecnie na posprzątaniu i upublicznieniu mojego kodu stacji pogodowych z wykorzystaniem transmisji RF i bridge w postaci NodeMCU v3. Cały mój projekt znajdziecie pod tym adresem na GitHub. Składa się on z trzech części:

  • rf_encoder – prosta klasa szyfrująca i deszyfrująca wiadomość RF
  • client – kod dla punktu pomiaru temperatury
  • server – kod dla NodeMCU v3 jako bridge między transmisją RF i MQTT

Encoder

Jak wiadomo transmisja RF odbywa się poprzez przesłanie wiadomości tekstowej, więc każdy może ją przechwycić i odczytać. Zastanawiałem się na ile informacje o temperaturze, wilgotności czy ciśnieniu mogą być wrażliwe i wymagające ochrony… Nie wiem i nie umiem na to pytanie odpowiedzieć. Pewnie wszystko zależy od tego czy mierzymy temperaturę na zewnątrz, czy może w jakimś swoim pomieszczeniu wewnątrz mieszkania, sejfie, skrytce, czy może lodówce 🙂 takie czynniki mogą wpłynąć na to, że nasze dane warto zaszyfrować. Może słowo szyfrować nie jest tu najlepsze ale generalnie chodzi o zakodowanie wiadomości tak, by nie była jawna.

Dlatego na wszelki wypadek postanowiłem przygotować taką możliwość i dla tych co chcą mogą sobie ustawić własny sposób kodowania wiadomości.

Jak to zrobić? Po ściągnięciu repozytorium z GitHub należy cały katalog rf_encoder przenieść do katalogu library w swoim środowisku IDE (u mnie to Arduino IDE) i tam zedytować plik rf_encoder.cpp według własnego uznania.

include "rf_encoder.h"

String RfEncoder::encode(String deviceId, String message) {
  // put some your logic here
  return deviceId + "|" + message;
}

String RfEncoder::decode(String message) {
  // put some your logic here
  return message;
}

Metoda encode służy do zaszyfrowania wiadomości. Przyjmuje ona dwa parametry unikalne ID (np. ABC123) urządzenia oraz wiadomość z odczytanymi pomiarami jak i numerem sensora w obrębie urządzenia (np. 0|21.19|39.25|997.96). Chwilowo nie będę się skupiał co która wartość oznacza, opiszę to w dalszej części artykułu.

Do zakodowania tej wiadomości można użyć dowolnego mechanizmu, ale tak by nie spowodować przekroczenia 61 znaków, jaki jest przewidziany na całą wiadomość. Nie będę podpowiadał jakiego kodowania można użyć, każdy może użyć czegoś gotowego lub napisać coś własnego, ważne żeby można było taką wiadomość odkodować przy pomocy funkcji decode, która powinna zwrócić dla powyższego przykładu następujący ciąg znaków: ABC123|0|21.19|39.25|997.96

Ważne jest to aby wraz z wiadomością przesłać ID urządzenia. Tak by potem druga strona dekodująca wiadomość była w stanie stwierdzić do którego urządzenia ją przypisać.

Klient, czyli urządzenie pomiarowe

Konfiguracja

Tak dla uproszczenia opisu całego schematu, przyjąłem notację, iż urządzenie które dokonuje pomiaru i wysyła jego dane będzie nazywało się Klientem.

Z czego zbudowany jest klient:

  • Atmega328P – baza całego układu, wraz z rezystorem 10k omów, dwoma kondensatorami ceramicznymi 22, kwarcem 16Mhz i jednym kondensatorem ceramicznym 104 na potrzeby wgrywania oprogramowania przez programator TTL
  • transmiter RF CC1101
  • DHT11, DHT22 lub BME280 – jeden lub dwa sensory dla jednej Atmegi
  • zasilania w postaci ogniwa 18650 o pojemności 8800mAh i napięciu 4.2V (realnie mniej, około 4V)

Całość połączona jak na poniższych zdjęciach:

Stacja pogodowa - transmisja RF
Klient widok ogólny
Stacja pogodowa
Klient podłączenie modułów

Teraz jeśli chodzi o samo kod programu, to znajdziecie go w katalogu client. W pierwszej kolejności należy skopiować sobie plik configration.h jako configuration_prod.h i ustawić odpowiednie parametry konfiguracji.

Ustawienia dotyczące transmitera RF można pozostawić bez zmian lub zmodyfikować według własnych potrzeb zgodnie ze specyfikacją biblioteki SmartRC-CC1101-Driver-Lib

#define RF_CCMODE 1
#define RF_MODULATION 0
#define RF_FREQUENCY 433.92
#define RF_SYNC_MODE 2
#define RF_POWER 12
#define RF_CRC 1
#define RF_POWER_PIN 8

Tu wspomnę jedynie o tym, że gdyby ktoś miał inny moduł transmisji RF i chciałby z niego skorzystać, to nic nie stoi na przeszkodzie, wówczas wymagana jest tylko zmiana pliku rf.h i zaimplementowanie odpowiednich metod.

class RfConnection {
  public:
    RfConnection();
    void begin();
    String waitForReceive(int maxIterations);
    String receive();
    void sendSensorData(String deviceId, String msg);
    void txMode();
    void rxMode();
    void sleepMode();
    void wakeUp();
  private :
    String decode(String input);
    String encode(String deviceId, String input);
    RfEncoder encoder;
};

Kolejnym krokiem konfiguracji jest ustalenie pod jakim pinem jest wpięty czujnik DHT.

#define PIN_DHT11 9

Nie jest to obowiązkowe do wypełnienia, można tą wartość podać oczywiście w trakcie inicjowania sensora w programie głównym. W szczególności należy pominąć jeśli takiego czujnika się nie używa. Jeśli jednak ktoś posiada inny czujnik, to proszę śmiało, można sobie go dodać modyfikując plik sensor.h. Należy dodać dodatkową wartość w enum SesnorType i dopisać w klasie Sensor obsługę tego czujnika. Jeśli ktoś by to zrobił i chciał się tym podzielić to będę wdzięczny za wystawienie pull requesta na GitHub.

Następna wartością która podlega konfiguracji to czas pomiędzy pomiarami.

#define SLEEP_INTERVAL_SEC 300

Jest to wartość wyrażona w sekundach i jest swego rodzaju przybliżeniem interwału pomiarów. Wynika to bowiem z pewnych sleepów użytych w trakcie wykonywania programu jak i z samych możliwości usypiania Atmegi328. A mianowicie jej deep sleep ma maksymalną wielkość 8s lub forever (co wymaga zewnętrznego wybudzenia, a to z kolei zewnętrznego zegara, co zużywa za duże zasoby prądu – pisałem o tym w poprzednim artykule). Dlatego Atmega wybudza się co 8 sekund i sprawdza czy upłynął już podany interwał czasu i jeśli tak to dokonuje pomiaru i zaczyna proces na nowo. Tak więc czas ten jest przybliżony i może się odchylać w granicach do 10s (taki też interwał pomiaru jest minimalny jaki warto ustawić). A jeśli ktoś chce mieć mniejsze czasy, to proszę bardzo, ale wymaga to już zmian w kodzie.

Ostatnia rzecz która podlega konfiguracji to unikalny identyfikator urządzenia:

#define DEVICE_UNIQ_ID "ABC123"

Identyfikator ten musi być unikalny w obrębie całego systemu i składać się z sześciu cyfr lub liter, tak by nie dublować odczytów z różnych urządzeń.

Można byłoby teraz już wgrać do naszej Atmegi328 kod, ale jest jeszcze jedna rzecz która wymaga ustawienia w źródle programu. W pliku client.ino mamy następujące dwie linijki kodu

Sensor sensorOne(0x76);
Sensor sensorTwo;

Odpowiadają one za włączenie i używanie sensorów. Przy powyższym zapisie, program będzie korzystał tylko z sensora pierwszego, który ma swój numer równy 0 i w tym przypadku będzie to czujnik BME280 o identyfikatorze 0x76. Gdybym chciał mieć drugi czujnik, który byłby czujnikiem DHT11, to musiałbym powyższy kod zamienić na następujący:

Sensor sensorOne(0x76);
Sensor sensorTwo(DHT11_PIN, DHT11);

Podając data pin dla sensora i typ sensora (DHT11, opcjonalnie może być DHT22).

I to już koniec konfiguracji, czas wgrać kod do naszego urządzenia pomiarowego.

Działanie programu

Tak jak opisywałem powyżej układ będzie się wybudzał co pewien okres czasu i sprawdzał czy należy dokonać pomiaru. Jeśli nie to wejdzie w głęboki stan uśpienia i wybudzi się za kolejne 8s. Jeśli tak, to dokona pomiaru i wyśle wiadomość, po czym przejdzie w stan głębokiego uśpienia i proces zacznie się na nowo. Większej filozofii nie ma.

Co do samej wiadomości i jej treści, to tak jak pisałem przy okazji rf_encodera, jest to tekst który po odkodowaniu powinien zawierać następujące wartości oddzielone pionowymi kreskami.

  • identyfikator urządzenia – symbol urządzenia składający się z sześciu liter i cyfr
  • numer sensora – kolejny numer modułu dokonującego pomiaru podpięty do urządzenia, numeracja zaczyna się od 0 i obejmuje obecnie scenariusz dla dwóch sensorów: 0 i 1 (jeśli ktoś potrzebuje więcej to trzeba zmienić kod programu ale o tym później)
  • temperaturę – wyrażoną w stopniach Celsjusza
  • wilgotność – wyrażoną w procentach
  • ciśnienie – wyrażone w hPa, jeśli urządzenie nie posiada takiego sensora zwraca null

Dla przykładu, będzie to np. coś takiego „ABC123|0|21.19|39.25|997.96„.

Taka wiadomość zakodowana lub nie jest przesyłana i odbierana po stronie Bridge.

Bridge

Bridge jak sama nazwa wskazuje jest to urządzenie pomostowe, którego zadaniem jest odebrać dane RF i zamienić je na odpowiedni pakiet MQTT rozesłany do brokera.

W moim przypadku Bridge składa się z:

  • NodeMCU v3 – jak baza do połączenia się z siecią WiFi
  • Receiver CC1101 – odbiornik radiowy

Całość po złożeniu wygląda następująco:

Bridge zestaw
Bridge podłączenie NodeMCU v3
Bridge podłączenie CC1101

Generalnie w tym przypadku podłączenie jest proste i zgodne z dokumentacją biblioteki – patrz rysunek.

Konfiguracja

W pierwszej kolejności przed wgraniem kodu należy skopiować domyślną konfigurację tworząc plik configuration_prod.h, a następnie ustawić w nim odpowiednie parametry.

Konfiguracja sieci- nazwa i hasło

// WIFI SETTINGS
#define SSID "YOUR_WIFI_SESSION_ID"
#define PASSWORD "YOUR_WIFI_PASSWORD"

Konfiguracja odbiornika RF, powinna być zgodna z ustawieniami transmitera po stronie Klienta.

#define RF_CCMODE 1
#define RF_MODULATION 0
#define RF_FREQUENCY 433.92
#define RF_SYNC_MODE 2
#define RF_POWER 12
#define RF_CRC 1

Konfiguracja połączenia z serwerem MQTT

#define MQTT_SERVER "MQTT_SERVER_IP"
#define MQTT_PORT 1883
#define MQTT_USER "MQTT_SERVER_USERNAME"
#define MQTT_PASS "MQTT_SERVER_PASSWORD"

Pozostała konfiguracja dotycząca ilości obsługiwanych urządzeń i przechowywanych dla nich wartości (obecnie jeszcze nie wykorzystywana, raczej służy tylko do podglądu).

#define MAX_NUMBER_OF_DEVICES 10      // max 10
#define SENSOR_DATA_LIST_LENGTH 20    // max 20

Mając pełną konfigurację możemy przejść do wgrania naszego kodu do NodeMCU.

Działanie

Bridge jest odpowiedzialny za przechwycenie i zdekodowanie pakietu. Do informacji jaka wyszła ze zdekodowanej wiadomości dorzuca informację o czasie jej odebrania (w postaci timestampa) i wylicza punkt rosy. Całość przechowuje w lokalnym storage dla każdego urządzenia z osobno.

Tak przygotowane dane są wysyłane do serwera MQTT na odpowiedni topic: ws/DEVICE_UNIQ_ID/SENSOR, gdzie DEVICE_UNIQ_ID, to symbol urządzenia. Treść przesyłanej wiadomości wygląda jak poniżej.

{
    "uniqId": "ABC123",
    "sensor": "0",
    "payload": {
        "time": 1607006383,
        "temp": 23,
        "hum": 60,
        "pressure": 999.72,
        "dewPoint": 14.79
    }
}

Bonus

Bridge dodatkowo wystawia serwer HTTP na porcie 80. Wchodząc na niego otrzymamy listę ostatnich odebranych wiadomości od wszystkich Klientów.

Podsumowanie

W ten oto sposób dotarliśmy do końca opisu tego projektu. Tak jak wspominałem we wcześniejszym artykule poprzez MQTT dane dostają się do Home Assistant i do mojego serwera z danymi. Home Assistant przyjmuje dane i je wyświetla, mój serwer też je gromadzi, ale na razie nie przekazuje dalej tych informacji i nie są obsługiwane one na aplikacji frontendowej.

Te braki to plan na najbliższą przyszłość. Dostosować frontendową aplikację do nowych możliwości związanych z przechowywaniem wartości ciśnienia i punktu rosy.

Do usłyszenia wkrótce, a jeśli ktoś ma pomysł jak usprawnić mój projekt proszę śmiało pisać w komentarzach lub na maila.

Stacja pogodowa – omówienie kodu
Przewiń na górę