소스 검색

Add mtp from https://github.com/Alex4386/f0-mtp

git-subtree-dir: mtp
git-subtree-mainline: e6d5a160ce3f265b3bcf1e27e34851d3729afb30
git-subtree-split: 727eef07aea083c5e45d2a41221be7ada62a4e8b
Willy-JL 1 년 전
부모
커밋
61fdc0b6ab

+ 25 - 0
mtp/.github/scripts/change_ver.sh

@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Check if $TAG_NAME is set
+if [ -z "$TAG_NAME" ]; then
+    echo "Error: \$TAG_NAME must be set"
+    exit 1
+fi
+
+# Extract the version from $TAG_NAME
+version="${TAG_NAME##v}"
+
+# Loop through all files in the current directory
+for file in *; do
+    # Check if the file is a regular file
+    if [ -f "$file" ]; then
+        # Make a temporary copy of the file
+        cp "$file" "$file.tmp"
+
+        # Perform the replacement
+        sed "s/fap_version=\"[0-9]*\.[0-9]*\"/fap_version=\"$version\"/" "$file.tmp" > "$file"
+
+        # Remove the temporary file
+        rm "$file.tmp"
+    fi
+done

+ 28 - 0
mtp/.github/scripts/renamer.sh

@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# Check if $DIST_DIR and $FILE_SUFFIX are set
+if [ -z "$FILE_SUFFIX" ] || [ -z "$DIST_FILE" ]; then
+    echo "Error: \$DIST_DIR and \$FILE_SUFFIX must be set"
+    exit 1
+fi
+
+# $DIST_FILE
+DIST_FILE=$(echo "$DIST_FILE" | head -n 1)
+DIST_DIR=$(dirname "$DIST_FILE")
+
+# Loop through files in $DIST_DIR
+for file in "$DIST_DIR"/*; do
+    # Check if the item is a regular file
+    if [ -f "$file" ]; then
+        # Extract the filename and extension
+        filename=$(basename "$file")
+        extension="${filename##*.}"
+        name="${filename%.*}"
+
+        # Construct the new filename
+        new_filename="${name}-${FILE_SUFFIX}.${extension}"
+
+        # Rename the file
+        mv "$file" "$DIST_DIR/${new_filename}"
+    fi
+done

+ 17 - 0
mtp/.github/workflows/lint.yml

@@ -0,0 +1,17 @@
+name: Lint
+on:
+    - push
+    - pull_request
+
+jobs:
+    all:
+        runs-on: ubuntu-latest
+        name: Lint
+        steps:
+          - name: Checkout
+            uses: actions/checkout@v4
+          - name: Linting
+            uses: flipperdevices/flipperzero-ufbt-action@v0.1.3
+            with:
+                sdk-channel: release
+                task: lint

+ 41 - 0
mtp/.github/workflows/nightly.yml

@@ -0,0 +1,41 @@
+name: Nightly
+on:
+    push:
+        branches:
+            - main
+
+jobs:
+    all:
+        runs-on: ubuntu-latest
+        name: Nightly Build for ${{ matrix.name }}
+        strategy:
+            matrix:
+                include:
+                  - name: (Official) Release channel
+                    sdk-ident: official_release
+                    sdk-channel: release
+                  - name: (Unleashed) Release channel
+                    sdk-ident: unleashed_release
+                    sdk-index-url: https://up.unleashedflip.com/directory.json
+                    sdk-channel: release
+                  - name: (Official) Dev channel
+                    sdk-ident: official_dev
+                    sdk-channel: dev
+                  - name: (Unleashed) Development channel
+                    sdk-ident: unleashed_dev
+                    sdk-index-url: https://up.unleashedflip.com/directory.json
+                    sdk-channel: dev
+        steps:
+          - name: Checkout
+            uses: actions/checkout@v4
+          - name: Build for ${{ matrix.name }}
+            uses: flipperdevices/flipperzero-ufbt-action@v0.1.3
+            id: build-app
+            with:
+                sdk-channel: ${{ matrix.sdk-channel }}
+                sdk-index-url: ${{ matrix.sdk-index-url }}
+          - name: Upload app artifacts
+            uses: actions/upload-artifact@v3
+            with:
+                name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }} (${{ matrix.sdk-ident }})
+                path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 56 - 0
mtp/.github/workflows/release.yml

@@ -0,0 +1,56 @@
+name: Build Release
+on:
+    release:
+        types: [created]
+
+jobs:
+    all:
+        runs-on: ubuntu-latest
+        name: Build Release for ${{ matrix.name }}
+        strategy:
+            matrix:
+                include:
+                  - name: (Official) Release channel
+                    sdk-ident: official_release
+                    sdk-channel: release
+                  - name: (Unleashed) Release channel
+                    sdk-ident: unleashed_release
+                    sdk-index-url: https://up.unleashedflip.com/directory.json
+                    sdk-channel: release
+                  - name: (Official) Dev channel
+                    sdk-ident: official_dev
+                    sdk-channel: dev
+                  - name: (Unleashed) Development channel
+                    sdk-ident: unleashed_dev
+                    sdk-index-url: https://up.unleashedflip.com/directory.json
+                    sdk-channel: dev
+        steps:
+          - name: Checkout
+            uses: actions/checkout@v4
+          - name: Change Version to ${{ github.event.release.tag_name }}
+            run: |
+                chmod +x ./.github/scripts/change_ver.sh
+                ./.github/scripts/change_ver.sh
+            env:
+                TAG_NAME: ${{ github.event.release.tag_name }}
+          - name: Build for ${{ matrix.name }}
+            uses: flipperdevices/flipperzero-ufbt-action@v0.1.3
+            id: build-app
+            with:
+                sdk-channel: ${{ matrix.sdk-channel }}
+                sdk-index-url: ${{ matrix.sdk-index-url }}
+          - name: Rename Built files for ${{ matrix.name }}
+            run: |
+                chmod +x ./.github/scripts/renamer.sh
+                ./.github/scripts/renamer.sh
+            env:
+                FILE_SUFFIX: ${{ matrix.sdk-ident }}-${{ steps.build-app.outputs.suffix }}
+                DIST_FILE: ${{ steps.build-app.outputs.fap-artifacts }}
+          - name: Upload packages to release
+            uses: svenstaro/upload-release-action@v2
+            with:
+                repo_token: ${{ secrets.GITHUB_TOKEN }}
+                file: ./dist/*
+                tag: ${{ github.ref }}
+                overwrite: true
+                file_glob: true

+ 6 - 0
mtp/.gitignore

@@ -0,0 +1,6 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig
+.env
+.ufbt

+ 1 - 0
mtp/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/Alex4386/f0-mtp main /

+ 5 - 0
mtp/CHANGELOG.md

@@ -0,0 +1,5 @@
+v1.0:
+ * The first release of the plugin
+
+v0.5:
+ * this is the example of the changelog

+ 32 - 0
mtp/HOW_TO_BUILD.md

@@ -0,0 +1,32 @@
+## Setup Build environment
+
+### Build Instruction
+
+1. Install `ufbt`:
+   ```bash
+   pip3 install ufbt
+   ```
+2. Clone this repository and enter the repository root.
+3. Run `ufbt update` to update the SDK for your flipper
+   - If you are using custom firmware, You should switch SDK. Here is the example for `unleashed` firmware:
+     ```bash
+     ufbt update --index-url=https://up.unleashedflip.com/directory.json
+     ```
+   - If you want to use different release channel, You can run update to that channel too. Here is the example for `dev` channel (`dev`, `rc`, `release` are supported):
+     ```bash
+     ufbt update --channel=dev
+     ```
+4. Run `ufbt` in the repository root:
+   ```bash
+   ufbt
+   ```
+5. Compiled binary is now available at `./dist/` directory.
+
+### Setup Visual Studio Code
+
+> [!WARNING]
+> This command will overwrite your `.vscode` directory and `.gitignore` on your root directory.
+> **Make sure to backup your changes before running this command.**
+
+1. Suppose your build environment is ready.
+2. Run `ufbt vscode_dist` to generate Visual Studio Code config.

+ 69 - 0
mtp/KICKSTART.md

@@ -0,0 +1,69 @@
+# Getting Started with `f0-template`
+
+Welcome on your path to creating Flipper Zero Application!  
+Here are the few stuff to get started!
+
+## Before you begin
+
+### Change `AppID`
+
+1. Open [application.fam](applciation.fam).
+2. head to `appid="demo_app"` and change it to your own.
+3. Open [./src/scenes/about/main.c](src/scenes/about/main.c).
+4. Update `#include <demo_app_icons.h>` into `#include <{app_id}_icons.h>`.  
+   If there is intelliSense error for missing variable, try rebuilding application.
+
+## Developer Resources
+
+Here are the resources for developing applications for Flipper Zero:
+
+- [Flipper Zero Firmware Docs](https://developer.flipper.net/flipperzero/doxygen/)
+  - [`struct` list](https://developer.flipper.net/flipperzero/doxygen/annotated.html) ([index](https://developer.flipper.net/flipperzero/doxygen/classes.html))
+- [Flipper Zero code examples](https://github.com/m1ch3al/flipper-zero-dev-tutorial)
+- [Lopaka, Graphics Editor for Embedded Devices](https://lopaka.app/)  
+  Note that Flipper Zero has screen dimension of `128x64`.
+
+## Directory Structure
+
+`f0-template` consists of following directories
+
+- `.github` - Reserved for GitHub Actions
+- `icons` - For generating `Icon` (Sprites) for use in GUI elements
+- `screenshots` - When uploaded to store, these are the Screenshots available from store page
+- `src` - Where the C source is located
+  - `src/main.c` - Where the main entrypoint is available.
+  - `src/scenes` - Scene definitions for Scene Manager - Refer to [Using SceneManager](#using-scenemanager)
+- `application.fam` - Flipper Zero's application manifest file - Refer to [this docs](https://developer.flipper.net/flipperzero/doxygen/app_manifests.html)
+- `CHANGELOG.md` - When uploaded to store, these are the changelog available from store page
+- `icon.png` - The Application Icon which is shown next to application name in Apps menu.
+
+### `icons`
+
+> [!WARNING]  
+> alpha channel of PNG is **NOT** supported.
+> Make sure that background is white!
+
+This directory is for generating `Icon` struct which can be used for drawing sprites in flipper zero `Canvas`.  
+It can be used as following:
+
+1. Put your `PNG` files into icons.
+2. Build (`ufbt build` or anything triggers build job) at least once before use.
+3. Import `{app_id}_icons.h`.
+4. On `draw_callback`, draw Icon by calling `canvas_draw_icon(canvas, loc_x, loc_y, &I_{png_filename});`
+5. Done!
+
+### Using `SceneManager`
+
+This template implements `SceneManager`, A "Scene" based framework for programming flipper zero GUIs.
+
+#### Creating Scenes
+
+The Scenes are defined via using macro: `SCENE_ACTION`. By default, the `f0-template` provides two example scenes
+
+1. Goto [`./src/scenes/list.h`](/src/scenes/list.h).
+2. Add your own scene by using macro: `SCENE_ACTION`.
+   (e.g. If you want to make new scene called `Store`, You should type `SCENE_ACTION(Store)`)
+3. Implement `_on_enter`, `_on_event`, `_on_exit`, `_get_view`, `_alloc`, `_free` accordingly. Refer to [`./src/scenes/home/main.c`](/src/scenes/home/main.c) for more info.  
+   (F0 doesn't make sure the precendence of `_on_exit` and `_free`. Please make sure those two are independent by checking each other's free'd state)
+4. Add headers to export those functions.
+5. Include your header in [`./src/scenes/import.h`](/src/scenes/import.h).

+ 21 - 0
mtp/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Sanghee Park
+
+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.

+ 78 - 0
mtp/README.md

@@ -0,0 +1,78 @@
+<h1 align="center">Flipper Zero: MTP</h1>
+
+<a href="https://www.youtube.com/watch?v=e35w9nYTJfE">
+See this in action on YouTube!<br>
+<img src="https://img.youtube.com/vi/e35w9nYTJfE/maxresdefault.jpg" width="100%"></a>
+
+## Build Status
+
+- **Latest Release**: [Download](https://github.com/Alex4386/f0-mtp/releases/latest)
+- **Latest Nightly**: [Download](https://github.com/Alex4386/f0-mtp/actions/workflows/nightly.yml) _(GitHub Login Required)_
+
+|                                           Nightly Build                                           |                                           Release Build                                           |
+| :-----------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------: |
+| ![Nightly Build](https://github.com/Alex4386/f0-mtp/actions/workflows/nightly.yml/badge.svg) | ![Release Build](https://github.com/Alex4386/f0-mtp/actions/workflows/release.yml/badge.svg) |
+
+## What is this?
+
+`f0-mtp` is a application that implements [`MTP (Media Transfer Protocol)` defined by `USB-IF`](https://www.usb.org/document-library/media-transfer-protocol-v11-spec-and-mtp-v11-adopters-agreement) on Flipper Zero.  
+This allows you to access the Flipper Zero's internal and SD card storages from your computer without the need of any additional drivers like [`HID-File-Transfer`](https://github.com/Kavakuo/HID-File-Transfer).  
+  
+If your computer can handle Android devices, it should be able to handle Flipper Zero as well with this application.  
+
+Due to limitation of the Flipper Zero's hardware, The MTP connection utilizes `Full Speed` USB, which is USB 1.1 (12Mbps). which may be slow&trade;.  
+
+> [!NOTE]
+> Flipper Zero utilizes `SPI` for SD card, which speed is limited to around 1MB/s.  
+> So, the speed of the MTP connection probably will not the bottleneck here.  
+> <b>So, If you need quick copy, Use a proper SD Card Reader.</b>
+
+### Before using...
+Here are some things you should know before using this application:
+
+> [!WARNING]
+> **DO NOT transfer files over 64K** in one go.  
+> This will:
+> - **Crash the Flipper** if the file is too big.
+> - **Corrupt the SD Card filesystem** due to current implementation's limitation.
+>   (If you know how to fix this issue, feel free to give me a PR!)
+>
+
+> [!WARNING]
+> **DO NOT** use `UNICODE` characters in the file/directory names.  
+> Flipper Zero's filesystem isn't designed to handle `UNICODE` characters. such as:
+> - `한글.txt`
+> - `日本語.txt`
+> - `中文.txt`
+
+### Features
+* Access Internal and SD card storages
+* List files and directories
+   - Navigate directories
+* Opening Files (Downloading Flipper files into Computer)
+  - Large file transfer now **WORKS**!
+    - It didn't work since the header did not have proper `size` defined, ignoring the further packets.
+    - Now utilizing even less memory via `streaming` support!
+* Move Files into Flipper
+  - **NEW!** Now you can upload files to Flipper Zero!
+  - **Note:** Flipper Zero has limited memory, please refrain from moving files bigger than 64K on one go. (for me 80K was a hard limit)
+* Deleting Files/Directories
+
+### Known Issues
+* Renaming directories, files are not supported yet.
+* Fix "memory leaks"  
+  - I'm currently busy working on code cleanup. So, I can't afford to do this right now.
+
+## How to build
+See [HOW_TO_BUILD.md](HOW_TO_BUILD.md) for more information.  
+
+## Special Thanks
+- [viveris/uMTP-Responder](https://github.com/viveris/uMTP-Responder)
+- [yoonghm/MTP](https://github.com/yoonghm/MTP)
+
+**and Special NOT Thanks to**:  
+- [Microsoft](https://microsoft.com) for making the [MTP Spec](https://www.usb.org/document-library/media-transfer-protocol-v11-spec-and-mtp-v11-adopters-agreement) so hard to understand. `>:(`  
+- [Microsoft](https://microsoft.com) for reserving place for programmers to live in `WideChar` and `MultiByte` hellscape. `>:(`
+
+## License
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

+ 32 - 0
mtp/application.fam

@@ -0,0 +1,32 @@
+# Please refer to:
+# https://developer.flipper.net/flipperzero/doxygen/app_manifests.html
+
+App(
+    appid="f0_mtp",  # Must be unique
+    name="MTP",  # Displayed in UI
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="entrypoint",
+    stack_size=2 * 1024, # size of memory stack it will allocate
+    
+    # source code settings
+    sources=[            # Due to limitation of the fbt,
+      "src/*.c*",        # you need to specify nested directories
+      "src/*/*.c*",      # manually since it doesn't support
+      "src/*/*/*.c*"     # recurse globbing such as "src/**/*.c*"
+    ], 
+
+    # Dependencies
+    requires=[
+
+    ],
+
+    # FAP Settings
+    fap_category="USB",
+    fap_description="Use Flipper Zero as a MTP device.",
+    fap_version="1.0",  # (major, minor)
+    fap_icon="icon.png",  # 10x10 1-bit PNG
+    fap_author="Alex4386",
+    fap_weburl="https://github.com/Alex4386/f0-mtp",
+    fap_icon_assets="icons",  # Image assets to compile for this application
+                              # available as {appid}_icons.h in the source code
+)

