prettyprint

2023年6月14日 星期三

[Raspberry Pi Pico W] BLE Ep 2. BTstack GATT server & GATT Client

 前一篇文章介紹透過BLE advertisement將溫濕度感應器的數值以Advertising packet傳送給附近的其他BLE browsers(scaners),不須經過connect即可獲得資料。

本篇文章介紹如何使用BTstack建立簡單的GATT server 與GATT client,將溫濕度透過ATT protocol傳送給其他設備。

一、ATT server

1. 首先要建立GATT profile: services & characteristics

(source: Bluetooth specification document)
在本範例中:建立三個primary server分別為
GAP_SERVICE、GATT_SERVICE與ORG_BLUETOOTH_SERVICE_ENVIRONMENTAL_SENSING
其分別包還的Characteristics如下圖所示:
將上述內容建立成gatt_ht_server.gatt,使用BTstack 工具compile_gatt.py將檔案轉換成相對應的文件規範格式。可將此編譯指令建立在CMakeLists.txt檔案中。
*註:pico SDK 1.5.1在CMake 已加入pico_btstack_make_gatt_header函數
可改成
pico_btstack_make_gatt_header(le_sm_peripheral PRIVATE "${CMAKE_CURRENT_LIST_DIR}/gatt_ht-server.gatt")

advertisement data只包含以下三個簡單的data type
設定att server的三個參數如下:
att_server_init(profile_data, att_read_callback, att_write_callback); 
(1) profile_data: 由上述compile_gatt.py產生的attribute database。
(2)att_read_callback: client read character呼叫的callback function。
(3)att_write_callbak: client write request呼叫的callback function。
設定timer_resource透過att_server_notify定時對client端發送notification。
詳細的程式碼附於文章末尾。

二、ATT Client:
1. scan advertisement、connect server、discovery service/characteristics、read/write characteristics and receive server notification流程如下圖所示。
相對應完整程式碼附於文章末尾

三、成果影片



四、程式碼
  • gatt_ht_server.c
#include <stdio.h>
#include "pico/stdlib.h"
#include "btstack.h"
#include "pico/cyw43_arch.h"
#include "dht11/dht11.h"
#include "gatt_ht_server.h"
#include "math.h"

#define HEARTBEAT_PERIOD_MS 3000

uint8_t adv_data[] = {
    // Flags: 0x02: General Discoverable Mode, 0x04:BR/EDR Not Supported
    0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06,
    // Local Name
    0x09, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, 'P', 'i', 'c', 'o', 'W', '-', 'H','T', 
    0x03, BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, 0x1a, 0x18,
};

uint8_t adv_data_len = sizeof(adv_data);

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static uint16_t att_read_callback(hci_con_handle_t con_handle, uint16_t att_handle, uint16_t offset, uint8_t * buffer, uint16_t buffer_size);
static int att_write_callback(hci_con_handle_t con_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size);
float temp;
float humi;
uint16_t temp_i, humi_i;
bool le_notification_enabled;
hci_con_handle_t con_handle;
static btstack_packet_callback_registration_t hci_event_callback_registration;
static btstack_timer_source_t temp_humi_noti;


static uint16_t att_read_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t offset, uint8_t * buffer, uint16_t buffer_size){
   
    if (att_handle == ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE_01_VALUE_HANDLE){      
            att_server_request_can_send_now_event(connection_handle);
            return att_read_callback_handle_little_endian_16(temp_i, offset, buffer, buffer_size);

    }
    if (att_handle == ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_HUMIDITY_01_VALUE_HANDLE){          
            uint16_t ret = att_read_callback_handle_little_endian_16(humi_i, offset, buffer, buffer_size);
            att_server_request_can_send_now_event(connection_handle);
            return ret;
    }
    
    return 0;
}


static int att_write_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size){
    UNUSED(transaction_mode);
    UNUSED(offset);
    UNUSED(buffer_size);
 
    if (att_handle == ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE_01_CLIENT_CONFIGURATION_HANDLE
        || att_handle == ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_HUMIDITY_01_CLIENT_CONFIGURATION_HANDLE) {
            le_notification_enabled = little_endian_read_16(buffer, 0) == GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION;
            con_handle = connection_handle;
        }
    return 0;
}

