nimble/host/mesh/src/heartbeat.c (357 lines of code) (raw):
/*
* Copyright (c) 2020 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#define MESH_LOG_MODULE BLE_MESH_HEARTBEAT_LOG
#include "mesh_priv.h"
#include "net.h"
#include "rpl.h"
#include "access.h"
#include "lpn.h"
#include "settings.h"
#include "transport.h"
#include "heartbeat.h"
#include "foundation.h"
#include "mesh/glue.h"
/* Heartbeat Publication information for persistent storage. */
struct hb_pub_val {
uint16_t dst;
uint8_t period;
uint8_t ttl;
uint16_t feat;
uint16_t net_idx:12,
indefinite:1;
};
struct bt_mesh_hb_cb hb_cb;
static struct bt_mesh_hb_pub pub;
static struct bt_mesh_hb_sub sub;
static struct k_work_delayable sub_timer;
static struct k_work_delayable pub_timer;
static int64_t sub_remaining(void)
{
if (sub.dst == BT_MESH_ADDR_UNASSIGNED) {
return 0U;
}
uint32_t rem_ms = k_ticks_to_ms_floor32(
k_work_delayable_remaining_get(&sub_timer));
return rem_ms / MSEC_PER_SEC;
}
static void hb_publish_end_cb(int err, void *cb_data)
{
if (pub.period && pub.count > 1) {
k_work_reschedule(&pub_timer, K_SECONDS(pub.period));
}
if (pub.count != 0xffff) {
pub.count--;
}
}
static void notify_recv(uint8_t hops, uint16_t feat)
{
sub.remaining = sub_remaining();
if (hb_cb.recv != NULL) {
hb_cb.recv(&sub, hops, feat);
}
}
static void notify_sub_end(void)
{
sub.remaining = 0;
if (hb_cb.sub_end != NULL) {
hb_cb.sub_end(&sub);
}
}
static void sub_end(struct ble_npl_event *work)
{
notify_sub_end();
}
static int heartbeat_send(const struct bt_mesh_send_cb *cb, void *cb_data)
{
uint16_t feat = 0U;
struct __packed {
uint8_t init_ttl;
uint16_t feat;
} hb;
struct bt_mesh_msg_ctx ctx = {
.net_idx = pub.net_idx,
.app_idx = BT_MESH_KEY_UNUSED,
.addr = pub.dst,
.send_ttl = pub.ttl,
};
struct bt_mesh_net_tx tx = {
.sub = bt_mesh_subnet_get(pub.net_idx),
.ctx = &ctx,
.src = bt_mesh_primary_addr(),
.xmit = bt_mesh_net_transmit_get(),
};
/* Do nothing if heartbeat publication is not enabled or the subnet is
* removed.
*/
if (!tx.sub || pub.dst == BT_MESH_ADDR_UNASSIGNED) {
return 0;
}
hb.init_ttl = pub.ttl;
if (bt_mesh_relay_get() == BT_MESH_RELAY_ENABLED) {
feat |= BT_MESH_FEAT_RELAY;
}
if (bt_mesh_gatt_proxy_get() == BT_MESH_GATT_PROXY_ENABLED) {
feat |= BT_MESH_FEAT_PROXY;
}
if (bt_mesh_friend_get() == BT_MESH_FRIEND_ENABLED) {
feat |= BT_MESH_FEAT_FRIEND;
}
if (bt_mesh_lpn_established()) {
feat |= BT_MESH_FEAT_LOW_POWER;
}
hb.feat = sys_cpu_to_be16(feat);
BT_DBG("InitTTL %u feat 0x%04x", pub.ttl, feat);
return bt_mesh_ctl_send(&tx, TRANS_CTL_OP_HEARTBEAT, &hb, sizeof(hb),
cb, cb_data);
}
static void hb_publish_start_cb(uint16_t duration, int err, void *cb_data)
{
if (err) {
hb_publish_end_cb(err, cb_data);
}
}
static void hb_publish(struct ble_npl_event *work)
{
static const struct bt_mesh_send_cb publish_cb = {
.start = hb_publish_start_cb,
.end = hb_publish_end_cb,
};
struct bt_mesh_subnet *sub;
int err;
BT_DBG("hb_pub.count: %u", pub.count);
/* Fast exit if disabled or expired */
if (pub.period == 0U || pub.count == 0U) {
return;
}
sub = bt_mesh_subnet_get(pub.net_idx);
if (!sub) {
BT_ERR("No matching subnet for idx 0x%02x", pub.net_idx);
pub.dst = BT_MESH_ADDR_UNASSIGNED;
return;
}
err = heartbeat_send(&publish_cb, NULL);
if (err) {
hb_publish_end_cb(err, NULL);
}
}
int bt_mesh_hb_recv(struct bt_mesh_net_rx *rx, struct os_mbuf *buf)
{
uint8_t init_ttl, hops;
uint16_t feat;
if (buf->om_len < 3) {
BT_ERR("Too short heartbeat message");
return -EINVAL;
}
init_ttl = (net_buf_simple_pull_u8(buf) & 0x7f);
feat = net_buf_simple_pull_be16(buf);
hops = (init_ttl - rx->ctx.recv_ttl + 1);
if (rx->ctx.addr != sub.src || rx->ctx.recv_dst != sub.dst) {
BT_DBG("No subscription for received heartbeat");
return 0;
}
if (!k_work_delayable_is_pending(&sub_timer)) {
BT_DBG("Heartbeat subscription inactive");
return 0;
}
sub.min_hops = MIN(sub.min_hops, hops);
sub.max_hops = MAX(sub.max_hops, hops);
if (sub.count < 0xffff) {
sub.count++;
}
BT_DBG("src 0x%04x TTL %u InitTTL %u (%u hop%s) feat 0x%04x",
rx->ctx.addr, rx->ctx.recv_ttl, init_ttl, hops,
(hops == 1U) ? "" : "s", feat);
notify_recv(hops, feat);
return 0;
}
static void pub_disable(void)
{
BT_DBG("");
pub.dst = BT_MESH_ADDR_UNASSIGNED;
pub.count = 0U;
pub.ttl = 0U;
pub.period = 0U;
k_work_cancel_delayable(&pub_timer);
}
uint8_t bt_mesh_hb_pub_set(struct bt_mesh_hb_pub *new_pub)
{
if (!new_pub || new_pub->dst == BT_MESH_ADDR_UNASSIGNED) {
pub_disable();
if (IS_ENABLED(CONFIG_BT_SETTINGS) &&
bt_mesh_is_provisioned()) {
bt_mesh_settings_store_schedule(
BT_MESH_SETTINGS_HB_PUB_PENDING);
}
return STATUS_SUCCESS;
}
if (!bt_mesh_subnet_get(new_pub->net_idx)) {
BT_ERR("Unknown NetKey 0x%04x", new_pub->net_idx);
return STATUS_INVALID_NETKEY;
}
new_pub->feat &= BT_MESH_FEAT_SUPPORTED;
pub = *new_pub;
if (!bt_mesh_is_provisioned()) {
return STATUS_SUCCESS;
}
/* The first Heartbeat message shall be published as soon as possible
* after the Heartbeat Publication Period state has been configured for
* periodic publishing.
*
* If the new configuration disables publishing this flushes
* the work item.
*/
k_work_reschedule(&pub_timer, K_NO_WAIT);
if (IS_ENABLED(CONFIG_BT_SETTINGS)) {
bt_mesh_settings_store_schedule(
BT_MESH_SETTINGS_HB_PUB_PENDING);
}
return STATUS_SUCCESS;
}
void bt_mesh_hb_pub_get(struct bt_mesh_hb_pub *get)
{
*get = pub;
}
uint8_t bt_mesh_hb_sub_set(uint16_t src, uint16_t dst, uint32_t period)
{
if (src != BT_MESH_ADDR_UNASSIGNED && !BT_MESH_ADDR_IS_UNICAST(src)) {
BT_WARN("Prohibited source address");
return STATUS_INVALID_ADDRESS;
}
if (BT_MESH_ADDR_IS_VIRTUAL(dst) || BT_MESH_ADDR_IS_RFU(dst) ||
(BT_MESH_ADDR_IS_UNICAST(dst) && dst != bt_mesh_primary_addr())) {
BT_WARN("Prohibited destination address");
return STATUS_INVALID_ADDRESS;
}
if (period > (1U << 16)) {
BT_WARN("Prohibited subscription period %u s", period);
return STATUS_CANNOT_SET;
}
/* Only an explicit address change to unassigned should trigger clearing
* of the values according to MESH/NODE/CFG/HBS/BV-02-C.
*/
if (src == BT_MESH_ADDR_UNASSIGNED || dst == BT_MESH_ADDR_UNASSIGNED) {
sub.src = BT_MESH_ADDR_UNASSIGNED;
sub.dst = BT_MESH_ADDR_UNASSIGNED;
sub.min_hops = 0U;
sub.max_hops = 0U;
sub.count = 0U;
sub.period = 0U;
} else if (period) {
sub.src = src;
sub.dst = dst;
sub.min_hops = BT_MESH_TTL_MAX;
sub.max_hops = 0U;
sub.count = 0U;
sub.period = period;
} else {
/* Clearing the period should stop heartbeat subscription
* without clearing the parameters, so we can still read them.
*/
sub.period = 0U;
}
/* Start the timer, which notifies immediately if the new
* configuration disables the subscription.
*/
k_work_reschedule(&sub_timer, K_SECONDS(sub.period));
return STATUS_SUCCESS;
}
void bt_mesh_hb_sub_reset_count(void)
{
sub.count = 0;
}
void bt_mesh_hb_sub_get(struct bt_mesh_hb_sub *get)
{
*get = sub;
get->remaining = sub_remaining();
}
void bt_mesh_hb_feature_changed(uint16_t features)
{
if (pub.dst == BT_MESH_ADDR_UNASSIGNED) {
return;
}
if (!(pub.feat & features)) {
return;
}
heartbeat_send(NULL, NULL);
}
void bt_mesh_hb_init(void)
{
pub.net_idx = BT_MESH_KEY_UNUSED;
k_work_init_delayable(&pub_timer, hb_publish);
k_work_init_delayable(&sub_timer, sub_end);
}
void bt_mesh_hb_start(void)
{
if (pub.count && pub.period) {
BT_DBG("Starting heartbeat publication");
k_work_reschedule(&pub_timer, K_NO_WAIT);
}
}
void bt_mesh_hb_suspend(void)
{
(void)k_work_cancel_delayable(&pub_timer);
}
void bt_mesh_hb_resume(void)
{
if (pub.period && pub.count) {
BT_DBG("Starting heartbeat publication");
k_work_reschedule(&pub_timer, K_NO_WAIT);
}
}
#if MYNEWT_VAL(BLE_MESH_SETTINGS)
static int hb_pub_set(int argc, char **argv, char *val)
{
struct bt_mesh_hb_pub pub;
struct hb_pub_val hb_val;
int len, err;
BT_DBG("val %s", val ? val : "(null)");
len = sizeof(hb_val);
err = settings_bytes_from_str(val, &hb_val, &len);
if (err) {
BT_ERR("Failed to decode value %s (err %d)", val, err);
return err;
}
if (len != sizeof(hb_val)) {
BT_ERR("Unexpected value length (%d != %zu)", len,
sizeof(hb_val));
return -EINVAL;
}
pub.dst = hb_val.dst;
pub.period = bt_mesh_hb_pwr2(hb_val.period);
pub.ttl = hb_val.ttl;
pub.feat = hb_val.feat;
pub.net_idx = hb_val.net_idx;
if (hb_val.indefinite) {
pub.count = 0xffff;
} else {
pub.count = 0;
}
(void) bt_mesh_hb_pub_set(&pub);
BT_DBG("Restored heartbeat publication");
return 0;
}
void bt_mesh_hb_pub_pending_store(void)
{
struct bt_mesh_hb_pub pub;
char buf[BT_SETTINGS_SIZE(sizeof(struct hb_pub_val))];
struct hb_pub_val val;
int err;
char *str;
bt_mesh_hb_pub_get(&pub);
if (pub.dst == BT_MESH_ADDR_UNASSIGNED) {
err = settings_save_one("bt_mesh/HBPub", NULL);
} else {
val.indefinite = (pub.count == 0xffff);
val.dst = pub.dst;
val.period = bt_mesh_hb_log(pub.period);
val.ttl = pub.ttl;
val.feat = pub.feat;
val.net_idx = pub.net_idx;
str = settings_str_from_bytes(&val, sizeof(val), buf, sizeof(buf));
if (!str) {
BT_ERR("Unable to encode configuration as value");
return;
}
BT_DBG("Saving configuration as value %s", str);
err = settings_save_one("bt_mesh/HBPub", str);
}
if (err) {
BT_ERR("Failed to store Heartbeat Publication");
} else {
BT_DBG("Stored Heartbeat Publication");
}
}
static struct conf_handler bt_mesh_hb_pub_conf_handler = {
.ch_name = "bt_mesh",
.ch_get = NULL,
.ch_set = hb_pub_set,
.ch_commit = NULL,
.ch_export = NULL,
};
#endif
void bt_mesh_hb_pub_init(void)
{
#if MYNEWT_VAL(BLE_MESH_SETTINGS)
int rc;
rc = conf_register(&bt_mesh_hb_pub_conf_handler);
SYSINIT_PANIC_ASSERT_MSG(rc == 0,
"Failed to register bt_mesh_hb_pub conf");
#endif
}