This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Zephyr Central to Peripheral communication

Hi,

I have some issues understanding exactly how a BT central acting as a Client can get the info it wants from a BT peripheral acting as a Server. I have a custom periheral (running zephyr BLE on a nrf52840) that I want to communicate with a custom central (same thing, nrf52840).

The custom peripheral I have managed to get working, and it consist of a single service where a client can write to one characteristic and the client can read what has been written in another characteristic. The code is as follows:

Peripheral:

read_write_service.c:

#define BT_UUID_READ_WRITE_SERVICE                                          \
  BT_UUID_DECLARE_128(0x3E, 0x09, 0x99, 0x10, 0x29, 0x3F, 0x11, 0xE4, 0x93, \
                      0xBD, 0xAF, 0xD0, 0xFE, 0x6D, 0x1D, 0xA0)
// input to the peripheral device
#define BT_UUID_INPUT                                                       \
  BT_UUID_DECLARE_128(0x3E, 0x09, 0x99, 0x11, 0x29, 0x3F, 0x11, 0xE4, 0x93, \
                      0xBD, 0xAF, 0xD0, 0xFE, 0x6D, 0x1D, 0xA1)
// output from the peripheral device
#define BT_UUID_OUTPUT                                                      \
  BT_UUID_DECLARE_128(0x3E, 0x09, 0x99, 0x11, 0x29, 0x3F, 0x11, 0xE4, 0x93, \
                      0xBD, 0xAF, 0xD0, 0xFE, 0x6D, 0x1D, 0xA2)
                     
static struct bt_gatt_attr attrs[] = {
    BT_GATT_PRIMARY_SERVICE(BT_UUID_READ_WRITE_SERVICE),
    BT_GATT_CHARACTERISTIC(BT_UUID_INPUT, BT_GATT_CHRC_WRITE,
                           BT_GATT_PERM_WRITE, NULL, write_input, NULL),
    BT_GATT_CHARACTERISTIC(BT_UUID_OUTPUT, BT_GATT_CHRC_READ, BT_GATT_PERM_READ,
                           read_output, NULL, NULL),
};

static struct bt_gatt_service read_write_service = BT_GATT_SERVICE(attrs);

u8_t a_number = 0;

void read_write_service_init() {
  bt_gatt_service_register(&read_write_service);
}

ssize_t write_input(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                    const void *buf, u16_t len, u16_t offset, u8_t flags) {
  const u8_t *new_number = buf;
  if (!len) {
    return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
  }

  if (*new_number >= 0 && *new_number <= 10) {
    a_number = *new_number;
  } else {
    return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
  }

  printk("write_input [%d]\n", a_number);
  return len;
}

ssize_t read_output(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                    void *buf, u16_t len, u16_t offset) {
  printk("read_output\n");
  return bt_gatt_attr_read(conn, attr, buf, len, offset, &a_number,
                           sizeof(a_number));
}
main.c:
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)

struct bt_conn *default_conn;

static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};

static void connected(struct bt_conn *conn, u8_t err) {
  if (err) {
    printk("Connection failed (err %u)\n", err);
  } else {
    printk("Connected\n");
    default_conn = bt_conn_ref(conn);
  }
}

static void disconnected(struct bt_conn *conn, u8_t reason) {
  printk("Disconnected (reason %u)\n", reason);

  if (default_conn) {
    bt_conn_unref(default_conn);
    default_conn = NULL;
  }
}

static struct bt_conn_cb conn_callbacks = {
    .connected = connected,
    .disconnected = disconnected,
};

static void bt_ready(int err) {
  if (err) {
    printk("Bluetooth init failed (err %d)\n", err);
    return;
  }

  read_write_service_init();

  /*if (IS_ENABLED(CONFIG_SETTINGS)) {
    settings_load();
  }*/

  err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);
  if (err) {
    printk("Advertising failed to start (err %d)\n", err);
    return;
  }

  printk("Advertising successfully started\n");
}

