Der Sketch für den Nucleon BaseNode

Der BaseNode mit BME280 als Wetterstation

Hardware

  1. Nucleon BaseNode (Aufbau hier)
  2. BME280 (Datenblatt)

Auf dem letzten Workshop haben wir nun den Nucleon BaseNode gebaut, hier geht es nun um den Sketch der zum Betrieb notwendig ist.

Der Sketch ist so aufgebaut das wir unterschiedliche Wetter/Klima Nodes in einer TTN Application verarbeiten können. Somit haben wir die Möglichkeit die entstehenden Daten geregelt zu empfangen und in unsere InfluxDB zu speichern. Auf diese kann dann unser Wetter Dashboard zugreifen und die Werte aller Nodes anzeigen.

Der Sketch kann natürlich auch an jede beliebige andere TTN Application seine Daten senden, aber mit der von uns bewirtschafteten Application ist die Datenverarbeitung schon geregelt.

Software

Wir nutzen zur Programmierung die Arduino IDE. Die notwendigen Libraries (Bibliotheken) haben wir schon alle im Gitlab zusammen getragen. Ihr könnt dieses Repository clonen um es als Sketchbook Speicherort in der Arduino IDE zu nutzen. So habt ihr dann auch gleich alle notwendigen Bibliotheken eingerichtet.

git clone https://gitlab.com/iotssl/nucleon-nodes.git

Für das hier beschriebene Projekt benötigen wir aber nur die folgenden Libraries:

Zusätzliche Rahmenbedingungen

Es wird natürlich ein Account auf der The Things Network Plattform benötigt. Dort dann eine Application anlegen oder bei uns Bescheid geben das wir den User als Collaborator (Mitarbeiter) in die Nucleon Wetter Application mit aufnehmen.

Dort in der Application wird dann ein neues Device angelegt, gebt dem Node einen sinnvollen Namen um ihn später auch im Dashboard von Grafana wieder finden zu können.

Der eigentliche Sketch

Der Sketch ist unter https://gitlab.com/iotssl/nucleon-nodes/tree/master/Weather/Weathernode_01 abgelegt.

Im Kopf der Datei ist die Payload Funktion hinterlegt um im The Things Network die empfangenen Daten wieder zurück in Felder zu decodieren

/************************************************************
 * TheThingsNetwork Payload function:
    function Decoder(bytes, port) 
    {
      var retValue =   { 
        bytes: bytes
      };
      
      retValue.batt = bytes[0] / 10.0;
      if (retValue.batt === 0)
         delete retValue.batt; 
     
      if (bytes.length >= 2)
      {
        retValue.humidity = bytes[1];
        if (retValue.humidity === 0)
          delete retValue.humidity; 
      } 
      if (bytes.length >= 3)
      {
        retValue.temperature = (((bytes[2] << 8) | bytes[3]) / 10.0) - 40.0;
      } 
      if (bytes.length >= 5)
      { 
        retValue.pressure = ((bytes[4] << 8) | bytes[5]); 
        if (retValue.pressure === 0)
          delete retValue.pressure; 
      }
       
      return retValue; 
    }
 * 
 * 
 ***********************************************************/

Danach Folgen die Deklarationen und Libs die wir benötigen um die Sensoren und die Kommunikation mit dem TTN aufzubauen.

/*******************************************************************************
* INCLUDE FILES
*******************************************************************************/

#include <lmic.h> 
#include <hal/hal.h>
#include <LowPower.h>
#include  "adcvcc.h"

Die lmic dient der Kommunikation zwischen dem Modem und dem TTN.
Die hal ist der Hardware Abstaction Layer, der das ansprechen der Hardware erleichtern soll.
Die LowPower dient dazu den Arduino in den Energiesparmodus zu versetzen.
Und die adcvcc Lib ist zum erfassen der Eingangsspannung

Nun folgen die beiden Libs für den BME280

/**********************************************************  
 *   Notwendige Libs für den BME280
 **********************************************************/
#include <Wire.h>  
#include  "BME280I2C.h"

Gefolgt von einer Handvoll Definitionen um uns das Leben etwas einfacher zu gestalten:

/**********************************************************
 * Defines
 **********************************************************/
#define debugSerial Serial 
#define SHOW_DEBUGINFO  
#define debugPrintLn(...) { if (debugSerial) debugSerial.println(__VA_ARGS__); }
#define debugPrint(...) { if (debugSerial) debugSerial.print(__VA_ARGS__); } 