BIN
mtp/icon.png


+ 1 - 0
mtp/icons/.gitkeep

@@ -0,0 +1 @@
+Drop 

BIN
mtp/icons/Connect_me.png


BIN
mtp/icons/DFU.png


BIN
mtp/icons/DolphinWait.png


BIN
mtp/icons/Pin_back_arrow.png


BIN
mtp/screenshots/flp0.png


BIN
mtp/screenshots/flp1.png


BIN
mtp/screenshots/flp2.png


+ 19 - 0
mtp/src/handler.c

@@ -0,0 +1,19 @@
+#include "main.h"
+
+bool scene_handler_event_forwarder(void* context, uint32_t event_id) {
+    App* app = (App*)context;
+    if(app == NULL || app->scene_manager == NULL) {
+        return false;
+    }
+
+    return scene_manager_handle_custom_event(app->scene_manager, event_id);
+}
+
+bool scene_handler_navigation_forwarder(void* context) {
+    App* app = (App*)context;
+    if(app == NULL || app->scene_manager == NULL) {
+        return false;
+    }
+
+    return scene_manager_handle_back_event(app->scene_manager);
+}

+ 36 - 0
mtp/src/main.c

@@ -0,0 +1,36 @@
+#include <stdio.h>
+#include <furi.h>
+#include <gui/gui.h>
+#include "main.h"
+#include "scenes/register.h"
+
+int main() {
+    App* app = malloc(sizeof(App));
+    furi_assert(app != NULL, "Failed to allocate memory for the app");
+
+    Gui* gui = furi_record_open(RECORD_GUI);
+    furi_assert(gui != NULL, "Failed to open the GUI record");
+
+    register_scenes(app);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+
+    // The default scene is always the first one!
+    scene_manager_next_scene(app->scene_manager, 0);
+    view_dispatcher_switch_to_view(app->view_dispatcher, 0);
+
+    view_dispatcher_run(app->view_dispatcher);
+
+    FURI_LOG_I("DemoApp", "Exiting application.");
+    free_scenes(app);
+
+    FURI_LOG_I("DemoApp", "Freed app.");
+
+    return 0;
+}
+
+// Stub entrypoint due to gcc complaining about
+// mismatching main function signature.
+int32_t entrypoint(void* p) {
+    UNUSED(p);
+    return main();
+}

+ 29 - 0
mtp/src/main.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+#include <gui/view_dispatcher.h>
+#include "scenes/import.h"
+
+typedef struct App {
+    SceneManager* scene_manager;
+    ViewDispatcher* view_dispatcher;
+
+    void** allocated_scenes;
+} App;
+
+/**
+ * Enum for scenes.
+ */
+typedef enum {
+#define SCENE_ACTION(scene) scene,
+#include "scenes/list.h"
+#undef SCENE_ACTION
+
+    AppSceneNum, // This should be the last element in the enumeration.
+} AppViews;
+
+/**
+ * Header definition for handler.c
+ */
+bool scene_handler_event_forwarder(void* context, uint32_t event_id);
+bool scene_handler_navigation_forwarder(void* context);

+ 7 - 0
mtp/src/scenes/import.h

@@ -0,0 +1,7 @@
+#pragma once
+
+/**
+ * This is the file for importing scenes in this directory
+ */
+
+#include "mtp/main.h"

+ 15 - 0
mtp/src/scenes/list.h

@@ -0,0 +1,15 @@
+// DO NOT SET PRAGMA ONCE OR OTHER HEADER GUARDING PATTERNS.
+// IT SHOULD BE INCLUDED MULTIPLE TIMES, AND IS INTENDED.
+
+/**
+ * This is the file for defining the scenes.
+ * 
+ * Use it as following:
+ * 1. Define the scene via SCENE_ACTION macro. (e.g. SCENE_ACTION(Home))
+ * 2. Implement the scene handlers in the corresponding file.
+ *    _on_enter, _on_event, _on_exit, _get_view, _alloc, _free
+ *      e.g. Home -> Home_on_enter should be implemented
+ * 3. Include the scene in the list of scenes in main.h.
+ */
+
+SCENE_ACTION(MTP)

+ 142 - 0
mtp/src/scenes/mtp/main.c

@@ -0,0 +1,142 @@
+#include <gui/view.h>
+#include <gui/modules/submenu.h>
+#include "../../main.h"
+#include "main.h"
+#include <f0_mtp_icons.h>
+#include "usb.h"
+#include "usb_desc.h"
+
+#define THIS_SCENE MTP
+
+AppMTP* tmp;
+
+void MTP_on_draw(Canvas* canvas, void* context);
+AppMTP* MTP_alloc() {
+    AppMTP* mtp = (AppMTP*)malloc(sizeof(AppMTP));
+    mtp->view = view_alloc();
+    view_set_context(mtp->view, mtp);
+    view_set_draw_callback(mtp->view, MTP_on_draw);
+
+    mtp->storage = furi_record_open(RECORD_STORAGE);
+    tmp = mtp;
+
+    return mtp;
+}
+
+void MTP_on_draw(Canvas* canvas, void* context) {
+    AppMTP* mtp = (AppMTP*)context;
+
+    canvas_clear(canvas);
+    canvas_set_bitmap_mode(canvas, true);
+
+    bool usb_connected = false;
+    if(mtp == NULL) {
+        // facepalm
+        if(tmp != NULL) {
+            usb_connected = tmp->usb_connected;
+        }
+    } else {
+        usb_connected = mtp->usb_connected;
+    }
+
+    if(usb_connected) {
+        canvas_set_bitmap_mode(canvas, true);
+        canvas_draw_icon(canvas, 0, 14, &I_DFU);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 43, 10, "MTP Connection");
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 3, 22, "Disconnect or");
+        canvas_draw_icon(canvas, 28, 23, &I_Pin_back_arrow);
+        canvas_draw_str(canvas, 3, 31, "Press");
+    } else {
+        canvas_draw_icon(canvas, 1, 31, &I_Connect_me);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 43, 10, "MTP Connection");
+        canvas_draw_str(canvas, 10, 25, "Plug me into computer!");
+        canvas_draw_icon(canvas, 2, 2, &I_Pin_back_arrow);
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str(canvas, 15, 10, "Exit");
+        canvas_draw_str(canvas, 61, 41, "Waiting for USB");
+        canvas_draw_str(canvas, 72, 50, "Connection...");
+    }
+}
+
+void MTP_free(void* ptr) {
+    AppMTP* home = (AppMTP*)ptr;
+    FURI_LOG_I("DemoApp", "Triggering Free for view");
+
+    view_free(home->view);
+    home->view = NULL;
+
+    free(home);
+}
+
+View* MTP_get_view(void* ptr) {
+    AppMTP* home = (AppMTP*)ptr;
+    return home->view;
+}
+
+void MTP_on_enter(void* context) {
+    App* app = (App*)context;
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, THIS_SCENE);
+    furi_assert(app->allocated_scenes != NULL, "App allocated scenes is NULL");
+
+    AppMTP* mtp = app->allocated_scenes[THIS_SCENE];
+    if(mtp != NULL) {
+        mtp->old_usb = furi_hal_usb_get_config();
+
+        // copy serial number
+        usb_mtp_interface.str_serial_descr = mtp->old_usb->str_serial_descr;
+
+        // set new usb mode for MTP mode
+        if(!furi_hal_usb_set_config(&usb_mtp_interface, mtp)) {
+            FURI_LOG_E("MTP", "Failed to set MTP mode");
+            return;
+        }
+    }
+}
+
+bool MTP_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+
+    if(event.type == SceneManagerEventTypeBack) {
+        return false;
+    }
+
+    return true;
+}
+
+void MTP_on_exit(void* context) {
+    App* app = (App*)context;
+    if(app == NULL) {
+        return;
+    }
+
+    if(app->allocated_scenes != NULL) {
+        AppMTP* mtp = app->allocated_scenes[THIS_SCENE];
+        if(mtp != NULL) {
+            // free all handles
+            FileHandle* handle = mtp->handles;
+            mtp->handles = NULL;
+
+            while(handle != NULL) {
+                FileHandle* next = handle->next;
+
+                if(handle->path != NULL) free(handle->path);
+                free(handle);
+
+                handle = next;
+            }
+        }
+    }
+
+    furi_record_close(RECORD_STORAGE);
+
+    // revert to old usb mode
+    furi_hal_usb_set_config(tmp->old_usb, NULL);
+
+    // if(app->view_dispatcher) view_dispatcher_switch_to_view(app->view_dispatcher, Home);
+    // if(app->scene_manager) scene_manager_previous_scene(app->scene_manager);
+}

+ 54 - 0
mtp/src/scenes/mtp/main.h

@@ -0,0 +1,54 @@
+#pragma once
+#include <gui/view.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/popup.h>
+#include <gui/scene_manager.h>
+#include <storage/storage.h>
+#include <furi_hal.h>
+
+typedef enum MTPState {
+    MTPStateOffline,
+    MTPStateReady,
+    MTPStateBusy,
+    MTPStateCanceled,
+    MTPStateError
+} MTPState;
+
+typedef struct {
+    uint32_t session_id;
+    bool session_open;
+} MTPSession;
+
+typedef struct FileHandle {
+    uint32_t handle;
+    char* path;
+    struct FileHandle* next;
+} FileHandle;
+
+typedef struct AppMTP {
+    Submenu* menu;
+    View* view;
+
+    bool usb_connected;
+
+    // usb stuff
+    FuriHalUsbInterface* old_usb;
+    FuriThread* worker_thread;
+    usbd_device* dev;
+
+    MTPState state;
+    bool is_working;
+    Storage* storage;
+    MTPSession session;
+
+    bool write_pending;
+
+    FileHandle* handles;
+} AppMTP;
+
+AppMTP* MTP_alloc();
+void MTP_free(void* ptr);
+View* MTP_get_view(void* ptr);
+void MTP_on_enter(void* context);
+bool MTP_on_event(void* context, SceneManagerEvent event);
+void MTP_on_exit(void* context);

+ 1247 - 0
mtp/src/scenes/mtp/mtp.c

