blob: 7f0d1598f4e8ef02e7014ee74c78d768467cb16a [file] [log] [blame]
/** @file
* @brief Interactive Bluetooth LE shell application
*
* Application allows implement Bluetooth LE functional commands performing
* simple diagnostic interaction between LE host stack and LE controller
*/
/*
* Copyright (c) 2015 Intel Corporation
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1) Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2) Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3) Neither the name of Intel Corporation nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <errno.h>
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <misc/printk.h>
#include <misc/byteorder.h>
#include <console/uart_console.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/conn.h>
#include <bluetooth/gatt.h>
#include "btshell.h"
#define DEVICE_NAME "test shell"
static struct bt_conn *default_conn = NULL;
static void device_found(const bt_addr_le_t *addr, int8_t rssi, uint8_t evtype,
const uint8_t *ad, uint8_t len)
{
char le_addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(addr, le_addr, sizeof(le_addr));
printk("[DEVICE]: %s, AD evt type %u, AD data len %u, RSSI %i\n",
le_addr, evtype, len, rssi);
}
static void connected(struct bt_conn *conn)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Connected: %s\n", addr);
if (!default_conn) {
default_conn = bt_conn_get(conn);
}
}
static void disconnected(struct bt_conn *conn)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Disconnected: %s\n", addr);
if (default_conn == conn) {
bt_conn_put(default_conn);
default_conn = NULL;
}
}
static void identity_resolved(struct bt_conn *conn, const bt_addr_le_t *rpa,
const bt_addr_le_t *identity)
{
char addr_identity[BT_ADDR_LE_STR_LEN];
char addr_rpa[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(identity, addr_identity, sizeof(addr_identity));
bt_addr_le_to_str(rpa, addr_rpa, sizeof(addr_rpa));
printk("Identity resolved %s -> %s\n", addr_rpa, addr_identity);
}
static void security_changed(struct bt_conn *conn, bt_security_t level)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Security changed: %s level %u\n", addr, level);
}
static struct bt_conn_cb conn_callbacks = {
.connected = connected,
.disconnected = disconnected,
.identity_resolved = identity_resolved,
.security_changed = security_changed,
};
static void cmd_init(int argc, char *argv[])
{
int err;
err = bt_enable(NULL);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
} else {
printk("Bluetooth initialized\n");
}
}
static int char2hex(const char *c, uint8_t *x)
{
if (*c >= '0' && *c <= '9') {
*x = *c - '0';
} else if (*c >= 'a' && *c <= 'f') {
*x = *c - 'a' + 10;
} else if (*c >= 'A' && *c <= 'F') {
*x = *c - 'A' + 10;
} else {
return -EINVAL;
}
return 0;
}
static int str2bt_addr_le(const char *str, const char *type, bt_addr_le_t *addr)
{
int i, j;
uint8_t tmp;
if (strlen(str) != 17) {
return -EINVAL;
}
for (i = 5, j = 1; *str != '\0'; str++, j++) {
if (!(j % 3) && (*str != ':')) {
return -EINVAL;
} else if (*str == ':') {
i--;
continue;
}
addr->val[i] = addr->val[i] << 4;
if (char2hex(str, &tmp) < 0) {
return -EINVAL;
}
addr->val[i] |= tmp;
}
if (!strcmp(type, "public") || !strcmp(type, "(public)")) {
addr->type = BT_ADDR_LE_PUBLIC;
} else if (!strcmp(type, "random") || !strcmp(type, "(random)")) {
addr->type = BT_ADDR_LE_RANDOM;
} else {
return -EINVAL;
}
return 0;
}
static void cmd_connect_le(int argc, char *argv[])
{
int err;
bt_addr_le_t addr;
struct bt_conn *conn;
if (argc < 2) {
printk("Peer address required\n");
return;
}
if (argc < 3) {
printk("Peer address type required\n");
return;
}
err = str2bt_addr_le(argv[1], argv[2], &addr);
if (err) {
printk("Invalid peer address (err %d)\n", err);
return;
}
conn = bt_conn_create_le(&addr);
if (!conn) {
printk("Connection failed\n");
} else {
printk("Connection pending\n");
/* unref connection obj in advance as app user */
bt_conn_put(conn);
}
}
static void cmd_disconnect(int argc, char *argv[])
{
struct bt_conn *conn;
int err;
if (default_conn) {
conn = default_conn;
} else {
bt_addr_le_t addr;
if (argc < 2) {
printk("Peer address required\n");
return;
}
if (argc < 3) {
printk("Peer address type required\n");
return;
}
err = str2bt_addr_le(argv[1], argv[2], &addr);
if (err) {
printk("Invalid peer address (err %d)\n", err);
return;
}
conn = bt_conn_lookup_addr_le(&addr);
}
if (!conn) {
printk("Not connected\n");
return;
}
err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
if (err) {
printk("Disconnection failed (err %d)\n", err);
}
}
static void cmd_active_scan_on(void)
{
int err;
err = bt_start_scanning(BT_SCAN_FILTER_DUP_ENABLE, device_found);
if (err) {
printk("Bluetooth set active scan failed (err %d)\n", err);
} else {
printk("Bluetooth active scan enabled\n");
}
}
static void cmd_scan_off(void)
{
int err;
err = bt_stop_scanning();
if (err) {
printk("Stopping scanning failed (err %d)\n", err);
} else {
printk("Scan successfully stopped\n");
}
}
static void cmd_scan(int argc, char *argv[])
{
const char *action;
if (argc < 2) {
printk("Scan [on/off] parameter required\n");
return;
}
action = (char*)argv[1];
if (!strcmp(action, "on")) {
cmd_active_scan_on();
} else if (!strcmp(action, "off")) {
cmd_scan_off();
} else {
printk("Scan [on/off] parameter required\n");
}
}
static void cmd_security(int argc, char *argv[])
{
int err, sec;
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("Security level required\n");
return;
}
sec = *argv[1] - '0';
err = bt_conn_security(default_conn, sec);
if (err) {
printk("Setting security failed (err %d)\n", err);
}
}
static void exchange_rsp(struct bt_conn *conn, uint8_t err)
{
printk("Exchange %s\n", err == 0 ? "successful" : "failed");
}
static void cmd_gatt_exchange_mtu(int argc, char *argv[])
{
int err;
if (!default_conn) {
printk("Not connected\n");
return;
}
err = bt_gatt_exchange_mtu(default_conn, exchange_rsp);
if (err) {
printk("Exchange failed (err %d)\n", err);
} else {
printk("Exchange pending\n");
}
}
static const struct bt_eir ad_discov[] = {
{
.len = 2,
.type = BT_EIR_FLAGS,
.data = { BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR },
},
{ }
};
static const struct bt_eir ad_non_discov[] = { { 0 } };
static const struct bt_eir sd[] = {
{
.len = sizeof(DEVICE_NAME),
.type = BT_EIR_NAME_COMPLETE,
.data = DEVICE_NAME,
},
{ }
};
static void cmd_advertise(int argc, char *argv[])
{
uint8_t adv_type;
int i;
const struct bt_eir *ad;
static struct {
const char *str;
uint8_t type;
} adv_t[] = {
{ "on", BT_LE_ADV_IND },
{ "direct", BT_LE_ADV_DIRECT_IND },
{ "scan", BT_LE_ADV_SCAN_IND },
{ "nconn", BT_LE_ADV_NONCONN_IND },
};
adv_type = adv_t[0].type;
if (argc >= 2) {
const char *adv_type_str = (char*)argv[1];
if (!strcmp(adv_type_str, "off")) {
if (bt_stop_advertising() < 0) {
printk("Failed to stop advertising\n");
} else {
printk("Advertising stopped\n");
}
return;
}
for (i = 0; i < ARRAY_SIZE(adv_t); i++) {
if (!strcmp(adv_type_str, adv_t[i].str)) {
adv_type = adv_t[i].type;
break;
}
}
if (i == ARRAY_SIZE(adv_t)) {
goto fail;
}
} else {
goto fail;
}
/* Parse advertisement data */
if (argc >= 3) {
const char *mode = (char*)argv[2];
if (!strcmp(mode, "discov")) {
ad = ad_discov;
} else if (!strcmp(mode, "non_discov")) {
ad = ad_non_discov;
} else {
goto fail;
}
} else {
ad = ad_discov;
}
if (bt_start_advertising(adv_type, ad, sd) < 0) {
printk("Failed to start advertising\n");
} else {
printk("Advertising started\n");
}
return;
fail:
printk("Usage: advertise <type> <ad mode>\n");
printk("type: off, ");
for (i = 0; i < sizeof(adv_t) / sizeof(adv_t[0]); i++) {
printk("%s, ", adv_t[i].str);
}
printk("\n");
printk("ad mode: discov, non_discov\n");
}
static struct bt_gatt_discover_params discover_params;
static struct bt_uuid uuid;
static uint8_t discover_func(const struct bt_gatt_attr *attr, void *user_data)
{
printk("Discover found handle %u\n", attr->handle);
return BT_GATT_ITER_CONTINUE;
}
static void discover_destroy(void *user_data)
{
struct bt_gatt_discover_params *params = user_data;
printk("Discover destroy\n");
memset(params, 0, sizeof(*params));
}
static void cmd_gatt_discover(int argc, char *argv[])
{
int err;
if (!default_conn) {
printk("Not connected\n");
return;
}
discover_params.func = discover_func;
discover_params.destroy = discover_destroy;
discover_params.start_handle = 0x0001;
discover_params.end_handle = 0xffff;
if (argc < 2) {
if (!strcmp(argv[0], "gatt-discover-primary") ||
!strcmp(argv[0], "gatt-discover-secondary")) {
printk("UUID type required\n");
return;
}
goto done;
}
/* Only set the UUID if the value is valid (non zero) */
uuid.u16 = strtoul(argv[1], NULL, 16);
if (uuid.u16) {
uuid.type = BT_UUID_16;
discover_params.uuid = &uuid;
}
if (argc > 2) {
discover_params.start_handle = strtoul(argv[2], NULL, 16);
if (argc > 3) {
discover_params.end_handle = strtoul(argv[3], NULL, 16);
}
}
done:
if (!strcmp(argv[0], "gatt-discover-secondary")) {
discover_params.type = BT_GATT_DISCOVER_SECONDARY;
} else if (!strcmp(argv[0], "gatt-discover-include")) {
discover_params.type = BT_GATT_DISCOVER_INCLUDE;
} else if (!strcmp(argv[0], "gatt-discover-characteristic")) {
discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;
} else if (!strcmp(argv[0], "gatt-discover-descriptor")) {
discover_params.type = BT_GATT_DISCOVER_DESCRIPTOR;
} else {
discover_params.type = BT_GATT_DISCOVER_PRIMARY;
}
err = bt_gatt_discover(default_conn, &discover_params);
if (err) {
printk("Discover failed (err %d)\n", err);
} else {
printk("Discover pending\n");
}
}
static void read_func(struct bt_conn *conn, int err, const void *data,
uint16_t length)
{
printk("Read complete: err %u length %u\n", err, length);
}
static void cmd_gatt_read(int argc, char *argv[])
{
int err;
uint16_t handle, offset = 0;
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("handle required\n");
return;
}
handle = strtoul(argv[1], NULL, 16);
if (argc > 2) {
offset = strtoul(argv[2], NULL, 16);
}
err = bt_gatt_read(default_conn, handle, offset, read_func);
if (err) {
printk("Read failed (err %d)\n", err);
} else {
printk("Read pending\n");
}
}
void cmd_gatt_mread(int argc, char *argv[])
{
uint16_t h[8];
int i, err;
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("Attribute handles in hex format to read required\n");
return;
}
if (argc - 1 > ARRAY_SIZE(h)) {
printk("Enter max %u handle items to read\n", ARRAY_SIZE(h));
return;
}
for (i = 0; i < argc - 1; i++) {
h[i] = strtoul(argv[i + 1], NULL, 16);
}
err = bt_gatt_read_multiple(default_conn, h, i, read_func);
if (err) {
printk("GATT multiple read request failed (err %d)\n", err);
}
}
static void write_func(struct bt_conn *conn, uint8_t err)
{
printk("Write complete: err %u\n", err);
}
static void cmd_gatt_write(int argc, char *argv[])
{
int err;
uint16_t handle, offset;
uint8_t data;
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("handle required\n");
return;
}
handle = strtoul(argv[1], NULL, 16);
if (argc < 3) {
printk("offset required\n");
return;
}
/* TODO: Add support for longer data */
offset = strtoul(argv[2], NULL, 16);
if (argc < 4) {
printk("data required\n");
return;
}
/* TODO: Add support for longer data */
data = strtoul(argv[3], NULL, 16);
err = bt_gatt_write(default_conn, handle, offset, &data, sizeof(data),
write_func);
if (err) {
printk("Write failed (err %d)\n", err);
} else {
printk("Write pending\n");
}
}
static void cmd_gatt_write_without_rsp(int argc, char *argv[])
{
int err;
uint16_t handle;
uint8_t data;
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("handle required\n");
return;
}
handle = strtoul(argv[1], NULL, 16);
if (argc < 3) {
printk("data required\n");
return;
}
data = strtoul(argv[2], NULL, 16);
err = bt_gatt_write_without_response(default_conn, handle, &data,
sizeof(data), false);
printk("Write Complete (err %d)\n", err);
}
static void cmd_gatt_write_signed(int argc, char *argv[])
{
int err;
uint16_t handle;
uint8_t data;
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("handle required\n");
return;
}
handle = strtoul(argv[1], NULL, 16);
if (argc < 3) {
printk("data required\n");
return;
}
data = strtoul(argv[2], NULL, 16);
err = bt_gatt_write_without_response(default_conn, handle, &data,
sizeof(data), true);
printk("Write Complete (err %d)\n", err);
}
static struct bt_gatt_subscribe_params subscribe_params;
static void subscribe_destroy(void *user_data)
{
struct bt_gatt_subscribe_params *params = user_data;
printk("Subscribe destroy\n");
params->value_handle = 0;
}
static void subscribe_func(struct bt_conn *conn, int err,
const void *data, uint16_t length)
{
if (!length) {
printk("Subscribe complete: err %u\n", err);
} else {
printk("Notification: data %p length %u\n", data, length);
}
}
static void cmd_gatt_subscribe(int argc, char *argv[])
{
int err;
uint16_t handle;
if (subscribe_params.value_handle) {
printk("Cannot subscribe: subscription to %u already exists\n",
subscribe_params.value_handle);
return;
}
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("handle required\n");
return;
}
handle = strtoul(argv[1], NULL, 16);
if (argc < 3) {
printk("value handle required\n");
return;
}
subscribe_params.value_handle = strtoul(argv[2], NULL, 16);
subscribe_params.value = BT_GATT_CCC_NOTIFY;
subscribe_params.func = subscribe_func;
subscribe_params.destroy = subscribe_destroy;
if (argc > 3) {
subscribe_params.value = strtoul(argv[3], NULL, 16);
}
err = bt_gatt_subscribe(default_conn, handle, &subscribe_params);
if (err) {
printk("Subscribe failed (err %d)\n", err);
} else {
printk("Subscribe pending\n");
}
}
static void cmd_gatt_unsubscribe(int argc, char *argv[])
{
int err;
uint16_t handle;
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("handle required\n");
return;
}
handle = strtoul(argv[1], NULL, 16);
if (!subscribe_params.value_handle) {
printk("No subscription found\n");
return;
}
err = bt_gatt_unsubscribe(default_conn, handle, &subscribe_params);
if (err) {
printk("Unsubscribe failed (err %d)\n", err);
} else {
printk("Unsubscribe success\n");
}
}
static int read_string(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
const char *str = attr->user_data;
return bt_gatt_attr_read(conn, attr, buf, len, offset, str,
strlen(str));
}
static uint16_t appearance_value = 0x0001;
static int read_appearance(struct bt_conn *conn,
const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset)
{
uint16_t appearance = sys_cpu_to_le16(appearance_value);
return bt_gatt_attr_read(conn, attr, buf, len, offset, &appearance,
sizeof(appearance));
}
/* GAP SERVICE (0x1800) */
static struct bt_uuid gap_uuid = {
.type = BT_UUID_16,
.u16 = BT_UUID_GAP,
};
static struct bt_uuid device_name_uuid = {
.type = BT_UUID_16,
.u16 = BT_UUID_GAP_DEVICE_NAME,
};
static struct bt_gatt_chrc name_chrc = {
.properties = BT_GATT_CHRC_READ,
.value_handle = 0x0003,
.uuid = &device_name_uuid,
};
static struct bt_uuid appeareance_uuid = {
.type = BT_UUID_16,
.u16 = BT_UUID_GAP_APPEARANCE,
};
static struct bt_gatt_chrc appearance_chrc = {
.properties = BT_GATT_CHRC_READ,
.value_handle = 0x0005,
.uuid = &appeareance_uuid,
};
static struct bt_gatt_attr attrs[] = {
BT_GATT_PRIMARY_SERVICE(0x0001, &gap_uuid),
BT_GATT_CHARACTERISTIC(0x0002, &name_chrc),
BT_GATT_DESCRIPTOR(0x0003, &device_name_uuid, BT_GATT_PERM_READ,
read_string, NULL, DEVICE_NAME),
BT_GATT_CHARACTERISTIC(0x0004, &appearance_chrc),
BT_GATT_DESCRIPTOR(0x0005, &appeareance_uuid, BT_GATT_PERM_READ,
read_appearance, NULL, NULL),
};
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Passkey for %s: %u\n", addr, passkey);
}
static void auth_passkey_entry(struct bt_conn *conn)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Enter passkey for %s\n", addr);
}
static void auth_cancel(struct bt_conn *conn)
{
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Pairing cancelled: %s\n", addr);
}
static struct bt_auth_cb auth_cb_display = {
.passkey_display = auth_passkey_display,
.passkey_entry = NULL,
.cancel = auth_cancel,
};
static struct bt_auth_cb auth_cb_input = {
.passkey_display = NULL,
.passkey_entry = auth_passkey_entry,
.cancel = auth_cancel,
};
static struct bt_auth_cb auth_cb_all = {
.passkey_display = auth_passkey_display,
.passkey_entry = auth_passkey_entry,
.cancel = auth_cancel,
};
static void cmd_auth(int argc, char *argv[])
{
if (argc < 2) {
printk("auth [display, input, all, none] parameter required\n");
return;
}
if (!strcmp(argv[1], "all")) {
bt_auth_cb_register(&auth_cb_all);
} else if (!strcmp(argv[1], "input")) {
bt_auth_cb_register(&auth_cb_input);
} else if (!strcmp(argv[1], "display")) {
bt_auth_cb_register(&auth_cb_display);
} else if (!strcmp(argv[1], "none")) {
bt_auth_cb_register(NULL);
} else {
printk("auth [display, input, all, none] parameter required\n");
}
}
static void cmd_auth_cancel(int argc, char *argv[])
{
if (!default_conn) {
printk("Not connected\n");
return;
}
bt_auth_cancel(default_conn);
}
static void cmd_auth_passkey(int argc, char *argv[])
{
unsigned int passkey;
if (!default_conn) {
printk("Not connected\n");
return;
}
if (argc < 2) {
printk("passkey required\n");
return;
}
passkey = atoi(argv[1]);
if (passkey > 999999) {
printk("Passkey should be between 0-999999\n");
return;
}
bt_auth_passkey_entry(default_conn, passkey);
}
struct shell_cmd commands[] = {
{ "init", cmd_init },
{ "connect", cmd_connect_le },
{ "disconnect", cmd_disconnect },
{ "scan", cmd_scan },
{ "advertise", cmd_advertise },
{ "security", cmd_security },
{ "auth", cmd_auth },
{ "auth-cancel", cmd_auth_cancel },
{ "auth-passkey", cmd_auth_passkey },
{ "gatt-exchange-mtu", cmd_gatt_exchange_mtu },
{ "gatt-discover-primary", cmd_gatt_discover },
{ "gatt-discover-secondary", cmd_gatt_discover },
{ "gatt-discover-include", cmd_gatt_discover },
{ "gatt-discover-characteristic", cmd_gatt_discover },
{ "gatt-discover-descriptor", cmd_gatt_discover },
{ "gatt-read", cmd_gatt_read },
{ "gatt-read-multiple", cmd_gatt_mread },
{ "gatt-write", cmd_gatt_write },
{ "gatt-write-without-response", cmd_gatt_write_without_rsp },
{ "gatt-write-signed", cmd_gatt_write_signed },
{ "gatt-subscribe", cmd_gatt_subscribe },
{ "gatt-unsubscribe", cmd_gatt_unsubscribe },
{ NULL, NULL }
};
#ifdef CONFIG_MICROKERNEL
void mainloop(void)
#else
void main(void)
#endif
{
bt_conn_cb_register(&conn_callbacks);
bt_gatt_register(attrs, ARRAY_SIZE(attrs));
shell_init("btshell> ", commands);
}