Meteorologická stanica a jej software 1

Tak a dostávame sa konečne k programovaniu mojej vlastnej meteostanici a tento krát sa pozrieme na jej softwareové vybavenie. Teda kód programu pre Arduino, ktoré zaznamenáva hodnoty z jednotlivých snímačov a posiela ich na server postavený na Raspberry Pi 3, na ktorom sú údaje uchovávané a posielané na dashboard Freeboard a rovnako aj projekt Wunderground, ale tomu sa budem venovať niekedy neskôr. Tak poďme na to!

meteostanica

Programovanie

Tak začneme arduinom. Ako vždy, kód som sa snažil čo najviac okomentovať aby bol čo najviac zrozumiteľný a pochopiteľný a aby som aj ja neskôr vedel načo to tam je a čo to robí.

Klasicky ako pri každom kóde, na začiatok si treba načítať všetky potrebné knižnice. Jednak pre jednotlive senzory, potom knižnice pre ich komunikáciu s arduinom.

#include <DallasTemperature.h>                      //DS18B20
#include <OneWire.h>                                //DS18B20
#include <Wire.h>                                   //MAX44009, SI1145, BME280
#include "Adafruit_SI1145.h"                        //SI1145
#include <BME280I2C.h>                              //BME280E
#include <EtherCard.h>                              //ENC28J60

Následne je potrebne zadefinovať premenné, ktoré budú použivať samotné senzory a premenné v ktorých si mi budeme ukladať údaje, ktoré budeme spracúvať.

//DS18B20
#define ONE_WIRE_BUS 2  //port pre teplomer
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
//Adresy jednotlivych teplomerov
DeviceAddress adresa_teplota_v_tieni = { 0x28, 0xEE, 0x4B, 0xFE, 0x19, 0x16, 0x02, 0xE7 };
DeviceAddress adresa_teplota_na_slnku = { 0x28, 0xEE, 0x56, 0xDF, 0x1C, 0x16, 0x01, 0x5F };
DeviceAddress adresa_teplota_prizemny_mraz = { 0x28, 0xC5, 0xA0, 0xB6, 0x06, 0x00, 0x00, 0x10 };
float teplota_v_tieni;
float teplota_na_slnku;
float teplota_prizemny_mraz;

//MAX4409
#define Addr 0x4A
float lux;

//SI1145
Adafruit_SI1145 uv = Adafruit_SI1145();
float UVindex;

//ML8511
int UVOUT = A0; //Output from the sensor
int REF_3V3 = A1; //3.3V power on the Arduino board
float UVintensity;

//BME280
BME280I2C bme;
bool metric = true;
void printBME280Data(Stream * client);
void printBME280CalculatedData(Stream* client);
float teplota;
float vlhkost;
float tlak;
float rosny_bod;

//ENC28J60
static byte mymac[] = { 0x74,0x69,0x69,0x2D,0x30,0x31 }; //definovanie MAC adresy
byte Ethernet::buffer[700];
static uint32_t timer;
const char meteo_server[] PROGMEM = "192.168.1.30";	//IP adresa servera na ktory sa budu posielat data
char update_parameters[200];
static uint32_t start_time = 1500;

//Moje premenne
byte iteration;	//pocet opakovani
boolean debug = true; //povolenie debugovania

Ako je z kódu jasne DS18B20 mam zapojené na digitálny pin 2, a v kóde sú uvedene adresy snímačov, ktoré si treba zistiť. Pre snímač BME280 si nastavujem premennú metric na true lebo chcem hodnoty v metrických jednotkách a teplota v celziových stupňoch. MAC adresu pre ENC28J60 si môžete zvoliť sami a zadajte IP adresu, na ktorú budú zasielané dáta. V premennej iteration mám ukladaný počet opakovaní za jeden cyklus a premennou debug povoľujem výpis hodnôt po sériovej linke. Pre normálny beh nie je potrebný.

