Nodo LoRaWan con Shield di Dragino.

Dragino è una ditta cinese che produce veri dispositivi per LoRa (e non solo). Fra questi uno shield Lora per Arduino.
Nulla di particolarmente complesso, giusto la componentistica per interfacciare una scheda RFM9x (monta una scheda LoRa  HopeRF Electronic- con chip SX1276 realizzato su licenza Semtech) e alcune porte per interfaccia verso il bus Arduino.
La scheda RFM9x (la x denota alcune varianti) è completa con tutta la circuiteria necessaria al chip SX1276.

Dragino LoRa Shiel per Arduino
Lo scambio dati con Arduino avviene attraverso interfaccia Seriale SPI (Clock, MOSI, MISO), più alcune linee che si possono definire di handshake denominate DIO(5:0) (Digital I/0), reset e CS (chip select). Alcuni jumper consentono di decidere a quale pin di Arduino corrisponde ai pin DIO.
Esistono diverse librerie utili ad implementare un nodo LoRaWAN, diverse derivate dalla libreria IBM LMIC (LoRaWAN MAC In C). Per Arduino si consiglia LMIC-Arduino.
I principali componenti della libreria sono:

  • Un semplice Sistema Operativo per la gestione del timing e la schedulazione dei job realtivi alle varie funzioni.
  • HAL (Hardware Abstraction Layer) definisce e gestisce le linee di I/O in modo astratto, facilitando il porting della libreria su diverse piattaforme.
  • LMIC la gestione delle funzionalità LoRaWAN.
  • AES funzioni dedicate alla criptografia.

Nel complesso LMIC realizza una macchina agli stati finiti che implementa la funzionalità della Classe A e B di LoRaWAN (specifica 1.0.3). La documentazione riportata su github.com è dettagliata. Di seguito lo schema della struttura di un nodo.

Schema HW e SW del nodo LoRA

Uso della libreria.
Nella implementazione di Arduino il timing non sfrutta l’interrupt della MCU, quindi, in alcuni casi può presentare criticità. I vari jobs (in particolare trasmissione e ricezione) devono essere brevi, la documentazione riporta: “Jobs must not be long-running”.
L’applicazione inizializza le variabili e le strutture necessarie al corretto funzionamento, e invoca la classica funzione setup() per l’inizializzazione dell’hardware:
static osjob_t sendjob;

void setup () {
// initialize run-time env
LMIC_reset();

// Start job (sending automatically starts OTAA too)
do_send(&sendjob);
}

osjob_t è una struttura che raggruppa i principali parametri utili alla gestione del protocollo


struct lmic_t { u1_t frame[MAX_LEN_FRAME]; u1_t dataLen; // 0 no data or zero length data, >0 byte count of data u1_t dataBeg; // 0 or start of data (dataBeg-1 is port) u1_t txCnt; u1_t txrxFlags; // transaction flags (TX-RX combo) u1_t pendTxPort; u1_t pendTxConf; // confirmed data u1_t pendTxLen; u1_t pendTxData[MAX_LEN_PAYLOAD]; u1_t bcnChnl; u1_t bcnRxsyms; ostime_t bcnRxtime; … … };

 

do_send() trasmette (uplink) verso la rete LoRa. Provvede alla operazione di join se questa non è già avvenuta. Le principali variabili utilizzate per l’invio dei dati sono nella struttura passata alla funzione: pendTxData[], pendTxLen, pendTxPort e pendTxConf.
Nella classe A, la ricezione avviene solo dopo la trasmissione, entro una opportuna finestra temporale. All’evento EV_TXCOMPLETE, atteso dopo la trasmissione, la struttura sendjob eventualmente conterrà i dati ricevuti. Le principali variabili sono: EV_RXCOMPLETE, EV_TXCOMPLETE , txrxFlags (per informazioni sullo stato), frame[] e dataLen / dataBeg per i dati ricevuti.
La struttura lmic_pinmap definisce i pin usati per controllare la schield, I dati vanno inseriti tenendo conto della configurazione hardware dello shield, il default, per la scheda Dragino, è il seguente:


const lmic_pinmap lmic_pins = {
  .nss = 10,
  .rxtx = LMIC_UNUSED_PIN,
  .rst = 9,
  .dio = { 2, 6, 7 },
};

Pin 10 per il chip select nelle operazioni SPI, non viene definito un pin per rxtx (LMIC_UNUSED_PIN) perché usato solo per modulazioni FSK, il reset del chip (pin 9), e le linee di I/O del chip (DIO[0], DIO[1], DIO[2]) configurate come input per gestire lo stato del chip sulla shield.
La funzione onEvent() si fa carico dei possibili eventi che si possono presentare. Come già citato, nella classe LoRaWAN A, i principali sono EV_RXCOMPLETE, EV_TXCOMPLETE.
Dopo un evento di trasmissione il sistema resta in attesa della finestra di ricezione e schedula una nuova trasmissione (di default dopo 1 minuto) dopo il tempo definito dalla variabile TX_INTERVAL.
os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);

