mesh: Move heartbeat to separate module

Encapsulates the Heartbeat state and functionality in a separate
heartbeat module, removing all manipulation of the heartbeat state from
the transport and config server modules.

this is port of 0dc9e5cd9695e4ab403bf13f4d51d84de6a5ebb9
diff --git a/nimble/host/mesh/include/mesh/cfg_srv.h b/nimble/host/mesh/include/mesh/cfg_srv.h
index f7adcec..c9045bc 100644
--- a/nimble/host/mesh/include/mesh/cfg_srv.h
+++ b/nimble/host/mesh/include/mesh/cfg_srv.h
@@ -33,8 +33,11 @@
 	uint8_t frnd;                 /* Friend state */
 	uint8_t default_ttl;          /* Default TTL */
 
-	/* Heartbeat Publication */
-	struct bt_mesh_hb_pub {
+	/** Heartbeat Publication parameters.
+	 *
+	 * @deprecated in favor of standalone API in bluetooth/mesh/heartbeat.h.
+	 */
+	struct {
 		struct k_delayed_work timer;
 
 		uint16_t dst;
@@ -43,10 +46,13 @@
 		uint8_t  ttl;
 		uint16_t feat;
 		uint16_t net_idx;
-	} hb_pub;
+	} hb_pub; /* _deprectated */
 
-	/* Heartbeat Subscription */
-	struct bt_mesh_hb_sub {
+	/** Heartbeat Subscription parameters.
+	 *
+	 * @deprecated in favor of standalone API in bluetooth/mesh/heartbeat.h.
+	 */
+	struct {
 		int64_t  expiry;
 
 		uint16_t src;
@@ -57,7 +63,7 @@
 
 		/* Optional subscription tracking function */
 		void (*func)(uint8_t hops, uint16_t feat);
-	} hb_sub;
+	} hb_sub; /* _deprectated */
 };
 
 extern const struct bt_mesh_model_op bt_mesh_cfg_srv_op[];
diff --git a/nimble/host/mesh/include/mesh/glue.h b/nimble/host/mesh/include/mesh/glue.h
index 6d080f3..237bd05 100644
--- a/nimble/host/mesh/include/mesh/glue.h
+++ b/nimble/host/mesh/include/mesh/glue.h
@@ -286,6 +286,7 @@
 #define BT_GATT_CCC_NOTIFY BLE_GATT_CHR_PROP_NOTIFY
 
 #define MIN(a, b) (((a) < (b)) ? (a) : (b))
+#define MAX(a, b) (((a) > (b)) ? (a) : (b))
 /** Description of different data types that can be encoded into
   * advertising data. Used to form arrays that are passed to the
   * bt_le_adv_start() function.
@@ -336,6 +337,7 @@
 void k_work_init(struct ble_npl_callout *work, ble_npl_event_fn handler);
 void k_delayed_work_init(struct k_delayed_work *w, ble_npl_event_fn *f);
 void k_delayed_work_cancel(struct k_delayed_work *w);
+bool k_delayed_work_pending(struct k_delayed_work *w);
 void k_delayed_work_submit(struct k_delayed_work *w, uint32_t ms);
 int64_t k_uptime_get(void);
 uint32_t k_uptime_get_32(void);
diff --git a/nimble/host/mesh/include/mesh/heartbeat.h b/nimble/host/mesh/include/mesh/heartbeat.h
new file mode 100644
index 0000000..b9990f6
--- /dev/null
+++ b/nimble/host/mesh/include/mesh/heartbeat.h
@@ -0,0 +1,123 @@
+/** @file
+ *  @brief Bluetooth Mesh Heartbeat API.
+ */
+
+/*
+ * Copyright (c) 2020 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#ifndef _BLUETOOTH_MESH_HEARTBEAT_H_
+#define _BLUETOOTH_MESH_HEARTBEAT_H_
+
+/**
+ * @brief Bluetooth Mesh
+ * @defgroup bt_mesh_heartbeat Bluetooth Mesh Heartbeat
+ * @ingroup bt_mesh
+ * @{
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Heartbeat Publication parameters */
+struct bt_mesh_hb_pub {
+	/** Destination address. */
+	uint16_t dst;
+	/** Remaining publish count. */
+	uint16_t count;
+	/** Time To Live value. */
+	uint8_t ttl;
+	/**
+	 * Bitmap of features that trigger a Heartbeat publication if
+	 * they change. Legal values are @ref BT_MESH_FEAT_RELAY,
+	 * @ref BT_MESH_FEAT_PROXY, @ref BT_MESH_FEAT_FRIEND and
+	 * @ref BT_MESH_FEAT_LOW_POWER.
+	 */
+	uint16_t feat;
+	/** Network index used for publishing. */
+	uint16_t net_idx;
+	/** Publication period in seconds. */
+	uint32_t period;
+};
+
+/** Heartbeat Subscription parameters. */
+struct bt_mesh_hb_sub {
+	/** Subscription period in seconds. */
+	uint32_t period;
+	/** Remaining subscription time in seconds. */
+	uint32_t remaining;
+	/** Source address to receive Heartbeats from. */
+	uint16_t src;
+	/** Destination address to received Heartbeats on. */
+	uint16_t dst;
+	/** The number of received Heartbeat messages so far. */
+	uint16_t count;
+	/**
+	 * Minimum hops in received messages, ie the shortest registered
+	 * path from the publishing node to the subscribing node. A
+	 * Heartbeat received from an immediate neighbor has hop
+	 * count = 1.
+	 */
+	uint8_t min_hops;
+	/**
+	 * Maximum hops in received messages, ie the longest registered
+	 * path from the publishing node to the subscribing node. A
+	 * Heartbeat received from an immediate neighbor has hop
+	 * count = 1.
+	 */
+	uint8_t max_hops;
+};
+
+/** Heartbeat callback structure */
+struct bt_mesh_hb_cb {
+	/** @brief Receive callback for heartbeats.
+	 *
+	 *  Gets called on every received Heartbeat that matches the current
+	 *  Heartbeat subscription parameters.
+	 *
+	 *  @param sub  Current Heartbeat subscription parameters.
+	 *  @param hops The number of hops the Heartbeat was received
+	 *              with.
+	 *  @param feat The feature set of the publishing node. The
+	 *              value is a bitmap of @ref BT_MESH_FEAT_RELAY,
+	 *              @ref BT_MESH_FEAT_PROXY,
+	 *              @ref BT_MESH_FEAT_FRIEND and
+	 *              @ref BT_MESH_FEAT_LOW_POWER.
+	 */
+	void (*recv)(const struct bt_mesh_hb_sub *sub, uint8_t hops,
+		     uint16_t feat);
+
+	/** @brief Subscription end callback for heartbeats.
+	 *
+	 *  Gets called when the subscription period ends, providing a summary
+	 *  of the received heartbeat messages.
+	 *
+	 *  @param sub Current Heartbeat subscription parameters.
+	 */
+	void (*sub_end)(const struct bt_mesh_hb_sub *sub);
+};
+
+/** @brief Get the current Heartbeat publication parameters.
+ *
+ *  @param get Heartbeat publication parameters return buffer.
+ */
+void bt_mesh_hb_pub_get(struct bt_mesh_hb_pub *get);
+
+/** @brief Get the current Heartbeat subscription parameters.
+ *
+ *  @param get Heartbeat subscription parameters return buffer.
+ */
+void bt_mesh_hb_sub_get(struct bt_mesh_hb_sub *get);
+
+extern struct bt_mesh_hb_cb hb_cb;
+
+#ifdef __cplusplus
+}
+#endif
+/**
+ * @}
+ */
+
+#endif /* _BLUETOOTH_MESH_HEARTBEAT_H_ */
\ No newline at end of file
diff --git a/nimble/host/mesh/src/cfg_srv.c b/nimble/host/mesh/src/cfg_srv.c
index 08595fe..82ae608 100644
--- a/nimble/host/mesh/src/cfg_srv.c
+++ b/nimble/host/mesh/src/cfg_srv.c
@@ -22,6 +22,7 @@
 #include "rpl.h"
 #include "lpn.h"
 #include "transport.h"