static void temp_humi_handle(struct btstack_timer_source *ts){

    if (le_notification_enabled) {

        if (dht11_get_data(&temp, &humi)) {
            temp_i=(uint16_t)round(temp*100);
            humi_i=(uint16_t)round(humi*100);
        printf("temp:%f, humi:%f\n", temp, humi);
            att_server_request_can_send_now_event(con_handle);
        }
    }

    btstack_run_loop_set_timer(ts, HEARTBEAT_PERIOD_MS);
    btstack_run_loop_add_timer(ts);
} 

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
    UNUSED(channel);
    UNUSED(size);

    if (packet_type != HCI_EVENT_PACKET) return;
    switch (hci_event_packet_get_type(packet)) {
        case HCI_EVENT_DISCONNECTION_COMPLETE:
            le_notification_enabled = 0;
            break;
        case ATT_EVENT_CAN_SEND_NOW:       
            att_server_notify(con_handle, ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE_01_VALUE_HANDLE, 
                        (uint8_t*)&temp_i, 2);
            att_server_notify(con_handle, ATT_CHARACTERISTIC_ORG_BLUETOOTH_CHARACTERISTIC_HUMIDITY_01_VALUE_HANDLE, 
                        (uint8_t*)&humi_i, 2);           
            break;
        default:
            break;
    }
}

int main()
{
    stdio_init_all();
    if (cyw43_arch_init()) {
        printf("cyw43_init error\n");
        return 0;
    }
    
    l2cap_init();
    sm_init();

    dht11_init();


    // setup advertisements
    uint16_t adv_int_min = 0x0030;
    uint16_t adv_int_max = 0x0030;
    uint8_t adv_type = 0;
    bd_addr_t null_addr;
    memset(null_addr, 0, 6);
    gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
    gap_advertisements_set_data(adv_data_len, (uint8_t*) adv_data);
    gap_advertisements_enable(1);

    // setup ATT server
    att_server_init(profile_data, att_read_callback, att_write_callback); 

    // register for HCI events
    hci_event_callback_registration.callback = &packet_handler;
    hci_add_event_handler(&hci_event_callback_registration);

    // register for ATT event
    att_server_register_packet_handler(packet_handler);

    // set  timer
    temp_humi_noti.process = &temp_humi_handle;
    btstack_run_loop_set_timer(&temp_humi_noti, HEARTBEAT_PERIOD_MS);
    btstack_run_loop_add_timer(&temp_humi_noti);
    

    hci_power_control(HCI_POWER_ON);

    while(1) {
        tight_loop_contents();
    }

    return 0;
}
  • gatt_ht_server.gatt
 PRIMARY_SERVICE, GAP_SERVICE
CHARACTERISTIC, GAP_DEVICE_NAME, READ, "PicoW-HT"

PRIMARY_SERVICE, GATT_SERVICE
CHARACTERISTIC, GATT_DATABASE_HASH, READ,

// Environment
PRIMARY_SERVICE, ORG_BLUETOOTH_SERVICE_ENVIRONMENTAL_SENSING
// Temperature Characteristic, with read and notify
CHARACTERISTIC,  ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE, READ | NOTIFY | DYNAMIC,
// Humidity Characteristic, with read and notify
CHARACTERISTIC,  ORG_BLUETOOTH_CHARACTERISTIC_HUMIDITY, READ | NOTIFY | DYNAMIC,
  • CMakeLists.txt(server)
 execute_process(COMMAND
          /home/duser/pico/pico-sdk/lib/btstack/tool/compile_gatt.py gatt_ht_server.gatt gatt_ht_server.h
          WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
          ECHO_OUTPUT_VARIABLE
          ECHO_ERROR_VARIABLE
        )
# Generated Cmake Pico project file

cmake_minimum_required(VERSION 3.13)

set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