void setup()
  {
    Serial.begin(9600);

    Serial.println("*************************");
    Serial.println("Inicializacia snimacov");
    Serial.println("*************************");
    //DS18B20
    sensors.begin();
    sensors.setResolution(adresa_teplota_v_tieni, 12);
    sensors.setResolution(adresa_teplota_na_slnku, 12);
    sensors.setResolution(adresa_teplota_na_slnku, 12);
    Serial.println("DS18B20   [OK]");
    
    //SI1145
    if (! uv.begin())
      {
        Serial.println("SI1145    [--]");
        while (1);
      }
    else Serial.println("SI1145    [OK]");
    
    //ML8511
    pinMode(UVOUT, INPUT);
    pinMode(REF_3V3, INPUT);
    Serial.println("ML8511    [OK]");

    
    //BME280
    while(!bme.begin())
      {
        Serial.println("BME280    [--]");
        delay(1000);
      }
    Serial.println("BME280    [OK]");
    
    //MAX44009
    Wire.beginTransmission(Addr);
    Wire.write(0x02);
    Wire.write(0x40);
    Wire.endTransmission();
    Serial.println("MAX44009  [OK]");

    //EN28J60
    if (ether.begin(sizeof Ethernet::buffer, mymac, 53) == 0) 
      Serial.println(F("Failed to access Ethernet controller"));
    else Serial.println("ENC28J60  [OK]");
    Serial.println("*************************");
    if (!ether.dhcpSetup())
      Serial.println(F("DHCP failed"));
  
    ether.printIp("IP:  ", ether.myip);
    ether.printIp("GW:  ", ether.gwip);  
    ether.printIp("DNS: ", ether.dnsip);  
    ether.parseIp(ether.hisip, "192.168.1.30");
    ether.printIp("SRV: ", ether.hisip);
    Serial.println("*********************************************************");
  }

Po zadefinovaní nasleduje inicializácia samotných senzorov. Tu si môžete upraviť s tým že si nastavíte veľkost prevodníka DS18B20 a teda jeho presnosť. Čim viac bitov, tým dlhšie trvá načítanie údajov ale rozdiel je v milisekundách. Štandardne je tuším 9 bitov. Ja ich mám nastavené na maximum teda na 12bitov a taktiež treba znova zadať IP adresu servera. Inak nie je potrebné nič meniť.

Aby som sa nechválil cudzím perím, tak potrebné kódy pre jednotlivé snímače a komponenty som našiel na internete, vytunil a prispôsobil si ich pre seba.

Hlavnú funkciu programu mám ošetrenú klasicky po svojom. Tá volá potrebné funkcie, v ktorých sú kódy pre obsluhu jednotlivých komponentov. Funkcie som pomenoval podľa názvu jednotlivého komponentu alebo nejako logicky. Logika celého programu je postavená tak, že neustále sa zbierajú hodnoty veličín, ktoré sa spočítavajú (samozrejme každá samostatne) a rovnako aj počet opakovaní (sčítaní). Ak sa prekročí limit 1 minúta a teda 60000 milisekúnd, vypočítajú sa priemerné hodnoty, ktoré sú následne odoslané na server. Po tomto sa hodnoty vynulujú a ide sa odznova.

void loop()
  {
    if (millis() > start_time)
      {
        Serial.println(millis()-start_time);
        start_time = millis() + 60000;   //dlzka jedneho cyklu v milisekundach
        nastav_premenne();  //Vypocita priemernu hodnotu
        if(debug == true) vypis_premenne();
        posli_data();       //poslanie dat na server
        nuluj_premenne();   //nulovanie hodnot
      }
  
    DS18B20();
    MAX44009();
    SI1145();
    ML8511();
    BME_280();
    ether.packetLoop(ether.packetReceive());
    iteration++;   //pocitanie poctu opakovani
  }

Teraz prejdem ku kódom k jednotlivým senzorom. Nebudem ich rozoberať pretože nie sú mojím dielom a nerozumiem im na 100% ale hlavne, že fungujú. Tu len krátko. Hneď na prvom je vidieť ako spočítavam údaje. A teda nový údaj sa rovná starý plus nový. Takto je to pre všetky premenné.

void DS18B20()
  {
    sensors.requestTemperatures();
    teplota_v_tieni = teplota_v_tieni + sensors.getTempC(adresa_teplota_v_tieni);
    teplota_na_slnku = teplota_na_slnku + sensors.getTempC(adresa_teplota_na_slnku);
    teplota_prizemny_mraz = teplota_prizemny_mraz + sensors.getTempC(adresa_teplota_prizemny_mraz);
  }

