RadioTest (»Nachricht Ping«)

Eine einfache RadioShuttle-Testanwendung („RadioTest“) ist für den Einstieg gedacht. Es gibt zwei Geräte, die miteinander kommunizieren und eine einfache Nachricht austauschen. Ein Gerät nennt sich Knoten und das zweite ist eine Station. Per Tastendruck (Taste A) wird eine Nachricht vom Knoten zur Station übertragen. Das geht auch umgekehrt, die Station kann auch eine Nachricht an den Knoten übertragen.

RadioShuttle RadioTest-Beispielcode

Das Beispiel ist in der RadioShuttle-Lieferung enthalten, hier der vereinfachte Code:


/*
 * The file is Licensed under the Apache License, Version 2.0
 * (c) 2017 Helmut Tschemernjak
 * 30826 Garbsen (Hannover) Germany
 */

#include "PinMap.h"
#include <arduino-mbed.h>
#include <sx1276-mbed-hal.h>
#include <RadioShuttle.h>
#include <RadioStatus.h>
#include <RadioSecurity.h>
#include <arduino-util.h>
#include <RTCZero.h>
// #define RADIO_SERVER 1

#ifdef RADIO_SERVER
bool server = true;
#else
bool server = false;
#endif

enum SensorsIDs { // Must be unique world wide.
  myTempSensorApp = 0x0001,
#ifdef RADIO_SERVER
  myDeviceID = 1,
  myCode = 0x20EE91D6, // station board id and code
  remoteDeviceID = 9,
#else
  myDeviceID = 9,
  myCode = 0x20EE91DE, // node board id and code
  remoteDeviceID = 1,
#endif
};

Im obigen Abschnitt wird festgelegt, welche App-ID und welche Gerätenummern von der Station und dem Knoten verwendet werden.


/*
 * For details review: SX1276GenericLib/sx1276/sx1276.h
 * Supported spreading factors SF 7,8, 9, 10, 11, (12 does not work well)
 * Working frequencies using the 125000 bandwidth which leaves
 * sufficient distance to the neighbour channel
 * EU: 868.1, 868.3, 868.5 (Default LoRaWAN EU channels)
 * EU: 865.1, 865.3, 865.5, 865.7, 865.9 (additional channels)
 * EU: 866.1, 866.3, 866.5, 866.7, 866.9 (additional channels)
 * EU: 867.1, 867.3, 867.5, 867.7, 867.9 (additional channels)
 * Utilisation of these channels should not exceed 1% per hour per node
 * Bandwidth changes other than 125k requires different channels distances
 */
const RadioShuttle::RadioProfile myProfile[] = {
/*
 * Our default profile
 * frequency, bandwidth, TX power, spreading factor
 */
 { 868100000, 125000, 14, 7 },
 { 0, 0, 0, 0 },
};

Hier wird die zu verwendende Frequenz, Kanalbandbreite, Sendeleistung und der LoRa-Spreadingfaktor festgelegt. Diese Daten müssen bei allen Funkteilnehmern identisch sein, dehalb für einen ersten Test bitte noch nichts ändern!

DigitalOut led(LED);
InterruptIn intr(SW0);
volatile int pressedCount = 0;

void SwitchInput(void) {
 dprintf("SwitchInput");
 led = !led;
 pressedCount++;
}

Radio *radio; // the LoRa radio modem
RadioShuttle *rs; // the RadioShuttle protocol
RadioStatusInterface *statusIntf; // the status add-on
RadioSecurityInterface *securityIntf; // the security 

Hier werden einige globale Variablen für die LED und den Taster A hinterlegt, zusätzlich eine Funktion „SwitchInput“, die aufgerufen wird, nachdem der Taster gedrückt worden ist. InterruptIn und DigitalOut sind mbed-kompatible Funktionen, natürlich können auch Arduino-Funktionen genutzt werden.


void setup() {
  MYSERIAL.begin(230400);
  InitSerial(&amp;amp;amp;MYSERIAL, 3000); // wait 2000ms that the Serial Monitor opens, otherwise turn off USB.
  SPI.begin();
  rtc.begin();

  dprintf("USBStatus: %s", SerialUSB_active == true ? "SerialUSB_active" : "SerialUSB_disbaled");

  led = 1;
  intr.mode(PullUp);
  intr.fall(callback(&amp;amp;SwitchInput));
  dprintf("MyRadioShuttle");

  RSCode err;
  RadioStatusInterface *statusIntf = NULL;
  RadioSecurityInterface *securityIntf = NULL;

  // RFM95
  radio = new SX1276Generic(NULL, RFM95_SX1276,
        LORA_SPI_MOSI, LORA_SPI_MISO, LORA_SPI_SCLK, LORA_CS, LORA_RESET, LORA_DIO0, LORA_DIO1, LORA_DIO2, LORA_DIO3, LORA_DIO4, LORA_DIO5);

  statusIntf = new MyRadioStatus();
  securityIntf = new RadioSecurity();
  rs = new RadioShuttle("MyRadioShuttle");
  err = rs-&amp;gt;AddLicense(myDeviceID, myCode);
  if (err)
    return;

  rs->AddRadio(radio, MODEM_LORA, myProfile);
  rs->AddRadioStatus(statusIntf);
  rs->ampAddRadioSecurity(securityIntf);
  rs->RegisterApplication(myTempSensorApp, &amp;amp;amp;TempSensorRecvHandler)
  if (server) {
    err = rs->Startup(RadioShuttle::RS_Station_Basic);
    dprintf("Startup as a Server: Station_Basic ID=%d", myDeviceID);
  } else {
    err = rs->Startup(RadioShuttle::RS_Node_Online);
    dprintf("Startup as a Node: RS_Node_Online ID=%d", myDeviceID);
  }
}

