ArduinoBLE

Für den kleinen Seeed Studio XIAO-NRF52840 brauche ich die ArduinoBLE Library für die Nutzung der BLE Dienste. Diese Library ist sehr einfach in der Anwendung.

Hier dokumentiere ich wie ich sie anwende und welche Erfahrung ich damit machte.

 

Using GATT

In Buetooth kann man Services mit Charakter festlegen und die den Client Verfügbar machen, sogenannte GATT Services.

Advertising

Mit Advertising publiziert man sein Device im Bluetooth Umfeld. Man kann damit bereits dynamische Daten mitgeben.

Goodies

Hier stehen Ergänzungen und Einschränkungen, welche in besonderen Fällen wichtig sein könnten.

BLE GATT Service

BLE Brief

Es gibt sehr viele und gute Information über BLuetooth LE, so dass ich davon ausgehe jeder kann sich schlau machen so wie er kann und will. Hier sieht man eine Variante mit ArduinoBLE die man ohne grosses Lesen, Lernen und Wissen zum laufen bringt. Dann hat man eine Basis zum Lernen und Spielen.

Um Bluetooth LE zu benutzen hat man verschiedene Möglichkeiten. Man kann mit Broadcasting verbindungslos Informationen versenden, verbindungsorientiert mit GATT Services oder mit Mesh many to many Daten austauschen.  Mein erster BLE Versuch habe ich connected orientiert mit GATT Service und der ArduinoBLE Library unternommen. Nachfolgend sind die Ergebnisse dokumentiert. Weil ich nicht wusste wie ich die Daten formatieren soll, habe ich den Binary Sensor Service BSS von Bluetooth implementiert (siehe den BSS Blog).

Ein Problem beim Implementieren von Bluetooth Lösungen ist die Tatsache, dass man einen BLE Partner braucht, der entweder zuhört oder Daten sendet. Um am Anfang es zu vereinfachen steht auf den Smartphone eine NFR Connect for Mobile App von Nordic Semiconductor zur Verfügung.  

 

Es wird empfohlen,  gewisse GATT Services immer zu implementiere. Im ArduinoBLE sind diese versteckt.

  • Generic Access Profile (GAP) service.
  • Name and appearance characteristics within the GAP service.

Später werde ich darauf zurückkommen.

UUID (universally unique identifier)

Eine UUID ist ein universell eindeutige ID , die garantiert überall zu jeder Zeit eindeutig ist“ (Bluetooth 4.2 spec, Vol 3, Part B, Section 2.5.1 UUID). Eine UUID ist ein 128-Bit-Wert. Es gibt von der Bluetooth SIG reservierte UUIDs, die im Allgemeinen durch ihre 16-Bit-Aliase dargestellt werden. Diese Aliasnamen dienen der Vereinfachung und stellen einen 128-Bit-Wert dar, der wie folgt berechnet wird:
128-Bit-Wert = 16-Bit-Wert * 2^96 + BluetoothBaseUUID

wobei die BluetoothBaseUUID 00000000-0000-1000-8000-00805F9B34FB lautet Beispiele sind:
0x180F -> Batteriedienst-UUID (128-Bit-UUID: 0000180F-0000-1000-8000-00805F9B34FB)
0x2A00 -> Gerätenamen-Charakteristik UUID (128-Bit UUID: 00002A00-0000-1000-8000-00805F9B34FB)

Da mein Sensor BLE Standard verwendet und ich nicht beabsichtige ein verkaufsfähiges Produkt zu erstellen, verwende ich überall die 16-Bit Aliases.

Programmbeschreibung

Das Programm macht mehr oder weniger nichts anderes als Sensoren abzufragen und bei einer Änderung sie an einen  Central sendet. Für die Kommunikation zwischen Peripheral und Central wird das Bluetooth Protokoll Binary Sensor Service umgesetzt. Im weiteren werden noch Advertising Daten broadcasted die nach einem individuellen Schema umgesetzt wurde.

Das Batterie und Power Management verschieben wir auf später, da es ein eigenes Thema mit entsprechendem Effort sein wird.

 

Im Setup wird alles BLE Spezifische  aufgesetzt und initialisiert. Danach ist eigentlich der Perifpheral im Loop bereit für Bluetooth Connection.

