RadioTest (Nachricht Ping)

Die 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 (Taster „A“) wird eine Nachricht vom Knoten zur Station übertragen. Das geht auch umgekehrt, die Station kann auch eine Nachricht an den Knoten übertragen, vorausgesetzt, der Knoten wurde als Node Online konfiguriert (Voreinstellung ist Node Offline).

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-Spreizfaktor 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 den 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;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;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-&gt;AddLicense(myDeviceID, myCode);
  if (err)
    return;
  
  rs->AddRadio(radio, MODEM_LORA, myProfile);
  rs->AddRadioStatus(statusIntf);
  rs->ampAddRadioSecurity(securityIntf);
  rs->RegisterApplication(myTempSensorApp, &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.

Passwortautorisierung

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

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


Das Passwort stellt sicher, dass fremde Geräte nicht mit Ihren Geräten kommunizieren können. Das Passwort wird pro App ID hinterlegt, somit ist es auch möglich, eine App für alle zu erlauben, andere Apps nur mit einem Passwort zuzulassen. Passwö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.

Passwörter werden nicht über das Netzwerk übertragen, daher können diese auch nicht entwendet werden. Anstelle des Passworts wird eine große Zufallszahl zusammen mit dem Passwort 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 Passwort hinterlegt worden 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 Passwort, 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-Passwort 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.