@@ -0,0 +1,1247 @@
+#include <furi.h>
+#include <storage/storage.h>
+#include "main.h"
+#include "mtp.h"
+#include "usb_desc.h"
+#include "utils.h"
+
+#define BLOCK_SIZE 65536
+
+// Supported operations (example, add as needed)
+uint16_t supported_operations[] = {
+    MTP_OP_GET_DEVICE_INFO,
+    MTP_OP_OPEN_SESSION,
+    MTP_OP_CLOSE_SESSION,
+    MTP_OP_GET_STORAGE_IDS,
+    MTP_OP_GET_STORAGE_INFO,
+    MTP_OP_GET_NUM_OBJECTS,
+    MTP_OP_GET_OBJECT_HANDLES,
+    MTP_OP_GET_OBJECT_INFO,
+    MTP_OP_GET_OBJECT,
+    MTP_OP_SEND_OBJECT_INFO,
+    MTP_OP_SEND_OBJECT,
+    MTP_OP_DELETE_OBJECT,
+    MTP_OP_GET_DEVICE_PROP_DESC,
+    MTP_OP_GET_DEVICE_PROP_VALUE,
+    MTP_OP_GET_OBJECT_PROPS_SUPPORTED,
+    MTP_OP_SET_OBJECT_PROP_VALUE};
+
+uint16_t supported_object_props[] = {
+    MTP_PROP_STORAGE_ID,
+    MTP_PROP_OBJECT_FORMAT,
+    MTP_PROP_OBJECT_FILE_NAME,
+};
+
+// Supported device properties (example, add as needed)
+uint16_t supported_device_properties[] = {
+    0xd402, // Device friendly name
+};
+
+uint16_t supported_playback_formats[] = {
+    MTP_FORMAT_UNDEFINED,
+    MTP_FORMAT_ASSOCIATION,
+};
+
+void merge_path(char* target, char* base, char* name) {
+    // implement this way since strcat is unavailable
+
+    char* ptr = target;
+    strcpy(target, base);
+    ptr += strlen(base);
+    strcpy(ptr, "/");
+    ptr++;
+    strcpy(ptr, name);
+}
+
+// temporary storage for buffer
+MTPDataPersistence persistence;
+
+void mtp_handle_bulk(AppMTP* mtp, uint8_t* buffer, uint32_t length) {
+    UNUSED(mtp);
+    if(persistence.left_bytes > 0) {
+        FURI_LOG_I("MTP", "Left bytes: %ld", persistence.left_bytes);
+        handle_mtp_data_packet(mtp, buffer, length, 1);
+        return;
+    }
+
+    if(length < 12) {
+        FURI_LOG_E("MTP", "Invalid MTP packet");
+        return;
+    }
+
+    struct MTPHeader* header = (struct MTPHeader*)buffer;
+    uint16_t type = header->type;
+
+    if(type == MTP_TYPE_COMMAND) {
+        struct MTPContainer* container = (struct MTPContainer*)buffer;
+        handle_mtp_command(mtp, container);
+    } else if(type == MTP_TYPE_DATA) {
+        if(header->len > length) {
+            persistence.left_bytes = header->len;
+        }
+        handle_mtp_data_packet(mtp, buffer, length, 0);
+    } else if(type == MTP_TYPE_RESPONSE) {
+        handle_mtp_response(mtp, header);
+    } else {
+        FURI_LOG_W("MTP", "Unsupported MTP packet type: %d", type);
+    }
+}
+
+void setup_persistence(struct MTPContainer* container) {
+    persistence.transaction_id = container->header.transaction_id;
+    memcpy(persistence.params, container->params, sizeof(uint32_t) * 5);
+}
+
+void handle_mtp_data_packet(AppMTP* mtp, uint8_t* buffer, int32_t length, bool cont) {
+    UNUSED(mtp);
+    uint8_t* ptr = buffer;
+    if(!cont) {
+        struct MTPHeader* header = (struct MTPHeader*)buffer;
+
+        if(header->transaction_id != persistence.transaction_id) {
+            // the params value can not be trusted.
+            // reset the params
+            for(int i = 0; i < 5; i++) {
+                persistence.params[i] = 0;
+            }
+        }
+
+        persistence.global_buffer = NULL;
+        uint32_t transaction_id = header->transaction_id;
+        persistence.op = header->op;
+
+        FURI_LOG_I("MTP", "Handling MTP data: 0x%04x", header->op);
+        FURI_LOG_I("MTP", "Transaction ID: %ld", transaction_id);
+        FURI_LOG_I("MTP", "Length: %ld", length);
+
+        ptr = (uint8_t*)buffer + sizeof(struct MTPHeader);
+
+        switch(header->op) {
+        case MTP_OP_SEND_OBJECT_INFO:
+        case MTP_OP_SET_OBJECT_PROP_VALUE: {
+            persistence.global_buffer = malloc(sizeof(uint8_t) * 256);
+            persistence.buffer_offset = 0;
+            break;
+        }
+        case MTP_OP_SEND_OBJECT: {
+            persistence.buffer_offset = 0;
+            persistence.left_bytes = header->len - sizeof(struct MTPHeader);
+        }
+        }
+    }
+
+    // for speicific operations.
+    if(persistence.op == MTP_OP_SEND_OBJECT) {
+        uint32_t handle = persistence.prev_handle;
+        if(handle == 0) {
+            FURI_LOG_E("MTP", "Invalid handle for MTP_OP_SEND_OBJECT");
+            send_mtp_response(
+                mtp, 3, MTP_RESP_INVALID_OBJECT_HANDLE, persistence.transaction_id, NULL);
+            return;
+        }
+
+        char* path = get_path_from_handle(mtp, handle);
+        int bytes_length = length - (ptr - buffer);
+        int offset = persistence.buffer_offset;
+
+        FURI_LOG_I("MTP", "Writing to file: %s with offset %02x", path, offset);
+        if(persistence.current_file == NULL) {
+            persistence.current_file = storage_file_alloc(mtp->storage);
+            if(!storage_file_open(persistence.current_file, path, FSAM_WRITE, FSOM_OPEN_EXISTING)) {
+                FURI_LOG_E("MTP", "Failed to open file: %s", path);
+                send_mtp_response(
+                    mtp, 3, MTP_RESP_INVALID_OBJECT_HANDLE, persistence.transaction_id, NULL);
+                storage_file_free(persistence.current_file);
+                persistence.current_file = NULL;
+                return;
+            }
+        }
+
+        File* file = persistence.current_file;
+        // no need since the file is already opened
+        // storage_file_seek(file, offset, SEEK_SET);
+
+        storage_file_write(file, ptr, bytes_length);
+
+        persistence.buffer_offset += bytes_length;
+        persistence.left_bytes -= bytes_length;
+
+        if(persistence.left_bytes <= 0) {
+            send_mtp_response(mtp, 3, MTP_RESP_OK, persistence.transaction_id, NULL);
+            storage_file_close(file);
+            storage_file_free(file);
+
+            persistence.current_file = NULL;
+        }
+    }
+
+    if(persistence.global_buffer != NULL) {
+        memcpy(persistence.global_buffer + persistence.buffer_offset, ptr, length);
+        persistence.buffer_offset += length;
+
+        if(persistence.left_bytes > 0) {
+            persistence.left_bytes -= length;
+        }
+
+        FURI_LOG_I("MTP", "Buffer offset: %ld", persistence.buffer_offset);
+        FURI_LOG_I("MTP", "Left bytes: %ld", persistence.left_bytes);
+
+        if(persistence.left_bytes == 0) {
+            handle_mtp_data_complete(mtp);
+        }
+    }
+}
+
+void handle_mtp_data_complete(AppMTP* mtp) {
+    // dump the buffer
+    print_bytes("MTP Data", persistence.global_buffer, persistence.buffer_offset);
+
+    switch(persistence.op) {
+    case MTP_OP_SEND_OBJECT_INFO: {
+        // parse the object info - persistence.global_buffer doesn't contain MTPHeader
+        struct ObjectInfoHeader* info = (struct ObjectInfoHeader*)(persistence.global_buffer);
+
+        uint8_t* ptr = persistence.global_buffer + sizeof(struct ObjectInfoHeader);
+        ptr += 12;
+
+        // dump the ptr
+        print_bytes(
+            "MTP FileName", ptr, persistence.buffer_offset - sizeof(struct ObjectInfoHeader));
+
+        bool is_dir = info->format == MTP_FORMAT_ASSOCIATION;
+        char* name = NULL;
+        if(CheckMTPStringHasUnicode(ptr)) {
+            // unicode isn't supported
+            name = is_dir ? "New Folder" : "New File";
+        } else {
+            name = ReadMTPString(ptr);
+        }
+
+        // if the name is blank, generate random name
+        if(*name == 0) {
+            char* random_name = malloc(sizeof(char) * 10);
+            strcpy(random_name, "file_");
+            int random = rand() % 1000;
+            char random_str[4];
+            itoa(random, random_str, 10);
+            strcpy(random_name + 5, random_str);
+            name = random_name;
+        }
+
+        FURI_LOG_I("MTP", "Creating object: %s", name);
+
+        uint32_t storage_id = persistence.params[0];
+        uint32_t parent = persistence.params[1];
+
+        char* base_path = get_base_path_from_storage_id(storage_id);
+        if(base_path == NULL) {
+            FURI_LOG_E("MTP", "Invalid storage ID: %ld", storage_id);
+            send_mtp_response(
+                mtp, 3, MTP_RESP_INVALID_STORAGE_ID, persistence.transaction_id, NULL);
+            break;
+        }
+
+        if(parent != 0xffffffff) {
+            base_path = get_path_from_handle(mtp, parent);
+            if(base_path == NULL) {
+                FURI_LOG_E("MTP", "Invalid parent handle: %ld", parent);
+                send_mtp_response(
+                    mtp, 3, MTP_RESP_INVALID_OBJECT_HANDLE, persistence.transaction_id, NULL);
+                break;
+            }
+        }
+
+        char* full_path = malloc(sizeof(char) * 256);
+        merge_path(full_path, base_path, name);
+
+        FURI_LOG_I("MTP", "Format: %04x", info->format);
+
+        if(is_dir) {
+            if(!storage_dir_exists(mtp->storage, full_path)) {
+                if(!storage_simply_mkdir(mtp->storage, full_path)) {
+                    FURI_LOG_E("MTP", "Failed to create directory: %s", full_path);
+                    send_mtp_response(
+                        mtp, 3, MTP_RESP_GENERAL_ERROR, persistence.transaction_id, NULL);
+                    free(full_path);
+                    break;
+                }
+            }
+        } else {
+            if(!storage_file_exists(mtp->storage, full_path)) {
+                // create file
+                File* file = storage_file_alloc(mtp->storage);
+                if(!storage_file_open(file, full_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
+                    FURI_LOG_E("MTP", "Failed to create file: %s", full_path);
+                    send_mtp_response(
+                        mtp, 3, MTP_RESP_GENERAL_ERROR, persistence.transaction_id, NULL);
+                    storage_file_free(file);
+                    free(full_path);
+                    break;
+                }
+
+                storage_file_close(file);
+                storage_file_free(file);
+
+                persistence.prev_handle = issue_object_handle(mtp, full_path);
+            }
+        }
+
+        uint32_t handle = issue_object_handle(mtp, full_path);
+        persistence.params[2] = handle;
+
+        free(name);
+        free(full_path);
+        send_mtp_response(mtp, 3, MTP_RESP_OK, persistence.transaction_id, persistence.params);
+
+        break;
+    }
+    default:
+        FURI_LOG_W("MTP", "Unsupported MTP operation in bulk transfer: 0x%04x", persistence.op);
+        send_mtp_response(mtp, 3, MTP_RESP_UNKNOWN, persistence.transaction_id, NULL);
+        break;
+    }
+
+    free(persistence.global_buffer);
+    persistence.global_buffer = NULL;
+}
+
+void handle_mtp_response(AppMTP* mtp, struct MTPHeader* header) {
+    UNUSED(mtp);
+    FURI_LOG_I("MTP", "Handling MTP response: 0x%04x", header->op);
+    FURI_LOG_I("MTP", "Transaction ID: %ld", header->transaction_id);
+    FURI_LOG_I("MTP", "Has Data: %d", persistence.global_buffer != NULL);
+    FURI_LOG_I("MTP", "Data length: %ld", persistence.buffer_offset);
+}
+
+void handle_mtp_command(AppMTP* mtp, struct MTPContainer* container) {
+    uint16_t mtp_op = container->header.op;
+    FURI_LOG_I("MTP", "Handling MTP operation: 0x%04x", mtp_op);
+
+    switch(mtp_op) {
+    case MTP_OP_GET_DEVICE_INFO:
+        FURI_LOG_I("MTP", "GetDeviceInfo operation");
+        send_device_info(mtp, container->header.transaction_id);
+        // Process the GetDeviceInfo operation
+        break;
+    case MTP_OP_OPEN_SESSION:
+    case MTP_OP_CLOSE_SESSION:
+        //FURI_LOG_I("MTP", "Open/CloseSession operation (STUB)");
+        send_mtp_response(mtp, 3, MTP_RESP_OK, container->header.transaction_id, NULL);
+        break;
+    case MTP_OP_GET_STORAGE_IDS:
+        FURI_LOG_I("MTP", "GetStorageIDs operation");
+        send_storage_ids(mtp, container->header.transaction_id);
+        break;
+    case MTP_OP_GET_STORAGE_INFO: {
+        FURI_LOG_I("MTP", "GetStorageInfo operation");
+        uint8_t* info = malloc(sizeof(uint8_t) * 256);
+        int length = GetStorageInfo(mtp, container->params[0], info);
+        send_mtp_response_buffer(
+            mtp,
+            MTP_TYPE_DATA,
+            MTP_OP_GET_STORAGE_INFO,
+            container->header.transaction_id,
+            info,
+            length);
+        send_mtp_response_buffer(
+            mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, container->header.transaction_id, NULL, 0);
+        free(info);
+        break;
+    }
+    case MTP_OP_GET_OBJECT_HANDLES:
+        FURI_LOG_I("MTP", "GetObjectHandles operation");
+        if(container->params[1]) {
+            send_mtp_response_buffer(
+                mtp,
+                MTP_TYPE_RESPONSE,
+                MTP_RESP_SPEC_BY_FORMAT_UNSUPPORTED,
+                container->header.transaction_id,
+                NULL,
+                0);
+            break;
+        } else {
+            uint8_t* buffer = malloc(sizeof(uint8_t) * 256);
+            int length = GetObjectHandles(mtp, container->params[0], container->params[2], buffer);
+            send_mtp_response_buffer(
+                mtp,
+                MTP_TYPE_DATA,
+                MTP_OP_GET_OBJECT_HANDLES,
+                container->header.transaction_id,
+                buffer,
+                length);
+
+            send_mtp_response_buffer(
+                mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, container->header.transaction_id, NULL, 0);
+            free(buffer);
+        }
+        break;
+    case MTP_OP_GET_OBJECT_INFO: {
+        FURI_LOG_I("MTP", "GetObjectInfo operation");
+        uint8_t* buffer = malloc(sizeof(uint8_t) * 512);
+        int length = GetObjectInfo(mtp, container->params[0], buffer);
+
+        if(length < 0) {
+            send_mtp_response(
+                mtp,
+                MTP_TYPE_RESPONSE,
+                MTP_RESP_INVALID_OBJECT_HANDLE,
+                container->header.transaction_id,
+                NULL);
+            break;
+        }
+
+        send_mtp_response_buffer(
+            mtp,
+            MTP_TYPE_DATA,
+            MTP_OP_GET_OBJECT_INFO,
+            container->header.transaction_id,
+            buffer,
+            length);
+        free(buffer);
+
+        send_mtp_response_buffer(
+            mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, container->header.transaction_id, NULL, 0);
+        break;
+    }
+    case MTP_OP_GET_OBJECT_PROPS_SUPPORTED: {
+        FURI_LOG_I("MTP", "GetObjectPropsSupported operation");
+        uint8_t* buffer = malloc(sizeof(uint8_t) * 256);
+        uint32_t count = sizeof(supported_object_props) / sizeof(uint16_t);
+        memcpy(buffer, &count, sizeof(uint32_t));
+        memcpy(buffer + sizeof(uint32_t), supported_object_props, sizeof(uint16_t) * count);
+
+        int length = sizeof(uint32_t) + sizeof(uint16_t) * count;
+        send_mtp_response_buffer(
+            mtp,
+            MTP_TYPE_DATA,
+            MTP_OP_GET_OBJECT_PROPS_SUPPORTED,
+            container->header.transaction_id,
+            buffer,
+            length);
+        free(buffer);
+        send_mtp_response_buffer(
+            mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, container->header.transaction_id, NULL, 0);
+        break;
+    }
+    case MTP_OP_DELETE_OBJECT:
+        FURI_LOG_I("MTP", "DeleteObject operation");
+        if(DeleteObject(mtp, container->params[0])) {
+            send_mtp_response(mtp, 3, MTP_RESP_OK, container->header.transaction_id, NULL);
+        } else {
+            send_mtp_response(
+                mtp, 3, MTP_RESP_INVALID_OBJECT_HANDLE, container->header.transaction_id, NULL);
+        }
+        break;
+
+    case MTP_OP_GET_DEVICE_PROP_VALUE:
+        FURI_LOG_I("MTP", "GetDevicePropValue operation");
+        send_device_prop_value(mtp, container->header.transaction_id, container->params[0]);
+        // Process the GetDevicePropValue operation
+        break;
+    case MTP_OP_GET_DEVICE_PROP_DESC:
+        FURI_LOG_I("MTP", "GetDevicePropDesc operation");
+        send_device_prop_desc(mtp, container->header.transaction_id, container->params[0]);
+        // Process the GetDevicePropDesc operation
+        break;
+    // Handle bulk transfer specific operations
+    case MTP_OP_SEND_OBJECT_INFO:
+        FURI_LOG_I("MTP", "SendObjectInfo operation");
+        setup_persistence(container);
+        break;
+    case MTP_OP_SEND_OBJECT:
+        FURI_LOG_I("MTP", "SendObject operation");
+        setup_persistence(container);
+        break;
+    case MTP_OP_GET_OBJECT: {
+        FURI_LOG_I("MTP", "GetObject operation");
+        GetObject(mtp, container->header.transaction_id, container->params[0]);
+
+        break;
+    }
+    // Handle other bulk operations here...
+    default:
+        FURI_LOG_W("MTP", "Unsupported MTP operation in bulk transfer: 0x%04x", mtp_op);
+        send_mtp_response(mtp, 3, MTP_RESP_UNKNOWN, container->header.transaction_id, NULL);
+        break;
+    }
+}
+
+void send_storage_ids(AppMTP* mtp, uint32_t transaction_id) {
+    uint32_t count;
+    uint32_t storage_ids[3];
+    GetStorageIDs(mtp, storage_ids, &count);
+
+    FURI_LOG_I("MTP", "Sending storage IDs: %ld storages", count);
+    uint32_t payload[3] = {count, storage_ids[0], storage_ids[1]};
+    send_mtp_response_buffer(
+        mtp,
+        MTP_TYPE_DATA,
+        MTP_OP_GET_STORAGE_IDS,
+        transaction_id,
+        (uint8_t*)payload,
+        sizeof(uint32_t) * (count + 1));
+    send_mtp_response_buffer(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL, 0);
+}
+
+void send_device_info(AppMTP* mtp, uint32_t transaction_id) {
+    uint8_t* response = malloc(sizeof(uint8_t) * 256);
+    int length = BuildDeviceInfo(response);
+    send_mtp_response_buffer(
+        mtp, MTP_TYPE_DATA, MTP_OP_GET_DEVICE_INFO, transaction_id, response, length);
+    send_mtp_response_buffer(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL, 0);
+    free(response);
+}
+
+void send_device_prop_value(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code) {
+    uint8_t* response = malloc(sizeof(uint8_t) * 256);
+    int length = GetDevicePropValue(prop_code, response);
+    send_mtp_response_buffer(
+        mtp, MTP_TYPE_DATA, MTP_OP_GET_DEVICE_PROP_VALUE, transaction_id, response, length);
+    send_mtp_response_buffer(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL, 0);
+    free(response);
+}
+
+void send_device_prop_desc(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code) {
+    uint8_t* response = malloc(sizeof(uint8_t) * 256);
+    int length = GetDevicePropDesc(prop_code, response);
+    send_mtp_response_buffer(
+        mtp, MTP_TYPE_DATA, MTP_OP_GET_DEVICE_PROP_DESC, transaction_id, response, length);
+    send_mtp_response_buffer(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL, 0);
+    free(response);
+}
+
+char* get_path_from_handle(AppMTP* mtp, uint32_t handle) {
+    FileHandle* current = mtp->handles;
+    while(current != NULL) {
+        if(current->handle == handle) {
+            return current->path;
+        }
+        current = current->next;
+    }
+    return NULL;
+}
+
+uint32_t issue_object_handle(AppMTP* mtp, char* path) {
+    int handle = 0;
+    int length = strlen(path);
+    char* path_store = malloc(sizeof(char) * (length + 1));
+    strcpy(path_store, path);
+
+    if(mtp->handles == NULL) {
+        mtp->handles = malloc(sizeof(FileHandle));
+        mtp->handles->handle = handle;
+        mtp->handles->path = path_store;
+        mtp->handles->next = NULL;
+        return handle;
+    }
+
+    FileHandle* current = mtp->handles;
+    if(strcmp(current->path, path) == 0) {
+        return current->handle;
+    }
+
+    while(current->next != NULL) {
+        if(strcmp(current->path, path) == 0) {
+            return current->handle;
+        }
+        current = current->next;
+        handle++;
+    }
+
+    current->next = malloc(sizeof(FileHandle));
+    current = current->next;
+    handle++;
+
+    current->handle = handle;
+    current->path = path_store;
+    current->next = NULL;
+    return handle;
+}
+
+char* get_base_path_from_storage_id(uint32_t storage_id) {
+    if(storage_id == INTERNAL_STORAGE_ID) {
+        return STORAGE_INT_PATH_PREFIX;
+    } else if(storage_id == EXTERNAL_STORAGE_ID) {
+        return STORAGE_EXT_PATH_PREFIX;
+    }
+    return NULL;
+}
+
+int list_and_issue_handles(
+    AppMTP* mtp,
+    uint32_t storage_id,
+    uint32_t association,
+    uint32_t* handles) {
+    Storage* storage = mtp->storage;
+    char* base_path = get_base_path_from_storage_id(storage_id);
+    if(base_path == NULL) {
+        base_path = "";
+    }
+
+    File* dir = storage_file_alloc(storage);
+    if(association == 0xffffffff) {
+        // count the objects in the root directory
+        storage_dir_open(dir, base_path);
+    } else {
+        char* path = get_path_from_handle(mtp, association);
+        FURI_LOG_I("MTP", "Association path: %s", path);
+        if(path == NULL) {
+            return 0;
+        }
+        storage_dir_open(dir, path);
+        base_path = path;
+    }
+
+    int count = 0;
+    FileInfo fileinfo;
+    char* file_name = malloc(sizeof(char) * 256);
+    char* full_path = malloc(sizeof(char) * 256);
+    while(storage_dir_read(dir, &fileinfo, file_name, 256)) {
+        if(file_info_is_dir(&fileinfo)) {
+            FURI_LOG_I("MTP", "Found directory: %s", file_name);
+        } else {
+            FURI_LOG_I("MTP", "Found file: %s", file_name);
+        }
+
+        merge_path(full_path, base_path, file_name);
+        FURI_LOG_I("MTP", "Full path: %s", full_path);
+
+        uint32_t handle = issue_object_handle(mtp, full_path);
+        if(handles != NULL) {
+            handles[count] = handle;
+        }
+        count++;
+    }
+
+    FURI_LOG_I("MTP", "Getting number of objects in storage %ld", storage_id);
+    FURI_LOG_I("MTP", "Base path: %s", base_path);
+    FURI_LOG_I("MTP", "Association: %ld", association);
+    FURI_LOG_I("MTP", "Number of objects: %d", count);
+
+    storage_dir_close(dir);
+    storage_file_free(dir);
+    free(file_name);
+    free(full_path);
+    return count;
+}
+
+struct GetObjectContext {
+    File* file;
+};
+
+int GetObject_callback(void* ctx, uint8_t* buffer, int length) {
+    struct GetObjectContext* obj_ctx = (struct GetObjectContext*)ctx;
+    return storage_file_read(obj_ctx->file, buffer, length);
+}
+
+void GetObject(AppMTP* mtp, uint32_t transaction_id, uint32_t handle) {
+    char* path = get_path_from_handle(mtp, handle);
+    if(path == NULL) {
+        send_mtp_response(
+            mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL);
+        return;
+    }
+
+    FURI_LOG_I("MTP", "Getting object: %s", path);
+
+    Storage* storage = mtp->storage;
+    File* file = storage_file_alloc(storage);
+    if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
+        FURI_LOG_E("MTP", "Failed to open file: %s", path);
+        send_mtp_response(
+            mtp, MTP_TYPE_RESPONSE, MTP_RESP_INVALID_OBJECT_HANDLE, transaction_id, NULL);
+        return;
+    }
+
+    uint32_t size = storage_file_size(file);
+
+    struct GetObjectContext ctx = {
+        .file = file,
+    };
+
+    send_mtp_response_stream(
+        mtp, MTP_TYPE_DATA, MTP_OP_GET_OBJECT, transaction_id, &ctx, GetObject_callback, size);
+
+    send_mtp_response(mtp, MTP_TYPE_RESPONSE, MTP_RESP_OK, transaction_id, NULL);
+
+    storage_file_close(file);
+    storage_file_free(file);
+}
+
+int GetObjectInfo(AppMTP* mtp, uint32_t handle, uint8_t* buffer) {
+    ObjectInfoHeader* header = (ObjectInfoHeader*)buffer;
+    uint8_t* ptr = buffer + sizeof(ObjectInfoHeader);
+
+    char* path = get_path_from_handle(mtp, handle);
+    if(path == NULL) {
+        return -1;
+    }
+
+    FURI_LOG_I("MTP", "Getting object info for handle %ld", handle);
+    FURI_LOG_I("MTP", "Path: %s", path);
+
+    header->protection_status = 0;
+
+    Storage* storage = mtp->storage;
+    File* file = storage_file_alloc(storage);
+    uint16_t length;
+
+    FileInfo fileinfo;
+    FS_Error err = storage_common_stat(storage, path, &fileinfo);
+    if(err != FSE_OK) {
+        FURI_LOG_E("MTP", "Failed to get file info: %s", filesystem_api_error_get_desc(err));
+        return -1;
+    }
+
+    if(memcmp(path, STORAGE_INT_PATH_PREFIX, strlen(STORAGE_INT_PATH_PREFIX)) == 0) {
+        FURI_LOG_I("MTP", "Object in Internal storage");
+        header->storage_id = INTERNAL_STORAGE_ID;
+    } else if(memcmp(path, STORAGE_EXT_PATH_PREFIX, strlen(STORAGE_EXT_PATH_PREFIX)) == 0) {
+        FURI_LOG_I("MTP", "Object in External storage");
+        header->storage_id = EXTERNAL_STORAGE_ID;
+    } else {
+        return -1;
+    }
+
+    if(file_info_is_dir(&fileinfo)) {
+        FURI_LOG_I("MTP", "Directory");
+
+        header->format = MTP_FORMAT_ASSOCIATION;
+        header->association_type = 0x0001; // Generic folder
+    } else {
+        FURI_LOG_I("MTP", "Undefined type");
+
+        header->format = MTP_FORMAT_UNDEFINED;
+    }
+
+    header->compressed_size = fileinfo.size;
+
+    header->thumb_format = 0;
+    header->thumb_compressed_size = 0;
+    header->thumb_pix_width = 0;
+    header->thumb_pix_height = 0;
+    header->image_pix_width = 0;
+    header->image_pix_height = 0;
+    header->image_bit_depth = 0;
+
+    /*
+    // is on root directory (/int or /ext)
+    if(strchr(path + 1, '/') == NULL) {
+        header->parent_object = 0;
+    } else {
+        char* parent_path = malloc(sizeof(char) * 256);
+        strcpy(parent_path, path);
+        char* last_slash = strrchr(parent_path, '/');
+        *last_slash = '\0';
+        header->parent_object = issue_object_handle(mtp, parent_path);
+
+        free(parent_path);
+    }
+    */
+
+    char* file_name = strrchr(path, '/');
+    FURI_LOG_I("MTP", "File name: %s", file_name + 1);
+
+    WriteMTPString(ptr, file_name + 1, &length);
+    ptr += length;
+
+    // get created
+    WriteMTPString(ptr, "20240608T010702", &length);
+    ptr += length;
+
+    // get last modified
+    WriteMTPString(ptr, "20240608T010702", &length);
+    ptr += length;
+
+    // get keywords
+    WriteMTPString(ptr, "", &length);
+    ptr += length;
+
+    storage_file_free(file);
+
+    return ptr - buffer;
+}
+
+bool CheckMTPStringHasUnicode(uint8_t* buffer) {
+    uint8_t* base = buffer;
+    int len = *base;
+
+    uint16_t* ptr = (uint16_t*)(base + 1);
+
+    for(int i = 0; i < len; i++) {
+        if(ptr[i] > 0x7F) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+char* ReadMTPString(uint8_t* buffer) {
+    int len16 = *(uint8_t*)buffer;
+    if(len16 == 0) {
+        return "";
+    }
+
+    char* str = malloc(sizeof(char) * len16);
+
+    uint8_t* base = buffer + 1;
+    uint16_t* ptr = (uint16_t*)base;
+
+    for(int i = 0; i < len16; i++) {
+        str[i] = *ptr++;
+    }
+
+    return str;
+}
+
+int GetNumObjects(AppMTP* mtp, uint32_t storage_id, uint32_t association) {
+    return list_and_issue_handles(mtp, storage_id, association, NULL);
+}
+
+int GetObjectHandles(AppMTP* mtp, uint32_t storage_id, uint32_t association, uint8_t* buffer) {
+    uint8_t* ptr = buffer;
+    uint16_t length;
+
+    UNUSED(length);
+
+    // For now, just return a single object handle
+    int count = GetNumObjects(mtp, storage_id, association);
+
+    *(uint32_t*)ptr = count;
+    ptr += sizeof(uint32_t);
+
+    uint32_t* handles = (uint32_t*)ptr;
+    length = list_and_issue_handles(mtp, storage_id, association, handles);
+    ptr += length * sizeof(uint32_t);
+
+    return ptr - buffer;
+}
+
+int GetDevicePropValue(uint32_t prop_code, uint8_t* buffer) {
+    uint8_t* ptr = buffer;
+    uint16_t length;
+
+    switch(prop_code) {
+    case 0xd402:
+        WriteMTPString(ptr, "Flipper Zero", &length);
+        ptr += length;
+        break;
+    default:
+        // Unsupported property
+        break;
+    }
+
+    return ptr - buffer;
+}
+
+int GetDevicePropDesc(uint32_t prop_code, uint8_t* buffer) {
+    uint8_t* ptr = buffer;
+    uint16_t length;
+
+    switch(prop_code) {
+    case 0xd402:
+        // Device friendly name
+        *(uint16_t*)ptr = prop_code;
+        ptr += 2;
+
+        // type is string
+        *(uint16_t*)ptr = 0xffff;
+        ptr += 2;
+
+        // read-only
+        *(uint16_t*)ptr = 0x0000;
+        ptr += 2;
+
+        length = GetDevicePropValue(prop_code, ptr);
+        ptr += length;
+
+        length = GetDevicePropValue(prop_code, ptr);
+        ptr += length;
+
+        // no-form
+        *(uint16_t*)ptr = 0x0000;
+        ptr += 2;
+        break;
+    default:
+        // Unsupported property
+        break;
+    }
+
+    return ptr - buffer;
+}
+
+void send_mtp_response_stream(
+    AppMTP* mtp,
+    uint16_t resp_type,
+    uint16_t resp_code,
+    uint32_t transaction_id,
+    void* callback_context,
+    int (*callback)(void* ctx, uint8_t* buffer, int length),
+    uint32_t length) {
+    int chunk_idx = 0;
+
+    size_t buffer_available = MTP_MAX_PACKET_SIZE;
+    uint8_t* buffer = malloc(sizeof(uint8_t) * buffer_available);
+    uint8_t* ptr = buffer;
+
+    uint32_t sent_length = 0;
+    FURI_LOG_I("MTP", "Sending MTP response stream: %ld bytes", length);
+
+    do {
+        buffer_available = MTP_MAX_PACKET_SIZE;
+        ptr = buffer; // reset the pointer
+
+        if(chunk_idx == 0) {
+            struct MTPHeader* hdr = (struct MTPHeader*)buffer;
+            hdr->len = length + sizeof(*hdr);
+            hdr->type = resp_type;
+            hdr->op = resp_code;
+            hdr->transaction_id = transaction_id;
+
+            ptr += sizeof(*hdr);
+            buffer_available -= sizeof(*hdr);
+        }
+
+        FURI_LOG_I("MTP", "Remaining bytes for packet: %d", buffer_available);
+
+        int read_bytes = callback(callback_context, ptr, buffer_available);
+        uint32_t usb_bytes = (ptr - buffer) + read_bytes;
+        FURI_LOG_I("MTP", "USB packet size: %ld", usb_bytes);
+
+        usbd_ep_write(mtp->dev, MTP_EP_IN_ADDR, buffer, usb_bytes);
+
+        sent_length += usb_bytes;
+        chunk_idx++;
+
+        FURI_LOG_I(
+            "MTP",
+            "Sent chunk %d (currently sent: %ld/%ld)",
+            chunk_idx,
+            sent_length - sizeof(struct MTPHeader),
+            length);
+    } while(sent_length < length + sizeof(struct MTPHeader));
+
+    free(buffer);
+}
+
+int send_mtp_response_buffer_callback(void* ctx, uint8_t* buffer, int size) {
+    struct MTPResponseBufferContext* context = (struct MTPResponseBufferContext*)ctx;
+    if(context->buffer == NULL || context->size == 0) {
+        return 0;
+    }
+
+    uint32_t remaining = context->size - context->sent;
+    uint32_t to_send = size;
+    if(remaining < to_send) {
+        to_send = remaining;
+    }
+
+    memcpy(buffer, context->buffer + context->sent, to_send);
+    context->sent += to_send;
+
+    return to_send;
+}
+
+void send_mtp_response_buffer(
+    AppMTP* mtp,
+    uint16_t resp_type,
+    uint16_t resp_code,
+    uint32_t transaction_id,
+    uint8_t* buffer,
+    uint32_t size) {
+    struct MTPResponseBufferContext* ctx = malloc(sizeof(struct MTPResponseBufferContext));
+    ctx->buffer = buffer;
+    ctx->size = size;
+    ctx->sent = 0;
+
+    send_mtp_response_stream(
+        mtp, resp_type, resp_code, transaction_id, ctx, send_mtp_response_buffer_callback, size);
+    free(ctx);
+}
+
+void send_mtp_response(
+    AppMTP* mtp,
+    uint16_t resp_type,
+    uint16_t resp_code,
+    uint32_t transaction_id,
+    uint32_t* params) {
+    uint32_t response[5] = {0};
+
+    if(params != NULL) {
+        memcpy(response, params, sizeof(uint32_t) * 5);
+    }
+
+    send_mtp_response_buffer(
+        mtp, resp_type, resp_code, transaction_id, (uint8_t*)response, sizeof(response));
+}
+
+int mtp_handle_class_control(AppMTP* mtp, usbd_device* dev, usbd_ctlreq* req) {
+    UNUSED(dev);
+
+    int value = -1;
+    uint32_t handle = req->wIndex;
+    uint8_t* buffer = req->data;
+    uint32_t size = req->wLength;
+
+    UNUSED(handle);
+    UNUSED(buffer);
+    UNUSED(size);
+
+    switch(req->bRequest) {
+    case MTP_REQ_CANCEL:
+        // Handle Cancel request
+        value = 0;
+        break;
+    case MTP_REQ_GET_EXT_EVENT_DATA:
+        // Handle GetExtEventData request
+        value = 0;
+        break;
+    case MTP_REQ_RESET:
+        // Handle Reset request
+        mtp->state = MTPStateReady;
+        value = 0;
+        break;
+    case MTP_REQ_GET_DEVICE_STATUS:
+        // Handle GetDeviceStatus request
+        struct mtp_device_status* status = (struct mtp_device_status*)req->data;
+        status->wLength = sizeof(*status);
+        status->wCode = (mtp->state == MTPStateBusy) ? MTP_RESP_DEVICE_BUSY : MTP_RESP_OK;
+        value = status->wLength;
+        break;
+    default:
+        // Unsupported request
+        break;
+    }
+
+    return value;
+}
+int BuildDeviceInfo(uint8_t* buffer) {
+    uint8_t* ptr = buffer;
+    uint16_t length;
+
+    // Standard version
+    *(uint16_t*)ptr = 100;
+    ptr += sizeof(uint16_t);
+
+    // Vendor extension ID
+    *(uint32_t*)ptr = MTP_VENDOR_EXTENSION_ID;
+    ptr += sizeof(uint32_t);
+
+    // Vendor extension version
+    *(uint16_t*)ptr = MTP_VENDOR_EXTENSION_VERSION;
+    ptr += sizeof(uint16_t);
+
+    // Vendor extension description
+    WriteMTPString(ptr, "microsoft.com: 1.0;", &length);
+    ptr += length;
+
+    // Functional mode
+    *(uint16_t*)ptr = MTP_FUNCTIONAL_MODE;
+    ptr += sizeof(uint16_t);
+
+    // Operations supported
+    length = sizeof(supported_operations) / sizeof(uint16_t);
+    *(uint32_t*)ptr = length; // Number of supported operations
+    ptr += sizeof(uint32_t);
+
+    for(int i = 0; i < length; i++) {
+        *(uint16_t*)ptr = supported_operations[i];
+        ptr += sizeof(uint16_t);
+    }
+
+    // Supported events (example, add as needed)
+    *(uint32_t*)ptr = 0; // Number of supported events
+    ptr += sizeof(uint32_t);
+
+    length = sizeof(supported_device_properties) / sizeof(uint16_t);
+    *(uint32_t*)ptr = length; // Number of supported device properties
+    ptr += sizeof(uint32_t);
+
+    for(int i = 0; i < length; i++) {
+        *(uint16_t*)ptr = supported_device_properties[i];
+        ptr += sizeof(uint16_t);
+    }
+
+    // Supported capture formats (example, add as needed)
+    *(uint32_t*)ptr = 0; // Number of supported capture formats
+    ptr += sizeof(uint32_t);
+
+    // Supported playback formats (example, add as needed)
+    length = sizeof(supported_playback_formats) / sizeof(uint16_t);
+    *(uint32_t*)ptr = length; // Number of supported playback formats
+    ptr += sizeof(uint32_t);
+
+    for(int i = 0; i < length; i++) {
+        *(uint16_t*)ptr = supported_playback_formats[i];
+        ptr += sizeof(uint16_t);
+    }
+
+    // Manufacturer
+    WriteMTPString(ptr, USB_MANUFACTURER_STRING, &length);
+    ptr += length;
+
+    // Model
+    WriteMTPString(ptr, USB_DEVICE_MODEL, &length);
+    ptr += length;
+
+    // Device version
+    WriteMTPString(ptr, MTP_DEVICE_VERSION, &length);
+    ptr += length;
+
+    // Serial number
+    WriteMTPString(ptr, MTP_DEVICE_SERIAL, &length);
+    ptr += length;
+
+    return ptr - buffer;
+}
+
+void GetStorageIDs(AppMTP* mtp, uint32_t* storage_ids, uint32_t* count) {
+    SDInfo sd_info;
+    FS_Error err = storage_sd_info(mtp->storage, &sd_info);
+    storage_ids[0] = INTERNAL_STORAGE_ID;
+    if(err != FSE_OK) {
+        FURI_LOG_E("MTP", "SD Card not found");
+        *count = 1; // We have only one storage
+        return;
+    }
+
+    storage_ids[1] = EXTERNAL_STORAGE_ID;
+    *count = 2; // We have two storages: internal and external
+}
+
+int GetStorageInfo(AppMTP* mtp, uint32_t storage_id, uint8_t* buf) {
+    MTPStorageInfoHeader* info = (MTPStorageInfoHeader*)buf;
+    uint8_t* ptr = buf + sizeof(MTPStorageInfoHeader);
+    uint16_t length;
+
+    info->free_space_in_objects = 20ul;
+    info->filesystem_type = 0x0002; // Generic hierarchical
+    info->access_capability = 0x0000; // Read-write
+    FURI_LOG_I("MTP", "Getting storage info for storage ID %04lx", storage_id);
+
+    if(storage_id == INTERNAL_STORAGE_ID) {
+        // Fill in details for internal storage
+        info->storage_type = 0x0003; // Fixed RAM
+
+        // Fill in details for internal storage
+        uint64_t total_space;
+        uint64_t free_space;
+        FS_Error err = storage_common_fs_info(
+            mtp->storage, STORAGE_INT_PATH_PREFIX, &total_space, &free_space);
+        if(err != FSE_OK) {
+            info->max_capacity = 0;
+            info->free_space_in_bytes = 0;
+        } else {
+            info->max_capacity = total_space / BLOCK_SIZE;
+            info->free_space_in_bytes = free_space / BLOCK_SIZE;
+        }
+
+        WriteMTPString(ptr, "Internal Storage", &length);
+        ptr += length;
+
+        WriteMTPString(ptr, "INT_STORAGE", &length);
+        ptr += length;
+    } else if(storage_id == EXTERNAL_STORAGE_ID) {
+        SDInfo sd_info;
+        FS_Error err = storage_sd_info(mtp->storage, &sd_info);
+
+        // Fill in details for internal storage
+        info->storage_type = 0x0004; // Removable RAM
+
+        if(err != FSE_OK) {
+            info->max_capacity = 0;
+            info->free_space_in_bytes = 0;
+
+            FURI_LOG_E("MTP", "SD Card not found");
+        } else {
+            // Fill in details for external storage
+            info->max_capacity = (uint64_t)sd_info.kb_total * 1024 / BLOCK_SIZE;
+            info->free_space_in_bytes = (uint64_t)sd_info.kb_free * 1024 / BLOCK_SIZE;
+        }
+
+        WriteMTPString(ptr, "SD Card", &length);
+        ptr += length;
+
+        WriteMTPString(ptr, "SD_CARD", &length);
+        ptr += length;
+    }
+
+    // try to convert into big endian???
+    //info->max_capacity = __builtin_bswap64(info->max_capacity);
+    //info->free_space_in_bytes = __builtin_bswap64(info->free_space_in_bytes);
+
+    return ptr - buf;
+}
+
+// Microsoft-style UTF-16LE string:
+void WriteMTPString(uint8_t* buffer, const char* str, uint16_t* length) {
+    uint8_t* ptr = buffer;
+    uint8_t str_len = strlen(str);
+
+    FURI_LOG_I("MTP", "Writing MTP string: %s", str);
+    FURI_LOG_I("MTP", "String length: %d", str_len);
+
+    // extra handling for empty string
+    if(str_len == 0) {
+        *ptr = 0x00;
+
+        // that's it!
+        *length = 1;
+        return;
+    }
+
+    *ptr = str_len + 1; // Length byte (number of characters including the null terminator)
+    ptr++;
+    while(*str) {
+        *ptr++ = *str++;
+        *ptr++ = 0x00; // UTF-16LE encoding (add null byte for each character)
+    }
+    *ptr++ = 0x00; // Null terminator (UTF-16LE)
+    *ptr++ = 0x00;
+
+    FURI_LOG_I("MTP", "String byte length: %d", ptr - buffer);
+    *length = ptr - buffer;
+}
+
+void WriteMTPBEString(uint8_t* buffer, const char* str, uint16_t* length) {
+    uint8_t* ptr = buffer;
+    uint8_t str_len = strlen(str);
+    *ptr++ = str_len + 1; // Length byte (number of characters including the null terminator)
+    while(*str) {
+        *ptr++ = 0x00; // UTF-16BE encoding (add null byte for each character)
+        *ptr++ = *str++;
+    }
+    *ptr++ = 0x00; // Null terminator (UTF-16LE)
+    *ptr++ = 0x00;
+    *length = ptr - buffer;
+}
+
+bool DeleteObject(AppMTP* mtp, uint32_t handle) {
+    UNUSED(mtp);
+    FURI_LOG_I("MTP", "Deleting object %ld", handle);
+
+    char* path = get_path_from_handle(mtp, handle);
+    if(path == NULL) {
+        return false;
+    }
+
+    FileInfo fileinfo;
+    FS_Error err = storage_common_stat(mtp->storage, path, &fileinfo);
+
+    if(err == FSE_OK) {
+        if(file_info_is_dir(&fileinfo)) {
+            if(!storage_simply_remove_recursive(mtp->storage, path)) {
+                return false;
+            }
+        } else {
+            if(storage_common_remove(mtp->storage, path) != FSE_OK) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    return false;
+}

+ 208 - 0
mtp/src/scenes/mtp/mtp.h

@@ -0,0 +1,208 @@
+#pragma once
+
+// MTP Device Serial
+#define MTP_DEVICE_SERIAL "HakureiReimu"
+#define MTP_DEVICE_VERSION "1.0"
+
+#define MTP_STANDARD_VERSION 100
+#define MTP_VENDOR_EXTENSION_ID 0x6
+#define MTP_VENDOR_EXTENSION_VERSION 100
+#define MTP_FUNCTIONAL_MODE 0x0
+
+#define MTP_TYPE_COMMAND 0x1
+#define MTP_TYPE_DATA 0x2
+#define MTP_TYPE_RESPONSE 0x3
+
+#define MTP_REQ_CANCEL 0x64
+#define MTP_REQ_GET_EXT_EVENT_DATA 0x65
+#define MTP_REQ_RESET 0x66
+#define MTP_REQ_GET_DEVICE_STATUS 0x67
+
+// MTP Operation Codes
+#define MTP_OP_GET_DEVICE_INFO 0x1001
+#define MTP_OP_OPEN_SESSION 0x1002
+#define MTP_OP_CLOSE_SESSION 0x1003
+#define MTP_OP_GET_STORAGE_IDS 0x1004
+#define MTP_OP_GET_STORAGE_INFO 0x1005
+#define MTP_OP_GET_NUM_OBJECTS 0x1006
+#define MTP_OP_GET_OBJECT_HANDLES 0x1007
+#define MTP_OP_GET_OBJECT_INFO 0x1008
+#define MTP_OP_GET_OBJECT 0x1009
+
+#define MTP_OP_DELETE_OBJECT 0x100B
+#define MTP_OP_SEND_OBJECT_INFO 0x100C
+#define MTP_OP_SEND_OBJECT 0x100D
+#define MTP_OP_GET_DEVICE_PROP_DESC 0x1014
+#define MTP_OP_GET_DEVICE_PROP_VALUE 0x1015
+
+#define MTP_OP_GET_OBJECT_PROPS_SUPPORTED 0x9801
+#define MTP_OP_GET_OBJECT_PROP_DESC 0x9802
+#define MTP_OP_GET_OBJECT_PROP_VALUE 0x9803
+#define MTP_OP_SET_OBJECT_PROP_VALUE 0x9804
+
+// MTP Object Props
+#define MTP_PROP_STORAGE_ID 0xDC01
+#define MTP_PROP_OBJECT_FORMAT 0xDC02
+#define MTP_PROP_OBJECT_FILE_NAME 0xDC07
+
+// MTP Response Codes
+#define MTP_RESP_UNKNOWN 0x2000
+#define MTP_RESP_OK 0x2001
+#define MTP_RESP_GENERAL_ERROR 0x2002
+#define MTP_RESP_SESSION_NOT_OPEN 0x2003
+#define MTP_RESP_INVALID_TRANSACTION_ID 0x2004
+#define MTP_RESP_OPERATION_NOT_SUPPORTED 0x2005
+#define MTP_RESP_PARAMETER_NOT_SUPPORTED 0x2006
+#define MTP_RESP_INCOMPLETE_TRANSFER 0x2007
+#define MTP_RESP_INVALID_STORAGE_ID 0x2008
+#define MTP_RESP_INVALID_OBJECT_HANDLE 0x2009
+#define MTP_RESP_DEVICE_PROP_NOT_SUPPORTED 0x200A
+#define MTP_RESP_STORE_FULL 0x200C
+#define MTP_RESP_OBJECT_WRITE_PROTECTED 0x200D
+#define MTP_RESP_STORE_READ_ONLY 0x200E
+#define MTP_RESP_ACCESS_DENIED 0x200F
+#define MTP_RESP_NO_THUMBNAIL_PRESENT 0x2010
+#define MTP_RESP_SELF_TEST_FAILED 0x2011
+#define MTP_RESP_PARTIAL_DELETION 0x2012
+#define MTP_RESP_STORE_NOT_AVAILABLE 0x2013
+#define MTP_RESP_SPEC_BY_FORMAT_UNSUPPORTED 0x2014
+#define MTP_RESP_NO_VALID_OBJECT_INFO 0x2015
+#define MTP_RESP_INVALID_CODE_FORMAT 0x2016
+#define MTP_RESP_UNKNOWN_VENDOR_CODE 0x2017
+#define MTP_RESP_CAPTURE_ALREADY_TERMINATED 0x2018
+#define MTP_RESP_DEVICE_BUSY 0x2019
+
+// Storage IDs
+#define INTERNAL_STORAGE_ID 0x00010001
+#define EXTERNAL_STORAGE_ID 0x00020001
+
+// MTP Playback formats
+#define MTP_FORMAT_UNDEFINED 0x3000
+#define MTP_FORMAT_ASSOCIATION 0x3001
+
+typedef struct {
+    uint32_t handle;
+    char name[256];
+    uint32_t size;
+    uint32_t parent_handle;
+    bool is_directory;
+} MTPObject;
+
+struct mtp_device_status {
+    uint16_t wLength;
+    uint16_t wCode;
+};
+
+// Storage information structure based on MTP specification
+typedef struct {
+    uint16_t storage_type;
+    uint16_t filesystem_type;
+    uint16_t access_capability;
+    uint64_t max_capacity;
+    uint64_t free_space_in_bytes;
+    uint32_t free_space_in_objects;
+} MTPStorageInfoHeader;
+
+typedef struct ObjectInfoHeader {
+    uint32_t storage_id;
+    uint16_t format;
+    uint16_t protection_status;
+    uint32_t compressed_size;
+    uint16_t thumb_format;
+    uint32_t thumb_compressed_size;
+    uint32_t thumb_pix_width;
+    uint32_t thumb_pix_height;
+    uint32_t image_pix_width;
+    uint32_t image_pix_height;
+    uint32_t image_bit_depth;
+    uint32_t parent_object;
+    uint16_t association_type;
+    uint32_t association_desc;
+} ObjectInfoHeader;
+
+struct MTPHeader {
+    uint32_t len; // 0
+    uint16_t type; // 4
+    uint16_t op; // 6
+    uint32_t transaction_id; // 8
+};
+
+typedef struct MTPDataPersistence {
+    uint32_t left_bytes;
+    uint8_t* global_buffer;
+    uint32_t buffer_offset;
+
+    uint16_t op;
+    uint32_t transaction_id;
+    uint32_t params[5];
+
+    uint32_t prev_handle;
+    File* current_file;
+} MTPDataPersistence;
+
+struct MTPContainer {
+    struct MTPHeader header; // 0
+    uint32_t params[5]; // 12
+};
+
+extern uint16_t supported_operations[];
+extern uint16_t supported_device_properties[];
+extern uint16_t supported_playback_formats[];
+
+void OpenSession(AppMTP* mtp, uint32_t session_id);
+void CloseSession(AppMTP* mtp);
+void GetStorageIDs(AppMTP* mtp, uint32_t* storage_ids, uint32_t* count);
+int GetStorageInfo(AppMTP* mtp, uint32_t storage_id, uint8_t* buf);
+bool DeleteObject(AppMTP* mtp, uint32_t handle);
+
+void mtp_handle_bulk(AppMTP* mtp, uint8_t* buffer, uint32_t length);
+void handle_mtp_command(AppMTP* mtp, struct MTPContainer* container);
+void handle_mtp_data_packet(AppMTP* mtp, uint8_t* buffer, int32_t length, bool cont);
+void handle_mtp_response(AppMTP* mtp, struct MTPHeader* container);
+int mtp_handle_class_control(AppMTP* mtp, usbd_device* dev, usbd_ctlreq* req);
+void send_mtp_response(
+    AppMTP* mtp,
+    uint16_t resp_type,
+    uint16_t resp_code,
+    uint32_t transaction_id,
+    uint32_t* params);
+void send_mtp_response_buffer(
+    AppMTP* mtp,
+    uint16_t resp_type,
+    uint16_t resp_code,
+    uint32_t transaction_id,
+    uint8_t* buffer,
+    uint32_t size);
+void send_mtp_response_stream(
+    AppMTP* mtp,
+    uint16_t resp_type,
+    uint16_t resp_code,
+    uint32_t transaction_id,
+    void* callback_context,
+    int (*callback)(void* ctx, uint8_t* buffer, int length),
+    uint32_t length);
+int BuildDeviceInfo(uint8_t* buffer);
+
+bool CheckMTPStringHasUnicode(uint8_t* buffer);
+char* ReadMTPString(uint8_t* buffer);
+void WriteMTPString(uint8_t* buffer, const char* str, uint16_t* length);
+void send_device_info(AppMTP* mtp, uint32_t transaction_id);
+void send_storage_ids(AppMTP* mtp, uint32_t transaction_id);
+
+void send_device_prop_value(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code);
+void send_device_prop_desc(AppMTP* mtp, uint32_t transaction_id, uint32_t prop_code);
+int GetDevicePropValue(uint32_t prop_code, uint8_t* buffer);
+int GetDevicePropDesc(uint32_t prop_code, uint8_t* buffer);
+int GetObjectHandles(AppMTP* mtp, uint32_t storage_id, uint32_t association, uint8_t* buffer);
+int GetObjectInfo(AppMTP* mtp, uint32_t handle, uint8_t* buffer);
+void GetObject(AppMTP* mtp, uint32_t transaction_id, uint32_t handle);
+char* get_base_path_from_storage_id(uint32_t storage_id);
+char* get_path_from_handle(AppMTP* mtp, uint32_t handle);
+uint32_t issue_object_handle(AppMTP* mtp, char* path);
+void handle_mtp_data_complete(AppMTP* mtp);
+
+struct MTPResponseBufferContext {
+    uint8_t* buffer;
+    uint32_t size;
+    uint32_t sent;
+};

+ 266 - 0
mtp/src/scenes/mtp/usb.c

@@ -0,0 +1,266 @@
+#include "main.h"
+#include "usb.h"
+#include "usbd_core.h"
+#include "usb_std.h"
+#include "usb_desc.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_usb.h>
+
+#include "mtp.h"
+
+AppMTP* global_mtp;
+uint8_t last_ep;
+
+typedef enum {
+    EventExit = 1 << 0,
+    EventReset = 1 << 1,
+    EventRx = 1 << 2,
+    EventTx = 1 << 3,
+    EventRxOther = 1 << 4,
+    EventInterrupt = 1 << 5,
+    EventAll = EventExit | EventReset | EventRx | EventTx | EventRxOther | EventInterrupt,
+} MTPEvent;
+
+int32_t usb_mtp_worker(void* ctx) {
+    AppMTP* mtp = ctx;
+    usbd_device* dev = mtp->dev;
+    uint8_t buffer[MTP_MAX_PACKET_SIZE];
+
+    while(true) {
+        MTPEvent flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, FuriWaitForever);
+        if(flags & EventExit) {
+            FURI_LOG_W("MTP", "Worker thread exit");
+            break;
+        }
+
+        if(flags & EventReset) {
+            FURI_LOG_W("MTP", "USB reset");
+            // Handle USB reset if necessary
+        }
+
+        if(flags & EventTx) {
+            FURI_LOG_W("MTP", "USB Tx event");
+            // Handle MTP RX/TX data here
+            // Implement the logic for processing MTP requests, sending responses, etc.
+            mtp->write_pending = false;
+        }
+
+        if(flags & EventRx) {
+            FURI_LOG_W("MTP", "USB Rx event");
+
+            int32_t readBytes = usbd_ep_read(dev, MTP_EP_OUT_ADDR, buffer, MTP_MAX_PACKET_SIZE);
+            FURI_LOG_I("MTP", "Received %ld bytes on RX", readBytes);
+
+            if(readBytes > 0) {
+                mtp_handle_bulk(mtp, buffer, readBytes);
+            }
+
+            // Handle MTP RX/TX data here
+            // Implement the logic for processing MTP requests, sending responses, etc.
+        }
+
+        if(flags & EventRxOther) {
+            FURI_LOG_W("MTP", "USB Rx Other event. ep: %02x", last_ep);
+        }
+
+        if(flags & EventInterrupt) {
+            FURI_LOG_W("MTP", "USB Interrupt event");
+            // Handle MTP RX/TX data here
+            // Implement the logic for processing MTP requests, sending responses, etc.
+        }
+    }
+
+    return 0;
+}
+
+usbd_respond usb_mtp_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) {
+    UNUSED(callback);
+
+    AppMTP* mtp = global_mtp;
+    int value = -1;
+    uint16_t w_index = req->wIndex;
+    uint16_t w_value = req->wValue;
+    uint16_t w_length = req->wLength;
+
+    FURI_LOG_I(
+        "MTP",
+        "Control Request: bmRequestType=0x%02x, bRequest=0x%02x, wValue=0x%04x, wIndex=0x%04x, wLength=0x%04x",
+        req->bmRequestType,
+        req->bRequest,
+        w_value,
+        w_index,
+        w_length);
+
+    if(req->bRequest == USB_STD_GET_DESCRIPTOR) {
+        if((w_value >> 8) == USB_DTYPE_STRING && (w_value & 0xff) == 0xee) {
+            value = (w_length < usb_mtp_os_string_len ? w_length : usb_mtp_os_string_len);
+            memcpy(req->data, usb_mtp_os_string, value);
+            return usbd_ack;
+        }
+    } else if((req->bmRequestType & USB_REQ_TYPE) == USB_REQ_VENDOR) {
+        if(req->bRequest == 1 && (req->bmRequestType & USB_EPDIR_IN) &&
+           (w_index == 4 || w_index == 5)) {
+            value =
+                (w_length < sizeof(mtp_ext_config_desc) ? w_length : sizeof(mtp_ext_config_desc));
+            memcpy(req->data, &mtp_ext_config_desc, value);
+            return usbd_ack;
+        }
+    } else if((req->bmRequestType & USB_REQ_TYPE) == USB_REQ_CLASS) {
+        value = mtp_handle_class_control(mtp, dev, req);
+    }
+
+    if(value >= 0) {
+        if(value > w_length) {
+            value = w_length;
+        }
+        usbd_ep_write(dev, 0x00, req->data, value);
+        return usbd_ack;
+    }
+
+    return usbd_fail;
+}
+
+void usb_mtp_txrx(usbd_device* dev, uint8_t event, uint8_t ep) {
+    UNUSED(ep);
+    UNUSED(event);
+
+    AppMTP* mtp = global_mtp;
+    if(!mtp || mtp->dev != dev) return;
+
+    if(ep == MTP_EP_OUT_ADDR) {
+        furi_thread_flags_set(furi_thread_get_id(mtp->worker_thread), EventRx);
+    } else if(ep == MTP_EP_IN_ADDR) {
+        furi_thread_flags_set(furi_thread_get_id(mtp->worker_thread), EventTx);
+    } else {
+        furi_thread_flags_set(furi_thread_get_id(mtp->worker_thread), EventRxOther);
+        last_ep = ep;
+    }
+}
+
+void usb_mtp_interrupt(usbd_device* dev, uint8_t event, uint8_t ep) {
+    UNUSED(ep);
+    UNUSED(event);
+
+    FURI_LOG_I("MTP", "USB Interrupt");
+
+    AppMTP* mtp = global_mtp;
+    if(!mtp || mtp->dev != dev) return;
+
+    furi_thread_flags_set(furi_thread_get_id(mtp->worker_thread), EventInterrupt);
+}
+
+usbd_respond usb_mtp_ep_config(usbd_device* dev, uint8_t cfg) {
+    AppMTP* mtp = global_mtp;
+    FURI_LOG_I("MTP", "USB EP config: cfg=%d", cfg);
+
+    switch(cfg) {
+    case 0: // deconfigure
+        FURI_LOG_I("MTP", "USB deconfigure");
+        usbd_ep_deconfig(dev, MTP_EP_IN_ADDR);
+        usbd_ep_deconfig(dev, MTP_EP_OUT_ADDR);
+        usbd_ep_deconfig(dev, MTP_EP_INT_IN_ADDR);
+        usbd_reg_endpoint(dev, MTP_EP_IN_ADDR, NULL);
+        usbd_reg_endpoint(dev, MTP_EP_OUT_ADDR, NULL);
+        usbd_reg_endpoint(dev, MTP_EP_INT_IN_ADDR, NULL);
+        if(mtp != NULL) mtp->usb_connected = false;
+        break;
+    case 1: // configure
+        FURI_LOG_I("MTP", "USB configure");
+        usbd_ep_config(dev, MTP_EP_OUT_ADDR, USB_EPTYPE_BULK, MTP_MAX_PACKET_SIZE);
+        usbd_ep_config(dev, MTP_EP_IN_ADDR, USB_EPTYPE_BULK, MTP_MAX_PACKET_SIZE);
+        usbd_ep_config(dev, MTP_EP_INT_IN_ADDR, USB_EPTYPE_INTERRUPT, USB_MAX_INTERRUPT_SIZE);
+        usbd_reg_endpoint(dev, MTP_EP_OUT_ADDR, usb_mtp_txrx);
+        usbd_reg_endpoint(dev, MTP_EP_IN_ADDR, usb_mtp_txrx);
+        usbd_reg_endpoint(dev, MTP_EP_INT_IN_ADDR, usb_mtp_interrupt);
+        if(mtp != NULL) mtp->usb_connected = true;
+        //usbd_ep_write(dev, MTP_MAX_PACKET_SIZE, 0, 0);
+        break;
+    default:
+        return usbd_fail;
+    }
+
+    return usbd_ack;
+}
+
+void usb_mtp_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) {
+    UNUSED(intf);
+    if(dev == NULL) {
+        FURI_LOG_E("MTP", "dev is NULL");
+    }
+
+    // Disconnect the device first.
+    usbd_connect(dev, false);
+
+    AppMTP* mtp = ctx;
+    global_mtp = mtp;
+    mtp->dev = dev;
+
+    FURI_LOG_I("MTP", "Initializing MTP device");
+
+    // Register the configuration and control handlers
+
+    usbd_reg_config(dev, usb_mtp_ep_config);
+    usbd_reg_control(dev, usb_mtp_control);
+    FURI_LOG_I("MTP", "Registered configuration and control handlers");
+
+    // Connect the device
+    usbd_connect(dev, true);
+    FURI_LOG_I("MTP", "Connected device");
+
+    // Initialize worker thread
+    mtp->worker_thread = furi_thread_alloc();
+    furi_thread_set_name(mtp->worker_thread, "FlipperMTPUsb");
+    furi_thread_set_stack_size(mtp->worker_thread, 1024);
+    furi_thread_set_context(mtp->worker_thread, ctx);
+    furi_thread_set_callback(mtp->worker_thread, usb_mtp_worker);
+    furi_thread_start(mtp->worker_thread);
+    FURI_LOG_I("MTP", "Started worker thread");
+}
+
+void usb_mtp_deinit(usbd_device* dev) {
+    usbd_reg_config(dev, NULL);
+    usbd_reg_control(dev, NULL);
+    FURI_LOG_I("MTP", "Unregistered configuration and control handlers");
+
+    AppMTP* mtp = global_mtp;
+    if(!mtp || mtp->dev != dev) {
+        FURI_LOG_E("MTP", "deinit mtp_cur leak");
+        return;
+    }
+
+    global_mtp = NULL;
+
+    furi_assert(mtp->worker_thread);
+    FURI_LOG_I("MTP", "Worker thread Condition pass");
+
+    furi_thread_flags_set(furi_thread_get_id(mtp->worker_thread), EventExit);
+    furi_thread_join(mtp->worker_thread);
+    furi_thread_free(mtp->worker_thread);
+    mtp->worker_thread = NULL;
+
+    mtp->usb_connected = false;
+    mtp->is_working = false;
+
+    FURI_LOG_I("MTP", "Deinit worker thread");
+}
+
+void usb_mtp_wakeup(usbd_device* dev) {
+    AppMTP* mtp = global_mtp;
+    if(!mtp || mtp->dev != dev) return;
+
+    FURI_LOG_I("MTP", "USB wakeup");
+
+    mtp->usb_connected = true;
+    UNUSED(dev);
+}
+
+void usb_mtp_suspend(usbd_device* dev) {
+    AppMTP* mtp = global_mtp;
+    if(!mtp || mtp->dev != dev) return;
+
+    FURI_LOG_I("MTP", "USB suspend");
+
+    mtp->usb_connected = false;
+    furi_thread_flags_set(furi_thread_get_id(mtp->worker_thread), EventReset);
+}

+ 21 - 0
mtp/src/scenes/mtp/usb.h

@@ -0,0 +1,21 @@
+#pragma once
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_usb.h>
+
+/* === START furi_hal_usb_i.h === */
+// https://github.com/flipperdevices/flipperzero-firmware/blob/03196fa11007c0f1e002cbb0b82102d8492456b5/targets/f7/furi_hal/furi_hal_usb_i.h#L5
+#define USB_EP0_SIZE 8
+
+enum UsbDevDescStr {
+    UsbDevLang = 0,
+    UsbDevManuf = 1,
+    UsbDevProduct = 2,
+    UsbDevSerial = 3,
+};
+/* ===   END furi_hal_usb_i.h === */
+
+void usb_mtp_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx);
+void usb_mtp_deinit(usbd_device* dev);
+void usb_mtp_wakeup(usbd_device* dev);
+void usb_mtp_suspend(usbd_device* dev);

