472 lines
9.8 KiB
C
472 lines
9.8 KiB
C
/*
|
|
* 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");
|
|
}
|
|
}
|