shell: backends: Add RPMsg shell backend

Introduce a backend for the Shell subsystem using a RPMsg endpoint.

This is useful for tooling & testing a remote processor from linux.

Signed-off-by: Pieter De Gendt <pieter.degendt@basalte.be>
diff --git a/include/zephyr/shell/shell_rpmsg.h b/include/zephyr/shell/shell_rpmsg.h
new file mode 100644
index 0000000..7a79c74
--- /dev/null
+++ b/include/zephyr/shell/shell_rpmsg.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2024 Basalte bv
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef SHELL_RPMSG_H__
+#define SHELL_RPMSG_H__
+
+#include <zephyr/kernel.h>
+#include <zephyr/shell/shell.h>
+#include <openamp/rpmsg.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern const struct shell_transport_api shell_rpmsg_transport_api;
+
+/** RPMsg received message placeholder */
+struct shell_rpmsg_rx {
+	/** Pointer to the data held by RPMsg endpoint */
+	void *data;
+	/** The length of the data */
+	size_t len;
+};
+
+/** RPMsg-based shell transport. */
+struct shell_rpmsg {
+	/** Handler function registered by shell. */
+	shell_transport_handler_t shell_handler;
+
+	/** Context registered by shell. */
+	void *shell_context;
+
+	/** Indicator if we are ready to read/write */
+	bool ready;
+
+	/** Setting for blocking mode */
+	bool blocking;
+
+	/** RPMsg endpoint */
+	struct rpmsg_endpoint ept;
+
+	/** Queue for received data. */
+	struct k_msgq rx_q;
+
+	/** Buffer for received messages */
+	struct shell_rpmsg_rx rx_buf[CONFIG_SHELL_RPMSG_MAX_RX];
+
+	/** The current rx message */
+	struct shell_rpmsg_rx rx_cur;
+
+	/** The number of bytes consumed from rx_cur */
+	size_t rx_consumed;
+};
+
+#define SHELL_RPMSG_DEFINE(_name)					\
+	static struct shell_rpmsg _name##_shell_rpmsg;			\
+	struct shell_transport _name = {				\
+		.api = &shell_rpmsg_transport_api,			\
+		.ctx = (struct shell_rpmsg *)&_name##_shell_rpmsg,	\
+	}
+
+/**
+ * @brief Initialize the Shell backend using the provided @p rpmsg_dev device.
+ *
+ * @param rpmsg_dev A pointer to an RPMsg device
+ * @return 0 on success or a negative value on error
+ */
+int shell_backend_rpmsg_init_transport(struct rpmsg_device *rpmsg_dev);
+
+/**
+ * @brief This function provides pointer to shell RPMsg backend instance.
+ *
+ * Function returns pointer to the shell RPMsg instance. This instance can be
+ * next used with shell_execute_cmd function in order to test commands behavior.
+ *
+ * @returns Pointer to the shell instance.
+ */
+const struct shell *shell_backend_rpmsg_get_ptr(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SHELL_RPMSG_H__ */
diff --git a/subsys/shell/backends/CMakeLists.txt b/subsys/shell/backends/CMakeLists.txt
index f6d87f2..7711996 100644
--- a/subsys/shell/backends/CMakeLists.txt
+++ b/subsys/shell/backends/CMakeLists.txt
@@ -24,3 +24,8 @@
   CONFIG_SHELL_BACKEND_MQTT
   shell_mqtt.c
 )
+
+zephyr_sources_ifdef(
+  CONFIG_SHELL_BACKEND_RPMSG
+  shell_rpmsg.c
+)
diff --git a/subsys/shell/backends/Kconfig.backends b/subsys/shell/backends/Kconfig.backends
index f4c245c..e47e3e0 100644
--- a/subsys/shell/backends/Kconfig.backends
+++ b/subsys/shell/backends/Kconfig.backends
@@ -354,6 +354,91 @@
 
 endif # SHELL_BACKEND_MQTT
 