+ 144 - 0
mtp/src/scenes/mtp/usb_desc.c

@@ -0,0 +1,144 @@
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_usb.h>
+#include "usb.h"
+#include "usb_desc.h"
+
+const struct usb_string_descriptor dev_manuf_desc = USB_STRING_DESC(USB_MANUFACTURER_STRING);
+const struct usb_string_descriptor dev_prod_desc = USB_STRING_DESC(USB_PRODUCT_STRING);
+
+const uint8_t usb_mtp_os_string[] = {
+    18,
+    USB_DTYPE_STRING,
+    /* Signature field: "MSFT100" */
+    'M',
+    0,
+    'S',
+    0,
+    'F',
+    0,
+    'T',
+    0,
+    '1',
+    0,
+    '0',
+    0,
+    '0',
+    0,
+    /* vendor code */
+    1,
+    /* padding */
+    0};
+const uint8_t usb_mtp_os_string_len = sizeof(usb_mtp_os_string);
+
+const struct usb_device_descriptor usb_mtp_dev_descr = {
+    .bLength = sizeof(struct usb_device_descriptor),
+    .bDescriptorType = USB_DTYPE_DEVICE,
+    .bcdUSB = VERSION_BCD(2, 0, 0),
+    .bDeviceClass = USB_CLASS_STILL_IMAGE, // MTP falls under Still Image class
+    .bDeviceSubClass = USB_SUBCLASS_MTP, // Subclass for MTP
+    .bDeviceProtocol = USB_PROTO_MTP, // Protocol for MTP
+    .bMaxPacketSize0 = USB_EP0_SIZE,
+    .idVendor = 0x0483, // STMicroelectronics
+    .idProduct = 0x5741, // Custom Product ID
+    .bcdDevice = BCD_VERSION,
+    .iManufacturer = UsbDevManuf, // UsbDevManuf
+    .iProduct = UsbDevProduct, // UsbDevProduct
+    .iSerialNumber = UsbDevSerial, // UsbDevSerial
+    .bNumConfigurations = 1,
+};
+
+const struct MtpDescriptor usb_mtp_cfg_descr = {
+    .config =
+        {
+            .bLength = sizeof(struct usb_config_descriptor),
+            .bDescriptorType = USB_DTYPE_CONFIGURATION,
+            .wTotalLength = sizeof(struct MtpDescriptor),
+            .bNumInterfaces = 1,
+            .bConfigurationValue = USB_CONF_VAL_MTP,
+            .iConfiguration = NO_DESCRIPTOR,
+            .bmAttributes = USB_CFG_ATTR_RESERVED | USB_CFG_ATTR_SELFPOWERED,
+            .bMaxPower = USB_CFG_POWER_MA(100),
+        },
+    .intf =
+        {
+            .bLength = sizeof(struct usb_interface_descriptor),
+            .bDescriptorType = USB_DTYPE_INTERFACE,
+            .bInterfaceNumber = 0,
+            .iInterface = UsbDevManuf,
+            .bAlternateSetting = 0,
+            .bNumEndpoints = 3,
+            .bInterfaceClass = USB_CLASS_STILL_IMAGE,
+            .bInterfaceSubClass = USB_SUBCLASS_MTP,
+            .bInterfaceProtocol = USB_PROTO_MTP,
+        },
+    .ep_in =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = MTP_EP_IN_ADDR,
+            .bmAttributes = USB_EPTYPE_BULK,
+            .wMaxPacketSize = MTP_MAX_PACKET_SIZE,
+            .bInterval = 0,
+        },
+    .ep_out =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = MTP_EP_OUT_ADDR,
+            .bmAttributes = USB_EPTYPE_BULK,
+            .wMaxPacketSize = MTP_MAX_PACKET_SIZE,
+            .bInterval = 0,
+        },
+    .ep_int_in =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = MTP_EP_INT_IN_ADDR,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = USB_MAX_INTERRUPT_SIZE,
+            .bInterval = 6,
+        },
+};
+
+/* MICROSOFT STUFF */
+const struct mtp_ext_config_desc mtp_ext_config_desc = {
+    .header =
+        {
+            .dwLength = sizeof(mtp_ext_config_desc),
+            .bcdVersion = BCD_VERSION,
+            .wIndex = 0x04,
+            .bCount = 1,
+        },
+    .function =
+        {
+            .bFirstInterfaceNumber = 0,
+            .bInterfaceCount = 1,
+            .compatibleID =
+                {
+                    'M',
+                    'T',
+                    'P',
+                    0,
+                    0,
+                    0,
+                    0,
+                    0,
+                },
+        },
+};
+
+FuriHalUsbInterface usb_mtp_interface = {
+    .init = usb_mtp_init,
+    .deinit = usb_mtp_deinit,
+    .wakeup = usb_mtp_wakeup,
+    .suspend = usb_mtp_suspend,
+
+    .dev_descr = (struct usb_device_descriptor*)&usb_mtp_dev_descr,
+
+    .str_manuf_descr = (void*)&dev_manuf_desc,
+    .str_prod_descr = (void*)&dev_prod_desc,
+    .str_serial_descr = NULL,
+
+    .cfg_descr = (void*)&usb_mtp_cfg_descr,
+};

