subsys/modem: Add modem modules

This PR adds the following modem modules to the subsys/modem
folder:

- chat: Light implementation of the Linux chat program, used to
        send and receive text based commands statically created
        scripts.

- cmux: Implementation of the CMUX protocol
- pipe: Thread-safe async data-in/data-out binding layer between
        modem  modules.

- ppp: Implementation of the PPP protocol, binding the Zephyr PPP
       L2 stack with the data-in/data-out pipe.

These modules use the abstract pipes to communicate between each
other. To bind them with the hardware, the following backends
are provided:

- TTY: modem pipe <-> POSIX TTY file
- UART: modem pipe <-> UART, async and ISR APIs supported

The backends are used to abstract away the physical layer, UART,
TTY, IPC, I2C, SPI etc, to a modem modules friendly pipe.

Signed-off-by: Bjarki Arge Andreasen <baa@trackunit.com>
diff --git a/include/zephyr/modem/backend/tty.h b/include/zephyr/modem/backend/tty.h
new file mode 100644
index 0000000..f992ce0
--- /dev/null
+++ b/include/zephyr/modem/backend/tty.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/kernel.h>
+#include <zephyr/types.h>
+#include <zephyr/device.h>
+#include <zephyr/sys/ring_buffer.h>
+#include <zephyr/sys/atomic.h>
+
+#include <zephyr/modem/pipe.h>
+
+#ifndef ZEPHYR_MODEM_BACKEND_TTY_
+#define ZEPHYR_MODEM_BACKEND_TTY_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct modem_backend_tty {
+	const char *tty_path;
+	int tty_fd;
+	struct modem_pipe pipe;
+	struct k_thread thread;
+	k_thread_stack_t *stack;
+	size_t stack_size;
+	atomic_t state;
+};
+
+struct modem_backend_tty_config {
+	const char *tty_path;
+	k_thread_stack_t *stack;
+	size_t stack_size;
+};
+
+struct modem_pipe *modem_backend_tty_init(struct modem_backend_tty *backend,
+					  const struct modem_backend_tty_config *config);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_MODEM_BACKEND_TTY_ */
diff --git a/include/zephyr/modem/backend/uart.h b/include/zephyr/modem/backend/uart.h
new file mode 100644
index 0000000..600d543
--- /dev/null
+++ b/include/zephyr/modem/backend/uart.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/kernel.h>
+#include <zephyr/types.h>
+#include <zephyr/device.h>
+#include <zephyr/drivers/uart.h>
+#include <zephyr/sys/ring_buffer.h>
+#include <zephyr/sys/atomic.h>
+
+#include <zephyr/modem/pipe.h>
+
+#ifndef ZEPHYR_MODEM_BACKEND_UART_
+#define ZEPHYR_MODEM_BACKEND_UART_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct modem_backend_uart_isr {
+	struct ring_buf receive_rdb[2];
+	struct ring_buf transmit_rb;
+	atomic_t transmit_buf_len;
+	uint8_t receive_rdb_used;
+	uint32_t transmit_buf_put_limit;
+};
+
+struct modem_backend_uart_async {
+	uint8_t *receive_bufs[2];
+	uint32_t receive_buf_size;
+	struct ring_buf receive_rdb[2];
+	uint8_t *transmit_buf;
+	uint32_t transmit_buf_size;
+	atomic_t state;
+};
+
+struct modem_backend_uart {
+	const struct device *uart;
+	struct modem_pipe pipe;
+	struct k_work receive_ready_work;
+
+	union {
+		struct modem_backend_uart_isr isr;
+		struct modem_backend_uart_async async;
+	};
+};
+
+struct modem_backend_uart_config {
+	const struct device *uart;
+	uint8_t *receive_buf;
+	uint32_t receive_buf_size;
+	uint8_t *transmit_buf;
+	uint32_t transmit_buf_size;
+};
+
+struct modem_pipe *modem_backend_uart_init(struct modem_backend_uart *backend,
+					   const struct modem_backend_uart_config *config);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_MODEM_BACKEND_UART_ */
diff --git a/include/zephyr/modem/chat.h b/include/zephyr/modem/chat.h
new file mode 100644
index 0000000..5f2ff4a
--- /dev/null
+++ b/include/zephyr/modem/chat.h
@@ -0,0 +1,310 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/kernel.h>
+#include <zephyr/types.h>
+#include <zephyr/device.h>
+#include <zephyr/sys/ring_buffer.h>
+
+#include <zephyr/modem/pipe.h>
+
+#ifndef ZEPHYR_MODEM_CHAT_
+#define ZEPHYR_MODEM_CHAT_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct modem_chat;
+
+/**
+ * @brief Callback called when matching chat is received
+ *
+ * @param chat Pointer to chat instance instance
+ * @param argv Pointer to array of parsed arguments
+ * @param argc Number of parsed arguments, arg 0 holds the exact match
+ * @param user_data Free to use user data set during modem_chat_init()
+ */
+typedef void (*modem_chat_match_callback)(struct modem_chat *chat, char **argv, uint16_t argc,
+					  void *user_data);
+
+/**
+ * @brief Modem chat match
+ */
+struct modem_chat_match {
+	/* Match array */
+	const uint8_t *match;
+	const uint8_t match_size;
+
+	/* Separators array */
+	const uint8_t *separators;
+	const uint8_t separators_size;
+
+	/* Set if modem chat instance shall use wildcards when matching */
+	const bool wildcards;
+
+	/* Type of modem chat instance */
+	const modem_chat_match_callback callback;
+};
+
+#define MODEM_CHAT_MATCH(_match, _separators, _callback)                                           \
+	{                                                                                          \
+		.match = (uint8_t *)(_match), .match_size = (uint8_t)(sizeof(_match) - 1),         \
+		.separators = (uint8_t *)(_separators),                                            \
+		.separators_size = (uint8_t)(sizeof(_separators) - 1), .wildcards = false,         \
+		.callback = _callback,                                                             \
+	}
+
+#define MODEM_CHAT_MATCH_WILDCARD(_match, _separators, _callback)                                  \
+	{                                                                                          \
+		.match = (uint8_t *)(_match), .match_size = (uint8_t)(sizeof(_match) - 1),         \
+		.separators = (uint8_t *)(_separators),                                            \
+		.separators_size = (uint8_t)(sizeof(_separators) - 1), .wildcards = true,          \
+		.callback = _callback,                                                             \
+	}
+
+#define MODEM_CHAT_MATCH_DEFINE(_sym, _match, _separators, _callback)                              \
+	const static struct modem_chat_match _sym = MODEM_CHAT_MATCH(_match, _separators, _callback)
+
+#define MODEM_CHAT_MATCHES_DEFINE(_sym, ...)                                                       \
+	const static struct modem_chat_match _sym[] = {__VA_ARGS__}
+
+/**
+ * @brief Modem chat script chat
+ */
+struct modem_chat_script_chat {
+	/** Request to send to modem formatted as char string */
+	const char *request;
+	/** Expected responses to request */
+	const struct modem_chat_match *const response_matches;
+	/** Number of elements in expected responses */
+	const uint16_t response_matches_size;
+	/** Timeout before chat script may continue to next step in milliseconds */
+	uint16_t timeout;
+};
+
+#define MODEM_CHAT_SCRIPT_CMD_RESP(_request, _response_match)                                      \
+	{                                                                                          \
+		.request = _request, .response_matches = &_response_match,                         \
+		.response_matches_size = 1, .timeout = 0,                                          \
+	}
+
+#define MODEM_CHAT_SCRIPT_CMD_RESP_MULT(_request, _response_matches)                               \
+	{                                                                                          \
+		.request = _request, .response_matches = _response_matches,                        \
+		.response_matches_size = ARRAY_SIZE(_response_matches), .timeout = 0,              \
+	}
+
+#define MODEM_CHAT_SCRIPT_CMD_RESP_NONE(_request, _timeout)                                        \
+	{                                                                                          \
+		.request = _request, .response_matches = NULL, .response_matches_size = 0,         \
+		.timeout = _timeout,                                                               \
+	}
+
+#define MODEM_CHAT_SCRIPT_CMDS_DEFINE(_sym, ...)                                                   \
+	const static struct modem_chat_script_chat _sym[] = {__VA_ARGS__}
+
+enum modem_chat_script_result {
+	MODEM_CHAT_SCRIPT_RESULT_SUCCESS,
+	MODEM_CHAT_SCRIPT_RESULT_ABORT,
+	MODEM_CHAT_SCRIPT_RESULT_TIMEOUT
+};
+
+/**
+ * @brief Callback called when script chat is received
+ *
+ * @param chat Pointer to chat instance instance
+ * @param result Result of script execution
+ * @param user_data Free to use user data set during modem_chat_init()
+ */
+typedef void (*modem_chat_script_callback)(struct modem_chat *chat,
+					   enum modem_chat_script_result result, void *user_data);
+
+/**
+ * @brief Modem chat script
+ */
+struct modem_chat_script {
+	/** Name of script */
+	const char *name;
+	/** Array of script chats */
+	const struct modem_chat_script_chat *script_chats;
+	/** Elements in array of script chats */
+	const uint16_t script_chats_size;
+	/** Array of abort matches */
+	const struct modem_chat_match *const abort_matches;
+	/** Number of elements in array of abort matches */
+	const uint16_t abort_matches_size;
+	/** Callback called when script execution terminates */
+	modem_chat_script_callback callback;
+	/** Timeout in seconds within which the script execution must terminate */
+	const uint32_t timeout;
+};
+
+#define MODEM_CHAT_SCRIPT_DEFINE(_sym, _script_chats, _abort_matches, _callback, _timeout)         \
+	static struct modem_chat_script _sym = {                                                   \
+		.name = #_sym,                                                                     \
+		.script_chats = _script_chats,                                                     \
+		.script_chats_size = ARRAY_SIZE(_script_chats),                                    \
+		.abort_matches = _abort_matches,                                                   \
+		.abort_matches_size = ARRAY_SIZE(_abort_matches),                                  \
+		.callback = _callback,                                                             \
+		.timeout = _timeout,                                                               \
+	}
+
+enum modem_chat_script_send_state {
+	/* No data to send */
+	MODEM_CHAT_SCRIPT_SEND_STATE_IDLE,
+	/* Sending request */
+	MODEM_CHAT_SCRIPT_SEND_STATE_REQUEST,
+	/* Sending delimiter */
+	MODEM_CHAT_SCRIPT_SEND_STATE_DELIMITER,
+};
+
+/**
+ * @brief Chat instance internal context
+ * @warning Do not modify any members of this struct directly
+ */
+struct modem_chat {
+	/* Pipe used to send and receive data */
+	struct modem_pipe *pipe;
+
+	/* User data passed with match callbacks */
+	void *user_data;
+
+	/* Receive buffer */
+	uint8_t *receive_buf;
+	uint16_t receive_buf_size;
+	uint16_t receive_buf_len;
+
+	/* Work buffer */
+	uint8_t work_buf[32];
+	uint16_t work_buf_len;
+
+	/* Chat delimiter */
+	uint8_t *delimiter;
+	uint16_t delimiter_size;
+	uint16_t delimiter_match_len;
+
+	/* Array of bytes which are discarded out by parser */
+	uint8_t *filter;
+	uint16_t filter_size;
+
+	/* Parsed arguments */
+	uint8_t **argv;
+	uint16_t argv_size;
+	uint16_t argc;
+
+	/* Matches
+	 * Index 0 -> Response matches
+	 * Index 1 -> Abort matches
+	 * Index 2 -> Unsolicited matches
+	 */
+	const struct modem_chat_match *matches[3];
+	uint16_t matches_size[3];
+
+	/* Script execution */
+	const struct modem_chat_script *script;
+	const struct modem_chat_script *pending_script;
+	struct k_work script_run_work;
+	struct k_work_delayable script_timeout_work;
+	struct k_work script_abort_work;
+	uint16_t script_chat_it;
+	atomic_t script_state;
+
+	/* Script sending */
+	uint16_t script_send_request_pos;
+	uint16_t script_send_delimiter_pos;
+	struct k_work_delayable script_send_work;
+	struct k_work_delayable script_send_timeout_work;
+
+	/* Match parsing */
+	const struct modem_chat_match *parse_match;
+	uint16_t parse_match_len;
+	uint16_t parse_arg_len;
+	uint16_t parse_match_type;
+
+	/* Process received data */
+	struct k_work_delayable process_work;
+	k_timeout_t process_timeout;
+};
+
+/**
+ * @brief Chat configuration
+ */
+struct modem_chat_config {
+	/** Free to use user data passed with modem match callbacks */
+	void *user_data;
+	/** Receive buffer used to store parsed arguments */
+	uint8_t *receive_buf;
+	/** Size of receive buffer should be longest line + longest match */
+	uint16_t receive_buf_size;
+	/** Delimiter */
+	uint8_t *delimiter;
+	/** Size of delimiter */
+	uint8_t delimiter_size;
+	/** Bytes which are discarded by parser */
+	uint8_t *filter;
+	/** Size of filter */
+	uint8_t filter_size;
+	/** Array of pointers used to point to parsed arguments */
+	uint8_t **argv;
+	/** Elements in array of pointers */
+	uint16_t argv_size;
+	/** Array of unsolicited matches */
+	const struct modem_chat_match *unsol_matches;
+	/** Elements in array of unsolicited matches */
+	uint16_t unsol_matches_size;
+	/** Delay from receive ready event to pipe receive occurs */
+	k_timeout_t process_timeout;
+};
+
+/**
+ * @brief Initialize modem pipe chat instance
+ * @param chat Chat instance
+ * @param config Configuration which shall be applied to Chat instance
+ * @note Chat instance must be attached to pipe
+ */
+int modem_chat_init(struct modem_chat *chat, const struct modem_chat_config *config);
+
+/**
+ * @brief Attach modem chat instance to pipe
+ * @param chat Chat instance
+ * @param pipe Pipe instance to attach Chat instance to
+ * @returns 0 if successful
+ * @returns negative errno code if failure
+ * @note Chat instance is enabled if successful
+ */
+int modem_chat_attach(struct modem_chat *chat, struct modem_pipe *pipe);
+
+/**
+ * @brief Run script
+ * @param chat Chat instance
+ * @param script Script to run
+ * @returns 0 if successful
+ * @returns -EBUSY if a script is currently running
+ * @returns -EPERM if modem pipe is not attached
+ * @returns -EINVAL if arguments or script is invalid
+ * @note Script runs asynchronously until complete or aborted.
+ */
+int modem_chat_script_run(struct modem_chat *chat, const struct modem_chat_script *script);
+
+/**
+ * @brief Abort script
+ * @param chat Chat instance
+ */
+void modem_chat_script_abort(struct modem_chat *chat);
+
+/**
+ * @brief Release pipe from chat instance
+ * @param chat Chat instance
+ */
+void modem_chat_release(struct modem_chat *chat);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_MODEM_CHAT_ */
diff --git a/include/zephyr/modem/cmux.h b/include/zephyr/modem/cmux.h
new file mode 100644
index 0000000..b64c00b
--- /dev/null
+++ b/include/zephyr/modem/cmux.h
@@ -0,0 +1,267 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * This library uses CMUX to create multiple data channels, called DLCIs, on a single serial bus.
+ * Each DLCI has an address from 1 to 63. DLCI address 0 is reserved for control commands.
+ *
+ * Design overview:
+ *
+ *     DLCI1 <-----------+                              +-------> DLCI1
+ *                       v                              v
+ *     DLCI2 <---> CMUX instance <--> Serial bus <--> Client <--> DLCI2
+ *                       ^                              ^
+ *     DLCI3 <-----------+                              +-------> DLCI3
+ *
+ * Writing to and from the CMUX instances is done using the modem_pipe API.
+ */
+
+#include <zephyr/kernel.h>
+#include <zephyr/types.h>
+#include <zephyr/sys/ring_buffer.h>
+#include <zephyr/sys/atomic.h>
+
+#include <zephyr/modem/pipe.h>
+
+#ifndef ZEPHYR_MODEM_CMUX_
+#define ZEPHYR_MODEM_CMUX_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct modem_cmux;
+
+enum modem_cmux_state {
+	MODEM_CMUX_STATE_DISCONNECTED = 0,
+	MODEM_CMUX_STATE_CONNECTING,
+	MODEM_CMUX_STATE_CONNECTED,
+	MODEM_CMUX_STATE_DISCONNECTING,
+};
+
+enum modem_cmux_event {
+	MODEM_CMUX_EVENT_CONNECTED = 0,
+	MODEM_CMUX_EVENT_DISCONNECTED,
+};
+
+typedef void (*modem_cmux_callback)(struct modem_cmux *cmux, enum modem_cmux_event event,
+				    void *user_data);
+
+enum modem_cmux_receive_state {
+	MODEM_CMUX_RECEIVE_STATE_SOF = 0,
+	MODEM_CMUX_RECEIVE_STATE_RESYNC_0,
+	MODEM_CMUX_RECEIVE_STATE_RESYNC_1,
+	MODEM_CMUX_RECEIVE_STATE_RESYNC_2,
+	MODEM_CMUX_RECEIVE_STATE_RESYNC_3,
+	MODEM_CMUX_RECEIVE_STATE_ADDRESS,
+	MODEM_CMUX_RECEIVE_STATE_ADDRESS_CONT,
+	MODEM_CMUX_RECEIVE_STATE_CONTROL,
+	MODEM_CMUX_RECEIVE_STATE_LENGTH,
+	MODEM_CMUX_RECEIVE_STATE_LENGTH_CONT,
+	MODEM_CMUX_RECEIVE_STATE_DATA,
+	MODEM_CMUX_RECEIVE_STATE_FCS,
+	MODEM_CMUX_RECEIVE_STATE_DROP,
+	MODEM_CMUX_RECEIVE_STATE_EOF,
+};
+
+enum modem_cmux_dlci_state {
+	MODEM_CMUX_DLCI_STATE_CLOSED,
+	MODEM_CMUX_DLCI_STATE_OPENING,
+	MODEM_CMUX_DLCI_STATE_OPEN,
+	MODEM_CMUX_DLCI_STATE_CLOSING,
+};
+
+enum modem_cmux_dlci_event {
+	MODEM_CMUX_DLCI_EVENT_OPENED,
+	MODEM_CMUX_DLCI_EVENT_CLOSED,
+};
+
+struct modem_cmux_dlci {
+	sys_snode_t node;
+
+	/* Pipe */
+	struct modem_pipe pipe;
+
+	/* Context */
+	uint16_t dlci_address;
+	struct modem_cmux *cmux;
+
+	/* Receive buffer */
+	struct ring_buf receive_rb;
+	struct k_mutex receive_rb_lock;
+
+	/* Work */
+	struct k_work_delayable open_work;
+	struct k_work_delayable close_work;
+
+	/* State */
+	enum modem_cmux_dlci_state state;
+};
+
+struct modem_cmux_frame {
+	uint16_t dlci_address;
+	bool cr;
+	bool pf;
+	uint8_t type;
+	const uint8_t *data;
+	uint16_t data_len;
+};
+
+struct modem_cmux_work {
+	struct k_work_delayable dwork;
+	struct modem_cmux *cmux;
+};
+
+struct modem_cmux {
+	/* Bus pipe */
+	struct modem_pipe *pipe;
+
+	/* Event handler */
+	modem_cmux_callback callback;
+	void *user_data;
+
+	/* DLCI channel contexts */
+	sys_slist_t dlcis;
+
+	/* State */
+	enum modem_cmux_state state;
+	bool flow_control_on;
+
+	/* Receive state*/
+	enum modem_cmux_receive_state receive_state;
+
+	/* Receive buffer */
+	uint8_t *receive_buf;
+	uint16_t receive_buf_size;
+	uint16_t receive_buf_len;
+
+	/* Transmit buffer */
+	struct ring_buf transmit_rb;
+	struct k_mutex transmit_rb_lock;
+
+	/* Received frame */
+	struct modem_cmux_frame frame;
+	uint8_t frame_header[5];
+	uint16_t frame_header_len;
+
+	/* Work */
+	struct k_work_delayable receive_work;
+	struct k_work_delayable transmit_work;
+	struct k_work_delayable connect_work;
+	struct k_work_delayable disconnect_work;
+
+	/* Synchronize actions */
+	struct k_event event;
+};
+
+/**
+ * @brief Contains CMUX instance configuration data
+ */
+struct modem_cmux_config {
+	/** Invoked when event occurs */
+	modem_cmux_callback callback;
+	/** Free to use pointer passed to event handler when invoked */
+	void *user_data;
+	/** Receive buffer */
+	uint8_t *receive_buf;
+	/** Size of receive buffer in bytes [127, ...] */
+	uint16_t receive_buf_size;
+	/** Transmit buffer */
+	uint8_t *transmit_buf;
+	/** Size of transmit buffer in bytes [149, ...] */
+	uint16_t transmit_buf_size;
+};
+
+/**
+ * @brief Initialize CMUX instance
+ * @param cmux CMUX instance
+ * @param config Configuration to apply to CMUX instance
+ */
+void modem_cmux_init(struct modem_cmux *cmux, const struct modem_cmux_config *config);
+
+/**
+ * @brief CMUX DLCI configuration
+ */
+struct modem_cmux_dlci_config {
+	/** DLCI channel address */
+	uint8_t dlci_address;
+	/** Receive buffer used by pipe */
+	uint8_t *receive_buf;
+	/** Size of receive buffer used by pipe [127, ...] */
+	uint16_t receive_buf_size;
+};
+
+/**
+ * @brief Initialize DLCI instance and register it with CMUX instance
+ *
+ * @param cmux CMUX instance which the DLCI will be registered to
+ * @param dlci DLCI instance which will be registered and configured
+ * @param config Configuration to apply to DLCI instance
+ */
+struct modem_pipe *modem_cmux_dlci_init(struct modem_cmux *cmux, struct modem_cmux_dlci *dlci,
+					const struct modem_cmux_dlci_config *config);
+
+/**
+ * @brief Initialize CMUX instance
+ *
+ * @param cmux CMUX instance
+ * @param pipe Pipe instance to attach CMUX instance to
+ */
+int modem_cmux_attach(struct modem_cmux *cmux, struct modem_pipe *pipe);
+
+/**
+ * @brief Connect CMUX instance
+ *
+ * @details This will send a CMUX connect request to target on the serial bus. If successful,
+ * DLCI channels can be now be opened using modem_pipe_open()
+ *
+ * @param cmux CMUX instance
+ *
+ * @note When connected, the bus pipe must not be used directly
+ */
+int modem_cmux_connect(struct modem_cmux *cmux);
+
+/**
+ * @brief Connect CMUX instance asynchronously
+ *
+ * @details This will send a CMUX connect request to target on the serial bus. If successful,
+ * DLCI channels can be now be opened using modem_pipe_open().
+ *
+ * @param cmux CMUX instance
+ *
+ * @note When connected, the bus pipe must not be used directly
+ */
+int modem_cmux_connect_async(struct modem_cmux *cmux);
+
+/**
+ * @brief Close down and disconnect CMUX instance
+ *
+ * @details This will close all open DLCI channels, and close down the CMUX connection.
+ *
+ * @param cmux CMUX instance
+ *
+ * @note When disconnected, the bus pipe can be used directly again
+ */
+int modem_cmux_disconnect(struct modem_cmux *cmux);
+
+/**
+ * @brief Close down and disconnect CMUX instance asynchronously
+ *
+ * @details This will close all open DLCI channels, and close down the CMUX connection.
+ *
+ * @param cmux CMUX instance
+ *
+ * @note When disconnected, the bus pipe can be used directly again
+ */
+int modem_cmux_disconnect_async(struct modem_cmux *cmux);
+
+void modem_cmux_release(struct modem_cmux *cmux);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_MODEM_CMUX_ */
diff --git a/include/zephyr/modem/pipe.h b/include/zephyr/modem/pipe.h
new file mode 100644
index 0000000..4fdd981
--- /dev/null
+++ b/include/zephyr/modem/pipe.h
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/types.h>
+#include <zephyr/kernel.h>
+
+#ifndef ZEPHYR_MODEM_PIPE_
+#define ZEPHYR_MODEM_PIPE_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct modem_pipe;
+
+typedef int (*modem_pipe_api_open)(void *data);
+
+typedef int (*modem_pipe_api_transmit)(void *data, const uint8_t *buf, size_t size);
+
+typedef int (*modem_pipe_api_receive)(void *data, uint8_t *buf, size_t size);
+
+typedef int (*modem_pipe_api_close)(void *data);
+
+struct modem_pipe_api {
+	modem_pipe_api_open open;
+	modem_pipe_api_transmit transmit;
+	modem_pipe_api_receive receive;
+	modem_pipe_api_close close;
+};
+
+enum modem_pipe_state {
+	MODEM_PIPE_STATE_CLOSED = 0,
+	MODEM_PIPE_STATE_OPEN,
+};
+
+enum modem_pipe_event {
+	MODEM_PIPE_EVENT_OPENED = 0,
+	MODEM_PIPE_EVENT_RECEIVE_READY,
+	MODEM_PIPE_EVENT_CLOSED,
+};
+
+typedef void (*modem_pipe_api_callback)(struct modem_pipe *pipe, enum modem_pipe_event event,
+					void *user_data);
+
+struct modem_pipe {
+	void *data;
+	struct modem_pipe_api *api;
+	modem_pipe_api_callback callback;
+	void *user_data;
+	enum modem_pipe_state state;
+	struct k_mutex lock;
+	struct k_condvar condvar;
+};
+
+/**
+ * @brief Initialize a modem pipe
+ *
+ * @param pipe Pipe instance to initialize
+ * @param data Pipe data to bind to pipe instance
+ * @param api Pipe API implementation to bind to pipe instance
+ */
+void modem_pipe_init(struct modem_pipe *pipe, void *data, struct modem_pipe_api *api);
+
+/**
+ * @brief Open pipe
+ *
+ * @param pipe Pipe instance
+ */
+int modem_pipe_open(struct modem_pipe *pipe);
+
+/**
+ * @brief Open pipe asynchronously
+ *
+ * @param pipe Pipe instance
+ */
+int modem_pipe_open_async(struct modem_pipe *pipe);
+
+/**
+ * @brief Attach pipe to callback
+ *
+ * @param pipe Pipe instance
+ * @param callback Callback called when pipe event occurs
+ * @param user_data Free to use user data passed with callback
+ */
+void modem_pipe_attach(struct modem_pipe *pipe, modem_pipe_api_callback callback, void *user_data);
+
+/**
+ * @brief Transmit data through pipe
+ *
+ * @param pipe Pipe to transmit through
+ * @param buf Destination for reveived data
+ * @param size Capacity of destination for recevied data
+ *
+ * @return Number of bytes placed in pipe
+ *
+ * @warning This call must be non-blocking
+ */
+int modem_pipe_transmit(struct modem_pipe *pipe, const uint8_t *buf, size_t size);
+
+/**
+ * @brief Reveive data through pipe
+ *
+ * @param pipe Pipe to receive from
+ * @param buf Destination for reveived data
+ * @param size Capacity of destination for recevied data
+ *
+ * @return Number of bytes received from pipe if any
+ * @return -EPERM if pipe is closed
+ * @return -errno code on error
+ *
+ * @warning This call must be non-blocking
+ */
+int modem_pipe_receive(struct modem_pipe *pipe, uint8_t *buf, size_t size);
+
+/**
+ * @brief Clear callback
+ *
+ * @param pipe Pipe instance
+ */
+void modem_pipe_release(struct modem_pipe *pipe);
+
+/**
+ * @brief Close pipe
+ *
+ * @param pipe Pipe instance
+ */
+int modem_pipe_close(struct modem_pipe *pipe);
+
+/**
+ * @brief Close pipe asynchronously
+ *
+ * @param pipe Pipe instance
+ */
+int modem_pipe_close_async(struct modem_pipe *pipe);
+
+/**
+ * @brief Notify user of pipe that it has opened
+ *
+ * @param pipe Pipe instance
+ *
+ * @note Invoked from instance which initialized the pipe instance
+ */
+void modem_pipe_notify_opened(struct modem_pipe *pipe);
+
+/**
+ * @brief Notify user of pipe that it has closed
+ *
+ * @param pipe Pipe instance
+ *
+ * @note Invoked from instance which initialized the pipe instance
+ */
+void modem_pipe_notify_closed(struct modem_pipe *pipe);
+
+/**
+ * @brief Notify user of pipe that data is ready to be received
+ *
+ * @param pipe Pipe instance
+ *
+ * @note Invoked from instance which initialized the pipe instance
+ */
+void modem_pipe_notify_receive_ready(struct modem_pipe *pipe);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_MODEM_PIPE_ */
diff --git a/include/zephyr/modem/ppp.h b/include/zephyr/modem/ppp.h
new file mode 100644
index 0000000..dc592fb
--- /dev/null
+++ b/include/zephyr/modem/ppp.h
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/kernel.h>
+#include <zephyr/types.h>
+#include <zephyr/net/net_if.h>
+#include <zephyr/net/net_pkt.h>
+#include <zephyr/sys/ring_buffer.h>
+#include <zephyr/sys/atomic.h>
+
+#include <zephyr/modem/pipe.h>
+
+#ifndef ZEPHYR_MODEM_PPP_
+#define ZEPHYR_MODEM_PPP_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum modem_ppp_receive_state {
+	/* Searching for start of frame and header */
+	MODEM_PPP_RECEIVE_STATE_HDR_SOF = 0,
+	MODEM_PPP_RECEIVE_STATE_HDR_FF,
+	MODEM_PPP_RECEIVE_STATE_HDR_7D,
+	MODEM_PPP_RECEIVE_STATE_HDR_23,
+	/* Writing bytes to network packet */
+	MODEM_PPP_RECEIVE_STATE_WRITING,
+	/* Unescaping next byte before writing to network packet */
+	MODEM_PPP_RECEIVE_STATE_UNESCAPING,
+};
+
+enum modem_ppp_transmit_state {
+	/* Idle */
+	MODEM_PPP_TRANSMIT_STATE_IDLE = 0,
+	/* Writing header */
+	MODEM_PPP_TRANSMIT_STATE_SOF,
+	MODEM_PPP_TRANSMIT_STATE_HDR_FF,
+	MODEM_PPP_TRANSMIT_STATE_HDR_7D,
+	MODEM_PPP_TRANSMIT_STATE_HDR_23,
+	/* Writing protocol */
+	MODEM_PPP_TRANSMIT_STATE_PROTOCOL_HIGH,
+	MODEM_PPP_TRANSMIT_STATE_ESCAPING_PROTOCOL_HIGH,
+	MODEM_PPP_TRANSMIT_STATE_PROTOCOL_LOW,
+	MODEM_PPP_TRANSMIT_STATE_ESCAPING_PROTOCOL_LOW,
+	/* Writing data */
+	MODEM_PPP_TRANSMIT_STATE_DATA,
+	MODEM_PPP_TRANSMIT_STATE_ESCAPING_DATA,
+	/* Writing FCS */
+	MODEM_PPP_TRANSMIT_STATE_FCS_LOW,
+	MODEM_PPP_TRANSMIT_STATE_ESCAPING_FCS_LOW,
+	MODEM_PPP_TRANSMIT_STATE_FCS_HIGH,
+	MODEM_PPP_TRANSMIT_STATE_ESCAPING_FCS_HIGH,
+	/* Writing end of frame */
+	MODEM_PPP_TRANSMIT_STATE_EOF,
+};
+
+typedef void (*modem_ppp_init_iface)(struct net_if *iface);
+
+struct modem_ppp {
+	/* Network interface instance is bound to */
+	struct net_if *iface;
+
+	/* Hook for PPP L2 network interface initialization */
+	modem_ppp_init_iface init_iface;
+
+	atomic_t state;
+
+	/* Buffers used for processing partial frames */
+	uint8_t *receive_buf;
+	uint8_t *transmit_buf;
+	uint16_t buf_size;
+
+	/* Wrapped PPP frames are sent and received through this pipe */
+	struct modem_pipe *pipe;
+
+	/* Receive PPP frame state */
+	enum modem_ppp_receive_state receive_state;
+
+	/* Allocated network packet being created */
+	struct net_pkt *rx_pkt;
+
+	/* Packet being sent */
+	enum modem_ppp_transmit_state transmit_state;
+	struct net_pkt *tx_pkt;
+	uint8_t tx_pkt_escaped;
+	uint16_t tx_pkt_protocol;
+	uint16_t tx_pkt_fcs;
+
+	/* Ring buffer used for transmitting partial PPP frame */
+	struct ring_buf transmit_rb;
+
+	struct k_fifo tx_pkt_fifo;
+
+	/* Work */
+	struct k_work send_work;
+	struct k_work process_work;
+};
+
+/**
+ * @brief Attach pipe to instance and connect
+ *
+ * @param ppp Modem PPP instance
+ * @param pipe Pipe to attach to modem PPP instance
+ */
+int modem_ppp_attach(struct modem_ppp *ppp, struct modem_pipe *pipe);
+
+/**
+ * @brief Get network interface modem PPP instance is bound to
+ *
+ * @param ppp Modem PPP instance
+ * @returns Pointer to network interface modem PPP instance is bound to
+ */
+struct net_if *modem_ppp_get_iface(struct modem_ppp *ppp);
+
+/**
+ * @brief Release pipe from instance
+ *
+ * @param ppp Modem PPP instance
+ */
+void modem_ppp_release(struct modem_ppp *ppp);
+
+/**
+ * @brief Initialize modem PPP instance device
+ * @param dev Device instance associated with network interface
+ * @warning Should not be used directly
+ */
+int modem_ppp_init_internal(const struct device *dev);
+
+/**
+ * @brief Define a modem PPP module and bind it to a network interface
+ *
+ * @details This macro defines the modem_ppp instance, initializes a PPP L2
+ * network device instance, and binds the modem_ppp instance to the PPP L2
+ * instance.
+ *
+ * @param _name Name of the statically defined modem_ppp instance
+ * @param _init_iface Hook for the PPP L2 network interface init function
+ * @param _prio Initialization priority of the PPP L2 net iface
+ * @param _mtu Max size of net_pkt data sent and received on PPP L2 net iface
+ * @param _buf_size Size of partial PPP frame transmit and receive buffers
+ */
+#define MODEM_PPP_DEFINE(_name, _init_iface, _prio, _mtu, _buf_size)                               \
+	extern const struct ppp_api modem_ppp_ppp_api;                                             \
+                                                                                                   \
+	static uint8_t _CONCAT(_name, _receive_buf)[_buf_size];                                    \
+	static uint8_t _CONCAT(_name, _transmit_buf)[_buf_size];                                   \
+                                                                                                   \
+	static struct modem_ppp _name = {                                                          \
+		.init_iface = _init_iface,                                                         \
+		.receive_buf = _CONCAT(_name, _receive_buf),                                       \
+		.transmit_buf = _CONCAT(_name, _transmit_buf),                                     \
+		.buf_size = _buf_size,                                                             \
+	};                                                                                         \
+                                                                                                   \
+	NET_DEVICE_INIT(_CONCAT(ppp_net_dev_, _name), "modem_ppp_" # _name,                        \
+			modem_ppp_init_internal, NULL, &_name, NULL, _prio, &modem_ppp_ppp_api,    \
+			PPP_L2, NET_L2_GET_CTX_TYPE(PPP_L2), _mtu)
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_MODEM_PPP_ */
diff --git a/subsys/CMakeLists.txt b/subsys/CMakeLists.txt
index 3c508d6..e4f155e 100644
--- a/subsys/CMakeLists.txt
+++ b/subsys/CMakeLists.txt
@@ -40,6 +40,7 @@
 add_subdirectory_ifdef(CONFIG_IMG_MANAGER dfu)
 add_subdirectory_ifdef(CONFIG_INPUT input)
 add_subdirectory_ifdef(CONFIG_JWT jwt)
