blob: 88749156e7f1173d72e0973702ee94c528278139 [file] [log] [blame]
/*
* Copyright 2018 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include <stdlib.h>
#include "gattSvcBattery.h"
#include "gatt.h"
#include "util.h"
#include "log.h"
#include "mt.h"
// For details, please see https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.battery_service.xml
//16-bit UUID(s) for Battery Service
#define BTLE_UUID_BATT_SVC_CHAR_BATTERY_LEVEL 0x2A19 // org.bluetooth.characteristic.battery_level
//CCC MAY be present (0x2902)
struct BattSvcConn {
struct BattSvcConn *prev;
struct BattSvcConn *next;
gatt_svc_batt_conn_t id;
gatt_client_conn_t gattConn;
uint8_t state;
uint16_t charStartHandle, charValHandle, charCccdHandle;
};
static GattSvcBattConnStateCbk mCbkState;
static GattSvcBattUpdateCbk mCbkRx;
static pthread_mutex_t mBattSvcLock = PTHREAD_MUTEX_INITIALIZER;
static struct BattSvcConn *mBattSvcDevs = NULL;
/*
* FUNCTION: gattSvcBattFindConnById
* USE: find a BattSvcConn struct given its id
* PARAMS: id - the id to find
* RETURN: the structure or NULL if not found
* NOTES: call with mBattSvcLock held
*/
static struct BattSvcConn *gattSvcBattFindConnById(gatt_svc_batt_conn_t id)
{
struct BattSvcConn *c;
for (c = mBattSvcDevs; c && c->id != id; c = c->next);
return c;
}
/*
* FUNCTION: gattSvcBattFindConnByGattConn
* USE: find a BattSvcConn struct given a gatt connection
* PARAMS: gattConn - the gatt connection to find
* RETURN: the structure or NULL if not found
* NOTES: call with mBattSvcLock held
*/
static struct BattSvcConn *gattSvcBattFindConnByGattConn(gatt_client_conn_t gattConn)
{
struct BattSvcConn *c;
for (c = mBattSvcDevs; c && c->gattConn != gattConn; c = c->next);
return c;
}
/*
* FUNCTION: gattSvcBattDetachWithDev
* USE: Detach from a given Battery Service given its connection structure
* PARAMS: svc - the connection structure
* RETURN: success
* NOTES: call with mBattSvcLock held
*/
static bool gattSvcBattDetachWithDev(struct BattSvcConn *svc)
{
if (svc->next)
svc->next->prev = svc->prev;
if (svc->prev)
svc->prev->next = svc->next;
else
mBattSvcDevs = svc->next;
free(svc);
return true;
}
/*
* FUNCTION: gattSvcBattDetach
* USE: Detach from a Battery Service
* PARAMS: handle - BattSvc connection handle
* RETURN: the structure or NULL if not found
* NOTES:
*/
bool gattSvcBattDetach(gatt_svc_batt_conn_t handle)
{
struct BattSvcConn *svc;
bool ret = false;
pthread_mutex_lock(&mBattSvcLock);
svc = gattSvcBattFindConnById(handle);
if (svc)
ret = gattSvcBattDetachWithDev(svc);
else {
handle = 0;
logw("Attempting to detach Battery Service from a nonexistent connection\n");
}
pthread_mutex_unlock(&mBattSvcLock);
if (handle)
mCbkState(handle, BTLE_BATT_SVC_CONN_STATE_TEARDOWN);
return ret;
}
/*
* FUNCTION: gattSvcBattDetachFromGatt
* USE: Detach from a GATT connection
* PARAMS: gattConn - GATT connection
* RETURN: the structure or NULL if not found
* NOTES:
*/
bool gattSvcBattDetachFromGatt(gatt_client_conn_t gattConn)
{
gatt_svc_batt_conn_t id = 0;
struct BattSvcConn *svc;
bool ret = false;
pthread_mutex_lock(&mBattSvcLock);
svc = gattSvcBattFindConnByGattConn(gattConn);
if (svc) {
ret = gattSvcBattDetachWithDev(svc);
id = svc->id;
}
else
logw("Attempting to detach Battery Service from an unknown GATT connection\n");
pthread_mutex_unlock(&mBattSvcLock);
if (id)
mCbkState(id, BTLE_BATT_SVC_CONN_STATE_TEARDOWN);
return ret;
}
/*
* FUNCTION: gattSvcBattDataArrivedCbk
* USE: Called when a notif/ind/read reply arrives
* PARAMS: gattConn - the GATT client connection id
* charValueHandle - the char value handle
* data - the data
* isReadReply - was this a read reply
* RETURN: NONE
* NOTES:
*/
static void gattSvcBattDataArrivedCbk(gatt_client_conn_t gattConn, uint16_t charValueHandle, sg data, bool isReadReply)
{
struct BattSvcConn *svc;
uint8_t percent = 255;
pthread_mutex_lock(&mBattSvcLock);
svc = gattSvcBattFindConnByGattConn(gattConn);
if (!svc) {
logw("No BattSvc instance found for GATT conn "GATTHANDLEFMT"\n", GATTHANDLECNV(gattConn));
goto out_unlock;
}
if (svc->state != BTLE_BATT_SVC_CONN_STATE_UP) {
logi("Dropping RXed data for connection that is not in UP state\n");
goto out_unlock;
}
if (charValueHandle != svc->charValHandle) {
logi("Dropping RXed data for unknown handle 0x%04x\n", charValueHandle);
goto out_unlock;
}
if (sgLength(data) != 1 || !sgSerializeCutFront(data, &percent, sizeof(percent)) || percent > 100) {
logi("Dropping RXed data as its length or data are invalid: %u, %u\n", (unsigned)sgLength(data), percent);
goto out_unlock;
}
sgFree(data);
pthread_mutex_unlock(&mBattSvcLock);
mCbkRx(gattConn, percent, isReadReply);
return;
out_unlock:
pthread_mutex_unlock(&mBattSvcLock);
sgFree(data);
}
/*
* FUNCTION: gattSvcBattAsyncDataArrivedCbk
* USE: Called when a notif/ind arrives
* PARAMS: gattConn - the GATT client connection id
* charValueHandle - the char value handle
* reliable - was this an indication
* data - the data
* RETURN: NONE
* NOTES:
*/
static void gattSvcBattAsyncDataArrivedCbk(gatt_client_conn_t gattConn, uint16_t charValueHandle, bool reliable, sg data)
{
gattSvcBattDataArrivedCbk(gattConn, charValueHandle, data, false);
}
/*
* FUNCTION: gattSvcBattReadReportReadCbk
* USE: Called by GATT when our read succeeds or fails
* PARAMS: gattConn - a gatt client connection
* trans - transaction ID (unused here as we can look up by gatt conn id)
* handle - handle we read (char's value handle)
* status - GATT status
* data - the data
* RETURN: NONE
* NOTES: it is likely that this success or failure is not actionable for us, so we do nothing
*/
static void gattSvcBattReadReportReadCbk(gatt_client_conn_t gattConn, uniq_t trans, uint16_t handle, uint8_t status, sg data)
{
if (status != GATT_CLI_STATUS_OK || !data)
logi("BattSvc GATT READ to handle 0x%04 failed with status 0x%02x\n", handle, status);
gattSvcBattDataArrivedCbk(gattConn, handle, data, true);
}
/*
* FUNCTION: gattSvcBattCccdWriteStatusCbk
* USE: Called when a notif/ind subscription happens
* PARAMS: gattConn - the GATT client connection id
* valHandle - the handle of the value handle in the char whose CCCD was written
* forSubscribe - true if status is of subscribe, false for unsubscribe
* status - GATT_CLI_STATUS_*
* RETURN: NONE
* NOTES:
*/
static void gattSvcBattCccdWriteStatusCbk(gatt_client_conn_t gattConn, uint16_t valHandle, bool forSubscribe, uint8_t status)
{
gatt_svc_batt_conn_t id = 0;
struct BattSvcConn *svc;
if (!forSubscribe)
return;
logd("CCCD subscribe status %u for val handle 0x%04x\n", status, valHandle);
pthread_mutex_lock(&mBattSvcLock);
svc = gattSvcBattFindConnByGattConn(gattConn);
if (!svc) {
logw("No BattSvc instance found for GATT conn "GATTHANDLEFMT"\n", GATTHANDLECNV(gattConn));
goto out_unlock;
}
if (svc->state != BTLE_BATT_SVC_CONN_STATE_WRITING_CCCDS) {
logw("Unxpected state %u when getting GATT CCCD write results for BattSvc\n", svc->state);
goto out_detach;
}
if (status != GATT_CLI_STATUS_OK) {
logw("a CCCD write with handle 0x%04x failed: 0x%02x - detaching!\n", valHandle, status);
goto out_detach;
}
// we're up now
svc->state = BTLE_BATT_SVC_CONN_STATE_UP;
pthread_mutex_unlock(&mBattSvcLock);
mCbkState(id, BTLE_BATT_SVC_CONN_STATE_UP);
return;
out_detach:
if (!gattSvcBattDetachWithDev(svc))
loge("Failed to detach on error in service search!\n");
out_unlock:
pthread_mutex_unlock(&mBattSvcLock);
}
/*
* FUNCTION: gattSvcBatteryAttachServiceTraversalCbk
* USE: Called by GATT when the Battery Service is found at request of gattSvcBattAttach()
* PARAMS: gattConn - the GATT client connection id
* trans - transaction ID (unused here as we can look up by BattSvc conn handle)
* data - the traversal data or NULL on error
* RETURN: NONE
* NOTES:
*/
static void gattSvcBatteryAttachServiceTraversalCbk(gatt_client_conn_t gattConn, uniq_t trans, const struct GattTraversedService* data)
{
const struct uuid battSvcUuid = BT_UUID_STATIC_16(BTLE_UUID_BATTERY_SERVICE);
struct BattSvcConn *svc;
gatt_svc_batt_conn_t id;
uint32_t i, j;
pthread_mutex_lock(&mBattSvcLock);
svc = gattSvcBattFindConnByGattConn(gattConn);
if (!svc) {
logw("Unexpected GATT service traversal callback for unknown-to-BattSvc connection "GATTHANDLEFMT"\n", GATTHANDLECNV(gattConn));
goto out_unlock;
}
if (svc->state != BTLE_BATT_SVC_CONN_STATE_GATT_DISCOVERY) {
logw("Unxpected state %u when getting GATT discovery results for BattSvc\n", svc->state);
goto out_detach;
}
if (!data || !uuidCmp(&data->uuid, &battSvcUuid)) {
logw("Error finding or traversing BattSvc service on GATT conn "GATTHANDLEFMT"\n", GATTHANDLECNV(gattConn));
goto out_detach;
}
// verify the only characteristic we care about exists, and see if it has a CCCD
for (i = 0; i < data->numChars; i++) {
uint16_t uuid16;
if (!uuidToUuid16(&uuid16, &data->chars[i].uuid) || uuid16 != BTLE_UUID_BATT_SVC_CHAR_BATTERY_LEVEL) {
logi("Ignoring unrecognized characteristic "UUIDFMT" in BattSvc\n", UUIDCONV(data->chars[i].uuid));
continue;
}
if (svc->charStartHandle) {
logw("BattSvc has more than one BatteryValue characteristics: 0x%04x and 0x%04x\n", svc->charStartHandle, data->chars[i].firstHandle);
goto out_detach;
}
if ((data->chars[i].charProps & (GATT_PROP_BROADCAST | GATT_PROP_READ | GATT_PROP_WRITE_NO_RSP | GATT_PROP_WRITE | GATT_PROP_INDICATE | GATT_PROP_AUTH_SIG_WRITE)) != GATT_PROP_READ) {
logw("BattSvc's BatteryValue characteristic has invalid props: 0x%02x\n", data->chars[i].charProps);
goto out_detach;
}
svc->charStartHandle = data->chars[i].firstHandle;
svc->charValHandle = data->chars[i].valHandle;
for (j = 0; j < data->chars[i].numDescrs; j++) {
if (!uuidToUuid16(&uuid16, &data->chars[i].descrs[j].uuid) || uuid16 != GATT_UUID_CHAR_CLI_CHAR_CFG) {
logi("Ignoring unrecognized descriptor "UUIDFMT" in BattSvc's BatteryValue characteristic\n", UUIDCONV(data->chars[i].descrs[j].uuid));
continue;
}
if (svc->charCccdHandle) {
logw("BattSvc's BatteryValue characteristic has more than one CCCD: 0x%04x and 0x%04x\n", svc->charCccdHandle, data->chars[i].descrs[j].handle);
goto out_detach;
}
svc->charCccdHandle = data->chars[i].descrs[j].handle;
}
if ((svc->charCccdHandle && !(data->chars[i].charProps & GATT_PROP_NOTIFY)) || (!svc->charCccdHandle && (data->chars[i].charProps & GATT_PROP_NOTIFY))) {
logw("BattSvc's BatteryValue 's CCCD existence disagrees with props: 0x%04x 0x%02x\n", svc->charCccdHandle, data->chars[i].charProps);
goto out_detach;
}
}
if (!svc->charStartHandle) {
logw("BattSvc failed to find BatteryValue characteristic!\n");
goto out_detach;
}
// if we have a CCCD, go to CCCD write stage, else just go to UP stage
id = svc->id;
if (svc->charCccdHandle) {
uint16_t charHandle = svc->charStartHandle, valHandle = data->chars[i].valHandle, cccdHandle = svc->charCccdHandle;
svc->state = BTLE_BATT_SVC_CONN_STATE_WRITING_CCCDS;
pthread_mutex_unlock(&mBattSvcLock);
mCbkState(id, BTLE_BATT_SVC_CONN_STATE_WRITING_CCCDS);
if (GATT_CLI_STATUS_OK != gattClientNotifsSubscribe(gattConn, charHandle, valHandle, cccdHandle, false, gattSvcBattCccdWriteStatusCbk, gattSvcBattAsyncDataArrivedCbk)) {
logw("Failed to write CCCD for {0x%04x,0x%04x,0x%04x)\n", charHandle, valHandle, cccdHandle);
if (!gattSvcBattDetachFromGatt(gattConn))
loge("Failed to detach from GATT conn on error in CCCD write!\n");
}
}
else {
svc->state = BTLE_BATT_SVC_CONN_STATE_UP;
pthread_mutex_unlock(&mBattSvcLock);
mCbkState(id, BTLE_BATT_SVC_CONN_STATE_UP);
}
return;
out_detach:
if (!gattSvcBattDetachWithDev(svc))
loge("Failed to detach on error in service search!\n");
out_unlock:
pthread_mutex_unlock(&mBattSvcLock);
}
/*
* FUNCTION: gattSvcBattAttach
* USE: Attach Battery Service to a GATT connection
* PARAMS: gattConn - a gatt client connection
* RETURN: BattSvc connection handle or zero on immediate failure
* NOTES: expect state callback with details
*/
gatt_svc_batt_conn_t gattSvcBattAttach(gatt_client_conn_t gattConn)
{
const struct uuid battSvcUuid = BT_UUID_STATIC_16(BTLE_UUID_BATTERY_SERVICE);
gatt_svc_batt_conn_t svcConn = 0;
struct BattSvcConn *svc;
svc = calloc(1, sizeof(struct BattSvcConn));
if (!svc)
return 0;
pthread_mutex_lock(&mBattSvcLock);
if (gattSvcBattFindConnByGattConn(gattConn)) {
logw("Battery Service is already attached to this GATT conn "GATTHANDLEFMT"\n", GATTHANDLECNV(gattConn));
pthread_mutex_unlock(&mBattSvcLock);
free(svc);
return 0;
}
svc->id = svcConn = uniqGetNext();
svc->gattConn = gattConn;
svc->state = BTLE_BATT_SVC_CONN_STATE_GATT_DISCOVERY;
svc->next = mBattSvcDevs;
if (mBattSvcDevs)
mBattSvcDevs->prev = svc;
mBattSvcDevs = svc;
pthread_mutex_unlock(&mBattSvcLock);
mCbkState(svcConn, BTLE_BATT_SVC_CONN_STATE_GATT_DISCOVERY);
logi("Battery Service attempting an attachment to GATT conn "GATTHANDLEFMT"\n", GATTHANDLECNV(gattConn));
if (GATT_CLI_STATUS_OK == gattClientUtilFindAndTraversePrimaryService(gattConn, &battSvcUuid, 0, gattSvcBatteryAttachServiceTraversalCbk))
return svcConn;
logw("Failed to start the GATT service search\n");
if (!gattSvcBattDetachFromGatt(svcConn))
loge("Failed to detach on error on attach!\n");
return 0;
}
/*
* FUNCTION: gattSvcBattRequestRead
* USE: Request a battery reading
* PARAMS: handle - BattSvc connection handle
* RETURN: fals eon immediate error, else wait for GattSvcBattUpdateCbk() cbk
* NOTES:
*/
bool gattSvcBattRequestRead(gatt_svc_batt_conn_t handle)
{
struct BattSvcConn *svc;
pthread_mutex_lock(&mBattSvcLock);
svc = gattSvcBattFindConnById(handle);
if (!svc)
logw("Attempting to get BattSvc level from a nonexistent BattSvc instance\n");
else if (svc->state != BTLE_BATT_SVC_CONN_STATE_UP)
logw("Attempting to get BattSvc level from a BattSvc instance in state %u\n", svc->state);
else {
gatt_client_conn_t conn = svc->gattConn;
uint16_t handle = svc->charValHandle;
pthread_mutex_unlock(&mBattSvcLock);
return GATT_CLI_STATUS_OK == gattClientRead(conn, handle, 0, 0, 0, gattSvcBattReadReportReadCbk);
}
pthread_mutex_unlock(&mBattSvcLock);
return false;
}
/*
* FUNCTION: gattSvcBattIsNotifSupported
* USE: See if a given BattSvc instance supports async notifs
* PARAMS: handle - BattSvc connection handle
* RETURN: true if yes, false on error or NO
* NOTES:
*/
bool gattSvcBattIsNotifSupported(gatt_svc_batt_conn_t handle)
{
struct BattSvcConn *svc;
bool ret = false;
pthread_mutex_lock(&mBattSvcLock);
svc = gattSvcBattFindConnById(handle);
if (!svc)
logw("Attempting to get BattSvc info from a nonexistent BattSvc instance\n");
else if (svc->state != BTLE_BATT_SVC_CONN_STATE_UP)
logw("Attempting to get BattSvc info from a BattSvc instance in state %u\n", svc->state);
else
ret = !!svc->charCccdHandle;
pthread_mutex_unlock(&mBattSvcLock);
return ret;
}
/*
* FUNCTION: gattSvcBattInit
* USE: Init BattSvc subsystem
* PARAMS: stateCbk - the state callback
* rxCbk - the data RX callback
* RETURN: the structure or NULL if not found
* NOTES: call with mHidLock held
*/
void gattSvcBattInit(GattSvcBattConnStateCbk stateCbk, GattSvcBattUpdateCbk rxCbk)
{
mCbkState = stateCbk;
mCbkRx = rxCbk;
}