
In diesem Teil des Tutorials fügen wir unserem Projekt ein Generic Attributes (GATT)-Profil hinzu. Der finale Code dieses Blogbeitrags kann auf Ruuvi GitHub im ruuviblog-Branch, Tag 3.16.0-alpha, heruntergeladen werden.
Folge bitte Teil 1 der Serie für Details zum Klonen des Repositorys und Kompilieren des Codes. Die finale Hex-Datei dieses Tutorials kann von Ruuvi Jenkins heruntergeladen werden.
Generic Attributes für Ruuvi
„Die Generic Attributes (GATT) definieren eine hierarchische Datenstruktur, die verbundenen Bluetooth Low Energy (LE)-Geräten zur Verfügung gestellt wird.“ — Bluetooth.com
Die GATT-Profile wurden von Adafruit gut erklärt. Schau dir bitte deren Tutorial für eine detailliertere Erklärung an, was GATT ist.
Auf dem RuuviTag implementieren wir ein einfaches Profil mit 3 Services:
- Device Firmware Update Service (DFU)
- Device Information Service (DIS)
- Nordic UART Service (NUS)
Der Device Firmware Update Service ermöglicht es dem Tag, den Bootloader ohne physische Benutzerinteraktion zu starten.
Der Device Information Service stellt grundlegende Informationen über das Gerät bereit, wie Hersteller, Hardwareversion und Firmwareversion.
Der Nordic UART Service wird verwendet, um eine bidirektionale Kommunikation zwischen RuuviTag und einem anderen Gerät zu ermöglichen.
GATT-Schnittstelle
Unsere Schnittstelle ist einfach genug: Der DFU-Service muss nur ohne Parameter initialisiert werden, der DIS-Service benötigt Strings für die anzuzeigenden Daten und NUS verwendet die zuvor definierte Kommunikationskanal-Schnittstelle. Zusätzlich haben wir eine gemeinsame Initialisierungsfunktion, die die GATT-Parameter einrichtet.
Unter der Haube steckt eine Menge Komplexität, etwa Verbindungsintervalle und deren Aushandlung, das Supervisory Timeout der Verbindung und so weiter. Aber wie zuvor wollen wir alles so einfach wie irgend möglich halten und diese Einstellungen als „vernünftige Standardwerte“ belassen.
#define HCI_ERROR_CODE_CONN_TERM_BY_LOCAL_HOST 0x16
typedef struct
{
char fw_version[32];
char model[32];
char hw_version[32];
char manufacturer[32];
char deviceid[32];
}ruuvi_interface_communication_ble4_gatt_dis_init_t;
/**
* Initializes GATT stack. Uses default values from sdk_config.h,
* these can be overridden in nrf5_sdk15_application_config.h
*
* Returns RUUVI_DRIVER_ERROR_INVALID_STATE
* if radio module is not initialized
* with handle RUUVI_INTERFACE_COMMUNICATION_RADIO_GATT
*
*
*/
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_init(void);
/**
* Initialize Nordic UART Service as a communication channel.
* ruuvi_interface_communication_radio_init(RUUVI_INTERFACE_COMMUNICATION_RADIO_GATT)
* must be called before initializing service
*
* Parameter channel: Pointer to communication interface which will be populated.
* Pointer will be copied, the structure
* must be retained. Adding any event handler to structure after
* initialization will take effect immediately
* Returns RUUVI_DRIVER_SUCCESS on success
* Returns RUUVI_DRIVER_ERROR_INVALID_STATE
* if radio module is not initialized with handle
* RUUVI_INTERFACE_COMMUNICATION_RADIO_GATT
* or if ruuvi_interface_communication_ble4_gatt_init
* has not been called.
* Returns error code from stack in case there is other error.
*/
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_init
(ruuvi_interface_communication_t* const channel);
/**
* Initialize BLE4 Device firmware update service.
*
* Returns RUUVI_DRIVER_SUCCESS on success
* Returns error code from stack in case there is error.
*/
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_dfu_init(void);
/**
* Initialize BLE4 Device Information service
*
* parameter dis: pointer to data which should be presented over DIS.
* Memory will be deep-copied
* Returns RUUVI_DRIVER_SUCCESS on success
* Returns error code from stack in case there is error.
*/
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_dis_init
(const ruuvi_interface_communication_ble4_gatt_dis_init_t* const dis);
/**
* Start or stop advertising GATT connection.
*
* parameter connectable: True to start advertising connectablity
* False to stop advertising connectablity
* parameter name: Name of the device to be advertised.
* parameter advertise_nus: True to enable advertising UUID of NUS in the scan response.
* Returns RUUVI_DRIVER_SUCCESS on success
* Returns error code from stack in case there is error.
*
*/
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_advertise_connectablity
(const bool connectable,
const char* const name,
const bool advertise_nus);
#endifGATT-Implementierung
Glücklicherweise hat Nordic Semiconductor die Hauptarbeit geleistet. Die Initialisierung von GATT selbst erfordert etwas Überlegung. Vorerst hardcodieren wir Standardwerte für Dinge wie das Verbindungsintervall in ruuvi_platform_communication_ble4_gatt.c.
Das Hinzufügen der DFU– und DIS-Services ist eine triviale Angelegenheit, bei der einfach die Funktionen des Software Development Kit (SDK) aufgerufen werden. NUS ist etwas kniffliger, da wir NUS mit unserer Anwendung verbinden müssen. Wir verwenden die zuvor definierte Kommunikationsschnittstelle, die Folgendes erfordert:
- Init – Zeile 1
- Uninit – Zeile 25
- Send – Zeile 43
- Read – Zeile 64
- Event Handler – Zeile 70
Zuvor wurde die Designentscheidung getroffen, nur einen Modus der Funkkommunikation gleichzeitig zuzulassen. Daher fügen wir eine Funktion hinzu, um die Verbindung zu nahegelegenen Geräten zu bewerben, anstatt die zuvor erstellte Advertising-Schnittstelle zu verwenden – Zeile 113.
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_init
(ruuvi_interface_communication_t* const _channel)
{
if(NULL == _channel) { return RUUVI_DRIVER_ERROR_NULL; }
uint32_t err_code;
ble_nus_init_t nus_init;
// Initialize NUS.
memset(&nus_init, 0, sizeof(nus_init));
nus_init.data_handler = nus_data_handler;
err_code = ble_nus_init(&m_nus, &nus_init);
channel = _channel;
channel->init = ruuvi_interface_communication_ble4_gatt_nus_init;
channel->uninit = ruuvi_interface_communication_ble4_gatt_nus_uninit;
channel->send = ruuvi_interface_communication_ble4_gatt_nus_send;
channel->read = ruuvi_interface_communication_ble4_gatt_nus_read;
m_gatt_is_init = true;
return ruuvi_platform_to_ruuvi_error(&err_code);
}
/**
*
*/
static ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_uninit
(ruuvi_interface_communication_t* const _channel)
{
if(NULL == _channel) { return RUUVI_DRIVER_ERROR_NULL; }
memset(_channel, 0, sizeof(ruuvi_interface_communication_t));
m_gatt_is_init = false;
// disconnect
if(BLE_CONN_HANDLE_INVALID != m_conn_handle)
{
sd_ble_gap_disconnect(m_conn_handle, HCI_ERROR_CODE_CONN_TERM_BY_LOCAL_HOST);
}
// Services cannot be uninitialized
return RUUVI_DRIVER_SUCCESS;
}
/**
*
*/
static ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_send
(ruuvi_interface_communication_message_t* const message)
{
if(NULL == message) { return RUUVI_DRIVER_ERROR_NULL; }
if(BLE_NUS_MAX_DATA_LEN < message->data_length) { return RUUVI_DRIVER_ERROR_DATA_SIZE; }
if(BLE_CONN_HANDLE_INVALID == m_conn_handle) { return RUUVI_DRIVER_ERROR_INVALID_STATE; }
if(message->repeat)
{
return RUUVI_DRIVER_ERROR_NOT_IMPLEMENTED;
}
ret_code_t err_code = NRF_SUCCESS;
uint16_t data_len = message->data_length;
err_code |= ble_nus_data_send(&m_nus, message->data, &data_len, m_conn_handle);
return ruuvi_platform_to_ruuvi_error(&err_code);
}
/**
*
*/
static ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_nus_read
(ruuvi_interface_communication_message_t* const message)
{
return RUUVI_DRIVER_ERROR_NOT_SUPPORTED;
}
static void nus_data_handler(ble_nus_evt_t * p_evt)
{
if(NULL == channel ||
NULL == channel->on_evt)
{
return;
}
switch(p_evt->type)
{
case BLE_NUS_EVT_RX_DATA:
channel->on_evt(RUUVI_INTERFACE_COMMUNICATION_RECEIVED,
(void*)p_evt->params.rx_data.p_data,
p_evt->params.rx_data.length);
break;
case BLE_NUS_EVT_COMM_STARTED:
channel->on_evt(RUUVI_INTERFACE_COMMUNICATION_CONNECTED, NULL, 0);
break;
case BLE_NUS_EVT_COMM_STOPPED:
channel->on_evt(RUUVI_INTERFACE_COMMUNICATION_DISCONNECTED, NULL, 0);
break;
case BLE_NUS_EVT_TX_RDY:
channel->on_evt(RUUVI_INTERFACE_COMMUNICATION_SENT, NULL, 0);
break;
default:
break;
}
}
/**
* Start or stop advertising GATT connection.
*
* parameter connectable: True to start advertising connectablity
* False to stop advertising connectablity
* parameter name: Name of the device to be advertised.
* parameter advertise_nus: True to enable advertising
* UUID of NUS in the scan response.
*
*/
ruuvi_driver_status_t ruuvi_interface_communication_ble4_gatt_advertise_connectablity
(const bool connectable,
const char* const name,
const bool advertise_nus)
{
ret_code_t err_code = NRF_SUCCESS;
if(!connectable)
{
err_code |= sd_ble_gap_adv_stop(m_adv_handle);
return ruuvi_platform_to_ruuvi_error(&err_code);
}
// Initialize advertising parameters (used when starting advertising).
memset(&m_adv_params, 0, sizeof(m_adv_params));
m_adv_params.filter_policy = BLE_GAP_ADV_FP_ANY;
m_adv_params.duration = 0; // Never time out.
m_adv_params.properties.type = BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED;
m_adv_params.p_peer_addr = NULL; // Undirected advertisement.
m_adv_params.interval =
MSEC_TO_UNITS(APPLICATION_CONNECTION_ADVERTISEMENT_INTERVAL, UNIT_0_625_MS);
// OPEN security for name
ble_gap_conn_sec_mode_t security;
uint8_t len = strlen(name);
security.sm = 1;
security.lv = 1;
err_code |= sd_ble_gap_device_name_set(&security, (uint8_t*)name, len);
memset(&m_adv_data, 0, sizeof(m_adv_data));
memset(&m_advertisement, 0, sizeof(m_advertisement));
memset(&m_scanresp, 0, sizeof(m_scanresp));
ble_advdata_t advdata = {0};
// Only valid flag
advdata.flags = BLE_GAP_ADV_FLAG_BR_EDR_NOT_SUPPORTED;
// Name will be read from the above GAP data
advdata.name_type = BLE_ADVDATA_FULL_NAME;
// Encode data
m_adv_data.adv_data.len = sizeof(m_advertisement);
err_code |= ble_advdata_encode(&advdata,
m_advertisement,
&m_adv_data.adv_data.len);
m_adv_data.adv_data.p_data = m_advertisement;
// Add scan response if requested
if(advertise_nus)
{
ble_advdata_t scanrsp = {0};
scanrsp.uuids_complete.uuid_cnt = 1;
scanrsp.uuids_complete.p_uuids = &(m_adv_uuids[0]);
// Encode data
m_adv_data.scan_rsp_data.len = sizeof(m_scanresp);
err_code |= ble_advdata_encode(&scanrsp,
m_scanresp,
&m_adv_data.scan_rsp_data.len);
m_adv_data.scan_rsp_data.p_data = m_scanresp;
}
// Configure advertisement and start
err_code |= sd_ble_gap_adv_set_configure(&m_adv_handle,
&m_adv_data,
&m_adv_params);
err_code |= sd_ble_gap_adv_start(m_adv_handle, NRF5_SDK15_BLE4_STACK_CONN_TAG);
return ruuvi_platform_to_ruuvi_error(&err_code);
}Es sollte beachtet werden, dass die read-Funktion vorerst nur einen Fehler zurückgibt. Das liegt daran, dass wir das externe Gerät nicht nach Daten abfragen können, sondern nur empfangene Datenereignisse verarbeiten können. Die Kanalspezifikation würde die Daten puffern und die Anwendung könnte sie zu einem späteren Zeitpunkt verarbeiten, aber das implementieren wir später. Wir haben auch noch keine wiederholten Sendevorgänge implementiert.
Integration von GATT in die Anwendung
Zuvor haben wir angedeutet, GATT zu verwenden, um Daten vom Beschleunigungssensor mit höherer Rate zu streamen. Lass uns das Beschleunigungssensor-FIFO-Streaming implementieren. Wir kommentieren die FIFO- und Aktivitätsinterrupt-Initialisierung beim Beschleunigungssensor-Boot aus und fügen Beschleunigungssensor-Tasks hinzu, um den FIFO-Puffer und Interrupt zu aktivieren und zu deaktivieren.
Der Beschleunigungssensor-FIFO-Task wird modifiziert, um die Daten im ASCII-Format mit explizitem Vorzeichen, einer Ziffer und zwei Nachkommastellen sowie Semikolon zu senden, z. B.
+0.08;+0.04;+1.04Dies passt in die 20 Bytes einer einzelnen GATT-Übertragung.
static void task_acceleration_fifo_full_task(void *p_event_data, uint16_t event_size)
{
ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
ruuvi_interface_acceleration_data_t data[32];
size_t data_len = sizeof(data);
err_code |= ruuvi_interface_lis2dh12_fifo_read(&data_len, data);
char msg[APPLICATION_LOG_BUFFER_SIZE] = { 0 };
for(int ii = 0; ii < data_len; ii++)
{
memset(msg, 0, sizeof(msg));
snprintf(msg, 21,"%+4.2f;%+4.2f;%+4.2f", data[ii].x_g, data[ii].y_g, data[ii].z_g );
ruuvi_interface_communication_message_t gatt_msg = { 0 };
memcpy(gatt_msg.data, msg, 20);
gatt_msg.data_length = 20;
// Loop here until data is sent
while(RUUVI_DRIVER_SUCCESS != task_gatt_send(&gatt_msg));
}
RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);
}
ruuvi_driver_status_t task_acceleration_fifo_use(const bool enable)
{
ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
if(true == enable)
{
err_code |= ruuvi_interface_lis2dh12_fifo_use(true);
err_code |= ruuvi_interface_lis2dh12_fifo_interrupt_use(true);
}
if(false == enable)
{
err_code |= ruuvi_interface_lis2dh12_fifo_use(false);
err_code |= ruuvi_interface_lis2dh12_fifo_interrupt_use(false);
}
}Wir initialisieren den Advertising-Task nicht in unserer Hauptinitialisierung, sondern initialisieren stattdessen den GATT-Task und starten das Bewerben der Verbindung im GATT-Modul. Die Initialisierung erfordert Daten für den DIS sowie einen Event-Handler für die NUS-Events.
Ein „Stolperstein“, auf den du achten solltest, ist das Trennen der Verbindung. Das Advertising läuft standardmäßig nicht weiter; wir müssen das Advertising für die Verbindung in Zeile 80 manuell neu starten, wenn wir uns erneut mit dem Tag verbinden möchten.
Wenn wir schon dabei sind, lassen wir den Watchdog etwas Nützliches tun: Wir füttern den Watchdog beim Ereignis „data sent“ in Zeile 85. Wenn wir die Verbindung zum Tag verlieren oder aus irgendeinem Grund keine Daten mehr gesendet werden können, startet das Tag nach 120 Sekunden neu.
static ruuvi_interface_communication_t channel;
ruuvi_driver_status_t task_gatt_init(void)
{
ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
ruuvi_interface_communication_ble4_gatt_dis_init_t dis = {0};
uint64_t id;
err_code |= ruuvi_interface_communication_id_get(&id);
size_t index = 0;
for (size_t ii = 0; ii < 8; ii ++)
{
index += snprintf(dis.deviceid + index,
sizeof(dis.deviceid)-index,
"%02X",
(uint8_t)(id >> ((7 - ii) * 8)) & 0xFF);
if(ii < 7)
{
index += snprintf(dis.deviceid + index,
sizeof(dis.deviceid)-index,
":");
}
}
memcpy(dis.fw_version,
APPLICATION_FW_VERSION,
sizeof(APPLICATION_FW_VERSION));
memcpy(dis.model,
RUUVI_BOARD_MODEL_STRING,
sizeof(RUUVI_BOARD_MODEL_STRING));
memcpy(dis.manufacturer,
RUUVI_BOARD_MANUFACTURER_STRING,
sizeof(RUUVI_BOARD_MANUFACTURER_STRING));
err_code |= ruuvi_interface_communication_radio_init
(RUUVI_INTERFACE_COMMUNICATION_RADIO_GATT);
// Init fails if something else - such as advertising - as reserved radio.
if(RUUVI_DRIVER_SUCCESS != err_code) { return err_code; }
err_code |= ruuvi_interface_communication_ble4_gatt_init();
RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);
err_code |= ruuvi_interface_communication_ble4_gatt_nus_init(&channel);
channel.on_evt = task_gatt_on_gatt;
RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);
err_code |= ruuvi_interface_communication_ble4_gatt_dfu_init();
RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);
err_code |= ruuvi_interface_communication_ble4_gatt_dis_init(&dis);
RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);
err_code |=
ruuvi_interface_communication_ble4_gatt_advertise_connectablity(true, "Ruuvi", true);
RUUVI_DRIVER_ERROR_CHECK(err_code, RUUVI_DRIVER_SUCCESS);
return err_code;
}
ruuvi_driver_status_t
task_gatt_on_gatt(ruuvi_interface_communication_evt_t evt,
void* p_data,
size_t data_len)
{
ruuvi_driver_status_t err_code = RUUVI_DRIVER_SUCCESS;
char str[21] = { 0 };
switch(evt)
{
case RUUVI_INTERFACE_COMMUNICATION_CONNECTED:
ruuvi_platform_log(RUUVI_INTERFACE_LOG_INFO, "Connected \r\n");
err_code |= task_acceleration_fifo_use(true);
ruuvi_interface_communication_message_t msg = {0};
memcpy(msg.data, "Hello! Here's data!", 20);
msg.data_length = 20;
channel.send(&msg);
break;
case RUUVI_INTERFACE_COMMUNICATION_DISCONNECTED:
ruuvi_platform_log(RUUVI_INTERFACE_LOG_INFO, "Disonnected \r\n");
err_code |= task_acceleration_fifo_use(false);
ruuvi_interface_communication_ble4_gatt_advertise_connectablity
(true, "Ruuvi", true);
break;
case RUUVI_INTERFACE_COMMUNICATION_SENT:
ruuvi_interface_watchdog_feed();
break;
case RUUVI_INTERFACE_COMMUNICATION_RECEIVED:
snprintf(str, data_len, "%s", p_data);
ruuvi_platform_log(RUUVI_INTERFACE_LOG_INFO, str);
break;
default:
break;
}
return err_code;
}
ruuvi_driver_status_t
task_gatt_send(ruuvi_interface_communication_message_t* const msg)
{
if(NULL == msg) { return RUUVI_DRIVER_ERROR_NULL; }
if(NULL == channel.send) { return RUUVI_DRIVER_ERROR_INVALID_STATE; }
return channel.send(msg);
}Probieren wir es aus. Das Tag wird als „Ruuvi“ beworben; wir können uns über nRF Connect damit verbinden.

