Bluetooth: Mesh: Added support for randomly delaying publications

The section 3.7.3.1 of the mesh specification recommends to delay
a message publication in certain cases:
- at power-up or upon state change for a time between 20 to 500 ms
- for periodic publications for a time between 20 to 50 ms

This change implements this recommendation by adding the
`CONFIG_BT_MESH_DELAYABLE_PUBLICATION` Kconfig option which enables
the randomization code and by adding the `bt_mesh_model_pub.delayable`
bit field which allows each model decide whether the publications
should be delayed for this model or not.

Signed-off-by: Pavel Vasilyev <pavel.vasilyev@nordicsemi.no>
This commit is contained in:
Pavel Vasilyev 2023-12-11 23:29:36 +01:00 committed by Carles Cufí
commit fe70e50d41
7 changed files with 311 additions and 16 deletions

View file

@ -255,6 +255,21 @@ message, it will send messages with delay close to expiration to free memory.
When the mesh stack is suspended or reset, messages not yet sent are removed and
the :c:member:`bt_mesh_send_cb.start` callback is raised with an error code.
Delayable publications
======================
The delayable publication functionality implements the specification recommendations for message
publication delays in the following cases:
* Between 20 to 500 milliseconds when the Bluetooth Mesh stack starts or when the publication is
triggered by the :c:func:`bt_mesh_model_publish` function
* Between 20 to 50 milliseconds for periodically published messages
This feature is optional and enabled with the :kconfig:option:`CONFIG_BT_MESH_DELAYABLE_PUBLICATION`
Kconfig option. When enabled, each model can enable or disable the delayable publication by setting
the :c:member:`bt_mesh_model_pub.delayable` bit field to ``1`` or ``0`` correspondingly. This bit
field can be changed at any time.
API reference
*************

View file

