blob: 133c552620f51bcb62d1cdf6d2b8d1447d9c4ffc [file] [log] [blame]
/*
* 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");
}
}