Der Loop macht fünf Sachen

  1.  Prüfen ob ein Central connected hat und sich dies mit den StateManager merken
  2. Nach einem Intervall von 2 Sek. prüfen ob ein digitaler Input gewechselt hat
  3. Analogeingänge prüfen ob ein Wert eines Piezo Element anliegt. wird ein Wert gelesen, dann wird er im Ringbuffer gespeichert.
  4.  Ist es der erste Wert im Ringbuffer, dann wird ein Timer gestartet um ein Werte Serie zu bilden.
  5. Ist der Timer abgelaufen, dann wird die Werte-Serie ausgewertet und der Ringbuffer geleert.
  • Der Statemanager verwaltet die verschiedenen Stauts und zeigt sie mit der farbigen LED an
  • Ist der Satus Connected wahr, dann werden bei Änderungen die Werte an den Central gesendet.
  • Erfüllen die Werte in der Werte-Serie die Kriterien, dann wird eine Vibration oder Glasbruch gemeldet.
  • Vibrationen werden nur bei geschlossenen Fenster ermittelt und nach einer gewissen Zeit zurück gesetzt.
  • Glasbruch werden immer ermittelt und bleiben erhalten bis sie manuell zurückgesetzt werden.
// This are the BLE Peripheral Information
#define    SYSID  "TIS.001.XIAO-NRF52840"
#define    MODEL  "TIS.HOBRO.WINDOW"
#define    SERIE  "001.001.240525"
#define    FWREV  "ArduinoBLE V 1.3.6"
#define    HWREV  "X 1.0-0"
#define    SWREV  "X 0.0-4"
#define    FIRMA  "TIS-Switzerland"
// Appearance of BLE
#define WINDOW_APPEARANCE   0X0706 // Catecory => Access Control, dh. = zugang zum objekt
// BLE Services
#define DEVICEINFOSERVICEVALUE          "180A"
#define BATTERYSERVICEVALUE             "180F"
#define BINARYSENSORSERVICEVALUE        "183B"
#define AUTOMATIONIOSERVICEVALUE        "1815"
// BLE Characteristics
#define SYSIDCHARACTERVALUE             "2A23"
#define MODELCHARACTERVALUE             "2A24"
#define SERIECHARACTERVALUE             "2A25"
#define FWREVCHARACTERVALUE             "2A26"
#define HWREVCHARACTERVALUE             "2A27"
#define SWREVCHARACTERVALUE             "2A28"
#define FIRMACHARACTERVALUE             "2A29"
// BLE Descriptor type
#define USERDESCRIPTION                 "2901"
#define PRESENTATIONFORMAT              "2904"
#define BATTERYLEVELCHARAKTERVALUE      "2A19"
#define BSSCONTROLPOINTCHARACTERVALUE   "2B2B"
#define BSSRESPONSECHARACTERVALUE       "2B2C"
// Common globals
#define CRITICALBATTERYLEVEL                 0 // adjust level if battery is connected
#define MINPIEZOLEVEL                      150 // the level must have to add to the serie of values
#define MINPIEZODELTA                       25 // the value differs from brevious to store;
#define BLEADR001               51156894393645  // first XIAO for not forget

In der Definitiondatei habe ich mehr oder weniger alle UUID die ich benötige definiert. Am Anfang stehen meine Device Information. Ich habe denGATT deviceInformation Service genommen, weil man damit schon mal auf dem Central lesbare Informationen sieht.

// Bluetooth® Services
BLEService                      deviceInfoService(   DEVICEINFOSERVICEVALUE   );
BLEService                      batteryService(      BATTERYSERVICEVALUE      );
BLEService                      binarySensorService( BINARYSENSORSERVICEVALUE ); 

BLECharacteristic               bssControlPointChar( BSSCONTROLPOINTCHARACTERVALUE,  BLEWrite | BLENotify,256, *buffer);
BLECharacteristic               bssResponseChar(     BSSRESPONSECHARACTERVALUE,      BLERead  | BLENotify,256, *buffer);
BLEUnsignedCharCharacteristic   batteryLevelChar(    BATTERYLEVELCHARAKTERVALUE,     BLERead  | BLENotify); 