# Initialise pico_sdk from installed location
# (note this can come from environment, CMake cache etc)
set(PICO_SDK_PATH "/home/duser/pico/pico-sdk")

set(PICO_BOARD pico_w CACHE STRING "Board type")

# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)

if (PICO_SDK_VERSION_STRING VERSION_LESS "1.4.0")
  message(FATAL_ERROR "Raspberry Pi Pico SDK version 1.4.0 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}")
endif()

project(gatt_ht_server C CXX ASM)

# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()

# Add executable. Default name is the project name, version 0.1

add_executable(gatt_ht_server gatt_ht_server.c )

pico_set_program_name(gatt_ht_server "gatt_ht_server")
pico_set_program_version(gatt_ht_server "0.1")

pico_enable_stdio_uart(gatt_ht_server 1)
pico_enable_stdio_usb(gatt_ht_server 0)

# Add the standard library to the build
target_link_libraries(gatt_ht_server
        pico_stdlib
        pico_cyw43_arch_none
        pico_btstack_cyw43
        pico_btstack_ble)

# Add the standard include files to the build
target_include_directories(gatt_ht_server PRIVATE
  ${CMAKE_CURRENT_LIST_DIR}
  ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required
)

add_subdirectory(dht11)
target_link_libraries(gatt_ht_server
        dht11)
pico_add_extra_outputs(gatt_ht_server)

  • gatt_ht_client.c
#include <stdio.h>
#include "pico/stdlib.h"
#include "btstack.h"
#include "pico/cyw43_arch.h"
#include "pico_tft.h"
#include "fonts/font_fixedsys_mono_16.h"
#include "tft_string/tft_string.h"

#define SERVER_DEVICE_NAME "PicoW-HT"
typedef enum {
    CLIENT_STOP=0,
    QUERY_SERVICE,
    QUERY_CHARACTERISTIC_TEMPERATURE,
    QUERY_CHARACTERISTIC_HUMIDITY,
    ENABLE_NOTIFICATION,
    ENABLE_COMPLETE
} client_event_query_t;

float rtemp, rhumi, ntemp, nhumi;

client_event_query_t client_event_query;

gatt_client_notification_t notification_listener_temp, notification_listener_humi;
static hci_con_handle_t connection_handler;
gatt_client_service_t environmental_sensing;
gatt_client_characteristic_t temperature, humidity;
static btstack_packet_callback_registration_t hci_event_callback_registration;

static void handle_hci_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void tft_show_message(uint8_t *msg);
static void tft_print_data();

uint8_t  advertisement_report_find_device(uint16_t uuid16, uint8_t * name, uint8_t * advertisement_report){
    // get advertisement from report event
    const uint8_t * adv_data = gap_event_advertising_report_get_data(advertisement_report);
    uint8_t         adv_len  = gap_event_advertising_report_get_data_length(advertisement_report);

    // iterate over advertisement data
    ad_context_t context;

    uint8_t local_name[40];
    uint8_t found = 0;
    for (ad_iterator_init(&context, adv_len, adv_data) ; ad_iterator_has_more(&context) ; ad_iterator_next(&context)){
        uint8_t data_type    = ad_iterator_get_data_type(&context);
        uint8_t data_size    = ad_iterator_get_data_len(&context);
        const uint8_t * data = ad_iterator_get_data(&context);
        int i;
        switch (data_type){
            case BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS:
                for (i=0; i<data_size;i+=2){
                    if (little_endian_read_16(data, i) == uuid16) {
                        found = found | 0x1;
                        break;
                    }
                }
                break;
            case BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME:
                memcpy(local_name, data, data_size);
                local_name[data_size]='\0';
                if (strcmp(local_name, name) == 0) {
                    found = found | 0x2;
                }
            break;
            default:
                break;
        }
    }
    return found;
}