void main(void) {
  printk("starting in main \n\n");

  int err;

  err = bt_enable(bt_ready);
  if (err) {
    printk("Bluetooth initialization failed.");
    return;
  }

  bt_conn_cb_register(&conn_callbacks);
}
Now, this is where my question begins: How can I read and write to the custom peripheral with a custom central? There are several perihperal guides and virtually none for a zephyr BLE central, just example code which is a bit hard to grapple. What I want to do with my custom central is:
- Do a service discovery and prompt to terminal a list of all services
- Do a characteristic discovery inside each service and prompt to terminal which characteristics can be read or written to
- Do an actual read and write to characteristics in the peripheral from the central.
This is how far I've gotten on the central side:
#define BT_UUID_READ_WRITE_SERVICE                                             \
	BT_UUID_DECLARE_128(0x3E, 0x09, 0x99, 0x10, 0x29, 0x3F, 0x11, 0xE4,    \
			    0x93, 0xBD, 0xAF, 0xD0, 0xFE, 0x6D, 0x1D, 0xA0)
// input to the peripheral device
#define BT_UUID_INPUT                                                          \
	BT_UUID_DECLARE_128(0x3E, 0x09, 0x99, 0x11, 0x29, 0x3F, 0x11, 0xE4,    \
			    0x93, 0xBD, 0xAF, 0xD0, 0xFE, 0x6D, 0x1D, 0xA1)
// output from the peripheral device
#define BT_UUID_OUTPUT                                                         \
	BT_UUID_DECLARE_128(0x3E, 0x09, 0x99, 0x11, 0x29, 0x3F, 0x11, 0xE4,    \
			    0x93, 0xBD, 0xAF, 0xD0, 0xFE, 0x6D, 0x1D, 0xA2)

#define BT_UUID_NAMING_CHARACTERISTIC                                          \
	BT_UUID_DECLARE_128(0x3E, 0x09, 0x99, 0x11, 0x29, 0x3F, 0x11, 0xE4,    \
			    0x93, 0xBD, 0xAF, 0xD0, 0xFE, 0x6D, 0x1D, 0xA3)

static void start_scan(void);

static struct bt_conn *default_conn;

static u8_t discovered(struct bt_conn *conn, const struct bt_gatt_attr *attr,
		       struct bt_gatt_discover_params *params);

static struct bt_gatt_discover_params discover_params;
static struct bt_uuid_128 uuid = BT_UUID_INIT_128(0);

static void device_found(const bt_addr_le_t *addr, s8_t rssi, u8_t type,
			 struct net_buf_simple *ad)
{
	char addr_str[BT_ADDR_LE_STR_LEN];
	int err;

	if (default_conn) {
		return;
	}

	/* We're only interested in connectable events */
	if (type != BT_GAP_ADV_TYPE_ADV_IND &&
	    type != BT_GAP_ADV_TYPE_ADV_DIRECT_IND) {
		return;
	}

	bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
	printk("Device found: %s (RSSI %d)\n", addr_str, rssi);

	/* connect only to devices in close proximity */
	if (rssi < -50) {
		return;
	}

	if (bt_le_scan_stop()) {
		return;
	}

	err = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN,
				BT_LE_CONN_PARAM_DEFAULT, &default_conn);
	if (err) {
		printk("Create conn to %s failed (%u)\n", addr_str, err);
		start_scan();
	}
}

static void start_scan(void)
{
	int err;

	/* This demo doesn't require active scan */
	err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, device_found);
	if (err) {
		printk("Scanning failed to start (err %d)\n", err);
		return;
	}

	printk("Scanning successfully started\n");
}

static u8_t discovered(struct bt_conn *conn, const struct bt_gatt_attr *attr,
		       struct bt_gatt_discover_params *params)
{
	int err;

	if (!attr) {
		printk("Discover complete\n");
		memset(params, 0, sizeof(*params));
		return BT_GATT_ITER_STOP;
	}

	if (bt_uuid_cmp(discover_params.uuid, BT_UUID_OUTPUT)) {
		memcpy(&uuid, BT_UUID_OUTPUT, sizeof(uuid));
		discover_params.uuid = &uuid.uuid;
		discover_params.start_handle = attr->handle + 1;
		discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;

		err = bt_gatt_discover(conn, &discover_params);
		if (err) {
			printk("Discover failed (err %d)\n", err);
		}
		char dest2[150];
		bt_uuid_to_str(params->uuid, dest2, sizeof(dest2));
		printk("Discovered attribute again- uuid: %s, handle: %u\n",
		       dest2, attr->handle);
		bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
		return BT_GATT_ITER_STOP;
	}

