Skip to main content

Stacja pogodowa na Arduino Nano

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

  1. Pomiar temperatury i wilgotności na zewnątrz pomieszczenia.
  2. 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.
  3. 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

Arduino Nano v3

Moduł LAN ENC28J60

ENC28J60 LAN Ethernet Network Board Module for arduino

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ń.

DHT11

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ń.
DHT22

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:

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.
ws

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.

  1. Standardową bibl. Ethernet z Arduono – nie pobierała IP z DHCP ani statycznie.
  2. EtherCard – nie działało UDP.
  3. 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

P1050615-001

Logi z Monitora szeregowego

ws-serial-4

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ę.
ws-graph

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

Miłej zabawy 🙂

markac

Full-stack Web Developer

3 thoughts to “Stacja pogodowa na Arduino Nano”

  1. 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 🙂

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.