How to subscribe/read a characteristic over BLE?

Hello! I'm trying to figure out how to program a nRF52832 to subscribe and read from a TX or custom characteristic on another nRF52832. 

My environment:

  • NRF Connect SDK in VSCode - Version 2.1.1
  • Zephyr v3.1.99

What I've done so far:

  • I've adapted the peripheral_uart example such that I can use nRF Connect app in iOS to connect to the peripheral BLE device, subscribe to the UART TX Characteristic, and see an incrementing integer in the Log every 3 seconds.
  • I've tested the central_uart example such that I can form a successful connection between the central nRF52832 device and the peripheral nRF52832 device I've described above. 

I can't seem to locate a tutorial on the procedure to subscribe to the TX characteristic or any custom characteristic for that matter. I've looked through some tutorials, such as:

For my code, I haven't really made many changes to either the peripheral_uart or central_uart base examples. 

My thread for writing the incrementing integer is as follows, and is part of the firmware on my peripheral device.

void ble_write_thread(void)
{
	/* Don't go any further until BLE is initialized */
	k_sem_take(&ble_init_ok, K_FOREVER);

	int i = 0;
	for (;;) {
		
		uint8_t testbuf[10];
		sprintf(testbuf, "%d", i);
		
		//send message stream on connection
		if (current_conn != 0){
			printk("\nMessage Sent!\n");
			bt_nus_send(NULL, testbuf, 10);
			i += 1;
			k_sleep(K_MSEC(3000));
		}
	}
}

I've looked into some of the functions found within Gatt Client API such as bt_gatt_subscribe() and bt_gatt_read(), but I'm having trouble getting them to work (if they're even the correct function to use). Is there any advice on how I should be polling and storing the output from the peripheral TX characteristic on my central device? It would also be great to know how to do the same for a custom characteristic, created in the same way as the tutorial I've linked above.

Thank you in advance!

  • Hi

    The central_uart should subscribe to the RX characteristic out of the box. Are you not able to get this to work? 

    If you look inside the discovery_complete(..) callback on line 293 of main.c you will notice it calls the bt_nus_subscribe_receive(..) function, the implementation of which can be found on line 172 of nus_client.c. 

    Here you can see how it calls bt_gatt_subscribe(..), after checking that notifications are not enabled already. It also sets a callback function called on_received which will be used to process incoming data from the connected peripheral. The on_received(..) callback function will in turn call the received callback, which triggers ble_data_received(..) in main.c. 

    If you want to make your own proprietary characteristic then a good way to do this is simply to copy the existing nus implementation, give it a different name (replacing all instance of "nus" or "NUS" with your own name), and then modifying it as you see fit. 

    You should also give it a different 128-bit UUID to ensure you don't get your device mixed up with all the devices using NUS. 

    Best regards
    Torbjørn

  • Hello Torbjørn,

    Thank you for such a quick reply! Upon messing around with my code a little bit, I ended up getting the TX characteristic to print out via Serial on the Central device, so the code does work as expected. I think there was an issue caused by having CONFIG_ASSERT=y on the peripheral prj.conf. 

    Your answer makes a lot of sense, and I took a look at the parts of the code you referenced, and the flow is more apparent to me now. One more thing that might be helpful for me is how to extract or store the TX characteristic data. I see in the nus_client.c file, the TX data is returned on line 44 by this snippet:

    if (nus->cb.received) {
    	return nus->cb.received(nus, data, length);
    }

    I'm having a bit of trouble tracing the value down in main(), but I suspect it may be somewhere near ble_data_received() and tx->data, but I haven't been fruitful in my attempts to track down where the data is being put. Any clarification would be appreciated!

  • Hi 

    The data is actually provided directly in the arguments of the ble_data_received function:

    static uint8_t ble_data_received(struct bt_nus_client *nus, const uint8_t *data, uint16_t len)

    The second argument points to the data buffer, and the third argument provides the length. 

    In theory you could delete more or less the entire implementation of this function, with the exception of the return call at the end, and just process the data in some other way. 

    (The standard example is not the best IMO as it uses dynamic memory allocation, and makes the flow of the code pretty hard to follow, and I have it on my todo list to try and provide a simpler example.)

    It is important to note that this callback function is running in an interrupt context, which means it is very limited what you are allowed to do in the callback directly. 

    The normal way to handle this is to move the data into a separate buffer, and set a flag or a semaphore that allows you to process the data from the thread context rather than in the interrupt. 

    Best regards
    Torbjørn

  • Hello!

    Once again, I appreciate the detailed answer! If there will be a simpler example in the future, I would definitely be interested in reading through it

    As for my original question, your answer worked for me! I originally tried printing the data you mentioned in your answer above, but I ended up getting weirdly interpreted values, as I was printing with a PRId8 type specifier. This would return 49 for 1, 50 for 2, etc. I changed the format specifier to a char, and the answer came as expected!

    static uint8_t ble_data_received(struct bt_nus_client *nus,
    								 const uint8_t *data, uint16_t len)
    {
        ...
            for (int i=0;i<len;i++){
        		printk("%c", data[i]);
        	}
        	printk("\n");
        }
    	
    	return BT_GATT_ITER_CONTINUE;

    I hope it seems that I've implemented this properly, but as you mentioned, there's likely a cleaner way to handle the data. 

    Anyways, thank you again for answering my questions!

  • Hi

    jksu said:
    This would return 49 for 1, 50 for 2, etc.

    Yes, 49 is the ASCII Code for '1', 50 for '2 and so on. 

    There is another trick to print non null terminated strings, as explained here
    I don't remember if I tested this with printk(..), but you could always give this a go. In the ble_data_received callback it should look something like this:

    printk("Received string: %*.s\n", len, data);

    Best regards
    Torbjørn

Related