+ 65 - 0
mtp/src/scenes/mtp/usb_desc.h

@@ -0,0 +1,65 @@
+#pragma once
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_usb.h>
+
+#define USB_MANUFACTURER_STRING "Flipper Devices Inc."
+#define USB_PRODUCT_STRING "Flipper Zero Virtual MTP Device"
+#define USB_DEVICE_MODEL "Flipper Zero"
+
+#define USB_SUBCLASS_MTP 0x01
+#define USB_PROTO_MTP 0x01
+#define USB_CONF_VAL_MTP 1
+
+// Endpoint Addresses
+#define MTP_EP_IN_ADDR 0x81
+#define MTP_EP_OUT_ADDR 0x02
+#define MTP_EP_INT_IN_ADDR 0x03
+
+#define MTP_MAX_PACKET_SIZE 64
+#define USB_MAX_INTERRUPT_SIZE 28
+#define BCD_VERSION VERSION_BCD(1, 0, 0)
+
+struct MtpDescriptor {
+    struct usb_config_descriptor config;
+    struct usb_interface_descriptor intf;
+
+    // I/O endpoints
+    struct usb_endpoint_descriptor ep_in;
+    struct usb_endpoint_descriptor ep_out;
+
+    // Interrupt endpoint
+    struct usb_endpoint_descriptor ep_int_in;
+} __attribute__((packed));
+
+extern const struct usb_string_descriptor dev_manuf_desc;
+extern const struct usb_string_descriptor dev_prod_desc;
+extern const struct usb_device_descriptor usb_mtp_dev_descr;
+extern const struct MtpDescriptor usb_mtp_cfg_descr;
+extern FuriHalUsbInterface usb_mtp_interface;
+extern const uint8_t usb_mtp_os_string[];
+extern const uint8_t usb_mtp_os_string_len;
+
+/* Microsoft Extended Configuration Descriptor Header Section */
+struct mtp_ext_config_desc_header {
+    uint32_t dwLength;
+    uint16_t bcdVersion;
+    uint16_t wIndex;
+    uint8_t bCount;
+    uint8_t reserved[7];
+};
+/* Microsoft Extended Configuration Descriptor Function Section */
+struct mtp_ext_config_desc_function {
+    uint8_t bFirstInterfaceNumber;
+    uint8_t bInterfaceCount;
+    uint8_t compatibleID[8];
+    uint8_t subCompatibleID[8];
+    uint8_t reserved[6];
+};
+
+struct mtp_ext_config_desc {
+    struct mtp_ext_config_desc_header header;
+    struct mtp_ext_config_desc_function function;
+};
+
+extern const struct mtp_ext_config_desc mtp_ext_config_desc;