#define TRACKINGINTERVAL    10    // 10 seconds (for tracking) 
// im TRACKINGINTERVAL schaltet den Node aus wenn ihr nicht unterweg seid
// es soll das Netz ja nicht unnötig mit Daten geflutet werden
#define FASTINTERVAL    60        // 60 seconds (for testing)
#define NORMALINTERVAL  900        // (5)15 minutes (normal)

Wir können recht einfach hier zwischen 3 Sende Intervallen wählen

  • TRACKINGINTERVAL welches nur bei Tracking Nodes eingesetzt werden sollte
  • FASTINTERVAL nutzt man wenn man einen Fehler sucht, so werden dann alle 60 Sekunden neue Werte versendet.
  • NORMALINTERVAL ist das Senden alle 15 Minuten, damit hat der Node bei einem 700mAh Akku eine Lebensdauer von über 130 Tagen bis der Akku erneut geladen werden möchte.

Der nächste Abschnitt behandelt das TTN Modem und beschreibt die physikalische Pin-Belegung auf dem Board:

/********************************************************
 * Pin Mapping RFM95W zu Arduino
 ********************************************************/
#define LMIC_NSS    6
#define LMIC_RXTX   LMIC_UNUSED_PIN
#define LMIC_RST    5
#define LMIC_DIO0   2
#define LMIC_DIO1   3
#define LMIC_DIO2   4


const lmic_pinmap lmic_pins = {
    .nss = LMIC_NSS,
    .rxtx = LMIC_RXTX,   
    .rst = LMIC_RST,
    .dio = {LMIC_DIO0, LMIC_DIO1, LMIC_DIO2},  
}; 

Nun benötigen wir die Zugangsdaten zum TTN um die TTN Config zu befüllen. Dieser holen wir uns aus der TTN Console. Der hier gezeigte Sketch ist für die ABP Kommunikation vorbereitet, eine OTAA Version wird später noch folgen, sie ist mittlerweile auch erfolgreich getestet worden, aber noch nicht veröffentlicht. (Stand 05.03.2019)

/*****************************************************
 * TTN Config
 *****************************************************/
/***************************************************** 
 *  ABP 
 *****************************************************/
static const PROGMEM u1_t NWKSKEY[16] = {  }; // LoRaWAN NwkSKey, network session key 
static const u1_t PROGMEM APPSKEY[16] = {  }; // LoRaWAN AppSKey, application session key 
static const u4_t DEVADDR = 0x000000 ; // LoRaWAN end-device address (DevAddr)
/****************************************************
 * OTAA
 * 
 * These callbacks are only used in over-the-air activation, so they are
 * left empty here (we cannot leave them out completely unless
 * DISABLE_JOIN is set in config.h, otherwise the linker will complain).
 ****************************************************/
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }

Die Variablen NWKSKEY, APPSKEY und DEVADDR sind direkt beim Node in der TTN Console heraus zu kopieren hier entsprechend einzutragen.

Das Objekt erstellen um die Daten zu versenden

/*************************************************** 
 * global enviromental parameters 
 ***************************************************/
static osjob_t sendjob; 

Noch einige Variablen für die Wetter Daten definieren

/***************************************************
 * Die Variablen für den BME280
 ***************************************************/
static float temp = 0.0;
static float pressure = 0.0;
static float humidity = 0.0;    
/**************************************************

Das notwendige Objekt für den BME bauen

/**************************************************
 * Das bme Objekt
 * Default : forced mode, standby time = 1000 ms
 * Oversampling = pressure ×1, temperature ×1, humidity ×1, filter off,
 */
BME280I2C bme;
/**************************************************

Das oben definierte Intervall zuweisen

int interval = TRACKINGINTERVAL; 

Die beiden notwendigen lmic Variablen auf 0 setzen

byte LMIC_transmitted = 0;
byte LMIC_event_Timeout = 0;

Somit sind alle Vorbereitungen getroffen. Nun folgen die Funktionen des Programms.

Hier wird einfach ein Counter hoch gezählt der innerhalb der Lib ADCVCC genutzt wird.

/* ======================================================================
Function: ISR(ADC_vect)
Purpose : IRQ Handler for ADC 
Input   : - 
Output  : - 
Comments: used for measuring 8 samples low power mode, ADC is then in 
          free running mode for 8 samples
====================================================================== */
ISR(ADC_vect)  
{
  // Increment ADC counter
  _adc_irq_cnt++;
}

Die folgende Funktion liest den BME aus und liefert die Rohwerte in die Variablen, sie werden kurz vor dem senden befüllt.

