|  | /* | 
|  | * Copyright (c) 2020 Nordic Semiconductor ASA | 
|  | * | 
|  | * SPDX-License-Identifier: Apache-2.0 | 
|  | */ | 
|  | #include <zephyr/bluetooth/mesh.h> | 
|  | #include <zephyr/sys/iterable_sections.h> | 
|  |  | 
|  | #include "net.h" | 
|  | #include "rpl.h" | 
|  | #include "access.h" | 
|  | #include "lpn.h" | 
|  | #include "settings.h" | 
|  | #include "mesh.h" | 
|  | #include "transport.h" | 
|  | #include "heartbeat.h" | 
|  | #include "foundation.h" | 
|  |  | 
|  | #define LOG_LEVEL CONFIG_BT_MESH_TRANS_LOG_LEVEL | 
|  | #include <zephyr/logging/log.h> | 
|  | LOG_MODULE_REGISTER(bt_mesh_hb); | 
|  |  | 
|  | /* 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; | 
|  | }; | 
|  |  | 
|  | 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 void notify_pub_sent(void) | 
|  | { | 
|  | STRUCT_SECTION_FOREACH(bt_mesh_hb_cb, cb) { | 
|  | if (cb->pub_sent) { | 
|  | cb->pub_sent(&pub); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | 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--; | 
|  | } | 
|  |  | 
|  | if (!err) { | 
|  | notify_pub_sent(); | 
|  | } | 
|  | } | 
|  |  | 
|  | static void notify_recv(uint8_t hops, uint16_t feat) | 
|  | { | 
|  | sub.remaining = sub_remaining(); | 
|  |  | 
|  | STRUCT_SECTION_FOREACH(bt_mesh_hb_cb, cb) { | 
|  | if (cb->recv) { | 
|  | cb->recv(&sub, hops, feat); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | static void notify_sub_end(void) | 
|  | { | 
|  | sub.remaining = 0; | 
|  |  | 
|  | STRUCT_SECTION_FOREACH(bt_mesh_hb_cb, cb) { | 
|  | if (cb->sub_end) { | 
|  | cb->sub_end(&sub); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | static void sub_end(struct k_work *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); | 
|  |  | 
|  | LOG_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 k_work *work) | 
|  | { | 
|  | static const struct bt_mesh_send_cb publish_cb = { | 
|  | .start = hb_publish_start_cb, | 
|  | .end = hb_publish_end_cb, | 
|  | }; | 
|  | struct bt_mesh_subnet *subnet; | 
|  | int err; | 
|  |  | 
|  | LOG_DBG("hb_pub.count: %u", pub.count); | 
|  |  | 
|  | /* Fast exit if disabled or expired */ | 
|  | if (pub.period == 0U || pub.count == 0U) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | subnet = bt_mesh_subnet_get(pub.net_idx); | 
|  | if (!subnet) { | 
|  | LOG_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 net_buf_simple *buf) | 
|  | { | 
|  | uint8_t init_ttl, hops; | 
|  | uint16_t feat; | 
|  |  | 
|  | if (buf->len < 3) { | 
|  | LOG_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) { | 
|  | LOG_DBG("No subscription for received heartbeat"); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | if (!k_work_delayable_is_pending(&sub_timer)) { | 
|  | LOG_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++; | 
|  | } | 
|  |  | 
|  | LOG_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) | 
|  | { | 
|  | LOG_DBG(""); | 
|  |  | 
|  | pub.dst = BT_MESH_ADDR_UNASSIGNED; | 
|  | pub.count = 0U; | 
|  | pub.period = 0U; | 
|  | pub.ttl = 0U; | 
|  | pub.feat = 0U; | 
|  | pub.net_idx = 0U; | 
|  |  | 
|  | /* Try to cancel, but it's OK if this still runs (or is | 
|  | * running) as the handler will be a no-op if it hasn't | 
|  | * already checked period for being non-zero. | 
|  | */ | 
|  | (void)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)) { | 
|  | LOG_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)) { | 
|  | LOG_WRN("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())) { | 
|  | LOG_WRN("Prohibited destination address"); | 
|  | return STATUS_INVALID_ADDRESS; | 
|  | } | 
|  |  | 
|  | if (period > (1U << 16)) { | 
|  | LOG_WRN("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(); | 
|  | } | 
|  |  | 
|  | static void hb_unsolicited_pub_end_cb(int err, void *cb_data) | 
|  | { | 
|  | if (!err) { | 
|  | notify_pub_sent(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void bt_mesh_hb_feature_changed(uint16_t features) | 
|  | { | 
|  | static const struct bt_mesh_send_cb pub_cb = { | 
|  | .end = hb_unsolicited_pub_end_cb, | 
|  | }; | 
|  |  | 
|  | if (pub.dst == BT_MESH_ADDR_UNASSIGNED) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (!(pub.feat & features)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | heartbeat_send(&pub_cb, 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) { | 
|  | LOG_DBG("Starting heartbeat publication"); | 
|  | k_work_reschedule(&pub_timer, K_NO_WAIT); | 
|  | } | 
|  | } | 
|  |  | 
|  | void bt_mesh_hb_suspend(void) | 
|  | { | 
|  | /* Best-effort suspend.  This cannot guarantee that an | 
|  | * in-progress publish will not complete. | 
|  | */ | 
|  | (void)k_work_cancel_delayable(&pub_timer); | 
|  | } | 
|  |  | 
|  | void bt_mesh_hb_resume(void) | 
|  | { | 
|  | if (pub.period && pub.count) { | 
|  | LOG_DBG("Starting heartbeat publication"); | 
|  | k_work_reschedule(&pub_timer, K_NO_WAIT); | 
|  | } | 
|  | } | 
|  |  | 
|  | static int hb_pub_set(const char *name, size_t len_rd, | 
|  | settings_read_cb read_cb, void *cb_arg) | 
|  | { | 
|  | struct bt_mesh_hb_pub hb_pub; | 
|  | struct hb_pub_val hb_val; | 
|  | int err; | 
|  |  | 
|  | err = bt_mesh_settings_set(read_cb, cb_arg, &hb_val, sizeof(hb_val)); | 
|  | if (err) { | 
|  | LOG_ERR("Failed to set \'hb_val\'"); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | hb_pub.dst = hb_val.dst; | 
|  | hb_pub.period = bt_mesh_hb_pwr2(hb_val.period); | 
|  | hb_pub.ttl = hb_val.ttl; | 
|  | hb_pub.feat = hb_val.feat; | 
|  | hb_pub.net_idx = hb_val.net_idx; | 
|  |  | 
|  | if (hb_val.indefinite) { | 
|  | hb_pub.count = 0xffff; | 
|  | } else { | 
|  | hb_pub.count = 0U; | 
|  | } | 
|  |  | 
|  | (void)bt_mesh_hb_pub_set(&hb_pub); | 
|  |  | 
|  | LOG_DBG("Restored heartbeat publication"); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | BT_MESH_SETTINGS_DEFINE(pub, "HBPub", hb_pub_set); | 
|  |  | 
|  | void bt_mesh_hb_pub_pending_store(void) | 
|  | { | 
|  | struct bt_mesh_hb_pub hb_pub; | 
|  | struct hb_pub_val val; | 
|  | int err; | 
|  |  | 
|  | bt_mesh_hb_pub_get(&hb_pub); | 
|  | if (hb_pub.dst == BT_MESH_ADDR_UNASSIGNED) { | 
|  | err = settings_delete("bt/mesh/HBPub"); | 
|  | } else { | 
|  | val.indefinite = (hb_pub.count == 0xffff); | 
|  | val.dst = hb_pub.dst; | 
|  | val.period = bt_mesh_hb_log(hb_pub.period); | 
|  | val.ttl = hb_pub.ttl; | 
|  | val.feat = hb_pub.feat; | 
|  | val.net_idx = hb_pub.net_idx; | 
|  |  | 
|  | err = settings_save_one("bt/mesh/HBPub", &val, sizeof(val)); | 
|  | } | 
|  |  | 
|  | if (err) { | 
|  | LOG_ERR("Failed to store Heartbeat Publication"); | 
|  | } else { | 
|  | LOG_DBG("Stored Heartbeat Publication"); | 
|  | } | 
|  | } |