Wir müssen zuerts die Services und die zugehörigen Characteristics deklarieren.  Bei den Characteristics muss man festlegen ob der Central schreiben, lesen darf und ob er benachrichtigt werden kann.

   if (!BLE.begin()) {
        StateManager.SetState(error);
        while (1);
   }
    binarySensor.InitNrOfSensors(8,0,4); // 4 ocl-shas, 4 ocl-glasbr. 0 human and 4 vibration
    BLE.setLocalName( "TIS-WindowMonitor");
    BLE.setDeviceName("TIS-WindowMonitor");
    // Battery Service
    batteryService.addCharacteristic(batteryLevelChar); 
    BLE.addService(batteryService);                     
    batteryLevelChar.writeValue(oldBatteryLevel);       // set initial value for this characteristic
    // Sensor Service
    binarySensorService.addCharacteristic(bssControlPointChar);
    binarySensorService.addCharacteristic(bssResponseChar);
    BLE.addService(binarySensorService);
    // Device Info Service
    BLECharacteristic  sysIdCharacter = BLECharacteristic(SYSIDCHARACTERVALUE, BLERead, SYSID);
    BLECharacteristic  modelCharacter = BLECharacteristic(MODELCHARACTERVALUE, BLERead, MODEL);
    BLECharacteristic  serieCharacter = BLECharacteristic(SERIECHARACTERVALUE, BLERead, SERIE);
    BLECharacteristic  fwRevCharacter = BLECharacteristic(FWREVCHARACTERVALUE, BLERead, FWREV);
    BLECharacteristic  hwRevCharacter = BLECharacteristic(HWREVCHARACTERVALUE, BLERead, HWREV);
    BLECharacteristic  swRevCharacter = BLECharacteristic(SWREVCHARACTERVALUE, BLERead, SWREV);
    BLECharacteristic  firmaCharacter = BLECharacteristic(FIRMACHARACTERVALUE, BLERead, FIRMA);
    deviceInfoService.addCharacteristic(sysIdCharacter);
    deviceInfoService.addCharacteristic(modelCharacter);
    deviceInfoService.addCharacteristic(serieCharacter);
    deviceInfoService.addCharacteristic(fwRevCharacter);
    deviceInfoService.addCharacteristic(hwRevCharacter);
    deviceInfoService.addCharacteristic(swRevCharacter);
    deviceInfoService.addCharacteristic(firmaCharacter);
    BLE.addService(deviceInfoService);

    BLE.setAppearance(WINDOW_APPEARANCE);
    BLE.setEventHandler(BLEConnected, blePeripheralConnectHandler);
    BLE.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);
    bssControlPointChar.setEventHandler(BLEWritten,bssControllPointWritten);
    BLE.advertise();  // shout out I am here

Im SETUP müssen wir die Characteristics den GATT Services hinzufügen und anschliessend dem BLE Peripheral beifügen.

  • Wir definieren die Namen damit wir im Central nach diesem Device suchen können. mit unterschiedlichen Namen bei setlocalname und setdevicename war das Auffinden nicht konsistent. Ich fand nicht heraus wann welcher im advertising gilt.
  • Die DeviceInformation Characteristics werden direkt mit Werten geladen, darum müssen sie im setup erstellt werden.
  • Anschliessend setzen wir die Appearance.
  • Drei Eventhandler, um Connection, Disconnection mitzubekommen und der dritte, wenn der Central uns etwas sendet.
  • Am Ende des SETUP starten wir mit dem advertise, damit uns jemand finden kann.
/// @brief                  This callback is excecuted whenever the character is written by the peripheral.
///                         It does disassembling the datastream, build the coresponding response  and semd
///                         it to the charcateristic in fragments if needed.
/// @param central          is the central that the characteristc belongs.
/// @param characteristic   is the character of hte central that has been written 
void bssControllPointWritten(BLEDevice central, BLECharacteristic characteristic) {


    int sTyp[]      = {PARVALTYPSOCL,PARVALTYPSHDE,PARVALTYPSVIP,PARVALTYPMOCL,PARVALTYPMHDE,PARVALTYPMVIP,240};
    byte b[256]     = {0};
    byte *ptBuffer  = &b[0]; 
    int len         = 0;
    int cmd;
    int idx ;

    if (characteristic.valueLength() > 0) {
        len = characteristic.readValue(ptBuffer, 256);
        JoinFragment.Add(ptBuffer, len);
    } else {
        // Serial.println("Callback can't read");
        return ;
    }
   
    int datalength = JoinFragment.GetLength();
    if (JoinFragment.IsLast() && datalength > 0){
        int result = binarySensor.DisasemblingControlCommand(0, JoinFragment.Data, datalength);
        if (result > 0) {
           cmd  = result /10;
           idx  = result %10;
           if (cmd == 1) binarySensor.CreateGetSensorStatusResponse(sTyp[idx], bssResponseChar);
           else 
           if (cmd == 2) binarySensor.CreateSettingSensorResponse(  sTyp[idx], bssResponseChar);
        }
    }
}

/// @brief                  If the central is connected then we set the state and then 
///                         we must start again the advertisment if we want to
///                         broadcast our sensors by advertise data.
/// @param central          The central that is connected.
void blePeripheralConnectHandler(BLEDevice central) {

    StateManager.SetState(connected);
    BLE.advertise();  
}