/*******************************************************************************
 * Die Funktion updateEnvParameters dient dazu Sensoren auszulesen
 * Sie wird beim Senden aufgerufen
*******************************************************************************/
void updateEnvParameters()
{
/*******************************************************************************
 * Die drei Variablen für Temperatur, Luftdruck und Luftfeuchtigkeit werden befüllt
 * Aufruf vor jedem Senden damit die Werte auch aktuell sind
*******************************************************************************/
   BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
   BME280::PresUnit presUnit(BME280::PresUnit_Pa);
 
    temp = bme.temp();
    pressure = bme.pres();    // 1 = hPa (milliBar)  
    humidity =  bme.hum();
} 

Die Funktion onEvent fängt die Events der lmic ab und arbeitet die hinterlegten Aktionen ab. Sie ist der wichtige Teil in der Kommunikation zwischen Node und TTN

/*******************************************************************************
 * onEvent
 * Hier werden die Events der LMIC abgefangen um Aktionen daraus zu generieren
 * Zur Optimierung sind einige debugPrint Aufrufe auskommentiert das würde den 
 * Code nur unnötig aufblähen. Zum Debuging kann man es jederzeit wieder einschalten
 *******************************************************************************/
void onEvent (ev_t ev) 
{
    debugPrint(os_getTime());
    debugPrint(": ");
    debugPrintLn(ev);
    switch(ev) 
    {
        case EV_SCAN_TIMEOUT:
            //debugPrintLn(F("EV_SCAN_TIMEOUT"));
            break;
        case EV_BEACON_FOUND:
            //debugPrintLn(F("EV_BEACON_FOUND"));
            break;
        case EV_BEACON_MISSED:
            //debugPrintLn(F("EV_BEACON_MISSED"));
            break;
        case EV_BEACON_TRACKED:
            //debugPrintLn(F("EV_BEACON_TRACKED"));
            break;
        case EV_JOINING:
            //debugPrintLn(F("EV_JOINING"));
            break;
        case EV_JOINED:
            //debugPrintLn(F("EV_JOINED"));
            break;
        case EV_RFU1:
            //debugPrintLn(F("EV_RFU1"));
            break;
        case EV_JOIN_FAILED:
            //debugPrintLn(F("EV_JOIN_FAILED"));
            break;
        case EV_REJOIN_FAILED:
            //debugPrintLn(F("EV_REJOIN_FAILED"));
            break;
        case EV_TXCOMPLETE:
            debugPrintLn(F("EV_TXCOMPLETE"));
            if (LMIC.txrxFlags & TXRX_ACK)
              debugPrintLn(F("R ACK")); // Received ack
            if (LMIC.dataLen) 
            {
              debugPrintLn(F("R "));
              debugPrintLn(LMIC.dataLen);
              debugPrintLn(F(" bytes")); // of payload
            }            
            // Schedule next transmission
            // os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
            LMIC_transmitted = 1; 
            break;
        case EV_LOST_TSYNC:
            //debugPrintLn(F("EV_LOST_TSYNC"));
            break;
        case EV_RESET:
            //debugPrintLn(F("EV_RESET"));
            break;
        case EV_RXCOMPLETE:
            // data received in ping slot
            //debugPrintLn(F("EV_RXCOMPLETE"));
            break;
        case EV_LINK_DEAD:
            //debugPrintLn(F("EV_LINK_DEAD"));
            break;
        case EV_LINK_ALIVE:
            //debugPrintLn(F("EV_LINK_ALIVE"));
            break;
         default:
            //debugPrintLn(F("Unknown event"));
            break;
    }
}

Das eigentliche versenden der Daten passiert in der Funktion do_send. Dort wird das holen der Sensor Daten hinterlegt, die Funktion updateEnvParameters() erledigt die Abfrage der Sensoren. Die Spannung am Eingang und damit am Akku fragt die Funktion readVcc() ab.

Der folgende Block codiert die Sensor Daten in die Bytes des Payload

        int t = (int)((temp + 40.0) * 10.0); 
        // t = t + 40; => t [-40..+85] => [0..125] => t = t * 10; => t [0..125] => [0..1250]
        int p = (int)(pressure);  // p [300..1100]
        int h = (int)(humidity);

Der Payload besteht aus dem folgenden Array:

        unsigned char mydata[6];
        mydata[0] = batvalue;      
        mydata[1] = h & 0xFF; 
        mydata[2] = t >> 8;
        mydata[3] = t & 0xFF;
        mydata[4] = p >> 8;
        mydata[5] = p & 0xFF;  

Dort werden dann die Sensordaten an das Array übertragen und mit dem Aufruf

        LMIC_setTxData2(2, mydata, sizeof(mydata), 0);

