diff --git a/doc/services/zbus/images/zbus_proxy_agent.png b/doc/services/zbus/images/zbus_proxy_agent.png new file mode 100644 index 0000000000000..b6f2b91ceba1e Binary files /dev/null and b/doc/services/zbus/images/zbus_proxy_agent.png differ diff --git a/doc/services/zbus/index.rst b/doc/services/zbus/index.rst index ef1fce0873992..2c7d8965bf55b 100644 --- a/doc/services/zbus/index.rst +++ b/doc/services/zbus/index.rst @@ -878,6 +878,136 @@ illustrates the runtime registration usage. the channel observer it was first associated with through :c:func:`zbus_chan_rm_obs`. +Multi-domain Communication +*************************** + +ZBus supports multi-domain communication, enabling message passing between different execution +domains such as CPU cores or separate devices. This is achieved through proxy agents that +forward messages between domains. + +.. figure:: images/zbus_proxy_agent.png + :alt: ZBus publish processing detail + :width: 75% + +.. + Temporary image illustrating zbus multi-domain communication with proxy agents. + +Concepts +======== + +Multi-domain zbus introduces several key concepts: + +* **Master channels**: Channels where messages are published locally within a domain +* **Shadow channels**: Read-only channels that mirror master channels from other domains +* **Proxy agents**: Background services that synchronize channel data between domains +* **Transport backends**: Communication mechanisms (IPC, UART) used by proxy agents + +The :c:macro:`ZBUS_MULTIDOMAIN_CHAN_DEFINE` macro enables conditional compilation to create +master channels in one domain and corresponding shadow channels in other domains. + +Transport Backends +================== + +ZBus multi-domain communication relies on transport backends to forward messages between different +execution domains. + +IPC Backend +----------- + +The IPC backend facilitates communication between CPU cores within the same system using +Inter-Process Communication mechanisms. + +See the :zephyr:code-sample:`zbus-ipc-forwarder` sample for a complete implementation. + +UART Backend +------------ + +The UART backend enables communication between physically separate devices over serial connections. +This backend extends zbus messaging across device boundaries, allowing distributed systems to +maintain a unified message bus architecture. + +See the :zephyr:code-sample:`zbus-uart-forwarder` sample for a complete implementation. + +Usage +===== + +Multi-domain zbus requires defining shared channels and setting up proxy agents. Here's a typical setup: + +**common.h** - Shared channel definitions: + +.. code-block:: c + + #include + #include + + struct request_data { + // ... + }; + + struct response_data { + // ... + }; + + ZBUS_MULTIDOMAIN_CHAN_DEFINE(request_channel, struct request_data, + NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0), + IS_ENABLED(DOMAIN_A), /* Master on domain A */ + 1 /* Include on both domains */); + + ZBUS_MULTIDOMAIN_CHAN_DEFINE(response_channel, struct response_data, + NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0), + IS_ENABLED(DOMAIN_B), /* Master on domain B */ + 1 /* Include on both domains */); + +**Domain A** - Requester setup: + +.. code-block:: c + + #define DOMAIN_A 1 + #include "common.h" + + /* Set up proxy agent and add channels for forwarding */ + #define IPC_INSTANCE DT_NODELABEL(ipc_node) + ZBUS_PROXY_AGENT_DEFINE(proxy_agent, ZBUS_MULTIDOMAIN_TYPE_IPC, IPC_INSTANCE); + ZBUS_PROXY_ADD_CHANNEL(proxy_agent, request_channel); + + void response_listener_cb(const struct zbus_channel *chan) { + const struct response_data *resp = zbus_chan_const_msg(chan); + LOG_INF("Received response: ..."); + } + ZBUS_LISTENER_DEFINE(response_listener, response_listener_cb); + ZBUS_CHAN_ADD_OBS(response_channel, response_listener, 0); + + int main(void) + { + struct request_data req = { /* ... */ }; + zbus_chan_pub(&request_channel, &req, K_MSEC(100)); + return 0; + } + +**Domain B** - Responder setup: + +.. code-block:: c + + #define DOMAIN_B 1 + #include "common.h" + + /* Set up proxy agent and add channels for forwarding */ + #define IPC_INSTANCE DT_NODELABEL(ipc_node) + ZBUS_PROXY_AGENT_DEFINE(proxy_agent, ZBUS_MULTIDOMAIN_TYPE_IPC, IPC_INSTANCE); + ZBUS_PROXY_ADD_CHANNEL(proxy_agent, response_channel); + + /* Observe requests and publish responses */ + void request_listener_cb(const struct zbus_channel *chan) { + const struct request_data *req = zbus_chan_const_msg(chan); + struct response_data resp = { /* ... */ }; + zbus_chan_pub(&response_channel, &resp, K_MSEC(100)); + LOG_INF("Processed request ..."); + } + ZBUS_LISTENER_DEFINE(request_listener, request_listener_cb); + ZBUS_CHAN_ADD_OBS(request_channel, request_listener, 0); + Samples ******* @@ -901,7 +1031,8 @@ available: observer registration feature; * :zephyr:code-sample:`zbus-confirmed-channel` implements a way of implement confirmed channel only with subscribers; -* :zephyr:code-sample:`zbus-benchmark` implements a benchmark with different combinations of inputs. +* :zephyr:code-sample:`zbus-ipc-forwarder` demonstrates multi-core communication using IPC forwarders; +* :zephyr:code-sample:`zbus-uart-forwarder` demonstrates inter-device communication using UART forwarders. Suggested Uses ************** @@ -913,6 +1044,13 @@ subscribers (if you need a thread) or listeners (if you need to be lean and fast the listener, another asynchronous message processing mechanism (like :ref:`message queues `) may be necessary to retain the pending message until it gets processed. +For multi-domain scenarios, use zbus to enable communication across execution boundaries: + +* **Multi-core systems**: Use IPC backend to coordinate between application and network processors, + or distribute workloads across multiple CPU cores. +* **Distributed devices**: Use UART backend to create distributed applications where multiple + connected devices participate in the same logical message bus over serial connections. + .. note:: ZBus can be used to transfer streams from the producer to the consumer. However, this can increase zbus' communication latency. So maybe consider a Pipe a good alternative for this @@ -954,6 +1092,30 @@ Related configuration options: observers to statically allocate. * :kconfig:option:`CONFIG_ZBUS_RUNTIME_OBSERVERS_NODE_ALLOC_NONE` use user-provided runtime observers nodes; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN` enable multi-domain communication support. + +Multi-domain Configuration Options +================================== + +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_IPC` enable IPC backend for multi-domain communication; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_UART` enable UART backend for multi-domain communication; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL` set the log level for multi-domain operations; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE` maximum message size for multi-domain + channels; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_CHANNEL_NAME_SIZE` maximum size of channel names in + multi-domain communication; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_PROXY_STACK_SIZE` stack size for proxy agent threads; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_PROXY_PRIORITY` priority of proxy agent threads; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_POOL_SIZE` number of sent messages to track for + acknowledgments; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS` maximum retry attempts for message + transmission; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT` initial acknowledgment timeout for + sent messages; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX` maximum acknowledgment timeout + for exponential backoff; +* :kconfig:option:`CONFIG_ZBUS_MULTIDOMAIN_UART_BUF_COUNT` number of UART buffers for multi-domain + communication; API Reference ************* diff --git a/include/zephyr/zbus/multidomain/zbus_multidomain.h b/include/zephyr/zbus/multidomain/zbus_multidomain.h new file mode 100644 index 0000000000000..f1055e4552e79 --- /dev/null +++ b/include/zephyr/zbus/multidomain/zbus_multidomain.h @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_H_ +#define ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_H_ + +#include +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_ZBUS_MULTIDOMAIN_UART) +#include +#endif +#if defined(CONFIG_ZBUS_MULTIDOMAIN_IPC) +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Zbus Multi-domain API + * @defgroup zbus_multidomain_apis Zbus Multi-domain APIs + * @ingroup zbus_apis + * @since 3.3.0 + * @version 1.0.0 + * @ingroup os_services + * @{ + */ + +/** + * @brief Structure for tracking sent messages awaiting acknowledgment. + * + * This structure is used internally by the proxy agent to keep track of messages + * that have been sent but not yet acknowledged. It contains a copy of the message, + * the number of transmit attempts, and a delayed work item for timeout handling. + */ +struct zbus_proxy_agent_tracked_msg { + /** Copy of the sent message */ + struct zbus_proxy_agent_msg msg; + + /** Pointer to the proxy agent configuration */ + struct zbus_proxy_agent_config *config; + + /** Number of transmit attempts made for this message */ + uint8_t transmit_attempts; + + /** Work item for handling acknowledgment timeout */ + struct k_work_delayable work; +}; + +/** + * @brief Configuration structure for the proxy agent. + * + * This structure holds the configuration for a proxy agent, including its name, + * type, backend specific API, and backend specific configuration. + */ +struct zbus_proxy_agent_config { + /* The name of the proxy agent */ + const char *name; + + /* The type of the proxy agent */ + enum zbus_multidomain_type type; + + /* Pointer to the backend specific API */ + const struct zbus_proxy_agent_api *api; + + /* Pointer to the backend specific configuration */ + void *backend_config; + + /* Pool for tracking sent messages awaiting acknowledgment */ + struct net_buf_pool *sent_msg_pool; + + /* List of sent messages awaiting acknowledgment */ + sys_slist_t sent_msg_list; +}; + +/** + * @brief Set up a proxy agent using the provided configuration. + * + * Starts the proxy agent thread and initializes the necessary resources. + * + * @note This macro sets up net_buf_pool for tracking sent messages, defines + * a zbus subscriber, and creates a thread for the proxy agent. + * + * @note the ZBUS_MULTIDOMAIN_SENT_MSG_POOL_SIZE configuration option + * must be set to a value greater than or equal to the maximum number of + * unacknowledged messages that can be in flight at any given time. + * + * @note The configuration options ZBUS_MULTIDOMAIN_PROXY_STACK_SIZE and + * ZBUS_MULTIDOMAIN_PROXY_PRIORITY define the stack size and priority of the + * proxy agent thread, respectively. + * + * @param _name The name of the proxy agent. + * @param _type The type of the proxy agent (enum zbus_multidomain_type) + * @param _nodeid The device node ID for the proxy agent. + */ +#define ZBUS_PROXY_AGENT_DEFINE(_name, _type, _nodeid) \ + NET_BUF_POOL_DEFINE(_name##_sent_msg_pool, CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_POOL_SIZE, \ + sizeof(struct zbus_proxy_agent_tracked_msg), sizeof(uint32_t), NULL); \ + _ZBUS_GENERATE_BACKEND_CONFIG(_name, _type, _nodeid); \ + struct zbus_proxy_agent_config _name##_config = { \ + .name = #_name, \ + .type = _type, \ + .api = _ZBUS_GET_API(_type), \ + .backend_config = _ZBUS_GET_CONFIG(_name, _type), \ + .sent_msg_pool = &_name##_sent_msg_pool, \ + }; \ + ZBUS_MSG_SUBSCRIBER_DEFINE(_name##_subscriber); \ + K_THREAD_DEFINE(_name##_thread_id, CONFIG_ZBUS_MULTIDOMAIN_PROXY_STACK_SIZE, \ + zbus_proxy_agent_thread, &_name##_config, &_name##_subscriber, NULL, \ + CONFIG_ZBUS_MULTIDOMAIN_PROXY_PRIORITY, 0, 0); + +/** + * @brief Add a channel to the proxy agent. + * + * @param _name The name of the proxy agent. + * @param _chan The channel to be added. + */ +#define ZBUS_PROXY_ADD_CHANNEL(_name, _chan) ZBUS_CHAN_ADD_OBS(_chan, _name##_subscriber, 0); + +/** + * @brief Thread function for the proxy agent. + * + * This function runs in a separate thread and continuously listens for messages + * on the zbus observer. It processes incoming messages and forwards them + * to the appropriate backend for sending. + * + * @param config Pointer to the configuration structure for the proxy agent. + * @param subscriber Pointer to the zbus observer that the proxy agent listens to. + * @return negative error code on failure. + */ +int zbus_proxy_agent_thread(struct zbus_proxy_agent_config *config, + const struct zbus_observer *subscriber); + +/** @cond INTERNAL_HIDDEN */ + +/** + * @brief Macros to generate backend specific configurations for the proxy agent. + * + * This macro generates the backend specific configurations based on the type of + * the proxy agent. + * + * @param _name The name of the proxy agent. + * @param _type The type of the proxy agent (enum zbus_multidomain_type). + * @param _nodeid The device node ID for the proxy agent. + * + * @note This macro finds the matching backend configuration macro from the + * backend specific header files. Requires the backend specific header files to + * define the macros in the format `_ZBUS_GENERATE_BACKEND_CONFIG_(_name, _nodeid)`. + */ +#define _ZBUS_GENERATE_BACKEND_CONFIG(_name, _type, _nodeid) \ + _ZBUS_GENERATE_BACKEND_CONFIG_##_type(_name, _nodeid) + +/** + * @brief Generic macros to get the API and configuration for the specified type of proxy agent. + * + * These macros are used to retrieve the API and configuration for the specified type of + * proxy agent. The type is specified as an argument to the macro. + * + * @param _type The type of the proxy agent (enum zbus_multidomain_type). + * @param _name The name of the proxy agent. + * + * @note These macros are used to retrieve the API and configuration for the specified type of + * proxy agent. Requires the backend specific header files to define the macros in the format + * `_ZBUS_GET_API_()` and `_ZBUS_GET_CONFIG_()`. + */ +#define _ZBUS_GET_API(_type) _ZBUS_GET_API_##_type() +#define _ZBUS_GET_CONFIG(_name, _type) _ZBUS_GET_CONFIG_##_type(_name) + +/** @endcond */ + +/** + * @} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_H_ */ diff --git a/include/zephyr/zbus/multidomain/zbus_multidomain_ipc.h b/include/zephyr/zbus/multidomain/zbus_multidomain_ipc.h new file mode 100644 index 0000000000000..ac7498998883b --- /dev/null +++ b/include/zephyr/zbus/multidomain/zbus_multidomain_ipc.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_IPC_H_ +#define ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_IPC_H_ + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Zbus Multi-domain API + * @defgroup zbus_multidomain_apis Zbus Multi-domain APIs + * @ingroup zbus_apis + * @since 3.3.0 + * @version 1.0.0 + * @ingroup os_services + * @{ + */ + +/** + * @brief Structure for IPC backend configuration. + */ +struct zbus_multidomain_ipc_config { + /** IPC device */ + const struct device *dev; + + /** IPC endpoint */ + struct ipc_ept ipc_ept; + + /** IPC endpoint configuration */ + struct ipc_ept_cfg *ept_cfg; + + /** Semaphore to signal when the IPC endpoint is bound */ + struct k_sem ept_bound_sem; + + /** Callback function for received messages */ + int (*recv_cb)(const struct zbus_proxy_agent_msg *msg); + + /** Callback function for ACKs */ + int (*ack_cb)(uint32_t msg_id, void *user_data); + + /** User data for the ACK callback */ + void *ack_cb_user_data; + + /** Work item for sending ACKs */ + struct k_work ack_work; + + /** Message ID to ACK */ + uint32_t ack_msg_id; +}; + +/** @cond INTERNAL_HIDDEN */ + +/* IPC backend API structure */ +extern const struct zbus_proxy_agent_api zbus_multidomain_ipc_api; + +/** + * @brief Macros to get the API and configuration for the IPC backend. + * + * These macros are used to retrieve the API and configuration for the IPC backend + * of the proxy agent. The macros are used in "zbus_multidomain.h" to define the + * backend specific configurations and API for the IPC type of proxy agent. + * + * @param _name The name of the proxy agent. + */ +#define _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC() &zbus_multidomain_ipc_api +#define _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(_name) (void *)&_name##_ipc_config + +/** + * @brief Macros to generate device specific backend configurations for the IPC type. + * + * This macro generates the backend specific configurations for the IPC type of + * proxy agent. The macro is used in "zbus_multidomain.h" to create the + * backend specific configurations for the IPC type of proxy agent. + * + * @param _name The name of the proxy agent. + * @param _nodeid The device node ID for the proxy agent. + */ +#define _ZBUS_GENERATE_BACKEND_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(_name, _nodeid) \ + static struct ipc_ept_cfg _name##_ipc_ept_cfg = { \ + .name = "ipc_ept_" #_name, \ + }; \ + static struct zbus_multidomain_ipc_config _name##_ipc_config = { \ + .dev = DEVICE_DT_GET(_nodeid), \ + .ept_cfg = &_name##_ipc_ept_cfg, \ + } + +/** @endcond */ + +/** + * @} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_IPC_H_ */ diff --git a/include/zephyr/zbus/multidomain/zbus_multidomain_types.h b/include/zephyr/zbus/multidomain/zbus_multidomain_types.h new file mode 100644 index 0000000000000..9f4e9dad88a2b --- /dev/null +++ b/include/zephyr/zbus/multidomain/zbus_multidomain_types.h @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_TYPES_H_ +#define ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_TYPES_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Zbus Multi-domain API + * @defgroup zbus_multidomain_apis Zbus Multi-domain APIs + * @ingroup zbus_apis + * @since 3.3.0 + * @version 1.0.0 + * @ingroup os_services + * @{ + */ + +/** + * @brief Type of the proxy agent. + * + * This enum defines the types of proxy agents that can be used in a multi-domain + * Zbus setup. Each type corresponds to a different communication backend. + */ +enum zbus_multidomain_type { + ZBUS_MULTIDOMAIN_TYPE_UART, + ZBUS_MULTIDOMAIN_TYPE_IPC +}; + +/** + * @brief Type of the proxy agent message. + * + * This enum defines the types of messages that can be sent or received by the proxy agent. + * A message can either be a data message or an acknowledgment (ACK) message. + */ +enum zbus_proxy_agent_msg_type { + ZBUS_PROXY_AGENT_MSG_TYPE_MSG = 0, + ZBUS_PROXY_AGENT_MSG_TYPE_ACK = 1, +}; + +/** + * @brief Message structure for the proxy agent. + * + * This structure represents a message that is sent or received by the proxy agent. + * It contains the size of the message, the actual message data, and the channel name + * associated with the message. + */ +struct zbus_proxy_agent_msg { + /* Type of the message: data or acknowledgment */ + uint8_t type; + + /* Message id. sys_clock_cycle when sent */ + uint32_t id; + + /* The size of the message */ + uint32_t message_size; + + /* The channel associated with the message */ + uint8_t message_data[CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE]; + + /* The length of the channel name */ + uint32_t channel_name_len; + + /* The name of the channel */ + char channel_name[CONFIG_ZBUS_MULTIDOMAIN_CHANNEL_NAME_SIZE]; + + /* CRC32 of the message for integrity check */ + uint32_t crc32; +} __packed; + +/** + * @brief Proxy agent API structure. + */ +struct zbus_proxy_agent_api { + /** + * @brief Initialize the backend for the proxy agent. + * + * This function is called to initialize the backend specific to the proxy agent. + * + * @param config Pointer to the backend specific configuration. + * @return int 0 on success, negative error code on failure. + */ + int (*backend_init)(void *config); + + /** + * @brief Send a message through the proxy agent. + * + * This function is called to send a message through the proxy agent. + * + * @param config Pointer to the backend specific configuration. + * @param msg Pointer to the message to be sent. + * @return int 0 on success, negative error code on failure. + */ + int (*backend_send)(void *config, struct zbus_proxy_agent_msg *msg); + + /** + * @brief Set the receive callback for the proxy agent. + * + * This function is called to set the callback function that will be invoked + * when a message is received by the backend. + * + * @param config Pointer to the backend specific configuration. + * @param recv_cb Pointer to the callback function to be set. + * @return int 0 on success, negative error code on failure. + */ + int (*backend_set_recv_cb)(void *config, + int (*recv_cb)(const struct zbus_proxy_agent_msg *msg)); + + /** + * @brief Set the acknowledgment callback for the proxy agent. + * + * This function is called to set the callback function that will be invoked + * when an acknowledgment is received for a sent message. + * + * @param config Pointer to the backend specific configuration. + * @param ack_cb Pointer to the acknowledgment callback function to be set. + * @param user_data Pointer to user data that will be passed to the acknowledgment callback. + * @return int 0 on success, negative error code on failure. + */ + int (*backend_set_ack_cb)(void *config, int (*ack_cb)(uint32_t msg_id, void *user_data), + void *user_data); +}; + +/** + * @brief Initialize a proxy agent message with CRC + * + * This function initializes a zbus_proxy_agent_msg structure with the provided + * channel and message data, automatically setting the message type, ID, and CRC. + * + * @param msg Pointer to the message structure to initialize + * @param message_data Pointer to the message data to include in the message + * @param data_size Size of the message data in bytes + * @param channel_name Pointer to the name of the channel associated with the message + * @param channel_name_len Length of the channel name in bytes + * @return 0 on success, negative error code on failure + */ +static inline int zbus_create_proxy_agent_msg(struct zbus_proxy_agent_msg *msg, void *message_data, + size_t data_size, const char *channel_name, + size_t channel_name_len) +{ + if (!msg || !message_data || !channel_name || data_size == 0 || + data_size > CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE || channel_name_len == 0 || + channel_name_len > CONFIG_ZBUS_MULTIDOMAIN_CHANNEL_NAME_SIZE) { + return -EINVAL; + } + + memset(msg, 0, sizeof(*msg)); + + msg->type = ZBUS_PROXY_AGENT_MSG_TYPE_MSG; + msg->id = sys_clock_cycle_get_32(); + msg->message_size = data_size; + memcpy(msg->message_data, message_data, data_size); + msg->channel_name_len = channel_name_len; + strncpy(msg->channel_name, channel_name, sizeof(msg->channel_name) - 1); + msg->channel_name[sizeof(msg->channel_name) - 1] = '\0'; + msg->crc32 = crc32_ieee((const uint8_t *)msg, sizeof(*msg) - sizeof(msg->crc32)); + return 0; +} + +/** + * @brief Initialize an ACK proxy agent message with CRC + * + * This function initializes a zbus_proxy_agent_msg structure as an acknowledgment (ACK) + * + * @param msg pointer to the message structure to initialize + * @param msg_id id of the message being acknowledged + * @return int 0 on success, negative error code on failure + */ +static inline int zbus_create_proxy_agent_ack_msg(struct zbus_proxy_agent_msg *msg, uint32_t msg_id) +{ + if (!msg) { + return -EINVAL; + } + + memset(msg, 0, sizeof(*msg)); + + msg->type = ZBUS_PROXY_AGENT_MSG_TYPE_ACK; + msg->id = msg_id; + msg->message_size = 0; + msg->crc32 = crc32_ieee((const uint8_t *)msg, sizeof(*msg) - sizeof(msg->crc32)); + return 0; +} + +/** + * @brief Verify the CRC32 of a proxy agent message + * + * This function verifies the CRC32 checksum of a given zbus_proxy_agent_msg structure. + * It calculates the CRC32 of the message (excluding the crc32 field itself) and compares + * it to the provided crc32 value in the structure. + * + * @param msg pointer to the message structure to verify + * @return int 0 if the CRC is valid, negative error code on failure + */ +static inline int verify_proxy_agent_msg_crc(const struct zbus_proxy_agent_msg *msg) +{ + if (!msg) { + return -EINVAL; + } + uint32_t calculated_crc = + crc32_ieee((const uint8_t *)msg, sizeof(*msg) - sizeof(msg->crc32)); + + if (calculated_crc != msg->crc32) { + return -EILSEQ; + } + + return 0; +} + +/** + * @} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_TYPES_H_ */ diff --git a/include/zephyr/zbus/multidomain/zbus_multidomain_uart.h b/include/zephyr/zbus/multidomain/zbus_multidomain_uart.h new file mode 100644 index 0000000000000..3aed5db1dc6cd --- /dev/null +++ b/include/zephyr/zbus/multidomain/zbus_multidomain_uart.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_UART_H_ +#define ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_UART_H_ + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Zbus Multi-domain API + * @defgroup zbus_multidomain_apis Zbus Multi-domain APIs + * @ingroup zbus_apis + * @since 3.3.0 + * @version 1.0.0 + * @ingroup os_services + * @{ + */ + +struct zbus_multidomain_uart_config { + /** UART device */ + const struct device *dev; + + /** Asynchronous RX buffer */ + uint8_t async_rx_buf[CONFIG_ZBUS_MULTIDOMAIN_UART_BUF_COUNT] + [sizeof(struct zbus_proxy_agent_msg)]; + + /** Index of the current async RX buffer */ + volatile uint8_t async_rx_buf_idx; + + /** Semaphore to signal when TX is done */ + struct k_sem tx_busy_sem; + + struct zbus_proxy_agent_msg tx_msg_copy; + + /** Callback function for received messages */ + int (*recv_cb)(const struct zbus_proxy_agent_msg *msg); + + /** Callback function for ACKs */ + int (*ack_cb)(uint32_t msg_id, void *user_data); + + /** User data for the ACK callback */ + void *ack_cb_user_data; + + /** Work item for sending ACKs */ + struct k_work ack_work; + + /** Message ID to ACK */ + uint32_t ack_msg_id; +}; + +/** @cond INTERNAL_HIDDEN */ + +/* UART backend API structure */ +extern const struct zbus_proxy_agent_api zbus_multidomain_uart_api; + +/** + * @brief Macros to get the API and configuration for the UART backend. + * + * These macros are used to retrieve the API and configuration for the UART backend + * of the proxy agent. The macros are used in "zbus_multidomain.h" to define the + * backend specific configurations and API for the UART type of proxy agent. + * + * @param _name The name of the proxy agent. + */ +#define _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_UART() &zbus_multidomain_uart_api +#define _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(_name) (void *)&_name##_uart_config + +/** + * @brief Macros to generate device specific backend configurations for the UART type. + * + * This macro generates the backend specific configurations for the UART type of + * proxy agent. The macro is used in "zbus_multidomain.h" to create the + * backend specific configurations for the UART type of proxy agent. + */ +#define _ZBUS_GENERATE_BACKEND_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(_name, _nodeid) \ + struct zbus_multidomain_uart_config _name##_uart_config = { \ + .dev = DEVICE_DT_GET(_nodeid), \ + .async_rx_buf = {{0}}, \ + .async_rx_buf_idx = 0, \ + } + +/** @endcond */ + +/** + * @} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_ZBUS_MULTIDOMAIN_UART_H_ */ diff --git a/include/zephyr/zbus/zbus.h b/include/zephyr/zbus/zbus.h index 4911fe298f369..d51b78eb023e7 100644 --- a/include/zephyr/zbus/zbus.h +++ b/include/zephyr/zbus/zbus.h @@ -109,6 +109,16 @@ struct zbus_channel { /** Mutable channel data struct. */ struct zbus_channel_data *data; + +#if defined(CONFIG_ZBUS_MULTIDOMAIN) || defined(__DOXYGEN__) + + /** Indicates if the channel is a shadow channel. + * A shadow channel is a channel that should not be used directly, but rather + * shadows another channel, usually one that is defined in another domain. + */ + bool is_shadow_channel; + +#endif /* CONFIG_ZBUS_MULTIDOMAIN */ }; /** @@ -275,7 +285,7 @@ struct zbus_channel_observation { #define _ZBUS_MESSAGE_NAME(_name) _CONCAT(_zbus_message_, _name) /* clang-format off */ -#define _ZBUS_CHAN_DEFINE(_name, _id, _type, _validator, _user_data) \ +#define _ZBUS_CHAN_DEFINE(_name, _id, _type, _validator, _user_data, _is_shadow) \ static struct zbus_channel_data _CONCAT(_zbus_chan_data_, _name) = { \ .observers_start_idx = -1, \ .observers_end_idx = -1, \ @@ -295,6 +305,7 @@ struct zbus_channel_observation { .user_data = _user_data, \ .validator = _validator, \ .data = &_CONCAT(_zbus_chan_data_, _name), \ + IF_ENABLED(CONFIG_ZBUS_MULTIDOMAIN, (.is_shadow_channel = _is_shadow,)) \ IF_ENABLED(ZBUS_MSG_SUBSCRIBER_NET_BUF_POOL_ISOLATION, \ (.msg_subscriber_pool = &_zbus_msg_subscribers_pool,)) \ } @@ -388,12 +399,46 @@ struct zbus_channel_observation { */ #define ZBUS_CHAN_DEFINE(_name, _type, _validator, _user_data, _observers, _init_val) \ static _type _ZBUS_MESSAGE_NAME(_name) = _init_val; \ - _ZBUS_CHAN_DEFINE(_name, ZBUS_CHAN_ID_INVALID, _type, _validator, _user_data); \ + _ZBUS_CHAN_DEFINE(_name, ZBUS_CHAN_ID_INVALID, _type, _validator, _user_data, false); \ + /* Extern declaration of observers */ \ + ZBUS_OBS_DECLARE(_observers); \ + /* Create all channel observations from observers list */ \ + FOR_EACH_FIXED_ARG_NONEMPTY_TERM(_ZBUS_CHAN_OBSERVATION, (;), _name, _observers) + +#if defined(CONFIG_ZBUS_MULTIDOMAIN) || defined(__DOXYGEN__) + +/** + * @brief Zbus shadow channel definition. + * + * This macro defines a shadow channel. + * Similar to ZBUS_CHAN_DEFINE, but defines the channel with the + * is_shadow_channel flag set to true, blocking the channel from + * being published to normally. + * + * @param _name The channel's name. + * @param _type The Message type. It must be a struct or union. + * @param _validator The validator function. + * @param _user_data A pointer to the user data. + * + * @see struct zbus_channel + * @param _observers The observers list. The sequence indicates the priority of the observer. The + * first the highest priority. + * @param _init_val The message initialization. + * + * @note This macro is used to define shadow channels in a multi-domain setup. + * Shadow channels are used to represent channels that are defined in another domain, allowing + * the current domain to observe them without directly publishing to them. + */ +#define ZBUS_SHADOW_CHAN_DEFINE(_name, _type, _validator, _user_data, _observers, _init_val) \ + static _type _ZBUS_MESSAGE_NAME(_name) = _init_val; \ + _ZBUS_CHAN_DEFINE(_name, ZBUS_CHAN_ID_INVALID, _type, _validator, _user_data, true); \ /* Extern declaration of observers */ \ ZBUS_OBS_DECLARE(_observers); \ /* Create all channel observations from observers list */ \ FOR_EACH_FIXED_ARG_NONEMPTY_TERM(_ZBUS_CHAN_OBSERVATION, (;), _name, _observers) +#endif /* CONFIG_ZBUS_MULTIDOMAIN */ + /** * @brief Zbus channel definition with numeric identifier. * @@ -412,12 +457,133 @@ struct zbus_channel_observation { */ #define ZBUS_CHAN_DEFINE_WITH_ID(_name, _id, _type, _validator, _user_data, _observers, _init_val) \ static _type _ZBUS_MESSAGE_NAME(_name) = _init_val; \ - _ZBUS_CHAN_DEFINE(_name, _id, _type, _validator, _user_data); \ + _ZBUS_CHAN_DEFINE(_name, _id, _type, _validator, _user_data, false); \ /* Extern declaration of observers */ \ ZBUS_OBS_DECLARE(_observers); \ /* Create all channel observations from observers list */ \ FOR_EACH_FIXED_ARG_NONEMPTY_TERM(_ZBUS_CHAN_OBSERVATION, (;), _name, _observers) +#if defined(CONFIG_ZBUS_MULTIDOMAIN) || defined(__DOXYGEN__) + +/** + * @brief Zbus shadow channel definition. + * + * This macro defines a shadow channel. + * Similar to ZBUS_CHAN_DEFINE, but defines the channel with the + * is_shadow_channel flag set to true, blocking the channel from + * being published to normally. + * + * @param _name The channel's name. + * @param _id The channel's unique numeric identifier. + * @param _type The Message type. It must be a struct or union. + * @param _validator The validator function. + * @param _user_data A pointer to the user data. + * + * @see struct zbus_channel + * @param _observers The observers list. The sequence indicates the priority of the observer. The + * first the highest priority. + * @param _init_val The message initialization. + * + * @note This macro is used to define shadow channels in a multi-domain setup. + * Shadow channels are used to represent channels that are defined in another domain, allowing + * the current domain to observe them without directly publishing to them. + */ +#define ZBUS_SHADOW_CHAN_DEFINE_WITH_ID(_name, _id, _type, _validator, _user_data, _observers, \ + _init_val) \ + static _type _ZBUS_MESSAGE_NAME(_name) = _init_val; \ + _ZBUS_CHAN_DEFINE(_name, _id, _type, _validator, _user_data, true); \ + /* Extern declaration of observers */ \ + ZBUS_OBS_DECLARE(_observers); \ + /* Create all channel observations from observers list */ \ + FOR_EACH_FIXED_ARG_NONEMPTY_TERM(_ZBUS_CHAN_OBSERVATION, (;), _name, _observers) + +/** + * @brief Macro to check if a channel is a shadow channel. + * + * @param _chan The channel to check. + * @return true if the channel is a shadow channel, false otherwise. + */ +#define ZBUS_CHANNEL_IS_SHADOW(_chan) ((_chan)->is_shadow_channel) + +/** + * @brief Macro to check if a channel is a master channel. + * + * @param _chan The channel to check. + * @return true if the channel is a master channel, false if it is a shadow channel. + */ +#define ZBUS_CHANNEL_IS_MASTER(_chan) (!(_chan)->is_shadow_channel) + +/** + * @brief Zbus multi-domain channel definition. + * + * This macro defines a channel that can be either a master or a shadow channel based on the + * is_master and is_included flags. If is_master is true, it defines a normal channel, otherwise + * it defines a shadow channel. If is_included is false, the channel will not be defined at all. + * Intended usage is in a shared header in multi-domain setups where the channel is defined in + * one domain and shadowed in others, using device specific defines to control the inclusion + * and master status. + * + * @param _name The channel's name. + * @param _type The Message type. It must be a struct or union. + * @param _validator The validator function. + * @param _user_data A pointer to the user data. + * @param _observers The observers list. The sequence indicates the priority of the observer. The + * first the highest priority. + * @param _init_val The message initialization. + * @param _is_master Indicates if this is the master channel (true) or a shadow channel (false). + * @param _is_included Indicates if the channel should be included in this device (true) or not + * (false). + * + * @note This macro is used to define channels in a multi-domain setup where the channel can be + * either a master channel or a shadow channel, depending on the device configuration. + */ +#define ZBUS_MULTIDOMAIN_CHAN_DEFINE(_name, _type, _validator, _user_data, _observers, _init_val, \ + _is_master, _is_included) \ + COND_CODE_1(_is_included, \ + (COND_CODE_1(_is_master, \ + (ZBUS_CHAN_DEFINE(_name, _type, _validator, _user_data, _observers, \ + _init_val)), \ + (ZBUS_SHADOW_CHAN_DEFINE(_name, _type, _validator, _user_data, _observers, \ + _init_val)))), \ + (/* Channel not included on this device - no definition*/)) + +/** + * @brief Zbus multi-domain channel definition with ID. + * + * This macro defines a channel that can be either a master or a shadow channel based on the + * is_master and is_included flags. If is_master is true, it defines a normal channel with a unique + * ID, otherwise it defines a shadow channel with the same ID. If is_included is false, the channel + * will not be defined at all. Intended usage is in a shared header in multi-domain setups where the + * channel is defined in one domain and shadowed in others, using device specific defines to control + * the inclusion and master status. + * + * @param _name The channel's name. + * @param _id The channel's unique numeric identifier. + * @param _type The Message type. It must be a struct or union. + * @param _validator The validator function. + * @param _user_data A pointer to the user data. + * @param _observers The observers list. The sequence indicates the priority of the observer. The + * first the highest priority. + * @param _init_val The message initialization. + * @param _is_master Indicates if this is the master channel (true) or a shadow channel (false). + * @param _is_included Indicates if the channel should be included in this device (true) or not + * (false). + * + * @note This macro is used to define channels in a multi-domain setup where the channel can be + * either a master channel or a shadow channel, depending on the device configuration. + */ +#define ZBUS_MULTIDOMAIN_CHAN_DEFINE_WITH_ID(_name, _id, _type, _validator, _user_data, \ + _observers, _init_val, _is_master, _is_included) \ + COND_CODE_1(_is_included, \ + (COND_CODE_1(_is_master, \ + (ZBUS_CHAN_DEFINE_WITH_ID(_name, _id, _type, _validator, _user_data, \ + _observers, _init_val)), \ + (ZBUS_SHADOW_CHAN_DEFINE_WITH_ID(_name, _id, _type, _validator, _user_data,\ + _observers, _init_val)))), \ + (/* Channel not included on this device - no definition*/)) + +#endif /* CONFIG_ZBUS_MULTIDOMAIN */ + /** * @brief Initialize a message. * @@ -575,9 +741,46 @@ struct zbus_channel_observation { * @retval -EFAULT A parameter is incorrect, the notification could not be sent to one or more * observer, or the function context is invalid (inside an ISR). The function only returns this * value when the @kconfig{CONFIG_ZBUS_ASSERT_MOCK} is enabled. + * @retval -EPERM Attempt to publish a shadow channel. Shadow channels can only be published to + * via the zbus_chan_pub_shadow function. The function only returns this value when the + * @kconfig{CONFIG_ZBUS_MULTIDOMAIN} is enabled. */ int zbus_chan_pub(const struct zbus_channel *chan, const void *msg, k_timeout_t timeout); +#if defined(CONFIG_ZBUS_MULTIDOMAIN) + +/** @cond INTERNAL_HIDDEN */ + +/** + * @brief Publish to a shadow channel + * + * This routine publishes a message to a shadow channel. + * + * @param chan The channel's reference. + * @param msg Reference to the message where the publish function copies the channel's + * message data from. + * @param timeout Waiting period to publish the channel, + * or one of the special values K_NO_WAIT and K_FOREVER. + * + * @retval 0 Channel published. + * @retval -ENOMSG The message is invalid based on the validator function or some of the + * observers could not receive the notification. + * @retval -EBUSY The channel is busy. + * @retval -EAGAIN Waiting period timed out. + * @retval -EFAULT A parameter is incorrect, the notification could not be sent to one or more + * observer, or the function context is invalid (inside an ISR). The function only returns this + * value when the @kconfig{CONFIG_ZBUS_ASSERT_MOCK} is enabled. + * + * @note This function is used to publish messages to shadow channels in a multi-domain setup, + * and should not be used by application logic directly. It is intended for internal use to + * handle shadow channels. + */ +int zbus_chan_pub_shadow(const struct zbus_channel *chan, const void *msg, k_timeout_t timeout); + +/** @endcond */ + +#endif /* CONFIG_ZBUS_MULTIDOMAIN */ + /** * @brief Read a channel * @@ -690,6 +893,20 @@ const struct zbus_channel *zbus_chan_from_id(uint32_t channel_id); #endif +#if defined(CONFIG_ZBUS_CHANNEL_NAME) || defined(__DOXYGEN__) + +/** + * @brief Retrieve a zbus channel from its name string + * + * @param name Name of the channel to retrieve. + * + * @retval NULL If channel with name @a name does not exist. + * @retval chan Channel pointer with name @a name otherwise. + */ +const struct zbus_channel *zbus_chan_from_name(const char *name); + +#endif + /** * @brief Get the reference for a channel message directly. * diff --git a/samples/subsys/zbus/ipc_forwarder/CMakeLists.txt b/samples/subsys/zbus/ipc_forwarder/CMakeLists.txt new file mode 100644 index 0000000000000..5b7df7963b597 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/CMakeLists.txt @@ -0,0 +1,20 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(zbus_ipc_forwarder) + +zephyr_library_include_directories( + src + common +) + +# Define which device this is +zephyr_compile_definitions(ZBUS_DEVICE_A=1) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/subsys/zbus/ipc_forwarder/Kconfig b/samples/subsys/zbus/ipc_forwarder/Kconfig new file mode 100644 index 0000000000000..dd4eea898970d --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/Kconfig @@ -0,0 +1,7 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +source "Kconfig.zephyr" diff --git a/samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild b/samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild new file mode 100644 index 0000000000000..89a28dd9eebc8 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild @@ -0,0 +1,47 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +source "share/sysbuild/Kconfig" + +choice + prompt "Remote board target" + default REMOTE_BOARD_NRF54H20_CPURAD if BOARD = "nrf54h20dk" + default REMOTE_BOARD_NRF5340_CPUNET if BOARD = "nrf5340dk" + default REMOTE_BOARD_NRF5340_CPUNET_SIM if BOARD = "nrf5340bsim" + +config REMOTE_BOARD_NRF54H20_CPURAD + bool "nrf54h20dk/nrf54h20/cpurad" + +config REMOTE_BOARD_NRF54H20_CPUPPR + bool "nrf54h20dk/nrf54h20/cpuppr" + +config REMOTE_BOARD_NRF54H20_CPUPPR_XIP + bool "nrf54h20dk/nrf54h20/cpuppr/xip" + +## Do not fit on nRF54H20_CPUFLPR without XIP + +config REMOTE_BOARD_NRF54H20_CPUFLPR_XIP + bool "nrf54h20dk/nrf54h20/cpuflpr/xip" + +config REMOTE_BOARD_NRF5340_CPUNET + bool "nrf5340dk/nrf5340/cpunet" + +config REMOTE_BOARD_NRF5340_CPUNET_SIM + bool "nrf5340bsim/nrf5340/cpunet" + help + Use this option to run the sample on nRF5340 CPU network simulator target. + This option is useful for development and testing without requiring physical hardware. + +endchoice + +config REMOTE_BOARD + string "The board used for remote target" + default "nrf54h20dk/nrf54h20/cpurad" if REMOTE_BOARD_NRF54H20_CPURAD + default "nrf54h20dk/nrf54h20/cpuppr" if REMOTE_BOARD_NRF54H20_CPUPPR + default "nrf54h20dk/nrf54h20/cpuppr/xip" if REMOTE_BOARD_NRF54H20_CPUPPR_XIP + default "nrf54h20dk/nrf54h20/cpuflpr/xip" if REMOTE_BOARD_NRF54H20_CPUFLPR_XIP + default "nrf5340dk/nrf5340/cpunet" if REMOTE_BOARD_NRF5340_CPUNET + default "nrf5340bsim/nrf5340/cpunet" if REMOTE_BOARD_NRF5340_CPUNET_SIM diff --git a/samples/subsys/zbus/ipc_forwarder/README.rst b/samples/subsys/zbus/ipc_forwarder/README.rst new file mode 100644 index 0000000000000..8a85aa6381e42 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/README.rst @@ -0,0 +1,87 @@ +.. zephyr:code-sample:: zbus-ipc-forwarder + :name: zbus Proxy agent - IPC forwarder + :relevant-api: zbus_apis + + Forward zbus messages between different domains using IPC. + +Overview +******** +This sample demonstrates zbus inter-domain communication on multi-core platforms. +The sample implements a request-response pattern where: + +- The **application CPU** acts as a requester, publishing requests on a master channel and receiving responses on a shadow channel +- The **remote CPU** acts as a responder, receiving requests on a shadow channel and publishing responses on a master channel +- **IPC forwarders** automatically synchronize channel data between CPU domains using the zbus proxy functionality + +The ``common/common.h`` file defines shared channels using :c:macro:`ZBUS_MULTIDOMAIN_CHAN_DEFINE` with conditional +compilation to create master channels on one domain and shadow channels on the other. + +This architecture enables message passing between different CPU cores while maintaining zbus's +publish-subscribe structure across domain boundaries. + +Building and Running +******************** + +Use sysbuild to build this sample for multi-core platforms: + +For nRF54H20 with CPURAD: + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/zbus/ipc_forwarder + :board: nrf54h20dk/nrf54h20/cpuapp + :goals: build + :west-args: --sysbuild -S nordic-log-stm + +It can also be built for the other cores in the nRF54H20 using sysbuild configurations: + +.. code-block:: + + SB_CONFIG_REMOTE_BOARD_NRF54H20_CPURAD=y # Default + SB_CONFIG_REMOTE_BOARD_NRF54H20_CPUPPR=y + SB_CONFIG_REMOTE_BOARD_NRF54H20_CPUPPR_XIP=y + SB_CONFIG_REMOTE_BOARD_NRF54H20_CPUFLPR_XIP=y + +For nRF5340: + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/zbus/ipc_forwarder + :board: nrf5340dk/nrf5340/cpuapp + :goals: build + :west-args: --sysbuild + +Sample Output +************* + +Application CPU Output +====================== + +.. code-block:: console + + *** Booting Zephyr OS build v4.2.0-1157-gaaa3626d8206 *** + main: ZBUS Multidomain IPC Forwarder Sample Application + main: Channel request_channel is a master channel + main: Channel response_channel is a shadow channel + main: Published on channel request_channel. Request ID=1, Min=-1, Max=1 + main: Received message on channel response_channel + main: Response ID: 1, Value: 0 + main: Published on channel request_channel. Request ID=2, Min=-2, Max=2 + main: Received message on channel response_channel + main: Response ID: 2, Value: -1 + +Remote CPU Output +================= + +.. code-block:: console + + *** Booting Zephyr OS build v4.2.0-1157-gaaa3626d8206 *** + main: ZBUS Multidomain IPC Forwarder Sample Application + main: Channel request_channel is a shadow channel + main: Channel response_channel is a master channel + main: Received message on channel request_channel + main: Request ID: 1, Min: -1, Max: 1 + main: Sending response: ID=1, Value=0 + main: Response published on channel response_channel + main: Received message on channel request_channel + main: Request ID: 2, Min: -2, Max: 2 + main: Sending response: ID=2, Value=-1 + main: Response published on channel response_channel diff --git a/samples/subsys/zbus/ipc_forwarder/boards/nrf5340bsim_nrf5340_cpuapp.conf b/samples/subsys/zbus/ipc_forwarder/boards/nrf5340bsim_nrf5340_cpuapp.conf new file mode 100644 index 0000000000000..25a5d4c17feaa --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/boards/nrf5340bsim_nrf5340_cpuapp.conf @@ -0,0 +1,7 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_SOC_NRF53_CPUNET_ENABLE=y diff --git a/samples/subsys/zbus/ipc_forwarder/boards/nrf5340dk_nrf5340_cpuapp.conf b/samples/subsys/zbus/ipc_forwarder/boards/nrf5340dk_nrf5340_cpuapp.conf new file mode 100644 index 0000000000000..25a5d4c17feaa --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/boards/nrf5340dk_nrf5340_cpuapp.conf @@ -0,0 +1,7 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_SOC_NRF53_CPUNET_ENABLE=y diff --git a/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp.overlay b/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp.overlay new file mode 100644 index 0000000000000..9ba7cb974c575 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp.overlay @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +&cpuapp_bellboard { + status = "okay"; +}; + +/ { + chosen { + /delete-property/ zephyr,bt-hci; + }; +}; diff --git a/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuflpr_xip.overlay b/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuflpr_xip.overlay new file mode 100644 index 0000000000000..3d9188325e3c0 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuflpr_xip.overlay @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + + /* Setup for cpuapp_cpuflpr_ipc */ +&cpuapp_bellboard { + status = "okay"; +}; + +/delete-node/ &cpuapp_cpurad_ipc; + +ipc0: &cpuapp_cpuflpr_ipc { + status = "okay"; +}; + +&cpuflpr_vevif { + status = "okay"; +}; + +/ { + chosen { + /delete-property/ zephyr,bt-hci; + }; +}; + +/* Necessary for the cpuflpr_xip to work */ +&cpuflpr_vpr { + status = "okay"; + execution-memory = <&cpuflpr_code_partition>; + /delete-property/ source-memory; +}; + +&uart120 { + status = "reserved"; + interrupt-parent = <&cpuflpr_clic>; +}; diff --git a/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr.overlay b/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr.overlay new file mode 100644 index 0000000000000..5e8d0fea080c0 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr.overlay @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + + /* Setup for cpuapp_cpuppr_ipc */ +&cpuapp_bellboard { + status = "okay"; +}; + +/delete-node/ &cpuapp_cpurad_ipc; + +ipc0: &cpuapp_cpuppr_ipc { + status = "okay"; +}; + +&cpuppr_vevif { + status = "okay"; +}; + +/ { + chosen { + /delete-property/ zephyr,bt-hci; + }; +}; + +/* Necessary for the cpuppr to work */ +&cpuppr_vpr { + status = "okay"; +}; + +&uart135 { + status = "reserved"; +}; diff --git a/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr_xip.overlay b/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr_xip.overlay new file mode 100644 index 0000000000000..429d755df79dc --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr_xip.overlay @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + + /* Setup for cpuapp_cpuppr_ipc */ +&cpuapp_bellboard { + status = "okay"; +}; + +/delete-node/ &cpuapp_cpurad_ipc; + +ipc0: &cpuapp_cpuppr_ipc { + status = "okay"; +}; + +&cpuppr_vevif { + status = "okay"; +}; + +/ { + chosen { + /delete-property/ zephyr,bt-hci; + }; +}; + +/* Necessary for the cpuppr_xip to work */ +&cpuppr_vpr { + status = "okay"; + execution-memory = <&cpuppr_code_partition>; + /delete-property/ source-memory; +}; + +&uart135 { + status = "reserved"; +}; diff --git a/samples/subsys/zbus/ipc_forwarder/common/common.h b/samples/subsys/zbus/ipc_forwarder/common/common.h new file mode 100644 index 0000000000000..d757114322ad4 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/common/common.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef COMMON_H +#define COMMON_H + +#include + +/* Sample data structures for request and response */ +struct request_data { + int request_id; + int min_value; + int max_value; +}; + +struct response_data { + int response_id; + int value; +}; + +/* Conditional compilation for device-specific code, needed if channels should be included on a + * subset of applications devices + */ +#if defined(ZBUS_DEVICE_A) || defined(ZBUS_DEVICE_B) +#define include_on_device_a_b 1 +#else +#define include_on_device_a_b 0 +#endif + +/* Define shared channels for request and response + * request_channel is master on device A and shadow on device B + * response_channel is shadow on device A and master on device B + */ +ZBUS_MULTIDOMAIN_CHAN_DEFINE(request_channel, struct request_data, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0), IS_ENABLED(ZBUS_DEVICE_A), include_on_device_a_b); + +ZBUS_MULTIDOMAIN_CHAN_DEFINE(response_channel, struct response_data, NULL, NULL, + ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0), IS_ENABLED(ZBUS_DEVICE_B), + include_on_device_a_b); + +#endif /* COMMON_H */ diff --git a/samples/subsys/zbus/ipc_forwarder/prj.conf b/samples/subsys/zbus/ipc_forwarder/prj.conf new file mode 100644 index 0000000000000..f3daf9b7949b6 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/prj.conf @@ -0,0 +1,27 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_LOG=y +# CONFIG_LOG_MODE_MINIMAL=y +CONFIG_LOG_DBG_COLOR_BLUE=y + +CONFIG_HEAP_MEM_POOL_SIZE=4096 + +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y + +CONFIG_CRC=y + +CONFIG_MBOX=y +CONFIG_IPC_SERVICE=y + +CONFIG_ZBUS_MULTIDOMAIN=y +CONFIG_ZBUS_MULTIDOMAIN_IPC=y +CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL_INF=y + +# Increase the size of the receive buffer to accommodate larger messages +CONFIG_PBUF_RX_READ_BUF_SIZE=384 diff --git a/samples/subsys/zbus/ipc_forwarder/remote/CMakeLists.txt b/samples/subsys/zbus/ipc_forwarder/remote/CMakeLists.txt new file mode 100644 index 0000000000000..a3ff54fa2cae4 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/CMakeLists.txt @@ -0,0 +1,22 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(remote_cpurad) + +# target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../common) + +zephyr_library_include_directories( + src + ../common +) + +# Define which device this is +zephyr_compile_definitions(ZBUS_DEVICE_B=1) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/subsys/zbus/ipc_forwarder/remote/Kconfig b/samples/subsys/zbus/ipc_forwarder/remote/Kconfig new file mode 100644 index 0000000000000..dd4eea898970d --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/Kconfig @@ -0,0 +1,7 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +source "Kconfig.zephyr" diff --git a/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf5340dk_nrf5340_cpunet.overlay b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf5340dk_nrf5340_cpunet.overlay new file mode 100644 index 0000000000000..d968d4110ade6 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf5340dk_nrf5340_cpunet.overlay @@ -0,0 +1,5 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuflpr.overlay b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuflpr.overlay new file mode 100644 index 0000000000000..1074bf5167e9e --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuflpr.overlay @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +&cpuapp_bellboard { + status = "okay"; +}; + +ipc0: &cpuapp_cpuflpr_ipc { + status = "okay"; +}; + +&cpuflpr_vevif { + status = "okay"; +}; + +/ { + chosen { + /delete-property/ zephyr,bt-hci; + }; +}; diff --git a/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuflpr_xip.overlay b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuflpr_xip.overlay new file mode 100644 index 0000000000000..1074bf5167e9e --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuflpr_xip.overlay @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +&cpuapp_bellboard { + status = "okay"; +}; + +ipc0: &cpuapp_cpuflpr_ipc { + status = "okay"; +}; + +&cpuflpr_vevif { + status = "okay"; +}; + +/ { + chosen { + /delete-property/ zephyr,bt-hci; + }; +}; diff --git a/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuppr.overlay b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuppr.overlay new file mode 100644 index 0000000000000..9baa52998b2ea --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuppr.overlay @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +&cpuapp_bellboard { + status = "okay"; +}; + +ipc0: &cpuapp_cpuppr_ipc { + status = "okay"; +}; + +&cpuppr_vevif { + status = "okay"; +}; + +/ { + chosen { + /delete-property/ zephyr,bt-hci; + }; +}; diff --git a/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuppr_xip.overlay b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuppr_xip.overlay new file mode 100644 index 0000000000000..9baa52998b2ea --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuppr_xip.overlay @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +&cpuapp_bellboard { + status = "okay"; +}; + +ipc0: &cpuapp_cpuppr_ipc { + status = "okay"; +}; + +&cpuppr_vevif { + status = "okay"; +}; + +/ { + chosen { + /delete-property/ zephyr,bt-hci; + }; +}; diff --git a/samples/subsys/zbus/ipc_forwarder/remote/prj.conf b/samples/subsys/zbus/ipc_forwarder/remote/prj.conf new file mode 100644 index 0000000000000..b8b1ad837d7d9 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/prj.conf @@ -0,0 +1,32 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_LOG=y +# CONFIG_LOG_MODE_MINIMAL=y +CONFIG_LOG_DBG_COLOR_BLUE=y + +# Boot without banners +# Makes logging output cleaner when using stm logging +CONFIG_BOOT_BANNER=y + +CONFIG_MBOX=y +CONFIG_IPC_SERVICE=y +CONFIG_HEAP_MEM_POOL_SIZE=4096 + +CONFIG_TEST_RANDOM_GENERATOR=y + +CONFIG_CRC=y + +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y + +CONFIG_ZBUS_MULTIDOMAIN=y +CONFIG_ZBUS_MULTIDOMAIN_IPC=y +CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL_INF=y + +# Increase the size of the receive buffer to accommodate larger messages +CONFIG_PBUF_RX_READ_BUF_SIZE=384 diff --git a/samples/subsys/zbus/ipc_forwarder/remote/src/main.c b/samples/subsys/zbus/ipc_forwarder/remote/src/main.c new file mode 100644 index 0000000000000..d2885c41d1ea1 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/remote/src/main.c @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#include "common.h" + +LOG_MODULE_REGISTER(main, LOG_LEVEL_DBG); + +/* NOTE: Shared channels request_channel and response_channel are defined in common.h */ + +/* Set up proxy agent for ipc0 and add response channel to be forwarded */ +#define IPC0_NODE DT_NODELABEL(ipc0) +ZBUS_PROXY_AGENT_DEFINE(ipc_proxy, ZBUS_MULTIDOMAIN_TYPE_IPC, IPC0_NODE); +ZBUS_PROXY_ADD_CHANNEL(ipc_proxy, response_channel); + +int get_random_value(int min, int max) +{ + return min + (sys_rand32_get() % (max - min + 1)); +} + +/* Listen for requests on the request channel, and respond on the response channel */ +static void ipc_forwarder_listener_cb(const struct zbus_channel *chan) +{ + int ret; + const struct request_data *data = zbus_chan_const_msg(chan); + struct response_data response = { + .response_id = data->request_id, + .value = get_random_value(data->min_value, data->max_value)}; + + LOG_INF("Received message on channel %s", chan->name); + LOG_INF("Request ID: %d, Min: %d, Max: %d", data->request_id, data->min_value, + data->max_value); + + LOG_INF("Sending response: ID=%d, Value=%d", response.response_id, response.value); + + ret = zbus_chan_pub(&response_channel, &response, K_MSEC(100)); + if (ret < 0) { + LOG_ERR("Failed to publish response on channel %s: %d", response_channel.name, ret); + } else { + LOG_INF("Response published on channel %s", response_channel.name); + } +} + +ZBUS_LISTENER_DEFINE(ipc_forwarder_listener, ipc_forwarder_listener_cb); +ZBUS_CHAN_ADD_OBS(request_channel, ipc_forwarder_listener, 0); + +bool print_channel_info(const struct zbus_channel *chan) +{ + if (!chan) { + LOG_ERR("Channel is NULL"); + return false; + } + LOG_INF("Channel %s is a %s channel", chan->name, + ZBUS_CHANNEL_IS_SHADOW(chan) ? "shadow" : "master"); + return true; +} + +int main(void) +{ + LOG_INF("ZBUS Multidomain IPC Forwarder Sample Application"); + zbus_iterate_over_channels(print_channel_info); + + return 0; +} diff --git a/samples/subsys/zbus/ipc_forwarder/sample.yaml b/samples/subsys/zbus/ipc_forwarder/sample.yaml new file mode 100644 index 0000000000000..bf9a22ebd2e5f --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/sample.yaml @@ -0,0 +1,123 @@ +sample: + name: IPC Forwarder + description: Sample application demonstrating Zbus IPC forwarder functionality. +tests: + sample.zbus.ipc_forwarder.nrf54h20_cpuapp_cpurad: + sysbuild: true + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + extra_args: + - SNIPPET=nordic-log-stm + - SB_CONFIG_REMOTE_BOARD_NRF54H20_CPURAD=y + timeout: 30 + harness: console + harness_config: + type: multi_line + ordered: false + regex: + - ".* app/main: Channel request_channel is a master channel" + - ".* app/main: Channel response_channel is a shadow channel" + - ".* rad/main: Channel request_channel is a shadow channel" + - ".* rad/main: Channel response_channel is a master channel" + - ".* app/main: Published on channel request_channel. Request ID=1, Min=-1, Max=1" + - ".* rad/main: Received message on channel request_channel" + - ".* rad/main: Request ID: 1, Min: -1, Max: 1" + - ".* rad/main: Sending response: ID=1, Value=.*" + - ".* rad/main: Response published on channel response_channel" + - ".* app/main: Received message on channel response_channel" + - ".* app/main: Response ID: 1, Value: .*" + + sample.zbus.ipc_forwarder.nrf54h20_cpuapp_cpuppr: + sysbuild: true + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + extra_args: + - SNIPPET=nordic-log-stm + - SB_CONFIG_REMOTE_BOARD_NRF54H20_CPUPPR=y + timeout: 30 + harness: console + harness_config: + type: multi_line + ordered: false + regex: + - ".* app/main: Channel request_channel is a master channel" + - ".* app/main: Channel response_channel is a shadow channel" + - ".* ppr/main: Channel request_channel is a shadow channel" + - ".* ppr/main: Channel response_channel is a master channel" + - ".* app/main: Published on channel request_channel. Request ID=1, Min=-1, Max=1" + - ".* ppr/main: Received message on channel request_channel" + - ".* ppr/main: Request ID: 1, Min: -1, Max: 1" + - ".* ppr/main: Sending response: ID=1, Value=.*" + - ".* ppr/main: Response published on channel response_channel" + - ".* app/main: Received message on channel response_channel" + - ".* app/main: Response ID: 1, Value: .*" + + sample.zbus.ipc_forwarder.nrf54h20_cpuapp_cpuppr_xip: + sysbuild: true + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + extra_args: + - SNIPPET=nordic-log-stm + - SB_CONFIG_REMOTE_BOARD_NRF54H20_CPUPPR_XIP=y + timeout: 30 + harness: console + harness_config: + type: multi_line + ordered: false + regex: + - ".* app/main: Channel request_channel is a master channel" + - ".* app/main: Channel response_channel is a shadow channel" + - ".* ppr/main: Channel request_channel is a shadow channel" + - ".* ppr/main: Channel response_channel is a master channel" + - ".* app/main: Published on channel request_channel. Request ID=1, Min=-1, Max=1" + - ".* ppr/main: Received message on channel request_channel" + - ".* ppr/main: Request ID: 1, Min: -1, Max: 1" + - ".* ppr/main: Sending response: ID=1, Value=.*" + - ".* ppr/main: Response published on channel response_channel" + - ".* app/main: Received message on channel response_channel" + - ".* app/main: Response ID: 1, Value: .*" + + sample.zbus.ipc_forwarder.nrf54h20_cpuapp_cpuflpr_xip: + sysbuild: true + platform_allow: + - nrf54h20dk/nrf54h20/cpuapp + extra_args: + - SNIPPET=nordic-log-stm + - SB_CONFIG_REMOTE_BOARD_NRF54H20_CPUFLPR_XIP=y + timeout: 30 + harness: console + harness_config: + type: multi_line + ordered: false + regex: + - ".* app/main: Channel request_channel is a master channel" + - ".* app/main: Channel response_channel is a shadow channel" + - ".* flpr/main: Channel request_channel is a shadow channel" + - ".* flpr/main: Channel response_channel is a master channel" + - ".* app/main: Published on channel request_channel. Request ID=1, Min=-1, Max=1" + - ".* flpr/main: Received message on channel request_channel" + - ".* flpr/main: Request ID: 1, Min: -1, Max: 1" + - ".* flpr/main: Sending response: ID=1, Value=.*" + - ".* flpr/main: Response published on channel response_channel" + - ".* app/main: Received message on channel response_channel" + - ".* app/main: Response ID: 1, Value: .*" + + sample.zbus.ipc_forwarder.nrf5340_cpuapp_cpunet: + sysbuild: true + platform_allow: + - nrf5340dk/nrf5340/cpuapp + - nrf5340bsim/nrf5340/cpuapp + integration_platforms: + - nrf5340bsim/nrf5340/cpuapp + tags: ipc + timeout: 30 + harness: console + harness_config: + type: multi_line + ordered: false + regex: + - ".* main: Channel request_channel is a master channel" + - ".* main: Channel response_channel is a shadow channel" + - ".* main: Published on channel request_channel. Request ID=1, Min=-1, Max=1" + - ".* main: Received message on channel response_channel" + - ".* main: Response ID: 1, Value: .*" diff --git a/samples/subsys/zbus/ipc_forwarder/src/main.c b/samples/subsys/zbus/ipc_forwarder/src/main.c new file mode 100644 index 0000000000000..446537bf9fc87 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/src/main.c @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include "common.h" + +LOG_MODULE_REGISTER(main, LOG_LEVEL_DBG); + +/* NOTE: Shared channels request_channel and response_channel are defined in common.h */ + +/* Set up proxy agent for ipc0 and add response channel to be forwarded */ +#define IPC0_NODE DT_NODELABEL(ipc0) +ZBUS_PROXY_AGENT_DEFINE(ipc_proxy, ZBUS_MULTIDOMAIN_TYPE_IPC, IPC0_NODE); +ZBUS_PROXY_ADD_CHANNEL(ipc_proxy, request_channel); + +/* Log response data coming from the response channel */ +void ipc_forwarder_listener_cb(const struct zbus_channel *chan) +{ + const struct response_data *data = zbus_chan_const_msg(chan); + + LOG_INF("Received message on channel %s", chan->name); + LOG_INF("Response ID: %d, Value: %d", data->response_id, data->value); +} + +ZBUS_LISTENER_DEFINE(ipc_forwarder_listener, ipc_forwarder_listener_cb); +ZBUS_CHAN_ADD_OBS(response_channel, ipc_forwarder_listener, 0); + +bool print_channel_info(const struct zbus_channel *chan) +{ + if (!chan) { + LOG_ERR("Channel is NULL"); + return false; + } + LOG_INF("Channel %s is a %s channel", chan->name, + ZBUS_CHANNEL_IS_SHADOW(chan) ? "shadow" : "master"); + return true; +} + +int main(void) +{ + LOG_INF("ZBUS Multidomain IPC Forwarder Sample Application"); + zbus_iterate_over_channels(print_channel_info); + + struct request_data data = {.request_id = 1, .min_value = -1, .max_value = 1}; + + while (1) { + int ret; + + ret = zbus_chan_pub(&request_channel, &data, K_MSEC(100)); + if (ret < 0) { + LOG_ERR("Failed to publish on channel %s: %d", request_channel.name, ret); + } else { + LOG_INF("Published on channel %s. Request ID=%d, Min=%d, Max=%d", + request_channel.name, data.request_id, data.min_value, + data.max_value); + } + + data.request_id++; + data.min_value -= 1; + data.max_value += 1; + k_sleep(K_SECONDS(5)); + } + return 0; +} diff --git a/samples/subsys/zbus/ipc_forwarder/sysbuild.cmake b/samples/subsys/zbus/ipc_forwarder/sysbuild.cmake new file mode 100644 index 0000000000000..d9c54139175d2 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/sysbuild.cmake @@ -0,0 +1,52 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +if(CONFIG_BOARD_NRF54H20DK_NRF54H20_CPUAPP) +endif() + +if("${SB_CONFIG_REMOTE_BOARD}" STREQUAL "") + message(FATAL_ERROR + "Target ${BOARD} not supported for this sample. " + "There is no remote board selected in Kconfig.sysbuild") +endif() + + +if("${SB_CONFIG_REMOTE_BOARD}" STREQUAL "nrf54h20dk/nrf54h20/cpuppr") + set(DTC_OVERLAY_FILE "${APP_DIR}/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr.overlay" CACHE STRING "" FORCE) + +elseif("${SB_CONFIG_REMOTE_BOARD}" STREQUAL "nrf54h20dk/nrf54h20/cpuppr/xip") + set(DTC_OVERLAY_FILE "${APP_DIR}/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr_xip.overlay" CACHE STRING "" FORCE) + +elseif("${SB_CONFIG_REMOTE_BOARD}" STREQUAL "nrf54h20dk/nrf54h20/cpuflpr") + set(DTC_OVERLAY_FILE "${APP_DIR}/boards/nrf54h20dk_nrf54h20_cpuapp_cpuflpr.overlay" CACHE STRING "" FORCE) + +elseif("${SB_CONFIG_REMOTE_BOARD}" STREQUAL "nrf54h20dk/nrf54h20/cpuflpr/xip") + set(DTC_OVERLAY_FILE "${APP_DIR}/boards/nrf54h20dk_nrf54h20_cpuapp_cpuflpr_xip.overlay" CACHE STRING "" FORCE) + +endif() + +# Add remote project +ExternalZephyrProject_Add( + APPLICATION remote_app + SOURCE_DIR ${APP_DIR}/remote + BOARD ${SB_CONFIG_REMOTE_BOARD} + BOARD_REVISION ${BOARD_REVISION} +) + +# Configure BabbleSim multi-core setup for nrf5340bsim targets +if("${SB_CONFIG_REMOTE_BOARD}" MATCHES "nrf5340bsim") + native_simulator_set_child_images(${DEFAULT_IMAGE} remote_app) + native_simulator_set_final_executable(${DEFAULT_IMAGE}) +endif() + +# Setup PM partitioning for remote +set_property(GLOBAL APPEND PROPERTY PM_DOMAINS REMOTE) +set_property(GLOBAL APPEND PROPERTY PM_REMOTE_IMAGES remote_app) +set_property(GLOBAL PROPERTY DOMAIN_APP_REMOTE remote_app) +set(REMOTE_PM_DOMAIN_DYNAMIC_PARTITION remote_app CACHE INTERNAL "") + +sysbuild_add_dependencies(CONFIGURE ${DEFAULT_IMAGE} remote_app) +sysbuild_add_dependencies(FLASH ${DEFAULT_IMAGE} remote_app) diff --git a/samples/subsys/zbus/uart_forwarder/README.rst b/samples/subsys/zbus/uart_forwarder/README.rst new file mode 100644 index 0000000000000..c7152c71031fe --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/README.rst @@ -0,0 +1,85 @@ +.. zephyr:code-sample:: zbus-uart-forwarder + :name: zbus Proxy agent - UART forwarder + :relevant-api: zbus_apis + + Forward zbus messages between different devices using UART. + +Overview +******** +This sample demonstrates zbus inter-device communication using UART forwarders between separate devices. +The sample implements a request-response pattern where: + +- **Device A** acts as a requester, publishing requests on a master channel and receiving responses on a shadow channel +- **Device B** acts as a responder, receiving requests on a shadow channel and publishing responses on a master channel +- **UART forwarders** automatically synchronize channel data between devices using the zbus proxy functionality over UART + +The ``common/common.h`` file defines shared channels using :c:macro:`ZBUS_MULTIDOMAIN_CHAN_DEFINE` with conditional +compilation to create master channels on one device and shadow channels on the other. + +This architecture enables message passing between different devices while maintaining zbus's +publish-subscribe structure across device boundaries. + +Building and Running +******************** + +This sample requires two separate devices connected via UART. Each device runs a different application: + +For Device A (Requester): + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/zbus/uart_forwarder/dev_a + :board: nrf5340dk/nrf5340/cpuapp + :goals: build flash + +For Device B (Responder): + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/zbus/uart_forwarder/dev_b + :board: nrf5340dk/nrf5340/cpuapp + :goals: build flash + +Hardware Setup +============== + +Connect the two devices via UART: + +- Device A TX → Device B RX +- Device A RX → Device B TX +- Connect GND between devices + +Sample Output +============= + +Device A Output (Requester) +**************************** + +.. code-block:: console + + *** Booting Zephyr OS build v4.2.0-1157-g7bf51d719a31 *** + main: ZBUS Multidomain UART Forwarder Sample Application + main: Channel request_channel is a master channel + main: Channel response_channel is a shadow channel + main: Published on channel request_channel. Request ID=1, Min=-1, Max=1 + main: Received message on channel response_channel + main: Response ID: 1, Value: 1 + main: Published on channel request_channel. Request ID=2, Min=-2, Max=2 + main: Received message on channel response_channel + main: Response ID: 2, Value: 0 + +Device B Output (Responder) +**************************** + +.. code-block:: console + + *** Booting Zephyr OS build v4.2.0-1157-g7bf51d719a31 *** + main: ZBUS Multidomain UART Forwarder Sample Application + main: Channel request_channel is a shadow channel + main: Channel response_channel is a master channel + main: Received message on channel request_channel + main: Request ID: 1, Min: -1, Max: 1 + main: Sending response: ID=1, Value=1 + main: Response published on channel response_channel + main: Received message on channel request_channel + main: Request ID: 2, Min: -2, Max: 2 + main: Sending response: ID=2, Value=-1 + main: Response published on channel response_channel diff --git a/samples/subsys/zbus/uart_forwarder/common/common.h b/samples/subsys/zbus/uart_forwarder/common/common.h new file mode 100644 index 0000000000000..d757114322ad4 --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/common/common.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef COMMON_H +#define COMMON_H + +#include + +/* Sample data structures for request and response */ +struct request_data { + int request_id; + int min_value; + int max_value; +}; + +struct response_data { + int response_id; + int value; +}; + +/* Conditional compilation for device-specific code, needed if channels should be included on a + * subset of applications devices + */ +#if defined(ZBUS_DEVICE_A) || defined(ZBUS_DEVICE_B) +#define include_on_device_a_b 1 +#else +#define include_on_device_a_b 0 +#endif + +/* Define shared channels for request and response + * request_channel is master on device A and shadow on device B + * response_channel is shadow on device A and master on device B + */ +ZBUS_MULTIDOMAIN_CHAN_DEFINE(request_channel, struct request_data, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0), IS_ENABLED(ZBUS_DEVICE_A), include_on_device_a_b); + +ZBUS_MULTIDOMAIN_CHAN_DEFINE(response_channel, struct response_data, NULL, NULL, + ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0), IS_ENABLED(ZBUS_DEVICE_B), + include_on_device_a_b); + +#endif /* COMMON_H */ diff --git a/samples/subsys/zbus/uart_forwarder/dev_a/CMakeLists.txt b/samples/subsys/zbus/uart_forwarder/dev_a/CMakeLists.txt new file mode 100644 index 0000000000000..d0303a9c261cf --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_a/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(zbus_uart_forwarder_dev_a) + + +zephyr_library_include_directories( + src + ../common +) + +# Define which device this is +zephyr_compile_definitions(ZBUS_DEVICE_A=1) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/subsys/zbus/uart_forwarder/dev_a/Kconfig b/samples/subsys/zbus/uart_forwarder/dev_a/Kconfig new file mode 100644 index 0000000000000..dd4eea898970d --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_a/Kconfig @@ -0,0 +1,7 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +source "Kconfig.zephyr" diff --git a/samples/subsys/zbus/uart_forwarder/dev_a/boards/nrf5340dk_nrf5340_cpuapp.overlay b/samples/subsys/zbus/uart_forwarder/dev_a/boards/nrf5340dk_nrf5340_cpuapp.overlay new file mode 100644 index 0000000000000..a7f449186a2ba --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_a/boards/nrf5340dk_nrf5340_cpuapp.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + aliases { + zbus-uart = &uart1; + }; +}; + +&uart1 { + status = "okay"; + current-speed = <1000000>; + /delete-property/ hw-flow-control; +}; diff --git a/samples/subsys/zbus/uart_forwarder/dev_a/boards/nrf54l15dk_nrf54l15_cpuapp.overlay b/samples/subsys/zbus/uart_forwarder/dev_a/boards/nrf54l15dk_nrf54l15_cpuapp.overlay new file mode 100644 index 0000000000000..ed84002e7ab38 --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_a/boards/nrf54l15dk_nrf54l15_cpuapp.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/{ + aliases { + zbus-uart = &uart30; + }; +}; + +&uart30 { + status = "okay"; + current-speed = <1000000>; + /delete-property/ hw-flow-control; +}; diff --git a/samples/subsys/zbus/uart_forwarder/dev_a/prj.conf b/samples/subsys/zbus/uart_forwarder/dev_a/prj.conf new file mode 100644 index 0000000000000..5ec712b7f3545 --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_a/prj.conf @@ -0,0 +1,25 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_LOG=y +# CONFIG_LOG_MODE_MINIMAL=y +CONFIG_LOG_DBG_COLOR_BLUE=y + +CONFIG_SERIAL=y +CONFIG_UART_ASYNC_API=y + +CONFIG_UART_1_NRF_ASYNC_LOW_POWER=y +CONFIG_UART_2_NRF_ASYNC_LOW_POWER=y + +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y + +CONFIG_CRC=y + +CONFIG_ZBUS_MULTIDOMAIN=y +CONFIG_ZBUS_MULTIDOMAIN_UART=y +CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL_INF=y diff --git a/samples/subsys/zbus/uart_forwarder/dev_a/src/main.c b/samples/subsys/zbus/uart_forwarder/dev_a/src/main.c new file mode 100644 index 0000000000000..7b3bca8b32fad --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_a/src/main.c @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include "common.h" + +LOG_MODULE_REGISTER(main, LOG_LEVEL_DBG); + +/* NOTE: Shared channels request_channel and response_channel are defined in common.h */ + +/* Set up proxy agent for UART1 and add response channel to be forwarded */ +#define UART_NODE DT_ALIAS(zbus_uart) +ZBUS_PROXY_AGENT_DEFINE(uart1_proxy, ZBUS_MULTIDOMAIN_TYPE_UART, UART_NODE); +ZBUS_PROXY_ADD_CHANNEL(uart1_proxy, request_channel); + +/* Log response data coming from the response channel */ +void uart_forwarder_listener_cb(const struct zbus_channel *chan) +{ + const struct response_data *data = zbus_chan_const_msg(chan); + + LOG_INF("Received message on channel %s", chan->name); + LOG_INF("Response ID: %d, Value: %d", data->response_id, data->value); +} + +ZBUS_LISTENER_DEFINE(uart_forwarder_listener, uart_forwarder_listener_cb); +ZBUS_CHAN_ADD_OBS(response_channel, uart_forwarder_listener, 0); + +bool print_channel_info(const struct zbus_channel *chan) +{ + if (!chan) { + LOG_ERR("Channel is NULL"); + return false; + } + LOG_INF("Channel %s is a %s channel", chan->name, + ZBUS_CHANNEL_IS_SHADOW(chan) ? "shadow" : "master"); + + return true; +} + +int main(void) +{ + LOG_INF("ZBUS Multidomain UART Forwarder Sample Application"); + zbus_iterate_over_channels(print_channel_info); + + struct request_data data = {.request_id = 1, .min_value = -1, .max_value = 1}; + + while (1) { + int ret = zbus_chan_pub(&request_channel, &data, K_MSEC(100)); + + if (ret < 0) { + LOG_ERR("Failed to publish on channel %s: %d", request_channel.name, ret); + } else { + LOG_INF("Published on channel %s. Request ID=%d, Min=%d, Max=%d", + request_channel.name, data.request_id, data.min_value, + data.max_value); + } + + data.request_id++; + data.min_value -= 1; + data.max_value += 1; + k_sleep(K_SECONDS(5)); + } + return 0; +} diff --git a/samples/subsys/zbus/uart_forwarder/dev_b/CMakeLists.txt b/samples/subsys/zbus/uart_forwarder/dev_b/CMakeLists.txt new file mode 100644 index 0000000000000..8ee9584768e23 --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_b/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(zbus_uart_forwarder_dev_b) + + +zephyr_library_include_directories( + src + ../common +) + +# Define which device this is +zephyr_compile_definitions(ZBUS_DEVICE_B=1) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/subsys/zbus/uart_forwarder/dev_b/Kconfig b/samples/subsys/zbus/uart_forwarder/dev_b/Kconfig new file mode 100644 index 0000000000000..dd4eea898970d --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_b/Kconfig @@ -0,0 +1,7 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +source "Kconfig.zephyr" diff --git a/samples/subsys/zbus/uart_forwarder/dev_b/boards/nrf5340dk_nrf5340_cpuapp.overlay b/samples/subsys/zbus/uart_forwarder/dev_b/boards/nrf5340dk_nrf5340_cpuapp.overlay new file mode 100644 index 0000000000000..a7f449186a2ba --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_b/boards/nrf5340dk_nrf5340_cpuapp.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + aliases { + zbus-uart = &uart1; + }; +}; + +&uart1 { + status = "okay"; + current-speed = <1000000>; + /delete-property/ hw-flow-control; +}; diff --git a/samples/subsys/zbus/uart_forwarder/dev_b/boards/nrf54l15dk_nrf54l15_cpuapp.overlay b/samples/subsys/zbus/uart_forwarder/dev_b/boards/nrf54l15dk_nrf54l15_cpuapp.overlay new file mode 100644 index 0000000000000..ed84002e7ab38 --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_b/boards/nrf54l15dk_nrf54l15_cpuapp.overlay @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/{ + aliases { + zbus-uart = &uart30; + }; +}; + +&uart30 { + status = "okay"; + current-speed = <1000000>; + /delete-property/ hw-flow-control; +}; diff --git a/samples/subsys/zbus/uart_forwarder/dev_b/prj.conf b/samples/subsys/zbus/uart_forwarder/dev_b/prj.conf new file mode 100644 index 0000000000000..0c47d82c626ca --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_b/prj.conf @@ -0,0 +1,27 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_LOG=y +# CONFIG_LOG_MODE_MINIMAL=y +CONFIG_LOG_DBG_COLOR_BLUE=y + +CONFIG_SERIAL=y +CONFIG_UART_ASYNC_API=y + +CONFIG_TEST_RANDOM_GENERATOR=y + +CONFIG_UART_1_NRF_ASYNC_LOW_POWER=y +CONFIG_UART_2_NRF_ASYNC_LOW_POWER=y + +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y + +CONFIG_CRC=y + +CONFIG_ZBUS_MULTIDOMAIN=y +CONFIG_ZBUS_MULTIDOMAIN_UART=y +CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL_INF=y diff --git a/samples/subsys/zbus/uart_forwarder/dev_b/src/main.c b/samples/subsys/zbus/uart_forwarder/dev_b/src/main.c new file mode 100644 index 0000000000000..01ebd3bc0e0d5 --- /dev/null +++ b/samples/subsys/zbus/uart_forwarder/dev_b/src/main.c @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#include "common.h" + +LOG_MODULE_REGISTER(main, LOG_LEVEL_DBG); + +/* NOTE: Shared channels request_channel and response_channel are defined in common.h */ + +/* Set up proxy agent for UART1 and add response channel to be forwarded */ +#define UART_NODE DT_ALIAS(zbus_uart) +ZBUS_PROXY_AGENT_DEFINE(uart1_proxy, ZBUS_MULTIDOMAIN_TYPE_UART, UART_NODE); +ZBUS_PROXY_ADD_CHANNEL(uart1_proxy, response_channel); + +int get_random_value(int min, int max) +{ + return min + (sys_rand32_get() % (max - min + 1)); +} + +/* Listen for requests on the request channel, and respond on the response channel */ +static void uart_forwarder_listener_cb(const struct zbus_channel *chan) +{ + int ret; + const struct request_data *data = zbus_chan_const_msg(chan); + struct response_data response = { + .response_id = data->request_id, + .value = get_random_value(data->min_value, data->max_value)}; + + LOG_INF("Received message on channel %s", chan->name); + LOG_INF("Request ID: %d, Min: %d, Max: %d", data->request_id, data->min_value, + data->max_value); + + LOG_INF("Sending response: ID=%d, Value=%d", response.response_id, response.value); + + ret = zbus_chan_pub(&response_channel, &response, K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to publish response on channel %s: %d", response_channel.name, ret); + } else { + LOG_INF("Response published on channel %s", response_channel.name); + } +} + +ZBUS_LISTENER_DEFINE(uart_forwarder_listener, uart_forwarder_listener_cb); +ZBUS_CHAN_ADD_OBS(request_channel, uart_forwarder_listener, 0); + +bool print_channel_info(const struct zbus_channel *chan) +{ + if (!chan) { + LOG_ERR("Channel is NULL"); + return false; + } + LOG_INF("Channel %s is a %s channel", chan->name, + ZBUS_CHANNEL_IS_SHADOW(chan) ? "shadow" : "master"); + return true; +} + +int main(void) +{ + LOG_INF("ZBUS Multidomain UART Forwarder Sample Application"); + zbus_iterate_over_channels(print_channel_info); + + return 0; +} diff --git a/subsys/zbus/CMakeLists.txt b/subsys/zbus/CMakeLists.txt index 6284e8a37fa43..4b8242410d0c5 100644 --- a/subsys/zbus/CMakeLists.txt +++ b/subsys/zbus/CMakeLists.txt @@ -5,7 +5,11 @@ zephyr_library() zephyr_library_sources(zbus.c) if(CONFIG_ZBUS_RUNTIME_OBSERVERS) - zephyr_library_sources(zbus_runtime_observers.c) + zephyr_library_sources(zbus_runtime_observers.c) +endif() + +if(CONFIG_ZBUS_MULTIDOMAIN) + add_subdirectory(multidomain) endif() zephyr_library_sources(zbus_iterable_sections.c) diff --git a/subsys/zbus/Kconfig b/subsys/zbus/Kconfig index 2e418f99ff284..08d2186851c25 100644 --- a/subsys/zbus/Kconfig +++ b/subsys/zbus/Kconfig @@ -107,6 +107,20 @@ config ZBUS_ASSERT_MOCK enabled, _ZBUS_ASSERT returns -EFAULT instead of assert. It makes it more straightforward to test invalid parameters. +config ZBUS_MULTIDOMAIN + bool "ZBus multidomain support" + depends on ZBUS_MSG_SUBSCRIBER + depends on ZBUS_CHANNEL_NAME + depends on CRC + help + Enables support for ZBus multidomain. This feature allows the ZBus to work with multiple domains, + enabling communication between them. + +if ZBUS_MULTIDOMAIN + +rsource "multidomain/Kconfig" + +endif # ZBUS_MULTIDOMAIN config HEAP_MEM_POOL_ADD_SIZE_ZBUS int "ZBus requested heap pool size." diff --git a/subsys/zbus/multidomain/CMakeLists.txt b/subsys/zbus/multidomain/CMakeLists.txt new file mode 100644 index 0000000000000..76c0603842c69 --- /dev/null +++ b/subsys/zbus/multidomain/CMakeLists.txt @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library_sources(zbus_multidomain.c) + +if(CONFIG_ZBUS_MULTIDOMAIN_UART) + zephyr_library_sources(zbus_multidomain_uart.c) +endif() + +if(CONFIG_ZBUS_MULTIDOMAIN_IPC) + zephyr_library_sources(zbus_multidomain_ipc.c) +endif() diff --git a/subsys/zbus/multidomain/Kconfig b/subsys/zbus/multidomain/Kconfig new file mode 100644 index 0000000000000..7cb4f604ee769 --- /dev/null +++ b/subsys/zbus/multidomain/Kconfig @@ -0,0 +1,99 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +menu "ZBus multidomain support" + +config ZBUS_MULTIDOMAIN_MESSAGE_SIZE + int "ZBus multidomain message size" + default 256 + help + Configures the size of messages used in ZBus multidomain communication. + This setting affects the maximum size of messages that can be sent between domains. + +config ZBUS_MULTIDOMAIN_CHANNEL_NAME_SIZE + int "ZBus multidomain channel name size" + default 32 + help + Configures the maximum size of channel names used in ZBus multidomain communication. + This setting affects how long channel names can be when communicating between domains. + +config ZBUS_MULTIDOMAIN_PROXY_STACK_SIZE + int "ZBus multidomain proxy stack size" + default 1024 + help + Configures the stack size (in bytes) for the ZBus multidomain proxy agent thread. + This setting affects the memory allocation for the proxy agent's operations. + +config ZBUS_MULTIDOMAIN_PROXY_PRIORITY + int "ZBus multidomain proxy thread priority" + default 7 + help + Configures the priority of the ZBus multidomain proxy agent thread. + This setting affects the scheduling of the proxy agent in relation to other threads. + +config ZBUS_MULTIDOMAIN_SENT_MSG_POOL_SIZE + int "ZBus multidomain sent message history size" + default 4 + help + Configures the number of sent messages to keep simultaneously in the history pool for + tracking acknowledgments and retransmissions in the ZBus multidomain. If more messages + than this number are sent without acknowledgment, the new messages will be dropped. + +config ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT + int "ZBus multidomain sent message initial acknowledgment timeout (ms)" + default 10 + help + Configures the initial timeout (in milliseconds) to wait for an acknowledgment + of a sent message in the ZBus multidomain proxy agent. If an acknowledgment is + not received within this time, the message may be retransmitted with a exponential backoff + +config ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX + int "ZBus multidomain sent message acknowledgment timeout maximum (ms)" + default 1000 + help + Configures the maximum timeout (in milliseconds) for waiting for an acknowledgment + of a sent message in the ZBus multidomain proxy agent. This setting limits the exponential + backoff for retransmissions to prevent excessively long wait times. + +config ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS + int "ZBus multidomain maximum transmit attempts" + default 10 + help + Configures the maximum number of attempts to transmit a message in the ZBus multidomain + proxy agent. If an acknowledgment is not received after this many attempts, the message + will be dropped. Setting this to 1 means no retransmissions will be attempted. + +config ZBUS_MULTIDOMAIN_UART + bool "ZBus multidomain UART backend" + depends on UART_ASYNC_API + help + Enables the ZBus multidomain UART backend, allowing for ZBus between domains over UART. + This feature is useful for systems that require inter-domain communication via UART. + +if ZBUS_MULTIDOMAIN_UART + +config ZBUS_MULTIDOMAIN_UART_BUF_COUNT + int "ZBus multidomain UART buffer count" + default 2 + range 2 128 + help + Configures the number of buffers used for the ZBus multidomain UART backend. + A minimum of 2 buffers is required for proper operation. + +endif # ZBUS_MULTIDOMAIN_UART + +config ZBUS_MULTIDOMAIN_IPC + bool "ZBus multidomain IPC backend" + depends on IPC_SERVICE + help + Enables the ZBus multidomain IPC backend, allowing communication between domains over IPC. + This feature is useful for systems that require inter-domain communication via IPC. + +module = ZBUS_MULTIDOMAIN +module-str = zbus_multidomain +source "subsys/logging/Kconfig.template.log_config" + +endmenu # ZBus multidomain support diff --git a/subsys/zbus/multidomain/zbus_multidomain.c b/subsys/zbus/multidomain/zbus_multidomain.c new file mode 100644 index 0000000000000..e9c8567ca3bf5 --- /dev/null +++ b/subsys/zbus/multidomain/zbus_multidomain.c @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +LOG_MODULE_REGISTER(zbus_multidomain, CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL); + +/* Forward declaration of static functions */ +static int zbus_proxy_agent_send(struct zbus_proxy_agent_config *config, + struct zbus_proxy_agent_msg *msg, uint8_t transmit_attempts); + +static void zbus_proxy_agent_sent_msg_pool_init(struct zbus_proxy_agent_config *config) +{ + if (!config) { + LOG_ERR("Invalid proxy agent configuration for message pool init"); + return; + } + + if (!config->sent_msg_pool) { + LOG_ERR("No send message pool defined for proxy agent %s", config->name); + return; + } + + sys_slist_init(&config->sent_msg_list); +} + +static struct zbus_proxy_agent_tracked_msg * +zbus_proxy_agent_find_sent_msg_data(struct zbus_proxy_agent_config *config, uint32_t msg_id) +{ + struct net_buf *buf; + + SYS_SLIST_FOR_EACH_CONTAINER(&config->sent_msg_list, buf, node) { + uint32_t *msg_id_ptr = net_buf_user_data(buf); + + if (*msg_id_ptr == msg_id) { + return (struct zbus_proxy_agent_tracked_msg *)buf->data; + } + } + return NULL; +} + +static int zbus_proxy_agent_sent_ack_timeout_stop(struct zbus_proxy_agent_config *config, + uint32_t msg_id) +{ + if (!config) { + LOG_ERR("Invalid proxy agent configuration for removing sent message buffer"); + return -EINVAL; + } + + if (!config->sent_msg_pool) { + LOG_ERR("No send message pool defined for proxy agent %s", config->name); + return -ENOSYS; + } + + /* Protect list traversal and modification */ + unsigned int key = irq_lock(); + + struct net_buf *prev_buf = NULL; + struct net_buf *buf; + + SYS_SLIST_FOR_EACH_CONTAINER_SAFE(&config->sent_msg_list, buf, prev_buf, node) { + uint32_t *msg_id_ptr = net_buf_user_data(buf); + + if (*msg_id_ptr == msg_id) { + struct zbus_proxy_agent_tracked_msg *data = + (struct zbus_proxy_agent_tracked_msg *)buf->data; + + /* Check if we're in ISR context - cannot use sync work operations */ + if (k_is_in_isr()) { + /* In ISR context: Mark config as NULL and remove from list + * Cannot cancel work synchronously, but work handler will see NULL + * config + */ + data->config = NULL; + sys_slist_remove(&config->sent_msg_list, + prev_buf ? &prev_buf->node : NULL, &buf->node); + net_buf_unref(buf); + irq_unlock(key); + LOG_DBG("ACK received in ISR context for message ID %d, removed " + "from tracking", + msg_id); + return 0; + } else if (k_current_get() != &k_sys_work_q.thread) { + /* In thread context (not work queue): Safe to cancel work + * synchronously + */ + struct k_work_sync sync; + + k_work_cancel_delayable_sync(&data->work, &sync); + } else { + /* In work queue context: Mark as NULL to prevent retransmission */ + data->config = NULL; + } + + sys_slist_remove(&config->sent_msg_list, prev_buf ? &prev_buf->node : NULL, + &buf->node); + net_buf_unref(buf); + irq_unlock(key); + return 0; + } + } + + irq_unlock(key); + LOG_WRN("Sent message ID %d not found in list of tracked messages", msg_id); + return -ENOENT; +} + +static void zbus_proxy_agent_sent_ack_timeout_handler(struct k_work *work) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct zbus_proxy_agent_tracked_msg *data = + CONTAINER_OF(dwork, struct zbus_proxy_agent_tracked_msg, work); + if (!data) { + LOG_ERR("Invalid sent message data in timeout handler"); + return; + } + uint32_t expected_msg_id = data->msg.id; + + if (!data->config) { + LOG_DBG("Timeout handler called for message ID %d but config is NULL, likely " + "already ACKed", + expected_msg_id); + return; + } + + unsigned int key = irq_lock(); + struct zbus_proxy_agent_tracked_msg *current_data = + zbus_proxy_agent_find_sent_msg_data(data->config, expected_msg_id); + + if (current_data != data) { + irq_unlock(key); + LOG_DBG("Timeout handler called for message ID %d but message no longer in " + "tracking list, likely already ACKed", + expected_msg_id); + return; + } + + if (k_work_delayable_is_pending(&data->work) == false) { + irq_unlock(key); + LOG_DBG("Timeout work for message ID %d was cancelled while waiting for lock", + expected_msg_id); + return; + } + + irq_unlock(key); + + LOG_WRN("Sent message ID %d timed out waiting for acknowledgment", expected_msg_id); + + data->transmit_attempts++; + if (data->transmit_attempts < CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS) { + LOG_WRN("Retrying to send message ID %d (attempt %d)", expected_msg_id, + data->transmit_attempts); + if (data->config) { + int ret = zbus_proxy_agent_send(data->config, &data->msg, + data->transmit_attempts); + if (ret < 0) { + LOG_ERR("Failed to resend message ID %d: %d", expected_msg_id, ret); + } else { + LOG_DBG("Resent message ID %d (attempt %d)", expected_msg_id, + data->transmit_attempts); + } + } + } else { + LOG_ERR("Max transmit attempts (%d) reached for message ID %d, giving up", + CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS, expected_msg_id); + if (data->config) { + int ret = zbus_proxy_agent_sent_ack_timeout_stop(data->config, + expected_msg_id); + if (ret < 0 && ret != -ENOENT) { + /* -ENOENT means ACK already arrived and removed the message, which + * is fine + */ + LOG_ERR("Failed to remove sent message ID %d from tracking pool: " + "%d", + expected_msg_id, ret); + } + } + } +} + +static int zbus_proxy_agent_sent_ack_timeout_start(struct zbus_proxy_agent_config *config, + struct zbus_proxy_agent_msg *msg, + uint8_t transmit_attempts) +{ + if (!config || !msg) { + LOG_ERR("Invalid parameters for adding sent message buffer"); + return -EINVAL; + } + + if (!config->sent_msg_pool) { + LOG_ERR("No send message pool defined for proxy agent %s", config->name); + return -ENOSYS; + } + + unsigned int key = irq_lock(); + + struct zbus_proxy_agent_tracked_msg *data = + zbus_proxy_agent_find_sent_msg_data(config, msg->id); + if (data) { + /* Message is already being tracked, just reschedule the timeout */ + data->transmit_attempts = transmit_attempts; + if (&data->msg != msg) { + memcpy(&data->msg, msg, sizeof(*msg)); + } + uint32_t timeout_ms = CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT + << transmit_attempts; + if (timeout_ms > CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX) { + timeout_ms = CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX; + } + k_work_reschedule(&data->work, K_MSEC(timeout_ms)); + irq_unlock(key); + LOG_DBG("Rescheduled ACK timeout for message ID %d (attempts: %d, timeout: %d ms)", + msg->id, transmit_attempts, timeout_ms); + return 0; + } + + struct net_buf *buf = net_buf_alloc(config->sent_msg_pool, K_NO_WAIT); + + if (!buf) { + irq_unlock(key); + LOG_ERR("Sent message pool full, cannot track message ID %d for proxy agent %s", + msg->id, config->name); + return -ENOMEM; + } + + data = net_buf_add(buf, sizeof(struct zbus_proxy_agent_tracked_msg)); + if (!data) { + net_buf_unref(buf); + irq_unlock(key); + return -ENOMEM; + } + data->config = config; + data->transmit_attempts = transmit_attempts; + if (&data->msg != msg) { + memcpy(&data->msg, msg, sizeof(*msg)); + } + k_work_init_delayable(&data->work, zbus_proxy_agent_sent_ack_timeout_handler); + + uint32_t *msg_id_ptr = net_buf_user_data(buf); + *msg_id_ptr = msg->id; + sys_slist_append(&config->sent_msg_list, &buf->node); + uint32_t timeout_ms = CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT << transmit_attempts; + + LOG_DBG("Scheduling ACK timeout for message ID %d in %d ms (attempts: %d)", msg->id, + timeout_ms, transmit_attempts); + + k_work_schedule_for_queue(&k_sys_work_q, &data->work, K_MSEC(timeout_ms)); + + irq_unlock(key); + return 0; +} + +static int zbus_proxy_agent_set_recv_cb(struct zbus_proxy_agent_config *config, + int (*recv_cb)(const struct zbus_proxy_agent_msg *msg)) +{ + int ret; + + if (!config || !config->api || !config->backend_config) { + LOG_ERR("Invalid proxy agent configuration"); + return -EINVAL; + } + + if (!config->api->backend_set_recv_cb) { + LOG_ERR("Backend set receive callback function is not defined"); + return -ENOSYS; + } + + ret = config->api->backend_set_recv_cb(config->backend_config, recv_cb); + if (ret < 0) { + LOG_ERR("Failed to set receive callback for proxy agent %s: %d", config->name, ret); + return ret; + } + + LOG_DBG("Receive callback set successfully for proxy agent %s", config->name); + return 0; +} + +static int zbus_proxy_agent_set_ack_cb(struct zbus_proxy_agent_config *config, + int (*ack_cb)(uint32_t msg_id, void *user_data)) +{ + if (!config || !config->api || !config->backend_config) { + LOG_ERR("Invalid proxy agent configuration"); + return -EINVAL; + } + + if (!config->api->backend_set_ack_cb) { + LOG_ERR("Backend set ACK callback function is not defined"); + return -ENOSYS; + } + + int ret = config->api->backend_set_ack_cb(config->backend_config, ack_cb, config); + + if (ret < 0) { + LOG_ERR("Failed to set ACK callback for proxy agent %s: %d", config->name, ret); + return ret; + } + + LOG_DBG("ACK callback set successfully for proxy agent %s", config->name); + return 0; +} + +static int zbus_proxy_agent_init(struct zbus_proxy_agent_config *config) +{ + int ret; + + if (!config || !config->api || !config->backend_config) { + LOG_ERR("Invalid proxy agent configuration"); + return -EINVAL; + } + + if (!config->api->backend_init) { + LOG_ERR("Backend init function is not defined"); + return -ENOSYS; + } + + ret = config->api->backend_init(config->backend_config); + if (ret < 0) { + LOG_ERR("Failed to initialize backend for proxy agent %s: %d", config->name, ret); + return ret; + } + + LOG_DBG("Proxy agent %s of type %d initialized successfully", config->name, config->type); + + return 0; +} + +static int zbus_proxy_agent_send(struct zbus_proxy_agent_config *config, + struct zbus_proxy_agent_msg *msg, uint8_t transmit_attempts) +{ + int ret; + + if (!config || !config->api || !msg) { + LOG_ERR("Invalid parameters for sending message"); + return -EINVAL; + } + + if (!config->api->backend_send) { + LOG_ERR("Backend send function is not defined"); + return -ENOSYS; + } + + /* Add message to tracking pool before sending to avoid race condition with ACKs */ + if (config->sent_msg_pool) { + ret = zbus_proxy_agent_sent_ack_timeout_start(config, msg, transmit_attempts); + if (ret < 0) { + LOG_ERR("Failed to track sent message ID %d for proxy agent %s: %d", + msg->id, config->name, ret); + return ret; + } + } + + ret = config->api->backend_send((void *)config->backend_config, msg); + if (ret < 0) { + LOG_ERR("Failed to send message via proxy agent %s: %d", config->name, ret); + + /* Remove from tracking pool since send failed */ + if (config->sent_msg_pool) { + ret = zbus_proxy_agent_sent_ack_timeout_stop(config, msg->id); + if (ret < 0) { + LOG_ERR("Failed to cleanup tracked message ID %d after send " + "failure: %d", + msg->id, ret); + } + } + return ret; + } + + LOG_DBG("Message sent successfully via proxy agent %s", config->name); + return 0; +} + +static int zbus_proxy_agent_msg_recv_cb(const struct zbus_proxy_agent_msg *msg) +{ + int ret; + + /* Find corresponding channel by name. + * TODO: Would create less overhead if using ID, but that would require ID to be enabled and + * stricter control of ID assignment. + */ + const struct zbus_channel *chan; + + if (!msg) { + LOG_ERR("Received NULL message in callback"); + return -EINVAL; + } + + chan = zbus_chan_from_name(msg->channel_name); + + if (!chan) { + LOG_ERR("No channel found for message with name %s", msg->channel_name); + return -ENOENT; + } + if (!chan->is_shadow_channel) { + LOG_ERR("Channel %s is not a shadow channel, cannot process message", chan->name); + return -EPERM; + } + + ret = zbus_chan_pub_shadow(chan, msg->message_data, K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to publish shadow message on channel %s: %d", chan->name, ret); + return ret; + } + + LOG_DBG("Published message on shadow channel %s", chan->name); + + return 0; +} + +static int zbus_proxy_agent_msg_ack_cb(uint32_t msg_id, void *user_data) +{ + if (!user_data) { + LOG_ERR("Received NULL user data in ACK callback"); + return -EINVAL; + } + + struct zbus_proxy_agent_config *config = (struct zbus_proxy_agent_config *)user_data; + + LOG_DBG("Received ACK for message ID %d via proxy agent %s", msg_id, config->name); + + int ret = zbus_proxy_agent_sent_ack_timeout_stop(config, msg_id); + + if (ret < 0) { + if (ret == -ENOENT) { + LOG_DBG("Message ID %d was not found in tracking pool (already processed " + "or never tracked)", + msg_id); + return -ENOENT; + } + LOG_WRN("Failed to remove sent message ID %d from tracking pool: %d", msg_id, ret); + return ret; + } + + LOG_DBG("Successfully processed ACK for message ID %d", msg_id); + return 0; +} + +int zbus_proxy_agent_thread(struct zbus_proxy_agent_config *config, + const struct zbus_observer *subscriber) +{ + int ret; + uint8_t message_data[CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE] = {0}; + const struct zbus_channel *chan; + + if (!config) { + LOG_ERR("Invalid proxy agent configuration for thread"); + return -EINVAL; + } + + LOG_DBG("Starting thread for proxy agent %s", config->name); + + ret = zbus_proxy_agent_set_recv_cb(config, zbus_proxy_agent_msg_recv_cb); + if (ret < 0) { + LOG_ERR("Failed to set receive callback for proxy agent %s: %d", config->name, ret); + return ret; + } + + ret = zbus_proxy_agent_set_ack_cb(config, zbus_proxy_agent_msg_ack_cb); + if (ret < 0) { + LOG_ERR("Failed to set ACK callback for proxy agent %s: %d", config->name, ret); + return ret; + } + + zbus_proxy_agent_sent_msg_pool_init(config); + + ret = zbus_proxy_agent_init(config); + if (ret < 0) { + LOG_ERR("Failed to initialize proxy agent %s: %d", config->name, ret); + return ret; + } + + while (!zbus_sub_wait_msg(subscriber, &chan, &message_data, K_FOREVER)) { + struct zbus_proxy_agent_msg msg; + + if (ZBUS_CHANNEL_IS_SHADOW(chan)) { + LOG_ERR("Forwarding of shadow channel %s, not supported by proxy agent", + chan->name); + continue; + } + + ret = zbus_create_proxy_agent_msg(&msg, message_data, chan->message_size, + chan->name, strlen(chan->name)); + if (ret < 0) { + LOG_ERR("Failed to create proxy agent message for channel %s: %d", + chan->name, ret); + continue; + } + + ret = zbus_proxy_agent_send(config, &msg, 0); + if (ret < 0) { + LOG_ERR("Failed to send message via proxy agent %s: %d", config->name, ret); + } + } + return 0; +} diff --git a/subsys/zbus/multidomain/zbus_multidomain_ipc.c b/subsys/zbus/multidomain/zbus_multidomain_ipc.c new file mode 100644 index 0000000000000..b2767027d913f --- /dev/null +++ b/subsys/zbus/multidomain/zbus_multidomain_ipc.c @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +LOG_MODULE_REGISTER(zbus_multidomain_ipc, CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL); + +int zbus_multidomain_ipc_backend_ack(void *config, uint32_t msg_id) +{ + int ret; + struct zbus_multidomain_ipc_config *ipc_config = + (struct zbus_multidomain_ipc_config *)config; + struct zbus_proxy_agent_msg ack_msg; + + ret = zbus_create_proxy_agent_ack_msg(&ack_msg, msg_id); + if (ret < 0) { + LOG_ERR("Failed to create ACK message: %d", ret); + return ret; + } + + if (!ipc_config) { + LOG_ERR("Invalid parameters to send ACK"); + return -EINVAL; + } + + ret = ipc_service_send(&ipc_config->ipc_ept, (void *)&ack_msg, sizeof(ack_msg)); + LOG_DBG("ipc_service_send returned %d", ret); + if (ret < 0) { + LOG_ERR("Failed to send ACK message: %d", ret); + return ret; + } + + LOG_DBG("Sent ACK for message %d via IPC device %s", msg_id, ipc_config->dev->name); + + return 0; +} + +static void zbus_multidomain_ipc_backend_ack_work_handler(struct k_work *work) +{ + int ret; + struct zbus_multidomain_ipc_config *ipc_config = + CONTAINER_OF(work, struct zbus_multidomain_ipc_config, ack_work); + + if (!ipc_config) { + LOG_ERR("Invalid IPC config in ACK work handler"); + return; + } + + ret = zbus_multidomain_ipc_backend_ack(ipc_config, ipc_config->ack_msg_id); + if (ret < 0) { + LOG_ERR("Failed to send ACK for message %d: %d", ipc_config->ack_msg_id, ret); + } +} + +void zbus_multidomain_ipc_bound_cb(void *config) +{ + struct zbus_multidomain_ipc_config *ipc_config = + (struct zbus_multidomain_ipc_config *)config; + + k_sem_give(&ipc_config->ept_bound_sem); + LOG_DBG("IPC endpoint %s bound", ipc_config->ept_cfg->name); +} + +void zbus_multidomain_ipc_error_cb(const char *error_msg, void *config) +{ + struct zbus_multidomain_ipc_config *ipc_config = + (struct zbus_multidomain_ipc_config *)config; + + LOG_ERR("IPC error: %s on endpoint %s", error_msg, ipc_config->ept_cfg->name); +} + +void zbus_multidomain_ipc_recv_cb(const void *data, size_t len, void *config) +{ + int ret; + struct zbus_multidomain_ipc_config *ipc_config = + (struct zbus_multidomain_ipc_config *)config; + const struct zbus_proxy_agent_msg *msg = (const struct zbus_proxy_agent_msg *)data; + + if (!data || !len) { + LOG_ERR("Received empty data on IPC endpoint"); + return; + } + + if (len != sizeof(struct zbus_proxy_agent_msg)) { + LOG_ERR("Invalid message size: expected %zu, got %zu", + sizeof(struct zbus_proxy_agent_msg), len); + return; + } + + /* Verify CRC32 */ + if (verify_proxy_agent_msg_crc(msg) != 0) { + LOG_ERR("Received message with invalid CRC, dropping"); + LOG_HEXDUMP_DBG((const uint8_t *)msg, sizeof(*msg), "Invalid message:"); + LOG_DBG("Received CRC32: 0x%08X, Expected CRC32: 0x%08X", msg->crc32, + crc32_ieee((const uint8_t *)msg, sizeof(*msg) - sizeof(msg->crc32))); + return; + } + + if (msg->type == ZBUS_PROXY_AGENT_MSG_TYPE_ACK) { + if (!ipc_config->ack_cb) { + LOG_ERR("ACK callback not set, dropping ACK"); + return; + } + + ret = ipc_config->ack_cb(msg->id, ipc_config->ack_cb_user_data); + if (ret < 0) { + LOG_ERR("Failed to process received ACK: %d", ret); + } + + return; + } else if (msg->type == ZBUS_PROXY_AGENT_MSG_TYPE_MSG) { + /* Schedule work to send ACK to avoid blocking the receive callback */ + ipc_config->ack_msg_id = msg->id; + if (!ipc_config->recv_cb) { + LOG_ERR("No receive callback set for IPC endpoint %s", + ipc_config->ept_cfg->name); + return; + } + + ret = ipc_config->recv_cb(msg); + if (ret < 0) { + LOG_ERR("Failed to process received message on IPC endpoint %s: %d", + ipc_config->ept_cfg->name, ret); + } + + ret = k_work_submit(&ipc_config->ack_work); + if (ret < 0) { + LOG_ERR("Failed to submit ACK work: %d", ret); + return; + } + } else { + LOG_WRN("Unknown message type: %d", msg->type); + } +} + +int zbus_multidomain_ipc_backend_set_recv_cb(void *config, + int (*recv_cb)(const struct zbus_proxy_agent_msg *msg)) +{ + struct zbus_multidomain_ipc_config *ipc_config = + (struct zbus_multidomain_ipc_config *)config; + + if (!ipc_config || !recv_cb) { + LOG_ERR("Invalid parameters to set receive callback"); + return -EINVAL; + } + + ipc_config->recv_cb = recv_cb; + LOG_DBG("Set receive callback for IPC endpoint %s", ipc_config->ept_cfg->name); + return 0; +} + +int zbus_multidomain_ipc_backend_set_ack_cb(void *config, + int (*ack_cb)(uint32_t msg_id, void *user_data), + void *user_data) +{ + struct zbus_multidomain_ipc_config *ipc_config = + (struct zbus_multidomain_ipc_config *)config; + + if (!ipc_config || !ack_cb) { + LOG_ERR("Invalid parameters to set ACK callback"); + return -EINVAL; + } + + ipc_config->ack_cb = ack_cb; + ipc_config->ack_cb_user_data = user_data; + LOG_DBG("Set ACK callback for IPC endpoint %s", ipc_config->ept_cfg->name); + return 0; +} + +int zbus_multidomain_ipc_backend_init(void *config) +{ + int ret; + struct zbus_multidomain_ipc_config *ipc_config = + (struct zbus_multidomain_ipc_config *)config; + + if (!config) { + LOG_ERR("Invalid IPC backend configuration"); + return -EINVAL; + } + + if (!ipc_config->dev) { + LOG_ERR("IPC device is NULL"); + return -ENODEV; + } + if (!ipc_config->ept_cfg) { + LOG_ERR("IPC device or endpoint configuration is NULL"); + return -EINVAL; + } + + ret = k_sem_init(&ipc_config->ept_bound_sem, 0, 1); + if (ret < 0) { + LOG_ERR("Failed to initialize IPC endpoint bound semaphore: %d", ret); + return ret; + } + + k_work_init(&ipc_config->ack_work, zbus_multidomain_ipc_backend_ack_work_handler); + + LOG_DBG("Initialized IPC endpoint bound semaphore for %s", ipc_config->ept_cfg->name); + + if (!device_is_ready(ipc_config->dev)) { + LOG_ERR("IPC device is not ready"); + return -ENODEV; + } + + /** Set up IPC endpoint configuration */ + ipc_config->ept_cfg->cb.received = zbus_multidomain_ipc_recv_cb; + ipc_config->ept_cfg->cb.error = zbus_multidomain_ipc_error_cb; + ipc_config->ept_cfg->cb.bound = zbus_multidomain_ipc_bound_cb; + ipc_config->ept_cfg->priv = ipc_config; + + ret = ipc_service_open_instance(ipc_config->dev); + if (ret < 0) { + LOG_ERR("Failed to open IPC instance %s: %d", ipc_config->dev->name, ret); + return ret; + } + ret = ipc_service_register_endpoint(ipc_config->dev, &ipc_config->ipc_ept, + ipc_config->ept_cfg); + if (ret < 0) { + LOG_ERR("Failed to register IPC endpoint %s: %d", ipc_config->ept_cfg->name, ret); + return ret; + } + ret = k_sem_take(&ipc_config->ept_bound_sem, K_FOREVER); + if (ret < 0) { + LOG_ERR("Failed to wait for IPC endpoint %s to be bound: %d", + ipc_config->ept_cfg->name, ret); + return ret; + } + LOG_DBG("ZBUS Multidomain IPC initialized for device %s with endpoint %s", + ipc_config->dev->name, ipc_config->ept_cfg->name); + + return 0; +} + +int zbus_multidomain_ipc_backend_send(void *config, struct zbus_proxy_agent_msg *msg) +{ + int ret; + struct zbus_multidomain_ipc_config *ipc_config = + (struct zbus_multidomain_ipc_config *)config; + + if (!ipc_config) { + LOG_ERR("Invalid IPC backend configuration for send"); + return -EINVAL; + } + + if (!msg || msg->message_size == 0) { + LOG_ERR("Invalid message to send on IPC endpoint %s", ipc_config->ept_cfg->name); + return -EINVAL; + } + + ret = ipc_service_send(&ipc_config->ipc_ept, (void *)msg, sizeof(*msg)); + if (ret < 0) { + LOG_ERR("Failed to send message on IPC endpoint %s: %d", ipc_config->ept_cfg->name, + ret); + return ret; + } + + LOG_DBG("Sent message of size %d on IPC endpoint %s", ret, ipc_config->ept_cfg->name); + + return 0; +} + +/* Define the IPC backend API */ +const struct zbus_proxy_agent_api zbus_multidomain_ipc_api = { + .backend_init = zbus_multidomain_ipc_backend_init, + .backend_send = zbus_multidomain_ipc_backend_send, + .backend_set_recv_cb = zbus_multidomain_ipc_backend_set_recv_cb, + .backend_set_ack_cb = zbus_multidomain_ipc_backend_set_ack_cb, +}; diff --git a/subsys/zbus/multidomain/zbus_multidomain_uart.c b/subsys/zbus/multidomain/zbus_multidomain_uart.c new file mode 100644 index 0000000000000..5b96f9d6b0a73 --- /dev/null +++ b/subsys/zbus/multidomain/zbus_multidomain_uart.c @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +LOG_MODULE_REGISTER(zbus_multidomain_uart, CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL); + +int zbus_multidomain_uart_backend_ack(void *config, uint32_t msg_id) +{ + int ret; + + struct zbus_multidomain_uart_config *uart_config = + (struct zbus_multidomain_uart_config *)config; + + if (!uart_config) { + LOG_ERR("Invalid parameters to send ACK"); + return -EINVAL; + } + + struct zbus_proxy_agent_msg ack_msg; + + ret = zbus_create_proxy_agent_ack_msg(&ack_msg, msg_id); + if (ret < 0) { + LOG_ERR("Failed to create ACK message: %d", ret); + return ret; + } + + ret = k_sem_take(&uart_config->tx_busy_sem, K_FOREVER); + if (ret < 0) { + LOG_ERR("Failed to take TX busy semaphore: %d", ret); + return ret; + } + + /* Create a copy of the ACK message to avoid issues with the caller + * modifying the message before it is sent. + */ + memcpy(&uart_config->tx_msg_copy, &ack_msg, sizeof(ack_msg)); + + ret = uart_tx(uart_config->dev, (uint8_t *)&uart_config->tx_msg_copy, + sizeof(uart_config->tx_msg_copy), SYS_FOREVER_US); + if (ret < 0) { + LOG_ERR("Failed to send ACK message: %d", ret); + k_sem_give(&uart_config->tx_busy_sem); + return ret; + } + LOG_DBG("Sent ACK for message %d via UART device %s", msg_id, uart_config->dev->name); + + return 0; +} + +static void uart_ack_work_handler(struct k_work *work) +{ + struct zbus_multidomain_uart_config *uart_config = + CONTAINER_OF(work, struct zbus_multidomain_uart_config, ack_work); + + if (!uart_config) { + LOG_ERR("Invalid UART config in ACK work handler"); + return; + } + int ret = zbus_multidomain_uart_backend_ack(uart_config, uart_config->ack_msg_id); + + if (ret < 0) { + LOG_ERR("Failed to send ACK for message %d: %d", uart_config->ack_msg_id, ret); + } +} + +static void handle_uart_recv_ack(struct zbus_multidomain_uart_config *uart_config, + const struct zbus_proxy_agent_msg *msg) +{ + int ret; + + if (!uart_config->ack_cb) { + LOG_ERR("ACK callback not set, dropping ACK"); + return; + } + + ret = uart_config->ack_cb(msg->id, uart_config->ack_cb_user_data); + if (ret < 0) { + LOG_ERR("Failed to process received ACK: %d", ret); + } +} + +static void handle_uart_recv_msg(const struct device *dev, + struct zbus_multidomain_uart_config *uart_config, + const struct zbus_proxy_agent_msg *msg) +{ + int ret; + /* Synchronize uart message with rx buffer */ + if (msg->message_size == 0 || msg->message_size > CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE) { + LOG_ERR("Invalid message size: %u", msg->message_size); + return; + } + + if (!uart_config->recv_cb) { + LOG_ERR("Receive callback not set, dropping message"); + return; + } + + ret = uart_config->recv_cb(msg); + if (ret < 0) { + LOG_ERR("Failed to process received message: %d", ret); + return; + } + + /* Schedule work to send ACK to avoid blocking the receive callback */ + uart_config->ack_msg_id = msg->id; + ret = k_work_submit(&uart_config->ack_work); + if (ret < 0) { + LOG_ERR("Failed to submit ACK work: %d", ret); + return; + } +} + +void zbus_multidomain_uart_backend_cb(const struct device *dev, struct uart_event *evt, + void *config) +{ + int ret; + struct zbus_multidomain_uart_config *uart_config = + (struct zbus_multidomain_uart_config *)config; + + switch (evt->type) { + case UART_TX_DONE: + k_sem_give(&uart_config->tx_busy_sem); + break; + + case UART_TX_ABORTED: + LOG_ERR("UART TX aborted"); + k_sem_give(&uart_config->tx_busy_sem); + break; + + case UART_RX_RDY: + + const struct zbus_proxy_agent_msg *msg = + (struct zbus_proxy_agent_msg *)evt->data.rx.buf; + + if (verify_proxy_agent_msg_crc(msg) != 0) { + LOG_ERR("Received message with invalid CRC, dropping"); + LOG_HEXDUMP_DBG((const uint8_t *)msg, sizeof(*msg), "Invalid message:"); + LOG_DBG("Received CRC32: 0x%08X, Expected CRC32: 0x%08X", msg->crc32, + crc32_ieee((const uint8_t *)msg, + sizeof(*msg) - sizeof(msg->crc32))); + + /* Force uart reset to dump buffer with possible overflow */ + ret = uart_rx_disable(dev); + if (ret < 0) { + LOG_ERR("Failed to disable RX for reset: %d", ret); + } + + return; + } + + if (msg->type == ZBUS_PROXY_AGENT_MSG_TYPE_ACK) { + handle_uart_recv_ack(uart_config, msg); + } else if (msg->type == ZBUS_PROXY_AGENT_MSG_TYPE_MSG) { + handle_uart_recv_msg(dev, uart_config, msg); + } else { + LOG_WRN("Unknown message type: %d", msg->type); + + /* Force uart reset to dump buffer with possible overflow */ + ret = uart_rx_disable(dev); + if (ret < 0) { + LOG_ERR("Failed to disable RX for reset: %d", ret); + } + return; + } + + break; + + case UART_RX_BUF_REQUEST: + ret = uart_rx_buf_rsp(dev, uart_config->async_rx_buf[uart_config->async_rx_buf_idx], + sizeof(uart_config->async_rx_buf[0])); + if (ret < 0) { + LOG_ERR("Failed to provide RX buffer: %d", ret); + } else { + uart_config->async_rx_buf_idx = (uart_config->async_rx_buf_idx + 1) % + CONFIG_ZBUS_MULTIDOMAIN_UART_BUF_COUNT; + LOG_DBG("Provided RX buffer %d", uart_config->async_rx_buf_idx); + } + break; + + case UART_RX_BUF_RELEASED: + break; + + case UART_RX_DISABLED: + LOG_WRN("UART RX disabled, re-enabling"); + ret = uart_rx_enable(dev, uart_config->async_rx_buf[uart_config->async_rx_buf_idx], + sizeof(uart_config->async_rx_buf[0]), SYS_FOREVER_US); + if (ret < 0) { + LOG_ERR("Failed to re-enable UART RX: %d", ret); + } + break; + + default: + LOG_DBG("Unhandled UART event: %d", evt->type); + break; + } +} + +int zbus_multidomain_uart_backend_set_recv_cb( + void *config, int (*recv_cb)(const struct zbus_proxy_agent_msg *msg)) +{ + struct zbus_multidomain_uart_config *uart_config = + (struct zbus_multidomain_uart_config *)config; + + if (!uart_config || !recv_cb) { + LOG_ERR("Invalid parameters to set receive callback"); + return -EINVAL; + } + + uart_config->recv_cb = recv_cb; + LOG_DBG("Set receive callback for UART device %s", uart_config->dev->name); + return 0; +} + +int zbus_multidomain_uart_backend_set_ack_cb(void *config, + int (*ack_cb)(uint32_t msg_id, void *user_data), + void *user_data) +{ + struct zbus_multidomain_uart_config *uart_config = + (struct zbus_multidomain_uart_config *)config; + + if (!uart_config || !ack_cb) { + LOG_ERR("Invalid parameters to set ACK callback"); + return -EINVAL; + } + + uart_config->ack_cb = ack_cb; + uart_config->ack_cb_user_data = user_data; + LOG_DBG("Set ACK callback for UART device %s", uart_config->dev->name); + return 0; +} + +int zbus_multidomain_uart_backend_init(void *config) +{ + int ret; + + if (!config) { + LOG_ERR("Invalid UART backend configuration"); + return -EINVAL; + } + + struct zbus_multidomain_uart_config *uart_config = + (struct zbus_multidomain_uart_config *)config; + + k_work_init(&uart_config->ack_work, uart_ack_work_handler); + + if (!device_is_ready(uart_config->dev)) { + LOG_ERR("Device %s is not ready", uart_config->dev->name); + return -ENODEV; + } + + ret = uart_callback_set(uart_config->dev, zbus_multidomain_uart_backend_cb, uart_config); + if (ret < 0) { + LOG_ERR("Failed to set UART callback: %d", ret); + return ret; + } + + ret = uart_rx_enable(uart_config->dev, uart_config->async_rx_buf[0], + sizeof(uart_config->async_rx_buf[0]), SYS_FOREVER_US); + if (ret < 0 && ret != -EBUSY) { /* -EBUSY if allready enabled */ + LOG_ERR("Failed to enable UART RX: %d", ret); + return ret; + } + + ret = k_sem_init(&uart_config->tx_busy_sem, 1, 1); + if (ret < 0) { + LOG_ERR("Failed to initialize TX busy semaphore: %d", ret); + return ret; + } + + LOG_DBG("ZBUS Multidomain UART initialized for device %s", uart_config->dev->name); + + return 0; +} + +int zbus_multidomain_uart_backend_send(void *config, struct zbus_proxy_agent_msg *msg) +{ + int ret; + + struct zbus_multidomain_uart_config *uart_config = + (struct zbus_multidomain_uart_config *)config; + + if (!config || !msg || msg->message_size == 0 || + msg->message_size > CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE) { + LOG_ERR("Invalid parameters to send message"); + return -EINVAL; + } + + ret = k_sem_take(&uart_config->tx_busy_sem, K_FOREVER); + if (ret < 0) { + LOG_ERR("Failed to take TX busy semaphore: %d", ret); + return ret; + } + + /* Create a copy of the message to hand to uart tx, as the original message may be modified + */ + memcpy(&uart_config->tx_msg_copy, msg, sizeof(uart_config->tx_msg_copy)); + + ret = uart_tx(uart_config->dev, (uint8_t *)&uart_config->tx_msg_copy, + sizeof(uart_config->tx_msg_copy), SYS_FOREVER_US); + if (ret < 0) { + LOG_ERR("Failed to send message via UART: %d", ret); + k_sem_give(&uart_config->tx_busy_sem); + return ret; + } + + LOG_DBG("Sent message of size %d via UART", uart_config->tx_msg_copy.message_size); + + return 0; +} + +/* Define the UART backend API */ +const struct zbus_proxy_agent_api zbus_multidomain_uart_api = { + .backend_init = zbus_multidomain_uart_backend_init, + .backend_send = zbus_multidomain_uart_backend_send, + .backend_set_recv_cb = zbus_multidomain_uart_backend_set_recv_cb, + .backend_set_ack_cb = zbus_multidomain_uart_backend_set_ack_cb, +}; diff --git a/subsys/zbus/zbus.c b/subsys/zbus/zbus.c index 0fff534c2481f..28911970ba2db 100644 --- a/subsys/zbus/zbus.c +++ b/subsys/zbus/zbus.c @@ -122,6 +122,26 @@ const struct zbus_channel *zbus_chan_from_id(uint32_t channel_id) #endif /* CONFIG_ZBUS_CHANNEL_ID */ +#if defined(CONFIG_ZBUS_CHANNEL_NAME) + +const struct zbus_channel *zbus_chan_from_name(const char *name) +{ + if (!name) { + return NULL; + } + + STRUCT_SECTION_FOREACH(zbus_channel, chan) { + if (strcmp(chan->name, name) == 0) { + /* Found matching channel */ + return chan; + } + } + /* No matching channel exists */ + return NULL; +} + +#endif /* CONFIG_ZBUS_CHANNEL_NAME */ + static inline int _zbus_notify_observer(const struct zbus_channel *chan, const struct zbus_observer *obs, k_timepoint_t end_time, struct net_buf *buf) @@ -383,6 +403,18 @@ int zbus_chan_pub(const struct zbus_channel *chan, const void *msg, k_timeout_t _ZBUS_ASSERT(k_is_in_isr() ? K_TIMEOUT_EQ(timeout, K_NO_WAIT) : true, "inside an ISR, the timeout must be K_NO_WAIT"); +#if defined(CONFIG_ZBUS_MULTIDOMAIN) + + /* Normal publish to a shadow channel is not allowed from application logic. */ + if (chan->is_shadow_channel) { + LOG_ERR("Channel is defined as shadow. Cannot publish to shadow channel %s from " + "application logic, only from ZBUS proxy agent", + _ZBUS_CHAN_NAME(chan)); + return -EPERM; + } + +#endif /* CONFIG_ZBUS_MULTIDOMAIN */ + if (k_is_in_isr()) { timeout = K_NO_WAIT; } @@ -414,6 +446,57 @@ int zbus_chan_pub(const struct zbus_channel *chan, const void *msg, k_timeout_t return err; } +#if defined(CONFIG_ZBUS_MULTIDOMAIN) + +int zbus_chan_pub_shadow(const struct zbus_channel *chan, const void *msg, k_timeout_t timeout) +{ + int err; + + _ZBUS_ASSERT(chan != NULL, "chan is required"); + _ZBUS_ASSERT(msg != NULL, "msg is required"); + _ZBUS_ASSERT(k_is_in_isr() ? K_TIMEOUT_EQ(timeout, K_NO_WAIT) : true, + "inside an ISR, the timeout must be K_NO_WAIT"); + + if (!chan->is_shadow_channel) { + /* Shadow publish to a non-shadow channel is not allowed */ + LOG_ERR("Channel is not defined as shadow. Cannot publish to non-shadow channel %s", + _ZBUS_CHAN_NAME(chan)); + return -EPERM; + } + + if (k_is_in_isr()) { + timeout = K_NO_WAIT; + } + + k_timepoint_t end_time = sys_timepoint_calc(timeout); + + if (chan->validator != NULL && !chan->validator(msg, chan->message_size)) { + return -ENOMSG; + } + + int context_priority = ZBUS_MIN_THREAD_PRIORITY; + + err = chan_lock(chan, timeout, &context_priority); + if (err) { + return err; + } + +#if defined(CONFIG_ZBUS_CHANNEL_PUBLISH_STATS) + chan->data->publish_timestamp = k_uptime_ticks(); + chan->data->publish_count += 1; +#endif + + memcpy(chan->message, msg, chan->message_size); + + err = _zbus_vded_exec(chan, end_time); + + chan_unlock(chan, context_priority); + + return err; +} + +#endif /* CONFIG_ZBUS_MULTIDOMAIN */ + int zbus_chan_read(const struct zbus_channel *chan, void *msg, k_timeout_t timeout) { _ZBUS_ASSERT(chan != NULL, "chan is required"); diff --git a/tests/subsys/zbus/channel_name/CMakeLists.txt b/tests/subsys/zbus/channel_name/CMakeLists.txt new file mode 100644 index 0000000000000..6a642b9590f1e --- /dev/null +++ b/tests/subsys/zbus/channel_name/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_channel_name) + +FILE(GLOB app_sources src/main.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/subsys/zbus/channel_name/prj.conf b/tests/subsys/zbus/channel_name/prj.conf new file mode 100644 index 0000000000000..b387f98a97f94 --- /dev/null +++ b/tests/subsys/zbus/channel_name/prj.conf @@ -0,0 +1,11 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_ZTEST=y +CONFIG_ASSERT=n +CONFIG_LOG=y +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_NAME=y diff --git a/tests/subsys/zbus/channel_name/src/main.c b/tests/subsys/zbus/channel_name/src/main.c new file mode 100644 index 0000000000000..1b23c7e28d13e --- /dev/null +++ b/tests/subsys/zbus/channel_name/src/main.c @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +struct msg { + int x; +}; + +ZBUS_CHAN_DEFINE(chan_a, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); +ZBUS_CHAN_DEFINE(chan_b, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); +ZBUS_CHAN_DEFINE(chan_c, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); +ZBUS_CHAN_DEFINE(chan_d, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); +ZBUS_CHAN_DEFINE(chan_e, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); +ZBUS_CHAN_DEFINE(chan_f, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); + +ZTEST(channel_name, test_channel_retrieval) +{ + /* Invalid/unknown channel names */ + zassert_is_null(zbus_chan_from_name("unknown")); + zassert_is_null(zbus_chan_from_name("")); + + /* Standard retrieval */ + zassert_equal(&chan_a, zbus_chan_from_name("chan_a")); + zassert_equal(&chan_b, zbus_chan_from_name("chan_b")); + zassert_equal(&chan_c, zbus_chan_from_name("chan_c")); + + /* Ensure no cross-talk between names */ + zassert_not_equal(&chan_d, zbus_chan_from_name("chan_e")); + zassert_not_equal(&chan_e, zbus_chan_from_name("chan_f")); + zassert_not_equal(&chan_f, zbus_chan_from_name("chan_d")); +} + +ZTEST_SUITE(channel_name, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/subsys/zbus/channel_name/testcase.yaml b/tests/subsys/zbus/channel_name/testcase.yaml new file mode 100644 index 0000000000000..1df46d8c1b62c --- /dev/null +++ b/tests/subsys/zbus/channel_name/testcase.yaml @@ -0,0 +1,5 @@ +tests: + message_bus.zbus.channel_name: + tags: zbus + integration_platforms: + - native_sim diff --git a/tests/subsys/zbus/multidomain/ipc_backend/CMakeLists.txt b/tests/subsys/zbus/multidomain/ipc_backend/CMakeLists.txt new file mode 100644 index 0000000000000..846b76af56ac8 --- /dev/null +++ b/tests/subsys/zbus/multidomain/ipc_backend/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_channel_name) + +FILE(GLOB app_sources src/main.c src/mock_ipc.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/subsys/zbus/multidomain/ipc_backend/app.overlay b/tests/subsys/zbus/multidomain/ipc_backend/app.overlay new file mode 100644 index 0000000000000..183a2db237106 --- /dev/null +++ b/tests/subsys/zbus/multidomain/ipc_backend/app.overlay @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + fake_ipc: fake_ipc_device { + compatible = "fake-ipc"; + status = "okay"; + }; +}; diff --git a/tests/subsys/zbus/multidomain/ipc_backend/dts/bindings/fake-ipc.yaml b/tests/subsys/zbus/multidomain/ipc_backend/dts/bindings/fake-ipc.yaml new file mode 100644 index 0000000000000..87c7f5356ab7e --- /dev/null +++ b/tests/subsys/zbus/multidomain/ipc_backend/dts/bindings/fake-ipc.yaml @@ -0,0 +1,3 @@ +description: Fake IPC backend for testing zbus multidomain + +compatible: "fake-ipc" diff --git a/tests/subsys/zbus/multidomain/ipc_backend/prj.conf b/tests/subsys/zbus/multidomain/ipc_backend/prj.conf new file mode 100644 index 0000000000000..f5eae81d78e40 --- /dev/null +++ b/tests/subsys/zbus/multidomain/ipc_backend/prj.conf @@ -0,0 +1,27 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_LOG=y +CONFIG_CRC=y +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y + +CONFIG_ZBUS_MULTIDOMAIN=y +CONFIG_ZBUS_MULTIDOMAIN_IPC=y +CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL_INF=y +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT=10 +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX=100 +CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS=5 +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_POOL_SIZE=16 + +CONFIG_ZTEST=y +CONFIG_ASSERT=n +CONFIG_ZTEST_MOCKING=y + +# IPC service support +CONFIG_IPC_SERVICE=y +CONFIG_MBOX=y diff --git a/tests/subsys/zbus/multidomain/ipc_backend/src/main.c b/tests/subsys/zbus/multidomain/ipc_backend/src/main.c new file mode 100644 index 0000000000000..4bdfb43fee5f5 --- /dev/null +++ b/tests/subsys/zbus/multidomain/ipc_backend/src/main.c @@ -0,0 +1,640 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include "mock_ipc.h" + +DEFINE_FFF_GLOBALS; + +/* Get pointer to the device tree registered fake IPC device */ +#define FAKE_IPC_DEVICE DEVICE_DT_GET(DT_NODELABEL(fake_ipc)) + +FAKE_VOID_FUNC1(fake_bound_callback, void *); +FAKE_VOID_FUNC3(fake_received_callback, const void *, size_t, void *); + +FAKE_VALUE_FUNC1(int, fake_multidomain_backend_recv_cb, const struct zbus_proxy_agent_msg *); +FAKE_VALUE_FUNC2(int, fake_multidomain_backend_ack_cb, uint32_t, void *); + +/* Generate backend config using the macro, + * generates zbus_multidomain_ipc_config name##_ipc_config (test_agent_ipc_config) + */ +#define FAKE_IPC_NODE DT_NODELABEL(fake_ipc) +_ZBUS_GENERATE_BACKEND_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent, FAKE_IPC_NODE); + +/* Delayed work handler function */ +static void delayed_bound_callback_work_handler(struct k_work *work) +{ + zassert_false(was_bound_callback_triggered(), + "Bound callback should not have been called yet"); + /* Trigger the bound callback to unblock backend_init */ + trigger_bound_callback(); +} + +/* Define and initialize delayed work globally */ +K_WORK_DELAYABLE_DEFINE(bound_callback_work, delayed_bound_callback_work_handler); + +void schedule_delayed_bound_callback_work(int delay_ms) +{ + k_work_schedule(&bound_callback_work, K_MSEC(delay_ms)); +} + +/* Verify that the mock IPC backend and its integration with the IPC service works as expected */ +ZTEST(ipc_backend, test_ipc_mock_backend) +{ + /* Test fake IPC device structure */ + zassert_not_null(FAKE_IPC_DEVICE, "Fake IPC device is NULL"); + zassert_not_null(FAKE_IPC_DEVICE->api, "Fake IPC device API is NULL"); + zassert_equal_ptr(FAKE_IPC_DEVICE->api, &fake_backend_ops, + "Device API doesn't match fake backend ops"); + + /* Test endpoint registration with callbacks */ + struct ipc_ept test_ept = {0}; + struct ipc_ept_cfg test_cfg = { + .name = "test_endpoint", + .cb = {.bound = fake_bound_callback, .received = fake_received_callback}, + .priv = &test_ept}; + int result; + + fake_ipc_register_endpoint_fake.return_val = 0; + fake_ipc_deregister_endpoint_fake.return_val = 0; + + result = ipc_service_register_endpoint(FAKE_IPC_DEVICE, &test_ept, &test_cfg); + zassert_equal(result, 0, "Expected successful endpoint registration"); + zassert_equal(fake_ipc_register_endpoint_fake.call_count, 1, + "Expected exactly one register call"); + + /* Test bound callback */ + trigger_bound_callback(); + zassert_equal(fake_bound_callback_fake.call_count, 1, "Expected bound callback called"); + zassert_equal_ptr(fake_bound_callback_fake.arg0_val, &test_ept, + "Expected correct private data"); + + /* Test data sending */ + static const char test_data[] = "test"; + + fake_ipc_send_fake.return_val = sizeof(test_data); + result = ipc_service_send(&test_ept, test_data, sizeof(test_data)); + zassert_equal(result, sizeof(test_data), "Expected successful send"); + zassert_equal(fake_ipc_send_fake.call_count, 1, "Expected exactly one send call"); + fake_ipc_send_fake.return_val = 0; + + /* Test received callback */ + static const char received_data[] = "hello"; + + trigger_received_callback(received_data, sizeof(received_data)); + zassert_equal(fake_received_callback_fake.call_count, 1, + "Expected received callback called"); + zassert_equal_ptr(fake_received_callback_fake.arg0_val, received_data, + "Expected correct data"); + zassert_equal(fake_received_callback_fake.arg1_val, sizeof(received_data), + "Expected correct length"); + zassert_equal_ptr(fake_received_callback_fake.arg2_val, &test_ept, + "Expected correct private data"); + + /* Test cleanup */ + result = ipc_service_deregister_endpoint(&test_ept); + zassert_equal(result, 0, "Expected successful endpoint deregistration"); + zassert_equal(fake_ipc_deregister_endpoint_fake.call_count, 1, + "Expected exactly one deregister call"); +} + +ZTEST(ipc_backend, test_backend_macros) +{ + /* Verify the config created with _ZBUS_GENERATE_BACKEND_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC */ + zassert_not_null(&test_agent_ipc_config, "Generated config should not be NULL"); + zassert_not_null(test_agent_ipc_config.dev, "Generated config device should not be NULL"); + zassert_equal_ptr(test_agent_ipc_config.dev, FAKE_IPC_DEVICE, + "Generated config device should be fake IPC device"); + zassert_not_null(test_agent_ipc_config.ept_cfg, + "Generated config endpoint should not be NULL"); + zassert_str_equal(test_agent_ipc_config.ept_cfg->name, "ipc_ept_test_agent", + "Generated config endpoint name should match"); + + /* Test the macros for getting API and config */ + struct zbus_multidomain_ipc_config *config; + const struct zbus_proxy_agent_api *api; + + /* Api from zbus_multidomain_ipc.c */ + extern const struct zbus_proxy_agent_api zbus_multidomain_ipc_api; + + api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + zassert_not_null(api, "API macro returned NULL"); + zassert_equal_ptr(api, &zbus_multidomain_ipc_api, "API macro returned incorrect API"); + + config = _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + zassert_not_null(config, "Config macro returned NULL"); + zassert_equal_ptr(config, &test_agent_ipc_config, "Config macro returned incorrect config"); +} + +ZTEST(ipc_backend, test_backend_init_valid) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + /* Schedule work to trigger bound callback after a short delay */ + schedule_delayed_bound_callback_work(1); + + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization"); + zassert_not_null(config->ept_cfg->cb.bound, "Expected bound callback to be set"); + zassert_not_null(config->ept_cfg->cb.received, "Expected received callback to be set"); + zassert_not_null(config->ept_cfg->cb.error, "Expected error callback to be set"); + zassert_equal_ptr(config->ept_cfg->priv, config, "Expected private data to be config"); + zassert_equal(fake_ipc_register_endpoint_fake.call_count, 1, + "Expected register_endpoint called"); + zassert_equal(fake_ipc_open_instance_fake.call_count, 1, "Expected open_instance called"); + zassert_true(was_bound_callback_triggered(), + "Expected bound callback to have been triggered"); +} + +ZTEST(ipc_backend, test_backend_init_null) +{ + int ret; + struct zbus_multidomain_ipc_config *config = NULL; + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + ret = api->backend_init(config); + zassert_equal(ret, -EINVAL, "Expected error on NULL config"); + /* Ensure backend_init still works with valid config afterwards */ + schedule_delayed_bound_callback_work(1); + config = _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization after NULL test"); + + /* Cleanup */ + ret = ipc_service_deregister_endpoint(&config->ipc_ept); + zassert_equal(ret, 0, "Expected successful endpoint deregistration"); + zassert_equal(fake_ipc_deregister_endpoint_fake.call_count, 1, + "Expected exactly one deregister call"); + ret = ipc_service_close_instance(config->dev); + zassert_equal(ret, 0, "Expected successful instance close"); + zassert_equal(fake_ipc_close_instance_fake.call_count, 1, "Expected close_instance called"); + + fake_ipc_open_instance_fake.return_val = -1; + reset_bound_callback_flag(); + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, fake_ipc_open_instance_fake.return_val, + "Expected fake_ipc_open_instance_fake failure to propagate out"); + zassert_false(was_bound_callback_triggered(), "Expected bound callback to not be called"); + fake_ipc_open_instance_fake.return_val = 0; + + fake_ipc_register_endpoint_fake.return_val = -1; + reset_bound_callback_flag(); + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, fake_ipc_register_endpoint_fake.return_val, + "Expected fake_ipc_register_endpoint_fake failure to propagate out"); + fake_ipc_register_endpoint_fake.return_val = 0; + + /* Initialize again to ensure no side effects from previous NULL test */ + reset_bound_callback_flag(); + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization after NULL test"); +} + +ZTEST(ipc_backend, test_backend_init_null_device) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + /* Save original device */ + const struct device *original_dev = config->dev; + + /* Test NULL device */ + config->dev = NULL; + ret = api->backend_init(config); + zassert_equal(ret, -ENODEV, "Expected error on NULL device"); + + /* Restore valid device */ + config->dev = original_dev; + + /* Ensure backend_init still works with valid config afterwards */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization after NULL device test"); +} + +ZTEST(ipc_backend, test_backend_init_missing_endpoint) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + /* Save original endpoint config */ + struct ipc_ept_cfg *original_ept_cfg = config->ept_cfg; + + /* Test NULL endpoint config */ + config->ept_cfg = NULL; + ret = api->backend_init(config); + zassert_equal(ret, -EINVAL, "Expected error on NULL endpoint config"); + + /* Restore valid endpoint config */ + config->ept_cfg = original_ept_cfg; + + /* Ensure backend_init still works with valid config afterwards */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, + "Expected successful backend initialization after NULL endpoint test"); +} + +ZTEST(ipc_backend, test_backend_send_valid) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + /* Initialize backend first */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization"); + + /* Set up fake send to return success */ + fake_ipc_send_fake.return_val = sizeof(struct zbus_proxy_agent_msg); + + /* Test valid message send */ + struct zbus_proxy_agent_msg test_msg; + + test_msg.type = ZBUS_PROXY_AGENT_MSG_TYPE_MSG; + test_msg.id = 1; + test_msg.message_size = 4; + memcpy(test_msg.message_data, "test", 4); + strcpy(test_msg.channel_name, "chan"); + test_msg.channel_name_len = strlen(test_msg.channel_name); + + ret = api->backend_send(config, &test_msg); + zassert_equal(ret, 0, "Expected successful message send"); + zassert_equal(fake_ipc_send_fake.call_count, 1, "Expected send called once"); + + /* Verify sent data */ + const struct zbus_proxy_agent_msg *sent_msg = + (const struct zbus_proxy_agent_msg *)fake_ipc_send_fake.arg2_val; + zassert_not_null(sent_msg, "Sent message should not be NULL"); + zassert_equal(sent_msg->type, test_msg.type, "Sent message type should match"); + zassert_equal(sent_msg->id, test_msg.id, "Sent message ID should match"); + zassert_equal(sent_msg->message_size, test_msg.message_size, + "Sent message size should match"); + zassert_mem_equal(sent_msg->message_data, test_msg.message_data, test_msg.message_size, + "Sent message data should match"); + zassert_equal(sent_msg->channel_name_len, test_msg.channel_name_len, + "Sent channel name length should match"); + zassert_str_equal(sent_msg->channel_name, test_msg.channel_name, + "Sent channel name should match"); + + /* Send fails */ + fake_ipc_send_fake.return_val = -1; + ret = api->backend_send(config, &test_msg); + zassert_equal(ret, fake_ipc_send_fake.return_val, + "Expected fake_ipc_send_fake failure to propagate out"); + fake_ipc_send_fake.return_val = 0; +} + +ZTEST(ipc_backend, test_backend_send_invalid) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + /* Initialize backend first */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization"); + + /* Test NULL message */ + ret = api->backend_send(config, NULL); + zassert_equal(ret, -EINVAL, "Expected error on NULL message"); + zassert_equal(fake_ipc_send_fake.call_count, 0, "Expected send not called on NULL message"); + + /* Test zero-length message - backend should reject before calling IPC send */ + struct zbus_proxy_agent_msg empty_msg = {0}; + + ret = api->backend_send(config, &empty_msg); + zassert_equal(ret, -EINVAL, "Expected error on zero-length message"); + zassert_equal(fake_ipc_send_fake.call_count, 0, "Expected send not called for zero-length"); + + /* Ensure backend_send still works with valid message afterwards */ + fake_ipc_send_fake.call_count = 0; /* Reset call count */ + fake_ipc_send_fake.return_val = sizeof(struct zbus_proxy_agent_msg); + + struct zbus_proxy_agent_msg valid_msg; + + valid_msg.type = ZBUS_PROXY_AGENT_MSG_TYPE_MSG; + valid_msg.id = 2; + valid_msg.message_size = 4; + memcpy(valid_msg.message_data, "data", 4); + strcpy(valid_msg.channel_name, "chan"); + valid_msg.channel_name_len = strlen(valid_msg.channel_name); + + ret = api->backend_send(config, &valid_msg); + zassert_equal(ret, 0, "Expected successful message send after invalid tests"); + zassert_equal(fake_ipc_send_fake.call_count, 1, "Expected send called once for valid msg"); +} + +ZTEST(ipc_backend, test_backend_send_invalid_config) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + /* Initialize backend first */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization"); + + /* Test NULL config */ + struct zbus_proxy_agent_msg test_msg; + + test_msg.type = ZBUS_PROXY_AGENT_MSG_TYPE_MSG; + test_msg.id = 1; + test_msg.message_size = 4; + memcpy(test_msg.message_data, "test", 4); + strcpy(test_msg.channel_name, "chan"); + test_msg.channel_name_len = strlen(test_msg.channel_name); + + ret = api->backend_send(NULL, &test_msg); + zassert_equal(ret, -EINVAL, "Expected error on NULL config"); + zassert_equal(fake_ipc_send_fake.call_count, 0, "Expected send not called on NULL config"); + + /* Ensure backend_send still works with valid config afterwards */ + fake_ipc_send_fake.call_count = 0; /* Reset call count */ + fake_ipc_send_fake.return_val = sizeof(struct zbus_proxy_agent_msg); + ret = api->backend_send(config, &test_msg); + zassert_equal(ret, 0, "Expected successful message send after NULL config test"); + zassert_equal(fake_ipc_send_fake.call_count, 1, "Expected send called once for valid msg"); +} + +ZTEST(ipc_backend, test_backend_set_recv_cb) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + ret = api->backend_set_recv_cb(config, fake_multidomain_backend_recv_cb); + zassert_equal(ret, 0, "Expected successful recv callback set"); + zassert_equal_ptr(config->recv_cb, fake_multidomain_backend_recv_cb, + "Expected recv callback to be set correctly"); + + ret = api->backend_set_recv_cb(config, NULL); + zassert_equal(ret, -EINVAL, "Expected error on NULL recv callback"); + zassert_equal_ptr(config->recv_cb, fake_multidomain_backend_recv_cb, + "Expected recv callback to remain unchanged after NULL set"); + + ret = api->backend_set_recv_cb(NULL, fake_multidomain_backend_recv_cb); + zassert_equal(ret, -EINVAL, "Expected error on NULL config"); + zassert_equal_ptr(config->recv_cb, fake_multidomain_backend_recv_cb, + "Expected recv callback to remain unchanged after NULL config"); +} + +ZTEST(ipc_backend, test_backend_set_ack_cb) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + void *user_data = (void *)0x12345678U; + + ret = api->backend_set_ack_cb(config, fake_multidomain_backend_ack_cb, user_data); + zassert_equal(ret, 0, "Expected successful ack callback set"); + zassert_equal_ptr(config->ack_cb, fake_multidomain_backend_ack_cb, + "Expected ack callback to be set correctly"); + zassert_equal_ptr(config->ack_cb_user_data, user_data, + "Expected ack user data to be set correctly"); + + ret = api->backend_set_ack_cb(config, NULL, user_data); + zassert_equal(ret, -EINVAL, "Expected error on NULL ack callback"); + zassert_equal_ptr(config->ack_cb, fake_multidomain_backend_ack_cb, + "Expected ack callback to remain unchanged after NULL set"); + zassert_equal_ptr(config->ack_cb_user_data, user_data, + "Expected ack user data to remain unchanged after NULL set"); + + ret = api->backend_set_ack_cb(NULL, fake_multidomain_backend_ack_cb, user_data); + zassert_equal(ret, -EINVAL, "Expected error on NULL config"); + zassert_equal_ptr(config->ack_cb, fake_multidomain_backend_ack_cb, + "Expected ack callback to remain unchanged after NULL config"); + zassert_equal_ptr(config->ack_cb_user_data, user_data, + "Expected ack user data to remain unchanged after NULL config"); +} + +ZTEST(ipc_backend, test_backend_recv) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + struct zbus_proxy_agent_msg test_msg; + + ret = zbus_create_proxy_agent_msg(&test_msg, "test", 4, "chan", 4); + zassert_equal(ret, 0, "Expected successful test message creation"); + + /* Initialize backend first */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization"); + + trigger_received_callback(&test_msg, sizeof(test_msg)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 0, + "Expected recv callback to not be called when not set"); + + ret = api->backend_set_recv_cb(config, fake_multidomain_backend_recv_cb); + zassert_equal(ret, 0, "Expected successful recv callback set"); + zassert_equal_ptr(config->recv_cb, fake_multidomain_backend_recv_cb, + "Expected recv callback to be set correctly"); + + /* Valid message */ + trigger_received_callback(&test_msg, sizeof(test_msg)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 1, + "Expected recv callback to be called once"); + zassert_equal_ptr(fake_multidomain_backend_recv_cb_fake.arg0_val, &test_msg, + "Expected recv callback to receive correct message"); + + k_sleep(K_MSEC(5)); /* Ensure works finish */ + zassert_equal(fake_ipc_send_fake.call_count, 1, "Expected send called once for ACK"); + const struct zbus_proxy_agent_msg *ack_msg = + (const struct zbus_proxy_agent_msg *)fake_ipc_send_fake.arg2_val; + zassert_not_null(ack_msg, "ACK message should not be NULL"); + zassert_equal(ack_msg->type, ZBUS_PROXY_AGENT_MSG_TYPE_ACK, + "ACK message type should match"); + zassert_equal(ack_msg->id, test_msg.id, "ACK message ID should match"); + + /* Invalid messages */ + trigger_received_callback(NULL, sizeof(test_msg)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 1, + "Expected recv callback not to be called on NULL message"); + + trigger_received_callback(&test_msg, 0); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 1, + "Expected recv callback not to be called on zero-length message"); + + trigger_received_callback(&test_msg, sizeof(test_msg) - 5); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 1, + "Expected recv callback not to be called on wrong length message"); + + fake_multidomain_backend_recv_cb_fake.return_val = -1; + /* Update message ID to differentiate calls */ + test_msg.id = 2; + test_msg.crc32 = + crc32_ieee((const uint8_t *)&test_msg, sizeof(test_msg) - sizeof(test_msg.crc32)); + trigger_received_callback(&test_msg, sizeof(test_msg)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 2, + "Expected recv callback to be called again"); + zassert_equal_ptr(fake_multidomain_backend_recv_cb_fake.arg0_val, &test_msg, + "Expected recv callback to receive correct message again"); + + fake_multidomain_backend_recv_cb_fake.return_val = 0; + + fake_ipc_send_fake.return_val = -1; + /* Update message ID to differentiate calls */ + test_msg.id = 3; + test_msg.crc32 = + crc32_ieee((const uint8_t *)&test_msg, sizeof(test_msg) - sizeof(test_msg.crc32)); + trigger_received_callback(&test_msg, sizeof(test_msg)); + k_sleep(K_MSEC(5)); /* Ensure works finish */ + fake_ipc_send_fake.return_val = 0; +} + +ZTEST(ipc_backend, test_backend_ack) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + void *user_data = (void *)0x87654321U; + + struct zbus_proxy_agent_msg ack_msg; + + ret = zbus_create_proxy_agent_ack_msg(&ack_msg, 42); + zassert_equal(ret, 0, "Expected successful ACK message creation"); + + /* Initialize backend first */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization"); + + trigger_received_callback(&ack_msg, sizeof(ack_msg)); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 0, + "Expected ack callback to not be called when not set"); + + ret = api->backend_set_ack_cb(config, fake_multidomain_backend_ack_cb, user_data); + zassert_equal(ret, 0, "Expected successful ack callback set"); + zassert_equal_ptr(config->ack_cb, fake_multidomain_backend_ack_cb, + "Expected ack callback to be set correctly"); + + trigger_received_callback(&ack_msg, sizeof(ack_msg)); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 1, + "Expected ack callback to be called once"); + zassert_equal(fake_multidomain_backend_ack_cb_fake.arg0_val, 42, + "Expected ack callback to receive correct message ID"); + zassert_equal_ptr(fake_multidomain_backend_ack_cb_fake.arg1_val, user_data, + "Expected ack callback to receive correct user data"); + + trigger_received_callback(NULL, sizeof(ack_msg)); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 1, + "Expected ack callback not to be called on NULL message"); + trigger_received_callback(&ack_msg, 0); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 1, + "Expected ack callback not to be called on zero-length message"); + + fake_multidomain_backend_ack_cb_fake.return_val = -1; + trigger_received_callback(&ack_msg, sizeof(ack_msg)); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 2, + "Expected ack callback to be called again"); + zassert_equal(fake_multidomain_backend_ack_cb_fake.arg0_val, 42, + "Expected ack callback to receive correct message ID again"); + zassert_equal_ptr(fake_multidomain_backend_ack_cb_fake.arg1_val, user_data, + "Expected ack callback to receive correct user data again"); +} + +ZTEST(ipc_backend, test_backend_invalid_message) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + /* Initialize backend first */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization"); + + /* Setup invalid message */ + struct zbus_proxy_agent_msg invalid_msg; + + ret = zbus_create_proxy_agent_msg(&invalid_msg, "invalid", 7, "chan", 4); + + invalid_msg.type = 99; /* Invalid type */ + invalid_msg.crc32 = crc32_ieee((const uint8_t *)&invalid_msg, + sizeof(invalid_msg) - sizeof(invalid_msg.crc32)); + + trigger_received_callback(&invalid_msg, sizeof(invalid_msg)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 0, + "Expected recv callback not to be called on invalid message type"); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 0, + "Expected ack callback not to be called on invalid message type"); + + invalid_msg.type = ZBUS_PROXY_AGENT_MSG_TYPE_MSG; + invalid_msg.id = 1; + invalid_msg.crc32 = 0; /* Invalid CRC */ + + trigger_received_callback(&invalid_msg, sizeof(invalid_msg)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 0, + "Expected recv callback not to be called on invalid CRC"); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 0, + "Expected ack callback not to be called on invalid CRC"); +} + +ZTEST(ipc_backend, test_backend_ipc_error) +{ + int ret; + struct zbus_multidomain_ipc_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_IPC(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_IPC(); + + /* Initialize backend first */ + schedule_delayed_bound_callback_work(1); + ret = api->backend_init(config); + zassert_equal(ret, 0, "Expected successful backend initialization"); + + /* Trigger error callback */ + trigger_error_callback("Test error"); + /* Asserted with regex in testcase.yaml */ +} + +static void test_setup(void *fixture) +{ + RESET_FAKE(fake_ipc_open_instance); + RESET_FAKE(fake_ipc_close_instance); + RESET_FAKE(fake_ipc_send); + RESET_FAKE(fake_ipc_register_endpoint); + RESET_FAKE(fake_ipc_deregister_endpoint); + RESET_FAKE(fake_bound_callback); + RESET_FAKE(fake_received_callback); + RESET_FAKE(fake_multidomain_backend_recv_cb); + RESET_FAKE(fake_multidomain_backend_ack_cb); + reset_bound_callback_flag(); + + /* Cancel any pending delayed work from previous tests */ + k_work_cancel_delayable(&bound_callback_work); +} + +ZTEST_SUITE(ipc_backend, NULL, NULL, test_setup, NULL, NULL); diff --git a/tests/subsys/zbus/multidomain/ipc_backend/src/mock_ipc.c b/tests/subsys/zbus/multidomain/ipc_backend/src/mock_ipc.c new file mode 100644 index 0000000000000..83f1fba1bebcf --- /dev/null +++ b/tests/subsys/zbus/multidomain/ipc_backend/src/mock_ipc.c @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "mock_ipc.h" + +#define DT_DRV_COMPAT fake_ipc + +DEFINE_FAKE_VALUE_FUNC4(int, fake_ipc_send, const struct device *, void *, const void *, size_t); +DEFINE_FAKE_VALUE_FUNC3(int, fake_ipc_register_endpoint, const struct device *, void **, + const struct ipc_ept_cfg *); + +DEFINE_FAKE_VALUE_FUNC1(int, fake_ipc_open_instance, const struct device *); +DEFINE_FAKE_VALUE_FUNC1(int, fake_ipc_close_instance, const struct device *); +DEFINE_FAKE_VALUE_FUNC2(int, fake_ipc_deregister_endpoint, const struct device *, void *); + +/* Global stored config for callback testing */ +static const struct ipc_ept_cfg *stored_ept_cfg; + +struct fake_ipc_data { + const struct ipc_ept_cfg *stored_ept_cfg; +}; + +int fake_ipc_send_with_copy(const struct device *instance, void *token, const void *data, + size_t len) +{ + /* Copy data to a static buffer to avoid issues with test data scope + * and lifetime. + * Would not be needed in a real backend implementation as data is copied + * during the sending between cores. + */ + static uint8_t ipc_data_copy[512]; + + if (len > sizeof(ipc_data_copy)) { + return -ENOMEM; + } + if (!data) { + return -EINVAL; + } + + memcpy(ipc_data_copy, data, len); + return fake_ipc_send(instance, token, ipc_data_copy, len); +} + +int fake_ipc_register_endpoint_with_storage(const struct device *instance, void **token, + const struct ipc_ept_cfg *cfg) +{ + struct fake_ipc_data *data = instance->data; + + data->stored_ept_cfg = cfg; + stored_ept_cfg = cfg; /* Also update global for callback testing */ + + return fake_ipc_register_endpoint(instance, token, cfg); +} + +/* Flag to track if bound callback was triggered */ +static volatile bool bound_callback_triggered; + +/* Callback testing support */ +void trigger_bound_callback(void) +{ + bound_callback_triggered = true; + if (stored_ept_cfg && stored_ept_cfg->cb.bound) { + stored_ept_cfg->cb.bound(stored_ept_cfg->priv); + } +} + +bool was_bound_callback_triggered(void) +{ + return bound_callback_triggered; +} + +void reset_bound_callback_flag(void) +{ + bound_callback_triggered = false; +} + +void trigger_unbound_callback(void) +{ + if (stored_ept_cfg && stored_ept_cfg->cb.unbound) { + stored_ept_cfg->cb.unbound(stored_ept_cfg->priv); + } +} + +void trigger_received_callback(const void *data, size_t len) +{ + if (stored_ept_cfg && stored_ept_cfg->cb.received) { + stored_ept_cfg->cb.received(data, len, stored_ept_cfg->priv); + } +} + +void trigger_error_callback(const char *error_msg) +{ + if (stored_ept_cfg && stored_ept_cfg->cb.error) { + stored_ept_cfg->cb.error(error_msg, stored_ept_cfg->priv); + } +} + +void clear_stored_ept_cfg(void) +{ + stored_ept_cfg = NULL; +} + +const struct ipc_service_backend fake_backend_ops = { + .open_instance = fake_ipc_open_instance, + .close_instance = fake_ipc_close_instance, + .send = fake_ipc_send_with_copy, + .register_endpoint = fake_ipc_register_endpoint_with_storage, + .deregister_endpoint = fake_ipc_deregister_endpoint, +}; + +/* Define fake IPC device using the same pattern as the working ipc_service test */ +#define DEFINE_FAKE_IPC_DEVICE(i) \ + static struct fake_ipc_data fake_ipc_data_##i; \ + \ + DEVICE_DT_INST_DEFINE(i, NULL, NULL, &fake_ipc_data_##i, NULL, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEVICE, &fake_backend_ops); + +DT_INST_FOREACH_STATUS_OKAY(DEFINE_FAKE_IPC_DEVICE) diff --git a/tests/subsys/zbus/multidomain/ipc_backend/src/mock_ipc.h b/tests/subsys/zbus/multidomain/ipc_backend/src/mock_ipc.h new file mode 100644 index 0000000000000..f0763031cc65b --- /dev/null +++ b/tests/subsys/zbus/multidomain/ipc_backend/src/mock_ipc.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef MOCK_IPC_H_ +#define MOCK_IPC_H_ + +#include +#include +#include + +DECLARE_FAKE_VALUE_FUNC1(int, fake_ipc_open_instance, const struct device *); +DECLARE_FAKE_VALUE_FUNC1(int, fake_ipc_close_instance, const struct device *); +DECLARE_FAKE_VALUE_FUNC4(int, fake_ipc_send, const struct device *, void *, const void *, size_t); +DECLARE_FAKE_VALUE_FUNC3(int, fake_ipc_register_endpoint, const struct device *, void **, + const struct ipc_ept_cfg *); +DECLARE_FAKE_VALUE_FUNC2(int, fake_ipc_deregister_endpoint, const struct device *, void *); + +extern const struct ipc_service_backend fake_backend_ops; + +/* Callback testing support */ +void trigger_bound_callback(void); +void trigger_unbound_callback(void); +void trigger_received_callback(const void *data, size_t len); +void trigger_error_callback(const char *error_msg); +void clear_stored_ept_cfg(void); + +/* Bound callback verification */ +bool was_bound_callback_triggered(void); +void reset_bound_callback_flag(void); + +#endif /* MOCK_IPC_H_ */ diff --git a/tests/subsys/zbus/multidomain/ipc_backend/testcase.yaml b/tests/subsys/zbus/multidomain/ipc_backend/testcase.yaml new file mode 100644 index 0000000000000..73611f98b0513 --- /dev/null +++ b/tests/subsys/zbus/multidomain/ipc_backend/testcase.yaml @@ -0,0 +1,26 @@ +tests: + message_bus.zbus.multidomain.ipc_backend: + platform_exclude: + - mps3/corstone300/fvp + - mps3/corstone310/fvp + - mps4/corstone315/fvp + - mps4/corstone320/fvp + tags: zbus + integration_platforms: + - native_sim + harness: console + harness_config: + type: multi_line + ordered: false + regex: + - ".* ACK callback not set, dropping ACK" + - ".* Failed to process received ACK: -1" + - ".* Received message with invalid CRC, dropping" + - ".* Unknown message type: 99" + - ".* No receive callback set for IPC endpoint ipc_ept_test_agent" + - ".* Received empty data on IPC endpoint" + - ".* Invalid message size: expected .*, got .*" + - ".* Failed to process received message on IPC endpoint ipc_ept_test_agent: -1" + - ".* Failed to send ACK message: -1" + - ".* Failed to send ACK for message 3: -1" + - ".* IPC error: Test error on endpoint ipc_ept_test_agent" diff --git a/tests/subsys/zbus/multidomain/proxy_agent/CMakeLists.txt b/tests/subsys/zbus/multidomain/proxy_agent/CMakeLists.txt new file mode 100644 index 0000000000000..3b26ca7924e68 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_channel_name) + +FILE(GLOB app_sources src/main.c src/zbus_multidomain_mock_backend.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_cortex_a53_qemu_cortex_a53_smp.conf b/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_cortex_a53_qemu_cortex_a53_smp.conf new file mode 100644 index 0000000000000..5c0f31b21c399 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_cortex_a53_qemu_cortex_a53_smp.conf @@ -0,0 +1,8 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Necessary to enable QEMU ICOUNT support for tests to complete on qemu_cortex_a53 +CONFIG_QEMU_ICOUNT=y diff --git a/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_riscv32_qemu_virt_riscv32_smp.conf b/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_riscv32_qemu_virt_riscv32_smp.conf new file mode 100644 index 0000000000000..31f98f900ac01 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_riscv32_qemu_virt_riscv32_smp.conf @@ -0,0 +1,8 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Necessary to enable QEMU ICOUNT support for tests to complete on qemu_riscv32 +CONFIG_QEMU_ICOUNT=y diff --git a/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_riscv64_qemu_virt_riscv64_smp.conf b/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_riscv64_qemu_virt_riscv64_smp.conf new file mode 100644 index 0000000000000..bf412ee135e6d --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_riscv64_qemu_virt_riscv64_smp.conf @@ -0,0 +1,8 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Necessary to enable QEMU ICOUNT support for tests to complete on qemu_riscv64 +CONFIG_QEMU_ICOUNT=y diff --git a/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_x86_64_atom.conf b/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_x86_64_atom.conf new file mode 100644 index 0000000000000..f3e551a488525 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_x86_64_atom.conf @@ -0,0 +1,9 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Necessary to enable QEMU ICOUNT support for tests to complete on qemu_x86_64 +CONFIG_QEMU_ICOUNT=y +CONFIG_QEMU_ICOUNT_SHIFT=6 diff --git a/tests/subsys/zbus/multidomain/proxy_agent/prj.conf b/tests/subsys/zbus/multidomain/proxy_agent/prj.conf new file mode 100644 index 0000000000000..e97af310e84f4 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/prj.conf @@ -0,0 +1,25 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_LOG=y +CONFIG_CRC=y +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y + +CONFIG_ZBUS_MULTIDOMAIN=y +CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL_INF=y +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT=10 +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX=100 +CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS=5 +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_POOL_SIZE=8 + +CONFIG_ZTEST=y +CONFIG_ASSERT=n +CONFIG_ZTEST_MOCKING=y + +CONFIG_MAIN_STACK_SIZE=2048 +CONFIG_ZBUS_MULTIDOMAIN_PROXY_STACK_SIZE=2048 diff --git a/tests/subsys/zbus/multidomain/proxy_agent/src/main.c b/tests/subsys/zbus/multidomain/proxy_agent/src/main.c new file mode 100644 index 0000000000000..4601cd3ae79c0 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/src/main.c @@ -0,0 +1,591 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include "zbus_multidomain_mock_backend.h" +#include + +/* Define test channels */ +ZBUS_CHAN_DEFINE(test_channel_1, uint32_t, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); +ZBUS_CHAN_DEFINE(test_channel_2, uint32_t, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); + +/* Define shadow channels for receiving tests */ +ZBUS_SHADOW_CHAN_DEFINE(test_shadow_channel_1, uint32_t, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); +ZBUS_SHADOW_CHAN_DEFINE(test_shadow_channel_2, uint32_t, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +/* Channel for max size testing */ +typedef struct { + uint8_t data[CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE]; +} max_size_msg_t; +ZBUS_CHAN_DEFINE(test_max_size_channel, max_size_msg_t, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT({})); + +/* Define the proxy agent using the mock backend */ +ZBUS_PROXY_AGENT_DEFINE(test_proxy_agent, ZBUS_MULTIDOMAIN_TYPE_MOCK, "test_mock_backend"); + +/* Add channels to the proxy agent */ +ZBUS_PROXY_ADD_CHANNEL(test_proxy_agent, test_channel_1); +ZBUS_PROXY_ADD_CHANNEL(test_proxy_agent, test_channel_2); +ZBUS_PROXY_ADD_CHANNEL(test_proxy_agent, test_max_size_channel); + +/* Global variables for tracking received messages in tests */ +static bool message_received; +static const struct zbus_channel *last_published_channel; + +/* Observer callback to track shadow channel publications */ +static void test_shadow_channel_observer_cb(const struct zbus_channel *chan) +{ + last_published_channel = chan; + message_received = true; +} + +/* Define observer for shadow channels */ +ZBUS_LISTENER_DEFINE(test_shadow_observer, test_shadow_channel_observer_cb); + +/* Add observer to shadow channels */ +ZBUS_CHAN_ADD_OBS(test_shadow_channel_1, test_shadow_observer, 3); +ZBUS_CHAN_ADD_OBS(test_shadow_channel_2, test_shadow_observer, 3); + +int get_total_timeout(int attempts) +{ + int total = 0; + + for (int i = 0; i < attempts; i++) { + int timeout = CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT << i; + + if (timeout > CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX) { + timeout = CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX; + } + total += timeout; + } + return total; +} + +ZTEST(proxy_agent_test, test_proxy_agent_creation) +{ + extern struct zbus_proxy_agent_config test_proxy_agent_config; + + zassert_not_null(&test_proxy_agent_config, "Proxy agent config should exist"); + zassert_str_equal(test_proxy_agent_config.name, "test_proxy_agent", "Name should match"); + zassert_equal((int)test_proxy_agent_config.type, (int)ZBUS_MULTIDOMAIN_TYPE_MOCK, + "Type should be MOCK"); + zassert_not_null(test_proxy_agent_config.api, "API should not be NULL"); + zassert_not_null(test_proxy_agent_config.backend_config, + "Backend config should not be NULL"); + zassert_not_null(test_proxy_agent_config.sent_msg_pool, "Sent msg pool should not be NULL"); +} + +ZTEST(proxy_agent_test, test_proxy_agent_initialization) +{ + extern struct zbus_proxy_agent_config test_proxy_agent_config; + extern const struct zbus_observer test_proxy_agent_subscriber; + + /* Test that the proxy agent was created with correct configuration */ + zassert_not_null(&test_proxy_agent_config, "Config should exist"); + zassert_not_null(&test_proxy_agent_subscriber, "Subscriber should exist"); + + /* Verify that the API structure is properly set up */ + zassert_not_null(test_proxy_agent_config.api, "API should not be NULL"); + zassert_not_null(test_proxy_agent_config.api->backend_init, + "Backend init should not be NULL"); + zassert_not_null(test_proxy_agent_config.api->backend_send, + "Backend send should not be NULL"); + zassert_not_null(test_proxy_agent_config.api->backend_set_recv_cb, + "Set recv CB should not be NULL"); + zassert_not_null(test_proxy_agent_config.api->backend_set_ack_cb, + "Set ack CB should not be NULL"); + + /* Test that the API functions can be called */ + int ret = test_proxy_agent_config.api->backend_init(test_proxy_agent_config.backend_config); + + zassert_equal(ret, 0, "Mock backend init should return 0"); +} + +ZTEST(proxy_agent_test, test_message_forwarding) +{ + uint32_t test_data = 0x12345678U; + + zbus_chan_pub(&test_channel_1, &test_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + /* Verify the backend send was called */ + zassert_true(mock_backend_send_fake.call_count == 1, + "Backend send should be called at least once"); + + /* Verify the sent message content */ + if (mock_backend_send_fake.call_count > 0) { + struct zbus_proxy_agent_msg *sent_msg = mock_backend_get_last_sent_message(); + + zassert_not_null(sent_msg, "Sent message should not be NULL"); + zassert_equal(sent_msg->type, ZBUS_PROXY_AGENT_MSG_TYPE_MSG, + "Message type should be MSG"); + zassert_str_equal(sent_msg->channel_name, "test_channel_1", + "Channel name should match"); + } +} + +ZTEST(proxy_agent_test, test_multiple_channels) +{ + uint32_t test_data1 = 0xAABBCCDDU; + uint32_t test_data2 = 0x11223344U; + + zbus_chan_pub(&test_channel_1, &test_data1, K_MSEC(100)); + zbus_chan_pub(&test_channel_2, &test_data2, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + zassert_true(mock_backend_send_fake.call_count == 2, + "Should send messages for both channels"); +} + +ZTEST(proxy_agent_test, test_retransmission_timeout) +{ + /* Disable auto-ACK for this test to observe retransmissions */ + mock_backend_set_auto_ack(false); + + size_t initial_send_count = mock_backend_send_fake.call_count; + + uint32_t test_data = 0xDEADBEEFU; + + zbus_chan_pub(&test_channel_1, &test_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + zassert_true(mock_backend_send_fake.call_count > initial_send_count, + "Message should be sent initially"); + + size_t first_send_count = mock_backend_send_fake.call_count; + + /* Wait for retransmission timeout */ + k_sleep(K_MSEC(get_total_timeout(2) - 1)); + + zassert_true(mock_backend_send_fake.call_count > first_send_count, + "Message should be retransmitted after timeout"); +} + +ZTEST(proxy_agent_test, test_ack_stops_retransmission) +{ + /* Disable auto-ACK initially to test manual ACK behavior */ + mock_backend_set_auto_ack(false); + + size_t initial_send_count = mock_backend_send_fake.call_count; + uint32_t test_data = 0xACEACE00U; + + zbus_chan_pub(&test_channel_1, &test_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + zassert_true(mock_backend_send_fake.call_count > initial_send_count, + "Message should be sent initially"); + + /* Get the message ID from the copied message to avoid use-after-scope */ + struct zbus_proxy_agent_msg *sent_msg = mock_backend_get_last_sent_message(); + + zassert_not_null(sent_msg, "Sent message should not be NULL"); + + uint32_t msg_id = sent_msg->id; + size_t send_count_after_first = mock_backend_send_fake.call_count; + + /* Simulate receiving an ACK for the message using the stored callback from backend */ + if (mock_backend_stored_ack_cb) { + mock_backend_stored_ack_cb(msg_id, mock_backend_stored_ack_user_data); + } + + printk("Sleeping to check no retransmission after ACK\n"); + /* Wait longer than retransmission timeout */ + k_sleep(K_MSEC(get_total_timeout(2) - 1)); + + zassert_equal(mock_backend_send_fake.call_count, send_count_after_first, + "No retransmissions should occur after ACK received"); +} + +ZTEST(proxy_agent_test, test_max_retransmission_attempts) +{ + /* Disable auto-ACK for this test to observe max retransmission behavior */ + mock_backend_set_auto_ack(false); + + size_t initial_send_count = mock_backend_send_fake.call_count; + uint32_t test_data = 0xDEADBEADU; + + zbus_chan_pub(&test_channel_1, &test_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + zassert_true(mock_backend_send_fake.call_count > initial_send_count, + "Message should be sent initially"); + + k_sleep(K_MSEC(get_total_timeout(CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS) + 10)); + + /* Verify that the number of sends matches max attempts (5 total sends) */ + size_t final_send_count = mock_backend_send_fake.call_count; + + zassert_equal(final_send_count, + initial_send_count + CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS, + "Should have exactly max retransmission attempts (5 total sends)"); + + k_sleep(K_MSEC(get_total_timeout(CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS))); + + zassert_equal(mock_backend_send_fake.call_count, final_send_count, + "Retransmissions should eventually stop after max attempts"); +} + +ZTEST(proxy_agent_test, test_message_content_in_retransmissions) +{ + /* Disable auto-ACK for this test to observe retransmissions */ + mock_backend_set_auto_ack(false); + + size_t initial_send_count = mock_backend_send_fake.call_count; + uint32_t test_data = 0x12345678U; + + printk("Send count: %u\n", mock_backend_send_fake.call_count); + + zbus_chan_pub(&test_channel_1, &test_data, K_MSEC(100)); + + k_sleep(K_MSEC(get_total_timeout(2) - 1)); + + /* Verify multiple sends occurred */ + printk("Send count: %u\n", mock_backend_send_fake.call_count); + zassert_true(mock_backend_send_fake.call_count >= initial_send_count + 1, + "At least initial send + 1 retransmission should occur, " + "initial_send_count=%u, send_count=%u", + initial_send_count, mock_backend_send_fake.call_count); + + /* Verify the content */ + struct zbus_proxy_agent_msg *last_sent_msg = mock_backend_get_last_sent_message(); + + zassert_not_null(last_sent_msg, "Last sent message should not be NULL"); + zassert_equal(last_sent_msg->type, ZBUS_PROXY_AGENT_MSG_TYPE_MSG, + "Should be a data message"); + zassert_str_equal(last_sent_msg->channel_name, "test_channel_1", + "Channel name should match"); + + /* Verify message data */ + uint32_t received_data; + + memcpy(&received_data, last_sent_msg->message_data, sizeof(received_data)); + + zassert_equal(received_data, test_data, "Message data should match original"); +} + +ZTEST(proxy_agent_test, test_backend_send_failure_cleanup) +{ + /* Configure backend to fail on send */ + mock_backend_send_fake.return_val = -EIO; + + size_t initial_count = mock_backend_send_fake.call_count; + uint32_t test_data = 0xFADEU; + + zbus_chan_pub(&test_channel_1, &test_data, K_MSEC(100)); + + k_sleep(K_MSEC(1)); + + /* Verify backend was called but failed */ + zassert_true(mock_backend_send_fake.call_count > initial_count, + "Backend send should be attempted"); + + /* Restore normal behavior */ + mock_backend_send_fake.return_val = 0; + + /* Verify system continues working after failure */ + size_t count_before_recovery = mock_backend_send_fake.call_count; + uint32_t recovery_data = 0xC0FFEEU; + + zbus_chan_pub(&test_channel_1, &recovery_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + zassert_true(mock_backend_send_fake.call_count > count_before_recovery, + "System should recover and send new messages after backend failure"); +} + +ZTEST(proxy_agent_test, test_concurrent_messages) +{ + /* Send multiple messages rapidly */ + for (int i = 0; i < 3; i++) { + uint32_t test_data = 0x1000 + i; + + zbus_chan_pub(&test_channel_1, &test_data, K_NO_WAIT); + } + + k_sleep(K_MSEC(20)); + + zassert_true(mock_backend_send_fake.call_count == 3, + "All concurrent messages should be sent, count=%u", + mock_backend_send_fake.call_count); +} + +ZTEST(proxy_agent_test, test_pool_exhaustion_recovery) +{ + mock_backend_set_auto_ack(false); + + /* Fill the message pool */ + for (int i = 0; i < CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_POOL_SIZE + 2; i++) { + uint32_t test_data = 0x2000 + i; + + zbus_chan_pub(&test_channel_1, &test_data, K_NO_WAIT); + k_sleep(K_MSEC(1)); /* Small delay between messages */ + } + k_sleep(K_MSEC(get_total_timeout(2))); + + printk("Send count after pool exhaustion: %u\n", mock_backend_send_fake.call_count); + zassert_true(mock_backend_send_fake.call_count >= 10, + "Multiple messages should be sent even with pool pressure"); + + /* Re-enable auto-ACK to clear pool */ + mock_backend_set_auto_ack(true); + /* Wait enough time for all messages to be ACKed and pool to recover */ + k_sleep(K_MSEC(get_total_timeout(CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS) + 20)); + + /* Reset counter to test recovery */ + RESET_FAKE(mock_backend_send); + mock_backend_send_fake.return_val = 0; + + /* Verify normal operation resumes */ + uint32_t recovery_data = 0xEC08E7U; + + zbus_chan_pub(&test_channel_1, &recovery_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + printk("Send count after pool recovery: %u\n", mock_backend_send_fake.call_count); + zassert_true(mock_backend_send_fake.call_count >= 1, + "Normal operation should resume after pool recovery"); +} + +ZTEST(proxy_agent_test, test_invalid_ack_message_id) +{ + /* Send ACK for non-existent message ID */ + if (mock_backend_stored_ack_cb) { + uint32_t invalid_id = 0xDEADBEEFU; + int ret = mock_backend_stored_ack_cb(invalid_id, mock_backend_stored_ack_user_data); + + zassert_equal(ret, -ENOENT, "ACK for invalid ID should return -ENOENT"); + } + + /* System should continue working normally after invalid ACK */ + uint32_t test_data = 0x87654321U; + + zbus_chan_pub(&test_channel_1, &test_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + zassert_true(mock_backend_send_fake.call_count >= 1, + "System should continue working after invalid ACK"); +} + +ZTEST(proxy_agent_test, test_duplicate_ack_handling) +{ + /* Disable auto-ACK for manual control */ + mock_backend_set_auto_ack(false); + + uint32_t test_data = 0xDCDE1234U; + + zbus_chan_pub(&test_channel_1, &test_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + zassert_true(mock_backend_send_fake.call_count >= 1, "Message should be sent"); + + /* Get the message ID from the copied message to avoid use-after-scope */ + struct zbus_proxy_agent_msg *sent_msg = mock_backend_get_last_sent_message(); + + zassert_not_null(sent_msg, "Sent message should not be NULL"); + uint32_t msg_id = sent_msg->id; + + /* Send duplicate ACKs manually */ + mock_backend_send_duplicate_ack(msg_id); + + /* Verify no retransmissions occur after first ACK */ + size_t count_after_acks = mock_backend_send_fake.call_count; + + k_sleep(K_MSEC(get_total_timeout(2) - 1)); + + zassert_equal(mock_backend_send_fake.call_count, count_after_acks, + "No retransmissions should occur after duplicate ACKs"); +} + +ZTEST(proxy_agent_test, test_message_size_edge_cases) +{ + max_size_msg_t max_data; + + memset(&max_data, 0xAB, sizeof(max_data)); + + /* Publish maximum size message */ + zbus_chan_pub(&test_max_size_channel, &max_data, K_MSEC(100)); + k_sleep(K_MSEC(1)); + + zassert_true(mock_backend_send_fake.call_count >= 1, "Should send maximum size message"); + + if (mock_backend_send_fake.call_count > 0) { + struct zbus_proxy_agent_msg *sent_msg = mock_backend_get_last_sent_message(); + + zassert_not_null(sent_msg, "Sent message should not be NULL"); + zassert_equal(sent_msg->message_size, sizeof(max_size_msg_t), + "Message size should be maximum"); + zassert_mem_equal(sent_msg->message_data, &max_data, sent_msg->message_size, + "Message data should match"); + } +} + +ZTEST(proxy_agent_test, test_backend_initialization_failure) +{ + /* Configure backend init to fail */ + mock_backend_init_fake.return_val = -ENODEV; + + extern struct zbus_proxy_agent_config test_proxy_agent_config; + + /* Call init directly to test failure handling */ + int ret = test_proxy_agent_config.api->backend_init(test_proxy_agent_config.backend_config); + + zassert_equal(ret, -ENODEV, "Backend init should fail with -ENODEV"); + + /* Restore normal behavior */ + mock_backend_init_fake.return_val = 0; +} + +ZTEST(proxy_agent_test, test_thread_safety_concurrent_publishing) +{ + /* Send messages from "different threads" rapidly with different data */ + for (int i = 0; i < 8; i++) { + uint32_t data1 = 0x1000 + i; + uint32_t data2 = 0x2000 + i; + + /* Simulate concurrent publishing */ + zbus_chan_pub(&test_channel_1, &data1, K_NO_WAIT); + zbus_chan_pub(&test_channel_2, &data2, K_NO_WAIT); + k_sleep(K_MSEC(1)); + } + + /* Give time for all messages to be processed */ + k_sleep(K_MSEC(10)); + + zassert_true(mock_backend_send_fake.call_count >= 10, + "Should handle concurrent messages from multiple channels"); +} + +ZTEST(proxy_agent_test, test_message_receiving_basic) +{ + zassert_true(mock_backend_has_recv_callback(), "Receive callback should be stored"); + + /* Create a message to simulate receiving from backend */ + struct zbus_proxy_agent_msg recv_msg; + uint32_t test_data = 0xABCDEF00U; + + mock_backend_create_test_message(&recv_msg, "test_shadow_channel_1", &test_data, + sizeof(test_data)); + + /* Simulate receiving the message via callback */ + int ret = mock_backend_get_stored_recv_cb()(&recv_msg); + + zassert_equal(ret, 0, "Receive callback should succeed"); + + zassert_true(message_received, "Message should be received on shadow channel"); + zassert_not_null(last_published_channel, "Published channel should be tracked"); + zassert_str_equal(last_published_channel->name, "test_shadow_channel_1", + "Should publish to correct shadow channel"); +} + +ZTEST(proxy_agent_test, test_message_receiving_unknown_channel) +{ + zassert_true(mock_backend_has_recv_callback(), "Receive callback should be stored"); + + /* Create a message for unknown channel */ + struct zbus_proxy_agent_msg recv_msg; + uint32_t test_data = 0xDEADBEEFU; + + mock_backend_create_test_message(&recv_msg, "unknown_channel", &test_data, + sizeof(test_data)); + + /* Simulate receiving the message via callback */ + int ret = mock_backend_get_stored_recv_cb()(&recv_msg); + + zassert_equal(ret, -ENOENT, "Should fail for unknown channel"); + + zassert_false(message_received, "No message should be received for unknown channel"); +} + +ZTEST(proxy_agent_test, test_message_receiving_non_shadow_channel) +{ + zassert_true(mock_backend_has_recv_callback(), "Receive callback should be stored"); + + /* Create a message for regular (non-shadow) channel */ + struct zbus_proxy_agent_msg recv_msg; + uint32_t test_data = 0xCAFEBABEU; + + mock_backend_create_test_message(&recv_msg, "test_channel_1", &test_data, + sizeof(test_data)); + + /* Simulate receiving the message via callback */ + int ret = mock_backend_get_stored_recv_cb()(&recv_msg); + + zassert_equal(ret, -EPERM, "Should fail for non-shadow channel"); + + zassert_false(message_received, "No message should be received for non-shadow channel"); +} + +ZTEST(proxy_agent_test, test_message_receiving_null_message) +{ + zassert_true(mock_backend_has_recv_callback(), "Receive callback should be stored"); + + int ret = mock_backend_get_stored_recv_cb()(NULL); + + zassert_equal(ret, -EINVAL, "Should fail for NULL message"); +} + +ZTEST(proxy_agent_test, test_message_receiving_max_size) +{ + zassert_true(mock_backend_has_recv_callback(), "Receive callback should be stored"); + + struct zbus_proxy_agent_msg recv_msg; + uint8_t pattern_data[CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE]; + + for (size_t i = 0; i < sizeof(pattern_data); i++) { + pattern_data[i] = (uint8_t)(i & 0xFF); + } + + mock_backend_create_test_message(&recv_msg, "test_shadow_channel_2", pattern_data, + sizeof(pattern_data)); + + /* Simulate receiving the message via callback */ + int ret = mock_backend_get_stored_recv_cb()(&recv_msg); + + zassert_equal(ret, 0, "Should handle maximum size message"); + + zassert_true(message_received, "Max size message should be received on shadow channel"); + zassert_not_null(last_published_channel, "Published channel should be tracked"); + zassert_str_equal(last_published_channel->name, "test_shadow_channel_2", + "Should publish to correct shadow channel"); +} + +static void test_setup(void *fixture) +{ + RESET_FAKE(mock_backend_init); + RESET_FAKE(mock_backend_send); + RESET_FAKE(mock_backend_set_recv_cb); + RESET_FAKE(mock_backend_set_ack_cb); + + mock_backend_init_fake.return_val = 0; + mock_backend_send_fake.return_val = 0; + mock_backend_set_recv_cb_fake.return_val = 0; + mock_backend_set_ack_cb_fake.return_val = 0; + message_received = false; + last_published_channel = NULL; + + mock_backend_set_auto_ack(true); +} + +static void test_teardown(void *fixture) +{ + /* Re-enable auto-ACK to clear any pending messages */ + mock_backend_set_auto_ack(true); + + /* Wait long enough for all messages to either be ACK'd or reach max attempts */ + k_sleep(K_MSEC(get_total_timeout(CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS + 1))); + + RESET_FAKE(mock_backend_init); + RESET_FAKE(mock_backend_send); + RESET_FAKE(mock_backend_set_recv_cb); + RESET_FAKE(mock_backend_set_ack_cb); +} + +ZTEST_SUITE(proxy_agent_test, NULL, NULL, test_setup, test_teardown, NULL); diff --git a/tests/subsys/zbus/multidomain/proxy_agent/src/zbus_multidomain_mock_backend.c b/tests/subsys/zbus/multidomain/proxy_agent/src/zbus_multidomain_mock_backend.c new file mode 100644 index 0000000000000..848fed21ecaa6 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/src/zbus_multidomain_mock_backend.c @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "zbus_multidomain_mock_backend.h" +#include +#include + +LOG_MODULE_REGISTER(mock_backend, LOG_LEVEL_DBG); + +/* Define FFF globals */ +DEFINE_FFF_GLOBALS; + +/* Define fake function instances */ +DEFINE_FAKE_VALUE_FUNC1(int, mock_backend_init, void *); +DEFINE_FAKE_VALUE_FUNC2(int, mock_backend_send, void *, struct zbus_proxy_agent_msg *); +DEFINE_FAKE_VALUE_FUNC2(int, mock_backend_set_recv_cb, void *, zbus_recv_cb_t); +DEFINE_FAKE_VALUE_FUNC3(int, mock_backend_set_ack_cb, void *, zbus_ack_cb_t, void *); + +/* Global state for auto-ACK functionality */ +bool mock_backend_auto_ack_enabled = true; +zbus_ack_cb_t mock_backend_stored_ack_cb; +void *mock_backend_stored_ack_user_data; + +/* Global state for receive callback */ +static zbus_recv_cb_t mock_backend_stored_recv_cb; + +/* Store a copy of the last sent message to avoid use-after-scope issues */ +static struct zbus_proxy_agent_msg last_sent_msg_copy; +static bool last_sent_msg_valid; + +/* Custom send implementation that provides auto-ACK */ +int mock_backend_send_with_auto_ack(void *config, struct zbus_proxy_agent_msg *msg) +{ + /* Create a copy of the message to avoid use-after-scope issues */ + if (msg) { + memcpy(&last_sent_msg_copy, msg, sizeof(last_sent_msg_copy)); + last_sent_msg_valid = true; + } + + LOG_DBG("Mock backend: Sending message ID %u on channel '%s'", msg ? msg->id : 0, + msg ? msg->channel_name : "NULL"); + + int ret = mock_backend_send(config, msg); + + /* If auto-ACK is enabled and ACK callback is stored, send ACK immediately */ + if (mock_backend_auto_ack_enabled && mock_backend_stored_ack_cb && msg) { + LOG_DBG("Auto-ACK: Sending immediate ACK for message ID %u", msg->id); + int ack_ret; + + ack_ret = mock_backend_stored_ack_cb(msg->id, mock_backend_stored_ack_user_data); + LOG_DBG("Auto-ACK: ACK callback returned %d", ack_ret); + } + + return ret; +} + +/* Custom ACK callback setter that stores the callback for auto-ACK use */ +int mock_backend_set_ack_cb_with_storage(void *config, zbus_ack_cb_t ack_cb, void *user_data) +{ + /* Store the callback and user data for auto-ACK functionality */ + mock_backend_stored_ack_cb = ack_cb; + mock_backend_stored_ack_user_data = user_data; + + LOG_DBG("Mock backend: Stored ACK callback %p with user data %p", ack_cb, user_data); + + return mock_backend_set_ack_cb(config, ack_cb, user_data); +} + +/* Helper function to enable/disable auto-ACK */ +void mock_backend_set_auto_ack(bool enabled) +{ + mock_backend_auto_ack_enabled = enabled; + LOG_DBG("Mock backend: Auto-ACK %s", enabled ? "enabled" : "disabled"); +} + +/* Helper function to manually send duplicate ACKs for testing */ +void mock_backend_send_duplicate_ack(uint32_t msg_id) +{ + if (mock_backend_stored_ack_cb) { + mock_backend_stored_ack_cb(msg_id, mock_backend_stored_ack_user_data); + + k_sleep(K_MSEC(1)); + + mock_backend_stored_ack_cb(msg_id, mock_backend_stored_ack_user_data); + } +} + +/* Custom receive callback setter that stores the callback */ +int mock_backend_set_recv_cb_with_storage(void *config, zbus_recv_cb_t recv_cb) +{ + if (!recv_cb) { + LOG_ERR("Invalid receive callback pointer"); + return -EINVAL; + } + + /* Store the callback for state management */ + mock_backend_stored_recv_cb = recv_cb; + + LOG_DBG("Mock backend: Stored receive callback %p", recv_cb); + + return mock_backend_set_recv_cb(config, recv_cb); +} + +/* Mock state management functions */ +zbus_recv_cb_t mock_backend_get_stored_recv_cb(void) +{ + return mock_backend_stored_recv_cb; +} + +void mock_backend_reset_callbacks(void) +{ + mock_backend_stored_recv_cb = NULL; + mock_backend_stored_ack_cb = NULL; + mock_backend_stored_ack_user_data = NULL; + last_sent_msg_valid = false; + LOG_DBG("Mock backend: All callbacks reset"); +} + +bool mock_backend_has_recv_callback(void) +{ + return mock_backend_stored_recv_cb; +} + +/* Get the copied message to avoid use-after-scope issues */ +struct zbus_proxy_agent_msg *mock_backend_get_last_sent_message(void) +{ + return last_sent_msg_valid ? &last_sent_msg_copy : NULL; +} + +/* Test helper function to create messages */ +void mock_backend_create_test_message(struct zbus_proxy_agent_msg *msg, const char *channel_name, + const void *data, size_t data_size) +{ + if (!msg) { + LOG_ERR("Invalid message pointer"); + return; + } + + if (!channel_name || strlen(channel_name) == 0) { + LOG_ERR("Invalid channel name"); + return; + } + + if (!data && data_size > 0) { + LOG_ERR("Invalid data pointer with non-zero size"); + return; + } + + if (data_size > sizeof(msg->message_data)) { + LOG_ERR("Data size %zu exceeds maximum %zu", data_size, sizeof(msg->message_data)); + return; + } + + if (strlen(channel_name) >= sizeof(msg->channel_name)) { + LOG_ERR("Channel name too long: %zu >= %zu", strlen(channel_name), + sizeof(msg->channel_name)); + return; + } + + memset(msg, 0, sizeof(*msg)); + msg->type = ZBUS_PROXY_AGENT_MSG_TYPE_MSG; + msg->id = k_cycle_get_32(); + msg->message_size = data_size; + + if (data_size > 0) { + memcpy(msg->message_data, data, data_size); + } + + msg->channel_name_len = strlen(channel_name); + strncpy(msg->channel_name, channel_name, sizeof(msg->channel_name) - 1); + msg->channel_name[sizeof(msg->channel_name) - 1] = '\0'; + + LOG_DBG("Created test message for channel '%s' with %zu bytes", channel_name, data_size); +} diff --git a/tests/subsys/zbus/multidomain/proxy_agent/src/zbus_multidomain_mock_backend.h b/tests/subsys/zbus/multidomain/proxy_agent/src/zbus_multidomain_mock_backend.h new file mode 100644 index 0000000000000..63af9a9f97903 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/src/zbus_multidomain_mock_backend.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZBUS_MULTIDOMAIN_MOCK_BACKEND_H_ +#define ZBUS_MULTIDOMAIN_MOCK_BACKEND_H_ + +#include +#include +#include + +/* Define MOCK backend type as a simple token for macro concatenation */ +enum test_zbus_multidomain_type { + ZBUS_MULTIDOMAIN_TYPE_MOCK = 99 +}; + +typedef int (*zbus_recv_cb_t)(const struct zbus_proxy_agent_msg *); +typedef int (*zbus_ack_cb_t)(uint32_t, void *); + +/* Global state for auto-ACK functionality */ +extern bool mock_backend_auto_ack_enabled; +extern zbus_ack_cb_t mock_backend_stored_ack_cb; +extern void *mock_backend_stored_ack_user_data; + +DECLARE_FAKE_VALUE_FUNC1(int, mock_backend_init, void *); +DECLARE_FAKE_VALUE_FUNC2(int, mock_backend_send, void *, struct zbus_proxy_agent_msg *); +DECLARE_FAKE_VALUE_FUNC2(int, mock_backend_set_recv_cb, void *, zbus_recv_cb_t); +DECLARE_FAKE_VALUE_FUNC3(int, mock_backend_set_ack_cb, void *, zbus_ack_cb_t, void *); + +/* Custom implementations for auto-ACK and callback storage */ +int mock_backend_send_with_auto_ack(void *config, struct zbus_proxy_agent_msg *msg); +int mock_backend_set_ack_cb_with_storage(void *config, zbus_ack_cb_t ack_cb, void *user_data); +int mock_backend_set_recv_cb_with_storage(void *config, zbus_recv_cb_t recv_cb); + +/* Helper to enable/disable auto-ACK */ +void mock_backend_set_auto_ack(bool enabled); + +/* Helper to manually send duplicate ACKs for testing */ +void mock_backend_send_duplicate_ack(uint32_t msg_id); + +/* Mock state management functions */ +zbus_recv_cb_t mock_backend_get_stored_recv_cb(void); +void mock_backend_reset_callbacks(void); +bool mock_backend_has_recv_callback(void); + +/* Test helper functions */ +void mock_backend_create_test_message(struct zbus_proxy_agent_msg *msg, const char *channel_name, + const void *data, size_t data_size); + +/* Get the copied message to avoid use-after-scope issues */ +struct zbus_proxy_agent_msg *mock_backend_get_last_sent_message(void); + +static struct zbus_proxy_agent_api mock_backend_api __attribute__((unused)) = { + .backend_init = mock_backend_init, + .backend_send = mock_backend_send_with_auto_ack, + .backend_set_recv_cb = mock_backend_set_recv_cb_with_storage, + .backend_set_ack_cb = mock_backend_set_ack_cb_with_storage, +}; + +struct zbus_multidomain_mock_config { + char *nodeid; +}; + +/* Define the required macros for MOCK backend type */ +#define _ZBUS_GENERATE_BACKEND_CONFIG_ZBUS_MULTIDOMAIN_TYPE_MOCK(_name, _nodeid) \ + static struct zbus_multidomain_mock_config _name##_backend_config = {.nodeid = _nodeid}; + +#define _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_MOCK() (&mock_backend_api) +#define _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_MOCK(_name) ((void *)&_name##_backend_config) + +#endif /* ZBUS_MULTIDOMAIN_MOCK_BACKEND_H_ */ diff --git a/tests/subsys/zbus/multidomain/proxy_agent/testcase.yaml b/tests/subsys/zbus/multidomain/proxy_agent/testcase.yaml new file mode 100644 index 0000000000000..94b73e80a4288 --- /dev/null +++ b/tests/subsys/zbus/multidomain/proxy_agent/testcase.yaml @@ -0,0 +1,12 @@ +tests: + message_bus.zbus.multidomain.proxy_agent: + platform_exclude: + - m2gl025_miv + - mps3/corstone300/fvp + - mps3/corstone310/fvp + - mps4/corstone315/fvp + - mps4/corstone320/fvp + tags: zbus + timeout: 180 + integration_platforms: + - native_sim diff --git a/tests/subsys/zbus/multidomain/uart_backend/CMakeLists.txt b/tests/subsys/zbus/multidomain/uart_backend/CMakeLists.txt new file mode 100644 index 0000000000000..6a642b9590f1e --- /dev/null +++ b/tests/subsys/zbus/multidomain/uart_backend/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_channel_name) + +FILE(GLOB app_sources src/main.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/subsys/zbus/multidomain/uart_backend/app.overlay b/tests/subsys/zbus/multidomain/uart_backend/app.overlay new file mode 100644 index 0000000000000..d602739055c3d --- /dev/null +++ b/tests/subsys/zbus/multidomain/uart_backend/app.overlay @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/ { + euart0: uart-emul0 { + compatible = "zephyr,uart-emul"; + status = "okay"; + current-speed = <0>; + rx-fifo-size = <256>; + tx-fifo-size = <256>; + }; + euart1: uart-emul1 { + compatible = "zephyr,uart-emul"; + status = "okay"; + current-speed = <0>; + rx-fifo-size = <512>; + tx-fifo-size = <512>; + }; +}; diff --git a/tests/subsys/zbus/multidomain/uart_backend/prj.conf b/tests/subsys/zbus/multidomain/uart_backend/prj.conf new file mode 100644 index 0000000000000..2dc6f7cdd3253 --- /dev/null +++ b/tests/subsys/zbus/multidomain/uart_backend/prj.conf @@ -0,0 +1,31 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_LOG=y +CONFIG_CRC=y +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y + +CONFIG_ZBUS_MULTIDOMAIN=y +CONFIG_ZBUS_MULTIDOMAIN_UART=y +CONFIG_ZBUS_MULTIDOMAIN_LOG_LEVEL_DBG=n +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT=10 +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_ACK_TIMEOUT_MAX=100 +CONFIG_ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS=5 +CONFIG_ZBUS_MULTIDOMAIN_SENT_MSG_POOL_SIZE=16 + +CONFIG_ZTEST=y +CONFIG_ASSERT=n +CONFIG_ZTEST_MOCKING=y + +CONFIG_MAIN_STACK_SIZE=2048 +CONFIG_TEST_EXTRA_STACK_SIZE=2048 + +# IPC service support +CONFIG_EMUL=y +CONFIG_SERIAL=y +CONFIG_UART_ASYNC_API=y diff --git a/tests/subsys/zbus/multidomain/uart_backend/src/main.c b/tests/subsys/zbus/multidomain/uart_backend/src/main.c new file mode 100644 index 0000000000000..235af87dffcca --- /dev/null +++ b/tests/subsys/zbus/multidomain/uart_backend/src/main.c @@ -0,0 +1,510 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +LOG_MODULE_REGISTER(uart_backend_test, LOG_LEVEL_DBG); + +struct test_msg { + uint32_t id; + uint8_t channel_name[16]; + uint8_t channel_name_len; + uint8_t data[32]; + uint8_t data_len; +}; + +const struct device *uart_dev = DEVICE_DT_GET(DT_NODELABEL(euart0)); +uint8_t async_rx_buf[4][sizeof(struct test_msg)]; +volatile uint8_t async_rx_buf_idx; +struct test_msg received_msg; + +DEFINE_FFF_GLOBALS; + +FAKE_VALUE_FUNC1(int, fake_multidomain_backend_recv_cb, const struct zbus_proxy_agent_msg *); +FAKE_VALUE_FUNC2(int, fake_multidomain_backend_ack_cb, uint32_t, void *); + +void test_uart_dev_uart_callback(const struct device *dev, struct uart_event *evt, void *user_data) +{ + int ret; + + switch (evt->type) { + case UART_TX_DONE: + LOG_INF("TX done event\n"); + break; + + case UART_RX_RDY: + LOG_INF("RX ready event, received %zu bytes\n", evt->data.rx.len); + LOG_HEXDUMP_INF(evt->data.rx.buf, evt->data.rx.len, "Received data:"); + memcpy(&received_msg, evt->data.rx.buf, evt->data.rx.len); + break; + + case UART_RX_BUF_REQUEST: + LOG_INF("RX buffer request event\n"); + ret = uart_rx_buf_rsp(dev, async_rx_buf[async_rx_buf_idx], + sizeof(async_rx_buf[async_rx_buf_idx])); + if (ret < 0) { + LOG_ERR("Failed to provide RX buffer: %d", ret); + } else { + async_rx_buf_idx = (async_rx_buf_idx + 1) % 4; + LOG_INF("Provided RX buffer %d", async_rx_buf_idx); + } + break; + default: + break; + } +} + +void test_uart_dev_tx_callback(const struct device *dev, size_t size, void *user_data) +{ + LOG_INF("Tx buffer got appended %zu bytes\n", size); +} + +ZTEST(uart_backend, test_uart_dev) +{ + int ret; + + struct test_msg message = { + .id = 1, + .channel_name = "test_channel", + .channel_name_len = sizeof("test_channel"), + .data = "test_data", + .data_len = sizeof("test_data"), + }; + + zassert_not_null(uart_dev, "UART device is NULL"); + zassert_true(device_is_ready(uart_dev), "UART device is not ready"); + + ret = uart_rx_enable(uart_dev, async_rx_buf[async_rx_buf_idx], + sizeof(async_rx_buf[async_rx_buf_idx]), SYS_FOREVER_US); + zassert_equal(ret, 0, "Failed to enable UART RX: %d", ret); + + ret = uart_callback_set(uart_dev, test_uart_dev_uart_callback, NULL); + zassert_equal(ret, 0, "Failed to set UART callback: %d", ret); + + /* Set up emulator TX callback */ + uart_emul_callback_tx_data_ready_set(uart_dev, test_uart_dev_tx_callback, NULL); + + ret = uart_tx(uart_dev, (uint8_t *)&message, sizeof(message), SYS_FOREVER_US); + zassert_equal(ret, 0, "Failed to send message via UART: %d", ret); + + /* Wait for TX to complete */ + k_sleep(K_MSEC(1)); + + ret = uart_emul_get_tx_data(uart_dev, (uint8_t *)&received_msg, sizeof(received_msg)); + LOG_INF("Got %d bytes from emulator TX buffer", ret); + zassert_equal(ret, sizeof(message), "Received message size mismatch: %d", ret); + zassert_mem_equal(&received_msg, &message, sizeof(message), + "Received message content mismatch"); + + ret = uart_emul_put_rx_data(uart_dev, (uint8_t *)&message, sizeof(message)); + zassert_equal(ret, sizeof(message), "Failed to put RX data: %d", ret); + + /* Wait for RX callback to process the data */ + k_sleep(K_MSEC(1)); + zassert_mem_equal(&received_msg, &message, sizeof(message), + "Received message content mismatch"); + + message.id = 2; + ret = uart_emul_put_rx_data(uart_dev, (uint8_t *)&message, sizeof(message) - 10); + zassert_equal(ret, sizeof(message) - 10, "Failed to put RX data: %d", ret); + zassert_not_equal(received_msg.id, message.id, + "Received message should not be updated on partial data"); + + ret = uart_rx_disable(uart_dev); + zassert_equal(ret, 0, "Failed to disable UART RX: %d", ret); +} + +/* + * Generate a backend config for the test agent using euart1 + * Generates struct zbus_multidomain_uart_config _name##_uart_config (test_agent_uart_config) + */ +#define EUART1_NODE DT_NODELABEL(euart1) +_ZBUS_GENERATE_BACKEND_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent, EUART1_NODE); + +ZTEST(uart_backend, test_backend_macros) +{ + /* Verify the condfig generated by _ZBUS_GENERATE_BACKEND_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART + */ + zassert_not_null(test_agent_uart_config.dev, "UART device in config is NULL"); + zassert_equal_ptr(test_agent_uart_config.dev, DEVICE_DT_GET(DT_NODELABEL(euart1)), + "UART device in config does not match expected device"); + zassert_equal(test_agent_uart_config.async_rx_buf_idx, 0, + "Initial async_rx_buf_idx is not 0"); + zassert_equal(sizeof(test_agent_uart_config.async_rx_buf), + CONFIG_ZBUS_MULTIDOMAIN_UART_BUF_COUNT * sizeof(struct zbus_proxy_agent_msg), + "async_rx_buf size is incorrect"); + zassert_equal(sizeof(test_agent_uart_config.async_rx_buf[0]), + sizeof(struct zbus_proxy_agent_msg), "async_rx_buf[0] size is incorrect"); + + /* Test the macros for getting API and config */ + struct zbus_multidomain_uart_config *config; + const struct zbus_proxy_agent_api *api; + + /* Api from zbus_multidomain_uart.c */ + extern const struct zbus_proxy_agent_api zbus_multidomain_uart_api; + + api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_UART(); + zassert_not_null(api, "API macro returned NULL"); + zassert_equal_ptr(api, &zbus_multidomain_uart_api, "API macro returned incorrect API"); + + config = _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent); + zassert_not_null(config, "Config macro returned NULL"); + zassert_equal_ptr(config, &test_agent_uart_config, + "Config macro returned incorrect config"); +} + +ZTEST(uart_backend, test_backend_init) +{ + int ret; + struct zbus_multidomain_uart_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_UART(); + + ret = api->backend_init((void *)config); + zassert_equal(ret, 0, "Failed to initialize UART backend: %d", ret); + + ret = k_sem_take(&config->tx_busy_sem, K_NO_WAIT); + zassert_equal(ret, 0, "TX busy semaphore should be available after init"); + k_sem_give(&config->tx_busy_sem); + + ret = api->backend_init(NULL); + zassert_not_equal(ret, 0, + "Expected failure when initializing UART backend with NULL config"); + + ret = api->backend_init(NULL); + zassert_equal(ret, -EINVAL, "Expected error on NULL config"); +} + +ZTEST(uart_backend, test_backend_send) +{ + int ret; + struct zbus_multidomain_uart_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_UART(); + + uint8_t data_buf[sizeof(struct zbus_proxy_agent_msg) + 32]; + + struct zbus_proxy_agent_msg test_msg; + + ret = zbus_create_proxy_agent_msg(&test_msg, "test", 4, "chan", 4); + zassert_equal(ret, 0, "Failed to create proxy agent message: %d", ret); + + ret = uart_emul_get_tx_data(config->dev, data_buf, sizeof(data_buf)); + zassert_equal(ret, 0, "Emulator TX buffer should be empty before send"); + + ret = api->backend_send((void *)config, &test_msg); + zassert_equal(ret, 0, "Failed to send message via UART backend: %d", ret); + /* Wait for TX to complete */ + k_sleep(K_MSEC(1)); + + ret = uart_emul_get_tx_data(config->dev, data_buf, sizeof(data_buf)); + zassert_equal(ret, sizeof(test_msg), "Emulator TX buffer size mismatch: %d", ret); + zassert_mem_equal(data_buf, &test_msg, sizeof(test_msg), + "Emulator TX buffer content mismatch"); + zassert_equal(k_sem_count_get(&config->tx_busy_sem), 1, + "TX busy semaphore should be available after send"); + /* Test sending with NULL config */ + ret = api->backend_send(NULL, &test_msg); + zassert_equal(ret, -EINVAL, "Expected error on NULL config"); + + /* Test sending with NULL message */ + ret = api->backend_send((void *)config, NULL); + zassert_equal(ret, -EINVAL, "Expected error on NULL message"); + + /* Test sending with zero-length message */ + struct zbus_proxy_agent_msg empty_msg = {0}; + + ret = api->backend_send((void *)config, &empty_msg); + zassert_equal(ret, -EINVAL, "Expected error on zero-length message"); + + /* Test sending with too large message */ + struct zbus_proxy_agent_msg large_msg; + + ret = zbus_create_proxy_agent_msg(&large_msg, "too_large", + CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE + 1, "chan", 4); + zassert_equal(ret, -EINVAL, "Expected error on too large message"); + + /* Manually create a too large message to bypass the size check in + * zbus_create_proxy_agent_msg + */ + large_msg.type = ZBUS_PROXY_AGENT_MSG_TYPE_MSG; + large_msg.id = 43; + large_msg.message_size = CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE + 1; + large_msg.channel_name_len = 4; + strncpy(large_msg.channel_name, "chan", sizeof(large_msg.channel_name) - 1); + large_msg.channel_name[sizeof(large_msg.channel_name) - 1] = '\0'; + large_msg.crc32 = crc32_ieee((const uint8_t *)&large_msg, + sizeof(large_msg) - sizeof(large_msg.crc32)); + ret = api->backend_send((void *)config, &large_msg); + zassert_equal(ret, -EINVAL, "Expected error on too large message"); +} + +ZTEST(uart_backend, test_backend_set_recv_cb) +{ + int ret; + struct zbus_multidomain_uart_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_UART(); + + ret = api->backend_set_recv_cb((void *)config, fake_multidomain_backend_recv_cb); + zassert_equal(ret, 0, "Failed to set recv callback: %d", ret); + zassert_equal_ptr(config->recv_cb, fake_multidomain_backend_recv_cb, + "Recv callback not set correctly"); + + ret = api->backend_set_recv_cb(NULL, fake_multidomain_backend_recv_cb); + zassert_equal(ret, -EINVAL, "Expected error on NULL config"); + zassert_equal_ptr(config->recv_cb, fake_multidomain_backend_recv_cb, + "Recv callback should remain unchanged after NULL config"); + + ret = api->backend_set_recv_cb((void *)config, NULL); + zassert_equal(ret, -EINVAL, "Expected error on NULL callback"); + zassert_equal_ptr(config->recv_cb, fake_multidomain_backend_recv_cb, + "Recv callback should remain unchanged after NULL callback"); +} + +ZTEST(uart_backend, test_backend_set_ack_cb) +{ + int ret; + struct zbus_multidomain_uart_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_UART(); + + void *user_data = (void *)0x12345678; + + ret = api->backend_set_ack_cb((void *)config, fake_multidomain_backend_ack_cb, user_data); + zassert_equal(ret, 0, "Failed to set ACK callback: %d", ret); + zassert_equal_ptr(config->ack_cb, fake_multidomain_backend_ack_cb, + "ACK callback not set correctly"); + zassert_equal_ptr(config->ack_cb_user_data, user_data, "ACK user data not set correctly"); + + ret = api->backend_set_ack_cb(NULL, fake_multidomain_backend_ack_cb, user_data); + zassert_equal(ret, -EINVAL, "Expected error on NULL config"); + zassert_equal_ptr(config->ack_cb, fake_multidomain_backend_ack_cb, + "ACK callback should remain unchanged after NULL config"); + zassert_equal_ptr(config->ack_cb_user_data, user_data, + "ACK user data should remain unchanged after NULL config"); + + ret = api->backend_set_ack_cb((void *)config, NULL, user_data); + zassert_equal(ret, -EINVAL, "Expected error on NULL callback"); + zassert_equal_ptr(config->ack_cb, fake_multidomain_backend_ack_cb, + "ACK callback should remain unchanged after NULL callback"); + zassert_equal_ptr(config->ack_cb_user_data, user_data, + "ACK user data should remain unchanged after NULL callback"); +} + +ZTEST(uart_backend, test_backend_recv) +{ + int ret; + struct zbus_multidomain_uart_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_UART(); + uint8_t data_buf[sizeof(struct zbus_proxy_agent_msg) + 32] = {0}; + + struct zbus_proxy_agent_msg test_msg; + + ret = zbus_create_proxy_agent_msg(&test_msg, "test", 4, "chan", 4); + zassert_equal(ret, 0, "Failed to create proxy agent message: %d", ret); + + ret = uart_emul_put_rx_data(config->dev, (uint8_t *)&test_msg, sizeof(test_msg)); + zassert_equal(ret, sizeof(test_msg), "Failed to put RX data: %d", ret); + /* Wait for RX to be processed */ + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 0, + "Recv callback should not be called when not set"); + + ret = api->backend_set_recv_cb((void *)config, fake_multidomain_backend_recv_cb); + zassert_equal(ret, 0, "Failed to set recv callback: %d", ret); + + test_msg.id = 93; + test_msg.crc32 = + crc32_ieee((const uint8_t *)&test_msg, sizeof(test_msg) - sizeof(test_msg.crc32)); + uart_emul_put_rx_data(config->dev, (uint8_t *)&test_msg, sizeof(test_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 1, + "Recv callback should be called once"); + + zassert_mem_equal(fake_multidomain_backend_recv_cb_fake.arg0_val, &test_msg, + sizeof(test_msg), "Recv callback received incorrect message"); + /* Should be an ACK message in TX buffer */ + ret = uart_emul_get_tx_data(config->dev, data_buf, sizeof(struct zbus_proxy_agent_msg)); + zassert_equal(ret, sizeof(struct zbus_proxy_agent_msg), + "Emulator TX buffer size mismatch for ACK: %d", ret); + + struct zbus_proxy_agent_msg *ack_msg = (struct zbus_proxy_agent_msg *)data_buf; + + zassert_equal(ack_msg->type, ZBUS_PROXY_AGENT_MSG_TYPE_ACK, "ACK message type incorrect"); + zassert_equal(ack_msg->id, 93, "ACK message ID incorrect"); + zassert_equal(ack_msg->message_size, 0, "ACK message size should be 0"); + + /* Invalid messages */ + struct zbus_proxy_agent_msg invalid_msg = {0}; + + uart_emul_put_rx_data(config->dev, (uint8_t *)&invalid_msg, sizeof(invalid_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 1, + "Recv callback should not be called on NULL message"); + + /* Update message to verify new message received*/ + test_msg.id = 95; + test_msg.crc32 = + crc32_ieee((const uint8_t *)&test_msg, sizeof(test_msg) - sizeof(test_msg.crc32)); + /* + * Partial reception is expected to: + * - not trigger the callback + * - Corrupt the internal state so that the next valid message is also not processed. + * - Recover when next valid message is received + * + * Partial -> not processed + * Valid -> not processed, curupted by previous failure + * Valid -> processed + */ + uart_emul_put_rx_data(config->dev, (uint8_t *)&test_msg, 30); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 1, + "Recv callback should not be called on partial message"); + /* Should not be an ACK message in TX buffer */ + ret = uart_emul_get_tx_data(config->dev, data_buf, sizeof(struct zbus_proxy_agent_msg)); + zassert_equal(ret, 0, "Emulator TX buffer should be empty after partial RX: %d", ret); + + /* First valid message after partial will be cprupted by previous partial and dropped */ + uart_emul_put_rx_data(config->dev, (uint8_t *)&test_msg, + sizeof(struct zbus_proxy_agent_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 1, + "Recv callback should not be called on partial message"); + /* Should not be an ACK message in TX buffer */ + ret = uart_emul_get_tx_data(config->dev, data_buf, sizeof(struct zbus_proxy_agent_msg)); + zassert_equal(ret, 0, "Emulator TX buffer should be empty after partial RX: %d", ret); + + uart_emul_put_rx_data(config->dev, (uint8_t *)&test_msg, + sizeof(struct zbus_proxy_agent_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 2, + "Recv callback should be called again"); + /* Should be an ACK message in TX buffer */ + ret = uart_emul_get_tx_data(config->dev, data_buf, sizeof(struct zbus_proxy_agent_msg)); + zassert_equal(ret, sizeof(struct zbus_proxy_agent_msg), + "Emulator TX buffer size mismatch for ACK: %d", ret); + ack_msg = (struct zbus_proxy_agent_msg *)data_buf; + zassert_equal(ack_msg->type, ZBUS_PROXY_AGENT_MSG_TYPE_ACK, "ACK message type incorrect"); + zassert_equal(ack_msg->id, 95, "ACK message ID incorrect"); + zassert_equal(ack_msg->message_size, 0, "ACK message size should be 0"); + + struct zbus_proxy_agent_msg too_large_msg = { + .type = ZBUS_PROXY_AGENT_MSG_TYPE_MSG, + .id = 52, + .message_size = CONFIG_ZBUS_MULTIDOMAIN_MESSAGE_SIZE + 1, + .message_data = {'f', 'a', 'k', 'e', ' ', 'd', 'a', 't', 'a'}, + .channel_name_len = 4, + .channel_name = "test"}; + too_large_msg.crc32 = crc32_ieee((const uint8_t *)&too_large_msg, + sizeof(too_large_msg) - sizeof(too_large_msg.crc32)); + + uart_emul_put_rx_data(config->dev, (uint8_t *)&too_large_msg, sizeof(too_large_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 2, + "Recv callback should be called again"); + + /* Should work after multiple failed messages */ + /* Update message to verify new message received*/ + test_msg.id = 95; + test_msg.crc32 = + crc32_ieee((const uint8_t *)&test_msg, sizeof(test_msg) - sizeof(test_msg.crc32)); + uart_emul_put_rx_data(config->dev, (uint8_t *)&test_msg, sizeof(test_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 3, + "Recv callback should be called again"); + /* Should be an ACK message in TX buffer */ + ret = uart_emul_get_tx_data(config->dev, data_buf, sizeof(struct zbus_proxy_agent_msg)); + zassert_equal(ret, sizeof(struct zbus_proxy_agent_msg), + "Emulator TX buffer size mismatch for ACK: %d", ret); + ack_msg = (struct zbus_proxy_agent_msg *)data_buf; + zassert_equal(ack_msg->type, ZBUS_PROXY_AGENT_MSG_TYPE_ACK, "ACK message type incorrect"); + zassert_equal(ack_msg->id, 95, "ACK message ID incorrect"); + zassert_equal(ack_msg->message_size, 0, "ACK message size should be 0"); + + /* Unknown message type */ + /* Update message to verify new message received*/ + test_msg.id = 96; + test_msg.type = 99; + test_msg.crc32 = + crc32_ieee((const uint8_t *)&test_msg, sizeof(test_msg) - sizeof(test_msg.crc32)); + uart_emul_put_rx_data(config->dev, (uint8_t *)&test_msg, sizeof(test_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_recv_cb_fake.call_count, 3, + "Recv callback should not be called"); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 0, + "ack callback should not be called"); +} + +ZTEST(uart_backend, test_backend_ack) +{ + int ret; + struct zbus_multidomain_uart_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent); + const struct zbus_proxy_agent_api *api = _ZBUS_GET_API_ZBUS_MULTIDOMAIN_TYPE_UART(); + struct zbus_proxy_agent_msg ack_msg; + + ret = zbus_create_proxy_agent_ack_msg(&ack_msg, 142); + + ret = api->backend_init((void *)config); + + uart_emul_put_rx_data(config->dev, (uint8_t *)&ack_msg, sizeof(ack_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 0, + "ACK callback should not be called when not set"); + + ret = api->backend_set_ack_cb((void *)config, fake_multidomain_backend_ack_cb, + (void *)0x12345678); + zassert_equal(ret, 0, "Failed to set ACK callback: %d", ret); + + uart_emul_put_rx_data(config->dev, (uint8_t *)&ack_msg, sizeof(ack_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 1, + "ACK callback should be called once"); + zassert_equal(fake_multidomain_backend_ack_cb_fake.arg0_val, 142, + "ACK callback received incorrect message ID"); + zassert_equal_ptr(fake_multidomain_backend_ack_cb_fake.arg1_val, (void *)0x12345678, + "ACK callback received incorrect user data"); + + fake_multidomain_backend_ack_cb_fake.return_val = -1; + uart_emul_put_rx_data(config->dev, (uint8_t *)&ack_msg, sizeof(ack_msg)); + k_sleep(K_MSEC(1)); + zassert_equal(fake_multidomain_backend_ack_cb_fake.call_count, 2, + "ACK callback should be called again"); + fake_multidomain_backend_ack_cb_fake.return_val = 0; +} + +static void test_teardown(void *fixture) +{ + uart_emul_flush_rx_data(uart_dev); + uart_emul_flush_tx_data(uart_dev); + + struct zbus_multidomain_uart_config *config = + _ZBUS_GET_CONFIG_ZBUS_MULTIDOMAIN_TYPE_UART(test_agent); + uart_emul_flush_rx_data(config->dev); + uart_emul_flush_tx_data(config->dev); + + memset(config->async_rx_buf, 0, sizeof(config->async_rx_buf)); + config->async_rx_buf_idx = 0; + k_sem_reset(&config->tx_busy_sem); + k_sem_give(&config->tx_busy_sem); + + RESET_FAKE(fake_multidomain_backend_recv_cb); + RESET_FAKE(fake_multidomain_backend_ack_cb); +} + +ZTEST_SUITE(uart_backend, NULL, NULL, NULL, test_teardown, NULL); diff --git a/tests/subsys/zbus/multidomain/uart_backend/testcase.yaml b/tests/subsys/zbus/multidomain/uart_backend/testcase.yaml new file mode 100644 index 0000000000000..e56c4ce4903fa --- /dev/null +++ b/tests/subsys/zbus/multidomain/uart_backend/testcase.yaml @@ -0,0 +1,22 @@ +tests: + message_bus.zbus.multidomain.uart_backend: + platform_exclude: + - mps3/corstone300/fvp + - mps3/corstone310/fvp + - mps4/corstone315/fvp + - mps4/corstone320/fvp + tags: zbus + timeout: 180 + integration_platforms: + - native_sim + harness: console + harness_config: + type: multi_line + ordered: false + regex: + - ".* ACK callback not set, dropping ACK" + - ".* Failed to process received ACK: -1" + - ".* Receive callback not set, dropping message" + - ".* Received message with invalid CRC, dropping" + - ".* Invalid message size: .*" + - ".* Unknown message type: 99" diff --git a/tests/subsys/zbus/shadow_channels/CMakeLists.txt b/tests/subsys/zbus/shadow_channels/CMakeLists.txt new file mode 100644 index 0000000000000..a6d9bb5be6157 --- /dev/null +++ b/tests/subsys/zbus/shadow_channels/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_shadow_channels) + +FILE(GLOB app_sources src/main.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/subsys/zbus/shadow_channels/prj.conf b/tests/subsys/zbus/shadow_channels/prj.conf new file mode 100644 index 0000000000000..22a7fd4fadc4f --- /dev/null +++ b/tests/subsys/zbus/shadow_channels/prj.conf @@ -0,0 +1,15 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: Apache-2.0 +# + +CONFIG_ZTEST=y +CONFIG_ASSERT=n +CONFIG_LOG=y +CONFIG_CRC=y +CONFIG_ZBUS=y +CONFIG_ZBUS_CHANNEL_ID=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y +CONFIG_ZBUS_MULTIDOMAIN=y diff --git a/tests/subsys/zbus/shadow_channels/src/main.c b/tests/subsys/zbus/shadow_channels/src/main.c new file mode 100644 index 0000000000000..cc021bf255941 --- /dev/null +++ b/tests/subsys/zbus/shadow_channels/src/main.c @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +struct msg { + int x; +}; + +enum channel_ids { + CHAN_B = 123, + CHAN_D = 125, +}; + +/* Normal channels */ +ZBUS_CHAN_DEFINE(chan_a, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); +ZBUS_CHAN_DEFINE_WITH_ID(chan_b, CHAN_B, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +/* Shadow channels */ +ZBUS_SHADOW_CHAN_DEFINE(chan_c, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); +ZBUS_SHADOW_CHAN_DEFINE_WITH_ID(chan_d, CHAN_D, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, + ZBUS_MSG_INIT(0)); + +/* Multidomain channels */ +ZBUS_MULTIDOMAIN_CHAN_DEFINE(chan_e, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0), + true, true); +ZBUS_MULTIDOMAIN_CHAN_DEFINE(chan_f, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0), + false, true); +ZBUS_MULTIDOMAIN_CHAN_DEFINE(chan_g, struct msg, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0), + false, false); + +ZTEST(shadow_channels, test_shadow_channel_identification) +{ + /* Check shadow channel identification */ + zassert_false(ZBUS_CHANNEL_IS_SHADOW(&chan_a)); + zassert_false(ZBUS_CHANNEL_IS_SHADOW(&chan_b)); + + zassert_true(ZBUS_CHANNEL_IS_SHADOW(&chan_c)); + zassert_true(ZBUS_CHANNEL_IS_SHADOW(&chan_d)); + + zassert_false(ZBUS_CHANNEL_IS_SHADOW(&chan_e)); + zassert_true(ZBUS_CHANNEL_IS_SHADOW(&chan_f)); + + /* Check master channel identification */ + zassert_true(ZBUS_CHANNEL_IS_MASTER(&chan_a)); + zassert_true(ZBUS_CHANNEL_IS_MASTER(&chan_b)); + + zassert_false(ZBUS_CHANNEL_IS_MASTER(&chan_c)); + zassert_false(ZBUS_CHANNEL_IS_MASTER(&chan_d)); + + zassert_true(ZBUS_CHANNEL_IS_MASTER(&chan_e)); + zassert_false(ZBUS_CHANNEL_IS_MASTER(&chan_f)); +} + +ZTEST(shadow_channels, test_shadow_channel_exclusion) +{ + /* NOTE: chan_g should not be defined as _is_included in the macro is false + * Therefore, zbus_chan_from_name("chan_g") should return NULL + */ + zassert_is_null(zbus_chan_from_name("chan_g")); +} + +ZTEST(shadow_channels, test_pub) +{ + struct msg msg = {42}; + + /* normal publish cannot be used on shadow channels */ + zassert_equal(-EPERM, zbus_chan_pub(&chan_c, &msg, K_NO_WAIT)); + zassert_equal(-EPERM, zbus_chan_pub(&chan_d, &msg, K_NO_WAIT)); + zassert_equal(-EPERM, zbus_chan_pub(&chan_f, &msg, K_NO_WAIT)); + + /* shadow publish can be used on shadow channels */ + zassert_equal(0, zbus_chan_pub_shadow(&chan_c, &msg, K_NO_WAIT)); + zassert_equal(0, zbus_chan_pub_shadow(&chan_d, &msg, K_NO_WAIT)); + zassert_equal(0, zbus_chan_pub_shadow(&chan_f, &msg, K_NO_WAIT)); + + /* shadow publish cannot be used on normal channels */ + zassert_equal(-EPERM, zbus_chan_pub_shadow(&chan_a, NULL, K_NO_WAIT)); + zassert_equal(-EPERM, zbus_chan_pub_shadow(&chan_b, NULL, K_NO_WAIT)); + zassert_equal(-EPERM, zbus_chan_pub_shadow(&chan_e, NULL, K_NO_WAIT)); +} + +ZTEST_SUITE(shadow_channels, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/subsys/zbus/shadow_channels/testcase.yaml b/tests/subsys/zbus/shadow_channels/testcase.yaml new file mode 100644 index 0000000000000..4a2d773286a64 --- /dev/null +++ b/tests/subsys/zbus/shadow_channels/testcase.yaml @@ -0,0 +1,5 @@ +tests: + message_bus.zbus.shadow_channels: + tags: zbus + integration_platforms: + - native_sim