+ 38 - 0
mtp/src/scenes/mtp/utils.c

@@ -0,0 +1,38 @@
+#include "utils.h"
+
+const char hex[] = "0123456789ABCDEF";
+
+void print_bytes(char* tag, uint8_t* bytes, size_t len) {
+    FURI_LOG_I("MTP", "Dumping bytes - TAG: %s", tag);
+    int lines = len > 0 ? (len / 16) + 1 : 0;
+    size_t last_line_len = len % 16;
+    char* line = (char*)malloc(16 * 3 + 3 + 16 + 1);
+    for(int i = 0; i < lines; i++) {
+        int line_len = i == lines - 1 ? last_line_len : 16;
+        for(int j = 0; j < 16; j++) {
+            if(j >= line_len) {
+                line[j * 3] = ' ';
+                line[j * 3 + 1] = ' ';
+                line[j * 3 + 2] = ' ';
+                continue;
+            }
+
+            // write hex without sprintf
+            line[j * 3] = hex[bytes[i * 16 + j] >> 4];
+            line[j * 3 + 1] = hex[bytes[i * 16 + j] & 0x0F];
+            line[j * 3 + 2] = ' ';
+        }
+        line[16 * 3] = ' ';
+        line[16 * 3 + 1] = '|';
+        line[16 * 3 + 2] = ' ';
+        for(int j = 0; j < line_len; j++) {
+            // write ascii without sprintf
+            line[16 * 3 + 3 + j] =
+                bytes[i * 16 + j] >= 32 && bytes[i * 16 + j] <= 126 ? bytes[i * 16 + j] : '.';
+        }
+        line[16 * 3 + 3 + line_len] = '\0';
+        FURI_LOG_I("MTP", line);
+    }
+    free(line);
+    FURI_LOG_I("MTP", "End of dump - TAG: %s", tag);
+}

