/* * 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 }