Werden die Daten übertragen.

Hier der komplette Funktionsaufruf mit den Kommentaren sollte es relativ gut lesbar sein:

/************************************************************************************
 * do_send
 * Hier werden nun die Daten versendet
 * Auch die Abfrage der Spannung ist an dieser Stelle eingebaut
 ************************************************************************************/
void do_send(osjob_t* j)
{
    // Check if there is not a current TX/RX job running
    if (LMIC.opmode & OP_TXRXPEND) 
    {
        debugPrintLn(F("OP_TXRXPEND")); //P_TXRXPEND, not sending
    } 
    else 
    {
        // Prepare upstream data transmission at the next possible time.
 
        // Here the sensor information should be retrieved 
        //  Pressure: 300...1100 hPa
        //  Temperature: -40…85°C  

        updateEnvParameters();
 
        int batt = (int)(readVcc() / 100);  // readVCC returns  mVolt need just 100mVolt steps
        byte batvalue = (byte)batt; // no problem putting it into a int. 
       
#ifdef SHOW_DEBUGINFO
        debugPrint(F("T="));
        debugPrintLn(temp); 
        
        debugPrint(F("P="));
        debugPrintLn(pressure); 
        
        debugPrint(F("H="));
        debugPrintLn(humidity); 
      
        debugPrint(F("B="));
        debugPrintLn(batt);
        debugPrint(F("BV="));
        debugPrintLn(batvalue);  
#endif
        int t = (int)((temp + 40.0) * 10.0); 
        // t = t + 40; => t [-40..+85] => [0..125] => t = t * 10; => t [0..125] => [0..1250]
        int p = (int)(pressure);  // p [300..1100]
        int h = (int)(humidity);
// Der Payload wird vorbereitet

        unsigned char mydata[6];
        mydata[0] = batvalue;      
        mydata[1] = h & 0xFF; 
        mydata[2] = t >> 8;
        mydata[3] = t & 0xFF;
        mydata[4] = p >> 8;
        mydata[5] = p & 0xFF;  
           
// Die Daten werden übertragen
// Wichtig ist an dieser Stelle das der Port 2 genutzt wird
            
        LMIC_setTxData2(2, mydata, sizeof(mydata), 0);
        debugPrintLn(F("PQ")); //Packet queued
    }
    // Next TX is scheduled after TX_COMPLETE event.
}

Damit alles was wir bis jetzt im Programm gesehen haben benötigt man natürlich auch noch ein Setup das wie der Loop (weiter unten) beim Arduino immer automatisch aufgerufen wird.

 /******************************************************************************+
  * Setup 
  * 
  * Hier werden die Parameter der LMIC eingestellt
  * Die Anmeldung des Node beim TTN
  * Die genutzte lokale Funk Frequenzen und die daraus resultierenden Kanäle
  * Aber auch die Datenrate und Sender Stärke (DR_SF7,14)
  * 
  */

  
void setup() {
    Serial.begin(115200);
    debugPrintLn(F("Starting"));
    // Init BME208
    if (!bme.begin()) 
    {  
        debugPrintLn(F("No valid bme280 sensor!"));
        delay(1000);  // allow serial to send.
        while (1) 
        {
           LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);   
        }

        switch(bme.chipModel())
        {
          case BME280::ChipModel_BME280:
            Serial.println("Found BME280 sensor! Success.");
            break;
          case BME280::ChipModel_BMP280:
            Serial.println("Found BMP280 sensor! No Humidity available.");
            break;
          default:
            Serial.println("Found UNKNOWN sensor! Error!");
        }
    }          
    // LMIC init
    os_init();
    // Reset the MAC state. Session and pending data transfers will be discarded.
    LMIC_reset();

    // Set static session parameters. Instead of dynamically establishing a session
    // by joining the network, precomputed session parameters are be provided.
    #ifdef PROGMEM
    // On AVR, these values are stored in flash and only copied to RAM
    // once. Copy them to a temporary buffer here, LMIC_setSession will
    // copy them into a buffer of its own again.
    uint8_t appskey[sizeof(APPSKEY)];
    uint8_t nwkskey[sizeof(NWKSKEY)];
    memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
    memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
    LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
    #else
    // If not running an AVR with PROGMEM, just use the arrays directly
    LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
    #endif

    #if defined(CFG_eu868)
    // Set up the channels used by the Things Network, which corresponds
    // to the defaults of most gateways. Without this, only three base
    // channels from the LoRaWAN specification are used, which certainly
    // works, so it is good for debugging, but can overload those
    // frequencies, so be sure to configure the full frequency range of
    // your network here (unless your network autoconfigures them).
    // Setting up channels should happen after LMIC_setSession, as that
    // configures the minimal channel set.
    // NA-US channels 0-71 are configured automatically
    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band


    // For single channel gateways: Restrict to channel 0 when defined above