os_setTimedCallback implementa una funzione di Callback. Il parametro do_send non è una variabile, ma il puntatore ad una funzione, in questo caso definita dall’utente, esterna alla libreria LMIC, che verrà invocata dal ‘Sistema Operativo’ quando convenuto. os_setTimedCallback altro non fa che inserire in una coda (di fatto una lista di puntatori a funzione) un job. La struttura della coda è la seguente:

typedef void (*osjobcb_t) (struct osjob_t*);
struct osjob_t {
    struct osjob_t* next;
    ostime_t deadline;
    osjobcb_t  func;
};

I job in coda possono essere nello sato di schedulato (in pratica da eseguire subito) o runnable (da eseguire entro un tempo prestabilito).

static struct {
    osjob_t* scheduledjobs;
    osjob_t* runnablejobs;
} OS;

Il loop principale del programma si riduce alla a richiamare la funzione os_runloop_once() che accede alla coda dei jobs e li esegue nell’ordine prestabilito (dipende da come sono stati sottomessi).

Trasmissione dati

L’evento EV_TXCOMPLETE è la parte più importante. All’evento si può ricevere un messaggio o schedulare la trasmissione del successivo. Questo è il relativo codice:
    case EV_TXCOMPLETE:
      Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
      if (LMIC.txrxFlags & TXRX_ACK)
        Serial.println(F("Received ack"));
      if (LMIC.dataLen) {
        Serial.println(F("Received "));
        Serial.println(LMIC.dataLen);
        Serial.println(F(" bytes of payload"));
        /*
        Qui si può inserire la gestione dei dati ricevuti
        */
      }
      // Schedule next transmission

      os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);

Nella funzione do_send si possono inserire i dati da trasmettere. Di seguito un esempio:

void do_send(osjob_t* j) {
  // Check if there is not a current TX/RX job running
  if (LMIC.opmode & OP_TXRXPEND) {
    Serial.println(F("OP_TXRXPEND, not sending"));
  } else {
    // Prepare upstream data transmission at the next possible time.
    Serial.println(F("READ SENSOR"));
    // lettura sensori. Il risultato della lettura è in mydata[]
    readSensor();  // result on mydata
    Serial.print("   Payload ");
    for (int i = 0; i < MAX_PAYLOAD; i++) {
      Serial.print(mydata[i] < 16 ? "0" : "");
      Serial.print(mydata[i], HEX);
      Serial.print(" ");
    }

    LMIC_setTxData2(1, mydata, sizeof(mydata), 0);
    Serial.println(F("Packet queued"));
  }
  // Next TX is scheduled after TX_COMPLETE event.
}

Di seguito di lettura di un sensore DHT11, per temperatura e umidità, che va completato con le opportune azioni di inizializzazione

void readSensor(void) {
  Serial.print("ENTRO");
  // Reading temperature or humidity takes about 250 milliseconds!
  // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
  float h = dht.readHumidity();
  // Read temperature as Celsius (the default)
  float t = dht.readTemperature();
  // Read temperature as Fahrenheit (isFahrenheit = true)
  float f = dht.readTemperature(true);

  // Check if any reads failed and exit early (to try again).
  if (isnan(h) || isnan(t) || isnan(f)) {
    Serial.println(F("Failed to read from DHT sensor!"));
    //return;
  }
  // Compute heat index in Fahrenheit (the default)
  float hif = dht.computeHeatIndex(f, h);
  // Compute heat index in Celsius (isFahreheit = false)
  float hic = dht.computeHeatIndex(t, h, false);

  mydata[0] = (int16_t)(t * 10) >> 8;
  mydata[1] = (int16_t)(t * 10);

  mydata[2] = (int16_t)(hic * 10) >> 8;
  mydata[3] = (int16_t)(hic * 10);

  mydata[4] = (uint8_t)h;

  Serial.print(F("Humidity: "));
  Serial.print(h);
  Serial.print(F(" %  Temperature: "));
  Serial.print(t);
  Serial.print(F(" °C "));
  Serial.print(F(" Heat index: "));
  Serial.print(hic);
  Serial.print(F("°C "));
}

Notare che il dato letto, ed esempio un float, viene convertito in due byte, operazione comoda per ridurre la lunghezza dell’array da trasmettere. Il valore andrà riconvertito in float dalla applicazione.

Configurazione
Nella libreria il file lmic\config.h viene usato per modificare alcuni parametri come, ad esempi, la frequenza di funzionamento (#define CFG_eu868 1) che di default è quella adottata nella regione europea. Altri possibili parametri sono riportati in documentazione.

 

Tags: 

Mi piace: 

5
Average: 5 (1 vote)

Commenti

Il maker ha ricominciato a

Il maker ha ricominciato a smanettare! Vedremo i prossimi frutti. 
73 Lucio

Mi piace: 

5
Average: 5 (1 vote)

Pagine