void MAX44009()
  {
    unsigned int data[2];
    Wire.beginTransmission(Addr);
    Wire.write(0x03);
    Wire.endTransmission();
     
    // Request 2 bytes of data
    Wire.requestFrom(Addr, 2);
     
    // Read 2 bytes of data luminance msb, luminance lsb
    if (Wire.available() == 2)
    {
    data[0] = Wire.read();
    data[1] = Wire.read();
    }
     
    // Convert the data to lux
    int exponent = (data[0] & 0xF0) >> 4;
    int mantissa = ((data[0] & 0x0F) << 4) | (data[1] & 0x0F);
    lux = lux + (pow(2, exponent) * mantissa * 0.045);
  }

void SI1145()
  {
    UVindex = UVindex + uv.readUV();
  }

void ML8511()
  {
    int uvLevel = averageAnalogRead(UVOUT);
    int refLevel = averageAnalogRead(REF_3V3);
    float outputVoltage = 3.3 / refLevel * uvLevel;
    UVintensity = UVintensity + mapfloat(outputVoltage, 0.99, 2.8, 0.0, 15.0); //Convert the voltage to a UV intensity level
  }

int averageAnalogRead(int pinToRead)
{
  byte numberOfReadings = 8;
  unsigned int runningValue = 0; 

  for(int x = 0 ; x < numberOfReadings ; x++)
  runningValue += analogRead(pinToRead);
  runningValue /= numberOfReadings;

  return(runningValue);  
}