	char dest[150];
	bt_uuid_to_str(attr->uuid, dest, sizeof(dest));
	printk("Discovered attribute - uuid: %s, handle: %u\n", dest,
	       attr->handle);
	bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
	return BT_GATT_ITER_STOP;
}

static void connected(struct bt_conn *conn, u8_t err)
{
	char addr[BT_ADDR_LE_STR_LEN];

	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));

	if (err) {
		printk("Failed to connect to %s (%u)\n", addr, err);

		bt_conn_unref(default_conn);
		default_conn = NULL;

		start_scan();
		return;
	}

	if (conn != default_conn) {
		return;
	}

	printk("Connected: %s\n", addr);

	discover_params.func = discovered;
	discover_params.start_handle = 0x0001;
	discover_params.end_handle = 0xFFFF;
	discover_params.type = BT_GATT_DISCOVER_PRIMARY;
	
	bt_gatt_discover(conn, &discover_params);
}

static void disconnected(struct bt_conn *conn, u8_t reason)
{
	char addr[BT_ADDR_LE_STR_LEN];

	if (conn != default_conn) {
		return;
	}

	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));

	printk("Disconnected: %s (reason 0x%02x)\n", addr, reason);

	bt_conn_unref(default_conn);
	default_conn = NULL;

	start_scan();
}

static struct bt_conn_cb conn_callbacks = {
	.connected = connected,
	.disconnected = disconnected,
};

