summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaurizio Pesce <[email protected]>2025-03-12 09:50:04 +0100
committerroundby <[email protected]>2025-08-06 06:14:13 +0200
commit283b06bb543f513d964dc3a6953115df897a93a1 (patch)
tree4b8fe0783a64d6ca6157649bef0b35a8c0c5e324
parentf01c4be350d21145162f4a3f283a298cd221f74d (diff)
Add MTP class device
-rw-r--r--examples/device/mtp/CMakeLists.txt39
-rw-r--r--examples/device/mtp/CMakePresets.json6
-rw-r--r--examples/device/mtp/Makefile11
-rw-r--r--examples/device/mtp/prj.conf6
-rw-r--r--examples/device/mtp/skip.txt0
-rw-r--r--examples/device/mtp/src/main.c113
-rw-r--r--examples/device/mtp/src/mtp_fs_example.c572
-rw-r--r--examples/device/mtp/src/tusb_config.h112
-rw-r--r--examples/device/mtp/src/usb_descriptors.c193
-rw-r--r--hw/bsp/espressif/components/tinyusb_src/CMakeLists.txt1
-rw-r--r--hw/bsp/rp2040/family.cmake1
-rw-r--r--lib/rt-thread/SConscript2
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/class/mtp/mtp.h438
-rw-r--r--src/class/mtp/mtp_device.c997
-rw-r--r--src/class/mtp/mtp_device.h75
-rw-r--r--src/class/mtp/mtp_device_storage.h149
-rw-r--r--src/device/usbd.c13
-rw-r--r--src/device/usbd.h19
-rw-r--r--src/tinyusb.mk1
-rw-r--r--src/tusb.h4
-rw-r--r--src/tusb_option.h4
-rw-r--r--test/fuzz/rules.mk1
-rw-r--r--tools/iar_template.ipcf6
24 files changed, 2764 insertions, 0 deletions
diff --git a/examples/device/mtp/CMakeLists.txt b/examples/device/mtp/CMakeLists.txt
new file mode 100644
index 000000000..e91eb8fd9
--- /dev/null
+++ b/examples/device/mtp/CMakeLists.txt
@@ -0,0 +1,39 @@
+cmake_minimum_required(VERSION 3.20)
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake)
+
+# gets PROJECT name for the example (e.g. <BOARD>-<DIR_NAME>)
+family_get_project_name(PROJECT ${CMAKE_CURRENT_LIST_DIR})
+
+project(${PROJECT} C CXX ASM)
+
+# Checks this example is valid for the family and initializes the project
+family_initialize_project(${PROJECT} ${CMAKE_CURRENT_LIST_DIR})
+
+# Espressif has its own cmake build system
+if(FAMILY STREQUAL "espressif")
+ return()
+endif()
+
+if (RTOS STREQUAL zephyr)
+ set(EXE_NAME app)
+else()
+ set(EXE_NAME ${PROJECT})
+ add_executable(${EXE_NAME})
+endif()
+
+# Example source
+target_sources(${EXE_NAME} PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/mtp_fs_example.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.c
+ )
+
+# Example include
+target_include_directories(${EXE_NAME} PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
+ )
+
+# Configure compilation flags and libraries for the example without RTOS.
+# See the corresponding function in hw/bsp/FAMILY/family.cmake for details.
+family_configure_device_example(${EXE_NAME} ${RTOS})
diff --git a/examples/device/mtp/CMakePresets.json b/examples/device/mtp/CMakePresets.json
new file mode 100644
index 000000000..5cd8971e9
--- /dev/null
+++ b/examples/device/mtp/CMakePresets.json
@@ -0,0 +1,6 @@
+{
+ "version": 6,
+ "include": [
+ "../../../hw/bsp/BoardPresets.json"
+ ]
+}
diff --git a/examples/device/mtp/Makefile b/examples/device/mtp/Makefile
new file mode 100644
index 000000000..7fa475da5
--- /dev/null
+++ b/examples/device/mtp/Makefile
@@ -0,0 +1,11 @@
+include ../../build_system/make/make.mk
+
+INC += \
+ src \
+ $(TOP)/hw \
+
+# Example source
+EXAMPLE_SOURCE += $(wildcard src/*.c)
+SRC_C += $(addprefix $(CURRENT_PATH)/, $(EXAMPLE_SOURCE))
+
+include ../../build_system/make/rules.mk
diff --git a/examples/device/mtp/prj.conf b/examples/device/mtp/prj.conf
new file mode 100644
index 000000000..2f5139d9d
--- /dev/null
+++ b/examples/device/mtp/prj.conf
@@ -0,0 +1,6 @@
+CONFIG_GPIO=y
+CONFIG_FPU=y
+CONFIG_NO_OPTIMIZATIONS=y
+CONFIG_UART_INTERRUPT_DRIVEN=y
+CONFIG_NRFX_POWER=y
+CONFIG_NRFX_UARTE0=y
diff --git a/examples/device/mtp/skip.txt b/examples/device/mtp/skip.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/examples/device/mtp/skip.txt
diff --git a/examples/device/mtp/src/main.c b/examples/device/mtp/src/main.c
new file mode 100644
index 000000000..709aeb16f
--- /dev/null
+++ b/examples/device/mtp/src/main.c
@@ -0,0 +1,113 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Ennebi Elettronica (https://ennebielettronica.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "bsp/board_api.h"
+#include "tusb.h"
+
+//--------------------------------------------------------------------+
+// MACRO CONSTANT TYPEDEF PROTYPES
+//--------------------------------------------------------------------+
+
+/* Blink pattern
+ * - 250 ms : device not mounted
+ * - 1000 ms : device mounted
+ * - 2500 ms : device is suspended
+ */
+enum {
+ BLINK_NOT_MOUNTED = 250,
+ BLINK_MOUNTED = 1000,
+ BLINK_SUSPENDED = 2500,
+};
+
+static uint32_t blink_interval_ms = BLINK_NOT_MOUNTED;
+
+void led_blinking_task(void);
+
+/*------------- MAIN -------------*/
+int main(void) {
+ board_init();
+
+ // init device stack on configured roothub port
+ tusb_rhport_init_t dev_init = {
+ .role = TUSB_ROLE_DEVICE,
+ .speed = TUSB_SPEED_AUTO
+ };
+ tusb_init(BOARD_TUD_RHPORT, &dev_init);
+
+ if (board_init_after_tusb) {
+ board_init_after_tusb();
+ }
+
+ while (1) {
+ tud_task(); // tinyusb device task
+ led_blinking_task();
+ }
+}
+
+//--------------------------------------------------------------------+
+// Device callbacks
+//--------------------------------------------------------------------+
+
+// Invoked when device is mounted
+void tud_mount_cb(void) {
+ blink_interval_ms = BLINK_MOUNTED;
+}
+
+// Invoked when device is unmounted
+void tud_umount_cb(void) {
+ blink_interval_ms = BLINK_NOT_MOUNTED;
+}
+
+// Invoked when usb bus is suspended
+// remote_wakeup_en : if host allow us to perform remote wakeup
+// Within 7ms, device must draw an average of current less than 2.5 mA from bus
+void tud_suspend_cb(bool remote_wakeup_en) {
+ (void) remote_wakeup_en;
+ blink_interval_ms = BLINK_SUSPENDED;
+}
+
+// Invoked when usb bus is resumed
+void tud_resume_cb(void) {
+ blink_interval_ms = tud_mounted() ? BLINK_MOUNTED : BLINK_NOT_MOUNTED;
+}
+
+//--------------------------------------------------------------------+
+// BLINKING TASK
+//--------------------------------------------------------------------+
+void led_blinking_task(void) {
+ static uint32_t start_ms = 0;
+ static bool led_state = false;
+
+ // Blink every interval ms
+ if (board_millis() - start_ms < blink_interval_ms) return; // not enough time
+ start_ms += blink_interval_ms;
+
+ board_led_write(led_state);
+ led_state = 1 - led_state; // toggle
+}
diff --git a/examples/device/mtp/src/mtp_fs_example.c b/examples/device/mtp/src/mtp_fs_example.c
new file mode 100644
index 000000000..41673ea53
--- /dev/null
+++ b/examples/device/mtp/src/mtp_fs_example.c
@@ -0,0 +1,572 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Ennebi Elettronica (https://ennebielettronica.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "class/mtp/mtp_device_storage.h"
+#include "tusb.h"
+
+#define MTPD_STORAGE_DESCRIPTION "storage"
+#define MTPD_VOLUME_IDENTIFIER "volume"
+
+//--------------------------------------------------------------------+
+// RAM FILESYSTEM
+//--------------------------------------------------------------------+
+#define FS_MAX_NODES 5UL
+#define FS_MAX_NODE_BYTES 128UL
+#define FS_MAX_NODE_NAME_LEN 64UL
+#define FS_ISODATETIME_LEN 26UL
+
+typedef struct
+{
+ uint32_t handle;
+ uint32_t parent;
+ uint32_t size;
+ bool allocated;
+ bool association;
+ char name[FS_MAX_NODE_NAME_LEN];
+ char created[FS_ISODATETIME_LEN];
+ char modified[FS_ISODATETIME_LEN];
+ uint8_t data[FS_MAX_NODE_BYTES];
+} fs_object_info_t;
+
+// Sample object file
+static fs_object_info_t _fs_objects[FS_MAX_NODES] = {
+ {
+ .handle = 1,
+ .parent = 0,
+ .allocated = true,
+ .association = false,
+ .name = "readme.txt",
+ .created = "20240104T111134.0",
+ .modified = "20241214T121110.0",
+ .data = "USB MTP on RAM Filesystem example\n",
+ .size = 34
+ }
+};
+
+//--------------------------------------------------------------------+
+// OPERATING STATUS
+//--------------------------------------------------------------------+
+typedef struct
+{
+ // Session
+ uint32_t session_id;
+ // Association traversal
+ uint32_t traversal_parent;
+ uint32_t traversal_index;
+ // Object open for reading
+ uint32_t read_handle;
+ uint32_t read_pos;
+ // Object open for writing
+ uint32_t write_handle;
+ uint32_t write_pos;
+ // Unique identifier
+ uint32_t last_handle;
+} fs_operation_t;
+
+static fs_operation_t _fs_operation = {
+ .last_handle = 1
+};
+
+//--------------------------------------------------------------------+
+// INTERNAL FUNCTIONS
+//--------------------------------------------------------------------+
+
+// Get pointer to object info from handle
+fs_object_info_t *fs_object_get_from_handle(uint32_t handle);
+
+// Get the number of allocated nodes in filesystem
+unsigned int fs_get_object_count(void);
+
+fs_object_info_t *fs_object_get_from_handle(uint32_t handle)
+{
+ fs_object_info_t *obj;
+ for (unsigned int i=0; i<FS_MAX_NODES; i++)
+ {
+ obj = &_fs_objects[i];
+ if (obj->allocated && obj->handle == handle)
+ return obj;
+ }
+ return NULL;
+}
+
+unsigned int fs_get_object_count(void)
+{
+ unsigned int s = 0;
+ for (unsigned int i = 0; i<FS_MAX_NODES; i++)
+ {
+ if (_fs_objects[i].allocated)
+ s++;
+ }
+ return s;
+}
+
+//--------------------------------------------------------------------+
+// API
+//--------------------------------------------------------------------+
+mtp_response_t tud_mtp_storage_open_session(uint32_t *session_id)
+{
+ if (*session_id == 0)
+ {
+ TU_LOG1("Invalid session ID\r\n");
+ return MTP_RESC_INVALID_PARAMETER;
+ }
+ if (_fs_operation.session_id != 0)
+ {
+ *session_id = _fs_operation.session_id;
+ TU_LOG1("ERR: Session %ld already open\r\n", _fs_operation.session_id);
+ return MTP_RESC_SESSION_ALREADY_OPEN;
+ }
+ _fs_operation.session_id = *session_id;
+ TU_LOG1("Open session with id %ld\r\n", _fs_operation.session_id);
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_close_session(uint32_t session_id)
+{
+ if (session_id != _fs_operation.session_id)
+ {
+ TU_LOG1("ERR: Session %ld not open\r\n", session_id);
+ return MTP_RESC_SESSION_NOT_OPEN;
+ }
+ _fs_operation.session_id = 0;
+ TU_LOG1("Session closed\r\n");
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_get_storage_id(uint32_t *storage_id)
+{
+ if (_fs_operation.session_id == 0)
+ {
+ TU_LOG1("ERR: Session not open\r\n");
+ return MTP_RESC_SESSION_NOT_OPEN;
+ }
+ *storage_id = STORAGE_ID(0x0001, 0x0001);
+ TU_LOG1("Retrieved storage identifier %ld\r\n", *storage_id);
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_get_storage_info(uint32_t storage_id, mtp_storage_info_t *info)
+{
+ if (_fs_operation.session_id == 0)
+ {
+ TU_LOG1("ERR: Session not open\r\n");
+ return MTP_RESC_SESSION_NOT_OPEN;
+ }
+ if (storage_id != STORAGE_ID(0x0001, 0x0001))
+ {
+ TU_LOG1("ERR: Unexpected storage id %ld\r\n", storage_id);
+ return MTP_RESC_INVALID_STORAGE_ID;
+ }
+ info->storage_type = MTP_STORAGE_TYPE_FIXED_RAM;
+ info->filesystem_type = MTP_FILESYSTEM_TYPE_GENERIC_HIERARCHICAL;
+ info->access_capability = MTP_ACCESS_CAPABILITY_READ_WRITE;
+ info->max_capacity_in_bytes = FS_MAX_NODES * FS_MAX_NODE_BYTES;
+ info->free_space_in_objects = FS_MAX_NODES - fs_get_object_count();
+ info->free_space_in_bytes = info->free_space_in_objects * FS_MAX_NODE_BYTES;
+ mtpd_gct_append_wstring(MTPD_STORAGE_DESCRIPTION);
+ mtpd_gct_append_wstring(MTPD_VOLUME_IDENTIFIER);
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_format(uint32_t storage_id)
+{
+ if (_fs_operation.session_id == 0)
+ {
+ TU_LOG1("ERR: Session not open\r\n");
+ return MTP_RESC_SESSION_NOT_OPEN;
+ }
+ if (storage_id != STORAGE_ID(0x0001, 0x0001))
+ {
+ TU_LOG1("ERR: Unexpected storage id %ld\r\n", storage_id);
+ return MTP_RESC_INVALID_STORAGE_ID;
+ }
+
+ // Simply deallocate all entries
+ for (unsigned int i=0; i<FS_MAX_NODES; i++)
+ _fs_objects[i].allocated = false;
+ TU_LOG1("Format completed\r\n");
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_association_get_object_handle(uint32_t storage_id, uint32_t parent_object_handle, uint32_t *next_child_handle)
+{
+ fs_object_info_t *obj;
+
+ if (_fs_operation.session_id == 0)
+ {
+ TU_LOG1("ERR: Session not open\r\n");
+ return MTP_RESC_SESSION_NOT_OPEN;
+ }
+ // We just have one storage, same reply if querying all storages
+ if (storage_id != 0xFFFFFFFF && storage_id != STORAGE_ID(0x0001, 0x0001))
+ {
+ TU_LOG1("ERR: Unexpected storage id %ld\r\n", storage_id);
+ return MTP_RESC_INVALID_STORAGE_ID;
+ }
+
+ // Request for objects with no parent (0xFFFFFFFF) are considered root objects
+ // Note: implementation may pass 0 as parent_object_handle
+ if (parent_object_handle == 0xFFFFFFFF)
+ parent_object_handle = 0;
+
+ if (parent_object_handle != _fs_operation.traversal_parent)
+ {
+ _fs_operation.traversal_parent = parent_object_handle;
+ _fs_operation.traversal_index = 0;
+ }
+
+ for (unsigned int i=_fs_operation.traversal_index; i<FS_MAX_NODES; i++)
+ {
+ obj = &_fs_objects[i];
+ if (obj->allocated && obj->parent == parent_object_handle)
+ {
+ _fs_operation.traversal_index = i+1;
+ *next_child_handle = obj->handle;
+ TU_LOG1("Association %ld -> child %ld\r\n", parent_object_handle, obj->handle);
+ return MTP_RESC_OK;
+ }
+ }
+ TU_LOG1("Association traversal completed\r\n");
+ _fs_operation.traversal_index = 0;
+ *next_child_handle = 0;
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_object_write_info(uint32_t storage_id, uint32_t parent_object, uint32_t *new_object_handle, const mtp_object_info_t *info)
+{
+ fs_object_info_t *obj = NULL;
+
+ if (_fs_operation.session_id == 0)
+ {
+ TU_LOG1("ERR: Session not open\r\n");
+ return MTP_RESC_SESSION_NOT_OPEN;
+ }
+ // Accept command on default storage
+ if (storage_id != 0xFFFFFFFF && storage_id != STORAGE_ID(0x0001, 0x0001))
+ {
+ TU_LOG1("ERR: Unexpected storage id %ld\r\n", storage_id);
+ return MTP_RESC_INVALID_STORAGE_ID;
+ }
+
+ if (info->object_compressed_size > FS_MAX_NODE_BYTES)
+ {
+ TU_LOG1("Object size %ld is more than maximum %ld\r\n", info->object_compressed_size, FS_MAX_NODE_BYTES);
+ return MTP_RESC_STORE_FULL;
+ }
+
+ // Request for objects with no parent (0xFFFFFFFF) are considered root objects
+ if (parent_object == 0xFFFFFFFF)
+ parent_object = 0;
+
+ // Ensure we are not creating an orphaned object outside root
+ if (parent_object != 0)
+ {
+ obj = fs_object_get_from_handle(parent_object);
+ if (obj == NULL)
+ {
+ TU_LOG1("Parent %ld does not exist\r\n", parent_object);
+ return MTP_RESC_INVALID_PARENT_OBJECT;
+ }
+ if (!obj->association)
+ {
+ TU_LOG1("Parent %ld is not an association\r\n", parent_object);
+ return MTP_RESC_INVALID_PARENT_OBJECT;
+ }
+ }
+
+ // Search for first free object
+ for (unsigned int i=0; i<FS_MAX_NODES; i++)
+ {
+ if (!_fs_objects[i].allocated)
+ {
+ obj = &_fs_objects[i];
+ break;
+ }
+ }
+
+ if (obj == NULL)
+ {
+ TU_LOG1("No space left on device\r\n");
+ return MTP_RESC_STORE_FULL;
+ }
+
+ // Fill-in structure
+ obj->allocated = true;
+ obj->handle = ++_fs_operation.last_handle;
+ obj->parent = parent_object;
+ obj->size = info->object_compressed_size;
+ obj->association = info->object_format == MTP_OBJF_ASSOCIATION;
+
+ // Extract variable data
+ uint16_t offset_data = sizeof(mtp_object_info_t);
+ mtpd_gct_get_string(&offset_data, obj->name, FS_MAX_NODE_NAME_LEN);
+ mtpd_gct_get_string(&offset_data, obj->created, FS_ISODATETIME_LEN);
+ mtpd_gct_get_string(&offset_data, obj->modified, FS_ISODATETIME_LEN);
+
+ TU_LOG1("Create %s %s with handle %ld, parent %ld and size %ld\r\n",
+ obj->association ? "association" : "object",
+ obj->name, obj->handle, obj->parent, obj->size);
+ *new_object_handle = obj->handle;
+ // Initialize operation
+ _fs_operation.write_handle = obj->handle;
+ _fs_operation.write_pos = 0;
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_object_read_info(uint32_t object_handle, mtp_object_info_t *info)
+{
+ const fs_object_info_t *obj;
+
+ if (_fs_operation.session_id == 0)
+ {
+ TU_LOG1("ERR: Session not open\r\n");
+ return MTP_RESC_SESSION_NOT_OPEN;
+ }
+
+ obj = fs_object_get_from_handle(object_handle);
+ if (obj == NULL)
+ {
+ TU_LOG1("ERR: Object with handle %ld does not exist\r\n", object_handle);
+ return MTP_RESC_INVALID_OBJECT_HANDLE;
+ }
+
+ memset(info, 0, sizeof(mtp_object_info_t));
+ info->storage_id = STORAGE_ID(0x0001, 0x0001);
+ if (obj->association)
+ {
+ info->object_format = MTP_OBJF_ASSOCIATION;
+ info->protection_status = MTP_PROTECTION_STATUS_NO_PROTECTION;
+ info->object_compressed_size = 0;
+ info->association_type = MTP_ASSOCIATION_UNDEFINED;
+ }
+ else
+ {
+ info->object_format = MTP_OBJF_UNDEFINED;
+ info->protection_status = MTP_PROTECTION_STATUS_NO_PROTECTION;
+ info->object_compressed_size = obj->size;
+ info->association_type = MTP_ASSOCIATION_UNDEFINED;
+ }
+ info->thumb_format = MTP_OBJF_UNDEFINED;
+ info->parent_object = obj->parent;
+
+ mtpd_gct_append_wstring(obj->name);
+ mtpd_gct_append_wstring(obj->created); // date_created
+ mtpd_gct_append_wstring(obj->modified); // date_modified
+ mtpd_gct_append_wstring(""); // keywords, not used
+
+ TU_LOG1("Retrieve object %s with handle %ld\r\n", obj->name, obj->handle);
+
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_object_write(uint32_t object_handle, const uint8_t *buffer, uint32_t size)
+{
+ fs_object_info_t *obj;
+
+ obj = fs_object_get_from_handle(object_handle);
+ if (obj == NULL)
+ {
+ TU_LOG1("ERR: Object with handle %ld does not exist\r\n", object_handle);
+ return MTP_RESC_INVALID_OBJECT_HANDLE;
+ }
+ // It's a requirement that this command is preceded by a write info
+ if (object_handle != _fs_operation.write_handle)
+ {
+ TU_LOG1("ERR: Object %ld not open for write\r\n", object_handle);
+ return MTP_RESC_NO_VALID_OBJECTINFO;
+ }
+
+ TU_LOG1("Write object %ld: data chunk at %ld/%ld bytes at offset %ld\r\n", object_handle, _fs_operation.write_pos, obj->size, size);
+ TU_ASSERT(obj->size >= _fs_operation.write_pos + size, MTP_RESC_INCOMPLETE_TRANSFER);
+ if (_fs_operation.write_pos + size < FS_MAX_NODE_BYTES)
+ memcpy(&obj->data[_fs_operation.write_pos], buffer, size);
+ _fs_operation.write_pos += size;
+ // Write operation completed
+ if (_fs_operation.write_pos == obj->size)
+ {
+ _fs_operation.write_handle = 0;
+ _fs_operation.write_pos = 0;
+ }
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_object_size(uint32_t object_handle, uint32_t *size)
+{
+ const fs_object_info_t *obj;
+ obj = fs_object_get_from_handle(object_handle);
+ if (obj == NULL)
+ {
+ TU_LOG1("ERR: Object with handle %ld does not exist\r\n", object_handle);
+ return MTP_RESC_INVALID_OBJECT_HANDLE;
+ }
+ *size = obj->size;
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_object_read(uint32_t object_handle, void *buffer, uint32_t buffer_size, uint32_t *read_count)
+{
+ const fs_object_info_t *obj;
+
+ obj = fs_object_get_from_handle(object_handle);
+
+ if (obj == NULL)
+ {
+ TU_LOG1("ERR: Object with handle %ld does not exist\r\n", object_handle);
+ return MTP_RESC_INVALID_OBJECT_HANDLE;
+ }
+ // It's not a requirement that this command is preceded by a read info
+ if (object_handle != _fs_operation.read_handle)
+ {
+ TU_LOG1("ERR: Object %ld not open for read\r\n", object_handle);
+ _fs_operation.read_handle = object_handle;
+ _fs_operation.read_pos = 0;
+ }
+
+ if (obj->size - _fs_operation.read_pos > buffer_size)
+ {
+ TU_LOG1("Read object %ld: %ld bytes at offset %ld\r\n", object_handle, buffer_size, _fs_operation.read_pos);
+ *read_count = buffer_size;
+ if (_fs_operation.read_pos + buffer_size < FS_MAX_NODE_BYTES)
+ memcpy(buffer, &obj->data[_fs_operation.read_pos], *read_count);
+ _fs_operation.read_pos += *read_count;
+ }
+ else
+ {
+ TU_LOG1("Read object %ld: %ld bytes at offset %ld\r\n", object_handle, obj->size - _fs_operation.read_pos, _fs_operation.read_pos);
+ *read_count = obj->size - _fs_operation.read_pos;
+ if (_fs_operation.read_pos + buffer_size < FS_MAX_NODE_BYTES)
+ memcpy(buffer, &obj->data[_fs_operation.read_pos], *read_count);
+ // Read operation completed
+ _fs_operation.read_handle = 0;
+ _fs_operation.read_pos = 0;
+ }
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_object_move(uint32_t object_handle, uint32_t new_parent_object_handle)
+{
+ fs_object_info_t *obj;
+
+ if (new_parent_object_handle == 0xFFFFFFFF)
+ new_parent_object_handle = 0;
+
+ // Ensure we are not moving to an nonexisting parent
+ if (new_parent_object_handle != 0)
+ {
+ obj = fs_object_get_from_handle(new_parent_object_handle);
+ if (obj == NULL)
+ {
+ TU_LOG1("Parent %ld does not exist\r\n", new_parent_object_handle);
+ return MTP_RESC_INVALID_PARENT_OBJECT;
+ }
+ if (!obj->association)
+ {
+ TU_LOG1("Parent %ld is not an association\r\n", new_parent_object_handle);
+ return MTP_RESC_INVALID_PARENT_OBJECT;
+ }
+ }
+
+ obj = fs_object_get_from_handle(object_handle);
+
+ if (obj == NULL)
+ {
+ TU_LOG1("ERR: Object with handle %ld does not exist\r\n", object_handle);
+ return MTP_RESC_INVALID_OBJECT_HANDLE;
+ }
+ TU_LOG1("Move object %ld to new parent %ld\r\n", object_handle, new_parent_object_handle);
+ obj->parent = new_parent_object_handle;
+ return MTP_RESC_OK;
+}
+
+mtp_response_t tud_mtp_storage_object_delete(uint32_t object_handle)
+{
+ fs_object_info_t *obj;
+
+ if (_fs_operation.session_id == 0)
+ {
+ TU_LOG1("ERR: Session not open\r\n");
+ return MTP_RESC_SESSION_NOT_OPEN;
+ }
+
+ if (object_handle == 0xFFFFFFFF)
+ object_handle = 0;
+
+ if (object_handle != 0)
+ {
+ obj = fs_object_get_from_handle(object_handle);
+
+ if (obj == NULL)
+ {
+ TU_LOG1("ERR: Object with handle %ld does not exist\r\n", object_handle);
+ return MTP_RESC_INVALID_OBJECT_HANDLE;
+ }
+ obj->allocated = false;
+ TU_LOG1("Delete object with handle %ld\r\n", object_handle);
+ }
+
+ if (object_handle == 0 || obj->association)
+ {
+ // Delete also children
+ for (unsigned int i=0; i<FS_MAX_NODES; i++)
+ {
+ obj = &_fs_objects[i];
+ if (obj->allocated && obj->parent == object_handle)
+ {
+ tud_mtp_storage_object_delete(obj->handle);
+ }
+ }
+ }
+
+ return MTP_RESC_OK;
+}
+
+void tud_mtp_storage_object_done(void)
+{
+}
+
+void tud_mtp_storage_cancel(void)
+{
+ fs_object_info_t *obj;
+
+ _fs_operation.traversal_parent = 0;
+ _fs_operation.traversal_index = 0;
+ _fs_operation.read_handle = 0;
+ _fs_operation.read_pos = 0;
+ // If write operation is canceled, discard object
+ if (_fs_operation.write_handle)
+ {
+ obj = fs_object_get_from_handle(_fs_operation.write_handle);
+ if (obj)
+ obj->allocated = false;
+ }
+ _fs_operation.write_handle = 0;
+ _fs_operation.write_pos = 0;
+}
+
+void tud_mtp_storage_reset(void)
+{
+ tud_mtp_storage_cancel();
+ _fs_operation.session_id = 0;
+}
diff --git a/examples/device/mtp/src/tusb_config.h b/examples/device/mtp/src/tusb_config.h
new file mode 100644
index 000000000..251a99fd3
--- /dev/null
+++ b/examples/device/mtp/src/tusb_config.h
@@ -0,0 +1,112 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Ennebi Elettronica (https://ennebielettronica.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef _TUSB_CONFIG_H_
+#define _TUSB_CONFIG_H_
+
+#ifdef __cplusplus
+ extern "C" {
+#endif
+
+//--------------------------------------------------------------------+
+// Board Specific Configuration
+//--------------------------------------------------------------------+
+
+// RHPort number used for device can be defined by board.mk, default to port 0
+#ifndef BOARD_TUD_RHPORT
+#define BOARD_TUD_RHPORT 0
+#endif
+
+// RHPort max operational speed can defined by board.mk
+#ifndef BOARD_TUD_MAX_SPEED
+#define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED
+#endif
+
+//--------------------------------------------------------------------
+// COMMON CONFIGURATION
+//--------------------------------------------------------------------
+
+// defined by compiler flags for flexibility
+#ifndef CFG_TUSB_MCU
+#error CFG_TUSB_MCU must be defined
+#endif
+
+#ifndef CFG_TUSB_OS
+#define CFG_TUSB_OS OPT_OS_NONE
+#endif
+
+#ifndef CFG_TUSB_DEBUG
+#define CFG_TUSB_DEBUG 0
+#endif
+
+// Enable Device stack
+#define CFG_TUD_ENABLED 1
+
+// Default is max speed that hardware controller could support with on-chip PHY
+#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED
+
+/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment.
+ * Tinyusb use follows macros to declare transferring memory so that they can be put
+ * into those specific section.
+ * e.g
+ * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") ))
+ * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4)))
+ */
+#ifndef CFG_TUSB_MEM_SECTION
+#define CFG_TUSB_MEM_SECTION
+#endif
+
+#ifndef CFG_TUSB_MEM_ALIGN
+#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4)))
+#endif
+
+//--------------------------------------------------------------------
+// DEVICE CONFIGURATION
+//--------------------------------------------------------------------
+
+#ifndef CFG_TUD_ENDPOINT0_SIZE
+#define CFG_TUD_ENDPOINT0_SIZE 64
+#endif
+
+//------------- CLASS -------------//
+#define CFG_TUD_MTP 1
+
+#define CFG_TUD_MANUFACTURER "TinyUsb Manufacturer"
+#define CFG_TUD_MODEL "TinyUsb Device"
+
+#define CFG_MTP_EP_SIZE 64
+#define CFG_MTP_EVT_EP_SIZE 64
+#define CFG_MTP_EVT_INTERVAL 100
+
+#define CFG_MTP_DEVICE_VERSION "1.0"
+#define CFG_MTP_SERIAL_NUMBER "0"
+#define CFG_MTP_INTERFACE (CFG_TUD_MODEL " MTP")
+#define CFG_MTP_STORAGE_ID_COUNT 1
+
+#ifdef __cplusplus
+ }
+#endif
+
+#endif /* _TUSB_CONFIG_H_ */
diff --git a/examples/device/mtp/src/usb_descriptors.c b/examples/device/mtp/src/usb_descriptors.c
new file mode 100644
index 000000000..73685c3fd
--- /dev/null
+++ b/examples/device/mtp/src/usb_descriptors.c
@@ -0,0 +1,193 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2019 Ha Thach (tinyusb.org)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "bsp/board_api.h"
+#include "tusb.h"
+
+/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug.
+ * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC.
+ *
+ * Auto ProductID layout's Bitmap:
+ * [MSB] MTP | VENDOR | MIDI | HID | MSC | CDC [LSB]
+ */
+#define _PID_MAP(itf, n) ( (CFG_TUD_##itf) << (n) )
+#define USB_PID (0x4000 | _PID_MAP(CDC, 0) | _PID_MAP(MSC, 1) | _PID_MAP(HID, 2) | \
+ _PID_MAP(MIDI, 3) | _PID_MAP(VENDOR, 4) | _PID_MAP(MTP, 5))
+
+#define USB_VID 0xCafe
+#define USB_BCD 0x0200
+
+//--------------------------------------------------------------------+
+// Device Descriptors
+//--------------------------------------------------------------------+
+tusb_desc_device_t const desc_device =
+{
+ .bLength = sizeof(tusb_desc_device_t),
+ .bDescriptorType = TUSB_DESC_DEVICE,
+ .bcdUSB = USB_BCD,
+ .bDeviceClass = TUSB_CLASS_UNSPECIFIED,
+ .bDeviceSubClass = 0x00,
+ .bDeviceProtocol = 0x00,
+ .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
+
+ .idVendor = USB_VID,
+ .idProduct = USB_PID,
+ .bcdDevice = 0x0100,
+
+ .iManufacturer = 0x01,
+ .iProduct = 0x02,
+ .iSerialNumber = 0x03,
+
+ .bNumConfigurations = 0x01
+};
+
+// Invoked when received GET DEVICE DESCRIPTOR
+// Application return pointer to descriptor
+uint8_t const *tud_descriptor_device_cb(void)
+{
+ return (uint8_t const *) &desc_device;
+}
+
+//--------------------------------------------------------------------+
+// Configuration Descriptor
+//--------------------------------------------------------------------+
+
+enum
+{
+ ITF_NUM_MTP = 0,
+ ITF_NUM_TOTAL
+};
+
+#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX
+ // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number
+ // 0 control, 1 In, 2 Bulk, 3 Iso, 4 In, 5 Bulk etc ...
+ #define EPNUM_MTP_EVT 0x81
+ #define EPNUM_MTP_OUT 0x02
+ #define EPNUM_MTP_IN 0x82
+
+#elif CFG_TUSB_MCU == OPT_MCU_CXD56
+ // CXD56 USB driver has fixed endpoint type (bulk/interrupt/iso) and direction (IN/OUT) by its number
+ // 0 control (IN/OUT), 1 Bulk (IN), 2 Bulk (OUT), 3 In (IN), 4 Bulk (IN), 5 Bulk (OUT), 6 In (IN)
+ #define EPNUM_MTP_EVT 0x83
+ #define EPNUM_MTP_OUT 0x02
+ #define EPNUM_MTP_IN 0x81
+
+#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY)
+ // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h
+ // e.g EP1 OUT & EP1 IN cannot exist together
+ #define EPNUM_MTP_EVT 0x81
+ #define EPNUM_MTP_OUT 0x03
+ #define EPNUM_MTP_IN 0x82
+
+#else
+ #define EPNUM_MTP_EVT 0x81
+ #define EPNUM_MTP_OUT 0x02
+ #define EPNUM_MTP_IN 0x82
+#endif
+
+#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MTP_DESC_LEN)
+
+uint8_t const desc_fs_configuration[] =
+{
+ // Config number, interface count, string index, total length, attribute, power in mA
+ TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100),
+ TUD_MTP_DESCRIPTOR(ITF_NUM_MTP, 4, EPNUM_MTP_EVT, CFG_MTP_EVT_EP_SIZE, CFG_MTP_EVT_INTERVAL, EPNUM_MTP_OUT, EPNUM_MTP_IN, CFG_MTP_EP_SIZE),
+};
+
+// Invoked when received GET CONFIGURATION DESCRIPTOR
+// Application return pointer to descriptor
+// Descriptor contents must exist long enough for transfer to complete
+uint8_t const *tud_descriptor_configuration_cb(uint8_t index)
+{
+ (void) index; // for multiple configurations
+ return desc_fs_configuration;
+}
+
+//--------------------------------------------------------------------+
+// String Descriptors
+//--------------------------------------------------------------------+
+
+// String Descriptor Index
+enum {
+ STRID_LANGID = 0,
+ STRID_MANUFACTURER,
+ STRID_PRODUCT,
+ STRID_SERIAL,
+ STRID_MTP,
+};
+
+// array of pointer to string descriptors
+char const *string_desc_arr[] =
+{
+ (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409)
+ CFG_TUD_MANUFACTURER, // 1: Manufacturer
+ CFG_TUD_MODEL, // 2: Product
+ NULL, // 3: Serials will use unique ID if possible
+ CFG_MTP_INTERFACE, // 4: MTP Interface
+};
+
+static uint16_t _desc_str[32 + 1];
+
+// Invoked when received GET STRING DESCRIPTOR request
+// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete
+uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
+ (void) langid;
+ size_t chr_count;
+
+ switch ( index ) {
+ case STRID_LANGID:
+ memcpy(&_desc_str[1], string_desc_arr[0], 2);
+ chr_count = 1;
+ break;
+
+ case STRID_SERIAL:
+ chr_count = board_usb_get_serial(_desc_str + 1, 32);
+ break;
+
+ default:
+ // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors.
+ // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors
+
+ if ( !(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0])) ) return NULL;
+
+ const char *str = string_desc_arr[index];
+
+ // Cap at max char
+ chr_count = strlen(str);
+ size_t const max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1; // -1 for string type
+ if ( chr_count > max_count ) chr_count = max_count;
+
+ // Convert ASCII string into UTF-16
+ for ( size_t i = 0; i < chr_count; i++ ) {
+ _desc_str[1 + i] = str[i];
+ }
+ break;
+ }
+
+ // first byte is length (including header), second byte is string type
+ _desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
+
+ return _desc_str;
+}
diff --git a/hw/bsp/espressif/components/tinyusb_src/CMakeLists.txt b/hw/bsp/espressif/components/tinyusb_src/CMakeLists.txt
index 00e288bad..beabcad9c 100644
--- a/hw/bsp/espressif/components/tinyusb_src/CMakeLists.txt
+++ b/hw/bsp/espressif/components/tinyusb_src/CMakeLists.txt
@@ -37,6 +37,7 @@ list(APPEND srcs
${tusb_src}/class/hid/hid_device.c
${tusb_src}/class/midi/midi_device.c
${tusb_src}/class/msc/msc_device.c
+ ${tusb_src}/class/mtp/mtp_device.c
${tusb_src}/class/net/ecm_rndis_device.c
${tusb_src}/class/net/ncm_device.c
${tusb_src}/class/usbtmc/usbtmc_device.c
diff --git a/hw/bsp/rp2040/family.cmake b/hw/bsp/rp2040/family.cmake
index 2182e9ad1..3bec5bf70 100644
--- a/hw/bsp/rp2040/family.cmake
+++ b/hw/bsp/rp2040/family.cmake
@@ -95,6 +95,7 @@ target_sources(tinyusb_device_base INTERFACE
${TOP}/src/class/hid/hid_device.c
${TOP}/src/class/midi/midi_device.c
${TOP}/src/class/msc/msc_device.c
+ ${TOP}/src/class/mtp/mtp_device.c
${TOP}/src/class/net/ecm_rndis_device.c
${TOP}/src/class/net/ncm_device.c
${TOP}/src/class/usbtmc/usbtmc_device.c
diff --git a/lib/rt-thread/SConscript b/lib/rt-thread/SConscript
index 34399fd45..99517a090 100644
--- a/lib/rt-thread/SConscript
+++ b/lib/rt-thread/SConscript
@@ -34,6 +34,8 @@ if GetDepend(["PKG_TINYUSB_DEVICE_ENABLE"]):
src += ["../../src/class/cdc/cdc_device.c"]
if GetDepend(["PKG_TINYUSB_DEVICE_MSC"]):
src += ["../../src/class/msc/msc_device.c", "port/msc_device_port.c"]
+ if GetDepend(["PKG_TINYUSB_DEVICE_MTP"]):
+ src += ["../../src/class/mtp/mtp_device.c"]
if GetDepend(["PKG_TINYUSB_DEVICE_HID"]):
src += ["../../src/class/hid/hid_device.c"]
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 55c52033c..9918407b3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -19,6 +19,7 @@ function(tinyusb_target_add TARGET)
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/hid/hid_device.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi_device.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/msc/msc_device.c
+ ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/mtp/mtp_device.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/net/ecm_rndis_device.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/net/ncm_device.c
${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/usbtmc/usbtmc_device.c
diff --git a/src/class/mtp/mtp.h b/src/class/mtp/mtp.h
new file mode 100644
index 000000000..33c6bb552
--- /dev/null
+++ b/src/class/mtp/mtp.h
@@ -0,0 +1,438 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Ennebi Elettronica (https://ennebielettronica.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * This file is part of the TinyUSB stack.
+ */
+
+#ifndef _TUSB_MTP_H_
+#define _TUSB_MTP_H_
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <time.h>
+#include "tusb_option.h"
+
+#if (CFG_TUD_ENABLED && CFG_TUD_MTP)
+
+#ifdef __cplusplus
+ extern "C" {
+#endif
+
+#define TU_ARRAY_LEN(a) (sizeof(a)/sizeof(a[0]))
+#define STORAGE_ID(physical_id, logical_id) ( (((uint32_t)physical_id & 0xFFFF) << 16) | ((uint32_t)logical_id & 0x0000FFFF) )
+typedef uint16_t wchar16_t;
+
+//--------------------------------------------------------------------+
+// Media Transfer Protocol Class Constant
+//--------------------------------------------------------------------+
+
+// Media Transfer Protocol Subclass
+typedef enum
+{
+ MTP_SUBCLASS = 1
+} mtp_subclass_type_t;
+
+// MTP Protocol.
+typedef enum
+{
+ MTP_PROTOCOL_STILL_IMAGE = 1,
+} mtp_protocol_type_t;
+
+// PTP/MTP protocol phases
+typedef enum
+{
+ MTP_PHASE_IDLE = 0,
+ MTP_PHASE_COMMAND,
+ MTP_PHASE_DATA_IN,
+ MTP_PHASE_DATA_OUT,
+ MTP_PHASE_RESPONSE,
+ MTP_PHASE_ERROR,
+ MTP_PHASE_NONE,
+} mtp_phase_type_t;
+
+// PTP/MTP Class requests
+typedef enum
+{
+ MTP_REQ_CANCEL = 0x64,
+ MTP_REQ_GET_EXT_EVENT_DATA = 0x65,
+ MTP_REQ_RESET = 0x66,
+ MTP_REQ_GET_DEVICE_STATUS = 0x67,
+} mtp_class_request_t;
+
+#define MTP_GENERIC_DATA_BLOCK_LENGTH 12
+#define MTP_MAX_PACKET_SIZE 512
+
+// PTP/MTP Generic container
+typedef struct TU_ATTR_PACKED
+{
+ uint32_t container_length;
+ uint16_t container_type;
+ uint16_t code;
+ uint32_t transaction_id;
+ uint32_t data[MTP_MAX_PACKET_SIZE / sizeof(uint32_t)];
+} mtp_generic_container_t;
+
+// PTP/MTP Container type
+typedef enum
+{
+ MTP_CONTAINER_TYPE_UNDEFINED = 0,
+ MTP_CONTAINER_TYPE_COMMAND_BLOCK = 1,
+ MTP_CONTAINER_TYPE_DATA_BLOCK = 2,
+ MTP_CONTAINER_TYPE_RESPONSE_BLOCK = 3,
+ MTP_CONTAINER_TYPE_EVENT_BLOCK = 4,
+} mtp_container_type_t;
+
+// Supported OperationCode
+typedef enum
+{
+ MTP_OPEC_GET_DEVICE_INFO = 0x1001u,
+ MTP_OPEC_OPEN_SESSION = 0x1002u,
+ MTP_OPEC_CLOSE_SESSION = 0x1003u,
+ MTP_OPEC_GET_STORAGE_IDS = 0x1004u,
+ MTP_OPEC_GET_STORAGE_INFO = 0x1005u,
+ MTP_OPEC_GET_NUM_OBJECTS = 0x1006u,
+ MTP_OPEC_GET_OBJECT_HANDLES = 0x1007u,
+ MTP_OPEC_GET_OBJECT_INFO = 0x1008u,
+ MTP_OPEC_GET_OBJECT = 0x1009u,
+ MTP_OPEC_GET_THUMB = 0x100Au,
+ MTP_OPEC_DELETE_OBJECT = 0x100Bu,
+ MTP_OPEC_SEND_OBJECT_INFO = 0x100Cu,
+ MTP_OPEC_SEND_OBJECT = 0x100Du,
+ MTP_OPEC_INITIAL_CAPTURE = 0x100Eu,
+ MTP_OPEC_FORMAT_STORE = 0x100Fu,
+ MTP_OPEC_RESET_DEVICE = 0x1010u,
+ MTP_OPEC_SELF_TEST = 0x1011u,
+ MTP_OPEC_SET_OBJECT_PROTECTION = 0x1012u,
+ MTP_OPEC_POWER_DOWN = 0x1013u,
+ MTP_OPEC_GET_DEVICE_PROP_DESC = 0x1014u,
+ MTP_OPEC_GET_DEVICE_PROP_VALUE = 0x1015u,
+ MTP_OPEC_SET_DEVICE_PROP_VALUE = 0x1016u,
+ MTP_OPEC_RESET_DEVICE_PROP_VALUE = 0x1017u,
+ MTP_OPEC_TERMINATE_OPEN_CAPTURE = 0x1018u,
+ MTP_OPEC_MOVE_OBJECT = 0x1019u,
+ MTP_OPEC_COPY_OBJECT = 0x101Au,
+ MTP_OPEC_GET_PARTIAL_OBJECT = 0x101Bu,
+ MTP_OPEC_INITIATE_OPEN_CAPTURE = 0x101Bu,
+ MTP_OPEC_GET_OBJECT_PROPS_SUPPORTED = 0x9801u,
+ MTP_OPEC_GET_OBJECT_PROP_DESC = 0x9802u,
+ MTP_OPEC_GET_OBJECT_PROP_VALUE = 0x9803u,
+ MTP_OPEC_SET_OBJECT_PROP_VALUE = 0x9804u,
+ MTP_OPEC_GET_OBJECT_PROPLIST = 0x9805u,
+ MTP_OPEC_GET_OBJECT_PROP_REFERENCES = 0x9810u,
+ MTP_OPEC_GETSERVICEIDS = 0x9301u,
+ MTP_OPEC_GETSERVICEINFO = 0x9302u,
+ MTP_OPEC_GETSERVICECAPABILITIES = 0x9303u,
+ MTP_OPEC_GETSERVICEPROPDESC = 0x9304u,
+} mtp_operation_code_t;
+
+// Supported EventCode
+typedef enum
+{
+ MTP_EVTC_OBJECT_ADDED = 0x4002,
+} mtp_event_code_t;
+
+
+// Supported Device Properties
+typedef enum
+{
+ MTP_DEVP_UNDEFINED = 0x5000u,
+ MTP_DEVP_BATTERY_LEVEL = 0x5001u,
+ MTP_DEVP_DEVICE_FRIENDLY_NAME = 0xD402u,
+} mtp_event_properties_t;
+
+// Supported Object Properties
+typedef enum
+{
+ MTP_OBJP_STORAGE_ID = 0xDC01u,
+ MTP_OBJP_OBJECT_FORMAT = 0xDC02u,
+ MTP_OBJP_PROTECTION_STATUS = 0xDC03u,
+ MTP_OBJP_OBJECT_SIZE = 0xDC04u,
+ MTP_OBJP_ASSOCIATION_TYPE = 0xDC05u,
+ MTP_OBJP_OBJECT_FILE_NAME = 0xDC07u,
+ MTP_OBJP_PARENT_OBJECT = 0xDC0Bu,
+ MTP_OBJP_PERSISTENT_UNIQUE_OBJECT_IDENTIFIER = 0xDC41u,
+ MTP_OBJP_NAME = 0xDC44u,
+} mtp_object_properties_t;
+
+// Object formats
+typedef enum
+{
+ MTP_OBJF_UNDEFINED = 0x3000u,
+ MTP_OBJF_ASSOCIATION = 0x3001u,
+ MTP_OBJF_TEXT = 0x3004u,
+} mtp_object_formats_t;
+
+// Predefined Object handles
+typedef enum
+{
+ MTP_OBJH_ROOT = 0x0000,
+} mtp_object_handles_t;
+
+// Datatypes
+typedef enum
+{
+ MTP_TYPE_UNDEFINED = 0x0000u,
+ MTP_TYPE_INT8 = 0x0001u,
+ MTP_TYPE_UINT8 = 0x0002u,
+ MTP_TYPE_INT16 = 0x0003u,
+ MTP_TYPE_UINT16 = 0x0004u,
+ MTP_TYPE_INT32 = 0x0005u,
+ MTP_TYPE_UINT32 = 0x0006u,
+ MTP_TYPE_INT64 = 0x0007u,
+ MTP_TYPE_UINT64 = 0x0008u,
+ MTP_TYPE_STR = 0xFFFFu,
+} mtp_datatypes_t;
+
+// Get/Set
+typedef enum
+{
+ MTP_MODE_GET = 0x00u,
+ MTP_MODE_GET_SET = 0x01u,
+} mtp_mode_get_set_t;
+
+tu_static const uint16_t mtp_operations_supported[] = {
+ MTP_OPEC_GET_DEVICE_INFO,
+ MTP_OPEC_OPEN_SESSION,
+ MTP_OPEC_CLOSE_SESSION,
+ MTP_OPEC_GET_STORAGE_IDS,
+ MTP_OPEC_GET_STORAGE_INFO,
+ MTP_OPEC_GET_NUM_OBJECTS,
+ MTP_OPEC_GET_OBJECT_HANDLES,
+ MTP_OPEC_GET_OBJECT_INFO,
+ MTP_OPEC_GET_OBJECT,
+ MTP_OPEC_DELETE_OBJECT,
+ MTP_OPEC_SEND_OBJECT_INFO,
+ MTP_OPEC_SEND_OBJECT,
+ MTP_OPEC_FORMAT_STORE,
+ MTP_OPEC_RESET_DEVICE,
+ MTP_OPEC_GET_DEVICE_PROP_DESC,
+ MTP_OPEC_GET_DEVICE_PROP_VALUE,
+ MTP_OPEC_SET_DEVICE_PROP_VALUE,
+};
+
+tu_static const uint16_t mtp_events_supported[] = {
+ MTP_EVTC_OBJECT_ADDED,
+};
+
+tu_static const uint16_t mtp_device_properties_supported[] = {
+ MTP_DEVP_DEVICE_FRIENDLY_NAME,
+};
+
+tu_static const uint16_t mtp_capture_formats[] = {
+ MTP_OBJF_UNDEFINED,
+ MTP_OBJF_ASSOCIATION,
+ MTP_OBJF_TEXT,
+};
+
+tu_static const uint16_t mtp_playback_formats[] = {
+ MTP_OBJF_UNDEFINED,
+ MTP_OBJF_ASSOCIATION,
+ MTP_OBJF_TEXT,
+};
+
+//--------------------------------------------------------------------+
+// Data structures
+//--------------------------------------------------------------------+
+
+// DeviceInfo Dataset
+#define MTP_EXTENSIONS "microsoft.com: 1.0; "
+typedef struct TU_ATTR_PACKED {
+ uint16_t standard_version;
+ uint32_t mtp_vendor_extension_id;
+ uint16_t mtp_version;
+ uint8_t mtp_extensions_len;
+ wchar16_t mtp_extensions[TU_ARRAY_LEN(MTP_EXTENSIONS)] TU_ATTR_PACKED;
+
+ uint16_t functional_mode;
+ /* Operations supported */
+ uint32_t operations_supported_len;
+ uint16_t operations_supported[TU_ARRAY_LEN(mtp_operations_supported)] TU_ATTR_PACKED;
+ /* Events supported */
+ uint32_t events_supported_len;
+ uint16_t events_supported[TU_ARRAY_LEN(mtp_events_supported)] TU_ATTR_PACKED;
+ /* Device properties supported */
+ uint32_t device_properties_supported_len;
+ uint16_t device_properties_supported[TU_ARRAY_LEN(mtp_device_properties_supported)] TU_ATTR_PACKED;
+ /* Capture formats */
+ uint32_t capture_formats_len;
+ uint16_t capture_formats[TU_ARRAY_LEN(mtp_capture_formats)] TU_ATTR_PACKED;
+ /* Playback formats */
+ uint32_t playback_formats_len;
+ uint16_t playback_formats[TU_ARRAY_LEN(mtp_playback_formats)] TU_ATTR_PACKED;
+} mtp_device_info_t;
+// The following fields will be dynamically added to the struct at runtime:
+// - wstring manufacturer
+// - wstring model
+// - wstring device_version
+// - wstring serial_number
+
+
+#define MTP_STRING_DEF(name, string) \
+ uint8_t name##_len; \
+ wchar16_t name[TU_ARRAY_LEN(string)];
+
+#define MTP_ARRAY_DEF(name, array) \
+ uint16_t name##_len; \
+ typeof(name) name[TU_ARRAY_LEN(array)];
+
+// StorageInfo dataset
+typedef struct TU_ATTR_PACKED {
+ uint16_t storage_type;
+ uint16_t filesystem_type;
+ uint16_t access_capability;
+ uint64_t max_capacity_in_bytes;
+ uint64_t free_space_in_bytes;
+ uint32_t free_space_in_objects;
+} mtp_storage_info_t;
+// The following fields will be dynamically added to the struct at runtime:
+// - wstring storage_description
+// - wstring volume_identifier
+
+// ObjectInfo Dataset
+typedef struct TU_ATTR_PACKED {
+ uint32_t storage_id;
+ uint16_t object_format;
+ uint16_t protection_status;
+ uint32_t object_compressed_size;
+ uint16_t thumb_format; // unused
+ uint32_t thumb_compressed_size; // unused
+ uint32_t thumb_pix_width; // unused
+ uint32_t thumb_pix_height; // unused
+ uint32_t image_pix_width; // unused
+ uint32_t image_pix_height; // unused
+ uint32_t image_bit_depth; // unused
+ uint32_t parent_object; // 0: root
+ uint16_t association_type;
+ uint32_t association_description; // not used
+ uint32_t sequence_number; // not used
+} mtp_object_info_t;
+// The following fields will be dynamically added to the struct at runtime:
+// - wstring filename;
+// - datetime_wstring date_created;
+// - datetime_wstring date_modified;
+// - wstring keywords;
+
+// Storage IDs
+typedef struct TU_ATTR_PACKED {
+ uint32_t storage_ids_len;
+ uint32_t storage_ids[CFG_MTP_STORAGE_ID_COUNT];
+} mtp_storage_ids_t;
+
+// DevicePropDesc Dataset
+typedef struct TU_ATTR_PACKED {
+ uint16_t device_property_code;
+ uint16_t datatype;
+ uint8_t get_set;
+} mtp_device_prop_desc_t;
+// The following fields will be dynamically added to the struct at runtime:
+// - wstring factory_def_value;
+// - wstring current_value_len;
+// - uint8_t form_flag;
+
+typedef struct TU_ATTR_PACKED {
+ uint16_t wLength;
+ uint16_t code;
+} mtp_device_status_res_t;
+
+typedef struct TU_ATTR_PACKED {
+ uint32_t object_handle;
+ uint32_t storage_id;
+ uint32_t parent_object_handle;
+} mtp_basic_object_info_t;
+
+//--------------------------------------------------------------------+
+// Definitions
+//--------------------------------------------------------------------+
+
+typedef enum {
+ MTP_STORAGE_TYPE_UNDEFINED = 0x0000u,
+ MTP_STORAGE_TYPE_FIXED_ROM = 0x0001u,
+ MTP_STORAGE_TYPE_REMOVABLE_ROM = 0x0002u,
+ MTP_STORAGE_TYPE_FIXED_RAM = 0x0003u,
+ MTP_STORAGE_TYPE_REMOVABLE_RAM = 0x0004u,
+} mtp_storage_type_t;
+
+typedef enum {
+ MTP_FILESYSTEM_TYPE_UNDEFINED = 0x0000u,
+ MTP_FILESYSTEM_TYPE_GENERIC_FLAT = 0x0001u,
+ MTP_FILESYSTEM_TYPE_GENERIC_HIERARCHICAL = 0x0002u,
+ MTP_FILESYSTEM_TYPE_DCF = 0x0003u,
+} mtp_filesystem_type_t;
+
+typedef enum {
+ MTP_ACCESS_CAPABILITY_READ_WRITE = 0x0000u,
+ MTP_ACCESS_CAPABILITY_READ_ONLY_WITHOUT_OBJECT_DELETION = 0x0001u,
+ MTP_ACCESS_CAPABILITY_READ_ONLY_WITH_OBJECT_DELETION = 0x0002u,
+} mtp_access_capability_t;
+
+typedef enum {
+ MTP_PROTECTION_STATUS_NO_PROTECTION = 0x0000u,
+ MTP_PROTECTION_STATUS_READ_ONLY = 0x0001u,
+ MTP_PROTECTION_STATUS_READ_ONLY_DATA = 0x8002u,
+ MTP_PROTECTION_NON_TRANSFERABLE_DATA = 0x8003u,
+} mtp_protection_status_t;
+
+typedef enum {
+ MTP_ASSOCIATION_UNDEFINED = 0x0000u,
+ MTP_ASSOCIATION_GENERIC_FOLDER = 0x0001u,
+ MTP_ASSOCIATION_GENERIC_ALBUM = 0x0002u,
+ MTP_ASSOCIATION_TIME_SEQUENCE = 0x0003u,
+ MTP_ASSOCIATION_HORIZONTAL_PANORAMIC = 0x0004u,
+ MTP_ASSOCIATION_VERTICAL_PANORAMIC = 0x0005u,
+ MTP_ASSOCIATION_2D_PANORAMIC = 0x0006u,
+} mtp_association_t;
+
+// Responses
+typedef enum {
+// Supported ResponseCode
+ MTP_RESC_UNDEFINED = 0x2000u,
+ MTP_RESC_OK = 0x2001u,
+ MTP_RESC_GENERAL_ERROR = 0x2002u,
+ MTP_RESC_SESSION_NOT_OPEN = 0x2003u,
+ MTP_RESC_INVALID_TRANSACTION_ID = 0x2004u,
+ MTP_RESC_OPERATION_NOT_SUPPORTED = 0x2005u,
+ MTP_RESC_PARAMETER_NOT_SUPPORTED = 0x2006u,
+ MTP_RESC_INCOMPLETE_TRANSFER = 0x2007u,
+ MTP_RESC_INVALID_STORAGE_ID = 0x2008u,
+ MTP_RESC_INVALID_OBJECT_HANDLE = 0x2009u,
+ MTP_RESC_STORE_FULL = 0x200Cu,
+ MTP_RESC_OBJECT_WRITE_PROTECTED = 0x200Du,
+ MTP_RESC_STORE_NOT_AVAILABLE = 0x2013u,
+ MTP_RESC_SPECIFICATION_BY_FORMAT_UNSUPPORTED = 0x2014u,
+ MTP_RESC_NO_VALID_OBJECTINFO = 0x2015u,
+ MTP_RESC_DEVICE_BUSY = 0x2019u,
+ MTP_RESC_INVALID_PARENT_OBJECT = 0x201Au,
+ MTP_RESC_INVALID_DEVICE_PROP_FORMAT = 0x201Bu,
+ MTP_RESC_INVALID_DEVICE_PROP_VALUE = 0x201Cu,
+ MTP_RESC_INVALID_PARAMETER = 0x201Du,
+ MTP_RESC_SESSION_ALREADY_OPEN = 0x201Eu,
+ MTP_RESC_TRANSACTION_CANCELLED = 0x201Fu,
+} mtp_response_t;
+
+#ifdef __cplusplus
+ }
+#endif
+
+#endif /* CFG_TUD_ENABLED && CFG_TUD_MTP */
+
+#endif /* _TUSB_MTP_H_ */
diff --git a/src/class/mtp/mtp_device.c b/src/class/mtp/mtp_device.c
new file mode 100644
index 000000000..854d4f10e
--- /dev/null
+++ b/src/class/mtp/mtp_device.c
@@ -0,0 +1,997 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Ennebi Elettronica (https://ennebielettronica.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * This file is part of the TinyUSB stack.
+ */
+
+#include "tusb_option.h"
+
+#if (CFG_TUD_ENABLED && CFG_TUD_MTP)
+
+//--------------------------------------------------------------------+
+// INCLUDE
+//--------------------------------------------------------------------+
+#include "device/dcd.h" // for faking dcd_event_xfer_complete
+#include "device/usbd.h"
+#include "device/usbd_pvt.h"
+
+#include "mtp_device.h"
+#include "mtp_device_storage.h"
+
+// Level where CFG_TUSB_DEBUG must be at least for this driver is logged
+#ifndef CFG_TUD_MTP_LOG_LEVEL
+ #define CFG_TUD_MTP_LOG_LEVEL CFG_TUD_LOG_LEVEL
+#endif
+
+#define TU_LOG_DRV(...) TU_LOG(CFG_TUD_MTP_LOG_LEVEL, __VA_ARGS__)
+
+//--------------------------------------------------------------------+
+// STRUCT
+//--------------------------------------------------------------------+
+typedef struct
+{
+ uint8_t itf_num;
+ uint8_t ep_in;
+ uint8_t ep_out;
+ uint8_t ep_evt;
+
+ // Bulk Only Transfer (BOT) Protocol
+ uint8_t phase;
+
+ uint32_t queued_len; // number of bytes queued from the DataIN Stage
+ uint32_t total_len; // byte to be transferred, can be smaller than total_bytes in cbw
+ uint32_t xferred_len; // number of bytes transferred so far in the Data Stage
+ uint32_t handled_len; // number of bytes already handled in the Data Stage
+ bool xfer_completed; // true when DATA-IN/DATA-OUT transfer is completed
+
+} mtpd_interface_t;
+
+typedef struct
+{
+ uint32_t session_id;
+ uint32_t transaction_id;
+} mtpd_context_t;
+
+//--------------------------------------------------------------------+
+// INTERNAL FUNCTION DECLARATION
+//--------------------------------------------------------------------+
+// Checker
+tu_static mtp_phase_type_t mtpd_chk_generic(const char *func_name, const bool err_cd, const uint16_t ret_code, const char *message);
+tu_static mtp_phase_type_t mtpd_chk_session_open(const char *func_name);
+
+// MTP commands
+tu_static mtp_phase_type_t mtpd_handle_cmd(void);
+tu_static mtp_phase_type_t mtpd_handle_data(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_get_device_info(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_open_session(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_close_session(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_get_storage_info(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_get_storage_ids(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_get_object_handles(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_get_object_info(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_get_object(void);
+tu_static mtp_phase_type_t mtpd_handle_dti_get_object(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_delete_object(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_get_device_prop_desc(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_get_device_prop_value(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_send_object_info(void);
+tu_static mtp_phase_type_t mtpd_handle_dto_send_object_info(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_send_object(void);
+tu_static mtp_phase_type_t mtpd_handle_dto_send_object(void);
+tu_static mtp_phase_type_t mtpd_handle_cmd_format_store(void);
+
+//--------------------------------------------------------------------+
+// MTP variable declaration
+//--------------------------------------------------------------------+
+CFG_TUD_MEM_SECTION CFG_TUSB_MEM_ALIGN tu_static mtpd_interface_t _mtpd_itf;
+CFG_TUD_MEM_SECTION CFG_TUSB_MEM_ALIGN tu_static mtp_generic_container_t _mtpd_gct;
+CFG_TUD_MEM_SECTION CFG_TUSB_MEM_ALIGN tu_static mtpd_context_t _mtpd_ctx;
+CFG_TUD_MEM_SECTION CFG_TUSB_MEM_ALIGN tu_static mtp_device_status_res_t _mtpd_device_status_res;
+CFG_TUD_MEM_SECTION CFG_TUSB_MEM_ALIGN tu_static uint32_t _mtpd_get_object_handle;
+CFG_TUD_MEM_SECTION CFG_TUSB_MEM_ALIGN tu_static mtp_basic_object_info_t _mtpd_soi;
+CFG_TUD_MEM_SECTION CFG_TUSB_MEM_ALIGN tu_static char _mtp_datestr[20];
+
+//--------------------------------------------------------------------+
+// USBD Driver API
+//--------------------------------------------------------------------+
+void mtpd_init(void) {
+ TU_LOG_DRV(" MTP mtpd_init\n");
+ tu_memclr(&_mtpd_itf, sizeof(mtpd_interface_t));
+ tu_memclr(&_mtpd_ctx, sizeof(mtpd_context_t));
+ _mtpd_get_object_handle = 0;
+ tu_memclr(&_mtpd_soi, sizeof(mtp_basic_object_info_t));
+}
+
+bool mtpd_deinit(void) {
+ TU_LOG_DRV(" MTP mtpd_deinit\n");
+ // nothing to do
+ return true;
+}
+
+void mtpd_reset(uint8_t rhport)
+{
+ TU_LOG_DRV(" MTP mtpd_reset\n");
+ (void) rhport;
+
+ // Close all endpoints
+ dcd_edpt_close_all(rhport);
+ tu_memclr(&_mtpd_itf, sizeof(mtpd_interface_t));
+ tu_memclr(&_mtpd_ctx, sizeof(mtpd_context_t));
+ tu_memclr(&_mtpd_gct, sizeof(mtp_generic_container_t));
+ _mtpd_get_object_handle = 0;
+ tu_memclr(&_mtpd_soi, sizeof(mtp_basic_object_info_t));
+}
+
+uint16_t mtpd_open(uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16_t max_len)
+{
+ TU_LOG_DRV(" MTP mtpd_open\n");
+ tusb_desc_endpoint_t const *ep_desc;
+ // only support SCSI's BOT protocol
+ TU_VERIFY(TUSB_CLASS_IMAGE == itf_desc->bInterfaceClass &&
+ MTP_SUBCLASS == itf_desc->bInterfaceSubClass &&
+ MTP_PROTOCOL_STILL_IMAGE == itf_desc->bInterfaceProtocol, 0);
+
+ // mtp driver length is fixed
+ uint16_t const mtpd_itf_size = sizeof(tusb_desc_interface_t) + 3 * sizeof(tusb_desc_endpoint_t);
+
+ // Max length must be at least 1 interface + 3 endpoints
+ TU_ASSERT(itf_desc->bNumEndpoints == 3 && max_len >= mtpd_itf_size);
+
+ _mtpd_itf.itf_num = itf_desc->bInterfaceNumber;
+
+ // Open interrupt IN endpoint
+ ep_desc = (tusb_desc_endpoint_t const *)tu_desc_next(itf_desc);
+ TU_ASSERT(ep_desc->bDescriptorType == TUSB_DESC_ENDPOINT && ep_desc->bmAttributes.xfer == TUSB_XFER_INTERRUPT, 0);
+ TU_ASSERT(usbd_edpt_open(rhport, ep_desc), 0);
+ _mtpd_itf.ep_evt = ep_desc->bEndpointAddress;
+
+ // Open endpoint pair
+ TU_ASSERT( usbd_open_edpt_pair(rhport, tu_desc_next(ep_desc), 2, TUSB_XFER_BULK, &_mtpd_itf.ep_out, &_mtpd_itf.ep_in), 0 );
+
+ // Prepare rx on bulk out EP
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)), CFG_MTP_EP_SIZE), 0);
+
+ return mtpd_itf_size;
+}
+
+// Invoked when a control transfer occurred on an interface of this class
+// Driver response accordingly to the request and the transfer stage (setup/data/ack)
+// return false to stall control endpoint (e.g unsupported request)
+bool mtpd_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request)
+{
+ TU_LOG_DRV(" MTP mtpd_control_xfer_cb: bmRequest=0x%2x, bRequest=0x%2x\n", request->bmRequestType, request->bRequest);
+ // nothing to do with DATA & ACK stage
+ if (stage != CONTROL_STAGE_SETUP) return true;
+
+ uint16_t len = 0;
+
+ switch ( request->bRequest )
+ {
+ case MTP_REQ_CANCEL:
+ TU_LOG_DRV(" MTP request: MTP_REQ_CANCEL\n");
+ tud_mtp_storage_cancel();
+ break;
+ case MTP_REQ_GET_EXT_EVENT_DATA:
+ TU_LOG_DRV(" MTP request: MTP_REQ_GET_EXT_EVENT_DATA\n");
+ break;
+ case MTP_REQ_RESET:
+ TU_LOG_DRV(" MTP request: MTP_REQ_RESET\n");
+ tud_mtp_storage_reset();
+ // Prepare for a new command
+ TU_ASSERT( usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)), CFG_MTP_EP_SIZE) );
+ break;
+ case MTP_REQ_GET_DEVICE_STATUS:
+ TU_LOG_DRV(" MTP request: MTP_REQ_GET_DEVICE_STATUS\n");
+ len = 4;
+ _mtpd_device_status_res.wLength = len;
+ // Cancel is synchronous, always answer OK
+ _mtpd_device_status_res.code = MTP_RESC_OK;
+ TU_ASSERT( tud_control_xfer(rhport, request, (uint8_t *)&_mtpd_device_status_res , len) );
+ break;
+
+ default:
+ TU_LOG_DRV(" MTP request: invalid request\r\n");
+ return false; // stall unsupported request
+ }
+ return true;
+}
+
+// Transfer on bulk endpoints
+bool mtpd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t xferred_bytes)
+{
+ const unsigned dir = tu_edpt_dir(ep_addr);
+
+ if (event != XFER_RESULT_SUCCESS)
+ return false;
+
+ // IN transfer completed
+ if (dir == TUSB_DIR_IN)
+ {
+ if (_mtpd_itf.phase == MTP_PHASE_RESPONSE)
+ {
+ // IN transfer completed, prepare for a new command
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)), CFG_MTP_EP_SIZE), 0);
+ _mtpd_itf.phase = MTP_PHASE_IDLE;
+ }
+ else if (_mtpd_itf.phase == MTP_PHASE_DATA_IN)
+ {
+ _mtpd_itf.xferred_len += xferred_bytes;
+ _mtpd_itf.handled_len = _mtpd_itf.xferred_len;
+
+ // Check if transfer completed
+ if (_mtpd_itf.xferred_len >= _mtpd_itf.total_len && (xferred_bytes == 0 || (xferred_bytes % CFG_MTP_EP_SIZE) != 0))
+ {
+ _mtpd_itf.phase = MTP_PHASE_RESPONSE;
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = MTP_RESC_OK;
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ _mtpd_gct.transaction_id = _mtpd_ctx.transaction_id;
+ if (_mtpd_ctx.session_id != 0)
+ {
+ _mtpd_gct.data[0] = _mtpd_ctx.session_id;
+ _mtpd_gct.container_length += sizeof(uint32_t);
+ }
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_in, ((uint8_t *)(&_mtpd_gct)), (uint16_t)_mtpd_gct.container_length), 0);
+ }
+ else
+ // Send next block of DATA
+ {
+ // Send Zero-Lenght Packet
+ if (_mtpd_itf.xferred_len == _mtpd_itf.total_len)
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_in, ((uint8_t *)(&_mtpd_gct.data)), 0 ));
+ }
+ else
+ {
+ _mtpd_itf.phase = mtpd_handle_data();
+ if (_mtpd_itf.phase == MTP_PHASE_RESPONSE)
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_in, ((uint8_t *)(&_mtpd_gct)), (uint16_t)_mtpd_gct.container_length));
+ else
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_in, ((uint8_t *)(&_mtpd_gct.data)), (uint16_t)_mtpd_itf.queued_len));
+ }
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ if (dir == TUSB_DIR_OUT)
+ {
+ if (_mtpd_itf.phase == MTP_PHASE_IDLE)
+ {
+ // A new command has been received. Ensure this is the last of the sequence.
+ _mtpd_itf.total_len = _mtpd_gct.container_length;
+ // Stall in case of unexpected block
+ if (_mtpd_gct.container_type != MTP_CONTAINER_TYPE_COMMAND_BLOCK)
+ {
+ return false;
+ }
+ _mtpd_itf.phase = MTP_PHASE_COMMAND;
+ _mtpd_itf.total_len = _mtpd_gct.container_length;
+ _mtpd_itf.xferred_len = xferred_bytes;
+ _mtpd_itf.handled_len = 0;
+ _mtpd_itf.xfer_completed = false;
+ TU_ASSERT(_mtpd_itf.total_len < sizeof(mtp_generic_container_t));
+ }
+
+ if (_mtpd_itf.phase == MTP_PHASE_COMMAND)
+ {
+ // A zero-length or a short packet termination is expected
+ if (xferred_bytes == CFG_MTP_EP_SIZE || (_mtpd_itf.total_len - _mtpd_itf.xferred_len) > 0 )
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)) + _mtpd_itf.xferred_len, (uint16_t)(_mtpd_itf.total_len - _mtpd_itf.xferred_len)));
+ }
+ else
+ {
+ // Handle command block
+ _mtpd_itf.phase = mtpd_handle_cmd();
+ if (_mtpd_itf.phase == MTP_PHASE_RESPONSE)
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_in, ((uint8_t *)(&_mtpd_gct)), (uint16_t)_mtpd_gct.container_length));
+ }
+ else if (_mtpd_itf.phase == MTP_PHASE_DATA_IN)
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_in, ((uint8_t *)(&_mtpd_gct)), (uint16_t)_mtpd_itf.queued_len));
+ _mtpd_itf.total_len = _mtpd_gct.container_length;
+ _mtpd_itf.xferred_len = 0;
+ _mtpd_itf.handled_len = 0;
+ _mtpd_itf.xfer_completed = false;
+ }
+ else if (_mtpd_itf.phase == MTP_PHASE_DATA_OUT)
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)), CFG_MTP_EP_SIZE), 0);
+ _mtpd_itf.xferred_len = 0;
+ _mtpd_itf.handled_len = 0;
+ _mtpd_itf.xfer_completed = false;
+ }
+ else
+ {
+ usbd_edpt_stall(rhport, _mtpd_itf.ep_out);
+ usbd_edpt_stall(rhport, _mtpd_itf.ep_in);
+ }
+ }
+ return true;
+ }
+
+ if (_mtpd_itf.phase == MTP_PHASE_DATA_OUT)
+ {
+ // First block of data
+ if (_mtpd_itf.xferred_len == 0)
+ {
+ _mtpd_itf.total_len = _mtpd_gct.container_length;
+ _mtpd_itf.handled_len = 0;
+ _mtpd_itf.xfer_completed = false;
+ }
+ _mtpd_itf.xferred_len += xferred_bytes;
+ // Stall in case of unexpected block
+ if (_mtpd_gct.container_type != MTP_CONTAINER_TYPE_DATA_BLOCK)
+ {
+ return false;
+ }
+
+ // A zero-length or a short packet termination
+ if (xferred_bytes < CFG_MTP_EP_SIZE)
+ {
+ _mtpd_itf.xfer_completed = true;
+ // Handle data block
+ _mtpd_itf.phase = mtpd_handle_data();
+ if (_mtpd_itf.phase == MTP_PHASE_DATA_IN || _mtpd_itf.phase == MTP_PHASE_RESPONSE)
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_in, ((uint8_t *)(&_mtpd_gct)), (uint16_t)_mtpd_gct.container_length));
+ }
+ else if (_mtpd_itf.phase == MTP_PHASE_DATA_OUT)
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)), CFG_MTP_EP_SIZE), 0);
+ _mtpd_itf.xferred_len = 0;
+ _mtpd_itf.xfer_completed = false;
+ }
+ else
+ {
+ usbd_edpt_stall(rhport, _mtpd_itf.ep_out);
+ usbd_edpt_stall(rhport, _mtpd_itf.ep_in);
+ }
+ }
+ else
+ {
+ // Handle data block when container is full
+ if (_mtpd_itf.xferred_len - _mtpd_itf.handled_len >= MTP_MAX_PACKET_SIZE - CFG_MTP_EP_SIZE)
+ {
+ _mtpd_itf.phase = mtpd_handle_data();
+ _mtpd_itf.handled_len = _mtpd_itf.xferred_len;
+ }
+ // Transfer completed: wait for zero-lenght packet
+ // Some platforms may not respect EP size and xferred_bytes may be more than CFG_MTP_EP_SIZE if
+ // the OUT EP is waiting for more data. Ensure we are not waiting for more than CFG_MTP_EP_SIZE.
+ if (_mtpd_itf.total_len == _mtpd_itf.xferred_len)
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct.data)), CFG_MTP_EP_SIZE), 0);
+ }
+ // First data block includes container header + container data
+ else if (_mtpd_itf.handled_len == 0)
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)) + _mtpd_itf.xferred_len, (uint16_t)TU_MIN(_mtpd_itf.total_len - _mtpd_itf.xferred_len, CFG_MTP_EP_SIZE)));
+ }
+ else
+ // Successive data block includes only container data
+ {
+ TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct.data)) + _mtpd_itf.xferred_len - _mtpd_itf.handled_len, (uint16_t)TU_MIN(_mtpd_itf.total_len - _mtpd_itf.xferred_len, CFG_MTP_EP_SIZE)));
+ }
+ }
+ }
+ }
+ return true;
+}
+
+//--------------------------------------------------------------------+
+// MTPD Internal functionality
+//--------------------------------------------------------------------+
+
+// Decode command and prepare response
+mtp_phase_type_t mtpd_handle_cmd(void)
+{
+ TU_ASSERT(_mtpd_gct.container_type == MTP_CONTAINER_TYPE_COMMAND_BLOCK);
+ _mtpd_ctx.transaction_id = _mtpd_gct.transaction_id;
+ if (_mtpd_gct.code != MTP_OPEC_SEND_OBJECT)
+ _mtpd_soi.object_handle = 0;
+
+ switch(_mtpd_gct.code)
+ {
+ case MTP_OPEC_GET_DEVICE_INFO:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_DEVICE_INFO\n");
+ return mtpd_handle_cmd_get_device_info();
+ case MTP_OPEC_OPEN_SESSION:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_OPEN_SESSION\n");
+ return mtpd_handle_cmd_open_session();
+ case MTP_OPEC_CLOSE_SESSION:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_CLOSE_SESSION\n");
+ return mtpd_handle_cmd_close_session();
+ case MTP_OPEC_GET_STORAGE_IDS:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_STORAGE_IDS\n");
+ return mtpd_handle_cmd_get_storage_ids();
+ case MTP_OPEC_GET_STORAGE_INFO:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_STORAGE_INFO for ID=%lu\n", _mtpd_gct.data[0]);
+ return mtpd_handle_cmd_get_storage_info();
+ case MTP_OPEC_GET_OBJECT_HANDLES:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_OBJECT_HANDLES\n");
+ return mtpd_handle_cmd_get_object_handles();
+ case MTP_OPEC_GET_OBJECT_INFO:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_OBJECT_INFO\n");
+ return mtpd_handle_cmd_get_object_info();
+ case MTP_OPEC_GET_OBJECT:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_OBJECT\n");
+ return mtpd_handle_cmd_get_object();
+ case MTP_OPEC_DELETE_OBJECT:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_DELETE_OBJECT\n");
+ return mtpd_handle_cmd_delete_object();
+ case MTP_OPEC_GET_DEVICE_PROP_DESC:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_DEVICE_PROP_DESC\n");
+ return mtpd_handle_cmd_get_device_prop_desc();
+ case MTP_OPEC_GET_DEVICE_PROP_VALUE:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_DEVICE_PROP_VALUE\n");
+ return mtpd_handle_cmd_get_device_prop_value();
+ case MTP_OPEC_SEND_OBJECT_INFO:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_SEND_OBJECT_INFO\n");
+ return mtpd_handle_cmd_send_object_info();
+ case MTP_OPEC_SEND_OBJECT:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_SEND_OBJECT\n");
+ return mtpd_handle_cmd_send_object();
+ case MTP_OPEC_FORMAT_STORE:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_FORMAT_STORE\n");
+ return mtpd_handle_cmd_format_store();
+ default:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_UNKNOWN_COMMAND %x!!!!\n", _mtpd_gct.code);
+ return false;
+ }
+ return true;
+}
+
+mtp_phase_type_t mtpd_handle_data(void)
+{
+ TU_ASSERT(_mtpd_gct.container_type == MTP_CONTAINER_TYPE_DATA_BLOCK);
+ _mtpd_ctx.transaction_id = _mtpd_gct.transaction_id;
+
+ switch(_mtpd_gct.code)
+ {
+ case MTP_OPEC_GET_OBJECT:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_GET_OBJECT-DATA_IN\n");
+ return mtpd_handle_dti_get_object();
+ case MTP_OPEC_SEND_OBJECT_INFO:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_SEND_OBJECT_INFO-DATA_OUT\n");
+ return mtpd_handle_dto_send_object_info();
+ case MTP_OPEC_SEND_OBJECT:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_SEND_OBJECT-DATA_OUT\n");
+ return mtpd_handle_dto_send_object();
+ default:
+ TU_LOG_DRV(" MTP command: MTP_OPEC_UNKNOWN_COMMAND %x!!!!\n", _mtpd_gct.code);
+ return false;
+ }
+ return true;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_get_device_info(void)
+{
+ TU_VERIFY_STATIC(sizeof(mtp_device_info_t) < MTP_MAX_PACKET_SIZE, "mtp_device_info_t shall fit in MTP_MAX_PACKET_SIZE");
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH + sizeof(mtp_device_info_t);
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_DATA_BLOCK;
+ _mtpd_gct.code = MTP_OPEC_GET_DEVICE_INFO;
+ mtp_device_info_t *d = (mtp_device_info_t *)_mtpd_gct.data;
+ d->standard_version = 100;
+ d->mtp_vendor_extension_id = 0x06;
+ d->mtp_version = 100;
+ d->mtp_extensions_len = TU_ARRAY_LEN(MTP_EXTENSIONS);
+ mtpd_wc16cpy((uint8_t *)d->mtp_extensions, MTP_EXTENSIONS);
+ d->functional_mode = 0x0000;
+ d->operations_supported_len = TU_ARRAY_LEN(mtp_operations_supported);
+ memcpy(d->operations_supported, mtp_operations_supported, sizeof(mtp_operations_supported));
+ d->events_supported_len = TU_ARRAY_LEN(mtp_events_supported);
+ memcpy(d->events_supported, mtp_events_supported, sizeof(mtp_events_supported));
+ d->device_properties_supported_len = TU_ARRAY_LEN(mtp_device_properties_supported);
+ memcpy(d->device_properties_supported, mtp_device_properties_supported, sizeof(mtp_device_properties_supported));
+ d->capture_formats_len = TU_ARRAY_LEN(mtp_capture_formats);
+ memcpy(d->capture_formats, mtp_capture_formats, sizeof(mtp_capture_formats));
+ d->playback_formats_len = TU_ARRAY_LEN(mtp_playback_formats);
+ memcpy(d->playback_formats, mtp_playback_formats, sizeof(mtp_playback_formats));
+ mtpd_gct_append_wstring(CFG_TUD_MANUFACTURER);
+ mtpd_gct_append_wstring(CFG_TUD_MODEL);
+ mtpd_gct_append_wstring(CFG_MTP_DEVICE_VERSION);
+ mtpd_gct_append_wstring(CFG_MTP_SERIAL_NUMBER);
+
+ _mtpd_itf.queued_len = _mtpd_gct.container_length;
+ return MTP_PHASE_DATA_IN;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_open_session(void)
+{
+ uint32_t session_id = _mtpd_gct.data[0];
+
+ mtp_response_t res = tud_mtp_storage_open_session(&session_id);
+ if (res == MTP_RESC_SESSION_ALREADY_OPEN)
+ {
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = res;
+ _mtpd_gct.container_length += sizeof(_mtpd_gct.data[0]);
+ _mtpd_gct.data[0] = session_id;
+ _mtpd_ctx.session_id = session_id;
+ return MTP_PHASE_RESPONSE;
+ }
+
+ mtp_phase_type_t phase;
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+
+ _mtpd_ctx.session_id = session_id;
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = MTP_RESC_OK;
+
+ return MTP_PHASE_RESPONSE;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_close_session(void)
+{
+ uint32_t session_id = _mtpd_gct.data[0];
+
+ mtp_response_t res = tud_mtp_storage_close_session(session_id);
+
+ _mtpd_ctx.session_id = session_id;
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = res;
+
+ return MTP_PHASE_RESPONSE;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_get_storage_ids(void)
+{
+ TU_VERIFY_STATIC(sizeof(mtp_storage_ids_t) < MTP_MAX_PACKET_SIZE, "mtp_storage_ids_t shall fit in MTP_MAX_PACKET_SIZE");
+
+ uint32_t storage_id;
+ mtp_response_t res = tud_mtp_get_storage_id(&storage_id);
+ mtp_phase_type_t phase;
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH + sizeof(mtp_storage_ids_t);
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_DATA_BLOCK;
+ _mtpd_gct.code = MTP_OPEC_GET_STORAGE_IDS;
+ mtp_storage_ids_t *d = (mtp_storage_ids_t *)_mtpd_gct.data;
+ if (storage_id == 0)
+ {
+ // Storage not accessible
+ d->storage_ids_len = 0;
+ d->storage_ids[0] = 0;
+ }
+ else
+ {
+ d->storage_ids_len = 1;
+ d->storage_ids[0] = storage_id;
+ }
+
+ _mtpd_itf.queued_len = _mtpd_gct.container_length;
+ return MTP_PHASE_DATA_IN;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_get_storage_info(void)
+{
+ TU_VERIFY_STATIC(sizeof(mtp_storage_info_t) < MTP_MAX_PACKET_SIZE, "mtp_storage_info_t shall fit in MTP_MAX_PACKET_SIZE");
+
+ uint32_t storage_id = _mtpd_gct.data[0];
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH + sizeof(mtp_storage_info_t);
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_DATA_BLOCK;
+ _mtpd_gct.code = MTP_OPEC_GET_STORAGE_INFO;
+
+ mtp_response_t res = tud_mtp_get_storage_info(storage_id, (mtp_storage_info_t *)_mtpd_gct.data);
+ mtp_phase_type_t phase;
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+
+ _mtpd_itf.queued_len = _mtpd_gct.container_length;
+ return MTP_PHASE_DATA_IN;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_get_object_handles(void)
+{
+ uint32_t storage_id = _mtpd_gct.data[0];
+ uint32_t object_format_code = _mtpd_gct.data[1]; // optional, not managed
+ uint32_t parent_object_handle = _mtpd_gct.data[2]; // folder specification, 0xffffffff=objects with no parent
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH + sizeof(uint32_t);
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_DATA_BLOCK;
+ _mtpd_gct.code = MTP_OPEC_GET_OBJECT_HANDLES;
+ _mtpd_gct.data[0] = 0;
+
+ mtp_phase_type_t phase;
+ if ((phase = mtpd_chk_generic(__func__, (object_format_code != 0), MTP_RESC_SPECIFICATION_BY_FORMAT_UNSUPPORTED, "specification by format unsupported")) != MTP_PHASE_NONE) return phase;
+ //list of all object handles on all storages, not managed
+ if ((phase = mtpd_chk_generic(__func__, (storage_id == 0xFFFFFFFF), MTP_RESC_OPERATION_NOT_SUPPORTED, "list of all object handles on all storages unsupported")) != MTP_PHASE_NONE) return phase;
+
+ tud_mtp_storage_object_done();
+ uint32_t next_child_handle = 0;
+ while(true)
+ {
+ mtp_response_t res = tud_mtp_storage_association_get_object_handle(storage_id, parent_object_handle, &next_child_handle);
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+ if (next_child_handle == 0)
+ break;
+ mtpd_gct_append_object_handle(next_child_handle);
+ }
+ tud_mtp_storage_object_done();
+
+ _mtpd_itf.queued_len = _mtpd_gct.container_length;
+ return MTP_PHASE_DATA_IN;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_get_object_info(void)
+{
+ TU_VERIFY_STATIC(sizeof(mtp_object_info_t) < MTP_MAX_PACKET_SIZE, "mtp_object_info_t shall fit in MTP_MAX_PACKET_SIZE");
+
+ uint32_t object_handle = _mtpd_gct.data[0];
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH + sizeof(mtp_object_info_t);
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_DATA_BLOCK;
+ _mtpd_gct.code = MTP_OPEC_GET_OBJECT_INFO;
+ mtp_response_t res = tud_mtp_storage_object_read_info(object_handle, (mtp_object_info_t *)_mtpd_gct.data);
+ mtp_phase_type_t phase;
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+
+ _mtpd_itf.queued_len = _mtpd_gct.container_length;
+ return MTP_PHASE_DATA_IN;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_get_object(void)
+{
+ _mtpd_get_object_handle = _mtpd_gct.data[0];
+
+ // Continue with DATA-IN
+ return mtpd_handle_dti_get_object();
+}
+
+mtp_phase_type_t mtpd_handle_dti_get_object(void)
+{
+ mtp_response_t res;
+ mtp_phase_type_t phase;
+ uint32_t file_size = 0;
+ res = tud_mtp_storage_object_size(_mtpd_get_object_handle, &file_size);
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH + file_size;
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_DATA_BLOCK;
+ _mtpd_gct.code = MTP_OPEC_GET_OBJECT;
+
+ uint32_t buffer_size;
+ uint32_t read_count;
+ // Data block must be multiple of EP size
+ if (_mtpd_itf.handled_len == 0)
+ {
+ // First data block: include container header
+ buffer_size = ((MTP_MAX_PACKET_SIZE + MTP_GENERIC_DATA_BLOCK_LENGTH) / CFG_MTP_EP_SIZE) * CFG_MTP_EP_SIZE - MTP_GENERIC_DATA_BLOCK_LENGTH;
+ res = tud_mtp_storage_object_read(_mtpd_get_object_handle, (void *)&_mtpd_gct.data, buffer_size, &read_count);
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+ _mtpd_itf.queued_len = MTP_GENERIC_DATA_BLOCK_LENGTH + read_count;
+ }
+ else
+ {
+ // Successive data block: consider only container data
+ buffer_size = (MTP_MAX_PACKET_SIZE / CFG_MTP_EP_SIZE) * CFG_MTP_EP_SIZE;
+ res = tud_mtp_storage_object_read(_mtpd_get_object_handle, (void *)&_mtpd_gct.data, buffer_size, &read_count);
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+ _mtpd_itf.queued_len = read_count;
+ }
+
+ // File completed
+ if (read_count < buffer_size)
+ {
+ tud_mtp_storage_object_done();
+ }
+
+ return MTP_PHASE_DATA_IN;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_delete_object(void)
+{
+ uint32_t object_handle = _mtpd_gct.data[0];
+ uint32_t object_code_format = _mtpd_gct.data[1]; // not used
+ (void) object_code_format;
+
+ mtp_response_t res = tud_mtp_storage_object_delete(object_handle);
+ mtp_phase_type_t phase;
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = MTP_RESC_OK;
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ return MTP_PHASE_RESPONSE;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_get_device_prop_desc(void)
+{
+ uint32_t device_prop_code = _mtpd_gct.data[0];
+
+ mtp_phase_type_t rt;
+ if ((rt = mtpd_chk_session_open(__func__)) != MTP_PHASE_NONE) return rt;
+
+ switch(device_prop_code)
+ {
+ case MTP_DEVP_DEVICE_FRIENDLY_NAME:
+ {
+ TU_VERIFY_STATIC(sizeof(mtp_device_prop_desc_t) < MTP_MAX_PACKET_SIZE, "mtp_device_info_t shall fit in MTP_MAX_PACKET_SIZE");
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_DATA_BLOCK;
+ _mtpd_gct.code = MTP_OPEC_GET_DEVICE_PROP_DESC;
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH + sizeof(mtp_device_prop_desc_t);
+ mtp_device_prop_desc_t *d = (mtp_device_prop_desc_t *)_mtpd_gct.data;
+ d->device_property_code = (uint16_t)(device_prop_code);
+ d->datatype = MTP_TYPE_STR;
+ d->get_set = MTP_MODE_GET;
+ mtpd_gct_append_wstring(CFG_TUD_MODEL); // factory_def_value
+ mtpd_gct_append_wstring(CFG_TUD_MODEL); // current_value_len
+ mtpd_gct_append_uint8(0x00); // form_flag
+ _mtpd_itf.queued_len = _mtpd_gct.container_length;
+ return MTP_PHASE_DATA_IN;
+ }
+ default:
+ break;
+ }
+
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = MTP_RESC_PARAMETER_NOT_SUPPORTED;
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ return MTP_PHASE_RESPONSE;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_get_device_prop_value(void)
+{
+ uint32_t device_prop_code = _mtpd_gct.data[0];
+
+ mtp_phase_type_t rt;
+ if ((rt = mtpd_chk_session_open(__func__)) != MTP_PHASE_NONE) return rt;
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_DATA_BLOCK;
+ _mtpd_gct.code = MTP_OPEC_GET_DEVICE_PROP_VALUE;
+
+ switch(device_prop_code)
+ {
+ // TODO support more device properties
+ case MTP_DEVP_DEVICE_FRIENDLY_NAME:
+ mtpd_gct_append_wstring(CFG_TUD_MODEL);
+ _mtpd_itf.queued_len = _mtpd_gct.container_length;
+ return MTP_PHASE_DATA_IN;
+ default:
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = MTP_RESC_PARAMETER_NOT_SUPPORTED;
+ return MTP_PHASE_RESPONSE;
+ }
+}
+
+mtp_phase_type_t mtpd_handle_cmd_send_object_info(void)
+{
+ _mtpd_soi.storage_id = _mtpd_gct.data[0];
+ _mtpd_soi.parent_object_handle = (_mtpd_gct.data[1] == 0xFFFFFFFF ? 0 : _mtpd_gct.data[1]);
+
+ // Enter OUT phase and wait for DATA BLOCK
+ return MTP_PHASE_DATA_OUT;
+}
+
+mtp_phase_type_t mtpd_handle_dto_send_object_info(void)
+{
+ uint32_t new_object_handle = 0;
+ mtp_response_t res = tud_mtp_storage_object_write_info(_mtpd_soi.storage_id, _mtpd_soi.parent_object_handle, &new_object_handle, (mtp_object_info_t *)_mtpd_gct.data);
+ mtp_phase_type_t phase;
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+
+ // Save send_object_info
+ _mtpd_soi.object_handle = new_object_handle;
+
+ // Response
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH + 3 * sizeof(uint32_t);
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = MTP_RESC_OK;
+ _mtpd_gct.data[0] = _mtpd_soi.storage_id;
+ _mtpd_gct.data[1] = _mtpd_soi.parent_object_handle;
+ _mtpd_gct.data[2] = _mtpd_soi.object_handle;
+ return MTP_PHASE_RESPONSE;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_send_object(void)
+{
+ // Enter OUT phase and wait for DATA BLOCK
+ return MTP_PHASE_DATA_OUT;
+}
+
+mtp_phase_type_t mtpd_handle_dto_send_object(void)
+{
+ uint8_t *buffer = (uint8_t *)&_mtpd_gct.data;
+ uint32_t buffer_size = _mtpd_itf.xferred_len - _mtpd_itf.handled_len;
+ // First block of DATA
+ if (_mtpd_itf.handled_len == 0)
+ {
+ buffer_size -= MTP_GENERIC_DATA_BLOCK_LENGTH;
+ }
+
+ if (buffer_size > 0)
+ {
+ mtp_response_t res = tud_mtp_storage_object_write(_mtpd_soi.object_handle, buffer, buffer_size);
+ mtp_phase_type_t phase;
+ if ((phase = mtpd_chk_generic(__func__, (res != MTP_RESC_OK), res, "")) != MTP_PHASE_NONE) return phase;
+ }
+
+ if (!_mtpd_itf.xfer_completed)
+ {
+ // Continue with next DATA BLOCK
+ return MTP_PHASE_DATA_OUT;
+ }
+
+ // Send completed
+ tud_mtp_storage_object_done();
+
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = MTP_RESC_OK;
+ return MTP_PHASE_RESPONSE;
+}
+
+mtp_phase_type_t mtpd_handle_cmd_format_store(void)
+{
+ uint32_t storage_id = _mtpd_gct.data[0];
+ uint32_t file_system_format = _mtpd_gct.data[1]; // not used
+ (void) file_system_format;
+
+ mtp_response_t res = tud_mtp_storage_format(storage_id);
+
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = res;
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ return MTP_PHASE_RESPONSE;
+}
+
+//--------------------------------------------------------------------+
+// Checker
+//--------------------------------------------------------------------+
+mtp_phase_type_t mtpd_chk_session_open(const char *func_name)
+{
+ (void)func_name;
+ if (_mtpd_ctx.session_id == 0)
+ {
+ TU_LOG_DRV(" MTP error: %s session not open\n", func_name);
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = MTP_RESC_SESSION_NOT_OPEN;
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ return MTP_PHASE_RESPONSE;
+ }
+ return MTP_PHASE_NONE;
+}
+
+mtp_phase_type_t mtpd_chk_generic(const char *func_name, const bool err_cd, const uint16_t ret_code, const char *message)
+{
+ (void)func_name;
+ (void)message;
+ if (err_cd)
+ {
+ TU_LOG_DRV(" MTP error in %s: (%x) %s\n", func_name, ret_code, message);
+ _mtpd_gct.container_type = MTP_CONTAINER_TYPE_RESPONSE_BLOCK;
+ _mtpd_gct.code = ret_code;
+ _mtpd_gct.container_length = MTP_GENERIC_DATA_BLOCK_LENGTH;
+ return MTP_PHASE_RESPONSE;
+ }
+ return MTP_PHASE_NONE;
+}
+
+//--------------------------------------------------------------------+
+// Generic container data
+//--------------------------------------------------------------------+
+void mtpd_wc16cpy(uint8_t *dest, const char *src)
+{
+ wchar16_t s;
+ while(true)
+ {
+ s = *src;
+ memcpy(dest, &s, sizeof(wchar16_t));
+ if (*src == 0) break;
+ ++src;
+ dest += sizeof(wchar16_t);
+ }
+}
+
+//--------------------------------------------------------------------+
+// Generic container function
+//--------------------------------------------------------------------+
+bool mtpd_gct_append_uint8(const uint8_t value)
+{
+ uint8_t *p_value = ((uint8_t *)&_mtpd_gct) + _mtpd_gct.container_length;
+ _mtpd_gct.container_length += sizeof(uint8_t);
+ // Verify space requirement (8 bit string length, number of wide characters including terminator)
+ TU_ASSERT(_mtpd_gct.container_length < sizeof(mtp_generic_container_t));
+ *p_value = value;
+ return true;
+}
+
+bool mtpd_gct_append_object_handle(const uint32_t object_handle)
+{
+ _mtpd_gct.container_length += sizeof(uint32_t);
+ TU_ASSERT(_mtpd_gct.container_length < sizeof(mtp_generic_container_t));
+ _mtpd_gct.data[0]++;
+ _mtpd_gct.data[_mtpd_gct.data[0]] = object_handle;
+ return true;
+}
+
+bool mtpd_gct_append_wstring(const char *s)
+{
+ size_t len = strlen(s) + 1;
+ TU_ASSERT(len <= UINT8_MAX);
+ uint8_t *p_len = ((uint8_t *)&_mtpd_gct)+_mtpd_gct.container_length;
+ _mtpd_gct.container_length += sizeof(uint8_t) + sizeof(wchar16_t) * len;
+ // Verify space requirement (8 bit string length, number of wide characters including terminator)
+ TU_ASSERT(_mtpd_gct.container_length < sizeof(mtp_generic_container_t));
+ *p_len = (uint8_t)len;
+ uint8_t *p_str = p_len + sizeof(uint8_t);
+ mtpd_wc16cpy(p_str, s);
+ return true;
+}
+
+bool mtpd_gct_get_string(uint16_t *offset_data, char *string, const uint16_t max_size)
+{
+ uint16_t size = *(((uint8_t *)&_mtpd_gct.data) + *offset_data);
+ if (size > max_size)
+ size = max_size;
+ TU_ASSERT(*offset_data + size < sizeof(_mtpd_gct.data));
+
+ uint8_t *s = ((uint8_t *)&_mtpd_gct.data) + *offset_data + sizeof(uint8_t);
+ for(uint16_t i = 0; i < size; i++)
+ {
+ string[i] = *s;
+ s += sizeof(wchar16_t);
+ }
+ *offset_data += (uint16_t)(sizeof(uint8_t) + size * sizeof(wchar16_t));
+ return true;
+}
+
+bool mtpd_gct_append_array(uint32_t array_size, const void *data, size_t type_size)
+{
+ TU_ASSERT(_mtpd_gct.container_length + sizeof(uint32_t) + array_size * type_size < sizeof(_mtpd_gct.data));
+ uint8_t *p = ((uint8_t *)&_mtpd_gct) + _mtpd_gct.container_length;
+ memcpy(p, &array_size, sizeof(uint32_t));
+ p += sizeof(uint32_t);
+ memcpy(p, data, array_size * type_size);
+ _mtpd_gct.container_length += sizeof(uint32_t) + array_size * type_size;
+ return true;
+}
+
+bool mtpd_gct_append_date(struct tm *timeinfo)
+{
+ // strftime is not supported by all platform, this implementation is just for reference
+ int len = snprintf(_mtp_datestr, sizeof(_mtpd_gct.data) - _mtpd_gct.container_length, "%04d%02d%02dT%02d%02d%02dZ",
+ timeinfo->tm_year + 1900,
+ timeinfo->tm_mon + 1,
+ timeinfo->tm_mday,
+ timeinfo->tm_hour,
+ timeinfo->tm_min,
+ timeinfo->tm_sec);
+ if (len == 0)
+ return false;
+ return mtpd_gct_append_wstring(_mtp_datestr);
+}
+
+#endif // (CFG_TUD_ENABLED && CFG_TUD_MTP)
diff --git a/src/class/mtp/mtp_device.h b/src/class/mtp/mtp_device.h
new file mode 100644
index 000000000..6531b36df
--- /dev/null
+++ b/src/class/mtp/mtp_device.h
@@ -0,0 +1,75 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Ennebi Elettronica (https://ennebielettronica.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * This file is part of the TinyUSB stack.
+ */
+
+#ifndef _TUSB_MTP_DEVICE_H_
+#define _TUSB_MTP_DEVICE_H_
+
+#include "common/tusb_common.h"
+#include "mtp.h"
+
+#if (CFG_TUD_ENABLED && CFG_TUD_MTP)
+
+#ifdef __cplusplus
+ extern "C" {
+#endif
+
+//--------------------------------------------------------------------+
+// Internal Class Driver API
+//--------------------------------------------------------------------+
+void mtpd_init (void);
+bool mtpd_deinit (void);
+void mtpd_reset (uint8_t rhport);
+uint16_t mtpd_open (uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16_t max_len);
+bool mtpd_control_xfer_cb (uint8_t rhport, uint8_t stage, tusb_control_request_t const *p_request);
+bool mtpd_xfer_cb (uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t xferred_bytes);
+
+//--------------------------------------------------------------------+
+// Helper functions
+//--------------------------------------------------------------------+
+// Generic container function
+void mtpd_wc16cpy(uint8_t *dest, const char *src);
+bool mtpd_gct_append_uint8(const uint8_t value);
+bool mtpd_gct_append_object_handle(const uint32_t object_handle);
+bool mtpd_gct_append_wstring(const char *s);
+bool mtpd_gct_get_string(uint16_t *offset_data, char *string, const uint16_t max_size);
+
+// Append the given array to the global context buffer
+// The function returns true if the data fits in the available buffer space.
+bool mtpd_gct_append_array(uint32_t array_size, const void *data, size_t type_size);
+
+// Append an UTC date string to the global context buffer
+// Required format is 'YYYYMMDDThhmmss.s' optionally added 'Z' for UTC or +/-hhmm for time zone
+// This function is provided for reference and only supports UTC format without partial seconds
+// The function returns true if the data fits in the available buffer space.
+bool mtpd_gct_append_date(struct tm *timeinfo);
+
+#ifdef __cplusplus
+ }
+#endif
+
+#endif /* CFG_TUD_ENABLED && CFG_TUD_MTP */
+
+#endif /* _TUSB_MTP_DEVICE_H_ */
diff --git a/src/class/mtp/mtp_device_storage.h b/src/class/mtp/mtp_device_storage.h
new file mode 100644
index 000000000..a9bbc9b90
--- /dev/null
+++ b/src/class/mtp/mtp_device_storage.h
@@ -0,0 +1,149 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2025 Ennebi Elettronica (https://ennebielettronica.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * This file is part of the TinyUSB stack.
+ */
+
+#ifndef _TUSB_MTP_DEVICE_STORAGE_H_
+#define _TUSB_MTP_DEVICE_STORAGE_H_
+
+#include "common/tusb_common.h"
+#include "mtp.h"
+
+#if (CFG_TUD_ENABLED && CFG_TUD_MTP)
+
+#ifdef __cplusplus
+ extern "C" {
+#endif
+
+//--------------------------------------------------------------------+
+// Storage Application Callbacks
+//--------------------------------------------------------------------+
+
+/*
+ * The entire MTP functionality is based on object handles, as described in MTP Specs v. 1.1 under 3.4.1.
+ * The major weakness of the protocol is that those handles are supposed to be unique within a session
+ * and for every enumerated object. There is no specified lifetime limit or way to control the expiration:
+ * once given, they have to persist for an indefinite time and number of iterations.
+ * If the filesystem does not provide unique persistent object handle for every entry, the best approach
+ * would be to keep a full association between generated handles and full file paths. The suggested
+ * approach with memory constrained devices is to keep a hard ID associated with each file or a volatile
+ * ID generated on the fly and invalidated on each operation that may rearrange the order.
+ * In order to invalidate existing IDS, it might be necessary to invalidate the whole session from
+ * the device side.
+ * Depending on the application, the handle could be also be the file name or a tag (i.e. host-only file access)
+ */
+
+
+// Initialize MTP storage subsystem
+//
+// The function shall check if the session is already opened and, in case, set session_id to the
+// ID of the current session.
+mtp_response_t tud_mtp_storage_open_session(uint32_t *session_id);
+
+// Close an open session
+mtp_response_t tud_mtp_storage_close_session(uint32_t session_id);
+
+// Get a storage ID valid within the current session
+//
+// TODO: while multiple storage IDs could be used, the implementation currently supports only 1.
+mtp_response_t tud_mtp_get_storage_id(uint32_t *storage_id);
+
+// Get storage information for the given ID
+//
+// The implementation shall fill all the fields required by the specification.
+// Note that the variable information (e.g. wstring file name, dates and tags shall be written by using the library functions)
+// In addition to the fixed mtp_storage_info_t structure, the function shall add storage descriptor string and
+// volume identifier string via tud_mtp_gct_append_wstring function.
+mtp_response_t tud_mtp_get_storage_info(uint32_t storage_id, mtp_storage_info_t *info);
+
+// Format the specified storage
+mtp_response_t tud_mtp_storage_format(uint32_t storage_id);
+
+// Traverse the given parent object handle and return a child handle for each call
+//
+// If the parent object has not been opened (or closed before) the function returns the first handle.
+// When next_child_handle is 0 all the handles have been listed.
+// TODO: traverse by ObjectFormatCode and ObjectHandle association. For now they are unsupported.
+mtp_response_t tud_mtp_storage_association_get_object_handle(uint32_t session_handle, uint32_t parent_object_handle, uint32_t *next_child_handle);
+
+// Called with the creation of a new object is requested.
+// The handle of the new object shall be returned in new_object_handle.
+// The structure info contains the information to be used for file creation, as passted by the host.
+// Note that the variable information (e.g. wstring file name, dates and tags shall be retrieved by using the library functions)
+mtp_response_t tud_mtp_storage_object_write_info(uint32_t storage_id, uint32_t parent_object, uint32_t *new_object_handle, const mtp_object_info_t *info);
+
+// Get object information related to a given object handle
+//
+// The structure info shall be filled according to MTP specifications. Note that
+// in addition to filling the fixed mtp_object_info_t structure, the caller must add the following fields via
+// library calls
+// - Filename (string, use tud_mtp_gct_append_wstring)
+// - Date created (string, use tud_gct_append_date or empty string)
+// - Date modified (string, use tud_gct_append_date or empty string)
+// - Keywords (string containing list of kw, separated by space, use tud_mtp_gct_append_wstring)
+// Note that the variable information (e.g. wstring file name, dates and tags shall be written by using the library functions)
+mtp_response_t tud_mtp_storage_object_read_info(uint32_t object_handle, mtp_object_info_t *info);
+
+// Get the object size.
+//
+// The object may be already open when this function is called.
+// The implementation shall not assume a specific call order between this function and tud_mtp_storage_object_read.
+// The function may leave the file open.
+mtp_response_t tud_mtp_storage_object_size(uint32_t object_handle, uint32_t *size);
+
+// Write object data
+//
+// The function shall open the object for writing if not already open.
+// The binary data shall be written to the file in full before this function is returned.
+mtp_response_t tud_mtp_storage_object_write(uint32_t object_handle, const uint8_t *buffer, uint32_t buffer_size);
+
+// Get object data
+//
+// The function shall open the object for reading if not already open.
+// The amount of data returned shall be the given size parameter.
+// read_count shall contain the effective number of bytes written. Iteration is terminated when read_count < buffer_size.
+mtp_response_t tud_mtp_storage_object_read(uint32_t object_handle, void *buffer, uint32_t buffer_size, uint32_t *read_count);
+
+// Move an object to a new parent
+mtp_response_t tud_mtp_storage_object_move(uint32_t object_handle, uint32_t new_parent_object_handle);
+
+// Delete the specified object
+mtp_response_t tud_mtp_storage_object_delete(uint32_t object_handle);
+
+// Issued when IO operation has been terminated (e.g. read, traverse), close open file handles
+void tud_mtp_storage_object_done(void);
+
+// Cancel any pending operation. Current operation shall be discarded.
+void tud_mtp_storage_cancel(void);
+
+// Restore the operation out of reset. Cancel any pending operation and close the session.
+void tud_mtp_storage_reset(void);
+
+#ifdef __cplusplus
+ }
+#endif
+
+#endif /* CFG_TUD_ENABLED && CFG_TUD_MTP */
+
+#endif /* _TUSB_MTP_DEVICE_STORAGE_H_ */
diff --git a/src/device/usbd.c b/src/device/usbd.c
index fb5cec49d..8f50d815f 100644
--- a/src/device/usbd.c
+++ b/src/device/usbd.c
@@ -309,6 +309,19 @@ tu_static usbd_class_driver_t const _usbd_driver[] = {
.sof = NULL
},
#endif
+
+ #if CFG_TUD_MTP
+ {
+ .name = DRIVER_NAME("MTP"),
+ .init = mtpd_init,
+ .deinit = mtpd_deinit,
+ .reset = mtpd_reset,
+ .open = mtpd_open,
+ .control_xfer_cb = mtpd_control_xfer_cb,
+ .xfer_cb = mtpd_xfer_cb,
+ .sof = NULL
+ },
+ #endif
};
enum { BUILTIN_DRIVER_COUNT = TU_ARRAY_SIZE(_usbd_driver) };
diff --git a/src/device/usbd.h b/src/device/usbd.h
index de6007fb3..b89a0200b 100644
--- a/src/device/usbd.h
+++ b/src/device/usbd.h
@@ -270,6 +270,25 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ
//--------------------------------------------------------------------+
+// MTP Descriptor Templates
+//--------------------------------------------------------------------+
+
+// Length of template descriptor: 30 bytes
+#define TUD_MTP_DESC_LEN (9 + 7 + 7 + 7)
+
+// Interface number, string index, EP Out & EP In address, EP size
+#define TUD_MTP_DESCRIPTOR(_itfnum, _stridx, _ep_evt, _ep_evt_size, _ep_evt_polling_interval, _epout, _epin, _epsize) \
+ /* Interface */\
+ 9, TUSB_DESC_INTERFACE, _itfnum, 0, 3, TUSB_CLASS_IMAGE, MTP_SUBCLASS, MTP_PROTOCOL_STILL_IMAGE, _stridx,\
+ /* Endpoint Interrupt */\
+ 7, TUSB_DESC_ENDPOINT, _ep_evt, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_ep_evt_size), _ep_evt_polling_interval,\
+ /* Endpoint Out */\
+ 7, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0,\
+ /* Endpoint In */\
+ 7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0
+
+
+//--------------------------------------------------------------------+
// HID Descriptor Templates
//--------------------------------------------------------------------+
diff --git a/src/tinyusb.mk b/src/tinyusb.mk
index a9f623c24..8f9d52de9 100644
--- a/src/tinyusb.mk
+++ b/src/tinyusb.mk
@@ -12,6 +12,7 @@ TINYUSB_SRC_C += \
src/class/hid/hid_device.c \
src/class/midi/midi_device.c \
src/class/msc/msc_device.c \
+ src/class/mtp/mtp_device.c \
src/class/net/ecm_rndis_device.c \
src/class/net/ncm_device.c \
src/class/usbtmc/usbtmc_device.c \
diff --git a/src/tusb.h b/src/tusb.h
index dfba21ddf..abe3e3c9b 100644
--- a/src/tusb.h
+++ b/src/tusb.h
@@ -88,6 +88,10 @@
#include "class/msc/msc_device.h"
#endif
+ #if CFG_TUD_MTP
+ #include "class/mtp/mtp_device.h"
+ #endif
+
#if CFG_TUD_AUDIO
#include "class/audio/audio_device.h"
#endif
diff --git a/src/tusb_option.h b/src/tusb_option.h
index 29fdcb0d6..e36a56de9 100644
--- a/src/tusb_option.h
+++ b/src/tusb_option.h
@@ -493,6 +493,10 @@
#define CFG_TUD_MSC 0
#endif
+#ifndef CFG_TUD_MTP
+ #define CFG_TUD_MTP 0
+#endif
+
#ifndef CFG_TUD_HID
#define CFG_TUD_HID 0
#endif
diff --git a/test/fuzz/rules.mk b/test/fuzz/rules.mk
index ee91c706d..24d6cc0d7 100644
--- a/test/fuzz/rules.mk
+++ b/test/fuzz/rules.mk
@@ -31,6 +31,7 @@ SRC_C += \
src/class/hid/hid_device.c \
src/class/midi/midi_device.c \
src/class/msc/msc_device.c \
+ src/class/mtp/mtp_device.c \
src/class/net/ecm_rndis_device.c \
src/class/net/ncm_device.c \
src/class/usbtmc/usbtmc_device.c \
diff --git a/tools/iar_template.ipcf b/tools/iar_template.ipcf
index 33a6ef045..c93795b9c 100644
--- a/tools/iar_template.ipcf
+++ b/tools/iar_template.ipcf
@@ -58,6 +58,12 @@
<path>$TUSB_DIR$/src/class/msc/msc_device.h</path>
<path>$TUSB_DIR$/src/class/msc/msc_host.h</path>
</group>
+ <group name="src/class/mtp">
+ <path>$TUSB_DIR$/src/class/mtp/mtp_device.c</path>
+ <path>$TUSB_DIR$/src/class/mtp/mtp.h</path>
+ <path>$TUSB_DIR$/src/class/mtp/mtp_device.h</path>
+ <path>$TUSB_DIR$/src/class/mtp/mtp_device_storage.h</path>
+ </group>
<group name="src/class/net">
<path>$TUSB_DIR$/src/class/net/ecm_rndis_device.c</path>
<path>$TUSB_DIR$/src/class/net/ncm_device.c</path>