+#include "heartbeat.h"
 #include "crypto.h"
 #include "access.h"
 #include "beacon.h"
@@ -643,9 +644,7 @@
 
 	bt_mesh_adv_update();
 
-	if (cfg->hb_pub.feat & BT_MESH_FEAT_PROXY) {
-		(void)bt_mesh_heartbeat_send(NULL, NULL);
-	}
+	bt_mesh_hb_feature_changed(BT_MESH_FEAT_PROXY);
 
 send_status:
 	send_gatt_proxy_status(model, ctx);
@@ -757,9 +756,7 @@
 		       BT_MESH_TRANSMIT_COUNT(cfg->relay_retransmit),
 		       BT_MESH_TRANSMIT_INT(cfg->relay_retransmit));
 
-		if ((cfg->hb_pub.feat & BT_MESH_FEAT_RELAY) && change) {
-			(void)bt_mesh_heartbeat_send(NULL, NULL);
-		}
+		bt_mesh_hb_feature_changed(BT_MESH_FEAT_RELAY);
 	} else {
 		BT_WARN("Invalid Relay value 0x%02x", buf->om_data[0]);
 		goto done;
@@ -1744,18 +1741,6 @@
 	send_net_key_status(model, ctx, idx, status);
 }
 
-static void hb_pub_disable(struct bt_mesh_cfg_srv *cfg)
-{
-	BT_DBG("");
-
-	cfg->hb_pub.dst = BT_MESH_ADDR_UNASSIGNED;
-	cfg->hb_pub.count = 0;
-	cfg->hb_pub.ttl = 0;
-	cfg->hb_pub.period = 0;
-
-	k_delayed_work_cancel(&cfg->hb_pub.timer);
-}
-
 static void net_key_del(struct bt_mesh_model *model,
 			struct bt_mesh_msg_ctx *ctx,
 			struct os_mbuf *buf)
@@ -2201,9 +2186,7 @@
 		}
 	}
 
-	if (cfg->hb_pub.feat & BT_MESH_FEAT_FRIEND) {
-		(void)bt_mesh_heartbeat_send(NULL, NULL);
-	}
+	bt_mesh_hb_feature_changed(BT_MESH_FEAT_FRIEND);
 
 send_status:
 	send_friend_status(model, ctx);
@@ -2317,17 +2300,6 @@
 	send_krp_status(model, ctx, idx, phase, status);
 }
 
-static uint8_t hb_log(uint16_t val)
-{
-	if (!val) {
-		return 0x00;
-	} else if (val == 0xffff) {
-		return 0xff;
-	} else {
-		return 32 - __builtin_clz(val);
-	}
-}
-
 static uint8_t hb_pub_count_log(uint16_t val)
 {
 	if (!val) {
@@ -2341,17 +2313,6 @@
 	}
 }
 