float mapfloat(float x, float in_min, float in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

void BME_280()
  {
    printBME280Data(&Serial);
    printBME280CalculatedData(&Serial);
  }

void printBME280Data(Stream* client)
  {
    float temp(NAN), hum(NAN), pres(NAN);
    uint8_t pressureUnit(1);                                           // unit: B000 = Pa, B001 = hPa, B010 = Hg, B011 = atm, B100 = bar, B101 = torr, B110 = N/m^2, B111 = psi
    bme.read(pres, temp, hum, metric, pressureUnit);                   // Parameters: (float& pressure, float& temp, float& humidity, bool celsius = false, uint8_t pressureUnit = 0x0)
    teplota = teplota + temp;
    vlhkost = vlhkost + hum;
    tlak = tlak + pres;
  }
void printBME280CalculatedData(Stream* client)
  {
    float altitude = bme.alt(metric);
    rosny_bod = rosny_bod + bme.dew(metric);
  }

Po tomto mám zistené všetky potrebné údaje a pokračujem ďalej. Teda dosiahol som čas 60 sekúnd a je potrebné pripraviť dáta na poslanie a potom vynulovať. To urobím tak, že danú hodnotu vydelím počtom opakovaní, teda urobím aritmetický priemer.

void nastav_premenne()
  {
    teplota_v_tieni = teplota_v_tieni/iteration;
    teplota_na_slnku = teplota_na_slnku/iteration;
    teplota_prizemny_mraz = teplota_prizemny_mraz/iteration;
    lux = round(lux/iteration);
    UVindex = UVindex/iteration;
    // the index is multiplied by 100 so to get the
    // integer index, divide by 100!
    UVindex /= 100.0;
    UVindex = round(UVindex);
    UVintensity = UVintensity/iteration;
    if(UVintensity < 0) UVintensity = 0;
    teplota = teplota/iteration;
    rosny_bod = rosny_bod/iteration;
    vlhkost = round(vlhkost/iteration);
    tlak = round(tlak/iteration);
  }

void nuluj_premenne()
  {
    teplota_v_tieni = 0;
    teplota_na_slnku = 0;
    teplota_prizemny_mraz = 0;
    lux = 0;
    UVindex = 0;
    UVintensity = 0;
    teplota = 0;
    rosny_bod = 0;
    vlhkost = 0;
    tlak = 0;
    iteration = 0;
  }

Po tomto môžem posielať dáta na server. No nie je to ešte celkom tak. Dáta budem na server posielať cez HTTP POST metódu a jednotlivé dáta je potrebné pripraviť na poslanie. Pre tých, ktorý nie sú doma v tomto, zjednodušene vysvetlím. Dáta sú posielané na server jednoduchým volaním stránky s danými parametrami.
Napríklad http://adresa_servera/skript.php?premenna1=hodnota1&premenna2=hodnota2
názorne http://192.168.1.30/update.php?teplota=22&vlhkost=56&tlak=1023.
Keďže jednotlivé premenné sú rôzneho typu a rôznej dĺžky treba si ich ped volaním skriptu pripraviť lebo môže dôjsť k niečomu podobnému
http://192.168.1.30/update.php?teplota=22 &vlhkost=56 &tlak=102.
Budú tam buď prázdne znaky alebo naopak nevložia sa všetky. Treba brať do úvahy aj znak “-” ktorý sa používa pri teplote a samozrejme aj desatinnú čiarku.

void posli_data()
  {
    byte i;

    if(teplota_v_tieni < -100) i = 4; else if(teplota_v_tieni > -100 && teplota_v_tieni < -9.99) i = 3; else if(teplota_v_tieni > -10 && teplota_v_tieni < 0) i = 2; else if(teplota_v_tieni >= 0 && teplota_v_tieni < 10) i = 1;
    else i = 2;
    char s_teplota_v_tieni[8];
    dtostrf(teplota_v_tieni, 2, 2, s_teplota_v_tieni);

    if(teplota_na_slnku < -100) i = 4; else if(teplota_na_slnku > -100 && teplota_na_slnku < -9.99) i = 3; else if(teplota_na_slnku > -10 && teplota_na_slnku < 0) i = 2; else if(teplota_na_slnku >= 0 && teplota_na_slnku < 10) i = 1;
    else i = 2;
    char s_teplota_na_slnku[8];
    dtostrf(teplota_na_slnku, i, 2, s_teplota_na_slnku);

    if(teplota_prizemny_mraz < -100) i = 4; else if(teplota_prizemny_mraz > -100 && teplota_prizemny_mraz < -9.99) i = 3; else if(teplota_prizemny_mraz > -10 && teplota_prizemny_mraz < 0) i = 2; else if(teplota_prizemny_mraz >= 0 && teplota_prizemny_mraz < 10) i = 1;
    else i = 2;
    char s_teplota_prizemny_mraz[8];
    dtostrf(teplota_prizemny_mraz, i, 2, s_teplota_prizemny_mraz);

    if(lux < 10) i = 1;
    else if(lux < 100) i = 2;
    else if(lux < 1000) i = 3;
    else if(lux < 10000) i = 4; else i = 5; char s_lux[6]; dtostrf(lux, i, 0, s_lux); if(UVindex > 9) i = 2;
    else i = 1;
    char s_UVindex[3];
    dtostrf(UVindex, i, 0, s_UVindex);

    if(UVintensity < 10) i = 1; else i = 2; char s_UVintensity[6]; dtostrf(UVintensity, i, 2, s_UVintensity); if(teplota > -100 && teplota < -9.99) i = 3; else if(teplota > -10 && teplota < 0) i = 2; else if(teplota >= 0 && teplota < 10) i = 1; else i = 2; char s_teplota[7]; dtostrf(teplota, i, 2, s_teplota); if(rosny_bod > -100 && rosny_bod < -9.99) i = 3; else if(rosny_bod > -10 && rosny_bod < 0) i = 2; else if(rosny_bod >= 0 && rosny_bod < 10) i = 1;
    else i = 2;
    char s_rosny_bod[7];
    dtostrf(rosny_bod, i, 2, s_rosny_bod);

    if(vlhkost < 10) i = 1;
    else i = 2;
    char s_vlhkost[3];
    dtostrf(vlhkost, i, 0, s_vlhkost);

    if(tlak < 1000) i = 3; else i = 4; char s_tlak[5]; dtostrf(tlak, i, 0, s_tlak);
//poskladanie vsetkych hodnot
sprintf(update_parameters, "teplota_v_tieni=%s&teplota_na_slnku=%s&teplota_prizemny_mraz=%s&osvetlenost=%s&uv_index=%s&uv_intenzita=%s&teplota=%s&rosny_bod=%s&vlhkost=%s&tlak=%s", s_teplota_v_tieni, s_teplota_na_slnku, s_teplota_prizemny_mraz, s_lux, s_UVindex, s_UVintensity, s_teplota, s_rosny_bod, s_vlhkost, s_tlak);
//volanie skryptu s parametrami
ether.browseUrl(PSTR("/meteostanica/update.php?action=update&"), update_parameters, meteo_server, my_callback); } static void my_callback (byte status, word off, word len) { Serial.println(">>>");
  Ethernet::buffer[off+300] = 0;
  Serial.print((const char*) Ethernet::buffer + off);
  Serial.println("...");
}

Ako je z kódu vidieť rátam s každou jednou variantou. Uvediem príklad na teplote. Ošetrujem teplotu kvôli tomu, že zaberajú rôzny počet znakov a teda 0 až 9C, 10C a viac, 0 až -9C a potom -10C a menej. Jedno miesto zaberie aj desatinná čiarka. Obdobne je to pri všetkých premenných. Pri teplote uvažujem aj o teplote menej ako 100C pretože ak senzor je chybný, alebo nie je zapojený hádže hodnotu tuším -127C. Po úprave jednotlivých veličín si vyskladám parametre pre php skript a následne ho volám.

No a na záver uvádzam kód pre moje debugovanie a teda výpis hodnôt jednotlivých premenných po sériovej linke.

void vypis_premenne()
  {
    //DS18B20
    Serial.print("Teplota v tieni: ");
    if (teplota_v_tieni != -127.00)
      {
        Serial.print(teplota_v_tieni);
        Serial.println(" °C");
      }
    else Serial.println("CHYBA SNIMACA, SKOTROLUJ HO !!!");
    Serial.print("Teplota na slnku: ");
    if (teplota_na_slnku != -127.00)
      {
        Serial.print(teplota_na_slnku);
        Serial.println(" °C");
      }
    else Serial.println("CHYBA SNIMACA, SKOTROLUJ HO !!!");
    Serial.print("Prizemny mraz: ");
    if (teplota_prizemny_mraz != -127.00)
      {
        Serial.print(teplota_prizemny_mraz);
        Serial.println(" °C");
      }
    else Serial.println("CHYBA SNIMACA, SKOTROLUJ HO !!!");

    //MAX44009
    Serial.print("Osvetlenost: ");
    Serial.print(lux);
    Serial.println(" lx");

    //SI1145
    Serial.print("UV Index: ");
    Serial.println(UVindex);

    //ML8511
    Serial.print("UV Intenzita: ");
    Serial.print(UVintensity);
    Serial.println(" mW/cm^2");

    //BME-280
    Serial.print("Teplota: ");
    Serial.print(teplota);
    Serial.println(" °C");
    Serial.print("Rosny bod: ");
    Serial.print(rosny_bod);
    Serial.println(" °C");
    Serial.print("Vlhkost: ");
    Serial.print(vlhkost);
    Serial.println("%");
    Serial.print("Tlak: ");
    Serial.print(tlak);
    Serial.println(" hPa");

    Serial.println("*********************************************************");
    Serial.print("Pocet iteracii: ");
    Serial.println(iteration);
    Serial.println("*********************************************************");
  }

Ospravedlňujem za názvy premenných v mojom programe. Niektoré sú pôvodné s anglickým názvom a niektoré slovenské. Snáď vám to nebude robiť problém. Keďže toho kódu je celkom dosť rozhodol som sa softwareové vybavenie rozdeliť na dva časti. Tento krát je to program pre arduino. Nabudúce uvediem kód pre server. Teda budú to php skripty, ktoré budú zabezpečovať ukladanie dát do vlastnej MySQL databázy, php a shell skriptu budú potom posielať dáta na už spomínaný Freeboard a Wunderground.

 

Serial o meteostanici
1. Meteorologická stanica a jej návrh
2. Meteorologická stanica a jej senzory
3. Meteorologická stanica a jej zapojenie
4. Meteorologická stanica a jej software