#ifdef CHANNEL0
    LMIC_disableChannel(1);
    LMIC_disableChannel(2);
    LMIC_disableChannel(3);
    LMIC_disableChannel(4);
    LMIC_disableChannel(5);
    LMIC_disableChannel(6);
    LMIC_disableChannel(7);
    LMIC_disableChannel(8);
#endif

    // TTN defines an additional channel at 869.525Mhz using SF9 for class B
    // devices' ping slots. LMIC does not have an easy way to define set this
    // frequency and support for class B is spotty and untested, so this
    // frequency is not configured here.
    #elif defined(CFG_us915)
    // NA-US channels 0-71 are configured automatically
    // but only one group of 8 should (a subband) should be active
    // TTN recommends the second sub band, 1 in a zero based count.
    // https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json
    LMIC_selectSubBand(1);
    #endif

    // Disable link check validation
    LMIC_setLinkCheckMode(0);

    // TTN uses SF9 for its RX2 window.
    LMIC.dn2Dr = DR_SF9;

    // Set data rate and transmit power for uplink (note: txpow seems to be ignored by the library)
    LMIC_setDrTxpow(DR_SF7,14); 
    debugPrintLn(F("S")); // Setup complete!"
//    delay(1000);  // allow serial to send.
}

Beim Setup werden die Parameter der lmic eingerichtet, die Anmeldedaten für das TTN übergeben und die Datenrate so wie Sendeleistung eingestellt.

Die letzte Funktion ist der Loop, in dem das Hauptprogramm abgearbeitet wird.

/*******************************************************************************
 * loop
 * Hier nun der Loop, also das Hauptprogramm
 * 
 */

void loop() 
{ 
    // Start job
    do_send(&sendjob);
    // Wait for response of the queued message (check if message is send correctly)
    os_runloop_once();
    // Continue until message is transmitted correctly
    //debugPrintLn("\tWaiting for transmittion\n"); 
    debugPrintLn(F("W\n"));
    while(LMIC_transmitted != 1) 
    {
        os_runloop_once();
        // Add timeout counter when nothing happens:
        LMIC_event_Timeout++;
        delay(1000);
        if (LMIC_event_Timeout >= 60) 
        {
            // Timeout when there's no "EV_TXCOMPLETE" event after 60 seconds
            debugPrintLn(F("\tETimeout, msg not tx\n"));
            break;
        } 
    }

    LMIC_transmitted = 0;
    LMIC_event_Timeout = 0;
    
/******************************************************************************    
 *     Hier wird der Node in den Schlafmodus gelegt
*******************************************************************************/
 #ifdef SHOW_DEBUGINFO
    debugPrintLn(F("Going to sleep."));
#endif  
 if (interval > TRACKINGINTERVAL)
   { 
    for (int i = 0; i < interval; i++)
    {  
          i +=7 ; // no normal 1 second run but 8 second loops m.      
          // Enter power down state for 8 s with ADC and BOD module disabled
          LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);   
    }
   }   
#ifdef SHOW_DEBUGINFO     
    debugPrintLn("---");
#endif
}

Im Loop legen wir den Node auch in den Schlafmodus um Energie zu sparen.

Nun haben wir das komplette Programm kennen gelernt. Die Übertragung des angepassten Programms erfolgt mit der Arduino IDE und einem angeschlossenen FTDI. Sobald die Daten auf dem Node sind beginnt das Programm mit der Arbeit und die Sensordaten werden schon mal in das TTN zu der ausgewählten Applikation übertragen.

Habt Ihr eine der Nucleon Applikationen gewählt ist es nicht notwendig den Payload Decoder einzurichten, wenn ihr eine eigene Applikation nutzt muss der Decoder noch in die Applikation eingebaut werden. Der Decoder liegt im Kopf des Sketch bei und wird unter dem Reiter Payload Formats in das Feld decoder eingetragen.

Im nächsten Beitrag werde ich die Aufbereitung der Daten mittels Node-RED erklären und danach noch den Aufbau der Grafana Visualisierung.

Ich hoffe das dieser Teil der BaseNode Dokumentation verständlich ist und euch beim bauen eurer eigenen Nodes behilflich sein wird.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.