-static uint16_t hb_pwr2(uint8_t val, uint8_t sub)
-{
-	if (!val) {
-		return 0x0000;
-	} else if (val == 0xff || val == 0x11) {
-		return 0xffff;
-	} else {
-		return (1 << (val - sub));
-	}
-}
-
 struct hb_pub_param {
 	uint16_t dst;
 	uint8_t  count_log;
@@ -2363,10 +2324,9 @@
 
 static void hb_pub_send_status(struct bt_mesh_model *model,
 			       struct bt_mesh_msg_ctx *ctx, uint8_t status,
-			       struct hb_pub_param *orig_msg)
+			        const struct bt_mesh_hb_pub *pub)
 {
 	struct os_mbuf *msg = BT_MESH_MODEL_BUF(OP_HEARTBEAT_PUB_STATUS, 10);
-	struct bt_mesh_cfg_srv *cfg = model->user_data;
 
 	BT_DBG("src 0x%04x status 0x%02x", ctx->addr, status);
 
@@ -2374,20 +2334,13 @@
 
 	net_buf_simple_add_u8(msg, status);
 
-	if (orig_msg) {
-		memcpy(net_buf_simple_add(msg, sizeof(*orig_msg)), orig_msg,
-		       sizeof(*orig_msg));
-		goto send;
-	}
+	net_buf_simple_add_le16(msg, pub->dst);
+	net_buf_simple_add_u8(msg, hb_pub_count_log(pub->count));
+	net_buf_simple_add_u8(msg, bt_mesh_hb_log(pub->period));
+	net_buf_simple_add_u8(msg, pub->ttl);
+	net_buf_simple_add_le16(msg, pub->feat);
+	net_buf_simple_add_le16(msg, pub->net_idx);
 
-	net_buf_simple_add_le16(msg, cfg->hb_pub.dst);
-	net_buf_simple_add_u8(msg, hb_pub_count_log(cfg->hb_pub.count));
-	net_buf_simple_add_u8(msg, cfg->hb_pub.period);
-	net_buf_simple_add_u8(msg, cfg->hb_pub.ttl);
-	net_buf_simple_add_le16(msg, cfg->hb_pub.feat);
-	net_buf_simple_add_le16(msg, cfg->hb_pub.net_idx);
-
-send:
 	if (bt_mesh_model_send(model, ctx, msg, NULL, NULL)) {
 		BT_ERR("Unable to send Heartbeat Publication Status");
 	}
@@ -2399,9 +2352,13 @@
 			      struct bt_mesh_msg_ctx *ctx,
 			      struct os_mbuf *buf)
 {
+	struct bt_mesh_hb_pub pub;
+
 	BT_DBG("src 0x%04x", ctx->addr);
 
-	hb_pub_send_status(model, ctx, STATUS_SUCCESS, NULL);
+	bt_mesh_hb_pub_get(&pub);
+
+	hb_pub_send_status(model, ctx, STATUS_SUCCESS, &pub);
 }
 
 static void heartbeat_pub_set(struct bt_mesh_model *model,
@@ -2409,27 +2366,32 @@
 			      struct os_mbuf *buf)
 {
 	struct hb_pub_param *param = (void *)buf->om_data;
-	struct bt_mesh_cfg_srv *cfg = model->user_data;
-	uint16_t dst, feat, idx;
+	struct bt_mesh_hb_pub pub;
 	uint8_t status;
 
 	BT_DBG("src 0x%04x", ctx->addr);
 
-	dst = sys_le16_to_cpu(param->dst);
+	pub.dst = sys_le16_to_cpu(param->dst);
+	pub.count = bt_mesh_hb_pwr2(param->count_log);
+	pub.period = bt_mesh_hb_pwr2(param->period_log);
+	pub.ttl = param->ttl;
+	pub.feat = sys_le16_to_cpu(param->feat);
+	pub.net_idx = sys_le16_to_cpu(param->net_idx);
+
 	/* All other address types but virtual are valid */
-	if (BT_MESH_ADDR_IS_VIRTUAL(dst)) {
+	if (BT_MESH_ADDR_IS_VIRTUAL(pub.dst)) {
 		status = STATUS_INVALID_ADDRESS;
-		goto failed;
+		goto rsp;
 	}
 
 	if (param->count_log > 0x11 && param->count_log != 0xff) {
 		status = STATUS_CANNOT_SET;
-		goto failed;
+		goto rsp;
 	}
 
 	if (param->period_log > 0x10) {
 		status = STATUS_CANNOT_SET;
-		goto failed;
+		goto rsp;
 	}
 
 	if (param->ttl > BT_MESH_TTL_MAX && param->ttl != BT_MESH_TTL_DEFAULT) {
@@ -2437,84 +2399,32 @@
 		return;
 	}
 
-	feat = sys_le16_to_cpu(param->feat);
-
-	idx = sys_le16_to_cpu(param->net_idx);
-	if (idx > 0xfff) {
-		BT_ERR("Invalid NetKeyIndex 0x%04x", idx);
+	if (pub.net_idx > 0xfff) {
+		BT_ERR("Invalid NetKeyIndex 0x%04x", pub.net_idx);
 		return;
 	}
 
-	if (!bt_mesh_subnet_get(idx)) {
-		status = STATUS_INVALID_NETKEY;
-		goto failed;
-	}
-
-	cfg->hb_pub.dst = dst;
-	cfg->hb_pub.period = param->period_log;
-	cfg->hb_pub.feat = feat & BT_MESH_FEAT_SUPPORTED;
-	cfg->hb_pub.net_idx = idx;
-
-	if (dst == BT_MESH_ADDR_UNASSIGNED) {
-		hb_pub_disable(cfg);
-	} else {
-		/* 2^(n-1) */
-		cfg->hb_pub.count = hb_pwr2(param->count_log, 1);
-		cfg->hb_pub.ttl = param->ttl;
-
-		BT_DBG("period %u ms", hb_pwr2(param->period_log, 1) * 1000);
-
-		/* The first Heartbeat message shall be published as soon
-		 * as possible after the Heartbeat Publication Period state
-		 * has been configured for periodic publishing.
-		 */
-		if (param->period_log && param->count_log) {
-			k_work_submit(&cfg->hb_pub.timer.work);
-		} else {
-			k_delayed_work_cancel(&cfg->hb_pub.timer);
-		}
-	}
-
-	if (IS_ENABLED(CONFIG_BT_SETTINGS)) {
-		bt_mesh_store_hb_pub();
-	}
-
-	hb_pub_send_status(model, ctx, STATUS_SUCCESS, NULL);
-
-	return;
-
-failed:
-	hb_pub_send_status(model, ctx, status, param);
+	status = bt_mesh_hb_pub_set(&pub);
+rsp:
+	hb_pub_send_status(model, ctx, status, &pub);
 }
 
 static void hb_sub_send_status(struct bt_mesh_model *model,
-			       struct bt_mesh_msg_ctx *ctx, uint8_t status)
+			       struct bt_mesh_msg_ctx *ctx,
+			       const struct bt_mesh_hb_sub *sub)
 {
 	struct os_mbuf *msg = BT_MESH_MODEL_BUF(OP_HEARTBEAT_SUB_STATUS, 9);
-	struct bt_mesh_cfg_srv *cfg = model->user_data;
-	uint16_t period;
-	int64_t uptime;
-
-	BT_DBG("src 0x%04x status 0x%02x", ctx->addr, status);
-
-	uptime = k_uptime_get();
-	if (uptime > cfg->hb_sub.expiry) {
-		period = 0;
-	} else {
-		period = (cfg->hb_sub.expiry - uptime) / 1000;
-	}
+	BT_DBG("src 0x%04x ", ctx->addr);
 
 	bt_mesh_model_msg_init(msg, OP_HEARTBEAT_SUB_STATUS);
 
-	net_buf_simple_add_u8(msg, status);
-
-	net_buf_simple_add_le16(msg, cfg->hb_sub.src);
-	net_buf_simple_add_le16(msg, cfg->hb_sub.dst);
-
-	net_buf_simple_add_u8(msg, hb_log(period));
-	net_buf_simple_add_u8(msg, hb_log(cfg->hb_sub.count));
-	net_buf_simple_add_u8(msg, cfg->hb_sub.min_hops);
-	net_buf_simple_add_u8(msg, cfg->hb_sub.max_hops);
+	net_buf_simple_add_u8(msg, STATUS_SUCCESS);
+	net_buf_simple_add_le16(msg, sub->src);
+	net_buf_simple_add_le16(msg, sub->dst);
+	net_buf_simple_add_u8(msg, bt_mesh_hb_log(sub->remaining));
+	net_buf_simple_add_u8(msg, bt_mesh_hb_log(sub->count));
+	net_buf_simple_add_u8(msg, sub->min_hops);
+	net_buf_simple_add_u8(msg, sub->max_hops);
 
 
 	if (bt_mesh_model_send(model, ctx, msg, NULL, NULL)) {
@@ -2528,92 +2438,58 @@
 			      struct bt_mesh_msg_ctx *ctx,
 			      struct os_mbuf *buf)
 {
+	struct bt_mesh_hb_sub sub;
+
 	BT_DBG("src 0x%04x", ctx->addr);
 
-	hb_sub_send_status(model, ctx, STATUS_SUCCESS);
+	bt_mesh_hb_sub_get(&sub);
+
+	hb_sub_send_status(model, ctx, &sub);
 }
 
 static void heartbeat_sub_set(struct bt_mesh_model *model,
 			      struct bt_mesh_msg_ctx *ctx,
 			      struct os_mbuf *buf)
 {
-	struct bt_mesh_cfg_srv *cfg = model->user_data;
+	uint8_t period_log, status;
+	struct bt_mesh_hb_sub sub;
 	uint16_t sub_src, sub_dst;
-	uint8_t sub_period;
-	int32_t period_ms;
+	uint32_t period;
 
 	BT_DBG("src 0x%04x", ctx->addr);
 
 	sub_src = net_buf_simple_pull_le16(buf);
 	sub_dst = net_buf_simple_pull_le16(buf);
-	sub_period = net_buf_simple_pull_u8(buf);
+	period_log = net_buf_simple_pull_u8(buf);
 
 	BT_DBG("sub_src 0x%04x sub_dst 0x%04x period 0x%02x",
-	       sub_src, sub_dst, sub_period);
+	       sub_src, sub_dst, period_log);
 
-	if (sub_src != BT_MESH_ADDR_UNASSIGNED &&
-	    !BT_MESH_ADDR_IS_UNICAST(sub_src)) {
-		BT_WARN("Prohibited source address");
+	if (period_log > 0x11) {
+		BT_WARN("Prohibited subscription period 0x%02x", period_log);
 		return;
 	}
 
-	if (BT_MESH_ADDR_IS_VIRTUAL(sub_dst) || BT_MESH_ADDR_IS_RFU(sub_dst) ||
-	    (BT_MESH_ADDR_IS_UNICAST(sub_dst) &&
-	     sub_dst != bt_mesh_primary_addr())) {
-		BT_WARN("Prohibited destination address");
-		return;
-	}
+	period = bt_mesh_hb_pwr2(period_log);
 
-	if (sub_period > 0x11) {
-		BT_WARN("Prohibited subscription period 0x%02x", sub_period);
-		return;
-	}
-
-	if (sub_src == BT_MESH_ADDR_UNASSIGNED ||
-	    sub_dst == BT_MESH_ADDR_UNASSIGNED ||
-	    sub_period == 0x00) {
-		/* Only an explicit address change to unassigned should
-		 * trigger clearing of the values according to
-		 * MESH/NODE/CFG/HBS/BV-02-C.
+	status = bt_mesh_hb_sub_set(sub_src, sub_dst, period);
+	if (status != STATUS_SUCCESS) {
+		/* All errors are caused by invalid packets, which should be
+		 * ignored.
 		 */
-		if (sub_src == BT_MESH_ADDR_UNASSIGNED ||
-		    sub_dst == BT_MESH_ADDR_UNASSIGNED) {
-			cfg->hb_sub.src = BT_MESH_ADDR_UNASSIGNED;
-			cfg->hb_sub.dst = BT_MESH_ADDR_UNASSIGNED;
-			cfg->hb_sub.min_hops = BT_MESH_TTL_MAX;
-			cfg->hb_sub.max_hops = 0;
-			cfg->hb_sub.count = 0;
-		}
-
-		period_ms = 0;
-	} else {
-		cfg->hb_sub.src = sub_src;
-		cfg->hb_sub.dst = sub_dst;
-		cfg->hb_sub.min_hops = BT_MESH_TTL_MAX;
-		cfg->hb_sub.max_hops = 0;
-		cfg->hb_sub.count = 0;
-		period_ms = hb_pwr2(sub_period, 1) * 1000;
+		return;
 	}
 
-	/* Let the transport layer know it needs to handle this address */
-	bt_mesh_set_hb_sub_dst(cfg->hb_sub.dst);
-
-	BT_DBG("period_ms %u", (unsigned) period_ms);
-
-	if (period_ms) {
-		cfg->hb_sub.expiry = k_uptime_get() + period_ms;
-	} else {
-		cfg->hb_sub.expiry = 0;
-	}
-
-	hb_sub_send_status(model, ctx, STATUS_SUCCESS);
+	bt_mesh_hb_sub_get(&sub);
 
 	/* MESH/NODE/CFG/HBS/BV-01-C expects the MinHops to be 0x7f after
 	 * disabling subscription, but 0x00 for subsequent Get requests.
 	 */
-	if (!period_ms) {
-		cfg->hb_sub.min_hops = 0;
+	if (!period_log) {
+		sub.min_hops = BT_MESH_TTL_MAX;
 	}
+
+	hb_sub_send_status(model, ctx, &sub);
 }
 
 const struct bt_mesh_model_op bt_mesh_cfg_srv_op[] = {
@@ -2667,59 +2543,6 @@
 	BT_MESH_MODEL_OP_END,
 };
 
-static void hb_publish_end_cb(int err, void *cb_data)
-{
-	struct bt_mesh_cfg_srv *cfg = cb_data;
-	uint16_t period_ms;
-
-	period_ms = hb_pwr2(cfg->hb_pub.period, 1) * 1000U;
-	if (period_ms && cfg->hb_pub.count > 1) {
-		k_delayed_work_submit(&cfg->hb_pub.timer, K_MSEC(period_ms));
-	}
-
-	if (cfg->hb_pub.count != 0xffff) {
-		cfg->hb_pub.count--;
-	}
-}
-
-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_cfg_srv *cfg = ble_npl_event_get_arg(work);
-	struct bt_mesh_subnet *sub;
-	int err;
-
-	BT_DBG("hb_pub.count: %u", cfg->hb_pub.count);
-
-	sub = bt_mesh_subnet_get(cfg->hb_pub.net_idx);
-	if (!sub) {
-		BT_ERR("No matching subnet for idx 0x%02x",
-		       cfg->hb_pub.net_idx);
-		cfg->hb_pub.dst = BT_MESH_ADDR_UNASSIGNED;
-		return;
-	}
-
-	if (cfg->hb_pub.count == 0) {
-		return;
-	}
-
-	err = bt_mesh_heartbeat_send(&publish_cb, cfg);
-	if (err) {
-		hb_publish_end_cb(err, cfg);
-	}
-}
-
 static bool conf_is_valid(struct bt_mesh_cfg_srv *cfg)
 {
 	if (cfg->relay > 0x02) {
@@ -2745,6 +2568,14 @@
 	return true;
 }
 
+static void (*hb_sub_cb)(uint8_t hops, uint16_t features);
+struct bt_mesh_hb_cb hb_cb;
+
+static void hb_recv_wrapper(const struct bt_mesh_hb_sub *sub, uint8_t hops, uint16_t features)
+{
+	hb_sub_cb(hops, features);
+}
+
 static int cfg_srv_init(struct bt_mesh_model *model)
 {
 	struct bt_mesh_cfg_srv *cfg = model->user_data;
@@ -2768,6 +2599,11 @@
 		return -EINVAL;
 	}
 
+	if (cfg->hb_sub.func) {
+		hb_sub_cb = cfg->hb_sub.func;
+		hb_cb.recv = hb_recv_wrapper;
+	}
+
 	/*
 	 * Configuration Model security is device-key based and only the local
 	 * device-key is allowed to access this model.
@@ -2786,11 +2622,6 @@
 		cfg->gatt_proxy = BT_MESH_GATT_PROXY_NOT_SUPPORTED;
 	}
 
-	k_delayed_work_init(&cfg->hb_pub.timer, hb_publish);
-	k_delayed_work_add_arg(&cfg->hb_pub.timer, cfg);
-	cfg->hb_pub.net_idx = BT_MESH_KEY_UNUSED;
-	cfg->hb_sub.expiry = 0;
-
 	cfg->model = model;
 
 	conf = cfg;
@@ -2828,49 +2659,9 @@
 
 void bt_mesh_cfg_reset(void)
 {
-	struct bt_mesh_cfg_srv *cfg = conf;
-
-	BT_DBG("");
-
-	bt_mesh_set_hb_sub_dst(BT_MESH_ADDR_UNASSIGNED);
-
-	cfg->hb_sub.src = BT_MESH_ADDR_UNASSIGNED;
-	cfg->hb_sub.dst = BT_MESH_ADDR_UNASSIGNED;
-	cfg->hb_sub.expiry = 0;
-
 	bt_mesh_model_foreach(mod_reset, NULL);
 }
 
-void bt_mesh_heartbeat(uint16_t src, uint16_t dst, uint8_t hops, uint16_t feat)
-{
-	struct bt_mesh_cfg_srv *cfg = conf;
-
-	if (src != cfg->hb_sub.src || dst != cfg->hb_sub.dst) {
-		BT_WARN("No subscription for received heartbeat");
-		return;
-	}
-
-	if (k_uptime_get() > cfg->hb_sub.expiry) {
-		BT_WARN("Heartbeat subscription period expired");
-		return;
-	}
-
-	cfg->hb_sub.min_hops = min(cfg->hb_sub.min_hops, hops);
-	cfg->hb_sub.max_hops = max(cfg->hb_sub.max_hops, hops);
-
-	if (cfg->hb_sub.count < 0xffff) {
-		cfg->hb_sub.count++;
-	}
-
-	BT_DBG("src 0x%04x dst 0x%04x hops %u min %u max %u count %u", src,
-	       dst, hops, cfg->hb_sub.min_hops, cfg->hb_sub.max_hops,
-	       cfg->hb_sub.count);
-
-	if (cfg->hb_sub.func) {
-		cfg->hb_sub.func(hops, feat);
-	}
-}
-
 uint8_t bt_mesh_net_transmit_get(void)
 {
 	if (conf) {
@@ -2935,16 +2726,6 @@
 	return DEFAULT_TTL;
 }
 
-struct bt_mesh_hb_pub *bt_mesh_hb_pub_get(void)
-{
-	return &conf->hb_pub;
-}
-
-void bt_mesh_hb_pub_disable(void)
-{
-	hb_pub_disable(conf);
-}
-
 struct bt_mesh_cfg_srv *bt_mesh_cfg_get(void)
 {
 	return conf;
diff --git a/nimble/host/mesh/src/foundation.h b/nimble/host/mesh/src/foundation.h
index b424e04..4444763 100644
--- a/nimble/host/mesh/src/foundation.h
+++ b/nimble/host/mesh/src/foundation.h
@@ -128,12 +128,8 @@
 
 void bt_mesh_cfg_reset(void);
 
-void bt_mesh_heartbeat(uint16_t src, uint16_t dst, uint8_t hops, uint16_t feat);
-
 void bt_mesh_attention(struct bt_mesh_model *model, uint8_t time);
 
-struct bt_mesh_hb_pub *bt_mesh_hb_pub_get(void);
-void bt_mesh_hb_pub_disable(void);
 struct bt_mesh_cfg_srv *bt_mesh_cfg_get(void);
 
 uint8_t bt_mesh_net_transmit_get(void);
diff --git a/nimble/host/mesh/src/glue.c b/nimble/host/mesh/src/glue.c
index e1c2ae3..2651199 100644
--- a/nimble/host/mesh/src/glue.c
+++ b/nimble/host/mesh/src/glue.c
@@ -408,6 +408,12 @@
 #endif
 }
 
+bool
+k_delayed_work_pending(struct k_delayed_work *w)
+{
+    return ble_npl_callout_is_active(&w->work);
+}
+
 void
 k_delayed_work_cancel(struct k_delayed_work *w)
 {
diff --git a/nimble/host/mesh/src/heartbeat.c b/nimble/host/mesh/src/heartbeat.c
new file mode 100644
index 0000000..d4defdf
--- /dev/null
+++ b/nimble/host/mesh/src/heartbeat.c
@@ -0,0 +1,350 @@
+/*
+ * 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"
+
+
+static struct bt_mesh_hb_pub pub;
+static struct bt_mesh_hb_sub sub;
+static struct k_delayed_work sub_timer;
+static struct k_delayed_work pub_timer;
+
+static int64_t sub_remaining(void)
+{
+	if (sub.dst == BT_MESH_ADDR_UNASSIGNED) {
+		return 0U;
+	}
+
+	return k_delayed_work_remaining_get(&sub_timer) / MSEC_PER_SEC;
+}
+
+static void hb_publish_end_cb(int err, void *cb_data)
+{
+	if (pub.period && pub.count > 1) {
+		k_delayed_work_submit(&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 */
+	if (pub.dst == BT_MESH_ADDR_UNASSIGNED) {
+		return 0U;
+	}
+
+	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);
+
+	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;
+	}
+
+	if (pub.count == 0U) {
+		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_delayed_work_pending(&sub_timer)) {
+		BT_DBG("Heartbeat subscription period expired");
+		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_delayed_work_cancel(&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_store_hb_pub();
+		}
+
+		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 (pub.period && pub.count) {
+		k_work_submit(&pub_timer.work);
+	} else {
+		k_delayed_work_cancel(&pub_timer);
+	}
+
+	if (IS_ENABLED(CONFIG_BT_SETTINGS)) {
+		bt_mesh_store_hb_pub();
+	}
+
+	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 = sub.period - sub_remaining();
+		k_delayed_work_cancel(&sub_timer);
+		notify_sub_end();
+	} 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;
+		k_delayed_work_submit(&sub_timer, K_SECONDS(period));
+	} else {
+		/* Clearing the period should stop heartbeat subscription
+		 * without clearing the parameters, so we can still read them.
+		 */
+		sub.period = sub.period - sub_remaining();
+		k_delayed_work_cancel(&sub_timer);
+		notify_sub_end();
+	}
+
+	return STATUS_SUCCESS;
+}
+
+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_delayed_work_init(&pub_timer, hb_publish);
+	k_delayed_work_init(&sub_timer, sub_end);
+}
+
+void bt_mesh_hb_start(void)
+{
+	if (pub.count && pub.period) {
+		BT_DBG("Starting heartbeat publication");
+		k_work_submit(&pub_timer.work);
+	}
+}
+
+void bt_mesh_hb_suspend(void)
+{
+	k_delayed_work_cancel(&pub_timer);
+}
+
+void bt_mesh_hb_resume(void)
+{
+	if (pub.period && pub.count) {
+		BT_DBG("Starting heartbeat publication");
+		k_work_submit(&pub_timer.work);
+	}
+}
diff --git a/nimble/host/mesh/src/heartbeat.h b/nimble/host/mesh/src/heartbeat.h
new file mode 100644
index 0000000..225fa03
--- /dev/null
+++ b/nimble/host/mesh/src/heartbeat.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2020 Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "mesh/heartbeat.h"
+
+static inline uint16_t bt_mesh_hb_pwr2(uint8_t val)
+{
+	if (!val) {
+		return 0x0000;
+	} else if (val == 0xff || val == 0x11) {
+		return 0xffff;
+	} else {
+		return (1 << (val - 1));
+	}
+}
+
+static inline uint8_t bt_mesh_hb_log(uint32_t val)
+{
+	if (!val) {
+		return 0x00;
+	} else if (val == 0xffff) {
+		return 0xff;
+	} else {
+		return 32 - __builtin_clz(val);
+	}
+}
+
+void bt_mesh_hb_init(void);
+void bt_mesh_hb_start(void);
+void bt_mesh_hb_suspend(void);
+void bt_mesh_hb_resume(void);
+
+int bt_mesh_hb_recv(struct bt_mesh_net_rx *rx, struct os_mbuf *buf);
+void bt_mesh_hb_feature_changed(uint16_t features);
+
+uint8_t bt_mesh_hb_pub_set(struct bt_mesh_hb_pub *hb_pub);
+uint8_t bt_mesh_hb_sub_set(uint16_t src, uint16_t dst, uint32_t period);
\ No newline at end of file
diff --git a/nimble/host/mesh/src/lpn.c b/nimble/host/mesh/src/lpn.c
index df0c6c2..947d20f 100644
--- a/nimble/host/mesh/src/lpn.c
+++ b/nimble/host/mesh/src/lpn.c
@@ -19,6 +19,7 @@
 #include "adv.h"
 #include "net.h"
 #include "transport.h"