Der DFU-Service schafft es nicht, das neue Paket automatisch hochzuladen, aber es ist möglich, in den Bootloader zu wechseln, indem man sich für Benachrichtigungen des Secure DFU Service registriert und den Befehl „enter bootloader“ sendet.

Und schließlich können wir den Beschleunigungsdatenstrom mit nRF UART prüfen. Kurz vor dem Trennen der Verbindung senden wir „Thanks for data!“ an den RuuviTag, um die bidirektionale Kommunikation zu verifizieren.


In unserem RuuviTag-Log bemerken wir, dass die DFU-Service-Initialisierung uns vor dem fehlenden Bootloader auf dem Entwicklungs-Tag warnt. Die Verbindung, das FIFO-Streaming und der Datenempfang funktionieren einwandfrei, und eine Weile nach der Trennung greift unser Watchdog ein und setzt den Tag zurück. Soweit, so gut.
Leistungsprofil
Schauen wir uns an, wie unser Leistungsprofil aussieht. Zuerst prüfen wir das Advertising.

Da wir die zusätzlichen FIFO-Lesevorgänge losgeworden sind, sind wir wieder bei 24,4 μA statt der vorherigen 29,7 μA. Als wir den Beschleunigungssensor mit 1 Hz betrieben haben, lag der Verbrauch bei 23 μA, der Mehrverbrauch kommt wahrscheinlich von der aktuellen Abtastrate von 10 Hz. Wie sieht es während der Verbindung aus?

