How to implement a write handler in nRF Connect SDK?

Hi,

Perhaps this question is too basic, but I haven't been able to find this anywhere.

I've followed this tutorial successfully with an nRF52832 DK:  nRF Connect SDK Bluetooth Low Energy tutorial part 1: Custom Service in Peripheral role

However, I'm missing the part about how to implement a peripheral write handler. In other words, how to make writing to a characteristic operate on a uC peripheral; something as simple as turning on/off a GPIO by writing 1/0 to a characteristic.

In this tutorial (using the old nRF SDK) it is clear how to implement this write handler and initialize it: https://novelbits.io/smart-ble-lightbulb-application-nrf52/. The function in question is the "static void led_write_handler" function that receives a led state uint8_t, and is able to write to an LED. Furthermore, it also shows how to declare a function pointer to it within structure linked to the service and how to initialize it from the main.c

My question is: how can I implement such a function and "register" it with the service? If there's a tutorial that shows how to do it, I'd greatly appreciate it.

Thanks in advance for your help



Parents
  • Hello,

    In the guide you refer to, look at Step 4, where you create your services and characteristics:

    /* LED Button Service Declaration and Registration */
    BT_GATT_SERVICE_DEFINE(my_service,
    BT_GATT_PRIMARY_SERVICE(BT_UUID_MY_SERIVCE),
    BT_GATT_CHARACTERISTIC(BT_UUID_MY_SERIVCE_RX,
    			       BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP,
    			       BT_GATT_PERM_READ | BT_GATT_PERM_WRITE, 
                       NULL, on_receive, NULL),
    BT_GATT_CHARACTERISTIC(BT_UUID_MY_SERIVCE_TX,
    			       BT_GATT_CHRC_NOTIFY,
    			       BT_GATT_PERM_READ,
                       NULL, NULL, NULL),
    BT_GATT_CCC(lbslc_ccc_cfg_changed,
            BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
    );

    On line 7 in that snippet, there is a parameter: "on_receive", which is a function pointer to the function that is triggered when the connected device writes to that characteristic.It can be implemented the way it is in the snippet right below that:

    /* This function is called whenever the RX Characteristic has been written to by a Client */
    static ssize_t on_receive(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 * buffer = buf;
        
    	printk("Received data, handle %d, conn %p, data: 0x", attr->handle, conn);
        for(u8_t i = 0; i < len; i++){
            printk("%02X", buffer[i]);
        }
        printk("\n");
    
    	return len;
    }

    So that function will be triggered whenever a connected device writes to the characteristic. Is that what you were looking for? Or do you need/want the callback to be in your main.c function? In that case, I happen to have a github repository that I used for a course at some point, where I did something similar, except that we forward the interrupt to a callback function in main.c. Feel free to have a look:

    https://github.com/edvinand/OmegaV_BLE_Course/tree/main/remote_controller/src

    Look at how I forwarded custom callback functions (remote_callbacks) in to the bluetooth_init() function in main(), and how these are used inside the remote.c file, inside the on_write() callback. In that case, you can use the callback function in main.c to toggle an LED or GPIO.

    Best regards,

    Edvin

  • Hi, thanks for your answer. The 2nd part, in particular. I finally find the time to go over it.

    If I understand correctly, you do the following:

    1) Create a structure of your service callbacks in your main.c

    2) In one of them (on_data_received) you're actually able to receive data to operate on a motor duty cycle, apparently.

    3) In your service .h file, you declare and structure with function prototypes to implement your callbacks

    4) In your service implementation, you log the received information (at least for on_write()). You also use a remote service callbacks structure to execute operate on your peripheral. In order to use them, you implement the bluetooth_init() function that will assign the structure and callbacks you created in the main.c to your service structure.

    Is that correct? If so, I only have one last doubt.

    On the on_write() function in your remote.c file, you do the following line:

        if (remote_service_callbacks.data_received) {
            remote_service_callbacks.data_received(conn, buf, len);
        }

    Why is the if case necessary? As far as I see, your data_received has a void return type, not a bool or an int, so how come will this if-statement return a 1 in order to execute your data_received() function.

    Perhaps my C knowledge isn't deep enough.

    If you can answer that, then I'll mark your answer as the accepted answer :)

  • builder01 said:

    1) Create a structure of your service callbacks in your main.c

    2) In one of them (on_data_received) you're actually able to receive data to operate on a motor duty cycle, apparently.

    3) In your service .h file, you declare and structure with function prototypes to implement your callbacks

    4) In your service implementation, you log the received information (at least for on_write()). You also use a remote service callbacks structure to execute operate on your peripheral. In order to use them, you implement the bluetooth_init() function that will assign the structure and callbacks you created in the main.c to your service structure.

    Yes. Pretty much. Not everything is strictly necessary, but it is good to see how you can pass events back and forth between files. Particularly trigger a callback function in main, even though "main.h" (if it existed) is not included from remote.c.

    builder01 said:
    Why is the if case necessary? As far as I see, your data_received has a void return type, not a bool or an int, so how come will this if-statement return a 1 in order to execute your data_received() function.

    It is correct that it is a void function. But when I call:

        if (remote_service_callbacks.data_received) {

    The remote_service_callbacks.data_received() function is not actually called. It could also be written as this:

        if (remote_service_callbacks.data_received != NULL) {

    Which may be more descriptive. It is just checking that the function is actually populated. If a function pointer is NULL, and you call it, it will cause a hardfault. So we just want to ensure that this is not the case.

    Best regards,

    Edvin

Reply
  • builder01 said:

    1) Create a structure of your service callbacks in your main.c

    2) In one of them (on_data_received) you're actually able to receive data to operate on a motor duty cycle, apparently.

    3) In your service .h file, you declare and structure with function prototypes to implement your callbacks

    4) In your service implementation, you log the received information (at least for on_write()). You also use a remote service callbacks structure to execute operate on your peripheral. In order to use them, you implement the bluetooth_init() function that will assign the structure and callbacks you created in the main.c to your service structure.

    Yes. Pretty much. Not everything is strictly necessary, but it is good to see how you can pass events back and forth between files. Particularly trigger a callback function in main, even though "main.h" (if it existed) is not included from remote.c.

    builder01 said:
    Why is the if case necessary? As far as I see, your data_received has a void return type, not a bool or an int, so how come will this if-statement return a 1 in order to execute your data_received() function.

    It is correct that it is a void function. But when I call:

        if (remote_service_callbacks.data_received) {

    The remote_service_callbacks.data_received() function is not actually called. It could also be written as this:

        if (remote_service_callbacks.data_received != NULL) {

    Which may be more descriptive. It is just checking that the function is actually populated. If a function pointer is NULL, and you call it, it will cause a hardfault. So we just want to ensure that this is not the case.

    Best regards,

    Edvin

Children
No Data
Related