+#include "heartbeat.h"
 #include "access.h"
 #include "beacon.h"
 #include "foundation.h"
@@ -195,7 +196,6 @@
 
 static void clear_friendship(bool force, bool disable)
 {
-	struct bt_mesh_cfg_srv *cfg = bt_mesh_cfg_get();
 	struct bt_mesh_lpn *lpn = &bt_mesh.lpn;
 
 	BT_DBG("force %u disable %u", force, disable);
@@ -242,9 +242,7 @@
 	 */
 	lpn->groups_changed = 1;
 
-	if (cfg->hb_pub.feat & BT_MESH_FEAT_LOW_POWER) {
-		(void)bt_mesh_heartbeat_send(NULL, NULL);
-	}
+	bt_mesh_hb_feature_changed(BT_MESH_FEAT_LOW_POWER);
 
 	if (disable) {
 		lpn_set_state(BT_MESH_LPN_DISABLED);
@@ -959,8 +957,6 @@
 	}
 
 	if (!lpn->established) {
-		struct bt_mesh_cfg_srv *cfg = bt_mesh_cfg_get();
-
 		/* This is normally checked on the transport layer, however
 		 * in this state we're also still accepting master
 		 * credentials so we need to ensure the right ones (Friend
@@ -975,9 +971,7 @@
 
 		BT_INFO("Friendship established with 0x%04x", lpn->frnd);
 
-		if (cfg->hb_pub.feat & BT_MESH_FEAT_LOW_POWER) {
-			(void)bt_mesh_heartbeat_send(NULL, NULL);
-		}
+		bt_mesh_hb_feature_changed(BT_MESH_FEAT_LOW_POWER);
 
 		if (lpn_cb) {
 			lpn_cb(lpn->frnd, true);
diff --git a/nimble/host/mesh/src/mesh.c b/nimble/host/mesh/src/mesh.c
index 2f9d3e3..01b8811 100644
--- a/nimble/host/mesh/src/mesh.c
+++ b/nimble/host/mesh/src/mesh.c
@@ -30,6 +30,7 @@
 #include "access.h"
 #include "foundation.h"
 #include "proxy.h"
+#include "heartbeat.h"
 #include "shell.h"
 #include "mesh_priv.h"
 #include "settings.h"
@@ -261,7 +262,7 @@
 		return err;
 	}
 
-	bt_mesh_hb_pub_disable();
+	bt_mesh_hb_suspend();
 
 	if (bt_mesh_beacon_get() == BT_MESH_BEACON_ENABLED) {
 		bt_mesh_beacon_disable();
@@ -303,6 +304,8 @@
 		return err;
 	}
 
+	bt_mesh_hb_resume();
+
 	if (bt_mesh_beacon_get() == BT_MESH_BEACON_ENABLED) {
 		bt_mesh_beacon_enable();
 	}
@@ -340,6 +343,7 @@
 
 	bt_mesh_net_init();
 	bt_mesh_trans_init();
+	bt_mesh_hb_init();
 	bt_mesh_beacon_init();
 	bt_mesh_adv_init();
 
@@ -403,6 +407,9 @@
 
 		bt_mesh_prov_complete(sub->net_idx, addr);
 	}
+
+	bt_mesh_hb_start();
+
 	bt_mesh_model_foreach(model_start, NULL);
 
 	return 0;
diff --git a/nimble/host/mesh/src/settings.c b/nimble/host/mesh/src/settings.c
index 7f60ad9..6aa6701 100644
--- a/nimble/host/mesh/src/settings.c
+++ b/nimble/host/mesh/src/settings.c
@@ -18,6 +18,7 @@
 #include "rpl.h"
 #include "crypto.h"
 #include "transport.h"
+#include "heartbeat.h"
 #include "access.h"
 #include "foundation.h"
 #include "proxy.h"
@@ -420,27 +421,12 @@
 
 static int hb_pub_set(int argc, char **argv, char *val)
 {
-	struct bt_mesh_hb_pub *pub = bt_mesh_hb_pub_get();
+	struct bt_mesh_hb_pub pub;
 	struct hb_pub_val hb_val;
 	int len, err;
 
 	BT_DBG("val %s", val ? val : "(null)");
 
-	if (!pub) {
-		return -ENOENT;
-	}
-
-	if (!val) {
-		pub->dst = BT_MESH_ADDR_UNASSIGNED;
-		pub->count = 0;
-		pub->ttl = 0;
-		pub->period = 0;
-		pub->feat = 0;
-
-		BT_DBG("Cleared heartbeat publication");
-		return 0;
-	}
-
 	len = sizeof(hb_val);
 	err = settings_bytes_from_str(val, &hb_val, &len);
 	if (err) {
@@ -454,18 +440,20 @@
 		return -EINVAL;
 	}
 
-	pub->dst = hb_val.dst;
-	pub->period = hb_val.period;
-	pub->ttl = hb_val.ttl;
-	pub->feat = hb_val.feat;
-	pub->net_idx = hb_val.net_idx;
+	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;
+		pub.count = 0xffff;
 	} else {
-		pub->count = 0;
+		pub.count = 0;
 	}
 
+	(void)bt_mesh_hb_pub_set(&pub);
+
 	BT_DBG("Restored heartbeat publication");
 
 	return 0;
@@ -1032,7 +1020,6 @@
 
 static int mesh_commit(void)
 {
-	struct bt_mesh_hb_pub *hb_pub;
 	struct bt_mesh_cfg_srv *cfg;
 
 	if (!bt_mesh_subnet_next(NULL)) {
@@ -1050,13 +1037,6 @@
 
 	bt_mesh_model_foreach(commit_mod, NULL);
 
-	hb_pub = bt_mesh_hb_pub_get();
-	if (hb_pub && hb_pub->dst != BT_MESH_ADDR_UNASSIGNED &&
-	    hb_pub->count && hb_pub->period) {
-		BT_DBG("Starting heartbeat publication");
-		k_work_submit(&hb_pub->timer.work);
-	}
-
 	cfg = bt_mesh_cfg_get();
 	if (cfg && stored_cfg.valid) {
 		cfg->net_transmit = stored_cfg.cfg.net_transmit;
@@ -1305,24 +1285,20 @@
 static void store_pending_hb_pub(void)
 {
 	char buf[BT_SETTINGS_SIZE(sizeof(struct hb_pub_val))];
-	struct bt_mesh_hb_pub *pub = bt_mesh_hb_pub_get();
 	struct hb_pub_val val;
 	char *str;
 	int err;
 
-	if (!pub) {
-		return;
-	}
-
-	if (pub->dst == BT_MESH_ADDR_UNASSIGNED) {
+	bt_mesh_hb_pub_get(&pub);
+	if (pub.dst == BT_MESH_ADDR_UNASSIGNED) {
 		str = NULL;
 	} else {
-		val.indefinite = (pub->count == 0xffff);
-		val.dst = pub->dst;
-		val.period = pub->period;
-		val.ttl = pub->ttl;
-		val.feat = pub->feat;
-		val.net_idx = pub->net_idx;
+		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));
diff --git a/nimble/host/mesh/src/subnet.h b/nimble/host/mesh/src/subnet.h
index da0a1df..154b5d4 100644
--- a/nimble/host/mesh/src/subnet.h
+++ b/nimble/host/mesh/src/subnet.h
@@ -9,7 +9,7 @@
 
 #include <stdint.h>
 #include <sys/types.h>
-
+#include "mesh/glue.h"
 #define BT_MESH_NET_FLAG_KR       BIT(0)
 #define BT_MESH_NET_FLAG_IVU      BIT(1)
 
diff --git a/nimble/host/mesh/src/transport.c b/nimble/host/mesh/src/transport.c
index 42ec8ea..c4ed595 100644
--- a/nimble/host/mesh/src/transport.c
+++ b/nimble/host/mesh/src/transport.c
@@ -26,6 +26,7 @@
 #include "access.h"
 #include "foundation.h"
 #include "settings.h"
+#include "heartbeat.h"
 #include "transport.h"
 #include "testing.h"
 
@@ -116,13 +117,6 @@
 
 static struct bt_mesh_va virtual_addrs[CONFIG_BT_MESH_LABEL_COUNT];
 
-static uint16_t hb_sub_dst = BT_MESH_ADDR_UNASSIGNED;
-
-void bt_mesh_set_hb_sub_dst(uint16_t addr)
-{
-	hb_sub_dst = addr;
-}
-
 static int send_unseg(struct bt_mesh_net_tx *tx, struct os_mbuf *sdu,
 		      const struct bt_mesh_send_cb *cb, void *cb_data,
 		      const uint8_t *ctl_op)
@@ -860,36 +854,6 @@
 	return 0;
 }
 
-static int trans_heartbeat(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;
-	}
-
-	if (rx->ctx.recv_dst != hb_sub_dst) {
-		BT_WARN("Ignoring heartbeat to non-subscribed destination");
-		return 0;
-	}
-
-	init_ttl = (net_buf_simple_pull_u8(buf) & 0x7f);
-	feat = net_buf_simple_pull_be16(buf);
-
-	hops = (init_ttl - rx->ctx.recv_ttl + 1);
-
-	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 == 1) ? "" : "s", feat);
-
-	bt_mesh_heartbeat(rx->ctx.addr, rx->ctx.recv_dst, hops, feat);
-
-	return 0;
-}
-
 static int ctl_recv(struct bt_mesh_net_rx *rx, uint8_t hdr,
 		    struct os_mbuf *buf, uint64_t *seq_auth)
 {
@@ -901,7 +865,7 @@
 	case TRANS_CTL_OP_ACK:
 		return trans_ack(rx, hdr, buf, seq_auth);
 	case TRANS_CTL_OP_HEARTBEAT:
-		return trans_heartbeat(rx, buf);
+		return bt_mesh_hb_recv(rx, buf);
 	}
 
 	/* Only acks and heartbeats may need processing without local_match */
@@ -1636,58 +1600,6 @@
 	}
 }
 