Die obige Initialisierung aktiviert das Funkmodul (RFM95) und die zugehörige RadioShuttle-Protokollsoftware mit einer Statusanzeige „MyRadioStatus“, welche Empfangs- und Sende-LEDs bei Aktivität blinken lässt. Per „Startup“-Funktion wird dann der Knoten bzw. der Server gestartet.

void loop() {
  static int cnt = 0;

  if (cnt != pressedCount) {
    int flags = RadioShuttle::MF_NeedsConfirm; // optional
    if (server) {
      static char msg[] = "The server feels very good today";
      rs->SendMsg(myTempSensorApp, msg, sizeof(msg), flags, remoteDeviceID);
    } else {
      static char msg[] = "Hello, the temperature is 26 celsius";
      rs->SendMsg(myTempSensorApp, msg, sizeof(msg), flags, remoteDeviceID);
    }
    cnt = pressedCount;
  }
  led = 0;
  sleep() // timer and radio interrupts will wakeup us
  led = 1;
  rs->RunShuttle(); // process all pending events
}

Die Hauptschleife prüft, ob der Taster gedrückt wurde und sendet eine Nachricht zur Gegenstation. Die Sleep-Funktion pausiert die Anwendung solange bis eine neue Nachricht reinkommt bzw. ein Taster gedrückt wird oder ein andere Aktivität den Sleep unterbricht. Bei Aktivität wird die LED-Anzeige eingeschaltet, bei viel Aktivität (z. B. USB-„Serial Monitor“-Fenster offen) blinkt die LED sehr schnell, was wie permanentes Leuchten aussieht. Bei gezogenem USB-Stecker und einem Neustart (Reset) sollte die LED aus sein und nur bei Aktivität kurz aufblinken.

Der RadioShuttle-Beispielcode enthält auch eine deepsleep()-Variante, bei der USB deaktiviert ist und ein Vielfaches an Strom gespart werden kann.

void TempSensorRecvHandler(int AppID, RadioShuttle::devid_t stationID, int msgID, int status, void *buffer, int length)
{
  switch(status) {
  case RadioShuttle::MS_SentCompleted: // A SendMsg has been sent.
    dprintf("MSG_SentCompleted: id=%d %d bytes", msgID, length);
    break;
  case RadioShuttle::MS_SentCompletedConfirmed:// A SendMsg has been sent and confirmed
    dprintf("MSG_SentCompletedConfirmed: id=%d %d bytes", msgID, length);
    break;
  case RadioShuttle::MS_SentTimeout: // A timeout occurred, number of retires exceeded
    dprintf("MSG_SentTimeout ID: %d", msgID);
   break;
  case RadioShuttle::MS_RecvData: // a simple input message
    dprintf("MSG_RecvData ID: %d, len=%d", msgID, length);
    // dump("MSG_RecvData", buffer, length);
    break;
  case RadioShuttle::MS_RecvDataConfirmed: // received a confirmed message
    dprintf("MSG_RecvDataConfirmed ID: %d, len=%d", msgID, length);
    // dump("MSG_RecvDataConfirmed", buffer, length);
    break;
  case RadioShuttle::MS_NoStationFound:
    dprintf("MSG_NoStationFound");
    break;
  case RadioShuttle::MS_NoStationSupportsApp:
    dprintf("MSG_NoStationSupportsApp");
    break;
  case RadioShuttle::MS_AuthenicationRequired: // the password does not match.
    dprintf("MSG_AuthenicationRequired");
    break;
  case RadioShuttle::MS_StationConnected: // a confirmation that the connection was accepted
    dprintf("MSG_StationConnected");
    break;
  case RadioShuttle::MS_StationDisconnected: // a confirmation that the disconnect was accepted
    dprintf("MSG_StationDisconnected");
    break;
  default:
    break;
  }
}

Der Receive Handler wird immer dann aufgerufen, wenn eine Nachricht versendet wurde oder eine neue Nachricht reinkommt bzw. ein Fehler aufgetreten ist. Abhängig von der Applikation können auch mehrere Receive Handler definiert werden, einer pro App-ID.