void main(void)
{
	int err;

	err = bt_enable(NULL);
	if (err) {
		printk("Bluetooth init failed (err %d)\n", err);
		return;
	}

	printk("Bluetooth initialized\n");

	bt_conn_cb_register(&conn_callbacks);

	start_scan();
}
Im not sure if the central-code is perfectly correct either, but I dont know where to go after doing a service discovery to get to the results i wanted as stated above. Hope any of you can help and would kindheartedly advice that one of your next zephyr/NCS-guides on this site could pertain a BLE Central guide.
Looking forward to hearing from you!
Best regards,
Jonas
  • Hello Jonas,

    I see you have attached some snippets from your peripheral, but before you start working on your central, are you able to write to and read from the characteristics that you want to using a phone and the nRF Connect app as a central? Alternatively on nRF Connect for Desktop using an extra DK?

    I believe that the discovery callback function (in your case "discovered()" is triggered several times, one time for each service and characteristic.

    Do you see any callbacks to your discover function?

    For service discovery and how to use the characteristics from a central, I would recommend that you take a look at the sample found in:

    NCS\nrf\samples\bluetooth\central_uart

    Best regards,

    Edvin

  • Hi Edvin, and thanks for your swift reply!

    Yes, I have tested with the nrf Connect Mobile successfully picks up the peripheral and can connect + read/write.

    With regards to the central_uart link, it was a bit hard to grasp. What I struggle with, is understanding how the elaborate callback scheme works. Is it possible for you to draw out an example of retrieving the read value from the peripheral and store it in e.g. a struct or push it to console/printk? 

  • Hello,

     

    Jonas Lien said:
    Is it possible for you to draw out an example of retrieving the read value from the peripheral and store it in e.g. a struct or push it to console/printk? 

     That is more or less what the central_uart example does.

    Look at how this example is built up:

    In the connected() callback function, gatt_discover() is called with a pointer to the connection, conn.

    Inside gatt_discover(), it calls bt_gatt_dm_start(), with the service that it is looking for as an input parameter. Exactly how you would do it if you want to list all services, I am not sure. However, a central typically (and also in your case) knows what services it is looking for. Isn't that also the case in your case?

    Now, after calling bt_gatt_dm_start, you will hopefully get a callback to discovery_complete, or whatever you called your discovery_cb.completed function.

    You need to look at the definitions of 

    bt_nus_handles_assign()
    bt_nus_subscribe_receive()

    to see what they do, and implement the similar thing in your application.

    Looking at the central_hrs example, which follows the implementation you have already started on, which looks to be the zephyr method (not used in NCS as far as I can see), it works in a similar fashion.

    From the zephyr\samples\bluetooth\central_hr example's connected callback function:

    printk("Connected: %s\n", addr);
    
    	if (conn == default_conn) {
    		memcpy(&uuid, BT_UUID_HRS, sizeof(uuid));
    		discover_params.uuid = &uuid.uuid;
    		discover_params.func = discover_func;
    		discover_params.start_handle = 0x0001;
    		discover_params.end_handle = 0xffff;
    		discover_params.type = BT_GATT_DISCOVER_PRIMARY;
    
    		err = bt_gatt_discover(default_conn, &discover_params);
    		if (err) {
    			printk("Discover failed(err %d)\n", err);
    			return;
    		}
    	}

    So it will start the discovery procedure for the Heart Rate Service UUID. If found, this will trigger the discover_func() callback:

    static uint8_t discover_func(struct bt_conn *conn,
    			     const struct bt_gatt_attr *attr,
    			     struct bt_gatt_discover_params *params)
    {
    	int err;
    
    	if (!attr) {
    		printk("Discover complete\n");
    		(void)memset(params, 0, sizeof(*params));
    		return BT_GATT_ITER_STOP;
    	}
    
    	printk("[ATTRIBUTE] handle %u\n", attr->handle);
    
    	if (!bt_uuid_cmp(discover_params.uuid, BT_UUID_HRS)) {
    		memcpy(&uuid, BT_UUID_HRS_MEASUREMENT, sizeof(uuid));
    		discover_params.uuid = &uuid.uuid;
    		discover_params.start_handle = attr->handle + 1;
    		discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;
    
    		err = bt_gatt_discover(conn, &discover_params);
    		if (err) {
    			printk("Discover failed (err %d)\n", err);
    		}
    	} else if (!bt_uuid_cmp(discover_params.uuid,
    				BT_UUID_HRS_MEASUREMENT)) {
    		memcpy(&uuid, BT_UUID_GATT_CCC, sizeof(uuid));
    		discover_params.uuid = &uuid.uuid;
    		discover_params.start_handle = attr->handle + 2;
    		discover_params.type = BT_GATT_DISCOVER_DESCRIPTOR;
    		subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
    
    		err = bt_gatt_discover(conn, &discover_params);
    		if (err) {
    			printk("Discover failed (err %d)\n", err);
    		}
    	} else {
    		subscribe_params.notify = notify_func;
    		subscribe_params.value = BT_GATT_CCC_NOTIFY;
    		subscribe_params.ccc_handle = attr->handle;
    
    		err = bt_gatt_subscribe(conn, &subscribe_params);
    		if (err && err != -EALREADY) {
    			printk("Subscribe failed (err %d)\n", err);
    		} else {
    			printk("[SUBSCRIBED]\n");
    		}
    
    		return BT_GATT_ITER_STOP;
    	}

    As you can see, it looks for several characteristic UUIDs. This is because it has several things that it wants to discover. First, if this is the callback for the service, it will pass the test:

    if (!bt_uuid_cmp(discover_params.uuidBT_UUID_HRS)) {
    meaning that the service was discovered (the first bt_gatt_discover() was called with discover_params.type = BT_GATT_DISCOVER_PRIMARY.)
    Next it will try to discover the hrs characteristic, and when that is discovered in the next callback, it will try to discover the descriptor (CCC), which is used to enable and disable notifications. If you prefer this method, try to replace the UUIDs with the ones that you are looking for. First for the service, then the characteristic, and finally the descriptor that you want to enable notifications on.
    When the notifications are enabled, look how it is linked to the notification callback, "notify_func", which will be called whenever a notification is received. Then you can print the data that you received using printk() from the notification callback.
    Best regards,
    Edvin
  • Thanks a lot for such a detailed answer, I really appreciate it and it helps clear things up. I sort of understand the callback process for service discovery now, but there's still a couple of things I'm unsure about.

    If we continue one the central heart rate service example and look at the notify_func as you mentioned. First of all, what the peripheral_hr sends out is:

    static void hrs_notify(void)
    {
    	static u8_t heartrate = 90U;
    
    	/* Heartrate measurements simulation */
    	heartrate++;
    	if (heartrate == 160U) {
    		heartrate = 90U;
    	}
    
    	bt_gatt_hrs_notify(heartrate);
    }

    Which is the simulated heartrate that we expect. However, what is displayed by the central_hr is

    static u8_t notify_func(struct bt_conn *conn,
    			struct bt_gatt_subscribe_params *params,
    			const void *data, u16_t length)
    {
    	if (!data) {
    		printk("[UNSUBSCRIBED]\n");
    		params->value_handle = 0U;
    		return BT_GATT_ITER_STOP;
    	}
    
    	printk("[NOTIFICATION] data %p length %u,\t", data, length);
    
    	return BT_GATT_ITER_CONTINUE;
    }

    and corresponds to terminal output:

    Alright. So my understanding is what is printed is the address that stores the data. Trying to dereference it, however, gets me no further. The data was sent as u8_t, but dereferencing it as either u8_t or u16_t (shown below) gives me the following (shown further below):

    static u8_t notify_func(struct bt_conn *conn,
    			struct bt_gatt_subscribe_params *params,
    			const void *data, u16_t length)
    {
    	if (!data) {
    		printk("[UNSUBSCRIBED]\n");
    		params->value_handle = 0U;
    		return BT_GATT_ITER_STOP;
    	}
    
    	printk("[NOTIFICATION] data %p length %u,\t", data, length);
    	u8_t bleData = *(u8_t*)data;
    	//memcpy(&bleData, data, sizeof(bleData));
    	printk("bleData: %u\n", bleData);
    
    
    	return BT_GATT_ITER_CONTINUE;
    }

    Output for u8_t:

    Output for u16_t:

    What am I doing wrong?

    Looking forward once again to your answer.

  • Please check out the implementation in the Nordic example, foiund in NCS\nrf\samples\bluetooth\central_hr_coded, and it's notify_func:

    static uint8_t notify_func(struct bt_conn *conn,
    		struct bt_gatt_subscribe_params *params,
    		const void *data, uint16_t length)
    {
    	if (!data) {
    		printk("[UNSUBSCRIBED]\n");
    		params->value_handle = 0U;
    		return BT_GATT_ITER_STOP;
    	}
    
    	if (length == 2) {
    		uint8_t hr_bpm = ((uint8_t *)data)[1];
    
    		printk("[NOTIFICATION] Heart Rate %u bpm\n", hr_bpm);
    	} else {
    		printk("[NOTIFICATION] data %p length %u\n", data, length);
    	}
    
    	return BT_GATT_ITER_CONTINUE;
    }

    which prints:

    Filters matched. Address: ef:1d:3e:56:18:9a (random) connectable: yes
    Connection pending
    Connected: ef:1d:3e:56:18:9a (random), tx_phy 4, rx_phy 4
    The discovery procedure succeeded
    [SUBSCRIBED]
    [NOTIFICATION] Heart Rate 91 bpm
    [NOTIFICATION] Heart Rate 92 bpm
    [NOTIFICATION] Heart Rate 93 bpm
    [NOTIFICATION] Heart Rate 94 bpm
    [NOTIFICATION] Heart Rate 95 bpm
    [NOTIFICATION] Heart Rate 96 bpm
    [NOTIFICATION] Heart Rate 97 bpm
    [NOTIFICATION] Heart Rate 98 bpm
    [NOTIFICATION] Heart Rate 99 bpm
    [NOTIFICATION] Heart Rate 100 bpm
    

    So what you are looking for is the line:

    uint8_t hr_bpm = ((uint8_t *)data)[1];

    Without having checked, I see that data[0] is 6. I believe it is following the Bluetooth specification for the Heart Rate Service. I believe it is the location of the sensor, or some similar metadata. In fact, it is. Check out the implementation of int bt_hrs_notify(uint16_t heartrate) in hrs.c, which is used by the peripheral: NCS\nrf\samples\bluetooth\peripheral_hr_coded.

    int bt_hrs_notify(uint16_t heartrate)
    {
    	int rc;
    	static uint8_t hrm[2];
    
    	hrm[0] = 0x06; /* uint8, sensor contact */
    	hrm[1] = heartrate;
    
    	rc = bt_gatt_notify(NULL, &hrs_svc.attrs[1], &hrm, sizeof(hrm));
    
    	return rc == -ENOTCONN ? 0 : rc;
    }

    Best regards,

    Edvin

Related