Discovery in nRF Connect/Zephyr as *discovery*

Hi, I'm new to nRF Connect, coming from the SDK. I'm building a Central/Peripheral system and I've got to the point where the Central is discovering what services and attributes the Peripheral provides.

The only Zephyr samples in which discovery is being performed is central_hr and central_ht. In both of these, the uuid of the bt_gatt_discover_params is being loaded with the UUID of the service and characteristic that it is looking for. This isn't really *discovery*, not like, say, the nRF Connect tool which upon connecting to an advertising device figures out the service UUID and the characteristic UUIDs without prior knowledge. Now, _that_ is *discovery*. But I figured out that the bt_gatt_discover() subsystem can be used as *discovery* by supplying NULL to the uuid member of bt_gatt_discover_params.

When I do this, the bt_gatt_discover_func_t callback gets called with a bt_gatt_attr argument that includes uuid and a user_data members. The table under the bt_gatt_discover_func_t description indicates what user_data is going to be.

So, I start with looking for what services are offered by my device (there's only one). Call bt_gatt_discover() with the uuid pointer of bt_gatt_discover_params set to NULL and type set to BT_GATT_DISCOVER_PRIMARY. When the callback is called, attr->uuid is a 16-bit UUID with value 0x2800, or BT_UUID_GATT_PRIMARY. Good. Per the table, user_data is a bt_gatt_service_val, and the uuid member of user_data is a 16-bit UUID with value 0x1801, or BT_UUID_GATT. This is true, but unhelpful. I was expecting the UUID of the service. This seems wrong.

Then I look for the characteristics. Call bt_gatt_discover() with the uuid pointer of bt_gatt_discover_params set to NULL and type set to BT_GATT_DISCOVER_CHARACTERISTIC. When the callback is called, attr->uuid is a 16-bit UUID with value 0x2803, or BT_UUID_GATT_CHRC. Good. Per the table, user_data is a bt_gatt_chrc, and the uuid member of user_data is the UUID I assigned to my characteristics. This is the expected behavior. Good.

Then I look for the CCCDs. Call bt_gatt_discover() with the uuid pointer of bt_gatt_discover_params set to NULL and type set to BT_GATT_DISCOVER_STD_CHAR_DESC. This results in bt_gatt_discover() returning a EINVAL error. Even though this type is in the bt_gatt_discover_func_t description. This also seems wrong.  Okay, then I'll follow the central_ht sample and use BT_GATT_DISCOVER_DESCRIPTOR. No error, and the callback function gets called. Per the table, user_data is supposed to be NULL, so I don't bother to look at it. Instead, attr->uuid contains the UUID of the custom service characteristic that "owns" this CCCD. Unexpected, undocumented, but I can go with that.

I'm not at a disadvantage with not getting the service UUID out of discovery, since the provided services are supposed to be in the advertising data, but I would have expected discovery to return the service UUID all the same. Is this a bug, or expected behavior?

  • Answer to my own question.

    If I continue doing bt_gatt_discover() calls, the first BT_UUID_GATT_PRIMARY object has a UUID of 0x1801 ( BT_UUID_GATT).  After the next call, I get a BT_UUID_GATT_PRIMARY with UUID of 0x1800 (BT_UUID_GAP).  After the next call, I get a BT_UUID_GATT_PRIMARY with the UUID of my custom service.

    So it is in fact there.

    Another interesting thing is, my custom service has one Notify characteristic with an associated CCCD.  The first bt_gatt_discover() looking for BT_GATT_DISCOVER_DESCRIPTOR, I get an attr->uuid which is the UUID of this Notify characteristic.  After the next call, I get a 16-bit UUID with value 0x2902, which is BT_UUID_GATT_CCC.  These two return episodes have different handles, and I'm thinking the first one is the one to use to subscribe to the notification, because that's what central_ht does.

    My working code now looks like:

    enum DISCOVER_PHASE {
        DISC_PHASE_SERVICE,        /* Looking for Service UUIDs */
        DISC_PHASE_CHRC,           /* Looking for Characteristic UUIDs */
        DISC_PHASE_CCCD,           /* Looking for CCCDs */
    };
    typedef enum DISCOVER_PHASE discover_phase_t;
    /* Phase of attribute discovery process */
    static discover_phase_t discover_phase;
    static uint16_t cccd_handle;   /* Handle to Notify CCCD for Report */
    static uint16_t ctrl_handle;   /* Handle for Control Characteristic */
    static uint16_t rpt_handle;    /* Handle for Report Characteristic */
    .
    .
    .
    static uint8_t discover_func(struct bt_conn *conn,
    			     const struct bt_gatt_attr *attr,
    			     struct bt_gatt_discover_params *params)
    {
        static uint16_t last_handle;
    	int err;
    
        /* @@@ */
        if(attr) {
            printk("[ATTRIBUTE] handle %u\n", attr->handle);
    
            printk("uuid type = %d, val=%x\n", attr->uuid->type, BT_UUID_16(attr->uuid)->val);
        }
        /* @@@ */
    
        switch(discover_phase) {
            case DISC_PHASE_SERVICE:
                if(attr) {
                    /* Getting here, attr->uuid will be BT_UUID_GATT_PRIMARY */
                    struct bt_gatt_service_val *svc_attr = attr->user_data;
                    printk("attr is a primary service.  UUID type is %d, val = %x\n",
                            svc_attr->uuid->type, BT_UUID_16(svc_attr->uuid)->val);
                    if(!bt_uuid_cmp(svc_attr->uuid, &cis_svc_uuid.uuid)) {
                        printk("Found custom service\n");
                    }
                    last_handle = attr->handle;
                }
                else {
                    /* No more service UUIDs.  Look for Characteristics. */
                    printk("No more services, looking for characteristics\n");
                    discover_params.uuid = NULL;
                    discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;
                    discover_params.start_handle = last_handle + 1;
    
                    err = bt_gatt_discover(conn, &discover_params);
                    if (err) {
                        printk("Discover failed - svc (err %d)\n", err);
                    }
                    discover_phase = DISC_PHASE_CHRC;
                    return BT_GATT_ITER_STOP;
                }
    
                break;
    
            case DISC_PHASE_CHRC:
                if(attr) {
                    /* Getting here, attr->uuid will be BT_UUID_GATT_CHRC */
                    struct bt_gatt_chrc *chrc_attr = attr->user_data;
                    printk("attr is a characteristic.  UUID type is %d, val = %x\n",
                            chrc_attr->uuid->type, BT_UUID_16(chrc_attr->uuid)->val);
                    if(!bt_uuid_cmp(chrc_attr->uuid, &cis_control_char_uuid.uuid)) {
                        printk("Found Control characteristic\n");
                        ctrl_handle = attr->handle;
                    }
                    if(!bt_uuid_cmp(chrc_attr->uuid, &cis_report_char_uuid.uuid)) {
                        printk("Found Report characteristic\n");
                        rpt_handle = attr->handle;
                    }
                    last_handle = attr->handle;
                }
                else {
                    /* Ran out of characteristics, look for CCCDs */
                    printk("No more characteristics, looking for CCCDs\n");
                    discover_params.type = BT_GATT_DISCOVER_DESCRIPTOR;
            		discover_params.start_handle = attr->handle + 1;
                    discover_params.start_handle = last_handle + 1;
    
                    err = bt_gatt_discover(conn, &discover_params);
                    if (err) {
                        printk("Discover failed - chrc (err %d)\n", err);
                    }
                    discover_phase = DISC_PHASE_CCCD;
                    return BT_GATT_ITER_STOP;
                }
                break;
    
            case DISC_PHASE_CCCD:
                if(attr) {
                    printk("attr is a CCCD.  UUID type is %d, val = %x\n",
                            attr->uuid->type, BT_UUID_16(attr->uuid)->val);
                    if(attr->user_data == NULL) {
                        printk("user_data is NULL\n");
                    }
                    else {
                        struct bt_gatt_ccc *dope = attr->user_data;
                        printk("user data is %x\n", dope->flags);
                    }
                    /* Check if this CCCD is associated with Report charact */
                    if(!bt_uuid_cmp(attr->uuid, &cis_report_char_uuid.uuid)) {
                        printk("Found CCCD for Report characteristic\n");
                        cccd_handle = attr->handle;
                    }
                    last_handle = attr->handle;
                }
                else {
                    printk("Discovery complete\n");
                    /* If all the handles have been set, then all the expected
                     * attributes were found, and the Sensor is ready for duty */
                    if(ctrl_handle && rpt_handle && cccd_handle) {
                        printk("Sensor ready\n");
                        is_ready = true;
                    }
                    else {
                        printk("Sensor not found\n");
                        is_ready = false;
                    }
                    when_sensor_ready();
                }
                break;
        }
    
      	return BT_GATT_ITER_CONTINUE;
    }
    
    int start_discovery(struct bt_conn *conn)
    {
        int err;
    
    	if (conn == m_conn) {
    		discover_params.uuid = NULL;
    		discover_params.func = discover_func;
    		discover_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE;
    		discover_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
    		discover_params.type = BT_GATT_DISCOVER_PRIMARY;
            discover_phase = DISC_PHASE_SERVICE;
            ctrl_handle = 0;
            rpt_handle  = 0;
            cccd_handle = 0;
    
    		err = bt_gatt_discover(m_conn, &discover_params);
    		if (err) {
    			printk("Discover failed - service (err %d)\n", err);
    			return err;
    		}
    	}
        return 0;
    }
    

    The output looks like:

    [ATTRIBUTE] handle 1
    uuid type = 0, val=2800
    attr is a primary service.  UUID type is 0, val = 1801
    [ATTRIBUTE] handle 9
    uuid type = 0, val=2800
    attr is a primary service.  UUID type is 0, val = 1800
    [ATTRIBUTE] handle 16
    uuid type = 0, val=2800
    attr is a primary service.  UUID type is 2, val = 1200
    Found custom service
    No more services, looking for characteristics
    [ATTRIBUTE] handle 17
    uuid type = 0, val=2803
    attr is a characteristic.  UUID type is 0, val = 1531
    Found Control characteristic
    [ATTRIBUTE] handle 19
    uuid type = 0, val=2803
    attr is a characteristic.  UUID type is 0, val = 1532
    Found Report characteristic
    No more characteristics, looking for CCCDs
    [ATTRIBUTE] handle 20
    uuid type = 0, val=1532
    attr is a CCCD.  UUID type is 0, val = 1532
    user_data is NULL
    Found CCCD for Report characteristic
    [ATTRIBUTE] handle 21
    uuid type = 0, val=2902
    attr is a CCCD.  UUID type is 0, val = 2902
    user_data is NULL
    Discovery complete
    Sensor ready

  • This was my exact problem with the discover function. I am also new to nRF Connect and I've been trying to figure out how to get this done. You have saved me a lot of time my friend. thank you. Slight smile

Related