Nachrichten Emfangsbestätigung

Grundsätzlich ist eine Nachrichtenübertragung gesichert, wenn das Flag RadioShuttle::MF_NeedsConfirm bei der SendMsg-Funktion gesetzt ist. Somit bekommt der die Nachricht versendende Knoten erst eine Meldung über die abgeschlossene Übertragung sobald die Gegenstation die Daten sicher empfangen hat. Somit wird ausgeschlossen, dass die Nachricht nicht oder nur teilweise angekommen ist.

Kennwortautorisierung

Für jede RadioShuttle App-ID kann ein Kennwort hinterlegt werden. Somit wird die Station nur Nachrichten annehmen, wenn das Kennwort zwischen Knoten und Station identisch ist. Im Original-Beispielcode ist ein Kennwort hinterlegt (bitte in ein eigenes ändern).

bool usePassword = true; // password the can used indepenend of AES
unsigned char samplePassword[] = { "RadioShuttleFly-PleaseChange" };

Das Kennwort stellt sicher, dass fremde Geräte nicht mit Ihren Geräten kommunizieren können. Das Kennwort wird pro App ID hinterlegt, somit ist es auch möglich, eine App für alle zu erlauben, andere Apps nur mit einem Kennwort zuzulassen. Kennwörter haben keinen Protokoll-Overhead, da diese nur beim Erstkontakt zwischen Knoten und Station überprüft werden müssen. Die Dokumentation Apps-Strategie enthält weitere Hintergrundinformation zu unterschiedlichen Apps.

Kennwörter werden nicht über das Netzwerk übertragen, daher können diese auch nicht entwendet werden. Anstelle des Kennworts wird eine große Zufallszahl zusammen mit dem Kennwort in einen SHA256-bit Hashcode gewandelt, nur dieser Hashcode wird übertragen und ändert sich bei jeder Anmeldung, z. B. beim Neustart eines Geräts.

AES 128-bit  Verschlüsselung von Nachrichten

Nachdem ein Kennwort hinterlegt ist, kann die AES-Verschlüsselung aktiviert werden.

bool useAES = true; // AES needs the usePassword option on
int flags = RadioShuttle::MF_Encrypted;

AES verschlüsselt alle App-Daten mit dem Kennwort, so dass kein Netzwerkteilnehmer. der die Kommunikation protokolliert, sehen kann, was in den Nachrichten enthalten ist. Auch gefäschte Nachrichten funktionierten bei AES nicht und werden verworfen.

Für RadioShuttle ist AES128-bit völlig ausreichend, da ein Durchprobieren von 340282366920938463463374607431768211456 Versuchen (2128) sehr lange dauert und nicht realistisch ist. RadioShuttle-Kennwort und AES-Verschlüsselung sind in einem getrennten Modul „RadioSecurityInterface“ enthalten und können für andere Anforderungen angepasst oder ausgetauscht werden.

Bei allen Vorteilen, welche die AES-Verschlüsselung bietet, gibt es auch einen entscheidenen Nachteil: Pro Nachricht hat AES128-bit eine Blocklänge von 16 Bytes, die mindestens übertragen werden muss. Das dauert bei SF7 ca. 100 Millisekunden zusätzlich, bei SF11 sind das schon fast eine halbe Sekunde zusätzlich.

Die AES-Verschlüsselung kann pro Nachricht festgelegt werden, die Flags RadioShuttle::MF_Encrypted bei der Funktion „SendMsg“ müssen gesetzt sein, um die Nachricht mit AES zu verschlüsseln. Dies ist beim RadioTest-Beispielcode bereits vorgesehen.

Logging des RadioShuttle-Netzwerkpakets

Das RadioTest-Beispiel aktiviert das Protokollieren von Netzwerktransaktionen und internen Vorgängen. Dies kann hilfreich sein, um zu überprüfen, was gerade passiert. Ein Deaktivieren der Funktion „EnablePacketTrace“ schaltet das Logging ab.

Logging-Funktion per dprintf und dump

Die Funktion „dprintf“ erlaubt eine „C printf“-kompatible Ausgabe mit einem Zeitstempel pro Ausgabezeile sowie einem automatischen Anhängen vom Zeilenendzeichen.

Die Funktion „dump“ bietet eine Hexdump-Möglichkeit mit Angabe eines Namens, einer Adresse und einer Länge. Beispiele hierfür sind in der RadioTest-Anwendung vorhanden.

RadioShuttle API-Dokumentation

Die Datei „RadioShuttle.h“ enthält die komplette API-Dokumentation für das RadioShuttle-Protokoll. Zusätzlich gibt es die Datei „RadioShuttleProtocol.rtf“, welche die zugrundeliegenden Konzepte des RadioShuttle-Protokolls dokumentiert.