From 844d13fec00497b80cbfe3c6745d312049eb7126 Mon Sep 17 00:00:00 2001 From: Arturo Buzarra Date: Fri, 5 Jul 2019 11:03:22 +0200 Subject: [PATCH] ble-gatt-server-example: add sample application to create a BLE GATT server https://jira.digi.com/browse/DEL-6649 Signed-off-by: Arturo Buzarra --- ble-gatt-server-example/Makefile | 39 + ble-gatt-server-example/README.md | 75 ++ ble-gatt-server-example/ble-gatt-server-app.c | 885 ++++++++++++++++++ ble-gatt-server-example/ble-gatt-server-app.h | 82 ++ ble-gatt-server-example/subprocess.c | 173 ++++ ble-gatt-server-example/subprocess.h | 37 + 6 files changed, 1291 insertions(+) create mode 100644 ble-gatt-server-example/Makefile create mode 100644 ble-gatt-server-example/README.md create mode 100644 ble-gatt-server-example/ble-gatt-server-app.c create mode 100644 ble-gatt-server-example/ble-gatt-server-app.h create mode 100644 ble-gatt-server-example/subprocess.c create mode 100644 ble-gatt-server-example/subprocess.h diff --git a/ble-gatt-server-example/Makefile b/ble-gatt-server-example/Makefile new file mode 100644 index 0000000..1281937 --- /dev/null +++ b/ble-gatt-server-example/Makefile @@ -0,0 +1,39 @@ +# +# Copyright (c) 2019 Digi International Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# +# + +PROGRAM := ble-gatt-server-example + +CFLAGS += -Wall + +LDLIBS += -lshared-mainloop -lbluetooth-internal + +all: $(PROGRAM) + +SRCS := $(wildcard *.c) +OBJS := $(SRCS:.c=.o) + +$(PROGRAM): $(OBJS) + $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@ + +.PHONY: install +install: $(PROGRAM) + install -d $(DESTDIR)/usr/bin + install -m 0755 $(PROGRAM) $(DESTDIR)/usr/bin/ + +.PHONY: clean +clean: + -rm -f *.o $(PROGRAM) diff --git a/ble-gatt-server-example/README.md b/ble-gatt-server-example/README.md new file mode 100644 index 0000000..f9b1e6e --- /dev/null +++ b/ble-gatt-server-example/README.md @@ -0,0 +1,75 @@ +BLE GATT Server Example Application +=================================== + +Example application to create a BLE GATT server. + +This application enables a BLE GATT server on the board. After that, the +application starts listening for BLE connections to provide custom BLE +GATT services and characteristics. + + +Running the application +----------------------- +Once the binary is in the target, you must configure the bluetooth +interface with the following commands: + +``` +~# btmgmt -i hci0 le on +hci0 Set Low Energy complete, settings: powered connectable bondable ssp br/edr le advertising secure-conn +~# btmgmt -i hci0 connectable on +hci0 Set Connectable complete, settings: powered connectable bondable ssp br/edr le advertising secure-conn +~# btmgmt -i hci0 pairable on +hci0 Set Bondable complete, settings: powered connectable bondable ssp br/edr le advertising secure-conn +~# btmgmt -i hci0 advertising on +hci0 Set Advertising complete, settings: powered connectable bondable ssp br/edr le advertising secure-conn +~# +``` + +After you initialize the bluetooth interface, you can start the sample application: + +``` +~# ./ble-gatt-server-example -h +Usage: + ble-gatt-server-app [options] +Options: + -i, --index Specify adapter index, e.g. hci0 + -t, --threshold The temperature threshold to send notification + -m, --mtu The ATT MTU to use + -v, --verbose Enable extra logging + -h, --help Display help +~# +~# ./ble-gatt-server-example --index hci0 --threshold 50 +Running GATT server +Started listening on ATT channel. Waiting for connections +Connect from XX:XX:XX:XX:XX:XX + +``` + +Compiling the application +------------------------- +This demo can be compiled using a Digi Embedded Yocto based toolchain. Make +sure to source the corresponding toolchain of the platform you are using, +for example, for ConnectCore 8X: + +``` +~$ . /environment-setup-aarch64-dey-linux +~$ make +``` + +For more information, see the [Digi Embedded Yocto online documentation](https://github.com/digi-embedded/meta-digi). + +License +------- +Copyright 2019, Digi International Inc. + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appears in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/ble-gatt-server-example/ble-gatt-server-app.c b/ble-gatt-server-example/ble-gatt-server-app.c new file mode 100644 index 0000000..d13b257 --- /dev/null +++ b/ble-gatt-server-example/ble-gatt-server-app.c @@ -0,0 +1,885 @@ +/* + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2014 Google Inc. + * Copyright (C) 2019 Digi International Inc. + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +#define _GNU_SOURCE /* for asprintf */ + +#include +#include +#include +#include +#include +#include +#include + +#include "bluetooth/bluetooth.h" +#include "bluetooth/hci.h" +#include "bluetooth/hci_lib.h" +#include "bluetooth/l2cap.h" + +#include "bluetooth-internal/uuid.h" +#include "bluetooth-internal/mainloop.h" +#include "bluetooth-internal/util.h" +#include "bluetooth-internal/att.h" +#include "bluetooth-internal/queue.h" +#include "bluetooth-internal/timeout.h" +#include "bluetooth-internal/gatt-db.h" +#include "bluetooth-internal/gatt-server.h" + +#include "subprocess.h" +#include "ble-gatt-server-app.h" + + +#define ATT_CID 4 +#define NOTIFICATION_TIMEOUT_MS 1000 +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) + +static bool verbose = false; +static bool keep_running = true; +static int temp_threshold = 50; + +struct server { + int fd; + struct bt_att *att; + struct gatt_db *db; + struct bt_gatt_server *gatt; + bool svc_chngd_enabled; + uint16_t cell_handle; + int *timeout_handlers; + int n_characteristics; +}; + +struct notify_cb_t { + uint16_t handle; + struct server *server; +}; + +void destroy_notify_data(void *user_data) +{ + free(user_data); +} + +static char *get_cmd_output(char *cmd) +{ + FILE *in; + char buff[512], *pbuf = NULL; + int ret_value; + + in = popen(cmd, "r"); + if (!in) { + syslog(LOG_WARNING, "execution of cmd '%s' failed\n", cmd); + return NULL; + } + + pbuf = fgets(buff, ARRAY_SIZE(buff), in); + if (pbuf) + buff[strlen(buff) - 1] = '\0'; + + ret_value = pclose(in); + + syslog(LOG_DEBUG, "cmd '%s' returned: '%s' (return value: %d)\n", cmd, buff, ret_value); + if (pbuf) + return strdup(buff); + + return pbuf; +} + +static void debug_cb(const char *str, void *user_data) +{ + const char *prefix = user_data; + + syslog(LOG_DEBUG ,"%s%s\n", prefix, str); +} + +static void att_disconnect_cb(int err, void *user_data) +{ + printf("Device disconnected: %s\n", strerror(err)); + + mainloop_quit(); +} + +static void confirm_write(struct gatt_db_attribute *attr, int err, + void *user_data) +{ + if (!err) + return; + + syslog(LOG_DEBUG, "Error caching attribute %p - err: %d\n", attr, err); +} + + +/* Generic Access Service (GAP) */ + +static void gap_device_name_read_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + uint8_t error = 0; + size_t len = 0; + uint8_t *value = NULL; + char *name = NULL; + + name = get_cmd_output("cat /etc/hostname"); + if (!name) { + error = BT_ATT_ERROR_INVALID_HANDLE; + goto done; + } + + len = strlen(name) + 1; + if (offset > len) { + error = BT_ATT_ERROR_INVALID_HANDLE; + goto done; + } + + len -= offset; + value = len ? (uint8_t *)&name[offset] : NULL; + +done: + syslog(LOG_DEBUG, "%s called, return: %s (error: 0x%x)\n", __func__, value, error); + gatt_db_attribute_read_result(attrib, id, error, value, len); + free(name); +} + +static void populate_gap_service(struct server *server) +{ + bt_uuid_t uuid; + struct gatt_db_attribute *service, *tmp; + uint16_t appearance; + + /* Add the GAP service */ + bt_uuid16_create(&uuid, UUID_GAP_SERVICE); + service = gatt_db_add_service(server->db, &uuid, true, 6); + + /* + * Device Name characteristic. Make the value dynamically read and + * written via callbacks. + */ + bt_uuid16_create(&uuid, GATT_CHARAC_DEVICE_NAME); + gatt_db_service_add_characteristic(service, &uuid, + BT_ATT_PERM_READ, + BT_GATT_CHRC_PROP_READ | + BT_GATT_CHRC_PROP_EXT_PROP, + gap_device_name_read_cb, + NULL, + server); + + bt_uuid16_create(&uuid, GATT_CHARAC_EXT_PROPER_UUID); + + /* + * Device Appearance characteristic. Reads and writes should obtain the + * value from the database. + */ + bt_uuid16_create(&uuid, GATT_CHARAC_APPEARANCE); + tmp = gatt_db_service_add_characteristic(service, &uuid, + BT_ATT_PERM_READ, + BT_GATT_CHRC_PROP_READ, + NULL, NULL, server); + + /* + * Write the appearance value to the database, since we're not using a + * callback. + */ + put_le16(128, &appearance); + gatt_db_attribute_write(tmp, 0, (void *) &appearance, + sizeof(appearance), + BT_ATT_OP_WRITE_REQ, + NULL, confirm_write, + NULL); + + gatt_db_service_set_active(service, true); +} + + +/* Generic Attribute Profile Service (GATT) */ + +static void gatt_service_changed_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + uint8_t error = 0; + + syslog(LOG_DEBUG, "Service Changed Read called\n"); + + gatt_db_attribute_read_result(attrib, id, error, NULL, 0); +} + +static void gatt_svc_chngd_ccc_read_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + struct server *server = user_data; + uint8_t error = 0; + uint8_t value[2]; + + syslog(LOG_DEBUG, "Service Changed CCC Read called\n"); + + value[0] = server->svc_chngd_enabled ? 0x02 : 0x00; + value[1] = 0x00; + + gatt_db_attribute_read_result(attrib, id, error, value, sizeof(value)); +} + +static void gatt_svc_chngd_ccc_write_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + const uint8_t *value, size_t len, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + struct server *server = user_data; + uint8_t error = 0; + + syslog(LOG_DEBUG, "Service Changed CCC Write called\n"); + + if (!value || len != 2) { + error = BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN; + goto done; + } + + if (offset) { + error = BT_ATT_ERROR_INVALID_OFFSET; + goto done; + } + + if (value[0] == 0x00) + server->svc_chngd_enabled = false; + else if (value[0] == 0x02) + server->svc_chngd_enabled = true; + else + error = BT_ATT_ERROR_INVALID_HANDLE; + + syslog(LOG_DEBUG, "Service Changed Enabled: %s\n", + server->svc_chngd_enabled ? "true" : "false"); + +done: + gatt_db_attribute_write_result(attrib, id, error); +} + +static void populate_gatt_service(struct server *server) +{ + bt_uuid_t uuid; + struct gatt_db_attribute *service, *svc_chngd; + + /* Add the GATT service */ + bt_uuid16_create(&uuid, UUID_GATT_SERVICE); + service = gatt_db_add_service(server->db, &uuid, true, 4); + + bt_uuid16_create(&uuid, GATT_CHARAC_SERVICE_CHANGED); + svc_chngd = gatt_db_service_add_characteristic(service, &uuid, + BT_ATT_PERM_READ, + BT_GATT_CHRC_PROP_READ | BT_GATT_CHRC_PROP_INDICATE, + gatt_service_changed_cb, + NULL, server); + gatt_db_attribute_get_handle(svc_chngd); + + bt_uuid16_create(&uuid, GATT_CLIENT_CHARAC_CFG_UUID); + gatt_db_service_add_descriptor(service, &uuid, + BT_ATT_PERM_READ | BT_ATT_PERM_WRITE, + gatt_svc_chngd_ccc_read_cb, + gatt_svc_chngd_ccc_write_cb, server); + + gatt_db_service_set_active(service, true); +} + + +/* Device Information Service */ + +static void temperature_read_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + uint8_t error = 0; + size_t len = 0; + uint8_t *value = NULL; + int degrees; + char *temp = NULL; + + temp = get_cmd_output("cat /sys/devices/virtual/thermal/thermal_zone0/temp"); + if (!temp) { + error = BT_ATT_ERROR_INVALID_HANDLE; + goto done; + } + + degrees = atoi(temp)/1000; + value = (uint8_t *) °rees; + len = 1; + +done: + syslog(LOG_DEBUG, "%s called, return: %s (error: 0x%x)\n", __func__, value, error); + gatt_db_attribute_read_result(attrib, id, error, value, len); + free(temp); +} + +static bool temperature_notify_cb(void *user_data) +{ + struct notify_cb_t *notify_data = user_data; + struct server *server = notify_data->server; + char *temp = NULL; + uint8_t notify_value; + int current_temp = 0; + + temp = get_cmd_output("cat /sys/devices/virtual/thermal/thermal_zone0/temp"); + if (!temp) { + return false; + } + + current_temp = atoi(temp)/1000; + notify_value = (current_temp >= temp_threshold) ? 0x01 : 0x00; + free(temp); + + /* If the Temperatue is higher than a threshold send notification */ + if (notify_value) { + printf("Send Temperature notification: %d degrees\n", current_temp); + bt_gatt_server_send_notification(server->gatt, + notify_data->handle, + ¬ify_value, + sizeof(notify_value)); + } + + return true; +} + +static void current_time_read_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + uint8_t error; + uint8_t *value = NULL; + size_t len = 0; + char *timestamp = NULL; + + timestamp = get_cmd_output("date +%s"); + if (!timestamp) { + error = BT_ATT_ERROR_INVALID_HANDLE; + goto done; + } + + len = strlen(timestamp) + 1; + if (offset > len) { + error = BT_ATT_ERROR_INVALID_OFFSET; + goto done; + } + + len -= offset; + value = len ? (uint8_t *) ×tamp[offset] : NULL; + error = 0; + +done: + syslog(LOG_DEBUG, "%s called, return: %s (error: 0x%x)\n", __func__, value, error); + gatt_db_attribute_read_result(attrib, id, error, value, len); + free(timestamp); +} + +static void current_time_write_cb(struct gatt_db_attribute *attrib, + unsigned int id, uint16_t offset, + const uint8_t *value, size_t len, + uint8_t opcode, struct bt_att *att, + void *user_data) +{ + uint8_t error = 0; + int status = 0; + char *new_timestamp = NULL; + char *cmd = NULL; + + /* Do not allow to partially write the Time (offset must be 0) */ + if (offset) { + error = BT_ATT_ERROR_INVALID_OFFSET; + goto done; + } + + /* + * We need to terminate the string, unfortunately 'value' is read only, + * so copy it all over. + */ + new_timestamp = malloc(len + 1); + if (!new_timestamp) { + error = BT_ATT_ERROR_INVALID_HANDLE; + goto done; + } + memcpy(new_timestamp, value, len); + new_timestamp[len] = '\0'; + + if (asprintf(&cmd, "echo %s", new_timestamp) < 0 || + safe_execute(cmd, &status) < 0 || status) { + syslog(LOG_ERR, "Failed to set Time, error(%d)\n", status); + error = BT_ATT_ERROR_INVALID_HANDLE; + } + +done: + syslog(LOG_DEBUG, "%s called, received: %s (error: 0x%x)\n", __func__, new_timestamp, error); + gatt_db_attribute_write_result(attrib, id, error); + free(cmd); + free(new_timestamp); +} + + +struct { + uint16_t uuid; + const char *description; + const uint8_t *format; + gatt_db_read_t read_cb; + gatt_db_write_t write_cb; + timeout_func_t notify_cb; +} const bt_characteristics[] = { + { + UUID_GATT_CHARACTERISTIC_CURRENT_TIME, + "Time in String format", + FORMAT_UTF_8, + current_time_read_cb, + current_time_write_cb, + NULL + }, + { + UUID_GATT_CHARACTERISTIC_TEMPERATURE_MEASUREMENT, + "Temperature in Integer format", + FORMAT_8_BIT_INT, + temperature_read_cb, + NULL, + temperature_notify_cb + }, +}; + +/* + * For each characteristic we create 4 handles: + * - Characteristic Declaration + * - Characteristic Value Declaration + * - Descriptor Declaration: + * - Characteristic User Description + * - Characteristic Presentation Format + */ +#define HANDLES_PER_CHARACTERISTIC 4 + +static void populate_device_information_service(struct server *server) +{ + bt_uuid_t uuid; + struct gatt_db_attribute *service; + int i,num_handles; + + /* Add Device Information Service */ + server->n_characteristics = ARRAY_SIZE(bt_characteristics); + bt_uuid16_create(&uuid, UUID_DEVICE_INFORMATION_SERVICE); + /* Add one handle for the Service Declaration */ + num_handles = (server->n_characteristics * HANDLES_PER_CHARACTERISTIC) + 1; + service = gatt_db_add_service(server->db, &uuid, true, num_handles); + + server->cell_handle = gatt_db_attribute_get_handle(service); + server->timeout_handlers = calloc(server->n_characteristics, sizeof(server->timeout_handlers[0])); + if (!server->timeout_handlers) { + syslog(LOG_ERR, "Out of memory, calloc failed!"); + exit(2); + } + + /* Add all characteristics */ + for (i = 0; i < server->n_characteristics; i++) { + uint32_t permissions = 0; + uint8_t properties = 0; + struct gatt_db_attribute *desc_att, *gatt_handle; + + bt_uuid16_create(&uuid, bt_characteristics[i].uuid); + + if (bt_characteristics[i].read_cb) + properties |= BT_GATT_CHRC_PROP_READ; + + if (bt_characteristics[i].write_cb) { + properties |= BT_GATT_CHRC_PROP_WRITE; + permissions |= BT_ATT_PERM_WRITE; + } + + if (bt_characteristics[i].notify_cb) + properties |= BT_GATT_CHRC_PROP_NOTIFY; + + permissions |= BT_ATT_PERM_READ; + + /* Characteristic */ + gatt_handle = gatt_db_service_add_characteristic( + service, + &uuid, + permissions, + properties, + bt_characteristics[i].read_cb, + bt_characteristics[i].write_cb, + server); + + /* Characteristic User Description */ + bt_uuid16_create(&uuid, GATT_CHARAC_USER_DESC_UUID); + desc_att = gatt_db_service_add_descriptor(service, + &uuid, + BT_ATT_PERM_READ, + NULL, + NULL, + server); + gatt_db_attribute_write(desc_att, + 0, + (const uint8_t *) bt_characteristics[i].description, + strlen(bt_characteristics[i].description) + 1, + BT_ATT_OP_READ_REQ, + NULL, + confirm_write, + NULL); + + /* Charateristic Presentation Format */ + bt_uuid16_create(&uuid, GATT_CHARAC_FMT_UUID); + desc_att = gatt_db_service_add_descriptor(service, + &uuid, + BT_ATT_PERM_READ, + NULL, + NULL, + server); + + gatt_db_attribute_write(desc_att, + 0, + bt_characteristics[i].format, + FORMAT_BYTES, + BT_ATT_OP_READ_REQ, + NULL, + confirm_write, + NULL); + + /* Notification timed function */ + if (bt_characteristics[i].notify_cb) { + struct notify_cb_t *notify_data = malloc(sizeof(*notify_data)); + + if (!notify_data) { + syslog(LOG_ERR, "Out of memory, calloc failed!"); + exit(2); + } + notify_data->server = server; + notify_data->handle = gatt_db_attribute_get_handle(gatt_handle); + + server->timeout_handlers[i] = timeout_add(NOTIFICATION_TIMEOUT_MS, bt_characteristics[i].notify_cb, notify_data, destroy_notify_data); + + /* Manually call the notify once to cache a first value */ + bt_characteristics[i].notify_cb(notify_data); + } + } + + gatt_db_service_set_active(service, true); +} + + +/* Main program */ +static void populate_db(struct server *server) +{ + populate_gap_service(server); + populate_gatt_service(server); + populate_device_information_service(server); +} + +static struct server *server_create(int fd, uint16_t mtu) +{ + struct server *server; + + server = new0(struct server, 1); + if (!server) { + syslog(LOG_ERR, "Failed to allocate memory for server\n"); + return NULL; + } + + server->att = bt_att_new(fd, false); + if (!server->att) { + syslog(LOG_ERR, "Failed to initialze ATT transport layer\n"); + goto fail; + } + + if (!bt_att_set_close_on_unref(server->att, true)) { + syslog(LOG_ERR, "Failed to set up ATT transport layer\n"); + goto fail; + } + + if (!bt_att_register_disconnect(server->att, att_disconnect_cb, NULL, + NULL)) { + syslog(LOG_ERR, "Failed to set ATT disconnect handler\n"); + goto fail; + } + + server->fd = fd; + server->db = gatt_db_new(); + if (!server->db) { + syslog(LOG_ERR, "Failed to create GATT database\n"); + goto fail; + } + + server->gatt = bt_gatt_server_new(server->db, server->att, mtu, 0); + if (!server->gatt) { + syslog(LOG_ERR, "Failed to create GATT server\n"); + goto fail; + } + + if (verbose) { + bt_att_set_debug(server->att, debug_cb, "att: ", NULL); + bt_gatt_server_set_debug(server->gatt, debug_cb, + "server: ", NULL); + } + + /* bt_gatt_server already holds a reference */ + populate_db(server); + + return server; + +fail: + gatt_db_unref(server->db); + bt_att_unref(server->att); + free(server); + + return NULL; +} + +static void server_destroy(struct server *server) +{ + int i; + + for (i = 0; i < server->n_characteristics; i++) + if (server->timeout_handlers[i]) + timeout_remove(server->timeout_handlers[i]); + + free(server->timeout_handlers); + + bt_gatt_server_unref(server->gatt); + gatt_db_unref(server->db); + bt_att_unref(server->att); + free(server); +} + +static void usage(void) +{ + printf("ble-gatt-server-app\n"); + printf("Usage:\n\tble-gatt-server-app [options]\n"); + + printf("Options:\n" + "\t-i, --index \t\tSpecify adapter index, e.g. hci0\n" + "\t-t, --threshold \t\tThe temperature threshold to send notification\n" + "\t-m, --mtu \t\t\tThe ATT MTU to use\n" + "\t-v, --verbose\t\t\tEnable extra logging\n" + "\t-h, --help\t\t\tDisplay help\n"); +} + +static struct option main_options[] = { + { "index", 1, 0, 'i' }, + { "threshold", 1, 0, 't' }, + { "mtu", 1, 0, 'm' }, + { "verbose", 0, 0, 'v' }, + { "help", 0, 0, 'h' }, + { } +}; + +static int l2cap_le_att_listen_and_accept(bdaddr_t *src, int sec, + uint8_t src_type) +{ + int sk, nsk; + struct sockaddr_l2 srcaddr, addr; + socklen_t optlen; + struct bt_security btsec; + char ba[18]; + + sk = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); + if (sk < 0) { + syslog(LOG_ERR, "Failed to create L2CAP socket"); + return -1; + } + + /* Set up source address */ + memset(&srcaddr, 0, sizeof(srcaddr)); + srcaddr.l2_family = AF_BLUETOOTH; + srcaddr.l2_cid = htobs(ATT_CID); + srcaddr.l2_bdaddr_type = src_type; + bacpy(&srcaddr.l2_bdaddr, src); + + if (bind(sk, (struct sockaddr *) &srcaddr, sizeof(srcaddr)) < 0) { + syslog(LOG_ERR, "Failed to bind L2CAP socket"); + goto fail; + } + + /* Set the security level */ + memset(&btsec, 0, sizeof(btsec)); + btsec.level = sec; + if (setsockopt(sk, SOL_BLUETOOTH, BT_SECURITY, &btsec, + sizeof(btsec)) != 0) { + syslog(LOG_ERR, "Failed to set L2CAP security level\n"); + goto fail; + } + + if (listen(sk, 10) < 0) { + syslog(LOG_ERR, "Listening on socket failed"); + goto fail; + } + printf("Started listening on ATT channel. Waiting for connections\n"); + + memset(&addr, 0, sizeof(addr)); + optlen = sizeof(addr); + + nsk = accept(sk, (struct sockaddr *) &addr, &optlen); + if (nsk < 0) { + syslog(LOG_ERR, "Accept failed"); + goto fail; + } + + ba2str(&addr.l2_bdaddr, ba); + printf("Connect from %s\n", ba); + close(sk); + + return nsk; + +fail: + close(sk); + return -1; +} + +static void signal_cb(int signum, void *user_data) +{ + switch (signum) { + case SIGINT: + case SIGTERM: + keep_running = false; + mainloop_quit(); + break; + default: + break; + } +} + +int main(int argc, char *argv[]) +{ + const int sec = BT_SECURITY_LOW; + const uint8_t src_type = BDADDR_LE_PUBLIC; + int opt; + bdaddr_t src_addr; + int dev_id = -1; + uint16_t mtu = 0; + sigset_t mask; + + setlogmask (LOG_UPTO (LOG_NOTICE)); + + while ((opt = getopt_long(argc, argv, "+hvrm:t:i:", + main_options, NULL)) != -1) { + switch (opt) { + case 'h': + usage(); + return EXIT_SUCCESS; + case 'v': + verbose = true; + setlogmask (LOG_UPTO (LOG_DEBUG)); + break; + case 'm': { + int arg; + + arg = atoi(optarg); + if (arg <= 0) { + syslog(LOG_ERR, "Invalid MTU: %d\n", arg); + return EXIT_FAILURE; + } + + if (arg > UINT16_MAX) { + syslog(LOG_ERR, "MTU too large: %d\n", arg); + return EXIT_FAILURE; + } + + mtu = (uint16_t) arg; + break; + } + case 't': { + int arg; + + arg = atoi(optarg); + if (arg <= 0) { + syslog(LOG_ERR, "Invalid Temperature threshold: %d\n", arg); + return EXIT_FAILURE; + } + + if (arg > UINT8_MAX) { + syslog(LOG_ERR, "Temperature threshold too large: %d\n", arg); + return EXIT_FAILURE; + } + + temp_threshold = arg; + break; + } + case 'i': + dev_id = hci_devid(optarg); + if (dev_id < 0) { + syslog(LOG_ERR, "Invalid adapter"); + return EXIT_FAILURE; + } + + break; + default: + syslog(LOG_ERR, "Invalid option: %c\n", opt); + return EXIT_FAILURE; + } + } + + argc -= optind; + argv -= optind; + optind = 0; + + openlog("ble-gatt-server-app", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); + printf("Running GATT server\n"); + + if (argc) { + usage(); + return EXIT_SUCCESS; + } + + while (keep_running == true) { + int fd; + struct server *server; + + if (dev_id == -1) { + bacpy(&src_addr, BDADDR_ANY); + } + else if (hci_devba(dev_id, &src_addr) < 0) { + syslog(LOG_ERR, "Adapter not available"); + return EXIT_FAILURE; + } + + fd = l2cap_le_att_listen_and_accept(&src_addr, sec, src_type); + if (fd < 0) { + syslog(LOG_ERR, "Failed to accept L2CAP ATT connection\n"); + return EXIT_FAILURE; + } + + mainloop_init(); + + server = server_create(fd, mtu); + if (!server) { + close(fd); + return EXIT_FAILURE; + } + + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + + mainloop_set_signal(&mask, signal_cb, NULL, NULL); + mainloop_run(); + + /* remove the application signals */ + sigdelset(&mask, SIGTERM); + sigdelset(&mask, SIGINT); + sigemptyset(&mask); + + server_destroy(server); + + close(fd); + } + + return EXIT_SUCCESS; +} diff --git a/ble-gatt-server-example/ble-gatt-server-app.h b/ble-gatt-server-example/ble-gatt-server-app.h new file mode 100644 index 0000000..8c69c74 --- /dev/null +++ b/ble-gatt-server-example/ble-gatt-server-app.h @@ -0,0 +1,82 @@ +/* + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2014 Google Inc. + * Copyright (C) 2019 Digi International Inc. + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +/* + * A full list of officially adopted BLE services can be seen on + * https://www.bluetooth.com/specifications/gatt/services/ + */ +#define UUID_GAP_SERVICE 0x1800 /* Generic Access Service (GAP) */ +#define UUID_GATT_SERVICE 0x1801 /* Generic Attribute Profile Service (GATT) */ +#define UUID_DEVICE_INFORMATION_SERVICE 0x180A /* Device Information Service */ + +/* + * See BT specification to list all official characteristics UUID + * from https://www.bluetooth.com/specifications/gatt/characteristics/ + */ +#define UUID_GATT_CHARACTERISTIC_CURRENT_TIME 0x2A2B +#define UUID_GATT_CHARACTERISTIC_TEMPERATURE_MEASUREMENT 0x2A1C + +/* + * See BT specification to list all GATT Bluetooth Format types + * from https://www.bluetooth.com/specifications/assigned-numbers/format-types/ + */ +#define BLE_GATT_CPF_FORMAT_UINT8 0x04 /* Unsigned 8-bit integer */ +#define BLE_GATT_CPF_FORMAT_UTF8S 0x19 /* UTF-8 string */ + +/* + * See BT specification to list all GATT Bluetooth Units + * from https://www.bluetooth.com/specifications/assigned-numbers/units/ + */ +#define BLE_GATT_CPF_UNIT_DEGREE_CELSIUS 0x27,0x2F /* Celsius temperature (degree Celsius) */ +#define BLE_GATT_CPF_UNIT_TIME_SECONDS 0x27,0x03 /* Time (second) */ + +/* + * See BT specification to list all GATT Bluetooth Namespace Descriptions + * from https://www.bluetooth.com/specifications/assigned-numbers/gatt-namespace-descriptors/ + */ +#define BLE_GATT_SIG_NAMESPACE 0x01 +#define BLE_GATT_SIG_DESCRIPTOR_UNKNOWN 0x00,0x00 +#define BLE_GATT_SIG_DESCRIPTOR_INTERNAL 0x01,0x0F + +/* + * See BT specification 4.2, section 3.3.3.5 + * for Characteristic Presentation Format descriptor: + * - Format (1 octet) + * - Exponent (1 octet) + * - Unit (2 octet) + * - Name Space (1 octet) + * - Description (2 octet) + */ +#define FORMAT_BYTES 7 + +const uint8_t FORMAT_8_BIT_INT[] = { + BLE_GATT_CPF_FORMAT_UINT8, + 0x00, + BLE_GATT_CPF_UNIT_DEGREE_CELSIUS, + BLE_GATT_SIG_NAMESPACE, + BLE_GATT_SIG_DESCRIPTOR_INTERNAL +}; + +const uint8_t FORMAT_UTF_8[] = { + BLE_GATT_CPF_FORMAT_UTF8S, + 0x00, + BLE_GATT_CPF_UNIT_TIME_SECONDS, + BLE_GATT_SIG_NAMESPACE, + BLE_GATT_SIG_DESCRIPTOR_UNKNOWN +}; diff --git a/ble-gatt-server-example/subprocess.c b/ble-gatt-server-example/subprocess.c new file mode 100644 index 0000000..f33c67f --- /dev/null +++ b/ble-gatt-server-example/subprocess.c @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2017 Digi International Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "subprocess.h" + +/** + * Convert a command string to an array of pointers to null-terminated strings + * + * @param command the command to tokenize + * @return strings array on sucess, NULL on error + */ +static char **tokenize_command(char *command) +{ + const char *delim = " "; + char **argv; + char *token, *buf; + int c = 1; + + argv = calloc(c, sizeof(char *)); + if (!argv) + goto done; + + /* 'strtok' changes the original string, so use a copy we can free */ + buf = strdup(command); + + token = strtok(buf, delim); + argv[c - 1] = token ? strdup(token) : NULL; + while (argv[c - 1]) { + void *tmp; + c++; + tmp = realloc(argv, c * sizeof(char *)); + if (!tmp) + free(argv); + argv = tmp; + if (!argv) + break; + token = strtok(NULL, delim); + argv[c - 1] = token ? strdup(token) : NULL; + } + free(buf); + +done: + return argv; +} + +/** + * Safely fork a child process and execute the given command with PATH lookup. + * + * @param command the command to execute + * @param child_status return child status of executable + * @return 0 on success, -1 on error with errno set + */ +int safe_execute(char *command, int *const child_status) +{ + int status = 0, i; + int sub_status = 0; + pid_t pid = 0; + pid_t sub_pid = 0; + struct sigaction const action_ignore = {.sa_handler = SIG_IGN }; + + char **argv = tokenize_command(command); + + if ((NULL == command) || (NULL == argv) || (NULL == child_status)) { + status = EINVAL; + goto done; + } + + /* Initialize child_status */ + *child_status = -1; + + /* + * Fork a child process to do the execvp() call because that call will not + * return if it works. We're doing it this way to try to make this + * function as "nice" to the calling application as possible by not + * inhibiting interrupts as can happen if the system() call is used (plus + * that utility can have weird side-effects if the calling application has + * suid or sgid privileges set). + */ + pid = fork(); + if (pid < 0) { + status = errno; + syslog(LOG_ERR, "[%s:%d] could not fork process: %s (%d)\n", + __func__, __LINE__, strerror(errno), errno); + goto done; + } + + /* + * If we're now the child process, do the execvp(), replacing our process + * with the called process. SIGINT and SIGTERM are ignored in child process + * as we don't have any good ways of cleaning up the subprocess after a + * signal. + */ + if (0 == pid) { + if (sigaction(SIGINT, &action_ignore, NULL) < 0) { + status = errno; + syslog + (LOG_ERR, "[%s:%d] error ignoring SIGINT: %s (%d)\n\n", + __func__, __LINE__, strerror(errno), errno); + } + + if (sigaction(SIGTERM, &action_ignore, NULL) < 0) { + status = errno; + syslog + (LOG_ERR, "[%s:%d] error ignoring SIGTERM: %s (%d)\n\n", + __func__, __LINE__, strerror(errno), errno); + } + + if (execvp(argv[0], (char *const *)argv) < 0) { + status = errno; + syslog + (LOG_ERR, "[%s:%d] error executing child process: %s (%d)\n", + __func__, __LINE__, strerror(errno), errno); + } + + _exit(127); + } + + /* + * Otherwise, if we're the parent, wait for the child process to terminate. + * If waitpid returns due to a signal (EINTR), ignore the error and + * continue to wait. + */ + do { + errno = 0; + sub_pid = waitpid(pid, &sub_status, 0); + if ((sub_pid < 0) && (EINTR != errno)) { + status = errno; + syslog + (LOG_ERR, "[%s:%d] error returned by waitpid: %s (%d)\n", + __func__, __LINE__, strerror(errno), errno); + goto done; + } + + if (WIFEXITED(sub_status)) { + *child_status = WEXITSTATUS(sub_status); + } + } while ((errno == EINTR) || (!WIFEXITED(sub_status))); + +done: + /* Free resources */ + i = 0; + while (argv && argv[i]) { + free(argv[i]); + i++; + } + free(argv); + + errno = status; + + return (status == 0) ? 0 : -1; +} diff --git a/ble-gatt-server-example/subprocess.h b/ble-gatt-server-example/subprocess.h new file mode 100644 index 0000000..2bbb579 --- /dev/null +++ b/ble-gatt-server-example/subprocess.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2017 Digi International Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SUBPROCESS_H_ +#define SUBPROCESS_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Safely fork a child process and execute the given command with PATH lookup. + * + * @param command the command to execute + * @param child_status return child status of executable + * @return 0 on success, -1 on error with errno set + */ +int safe_execute(char *command, int *const child_status); + +#ifdef __cplusplus +} +#endif + +#endif /* SUBPROCESS_H_ */