void blePeripheralDisconnectHandler(BLEDevice central) {

   // BLE.advertise(); 
    StateManager.SetState(disconnected);
}

Der StateManager gibt mir den aktuellen Status des Peripheral. Damit wird die BuildinLED  bedient und die BLE Connection  aktualisiert.  Somit kann ich die Sensoren abfragen unabhängig des BLE Dienstes. Die LED Farben werden anhand der Truetable priorisiert und gesetzt. Der StateManager verwaltet alle Status so dass keine globalen Variablen verwendet werden müssen.

    /// @brief          Sets the private property of different state and then depending of the property 
    ///                 the state of the applicateion is evaluatet.  The code is based on a true tabelle
    ///                 which says when should the buildin led have what color. the color shows you the current state of
    ///                 the device.
    /// @param state    the property you want set.
    /// @return         not used yet.
    int StatusOfWindow::SetState(windowState state){

        ledColor color;
        switch(state) {
            case disconnected: isConnected  = false; break;
            case connected:    isConnected  = true;  break;
            case vibrationOff:  _vibration  = false; break; 
            case vibrationOn:   _vibration  = true;  break; 
            case glassOk:     _glassbroken  = false; break; 
            case glassBreak:  _glassbroken  = true;  break; 
            case batLow:       _batteryLow  = true;  break; 
            case batOk:        _batteryLow  = false; break; 
            case closeLe:        _OpenLeft  = false; break; 
            case openLe:         _OpenLeft  = true;  break; 
            case closeMi:      _openMiddle  = false; break; 
            case openMi:       _openMiddle  = true;  break;
            case closeRi:       _openRight  = false; break;
            case openRi:        _openRight  = true;  break;
            case error:             _error  = true;  break;  
            case errorOff:          _error  = false; break;
            default:    break;
        }
        if (_batteryLow || _error) color = red; 
            else if (_glassbroken) color = magenta;
                else if ( _OpenLeft || _openMiddle || _openRight) color = green;
                    else if (_vibration) color = yellow;
                        else if (!isConnected) color = blue;
                            else color = none;
        SetBoardLed(color);
        return 0;
    }

Goodies

Goodies

Link auf Source 

Develope Info

Windows 11 Pro, 64Bit HP Elitdesk

  • Visual Studio Code 1.9.5
  • Platform.IO Core 6
  • C++
  • ArduinoBLE Library
  • BLE Device Nordic NRF52840 (Seeed Studio XIAO BLE 52840 )
  • BSS  nach Bluetooth Binary Sensor Service Specification V1.0 , Rev.  2019-07-02
Challenges
  • Da meine Sensorerfassung auch laufen soll, wenn keine Connection vorhanden ist, habe ich einen StateManager gebaut der den Status des Peripheral festhält. Dabei stellte ich fest, wenn der Returnwert vom BLE.Central nicht verwendet wird, man ein komisches Verhalten bekommt, so dass man keine Daten senden kann. Obschon die Eventhandler connected melden kommt kene richtige Connection zustande. Sobald man den Returnwert im Loop verwendet klappt alles.
  • ArduinoBLE Dokumentation erklärt nicht viel über das Advertising. Zusammen mit Windows Bluetooth LE ist es eine Herausforderung es zu verstehen.
  • Wenn die Advertising Meldung zu gross ist, wird nichts gesendet.
  • Um einen neuen Wert zu senden /übernehmen muss nach der Aktualisierung der globalen Variable noch BLE.Advertising aufgerufen werden.
  • XIAO-NRF52840 kann keine GATT Daten mit 256Byte senden. Bei mir wurden nur 247 Byte übertragen. Daher habe ich die Meldungsgrösse auf 240Byte beschränkt. Somit konnte ich die BSS implementation mit Data splitting testen.
Lession learned
  • Ich dachte wenn ich diesen Standard implementiere ist es einfacher den Peripheral zu programmieren. Die Specification gab mehr zu tun als erwartet.
  • Die Seeed Studio XIAO Devices sind sehr heikel mit den USB im Fehlerfall. jeder muss anders behandelt werden.
Links
Release notes
  • V1.0-0  BLE-BSS build 23.034 20224
Questions
    Hier sind Fragen aufgeführt, für die ich noch eine Antwort suche.
  • Q-001: Warum muss  central = BLE.central(); die Variable central verwendet werden damit das Connect richtig geht.?
  • Q-002: Warum hat der XIAO-NRF52840 Gatt eine max. Writegrösse von 247 Byte?

Hier ended die Seite

Loading...