static void handle_hci_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
    UNUSED(channel);
    UNUSED(size);
    bd_addr_t addr;
    bd_addr_type_t addr_type; 
    if (packet_type != HCI_EVENT_PACKET) return;
    
    uint8_t event = hci_event_packet_get_type(packet);
    switch (event) {
        case BTSTACK_EVENT_STATE:
            // BTstack activated, get started
            if (btstack_event_state_get_state(packet) != HCI_STATE_WORKING) break;
            gap_set_scan_parameters(0,0x0030, 0x0030);
            gap_start_scan();
            break;
        case GAP_EVENT_ADVERTISING_REPORT:
            tft_show_message("Scan Device...");
            if (advertisement_report_find_device(ORG_BLUETOOTH_SERVICE_ENVIRONMENTAL_SENSING, SERVER_DEVICE_NAME, packet)==0x3) {
                gap_event_advertising_report_get_address(packet, addr);
                addr_type = gap_event_advertising_report_get_address_type(packet);
                gap_stop_scan();
                gap_connect(addr, addr_type);
            }
            break;
        case HCI_EVENT_LE_META:
            // wait for connection complete
            if (hci_event_le_meta_get_subevent_code(packet) !=  HCI_SUBEVENT_LE_CONNECTION_COMPLETE) break;
            connection_handler = hci_subevent_le_connection_complete_get_connection_handle(packet);
            // query primary services
            client_event_query=QUERY_SERVICE;
            gatt_client_discover_primary_services_by_uuid16(handle_gatt_client_event, connection_handler,
                        ORG_BLUETOOTH_SERVICE_ENVIRONMENTAL_SENSING);
            break;
        case HCI_EVENT_DISCONNECTION_COMPLETE:
            tft_show_message("Device Disconn");
            client_event_query = CLIENT_STOP;
            gap_start_scan();
            break;
        default:
            break;
    }
}
static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
    UNUSED(packet_type);
    UNUSED(channel);
    UNUSED(size);
    uint16_t temp;

    switch(hci_event_packet_get_type(packet)){
        case GATT_EVENT_SERVICE_QUERY_RESULT:

            gatt_event_service_query_result_get_service(packet, &environmental_sensing);
        break;
        case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT:

            if (client_event_query == QUERY_CHARACTERISTIC_TEMPERATURE) { 

                gatt_event_characteristic_query_result_get_characteristic(packet, &temperature);

            }
            if (client_event_query == QUERY_CHARACTERISTIC_HUMIDITY) { 

                gatt_event_characteristic_query_result_get_characteristic(packet, &humidity);

            }
        break;
        case GATT_EVENT_QUERY_COMPLETE:

            switch(client_event_query) {
                case QUERY_SERVICE:
                    client_event_query = QUERY_CHARACTERISTIC_TEMPERATURE;
                    gatt_client_discover_characteristics_for_service_by_uuid16(handle_gatt_client_event, connection_handler, 
                        &environmental_sensing, ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE);                
                break;
                case QUERY_CHARACTERISTIC_HUMIDITY:
                    client_event_query = ENABLE_NOTIFICATION;
                    gatt_client_listen_for_characteristic_value_updates(&notification_listener_temp, handle_gatt_client_event, 
                        connection_handler, &temperature);
                    gatt_client_listen_for_characteristic_value_updates(&notification_listener_humi, handle_gatt_client_event, 
                       connection_handler, &humidity);
                    // enable notifications
                    gatt_client_write_client_characteristic_configuration(handle_gatt_client_event, connection_handler,
                        &temperature, GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION);
                    //gatt_client_write_client_characteristic_configuration(handle_gatt_client_event, connection_handler,
                     //   &humidity, GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION);

                break;
                case QUERY_CHARACTERISTIC_TEMPERATURE:
                    client_event_query = QUERY_CHARACTERISTIC_HUMIDITY;
                    gatt_client_discover_characteristics_for_service_by_uuid16(handle_gatt_client_event, connection_handler, 
                        &environmental_sensing,ORG_BLUETOOTH_CHARACTERISTIC_HUMIDITY);
                break;
                case ENABLE_NOTIFICATION:
                client_event_query = ENABLE_COMPLETE;
                            
                break;
            }
            break;
            case GATT_EVENT_CHARACTERISTIC_VALUE_QUERY_RESULT:
                if (humidity.value_handle == gatt_event_characteristic_value_query_result_get_value_handle(packet)) { 
                    temp = little_endian_read_16(gatt_event_characteristic_value_query_result_get_value(packet), 0);
                    rhumi = temp/100.0;
                    tft_print_data();
                    printf("humidity read:%.02f%c\n", rhumi, '%');
                }
                if (temperature.value_handle == gatt_event_characteristic_value_query_result_get_value_handle(packet)) { 
                    temp = little_endian_read_16(gatt_event_characteristic_value_query_result_get_value(packet), 0);
                    rtemp = temp/100.0;
                    tft_print_data();
                    printf("temperature read:%.02f C\n", rtemp);
                }

                 
            break;
            case GATT_EVENT_NOTIFICATION:
                if (temperature.value_handle == gatt_event_notification_get_value_handle(packet)) { 
                    ntemp = little_endian_read_16(gatt_event_notification_get_value(packet), 0)/100.0;
                    tft_print_data();
                    printf("temp:%.02f C\n", ntemp); 
                }
                if (humidity.value_handle == gatt_event_notification_get_value_handle(packet)) { 
                    nhumi = little_endian_read_16(gatt_event_notification_get_value(packet), 0)/100.0;
                    tft_print_data();
                    printf("humi:%.02f %c\n", nhumi, '%');
                }
            break;
            case ATT_EVENT_CAN_SEND_NOW:
            printf("att send\n");
            break;
        default:
            break;
    }
}