+ 3 - 0
mtp/src/scenes/mtp/utils.h

@@ -0,0 +1,3 @@
+#pragma once
+#include <furi.h>
+void print_bytes(char* tag, uint8_t* bytes, size_t len);

+ 101 - 0
mtp/src/scenes/register.c

@@ -0,0 +1,101 @@
+#include "../main.h"
+
+/**
+ * SceneManagerHandlers initialization using the macro.
+ */
+void (*const scene_on_enter_handlers[])(void* context) = {
+#define SCENE_ACTION(scene) scene##_on_enter,
+#include "list.h"
+#undef SCENE_ACTION
+};
+
+bool (*const scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#define SCENE_ACTION(scene) scene##_on_event,
+#include "list.h"
+#undef SCENE_ACTION
+};
+
+void (*const scene_on_exit_handlers[])(void* context) = {
+#define SCENE_ACTION(scene) scene##_on_exit,
+#include "list.h"
+#undef SCENE_ACTION
+};
+
+const SceneManagerHandlers scene_handlers = {
+    .on_enter_handlers = scene_on_enter_handlers,
+    .on_event_handlers = scene_on_event_handlers,
+    .on_exit_handlers = scene_on_exit_handlers,
+    .scene_num = AppSceneNum,
+};
+
+/**
+ * Register all scenes.
+ */
+
+void register_scenes(App* app) {
+    app->scene_manager = scene_manager_alloc(&scene_handlers, app);
+    furi_assert(app->scene_manager != NULL, "Failed to allocate scene manager.");
+
+    app->view_dispatcher = view_dispatcher_alloc();
+    furi_assert(app->view_dispatcher != NULL, "Failed to allocate view dispatcher.");
+
+    if(app->allocated_scenes == NULL) {
+        app->allocated_scenes = (void**)malloc(sizeof(void*) * AppSceneNum);
+    }
+
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, scene_handler_event_forwarder);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, scene_handler_navigation_forwarder);
+
+    View* view = NULL;
+#define SCENE_ACTION(scene)                                                                   \
+    app->allocated_scenes[scene] = (void*)scene##_alloc();                                    \
+    furi_assert(                                                                              \
+        app->allocated_scenes[scene] != NULL, "Failed to allocate scene: " STRINGIFY(scene)); \
+    view = scene##_get_view(app->allocated_scenes[scene]);                                    \
+    furi_assert(view != NULL, "Failed to get view for scene: " STRINGIFY(scene));             \
+    view_dispatcher_add_view(app->view_dispatcher, scene, view);
+#include "list.h"
+#undef SCENE_ACTION
+}
+
+/**
+ * Free all scenes.
+ */
+void free_scenes(App* app) {
+    FURI_LOG_I("DemoApp", "Freeing scenes.");
+    furi_assert(app != NULL, "App is NULL.");
+    void* tmp;
+#define SCENE_ACTION(scene)                                                                    \
+    if(app->allocated_scenes != NULL) {                                                        \
+        tmp = app->allocated_scenes[scene];                                                    \
+        app->allocated_scenes[scene] = NULL;                                                   \
+        FURI_LOG_I("DemoApp", "Freeing scene " STRINGIFY(scene) ".");                          \
+        if(tmp != NULL) scene##_free(tmp);                                                     \
+        FURI_LOG_I("DemoApp", "Free'd scene " STRINGIFY(scene) ".");                           \
+    }                                                                                          \
+    if(app->view_dispatcher != NULL) view_dispatcher_remove_view(app->view_dispatcher, scene); \
+    FURI_LOG_I("DemoApp", "Removed from dispatcher " STRINGIFY(scene) ".");
+
+#include "list.h"
+#undef SCENE_ACTION
+
+    FURI_LOG_I("DemoApp", "Freeing allocated scenes.");
+    furi_assert(app->allocated_scenes != NULL, "Allocated scenes is NULL.");
+    free(app->allocated_scenes);
+    app->allocated_scenes = NULL;
+
+    FURI_LOG_I("DemoApp", "Freeing View dispatcher.");
+    furi_assert(app->view_dispatcher != NULL, "View dispatcher is NULL.");
+    view_dispatcher_free(app->view_dispatcher);
+
+    FURI_LOG_I("DemoApp", "Freeing SceneManager");
+    furi_assert(app->scene_manager != NULL, "Scene manager is NULL.");
+    scene_manager_free(app->scene_manager);
+
+    FURI_LOG_I("DemoApp", "Freeing App");
+    free(app);
+}

+ 5 - 0
mtp/src/scenes/register.h

@@ -0,0 +1,5 @@
+#pragma once
+#include "../main.h"
+
+void register_scenes(App* app);
+void free_scenes(App* app);