В первой части статьи мы настраивали средства разработки, пытались разобраться с тем, как устроен код, как и чем его отлаживать, но так и не написали ни единой строки кода. Исправим это во второй части. Напишем собственный BLE профиль для CC2541.
Предположим, нам для нашего устройства нужен профиль, который будет поддерживать следующие функции:
— установка и чтение значений двух каналов ШИМ (от 0 до 255),
— получение состояния нажатия двух кнопок,
— чтение состояния одной из этих кнопок.
По факту нам понадобятся четыре характеристики профиля — две отвечающие за состояние ШИМ каналов и две нотифицирующие состояние кнопок.
2. UUID сервиса и характеристик
Как говорилось в предыдущей части, 16-байтные адреса сервисов простой смертный использовать не может. Для этого как минимум нужно быть Associated Member Bluetooth SIG. В общем, для нас выделены 128-битные UUID сервисов. Для генерации UUID нашего сервиса воспользуемся вот этим сервисом.
Нам потребуется пять UUID — четыре для характеристик и один для сервиса. Чтобы UUID шли по порядку, выбираем алгоритм «Time/node based». После генерации получаем примерно такой набор UUID:
Теперь все готово для того, чтобы начать писать код профиля. Обзовем наш профиль «HabrProfile» и добавим его в проект «SimpleBLEPerepherial».
3. Создание заголовочного файла
В папке \Projects\Profiles стека создадим папку HabrProfile, а в ней файлы habrprofile.h и habrprofile.c
Дальше добавим файлы в проект SimpleBLEPerepherial в папку PROFILES.
Заголовочный файл должен содержать:
-UUID профиля и характеристик, которые были получены ваше
-Именование атрибутов профиля (чтобы удобно было обращаться к ним из основной программы)
-Объявление функций для работы с характеристиками и профилем из внешней программы
-Определение типа функции колбэк вызова профиля
#define HABR_UUID(uuid) 0x66, 0x9a, 0x0c, 0x20, 0x00, 0x08, 0xa9, 0xb4, 0xe4, 0x11, 0xd7, 0x85, uuid, 0x50 , 0x2e, 0x51
#define HH_BUTTON1_STATE_ATTR 0
#define HH_BUTTON2_STATE_ATTR 1
#define HH_CHANNEL1_PWM_ATTR 2
#define HH_CHANNEL2_PWM_ATTR 3
#define HH_SERVICE_UUID 0x10
#define HH_BUTTON1_STATE_UUID 0x11
#define HH_BUTTON2_STATE_UUID 0x12
#define HH_CHANNEL1_PWM_UUID 0x13
#define HH_CHANNEL2_PWM_UUID 0x14
#define HH_SERVICE 0x00000001
typedef void (*habrControlCB_t)( uint8 paramID ) ;
typedef struct
{
habrControlCB_t pfnHabrCB; // Called when some att changed
} HarbCBs_t;
extern bStatus_t Habr_AddService();
extern bStatus_t Habr_RegisterAppCBs( HarbCBs_t *appCallbacks );
extern bStatus_t Habr_SetParameter( uint8 param, uint8 len, void *value );
extern bStatus_t Habr_GetParameter( uint8 param, void *value );
Первое, с чем мы сталкиваемся – это с тем, что 15 байтов из 16 наших UUID совпадают. Соответственно, разумно объединить их в общий дефайн, учитывая однако факт, что порядок байтов в Bluetooth — big-endian, а в записи UUID, полученной нами — little endian. Посему запись байтов в дефайне перевернута зеркально.
Функции AddService и RegisterAppCBs служат для регистрации профиля в стеке и привязки функций обратного вызова программы к профилю.
Функции SetParameter и GetParameter нужны для управления значениями характеристик профиля.
Кроме того, нам понадобится сделать обработчики для событий установки и чтения переменных по протоколу, но об этом позже. Для начала разметим таблицу профиля в исполняемом файле.
4. Таблица сервисов
Итак, у нас есть четыре характеристики, две из которых могут нотифицировать пользовательское приложение об изменении значения характеристики. Как говорилось в первой части статьи, для инициализации одной переменной для чтения или записи требуется три записи в таблице устройства, для нотифицируемой переменной — четыре, то есть для всех переменных профиля нам понадобится 14 записей, добавив к ним запись, объявляющую профиль, получим 15 записей.
Самое важное сейчас — правильно задать таблицу устройства.
Первое, что нужно сделать — сформировать UUID профиля и характеристик в переменные в виде:
CONST uint8 HhServUUID[ATT_UUID_SIZE] =
{
HABR_UUID(HH_SERVICE_UUID)
};
Далее определяем переменные/константы, которые будут отвечать за параметры конкретных характеристик:
static uint8 hhButton1CharProps = GATT_PROP_NOTIFY; //определит параметры доступа к переменной
static uint8 hhButton1Value = 0x00; //определит значение по умолчанию
static gattCharCfg_t hhButton1Config[GATT_MAX_NUM_CONN]; //параметры уведомления - только для переменных NOTIFY
static uint8 hhButton1UserDesc[]="Button 1 variable\0"; //текстовое описание характеристики
И заносим характеристики в таблицу gatt характеристик устройства (массив типа gattAttribute_t) в виде:
{
gattAttrType_t type; // содержит длину handle и указатель на UUID атрибута.
uint8 permissions; // права доступа к атрибуту.
uint16 handle; // устанавливается стеком самостоятельно - писать 0.
uint8* const pValue; // значение (до 512 байт).
}
Это создает небольшую путаницу. С одной стороны, у нас есть переменная, которая определяет права доступа к характеристике (в предыдущем листинге — GATT_PROP_NOTIFY). С другой стороны, есть запись, отвечающая за права доступа к атрибуту. Проясним эту разницу на нашем примере. В нашем профиле есть нотификация от обеих кнопок и есть возможность чтения состояния одной из них (второй).
Тогда для первой настройки характеристики — GATT_PROP_NOTIFY, но нет разрешения на чтение либо запись.
Для второй настройки характеристики — GATT_PROP_NOTIFY | GATT_PROP_READ, кроме того, должно быть объявлено разрешение на чтение в GATT таблице устройства (иначе не будет вызван колбэк с запросом на чтение) — GATT_PERMIT_READ.
Более подробно — в полной таблице атрибутов:
#include "bcomdef.h"
#include "OSAL.h"
#include "linkdb.h"
#include "att.h"
#include "gatt.h"
#include "gatt_uuid.h"
#include "gattservapp.h"
#include "habrprofile.h"
#include "OSAL_Clock.h"
#define SERVAPP_NUM_ATTR_SUPPORTED 15
#define UUID_SIZE 16
CONST uint8 hhServUUID[ATT_UUID_SIZE] =
{
HABR_UUID(HH_SERVICE_UUID)
};
CONST uint8 hhButton1UUID[ATT_UUID_SIZE] =
{
HABR_UUID(HH_BUTTON1_STATE_UUID)
};
CONST uint8 hhButton2UUID[ATT_UUID_SIZE] =
{
HABR_UUID(HH_BUTTON2_STATE_UUID)
};
CONST uint8 hhPWM1UUID[ATT_UUID_SIZE] =
{
HABR_UUID(HH_CHANNEL1_PWM_UUID)
};
CONST uint8 hhPWM2UUID[ATT_UUID_SIZE] =
{
HABR_UUID(HH_CHANNEL2_PWM_UUID)
};
static HarbCBs_t *habrahabrAppCBs_t = NULL;
//attribute definitions
static CONST gattAttrType_t hhService = {ATT_UUID_SIZE, hhServUUID};
static uint8 hhButton1CharProps = GATT_PROP_NOTIFY;
static uint8 hhButton1Value = 0x00;
static gattCharCfg_t hhButton1Config[GATT_MAX_NUM_CONN];
static uint8 hhButton1UserDesc[]="Button 1 variable\0";
static uint8 hhButton2CharProps = GATT_PROP_NOTIFY|GATT_PROP_READ;
static uint8 hhButton2Value = 0x00;
static gattCharCfg_t hhButton2Config[GATT_MAX_NUM_CONN];
static uint8 hhButton2UserDesc[]="Button 2 variable\0";
static uint8 hhPWM1CharProps = GATT_PROP_READ | GATT_PROP_WRITE;
static uint8 hhPWM1Value = 0x00;
static uint8 hhPWM1UserDesc[] = "PWM 1 variable\0";
static uint8 hhPWM2CharProps = GATT_PROP_READ | GATT_PROP_WRITE;
static uint8 hhPWM2Value = 0x00;
static uint8 hhPWM2UserDesc[] = "PWM 2 variable\0";
//attribute table
static gattAttribute_t HabrProfileAttrTable[15]={
//Service
{
{ ATT_BT_UUID_SIZE, primaryServiceUUID },
GATT_PERMIT_READ,
0,
(uint8 *)&hhServUUID
},
//Button1
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&hhButton1CharProps
},
{
{UUID_SIZE, hhButton1UUID },
0,
0,
(uint8 *)&hhButton1Value
},
{
{ATT_BT_UUID_SIZE , clientCharCfgUUID},
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
(uint8 *)hhButton1Config
},
{
{ ATT_BT_UUID_SIZE, charUserDescUUID },
GATT_PERMIT_READ,
0,
hhButton1UserDesc
} ,
//Button2
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&hhButton2CharProps
},
{
{UUID_SIZE, hhButton2UUID },
GATT_PERMIT_READ,
0,
(uint8 *)&hhButton2Value
},
{
{ATT_BT_UUID_SIZE , clientCharCfgUUID},
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
(uint8 *)hhButton2Config
},
{
{ ATT_BT_UUID_SIZE, charUserDescUUID },
GATT_PERMIT_READ,
0,
hhButton2UserDesc
} ,
//PWM channel 1
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&hhPWM1CharProps
},
{
{UUID_SIZE, hhPWM1UUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
(uint8*)&hhPWM1Value
},
{
{ ATT_BT_UUID_SIZE, charUserDescUUID },
GATT_PERMIT_READ,
0,
hhPWM1UserDesc
} ,
//PWM channel 2
{
{ ATT_BT_UUID_SIZE, characterUUID },
GATT_PERMIT_READ,
0,
&hhPWM2CharProps
},
{
{UUID_SIZE, hhPWM2UUID },
GATT_PERMIT_READ | GATT_PERMIT_WRITE,
0,
(uint8*)&hhPWM2Value
},
{
{ ATT_BT_UUID_SIZE, charUserDescUUID },
GATT_PERMIT_READ,
0,
hhPWM2UserDesc
}
};
5. Пользовательские функции
Следующий этап – описать функции, вызываемые из основной программы для:
— регистрации профиля,
— назначения функции обратного вызова,
— чтения переменных,
— записи переменных.
Для регистрации профиля в стеке в первую очередь нужно объявить функции обратного вызова профиля — как раз те функции, которые вызываются, когда происходит внешнее событие — запрос на чтение или запись характеристики, а также функцию, вызываемую при изменении статуса соединения.
static uint8 hh_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr,
uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen );
static bStatus_t hh_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr,
uint8 *pValue, uint8 len, uint16 offset );
static void hh_HandleConnStatusCB( uint16 connHandle, uint8 changeType );
CONST gattServiceCBs_t HH_CBs =
{
hh_ReadAttrCB, // Read callback function pointer
hh_WriteAttrCB, // Write callback function pointer
NULL
};
Окей, теперь можно зарегистрировать профиль в стеке и проверить корректность таблицы характеристик профиля. Функция регистрации профиля в стеке подразумевает помимо непосредственно вызова GATTServApp_RegisterService еще и регистрацию функции обратного вызова при изменении статуса соединения и инициализацию переменных конфигурации (для тех характеристик, которые должны быть нотифицируемыми):
bStatus_t Habr_AddService()
{
uint8 status = SUCCESS;
GATTServApp_InitCharCfg( INVALID_CONNHANDLE, hhButton1Config );
GATTServApp_InitCharCfg( INVALID_CONNHANDLE, hhButton2Config );
VOID linkDB_Register( hh_HandleConnStatusCB );
status = GATTServApp_RegisterService(HabrProfileAttrTable, GATT_NUM_ATTRS(HabrProfileAttrTable), &HH_CBs );
return ( status );
}
Проверим правильность таблицы атрибутов. Для этого в SimpleBLEPerepherial.c в функции SimpleBLEPeripheral_Init вызовем Habr_AddService, предварительно добавив инклуд заголовочника (и не забыв добавить путь к заголовочнику для компилятора — строку "$PROJ_DIR$\..\..\Profiles\HabrProfile"). Прошьем отладочную плату, подключимся к ней через BLE Device Monitor и проверим полученную таблицу атрибутов:
Важно сверить UUID, состав профиля. Если все хорошо, идем дальше.
Опущу описание функции
bStatus_t Habr_RegisterAppCBs( HarbCBs_t *appCallbacks ){
if ( appCallbacks )
{
habrahabrAppCBs_t = appCallbacks;
return ( SUCCESS );
}
else
{
return ( bleAlreadyInRequestedMode );
}
}
bStatus_t Habr_SetParameter( uint8 param, uint8 len, void *value ){
bStatus_t ret = SUCCESS;
switch ( param )
{
case HH_BUTTON1_STATE_ATTR:
if(len == sizeof(uint8))
{
hhButton1Value = *((uint8*)value);
GATTServApp_ProcessCharCfg (hhButton1Config, (uint8 *)&hhButton1Value, FALSE, HabrProfileAttrTable
,GATT_NUM_ATTRS(HabrProfileAttrTable), INVALID_TASK_ID);
}
else{
ret = bleInvalidRange;
}
break;
case HH_BUTTON2_STATE_ATTR:
if(len == sizeof(uint8))
{
hhButton2Value = *((uint8*)value);
GATTServApp_ProcessCharCfg (hhButton2Config, (uint8 *)&hhButton2Value, FALSE, HabrProfileAttrTable
,GATT_NUM_ATTRS(HabrProfileAttrTable), INVALID_TASK_ID);
}
else{
ret = bleInvalidRange;
}
break;
case HH_CHANNEL1_PWM_ATTR:
if(len == sizeof(uint8))
{
hhPWM1Value = *((uint8*)value);
}
else{
ret = bleInvalidRange;
}
break;
case HH_CHANNEL2_PWM_ATTR:
if(len == sizeof(uint8))
{
hhPWM2Value = *((uint8*)value);
}
else{
ret = bleInvalidRange;
}
break;
default:
ret = INVALIDPARAMETER;
break;
}
return(ret);
}
Останавливаться на функции регистрации колбэка не вижу смысла. Немного подробней рассмотрим функции записи и чтения значения переменных, а в первую очередь функцию записи значений в профиль. Тут стоит обратить внимание на то, что необходимо обязательно делать вызов GATTServApp_ProcessCharCfg — эта функция обеспечит собственно нотификацию.
Дело за малым — дописать функции для обработки событий стека.
6. Колбэк функции BLE стека
Обработкой событий стека, как было сказано выше, займутся три функции — колбэк запроса чтения значения атрибута, колбэк запроса чтения записи атрибута, колбэк состояния соединения.
Научить профиль отдавать свои характеристики на чтение очень просто (особенно в нашем случае, когда все характеристики – значения одного типа uint8) — для этого нужно убедиться, что мы имеем дело с правильными характеристиками. Стек в ответ от функции получает три значения — status, pLen (так что архиважно всегда устанавливать точное значение pLen) и pValue. Все три значения передаются дальше и могут быть получены нами на приемной стороне.
static uint8 hh_ReadAttrCB( uint16 connHandle, gattAttribute_t *pAttr, uint8 *pValue, uint8 *pLen, uint16 offset, uint8 maxLen )
{
bStatus_t status = SUCCESS;
if ( offset > 0 )
{
return ( ATT_ERR_ATTR_NOT_LONG );
}
if ( pAttr->type.len == ATT_UUID_SIZE )
{
// 128-bit UUID
uint8 uuid[ATT_UUID_SIZE];
osal_memcpy(uuid, pAttr->type.uuid, ATT_UUID_SIZE);
if(osal_memcmp(uuid,hhPWM2UUID,ATT_UUID_SIZE)||
osal_memcmp(uuid,hhPWM1UUID,ATT_UUID_SIZE)||
osal_memcmp(uuid,hhButton2UUID,ATT_UUID_SIZE)||
osal_memcmp(uuid,hhButton1UUID,ATT_UUID_SIZE))
{
*pLen = 1;
pValue[0] = *pAttr->pValue;
}
}
else
{
// 16-bit UUID
*pLen = 0;
status = ATT_ERR_INVALID_HANDLE;
}
return ( status );
}
}
Заодно проверим чтение характеристик — все ли работает корректно (кстати, мы ожидаем ошибку чтения для переменной первой кнопки):
Запись переменных в профиль происходит подобным образом, однако в функции чтения мы группировали переменные — здесь это делать не желательно, поскольку хочется, чтобы вызываемый профилем колбэк понимал, какая конкретно характеристика была изменена. Достигается это за счет определения переменной notify. Если она была установлена, то в данной функция вызовет функцию в пользовательском приложении с параметром notify.
Кроме того, помимо обработки записи значений ШИМ, эта функция включает (и выключает) нотификацию, если было записано значение для атрибута конфигурации нотифицируемой характеристики — это достигается вызовом функции GATTServApp_ProcessCCCWriteReq();
static bStatus_t hh_WriteAttrCB( uint16 connHandle, gattAttribute_t *pAttr,
uint8 *pValue, uint8 len, uint16 offset ){
bStatus_t status = SUCCESS;
uint8 notify = 0xFF;
if ( pAttr->type.len == ATT_UUID_SIZE )
{
const uint8 uuid[ATT_UUID_SIZE] = {
HABR_UUID(pAttr->type.uuid[12])
};
if(osal_memcmp(uuid,hhPWM1UUID,ATT_UUID_SIZE))
{
if ( offset == 0 )
{
if ( len != 1 ){
status = ATT_ERR_INVALID_VALUE_SIZE;
}
}
else
{
status = ATT_ERR_ATTR_NOT_LONG;
}
if ( status == SUCCESS )
{
uint8 *pCurValue = (uint8 *)pAttr->pValue;
*pCurValue = pValue[0];
notify = HH_CHANNEL1_PWM_ATTR;
}
}
else if(osal_memcmp(uuid,hhPWM2UUID,ATT_UUID_SIZE)){
if ( offset == 0 )
{
if ( len != 1 ){
status = ATT_ERR_INVALID_VALUE_SIZE;
}
}
else
{
status = ATT_ERR_ATTR_NOT_LONG;
}
if ( status == SUCCESS )
{
uint8 *pCurValue = (uint8 *)pAttr->pValue;
*pCurValue = pValue[0];
notify = HH_CHANNEL2_PWM_ATTR;
}
}
}
else if (pAttr->type.len== ATT_BT_UUID_SIZE)
{
uint16 uuid= BUILD_UINT16(pAttr->type.uuid[0],pAttr->type.uuid[1]);
switch(uuid){
case GATT_CLIENT_CHAR_CFG_UUID:
status=GATTServApp_ProcessCCCWriteReq(connHandle, pAttr, pValue, len, offset, GATT_CLIENT_CFG_NOTIFY);
break;
default:
status = ATT_ERR_ATTR_NOT_FOUND;
}
}
else{
status = ATT_ERR_INVALID_HANDLE;
}
// If an attribute changed then callback function to notify application of change
if ( (notify != 0xFF) && habrahabrAppCBs_t && habrahabrAppCBs_t->pfnHabrCB )
habrahabrAppCBs_t->pfnHabrCB(notify);
return ( status );
}
Профиль почти готов. Последнее что в него нужно добавить — функцию, которая отключит нотификацию переменных при потере соединения.
static void hh_HandleConnStatusCB( uint16 connHandle, uint8 changeType ){
if ( connHandle != LOOPBACK_CONNHANDLE )
{
if ( ( changeType == LINKDB_STATUS_UPDATE_REMOVED ) ||
( ( changeType == LINKDB_STATUS_UPDATE_STATEFLAGS ) &&
( !linkDB_Up( connHandle ) ) ) )
{
GATTServApp_InitCharCfg ( connHandle, hhButton1Config);
GATTServApp_InitCharCfg ( connHandle, hhButton2Config);
}
}
}
Профиль готов! Теперь убедимся, что он работает корректно.
7. Связь с пользовательским приложением
Оторвемся от периферии и сделаем такой сценарий:
При установке значения канала PWM1 то же значение передается нам через переменную Button1. Таким же образом поставим в соответствие PWM2 и Button2.
Для этого нам понадобится в файле SimpleBLEPerepherial:
— Объявить колбэк профиля,
— Зарегистрировать его в профиле,
— Реализовать алгоритм.
Начнем. Объявим собственно колбэк и структуру, которая будет регистрироваться для исполнения колбэка. На первый взгляд такая запись может показаться чересчур сложной, однако если нам потребуется строить профиль с несколькими колбэками (например, если захочется добавить уведомление о чтении переменной), такой подход более чем оправдает себя. Да и вообще все колбэки стека построены именно таким образом.
static void habrProfileCB (uint8 paramID);
static HarbCBs_t HabrProfCBStruct =
{
habrProfileCB // Characteristic value change callback
};
В теле функции SimpleBLEPeripheral_Init зарегистрируем в профиле эту структуру:
Habr_AddService();
Habr_RegisterAppCBs(&HabrProfCBStruct);
В функции hh_WriteAttrCB мы уже реализовали передачу в колбэк информации о том, какая характеристика была записана. Дело только за тем, чтобы сейчас эту информацию обработать:
static void habrProfileCB (uint8 paramID){
uint8 u8buffer;
switch(paramID){
case HH_CHANNEL1_PWM_ATTR:
Habr_GetParameter(HH_CHANNEL1_PWM_ATTR, &u8buffer);
Habr_SetParameter(HH_BUTTON1_STATE_ATTR, sizeof(uint8), &u8buffer);
break;
case HH_CHANNEL2_PWM_ATTR:
Habr_GetParameter(HH_CHANNEL2_PWM_ATTR, &u8buffer);
Habr_SetParameter(HH_BUTTON2_STATE_ATTR, sizeof(uint8), &u8buffer);
break;
default:
break;
}
}
И наконец проверим, что все работает. Оно и правда работает — можно убедиться в консоли:
Интеграцию с периферией контроллера читателю предлагается сделать самостоятельно.
Спасибо за внимание!
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий