samples: drivers: uart: Add a 'passthrough' example

This sample can be used as a "virtual wire", allowing direct access to
devices connected to the target processor - for example: GPS, Cellular,
etc...

This is also useful as a utility - no other connectivity options exist
for UART, where other interfaces like SPI and I2C have shell commands.

Signed-off-by: Attie Grande <attie.grande@argentum-systems.co.uk>
diff --git a/samples/drivers/uart/passthrough/CMakeLists.txt b/samples/drivers/uart/passthrough/CMakeLists.txt
new file mode 100644
index 0000000..ca7c7f7
--- /dev/null
+++ b/samples/drivers/uart/passthrough/CMakeLists.txt
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: Apache-2.0
+
+cmake_minimum_required(VERSION 3.20.0)
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+project(passthrough)
+
+target_sources(app PRIVATE src/main.c)
diff --git a/samples/drivers/uart/passthrough/README.rst b/samples/drivers/uart/passthrough/README.rst
new file mode 100644
index 0000000..94d58f3
--- /dev/null
+++ b/samples/drivers/uart/passthrough/README.rst
@@ -0,0 +1,50 @@
+.. zephyr:code-sample:: uart-passthrough
+   :name: UART Passthrough
+   :relevant-api: uart_interface
+
+   Pass data directly between the console and another UART interface.
+
+Overview
+********
+
+This sample will virtually connect two UART interfaces together, as if Zephyr
+and the processor were not present. Data read from the console is transmitted
+to the "*other*" interface, and data read from the "*other*" interface is
+relayed to the console.
+
+The source code for this sample application can be found at:
+:zephyr_file:`samples/drivers/uart/passthrough`.
+
+Requirements
+************
+
+#. One UART interface, identified as Zephyr's console.
+#. A second UART connected to something interesting (e.g: GPS), identified as
+   the chosen ``uart,passthrough`` device - the pins and baudrate will need to
+   be configured correctly.
+
+Building and Running
+********************
+
+Build and flash the sample as follows, changing ``nucleo_l476rg`` for your
+board:
+
+.. zephyr-app-commands::
+   :zephyr-app: samples/drivers/uart/passthrough
+   :board: nucleo_l476rg
+   :goals: build flash
+   :compact:
+
+Sample Output
+=============
+
+.. code-block:: console
+
+    *** Booting Zephyr OS build zephyr-v3.5.0-2988-gb84bab36b941 ***
+    Console Device: 0x8003940
+    Other Device:   0x800392c
+    $GNGSA,A,3,31,29,25,26,,,,,,,,,11.15,10.66,3.29,1*06
+    $GNGSA,A,3,,,,,,,,,,,,,11.15,10.66,3.29,2*0F
+    $GNGSA,A,3,,,,,,,,,,,,,11.15,10.66,3.29,3*0E
+    $GNGSA,A,3,,,,,,,,,,,,,11.15,10.66,3.29,4*09
+    $GNGSA,A,3,,,,,,,,,,,,,11.15,10.66,3.29,5*08
diff --git a/samples/drivers/uart/passthrough/boards/nucleo_l476rg.overlay b/samples/drivers/uart/passthrough/boards/nucleo_l476rg.overlay
new file mode 100644
index 0000000..5292b54
--- /dev/null
+++ b/samples/drivers/uart/passthrough/boards/nucleo_l476rg.overlay
@@ -0,0 +1,12 @@
+/ {
+	chosen {
+		uart,passthrough = &uart4;
+	};
+};
+
+&uart4 {
+	pinctrl-0 = <&uart4_tx_pa0 &uart4_rx_pa1>;
+	pinctrl-names = "default";
+	current-speed = <9600>;
+	status = "okay";
+};
diff --git a/samples/drivers/uart/passthrough/prj.conf b/samples/drivers/uart/passthrough/prj.conf
new file mode 100644
index 0000000..70eec2f
--- /dev/null
+++ b/samples/drivers/uart/passthrough/prj.conf
@@ -0,0 +1,3 @@
+CONFIG_SERIAL=y
+CONFIG_RING_BUFFER=y
+CONFIG_UART_INTERRUPT_DRIVEN=y
diff --git a/samples/drivers/uart/passthrough/sample.yml b/samples/drivers/uart/passthrough/sample.yml
new file mode 100644
index 0000000..5e4abf9
--- /dev/null
+++ b/samples/drivers/uart/passthrough/sample.yml
@@ -0,0 +1,14 @@
+sample:
+  name: UART Passthrough
+tests:
+  sample.drivers.uart:
+    integration_platforms:
+      - qemu_x86
+    tags:
+      - serial
+      - uart
+    filter: CONFIG_SERIAL and
+            CONFIG_UART_INTERRUPT_DRIVEN and
+            dt_chosen_enabled("zephyr,console") and
+            dt_chosen_enabled("uart,passthrough")
+    harness: keyboard
diff --git a/samples/drivers/uart/passthrough/src/main.c b/samples/drivers/uart/passthrough/src/main.c
new file mode 100644
index 0000000..2168207
--- /dev/null
+++ b/samples/drivers/uart/passthrough/src/main.c
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2024 Argentum Systems Ltd.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/kernel.h>
+#include <zephyr/device.h>
+#include <zephyr/drivers/uart.h>
+#include <zephyr/sys/ring_buffer.h>
+
+#include <stdio.h>
+#include <string.h>
+
+struct patch_info {
+	const uint8_t * const name;
+
+	const struct device *rx_dev;
+	struct ring_buf *rx_ring_buf;
+	bool rx_error;
+	bool rx_overflow;
+
+	const struct device *tx_dev;
+};
+
+#define DEV_CONSOLE DEVICE_DT_GET(DT_CHOSEN(zephyr_console))
+#define DEV_OTHER   DEVICE_DT_GET(DT_CHOSEN(uart_passthrough))
+
+#define RING_BUF_SIZE 64
+
+RING_BUF_DECLARE(rb_console, RING_BUF_SIZE);
+struct patch_info patch_c2o = {
+	.name = "c2o",
+
+	.rx_dev = DEV_CONSOLE,
+	.rx_ring_buf = &rb_console,
+	.rx_error = false,
+	.rx_overflow = false,
+
+	.tx_dev = DEV_OTHER,
+};
+
+RING_BUF_DECLARE(rb_other, RING_BUF_SIZE);
+struct patch_info patch_o2c = {
+	.name = "o2c",
+
+	.rx_dev = DEV_OTHER,
+	.rx_ring_buf = &rb_other,
+	.rx_error = false,
+	.rx_overflow = false,
+
+	.tx_dev = DEV_CONSOLE,
+};
+
+static void uart_cb(const struct device *dev, void *ctx)
+{
+	struct patch_info *patch = (struct patch_info *)ctx;
+	int ret;
+	uint8_t *buf;
+	uint32_t len;
+
+	while (uart_irq_update(patch->rx_dev) > 0) {
+		ret = uart_irq_rx_ready(patch->rx_dev);
+		if (ret < 0) {
+			patch->rx_error = true;
+		}
+		if (ret <= 0) {
+			break;
+		}
+
+		len = ring_buf_put_claim(patch->rx_ring_buf, &buf, RING_BUF_SIZE);
+		if (len == 0) {
+			/* no space for Rx, disable the IRQ */
+			uart_irq_rx_disable(patch->rx_dev);
+			patch->rx_overflow = true;
+			break;
+		}
+
+		ret = uart_fifo_read(patch->rx_dev, buf, len);
+		if (ret < 0) {
+			patch->rx_error = true;
+		}
+		if (ret <= 0) {
+			break;
+		}
+		len = ret;
+
+		ret = ring_buf_put_finish(patch->rx_ring_buf, len);
+		if (ret != 0) {
+			patch->rx_error = true;
+			break;
+		}
+	}
+}
+
+static void passthrough(struct patch_info *patch)
+{
+	int ret;
+	uint8_t *buf;
+	uint32_t len;
+
+	if (patch->rx_error) {
+		printk("<<%s: Rx Error!>>\n", patch->name);
+		patch->rx_error = false;
+	}
+
+	if (patch->rx_overflow) {
+		printk("<<%s: Rx Overflow!>>\n", patch->name);
+		patch->rx_overflow = false;
+	}
+
+	len = ring_buf_get_claim(patch->rx_ring_buf, &buf, RING_BUF_SIZE);
+	if (len == 0) {
+		goto done;
+	}
+
+	ret = uart_fifo_fill(patch->tx_dev, buf, len);
+	if (ret < 0) {
+		goto error;
+	}
+	len = ret;
+
+	ret = ring_buf_get_finish(patch->rx_ring_buf, len);
+	if (ret < 0) {
+		goto error;
+	}
+
+done:
+	uart_irq_rx_enable(patch->rx_dev);
+	return;
+
+error:
+	printk("<<%s: Tx Error!>>\n", patch->name);
+}
+
+int main(void)
+{
+	printk("Console Device: %p\n", patch_c2o.rx_dev);
+	printk("Other Device:   %p\n", patch_o2c.rx_dev);
+
+	uart_irq_callback_user_data_set(patch_c2o.rx_dev, uart_cb, (void *)&patch_c2o);
+	uart_irq_callback_user_data_set(patch_o2c.rx_dev, uart_cb, (void *)&patch_o2c);
+
+	for (;;) {
+		passthrough(&patch_c2o);
+		passthrough(&patch_o2c);
+	}
+
+	return 0;
+}