From f967a76fb0f608a95e9cc007ecffb90318b51a91 Mon Sep 17 00:00:00 2001
From: Ryan Adolf <ryanadolf123@gmail.com>
Date: Tue, 22 Apr 2025 23:44:11 -0400
Subject: [PATCH] Add CDC host

---
 locale/circuitpython.pot                |   4 +
 py/circuitpy_defns.mk                   |   2 +
 shared-bindings/usb/__init__.c          |   2 +
 shared-bindings/usb/cdc_host/Serial.c   | 274 ++++++++++++++++++++++++
 shared-bindings/usb/cdc_host/Serial.h   |  30 +++
 shared-bindings/usb/cdc_host/__init__.c |  57 +++++
 shared-bindings/usb/cdc_host/__init__.h |  11 +
 shared-module/usb/cdc_host/Serial.c     | 119 ++++++++++
 shared-module/usb/cdc_host/Serial.h     |  16 ++
 shared-module/usb/cdc_host/__init__.c   |   0
 shared-module/usb/cdc_host/__init__.h   |   0
 supervisor/shared/usb/tusb_config.h     |   2 +-
 supervisor/supervisor.mk                |   1 +
 13 files changed, 517 insertions(+), 1 deletion(-)
 create mode 100644 shared-bindings/usb/cdc_host/Serial.c
 create mode 100644 shared-bindings/usb/cdc_host/Serial.h
 create mode 100644 shared-bindings/usb/cdc_host/__init__.c
 create mode 100644 shared-bindings/usb/cdc_host/__init__.h
 create mode 100644 shared-module/usb/cdc_host/Serial.c
 create mode 100644 shared-module/usb/cdc_host/Serial.h
 create mode 100644 shared-module/usb/cdc_host/__init__.c
 create mode 100644 shared-module/usb/cdc_host/__init__.h

diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot
index 6beb320527e43..8e3379252549e 100644
--- a/locale/circuitpython.pot
+++ b/locale/circuitpython.pot
@@ -2968,6 +2968,10 @@ msgstr ""
 msgid "destination buffer must be an array of type 'H' for bit_depth = 16"
 msgstr ""
 
+#: shared-bindings/usb/cdc_host/__init__.c
+msgid "device must be a usb.core.Device object"
+msgstr ""
+
 #: py/objdict.c
 msgid "dict update sequence has wrong length"
 msgstr ""
diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk
index fa90481e648ea..1f2dac4c3ecee 100644
--- a/py/circuitpy_defns.mk
+++ b/py/circuitpy_defns.mk
@@ -785,6 +785,8 @@ SRC_SHARED_MODULE_ALL = \
 	usb/__init__.c \
 	usb/core/__init__.c \
 	usb/core/Device.c \
+  usb/cdc_host/Serial.c \
+  usb/cdc_host/__init__.c \
 	usb/util/__init__.c \
 	ustack/__init__.c \
 	vectorio/Circle.c \
diff --git a/shared-bindings/usb/__init__.c b/shared-bindings/usb/__init__.c
index ea05229984c97..4ee289960ac57 100644
--- a/shared-bindings/usb/__init__.c
+++ b/shared-bindings/usb/__init__.c
@@ -11,6 +11,7 @@
 #include "shared-bindings/usb/__init__.h"
 #include "shared-bindings/usb/core/__init__.h"
 #include "shared-bindings/usb/util/__init__.h"
+#include "shared-bindings/usb/cdc_host/__init__.h"
 #include "supervisor/usb.h"
 
 //| """PyUSB-compatible USB host API
@@ -23,6 +24,7 @@ static mp_rom_map_elem_t usb_module_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__),        MP_OBJ_NEW_QSTR(MP_QSTR_usb) },
     { MP_ROM_QSTR(MP_QSTR_core),          MP_OBJ_FROM_PTR(&usb_core_module) },
     { MP_ROM_QSTR(MP_QSTR_util),          MP_OBJ_FROM_PTR(&usb_util_module) },
+    { MP_ROM_QSTR(MP_QSTR_cdc_host),      MP_OBJ_FROM_PTR(&usb_cdc_host_module) },
 };
 
 static MP_DEFINE_CONST_DICT(usb_module_globals, usb_module_globals_table);
diff --git a/shared-bindings/usb/cdc_host/Serial.c b/shared-bindings/usb/cdc_host/Serial.c
new file mode 100644
index 0000000000000..a018b471ed2e9
--- /dev/null
+++ b/shared-bindings/usb/cdc_host/Serial.c
@@ -0,0 +1,274 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: 2025 rianadon
+//
+// SPDX-License-Identifier: MIT
+
+#include "py/stream.h"
+#include "py/objproperty.h"
+#include "py/runtime.h"
+#include "py/stream.h"
+
+#include "shared-bindings/usb/cdc_host/Serial.h"
+
+#include "tusb.h"
+#include "class/cdc/cdc_host.h"
+
+//| class Serial:
+//|     """Receives cdc commands over USB"""
+//|
+//|     def __init__(self, device: Device) -> None:
+//|         """You cannot create an instance of `usb_cdc.Serial`.
+//|         The available instances are in the ``usb_cdc.serials`` tuple."""
+//|         ...
+//|
+//|     def read(self, size: int = -1) -> bytes:
+//|         """Read at most ``size`` bytes. If ``size`` exceeds the internal buffer size,
+//|         only the bytes in the buffer will be read. If ``size`` is not specified or is ``-1``,
+//|         read as many bytes as possible, until the timeout expires.
+//|         If `timeout` is > 0 or ``None``, and fewer than ``size`` bytes are available,
+//|         keep waiting until the timeout expires or ``size`` bytes are available.
+//|
+//|         If no bytes are read, return ``b''``. This is unlike, say, `busio.UART.read()`, which
+//|         would return ``None``.
+//|
+//|         :return: Data read
+//|         :rtype: bytes"""
+//|         ...
+//|
+//|     def readinto(self, buf: WriteableBuffer) -> int:
+//|         """Read bytes into the ``buf``. Read at most ``len(buf)`` bytes. If `timeout`
+//|         is > 0 or ``None``, keep waiting until the timeout expires or ``len(buf)``
+//|         bytes are available.
+//|
+//|         :return: number of bytes read and stored into ``buf``
+//|         :rtype: int"""
+//|         ...
+//|
+//|     def readline(self, size: int = -1) -> Optional[bytes]:
+//|         r"""Read a line ending in a newline character ("\\n"), including the newline.
+//|         Return everything readable if no newline is found and ``timeout`` is 0.
+//|         Return ``None`` in case of error.
+//|
+//|         This is a binary stream: the newline character "\\n" cannot be changed.
+//|         If the host computer transmits "\\r" it will also be included as part of the line.
+//|
+//|         :param int size: maximum number of characters to read. ``-1`` means as many as possible.
+//|         :return: the line read
+//|         :rtype: bytes or None"""
+//|         ...
+//|
+//|     def readlines(self) -> List[Optional[bytes]]:
+//|         """Read multiple lines as a list, using `readline()`.
+//|
+//|         .. warning:: If ``timeout`` is ``None``,
+//|           `readlines()` will never return, because there is no way to indicate end of stream.
+//|
+//|         :return: a list of the line read
+//|         :rtype: list"""
+//|         ...
+//|
+//|     def write(self, buf: ReadableBuffer) -> int:
+//|         """Write as many bytes as possible from the buffer of bytes.
+//|
+//|         :return: the number of bytes written
+//|         :rtype: int"""
+//|         ...
+//|
+//|     def flush(self) -> None:
+//|         """Force out any unwritten bytes, waiting until they are written."""
+//|         ...
+//|
+
+static mp_uint_t usb_host_cdc_serial_read_stream(mp_obj_t self_in, void *buf_in, mp_uint_t size, int *errcode) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    byte *buf = buf_in;
+
+    if (size == 0) {
+        return 0;
+    }
+
+    return common_hal_usb_host_cdc_serial_read(self, buf, size, errcode);
+}
+
+static mp_uint_t usb_host_cdc_serial_write_stream(mp_obj_t self_in, const void *buf_in, mp_uint_t size, int *errcode) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    const byte *buf = buf_in;
+
+    return common_hal_usb_host_cdc_serial_write(self, buf, size, errcode);
+}
+
+static mp_uint_t usb_host_cdc_serial_ioctl_stream(mp_obj_t self_in, mp_uint_t request, mp_uint_t arg, int *errcode) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_uint_t ret = 0;
+    switch (request) {
+        case MP_STREAM_POLL: {
+            mp_uint_t flags = arg;
+            ret = 0;
+            if ((flags & MP_STREAM_POLL_RD) && common_hal_usb_host_cdc_serial_get_in_waiting(self) > 0) {
+                ret |= MP_STREAM_POLL_RD;
+            }
+            if ((flags & MP_STREAM_POLL_WR) && common_hal_usb_host_cdc_serial_get_out_waiting(self) < CFG_TUH_CDC_TX_BUFSIZE) {
+                ret |= MP_STREAM_POLL_WR;
+            }
+            break;
+        }
+
+        case MP_STREAM_FLUSH:
+            common_hal_usb_host_cdc_serial_flush(self);
+            ret = 0;
+            break;
+
+        default:
+            *errcode = MP_EINVAL;
+            ret = MP_STREAM_ERROR;
+    }
+    return ret;
+}
+
+// connected property
+//|     connected: bool
+//|     """True if this Serial object represents a mounted CDC device
+//|     and the remote device is asserting DTR (Data Terminal Ready). (read-only)
+//|     """
+static mp_obj_t usb_host_cdc_serial_get_connected(mp_obj_t self_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    return mp_obj_new_bool(common_hal_usb_host_cdc_serial_get_connected(self));
+}
+MP_DEFINE_CONST_FUN_OBJ_1(usb_host_cdc_serial_get_connected_obj, usb_host_cdc_serial_get_connected);
+
+MP_PROPERTY_GETTER(usb_host_cdc_serial_connected_obj,
+    (mp_obj_t)&usb_host_cdc_serial_get_connected_obj);
+
+// in_waiting property
+//|     in_waiting: int
+//|     """Returns the number of bytes waiting to be read from the
+//|     CDC device's input buffer. (read-only)"""
+static mp_obj_t usb_host_cdc_serial_get_in_waiting(mp_obj_t self_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    return mp_obj_new_int(common_hal_usb_host_cdc_serial_get_in_waiting(self));
+}
+MP_DEFINE_CONST_FUN_OBJ_1(usb_host_cdc_serial_get_in_waiting_obj, usb_host_cdc_serial_get_in_waiting);
+
+MP_PROPERTY_GETTER(usb_host_cdc_serial_in_waiting_obj,
+    (mp_obj_t)&usb_host_cdc_serial_get_in_waiting_obj);
+
+// out_waiting property
+//|     out_waiting: int
+//|     """Returns the number of bytes waiting to be written to the
+//|     CDC device's output buffer. (read-only)"""
+static mp_obj_t usb_host_cdc_serial_get_out_waiting(mp_obj_t self_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    return mp_obj_new_int(common_hal_usb_host_cdc_serial_get_out_waiting(self));
+}
+MP_DEFINE_CONST_FUN_OBJ_1(usb_host_cdc_serial_get_out_waiting_obj, usb_host_cdc_serial_get_out_waiting);
+
+MP_PROPERTY_GETTER(usb_host_cdc_serial_out_waiting_obj,
+    (mp_obj_t)&usb_host_cdc_serial_get_out_waiting_obj);
+
+// reset_input_buffer method
+//|     def reset_input_buffer(self) -> None:
+//|         """Clears any unread bytes from the input buffer."""
+//|         ...
+static mp_obj_t usb_host_cdc_serial_reset_input_buffer(mp_obj_t self_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_usb_host_cdc_serial_reset_input_buffer(self);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_1(usb_host_cdc_serial_reset_input_buffer_obj, usb_host_cdc_serial_reset_input_buffer);
+
+// reset_output_buffer method
+//|     def reset_output_buffer(self) -> None:
+//|         """Clears any unwritten bytes from the output buffer."""
+//|         ...
+static mp_obj_t usb_host_cdc_serial_reset_output_buffer(mp_obj_t self_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_usb_host_cdc_serial_reset_output_buffer(self);
+    return mp_const_none; // Standard method returns None
+}
+MP_DEFINE_CONST_FUN_OBJ_1(usb_host_cdc_serial_reset_output_buffer_obj, usb_host_cdc_serial_reset_output_buffer);
+
+// timeout property
+//|     timeout: Optional[float]
+//|     """The read timeout value in seconds. `None means wait indefinitely.//|     0 means non-blocking. Positive value is the timeout in seconds."""
+static mp_obj_t usb_host_cdc_serial_get_timeout(mp_obj_t self_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_float_t timeout = common_hal_usb_host_cdc_serial_get_timeout(self);
+    return (timeout < 0.0f) ? mp_const_none : mp_obj_new_float(timeout);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(usb_host_cdc_serial_get_timeout_obj, usb_host_cdc_serial_get_timeout);
+
+static mp_obj_t usb_host_cdc_serial_set_timeout(mp_obj_t self_in, mp_obj_t timeout_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_usb_host_cdc_serial_set_timeout(self,
+        timeout_in == mp_const_none ? -1.0f : mp_obj_get_float(timeout_in));
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_2(usb_host_cdc_serial_set_timeout_obj, usb_host_cdc_serial_set_timeout);
+
+MP_PROPERTY_GETSET(usb_host_cdc_serial_timeout_obj,
+    (mp_obj_t)&usb_host_cdc_serial_get_timeout_obj,
+    (mp_obj_t)&usb_host_cdc_serial_set_timeout_obj);
+
+// write_timeout property
+//|     write_timeout: Optional[float]
+//|     """The write timeout value in seconds. `None means wait indefinitely.//|     0 means non-blocking. Positive value is the timeout in seconds."""
+static mp_obj_t usb_host_cdc_serial_get_write_timeout(mp_obj_t self_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    mp_float_t write_timeout = common_hal_usb_host_cdc_serial_get_write_timeout(self);
+    return (write_timeout < 0.0f) ? mp_const_none : mp_obj_new_float(write_timeout);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(usb_host_cdc_serial_get_write_timeout_obj, usb_host_cdc_serial_get_write_timeout);
+
+static mp_obj_t usb_host_cdc_serial_set_write_timeout(mp_obj_t self_in, mp_obj_t write_timeout_in) {
+    usb_cdc_host_serial_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    common_hal_usb_host_cdc_serial_set_write_timeout(self,
+        write_timeout_in == mp_const_none ? -1.0f : mp_obj_get_float(write_timeout_in));
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_2(usb_host_cdc_serial_set_write_timeout_obj, usb_host_cdc_serial_set_write_timeout);
+
+MP_PROPERTY_GETSET(usb_host_cdc_serial_write_timeout_obj,
+    (mp_obj_t)&usb_host_cdc_serial_get_write_timeout_obj,
+    (mp_obj_t)&usb_host_cdc_serial_set_write_timeout_obj);
+
+
+static const mp_rom_map_elem_t usb_host_cdc_serial_locals_dict_table[] = {
+    { MP_ROM_QSTR(MP_QSTR_flush),        MP_ROM_PTR(&mp_stream_flush_obj) },
+    { MP_ROM_QSTR(MP_QSTR_read),     MP_ROM_PTR(&mp_stream_read_obj) },
+    { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) },
+    { MP_ROM_QSTR(MP_QSTR_readline),     MP_ROM_PTR(&mp_stream_unbuffered_readline_obj)},
+    { MP_ROM_QSTR(MP_QSTR_readlines),    MP_ROM_PTR(&mp_stream_unbuffered_readlines_obj)},
+    { MP_ROM_QSTR(MP_QSTR_write),    MP_ROM_PTR(&mp_stream_write_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_in_waiting),          MP_ROM_PTR(&usb_host_cdc_serial_in_waiting_obj) },
+    { MP_ROM_QSTR(MP_QSTR_out_waiting),         MP_ROM_PTR(&usb_host_cdc_serial_out_waiting_obj) },
+    { MP_ROM_QSTR(MP_QSTR_reset_input_buffer),  MP_ROM_PTR(&usb_host_cdc_serial_reset_input_buffer_obj) },
+    { MP_ROM_QSTR(MP_QSTR_reset_output_buffer), MP_ROM_PTR(&usb_host_cdc_serial_reset_output_buffer_obj) },
+    { MP_ROM_QSTR(MP_QSTR_timeout),             MP_ROM_PTR(&usb_host_cdc_serial_timeout_obj) },
+    { MP_ROM_QSTR(MP_QSTR_write_timeout),       MP_ROM_PTR(&usb_host_cdc_serial_write_timeout_obj) },
+
+    { MP_ROM_QSTR(MP_QSTR_connected),     MP_ROM_PTR(&usb_host_cdc_serial_connected_obj) },
+
+    // TODO: Add baudrate, data_bits, parity, stop_bits properties/methods.
+};
+static MP_DEFINE_CONST_DICT(usb_host_cdc_serial_locals_dict, usb_host_cdc_serial_locals_dict_table);
+
+static const mp_stream_p_t usb_host_cdc_serial_stream_p = {
+    .read = usb_host_cdc_serial_read_stream,
+    .write = usb_host_cdc_serial_write_stream,
+    .ioctl = usb_host_cdc_serial_ioctl_stream,
+    .is_text = false,
+    .pyserial_read_compatibility = true,
+    .pyserial_readinto_compatibility = true,
+    .pyserial_dont_return_none_compatibility = true,
+};
+
+MP_DEFINE_CONST_OBJ_TYPE(
+    usb_cdc_host_serial_type,
+    MP_QSTR_Serial,
+    MP_TYPE_FLAG_ITER_IS_ITERNEXT | MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS,
+    locals_dict, &usb_host_cdc_serial_locals_dict,
+    iter, mp_stream_unbuffered_iter,
+    protocol, &usb_host_cdc_serial_stream_p
+    );
diff --git a/shared-bindings/usb/cdc_host/Serial.h b/shared-bindings/usb/cdc_host/Serial.h
new file mode 100644
index 0000000000000..ac41e6190b854
--- /dev/null
+++ b/shared-bindings/usb/cdc_host/Serial.h
@@ -0,0 +1,30 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: 2025 rianadon
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "shared-module/usb/cdc_host/Serial.h"
+
+extern const mp_obj_type_t usb_cdc_host_serial_type;
+
+size_t common_hal_usb_host_cdc_serial_read(usb_cdc_host_serial_obj_t *self, uint8_t *data, size_t len, int *errcode);
+size_t common_hal_usb_host_cdc_serial_write(usb_cdc_host_serial_obj_t *self, const uint8_t *data, size_t len, int *errcode);
+
+uint32_t common_hal_usb_host_cdc_serial_get_in_waiting(usb_cdc_host_serial_obj_t *self);
+uint32_t common_hal_usb_host_cdc_serial_get_out_waiting(usb_cdc_host_serial_obj_t *self);
+
+void common_hal_usb_host_cdc_serial_reset_input_buffer(usb_cdc_host_serial_obj_t *self);
+uint32_t common_hal_usb_host_cdc_serial_reset_output_buffer(usb_cdc_host_serial_obj_t *self);
+
+uint32_t common_hal_usb_host_cdc_serial_flush(usb_cdc_host_serial_obj_t *self);
+
+bool common_hal_usb_host_cdc_serial_get_connected(usb_cdc_host_serial_obj_t *self);
+
+mp_float_t common_hal_usb_host_cdc_serial_get_timeout(usb_cdc_host_serial_obj_t *self);
+void common_hal_usb_host_cdc_serial_set_timeout(usb_cdc_host_serial_obj_t *self, mp_float_t timeout);
+
+mp_float_t common_hal_usb_host_cdc_serial_get_write_timeout(usb_cdc_host_serial_obj_t *self);
+void common_hal_usb_host_cdc_serial_set_write_timeout(usb_cdc_host_serial_obj_t *self, mp_float_t write_timeout);
diff --git a/shared-bindings/usb/cdc_host/__init__.c b/shared-bindings/usb/cdc_host/__init__.c
new file mode 100644
index 0000000000000..3bcdb9ebd0260
--- /dev/null
+++ b/shared-bindings/usb/cdc_host/__init__.c
@@ -0,0 +1,57 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: 2025 rianadon
+//
+// SPDX-License-Identifier: MIT
+
+#include <stdint.h>
+
+#include "py/obj.h"
+#include "py/objmodule.h"
+#include "py/runtime.h"
+
+#include "shared-bindings/usb/cdc_host/Serial.h"
+#include "shared-bindings/usb/core/Device.h"
+
+#include "tusb.h"
+#include "class/cdc/cdc_host.h"
+
+static mp_obj_t usb_cdc_host_find(mp_obj_t device_in, mp_obj_t interface_in) {
+    if (!mp_obj_is_type(device_in, &usb_core_device_type)) {
+        mp_raise_TypeError(MP_ERROR_TEXT("device must be a usb.core.Device object"));
+    }
+
+    usb_core_device_obj_t *device_obj = MP_OBJ_TO_PTR(device_in);
+    uint8_t daddr = device_obj->device_address;
+
+    mp_int_t interface_num = mp_obj_get_int(interface_in);
+    uint8_t cdc_idx = tuh_cdc_itf_get_index(daddr, (uint8_t)interface_num);
+
+    if (cdc_idx == TUSB_INDEX_INVALID_8) {
+        return mp_const_none;
+    }
+
+    usb_cdc_host_serial_obj_t *serial_obj = mp_obj_malloc(usb_cdc_host_serial_obj_t, &usb_cdc_host_serial_type);
+    serial_obj->idx = cdc_idx;
+    serial_obj->timeout = -1.0f;
+    serial_obj->write_timeout = -1.0f;
+
+    return MP_OBJ_FROM_PTR(serial_obj);
+}
+
+static MP_DEFINE_CONST_FUN_OBJ_2(usb_cdc_host_find_obj, usb_cdc_host_find);
+
+static const mp_rom_map_elem_t usb_cdc_host_module_globals_table[] = {
+    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_usb_dot_cdc_host) },
+    { MP_ROM_QSTR(MP_QSTR_Serial),   MP_ROM_PTR(&usb_cdc_host_serial_type) },
+    { MP_ROM_QSTR(MP_QSTR_find),   MP_ROM_PTR(&usb_cdc_host_find_obj) },
+};
+
+static MP_DEFINE_CONST_DICT(usb_cdc_host_module_globals, usb_cdc_host_module_globals_table);
+
+const mp_obj_module_t usb_cdc_host_module = {
+    .base = { &mp_type_module },
+    .globals = (mp_obj_dict_t *)&usb_cdc_host_module_globals,
+};
+
+MP_REGISTER_MODULE(MP_QSTR_usb_dot_cdc_host, usb_cdc_host_module);
diff --git a/shared-bindings/usb/cdc_host/__init__.h b/shared-bindings/usb/cdc_host/__init__.h
new file mode 100644
index 0000000000000..260aec4bace2d
--- /dev/null
+++ b/shared-bindings/usb/cdc_host/__init__.h
@@ -0,0 +1,11 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: 2025 rianadon
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "shared-module/usb/cdc_host/__init__.h"
+
+extern const mp_obj_module_t usb_cdc_host_module;
diff --git a/shared-module/usb/cdc_host/Serial.c b/shared-module/usb/cdc_host/Serial.c
new file mode 100644
index 0000000000000..089cc86d3e7c9
--- /dev/null
+++ b/shared-module/usb/cdc_host/Serial.c
@@ -0,0 +1,119 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: 2025 rianadon
+//
+// SPDX-License-Identifier: MIT
+
+#include "shared/runtime/interrupt_char.h"
+#include "shared-bindings/usb/cdc_host/Serial.h"
+#include "shared-module/usb/cdc_host/Serial.h"
+#include "supervisor/shared/tick.h"
+
+#include "tusb.h"
+#include "class/cdc/cdc_host.h"
+#include "py/stream.h"
+
+size_t common_hal_usb_host_cdc_serial_read(usb_cdc_host_serial_obj_t *self, uint8_t *data, size_t len, int *errcode) {
+    if (!tuh_cdc_mounted(self->idx)) {
+        *errcode = ENODEV;
+        return 0;
+    }
+
+    size_t total_read = tuh_cdc_read(self->idx, data, len);
+    *errcode = 0;
+    return total_read;
+}
+
+size_t common_hal_usb_host_cdc_serial_write(usb_cdc_host_serial_obj_t *self, const uint8_t *data, size_t len, int *errcode) {
+    if (!tuh_cdc_mounted(self->idx)) {
+        *errcode = ENODEV;
+        return 0;
+    }
+
+    size_t total_written = tuh_cdc_write(self->idx, data, len);
+    *errcode = 0; // Success
+    return total_written;
+}
+
+
+uint32_t common_hal_usb_host_cdc_serial_get_in_waiting(usb_cdc_host_serial_obj_t *self) {
+    if (!tuh_cdc_mounted(self->idx)) {
+        return 0;
+    }
+    return tuh_cdc_read_available(self->idx);
+}
+
+uint32_t common_hal_usb_host_cdc_serial_get_out_waiting(usb_cdc_host_serial_obj_t *self) {
+    if (!tuh_cdc_mounted(self->idx)) {
+        return 0;
+    }
+    uint32_t available_space = tuh_cdc_write_available(self->idx);
+    if (available_space > CFG_TUH_CDC_TX_BUFSIZE) {
+        return 0;
+    }
+    return CFG_TUH_CDC_TX_BUFSIZE - available_space;
+}
+
+void common_hal_usb_host_cdc_serial_reset_input_buffer(usb_cdc_host_serial_obj_t *self) {
+    if (tuh_cdc_mounted(self->idx)) {
+        tuh_cdc_read_clear(self->idx);
+    }
+}
+
+uint32_t common_hal_usb_host_cdc_serial_reset_output_buffer(usb_cdc_host_serial_obj_t *self) {
+    uint32_t bytes_cleared = 0;
+    if (tuh_cdc_mounted(self->idx)) {
+        bytes_cleared = common_hal_usb_host_cdc_serial_get_out_waiting(self);
+        tuh_cdc_write_clear(self->idx);
+    }
+    return bytes_cleared;
+}
+
+uint32_t common_hal_usb_host_cdc_serial_flush(usb_cdc_host_serial_obj_t *self) {
+    if (!tuh_cdc_mounted(self->idx)) {
+        return 0;
+    }
+
+    uint64_t start_ticks = supervisor_ticks_ms64();
+    uint64_t timeout_ticks = (self->write_timeout < 0) ? 0 : float_to_uint64(self->write_timeout * 1000);
+
+    uint32_t initial_waiting = common_hal_usb_host_cdc_serial_get_out_waiting(self);
+
+    while (common_hal_usb_host_cdc_serial_get_out_waiting(self) > 0) {
+        tuh_cdc_write_flush(self->idx);
+
+        if (!(self->write_timeout < 0 || self->write_timeout > 0)) {
+            return initial_waiting - common_hal_usb_host_cdc_serial_get_out_waiting(self);
+        }
+
+        if (self->write_timeout > 0) {
+            if (supervisor_ticks_ms64() - start_ticks >= timeout_ticks) {
+                return initial_waiting - common_hal_usb_host_cdc_serial_get_out_waiting(self);
+            }
+        }
+
+        RUN_BACKGROUND_TASKS;
+    }
+
+    return initial_waiting;
+}
+
+bool common_hal_usb_host_cdc_serial_get_connected(usb_cdc_host_serial_obj_t *self) {
+    return tuh_cdc_mounted(self->idx) && tuh_cdc_connected(self->idx);
+}
+
+mp_float_t common_hal_usb_host_cdc_serial_get_timeout(usb_cdc_host_serial_obj_t *self) {
+    return self->timeout;
+}
+
+void common_hal_usb_host_cdc_serial_set_timeout(usb_cdc_host_serial_obj_t *self, mp_float_t timeout) {
+    self->timeout = timeout;
+}
+
+mp_float_t common_hal_usb_host_cdc_serial_get_write_timeout(usb_cdc_host_serial_obj_t *self) {
+    return self->write_timeout;
+}
+
+void common_hal_usb_host_cdc_serial_set_write_timeout(usb_cdc_host_serial_obj_t *self, mp_float_t write_timeout) {
+    self->write_timeout = write_timeout;
+}
diff --git a/shared-module/usb/cdc_host/Serial.h b/shared-module/usb/cdc_host/Serial.h
new file mode 100644
index 0000000000000..2e6c6272ef6e6
--- /dev/null
+++ b/shared-module/usb/cdc_host/Serial.h
@@ -0,0 +1,16 @@
+// This file is part of the CircuitPython project: https://circuitpython.org
+//
+// SPDX-FileCopyrightText: 2025 rianadon
+//
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include "py/obj.h"
+
+typedef struct {
+    mp_obj_base_t base;
+    mp_float_t timeout;        // Read timeout (s). <0 means block forever
+    mp_float_t write_timeout;  // Write timeout (s). <0 means block forever
+    uint8_t idx;               // TinyUSB CDC interface index
+} usb_cdc_host_serial_obj_t;
diff --git a/shared-module/usb/cdc_host/__init__.c b/shared-module/usb/cdc_host/__init__.c
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/shared-module/usb/cdc_host/__init__.h b/shared-module/usb/cdc_host/__init__.h
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/supervisor/shared/usb/tusb_config.h b/supervisor/shared/usb/tusb_config.h
index ce39925f45b22..de9eca5501604 100644
--- a/supervisor/shared/usb/tusb_config.h
+++ b/supervisor/shared/usb/tusb_config.h
@@ -185,7 +185,7 @@ extern "C" {
 
 // 2 hubs so we can support "7 port" hubs which have two internal hubs.
 #define CFG_TUH_HUB                 2
-#define CFG_TUH_CDC                 0
+#define CFG_TUH_CDC                 2
 #define CFG_TUH_MSC                 0
 #define CFG_TUH_VENDOR              0
 #define CFG_TUH_API_EDPT_XFER       1
diff --git a/supervisor/supervisor.mk b/supervisor/supervisor.mk
index 0ecc6ea3acfe6..3eb1f3388b31d 100644
--- a/supervisor/supervisor.mk
+++ b/supervisor/supervisor.mk
@@ -185,6 +185,7 @@ ifeq ($(CIRCUITPY_TINYUSB),1)
     SRC_SUPERVISOR += \
       lib/tinyusb/src/host/hub.c \
       lib/tinyusb/src/host/usbh.c \
+      lib/tinyusb/src/class/cdc/cdc_host.c \
 
   endif