Bluetooth broadcast can only send data in one direction and the data can be received by any other device. It is not possible to realize the device receiving data and it is not safe. In my project, not only the Bluetooth device needs to send data to the WeChat applet, but also the WeChat applet needs to send data to the Bluetooth device, so I have to give up using broadcast communication.
By consulting the information, I learned that to achieve two-way data transmission and reception between Bluetooth devices and WeChat applet, it is necessary to use a custom service and then read and write characteristic values. First, learn the concepts that are often used in the following programs.
In GAP, peripheral devices broadcast data in two ways: Advertising Data Payload and Scan Response Data Payload. Each data can contain up to 31 bytes. Advertising data is necessary because the peripheral device must broadcast continuously to let the central device know its existence. The peripheral device will set a broadcast interval, and in each broadcast interval, it will resend its own advertising data. In most cases, the peripheral device broadcasts itself to let the central device find itself and establish a GATT connection to exchange more data. GATT connection is exclusive, that is, a peripheral device can only be connected to one central device at a time . Once the peripheral device is connected, it will stop broadcasting immediately, so that the peripheral device is invisible to other central devices . When the connection is disconnected, the peripheral device starts broadcasting again. If the central device and the peripheral device need two-way communication, the only way is to establish a GATT connection.
The two parties in GATT communication are in a client/server relationship. The peripheral device acts as a GATT server , which maintains the ATT lookup table and the definition of service and characteristic. The central device is a GATT client , which initiates requests to the server. It should be noted that all communication events are initiated by the client (also called the master) and receive responses from the server (also called the slave).
A Bluetooth device can have multiple services, and a service can have multiple characteristic values. Each characteristic value has its own attributes, such as size, permission, value, and descriptor. See the figure below:
The following is excerpted from some authoritative materials, and the translation is not easy to understand.
It can be seen that the program first determines that the current app status is APPM_CREATE_DB, then calls the Service_Add function to add the service. After adding, the app status is set to APPM_READY. After all services are added, it starts broadcasting.
The final step in the Service_Add function is to add the service through the macro in app.h:
/* List of functions used to create the database */
#define SERVICE_ADD_FUNCTION_LIST \
DEFINE_SERVICE_ADD_FUNCTION(Batt_ServiceAdd_Server), \
DEFINE_SERVICE_ADD_FUNCTION(CustomService_ServiceAdd)
//DEFINE_SERVICE_ADD_FUNCTION(CustomService_Service2Add)
If you want to add your own defined service, you only need to implement your own CustomService_ServiceAdd function. The CustomService_ServiceAdd function is implemented in ble_custom.c:
/* ----------------------------------------------------------------------------
* Function : void CustomService_ServiceAdd(void)
* ----------------------------------------------------------------------------
* Description : Send request to add custom profile into the attribute
* database.Defines the different access functions
* (setter/getter commands to access the different
* characteristic attributes).
* Inputs : None
* Outputs : None
* Assumptions : None
* ------------------------------------------------------------------------- */
void CustomService_ServiceAdd(void)
{
struct gattm_add_svc_req *req =
KE_MSG_ALLOC_DYN(GATTM_ADD_SVC_REQ,
TASK_GATTM, TASK_APP,
gattm_add_svc_req,
CS_IDX_NB * sizeof(struct gattm_att_desc));
const uint8_t svc_uuid[ATT_UUID_128_LEN] = CS_SVC_UUID;
const struct gattm_att_desc att[CS_IDX_NB] =
{
/* Attribute Index = Attribute properties: UUID,
* Permissions,
* Max size,
* Extra permissions */
/* TX Characteristic */
[CS_IDX_TX_VALUE_CHAR] = ATT_DECL_CHAR(),
[CS_IDX_TX_VALUE_VAL] = ATT_DECL_CHAR_UUID_128(CS_CHARACTERISTIC_TX_UUID,
PERM(RD, ENABLE) | PERM(NTF, ENABLE),
CS_TX_VALUE_MAX_LENGTH),
[CS_IDX_TX_VALUE_CCC] = ATT_DECL_CHAR_CCC(),
[CS_IDX_TX_VALUE_USR_DSCP] = ATT_DECL_CHAR_USER_DESC(CS_USER_DESCRIPTION_MAX_LENGTH),
/* RX Characteristic */
[CS_IDX_RX_VALUE_CHAR] = ATT_DECL_CHAR(),
[CS_IDX_RX_VALUE_VAL] = ATT_DECL_CHAR_UUID_128(CS_CHARACTERISTIC_RX_UUID,
PERM(RD, ENABLE) | PERM(WRITE_REQ, ENABLE)
| PERM(WRITE_COMMAND, ENABLE),
CS_RX_VALUE_MAX_LENGTH),
[CS_IDX_RX_VALUE_CCC] = ATT_DECL_CHAR_CCC(),
[CS_IDX_RX_VALUE_USR_DSCP] = ATT_DECL_CHAR_USER_DESC(CS_USER_DESCRIPTION_MAX_LENGTH),
};
/* Fill the add custom service message */
req->svc_desc.start_hdl = 0;
req->svc_desc.task_id = TASK_APP;
req->svc_desc.perm = PERM(SVC_UUID_LEN, UUID_128);
req->svc_desc.nb_att = CS_IDX_NB;
memcpy(&req->svc_desc.uuid[0], &svc_uuid[0], ATT_UUID_128_LEN);
for (unsigned int i = 0; i < CS_IDX_NB; i++)
{
memcpy(&req->svc_desc.atts[i], &att[i],
sizeof(struct gattm_att_desc));
}
/* Send the message */
ke_msg_send(req);
}
The uuids of services and characteristic values are defined in ble_custom.h (uuids follow certain specifications):
/* Custom service UUIDs */
#define CS_SVC_UUID { 0x24, 0xdc, 0x0e, 0x6e, 0x01, 0x40, \
0xca, 0x9e, 0xe5, 0xa9, 0xa3, 0x00, \
0xb5, 0xf3, 0x93, 0xe0 }
#define CS_CHARACTERISTIC_TX_UUID { 0x24, 0xdc, 0x0e, 0x6e, 0x02, 0x40, \
0xca, 0x9e, 0xe5, 0xa9, 0xa3, 0x00, \
0xb5, 0xf3, 0x93, 0xe0 }
#define CS_CHARACTERISTIC_RX_UUID { 0x24, 0xdc, 0x0e, 0x6e, 0x03, 0x40, \
0xca, 0x9e, 0xe5, 0xa9, 0xa3, 0x00, \
0xb5, 0xf3, 0x93, 0xe0 }
For the sake of convenience, here I call the mobile Bluetooth debugging app that actively requests data the host, and the passive Bluetooth device RSL10 board the slave. If the host requests to read the characteristic value, the callback function GATTC_ReadReqInd in ble_custom.c will be called.
We can fill the data that needs to be sent to the host in the GATTC_ReadReqInd function and fill the data into valptr. In the program, I send "eeworld" to the host. In actual applications, we can fill the data into cs_env[device_indx].tx_value in the main loop or other functions.
int GATTC_ReadReqInd(ke_msg_id_t const msg_id,
struct gattc_read_req_ind const *param,
ke_task_id_t const dest_id,
ke_task_id_t const src_id)
{
uint8_t length = 0;
uint8_t status = GAP_ERR_NO_ERROR;
uint16_t attnum;
uint8_t *valptr = NULL;
PRINTF("===================read\n");
/* Retrieve the index of environment structure representing peer device */
signed int device_indx = Find_Connected_Device_Index(KE_IDX_GET(src_id));
if (device_indx == INVALID_DEV_IDX)
{
return (KE_MSG_CONSUMED);
}
struct gattc_read_cfm *cfm;
/* Set the attribute handle using the attribute index
* in the custom service */
if (param->handle > cs_env[device_indx].start_hdl)
{
attnum = (param->handle - cs_env[device_indx].start_hdl - 1);
}
else
{
status = ATT_ERR_INVALID_HANDLE;
}
PRINTF("==== attnum=0x%02x param->handle=0x%04x ====\n",attnum,param->handle);
/* If there is no error, send back the requested attribute value */
if (status == GAP_ERR_NO_ERROR)
{
switch (attnum)
{
/* RX characteristic*/
case CS_IDX_RX_VALUE_VAL:
{
length = CS_RX_VALUE_MAX_LENGTH;
valptr = (uint8_t *)&cs_env[device_indx].rx_value;
}
break;
case CS_IDX_RX_VALUE_CCC:
{
length = 2;
valptr = (uint8_t *)&cs_env[device_indx].rx_cccd_value;
}
break;
case CS_IDX_RX_VALUE_USR_DSCP:
{
length = strlen(CS_RX_CHARACTERISTIC_NAME);
valptr = (uint8_t *)CS_RX_CHARACTERISTIC_NAME;
}
break;
/* TX characteristic */
case CS_IDX_TX_VALUE_VAL:
{
length = CS_TX_VALUE_MAX_LENGTH;
valptr = (uint8_t *)&cs_env[device_indx].tx_value;
for(int n=0;n<5;n++)
{
PRINTF("====Read=== tx_value[%d]=0x%02x\n",n,cs_env[device_indx].tx_value[n]);
}
}
break;
case CS_IDX_TX_VALUE_CCC:
{
length = 2;
valptr = (uint8_t *)&cs_env[device_indx].tx_cccd_value;
PRINTF("====Read=== tx_cccd_value=0x%04x\n",cs_env[device_indx].tx_cccd_value);
}
break;
case CS_IDX_TX_VALUE_USR_DSCP:
{
length = strlen(CS_TX_CHARACTERISTIC_NAME);
valptr = (uint8_t *)CS_TX_CHARACTERISTIC_NAME;
}
break;
default:
{
status = ATT_ERR_READ_NOT_PERMITTED;
}
break;
}
}
/* Allocate and build message */
cfm = KE_MSG_ALLOC_DYN(GATTC_READ_CFM,
KE_BUILD_ID(TASK_GATTC, ble_env[device_indx].conidx),
TASK_APP,
gattc_read_cfm, length);
length=8;
memcpy(valptr, "eeworld", length);
if (valptr != NULL)
{
memcpy(cfm->value, valptr, length);
}
cfm->handle = param->handle;
cfm->length = length;
cfm->status = status;
/* Send the message */
ke_msg_send(cfm);
return (KE_MSG_CONSUMED);
}
The host turns on or off notifications and sends data to the slave, which will call GATTC_WriteReqInd in ble_custom.c. Let's take a look at the log first:
int GATTC_WriteReqInd(ke_msg_id_t const msg_id,
struct gattc_write_req_ind const *param,
ke_task_id_t const dest_id,
ke_task_id_t const src_id)
{
/* Retrieve the index of environment structure representing peer device */
signed int device_indx = Find_Connected_Device_Index(KE_IDX_GET(src_id));
if (device_indx == INVALID_DEV_IDX)
{
return (KE_MSG_CONSUMED);
}
struct gattc_write_cfm *cfm = KE_MSG_ALLOC(GATTC_WRITE_CFM,KE_BUILD_ID(TASK_GATTC, ble_env[device_indx].conidx),TASK_APP, gattc_write_cfm);
uint8_t status = GAP_ERR_NO_ERROR;
uint16_t attnum;
uint8_t *valptr = NULL;
/* Check that offset is not zero */
if (param->offset)
{
status = ATT_ERR_INVALID_OFFSET;
}
/* Set the attribute handle using the attribute index
* in the custom service */
if (param->handle > cs_env[device_indx].start_hdl)
{
attnum = (param->handle - cs_env[device_indx].start_hdl - 1);
}
else
{
status = ATT_ERR_INVALID_HANDLE;
}
PRINTF("[%s:%d %s]==== write === attnum=0x%02x param->handle=0x%04x cs_env[device_indx].start_hdl=0x%04x\n",__FILE__,__LINE__,__FUNCTION__,attnum,param->handle,cs_env[device_indx].start_hdl);
/* If there is no error, save the requested attribute value */
if (status == GAP_ERR_NO_ERROR)
{
switch (attnum)
{
case CS_IDX_RX_VALUE_VAL:
{
valptr = (uint8_t *)&cs_env[device_indx].rx_value;
cs_env[device_indx].rx_value_changed = true;
PRINTF("[%s:%d %s]========= rx_value ==========\n",__FILE__,__LINE__,__FUNCTION__);
}
break;
case CS_IDX_RX_VALUE_CCC:
{
valptr = (uint8_t *)&cs_env[device_indx].rx_cccd_value;
PRINTF("[%s:%d %s]=================== rx_cccd_value:0x%04x\n",__FILE__,__LINE__,__FUNCTION__,cs_env[device_indx].rx_cccd_value);
}
break;
case CS_IDX_TX_VALUE_CCC:
{
valptr = (uint8_t *)&cs_env[device_indx].tx_cccd_value;
//这是原来的值
PRINTF("[%s:%d %s]=================== tx_cccd_value:0x%04x\n",__FILE__,__LINE__,__FUNCTION__,cs_env[device_indx].tx_cccd_value);
//要写入的新值
PRINTF("[%s:%d %s]=================== will write 0x",__FILE__,__LINE__,__FUNCTION__);
//大端模式传输
for(int i=param->length-1;i>=0;i--)
{
PRINTF("%02x",param->value[i]);
}
PRINTF(" to it\n");
}
break;
default:
{
status = ATT_ERR_WRITE_NOT_PERMITTED;
}
break;
}
}
if (valptr != NULL)
{
memcpy(valptr, param->value, param->length);
}
cfm->handle = param->handle;
cfm->status = status;
/* Send the message */
ke_msg_send(cfm);
return (KE_MSG_CONSUMED);
}
In the program, valptr = (uint8_t *)&cs_env[device_indx].rx_value, then the host sends data (data is stored in param->value) attnum==CS_IDX_RX_VALUE_VAL, sets the receive flag cs_env[device_indx].rx_value_changed = true; finally, memcopy is called to copy the data to the memory pointed to by valptr, i.e. cs_env[device_indx].rx_value. We can read the data in app.c:
#include "app.h"
#include <printf.h>
#include <stdio.h>
int main(void)
{
App_Initialize();
/* Debug/trace initialization. In order to enable UART or RTT trace,
* configure the 'OUTPUT_INTERFACE' macro in printf.h */
printf_init();
PRINTF("__peripheral_server has started!\n");
/* Main application loop:
* - Run the kernel scheduler
* - Send notifications for the battery voltage and RSSI values
* - Refresh the watchdog and wait for an interrupt before continuing */
while (1)
{
Kernel_Schedule();
for (int i = 0; i < NUM_MASTERS; i++)
{
if (ble_env[i].state == APPM_CONNECTED)
{
/* Send battery level if battery service is enabled */
if (app_env.send_batt_ntf[i] && bass_support_env[i].enable)
{
PRINTF("__SEND BATTERY LEVEL %d\n",app_env.batt_lvl);
app_env.send_batt_ntf[i] = false;
Batt_LevelUpdateSend(ble_env[i].conidx,app_env.batt_lvl, 0);
}
/* Update custom service characteristics, send notifications if notification is enabled */
if (cs_env[i].tx_value_changed && cs_env[i].sent_success)
{
cs_env[i].tx_value_changed = false;
(cs_env[i].val_notif)++;
if (cs_env[i].tx_cccd_value & ATT_CCC_START_NTF)
{
PRINTF("[%s:%d %s]==== notifications is enabled ====\n",__FILE__,__LINE__,__FUNCTION__);
//memset(cs_env[i].tx_value, cs_env[i].val_notif,CS_TX_VALUE_MAX_LENGTH);
//CustomService_SendNotification(ble_env[i].conidx,CS_IDX_TX_VALUE_VAL,&cs_env[i].tx_value[0],CS_TX_VALUE_MAX_LENGTH);
memset(cs_env[i].tx_value, cs_env[i].val_notif,1);
CustomService_SendNotification(ble_env[i].conidx,CS_IDX_TX_VALUE_VAL,&cs_env[i].tx_value[0],1);
}
}
if (cs_env[i].rx_value_changed)
{
PRINTF("[%s:%d %s]==== get data from phone start_hdl:0x%04x ====\n",__FILE__,__LINE__,__FUNCTION__,cs_env[i].start_hdl);
for (int j=0;j<5;j++)
{
PRINTF("[%s:%d %s]==== get data from phone %d:0x%02x ====\n",__FILE__,__LINE__,__FUNCTION__,j,cs_env[i].rx_value[j]);
}
cs_env[i].rx_value_changed=false;
}
}
}
/* Refresh the watchdog timer */
Sys_Watchdog_Refresh();
/* Wait for an event before executing the scheduler again */
SYS_WAIT_FOR_EVENT;
}
}
The details are as follows: first determine the receiving flag, and then read the data:
if (cs_env[i].rx_value_changed)
{
PRINTF("[%s:%d %s]==== get data from phone start_hdl:0x%04x ====\n",__FILE__,__LINE__,__FUNCTION__,cs_env[i].start_hdl);
for (int j=0;j<5;j++)
{
PRINTF("[%s:%d %s]==== get data from phone %d:0x%02x ====\n",__FILE__,__LINE__,__FUNCTION__,j,cs_env[i].rx_value[j]);
}
cs_env[i].rx_value_changed=false;
}
The cs_env structure variable defined in the example does not define a variable to indicate the actual length of the received data. In actual use, you can add it and assign the data length when GATTC_WriteReqInd copies the data in the receiving function.
If the notification is turned on or off, attnum==CS_IDX_TX_VALUE_CCC. If the notification is turned on, the host sends data 0x0001 to the slave, and if the notification is turned off, the host sends data 0x0000 to the slave, and writes this data into cs_env[device_indx].tx_cccd_value. In the example, the notification data change flag cs_env.tx_value_changed is set regularly in the APP_Timer function in app_process.c.
Summary: I have a question, that is, without custom variables, how to distinguish UUID when sending and receiving data in the main loop? Is there any API interface to obtain UUID according to device_indx? Because the official SDK has not yet had the energy to study it carefully. In the routine, attnum = (param->handle - cs_env[device_indx].start_hdl - 1); and start_hdl is dynamically applied in the SDK (you can also set a fixed value yourself).
Finally, a modified routine is attached.
peripheral_server.rar
(619.09 KB, downloads: 4)