blob: 41685b34e8b741cac39f17b114c6635a43dccd94 [file]
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/kernel.h>
#include <errno.h>
#include <stdlib.h>
#include <zephyr/sys/slist.h>
#include <zephyr/net_buf.h>
#include <zephyr/bluetooth/mesh.h>
#include "msg.h"
#include "access.h"
#include "net.h"
#define LOG_LEVEL CONFIG_BT_MESH_ACCESS_LOG_LEVEL
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(bt_mesh_delayable_msg);
static void delayable_msg_handler(struct k_work *w);
static bool push_msg_from_delayable_msgs(void);
static struct delayable_msg_chunk {
sys_snode_t node;
uint8_t data[CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_CHUNK_SIZE];
} delayable_msg_chunks[CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_CHUNK_COUNT];
static struct delayable_msg_ctx {
sys_snode_t node;
sys_slist_t chunks;
struct bt_mesh_msg_ctx ctx;
uint16_t src_addr;
const struct bt_mesh_send_cb *cb;
void *cb_data;
uint32_t fired_time;
uint16_t len;
} delayable_msgs_ctx[CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_COUNT];
static struct {
sys_slist_t busy_ctx;
sys_slist_t free_ctx;
sys_slist_t free_chunks;
struct k_work_delayable random_delay;
} access_delayable_msg = {.random_delay = Z_WORK_DELAYABLE_INITIALIZER(delayable_msg_handler)};
/* Serializes access to busy_ctx, free_ctx, free_chunks, and the fired_time field of any ctx
* currently on busy_ctx. Must never be held across a call that can block (bt_mesh_access_send(),
* bt_rand(), user callbacks).
*/
static struct k_spinlock lock;
/* Caller must hold `lock`. */
static void put_ctx_to_busy_list(struct delayable_msg_ctx *ctx)
{
struct delayable_msg_ctx *curr_ctx;
sys_slist_t *list = &access_delayable_msg.busy_ctx;
sys_snode_t *head = sys_slist_peek_head(list);
sys_snode_t *curr = head;
sys_snode_t *prev = curr;
if (!head) {
sys_slist_append(list, &ctx->node);
return;
}
do {
curr_ctx = CONTAINER_OF(curr, struct delayable_msg_ctx, node);
if (ctx->fired_time < curr_ctx->fired_time) {
if (curr == head) {
sys_slist_prepend(list, &ctx->node);
} else {
sys_slist_insert(list, prev, &ctx->node);
}
return;
}
prev = curr;
} while ((curr = sys_slist_peek_next(curr)));
sys_slist_append(list, &ctx->node);
}
static struct delayable_msg_ctx *peek_pending_msg(void)
{
struct delayable_msg_ctx *pending_msg = NULL;
sys_snode_t *node = sys_slist_peek_head(&access_delayable_msg.busy_ctx);
if (node) {
pending_msg = CONTAINER_OF(node, struct delayable_msg_ctx, node);
}
return pending_msg;
}
static void reschedule_delayable_msg(struct delayable_msg_ctx *msg)
{
int32_t remaining;
k_timeout_t delay = K_NO_WAIT;
struct delayable_msg_ctx *pending_msg;
k_spinlock_key_t key = k_spin_lock(&lock);
if (msg) {
put_ctx_to_busy_list(msg);
}
pending_msg = peek_pending_msg();
if (!pending_msg) {
k_spin_unlock(&lock, key);
return;
}
remaining = (int32_t)(pending_msg->fired_time - k_uptime_get_32());
if (remaining > 0) {
delay = K_MSEC(remaining);
}
/* k_work_reschedule() is safe to call under our spinlock: it uses its own internal spinlock
* and does not block. Holding the lock here makes the "select head + arm timer" step atomic
* w.r.t. concurrent insertions.
*/
k_work_reschedule(&access_delayable_msg.random_delay, delay);
k_spin_unlock(&lock, key);
}
static int allocate_delayable_msg_chunks(struct delayable_msg_ctx *msg, int number)
{
sys_snode_t *node;
k_spinlock_key_t key = k_spin_lock(&lock);
for (int i = 0; i < number; i++) {
node = sys_slist_get(&access_delayable_msg.free_chunks);
if (!node) {
k_spin_unlock(&lock, key);
LOG_WRN("Unable allocate %u chunks, allocated %u", number, i);
return i;
}
sys_slist_append(&msg->chunks, node);
}
k_spin_unlock(&lock, key);
return number;
}
static void release_delayable_msg_chunks(struct delayable_msg_ctx *msg)
{
sys_snode_t *node;
k_spinlock_key_t key = k_spin_lock(&lock);
while ((node = sys_slist_get(&msg->chunks))) {
sys_slist_append(&access_delayable_msg.free_chunks, node);
}
k_spin_unlock(&lock, key);
}
static struct delayable_msg_ctx *allocate_delayable_msg_ctx(void)
{
struct delayable_msg_ctx *msg;
sys_snode_t *node;
k_spinlock_key_t key;
key = k_spin_lock(&lock);
node = sys_slist_get(&access_delayable_msg.free_ctx);
k_spin_unlock(&lock, key);
if (!node) {
LOG_WRN("Purge pending delayable message.");
/* May block, so it must not be called with the lock held. */
if (!push_msg_from_delayable_msgs()) {
return NULL;
}
key = k_spin_lock(&lock);
node = sys_slist_get(&access_delayable_msg.free_ctx);
k_spin_unlock(&lock, key);
/* Another context may have taken the purged ctx before we retried. */
if (!node) {
return NULL;
}
}
msg = CONTAINER_OF(node, struct delayable_msg_ctx, node);
/* msg is off every list here, so it is private to this caller. */
sys_slist_init(&msg->chunks);
return msg;
}
static void release_delayable_msg_ctx(struct delayable_msg_ctx *ctx)
{
k_spinlock_key_t key = k_spin_lock(&lock);
/* Not on busy_ctx when called from the manage() error path. */
(void)sys_slist_find_and_remove(&access_delayable_msg.busy_ctx, &ctx->node);
sys_slist_append(&access_delayable_msg.free_ctx, &ctx->node);
k_spin_unlock(&lock, key);
}
/* Releases `msg` and reports `err` to its sender, which is skipped for a successful send because
* the transport owns the callback from then on. Must not be called with the lock held.
*/
static void complete_delayable_msg(struct delayable_msg_ctx *msg, int err)
{
/* Capture the callback before releasing the ctx; another caller may take it immediately
* and overwrite msg->cb / msg->cb_data.
*/
const struct bt_mesh_send_cb *cb = msg->cb;
void *cb_data = msg->cb_data;
release_delayable_msg_chunks(msg);
release_delayable_msg_ctx(msg);
if (err && cb && cb->start) {
cb->start(0, err, cb_data);
}
}
/* Signed delta keeps this correct across the 32-bit uptime wrap. */
static bool msg_expired(const struct delayable_msg_ctx *msg)
{
return (int32_t)(k_uptime_get_32() - msg->fired_time) >= 0;
}
/* Takes the head off busy_ctx only if it is due. k_work_reschedule() cannot withdraw a submission
* that already happened, so the handler can run for an expiry another context has consumed; this
* leaves such an invocation with nothing to send.
*/
static struct delayable_msg_ctx *take_expired_msg(void)
{
struct delayable_msg_ctx *msg;
k_spinlock_key_t key = k_spin_lock(&lock);
msg = peek_pending_msg();
if (msg && !msg_expired(msg)) {
msg = NULL;
} else if (msg) {
(void)sys_slist_get(&access_delayable_msg.busy_ctx);
}
k_spin_unlock(&lock, key);
return msg;
}
/* Takes the head whatever its fired_time, so the purge path can force out the earliest msg. */
static struct delayable_msg_ctx *take_earliest_msg(void)
{
sys_snode_t *node;
k_spinlock_key_t key = k_spin_lock(&lock);
/* Dequeueing under the lock stops concurrent callers from sending and releasing the same
* msg twice.
*/
node = sys_slist_get(&access_delayable_msg.busy_ctx);
k_spin_unlock(&lock, key);
return node ? CONTAINER_OF(node, struct delayable_msg_ctx, node) : NULL;
}
/* Sends `msg` and releases it. Returns false if a transient failure put it back on busy_ctx. */
static bool send_delayable_msg(struct delayable_msg_ctx *msg)
{
sys_snode_t *node;
struct delayable_msg_chunk *chunk;
uint16_t len = msg->len;
int err;
k_spinlock_key_t key;
/* bt_mesh_suspend() sets BT_MESH_SUSPENDED and bt_mesh_reset() clears BT_MESH_VALID before
* the queue is drained, so either one means the stack is down. Never hand a message to the
* transport in that state, however it ended up on busy_ctx.
*/
if (atomic_test_bit(bt_mesh.flags, BT_MESH_SUSPENDED) ||
!atomic_test_bit(bt_mesh.flags, BT_MESH_VALID)) {
complete_delayable_msg(msg, -ENODEV);
return true;
}
NET_BUF_SIMPLE_DEFINE(buf, BT_MESH_TX_SDU_MAX);
/* msg is off busy_ctx, so it is unreachable from other contexts and its chunks can be
* walked unlocked.
*/
SYS_SLIST_FOR_EACH_NODE(&msg->chunks, node) {
uint16_t tmp = MIN((uint16_t)CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_CHUNK_SIZE, len);
chunk = CONTAINER_OF(node, struct delayable_msg_chunk, node);
memcpy(net_buf_simple_add(&buf, tmp), chunk->data, tmp);
len -= tmp;
}
msg->ctx.rnd_delay = false;
/* Blocking call: must NOT hold the spinlock. */
err = bt_mesh_access_send(&msg->ctx, &buf, msg->src_addr, msg->cb, msg->cb_data);
msg->ctx.rnd_delay = true;
if (err == -EBUSY || err == -ENOBUFS) {
/* Transient failure: retry the msg 10 ms later, re-sorted into busy_ctx, and
* re-arm the timer for whatever is now the head.
*/
key = k_spin_lock(&lock);
msg->fired_time += 10;
put_ctx_to_busy_list(msg);
k_spin_unlock(&lock, key);
reschedule_delayable_msg(NULL);
return false;
}
/* User callback: must NOT hold the spinlock. */
complete_delayable_msg(msg, err);
return true;
}
static bool push_msg_from_delayable_msgs(void)
{
struct delayable_msg_ctx *msg = take_earliest_msg();
if (!msg) {
return false;
}
return send_delayable_msg(msg);
}
static void delayable_msg_handler(struct k_work *w)
{
struct delayable_msg_ctx *msg = take_expired_msg();
if (msg) {
(void)send_delayable_msg(msg);
}
/* Re-arms for the next msg, immediately if one is already due. */
reschedule_delayable_msg(NULL);
}
int bt_mesh_delayable_msg_manage(struct bt_mesh_msg_ctx *ctx, struct net_buf_simple *buf,
uint16_t src_addr, const struct bt_mesh_send_cb *cb, void *cb_data)
{
sys_snode_t *node;
struct delayable_msg_ctx *msg;
uint16_t random_delay;
int total_number = DIV_ROUND_UP(buf->len, CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_CHUNK_SIZE);
int allocated_number = 0;
uint16_t len = buf->len;
if (atomic_test_bit(bt_mesh.flags, BT_MESH_SUSPENDED)) {
LOG_WRN("Refusing to allocate message context while suspended");
return -ENODEV;
}
if (total_number > CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_CHUNK_COUNT) {
return -EINVAL;
}
msg = allocate_delayable_msg_ctx();
if (!msg) {
LOG_WRN("No available free delayable message context.");
return -ENOMEM;
}
do {
allocated_number +=
allocate_delayable_msg_chunks(msg, total_number - allocated_number);
if (total_number > allocated_number) {
LOG_DBG("Unable allocate %u chunks, allocated %u", total_number,
allocated_number);
if (!push_msg_from_delayable_msgs()) {
LOG_WRN("No available chunk memory.");
release_delayable_msg_chunks(msg);
release_delayable_msg_ctx(msg);
return -ENOMEM;
}
}
} while (total_number > allocated_number);
SYS_SLIST_FOR_EACH_NODE(&msg->chunks, node) {
uint16_t tmp = MIN(CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_CHUNK_SIZE, buf->len);
struct delayable_msg_chunk *chunk =
CONTAINER_OF(node, struct delayable_msg_chunk, node);
memcpy(chunk->data, net_buf_simple_pull_mem(buf, tmp), tmp);
}
bt_rand(&random_delay, sizeof(uint16_t));
random_delay = 20 + random_delay % (BT_MESH_ADDR_IS_UNICAST(ctx->recv_dst) ? 30 : 480);
msg->fired_time = k_uptime_get_32() + random_delay;
msg->ctx = *ctx;
msg->src_addr = src_addr;
msg->cb = cb;
msg->cb_data = cb_data;
msg->len = len;
reschedule_delayable_msg(msg);
return 0;
}
void bt_mesh_delayable_msg_init(void)
{
sys_slist_init(&access_delayable_msg.busy_ctx);
sys_slist_init(&access_delayable_msg.free_ctx);
sys_slist_init(&access_delayable_msg.free_chunks);
for (int i = 0; i < CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_COUNT; i++) {
sys_slist_append(&access_delayable_msg.free_ctx, &delayable_msgs_ctx[i].node);
}
for (int i = 0; i < CONFIG_BT_MESH_ACCESS_DELAYABLE_MSG_CHUNK_COUNT; i++) {
sys_slist_append(&access_delayable_msg.free_chunks, &delayable_msg_chunks[i].node);
}
}
void bt_mesh_delayable_msg_stop(void)
{
sys_snode_t *node;
k_spinlock_key_t key;
k_work_cancel_delayable(&access_delayable_msg.random_delay);
for (;;) {
key = k_spin_lock(&lock);
node = sys_slist_get(&access_delayable_msg.busy_ctx);
k_spin_unlock(&lock, key);
if (!node) {
break;
}
complete_delayable_msg(CONTAINER_OF(node, struct delayable_msg_ctx, node), -ENODEV);
}
}