Ein Problem wird sofort aus dem Leistungsprofil ersichtlich: Unser Tag kann Daten nicht schnell genug senden und das Programm läuft in Zeile 16 des task_acceleration.c-Gists oben in einer Schleife und wartet darauf, dass der Puffer geleert wird. Wir müssen uns in Zukunft Möglichkeiten ansehen, den GATT-Durchsatz zu steigern und den Tag zwischen den Verbindungsintervallen schlafen zu lassen.
Fazit
Wir haben jetzt das GATT-Profil implementiert und können Beschleunigungssensordaten darüber streamen. Es bleibt jedoch noch mehr Arbeit, um den GATT-Durchsatz und Verbrauch zu optimieren.
Im nächsten Teil der Serie implementieren wir NFC-Schreibvorgänge auf den Tag. Bald werden wir alle Features, die wir verwenden werden, zumindest auf Proof-of-Concept-Ebene implementiert haben, dann können wir einen Feature-Freeze durchführen und unsere Firmware als Beta-Version betrachten. Dann können wir anfangen, Bugs zu beheben, Dokumentation und Tests zu verbessern und Dinge wie den GATT-Durchsatz zu optimieren.
Bleib dran und folge @ojousima und @ruuvicom auf Twitter für #FirmwareFriday-Posts!