@ -713,6 +713,8 @@ struct bt_mesh_model_pub {
uint8_t period_div:4, /**< Divisor for the Period. */
count:4; /**< Transmissions left. */
uint8_t delayable:1; /**< Use random delay for publishing. */
uint32_t period_start; /**< Start of the current period. */
/** @brief Publication buffer, containing the publication message.

View file

@ -672,6 +672,15 @@ config BT_MESH_ACCESS_DELAYABLE_MSG_CHUNK_COUNT
endif # BT_MESH_ACCESS_DELAYABLE_MSG
config BT_MESH_DELAYABLE_PUBLICATION
bool "Delayable publication"
default y
help
When enabled, the periodic publications are randomly delayed by 20 to 50ms. Publications
triggered at the start of the stack or by the bt_mesh_model_publish() call are delayed by
20 to 500ms. This option reduces the probability of collisions when multiple nodes publish
at the same time.
endmenu # Access layer
menu "Models"

View file

@ -33,6 +33,11 @@
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(bt_mesh_access);
/* 20 - 50ms */
#define RANDOM_DELAY_SHORT 30
/* 20 - 500ms */
#define RANDOM_DELAY_LONG 480
/* Model publication information for persistent storage. */
struct mod_pub_val {
struct {
@ -761,8 +766,16 @@ static int32_t next_period(const struct bt_mesh_model *mod)
if (period && elapsed >= period) {
LOG_WRN("Retransmission interval is too short");
/* Return smallest positive number since 0 means disabled */
return 1;
if (!!pub->delayable) {
LOG_WRN("Publication period is too short for"
" retransmissions");
}
/* Keep retransmitting the message with the interval sacrificing the
* next publication period start.
*/
return BT_MESH_PUB_TRANSMIT_INT(mod->pub->retransmit);
}
}
@ -775,6 +788,11 @@ static int32_t next_period(const struct bt_mesh_model *mod)
if (elapsed >= period) {
LOG_WRN("Publication sending took longer than the period");
if (!!pub->delayable) {
LOG_WRN("Publication period is too short to be delayable");
}
/* Return smallest positive number since 0 means disabled */
return 1;
}
@ -855,6 +873,39 @@ static int pub_period_start(struct bt_mesh_model_pub *pub)
return 0;
}
static uint16_t pub_delay_get(int random_delay_window)
{
if (!IS_ENABLED(CONFIG_BT_MESH_DELAYABLE_PUBLICATION)) {
return 0;
}
uint16_t num = 0;
(void)bt_rand(&num, sizeof(num));
return 20 + (num % random_delay_window);
}
static int pub_delay_schedule(struct bt_mesh_model_pub *pub, int delay)
{
uint16_t random;
int err;
if (!IS_ENABLED(CONFIG_BT_MESH_DELAYABLE_PUBLICATION)) {
return -ENOTSUP;
}
random = pub_delay_get(delay);
err = k_work_reschedule(&pub->timer, K_MSEC(random));
if (err < 0) {
LOG_ERR("Unable to delay publication (err %d)", err);
return err;
}
LOG_DBG("Publication delayed by %dms", random);
return 0;
}
static void mod_publish(struct k_work *work)
{
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
@ -890,6 +941,13 @@ static void mod_publish(struct k_work *work)
if (err) {
return;
}
/* Delay the first publication in a period. */
if (!!pub->delayable && !pub_delay_schedule(pub, RANDOM_DELAY_SHORT)) {
/* Increment count as it would do BT_MESH_PUB_MSG_TOTAL */
pub->count++;
return;
}
}
err = publish_transmit(pub->mod);
@ -1564,6 +1622,18 @@ int bt_mesh_model_publish(const struct bt_mesh_model *model)
LOG_DBG("Publish Retransmit Count %u Interval %ums", pub->count,
BT_MESH_PUB_TRANSMIT_INT(pub->retransmit));
/* Delay the publication for longer time when the publication is triggered manually (section
* 3.7.3.1):
*
* When the publication of a message is the result of a power-up, a state transition
* progress update, or completion of a state transition, multiple nodes may be reporting the
* state change at the same time. To reduce the probability of a message collision, these
* messages should be sent with a random delay between 20 and 500 milliseconds.
*/
if (!!pub->delayable && !pub_delay_schedule(pub, RANDOM_DELAY_LONG)) {
return 0;
}
k_work_reschedule(&pub->timer, K_NO_WAIT);
return 0;
@ -2568,8 +2638,21 @@ static void commit_mod(const struct bt_mesh_model *mod, const struct bt_mesh_ele
int32_t ms = bt_mesh_model_pub_period_get(mod);
if (ms > 0) {
LOG_DBG("Starting publish timer (period %u ms)", ms);
k_work_schedule(&mod->pub->timer, K_MSEC(ms));
/* Delay the first publication after power-up for longer time (section
* 3.7.3.1):
*
* When the publication of a message is the result of a power-up, a state
* transition progress update, or completion of a state transition, multiple
* nodes may be reporting the state change at the same time. To reduce the
* probability of a message collision, these messages should be sent with a
* random delay between 20 and 500 milliseconds.
*/
uint16_t random;
random = !!mod->pub->delayable ? pub_delay_get(RANDOM_DELAY_LONG) : 0;
LOG_DBG("Starting publish timer (period %u ms, delay %u ms)", ms, random);
k_work_schedule(&mod->pub->timer, K_MSEC(ms + random));
}
}

View file

@ -60,6 +60,7 @@ static const struct {
uint8_t div;
int32_t period_ms;
} test_period[] = {
{ BT_MESH_PUB_PERIOD_100MS(1), 0, 100 },
{ BT_MESH_PUB_PERIOD_100MS(5), 0, 500 },
{ BT_MESH_PUB_PERIOD_SEC(2), 0, 2000 },
{ BT_MESH_PUB_PERIOD_10SEC(1), 0, 10000 },
@ -522,6 +523,91 @@ static void msgf_publish(void)
bt_mesh_model_publish(model);
}
static void pub_delayable_check(int32_t interval, uint8_t count)
{
int64_t timestamp = k_uptime_get();
int err;
for (size_t j = 0; j < count; j++) {
/* Every new publication will release semaphore in the update handler and the time
* between two consecutive publications will be measured.
*/
err = k_sem_take(&publish_sem, K_SECONDS(20));
if (err) {
FAIL("Send timed out");
}
int32_t time_delta = k_uptime_delta(&timestamp);
int32_t pub_delta = time_delta - interval;
LOG_DBG("Send time: %d delta: %d pub_delta: %d", (int32_t)timestamp, time_delta,
pub_delta);
if (j == 0) {
/* The first delta will be between the messages published manually and next
* publication (or retransmission). So the time difference should not be
* longer than 500 - 20 + 10 (margin):
*
* |---|-------|--------|-------|---->
* M1 20ms tx(M1) 500ms
* update()
*/
ASSERT_IN_RANGE(pub_delta, 0, 510);
} else {
/* Time difference between the consequtive update callback calls should be
* within a small margin like without random delay as the callbacks should
* be called at the regular interval or immediately (if it passed the next
* period time).
*/
ASSERT_IN_RANGE(pub_delta, 0, 10);
}
}
}
static void recv_delayable_check(int32_t interval, uint8_t count)
{
int64_t timestamp;
int err;
/* The measurement starts by the first received message. */
err = k_sem_take(&publish_sem, K_SECONDS(20));
if (err) {
FAIL("Recv timed out");
}
timestamp = k_uptime_get();
for (size_t j = 0; j < count; j++) {
/* Every new received message will release semaphore in the message handler and
* the time between two consecutive publications will be measured.
*/
err = k_sem_take(&publish_sem, K_SECONDS(20));
if (err) {
FAIL("Recv timed out");
}
int32_t time_delta = k_uptime_delta(&timestamp);
/* First message can be delayed up to 500ms, others for up to 50ms. */
int32_t upper_delay = j == 0 ? 500 : 50;
/*
* Lower boundary: tx2 - tx1 + interval
* |---|-------|---------------|-------|----->
* M1 tx1(50ms/500ms) M2 tx2(20ms)
*
* Upper boundary: tx2 - tx1 + interval
* |---|-------|--------|-----------|----->
* M1 tx1(20ms) M2 tx2(50ms/500ms)
*/
int32_t lower_boundary = 20 - upper_delay + interval;
int32_t upper_boundary = upper_delay - 20 + interval;
LOG_DBG("Recv time: %d delta: %d boundaries: %d/%d", (int32_t)timestamp, time_delta,
lower_boundary, upper_boundary);
ASSERT_IN_RANGE(time_delta, lower_boundary, upper_boundary + RX_JITTER_MAX);
}
}
static void pub_jitter_check(int32_t interval, uint8_t count)
{
int64_t timestamp = k_uptime_get();
@ -578,8 +664,8 @@ static void recv_jitter_check(int32_t interval, uint8_t count)
jitter = MAX(pub_delta, jitter);
LOG_DBG("Recv time: %d delta: %d jitter: %d", (int32_t)timestamp, time_delta,
jitter);
LOG_DBG("Recv time: %d delta: %d jitter: %d, j: %d", (int32_t)timestamp, time_delta,
jitter, j);
}
LOG_INF("Recv jitter: %d", jitter);
@ -589,17 +675,19 @@ static void recv_jitter_check(int32_t interval, uint8_t count)
/* Test publish period states by publishing a message and checking interval between update handler
* calls.
*/
static void test_tx_period(void)
static void tx_period(bool delayable)
{
const struct bt_mesh_model *model = &models[2];
bt_mesh_test_cfg_set(NULL, 60);
bt_mesh_test_cfg_set(NULL, 70);
bt_mesh_device_setup(&prov, &local_comp);
provision(UNICAST_ADDR1);
common_configure(UNICAST_ADDR1);
k_sem_init(&publish_sem, 0, 1);
model->pub->delayable = delayable;
for (size_t i = 0; i < ARRAY_SIZE(test_period); i++) {
pub_param_set(test_period[i].period, 0);
@ -611,7 +699,11 @@ static void test_tx_period(void)
/* Start publishing messages and measure jitter. */
msgf_publish();
publish_allow = true;
pub_jitter_check(test_period[i].period_ms, PUB_PERIOD_COUNT);
if (delayable) {
pub_delayable_check(test_period[i].period_ms, PUB_PERIOD_COUNT);
} else {
pub_jitter_check(test_period[i].period_ms, PUB_PERIOD_COUNT);
}
/* Disable periodic publication before the next test iteration. */
publish_allow = false;
@ -626,9 +718,9 @@ static void test_tx_period(void)
/* Receive a periodically published message and check publication period by measuring interval
* between message handler calls.
*/
static void test_rx_period(void)
static void rx_period(bool delayable)
{
bt_mesh_test_cfg_set(NULL, 60);
bt_mesh_test_cfg_set(NULL, 70);
bt_mesh_device_setup(&prov, &local_comp);
provision(UNICAST_ADDR2);
common_configure(UNICAST_ADDR2);
@ -636,16 +728,40 @@ static void test_rx_period(void)
k_sem_init(&publish_sem, 0, 1);
for (size_t i = 0; i < ARRAY_SIZE(test_period); i++) {
recv_jitter_check(test_period[i].period_ms, PUB_PERIOD_COUNT);
if (delayable) {
recv_delayable_check(test_period[i].period_ms, PUB_PERIOD_COUNT);
} else {
recv_jitter_check(test_period[i].period_ms, PUB_PERIOD_COUNT);
}
}
PASS();
}
static void test_tx_period(void)
{
tx_period(false);
}
static void test_rx_period(void)
{
rx_period(false);
}
static void test_tx_period_delayable(void)
{
tx_period(true);
}
static void test_rx_period_delayable(void)
{
rx_period(true);
}
/* Test publish retransmit interval and count states by publishing a message and checking interval
* between update handler calls.
*/
static void test_tx_transmit(void)
static void tx_transmit(bool delayable)
{
const struct bt_mesh_model *model = &models[2];
uint8_t status;
@ -672,6 +788,7 @@ static void test_tx_transmit(void)
publish_allow = true;
model->pub->retr_update = true;
model->pub->delayable = delayable;
for (size_t i = 0; i < ARRAY_SIZE(test_transmit); i++) {
pub_param_set(0, test_transmit[i]);
@ -683,7 +800,11 @@ static void test_tx_transmit(void)
/* Start publishing messages and measure jitter. */
msgf_publish();
pub_jitter_check(interval, count);
if (delayable) {
pub_delayable_check(interval, count);
} else {
pub_jitter_check(interval, count);
}
/* Let the receiver hit the first semaphore. */
k_sleep(K_SECONDS(1));
@ -695,7 +816,7 @@ static void test_tx_transmit(void)
/* Receive a published message and check retransmission interval by measuring interval between
* message handler calls.
*/
static void test_rx_transmit(void)
static void rx_transmit(bool delayable)
{
bt_mesh_test_cfg_set(NULL, 60);
bt_mesh_device_setup(&prov, &local_comp);
@ -708,12 +829,36 @@ static void test_rx_transmit(void)
int32_t interval = BT_MESH_PUB_TRANSMIT_INT(test_transmit[i]);
int count = BT_MESH_PUB_TRANSMIT_COUNT(test_transmit[i]);
recv_jitter_check(interval, count);
if (delayable) {
recv_delayable_check(interval, count);
} else {
recv_jitter_check(interval, count);
}
}
PASS();
}
static void test_tx_transmit(void)
{
tx_transmit(false);
}
static void test_rx_transmit(void)
{
rx_transmit(false);
}
static void test_tx_transmit_delayable(void)
{
tx_transmit(true);
}
static void test_rx_transmit_delayable(void)
{
rx_transmit(true);
}
/* Cancel one of messages to be published and check that the next one is published when next period
* starts.
*/
@ -841,6 +986,13 @@ static const struct bst_test_instance test_access[] = {
TEST_CASE(tx, cancel, "Access: Cancel a message during publication"),
TEST_CASE(rx, cancel, "Access: Receive published messages except cancelled"),
TEST_CASE(tx, period_delayable, "Access: Test delayable periodic publication"),
TEST_CASE(rx, period_delayable, "Access: Receive delayable periodic publication"),
TEST_CASE(tx, transmit_delayable, "Access: Test delayable publication with retransmission"),
TEST_CASE(rx, transmit_delayable, "Access: Receive delayable publication with"
" retransmissions"),
BSTEST_END_MARKER
};

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Copyright 2023 Nordic Semiconductor
# SPDX-License-Identifier: Apache-2.0
source $(dirname "${BASH_SOURCE[0]}")/../../_mesh_test.sh
RunTest mesh_access_pub_period_delayable_retr \
access_tx_period_delayable access_rx_period_delayable
conf=prj_mesh1d1_conf
RunTest mesh_access_pub_period_delayable_retr_1d1 \
access_tx_period_delayable access_rx_period_delayable
conf=prj_mesh1d1_conf
overlay=overlay_psa_conf
RunTest mesh_access_pub_period_delayable_retr_psa \
access_tx_period_delayable access_rx_period_delayable

View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Copyright 2023 Nordic Semiconductor
# SPDX-License-Identifier: Apache-2.0
source $(dirname "${BASH_SOURCE[0]}")/../../_mesh_test.sh
RunTest mesh_access_pub_transmit_delayable_retr \
access_tx_transmit_delayable access_rx_transmit_delayable
conf=prj_mesh1d1_conf
RunTest mesh_access_pub_transmit_delayable_retr_1d1 \
access_tx_transmit_delayable access_rx_transmit_delayable
conf=prj_mesh1d1_conf
overlay=overlay_psa_conf
RunTest mesh_access_pub_transmit_delayable_retr_psa \
access_tx_period_delayable access_rx_period_delayable