-int bt_mesh_heartbeat_send(const struct bt_mesh_send_cb *cb, void *cb_data)
-{
-	struct bt_mesh_cfg_srv *cfg = bt_mesh_cfg_get();
-	uint16_t feat = 0U;
-	struct __packed {
-		uint8_t  init_ttl;
-		uint16_t feat;
-	} hb;
-	struct bt_mesh_msg_ctx ctx = {
-		.net_idx = cfg->hb_pub.net_idx,
-		.app_idx = BT_MESH_KEY_UNUSED,
-		.addr = cfg->hb_pub.dst,
-		.send_ttl = cfg->hb_pub.ttl,
-	};
-	struct bt_mesh_net_tx tx = {
-		.sub = bt_mesh_subnet_get(cfg->hb_pub.net_idx),
-		.ctx = &ctx,
-		.src = bt_mesh_model_elem(cfg->model)->addr,
-		.xmit = bt_mesh_net_transmit_get(),
-	};
-
-	/* Do nothing if heartbeat publication is not enabled */
-	if (cfg->hb_pub.dst == BT_MESH_ADDR_UNASSIGNED) {
-		return 0;
-	}
-
-	hb.init_ttl = cfg->hb_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", cfg->hb_pub.ttl, feat);
-
-	return bt_mesh_ctl_send(&tx, TRANS_CTL_OP_HEARTBEAT, &hb, sizeof(hb),
-			cb, cb_data);
-}
-
 struct bt_mesh_va *bt_mesh_va_get(uint16_t index)
 {
 	if (index >= ARRAY_SIZE(virtual_addrs)) {
diff --git a/nimble/host/mesh/src/transport.h b/nimble/host/mesh/src/transport.h
index 13fc9bc..4a9f7e3 100644
--- a/nimble/host/mesh/src/transport.h
+++ b/nimble/host/mesh/src/transport.h
@@ -89,8 +89,6 @@
 	uint8_t  uuid[16];
 };
 
-void bt_mesh_set_hb_sub_dst(uint16_t addr);
-
 bool bt_mesh_tx_in_progress(void);
 
 void bt_mesh_rx_reset(void);
@@ -107,8 +105,6 @@
 
 void bt_mesh_trans_reset(void);
 
-int bt_mesh_heartbeat_send(const struct bt_mesh_send_cb *cb, void *cb_data);
-
 struct bt_mesh_va *bt_mesh_va_get(uint16_t index);
 
 struct bt_mesh_va *bt_mesh_va_find(uint8_t uuid[16]);
diff --git a/nimble/host/mesh/syscfg.yml b/nimble/host/mesh/syscfg.yml
index d31238f..ec14d98 100644
--- a/nimble/host/mesh/syscfg.yml
+++ b/nimble/host/mesh/syscfg.yml
@@ -722,6 +722,15 @@
             Minimum level for the BLE Mesh Replay protection list log.
         value: 1
 
+    BLE_MESH_HEARTBEAT_LOG_MOD:
+        description: >
+            Numeric module ID to use for BLE Mesh Replay protection list messages.
+        value: 26
+    BLE_MESH_HEARTBEAT_LOG_LVL:
+        description: >
+            Minimum level for the BLE Mesh Replay protection list log.
+        value: 1
+
 syscfg.logs:
     BLE_MESH_LOG:
         module: MYNEWT_VAL(BLE_MESH_LOG_MOD)
@@ -791,6 +800,10 @@
         module: MYNEWT_VAL(BLE_MESH_PROVISIONER_LOG_MOD)
         level: MYNEWT_VAL(BLE_MESH_PROVISIONER_LOG_LVL)
 
+    BLE_MESH_HEARTBEAT_LOG:
+        module: MYNEWT_VAL(BLE_MESH_HEARTBEAT_LOG_MOD)
+        level: MYNEWT_VAL(BLE_MESH_HEARTBEAT_LOG_LVL)
+
 syscfg.vals.BLE_MESH_SHELL:
     BLE_MESH_CFG_CLI: 1
     BLE_MESH_HEALTH_CLI: 1
diff --git a/porting/examples/linux_blemesh/include/logcfg/logcfg.h b/porting/examples/linux_blemesh/include/logcfg/logcfg.h
index d2903ed..798418d 100644
--- a/porting/examples/linux_blemesh/include/logcfg/logcfg.h
+++ b/porting/examples/linux_blemesh/include/logcfg/logcfg.h
@@ -127,6 +127,13 @@
 #define BLE_MESH_PROV_DEVICE_LOG_CRITICAL(...) MODLOG_CRITICAL(24, __VA_ARGS__)
 #define BLE_MESH_PROV_DEVICE_LOG_DISABLED(...) MODLOG_DISABLED(24, __VA_ARGS__)
 
+#define BLE_MESH_HEARTBEAT_LOG_DEBUG(...) IGNORE(__VA_ARGS__)
+#define BLE_MESH_HEARTBEAT_LOG_INFO(...) MODLOG_INFO(25, __VA_ARGS__)
+#define BLE_MESH_HEARTBEAT_LOG_WARN(...) MODLOG_WARN(25, __VA_ARGS__)
+#define BLE_MESH_HEARTBEAT_LOG_ERROR(...) MODLOG_ERROR(25, __VA_ARGS__)
+#define BLE_MESH_HEARTBEAT_LOG_CRITICAL(...) MODLOG_CRITICAL(25, __VA_ARGS__)
+#define BLE_MESH_HEARTBEAT_LOG_DISABLED(...) MODLOG_DISABLED(25, __VA_ARGS__)
+
 #define DFLT_LOG_DEBUG(...) IGNORE(__VA_ARGS__)
 #define DFLT_LOG_INFO(...) MODLOG_INFO(0, __VA_ARGS__)
 #define DFLT_LOG_WARN(...) MODLOG_WARN(0, __VA_ARGS__)