+config SHELL_BACKEND_RPMSG
+	bool "RPMsg backend."
+	depends on OPENAMP
+	help
+	  Enable RPMsg backend.
+
+if SHELL_BACKEND_RPMSG
+
+config SHELL_PROMPT_RPMSG
+	string "Displayed prompt name"
+	default "ipc:~$ "
+	help
+	  Displayed prompt name for RPMsg backend. If prompt is set, the shell will
+	  send two newlines during initialization.
+
+config SHELL_RPMSG_SERVICE_NAME
+	string "Service name"
+	default "rpmsg-tty"
+	help
+	  The service name associated with the RPMsg endpoint.
+
+config SHELL_RPMSG_SRC_ADDR
+	hex "Local address"
+	default 0xffffffff # The ANY address
+	help
+	  Local address of the RPMsg endpoint.
+
+config SHELL_RPMSG_DST_ADDR
+	hex "Remote address"
+	default 0xffffffff # The ANY address
+	help
+	  Target address of the RPMsg endpoint.
+
+config SHELL_RPMSG_MAX_RX
+	int "Receive buffer size"
+	default 10
+	help
+	  The maximum number of received messages to be queued.
+
+module = SHELL_BACKEND_RPMSG
+default-timeout = 100
+source "subsys/shell/Kconfig.template.shell_log_queue_timeout"
+
+default-size = 512
+source "subsys/shell/Kconfig.template.shell_log_queue_size"
+
+choice
+	prompt "Initial log level limit"
+	default SHELL_RPMSG_INIT_LOG_LEVEL_DEFAULT
+
+config SHELL_RPMSG_INIT_LOG_LEVEL_DEFAULT
+	bool "System limit (LOG_MAX_LEVEL)"
+
+config SHELL_RPMSG_INIT_LOG_LEVEL_DBG
+	bool "Debug"
+
+config SHELL_RPMSG_INIT_LOG_LEVEL_INF
+	bool "Info"
+
+config SHELL_RPMSG_INIT_LOG_LEVEL_WRN
+	bool "Warning"
+
+config SHELL_RPMSG_INIT_LOG_LEVEL_ERR
+	bool "Error"
+
+config SHELL_RPMSG_INIT_LOG_LEVEL_NONE
+	bool "None"
+
+endchoice # SHELL_RPMSG_INIT_LOG_LEVEL
+
+config SHELL_RPMSG_INIT_LOG_LEVEL
+	int
+	default 0 if SHELL_RPMSG_INIT_LOG_LEVEL_NONE
+	default 1 if SHELL_RPMSG_INIT_LOG_LEVEL_ERR
+	default 2 if SHELL_RPMSG_INIT_LOG_LEVEL_WRN
+	default 3 if SHELL_RPMSG_INIT_LOG_LEVEL_INF
+	default 4 if SHELL_RPMSG_INIT_LOG_LEVEL_DBG
+	default 5 if SHELL_RPMSG_INIT_LOG_LEVEL_DEFAULT
+
+module = SHELL_RPMSG
+module-str = RPMsg shell backend
+source "subsys/logging/Kconfig.template.log_config"
+
+endif # SHELL_BACKEND_RPMSG
+
 config SHELL_BACKEND_TELNET
 	bool "TELNET backend."
 	depends on NET_TCP
