Wstęp
Do napisania tego projektu zachęcił mnie podobny projekt kolegi, który był jednak napisany na Raspberry Pi.
Po co strzelać z armaty do muchy – pomyślałem.
Jako, że byłby to także fajny gadżet do wykorzystania na stronie internetowej, postanowiłem zamówić niezbędne podzespoły i w dwa dni powstał bazowy projekt, który pozwalał już komunikować się zdalnie z serwerem www.
Założenia
- Pomiar temperatury i wilgotności na zewnątrz pomieszczenia.
- Wysyłanie danych na serwer WWW – na początku była to komunikacja za pomocą protokołu UDP, ale żeby nie komplikować sprawy po stronie serwera, postanowiłem wykorzystać protokoł TCP.
- Odczyt danych i statusu pracy urządzenia możliwy także w aplikacji Bluetooth Terminal na Androida.
Podzespoły
Na początek należy zaopatrzyć się w podzespoły:
Arduino Nano 3.0 ATmega328P z CH340G
Moduł LAN ENC28J60
Czujnik temperatury i wilgotności
Osobiście zakupiłem dwa różne sensory, jeden do pomiarów wewnątrz pomieszczenia i drugi do pomiarów na zewnątrz.
Na potrzeby tego artykułu wystarczy zakupić jeden z poniższych czujników.
DHT11
Ze względu na swoje parametry, do zastosowania wyłącznie wewnątrz pomieszczeń.
Parametr | Wartość |
---|---|
Zasilanie | 3-5V |
Temperatura | 0-50° o dokładności ±2°C |
Wilgotność | 20-90%RH o dokładności 5% |
DHT22
Do pomiarów na zewnątrz pomieszczeń.
Parametr | Wartość |
---|---|
Zasilanie | 3-5V |
Temperatura | -40-80° o dokładności ±0.5°C |
Wilgotność | 0-100%RH o dokładności 2-5% |
Moduły, które zostały zakupione na Aliexpress:
- Nano 3.0 controller compatible for arduino nano CH340 USB driver NO CABLE
- ENC28J60 LAN Ethernet Network Board Module for arduino 25MHZ Crystal AVR 51 LPC STM32 3.3V
- Orignial-sensor Temperature and humidity sensor module Fapplication DHT-11 DHT11+PCB
- DHT22 Digital Temperature and Humidity Sensor AM2302 Module+PCB with Cable for Arduino
– został użyty w projekcie - HC-06 Bluetooth serial pass-through module wireless serial communication from machine Wireless HC06 for arduino
– opcjonalnie, jeśli zależy Ci na odczytywaniu statusu urządzenia na telefonie przez BT.
Schemat i podłączenie
Pierwszy raz mam styczność z programem Fritzing, więc schemat może nie jest za czytelny, ale wbrew pozorom, podłączenie jest bardzo proste.
Podłączenie
Arduino Nano przychodzi praktycznie złożone. Jedyne co musimy zrobić, to dolutować listwę z pinami.
Rezystor 4k7 widoczny na schemacie nie jest konieczny, gdyż wskazane przeze mnie sensory posiadają go wbudowanego.
Wszystkie moduły posiadają możliwość zasilania przez 3V3 i 5V, więc od Ciebie zależy na jakie zasilanie się zdecydujesz.
Moduł LAN podłączamy wg poniższej tabelki (lub schematu). Oznaczenia mogą się nieznacznie różnić w zależności od producenta modułu LAN.
ENC28J60 | Nano |
---|---|
5V | 5V |
SO (MISO) | D12 |
SCK | D13 |
ST (MOSI) | D11 |
RST | RST |
CS | D10 |
Biblioteki
Z modułem ENC28J60 przetestowałem 3 biblioteki.
- Standardową bibl. Ethernet z Arduono – nie pobierała IP z DHCP ani statycznie.
- EtherCard – nie działało UDP.
- UIPEthernet – wszystko działało (przynajmniej to co mnie interesowało).
Z wiadomych względów, projekt został oparty o trzecią bibliotekę, chociaż podobno najbardziej zasobożerną.
Przydadzą się jeszcze oczywiście sterowniki do konwertera USB To RS232:
CH341SER.ZIP – uruchamiamy program SETUP.EXE z folderu CH341SER i postępujemy zgodnie z instrukcją.
Następną biblioteką jaką będziemy potrzebowali jest arduino-DHT.
Bardzo prosta, ładnie napisana biblioteka.
Serwer
Dane będą po prostu zapisywane (dodawane) do pliku w formacie CSV.
Nic nie stoi na przeszkodzie, abyś przerobił skrypt wg. własnego uznania tak, aby zapisywał dane w wybranym formacie lub w bazie danych.
Musisz oczywiście posiadać serwer WWW z zainstalowanym interpreterem PHP, ale to chyba oczywiste.
<?php define('DEBUG', false); // Lokalna strefa czasowa date_default_timezone_set('Europe/Warsaw'); // Wybór źródła danych if (constant('DEBUG')) { $data = [ 'Humidity' => 45.5, 'Temperature' => 23.1 ]; file_put_contents('headers.txt', var_export($_SERVER, true), FILE_APPEND); } else { $data = &$_POST; } // Uwierzytelnienie if (constant('DEBUG') || (!empty($_SERVER['HTTP_X_APIKEY']) && $_SERVER['HTTP_X_APIKEY'] == '4Jrt39dj')) { // Dozwolona lista przesyłanych pól $whiteList = [ 'Temperature', 'Humidity' ]; // Ustaw wartości domyślne $whiteList = array_fill_keys($whiteList, NULL); // Dodaj lokalny znacznik czasu $storeData['DateTime'] = date('Y-m-d H:i:s'); // Przygotuj tablicę do zapisu $storeData = array_merge($storeData, $whiteList, array_intersect_key($data, $whiteList)); // Zapisz jako CSV $fp = fopen('weather.txt', 'a'); fputcsv($fp, $storeData); fclose($fp); if (constant('DEBUG')) { print_r($storeData); } } ?>
Wartym uwagi jest wspomnienie o warunku sprawdzającym istnienie poprawnego nagłówka X-ApiKey, który przechowuje klucz API, nasz sekretny kod/hasło, który pozwala nam sprawdzić, czy odebrane dane pochodzą z zaufanego źródła (naszego programu).
Dodatkowo, jeśli posiadasz stałe publiczne IP, możesz dodać dodatkowy warunek sprawdzający tenże adres.
Klient
Czas na główny program, który będzie przekazywał dane z sensorów na serwer metodą POST.
#include <DHT.h> #include <UIPEthernet.h> #define WS_HOST_NAME "ws.example.com" #define WS_HOST_PORT 80 const static uint8_t REQUEST_BUFFER_SIZE = 250; const static uint8_t mac[] = { 0x1A, 0x2B, 0x3C, 0x4D, 0x5E, 0x6F }; const static uint16_t timerInterval = 1000 * 60; // max. 65,535; const static char wsHost[] PROGMEM = WS_HOST_NAME; const static char wsUri[] PROGMEM = "/ws.php"; const static char wsAgent[] PROGMEM = "WeatherStation/1.0"; const static char wsApiKey[] PROGMEM = "4Jrt39dj"; const static char wsRequestHeaders[] PROGMEM = "POST %S HTTP/1.1\r\nUser-Agent: %S\r\nHost: %S\r\nAccept: */*\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: %d\r\nConnection: keep-alive\r\nX-ApiKey: %S\r\n\r\n"; static char requestBuffer[REQUEST_BUFFER_SIZE] = ""; static uint16_t connectRetryCount = 0; static uint16_t timerLastMeasurement = 0; EthernetClient client; DHT outSensor; void setup() { Serial.begin(9600); outSensor.setup(A0); do { if (connectRetryCount++ > 0) Serial.println("FAIL"); Serial.print(F("Collect DHCP information...")); } while (Ethernet.begin(mac) == 0); Serial.println("DONE"); Serial.print(F("IP Address: ")); Serial.println(Ethernet.localIP()); Serial.print(F("Subnet Mask: ")); Serial.println(Ethernet.subnetMask()); Serial.print(F("Default Gateway: ")); Serial.println(Ethernet.gatewayIP()); Serial.print(F("DNS Server: ")); Serial.println(Ethernet.dnsServerIP()); } void loop() { uint16_t timerNow = millis(); if (timerNow - timerLastMeasurement >= timerInterval) { timerLastMeasurement = timerNow; timerCallback(); } } void timerCallback() { //delay(outSensor.getMinimumSamplingPeriod()); Serial.print(F("Reading data from DHT...")); float humidity = outSensor.getHumidity(); float temperature = outSensor.getTemperature(); Serial.println(outSensor.getStatusString()); if (outSensor.getStatus() != DHT::ERROR_NONE) return; Serial.print("DHT22"); Serial.print("\t"); Serial.print(humidity, 1); Serial.print("%\t"); Serial.print(temperature, 1); Serial.write(176); Serial.println("C"); //Serial.print("\t"); //Serial.println(outSensor.toFahrenheit(temperature), 1); Serial.print(F("Connecting to server...")); if (Ethernet.maintain() % 2 == 0 && client.connect(WS_HOST_NAME, WS_HOST_PORT) && client.connected()) { Serial.println("DONE"); String data; data.reserve(40); data.concat("Temperature="); data.concat(temperature); data.concat("&Humidity="); data.concat(humidity); uint8_t len = data.length(); bool written; sprintf_P(requestBuffer, wsRequestHeaders, wsUri, wsAgent, wsHost, len, wsApiKey); Serial.print(F("Sending data...")); written = client.write((const uint8_t *)requestBuffer, strlen(requestBuffer)) && client.write((const uint8_t *)data.c_str(), len); Serial.println(written ? "DONE" : "FAIL"); client.stop(); } else { Serial.println("FAIL(" + String(connectRetryCount++) + ")"); } }
Gotowe urządzenie
Logi z Monitora szeregowego
Plik wynikowy
„2016-02-04 21:39:52”,11.00,40.60
„2016-02-04 21:40:52”,7.90,52.00
„2016-02-04 21:41:52”,5.20,61.80
„2016-02-04 21:42:52”,3.80,68.20
„2016-02-04 21:43:52”,2.80,74.70
„2016-02-04 21:44:52”,2.30,78.80
„2016-02-04 21:45:52”,1.90,81.90
„2016-02-04 21:46:52”,1.70,84.70
„2016-02-04 21:47:52”,1.50,86.30
„2016-02-04 21:48:52”,1.50,87.40
„2016-02-04 21:49:52”,1.40,88.50
Wizualizacja danych
Przykładowy wykres, sporządzony po wystawieniu czujnika za okno.
Widać spadek temperatury w czasie. Po ok. 10 minutach następuje ustabilizowanie się wyników.
Temperatura okazała się identyczna ze wskazywaną przez Google, po wpisaniu „Pogoda” w wyszukiwarkę.
Na zakończenie
Należy pamiętać o zmianie podstawowych stałych, jak nazwa hosta docelowego, ścieżki URI do skryptu, adresu MAC karty sieciowej!
Osobiście przepisałem po prostu MAC z mojego popsutego już laptopa (nie użyty w tym artykule).
Przy wszelkich zmianach, należy wziąć pod uwagę wielkość bufora ustalanego przez stałą REQUEST_BUFFER_SIZE.
O częstotliwości pomiarów decyduje stała timerInterval – maksymalna wartość to nieco ponad 1 minuta.
Kod źródłowy i niezbędne biblioteki
- https://github.com/fu-hsi/arduino-weather-station
- https://github.com/ntruchsess/arduino_uip
- https://github.com/markruys/arduino-DHT
Bardzo fajny i dobrze wytłumaczony kod i sposób działania.
Spoko opisane wszystko !!! Dobre fotki…
Bardzo ciekawy, solidnie napisany artykuł, zawierający dużo cennych informacji, uwag oraz wskazówek, które z pewnością się przydadzą w tym temacie 🙂