diff --git a/examples/device/mtp/CMakeLists.txt b/examples/device/mtp/CMakeLists.txt new file mode 100644 index 0000000000..e91eb8fd9e --- /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. -) +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 0000000000..5cd8971e9a --- /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 0000000000..7fa475da55 --- /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 0000000000..2f5139d9d6 --- /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 0000000000..e69de29bb2 diff --git a/examples/device/mtp/src/main.c b/examples/device/mtp/src/main.c new file mode 100644 index 0000000000..709aeb16f9 --- /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 +#include +#include + +#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 0000000000..8e8d6d93d9 --- /dev/null +++ b/examples/device/mtp/src/mtp_fs_example.c @@ -0,0 +1,315 @@ +/* + * 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" + +static uint32_t curr_session_id; + +//--------------------------------------------------------------------+ +// FILEINFO LIST +//--------------------------------------------------------------------+ +#define FS_MAX_CAPACITY (1024UL * 1024UL) +#define FS_FREE_SPACE (256UL * 1024UL) +typedef struct +{ + uint32_t parent_object; + uint32_t object_handle; + const char *filename; + uint32_t size; + const char *date_created; + const char *date_modified; + bool is_association; + bool is_readonly; +} mtpd_fileinfo_t; + +#define MTP_FILE_NUM 8 +static const mtpd_fileinfo_t mtpd_fileinfo[MTP_FILE_NUM] = +{ + {.parent_object = MTP_OBJH_ROOT, .object_handle = 0x5ff1, .filename = "folder1", .size = 0, .date_created = "20240104T111134.0", .date_modified = "20241214T121110.0", .is_association = true, .is_readonly = false}, + {.parent_object = MTP_OBJH_ROOT, .object_handle = 0x1b6a, .filename = "folder2", .size = 0, .date_created = "20240609T111325.0", .date_modified = "20241214T121110.0", .is_association = true, .is_readonly = false}, + {.parent_object = MTP_OBJH_ROOT, .object_handle = 0x7ef1, .filename = "file0-1.txt", .size = 149, .date_created = "20241211T111410.0", .date_modified = "20241211T121110.0", .is_association = false, .is_readonly = false}, + {.parent_object = MTP_OBJH_ROOT, .object_handle = 0x67bf, .filename = "file0-2.txt", .size = 149, .date_created = "20241015T131622.0", .date_modified = "20241211T121110.0", .is_association = false, .is_readonly = true}, + {.parent_object = 0x5ff1, .object_handle = 0x1ab5, .filename = "file1-1.txt", .size = 1043, .date_created = "20240810T151040.0", .date_modified = "20241211T121110.0", .is_association = false, .is_readonly = false}, + {.parent_object = 0x5ff1, .object_handle = 0x5e74, .filename = "file1-2.txt", .size = 963, .date_created = "20240222T164224.0", .date_modified = "20241211T121115.0", .is_association = false, .is_readonly = false}, + {.parent_object = 0x5ff1, .object_handle = 0x31bc, .filename = "file1-3.txt", .size = 455, .date_created = "20240914T081224.0", .date_modified = "20241211T121120.0", .is_association = false, .is_readonly = false}, + {.parent_object = 0x1b6a, .object_handle = 0xa542, .filename = "file2-1.txt", .size = 637, .date_created = "20241102T081224.0", .date_modified = "20241211T121120.0", .is_association = false, .is_readonly = false}, +}; + +const char mtpd_filecontent[] = \ + "TinyUSB example content file\n"\ + "LgpHFNxGYy Zvbv7JsJPK pN2laq3F63 LkfhN01bwH NBNszybG8s 6LQmMeZtIg 58LYldVs9Q o8DxQAn5sR R7fxAR3bZ4\n"\ + "R18BmVfk1R CNFSSZeexW FjX6VNL4ti oaIv6nI6Vg FeSXtFLLYj hmbeznLxDL i3ruRE1xYf G7vz5d4N86 0HTghej4ME\n"\ + "xQ4rvmQGV3 GQDNXWaEiN uyeH2rrlgs jJPnnBzUz0 M7y8QnOH39 iiunjjwGej 1v8LP2tHvk eUeinbLdM4 B8WTBPZv9i\n"\ + "aa7NV35z0f PEfTtXSLvy bmkBvfiRxd jhMxdzYCXk 9KKubAScMN JGXftGkCac 2WLDR9HXvc EJ3G6dsK8B 5arz1WCgJW\n"\ + "eH818ZqFs6 iyjo6ewEvr EseV7OZiqe bLjyG4XDlY KKH7X45rEC 7iulgvD1Hf uvnknHpVFB 6MVgqPNnG7 9aaFq7L85z\n"\ + "cBOPGGSSjW l268GETzWe Pt3738YroR RUDFVWq6O7 rgSgmblusz 9GS3NWuhSu nAXRjkK3NT 1ApWRMLF8F wkCVEIkc5I\n"\ + "hA46uJuKuh YdJzZduNCw CtBjzKBzkU KNFL4feUJT 5Dx6V0nMYd eXrZw4lV9G PAT199yeO8 I8RriKIgbS PxqhiACeow\n"\ + "3YpLHOqc2G vsYM60ucKk Ophi0Y1QiR zHJqDiKNBU y92s8d3Rnw cmPjsGemif tYL6VGmo4l X8YpGMTfJJ 5GeUxgmHoA\n"\ + "LI89X8yJrY JHeNRJLVRD 7UJBgQH7SJ ec06d8rBjl qJ1aTqxmWY JS2RFlSysr cxTGFbQ5uG MeLWANNbdn VeJyvOH0Gi\n"\ + "P8xcCI5Fov rmK36tw9E6 fLAUDmBEla MLM1BCBdvS wY9CFWaW42 50aw1E49gy JiZtWt8idW DzXF5skono 15i7mkMLt6\n"\ + "gJoTH6cbc8 32g5cMMcUq yona4LvEBT LLCGznyWUK m7ND6rG5kJ KAqtDHdThV 6MUEQkzjzp VcQ1NF1Rks ebPjPZieDx\n"\ + "1RHp6HSZlA gW6rPIlJHW g0tJTPPA25 Ac2jz92CIl 5aixI4Cb6w bFrycTwz69 0m5BMYaPww 1YR8e0ReBe S7nEIHCGLZ\n"\ + "d4aYGcPbPT zfrwrVsQdl fc7w2zYb4a 2w07GERNgp 7clAd2kKEo TeCTN6F8tE UyaVCQfg0u PQtFD7ZyHM LyiKlqAJXY\n"\ + "nMfwqcQaeL cR5PYYS3YA 4iMWdlvJbn KY7CVW0bWN zJBsTAVXiz vUgIfhMxQb mstlnhQVCn qMZ0nSIX7p A6nePUrUQd\n"\ + "rhzckKgtIe P6IV3MJANR a6AGKVWIub qVK2vLI9ez P8wum3U12r P18KAwkbAZ ZCKuslh6xG SGctC8XmXJ YVBcJw6rnW\n"\ + "AQs0ZjrVRc WxssLuiMAI sY0GqFLGdV BYgjD6gsec z4wlNLvjWg VEURkm0NQe 4K6AzL1X8i Rd2k3Uw5V2 IywOcCJgm6\n"\ + "4z0C8wJHL9 m29dRd37xL MFwhC3B3yT eWtyIlVZCZ MkHuHVzJY1 Lg8lpUvHQG GretKf2Ryk FMNE10CfA1 ueAMantydr\n"\ + "Be6waQB14x QgwadoIp3i pTYmuLkB65 VDzEmIswD3 Utca5kNkkq elXoVThHL5 JmAPXc0BiU PmKZbpYqBh Xo7WssVpX7\n"; + +const mtpd_fileinfo_t *mtpd_get_fileinfo(uint32_t object_handle); + +const mtpd_fileinfo_t *mtpd_get_fileinfo(uint32_t object_handle) +{ + for (int h = 0; h < MTP_FILE_NUM; h++) + { + if (mtpd_fileinfo[h].object_handle == object_handle) + return &mtpd_fileinfo[h]; + } + return NULL; +} + +//--------------------------------------------------------------------+ +// API +//--------------------------------------------------------------------+ +mtp_response_t tud_mtp_storage_open_session(uint32_t *session_id) +{ + if (*session_id == 0) + return MTP_RESC_INVALID_PARAMETER; + if (curr_session_id != 0) + { + *session_id = curr_session_id; + return MTP_RESC_SESSION_ALREADY_OPEN; + } + curr_session_id = *session_id; + return MTP_RESC_OK; +} + +mtp_response_t tud_mtp_storage_close_session(uint32_t session_id) +{ + if (session_id != curr_session_id) + return MTP_RESC_SESSION_NOT_OPEN; + curr_session_id = 0; + return MTP_RESC_OK; +} + +mtp_response_t tud_mtp_get_storage_id(uint32_t *storage_id) +{ + if (curr_session_id == 0) + return MTP_RESC_SESSION_NOT_OPEN; + *storage_id = STORAGE_ID(0x0001, 0x0001); + return MTP_RESC_OK; +} + +mtp_response_t tud_mtp_get_storage_info(uint32_t storage_id, mtp_storage_info_t *info) +{ + if (curr_session_id == 0) + return MTP_RESC_SESSION_NOT_OPEN; + if (storage_id != STORAGE_ID(0x0001, 0x0001)) + return MTP_RESC_INVALID_STORAGE_ID; + info->storage_type = MTP_STORAGE_TYPE_FIXED_ROM; + info->filesystem_type = MTP_FILESYSTEM_TYPE_GENERIC_HIERARCHICAL; + info->access_capability = MTP_ACCESS_CAPABILITY_READ_WRITE; + info->max_capacity_in_bytes = FS_MAX_CAPACITY; + info->free_space_in_bytes = FS_FREE_SPACE; + // Free space in objects is unsupported + info->free_space_in_objects = 0xFFFFFFFFul; + + mtpd_gct_append_wstring(MTPD_STORAGE_DESCRIPTION); + mtpd_gct_append_wstring(MTPD_VOLUME_IDENTIFIER); + return MTP_RESC_OK; +} + +mtp_response_t tud_mpt_storage_format(uint32_t storage_id) +{ + if (curr_session_id == 0) + return MTP_RESC_SESSION_NOT_OPEN; + if (storage_id != STORAGE_ID(0x0001, 0x0001)) + return MTP_RESC_INVALID_STORAGE_ID; + // TODO execute device format + 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) +{ + static uint32_t current_parent_object = 0xffff; + static int object_count = 0; + + if (curr_session_id == 0) + return MTP_RESC_SESSION_NOT_OPEN; + if (storage_id != STORAGE_ID(0x0001, 0x0001)) + return MTP_RESC_INVALID_STORAGE_ID; + // Request for objects with no parent (0xFFFFFFFF) are considered root objects + if (parent_object_handle == 0xFFFFFFFF) + parent_object_handle = 0; + + if (parent_object_handle != current_parent_object) + { + current_parent_object = parent_object_handle; + object_count = 0; + } + + for (int h = 0, cnt = 0; h < MTP_FILE_NUM; h++) + { + if (mtpd_fileinfo[h].parent_object == current_parent_object) + cnt++; + if (cnt > object_count) + { + object_count = cnt; + *next_child_handle = mtpd_fileinfo[h].object_handle; + return MTP_RESC_OK; + } + } + *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) +{ + (void)parent_object; + (void)new_object_handle; + (void)info; + if (curr_session_id == 0) + return MTP_RESC_SESSION_NOT_OPEN; + if (storage_id != STORAGE_ID(0x0001, 0x0001)) + return MTP_RESC_INVALID_STORAGE_ID; + // TODO Object write not implemented + return MTP_RESC_OPERATION_NOT_SUPPORTED; +} + +mtp_response_t tud_mtp_storage_object_read_info(uint32_t object_handle, mtp_object_info_t *info) +{ + if (curr_session_id == 0) + return MTP_RESC_SESSION_NOT_OPEN; + + const mtpd_fileinfo_t *fileinfo = mtpd_get_fileinfo(object_handle); + if (fileinfo == NULL) + return MTP_RESC_INVALID_OBJECT_HANDLE; + + memset(info, 0, sizeof(mtp_object_info_t)); + info->storage_id = STORAGE_ID(0x0001, 0x0001); + if (fileinfo->is_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 = fileinfo->size; + info->association_type = MTP_ASSOCIATION_UNDEFINED; + } + info->thumb_format = MTP_OBJF_UNDEFINED; + info->parent_object = fileinfo->parent_object; + + mtpd_gct_append_wstring(fileinfo->filename); + mtpd_gct_append_wstring(fileinfo->date_created); // date_created + mtpd_gct_append_wstring(fileinfo->date_modified); // date_modified + mtpd_gct_append_wstring(""); // keywords, not used + + return MTP_RESC_OK; +} + +mtp_response_t tud_mtp_storage_object_write(uint32_t object_handle, const uint8_t *buffer, uint32_t size) +{ + (void)object_handle; + (void)buffer; + (void)size; + if (curr_session_id == 0) + return MTP_RESC_SESSION_NOT_OPEN; + // TODO Object write not implemented + return MTP_RESC_OPERATION_NOT_SUPPORTED; +} + +mtp_response_t tud_mtp_storage_object_size(uint32_t object_handle, uint32_t *size) +{ + const mtpd_fileinfo_t *fileinfo = mtpd_get_fileinfo(object_handle); + if (fileinfo == NULL) + return MTP_RESC_INVALID_OBJECT_HANDLE; + *size = fileinfo->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) +{ + static uint32_t current_object_handle = 0xffff; + static int pos = 0; + + const mtpd_fileinfo_t *fileinfo = mtpd_get_fileinfo(object_handle); + if (fileinfo == NULL) + return MTP_RESC_INVALID_OBJECT_HANDLE; + if (object_handle != current_object_handle) + { + current_object_handle = object_handle; + pos = 0; + } + if (sizeof(mtpd_filecontent) - pos > buffer_size) + { + *read_count = buffer_size; + memcpy(buffer, &mtpd_filecontent[pos], *read_count); + pos += *read_count; + } + else + { + *read_count = sizeof(mtpd_filecontent) - pos; + memcpy(buffer, &mtpd_filecontent[pos], *read_count); + pos = 0; + } + return MTP_RESC_OK; +} + +mtp_response_t tud_mtp_storage_object_move(uint32_t object_handle, uint32_t new_parent_object_handle) +{ + (void)object_handle; + (void)new_parent_object_handle; + return MTP_RESC_OPERATION_NOT_SUPPORTED; +} + +mtp_response_t tud_mtp_storage_object_delete(uint32_t object_handle) +{ + if (curr_session_id == 0) + return MTP_RESC_SESSION_NOT_OPEN; + if (object_handle == 0xFFFFFFFF) + { + // TODO delete all objects + return MTP_RESC_OK; + } + const mtpd_fileinfo_t *fileinfo = mtpd_get_fileinfo(object_handle); + if (fileinfo == NULL) + return MTP_RESC_INVALID_OBJECT_HANDLE; + + // TODO delete object + return MTP_RESC_OK; +} + +void tud_mtp_storage_object_done(void) +{ +} diff --git a/examples/device/mtp/src/tusb_config.h b/examples/device/mtp/src/tusb_config.h new file mode 100644 index 0000000000..a40f47fcfe --- /dev/null +++ b/examples/device/mtp/src/tusb_config.h @@ -0,0 +1,116 @@ +/* + * 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 + +#define EPNUM_MTP_EVT 0x81 +#define EPNUM_MTP_IN 0x83 +#define EPNUM_MTP_OUT 0x04 + +#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 0000000000..8498ddd908 --- /dev/null +++ b/examples/device/mtp/src/usb_descriptors.c @@ -0,0 +1,185 @@ +/* + * 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 "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] 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)) + +//--------------------------------------------------------------------+ +// Device Descriptors +//--------------------------------------------------------------------+ +tusb_desc_device_t const desc_device = +{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = 0xCafe, + .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; +} + +tusb_desc_device_qualifier_t const desc_device_qualifier = +{ + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = 0x0201, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = 0x01, + .bReserved = 0 +}; + +// Invoked when received GET DEVICE DESCRIPTOR_QUALIFIER +// Application return pointer to descriptor +uint8_t const *tud_descriptor_device_qualifier_cb(void) +{ + return (uint8_t const *) &desc_device_qualifier; +} + +//--------------------------------------------------------------------+ +// Configuration Descriptor +//--------------------------------------------------------------------+ + +enum +{ + ITF_NUM_MTP, + ITF_NUM_TOTAL +}; + +#define MTP_DESC_LEN TUD_MTP_DESC_LEN + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + 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 00e288badb..beabcad9cf 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 2182e9ad10..3bec5bf70e 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 34399fd454..99517a090e 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 55c52033c3..9918407b35 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 0000000000..33c6bb5529 --- /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 +#include +#include +#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 0000000000..51407b4f41 --- /dev/null +++ b/src/class/mtp/mtp_device.c @@ -0,0 +1,961 @@ +/* + * 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 uint32_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_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_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; + mtpd_interface_t *p_mtp = &_mtpd_itf; + + switch ( request->bRequest ) + { + case MTP_REQ_CANCEL: + TU_LOG_DRV(" MTP request: MTP_REQ_CANCEL\n"); + 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"); + // TODO StorageCancel + // Prepare for a new transaction + 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"); + switch (p_mtp->phase) + { + case MTP_PHASE_RESPONSE: + case MTP_PHASE_DATA_OUT: + case MTP_PHASE_DATA_IN: + len = 4; + _mtpd_device_status_res.wLength = len; + _mtpd_device_status_res.code = MTP_RESC_DEVICE_BUSY; + break; + case MTP_PHASE_IDLE: + len = 4; + _mtpd_device_status_res.wLength = len; + _mtpd_device_status_res.code = MTP_RESC_OK; + break; + default: + break; + } + TU_ASSERT( tud_control_xfer(rhport, request, (uint8_t *)&_mtpd_device_status_res , len) ); + break; + + default: 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) +{ + mtpd_interface_t *p_mtp = &_mtpd_itf; + const unsigned dir = tu_edpt_dir(ep_addr); + + if (event != XFER_RESULT_SUCCESS) + return false; + + // IN transfer completed + if (dir == TUSB_DIR_IN) + { + if (p_mtp->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); + p_mtp->phase = MTP_PHASE_IDLE; + } + else if (p_mtp->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)), _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)), _mtpd_gct.container_length)); + else + TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_in, ((uint8_t *)(&_mtpd_gct.data)), _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. + p_mtp->total_len = _mtpd_gct.container_length; + // Stall in case of unexpected block + if (_mtpd_gct.container_type != MTP_CONTAINER_TYPE_COMMAND_BLOCK) + { + return false; + } + p_mtp->phase = MTP_PHASE_COMMAND; + p_mtp->total_len = _mtpd_gct.container_length; + p_mtp->xferred_len = xferred_bytes; + p_mtp->handled_len = 0; + p_mtp->xfer_completed = false; + TU_ASSERT(p_mtp->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 || (p_mtp->total_len - p_mtp->xferred_len) > 0 ) + { + TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)) + p_mtp->xferred_len, p_mtp->total_len - p_mtp->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)), _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)), _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) + { + p_mtp->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)), _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 (p_mtp->xferred_len - p_mtp->handled_len >= MTP_MAX_PACKET_SIZE - CFG_MTP_EP_SIZE) + { + _mtpd_itf.phase = mtpd_handle_data(); + p_mtp->handled_len = p_mtp->xferred_len; + } + // Transfer completed: wait for zero-lenght packet + if ((p_mtp->total_len - p_mtp->xferred_len) == 0) + { + 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 (p_mtp->handled_len == 0) + { + TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct)) + p_mtp->xferred_len, p_mtp->total_len - p_mtp->xferred_len)); + } + else + // Successive data block includes only container data + { + TU_ASSERT(usbd_edpt_xfer(rhport, _mtpd_itf.ep_out, ((uint8_t *)(&_mtpd_gct.data)) + p_mtp->xferred_len - p_mtp->handled_len, p_mtp->total_len - p_mtp->xferred_len)); + } + } + } + } + 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"); + break; + 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_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_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; + 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_INFO; + _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_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_mpt_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 uint32_t ret_code, const char *message) +{ + (void)func_name; + (void)message; + if (err_cd) + { + TU_LOG_DRV(" MTP error in %s: (%lx) %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) +{ + uint8_t len = strlen(s) + 1; + 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 = 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 += 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 + size_t 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 0000000000..6531b36df8 --- /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 0000000000..33d9069c3e --- /dev/null +++ b/src/class/mtp/mtp_device_storage.h @@ -0,0 +1,143 @@ +/* + * 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_mpt_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); + +// Cancel any pending operation on objects (e.g. read, traverse) and close file handles +void tud_mtp_storage_object_done(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 fb5cec49db..8f50d815f3 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 de6007fb39..b89a0200bf 100644 --- a/src/device/usbd.h +++ b/src/device/usbd.h @@ -269,6 +269,25 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ 7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0 +//--------------------------------------------------------------------+ +// 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 a9f623c24c..8f9d52de9c 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 dfba21ddf6..abe3e3c9b8 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 29fdcb0d65..e36a56de90 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 ee91c706d5..24d6cc0d7f 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 33a6ef045b..c93795b9cb 100644 --- a/tools/iar_template.ipcf +++ b/tools/iar_template.ipcf @@ -58,6 +58,12 @@ $TUSB_DIR$/src/class/msc/msc_device.h $TUSB_DIR$/src/class/msc/msc_host.h + + $TUSB_DIR$/src/class/mtp/mtp_device.c + $TUSB_DIR$/src/class/mtp/mtp.h + $TUSB_DIR$/src/class/mtp/mtp_device.h + $TUSB_DIR$/src/class/mtp/mtp_device_storage.h + $TUSB_DIR$/src/class/net/ecm_rndis_device.c $TUSB_DIR$/src/class/net/ncm_device.c