void tft_show_message(uint8_t* msg) {
    tft_fill_rect(0,0,TFT_WIDTH, TFT_HEIGHT, 0xffff);
    tft_draw_string(20,5, "ATT Read:", 0xf800, &font_fixedsys_mono_16);
}
void tft_print_data() {
    char ts[50];
    tft_fill_rect(0,0,TFT_WIDTH, TFT_HEIGHT, 0xffff);
    tft_draw_string(20,5, "ATT Read:", 0xf800, &font_fixedsys_mono_16);
    sprintf(ts, "T:%.02f C, H:%.0f%c", rtemp, rhumi, '%');
    tft_draw_string(10, 30, ts, 0x001f, &font_fixedsys_mono_16);
    tft_draw_string(10,70, "ATT Notification:", 0xf800, &font_fixedsys_mono_16);
    sprintf(ts, "T:%.02f C, H:%.0f%c", ntemp, nhumi, '%');
    tft_draw_string(10, 95, ts, 0x001f, &font_fixedsys_mono_16);
}

int main()
{
    stdio_init_all();
    tft_init();
    
    if (cyw43_arch_init()) {
        printf("cyw43_init error\n");
        return 0;
    }
    client_event_query = CLIENT_STOP;
    
    l2cap_init();
    sm_init();

      // setup GATT client
    l2cap_init();

    // Initialize GATT client 
    gatt_client_init();

    // Optinoally, Setup security manager
    sm_init();
    sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);

    // register for HCI events
    hci_event_callback_registration.callback = &handle_hci_event;
    hci_add_event_handler(&hci_event_callback_registration);


    hci_power_control(HCI_POWER_ON);
    while(1) {
        if (client_event_query == ENABLE_COMPLETE) {
            sleep_ms(4500);
            gatt_client_read_value_of_characteristics_by_uuid16(handle_gatt_client_event, connection_handler, temperature.start_handle, temperature.end_handle, ORG_BLUETOOTH_CHARACTERISTIC_TEMPERATURE);
            sleep_ms(100);
            gatt_client_read_value_of_characteristics_by_uuid16(handle_gatt_client_event, connection_handler, humidity.start_handle, humidity.end_handle, ORG_BLUETOOTH_CHARACTERISTIC_HUMIDITY);
        }
        tight_loop_contents();
    }
    

    return 0;
}
  • gatt_ht_client.gatt
PRIMARY_SERVICE, GAP_SERVICE
CHARACTERISTIC, GAP_DEVICE_NAME, READ, "GATT HT Client"





沒有留言:

張貼留言