前一篇文章介紹透過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_btstack_make_gatt_header(le_sm_peripheral PRIVATE "${CMAKE_CURRENT_LIST_DIR}/gatt_ht-server.gatt")
advertisement data只包含以下三個簡單的data type
(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(¬ification_listener_temp, handle_gatt_client_event,
connection_handler, &temperature);
gatt_client_listen_for_characteristic_value_updates(¬ification_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"
沒有留言:
張貼留言