From 2dfb3e5ebf89db9e3fe3f63d70e7cf8205c2dce5 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 19 Aug 2025 10:48:14 +0200 Subject: [PATCH 01/15] zbus: add zbus_chan_from_name() function Add a new API function zbus_chan_from_name() that allows retrieving a zbus channel by its name string. This complements the existing zbus_chan_from_id() function and provides more flexibility for channel lookup operations. The function performs a linear search through all channels using STRUCT_SECTION_FOREACH and compares channel names using strcmp(). It returns NULL if no matching channel is found, maintaining consistency with the existing zbus_chan_from_id() API. The implementation is conditionally compiled when CONFIG_ZBUS_CHANNEL_NAME is enabled, ensuring it's only available when channel names are configured in the system. Signed-off-by: Trond F. Christiansen --- include/zephyr/zbus/zbus.h | 14 ++++++++++++++ subsys/zbus/zbus.c | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/include/zephyr/zbus/zbus.h b/include/zephyr/zbus/zbus.h index 4911fe298f369..da840ffccc4e3 100644 --- a/include/zephyr/zbus/zbus.h +++ b/include/zephyr/zbus/zbus.h @@ -690,6 +690,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/subsys/zbus/zbus.c b/subsys/zbus/zbus.c index 0fff534c2481f..7ff6d81645a2e 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) From 8fd053a768a45523394a23b623bcd5fcdec236f5 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 19 Aug 2025 11:09:42 +0200 Subject: [PATCH 02/15] zbus: add multidomain support infrastructure Add core multidomain support to ZBus, enabling communication between separate domains. This includes: - New shadow channel concept with is_shadow_channel flag - ZBUS_SHADOW_CHAN_DEFINE macros for defining shadow channels - ZBUS_MULTIDOMAIN_CHAN_DEFINE for conditional channel definitions - ZBUS_CHANNEL_IS_SHADOW macro for checking shadow status - ZBUS_CHANNEL_IS_MASTER macro for checking master status - zbus_chan_pub_shadow() for internal shadow channel publishing - Protection against publishing to shadow channels from application code - Configuration options for multidomain support Shadow channels represent channels defined in other domains, allowing observation without direct publishing. The multidomain macros enable shared channel definitions that create master channels in one domain and shadow channels in others. Signed-off-by: Trond F. Christiansen --- include/zephyr/zbus/zbus.h | 209 ++++++++++++++++++++++++++++++++++++- subsys/zbus/Kconfig | 8 ++ subsys/zbus/zbus.c | 63 +++++++++++ 3 files changed, 277 insertions(+), 3 deletions(-) diff --git a/include/zephyr/zbus/zbus.h b/include/zephyr/zbus/zbus.h index da840ffccc4e3..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 * diff --git a/subsys/zbus/Kconfig b/subsys/zbus/Kconfig index 2e418f99ff284..115bacb71a56e 100644 --- a/subsys/zbus/Kconfig +++ b/subsys/zbus/Kconfig @@ -107,6 +107,14 @@ 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. config HEAP_MEM_POOL_ADD_SIZE_ZBUS int "ZBus requested heap pool size." diff --git a/subsys/zbus/zbus.c b/subsys/zbus/zbus.c index 7ff6d81645a2e..28911970ba2db 100644 --- a/subsys/zbus/zbus.c +++ b/subsys/zbus/zbus.c @@ -403,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; } @@ -434,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"); From 9f775cbc1bdbeab7b88a2392fc933ae50bd667ac Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 19 Aug 2025 12:51:05 +0200 Subject: [PATCH 03/15] zbus: add multidomain types and proxy agent framework Add the type definitions and API framework for ZBus multidomain proxy agents. This provides the foundation for backend-agnostic communication between domains. - Define zbus_multidomain_type enum for different backends - Add zbus_proxy_agent_msg structure for inter-domain messages - Create zbus_proxy_agent_api for backend abstraction - Add zbus_proxy_agent_config for agent configuration - Implement core proxy agent functions (init, send, thread) - Add retransmission logic - Add macro system for backend-specific configuration generation The proxy agent framework allows different communication backends (UART, IPC, etc.) to be plugged in using a common API interface. Signed-off-by: Trond F. Christiansen --- .../zbus/multidomain/zbus_multidomain.h | 182 +++++++ .../zbus/multidomain/zbus_multidomain_types.h | 224 ++++++++ subsys/zbus/CMakeLists.txt | 6 +- subsys/zbus/Kconfig | 6 + subsys/zbus/multidomain/CMakeLists.txt | 3 + subsys/zbus/multidomain/Kconfig | 73 +++ subsys/zbus/multidomain/zbus_multidomain.c | 484 ++++++++++++++++++ 7 files changed, 977 insertions(+), 1 deletion(-) create mode 100644 include/zephyr/zbus/multidomain/zbus_multidomain.h create mode 100644 include/zephyr/zbus/multidomain/zbus_multidomain_types.h create mode 100644 subsys/zbus/multidomain/CMakeLists.txt create mode 100644 subsys/zbus/multidomain/Kconfig create mode 100644 subsys/zbus/multidomain/zbus_multidomain.c diff --git a/include/zephyr/zbus/multidomain/zbus_multidomain.h b/include/zephyr/zbus/multidomain/zbus_multidomain.h new file mode 100644 index 0000000000000..344fdfd37b041 --- /dev/null +++ b/include/zephyr/zbus/multidomain/zbus_multidomain.h @@ -0,0 +1,182 @@ +/* + * 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 + +#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_types.h b/include/zephyr/zbus/multidomain/zbus_multidomain_types.h new file mode 100644 index 0000000000000..1eee7d08dc05a --- /dev/null +++ b/include/zephyr/zbus/multidomain/zbus_multidomain_types.h @@ -0,0 +1,224 @@ +/* + * 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 { +}; + +/** + * @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/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 115bacb71a56e..08d2186851c25 100644 --- a/subsys/zbus/Kconfig +++ b/subsys/zbus/Kconfig @@ -116,6 +116,12 @@ config ZBUS_MULTIDOMAIN 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." default 2048 if ZBUS_MSG_SUBSCRIBER_BUF_ALLOC_DYNAMIC && !ZBUS_RUNTIME_OBSERVERS_NODE_ALLOC_DYNAMIC diff --git a/subsys/zbus/multidomain/CMakeLists.txt b/subsys/zbus/multidomain/CMakeLists.txt new file mode 100644 index 0000000000000..9b8ed5dc0b86f --- /dev/null +++ b/subsys/zbus/multidomain/CMakeLists.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library_sources(zbus_multidomain.c) diff --git a/subsys/zbus/multidomain/Kconfig b/subsys/zbus/multidomain/Kconfig new file mode 100644 index 0000000000000..6425c8b408217 --- /dev/null +++ b/subsys/zbus/multidomain/Kconfig @@ -0,0 +1,73 @@ +# +# 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. + +module = ZBUS_MULTIDOMAIN +module-str = zbus_multidomain +source "subsys/logging/Kconfig.template.log_config" + +endmenu # ZBus multidomain support \ No newline at end of file diff --git a/subsys/zbus/multidomain/zbus_multidomain.c b/subsys/zbus/multidomain/zbus_multidomain.c new file mode 100644 index 0000000000000..357b05e48ea69 --- /dev/null +++ b/subsys/zbus/multidomain/zbus_multidomain.c @@ -0,0 +1,484 @@ +/* + * 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; + + /* Cancel the delayed work if not in the work queue context + * If we are in the work queue context, the work item is + * already being processed, and will finish naturally. + */ + if (k_current_get() != &k_sys_work_q.thread) { + struct k_work_sync sync; + + k_work_cancel_delayable_sync(&data->work, &sync); + } else { + /* Mark as NULL to prevent retransmission from work context */ + 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; +} From 1a9718e8daeb2dfa899b51711fb4e0e2873cec49 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 19 Aug 2025 13:09:59 +0200 Subject: [PATCH 04/15] zbus: add UART backend for multidomain communication Implement UART backend for ZBus multidomain communication, enabling message forwarding between domains over UART connections. - UART-specific configuration structure with async buffers - Error handling and UART recovery mechanisms - Integration with proxy agent framework via API structure - Configurable buffer count for UART operations - Schedule ACK sending as work to avoid blocking RX callback The UART backend uses async UART APIs for non-blocking communication and implements proper buffer cycling for continuous reception. Signed-off-by: Trond F. Christiansen --- .../zbus/multidomain/zbus_multidomain.h | 4 + .../zbus/multidomain/zbus_multidomain_types.h | 1 + .../zbus/multidomain/zbus_multidomain_uart.h | 103 ++++++ subsys/zbus/multidomain/CMakeLists.txt | 4 + subsys/zbus/multidomain/Kconfig | 21 +- subsys/zbus/multidomain/zbus_multidomain.c | 26 +- .../zbus/multidomain/zbus_multidomain_uart.c | 322 ++++++++++++++++++ 7 files changed, 474 insertions(+), 7 deletions(-) create mode 100644 include/zephyr/zbus/multidomain/zbus_multidomain_uart.h create mode 100644 subsys/zbus/multidomain/zbus_multidomain_uart.c diff --git a/include/zephyr/zbus/multidomain/zbus_multidomain.h b/include/zephyr/zbus/multidomain/zbus_multidomain.h index 344fdfd37b041..3fcddd641064e 100644 --- a/include/zephyr/zbus/multidomain/zbus_multidomain.h +++ b/include/zephyr/zbus/multidomain/zbus_multidomain.h @@ -15,6 +15,10 @@ #include #include +#if defined(CONFIG_ZBUS_MULTIDOMAIN_UART) +#include +#endif + #ifdef __cplusplus extern "C" { #endif diff --git a/include/zephyr/zbus/multidomain/zbus_multidomain_types.h b/include/zephyr/zbus/multidomain/zbus_multidomain_types.h index 1eee7d08dc05a..59a6788c04f2a 100644 --- a/include/zephyr/zbus/multidomain/zbus_multidomain_types.h +++ b/include/zephyr/zbus/multidomain/zbus_multidomain_types.h @@ -32,6 +32,7 @@ extern "C" { * Zbus setup. Each type corresponds to a different communication backend. */ enum zbus_multidomain_type { + ZBUS_MULTIDOMAIN_TYPE_UART }; /** 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/subsys/zbus/multidomain/CMakeLists.txt b/subsys/zbus/multidomain/CMakeLists.txt index 9b8ed5dc0b86f..19aec5aba8180 100644 --- a/subsys/zbus/multidomain/CMakeLists.txt +++ b/subsys/zbus/multidomain/CMakeLists.txt @@ -1,3 +1,7 @@ # 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() diff --git a/subsys/zbus/multidomain/Kconfig b/subsys/zbus/multidomain/Kconfig index 6425c8b408217..f90dc78e2e938 100644 --- a/subsys/zbus/multidomain/Kconfig +++ b/subsys/zbus/multidomain/Kconfig @@ -66,8 +66,27 @@ config ZBUS_MULTIDOMAIN_MAX_TRANSMIT_ATTEMPTS 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 + module = ZBUS_MULTIDOMAIN module-str = zbus_multidomain source "subsys/logging/Kconfig.template.log_config" -endmenu # ZBus multidomain support \ No newline at end of file +endmenu # ZBus multidomain support diff --git a/subsys/zbus/multidomain/zbus_multidomain.c b/subsys/zbus/multidomain/zbus_multidomain.c index 357b05e48ea69..e9c8567ca3bf5 100644 --- a/subsys/zbus/multidomain/zbus_multidomain.c +++ b/subsys/zbus/multidomain/zbus_multidomain.c @@ -69,16 +69,30 @@ static int zbus_proxy_agent_sent_ack_timeout_stop(struct zbus_proxy_agent_config struct zbus_proxy_agent_tracked_msg *data = (struct zbus_proxy_agent_tracked_msg *)buf->data; - /* Cancel the delayed work if not in the work queue context - * If we are in the work queue context, the work item is - * already being processed, and will finish naturally. - */ - if (k_current_get() != &k_sys_work_q.thread) { + /* 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 { - /* Mark as NULL to prevent retransmission from work context */ + /* In work queue context: Mark as NULL to prevent retransmission */ data->config = NULL; } 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, +}; From 4092eea8f89fa7f45c6c7ec525fcf93fff277287 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 19 Aug 2025 14:21:29 +0200 Subject: [PATCH 05/15] samples: zbus: add ZBus multidomain UART forwarder sample Add sample application demonstrating ZBus multidomain communication over UART between two devices. Shows practical usage of shadow channels and proxy agents for inter-domain message forwarding. The sample consists of two devices: - Device A: Acts as request publisher with master request_channel - Device B: Acts as request listener/responder with shadow request_channel Device A publishes periodic requests which are automatically forwarded via UART to Device B. Device B processes requests and sends responses back through the response_channel, demonstrating bidirectional multidomain zbus communication. Key features demonstrated: - ZBUS_MULTIDOMAIN_CHAN_DEFINE for shared conditional channel definitions - ZBUS_PROXY_AGENT_DEFINE for UART backend configuration - Shadow vs master channel behavior - Automatic message forwarding via proxy agents Signed-off-by: Trond F. Christiansen --- .../zbus/uart_forwarder/common/common.h | 44 +++++++++++ .../zbus/uart_forwarder/dev_a/CMakeLists.txt | 21 ++++++ .../subsys/zbus/uart_forwarder/dev_a/Kconfig | 7 ++ .../boards/nrf5340dk_nrf5340_cpuapp.overlay | 17 +++++ .../boards/nrf54l15dk_nrf54l15_cpuapp.overlay | 17 +++++ .../subsys/zbus/uart_forwarder/dev_a/prj.conf | 25 +++++++ .../zbus/uart_forwarder/dev_a/src/main.c | 72 ++++++++++++++++++ .../zbus/uart_forwarder/dev_b/CMakeLists.txt | 21 ++++++ .../subsys/zbus/uart_forwarder/dev_b/Kconfig | 7 ++ .../boards/nrf5340dk_nrf5340_cpuapp.overlay | 17 +++++ .../boards/nrf54l15dk_nrf54l15_cpuapp.overlay | 17 +++++ .../subsys/zbus/uart_forwarder/dev_b/prj.conf | 27 +++++++ .../zbus/uart_forwarder/dev_b/src/main.c | 73 +++++++++++++++++++ 13 files changed, 365 insertions(+) create mode 100644 samples/subsys/zbus/uart_forwarder/common/common.h create mode 100644 samples/subsys/zbus/uart_forwarder/dev_a/CMakeLists.txt create mode 100644 samples/subsys/zbus/uart_forwarder/dev_a/Kconfig create mode 100644 samples/subsys/zbus/uart_forwarder/dev_a/boards/nrf5340dk_nrf5340_cpuapp.overlay create mode 100644 samples/subsys/zbus/uart_forwarder/dev_a/boards/nrf54l15dk_nrf54l15_cpuapp.overlay create mode 100644 samples/subsys/zbus/uart_forwarder/dev_a/prj.conf create mode 100644 samples/subsys/zbus/uart_forwarder/dev_a/src/main.c create mode 100644 samples/subsys/zbus/uart_forwarder/dev_b/CMakeLists.txt create mode 100644 samples/subsys/zbus/uart_forwarder/dev_b/Kconfig create mode 100644 samples/subsys/zbus/uart_forwarder/dev_b/boards/nrf5340dk_nrf5340_cpuapp.overlay create mode 100644 samples/subsys/zbus/uart_forwarder/dev_b/boards/nrf54l15dk_nrf54l15_cpuapp.overlay create mode 100644 samples/subsys/zbus/uart_forwarder/dev_b/prj.conf create mode 100644 samples/subsys/zbus/uart_forwarder/dev_b/src/main.c 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; +} From ee1a1790af97eff2d786bb9d5a59914be9145514 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Wed, 20 Aug 2025 10:01:32 +0200 Subject: [PATCH 06/15] zbus: add IPC backend for multidomain communication Implement IPC backend for ZBus multidomain communication, enabling message forwarding between domains over IPC connections. - IPC-spesific configuration structures - Integration with proxy agent framework via API structure and macros - Schedule ACK sending as work to avoid blocking in interrupt context The IPC backend uses Zephyr's IPC service for inter-core communication with async operation and proper endpoint binding using semaphore synchronization for reliable zbus message forwarding. Signed-off-by: Trond F. Christiansen --- .../zbus/multidomain/zbus_multidomain.h | 3 + .../zbus/multidomain/zbus_multidomain_ipc.h | 108 +++++++ .../zbus/multidomain/zbus_multidomain_types.h | 3 +- subsys/zbus/multidomain/CMakeLists.txt | 4 + subsys/zbus/multidomain/Kconfig | 7 + .../zbus/multidomain/zbus_multidomain_ipc.c | 272 ++++++++++++++++++ 6 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 include/zephyr/zbus/multidomain/zbus_multidomain_ipc.h create mode 100644 subsys/zbus/multidomain/zbus_multidomain_ipc.c diff --git a/include/zephyr/zbus/multidomain/zbus_multidomain.h b/include/zephyr/zbus/multidomain/zbus_multidomain.h index 3fcddd641064e..f1055e4552e79 100644 --- a/include/zephyr/zbus/multidomain/zbus_multidomain.h +++ b/include/zephyr/zbus/multidomain/zbus_multidomain.h @@ -18,6 +18,9 @@ #if defined(CONFIG_ZBUS_MULTIDOMAIN_UART) #include #endif +#if defined(CONFIG_ZBUS_MULTIDOMAIN_IPC) +#include +#endif #ifdef __cplusplus extern "C" { 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 index 59a6788c04f2a..9f4e9dad88a2b 100644 --- a/include/zephyr/zbus/multidomain/zbus_multidomain_types.h +++ b/include/zephyr/zbus/multidomain/zbus_multidomain_types.h @@ -32,7 +32,8 @@ extern "C" { * Zbus setup. Each type corresponds to a different communication backend. */ enum zbus_multidomain_type { - ZBUS_MULTIDOMAIN_TYPE_UART + ZBUS_MULTIDOMAIN_TYPE_UART, + ZBUS_MULTIDOMAIN_TYPE_IPC }; /** diff --git a/subsys/zbus/multidomain/CMakeLists.txt b/subsys/zbus/multidomain/CMakeLists.txt index 19aec5aba8180..76c0603842c69 100644 --- a/subsys/zbus/multidomain/CMakeLists.txt +++ b/subsys/zbus/multidomain/CMakeLists.txt @@ -5,3 +5,7 @@ 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 index f90dc78e2e938..7cb4f604ee769 100644 --- a/subsys/zbus/multidomain/Kconfig +++ b/subsys/zbus/multidomain/Kconfig @@ -85,6 +85,13 @@ config ZBUS_MULTIDOMAIN_UART_BUF_COUNT 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" 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, +}; From 3728a50284c83ec4470b547de30744991b596498 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Wed, 20 Aug 2025 10:11:47 +0200 Subject: [PATCH 07/15] samples: zbus: add ZBus multidomain IPC forwarder sample Add sample application demonstrating ZBus multidomain communication over IPC between CPU cores. Shows practical usage of shadow channels and proxy agents for inter-core message forwarding. The sample consists of two applications: - CPUAPP: Acts as request publisher with master request_channel - CPURAD: Acts as request listener/responder with shadow request_channel CPUAPP publishes periodic requests which are automatically forwarded via IPC to CPURAD. CPURAD processes requests and sends responses back through the response_channel, demonstrating bidirectional multidomain zbus communication. Key features demonstrated: - ZBUS_MULTIDOMAIN_CHAN_DEFINE for shared conditional channel definitions - ZBUS_PROXY_AGENT_DEFINE for IPC backend configuration - Shadow vs master channel behavior across CPU cores - Automatic message forwarding via proxy agents - Sysbuild configuration for multi-core applications Supports nRF5340DK and nRF54H20DK platforms with appropriate device tree overlays for IPC configuration. Signed-off-by: Trond F. Christiansen --- .../subsys/zbus/ipc_forwarder/CMakeLists.txt | 20 +++++ samples/subsys/zbus/ipc_forwarder/Kconfig | 7 ++ .../zbus/ipc_forwarder/Kconfig.sysbuild | 39 ++++++++++ .../boards/nrf5340dk_nrf5340_cpuapp.conf | 7 ++ .../boards/nrf54h20dk_nrf54h20_cpuapp.overlay | 15 ++++ ...4h20dk_nrf54h20_cpuapp_cpuflpr_xip.overlay | 38 ++++++++++ .../nrf54h20dk_nrf54h20_cpuapp_cpuppr.overlay | 35 +++++++++ ...54h20dk_nrf54h20_cpuapp_cpuppr_xip.overlay | 37 ++++++++++ .../subsys/zbus/ipc_forwarder/common/common.h | 44 +++++++++++ samples/subsys/zbus/ipc_forwarder/prj.conf | 27 +++++++ .../zbus/ipc_forwarder/remote/CMakeLists.txt | 22 ++++++ .../subsys/zbus/ipc_forwarder/remote/Kconfig | 7 ++ .../boards/nrf5340dk_nrf5340_cpunet.overlay | 5 ++ .../nrf54h20dk_nrf54h20_cpuflpr.overlay | 23 ++++++ .../nrf54h20dk_nrf54h20_cpuflpr_xip.overlay | 23 ++++++ .../boards/nrf54h20dk_nrf54h20_cpuppr.overlay | 23 ++++++ .../nrf54h20dk_nrf54h20_cpuppr_xip.overlay | 23 ++++++ .../subsys/zbus/ipc_forwarder/remote/prj.conf | 32 ++++++++ .../zbus/ipc_forwarder/remote/src/main.c | 73 +++++++++++++++++++ samples/subsys/zbus/ipc_forwarder/src/main.c | 72 ++++++++++++++++++ .../subsys/zbus/ipc_forwarder/sysbuild.cmake | 46 ++++++++++++ 21 files changed, 618 insertions(+) create mode 100644 samples/subsys/zbus/ipc_forwarder/CMakeLists.txt create mode 100644 samples/subsys/zbus/ipc_forwarder/Kconfig create mode 100644 samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild create mode 100644 samples/subsys/zbus/ipc_forwarder/boards/nrf5340dk_nrf5340_cpuapp.conf create mode 100644 samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuflpr_xip.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/boards/nrf54h20dk_nrf54h20_cpuapp_cpuppr_xip.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/common/common.h create mode 100644 samples/subsys/zbus/ipc_forwarder/prj.conf create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/CMakeLists.txt create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/Kconfig create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/boards/nrf5340dk_nrf5340_cpunet.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuflpr.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuflpr_xip.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuppr.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/boards/nrf54h20dk_nrf54h20_cpuppr_xip.overlay create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/prj.conf create mode 100644 samples/subsys/zbus/ipc_forwarder/remote/src/main.c create mode 100644 samples/subsys/zbus/ipc_forwarder/src/main.c create mode 100644 samples/subsys/zbus/ipc_forwarder/sysbuild.cmake 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..caec012e13878 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild @@ -0,0 +1,39 @@ +# +# 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" + +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" + +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 \ No newline at end of file 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/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..d375ab8575067 --- /dev/null +++ b/samples/subsys/zbus/ipc_forwarder/sysbuild.cmake @@ -0,0 +1,46 @@ +# +# 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} +) + +# 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) From e95fbc16adf76ac794135edf49c9aac7053c244c Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 9 Sep 2025 13:39:13 +0200 Subject: [PATCH 08/15] tests: zbus: add channel name test suite Add tests to verify the zbus_chan_from_name() functionality when CONFIG_ZBUS_CHANNEL_NAME is enabled. Signed-off-by: Trond F. Christiansen --- tests/subsys/zbus/channel_name/CMakeLists.txt | 13 +++++++ tests/subsys/zbus/channel_name/prj.conf | 11 ++++++ tests/subsys/zbus/channel_name/src/main.c | 39 +++++++++++++++++++ tests/subsys/zbus/channel_name/testcase.yaml | 5 +++ 4 files changed, 68 insertions(+) create mode 100644 tests/subsys/zbus/channel_name/CMakeLists.txt create mode 100644 tests/subsys/zbus/channel_name/prj.conf create mode 100644 tests/subsys/zbus/channel_name/src/main.c create mode 100644 tests/subsys/zbus/channel_name/testcase.yaml 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 From 60c0c061bb9523971d7010a9f3f74b39d54889e3 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 9 Sep 2025 13:54:16 +0200 Subject: [PATCH 09/15] tests: zbus: add shadow channel test suite Add test suite to verify the shadow channel functionality when CONFIG_ZBUS_MULTIDOMAIN is enabled. Signed-off-by: Trond F. Christiansen --- .../zbus/shadow_channels/CMakeLists.txt | 13 +++ tests/subsys/zbus/shadow_channels/prj.conf | 15 ++++ tests/subsys/zbus/shadow_channels/src/main.c | 89 +++++++++++++++++++ .../subsys/zbus/shadow_channels/testcase.yaml | 5 ++ 4 files changed, 122 insertions(+) create mode 100644 tests/subsys/zbus/shadow_channels/CMakeLists.txt create mode 100644 tests/subsys/zbus/shadow_channels/prj.conf create mode 100644 tests/subsys/zbus/shadow_channels/src/main.c create mode 100644 tests/subsys/zbus/shadow_channels/testcase.yaml 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 From 3807c25bb140ee5fce09eac7f94cf780123a98e8 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 9 Sep 2025 14:05:15 +0200 Subject: [PATCH 10/15] tests: zbus: add multidomain proxy agent test suite Add comprehensive test suite for the zbus multidomain proxy agent functionality. Implements a mock backend to enable isolated testing without requiring actual hardware communication. Signed-off-by: Trond F. Christiansen --- .../multidomain/proxy_agent/CMakeLists.txt | 13 + .../qemu_cortex_a53_qemu_cortex_a53_smp.conf | 8 + .../qemu_riscv32_qemu_virt_riscv32_smp.conf | 8 + .../qemu_riscv64_qemu_virt_riscv64_smp.conf | 8 + .../proxy_agent/boards/qemu_x86_64_atom.conf | 9 + .../zbus/multidomain/proxy_agent/prj.conf | 25 + .../zbus/multidomain/proxy_agent/src/main.c | 591 ++++++++++++++++++ .../src/zbus_multidomain_mock_backend.c | 177 ++++++ .../src/zbus_multidomain_mock_backend.h | 73 +++ .../multidomain/proxy_agent/testcase.yaml | 12 + 10 files changed, 924 insertions(+) create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/CMakeLists.txt create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_cortex_a53_qemu_cortex_a53_smp.conf create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_riscv32_qemu_virt_riscv32_smp.conf create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_riscv64_qemu_virt_riscv64_smp.conf create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/boards/qemu_x86_64_atom.conf create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/prj.conf create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/src/main.c create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/src/zbus_multidomain_mock_backend.c create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/src/zbus_multidomain_mock_backend.h create mode 100644 tests/subsys/zbus/multidomain/proxy_agent/testcase.yaml 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 From f70c63f41a45e9f9827804a7bfa0403114cccaa2 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 9 Sep 2025 14:10:05 +0200 Subject: [PATCH 11/15] tests: zbus: add multidomain IPC backend test suite Add comprehensive test suite for the zbus multidomain IPC backend. The implementation includes a mock IPC backend to enable isolated testing without requiring actual hardware communication between cores. Signed-off-by: Trond F. Christiansen --- .../multidomain/ipc_backend/CMakeLists.txt | 13 + .../zbus/multidomain/ipc_backend/app.overlay | 12 + .../ipc_backend/dts/bindings/fake-ipc.yaml | 3 + .../zbus/multidomain/ipc_backend/prj.conf | 27 + .../zbus/multidomain/ipc_backend/src/main.c | 640 ++++++++++++++++++ .../multidomain/ipc_backend/src/mock_ipc.c | 123 ++++ .../multidomain/ipc_backend/src/mock_ipc.h | 34 + .../multidomain/ipc_backend/testcase.yaml | 26 + 8 files changed, 878 insertions(+) create mode 100644 tests/subsys/zbus/multidomain/ipc_backend/CMakeLists.txt create mode 100644 tests/subsys/zbus/multidomain/ipc_backend/app.overlay create mode 100644 tests/subsys/zbus/multidomain/ipc_backend/dts/bindings/fake-ipc.yaml create mode 100644 tests/subsys/zbus/multidomain/ipc_backend/prj.conf create mode 100644 tests/subsys/zbus/multidomain/ipc_backend/src/main.c create mode 100644 tests/subsys/zbus/multidomain/ipc_backend/src/mock_ipc.c create mode 100644 tests/subsys/zbus/multidomain/ipc_backend/src/mock_ipc.h create mode 100644 tests/subsys/zbus/multidomain/ipc_backend/testcase.yaml 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" From 4a5e0e5c53a9403751662f38d0bb2dadc3ad111f Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 9 Sep 2025 14:13:50 +0200 Subject: [PATCH 12/15] tests: zbus: add multidomain UART backend test suite Add comprehensive test suite for the zbus multidomain UART backend. The implementation uses UART emulator devices to enable testing without requiring physical UART hardware. Signed-off-by: Trond F. Christiansen --- .../multidomain/uart_backend/CMakeLists.txt | 13 + .../zbus/multidomain/uart_backend/app.overlay | 22 + .../zbus/multidomain/uart_backend/prj.conf | 31 ++ .../zbus/multidomain/uart_backend/src/main.c | 510 ++++++++++++++++++ .../multidomain/uart_backend/testcase.yaml | 17 + 5 files changed, 593 insertions(+) create mode 100644 tests/subsys/zbus/multidomain/uart_backend/CMakeLists.txt create mode 100644 tests/subsys/zbus/multidomain/uart_backend/app.overlay create mode 100644 tests/subsys/zbus/multidomain/uart_backend/prj.conf create mode 100644 tests/subsys/zbus/multidomain/uart_backend/src/main.c create mode 100644 tests/subsys/zbus/multidomain/uart_backend/testcase.yaml 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..4f82043b129b2 --- /dev/null +++ b/tests/subsys/zbus/multidomain/uart_backend/testcase.yaml @@ -0,0 +1,17 @@ +tests: + message_bus.zbus.uart_backend: + 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" From b125b07d93812f46c2b436d6f6202a156e88b612 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Mon, 15 Sep 2025 13:46:43 +0200 Subject: [PATCH 13/15] samples: zbus: ipc_forwarder: Add support for nrf5340bsim target Adds support for running the Zbus IPC forwarder sample on the nrf5340bsim simulation platform to enable development and CI without physical hardware. Signed-off-by: Trond F. Christiansen --- samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild | 10 +++++++++- .../boards/nrf5340bsim_nrf5340_cpuapp.conf | 7 +++++++ samples/subsys/zbus/ipc_forwarder/sysbuild.cmake | 6 ++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 samples/subsys/zbus/ipc_forwarder/boards/nrf5340bsim_nrf5340_cpuapp.conf diff --git a/samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild b/samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild index caec012e13878..89a28dd9eebc8 100644 --- a/samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild +++ b/samples/subsys/zbus/ipc_forwarder/Kconfig.sysbuild @@ -10,6 +10,7 @@ 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" @@ -28,6 +29,12 @@ config REMOTE_BOARD_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 @@ -36,4 +43,5 @@ config REMOTE_BOARD 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 \ No newline at end of file + 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/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/sysbuild.cmake b/samples/subsys/zbus/ipc_forwarder/sysbuild.cmake index d375ab8575067..d9c54139175d2 100644 --- a/samples/subsys/zbus/ipc_forwarder/sysbuild.cmake +++ b/samples/subsys/zbus/ipc_forwarder/sysbuild.cmake @@ -36,6 +36,12 @@ ExternalZephyrProject_Add( 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) From e346675e15ac859fd7dfa9e669ac04ce05926492 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Mon, 15 Sep 2025 13:47:44 +0200 Subject: [PATCH 14/15] samples: zbus: ipc_forwarder: sample.yaml test definitions Adds sample.yaml with test definitions for the IPC forwarder sample, enabling testing using console harness with output validation. Signed-off-by: Trond F. Christiansen --- samples/subsys/zbus/ipc_forwarder/sample.yaml | 123 ++++++++++++++++++ .../multidomain/uart_backend/testcase.yaml | 7 +- 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 samples/subsys/zbus/ipc_forwarder/sample.yaml 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/tests/subsys/zbus/multidomain/uart_backend/testcase.yaml b/tests/subsys/zbus/multidomain/uart_backend/testcase.yaml index 4f82043b129b2..e56c4ce4903fa 100644 --- a/tests/subsys/zbus/multidomain/uart_backend/testcase.yaml +++ b/tests/subsys/zbus/multidomain/uart_backend/testcase.yaml @@ -1,5 +1,10 @@ tests: - message_bus.zbus.uart_backend: + 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: From 5fb5bc85acb659a49dc9c0d4e78bfad5071ac917 Mon Sep 17 00:00:00 2001 From: "Trond F. Christiansen" Date: Tue, 16 Sep 2025 14:29:04 +0200 Subject: [PATCH 15/15] doc: zbus: add documentation for multi-domain zbus Add documentation for the multi-domain zbus feature, and multi-domain zbus samples. Signed-off-by: Trond F. Christiansen --- doc/services/zbus/images/zbus_proxy_agent.png | Bin 0 -> 102849 bytes doc/services/zbus/index.rst | 164 +++++++++++++++++- samples/subsys/zbus/ipc_forwarder/README.rst | 87 ++++++++++ samples/subsys/zbus/uart_forwarder/README.rst | 85 +++++++++ 4 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 doc/services/zbus/images/zbus_proxy_agent.png create mode 100644 samples/subsys/zbus/ipc_forwarder/README.rst create mode 100644 samples/subsys/zbus/uart_forwarder/README.rst 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 0000000000000000000000000000000000000000..b6f2b91ceba1e39ff188c023e30009fb94ee3434 GIT binary patch literal 102849 zcmeFZWl&sQw=PN)2njC1g9mrFH$cz^f+x7UyLSkZ06_vl8Vw#ixI=;l_uvrR8@HyR z@8tdVu2c8?Ip^oCTYFWd($#dYwdR;Zp63~3b@&?=O77(^>wV8yXsR=iR@139Q)UXlO6c6l7j$dZ+Czc=;1r+@c>^c?zc5 z`!b2sWaY<>JW%|S?T#ly_#@*Hks2PGmGE$cZ}x?#D&Cu1d9{=%>@k{&5d1h?DPNiv z$I7#dp7}v>A4|`wrkRkaz{PT8#q}O<)v%+b=f<_>kToAZxQDw#j-hGt(SKf|aiIsX z{^z9--@~&1y!tzWTYq=kXlU=HrT^Y72^`qZ{^x`LvBdwF=l@qup)tR6|2>_pt#*>O z=7gXC3!$s3;Kj&KM;;iv2Sv@ZGr?jO^3(}MCMGA^0t)CvAot+%l?iWy@p?xE8kNT-qFolyU8mX{H zM0$23*AHXM(qHAg<{W44|5LVDe?lU{Adugaav@24;QZ?vs@m{X zwqW47*OXn1B!if^=kk0cb<41Q&CUTe{W)B^K%et>8?+ZIKfzA~I&$ia>(ZcW` zCr6t?o9|MHf)bU_{A@skL1W_CU@f&LrMPPg*OD<8#`ch+1xlh~yeArJ`{%|GPUQgK z{HL$iL()ky*0plWk~AHjul_7TDOti3X#|I@!*^I3N*nWh);iqijaGzKN;G{N8_>}o zo=5CR2yrae-abfBQOS>>xuWK_@K#4bTM_x8FjA&*4mRYW0VB8AOY6B!qm-GQ%%$>o zsD9P<@j=w3mHXRzq0~!_J;U45%(}Y58w{c7m+4Z1=l9%BV>^Vjsw8hIjjuWKMH-7%88czDcW4%W7fs}(+wjA28coAcWT zu+Hj%&poJf9mX$LQdEIA>!-Uj?qOM&;YQ83f|kWPlGkhh^eJd`39MhoB%VclrVT;* zUS708&p0~rW=hsDZ~OQbDPtcul8@yhZ^n1(iAk5(`yCNswDSju6lR0h#hI) zo*tUvK+?u6YsZr~C9W6eYWKDMDW0SyD0=Bkfz>ZA+j=a;jG^^u(0R zB09cQS+Up_h05Rv)?wB~`P+B5cs-*wDG~$esZMbUL!V!Fu;H{HvXW;9E3VPin7S@$ zp3m7F<_XUVT1fu-AboW^zR5Ak&k5O*BIAhApS|tuzgLV=^ov5+g zA}Gb7+DKUvo-y*26iC-*NNoEUzHc$7U@59+WvX+gGgGQ~JY^<^^htw%-*?ePqf7Or zt>;R|k?TZ6;Y+)RGvk)`rxxUJ>U<2hPDNZYQW`~$7I#v~x`bhz^f$f&@jY|Ww8_ne zu}&SZ>0Ip-9ALgY#q26ur$U$xtDSAhtPBrVZ9ks1p=(=io5a!SS2eHXUJdPc+y|3M z5G{0&7T_YD=2I1P2j!QpH)Ed>1B<+xsL+6`zds><-qnqpo(W#pn<-ek% zq5aj~CQJW7?}W~;yWdp;(MaVY8b&R|Xs2Q5ro-W(Cv6CmVDkyZ;-b!N4F^_;^z zSFbhRsm{p(>GWpemAmgvbntaNJiT#abH8@~)iFfOuWLTZX$4Zv@$dg$_sO6$^aH75mdC!m!V{ut- zmWr~a)uq=L^Cw8&{M)HJ|C9m#=^?V+zX@X{QFd^BT-Q&0eVv4=yXfS`;w66?#YDHQ)NzL>Be9O*9$f{+@kX8#3?DulWePhsexOVB4l!)`+UY;@< zVrKi{EU7%WgPz#@X6d)5{PvFK5Ql)1U&{+rbkBx|^?t&rb=uWR6@T!rsm+tTA1HDG;xEt zI%U<~-&rD`lH}hFoQwFiMLC&{n>}i)#L5Cfd!QKDa@%XE58;ss`{EHtKm09KE8$Td z!}6s;5?apfakm zBlAC|&V8$Y7#+*2!@;eEdBA}(y)8jcOX$O9!2$SGt5NnZh~?7(i*4d5;wVOTUTIs& zy4GT6{}{s*tkM)xNyC=oD3qBMj78FQHj*rvY4=C3_Y?|ICEFj!&bkzc;^e*MB~Sci zoP5$po9}lx{1KZgXg7jdkXzKn%VVKwrD-it0XZI)pYKP)ZVY$4$Pn^uc_GOt*6vSZ zwZOj}#gvJcVLAUBcCn+hSzg=7jM~1qY3O>bMw7MSRdakm{iy(H6XTX|_P}ngiNnh3 zCJ-?}DIGWWj=ljBc(y5yGzocGwSgC0ZCk zn#@WLHIYijJ#=rndj$fK4TqY=<2QC4jY&xXGB~tcWKk$Y;C|f!N1mX5%^A9-4#&`t zpN>CMnjMG;8>|cwZvNcb_5RZ3@c+&wIlsv_;=lSe%Gr z8E$QE+PuCBxr!Nm;9O*D2Nm`|Ts$HW$>sv#_GgFIAXK^f zYraw2;so1h#g3G0<63$}u=^X$-|MHD=5JV%`gPa0e=t0Wj(9A(AdK{+v1JJPZC;tj z$zgJAzOnd{jg=@vfRCS2B))y^-RX##)Py1i*j|Gpkazl*k^WonYb@a7;)nt$JIO`g zoX6genkHLG-ilY2@TjnS#^UqyE?Lj^Y0tP`^STS_dZ%?Vm(`ux8^4l;)FGLlJ{$&( zb%)zZV_Hhlxb=PJJR1oys1Lh#op?#l zBX6tz8yYCreBF75z|vuOte|*Kz_`W2wm#9N$!Ib5X%wQUDgWL3nJmKdU1h%Uv3%{6 zzKpZHe@uq`y|L;0Z)y2{q}F3CF^tE1cu?@(Ogbb}Zf-8&Y$OrZHN7bTczNZFJ^7qg zDcQz_h>0w!dz=v=^R4&?<;(>Jp`ZFhz>TFAgLr^rcNB$7%!)$xxZ6KVLaJU~`QDJW z@G@H7i^m#9e^>Uz+>i!;;62KDDNyPr#MC7D)SQ+O0Z1>&(0B-A6T1ysx~tyU`xhuQ zk**2mr)X#+S9ck})|JuJG%$ahQTC6pl#ahlzec{D+8FYzts=m&;B#|F5rh9Dg#ZK(Gtq=ww=ZXAjf=V_@kCyV5cZ1FJK?ChWY-%xKJw^!=U)YB#vE@v&Xf@p_yTvePx!}3 zAkk5CZjp6NV8zD;PT?tfGch~kf#fnl;${0Gw(2obwvcW z^GR}gKHGUby0he&YyOuUfgANrBN~`!XcTu9e$btb zaClhrorYOa+;bA+F!SKlHHx?=k3=iW!W-QZn#b{Ry6Hk*67KmM3oPYv6JQY#FdV!Y zu;y!ks0`;w+uI%#l@!j@pG=)6LOPY#SyZY$(Q;>6aqOK;G~5dC+=*&4qlNBwP4hu1>`PPwUFN5N0iCnOtko8bynKC=d)0Gm zLpxs$dtdCSonOS*R=t`SXT`d^Z;M0!eB_*M=U@Y6RQMCq_6}Gyw01Ds?1DMvQ~0)X zs-AAiY&eYfDD9|BE*4rH4K1=mFFvs6iX_(ufkJj0UFwmCgA5hbi1H{gYwWmUR9dv`|nLJd(KGI2HaZQw$hz0;bRdS?+G!i zudj4DA3%&ul&|5)!Yg->OAFs4@v)10ZUsQ)TjRab3CDZu}bUXs?E+i}=wBBNttrd(u2yE{B?P88x^ zvij&pTc@zZ&arWlAusi6vMA!M-PGF2#NODU@RgwHEvk@Muy>*{RQZ+fL_~*5M48G^ zXOeDtTc?QiRKe!!`A7l6l2xw$X>UL8^Qh>CwDc}XakndV{Q>CX(NXbp^sa7RI<(P? zUmSaZ56M1<38KzOX=E10^tIplT<+N%v;P7!Ct}ll0s6{TF0&gvS+|WyId9v6ck}l zQ2MG*gqR6~S8qw`H;708LO;S17`X}Ylk@QM@^GN6U-4{E67fGjU9Kt^H2;Jd;|*P= zRq|w*u&d4#qPmMO+%dt%$xqdJt zOc#D*cb)!xit}XSKomy)uHUMMG%i-?+X3paQ<9F3!(xZZ12;SFdj-HjiVixUqWe0N zf5!S7u_Kg>b@fMWmhLtzWpjDse4QL>1kh@6EvOv+sJFe6sYx!a48V6C3|kWt(=#uQ z>R2TZ8+ME=@E1dh>wkuC1BR!MR+64R?oi6~zwE1w5zV?IUVnDTf{N?A5^#Bv8_kF< zsk7w%90jp1^gx_Oyt*byetrhX%Gdhr;@7=<>8L*tza(7tPomfF7IjVvuc^Le237=7 z8hYDRu$QT*fXx-06=P=&KA8B2Gh}slorscni0wt8^b)$?)+f3VLK3UZy>#@AItdAO zwW;Y5r*1+GsFH-i?9c~~l;fSE0H&C6W>*;U7L&2*RyPmO@S6mqF6GSMCAf31^wZLz z$WtRMs${0C9j2vH-|cvu|EvHoe1p&~Gkr{&;1&EpQP)qZ%bQkX)-^WoPt6t{SvU6#l`Gjsw)Rjx}-IyhZGr>$lG4$ zG`O?h!LSobYK+DwbJB7YK@>gWJ_q)`BPjSrV&aD}{Ehi-igOXGIhzxBI;3kb zIjdJ&3&wD|cea6i6cP)VQFf|D8|Ced=5Cc}Tdp^f04MX>M2_i{`b)1l>S9)J`4uie>FMS0W&EV4@7S z-cwvdE+$~L*9wCMU0o#_x?+OJ3ocN#t=8>F6*!5V?E!JEwoM+uAHL=uE zjt8z9g;Z(27bN6USj?=o_KtGg`w*rl`aqn#@jW9J!OU#%4}Yd_wi-s-7mtnfgwhWii!~VdzaHrCNiz9ZlxdZ0eTFjNQs*wC?6yF z9AjF^4F;a3O;3`vzmhDlR-bf3NvZo(}jo{DBKetDEZD0sa zYgf&rs}MA7+_@`p&{9RZAh*>|mBMcLRuqo07Ntxzl)HzgylI9Rz1lo4e%Uqz^Z!n_ zMo7$6ngi0QU$^{If||aTRVKE;OHRm2n_%g|z^lI(s>#i}#ZH|lvfy8bf9)cHb|z9v z4+3+;1L)iHufE{o|6L_lPgPK{lERZcd|MWckB4i!%c^uZ4kSH3 z-0r4TynPCSY%cCGS9ntlW#YhmR;Qg=?Gtic9G+@h&$RmF47$fpZLN+(KCQuimneI* zyvS^wS!Yd_DOjd%EB^7zn9Gf9=b3Nt=t#zm*F4^D0`xMbado?$-I7^Mn$G@(97Lz^ ztGROEm|`>I-&+hdWYZso`3zt0^&`VFC~<^)y$Hhf76OKWPrkJDzY^Yv`QSp3Qkt)NZoDvqcaX^dX?k(xGE7`c!Lq zg)Q_%e|n&kPkl)}4D@SQgWwjBhclZT$xX^{(67+Bg6u4mYYwO3B+S9TViS`5Xz0X6 z*!-=fvSTHdYrm9BoUfr5>+)Cp;pd}}7Z&zevrg>?i#O&3i%&>P3&-UViZ;aCgw|Og zL%1Q9RBk?k~ep6DR~e=@BE?RLKxnKL29L;|>zx zNs$j_z{BHiMjWgAQ{p`_5moPQd9$bLioH%tE0^nxlE2OnFfy-cBW+=u+ueadHD_n- zW_|{_K-OL|xb3koeMqKUXFilJ)Zs? zjyAhm<5L}KM8zLylR|X(} z*CcI}?Jnad*M~q<8HXFeWc2<81;ZIY#VktQN<94Sr9<4|>x2 zuNb-*Be6kh4hP7O#V{lo`ga??C|cwPvZn#d@xJ z+Wf1|la;5@s>UhGu1~NW35lMlUd_0$QJbr2lLA-$&n&>SINx5`a@|3t4V8|9AS-KU zI1){>f3$+(#sMdrkvJdIAoO*ydQF6yaP;oRrV&s&R7#5Cg$TL|vNDtEDCb%mBUFU$ zW71%X`#p+iEIE(}48hcAP+cyXogMi{zcOsl$mSK1H3pw{n@>%A@-ux6!l+m*$@JiRfJ>&f_{ zwxaB)tsU_|2ceQO#ah|awSI%+BK-z*EW#&Favndc@YJcrtMl5w+&upKUi%`+2vxrg zglp5y_cm9gN5cwphf8|L&ASVY*; zF!iyNlsc~mIGIh=fl58gfuL5|yaPE=q;btQ7f$Xga5Fi>z*9^Sph8NlIPkhTg*^5& z!!I{CszNSMhICd#;tOV6PTMBf|Hc@Ab~#c~+ZT=g?8QJH(%U6_d(>%@4EvAt zkv!2k@mxphS$587_(o4Xk1~3*LH4@Vd72Olyc#xLT;3q%^~e>LM!G74A?z*^s?f{I zf$t@iW0nAN{fc7{b2bqrkUu#M`pxls-nRV=07Cyte*40C0(3l*j1I2pWexbOH^fdw08|UO1ZIC7z3tYFsh({iLHlwJUgQC?Z~e`^X|t^mErQ%6i1k(R*qKh(*}9F`T5qiPeCV73LV?#&%N zp3`Sya#I!uK%$;XJhT9!fitc(y)?(fkw5AO-QV$#HhF3y43ZGjy2cb zOpnSo$I7_`Q!vFD;B2dYl|7R)tVx|S1F`*Us`H|Avn%0+dMVV1UDFNdD-4)~XS3Gz z7yV8xu9u181nBB#as!=JE86L5!5?{;-}y9TmZPFo?j&l!qXh(!9lOYU1>ZRDGgH;A{1`3Z)VFZJhWgNo^Jm9>l?u95;RoMe5>#0j^Zz>$?r=RL@UfTf zz*zlG1iKh7*!sGMY!2kj7n3+?g=d)n=@~HgS-+L%Ra&lX^mC2A(aKb1JO;U* zKLhkh5o2`6{k4U~$D#a@i~8nIe`B}JO0e`9NL+|OVw`r%)^gi2jue%^m110!=o?>1 zCsn=}W1^-KN#`;BYYc-Vx^ka^zG-slyn#W%C+yc1yN|A%Zg+Wmlkyk1RVp*9=ynI_ zxOnLK7{xxk^R>FD=os($oTIS;$>^J)m=qLX9B#P?hh21C>@9rPrWW$>@A-9;*v46U zvsp4Iuy0ymp)Mi}6G_CN@mdrx?R@_#-*B-0ONZAYup!apBy-YOU#@IM+u`3FNJI;V z`o(9;gM)o(uS}a!>u%)wucHpfNX*FJ9KYuPT&q&BHm>8AwlCCs_+P?Tip`FTi)Zw~ z=q~7B#7cR+p(lP?=I48!Har#3!&a^i`pA~t_@FUXU^NW()DAjB16v#%UV3$ZD*5nJ z+q~OBk%Qz|c$6(aFN%J!YsTkS8ak{KyVt$f3Zh%w~S@7n)t zV@Wu=s#)xA?8DbKy9`bpX+(hYk=JY+OR4c=76KI;aWe+JYeS_E6 zNdnH)F;*ViO`YOIvE%w@bCuGAdOg{|MDG8z52XCD>_M0M)HkzU$Eovfnkspo(*}G} z-=O6C%PX^bBDud@F2Cs=-6<2Nq5YMd13Cr^k|xJS@P;}$ncNzS%MK0Ure-h@(*|u? zoRcC)BOgQ& z0=W?8XaJ?LAa}pT+uGbupGZw_^r^Gf)|2`ThE()tTirk_aOIIEXcmxch*zF>K!N<8 zfuWDNEHpIN!OJLSib!_pH@w}A5^sYZj}>pEmh5|cx#OgP$gA4nHj2b)qe>#d2BWdE z^(FttHGX&e$7pCD+u!m3*&6;QXENW?aIpD{#yhQ}>AS^75D%ocPjj-`-yl!r}0RMh~+}A|fJwLBVlm6&01` z<>i5b?}dehB_-w&B*et|nk?HgHM$1+`UB0c-@J)UO(jpy9<_e*f;m6`2KY@HMN1-6&zMrX>)UPSy{Bg=ZJ{7{&janGcz+A8|HX+TG|g+M%H{O zU^V&PSy@>e92^6+K0ZDo>Cq}jhldzXpPE-DeErJD&p*lx{suk)sf~z;n4Q%Xd7bq} zMn;AURk^XTk)2IzspoQZ>Kowb$Pv%Z#Wm8=kuzu?mnnrbVTfdsIQ%r>qxSCII}MHT zje=O@4{)F0ZrvkP3RqvhG*+QL3X;QMW@EFrvB}c-s%YWuEfinc*w|Q96rlp-+l3}C ztIEl#sikJ0szG~^bPh?$OyTSg!NCRw2Cy{ZTMUf0kM{)y>%M(^!Kmi{mzgl)@X#4n zqV>pBOH0ecgTKlFobZJ1#Z}}~sh*&hmzT)vg7NkBb?%wYHnqpWG@P6Dk|8SgFQVxQ&UOq79JiR;JV1wt*tGA z^3O6m=N;C38*6J^GxTkZm6erYVUO<|Xk_G#6;N8$j?v#8&vCG`JJ{RjYOJ~Xn48m| zyrLhLJE}>=RoB-q(JtO5>|K)WUAlKy$1=n6 z0u6f3l{m?`m0+V|-<|#a{msoDdsc}e2CtVsy9~F&{A5vAS62gq(z$SI`to-xN=0ik z^gf!A!;G{Z1M;`oih;aw7fLIUG8lICyyCgtalx%s67jEITjq z;&cWQ6BF$uJ`M<34?#VxJ?SL!jr zYo~8L`=d87Y^tI%G+Sc_vsX%FGZ~spNl7VhyG^;84BUWOb>8c}Sb`2CeX+^-tk3sm zAhiZfo(3(x2`b?c5x(a$Rxo?H7%Fj>m51po>+5H;cIDh{LJSNHE1Xx`1(}wCikBxJ zdIjwMl+Z|rjW7=m4z7RzHQk@Bf$YCGKl0q0rfJpuG#nYvq)H{>pRC{yZ`h=zrQP~E zHm2@!l&_L5;<_d;l2=%`J?GSB(VlA35jL329jwAl7Nu9`Xu%=&>eVaGmQ(x_Vyoc{ z5I~&iD=RC<@TLQv86I9nY9mQFEu{uaN=l;0 z1$kyR22+!gljREzsS|tulpD37Q~SrN07$;%fAWl=|Fw*a*u}itU1B6eeJ#{1s#^+7 zZFo@cyxd-9(3E@;aL{s*oC*?T^JKBk-CW>aPK<=9fW4!mVS_7!+a`@f00s@YfW0wo z+H%LoCAVR5aGOCS1O&X$TYtvX%jP$Qnw)J&9u3hq`uc3Qab0a{V6}u+sS*JQhq?vX z0_z&xBhHL9r>}?UpGkQwS31LUS)2T?j?ee!u6n2gFHKT)WNAInw%kRI1SzPfZs*;G zx$Bg#FAh|dmGkaAwBDJ9piKVOF@{_aUe*LR#rHZ`3|Q|Y66qQo+>a7mG?tLp(1`c` z7$bqOwzp@CzZ_SUw|M%}p5E;px zo0(8Uij^|MmXz>G^rz1ce?GtgIdqC&>NnW3pCMMt%F1_f#6l?>MaFN@M`)>cq{iML z-K$rAeb5H|`!mQrLc5q~NdvF&`r~+kDz%XFdj&!xbq1Wk-W=b}q4WV|GMcsZBqK}5 z304-CPSLMOOZtN(Vot+>3HHQ4x{uc{g5Nmdy!${6?*9K&1-6H-}T)A#d z#1)AW6>m>8rDz~hC_VelO9|wmG^3t{McySXZ7U&Jlq|?kQd!>M>oDpv;C#xd0^A2Y z4ztyGbh0DNBIzo~SM<^X%YStWH4mCkuqz%nJ^>9zaJ-jZO@6|Q)pD`uXImoz3f-TQ zcaW|Om4YVkqwMD+A1J<_D;y7Rb>ootbP#F|;yW(l(LQRu@ zKOA>#Vxm)X&6$FPoSYTzm6Dj)t$Fwn)FSrGkl52x_YI9p1R{d`0WU8vjDrHy?23wt zfr6K;tOOV4@VPq9aYhXt9k2aa{tb3hQ`6>_mTkfs-Smu%A#*HH>_sdAw3(RD%y*Q{ zdc&nG7>D9<5s;Dx#rpN@NAc6;Mw0on<@x!t^F2Hq94hnT{Nm#2!r8tT@IgLbTq!Vt z*m-JxesV(KWjZi4WVvIvu(03<{%GDmJWSG9c9fQuhL4Zm88Fc`*}Jl`?cP{j&Bv%` zZk~I2if0v%BqZT9G%ur-T;8A}X1KqE#V_kdCj;r;N-SYBD#B3iX~={pt)hy6@tC@zof#COqy z=6H7tW@ctd6q1)_EyE8^%4Li;{+_dy2fuJh70mT6w?AS`T{B6GkMH1PaG{t2zLn1) zQ#@(d?EPg%KuGBAd@&MnwhJjvm&%ftmp7L9w1(X^-P5cK;iPv4h!5;T~m`DE=NXA-aj8wU0to;;Mx_Jqv`)Efh}uIVeR06nv}GcKB7Fp zwLwfmB14#!!708|hn3c2^M%iVrl#hnCZEXiIGLSx8Xvuvtz~#ph>HVG3Cqs5Ku|_j zq6=4=EP5#w7k$H_D$gEwB(UlvJB7(FNXi>lbtY-{0#Ne3_AvPi^7a0o($xBA`p1FH z0s@H*R0>ofaRmk5ALDvHU-AsP=(h#lB%3ARx&j+)7_s@R>b!aLf(mD{4GLw{)6me6 zl@0TDj=H2`V`Ysb{mdNSi%&oRGn4gqdwqw72x5d4m6Xix?4hKn2nwXy+qd7<3m3gt zpP4=*e=xPPW1BPjU6ZBj*Iksm-@ld6%2Vfdd7U&qKR+;K;&Jw?@vhYlP+gR4hfA9G zBJLfS`41ID6G89(zkR%MniR&asioC5%(AzGu{RM*J1 z?|O^80tkmdDv`sWkF5-x1j;3ObE=`Mo1&QKyP57)GK>4)6!vEpjq`s!wfQmZC(&JU z1Jjmd_jUgB%Gny@zZUp2%SAS4G;B~^L&HNPy}aDX-MxCDA83$h4kCW%Y&7&QUxJFU zr=65%(AVD&_!S1t19a>)A-FaPkk3Tk0P>@yl>j_aQL)lin*70kUEuvtP+9wbuJwPU zkbuVl9z`Iv=GqV+A8*iqw@bv!ey0*BTRmd{TDD}o zMWs5e=D~w;B;Ne*rgLh&Vx%z#NQHn&rFqiACJ9s_r=`|(+)DRt^#=XG;z)22N^4+j z8yg#zgq?J+#R^+#cOsx>+#UZXll?E-TCSF~2czTsvE~C5@SOb_`10GA!oo0Id^|jt zVyrPk5GF{gWniu(upJ`W<%TVqED7p`)t=Je8Cy@8pntE2hPHSA|9a;HhFD6*w=rla zdV1Ggn-ak;ps}?ZMOyH{Ab)C#vz|Q;{$>0EjLQlvCZeGM;$q83)?zs90`iK6j&MV@ zZmCKEFIkjpgMOi=F3)(qVL8askAq(^#smC8BjDl>ESpI+J=-zX9efGwqV~Vrw!Gz> z?Eht3jRF;i|F-SCyA)IZ(9mLy9SMyLWBg#IWTwP@kQwroQ$ZwL6+o~0jr0u+FliaGb+9-O|>;8$=?z9!Phgof+bOivbr4fWrqvG43mb6ii+2AU~8tTpdKv*H)r&%o}M4_@`z3@`jFqqzgI4r zLKr}Bz&077ii&;P;{`AoCZ>WPKYlE>_@O_3+}O|%zWRMB@S2^8sYkP7-SsL>*bNl; zL8yqa{d_$)JG*k8dJCX9+78qFwnqOt$X;1%s|29a(6o$ zjt<5F#CLf%Wwg7qlPuWixj$QJ(eHb{cXhcMGrkQ-N8I|r;2_wG5@4{?Q;3QRm8d7% zjje+NdrJbqc2r3T36vu4@|R^)K5JhDa{d5z2$-9dr6o_K`yasI-c@z?^laW{ttIdb2FY#kbsTC5wxs?C4<7WyP^(g- zS=O!qpF9#b`sU`oKlJ)vOL%?(EUZ%{olZAu zjx&`EbaWjPf<^`gEj}k3cLK`M-(SHWu_;ZnwS#1Hgndu9dgGbcnYmQUrvUb_pRHyF zUpOxP7N6DiZ<>8?ZfQwOOgv_i%b>|ZG|mkKk)%~207Pi=03gE{09me}fC-hn6dNel z8jz5bn#yXg_4e({Tp*ihcvC!o{=1HK$Gs7Xx?TcmRJ+o>d-rH)XwJ^gEG#Uf>XrdC z1BiBd`fcwpJ~8n|Nho3Sf6oGJY-sE06_0Q4?Ce~&1AJ)K6a9>ohh9i%8mW^tz8#Wp zEF}-}&n!g{bbBlhpvUFH;$l0XP9-Jjn3$OO_y}jd1wzc})+n<^{-Nrb@6E4Z^iXAR zMdZzmw3Jj|UmwtvE}aI9x$Ku(#U&&RFN;XOeEBjoGz22)?q;EKIivn3gMuLa=C5f} z^Z@B$G*dq7jj_A5l@$1G07lY(1-aIg@uj8A?U=O}PajKz1gxwa^~?D1_!!7z*&h+^ z_gf61ATt*gu}EPUp(!zve=jX%XJ!UO5ja1{rvNNd8iC(Turb3RDsR z7H%uz{UWOX|L?93r{fP?a z18Oe`TU%QJIPLB2MIMNjmz3=FbG8v?07U}{SZW;GNeaK+R_|j&=#597I4D=wlfVjyV1y)d*-!rv={z7zg!8q?iwrMc{vIFK*pf<1Nf9yYetM#F57gl*t6>nL zS&_%Uk$}wNkRJr--4R$v`#7jj;a2kaIS1cB9Rqh3djBChI#!UKfk8TM^jMXG3k66k zozNDFp0m9fjXZTslJ)s{eT|^vf3n`39spq}L%h9C7bqB{^~X*@@@K@A+|FkwCw7jG z0+W~6hnv5K=l|(p2TE6*Fu3V$`|7Z;uAlGEi2;? z6ug)y)EE}O(0}s=wxQeuI32+pg+1n^&mGa$EH`8rQJ_2-l!we^wv`!jI%*-4%(%w3k&8n%*%?%lXcxfi~nsak=gVOmobLR{Jszc+79)6Dth%BkU zMN_-L;0yEfkoDo;zi}PKfb0l#CCflm2yXDNH-LwUfd>K#%HO-An3u;&CT+L97khf- z>K*aJ1JsENdI{s;a6&p-^%}%8Zck_y-{Ka2m3m@}pml1N`g0 z{*orQu`#9WNlhOpb->=1K7moPRsL758sl8kjDmtXJhpi^pq$5TTMea|sFxsw5`o_$ zfYJvU?E5*_21NlKjlpPa+a09fedd3(v?jxzkq-y5b%+c0; z_39TezrBNrC(yM1{=8>b_d)VK@NP~(Pdh*1)iu7@2Mvf$XkuCG3w9(og^=@4$e@^>mvWiMH zE-k3vkYGJO0NiA}+2VWQ%_k~>*a*PiG16@S$O5i)$2c9j621cUstI7TtC$W>X{P3C zzYE9Zh&=Ve?igxLF~_RdJ2ku513x#TuOS-$ zf&*=zHbWr&AjnL@S&Jp%=;%m_`gzXi%j)Fyf7>!gzX@<~Vn7x=ia~!y<_-#dbFZ_tZa_j)`1KLCN_2km&twT6gesBJ2f?$htrn6*PtUL$j3MB?~>gl z0PLik!WRyu01R!tF98_&mN|e((tRt|SqGF{YX8Gm5;6e2U!0!m*Vqz4115LRfV1&U zb`*iqhAjPOsWkwU?$G=L&{6yF;eM|cdRlrq0X{x1z<+N!7#O-VTddHtnv}J5bkH9? z!sO)w$@u6Mo1~7Zl2YVIdQY6TvO=PaJIJq~TDEI|Mi*~@=yr|O{Ra>BkN17Ny+cAn zk77itTWeZdGvG9nmQ1$wTA)u|-CtqW(-{t+p!R+)z#t&yXcv={k%jbPLO=&21h-yW zOAAwDzedBvgl3^U%qIR6NPPFFy1Qj95qG`7UY7)bf6q&s?QLzd+m=6l!VqD}X%nf@ zWrtY&8rgQYn<@)HY!^6?gXbtjfu&sjAI9D~tjf0A9$iRFx6&X2N~v@Uh=>S?NT(tp zjdX_~B_JrEq<{#L(j5{4(z)pFZfVY3@3+6-x3BB$-#Poa-alRy)_T_c-1nSgjxpw3 zv~u2>ZnT8rM15*f6us4TD7;--M=Rk_k$TU)o!30lRzTr=f=Dy zWIGMqn|}r!A6C-VJg4v!g%CB&m@2g}Hz)la3-1eBo5a`0ii#AAnuQp=nQ5T5cGgF4 z)wa(q+5A+%Wa2G){@?+|`KQe$j?GC`kS@T!G@asT-;-97f3qADKzAsJF|aT{r!4-dm*9{5D-9(3zd|^ z*w+a;6M6_c@V+96d8MT>;j)k>n&#eO=#*NLSR=@01kUh$3;h{_pfkn4Ks=2~qvW;B*+wMsls#9(WgGW5xy~Uan4= zIbR&89iZ)umEA|^;Y_6Svat9QDgpBgCx>gCh-^@^u%4ASZh(Z9R0z1MOP@I*$e(b& zE@nL;TIy)t-s6=);h5GBOy9Xx%0dFHGM-pr4jG@5jh5TpU}E9s?y7Z^N!$Vqaz@m; zK2mhI;Lf-<0LnRZB*2C9)4$H6r@J>E9pCZ;4P8-@z(9BtaLLRJ2?a&-Uo!67xBql? zQI@VIDCI)#_%^pHumQ0gL9bemDKh3XfgUivd(`^6{O0>yW26%`az zvj^R3=zTfPzVuprzsbr9s#btk+ira{;f7Fn(W`)0Zkq-`cqG92p zN121>0sKwkP!8+riiwNES<6du;G>4=7FX{i+Bs&C(#JO*G6Lxy5JEB;pDyfp#K!u_E!e9oN7 zuQK`10pnNx{X6T6=(w{p58gB>C>K3FYot?p%u9gGI2#P2qNU{&wn$dWnBy?e(bk5# zY5BEdT59V0_6ts8YSeZ6_!(rj9@NMFom(uq2W zU7qc2BxwK0%zUJIA4o|8I+1}*dwv3Xv7^{a)2-UMw5+~MUvF!WI^C3Ua9L9P=|z4$ z!d}VD3?iV=JJ)j3m~hnN;NH}jNUd6{tlX^Rru@m3>W3+H>)9uIX~z3FT)z8BPwM{u zZ3%0<^Wn~yPhb4IPl@z-ofpSlH(z}g?G|4WGd(cpk}VtC0FEx#-cxi}CP7|$J{@dB zrgp4W|Acj&!#+#CDE|mU`vaA8GMk|3r{ct%6t&-yBN&B2xW{Lu4guosY2&%O@NQ@z)_UZ zzfv5T-PJUx2C4-z-?6QtD(|Biw?YvkxoLidD$y`>F4TIJ06#L$Ponvg|Qpr z-%A@(8XoAssX?qHY!j1_rKQ`dXY)%+PHBB>Q3TclilbLrl(Ik(y$c9fIXGGDj)Z&* zT(STHq=Uh011k<2=V4nFySs0etgNykENU`CLaxfHoSD~9N)cPv9xuikcxkDr6^*Rt za4sy-gV--01sa|3QXk&w;YK{17+yu-RZ7%H^}NeFq#n#KVc}mRrlMNX_A5XSN(EAr zlQPsklto^<_IAiiX=%STP8KO*T@8npV~bpazME_&?X1%G4@%!r| zy!Y<)3C~)BpX|oN_hz-hIv4BmeZ#3!r1V2WeIbo#nULKwGq)?NDN6)-$dDkvKUE?3 z&MuW6h)kGTv|Z6#2U6L|snp3G^l-(Sd4-%A-@X-_QN+~O-!D`BblIaG({By9&3p%s`FZEO8+P}Zatvmo^r3o0jPtEw@u4{S`64;c;3nixNVorec$vWd9!wo^d zV-@Y5%V)F~%;5v_yJ;Y}F2V-Q9cyt%Q{AXR&^(_f#+doTk?)XH)oEg{q7upbTJX!0 z(QHwonW(_))YPkSVm2d%#t@RKgNE{oWs|Dw?;1Ur{5fKo=ej~=;IRYFqng7}i@eZi z4WNBJ6dRn>w6t6g+42InI{LR&33S5N4kqA9JDTMrRjg)ZYY%K~1jofuFICMu1yto; z!^c8C8Faks#>{L^2FVU$#b&Dpxoj_2vV&f3z19PnH00WZn{LTX`vm8@vt0#Kt!bOO z^Pz(;8oG7?Y&$}G`Urve&X2zR)(Y(+7B>VN9Zf799S=t!ex0U#ozA(B0 zwQ+?~^vQm|atVz|(Q&8Xlmng3x*JYs(|V=I+Y!r6H##Cctfg{N*T!7+Nm&k}FP)1( zRv+s06^Xl)F(>Q|WL7L!6+n3d_{;73&~aSy$lBp*r}N(9D85&%#K@*GGAi{}Go7T4 zhDT4HIC$ZR-|~@q@PLcoadjY3)LFlS;NM9+6hb(iN`H7?BnfGhQv6dyl+GbwNl zAMBf5!NOFBOG)dcd9DAlk$ZMkC9&b;4jvkNcMp&1y|ee1d|=kH+0lbZ@w|9f5@NCQ;Tg%3FG0`gm?)akzYAh(8pP zOohIMD#+zE%cf3Yzqs%mb4M4diNC`0jW1u=R?i-EWJ`L2rKGIvOi(Xe8WI#V8p4Q^ ztM%qL9-!l{G4SSE2l(l!mH;Cgx;X<*#st3CeX@b9z!FS2PkDC!^{%wzP@Qixd!ZGh zN9Dl9!C|lGJe}$WF3Mq}2*7}PM;}<^p^QeVtJw}hT?9$t=MUEl(6BsbgW0$%8$+)W z5joe`cH;_pwYR4!qMj$kPVUG&y*!ST z<2uL)+uPfK2R1fg$FnKMeLLVDwG==akAn-Tcn>-)a_#RrBwSj9rx@_0>0_WA7UBlo zI}Pu!+`K6~H4i|&Y9j9@bmNMYq2pY=TKVeP?e<;pZmn|mt)*mUvYR&OK6^HsQv#W7 zZ=&&7QVJ3&_y{Gk~XJ_|&JvZZzNlQzMUu7V~ z#RYX#4m?JHaB|hN)pa!Uk%57Ma&mG8F4Dk?JJiH%sGB+M%r;ZeRq=r`3EH9{MJXc= zXZUge=;~@38VXSZsqO6BB6jD~Z;76;s=B$IpjLB&K1hV8Z1Y~(%g=cRLl6)I;&VIO zFI_T=iwCUALwn8EK}XrE!|U(w2Qv5D1UC=QC6K|`gtY894Ulrz))hXnw7H6x6c+Zm z-WytR$fG#CDIfCwy+f&p{bI*gskfuEt#<(8oBC-#d$tSi!iej<4~hMuDnO1%(Q`9? zN=R6*)J|jrZWM}}lCkvk^vkm)Nl5{*gNBBNxa&7I1a6PD?OE&Wjsxcc1j+k{ zb3oPT418$*5~Q6|S~^r>=ra}@w-zm{rUn$chg$Lq#O$s?3qGK?B`=S=DY~d(D0sO{YDOtiZAst6I^x`HPTb1z&&_Zy;M>ha6xH{5OMSl5G@w%VYH0ukf z2B0n-(w2$%cEX7PLReW{J!T2*Js8@6w&*%8hDC)~RX%1fEiDZW-_>dw8k+bIAEr6K zJ=fJegTh_^+8&+r_{sDxQzEB(V_ zDMe>zW(X{1lBXlxakpNw$oIhm8X3SGz*9_?sF6h{cNCeBArc?x2yp^}6K>6Ef;nVX z&v{}&Aar;G7mO3-^HI)9){Yt69SOm3E0y?=no2%5&e(Rj*m2y!yC1UvTK>|rn_Vy6 zpkVKMz`@72AN4v)DFaFY`$Lb)p(dM)q#CRY zU}!EZyk|8e_w?ygOH0d_FJ}QACQf8ghBlYn zsI6aP`>PTvZnk6(6q^aHAh4^ox3cx~BSJ$%BO<_wn5VPJg(Ho6w4UFr#T!8l0@v-R z!A9zN3>CrFkBlVkB9l%iib@dA)%dUB!@t8h1c%CGK=vXA7S<%#l0j0F0E38~ogE;$ ztgJ##xrYy-`{dQDSGq?_;9Pi`2vGc1o-QRo-HaFjC$xIt9>N|PwDjGEU6zu9b{}k}`9@64sfQ5!OiQ$7 zRq%=a^{z;$(MDd~OV7xda$C!t-0+2$(4GsR(np};13}nJcy8gWsA@0W&B9 zoCmdp-%4Q16&sOY{6ZEGlc=1cG@V{EC_1^U*fX<6>Cyi6b|fXZgcPj*TDv;HA*eM7 z3Ob|y_j>MHJkk9>k4Z~y*rW)JSXL$Kn?P6~{G3>yaaVIlst_$9LZAE#3v+5$^NcNf zh+mBN64X2wE6U#B00S=?N)~f=XN1Qyq*>{3%V%esvmH^LX)|MkA=MKe05L#JKB`!^Fr5HX zO|8a7+^U9OiuggKXDO_*L=SxGtSobjlN~=H5|!8t_K+Cg(tXjX_J6Nmrjf&@Me)BL z3NWPg5`md!b1yKf28076^kq<+_ z^7VxWaL^HxIJ%+)LHH+Ff1S|}NdA96Y}`mAF2EyLONV zu#32I<;weqY57L4DMFk3dU_D>;2Zw63$ya`%d~U-7=b@5%S?&=_gPeFnST1>Uu*Q= zH{AbpFLCR7MSz~iHV%P!hkkK!_(EE)Pl|gOvB-yFZ>vf{&nI2 z+5-*s!Pph3c33Ur*%PRoy8k)6y#Iu)U`R3y_492XMcrrJAkwI1tD`+qR*q$9L(aev z5(Ph+!aqmW^7ZVW|88wEaR>1(GJHw_{?clUMl-3R<)v@G*sj9XN94 zK7$aXtBd~?kmA2$9W=Uu-H1XKY@c$a?t`h{9Dz;U1+_>`9m8K6$oMs{T-pzP4pYh+~osqgez z^Dd8CB!Ru|>+iRzSfJcGybsSicc)K*2ko<%>n@mrJ?Ip&*J1k&W~m|cU1l)lqde#c zNk~f4_$`KB85?6_VM&0y<)5X`Vh_p6(tQjuq*bH1$d*coYL5z&q<2B)v`d2=_Pj7; zihr*EITHP6V=NlmeOF0)sFgbeIkGhD9V#e&ii|}23jIv~*C4od0739)IK#g};4v?c zyY7!pEzTGq5&&wi-RBmKVu8B_m&)$rI-)PD5flk-x%2CyAusnFIrV%w10Oa|_V6BP zyz{~MXRY)CJJq!`ij-A0Y6<0P#f+C@z8HhE;&%UA!pDg_{yq9*{Mj#;ezpHRO1YS5$8%j z7qdB+{yoOs5GQU**@7aXVHGts6>JG;pfL`LFh1%|0X zkek4qasGE|>YvRCH7M)HTdb_Vs}@l&@)+1_w5O`7bC4slYispIDV0VTnOAqgiGdOo z_^2V~rJDt{dJB_hhL|c-FxtOI^-8EvHA4tLd#)ucp-0d>*W~CT@{rJG>I3Ym*02e} zz{WJwt5+OE9Co#i=Xsy#o+oByL1B2Ci>qWTUUyB4*dO}*02_gu)$x_(gN;>9i9dgI zw$Jq3@9-s_qw*5nQioQ~5su>JWAO+^#+%pomVDQUqNt93>{tMo$f2Gi1e!l1jvpg? z=b?CTs*v}^8I*us`ub6i(u!>h<0sH;y}%@(f}NGQ&u4-8HH%#g{Ua_D)5di&M-@1u zV11jhZLQYs>nhIS2akN|m^Jj~oIzjK->s>sfsYijS|#Rc&z?O40DAMUWzh&2QJ|L$ z#qPm^g2z*UU|azzH^4m*em0(~?Lt+N%6v6HMPU_l58Lu0mVTtwD@l84TwevYp`MQ)E-Ie@J|22W*A6{NldJPx@4oY zapnkFcX0p~D?V?<7r^Z2*PfU|=NHrwEezhY#Kg8>ErHg;v_zt1X7Fo+_9G59ZEzJJ zP=%m!l30c1$UUCb_pY;FOp|blh`hlWI%c#qpp|)Bz@}M_L&adKTsEVTV9##wQ|ZWM zo2rl1=r4zkteTlX{`gSev zq2_RUa!%IKJnyijri+Yw^WK5Tz|hMJ{tqG>&X*u1SeMUW@sP^%L}-%B%12e!xAi4k zO;oC>d;-7+=KAwCsQgRhRG&Qq!}Lgv``@2?mo#AWMGg%K6paCeD|t3U1T`m6h2GHq zgfg`q?=AgZUx~NL1kWEzY0$y+%`{55T|c(P#Eugo@xJULShCKijf`a2DfV$;;m(wE z@A{>5YAKU^6pOsxG;<*%&S=4r2ytqATvnEx5@qdK*X2dg>RQMu^%S>qiIJeh<&|uS zK;ElG*W*jSW>rEr(6P&kO{?h8@5ls^fKHtrL-SqS5#giuDHVM*L{>;*Re`4*P0RFc z4jNG>*Xl(-gI$rxc};`M^o3;TWPonR8NiJtVWE^~-PB_k$AOv5%41Dgq0{x4uDGcqy& z$11w#GI*cajg?v<1T2Sbq0ty96N@n`fiZ*26C^+pD2MnUG?|w`87=@9H}__=hJFRK z^!4}knVXwm>}N=hY#atMcnJvzsE3-uT_t<$%7gnYRW|e;5kubjXAT}7@8gA7S!h!C z#jd_Ida9Aae$=S&lziNs$}Jsn-+`P`f67JwIk`s_iB4IXGmow_e{uLFgLD0>U!PT6 zOog$C8Yqkm%jVujz`QnOSL3wQlA7Gv3%2k@wE9IZ9Kwgcmw#{Y^Sf+$YFcSJ@G{>( zGu#mI4@l9teQ`=?tsm5c0ej<8!k2ye-CB=$%H1TRW6cBM9D$A`-xn)k?nNUzL-+VT z+g)~x%qresPHhiN&C)rfAC8X~blvZ>D0EXsy12yF)KtQA#-?{8^|r(Wvsi z>`g-2Cd|5)9~*b=HZUHjR4?=6R!!)`hwrOtZfOCb$O{hX!?e4%ZvB8n0fb=>{U`{L zs>p#DA{oZdjkH?_2AJbx~BIBev7zGeU&v6m6vXGRd1q5Y;< zlM^2PJn*UC`&a0_&vwAH4`CKu=wFKIOKDLq5 z({C;$`&Rm&xbg8Za^nAJTN1c>1p_Tw4bD_%X4ABj68SKy0B-EpRbs5HTvAQZfP=Y1WiD(jW{KgueqZ|9SatjV6aF$;X(kyWEG z$Zzz0`&r<+Ody}N)&&~jqCnU9-D|dEiebu=)tDB@U*?VFPdbv&b+={7g&8Wiz1)v%w^ql|==>|RUhS8{b^ zV^axP`SGHCT_|`nwJXZ%>+6B1I`8(r*n`ex_%60}r&CC!-h1&=4r@aj5EwM>->AItR3ld#Iu0+Pf&(uHii(G>sQB$O)T{vr zN~oPG{05-)0+@OaL3gW$NdTktAUxXlD+*RTd^2WM1|1&UT7~mVzzM=+gOae2RI&5@ zesEI&<)dHeumawtK>X`h>P?DJ!W)D=DBw`EDByW7XpFjIW@dH-EH8N9Z9ycOf{_SO zEVn|Sg%c)}U`~VK9~_$QAWF_b$Nt_P^utf8*SZgkjI1MwNxULu=LI|?ie!&A@_RA_ zv$9}vxaRpy`eRIlCRh0e>#Oj9tXU!+88y2pahyAbvo2z%-sLmeLTtuUZe)-?l0DwJ zZ%H_36^+?-zhwE_G6~~7-zaJlenlkK$Gl^uSU0`{=3$`#dE*pzyART`XSoM4-!aFBB z8%P#_d6%$d?cqt+0W^miD%$uM8ZxeY&C2RD5p`VsG=y?_B8sIdz*B{QQV_VCBC}p* z7*gS~GmA^c3g&xkY;22LW&n20%~zpYXU+Sz53g~ET)GVuy|Z(3Xl8n8tg3UL{J?Mf zIxQ0K>Qw}^?sx)4jp#5b5(ttd_6KN~`t#>gHPjFb=!@ohtN7><^fjY@h>QEPsovMc zF57G?G%b+4*`bAjC9Q)eECzZfyV%&#Q$xmkEmwR7L&h6Fu!x;k{SKrhgjElE(OHvu z_h-SXjrmo?=gzRP#s*>vT*Lvv9i5-&*y|J62ot%(VCKf&m zX4s_-hMZ&Hu~9O*qNN_LmTc79*|YM~UNEoKOzc*~_29pf59M#YOtoXDdhr5+G`>Ph zp7>L8{PhTec*CN(@Ufm7xMVFb)NQzksOUR70z>SQ)6WlK*cprXDTv1**<9XX@0o?b zKvQLv8e&=^{;n)5EojD}@#h}>CI7w~2Ig$8^Ha5eo~MYOwVfuTJ6QGBEnO230UO_4 zxvxkNQ-d9fX(V@p7d_;P?61FL1%h~pNl%6lzvPMFa!p-V%p@;D0gKbR1FyFBCP&DZ zK)iu8+4zqC)IDr8OnrDH^j%ig(BL2pu`yFiq~z9h0h1m42nclY&pbTNj3UGrJ>Eb| zNJ*Z8RZiiW7_10r^OR1N86L{}u`}Ph^7njw z{CD{^-PVJ`XR67bRzdd=T7K*=_a8kudTkQU((tt<)cakVe{&g=7%hi{v_3ro_L%o=c&U$fy#~1mrIVj^KH;TgaGkx{YuIApwYITV*PVf_u{FG z&_A~Y);r#evA6fBUB6)hbC<>bwycI6y6>>wWSw5EIGR;>aOqH5qB&!0cuFMn2@etC zH&6d_WSmARDToI-D`}IaY~vp|*EI%Nx>ekd-{8fhB?k7~{A&ab;T43`{ino#{!r(W zU}G}h*{-hftReIR*goC%@~f0om?TrNcj8(f%|6Z@gij|{AbQmy<0S?n^XfBI!=e{c zUQmptFyRo%xM~?ud+c9BJa~xkVPRsXy?K+q*44`#oWIa31P-sR20{AWdv@p0J*kKi z{6xq!40bB3N-{H-p|7QU<{b?LgSUQu&=yc>HCB3bbObdsXbz|X3rKcU*CrU7VX=4> zlB@y6!FZg$Z;)-|Aq_#dAuXDGz zzrIq0R(=D-2fQCZ^}lOK9#+=xsi`O}1u_~`(7*sfuFTEP=i%ligf@JP7vPTuG8y!z z3sB{O4YhW1a#YcnZ_C${s;_aL`(e7XdCJZq_eS}-hS6!n@)-78?3a6#mc#Em3R*?q z&5}NeU!T;CvD?tiIm4H2Uhbzc;XO2Aay)eH?mdyNoABYtzyHbft*f*Q2}Q8c@n<1= zreEy08b^wjN6-mauc3eV$!T;o{VxIS48eNp;P6A9huP7azAL#c^U zYUzLdI$FnLClotnc|Z~&9&mz%xHin7H6otR~KTT+eH zYVg>@91+QroOe_6Z| zmHwh&4tV5T_j~1xwMs`Up_L;Z$VsRbN?x@^!A6x)0d|HuV|&V>UlGz9swp!DItv|^ zdmiN`a5$=TCfMi(U>L3Nx@^kkoUD+W-I+eIh>Us<(NQpVX3fU1D9wgJ*Ly=Yt~<{6 z9D955Uh6$t!UqpuP96uZeUMsCes@5#J;B@Ty!y>&u-hLAS0 zyGFNTynR}def0w+Y9c)mh~Eam`L(}HEw&escQ6=l^`^xFTfVF7BXxN+8xU-FH+7ln zEhFXBJ2}GoO&A+V*w|m-g|3v;5cJ~c1)HV7+2*aL7PR`pzuOZgPm)7O8b92%_BReWL7N*5gfvE>ieeV4og*^+)4T!fn@ zi)_7CiF`_acAWoc(^rdEOoUeJ_we1}V$1;3J0|2#n-5(vFZRWx->N+GJDY12$6xxk zs6#nhFIBxS*pV1U5dT#Bw{V`r2sCYUi|xH{dj+%@NHRcXL-9UJVGUon05nuAw1ldw zKa`WR;FX5PBuVfWfGrRKW4M4d9{mV&hN7XS0xldBIq(5$#h^X;au52S(||RFu@9`Q z58W4UM2PK%8k{i;2^}>UzS8o7x)`L;Ty+cZgFxv_A=^vY3zJNI)Ppo*7NFs@LjNuF z&KMNDl!YD$Pft(KU!mFF2P!@y$+Mr}O9eX*qL>7%hCEt@-!d~>XKVqZcfKsJCDu0O zb78F1%ObB;g6AdvSQpJdV3w#mNY#`1@QgbH27e*#V3k7Y)=JM9}eA@=N?VS(RAP~=RW9bPrPPe9AS)y@^WC5uxdJ{0E<2y|20w^thMEEK-4wpW935VhA>J}lr*mC7!SkaIQub5Uh(u_|;890_~%uNdgKi4OYbwJ|BqB_A4x2;g>&pPF3nS z<~pvtB!(nPWC794z-2VXxamQajYC+A7KMzcxF7{Rh$>VLQDQHeJ{+s&;S&&6?-xys zuG$Wg+|@t<6*iQevH#J2?cOz2HocSA{}AP1 z)!phZs&Y2!&-)Upf6I!Vq}TOx`ANR~N7k@wy@j`qqFA7WbnBaYJb!tT!NghIHo}Vf z#di<#yUO8HBnA%5mxjMvz5?ts$^cmE_IT`}OkT0ELNK>ZZQjdA^IIs>GNOE(hd%MRXU#8a;uF66JQ@vzRk@ld8kS=@fh%1tlhER8|fb0{wpJZ>POjRq4Nu3tA?Uy*H3#3uBv ztzk5pZwl)9AV8FjPOeVJPrW~*QY8KMXa@w;OA7K&0=CO<3@h@{Ul+`VWKCThC?YQP zPAov2o17bNZI_;0;a&3{*U@~I#Ag)Nh#qb@SC#=&RD>>`Ju*j?|KStPR1B;yMYlYo) zcI9VrmnNs1BXcbn#bd9uQvn#R8cga88kP!`(TaFgeoMVAA<`G9LgM(1O5X+TK2>Ir z1n3zcj-eYSiZQ2hZ+(xq)-JbAp$h&MYbq)q^-60I_xGjMcu8Gp?JvjBK@)G|=8TYr z0J%(iQKyq9Qg%PqDAm-GFT1fRvpY4INF48I#TaXCUJ1_rFE$^nDbD(AQHd9pa-HnA zvyY}Kw|K_A*e?enKVs4`5UJ=Cj9PAa7EEs1CjZpanfm#HEt?M|qdM}Vb|_Enl-&%E zduP^GDf?|vy&px|mvfVECPurcO9Dl@b$ix z!Yi|%Hq1B7=|TrfZ5LUVCQ5XilY~^otdFMAYTV3q@I|g?T~&{`0q?nbyZVVP_&I@C zUV%o}0xR^?(x{k(c{dFO-N1u23Of45G%&_KeDu`t_q116m1v>8a>6a875b&h$mKVn zC8F0Gg2@aEWeWmFt)&1u4gmq^^{^XipTJll6EX}Y7M5@S2=u6Ni67heT(+m7j_s`y zhfSbjaRxLC#+?@p{;&JLAERtRRnA*5e-7+&Q3D&;M#;Z_ z&#nYoc|w~_<_gJR+tt1(&_oPxI2mB~X@it3N(rB87o0K8FLz#Y2~wJ(mKLu6*=F)@ z+W^?Dv=VNCdE2}M`sQn_(omb9D2zdp{{G{~A#j9p{rxdEG%>jAo;(;BB)E2mk(2XD zKC-MB>>6wPEB!SVj|Svx3TIV8(xr+uupL4&5g-4C4n|Kp>9I#JdrL{_(2*Qgg#?rf z36CRN9%{EQ<)nJnhl`8HA{eL}Ol>Czf043mVBopaPFPgamzj&!Ylhp7IRBh2eXwo0 zJ|+*?{$TiD(VXtRcy3-j~C++?_i)07}`(| z3%thTrB*bk$pE3uJU>7{HU^bY&&#V8I#`R%R6Rb;3K9VQ9?GqvZGBeiZtLag%1cLn>9GkhyRaaSzH zSyY=1+sOo!<>VL+J$^7>CgAyeag8`AlXs*tYaI4fudK&qUEZ;G(HtqV^09kh4JAB; z=W4ih>A{(pw?iyz34MdOQ!9%tl6tiMac)Ai_@5mku3Bm?Fa*b`N}QF|1SYar(Vzme z>r2p(5fFf2g467pCVUIx&pSLkMD@@FU%^aBqJngS;UqA`Rx1W-S)G9%V8meD0T>Nn zb|uQY6sZjKC)U3$L%Z|x8=865yN~~m@daoPVCP}NPz-Ax?QIn1fa|}*6&`T#lnAy* zi+v9$Lf_8WIuL_+L%%3YPUpDod6pRA|uvnzT@G>6#!5c+;&j>(*(FPP;Q zdCzxjb$lbBnaDey-^Y)Z7J{mg>KXWHp^NU-5TGD>v z`8H18BKYi!VXR}IyTRFAwH~^44$XZ!8WKnK(TZ}s=e(q{Dx#DI*wPR50u6iHTip2$ zG82#$7Nd^lg`Dnx6Ss^Pk3rmWM}BCvKdE`!p+aHq(`;4soc0FaHBVAfmG}==D}#eg zqnF>5ZAj~V)$QqIV^T5QKilg*74a_x^DkU8YwSUp*}V&Ye`X~4&)bf1$2^cvSCPOC zMsVt)#_x@D~`QJ6F>nW`CV>i+E+l~vY}T}`12B()*n zRmxl=X=iR)B}Q^RlkzhZIJ!-5PQaQskHm=28I!Dc{qf}I@W!yI<=G~ z`4M@x42G(YM{3)jK=`Er>NooLo=fFijmDdU;N{xg-jxlF5V9&Dj?c{Qc>JKL;1n|54;bph z#f90Et-+JBFjerNHeFg)G`Wca`?=_I@UN$LI&{z6t zGvEE&jHXulJ<0aF$k^u3Qf84|{YUpb9vHq!@>bUF2A=AIT1?b0 zI4IXQ*D_0zI9KNyv4W)S<$am)V9L9W%>3b|*=uFRq_y*eugu?uKOM<@fpp9ZqD=Rn zKliA^z-VHnBBMmLU+~UM=Hi=6U>>>%>07j~xf+rsXvZICPjkd?jEwW3#Wn69f1DGU z?-6-;2Z7->az^9cf@9<7{5e$>sW`OWu}`(;0+o{?2# zGvm3cl0At$5%7M!`_$DuNQTti8gMeB#E@}RNLLmgS8-8>Q_NF6t*Ou_5H?9BMu>6= zZbdzvWSei;VAbI-QcuplOC?JFR|P$UQ~dzHa(PemI-2Rj2fLD>xx{DlQ#n2u8okIl zJi$_sRgD<>jeXDJpklS@kp6NIcRS;K#E;te;*odeWaI8{)Ku;S?uc%R4SfyOZ7mj| z7Lg!*lP0b5{BO$WqOBfVQir{S8nLA>t%1ygf!~%P%}A!=>+bz6FA0(S2gHuo)JWIg zwVywqq$p(F6^<_9s~$>c9dp*#yX@{chyWAVD7NUyS+ntF;PyQ`Dp?hB*_aDJmb(-x z#xJhg{V^`XoM*;%*SW8DM!Az$Q+9So=<_D_@~d5mC0 z(OWEnEA>&}SJ#_713yk@{8F=;26&^UrnSD3X8Bj*k8eZeB2E{4Hyr39I`h%T!%p4HXkl`WYL z)}0vEvg3MB3bh&+WeN2>ofG=fzrC^BM}|#ZoSV|STa7VQggT~95yB6ktX~1_pyyYI(clkDxUKLJI&OE;<$s!7}aTa@Kej!79 z*3^;L$cpaA(ur%#;>Rzh>WQz{8+gSmjZ19#9tGnWA?8g7Yfh%^tc)y)8pG-~lI{t$ z8j@p3{fvulNc&!vEUfqDPhS%D;|EK}%V`mWv_jBg0>+)4e}Fds7Xb0l>*IzT$2&pX zVq9iEZ#+Se_#kwT%nUQh6Ehj^ik~GDF-+C{!17_l>B?wx>vd$~?!-j+`BYte7*Ct3 zc-blP_VYlu%V{6ILS9!%Ty&}EJpiOOpP#Uzx=NA0#W54s$nXGejuXU8s^rE4c?4q{ zl`9|n^N@`f*F21u4YtIcSiA8zs0%*L2Q8rgV^R1en+eqZCPHvOr$>1-ok5#9Y`;)>e2=QH|zOt0lf&n8}o7({cB~a zM59(&OFV)ri)N-nC&?{JF$slYyXrP?Rq*j6``F%DUzAr95C#X0s%FUR=6<(NOu(%{ z_hWtq3dX_KrwZ|+!X|Z>f$H4_UP&*NiWCWm$QKERngc_k30B!oEh!GddX?n+3FJRLKoeCK5?C5RyqAwWZRQVe zFB}~ta@JWQ>slsJ&x@_sr;BM$vdw-ZSaZr$G90Ue zTrHONfy;Fwh8(%74PgxlOeZ;4+?`U;>(5e72FwZyrOx@+@@JKrA)JuXgrG0p zYE`Z(w|tstXNTV6t(syOWy`@31(Mx>!7`ttB#rf8U7UT?74UhTp6gK-=6P|I+QN5+ zT48;yA(z41EMkUTR_I)mEwNh*pJ^-$l@c7+zV^#;99rK(*bauB-_+7P{l(Or5}q&& z+zUNnRiUfmp@iA$T(&ei1+ zwl|}GUT$xTF9REL4d|eqn}M;r`iFOfMC3yAL2^w=UF{I{nE0#Rv)UG-=*#j83?JXL zNjh_8?Ivi{lB6q-d7lBMlz~WkD>8w2XNC zl?a~wxY&1PtxYC?W{Sss_PqApeU*&ZhL?^D0bJKksFG>FPH67A^=>+8R2MrjVKi6klFH^-|d!}PNc{HM8rGZGDy^^opQ&-t= zc5sSKAxnTWpy#<{xFM@ZP&yH>l-!kVnUU7t6cECTm9X|lU~C}bujUd_KlTbH;y~T; zul(uG?r+I5Dat%0~a% zAJ6(w_^`axmYk@^_|cVgR6HUjqj&&VRj`~d~og}?vtm1w|>_fJ81npI&=R#hsWn&bG53(;V?&x(tD)<%B zDYr3Vx{kQ#-Zt}arkyrE>*bp`ZYEvbg2pIHWH~nC_Vx~acH1Aa|A(osjH+vCqP%!; zLU4C?cL?qT5AN>n4k5UEfB?ar;O_43?(Qyg@@BrZ<`-Pfx^$ne?&^|VyF8tE!oqW6PxEN(lC;Fc&67f{jWQt~_TJBhGLY+*rkGsdEkqfr?pEb%kRyWC>1GA;35 zWAaIoVqcnth8KAMIL&nP#_L-e@cVoAoauRyY&{}(zSiiEyQpFm9wo<&B1x2!N){X| zM6`uSj3O-DS^F*M2w%I3Ss(Xd-MI9DL1$xS&EunS=7>y&p~eJ#;sM!jycEbdtfgfh zcY(^M%vV^p`xF2ltdc{^8b7?Vtvn{8|T3^P3U(bCN&AXii~V-AlUN4I)c zkpQlsV{wYvgE?mjYp`mY+ zXJKy-> zb2BPo-I;sW>KqUIvk~2xT^R%K@Xe|ma%99$M5+q-3q0bZV7qnhMLYXh)@g4=~7@fEEt?YTUU0GT0+c_Dw zsChN616@w@)5zISyH^sD{zm7g#zC$br;e4_c=T%E!)n`HPWZ_yxd==_J z#)hZq39jEp3Fjx|%G8WFeAV;~lmzpp$LFsNwh=zx1bXmJzUy}t|NT8J$Dv7 zGSo4(T~;m|jjfb32hOflQ+S&t9oo$OxqSRLFO^avsaVsvzrD`AFn~DaV=3;wAgfzy z!Qr9p4&zn-`MT&X8&>BC2&bI?^yNO*afM7rBkz7mS3?<0-#YzrH%UBO_(c158qZev z$K^?kb~tp9sBK!&OvLU9#A=ojr+*~Xd;JhkXv(unFz!&`=VJ)Mrq_C0fR+9*A0fk( z91pa6Zr_ZLFJI)c=V%ngNFg?GHHj_H*j>a;r(fW<1aWuqTz^&Kh02yhVTSt&W|=*H z4e=p*j!jeV3@(O);d$?n*Q~OPt_~TGpvC!#3nb{Brmj27<*l?`EAyAbH&b1AvGy=) zDoDJ)T-U?AAN@^2L1yXEB}>mC>GqYhQuh}(S95C-GtId7&~}$##*?j0{+CWM_?i?3 zLUrXW?VB!xr93RzZD34v(Taj@3&e$wQ{;r~RIE1fGAS+niI` zh?4!RA~;8dvReAljJA&di2&7%wDR?^%_P_N$h~-2zT9(^fR4dq%wjHKh2jMC>h_nD z`j?kUm|-01Actx28V|F$sZD! zhyZV|#fO23HSm~1Oe`nVr*I1TRY{EK_;Zhmro8W&>E!a~lw}jDzjS$cweT_+T3g0= z+2T5EYAS|AGFkG`vM-X>4s0<^EB~mAqsyNXR;BseNR#vD)D}F06Xwh~QsnqAzh0#` zv8ly^`_g?pi@p0`p0@ZM>w29PA9tFKi%l#udd{D#7R|^U2pl|~9Ct1+9|yIEFmiM3 zJ+b69oeW6k6h_LS18;=vI<(Nb>uwn}Korkh+)5T}B`#%4*!Jr2l0UJ6yLy`&Kwn>N zw%~!;diec%Lui#6ZuPmq>AmAj)%C2#Kq)ouH+*U$@wae2zuopdL`sRvn__yI^oQ09 z&CA@Kb!2eGx&qU%p)GnJ_VM8;S&Vv2TJ?5wJ(q6;aihp`Xf=k9@71p*5bb@H+`7+G zMy#>nO2D-w2DzrTdkH~cM^Y}$qzAndT0NE>=%y&JP*k@Dj`gy4pTAbgXQ#K0M_@ zZl3j`Ka1Kv?*17io)Yv;ayy9x(gwCI^wai7Q6g{TV;pO2#7F%8_l;k<`vbUyl~^~d0LHgQ-CoC*SJu*nDz6R#E-B!|zY>0NosmMJpoj?F3z@7{<)(65yLVF5mj1iu45 z#dkEQQ7ocFPRoV1)5nR2_3!YUTsZ*8EEsdEV(*vI)rGe0#l%YEKKq*=kT|S7VpTZu zWY8b?MhoO0^#^lBm`ErR6!40sw%_7C+L=;mzRL4|fZAgE80Y%92z+Cx)SB^B(l1aAMFT1BtC7--_k%4e7J*;3D2b*v ze){s6OUa~=CdBWGgKLvlIB!v};tEf+T?S2Ek%(wAtjNLg!a8AN-IIiN=JXU}N_$i2 z)9X8>-7DG039mwu4t@#+=+aA$uzVgQd$A#%zQ8uiMMeDSViQ8;dm&gAHdg?Ie?Yaj zMjo`jv14d7EG$RGvOJh^pK}c2;ba93DA}GLq`Ucy95pWZU4}Z-QA*srdcJWH0)BNpdw9d!^Q8W+4@8DvNwpkXeFd6^5woO$`fbIJ`-ucbDE9FgXCajF?EN~* z$9HdDKN&J$rL{!AFWa|_rY;9>+QDI@o#TdcWt7!w@_FuFrI?*2 z+}n-LIYsg-dh4#tU$?_LA%rYQp<>lbk07@RU47*m=cSD3ZRlZZHDK9>6rJc$!QQYEC9|HRa*{+m*wlAQ zZ43c7jk1sM0atF5hWUI=tJq~nN`GRbZLr9!Ebz|t%`mwd5LZph@X~Ik$6Y7E(f8{pwt~72Q@m$OWaZCMpGEb5}q0xb8gqzubC^14KIidNKk4+!_Ar z1lC@F`vwR-aysq~j%WH(jJ9+CV0(d0A+(afkNTN1{R1mhwqEzC)cr%9Yi<57rYfqhu3kQkB&?8^)kNTNh@sEY&{ghShIw2Tm!U{8;=j8dqEj=@`2wWyVhX%k@NS-;(%(Q>n)0|UZbUL^xMsWg(W zr&iEs8)`_M#y0t{7fagEo97*}V+5aHw(|siUmR>*((+oXm<8rMd)|y_!!rcFSGUx6 zV|&6eI)ZBbE4iVB(hI5O^3~UPUjOWb2ZC=x$xmP9NPt1ce3KrSkR8E7TiVxF+`fa3 zyV&j;`78-;SRM?H$iPDt4N>LTlA;8tO3Tn9vE|u8W+(C{1mGkCBy9&IK-bn{S;^Vi=lmGYZXNqQgvOmS zJ_fQba8_L6WKQiQaScb)Y+G&!kHq1T!^`#NS5D*=9M&EkGB>1BcLrCr49zz1XtQY1 z19yH>T}!=ngYZ`LNTpu~hoq9o?@N><+=+e%UGHhZ+)_Af;gg;suCHZC$5aMma)C-p zz$R4?{iz&5bOxS^+YLVIrw%XsjQEbPVZQ@l_f|zdi4tHdw(61zpc90EV`b5?H6D%| z_L*)9i?K&!X|wzhJN|x^r=IuNr{bf=*5MfXy2Bg-&L?sFOJzGwe`h+6D+QqvJjVsD zd=AD(K283DQFaJzp5Wf6N#VYDQL8{aeXtGl7@y$zSwZ(>SQLd?^yiI2?^i-fIBl+P zk$YW0WHGlkzZBFPsChQSe3Pmg%kCG7y*63iMk!P;X6D^KIifQ~k?=)aEe z5>T(S#^@1xPP05mbENqxc1%anPH#3rgFAb;qlD|G%twQ`fFKw%9}pA7JnbgvN3xp+cdpxRCeAKtE7Ww9+X~Bym>K$T z&_2SW=XuZCv)>#XFIPF=+g%>|;{!+yv)>`P!pA#&3yGnepT_}9N8IAhHGkuylUc$;IyG2%R(_mHew&2~;_ejz1Y1<_CM<*mrIz%x0_qa>muuE&~3y%%)pI{yj;GDwM{#{#pHY4I_=eN*ho|g zBK*(j5AB1i$HOw~D?)g=>7&tnRo5sK1bIIA6QG3c_-d*+O&FKA8(;ah^rC*9IiFX( z&kLSk@(VAzp^~$&S>LntMzL{Y4vyS$w%>?U^sd+gBl22+r;!y}KH%U)oeT<|{M z>n+azDjr;+{A#wvJTF6_<(#dxOd+MAVHlY57Obw0A>5kXW%fl|jL%+04UFyRQ9f@( z5;laUfSdrj+xtWlpn1dh{Wu@UM0DmZCtL_xQkspQkJgo!@3Zg+igBVyEPOm~Ct zlTW8f+zVwRPU^xb&Q@P6J=caa6zs?$TR4-@@rof(K3mOZ^!D>iWJTqB=B09zF?lJ5 zH+)pHn+Y2_3+sze&mEGEwCmoT=TuSk{ENGAZeN2RxIPcV;||Em8%J-;s|*d?BM@Z-zO{?OG|xR@tZ*FRrWzM2|p05 zeH!ccTN`RK{wQ6f5YyjNpU&)IdMzK$-y~tNtY4q$8E2*&2p6cKukhO-^;ez^`({nMay(ry4!gX#zeCv-QuD?^O;|#;+xm{v}AIoRgQ@|!@ zCWZ6Qf#oA!j4f?6ITSU78(O^FVJ2T6PIIBJ+#x*SoAA5H@0@{;{JK`RF2)a+J7)at z*X6!iW!a{?MAXFn@PqHB^U!U|jLSk~JczCFD2b8BHKj;{dG%%?P1q-1VRTE{#xJee zI`@26OyP$@Ur%-mrCB(Hk(QKnDe^PH%k-Md6ZJD{o2)~dzQLajg~poBosA$jz6`-q zuQN9>+H5-h*14`LG7JB${sCo?8@X&A4=-)M_H)UjIRE`*fl=oZJ!Pp0cT*F@<%1V* z&<}FwBALnwRf5-E`fIXTnD28{hwKN{mh{d!j&^DHV_&lc{UWn3VxHkF=j=(t>Ye8h zZ}6<4udi&MirH=3gnR`TP@L}(A@ugE@%OOh|{$iY9=u;9}j6|AH!C?3v|KsD2J z->rytHqwAx6wYC;QZ_{fmg?)_Cp>8fwD<|c?%b@t>gvu|KyzOe=nzB&DQ>$!zaPQ0 zzwNIDT@}~Adm&$!zNxLeK+b*0P**T&6_^#{8C?mkWKEPb_ls?x#m zBMU7I0ByMwNd%ctfk4My;gQFKb1

OGYKIA%pbg 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 z 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�A(0h;A4&H?+<06tZyK3*#fN!?9Wh&Q$TTXmW3J9uX}-8MefV*55D( zCXs*D8S=__t%SqnMXH;kBoM?q*YV!iZH|!uttCLWch9;JuxT4~83K@PH0ltXd!J_- znON`+ZS!0T&q8si+USbqAFfi1(Fz8ys#VTj_xT>WlyS5KLQc9NYMOw)1c`M=Js0)M zq_GWGBaBHHokKA5rE|P)icVKIU+gw;&j5Jckn?tta@Kxi?SF5C4M5{6jx6JRO|XH2 z%^OJlar<#-32p0_IzXd~Spz~ZOm2Fo^y>&St>Gdv%jEP+Bg;?P1U{X-=qcyj;$1v! zJrtKX8&F9(Dh^hher+IYy1-(!SAp)ayea|510>Cg6Wm;P;=OY4xOk{>Xh$ LlCF_74*GupM_txQ literal 0 HcmV?d00001 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/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/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