bluetooth: host: Persist Service Changed data

Add support for persisted Service Changed data, to fix the case of a
paired device not reconnecting before a reboot and thus not receiving
SC indication. It also enables support for GATT database being changed
during a firmware update.

Move Service Changed data outside of the CCC struct and make it
persistent by adding support for a bt/sc/... setting.

Signed-off-by: François Delawarde <fnde@oticon.com>
This commit is contained in:
François Delawarde 2019-10-04 14:29:11 +02:00 committed by Carles Cufí
commit 1ce95de6ae
2 changed files with 344 additions and 69 deletions

View file

@ -563,7 +563,6 @@ struct bt_gatt_ccc_cfg {
u8_t id; u8_t id;
bt_addr_le_t peer; bt_addr_le_t peer;
u16_t value; u16_t value;
u8_t data[4] __aligned(4);
}; };
/* Internal representation of CCC value */ /* Internal representation of CCC value */

View file

@ -183,13 +183,123 @@ BT_GATT_SERVICE_DEFINE(_2_gap_svc,
#endif #endif
); );
#if defined(CONFIG_BT_GATT_SERVICE_CHANGED) struct sc_data {
static void sc_ccc_cfg_changed(const struct bt_gatt_attr *attr, u16_t start;
u16_t end;
} __packed;
struct gatt_sc_cfg {
u8_t id;
bt_addr_le_t peer;
struct sc_data data;
};
#define SC_CFG_MAX (CONFIG_BT_MAX_PAIRED + CONFIG_BT_MAX_CONN)
static struct gatt_sc_cfg sc_cfg[SC_CFG_MAX];
static struct gatt_sc_cfg *find_sc_cfg(u8_t id, bt_addr_le_t *addr)
{
BT_DBG("id: %u, addr: %s", id, bt_addr_le_str(addr));
for (size_t i = 0; i < ARRAY_SIZE(sc_cfg); i++) {
if (id == sc_cfg[i].id &&
!bt_addr_le_cmp(&sc_cfg[i].peer, addr)) {
return &sc_cfg[i];
}
}
return NULL;
}
static void sc_store(struct gatt_sc_cfg *cfg)
{
char key[BT_SETTINGS_KEY_MAX];
int err;
if (cfg->id) {
char id_str[4];
u8_to_dec(id_str, sizeof(id_str), cfg->id);
bt_settings_encode_key(key, sizeof(key), "sc",
&cfg->peer, id_str);
} else {
bt_settings_encode_key(key, sizeof(key), "sc",
&cfg->peer, NULL);
}
err = settings_save_one(key, (char *)&cfg->data, sizeof(cfg->data));
if (err) {
BT_ERR("failed to store SC (err %d)", err);
return;
}
BT_DBG("stored SC for %s (%s, 0x%04x-0x%04x)",
bt_addr_le_str(&cfg->peer), log_strdup(key), cfg->data.start,
cfg->data.end);
}
static void sc_clear(struct gatt_sc_cfg *cfg)
{
BT_DBG("peer %s", bt_addr_le_str(&cfg->peer));
if (IS_ENABLED(CONFIG_BT_SETTINGS)) {
bool modified = false;
if (cfg->data.start || cfg->data.end) {
modified = true;
}
if (modified && bt_addr_le_is_bonded(cfg->id, &cfg->peer)) {
char key[BT_SETTINGS_KEY_MAX];
int err;
if (cfg->id) {
char id_str[4];
u8_to_dec(id_str, sizeof(id_str), cfg->id);
bt_settings_encode_key(key, sizeof(key), "sc",
&cfg->peer, id_str);
} else {
bt_settings_encode_key(key, sizeof(key), "sc",
&cfg->peer, NULL);
}
err = settings_delete(key);
if (err) {
BT_ERR("failed to delete SC (err %d)", err);
} else {
BT_DBG("deleted SC for %s (%s)",
bt_addr_le_str(&cfg->peer),
log_strdup(key));
}
}
}
memset(cfg, 0, sizeof(*cfg));
}
static bool sc_ccc_cfg_write(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
u16_t value) u16_t value)
{ {
BT_DBG("value 0x%04x", value); BT_DBG("value 0x%04x", value);
if (value != BT_GATT_CCC_INDICATE) {
struct gatt_sc_cfg *cfg;
/* Clear SC configuration if unsubscribed */
cfg = find_sc_cfg(conn->id, &conn->le.dst);
if (cfg) {
sc_clear(cfg);
} }
#endif /* CONFIG_BT_GATT_SERVICE_CHANGED */ }
return true;
}
static struct _bt_gatt_ccc sc_ccc = BT_GATT_CCC_INITIALIZER(NULL,
sc_ccc_cfg_write,
NULL);
#if defined(CONFIG_BT_GATT_CACHING) #if defined(CONFIG_BT_GATT_CACHING)
enum { enum {
@ -508,7 +618,7 @@ BT_GATT_SERVICE_DEFINE(_1_gatt_svc,
*/ */
BT_GATT_CHARACTERISTIC(BT_UUID_GATT_SC, BT_GATT_CHRC_INDICATE, BT_GATT_CHARACTERISTIC(BT_UUID_GATT_SC, BT_GATT_CHRC_INDICATE,
BT_GATT_PERM_NONE, NULL, NULL, NULL), BT_GATT_PERM_NONE, NULL, NULL, NULL),
BT_GATT_CCC(sc_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), BT_GATT_CCC_MANAGED(&sc_ccc, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
#if defined(CONFIG_BT_GATT_CACHING) #if defined(CONFIG_BT_GATT_CACHING)
BT_GATT_CHARACTERISTIC(BT_UUID_GATT_CLIENT_FEATURES, BT_GATT_CHARACTERISTIC(BT_UUID_GATT_CLIENT_FEATURES,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE, BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
@ -608,7 +718,6 @@ populate:
} }
#endif /* CONFIG_BT_GATT_DYNAMIC_DB */ #endif /* CONFIG_BT_GATT_DYNAMIC_DB */
#if defined(CONFIG_BT_GATT_SERVICE_CHANGED)
enum { enum {
SC_RANGE_CHANGED, /* SC range changed */ SC_RANGE_CHANGED, /* SC range changed */
SC_INDICATE_PENDING, /* SC indicate pending */ SC_INDICATE_PENDING, /* SC indicate pending */
@ -685,7 +794,6 @@ static void sc_process(struct k_work *work)
atomic_set_bit(sc->flags, SC_INDICATE_PENDING); atomic_set_bit(sc->flags, SC_INDICATE_PENDING);
} }
#endif /* CONFIG_BT_GATT_SERVICE_CHANGED */
#if defined(CONFIG_BT_SETTINGS_CCC_STORE_ON_WRITE) #if defined(CONFIG_BT_SETTINGS_CCC_STORE_ON_WRITE)
static struct gatt_ccc_store { static struct gatt_ccc_store {
@ -758,15 +866,22 @@ void bt_gatt_init(void)
*/ */
k_delayed_work_submit(&db_hash_work, DB_HASH_TIMEOUT); k_delayed_work_submit(&db_hash_work, DB_HASH_TIMEOUT);
#endif /* CONFIG_BT_GATT_CACHING */ #endif /* CONFIG_BT_GATT_CACHING */
#if defined(CONFIG_BT_GATT_SERVICE_CHANGED)
if (IS_ENABLED(CONFIG_BT_GATT_SERVICE_CHANGED)) {
k_delayed_work_init(&gatt_sc.work, sc_process); k_delayed_work_init(&gatt_sc.work, sc_process);
#endif /* CONFIG_BT_GATT_SERVICE_CHANGED */ if (IS_ENABLED(CONFIG_BT_SETTINGS)) {
/* Make sure to not send SC indications until SC
* settings are loaded
*/
atomic_set_bit(gatt_sc.flags, SC_INDICATE_PENDING);
}
}
#if defined(CONFIG_BT_SETTINGS_CCC_STORE_ON_WRITE) #if defined(CONFIG_BT_SETTINGS_CCC_STORE_ON_WRITE)
k_delayed_work_init(&gatt_ccc_store.work, ccc_delayed_store); k_delayed_work_init(&gatt_ccc_store.work, ccc_delayed_store);
#endif #endif
} }
#if defined(CONFIG_BT_GATT_SERVICE_CHANGED)
static bool update_range(u16_t *start, u16_t *end, u16_t new_start, static bool update_range(u16_t *start, u16_t *end, u16_t new_start,
u16_t new_end) u16_t new_end)
{ {
@ -790,28 +905,32 @@ static bool update_range(u16_t *start, u16_t *end, u16_t new_start,
return true; return true;
} }
static void sc_indicate(struct gatt_sc *sc, u16_t start, u16_t end) #if defined(CONFIG_BT_GATT_DYNAMIC_DB) || \
(defined(CONFIG_BT_GATT_CACHING) && defined(CONFIG_BT_SETTINGS))
static void sc_indicate(u16_t start, u16_t end)
{ {
if (!atomic_test_and_set_bit(sc->flags, SC_RANGE_CHANGED)) { BT_DBG("start 0x%04x end 0x%04x", start, end);
sc->start = start;
sc->end = end; if (!atomic_test_and_set_bit(gatt_sc.flags, SC_RANGE_CHANGED)) {
gatt_sc.start = start;
gatt_sc.end = end;
goto submit; goto submit;
} }
if (!update_range(&sc->start, &sc->end, start, end)) { if (!update_range(&gatt_sc.start, &gatt_sc.end, start, end)) {
return; return;
} }
submit: submit:
if (atomic_test_bit(sc->flags, SC_INDICATE_PENDING)) { if (atomic_test_bit(gatt_sc.flags, SC_INDICATE_PENDING)) {
BT_DBG("indicate pending, waiting until complete..."); BT_DBG("indicate pending, waiting until complete...");
return; return;
} }
/* Reschedule since the range has changed */ /* Reschedule since the range has changed */
k_delayed_work_submit(&sc->work, SC_TIMEOUT); k_delayed_work_submit(&gatt_sc.work, SC_TIMEOUT);
} }
#endif /* CONFIG_BT_GATT_SERVICE_CHANGED */ #endif /* BT_GATT_DYNAMIC_DB || (BT_GATT_CACHING && BT_SETTINGS) */
#if defined(CONFIG_BT_GATT_DYNAMIC_DB) #if defined(CONFIG_BT_GATT_DYNAMIC_DB)
static void db_changed(void) static void db_changed(void)
@ -867,7 +986,7 @@ int bt_gatt_service_register(struct bt_gatt_service *svc)
return err; return err;
} }
sc_indicate(&gatt_sc, svc->attrs[0].handle, sc_indicate(svc->attrs[0].handle,
svc->attrs[svc->attr_count - 1].handle); svc->attrs[svc->attr_count - 1].handle);
db_changed(); db_changed();
@ -883,7 +1002,7 @@ int bt_gatt_service_unregister(struct bt_gatt_service *svc)
return -ENOENT; return -ENOENT;
} }
sc_indicate(&gatt_sc, svc->attrs[0].handle, sc_indicate(svc->attrs[0].handle,
svc->attrs[svc->attr_count - 1].handle); svc->attrs[svc->attr_count - 1].handle);
db_changed(); db_changed();
@ -1199,7 +1318,6 @@ static void clear_ccc_cfg(struct bt_gatt_ccc_cfg *cfg)
bt_addr_le_copy(&cfg->peer, BT_ADDR_LE_ANY); bt_addr_le_copy(&cfg->peer, BT_ADDR_LE_ANY);
cfg->id = 0U; cfg->id = 0U;
cfg->value = 0U; cfg->value = 0U;
memset(cfg->data, 0, sizeof(cfg->data));
} }
static struct bt_gatt_ccc_cfg *find_ccc_cfg(const struct bt_conn *conn, static struct bt_gatt_ccc_cfg *find_ccc_cfg(const struct bt_conn *conn,
@ -1459,7 +1577,8 @@ static int gatt_indicate(struct bt_conn *conn, u16_t handle,
* notifications and indications to such a client until it becomes * notifications and indications to such a client until it becomes
* change-aware. * change-aware.
*/ */
if (!(params->func && params->func == sc_indicate_rsp) && if (!(params->func && (params->func == sc_indicate_rsp ||
params->func == sc_restore_rsp)) &&
!bt_gatt_change_aware(conn, false)) { !bt_gatt_change_aware(conn, false)) {
return -EAGAIN; return -EAGAIN;
} }
@ -1487,40 +1606,43 @@ static int gatt_indicate(struct bt_conn *conn, u16_t handle,
return gatt_send(conn, buf, gatt_indicate_rsp, params, NULL); return gatt_send(conn, buf, gatt_indicate_rsp, params, NULL);
} }
#if defined(CONFIG_BT_GATT_SERVICE_CHANGED) static void sc_save(u8_t id, bt_addr_le_t *peer, u16_t start, u16_t end)
struct sc_data {
u16_t start;
u16_t end;
};
static void sc_save(struct bt_gatt_ccc_cfg *cfg,
struct bt_gatt_indicate_params *params)
{ {
struct sc_data data; struct gatt_sc_cfg *cfg;
struct sc_data *stored; bool modified = false;
memcpy(&data, params->data, params->len); BT_DBG("peer %s start 0x%04x end 0x%04x", bt_addr_le_str(peer), start,
end);
data.start = sys_le16_to_cpu(data.start); cfg = find_sc_cfg(id, peer);
data.end = sys_le16_to_cpu(data.end); if (!cfg) {
/* Find and initialize a free sc_cfg entry */
cfg = find_sc_cfg(BT_ID_DEFAULT, BT_ADDR_LE_ANY);
if (!cfg) {
BT_ERR("unable to save SC: no cfg left");
return;
}
/* Load data stored */ cfg->id = id;
stored = (struct sc_data *)cfg->data; bt_addr_le_copy(&cfg->peer, peer);
}
/* Check if there is any change stored */ /* Check if there is any change stored */
if (!stored->start && !stored->end) { if (!(cfg->data.start || cfg->data.end)) {
*stored = data; cfg->data.start = start;
cfg->data.end = end;
modified = true;
goto done; goto done;
} }
update_range(&stored->start, &stored->end, modified = update_range(&cfg->data.start, &cfg->data.end, start, end);
data.start, data.end);
done: done:
BT_DBG("peer %s start 0x%04x end 0x%04x", bt_addr_le_str(&cfg->peer), if (IS_ENABLED(CONFIG_BT_SETTINGS) &&
stored->start, stored->end); modified && bt_addr_le_is_bonded(cfg->id, &cfg->peer)) {
sc_store(cfg);
}
} }
#endif /* CONFIG_BT_GATT_SERVICE_CHANGED */
static u8_t notify_cb(const struct bt_gatt_attr *attr, void *user_data) static u8_t notify_cb(const struct bt_gatt_attr *attr, void *user_data)
{ {
@ -1550,10 +1672,14 @@ static u8_t notify_cb(const struct bt_gatt_attr *attr, void *user_data)
conn = bt_conn_lookup_addr_le(cfg->id, &cfg->peer); conn = bt_conn_lookup_addr_le(cfg->id, &cfg->peer);
if (!conn) { if (!conn) {
if (IS_ENABLED(CONFIG_BT_GATT_SERVICE_CHANGED)) { if (IS_ENABLED(CONFIG_BT_GATT_SERVICE_CHANGED) &&
if (ccc->cfg_changed == sc_ccc_cfg_changed) { ccc == &sc_ccc) {
sc_save(cfg, data->ind_params); struct sc_data *sc;
}
sc = (struct sc_data *)data->ind_params->data;
sc_save(cfg->id, &cfg->peer,
sys_le16_to_cpu(sc->start),
sys_le16_to_cpu(sc->end));
} }
continue; continue;
} }
@ -1763,24 +1889,66 @@ u8_t bt_gatt_check_perm(struct bt_conn *conn, const struct bt_gatt_attr *attr,
return 0; return 0;
} }
#if defined(CONFIG_BT_GATT_SERVICE_CHANGED) static void sc_restore_rsp(struct bt_conn *conn,
static void sc_restore(struct bt_gatt_ccc_cfg *cfg) const struct bt_gatt_attr *attr, u8_t err)
{ {
struct sc_data *data = (struct sc_data *)cfg->data; #if defined(CONFIG_BT_GATT_CACHING)
struct gatt_cf_cfg *cfg;
#endif
if (!data->start && !data->end) { BT_DBG("err 0x%02x", err);
#if defined(CONFIG_BT_GATT_CACHING)
/* BLUETOOTH CORE SPECIFICATION Version 5.1 | Vol 3, Part G page 2347:
* 2.5.2.1 Robust Caching
* A connected client becomes change-aware when...
* The client receives and confirms a Service Changed indication.
*/
cfg = find_cf_cfg(conn);
if (cfg && CF_ROBUST_CACHING(cfg)) {
atomic_set_bit(cfg->flags, CF_CHANGE_AWARE);
BT_DBG("%s change-aware", bt_addr_le_str(&cfg->peer));
}
#endif
}
static struct bt_gatt_indicate_params sc_restore_params[CONFIG_BT_MAX_CONN];
static void sc_restore(struct bt_conn *conn)
{
struct gatt_sc_cfg *cfg;
u16_t sc_range[2];
u8_t index;
cfg = find_sc_cfg(conn->id, &conn->le.dst);
if (!cfg) {
BT_DBG("no SC data found");
return;
}
if (!(cfg->data.start || cfg->data.end)) {
return; return;
} }
BT_DBG("peer %s start 0x%04x end 0x%04x", bt_addr_le_str(&cfg->peer), BT_DBG("peer %s start 0x%04x end 0x%04x", bt_addr_le_str(&cfg->peer),
data->start, data->end); cfg->data.start, cfg->data.end);
sc_indicate(&gatt_sc, data->start, data->end); sc_range[0] = sys_cpu_to_le16(cfg->data.start);
sc_range[1] = sys_cpu_to_le16(cfg->data.end);
index = bt_conn_index(conn);
sc_restore_params[index].attr = &_1_gatt_svc.attrs[2];
sc_restore_params[index].func = sc_restore_rsp;
sc_restore_params[index].data = &sc_range[0];
sc_restore_params[index].len = sizeof(sc_range);
if (bt_gatt_indicate(conn, &sc_restore_params[index])) {
BT_ERR("SC restore indication failed");
}
/* Reset config data */ /* Reset config data */
(void)memset(cfg->data, 0, sizeof(cfg->data)); sc_clear(cfg);
} }
#endif /* CONFIG_BT_GATT_SERVICE_CHANGED */
struct conn_data { struct conn_data {
struct bt_conn *conn; struct bt_conn *conn;
@ -1836,10 +2004,9 @@ static u8_t update_ccc(const struct bt_gatt_attr *attr, void *user_data)
if (ccc->cfg[i].value) { if (ccc->cfg[i].value) {
gatt_ccc_changed(attr, ccc); gatt_ccc_changed(attr, ccc);
if (IS_ENABLED(CONFIG_BT_GATT_SERVICE_CHANGED)) { if (IS_ENABLED(CONFIG_BT_GATT_SERVICE_CHANGED) &&
if (ccc->cfg_changed == sc_ccc_cfg_changed) { ccc == &sc_ccc) {
sc_restore(&ccc->cfg[i]); sc_restore(conn);
}
} }
return BT_GATT_ITER_CONTINUE; return BT_GATT_ITER_CONTINUE;
} }
@ -3694,6 +3861,20 @@ static int bt_gatt_clear_cf(u8_t id, const bt_addr_le_t *addr)
return settings_delete(key); return settings_delete(key);
#endif /* CONFIG_BT_GATT_CACHING */ #endif /* CONFIG_BT_GATT_CACHING */
return 0; return 0;
}
static int sc_clear_by_addr(u8_t id, const bt_addr_le_t *addr)
{
if (IS_ENABLED(CONFIG_BT_GATT_SERVICE_CHANGED)) {
struct gatt_sc_cfg *cfg;
cfg = find_sc_cfg(id, (bt_addr_le_t *)addr);
if (cfg) {
sc_clear(cfg);
}
}
return 0;
} }
int bt_gatt_clear(u8_t id, const bt_addr_le_t *addr) int bt_gatt_clear(u8_t id, const bt_addr_le_t *addr)
@ -3705,7 +3886,17 @@ int bt_gatt_clear(u8_t id, const bt_addr_le_t *addr)
return err; return err;
} }
return bt_gatt_clear_cf(id, addr); err = sc_clear_by_addr(id, addr);
if (err < 0) {
return err;
}
err = bt_gatt_clear_cf(id, addr);
if (err < 0) {
return err;
}
return 0;
} }
static void ccc_clear(struct _bt_gatt_ccc *ccc, static void ccc_clear(struct _bt_gatt_ccc *ccc,
@ -3845,6 +4036,82 @@ static int ccc_set(const char *name, size_t len_rd, settings_read_cb read_cb,
SETTINGS_STATIC_HANDLER_DEFINE(bt_ccc, "bt/ccc", NULL, ccc_set, NULL, NULL); SETTINGS_STATIC_HANDLER_DEFINE(bt_ccc, "bt/ccc", NULL, ccc_set, NULL, NULL);
#if defined(CONFIG_BT_GATT_SERVICE_CHANGED)
static int sc_set(const char *name, size_t len_rd, settings_read_cb read_cb,
void *cb_arg)
{
struct gatt_sc_cfg *cfg;
u8_t id;
bt_addr_le_t addr;
int len, err;
const char *next;
if (!name) {
BT_ERR("Insufficient number of arguments");
return -EINVAL;
}
err = bt_settings_decode_key(name, &addr);
if (err) {
BT_ERR("Unable to decode address %s", log_strdup(name));
return -EINVAL;
}
settings_name_next(name, &next);
if (!next) {
id = BT_ID_DEFAULT;
} else {
id = strtol(next, NULL, 10);
}
cfg = find_sc_cfg(id, &addr);
if (!cfg && len_rd) {
/* Find and initialize a free sc_cfg entry */
cfg = find_sc_cfg(BT_ID_DEFAULT, BT_ADDR_LE_ANY);
if (!cfg) {
BT_ERR("Unable to restore SC: no cfg left");
return -ENOMEM;
}
cfg->id = id;
bt_addr_le_copy(&cfg->peer, &addr);
}
if (len_rd) {
len = read_cb(cb_arg, &cfg->data, sizeof(cfg->data));
if (len < 0) {
BT_ERR("Failed to decode value (err %d)", len);
return len;
}
BT_DBG("Read SC: len %d", len);
BT_DBG("Restored SC for %s", bt_addr_le_str(&addr));
} else if (cfg) {
/* Clear configuration */
memset(cfg, 0, sizeof(*cfg));
BT_DBG("Removed SC for %s", bt_addr_le_str(&addr));
}
return 0;
}
static int sc_commit(void)
{
atomic_clear_bit(gatt_sc.flags, SC_INDICATE_PENDING);
if (atomic_test_bit(gatt_sc.flags, SC_RANGE_CHANGED)) {
/* Schedule SC indication since the range has changed */
k_delayed_work_submit(&gatt_sc.work, SC_TIMEOUT);
}
return 0;
}
SETTINGS_STATIC_HANDLER_DEFINE(bt_sc, "bt/sc", NULL, sc_set, sc_commit, NULL);
#endif /* CONFIG_BT_GATT_SERVICE_CHANGED */
#if defined(CONFIG_BT_GATT_CACHING) #if defined(CONFIG_BT_GATT_CACHING)
static int cf_set(const char *name, size_t len_rd, settings_read_cb read_cb, static int cf_set(const char *name, size_t len_rd, settings_read_cb read_cb,
void *cb_arg) void *cb_arg)
@ -3927,6 +4194,15 @@ static int db_hash_commit(void)
BT_HEXDUMP_DBG(db_hash, sizeof(db_hash), "New Hash: "); BT_HEXDUMP_DBG(db_hash, sizeof(db_hash), "New Hash: ");
/**
* GATT database has been modified since last boot, likely due to
* a firmware update or a dynamic service that was not re-registered on
* boot. Indicate Service Changed to all bonded devices for the full
* database range to invalidate client-side cache and force discovery on
* reconnect.
*/
sc_indicate(0x0001, 0xffff);
/* Hash did not match overwrite with current hash */ /* Hash did not match overwrite with current hash */
db_hash_store(); db_hash_store();