+add_subdirectory_ifdef(CONFIG_MODEM_MODULES modem)
 add_subdirectory_ifdef(CONFIG_NET_BUF net)
 add_subdirectory_ifdef(CONFIG_RETENTION retention)
 add_subdirectory_ifdef(CONFIG_SENSING sensing)
diff --git a/subsys/Kconfig b/subsys/Kconfig
index c2b4b91..322ce24 100644
--- a/subsys/Kconfig
+++ b/subsys/Kconfig
@@ -24,6 +24,7 @@
 source "subsys/lorawan/Kconfig"
 source "subsys/mgmt/Kconfig"
 source "subsys/modbus/Kconfig"
+source "subsys/modem/Kconfig"
 source "subsys/net/Kconfig"
 source "subsys/pm/Kconfig"
 source "subsys/portability/Kconfig"
diff --git a/subsys/modem/CMakeLists.txt b/subsys/modem/CMakeLists.txt
new file mode 100644
index 0000000..d491e19
--- /dev/null
+++ b/subsys/modem/CMakeLists.txt
@@ -0,0 +1,15 @@
+# Copyright (c) 2023 Trackunit Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+if(CONFIG_MODEM_MODULES)
+
+zephyr_library()
+
+zephyr_library_sources_ifdef(CONFIG_MODEM_CHAT modem_chat.c)
+zephyr_library_sources_ifdef(CONFIG_MODEM_CMUX modem_cmux.c)
+zephyr_library_sources_ifdef(CONFIG_MODEM_PIPE modem_pipe.c)
+zephyr_library_sources_ifdef(CONFIG_MODEM_PPP modem_ppp.c)
+
+add_subdirectory(backends)
+
+endif()
diff --git a/subsys/modem/Kconfig b/subsys/modem/Kconfig
new file mode 100644
index 0000000..7ad98d2
--- /dev/null
+++ b/subsys/modem/Kconfig
@@ -0,0 +1,54 @@
+# Copyright (c) 2023 Trackunit Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+menuconfig MODEM_MODULES
+	bool "Modem modules"
+
+if MODEM_MODULES
+
+config MODEM_CHAT
+	bool "Modem chat module"
+	select RING_BUFFER
+	select MODEM_PIPE
+
+if MODEM_CHAT
+
+config MODEM_CHAT_LOG_BUFFER
+	int "Modem chat log buffer size"
+	default 128
+
+endif
+
+config MODEM_CMUX
+	bool "Modem CMUX module"
+	select MODEM_PIPE
+	select RING_BUFFER
+	select EVENTS
+	select CRC
+
+config MODEM_PIPE
+	bool "Modem pipe module"
+
+config MODEM_PPP
+	bool "Modem PPP module"
+	depends on NET_L2_PPP
+	select MODEM_PIPE
+	select RING_BUFFER
+	select CRC
+
+if MODEM_PPP
+
+config MODEM_PPP_NET_BUF_FRAG_SIZE
+	int "Network buffer fragment size"
+	default NET_BUF_DATA_SIZE if NET_BUF_FIXED_DATA_SIZE
+	default 128
+
+endif
+
+module = MODEM_MODULES
+module-str = modem_modules
+source "subsys/logging/Kconfig.template.log_config"
+
+rsource "backends/Kconfig"
+
+endif
diff --git a/subsys/modem/backends/CMakeLists.txt b/subsys/modem/backends/CMakeLists.txt
new file mode 100644
index 0000000..471452e
--- /dev/null
+++ b/subsys/modem/backends/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright (c) 2023 Trackunit Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_library()
+
+zephyr_library_sources_ifdef(CONFIG_MODEM_BACKEND_TTY modem_backend_tty.c)
+zephyr_library_sources_ifdef(CONFIG_MODEM_BACKEND_UART modem_backend_uart.c)
+zephyr_library_sources_ifdef(CONFIG_MODEM_BACKEND_UART_ISR modem_backend_uart_isr.c)
+zephyr_library_sources_ifdef(CONFIG_MODEM_BACKEND_UART_ASYNC modem_backend_uart_async.c)
diff --git a/subsys/modem/backends/Kconfig b/subsys/modem/backends/Kconfig
new file mode 100644
index 0000000..f2b45d9
--- /dev/null
+++ b/subsys/modem/backends/Kconfig
@@ -0,0 +1,24 @@
+# Copyright (c) 2023 Trackunit Corporation
+# SPDX-License-Identifier: Apache-2.0
+
+config MODEM_BACKEND_TTY
+	bool "Modem TTY backend module"
+	select MODEM_PIPE
+	depends on ARCH_POSIX
+
+config MODEM_BACKEND_UART
+	bool "Modem UART backend module"
+	select MODEM_PIPE
+	depends on UART_INTERRUPT_DRIVEN || UART_ASYNC_API
+
+if MODEM_BACKEND_UART
+
+config MODEM_BACKEND_UART_ISR
+	bool "Modem UART backend module interrupt driven implementation"
+	default y if UART_INTERRUPT_DRIVEN
+
+config MODEM_BACKEND_UART_ASYNC
+	bool "Modem UART backend module async implementation"
+	default y if UART_ASYNC_API
+
+endif # MODEM_BACKEND_UART
diff --git a/subsys/modem/backends/modem_backend_tty.c b/subsys/modem/backends/modem_backend_tty.c
new file mode 100644
index 0000000..b8864fc
--- /dev/null
+++ b/subsys/modem/backends/modem_backend_tty.c
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/modem/backend/tty.h>
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(modem_backend_tty);
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <poll.h>
+#include <string.h>
+
+#define MODEM_BACKEND_TTY_THREAD_PRIO          (10)
+#define MODEM_BACKEND_TTY_THREAD_RUN_PERIOD_MS (1000)
+#define MODEM_BACKEND_TTY_THREAD_POLL_DELAY    (100)
+
+#define MODEM_BACKEND_TTY_STATE_RUN_BIT (1)
+
+static void modem_backend_tty_routine(void *p1, void *p2, void *p3)
+{
+	struct modem_backend_tty *backend = (struct modem_backend_tty *)p1;
+	struct pollfd pd;
+
+	ARG_UNUSED(p2);
+	ARG_UNUSED(p3);
+
+	pd.fd = backend->tty_fd;
+	pd.events = POLLIN;
+
+	/* Run until run flag is cleared. Check every MODEM_BACKEND_TTY_THREAD_RUN_PERIOD_MS */
+	while (atomic_test_bit(&backend->state, MODEM_BACKEND_TTY_STATE_RUN_BIT)) {
+		/* Clear events */
+		pd.revents = 0;
+
+		if (poll(&pd, 1, MODEM_BACKEND_TTY_THREAD_RUN_PERIOD_MS) < 0) {
+			LOG_ERR("Poll operation failed");
+			break;
+		}
+
+		if (pd.revents & POLLIN) {
+			modem_pipe_notify_receive_ready(&backend->pipe);
+		}
+
+		k_sleep(K_MSEC(MODEM_BACKEND_TTY_THREAD_POLL_DELAY));
+	}
+}
+
+static int modem_backend_tty_open(void *data)
+{
+	struct modem_backend_tty *backend = (struct modem_backend_tty *)data;
+
+	if (atomic_test_and_set_bit(&backend->state, MODEM_BACKEND_TTY_STATE_RUN_BIT)) {
+		return -EALREADY;
+	}
+
+	backend->tty_fd = open(backend->tty_path, (O_RDWR | O_NONBLOCK), 0644);
+	if (backend->tty_fd < 0) {
+		return -EPERM;
+	}
+
+	k_thread_create(&backend->thread, backend->stack, backend->stack_size,
+			modem_backend_tty_routine, backend, NULL, NULL,
+			MODEM_BACKEND_TTY_THREAD_PRIO, 0, K_NO_WAIT);
+
+	modem_pipe_notify_opened(&backend->pipe);
+	return 0;
+}
+
+static int modem_backend_tty_transmit(void *data, const uint8_t *buf, size_t size)
+{
+	struct modem_backend_tty *backend = (struct modem_backend_tty *)data;
+
+	return write(backend->tty_fd, buf, size);
+}
+
+static int modem_backend_tty_receive(void *data, uint8_t *buf, size_t size)
+{
+	int ret;
+	struct modem_backend_tty *backend = (struct modem_backend_tty *)data;
+
+	ret = read(backend->tty_fd, buf, size);
+	return (ret < 0) ? 0 : ret;
+}
+
+static int modem_backend_tty_close(void *data)
+{
+	struct modem_backend_tty *backend = (struct modem_backend_tty *)data;
+
+	if (!atomic_test_and_clear_bit(&backend->state, MODEM_BACKEND_TTY_STATE_RUN_BIT)) {
+		return -EALREADY;
+	}
+
+	k_thread_join(&backend->thread, K_MSEC(MODEM_BACKEND_TTY_THREAD_RUN_PERIOD_MS * 2));
+	close(backend->tty_fd);
+	modem_pipe_notify_closed(&backend->pipe);
+	return 0;
+}
+
+struct modem_pipe_api modem_backend_tty_api = {
+	.open = modem_backend_tty_open,
+	.transmit = modem_backend_tty_transmit,
+	.receive = modem_backend_tty_receive,
+	.close = modem_backend_tty_close,
+};
+
+struct modem_pipe *modem_backend_tty_init(struct modem_backend_tty *backend,
+					  const struct modem_backend_tty_config *config)
+{
+	__ASSERT_NO_MSG(backend != NULL);
+	__ASSERT_NO_MSG(config != NULL);
+	__ASSERT_NO_MSG(config->tty_path != NULL);
+
+	memset(backend, 0x00, sizeof(*backend));
+	backend->tty_path = config->tty_path;
+	backend->stack = config->stack;
+	backend->stack_size = config->stack_size;
+	atomic_set(&backend->state, 0);
+	modem_pipe_init(&backend->pipe, backend, &modem_backend_tty_api);
+	return &backend->pipe;
+}
diff --git a/subsys/modem/backends/modem_backend_uart.c b/subsys/modem/backends/modem_backend_uart.c
new file mode 100644
index 0000000..91e062b
--- /dev/null
+++ b/subsys/modem/backends/modem_backend_uart.c
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2023 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "modem_backend_uart_isr.h"
+#include "modem_backend_uart_async.h"
+
+#include <zephyr/modem/backend/uart.h>
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(modem_backend_uart);
+
+#include <string.h>
+
+static void modem_backend_uart_receive_ready_handler(struct k_work *item)
+{
+	struct modem_backend_uart *backend =
+		CONTAINER_OF(item, struct modem_backend_uart, receive_ready_work);
+
+	modem_pipe_notify_receive_ready(&backend->pipe);
+}
+
+struct modem_pipe *modem_backend_uart_init(struct modem_backend_uart *backend,
+					   const struct modem_backend_uart_config *config)
+{
+	__ASSERT_NO_MSG(config->uart != NULL);
+	__ASSERT_NO_MSG(config->receive_buf != NULL);
+	__ASSERT_NO_MSG(config->receive_buf_size > 1);
+	__ASSERT_NO_MSG((config->receive_buf_size % 2) == 0);
+	__ASSERT_NO_MSG(config->transmit_buf != NULL);
+	__ASSERT_NO_MSG(config->transmit_buf_size > 0);
+
+	memset(backend, 0x00, sizeof(*backend));
+	backend->uart = config->uart;
+	k_work_init(&backend->receive_ready_work, modem_backend_uart_receive_ready_handler);
+
+#ifdef CONFIG_UART_ASYNC_API
+	if (modem_backend_uart_async_is_supported(backend)) {
+		modem_backend_uart_async_init(backend, config);
+		return &backend->pipe;
+	}
+#endif /* CONFIG_UART_ASYNC_API */
+
+#ifdef CONFIG_UART_INTERRUPT_DRIVEN
+	modem_backend_uart_isr_init(backend, config);
+
+	return &backend->pipe;
+#endif /* CONFIG_UART_INTERRUPT_DRIVEN */
+
+	__ASSERT(0, "No supported UART API");
+
+	return NULL;
+}
diff --git a/subsys/modem/backends/modem_backend_uart_async.c b/subsys/modem/backends/modem_backend_uart_async.c
new file mode 100644
index 0000000..a10822a
--- /dev/null
+++ b/subsys/modem/backends/modem_backend_uart_async.c
@@ -0,0 +1,260 @@
+/*
+ * Copyright (c) 2023 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "modem_backend_uart_async.h"
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_DECLARE(modem_backend_uart);
+
+#include <zephyr/kernel.h>
+#include <string.h>
+
+#define MODEM_BACKEND_UART_ASYNC_STATE_TRANSMITTING_BIT       (0)
+#define MODEM_BACKEND_UART_ASYNC_STATE_RX_BUF0_USED_BIT       (1)
+#define MODEM_BACKEND_UART_ASYNC_STATE_RX_BUF1_USED_BIT       (2)
+#define MODEM_BACKEND_UART_ASYNC_STATE_RX_RBUF_USED_INDEX_BIT (3)
+
+#define MODEM_BACKEND_UART_ASYNC_BLOCK_MIN_SIZE (8)
+
+static void modem_backend_uart_async_flush(struct modem_backend_uart *backend)
+{
+	uint8_t c;
+
+	while (uart_fifo_read(backend->uart, &c, 1) > 0) {
+		continue;
+	}
+}
+
+static uint8_t modem_backend_uart_async_rx_rbuf_used_index(struct modem_backend_uart *backend)
+{
+	return atomic_test_bit(&backend->async.state,
+			       MODEM_BACKEND_UART_ASYNC_STATE_RX_RBUF_USED_INDEX_BIT);
+}
+
+static void modem_backend_uart_async_rx_rbuf_used_swap(struct modem_backend_uart *backend)
+{
+	uint8_t rx_rbuf_index = modem_backend_uart_async_rx_rbuf_used_index(backend);
+
+	if (rx_rbuf_index) {
+		atomic_clear_bit(&backend->async.state,
+				 MODEM_BACKEND_UART_ASYNC_STATE_RX_RBUF_USED_INDEX_BIT);
+	} else {
+		atomic_set_bit(&backend->async.state,
+			       MODEM_BACKEND_UART_ASYNC_STATE_RX_RBUF_USED_INDEX_BIT);
+	}
+}
+
+static void modem_backend_uart_async_event_handler(const struct device *dev,
+						   struct uart_event *evt, void *user_data)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *) user_data;
+
+	uint8_t receive_rb_used_index;
+	uint32_t received;
+
+	switch (evt->type) {
+	case UART_TX_DONE:
+		atomic_clear_bit(&backend->async.state,
+				 MODEM_BACKEND_UART_ASYNC_STATE_TRANSMITTING_BIT);
+
+		break;
+
+	case UART_RX_BUF_REQUEST:
+		if (!atomic_test_and_set_bit(&backend->async.state,
+					     MODEM_BACKEND_UART_ASYNC_STATE_RX_BUF0_USED_BIT)) {
+			uart_rx_buf_rsp(backend->uart, backend->async.receive_bufs[0],
+					backend->async.receive_buf_size);
+
+			break;
+		}
+
+		if (!atomic_test_and_set_bit(&backend->async.state,
+					     MODEM_BACKEND_UART_ASYNC_STATE_RX_BUF1_USED_BIT)) {
+			uart_rx_buf_rsp(backend->uart, backend->async.receive_bufs[1],
+					backend->async.receive_buf_size);
+
+			break;
+		}
+
+		LOG_WRN("No receive buffer available");
+		break;
+
+	case UART_RX_BUF_RELEASED:
+		if (evt->data.rx_buf.buf == backend->async.receive_bufs[0]) {
+			atomic_clear_bit(&backend->async.state,
+					 MODEM_BACKEND_UART_ASYNC_STATE_RX_BUF0_USED_BIT);
+
+			break;
+		}
+
+		if (evt->data.rx_buf.buf == backend->async.receive_bufs[1]) {
+			atomic_clear_bit(&backend->async.state,
+					 MODEM_BACKEND_UART_ASYNC_STATE_RX_BUF1_USED_BIT);
+
+			break;
+		}
+
+		LOG_WRN("Unknown receive buffer released");
+		break;
+
+	case UART_RX_RDY:
+		receive_rb_used_index = modem_backend_uart_async_rx_rbuf_used_index(backend);
+
+		received = ring_buf_put(&backend->async.receive_rdb[receive_rb_used_index],
+				       &evt->data.rx.buf[evt->data.rx.offset],
+				       evt->data.rx.len);
+
+		if (received < evt->data.rx.len) {
+			ring_buf_reset(&backend->async.receive_rdb[receive_rb_used_index]);
+			LOG_WRN("Receive buffer overrun");
+			break;
+		}
+
+		k_work_submit(&backend->receive_ready_work);
+		break;
+
+	case UART_TX_ABORTED:
+		LOG_WRN("Transmit aborted");
+
+	default:
+		break;
+	}
+}
+
+static int modem_backend_uart_async_open(void *data)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)data;
+	int ret;
+
+	atomic_set(&backend->async.state, 0);
+	modem_backend_uart_async_flush(backend);
+	ring_buf_reset(&backend->async.receive_rdb[0]);
+	ring_buf_reset(&backend->async.receive_rdb[1]);
+
+	/* Reserve receive buffer 0 */
+	atomic_set_bit(&backend->async.state,
+		       MODEM_BACKEND_UART_ASYNC_STATE_RX_BUF0_USED_BIT);
+
+	/*
+	 * Receive buffer 0 is used internally by UART, receive ring buffer 0 is
+	 * used to store received data.
+	 */
+	ret = uart_rx_enable(backend->uart, backend->async.receive_bufs[0],
+			     backend->async.receive_buf_size, 3000);
+
+	if (ret < 0) {
+		return ret;
+	}
+
+	modem_pipe_notify_opened(&backend->pipe);
+	return 0;
+}
+
+static int modem_backend_uart_async_transmit(void *data, const uint8_t *buf, size_t size)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)data;
+	bool transmitting;
+	uint32_t bytes_to_transmit;
+	int ret;
+
+	transmitting = atomic_test_and_set_bit(&backend->async.state,
+					       MODEM_BACKEND_UART_ASYNC_STATE_TRANSMITTING_BIT);
+
+	if (transmitting) {
+		return 0;
+	}
+
+	/* Determine amount of bytes to transmit */
+	bytes_to_transmit = (size < backend->async.transmit_buf_size)
+			  ? size
+			  : backend->async.transmit_buf_size;
+
+	/* Copy buf to transmit buffer which is passed to UART */
+	memcpy(backend->async.transmit_buf, buf, bytes_to_transmit);
+
+	ret = uart_tx(backend->uart, backend->async.transmit_buf, bytes_to_transmit,
+		      SYS_FOREVER_US);
+
+	if (ret < 0) {
+		LOG_WRN("Failed to start async transmit");
+		return ret;
+	}
+
+	return (int)bytes_to_transmit;
+}
+
+static int modem_backend_uart_async_receive(void *data, uint8_t *buf, size_t size)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)data;
+
+	uint32_t received;
+	uint8_t receive_rdb_unused;
+
+	received = 0;
+	receive_rdb_unused = modem_backend_uart_async_rx_rbuf_used_index(backend) ? 0 : 1;
+
+	/* Read data from unused ring double buffer first */
+	received += ring_buf_get(&backend->async.receive_rdb[receive_rdb_unused], buf, size);
+
+	if (ring_buf_is_empty(&backend->async.receive_rdb[receive_rdb_unused]) == false) {
+		return (int)received;
+	}
+
+	/* Swap receive ring double buffer */
+	modem_backend_uart_async_rx_rbuf_used_swap(backend);
+
+	/* Read data from previously used buffer */
+	receive_rdb_unused = modem_backend_uart_async_rx_rbuf_used_index(backend) ? 0 : 1;
+
+	received += ring_buf_get(&backend->async.receive_rdb[receive_rdb_unused],
+				   &buf[received], (size - received));
+
+	return (int)received;
+}
+
+static int modem_backend_uart_async_close(void *data)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)data;
+
+	uart_rx_disable(backend->uart);
+	modem_pipe_notify_closed(&backend->pipe);
+	return 0;
+}
+
+struct modem_pipe_api modem_backend_uart_async_api = {
+	.open = modem_backend_uart_async_open,
+	.transmit = modem_backend_uart_async_transmit,
+	.receive = modem_backend_uart_async_receive,
+	.close = modem_backend_uart_async_close,
+};
+
+bool modem_backend_uart_async_is_supported(struct modem_backend_uart *backend)
+{
+	return uart_callback_set(backend->uart, modem_backend_uart_async_event_handler,
+				 backend) == 0;
+}
+
+void modem_backend_uart_async_init(struct modem_backend_uart *backend,
+				   const struct modem_backend_uart_config *config)
+{
+	uint32_t receive_buf_size_quarter = config->receive_buf_size / 4;
+
+	/* Split receive buffer into 4 buffers, use 2 parts for UART receive double buffer */
+	backend->async.receive_buf_size = receive_buf_size_quarter;
+	backend->async.receive_bufs[0] = &config->receive_buf[0];
+	backend->async.receive_bufs[1] = &config->receive_buf[receive_buf_size_quarter];
+
+	/* Use remaining 2 parts for receive double ring buffer */
+	ring_buf_init(&backend->async.receive_rdb[0], receive_buf_size_quarter,
+		      &config->receive_buf[receive_buf_size_quarter * 2]);
+
+	ring_buf_init(&backend->async.receive_rdb[1], receive_buf_size_quarter,
+		      &config->receive_buf[receive_buf_size_quarter * 3]);
+
+	backend->async.transmit_buf = config->transmit_buf;
+	backend->async.transmit_buf_size = config->transmit_buf_size;
+	modem_pipe_init(&backend->pipe, backend, &modem_backend_uart_async_api);
+}
diff --git a/subsys/modem/backends/modem_backend_uart_async.h b/subsys/modem/backends/modem_backend_uart_async.h
new file mode 100644
index 0000000..dce7e2b
--- /dev/null
+++ b/subsys/modem/backends/modem_backend_uart_async.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/modem/backend/uart.h>
+
+#ifndef ZEPHYR_MODEM_BACKEND_UART_ASYNC_
+#define ZEPHYR_MODEM_BACKEND_UART_ASYNC_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+bool modem_backend_uart_async_is_supported(struct modem_backend_uart *backend);
+
+void modem_backend_uart_async_init(struct modem_backend_uart *backend,
+				   const struct modem_backend_uart_config *config);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_MODEM_BACKEND_UART_ASYNC_ */
diff --git a/subsys/modem/backends/modem_backend_uart_isr.c b/subsys/modem/backends/modem_backend_uart_isr.c
new file mode 100644
index 0000000..7c88cb4
--- /dev/null
+++ b/subsys/modem/backends/modem_backend_uart_isr.c
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2023 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "modem_backend_uart_isr.h"
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_DECLARE(modem_backend_uart);
+
+#include <string.h>
+
+static void modem_backend_uart_isr_flush(struct modem_backend_uart *backend)
+{
+	uint8_t c;
+
+	while (uart_fifo_read(backend->uart, &c, 1) > 0) {
+		continue;
+	}
+}
+
+static void modem_backend_uart_isr_irq_handler_receive_ready(struct modem_backend_uart *backend)
+{
+	uint32_t size;
+	struct ring_buf *receive_rb;
+	uint8_t *buffer;
+	int ret;
+
+	receive_rb = &backend->isr.receive_rdb[backend->isr.receive_rdb_used];
+	size = ring_buf_put_claim(receive_rb, &buffer, UINT32_MAX);
+	if (size == 0) {
+		LOG_WRN("Receive buffer overrun");
+		ring_buf_put_finish(receive_rb, 0);
+		ring_buf_reset(receive_rb);
+		size = ring_buf_put_claim(receive_rb, &buffer, UINT32_MAX);
+	}
+
+	ret = uart_fifo_read(backend->uart, buffer, size);
+	if (ret < 0) {
+		ring_buf_put_finish(receive_rb, 0);
+	} else {
+		ring_buf_put_finish(receive_rb, (uint32_t)ret);
+	}
+
+	if (ret > 0) {
+		k_work_submit(&backend->receive_ready_work);
+	}
+}
+
+static void modem_backend_uart_isr_irq_handler_transmit_ready(struct modem_backend_uart *backend)
+{
+	uint32_t size;
+	uint8_t *buffer;
+	int ret;
+
+	if (ring_buf_is_empty(&backend->isr.transmit_rb) == true) {
+		uart_irq_tx_disable(backend->uart);
+		return;
+	}
+
+	size = ring_buf_get_claim(&backend->isr.transmit_rb, &buffer, UINT32_MAX);
+	ret = uart_fifo_fill(backend->uart, buffer, size);
+	if (ret < 0) {
+		ring_buf_get_finish(&backend->isr.transmit_rb, 0);
+	} else {
+		ring_buf_get_finish(&backend->isr.transmit_rb, (uint32_t)ret);
+
+		/* Update transmit buf capacity tracker */
+		atomic_sub(&backend->isr.transmit_buf_len, (uint32_t)ret);
+	}
+}
+
+static void modem_backend_uart_isr_irq_handler(const struct device *uart, void *user_data)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)user_data;
+
+	if (uart_irq_update(uart) < 1) {
+		return;
+	}
+
+	if (uart_irq_rx_ready(uart)) {
+		modem_backend_uart_isr_irq_handler_receive_ready(backend);
+	}
+
+	if (uart_irq_tx_ready(uart)) {
+		modem_backend_uart_isr_irq_handler_transmit_ready(backend);
+	}
+}
+
+static int modem_backend_uart_isr_open(void *data)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)data;
+
+	ring_buf_reset(&backend->isr.receive_rdb[0]);
+	ring_buf_reset(&backend->isr.receive_rdb[1]);
+	ring_buf_reset(&backend->isr.transmit_rb);
+	atomic_set(&backend->isr.transmit_buf_len, 0);
+	modem_backend_uart_isr_flush(backend);
+	uart_irq_rx_enable(backend->uart);
+	uart_irq_tx_enable(backend->uart);
+	modem_pipe_notify_opened(&backend->pipe);
+	return 0;
+}
+
+static bool modem_backend_uart_isr_transmit_buf_above_limit(struct modem_backend_uart *backend)
+{
+	return backend->isr.transmit_buf_put_limit < atomic_get(&backend->isr.transmit_buf_len);
+}
+
+static int modem_backend_uart_isr_transmit(void *data, const uint8_t *buf, size_t size)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)data;
+	int written;
+
+	if (modem_backend_uart_isr_transmit_buf_above_limit(backend) == true) {
+		return 0;
+	}
+
+	uart_irq_tx_disable(backend->uart);
+	written = ring_buf_put(&backend->isr.transmit_rb, buf, size);
+	uart_irq_tx_enable(backend->uart);
+
+	/* Update transmit buf capacity tracker */
+	atomic_add(&backend->isr.transmit_buf_len, written);
+	return written;
+}
+
+static int modem_backend_uart_isr_receive(void *data, uint8_t *buf, size_t size)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)data;
+
+	uint32_t read_bytes;
+	uint8_t receive_rdb_unused;
+
+	read_bytes = 0;
+	receive_rdb_unused = (backend->isr.receive_rdb_used == 1) ? 0 : 1;
+
+	/* Read data from unused ring double buffer first */
+	read_bytes += ring_buf_get(&backend->isr.receive_rdb[receive_rdb_unused], buf, size);
+
+	if (ring_buf_is_empty(&backend->isr.receive_rdb[receive_rdb_unused]) == false) {
+		return (int)read_bytes;
+	}
+
+	/* Swap receive ring double buffer */
+	uart_irq_rx_disable(backend->uart);
+	backend->isr.receive_rdb_used = receive_rdb_unused;
+	uart_irq_rx_enable(backend->uart);
+
+	/* Read data from previously used buffer */
+	receive_rdb_unused = (backend->isr.receive_rdb_used == 1) ? 0 : 1;
+
+	read_bytes += ring_buf_get(&backend->isr.receive_rdb[receive_rdb_unused],
+				   &buf[read_bytes], (size - read_bytes));
+
+	return (int)read_bytes;
+}
+
+static int modem_backend_uart_isr_close(void *data)
+{
+	struct modem_backend_uart *backend = (struct modem_backend_uart *)data;
+
+	uart_irq_rx_disable(backend->uart);
+	uart_irq_tx_disable(backend->uart);
+	modem_pipe_notify_closed(&backend->pipe);
+	return 0;
+}
+
+struct modem_pipe_api modem_backend_uart_isr_api = {
+	.open = modem_backend_uart_isr_open,
+	.transmit = modem_backend_uart_isr_transmit,
+	.receive = modem_backend_uart_isr_receive,
+	.close = modem_backend_uart_isr_close,
+};
+
+void modem_backend_uart_isr_init(struct modem_backend_uart *backend,
+				 const struct modem_backend_uart_config *config)
+{
+	uint32_t receive_double_buf_size;
+
+	backend->isr.transmit_buf_put_limit =
+		config->transmit_buf_size - (config->transmit_buf_size / 4);
+
+	receive_double_buf_size = config->receive_buf_size / 2;
+
+	ring_buf_init(&backend->isr.receive_rdb[0], receive_double_buf_size,
+		      &config->receive_buf[0]);
+
+	ring_buf_init(&backend->isr.receive_rdb[1], receive_double_buf_size,
+		      &config->receive_buf[receive_double_buf_size]);
+
+	ring_buf_init(&backend->isr.transmit_rb, config->transmit_buf_size,
+		      config->transmit_buf);
+
+	atomic_set(&backend->isr.transmit_buf_len, 0);
+	uart_irq_rx_disable(backend->uart);
+	uart_irq_tx_disable(backend->uart);
+	uart_irq_callback_user_data_set(backend->uart, modem_backend_uart_isr_irq_handler,
+					backend);
+
+	modem_pipe_init(&backend->pipe, backend, &modem_backend_uart_isr_api);
+}
diff --git a/subsys/modem/backends/modem_backend_uart_isr.h b/subsys/modem/backends/modem_backend_uart_isr.h
new file mode 100644
index 0000000..31c0a71
--- /dev/null
+++ b/subsys/modem/backends/modem_backend_uart_isr.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/modem/backend/uart.h>
+
+#ifndef ZEPHYR_MODEM_BACKEND_UART_ISR_
+#define ZEPHYR_MODEM_BACKEND_UART_ISR_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void modem_backend_uart_isr_init(struct modem_backend_uart *backend,
+				 const struct modem_backend_uart_config *config);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ZEPHYR_MODEM_BACKEND_UART_ISR_ */
diff --git a/subsys/modem/modem_chat.c b/subsys/modem/modem_chat.c
new file mode 100644
index 0000000..f603cfb
--- /dev/null
+++ b/subsys/modem/modem_chat.c
@@ -0,0 +1,788 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(modem_chat, CONFIG_MODEM_MODULES_LOG_LEVEL);
+
+#include <zephyr/kernel.h>
+#include <string.h>
+
+#include <zephyr/modem/chat.h>
+
+#define MODEM_CHAT_MATCHES_INDEX_RESPONSE (0)
+#define MODEM_CHAT_MATCHES_INDEX_ABORT	  (1)
+#define MODEM_CHAT_MATCHES_INDEX_UNSOL	  (2)
+
+#define MODEM_CHAT_SCRIPT_STATE_RUNNING_BIT (0)
+
+#if defined(CONFIG_LOG) && (CONFIG_MODEM_MODULES_LOG_LEVEL == LOG_LEVEL_DBG)
+
+static char log_buffer[CONFIG_MODEM_CHAT_LOG_BUFFER];
+
+static void modem_chat_log_received_command(struct modem_chat *chat)
+{
+	uint16_t log_buffer_pos = 0;
+	uint16_t argv_len;
+
+	for (uint16_t i = 0; i < chat->argc; i++) {
+		argv_len = (uint16_t)strlen(chat->argv[i]);
+
+		/* Validate argument fits in log buffer including termination */
+		if (sizeof(log_buffer) < (log_buffer_pos + argv_len + 1)) {
+			LOG_WRN("log buffer overrun");
+			break;
+		}
+
+		/* Copy argument and append space */
+		memcpy(&log_buffer[log_buffer_pos], chat->argv[i], argv_len);
+		log_buffer_pos += argv_len;
+		log_buffer[log_buffer_pos] = ' ';
+		log_buffer_pos++;
+	}
+
+	/* Terminate line after last argument, overwriting trailing space */
+	log_buffer_pos = log_buffer_pos == 0 ? log_buffer_pos : log_buffer_pos - 1;
+	log_buffer[log_buffer_pos] = '\0';
+
+	LOG_DBG("%s", log_buffer);
+}
+
+#else
+
+static void modem_chat_log_received_command(struct modem_chat *chat)
+{
+}
+
+#endif
+
+static void modem_chat_script_stop(struct modem_chat *chat, enum modem_chat_script_result result)
+{
+	/* Handle result */
+	if (result == MODEM_CHAT_SCRIPT_RESULT_SUCCESS) {
+		LOG_DBG("%s: complete", chat->script->name);
+	} else if (result == MODEM_CHAT_SCRIPT_RESULT_ABORT) {
+		LOG_WRN("%s: aborted", chat->script->name);
+	} else {
+		LOG_WRN("%s: timed out", chat->script->name);
+	}
+
+	/* Clear script running state */
+	atomic_clear_bit(&chat->script_state, MODEM_CHAT_SCRIPT_STATE_RUNNING_BIT);
+
+	/* Call back with result */
+	if (chat->script->callback != NULL) {
+		chat->script->callback(chat, result, chat->user_data);
+	}
+
+	/* Clear reference to script */
+	chat->script = NULL;
+
+	/* Clear response and abort commands */
+	chat->matches[MODEM_CHAT_MATCHES_INDEX_ABORT] = NULL;
+	chat->matches_size[MODEM_CHAT_MATCHES_INDEX_ABORT] = 0;
+	chat->matches[MODEM_CHAT_MATCHES_INDEX_RESPONSE] = NULL;
+	chat->matches_size[MODEM_CHAT_MATCHES_INDEX_RESPONSE] = 0;
+
+	/* Cancel timeout work */
+	k_work_cancel_delayable(&chat->script_timeout_work);
+}
+
+static void modem_chat_script_send(struct modem_chat *chat)
+{
+	/* Initialize script send work */
+	chat->script_send_request_pos = 0;
+	chat->script_send_delimiter_pos = 0;
+
+	/* Schedule script send work */
+	k_work_schedule(&chat->script_send_work, K_NO_WAIT);
+}
+
+static void modem_chat_script_next(struct modem_chat *chat, bool initial)
+{
+	const struct modem_chat_script_chat *script_chat;
+
+	/* Advance iterator if not initial */
+	if (initial == true) {
+		/* Reset iterator */
+		chat->script_chat_it = 0;
+	} else {
+		/* Advance iterator */
+		chat->script_chat_it++;
+	}
+
+	/* Check if end of script reached */
+	if (chat->script_chat_it == chat->script->script_chats_size) {
+		modem_chat_script_stop(chat, MODEM_CHAT_SCRIPT_RESULT_SUCCESS);
+
+		return;
+	}
+
+	LOG_DBG("%s: step: %u", chat->script->name, chat->script_chat_it);
+
+	script_chat = &chat->script->script_chats[chat->script_chat_it];
+
+	/* Set response command handlers */
+	chat->matches[MODEM_CHAT_MATCHES_INDEX_RESPONSE] = script_chat->response_matches;
+	chat->matches_size[MODEM_CHAT_MATCHES_INDEX_RESPONSE] = script_chat->response_matches_size;
+
+	/* Check if work must be sent */
+	if (strlen(script_chat->request) > 0) {
+		LOG_DBG("sending: %s", script_chat->request);
+		modem_chat_script_send(chat);
+	}
+}
+
+static void modem_chat_script_start(struct modem_chat *chat, const struct modem_chat_script *script)
+{
+	/* Save script */
+	chat->script = script;
+
+	/* Set abort matches */
+	chat->matches[MODEM_CHAT_MATCHES_INDEX_ABORT] = script->abort_matches;
+	chat->matches_size[MODEM_CHAT_MATCHES_INDEX_ABORT] = script->abort_matches_size;
+
+	LOG_DBG("running script: %s", chat->script->name);
+
+	/* Set first script command */
+	modem_chat_script_next(chat, true);
+
+	/* Start timeout work if script started */
+	if (chat->script != NULL) {
+		k_work_schedule(&chat->script_timeout_work, K_SECONDS(chat->script->timeout));
+	}
+}
+
+static void modem_chat_script_run_handler(struct k_work *item)
+{
+	struct modem_chat *chat = CONTAINER_OF(item, struct modem_chat, script_run_work);
+
+	/* Start script */
+	modem_chat_script_start(chat, chat->pending_script);
+}
+
+static void modem_chat_script_timeout_handler(struct k_work *item)
+{
+	struct modem_chat *chat = CONTAINER_OF(item, struct modem_chat, script_timeout_work);
+
+	/* Abort script */
+	modem_chat_script_stop(chat, MODEM_CHAT_SCRIPT_RESULT_TIMEOUT);
+}
+
+static void modem_chat_script_abort_handler(struct k_work *item)
+{
+	struct modem_chat *chat = CONTAINER_OF(item, struct modem_chat, script_abort_work);
+
+	/* Validate script is currently running */
+	if (chat->script == NULL) {
+		return;
+	}
+
+	/* Abort script */
+	modem_chat_script_stop(chat, MODEM_CHAT_SCRIPT_RESULT_ABORT);
+}
+
+static bool modem_chat_script_send_request(struct modem_chat *chat)
+{
+	const struct modem_chat_script_chat *script_chat =
+		&chat->script->script_chats[chat->script_chat_it];
+
+	uint16_t script_chat_request_size = strlen(script_chat->request);
+	uint8_t *script_chat_request_start;
+	uint16_t script_chat_request_remaining;
+	int ret;
+
+	/* Validate data to send */
+	if (script_chat_request_size == chat->script_send_request_pos) {
+		return true;
+	}
+
+	script_chat_request_start = (uint8_t *)&script_chat->request[chat->script_send_request_pos];
+	script_chat_request_remaining = script_chat_request_size - chat->script_send_request_pos;
+
+	/* Send data through pipe */
+	ret = modem_pipe_transmit(chat->pipe, script_chat_request_start,
+				  script_chat_request_remaining);
+
+	/* Validate transmit successful */
+	if (ret < 1) {
+		return false;
+	}
+
+	/* Update script send position */
+	chat->script_send_request_pos += (uint16_t)ret;
+
+	/* Check if data remains */
+	if (chat->script_send_request_pos < script_chat_request_size) {
+		return false;
+	}
+
+	return true;
+}
+
+static bool modem_chat_script_send_delimiter(struct modem_chat *chat)
+{
+	uint8_t *script_chat_delimiter_start;
+	uint8_t script_chat_delimiter_remaining;
+	int ret;
+
+	/* Validate data to send */
+	if (chat->delimiter_size == chat->script_send_delimiter_pos) {
+		return true;
+	}
+
+	script_chat_delimiter_start = (uint8_t *)&chat->delimiter[chat->script_send_delimiter_pos];
+	script_chat_delimiter_remaining = chat->delimiter_size - chat->script_send_delimiter_pos;
+
+	/* Send data through pipe */
+	ret = modem_pipe_transmit(chat->pipe, script_chat_delimiter_start,
+				  script_chat_delimiter_remaining);
+
+	/* Validate transmit successful */
+	if (ret < 1) {
+		return false;
+	}
+
+	/* Update script send position */
+	chat->script_send_delimiter_pos += (uint8_t)ret;
+
+	/* Check if data remains */
+	if (chat->script_send_delimiter_pos < chat->delimiter_size) {
+		return false;
+	}
+
+	return true;
+}
+
+static bool modem_chat_script_chat_is_no_response(struct modem_chat *chat)
+{
+	const struct modem_chat_script_chat *script_chat =
+		&chat->script->script_chats[chat->script_chat_it];
+
+	return (script_chat->response_matches_size == 0) ? true : false;
+}
+
+static uint16_t modem_chat_script_chat_get_send_timeout(struct modem_chat *chat)
+{
+	const struct modem_chat_script_chat *script_chat =
+		&chat->script->script_chats[chat->script_chat_it];
+
+	return script_chat->timeout;
+}
+
+static void modem_chat_script_send_handler(struct k_work *item)
+{
+	struct modem_chat *chat = CONTAINER_OF(item, struct modem_chat, script_send_work);
+	uint16_t timeout;
+
+	/* Validate script running */
+	if (chat->script == NULL) {
+		return;
+	}
+
+	/* Send request */
+	if (modem_chat_script_send_request(chat) == false) {
+		k_work_schedule(&chat->script_send_work, chat->process_timeout);
+		return;
+	}
+
+	/* Send delimiter */
+	if (modem_chat_script_send_delimiter(chat) == false) {
+		k_work_schedule(&chat->script_send_work, chat->process_timeout);
+		return;
+	}
+
+	/* Check if script command is no response */
+	if (modem_chat_script_chat_is_no_response(chat)) {
+		timeout = modem_chat_script_chat_get_send_timeout(chat);
+
+		if (timeout == 0) {
+			modem_chat_script_next(chat, false);
+		} else {
+			k_work_schedule(&chat->script_send_timeout_work, K_MSEC(timeout));
+		}
+	}
+}
+
+static void modem_chat_script_send_timeout_handler(struct k_work *item)
+{
+	struct modem_chat *chat = CONTAINER_OF(item, struct modem_chat, script_send_timeout_work);
+
+	/* Validate script is currently running */
+	if (chat->script == NULL) {
+		return;
+	}
+
+	modem_chat_script_next(chat, false);
+}
+
+static void modem_chat_parse_reset(struct modem_chat *chat)
+{
+	/* Reset parameters used for parsing */
+	chat->receive_buf_len = 0;
+	chat->delimiter_match_len = 0;
+	chat->argc = 0;
+	chat->parse_match = NULL;
+}
+
+/* Exact match is stored at end of receive buffer */
+static void modem_chat_parse_save_match(struct modem_chat *chat)
+{
+	uint8_t *argv;
+
+	/* Store length of match including NULL to avoid overwriting it if buffer overruns */
+	chat->parse_match_len = chat->receive_buf_len + 1;
+
+	/* Copy match to end of receive buffer */
+	argv = &chat->receive_buf[chat->receive_buf_size - chat->parse_match_len];
+
+	/* Copy match to end of receive buffer (excluding NULL) */
+	memcpy(argv, &chat->receive_buf[0], chat->parse_match_len - 1);
+
+	/* Save match */
+	chat->argv[chat->argc] = argv;
+
+	/* Terminate match */
+	chat->receive_buf[chat->receive_buf_size - 1] = '\0';
+
+	/* Increment argument count */
+	chat->argc++;
+}
+
+static bool modem_chat_match_matches_received(struct modem_chat *chat,
+					      const struct modem_chat_match *match)
+{
+	for (uint16_t i = 0; i < match->match_size; i++) {
+		if ((match->match[i] == chat->receive_buf[i]) ||
+		    (match->wildcards == true && match->match[i] == '?')) {
+			continue;
+		}
+
+		return false;
+	}
+
+	return true;
+}
+
+static bool modem_chat_parse_find_match(struct modem_chat *chat)
+{
+	/* Find in all matches types */
+	for (uint16_t i = 0; i < ARRAY_SIZE(chat->matches); i++) {
+		/* Find in all matches of matches type */
+		for (uint16_t u = 0; u < chat->matches_size[i]; u++) {
+			/* Validate match size matches received data length */
+			if (chat->matches[i][u].match_size != chat->receive_buf_len) {
+				continue;
+			}
+
+			/* Validate match */
+			if (modem_chat_match_matches_received(chat, &chat->matches[i][u]) ==
+			    false) {
+				continue;
+			}
+
+			/* Complete match found */
+			chat->parse_match = &chat->matches[i][u];
+			chat->parse_match_type = i;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static bool modem_chat_parse_is_separator(struct modem_chat *chat)
+{
+	for (uint16_t i = 0; i < chat->parse_match->separators_size; i++) {
+		if ((chat->parse_match->separators[i]) ==
+		    (chat->receive_buf[chat->receive_buf_len - 1])) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static bool modem_chat_parse_end_del_start(struct modem_chat *chat)
+{
+	for (uint8_t i = 0; i < chat->delimiter_size; i++) {
+		if (chat->receive_buf[chat->receive_buf_len - 1] == chat->delimiter[i]) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+static bool modem_chat_parse_end_del_complete(struct modem_chat *chat)
+{
+	/* Validate length of end delimiter */
+	if (chat->receive_buf_len < chat->delimiter_size) {
+		return false;
+	}
+
+	/* Compare end delimiter with receive buffer content */
+	return (memcmp(&chat->receive_buf[chat->receive_buf_len - chat->delimiter_size],
+		       chat->delimiter, chat->delimiter_size) == 0)
+		       ? true
+		       : false;
+}
+
+static void modem_chat_on_command_received_unsol(struct modem_chat *chat)
+{
+	/* Callback */
+	if (chat->parse_match->callback != NULL) {
+		chat->parse_match->callback(chat, (char **)chat->argv, chat->argc, chat->user_data);
+	}
+}
+
+static void modem_chat_on_command_received_abort(struct modem_chat *chat)
+{
+	/* Callback */
+	if (chat->parse_match->callback != NULL) {
+		chat->parse_match->callback(chat, (char **)chat->argv, chat->argc, chat->user_data);
+	}
+
+	/* Abort script */
+	modem_chat_script_stop(chat, MODEM_CHAT_SCRIPT_RESULT_ABORT);
+}
+
+static void modem_chat_on_command_received_resp(struct modem_chat *chat)
+{
+	/* Callback */
+	if (chat->parse_match->callback != NULL) {
+		chat->parse_match->callback(chat, (char **)chat->argv, chat->argc, chat->user_data);
+	}
+
+	/* Advance script */
+	modem_chat_script_next(chat, false);
+}
+
+static bool modem_chat_parse_find_catch_all_match(struct modem_chat *chat)
+{
+	/* Find in all matches types */
+	for (uint16_t i = 0; i < ARRAY_SIZE(chat->matches); i++) {
+		/* Find in all matches of matches type */
+		for (uint16_t u = 0; u < chat->matches_size[i]; u++) {
+			/* Validate match config is matching previous bytes */
+			if (chat->matches[i][u].match_size == 0) {
+				chat->parse_match = &chat->matches[i][u];
+				chat->parse_match_type = i;
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+static void modem_chat_on_command_received(struct modem_chat *chat)
+{
+	modem_chat_log_received_command(chat);
+
+	switch (chat->parse_match_type) {
+	case MODEM_CHAT_MATCHES_INDEX_UNSOL:
+		modem_chat_on_command_received_unsol(chat);
+		break;
+
+	case MODEM_CHAT_MATCHES_INDEX_ABORT:
+		modem_chat_on_command_received_abort(chat);
+		break;
+
+	case MODEM_CHAT_MATCHES_INDEX_RESPONSE:
+		modem_chat_on_command_received_resp(chat);
+		break;
+	}
+}
+
+static void modem_chat_on_unknown_command_received(struct modem_chat *chat)
+{
+	/* Terminate received command */
+	chat->receive_buf[chat->receive_buf_len - chat->delimiter_size] = '\0';
+
+	/* Try to find catch all match */
+	if (modem_chat_parse_find_catch_all_match(chat) == false) {
+		LOG_DBG("%s", chat->receive_buf);
+		return;
+	}
+
+	/* Parse command */
+	chat->argv[0] = "";
+	chat->argv[1] = chat->receive_buf;
+	chat->argc = 2;
+
+	modem_chat_on_command_received(chat);
+}
+
+static void modem_chat_process_byte(struct modem_chat *chat, uint8_t byte)
+{
+	/* Validate receive buffer not overrun */
+	if (chat->receive_buf_size == chat->receive_buf_len) {
+		LOG_WRN("receive buffer overrun");
+		modem_chat_parse_reset(chat);
+		return;
+	}
+
+	/* Validate argv buffer not overrun */
+	if (chat->argc == chat->argv_size) {
+		LOG_WRN("argv buffer overrun");
+		modem_chat_parse_reset(chat);
+		return;
+	}
+
+	/* Copy byte to receive buffer */
+	chat->receive_buf[chat->receive_buf_len] = byte;
+	chat->receive_buf_len++;
+
+	/* Validate end delimiter not complete */
+	if (modem_chat_parse_end_del_complete(chat) == true) {
+		/* Filter out empty lines */
+		if (chat->receive_buf_len == chat->delimiter_size) {
+			/* Reset parser */
+			modem_chat_parse_reset(chat);
+			return;
+		}
+
+		/* Check if match exists */
+		if (chat->parse_match == NULL) {
+			/* Handle unknown command */
+			modem_chat_on_unknown_command_received(chat);
+
+			/* Reset parser */
+			modem_chat_parse_reset(chat);
+			return;
+		}
+
+		/* Check if trailing argument exists */
+		if (chat->parse_arg_len > 0) {
+			chat->argv[chat->argc] =
+				&chat->receive_buf[chat->receive_buf_len - chat->delimiter_size -
+						   chat->parse_arg_len];
+			chat->receive_buf[chat->receive_buf_len - chat->delimiter_size] = '\0';
+			chat->argc++;
+		}
+
+		/* Handle received command */
+		modem_chat_on_command_received(chat);
+
+		/* Reset parser */
+		modem_chat_parse_reset(chat);
+		return;
+	}
+
+	/* Validate end delimiter not started */
+	if (modem_chat_parse_end_del_start(chat) == true) {
+		return;
+	}
+
+	/* Find matching command if missing */
+	if (chat->parse_match == NULL) {
+		/* Find matching command */
+		if (modem_chat_parse_find_match(chat) == false) {
+			return;
+		}
+
+		/* Save match */
+		modem_chat_parse_save_match(chat);
+
+		/* Prepare argument parser */
+		chat->parse_arg_len = 0;
+		return;
+	}
+
+	/* Check if separator reached */
+	if (modem_chat_parse_is_separator(chat) == true) {
+		/* Check if argument is empty */
+		if (chat->parse_arg_len == 0) {
+			/* Save empty argument */
+			chat->argv[chat->argc] = "";
+		} else {
+			/* Save pointer to start of argument */
+			chat->argv[chat->argc] =
+				&chat->receive_buf[chat->receive_buf_len - chat->parse_arg_len - 1];
+
+			/* Replace separator with string terminator */
+			chat->receive_buf[chat->receive_buf_len - 1] = '\0';
+		}
+
+		/* Increment argument count */
+		chat->argc++;
+
+		/* Reset parse argument length */
+		chat->parse_arg_len = 0;
+		return;
+	}
+
+	/* Increment argument length */
+	chat->parse_arg_len++;
+}
+
+static bool modem_chat_discard_byte(struct modem_chat *chat, uint8_t byte)
+{
+	for (uint8_t i = 0; i < chat->filter_size; i++) {
+		if (byte == chat->filter[i]) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/* Process chunk of received bytes */
+static void modem_chat_process_bytes(struct modem_chat *chat)
+{
+	for (uint16_t i = 0; i < chat->work_buf_len; i++) {
+		if (modem_chat_discard_byte(chat, chat->work_buf[i])) {
+			continue;
+		}
+
+		modem_chat_process_byte(chat, chat->work_buf[i]);
+	}
+}
+
+static void modem_chat_process_handler(struct k_work *item)
+{
+	struct modem_chat *chat = CONTAINER_OF(item, struct modem_chat, process_work);
+	int ret;
+
+	/* Fill work buffer */
+	ret = modem_pipe_receive(chat->pipe, chat->work_buf, sizeof(chat->work_buf));
+	if (ret < 1) {
+		return;
+	}
+
+	/* Save received data length */
+	chat->work_buf_len = (size_t)ret;
+
+	/* Process data */
+	modem_chat_process_bytes(chat);
+	k_work_schedule(&chat->process_work, K_NO_WAIT);
+}
+
+static void modem_chat_pipe_callback(struct modem_pipe *pipe, enum modem_pipe_event event,
+				     void *user_data)
+{
+	struct modem_chat *chat = (struct modem_chat *)user_data;
+
+	if (event == MODEM_PIPE_EVENT_RECEIVE_READY) {
+		k_work_schedule(&chat->process_work, chat->process_timeout);
+	}
+}
+
+/*********************************************************
+ * GLOBAL FUNCTIONS
+ *********************************************************/
+int modem_chat_init(struct modem_chat *chat, const struct modem_chat_config *config)
+{
+	__ASSERT_NO_MSG(chat != NULL);
+	__ASSERT_NO_MSG(config != NULL);
+	__ASSERT_NO_MSG(config->receive_buf != NULL);
+	__ASSERT_NO_MSG(config->receive_buf_size > 0);
+	__ASSERT_NO_MSG(config->argv != NULL);
+	__ASSERT_NO_MSG(config->argv_size > 0);
+	__ASSERT_NO_MSG(config->delimiter != NULL);
+	__ASSERT_NO_MSG(config->delimiter_size > 0);
+	__ASSERT_NO_MSG(!((config->filter == NULL) && (config->filter > 0)));
+	__ASSERT_NO_MSG(!((config->unsol_matches == NULL) && (config->unsol_matches_size > 0)));
+
+	memset(chat, 0x00, sizeof(*chat));
+	chat->pipe = NULL;
+	chat->user_data = config->user_data;
+	chat->receive_buf = config->receive_buf;
+	chat->receive_buf_size = config->receive_buf_size;
+	chat->argv = config->argv;
+	chat->argv_size = config->argv_size;
+	chat->delimiter = config->delimiter;
+	chat->delimiter_size = config->delimiter_size;
+	chat->filter = config->filter;
+	chat->filter_size = config->filter_size;
+	chat->matches[MODEM_CHAT_MATCHES_INDEX_UNSOL] = config->unsol_matches;
+	chat->matches_size[MODEM_CHAT_MATCHES_INDEX_UNSOL] = config->unsol_matches_size;
+	chat->process_timeout = config->process_timeout;
+	atomic_set(&chat->script_state, 0);
+	k_work_init_delayable(&chat->process_work, modem_chat_process_handler);
+	k_work_init(&chat->script_run_work, modem_chat_script_run_handler);
+	k_work_init_delayable(&chat->script_timeout_work, modem_chat_script_timeout_handler);
+	k_work_init(&chat->script_abort_work, modem_chat_script_abort_handler);
+	k_work_init_delayable(&chat->script_send_work, modem_chat_script_send_handler);
+	k_work_init_delayable(&chat->script_send_timeout_work,
+			      modem_chat_script_send_timeout_handler);
+
+	return 0;
+}
+
+int modem_chat_attach(struct modem_chat *chat, struct modem_pipe *pipe)
+{
+	chat->pipe = pipe;
+	modem_chat_parse_reset(chat);
+	modem_pipe_attach(chat->pipe, modem_chat_pipe_callback, chat);
+	return 0;
+}
+
+int modem_chat_script_run(struct modem_chat *chat, const struct modem_chat_script *script)
+{
+	bool script_is_running;
+
+	if (chat->pipe == NULL) {
+		return -EPERM;
+	}
+
+	/* Validate script */
+	if ((script->script_chats == NULL) || (script->script_chats_size == 0) ||
+	    ((script->abort_matches != NULL) && (script->abort_matches_size == 0))) {
+		return -EINVAL;
+	}
+
+	/* Validate script commands */
+	for (uint16_t i = 0; i < script->script_chats_size; i++) {
+		if ((strlen(script->script_chats[i].request) == 0) &&
+		    (script->script_chats[i].response_matches_size == 0)) {
+			return -EINVAL;
+		}
+	}
+
+	script_is_running =
+		atomic_test_and_set_bit(&chat->script_state, MODEM_CHAT_SCRIPT_STATE_RUNNING_BIT);
+
+	if (script_is_running == true) {
+		return -EBUSY;
+	}
+
+	chat->pending_script = script;
+	k_work_submit(&chat->script_run_work);
+	return 0;
+}
+
+void modem_chat_script_abort(struct modem_chat *chat)
+{
+	k_work_submit(&chat->script_abort_work);
+}
+
+void modem_chat_release(struct modem_chat *chat)
+{
+	struct k_work_sync sync;
+
+	if (chat->pipe) {
+		modem_pipe_release(chat->pipe);
+	}
+
+	k_work_cancel_sync(&chat->script_run_work, &sync);
+	k_work_cancel_sync(&chat->script_abort_work, &sync);
+	k_work_cancel_delayable_sync(&chat->process_work, &sync);
+	k_work_cancel_delayable_sync(&chat->script_send_work, &sync);
+
+	chat->pipe = NULL;
+	chat->receive_buf_len = 0;
+	chat->work_buf_len = 0;
+	chat->argc = 0;
+	chat->script = NULL;
+	chat->script_chat_it = 0;
+	atomic_set(&chat->script_state, 0);
+	chat->script_send_request_pos = 0;
+	chat->script_send_delimiter_pos = 0;
+	chat->parse_match = NULL;
+	chat->parse_match_len = 0;
+	chat->parse_arg_len = 0;
+}
diff --git a/subsys/modem/modem_cmux.c b/subsys/modem/modem_cmux.c
new file mode 100644
index 0000000..cd45d7f
--- /dev/null
+++ b/subsys/modem/modem_cmux.c
@@ -0,0 +1,1016 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(modem_cmux, CONFIG_MODEM_MODULES_LOG_LEVEL);
+
+#include <zephyr/kernel.h>
+#include <zephyr/sys/crc.h>
+#include <zephyr/modem/cmux.h>
+
+#include <string.h>
+
+#define MODEM_CMUX_FCS_POLYNOMIAL		(0xE0)
+#define MODEM_CMUX_FCS_INIT_VALUE		(0xFF)
+#define MODEM_CMUX_EA				(0x01)
+#define MODEM_CMUX_CR				(0x02)
+#define MODEM_CMUX_PF				(0x10)
+#define MODEM_CMUX_FRAME_SIZE_MAX		(0x08)
+#define MODEM_CMUX_DATA_SIZE_MIN		(0x08)
+#define MODEM_CMUX_DATA_FRAME_SIZE_MIN		(MODEM_CMUX_FRAME_SIZE_MAX + \
+						 MODEM_CMUX_DATA_SIZE_MIN)
+
+#define MODEM_CMUX_CMD_DATA_SIZE_MAX		(0x04)
+#define MODEM_CMUX_CMD_FRAME_SIZE_MAX		(MODEM_CMUX_FRAME_SIZE_MAX + \
+						 MODEM_CMUX_CMD_DATA_SIZE_MAX)
+
+#define MODEM_CMUX_T1_TIMEOUT			(K_MSEC(330))
+#define MODEM_CMUX_T2_TIMEOUT			(K_MSEC(660))
+
+#define MODEM_CMUX_EVENT_CONNECTED_BIT		(BIT(0))
+#define MODEM_CMUX_EVENT_DISCONNECTED_BIT	(BIT(1))
+
+enum modem_cmux_frame_types {
+	MODEM_CMUX_FRAME_TYPE_RR = 0x01,
+	MODEM_CMUX_FRAME_TYPE_UI = 0x03,
+	MODEM_CMUX_FRAME_TYPE_RNR = 0x05,
+	MODEM_CMUX_FRAME_TYPE_REJ = 0x09,
+	MODEM_CMUX_FRAME_TYPE_DM = 0x0F,
+	MODEM_CMUX_FRAME_TYPE_SABM = 0x2F,
+	MODEM_CMUX_FRAME_TYPE_DISC = 0x43,
+	MODEM_CMUX_FRAME_TYPE_UA = 0x63,
+	MODEM_CMUX_FRAME_TYPE_UIH = 0xEF,
+};
+
+enum modem_cmux_command_types {
+	MODEM_CMUX_COMMAND_NSC = 0x04,
+	MODEM_CMUX_COMMAND_TEST = 0x08,
+	MODEM_CMUX_COMMAND_PSC = 0x10,
+	MODEM_CMUX_COMMAND_RLS = 0x14,
+	MODEM_CMUX_COMMAND_FCOFF = 0x18,
+	MODEM_CMUX_COMMAND_PN = 0x20,
+	MODEM_CMUX_COMMAND_RPN = 0x24,
+	MODEM_CMUX_COMMAND_FCON = 0x28,
+	MODEM_CMUX_COMMAND_CLD = 0x30,
+	MODEM_CMUX_COMMAND_SNC = 0x34,
+	MODEM_CMUX_COMMAND_MSC = 0x38,
+};
+
+struct modem_cmux_command_type {
+	uint8_t ea: 1;
+	uint8_t cr: 1;
+	uint8_t value: 6;
+};
+
+struct modem_cmux_command_length {
+	uint8_t ea: 1;
+	uint8_t value: 7;
+};
+
+struct modem_cmux_command {
+	struct modem_cmux_command_type type;
+	struct modem_cmux_command_length length;
+	uint8_t value[];
+};
+
+static int modem_cmux_wrap_command(struct modem_cmux_command **command, const uint8_t *data,
+				   uint16_t data_len)
+{
+	if ((data == NULL) || (data_len < 2)) {
+		return -EINVAL;
+	}
+
+	(*command) = (struct modem_cmux_command *)data;
+
+	if (((*command)->length.ea == 0) || ((*command)->type.ea == 0)) {
+		return -EINVAL;
+	}
+
+	if ((*command)->length.value != (data_len - 2)) {
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static struct modem_cmux_command *modem_cmux_command_wrap(uint8_t *data)
+{
+	return (struct modem_cmux_command *)data;
+}
+
+static void modem_cmux_log_unknown_frame(struct modem_cmux *cmux)
+{
+	char data[24];
+	uint8_t data_cnt = (cmux->frame.data_len < 8) ? cmux->frame.data_len : 8;
+
+	for (uint8_t i = 0; i < data_cnt; i++) {
+		snprintk(&data[i * 3], sizeof(data) - (i * 3), "%02X,", cmux->frame.data[i]);
+	}
+
+	/* Remove trailing */
+	if (data_cnt > 0) {
+		data[(data_cnt * 3) - 1] = '\0';
+	}
+
+	LOG_DBG("ch:%u, type:%u, data:%s", cmux->frame.dlci_address, cmux->frame.type, data);
+}
+
+static void modem_cmux_raise_event(struct modem_cmux *cmux, enum modem_cmux_event event)
+{
+	if (cmux->callback == NULL) {
+		return;
+	}
+
+	cmux->callback(cmux, event, cmux->user_data);
+}
+
+static void modem_cmux_bus_callback(struct modem_pipe *pipe, enum modem_pipe_event event,
+				    void *user_data)
+{
+	struct modem_cmux *cmux = (struct modem_cmux *)user_data;
+
+	if (event == MODEM_PIPE_EVENT_RECEIVE_READY) {
+		k_work_schedule(&cmux->receive_work, K_NO_WAIT);
+	}
+}
+
+static uint16_t modem_cmux_transmit_frame(struct modem_cmux *cmux,
+					  const struct modem_cmux_frame *frame)
+{
+	uint8_t byte;
+	uint8_t fcs;
+	uint16_t space;
+	uint16_t data_len;
+
+	space = ring_buf_space_get(&cmux->transmit_rb) - MODEM_CMUX_FRAME_SIZE_MAX;
+	data_len = (space < frame->data_len) ? space : frame->data_len;
+
+	/* SOF */
+	byte = 0xF9;
+	ring_buf_put(&cmux->transmit_rb, &byte, 1);
+
+	/* DLCI Address (Max 63) */
+	byte = 0x01 | (frame->cr << 1) | (frame->dlci_address << 2);
+	fcs = crc8(&byte, 1, MODEM_CMUX_FCS_POLYNOMIAL, MODEM_CMUX_FCS_INIT_VALUE, true);
+	ring_buf_put(&cmux->transmit_rb, &byte, 1);
+
+	/* Frame type and poll/final */
+	byte = frame->type | (frame->pf << 4);
+	fcs = crc8(&byte, 1, MODEM_CMUX_FCS_POLYNOMIAL, fcs, true);
+	ring_buf_put(&cmux->transmit_rb, &byte, 1);
+
+	/* Data length */
+	if (data_len > 127) {
+		byte = data_len << 1;
+		fcs = crc8(&byte, 1, MODEM_CMUX_FCS_POLYNOMIAL, fcs, true);
+		ring_buf_put(&cmux->transmit_rb, &byte, 1);
+		byte = 0x01 | (data_len >> 7);
+		ring_buf_put(&cmux->transmit_rb, &byte, 1);
+	} else {
+		byte = 0x01 | (data_len << 1);
+		ring_buf_put(&cmux->transmit_rb, &byte, 1);
+	}
+
+	/* FCS final */
+	if (frame->type == MODEM_CMUX_FRAME_TYPE_UIH) {
+		fcs = 0xFF - crc8(&byte, 1, MODEM_CMUX_FCS_POLYNOMIAL, fcs, true);
+	} else {
+		fcs = crc8(&byte, 1, MODEM_CMUX_FCS_POLYNOMIAL, fcs, true);
+		fcs = 0xFF - crc8(frame->data, data_len, MODEM_CMUX_FCS_POLYNOMIAL, fcs, true);
+	}
+
+	/* Data */
+	ring_buf_put(&cmux->transmit_rb, frame->data, data_len);
+
+	/* FCS */
+	ring_buf_put(&cmux->transmit_rb, &fcs, 1);
+
+	/* EOF */
+	byte = 0xF9;
+	ring_buf_put(&cmux->transmit_rb, &byte, 1);
+	k_work_schedule(&cmux->transmit_work, K_NO_WAIT);
+	return data_len;
+}
+
+static bool modem_cmux_transmit_cmd_frame(struct modem_cmux *cmux,
+					  const struct modem_cmux_frame *frame)
+{
+	uint16_t space;
+
+	k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER);
+	space = ring_buf_space_get(&cmux->transmit_rb);
+
+	if (space < MODEM_CMUX_CMD_FRAME_SIZE_MAX) {
+		k_mutex_unlock(&cmux->transmit_rb_lock);
+		return false;
+	}
+
+	modem_cmux_transmit_frame(cmux, frame);
+	k_mutex_unlock(&cmux->transmit_rb_lock);
+	return true;
+}
+
+static int16_t modem_cmux_transmit_data_frame(struct modem_cmux *cmux,
+					      const struct modem_cmux_frame *frame)
+{
+	uint16_t space;
+	int ret;
+
+	k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER);
+
+	if (cmux->flow_control_on == false) {
+		k_mutex_unlock(&cmux->transmit_rb_lock);
+		return 0;
+	}
+
+	space = ring_buf_space_get(&cmux->transmit_rb);
+
+	/*
+	 * Two command frames are reserved for command channel, and we shall prefer
+	 * waiting for more than MODEM_CMUX_DATA_FRAME_SIZE_MIN bytes available in the
+	 * transmit buffer rather than transmitting a few bytes at a time. This avoids
+	 * excessive wrapping overhead, since transmitting a single byte will require 8
+	 * bytes of wrapping.
+	 */
+	if (space < ((MODEM_CMUX_CMD_FRAME_SIZE_MAX * 2) + MODEM_CMUX_DATA_FRAME_SIZE_MIN)) {
+		k_mutex_unlock(&cmux->transmit_rb_lock);
+		return -ENOMEM;
+	}
+
+	ret = modem_cmux_transmit_frame(cmux, frame);
+	k_mutex_unlock(&cmux->transmit_rb_lock);
+	return ret;
+}
+
+static void modem_cmux_acknowledge_received_frame(struct modem_cmux *cmux)
+{
+	struct modem_cmux_command *command;
+	struct modem_cmux_frame frame;
+	uint8_t data[MODEM_CMUX_CMD_DATA_SIZE_MAX];
+
+	if (sizeof(data) < cmux->frame.data_len) {
+		LOG_WRN("Command acknowledge buffer overrun");
+		return;
+	}
+
+	memcpy(&frame, &cmux->frame, sizeof(cmux->frame));
+	memcpy(data, cmux->frame.data, cmux->frame.data_len);
+	modem_cmux_wrap_command(&command, data, cmux->frame.data_len);
+	command->type.cr = 0;
+	frame.data = data;
+	frame.data_len = cmux->frame.data_len;
+
+	if (modem_cmux_transmit_cmd_frame(cmux, &frame) == false) {
+		LOG_WRN("Command acknowledge buffer overrun");
+	}
+}
+
+static void modem_cmux_on_msc_command(struct modem_cmux *cmux)
+{
+	modem_cmux_acknowledge_received_frame(cmux);
+}
+
+static void modem_cmux_on_fcon_command(struct modem_cmux *cmux)
+{
+	k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER);
+	cmux->flow_control_on = true;
+	k_mutex_unlock(&cmux->transmit_rb_lock);
+	modem_cmux_acknowledge_received_frame(cmux);
+}
+
+static void modem_cmux_on_fcoff_command(struct modem_cmux *cmux)
+{
+	k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER);
+	cmux->flow_control_on = false;
+	k_mutex_unlock(&cmux->transmit_rb_lock);
+	modem_cmux_acknowledge_received_frame(cmux);
+}
+
+static void modem_cmux_on_cld_command(struct modem_cmux *cmux)
+{
+	if (cmux->state != MODEM_CMUX_STATE_DISCONNECTING) {
+		LOG_WRN("Unexpected close down");
+	}
+
+	cmux->state = MODEM_CMUX_STATE_DISCONNECTED;
+	k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER);
+	cmux->flow_control_on = false;
+	k_mutex_unlock(&cmux->transmit_rb_lock);
+	k_work_cancel_delayable(&cmux->disconnect_work);
+	modem_cmux_raise_event(cmux, MODEM_CMUX_EVENT_DISCONNECTED);
+	k_event_clear(&cmux->event, MODEM_CMUX_EVENT_CONNECTED_BIT);
+	k_event_post(&cmux->event, MODEM_CMUX_EVENT_DISCONNECTED_BIT);
+}
+
+static void modem_cmux_on_control_frame_ua(struct modem_cmux *cmux)
+{
+	if (cmux->state != MODEM_CMUX_STATE_CONNECTING) {
+		LOG_DBG("Unexpected UA frame");
+
+		return;
+	}
+
+	cmux->state = MODEM_CMUX_STATE_CONNECTED;
+	k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER);
+	cmux->flow_control_on = true;
+	k_mutex_unlock(&cmux->transmit_rb_lock);
+	k_work_cancel_delayable(&cmux->connect_work);
+	modem_cmux_raise_event(cmux, MODEM_CMUX_EVENT_CONNECTED);
+	k_event_clear(&cmux->event, MODEM_CMUX_EVENT_DISCONNECTED_BIT);
+	k_event_post(&cmux->event, MODEM_CMUX_EVENT_CONNECTED_BIT);
+}
+
+static void modem_cmux_on_control_frame_uih(struct modem_cmux *cmux)
+{
+	struct modem_cmux_command *command;
+
+	if ((cmux->state != MODEM_CMUX_STATE_CONNECTED) &&
+	    (cmux->state != MODEM_CMUX_STATE_DISCONNECTING)) {
+		LOG_DBG("Unexpected UIH frame");
+		return;
+	}
+
+	if (modem_cmux_wrap_command(&command, cmux->frame.data, cmux->frame.data_len) < 0) {
+		LOG_WRN("Invalid command");
+		return;
+	}
+
+	switch (command->type.value) {
+	case MODEM_CMUX_COMMAND_CLD:
+		modem_cmux_on_cld_command(cmux);
+		break;
+
+	case MODEM_CMUX_COMMAND_MSC:
+		modem_cmux_on_msc_command(cmux);
+		break;
+
+	case MODEM_CMUX_COMMAND_FCON:
+		modem_cmux_on_fcon_command(cmux);
+		break;
+
+	case MODEM_CMUX_COMMAND_FCOFF:
+		modem_cmux_on_fcoff_command(cmux);
+		break;
+
+	default:
+		LOG_DBG("Unknown command");
+		break;
+	}
+}
+
+static void modem_cmux_on_control_frame(struct modem_cmux *cmux)
+{
+	switch (cmux->frame.type) {
+	case MODEM_CMUX_FRAME_TYPE_UA:
+		modem_cmux_on_control_frame_ua(cmux);
+		break;
+
+	case MODEM_CMUX_FRAME_TYPE_UIH:
+		modem_cmux_on_control_frame_uih(cmux);
+		break;
+
+	default:
+		modem_cmux_log_unknown_frame(cmux);
+		break;
+	}
+}
+
+static struct modem_cmux_dlci *modem_cmux_find_dlci(struct modem_cmux *cmux)
+{
+	sys_snode_t *node;
+	struct modem_cmux_dlci *dlci;
+
+	SYS_SLIST_FOR_EACH_NODE(&cmux->dlcis, node) {
+		dlci = (struct modem_cmux_dlci *)node;
+
+		if (dlci->dlci_address == cmux->frame.dlci_address) {
+			return dlci;
+		}
+	}
+
+	return NULL;
+}
+
+static void modem_cmux_on_dlci_frame_ua(struct modem_cmux_dlci *dlci)
+{
+	switch (dlci->state) {
+	case MODEM_CMUX_DLCI_STATE_OPENING:
+		dlci->state = MODEM_CMUX_DLCI_STATE_OPEN;
+		modem_pipe_notify_opened(&dlci->pipe);
+		k_work_cancel_delayable(&dlci->open_work);
+		break;
+
+	case MODEM_CMUX_DLCI_STATE_CLOSING:
+		dlci->state = MODEM_CMUX_DLCI_STATE_CLOSED;
+		modem_pipe_notify_closed(&dlci->pipe);
+		k_work_cancel_delayable(&dlci->close_work);
+		break;
+
+	default:
+		LOG_DBG("Unexpected UA frame");
+		break;
+	}
+}
+
+static void modem_cmux_on_dlci_frame_uih(struct modem_cmux_dlci *dlci)
+{
+	struct modem_cmux *cmux = dlci->cmux;
+
+	if (dlci->state != MODEM_CMUX_DLCI_STATE_OPEN) {
+		LOG_DBG("Unexpected UIH frame");
+		return;
+	}
+
+	k_mutex_lock(&dlci->receive_rb_lock, K_FOREVER);
+	ring_buf_put(&dlci->receive_rb, cmux->frame.data, cmux->frame.data_len);
+	k_mutex_unlock(&dlci->receive_rb_lock);
+	modem_pipe_notify_receive_ready(&dlci->pipe);
+}
+
+static void modem_cmux_on_dlci_frame(struct modem_cmux *cmux)
+{
+	struct modem_cmux_dlci *dlci;
+
+	dlci = modem_cmux_find_dlci(cmux);
+
+	if (dlci == NULL) {
+		LOG_WRN("Could not find DLCI: %u", cmux->frame.dlci_address);
+
+		return;
+	}
+
+	switch (cmux->frame.type) {
+	case MODEM_CMUX_FRAME_TYPE_UA:
+		modem_cmux_on_dlci_frame_ua(dlci);
+		break;
+
+	case MODEM_CMUX_FRAME_TYPE_UIH:
+		modem_cmux_on_dlci_frame_uih(dlci);
+		break;
+
+	default:
+		modem_cmux_log_unknown_frame(cmux);
+		break;
+	}
+}
+
+static void modem_cmux_on_frame(struct modem_cmux *cmux)
+{
+	if (cmux->frame.dlci_address == 0) {
+		modem_cmux_on_control_frame(cmux);
+		return;
+	}
+
+	modem_cmux_on_dlci_frame(cmux);
+}
+
+static void modem_cmux_process_received_byte(struct modem_cmux *cmux, uint8_t byte)
+{
+	uint8_t fcs;
+	static const uint8_t resync[3] = {0xF9, 0xF9, 0xF9};
+
+	switch (cmux->receive_state) {
+	case MODEM_CMUX_RECEIVE_STATE_SOF:
+		if (byte == 0xF9) {
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_ADDRESS;
+			break;
+		}
+
+		/* Send resync flags */
+		modem_pipe_transmit(cmux->pipe, resync, sizeof(resync));
+
+		/* Await resync flags */
+		cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_RESYNC_0;
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_RESYNC_0:
+		if (byte == 0xF9) {
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_RESYNC_1;
+		}
+
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_RESYNC_1:
+		if (byte == 0xF9) {
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_RESYNC_2;
+		} else {
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_RESYNC_0;
+		}
+
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_RESYNC_2:
+		if (byte == 0xF9) {
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_RESYNC_3;
+		} else {
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_RESYNC_0;
+		}
+
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_RESYNC_3:
+		if (byte == 0xF9) {
+			break;
+		}
+
+	case MODEM_CMUX_RECEIVE_STATE_ADDRESS:
+		/* Initialize */
+		cmux->receive_buf_len = 0;
+		cmux->frame_header_len = 0;
+
+		/* Store header for FCS */
+		cmux->frame_header[cmux->frame_header_len] = byte;
+		cmux->frame_header_len++;
+
+		/* Get CR */
+		cmux->frame.cr = (byte & 0x02) ? true : false;
+
+		/* Get DLCI address */
+		cmux->frame.dlci_address = (byte >> 2) & 0x3F;
+
+		/* Await control */
+		cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_CONTROL;
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_CONTROL:
+		/* Store header for FCS */
+		cmux->frame_header[cmux->frame_header_len] = byte;
+		cmux->frame_header_len++;
+
+		/* Get PF */
+		cmux->frame.pf = (byte & MODEM_CMUX_PF) ? true : false;
+
+		/* Get frame type */
+		cmux->frame.type = byte & (~MODEM_CMUX_PF);
+
+		/* Await data length */
+		cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_LENGTH;
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_LENGTH:
+		/* Store header for FCS */
+		cmux->frame_header[cmux->frame_header_len] = byte;
+		cmux->frame_header_len++;
+
+		/* Get first 7 bits of data length */
+		cmux->frame.data_len = (byte >> 1);
+
+		/* Check if length field continues */
+		if ((byte & MODEM_CMUX_EA) == 0) {
+			/* Await continued length field */
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_LENGTH_CONT;
+			break;
+		}
+
+		/* Check if no data field */
+		if (cmux->frame.data_len == 0) {
+			/* Await FCS */
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_FCS;
+			break;
+		}
+
+		/* Await data */
+		cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_DATA;
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_LENGTH_CONT:
+		/* Store header for FCS */
+		cmux->frame_header[cmux->frame_header_len] = byte;
+		cmux->frame_header_len++;
+
+		/* Get last 8 bits of data length */
+		cmux->frame.data_len |= ((uint16_t)byte) << 7;
+
+		/* Await data */
+		cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_DATA;
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_DATA:
+		/* Copy byte to data */
+		cmux->receive_buf[cmux->receive_buf_len] = byte;
+		cmux->receive_buf_len++;
+
+		/* Check if datalen reached */
+		if (cmux->frame.data_len == cmux->receive_buf_len) {
+			/* Await FCS */
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_FCS;
+			break;
+		}
+
+		/* Check if receive buffer overrun */
+		if (cmux->receive_buf_len == cmux->receive_buf_size) {
+			LOG_DBG("Receive buf overrun");
+
+			/* Drop frame */
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_EOF;
+			break;
+		}
+
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_FCS:
+		/* Compute FCS */
+		if (cmux->frame.type == MODEM_CMUX_FRAME_TYPE_UIH) {
+			fcs = 0xFF - crc8(cmux->frame_header, cmux->frame_header_len,
+					  MODEM_CMUX_FCS_POLYNOMIAL, MODEM_CMUX_FCS_INIT_VALUE,
+					  true);
+		} else {
+			fcs = crc8(cmux->frame_header, cmux->frame_header_len,
+				   MODEM_CMUX_FCS_POLYNOMIAL, MODEM_CMUX_FCS_INIT_VALUE, true);
+
+			fcs = 0xFF - crc8(cmux->frame.data, cmux->frame.data_len,
+					  MODEM_CMUX_FCS_POLYNOMIAL, fcs, true);
+		}
+
+		/* Validate FCS */
+		if (fcs != byte) {
+			LOG_WRN("Frame FCS error");
+
+			/* Drop frame */
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_DROP;
+			break;
+		}
+
+		cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_EOF;
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_DROP:
+		LOG_WRN("Dropped frame");
+		cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_SOF;
+		break;
+
+	case MODEM_CMUX_RECEIVE_STATE_EOF:
+		/* Validate byte is EOF */
+		if (byte != 0xF9) {
+			/* Unexpected byte */
+			cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_SOF;
+			break;
+		}
+
+		LOG_DBG("Received frame");
+
+		/* Process frame */
+		cmux->frame.data = cmux->receive_buf;
+		modem_cmux_on_frame(cmux);
+
+		/* Await start of next frame */
+		cmux->receive_state = MODEM_CMUX_RECEIVE_STATE_SOF;
+		break;
+
+	default:
+		break;
+	}
+}
+
+static void modem_cmux_receive_handler(struct k_work *item)
+{
+	struct modem_cmux *cmux = CONTAINER_OF(item, struct modem_cmux, receive_work);
+	uint8_t buf[16];
+	int ret;
+
+	/* Receive data from pipe */
+	ret = modem_pipe_receive(cmux->pipe, buf, sizeof(buf));
+	if (ret < 1) {
+		return;
+	}
+
+	/* Process received data */
+	for (uint16_t i = 0; i < (uint16_t)ret; i++) {
+		modem_cmux_process_received_byte(cmux, buf[i]);
+	}
+
+	/* Reschedule received work */
+	k_work_schedule(&cmux->receive_work, K_NO_WAIT);
+}
+
+static void modem_cmux_transmit_handler(struct k_work *item)
+{
+	struct modem_cmux *cmux = CONTAINER_OF(item, struct modem_cmux, transmit_work);
+	uint8_t *reserved;
+	uint32_t reserved_size;
+	int ret;
+
+	k_mutex_lock(&cmux->transmit_rb_lock, K_FOREVER);
+
+	/* Reserve data to transmit from transmit ring buffer */
+	reserved_size = ring_buf_get_claim(&cmux->transmit_rb, &reserved, UINT32_MAX);
+
+	/* Transmit reserved data */
+	ret = modem_pipe_transmit(cmux->pipe, reserved, reserved_size);
+	if (ret < 1) {
+		ring_buf_get_finish(&cmux->transmit_rb, 0);
+		k_mutex_unlock(&cmux->transmit_rb_lock);
+		k_work_schedule(&cmux->transmit_work, K_NO_WAIT);
+
+		return;
+	}
+
+	/* Release remaining reserved data */
+	ring_buf_get_finish(&cmux->transmit_rb, ret);
+
+	/* Resubmit transmit work if data remains */
+	if (ring_buf_is_empty(&cmux->transmit_rb) == false) {
+		k_work_schedule(&cmux->transmit_work, K_NO_WAIT);
+	}
+
+	k_mutex_unlock(&cmux->transmit_rb_lock);
+}
+
+static void modem_cmux_connect_handler(struct k_work *item)
+{
+	struct modem_cmux *cmux = CONTAINER_OF(item, struct modem_cmux, connect_work);
+
+	cmux->state = MODEM_CMUX_STATE_CONNECTING;
+
+	struct modem_cmux_frame frame = {
+		.dlci_address = 0,
+		.cr = true,
+		.pf = true,
+		.type = MODEM_CMUX_FRAME_TYPE_SABM,
+		.data = NULL,
+		.data_len = 0,
+	};
+
+	modem_cmux_transmit_cmd_frame(cmux, &frame);
+	k_work_schedule(&cmux->connect_work, MODEM_CMUX_T1_TIMEOUT);
+}
+
+static void modem_cmux_disconnect_handler(struct k_work *item)
+{
+	struct modem_cmux *cmux = CONTAINER_OF(item, struct modem_cmux, disconnect_work);
+	struct modem_cmux_command *command;
+	uint8_t data[2];
+
+	cmux->state = MODEM_CMUX_STATE_DISCONNECTING;
+
+	command = modem_cmux_command_wrap(data);
+	command->type.ea = 1;
+	command->type.cr = 1;
+	command->type.value = MODEM_CMUX_COMMAND_CLD;
+	command->length.ea = 1;
+	command->length.value = 0;
+
+	struct modem_cmux_frame frame = {
+		.dlci_address = 0,
+		.cr = true,
+		.pf = false,
+		.type = MODEM_CMUX_FRAME_TYPE_UIH,
+		.data = data,
+		.data_len = sizeof(data),
+	};
+
+	/* Transmit close down command */
+	modem_cmux_transmit_cmd_frame(cmux, &frame);
+	k_work_schedule(&cmux->disconnect_work, MODEM_CMUX_T1_TIMEOUT);
+}
+
+static int modem_cmux_dlci_pipe_api_open(void *data)
+{
+	struct modem_cmux_dlci *dlci = (struct modem_cmux_dlci *)data;
+
+	if (k_work_delayable_is_pending(&dlci->open_work) == true) {
+		return -EBUSY;
+	}
+
+	k_work_schedule(&dlci->open_work, K_NO_WAIT);
+	return 0;
+}
+
+static int modem_cmux_dlci_pipe_api_transmit(void *data, const uint8_t *buf, size_t size)
+{
+	struct modem_cmux_dlci *dlci = (struct modem_cmux_dlci *)data;
+	struct modem_cmux *cmux = dlci->cmux;
+
+	struct modem_cmux_frame frame = {
+		.dlci_address = dlci->dlci_address,
+		.cr = false,
+		.pf = false,
+		.type = MODEM_CMUX_FRAME_TYPE_UIH,
+		.data = buf,
+		.data_len = size,
+	};
+
+	return modem_cmux_transmit_data_frame(cmux, &frame);
+}
+
+static int modem_cmux_dlci_pipe_api_receive(void *data, uint8_t *buf, size_t size)
+{
+	struct modem_cmux_dlci *dlci = (struct modem_cmux_dlci *)data;
+	uint32_t ret;
+
+	k_mutex_lock(&dlci->receive_rb_lock, K_FOREVER);
+	ret = ring_buf_get(&dlci->receive_rb, buf, size);
+	k_mutex_unlock(&dlci->receive_rb_lock);
+	return ret;
+}
+
+static int modem_cmux_dlci_pipe_api_close(void *data)
+{
+	struct modem_cmux_dlci *dlci = (struct modem_cmux_dlci *)data;
+
+	if (k_work_delayable_is_pending(&dlci->close_work) == true) {
+		return -EBUSY;
+	}
+
+	k_work_schedule(&dlci->close_work, K_NO_WAIT);
+	return 0;
+}
+
+struct modem_pipe_api modem_cmux_dlci_pipe_api = {
+	.open = modem_cmux_dlci_pipe_api_open,
+	.transmit = modem_cmux_dlci_pipe_api_transmit,
+	.receive = modem_cmux_dlci_pipe_api_receive,
+	.close = modem_cmux_dlci_pipe_api_close,
+};
+
+static void modem_cmux_dlci_open_handler(struct k_work *item)
+{
+	struct modem_cmux_dlci *dlci = CONTAINER_OF(item, struct modem_cmux_dlci, open_work);
+
+	dlci->state = MODEM_CMUX_DLCI_STATE_OPENING;
+
+	struct modem_cmux_frame frame = {
+		.dlci_address = dlci->dlci_address,
+		.cr = true,
+		.pf = true,
+		.type = MODEM_CMUX_FRAME_TYPE_SABM,
+		.data = NULL,
+		.data_len = 0,
+	};
+
+	modem_cmux_transmit_cmd_frame(dlci->cmux, &frame);
+	k_work_schedule(&dlci->open_work, MODEM_CMUX_T1_TIMEOUT);
+}
+
+static void modem_cmux_dlci_close_handler(struct k_work *item)
+{
+	struct modem_cmux_dlci *dlci = CONTAINER_OF(item, struct modem_cmux_dlci, close_work);
+	struct modem_cmux *cmux = dlci->cmux;
+
+	dlci->state = MODEM_CMUX_DLCI_STATE_CLOSING;
+
+	struct modem_cmux_frame frame = {
+		.dlci_address = dlci->dlci_address,
+		.cr = true,
+		.pf = true,
+		.type = MODEM_CMUX_FRAME_TYPE_DISC,
+		.data = NULL,
+		.data_len = 0,
+	};
+
+	modem_cmux_transmit_cmd_frame(cmux, &frame);
+	k_work_schedule(&dlci->close_work, MODEM_CMUX_T1_TIMEOUT);
+}
+
+static void modem_cmux_dlci_pipes_notify_closed(struct modem_cmux *cmux)
+{
+	sys_snode_t *node;
+	struct modem_cmux_dlci *dlci;
+
+	SYS_SLIST_FOR_EACH_NODE(&cmux->dlcis, node) {
+		dlci = (struct modem_cmux_dlci *)node;
+		modem_pipe_notify_closed(&dlci->pipe);
+	}
+}
+
+void modem_cmux_init(struct modem_cmux *cmux, const struct modem_cmux_config *config)
+{
+	__ASSERT_NO_MSG(cmux != NULL);
+	__ASSERT_NO_MSG(config != NULL);
+	__ASSERT_NO_MSG(config->receive_buf != NULL);
+	__ASSERT_NO_MSG(config->receive_buf_size >= 126);
+	__ASSERT_NO_MSG(config->transmit_buf != NULL);
+	__ASSERT_NO_MSG(config->transmit_buf_size >= 148);
+
+	memset(cmux, 0x00, sizeof(*cmux));
+	cmux->callback = config->callback;
+	cmux->user_data = config->user_data;
+	cmux->receive_buf = config->receive_buf;
+	cmux->receive_buf_size = config->receive_buf_size;
+	sys_slist_init(&cmux->dlcis);
+	cmux->state = MODEM_CMUX_STATE_DISCONNECTED;
+	ring_buf_init(&cmux->transmit_rb, config->transmit_buf_size, config->transmit_buf);
+	k_mutex_init(&cmux->transmit_rb_lock);
+	k_work_init_delayable(&cmux->receive_work, modem_cmux_receive_handler);
+	k_work_init_delayable(&cmux->transmit_work, modem_cmux_transmit_handler);
+	k_work_init_delayable(&cmux->connect_work, modem_cmux_connect_handler);
+	k_work_init_delayable(&cmux->disconnect_work, modem_cmux_disconnect_handler);
+	k_event_init(&cmux->event);
+	k_event_post(&cmux->event, MODEM_CMUX_EVENT_DISCONNECTED_BIT);
+}
+
+struct modem_pipe *modem_cmux_dlci_init(struct modem_cmux *cmux, struct modem_cmux_dlci *dlci,
+					const struct modem_cmux_dlci_config *config)
+{
+	__ASSERT_NO_MSG(cmux != NULL);
+	__ASSERT_NO_MSG(dlci != NULL);
+	__ASSERT_NO_MSG(config != NULL);
+	__ASSERT_NO_MSG(config->dlci_address < 64);
+	__ASSERT_NO_MSG(config->receive_buf != NULL);
+	__ASSERT_NO_MSG(config->receive_buf_size >= 126);
+
+	memset(dlci, 0x00, sizeof(*dlci));
+	dlci->cmux = cmux;
+	dlci->dlci_address = config->dlci_address;
+	ring_buf_init(&dlci->receive_rb, config->receive_buf_size, config->receive_buf);
+	k_mutex_init(&dlci->receive_rb_lock);
+	modem_pipe_init(&dlci->pipe, dlci, &modem_cmux_dlci_pipe_api);
+	k_work_init_delayable(&dlci->open_work, modem_cmux_dlci_open_handler);
+	k_work_init_delayable(&dlci->close_work, modem_cmux_dlci_close_handler);
+	dlci->state = MODEM_CMUX_DLCI_STATE_CLOSED;
+	sys_slist_append(&dlci->cmux->dlcis, &dlci->node);
+	return &dlci->pipe;
+}
+
+int modem_cmux_attach(struct modem_cmux *cmux, struct modem_pipe *pipe)
+{
+	cmux->pipe = pipe;
+	ring_buf_reset(&cmux->transmit_rb);
+	modem_pipe_attach(cmux->pipe, modem_cmux_bus_callback, cmux);
+	return 0;
+}
+
+int modem_cmux_connect(struct modem_cmux *cmux)
+{
+	__ASSERT_NO_MSG(cmux->pipe != NULL);
+
+	if (k_event_wait(&cmux->event, MODEM_CMUX_EVENT_CONNECTED_BIT, false, K_NO_WAIT)) {
+		return -EALREADY;
+	}
+
+	if (k_work_delayable_is_pending(&cmux->connect_work) == false) {
+		k_work_schedule(&cmux->connect_work, K_NO_WAIT);
+	}
+
+	if (k_event_wait(&cmux->event, MODEM_CMUX_EVENT_CONNECTED_BIT, false,
+			 MODEM_CMUX_T2_TIMEOUT) == 0) {
+		return -EAGAIN;
+	}
+
+	return 0;
+}
+
+int modem_cmux_connect_async(struct modem_cmux *cmux)
+{
+	__ASSERT_NO_MSG(cmux->pipe != NULL);
+
+	if (k_work_delayable_is_pending(&cmux->connect_work) == true) {
+		return -EBUSY;
+	}
+
+	k_work_schedule(&cmux->connect_work, K_NO_WAIT);
+	return 0;
+}
+
+int modem_cmux_disconnect(struct modem_cmux *cmux)
+{
+	if (k_event_wait(&cmux->event, MODEM_CMUX_EVENT_DISCONNECTED_BIT, false, K_NO_WAIT)) {
+		return -EALREADY;
+	}
+
+	if (k_work_delayable_is_pending(&cmux->disconnect_work) == false) {
+		k_work_schedule(&cmux->disconnect_work, K_NO_WAIT);
+	}
+
+	if (k_event_wait(&cmux->event, MODEM_CMUX_EVENT_DISCONNECTED_BIT, false,
+			 MODEM_CMUX_T2_TIMEOUT) == 0) {
+		return -EAGAIN;
+	}
+
+	return 0;
+}
+
+int modem_cmux_disconnect_async(struct modem_cmux *cmux)
+{
+	if (k_work_delayable_is_pending(&cmux->disconnect_work) == true) {
+		return -EBUSY;
+	}
+
+	k_work_schedule(&cmux->disconnect_work, K_NO_WAIT);
+	return 0;
+}
+
+void modem_cmux_release(struct modem_cmux *cmux)
+{
+	struct k_work_sync sync;
+
+	/* Close DLCI pipes */
+	modem_cmux_dlci_pipes_notify_closed(cmux);
+
+	/* Release bus pipe */
+	if (cmux->pipe) {
+		modem_pipe_release(cmux->pipe);
+	}
+
+	/* Cancel all work */
+	k_work_cancel_delayable_sync(&cmux->connect_work, &sync);
+	k_work_cancel_delayable_sync(&cmux->disconnect_work, &sync);
+	k_work_cancel_delayable_sync(&cmux->transmit_work, &sync);
+	k_work_cancel_delayable_sync(&cmux->receive_work, &sync);
+
+	/* Unreference pipe */
+	cmux->pipe = NULL;
+}
diff --git a/subsys/modem/modem_pipe.c b/subsys/modem/modem_pipe.c
new file mode 100644
index 0000000..ca2a260
--- /dev/null
+++ b/subsys/modem/modem_pipe.c
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/modem/pipe.h>
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(modem_pipe, CONFIG_MODEM_MODULES_LOG_LEVEL);
+
+void modem_pipe_init(struct modem_pipe *pipe, void *data, struct modem_pipe_api *api)
+{
+	__ASSERT_NO_MSG(pipe != NULL);
+	__ASSERT_NO_MSG(data != NULL);
+	__ASSERT_NO_MSG(api != NULL);
+
+	pipe->data = data;
+	pipe->api = api;
+	pipe->callback = NULL;
+	pipe->user_data = NULL;
+	pipe->state = MODEM_PIPE_STATE_CLOSED;
+
+	k_mutex_init(&pipe->lock);
+	k_condvar_init(&pipe->condvar);
+}
+
+int modem_pipe_open(struct modem_pipe *pipe)
+{
+	int ret;
+
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+	ret = pipe->api->open(pipe->data);
+
+	if (ret < 0) {
+		k_mutex_unlock(&pipe->lock);
+		return ret;
+	}
+
+	if (pipe->state == MODEM_PIPE_STATE_OPEN) {
+		k_mutex_unlock(&pipe->lock);
+		return 0;
+	}
+
+	k_condvar_wait(&pipe->condvar, &pipe->lock, K_MSEC(10000));
+	ret = (pipe->state == MODEM_PIPE_STATE_OPEN) ? 0 : -EAGAIN;
+	k_mutex_unlock(&pipe->lock);
+	return ret;
+}
+
+int modem_pipe_open_async(struct modem_pipe *pipe)
+{
+	int ret;
+
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+	ret = pipe->api->open(pipe->data);
+	k_mutex_unlock(&pipe->lock);
+	return ret;
+}
+
+void modem_pipe_attach(struct modem_pipe *pipe, modem_pipe_api_callback callback, void *user_data)
+{
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+	pipe->callback = callback;
+	pipe->user_data = user_data;
+	k_mutex_unlock(&pipe->lock);
+}
+
+int modem_pipe_transmit(struct modem_pipe *pipe, const uint8_t *buf, size_t size)
+{
+	int ret;
+
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+
+	if (pipe->state == MODEM_PIPE_STATE_CLOSED) {
+		k_mutex_unlock(&pipe->lock);
+		return -EPERM;
+	}
+
+	ret = pipe->api->transmit(pipe->data, buf, size);
+	k_mutex_unlock(&pipe->lock);
+	return ret;
+}
+
+int modem_pipe_receive(struct modem_pipe *pipe, uint8_t *buf, size_t size)
+{
+	int ret;
+
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+
+	if (pipe->state == MODEM_PIPE_STATE_CLOSED) {
+		k_mutex_unlock(&pipe->lock);
+		return -EPERM;
+	}
+
+	ret = pipe->api->receive(pipe->data, buf, size);
+	k_mutex_unlock(&pipe->lock);
+	return ret;
+}
+
+void modem_pipe_release(struct modem_pipe *pipe)
+{
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+	pipe->callback = NULL;
+	pipe->user_data = NULL;
+	k_mutex_unlock(&pipe->lock);
+}
+
+int modem_pipe_close(struct modem_pipe *pipe)
+{
+	int ret;
+
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+	ret = pipe->api->close(pipe->data);
+	if (ret < 0) {
+		k_mutex_unlock(&pipe->lock);
+		return ret;
+	}
+
+	if (pipe->state == MODEM_PIPE_STATE_CLOSED) {
+		k_mutex_unlock(&pipe->lock);
+		return 0;
+	}
+
+	k_condvar_wait(&pipe->condvar, &pipe->lock, K_MSEC(10000));
+	ret = (pipe->state == MODEM_PIPE_STATE_CLOSED) ? 0 : -EAGAIN;
+	k_mutex_unlock(&pipe->lock);
+	return ret;
+}
+
+int modem_pipe_close_async(struct modem_pipe *pipe)
+{
+	int ret;
+
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+	ret = pipe->api->close(pipe->data);
+	k_mutex_unlock(&pipe->lock);
+	return ret;
+}
+
+void modem_pipe_notify_opened(struct modem_pipe *pipe)
+{
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+	pipe->state = MODEM_PIPE_STATE_OPEN;
+
+	if (pipe->callback != NULL) {
+		pipe->callback(pipe, MODEM_PIPE_EVENT_OPENED, pipe->user_data);
+	}
+
+	k_condvar_signal(&pipe->condvar);
+	k_mutex_unlock(&pipe->lock);
+}
+
+void modem_pipe_notify_closed(struct modem_pipe *pipe)
+{
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+	pipe->state = MODEM_PIPE_STATE_CLOSED;
+
+	if (pipe->callback != NULL) {
+		pipe->callback(pipe, MODEM_PIPE_EVENT_CLOSED, pipe->user_data);
+	}
+
+	k_condvar_signal(&pipe->condvar);
+	k_mutex_unlock(&pipe->lock);
+}
+
+void modem_pipe_notify_receive_ready(struct modem_pipe *pipe)
+{
+	k_mutex_lock(&pipe->lock, K_FOREVER);
+
+	if (pipe->callback != NULL) {
+		pipe->callback(pipe, MODEM_PIPE_EVENT_RECEIVE_READY, pipe->user_data);
+	}
+
+	k_mutex_unlock(&pipe->lock);
+}
diff --git a/subsys/modem/modem_ppp.c b/subsys/modem/modem_ppp.c
new file mode 100644
index 0000000..48a550d
--- /dev/null
+++ b/subsys/modem/modem_ppp.c
@@ -0,0 +1,507 @@
+/*
+ * Copyright (c) 2022 Trackunit Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/net/ppp.h>
+#include <zephyr/sys/crc.h>
+#include <zephyr/modem/ppp.h>
+#include <string.h>
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(modem_ppp, CONFIG_MODEM_MODULES_LOG_LEVEL);
+
+#define MODEM_PPP_STATE_ATTACHED_BIT	(0)
+#define MODEM_PPP_FRAME_TAIL_SIZE	(2)
+
+#define MODEM_PPP_CODE_DELIMITER	(0x7E)
+#define MODEM_PPP_CODE_ESCAPE		(0x7D)
+#define MODEM_PPP_VALUE_ESCAPE		(0x20)
+
+static uint16_t modem_ppp_fcs_init(uint8_t byte)
+{
+	return crc16_ccitt(0xFFFF, &byte, 1);
+}
+
+static uint16_t modem_ppp_fcs_update(uint16_t fcs, uint8_t byte)
+{
+	return crc16_ccitt(fcs, &byte, 1);
+}
+
+static uint16_t modem_ppp_fcs_final(uint16_t fcs)
+{
+	return fcs ^ 0xFFFF;
+}
+
+static uint16_t modem_ppp_ppp_protocol(struct net_pkt *pkt)
+{
+	if (net_pkt_family(pkt) == AF_INET) {
+		return PPP_IP;
+	}
+
+	if (net_pkt_family(pkt) == AF_INET6) {
+		return PPP_IPV6;
+	}
+
+	LOG_WRN("Unsupported protocol");
+	return 0;
+}
+
+static uint8_t modem_ppp_wrap_net_pkt_byte(struct modem_ppp *ppp)
+{
+	uint8_t byte;
+
+	switch (ppp->transmit_state) {
+	case MODEM_PPP_TRANSMIT_STATE_IDLE:
+		LOG_WRN("Invalid transmit state");
+		return 0;
+
+	/* Writing header */
+	case MODEM_PPP_TRANSMIT_STATE_SOF:
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_HDR_FF;
+		return MODEM_PPP_CODE_DELIMITER;
+
+	case MODEM_PPP_TRANSMIT_STATE_HDR_FF:
+		net_pkt_cursor_init(ppp->tx_pkt);
+		ppp->tx_pkt_fcs = modem_ppp_fcs_init(0xFF);
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_HDR_7D;
+		return 0xFF;
+
+	case MODEM_PPP_TRANSMIT_STATE_HDR_7D:
+		ppp->tx_pkt_fcs = modem_ppp_fcs_update(ppp->tx_pkt_fcs, 0x03);
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_HDR_23;
+		return MODEM_PPP_CODE_ESCAPE;
+
+	case MODEM_PPP_TRANSMIT_STATE_HDR_23:
+		if (net_pkt_is_ppp(ppp->tx_pkt) == true) {
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_DATA;
+		} else {
+			ppp->tx_pkt_protocol = modem_ppp_ppp_protocol(ppp->tx_pkt);
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_PROTOCOL_HIGH;
+		}
+
+		return 0x23;
+
+	/* Writing protocol */
+	case MODEM_PPP_TRANSMIT_STATE_PROTOCOL_HIGH:
+		byte = (ppp->tx_pkt_protocol >> 8) & 0xFF;
+		ppp->tx_pkt_fcs = modem_ppp_fcs_update(ppp->tx_pkt_fcs, byte);
+
+		if ((byte == MODEM_PPP_CODE_DELIMITER) || (byte == MODEM_PPP_CODE_ESCAPE) ||
+		    (byte < MODEM_PPP_VALUE_ESCAPE)) {
+			ppp->tx_pkt_escaped = byte ^ MODEM_PPP_VALUE_ESCAPE;
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_ESCAPING_PROTOCOL_HIGH;
+			return MODEM_PPP_CODE_ESCAPE;
+		}
+
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_PROTOCOL_LOW;
+		return byte;
+
+	case MODEM_PPP_TRANSMIT_STATE_ESCAPING_PROTOCOL_HIGH:
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_PROTOCOL_LOW;
+		return ppp->tx_pkt_escaped;
+
+	case MODEM_PPP_TRANSMIT_STATE_PROTOCOL_LOW:
+		byte = ppp->tx_pkt_protocol & 0xFF;
+		ppp->tx_pkt_fcs = modem_ppp_fcs_update(ppp->tx_pkt_fcs, byte);
+
+		if ((byte == MODEM_PPP_CODE_DELIMITER) || (byte == MODEM_PPP_CODE_ESCAPE) ||
+		    (byte < MODEM_PPP_VALUE_ESCAPE)) {
+			ppp->tx_pkt_escaped = byte ^ MODEM_PPP_VALUE_ESCAPE;
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_ESCAPING_PROTOCOL_LOW;
+			return MODEM_PPP_CODE_ESCAPE;
+		}
+
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_DATA;
+		return byte;
+
+	case MODEM_PPP_TRANSMIT_STATE_ESCAPING_PROTOCOL_LOW:
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_DATA;
+		return ppp->tx_pkt_escaped;
+
+	/* Writing data */
+	case MODEM_PPP_TRANSMIT_STATE_DATA:
+		net_pkt_read_u8(ppp->tx_pkt, &byte);
+		ppp->tx_pkt_fcs = modem_ppp_fcs_update(ppp->tx_pkt_fcs, byte);
+
+		if ((byte == MODEM_PPP_CODE_DELIMITER) || (byte == MODEM_PPP_CODE_ESCAPE) ||
+		    (byte < MODEM_PPP_VALUE_ESCAPE)) {
+			ppp->tx_pkt_escaped = byte ^ MODEM_PPP_VALUE_ESCAPE;
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_ESCAPING_DATA;
+			return MODEM_PPP_CODE_ESCAPE;
+		}
+
+		if (net_pkt_remaining_data(ppp->tx_pkt) == 0) {
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_FCS_LOW;
+		}
+
+		return byte;
+
+	case MODEM_PPP_TRANSMIT_STATE_ESCAPING_DATA:
+		if (net_pkt_remaining_data(ppp->tx_pkt) == 0) {
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_FCS_LOW;
+		} else {
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_DATA;
+		}
+
+		return ppp->tx_pkt_escaped;
+
+	/* Writing FCS */
+	case MODEM_PPP_TRANSMIT_STATE_FCS_LOW:
+		ppp->tx_pkt_fcs = modem_ppp_fcs_final(ppp->tx_pkt_fcs);
+		byte = ppp->tx_pkt_fcs & 0xFF;
+
+		if ((byte == MODEM_PPP_CODE_DELIMITER) || (byte == MODEM_PPP_CODE_ESCAPE) ||
+		    (byte < MODEM_PPP_VALUE_ESCAPE)) {
+			ppp->tx_pkt_escaped = byte ^ MODEM_PPP_VALUE_ESCAPE;
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_ESCAPING_FCS_LOW;
+			return MODEM_PPP_CODE_ESCAPE;
+		}
+
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_FCS_HIGH;
+		return byte;
+
+	case MODEM_PPP_TRANSMIT_STATE_ESCAPING_FCS_LOW:
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_FCS_HIGH;
+		return ppp->tx_pkt_escaped;
+
+	case MODEM_PPP_TRANSMIT_STATE_FCS_HIGH:
+		byte = (ppp->tx_pkt_fcs >> 8) & 0xFF;
+
+		if ((byte == MODEM_PPP_CODE_DELIMITER) || (byte == MODEM_PPP_CODE_ESCAPE) ||
+		    (byte < MODEM_PPP_VALUE_ESCAPE)) {
+			ppp->tx_pkt_escaped = byte ^ MODEM_PPP_VALUE_ESCAPE;
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_ESCAPING_FCS_HIGH;
+			return MODEM_PPP_CODE_ESCAPE;
+		}
+
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_EOF;
+		return byte;
+
+	case MODEM_PPP_TRANSMIT_STATE_ESCAPING_FCS_HIGH:
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_EOF;
+		return ppp->tx_pkt_escaped;
+
+	/* Writing end of frame */
+	case MODEM_PPP_TRANSMIT_STATE_EOF:
+		ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_IDLE;
+		return MODEM_PPP_CODE_DELIMITER;
+	}
+
+	return 0;
+}
+
+static void modem_ppp_process_received_byte(struct modem_ppp *ppp, uint8_t byte)
+{
+	switch (ppp->receive_state) {
+	case MODEM_PPP_RECEIVE_STATE_HDR_SOF:
+		if (byte == MODEM_PPP_CODE_DELIMITER) {
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_FF;
+		}
+
+		break;
+
+	case MODEM_PPP_RECEIVE_STATE_HDR_FF:
+		if (byte == MODEM_PPP_CODE_DELIMITER) {
+			break;
+		}
+
+		if (byte == 0xFF) {
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_7D;
+		} else {
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+		}
+
+		break;
+
+	case MODEM_PPP_RECEIVE_STATE_HDR_7D:
+		if (byte == MODEM_PPP_CODE_ESCAPE) {
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_23;
+		} else {
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+		}
+
+		break;
+
+	case MODEM_PPP_RECEIVE_STATE_HDR_23:
+		if (byte == 0x23) {
+			ppp->rx_pkt = net_pkt_rx_alloc_with_buffer(ppp->iface,
+				CONFIG_MODEM_PPP_NET_BUF_FRAG_SIZE, AF_UNSPEC, 0, K_NO_WAIT);
+
+			if (ppp->rx_pkt == NULL) {
+				LOG_WRN("Dropped frame, no net_pkt available");
+				ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+				break;
+			}
+
+			LOG_DBG("Receiving PPP frame");
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_WRITING;
+			net_pkt_cursor_init(ppp->rx_pkt);
+
+		} else {
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+		}
+
+		break;
+
+	case MODEM_PPP_RECEIVE_STATE_WRITING:
+		if (byte == MODEM_PPP_CODE_DELIMITER) {
+			LOG_DBG("Received PPP frame");
+
+			/* Remove FCS */
+			net_pkt_remove_tail(ppp->rx_pkt, MODEM_PPP_FRAME_TAIL_SIZE);
+			net_pkt_cursor_init(ppp->rx_pkt);
+			net_pkt_set_ppp(ppp->rx_pkt, true);
+
+			if (net_recv_data(ppp->iface, ppp->rx_pkt) < 0) {
+				LOG_WRN("Net pkt could not be processed");
+				net_pkt_unref(ppp->rx_pkt);
+			}
+
+			ppp->rx_pkt = NULL;
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+			break;
+		}
+
+		if (net_pkt_available_buffer(ppp->rx_pkt) == 1) {
+			if (net_pkt_alloc_buffer(ppp->rx_pkt, CONFIG_MODEM_PPP_NET_BUF_FRAG_SIZE,
+						 AF_INET, K_NO_WAIT) < 0) {
+				LOG_WRN("Failed to alloc buffer");
+				net_pkt_unref(ppp->rx_pkt);
+				ppp->rx_pkt = NULL;
+				ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+				break;
+			}
+		}
+
+		if (byte == MODEM_PPP_CODE_ESCAPE) {
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_UNESCAPING;
+			break;
+		}
+
+		if (net_pkt_write_u8(ppp->rx_pkt, byte) < 0) {
+			LOG_WRN("Dropped PPP frame");
+			net_pkt_unref(ppp->rx_pkt);
+			ppp->rx_pkt = NULL;
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+		}
+
+		break;
+
+	case MODEM_PPP_RECEIVE_STATE_UNESCAPING:
+		if (net_pkt_write_u8(ppp->rx_pkt, (byte ^ MODEM_PPP_VALUE_ESCAPE)) < 0) {
+			LOG_WRN("Dropped PPP frame");
+			net_pkt_unref(ppp->rx_pkt);
+			ppp->rx_pkt = NULL;
+			ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+			break;
+		}
+
+		ppp->receive_state = MODEM_PPP_RECEIVE_STATE_WRITING;
+		break;
+	}
+}
+
+static void modem_ppp_pipe_callback(struct modem_pipe *pipe, enum modem_pipe_event event,
+				    void *user_data)
+{
+	struct modem_ppp *ppp = (struct modem_ppp *)user_data;
+
+	if (event == MODEM_PIPE_EVENT_RECEIVE_READY) {
+		k_work_submit(&ppp->process_work);
+	}
+}
+
+static void modem_ppp_send_handler(struct k_work *item)
+{
+	struct modem_ppp *ppp = CONTAINER_OF(item, struct modem_ppp, send_work);
+	uint8_t byte;
+	uint8_t *reserved;
+	uint32_t reserved_size;
+	int ret;
+
+	if (ppp->tx_pkt == NULL) {
+		ppp->tx_pkt = k_fifo_get(&ppp->tx_pkt_fifo, K_NO_WAIT);
+	}
+
+	if (ppp->tx_pkt != NULL) {
+		/* Initialize wrap */
+		if (ppp->transmit_state == MODEM_PPP_TRANSMIT_STATE_IDLE) {
+			ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_SOF;
+		}
+
+		/* Fill transmit ring buffer */
+		while (ring_buf_space_get(&ppp->transmit_rb) > 0) {
+			byte = modem_ppp_wrap_net_pkt_byte(ppp);
+
+			ring_buf_put(&ppp->transmit_rb, &byte, 1);
+
+			if (ppp->transmit_state == MODEM_PPP_TRANSMIT_STATE_IDLE) {
+				net_pkt_unref(ppp->tx_pkt);
+				ppp->tx_pkt = k_fifo_get(&ppp->tx_pkt_fifo, K_NO_WAIT);
+				break;
+			}
+		}
+	}
+
+	reserved_size = ring_buf_get_claim(&ppp->transmit_rb, &reserved, UINT32_MAX);
+	if (reserved_size == 0) {
+		ring_buf_get_finish(&ppp->transmit_rb, 0);
+		return;
+	}
+
+	ret = modem_pipe_transmit(ppp->pipe, reserved, reserved_size);
+	if (ret < 0) {
+		ring_buf_get_finish(&ppp->transmit_rb, 0);
+	} else {
+		ring_buf_get_finish(&ppp->transmit_rb, (uint32_t)ret);
+	}
+
+	/* Resubmit send work if data remains */
+	if ((ring_buf_is_empty(&ppp->transmit_rb) == false) || (ppp->tx_pkt != NULL)) {
+		k_work_submit(&ppp->send_work);
+	}
+}
+
+static void modem_ppp_process_handler(struct k_work *item)
+{
+	struct modem_ppp *ppp = CONTAINER_OF(item, struct modem_ppp, process_work);
+	int ret;
+
+	ret = modem_pipe_receive(ppp->pipe, ppp->receive_buf, ppp->buf_size);
+	if (ret < 1) {
+		return;
+	}
+
+	for (int i = 0; i < ret; i++) {
+		modem_ppp_process_received_byte(ppp, ppp->receive_buf[i]);
+	}
+
+	k_work_submit(&ppp->process_work);
+}
+
+static void modem_ppp_ppp_api_init(struct net_if *iface)
+{
+	const struct device *dev = net_if_get_device(iface);
+	struct modem_ppp *ppp = (struct modem_ppp *)dev->data;
+
+	net_ppp_init(iface);
+	net_if_flag_set(iface, NET_IF_NO_AUTO_START);
+	net_if_carrier_off(iface);
+
+	if (ppp->init_iface != NULL) {
+		ppp->init_iface(iface);
+	}
+
+	ppp->iface = iface;
+}
+
+static int modem_ppp_ppp_api_start(const struct device *dev)
+{
+	return 0;
+}
+
+static int modem_ppp_ppp_api_stop(const struct device *dev)
+{
+	return 0;
+}
+
+static int modem_ppp_ppp_api_send(const struct device *dev, struct net_pkt *pkt)
+{
+	struct modem_ppp *ppp = (struct modem_ppp *)dev->data;
+
+	if (atomic_test_bit(&ppp->state, MODEM_PPP_STATE_ATTACHED_BIT) == false) {
+		return -EPERM;
+	}
+
+	/* Validate packet protocol */
+	if ((net_pkt_is_ppp(pkt) == false) && (net_pkt_family(pkt) != AF_INET) &&
+	    (net_pkt_family(pkt) != AF_INET6)) {
+		return -EPROTONOSUPPORT;
+	}
+
+	/* Validate packet data length */
+	if (((net_pkt_get_len(pkt) < 2) && (net_pkt_is_ppp(pkt) == true)) ||
+	    ((net_pkt_get_len(pkt) < 1))) {
+		return -ENODATA;
+	}
+
+	net_pkt_ref(pkt);
+	k_fifo_put(&ppp->tx_pkt_fifo, pkt);
+	k_work_submit(&ppp->send_work);
+	return 0;
+}
+
+const struct ppp_api modem_ppp_ppp_api = {
+	.iface_api.init = modem_ppp_ppp_api_init,
+	.start = modem_ppp_ppp_api_start,
+	.stop = modem_ppp_ppp_api_stop,
+	.send = modem_ppp_ppp_api_send,
+};
+
+int modem_ppp_attach(struct modem_ppp *ppp, struct modem_pipe *pipe)
+{
+	if (atomic_test_and_set_bit(&ppp->state, MODEM_PPP_STATE_ATTACHED_BIT) == true) {
+		return 0;
+	}
+
+	modem_pipe_attach(pipe, modem_ppp_pipe_callback, ppp);
+	ppp->pipe = pipe;
+	return 0;
+}
+
+struct net_if *modem_ppp_get_iface(struct modem_ppp *ppp)
+{
+	return ppp->iface;
+}
+
+void modem_ppp_release(struct modem_ppp *ppp)
+{
+	struct k_work_sync sync;
+	struct net_pkt *pkt;
+
+	if (atomic_test_and_clear_bit(&ppp->state, MODEM_PPP_STATE_ATTACHED_BIT) == false) {
+		return;
+	}
+
+	modem_pipe_release(ppp->pipe);
+	k_work_cancel_sync(&ppp->send_work, &sync);
+	k_work_cancel_sync(&ppp->process_work, &sync);
+	ppp->pipe = NULL;
+	ppp->receive_state = MODEM_PPP_RECEIVE_STATE_HDR_SOF;
+
+	if (ppp->rx_pkt != NULL) {
+		net_pkt_unref(ppp->rx_pkt);
+		ppp->rx_pkt = NULL;
+	}
+
+	ppp->transmit_state = MODEM_PPP_TRANSMIT_STATE_IDLE;
+
+	if (ppp->tx_pkt != NULL) {
+		net_pkt_unref(ppp->tx_pkt);
+		ppp->tx_pkt = NULL;
+	}
+
+	while (1) {
+		pkt = k_fifo_get(&ppp->tx_pkt_fifo, K_NO_WAIT);
+		if (pkt == NULL) {
+			break;
+		}
+
+		net_pkt_unref(pkt);
+	}
+}
+
+int modem_ppp_init_internal(const struct device *dev)
+{
+	struct modem_ppp *ppp = (struct modem_ppp *)dev->data;
+
+	atomic_set(&ppp->state, 0);
+	ring_buf_init(&ppp->transmit_rb, ppp->buf_size, ppp->transmit_buf);
+	k_work_init(&ppp->send_work, modem_ppp_send_handler);
+	k_work_init(&ppp->process_work, modem_ppp_process_handler);
+	k_fifo_init(&ppp->tx_pkt_fifo);
+
+	return 0;
+}