diff --git a/subsys/shell/backends/shell_rpmsg.c b/subsys/shell/backends/shell_rpmsg.c
new file mode 100644
index 0000000..846b73a
--- /dev/null
+++ b/subsys/shell/backends/shell_rpmsg.c
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2024 Basalte bv
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/shell/shell_rpmsg.h>
+
+SHELL_RPMSG_DEFINE(shell_transport_rpmsg);
+SHELL_DEFINE(shell_rpmsg, CONFIG_SHELL_PROMPT_RPMSG, &shell_transport_rpmsg,
+	     CONFIG_SHELL_BACKEND_RPMSG_LOG_MESSAGE_QUEUE_SIZE,
+	     CONFIG_SHELL_BACKEND_RPMSG_LOG_MESSAGE_QUEUE_TIMEOUT, SHELL_FLAG_OLF_CRLF);
+
+static int rpmsg_shell_cb(struct rpmsg_endpoint *ept, void *data,
+			  size_t len, uint32_t src, void *priv)
+{
+	const struct shell_transport *transport = (const struct shell_transport *)priv;
+	struct shell_rpmsg *sh_rpmsg = (struct shell_rpmsg *)transport->ctx;
+	struct shell_rpmsg_rx rx;
+
+	if (len == 0) {
+		return RPMSG_ERR_NO_BUFF;
+	}
+
+	rx.data = data;
+	rx.len = len;
+	if (k_msgq_put(&sh_rpmsg->rx_q, &rx, K_NO_WAIT) != 0) {
+		return RPMSG_ERR_NO_MEM;
+	}
+
+	rpmsg_hold_rx_buffer(ept, data);
+	sh_rpmsg->shell_handler(SHELL_TRANSPORT_EVT_RX_RDY, sh_rpmsg->shell_context);
+
+	return RPMSG_SUCCESS;
+}
+
+static int uninit(const struct shell_transport *transport)
+{
+	struct shell_rpmsg *sh_rpmsg = (struct shell_rpmsg *)transport->ctx;
+
+	if (!sh_rpmsg->ready) {
+		return -ENODEV;
+	}
+
+	rpmsg_destroy_ept(&sh_rpmsg->ept);
+	sh_rpmsg->ready = false;
+
+	return 0;
+}
+
+static int init(const struct shell_transport *transport,
+		const void *config,
+		shell_transport_handler_t evt_handler,
+		void *context)
+{
+	struct shell_rpmsg *sh_rpmsg = (struct shell_rpmsg *)transport->ctx;
+	struct rpmsg_device *rdev;
+	int ret;
+
+	if (sh_rpmsg->ready) {
+		return -EALREADY;
+	}
+
+	if (config == NULL) {
+		return -EINVAL;
+	}
+	rdev = (struct rpmsg_device *)config;
+
+	k_msgq_init(&sh_rpmsg->rx_q, (char *)sh_rpmsg->rx_buf, sizeof(struct shell_rpmsg_rx),
+		    CONFIG_SHELL_RPMSG_MAX_RX);
+
+	ret = rpmsg_create_ept(&sh_rpmsg->ept, rdev, CONFIG_SHELL_RPMSG_SERVICE_NAME,
+			       CONFIG_SHELL_RPMSG_SRC_ADDR, CONFIG_SHELL_RPMSG_DST_ADDR,
+			       rpmsg_shell_cb, NULL);
+	if (ret < 0) {
+		return ret;
+	}
+
+	sh_rpmsg->ept.priv = (void *)transport;
+
+	sh_rpmsg->shell_handler = evt_handler;
+	sh_rpmsg->shell_context = context;
+	sh_rpmsg->ready = true;
+
+	return 0;
+}
+
+static int enable(const struct shell_transport *transport, bool blocking)
+{
+	struct shell_rpmsg *sh_rpmsg = (struct shell_rpmsg *)transport->ctx;
+
+	if (!sh_rpmsg->ready) {
+		return -ENODEV;
+	}
+
+	sh_rpmsg->blocking = blocking;
+
+	return 0;
+}
+
+static int write(const struct shell_transport *transport,
+		 const void *data, size_t length, size_t *cnt)
+{
+	struct shell_rpmsg *sh_rpmsg = (struct shell_rpmsg *)transport->ctx;
+	int ret;
+
+	*cnt = 0;
+
+	if (!sh_rpmsg->ready) {
+		return -ENODEV;
+	}
+
+	if (sh_rpmsg->blocking) {
+		ret = rpmsg_send(&sh_rpmsg->ept, data, (int)length);
+	} else {
+		ret = rpmsg_trysend(&sh_rpmsg->ept, data, (int)length);
+	}
+
+	/* Set TX ready in any case, as we have no way to recover otherwise */
+	sh_rpmsg->shell_handler(SHELL_TRANSPORT_EVT_TX_RDY, sh_rpmsg->shell_context);
+
+	if (ret < 0) {
+		return ret;
+	}
+
+	*cnt = (size_t)ret;
+
+	return 0;
+}
+
+static int read(const struct shell_transport *transport,
+		void *data, size_t length, size_t *cnt)
+{
+	struct shell_rpmsg *sh_rpmsg = (struct shell_rpmsg *)transport->ctx;
+	struct shell_rpmsg_rx *rx = &sh_rpmsg->rx_cur;
+	size_t read_len;
+	bool release = true;
+
+	if (!sh_rpmsg->ready) {
+		return -ENODEV;
+	}
+
+	/* Check if we still have pending data */
+	if (rx->data == NULL) {
+		int ret = k_msgq_get(&sh_rpmsg->rx_q, rx, K_NO_WAIT);
+
+		if (ret < 0) {
+			rx->data = NULL;
+			goto no_data;
+		}
+
+		__ASSERT_NO_MSG(rx->len > 0);
+		sh_rpmsg->rx_consumed = 0;
+	}
+
+	__ASSERT_NO_MSG(rx->len > sh_rpmsg->rx_consumed);
+	read_len = rx->len - sh_rpmsg->rx_consumed;
+	if (read_len > length) {
+		read_len = length;
+		release = false;
+	}
+
+	*cnt = read_len;
+	memcpy(data, &((char *)rx->data)[sh_rpmsg->rx_consumed], read_len);
+
+	if (release) {
+		rpmsg_release_rx_buffer(&sh_rpmsg->ept, rx->data);
+		rx->data = NULL;
+	} else {
+		sh_rpmsg->rx_consumed += read_len;
+	}
+
+	return 0;
+
+no_data:
+	*cnt = 0;
+	return 0;
+}
+
+const struct shell_transport_api shell_rpmsg_transport_api = {
+	.init = init,
+	.uninit = uninit,
+	.enable = enable,
+	.read = read,
+	.write = write,
+};
+
+int shell_backend_rpmsg_init_transport(struct rpmsg_device *rpmsg_dev)
+{
+	bool log_backend = CONFIG_SHELL_RPMSG_INIT_LOG_LEVEL > 0;
+	uint32_t level = (CONFIG_SHELL_RPMSG_INIT_LOG_LEVEL > LOG_LEVEL_DBG) ?
+		      CONFIG_LOG_MAX_LEVEL : CONFIG_SHELL_RPMSG_INIT_LOG_LEVEL;
+	static const struct shell_backend_config_flags cfg_flags =
+					SHELL_DEFAULT_BACKEND_CONFIG_FLAGS;
+
+	return shell_init(&shell_rpmsg, rpmsg_dev, cfg_flags, log_backend, level);
+}
+
+const struct shell *shell_backend_rpmsg_get_ptr(void)
+{
+	return &shell_rpmsg;
+}