瀏覽代碼

Add mass_storage from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: mass_storage
git-subtree-mainline: e030e184de83d9245b4e8722d626e3b20f81daf5
git-subtree-split: a9251ed214cd7b6702fd18ab3f8ae3fa8afd8370
Willy-JL 1 年之前
父節點
當前提交
8f56fb7ba6

+ 13 - 0
mass_storage/.catalog/CHANGELOG.md

@@ -0,0 +1,13 @@
+## v.1.2
+
+ * Fix deadlock on disk eject
+ * Locked USB notification
+
+## v.1.1
+
+ * Faster image creation
+ * Speed and transfer size in UI
+
+## v.1.0
+
+Initial release.

+ 3 - 0
mass_storage/.catalog/README.md

@@ -0,0 +1,3 @@
+# USB Mass Storage emulator
+
+This application allows you to use your Flipper Zero as a (very slow) USB mass storage device. You can create a disk image of up to 64MB, and then format and mount it on your computer via USB. All the images are stored on the SD card, which allows you to have multiple images and switch between them.

二進制
mass_storage/.catalog/screenshots/1.png


二進制
mass_storage/.catalog/screenshots/2.png


+ 1 - 0
mass_storage/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev base_pack/mass_storage

+ 16 - 0
mass_storage/application.fam

@@ -0,0 +1,16 @@
+App(
+    appid="mass_storage",
+    name="Mass Storage",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="mass_storage_app",
+    requires=[
+        "gui",
+        "dialogs",
+    ],
+    stack_size=2 * 1024,
+    fap_description="Implements a mass storage device over USB for disk images",
+    fap_version="1.3",
+    fap_icon="assets/mass_storage_10px.png",
+    fap_icon_assets="assets",
+    fap_category="USB",
+)

二進制
mass_storage/assets/ActiveConnection_50x64.png


二進制
mass_storage/assets/Drive_112x35.png


二進制
mass_storage/assets/mass_storage_10px.png


+ 266 - 0
mass_storage/helpers/mass_storage_scsi.c

@@ -0,0 +1,266 @@
+#include "mass_storage_scsi.h"
+
+#include <core/log.h>
+
+#define TAG "MassStorageSCSI"
+
+#define SCSI_TEST_UNIT_READY (0x00)
+#define SCSI_REQUEST_SENSE (0x03)
+#define SCSI_INQUIRY (0x12)
+#define SCSI_READ_FORMAT_CAPACITIES (0x23)
+#define SCSI_READ_CAPACITY_10 (0x25)
+#define SCSI_MODE_SENSE_6 (0x1A)
+#define SCSI_READ_10 (0x28)
+#define SCSI_PREVENT_MEDIUM_REMOVAL (0x1E)
+#define SCSI_START_STOP_UNIT (0x1B)
+#define SCSI_WRITE_10 (0x2A)
+
+bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len) {
+    if(!len) {
+        scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
+        scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
+        return false;
+    }
+    FURI_LOG_T(TAG, "START %02X", cmd[0]);
+    scsi->cmd = cmd;
+    scsi->cmd_len = len;
+    scsi->rx_done = false;
+    scsi->tx_done = false;
+    switch(cmd[0]) {
+    case SCSI_WRITE_10: {
+        if(len < 10) return false;
+        scsi->write_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
+        scsi->write_10.count = cmd[7] << 8 | cmd[8];
+        FURI_LOG_D(TAG, "SCSI_WRITE_10 %08lX %04X", scsi->write_10.lba, scsi->write_10.count);
+        return true;
+    }; break;
+    case SCSI_READ_10: {
+        if(len < 10) return false;
+        scsi->read_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
+        scsi->read_10.count = cmd[7] << 8 | cmd[8];
+        FURI_LOG_D(TAG, "SCSI_READ_10 %08lX %04X", scsi->read_10.lba, scsi->read_10.count);
+        return true;
+    }; break;
+    }
+    return true;
+}
+
+bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len) {
+    FURI_LOG_T(TAG, "RX %02X len %lu", scsi->cmd[0], len);
+    if(scsi->rx_done) return false;
+    switch(scsi->cmd[0]) {
+    case SCSI_WRITE_10: {
+        uint32_t block_size = SCSI_BLOCK_SIZE;
+        uint16_t blocks = len / block_size;
+        bool result =
+            scsi->fn.write(scsi->fn.ctx, scsi->write_10.lba, blocks, data, blocks * block_size);
+        scsi->write_10.lba += blocks;
+        scsi->write_10.count -= blocks;
+        if(!scsi->write_10.count) {
+            scsi->rx_done = true;
+        }
+        return result;
+    }; break;
+    default: {
+        FURI_LOG_W(TAG, "unexpected scsi rx data cmd=%02X", scsi->cmd[0]);
+        scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
+        scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
+        return false;
+    }; break;
+    }
+}
+
+bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap) {
+    FURI_LOG_T(TAG, "TX %02X cap %lu", scsi->cmd[0], cap);
+    if(scsi->tx_done) return false;
+    switch(scsi->cmd[0]) {
+    case SCSI_REQUEST_SENSE: {
+        FURI_LOG_D(TAG, "SCSI_REQUEST_SENSE");
+        if(cap < 18) return false;
+        memset(data, 0, cap);
+        data[0] = 0x70; // fixed format sense data
+        data[1] = 0; // obsolete
+        data[2] = scsi->sk; // sense key
+        data[3] = 0; // information
+        data[4] = 0; // information
+        data[5] = 0; // information
+        data[6] = 0; // information
+        data[7] = 10; // additional sense length (len-8)
+        data[8] = 0; // command specific information
+        data[9] = 0; // command specific information
+        data[10] = 0; // command specific information
+        data[11] = 0; // command specific information
+        data[12] = scsi->asc; // additional sense code
+        data[13] = 0; // additional sense code qualifier
+        data[14] = 0; // field replaceable unit code
+        data[15] = 0; // sense key specific information
+        data[16] = 0; // sense key specific information
+        data[17] = 0; // sense key specific information
+        *len = 18;
+        scsi->sk = 0;
+        scsi->asc = 0;
+        scsi->tx_done = true;
+        return true;
+    }; break;
+    case SCSI_INQUIRY: {
+        FURI_LOG_D(TAG, "SCSI_INQUIRY");
+        if(scsi->cmd_len < 5) return false;
+
+        if(cap < 36) return false;
+
+        bool evpd = scsi->cmd[1] & 1;
+        uint8_t page_code = scsi->cmd[2];
+        if(evpd == 0) {
+            if(page_code != 0) return false;
+
+            data[0] = 0x00; // device type: direct access block device
+            data[1] = 0x80; // removable: true
+            data[2] = 0x04; // version
+            data[3] = 0x02; // response data format
+            data[4] = 31; // additional length (len - 5)
+            data[5] = 0; // flags
+            data[6] = 0; // flags
+            data[7] = 0; // flags
+            memcpy(data + 8, "Flipper ", 8); // vendor id
+            memcpy(data + 16, "Mass Storage    ", 16); // product id
+            memcpy(data + 32, "0001", 4); // product revision level
+            *len = 36;
+            scsi->tx_done = true;
+            return true;
+        } else {
+            if(page_code != 0x80) {
+                FURI_LOG_W(TAG, "Unsupported VPD code %02X", page_code);
+                return false;
+            }
+            data[0] = 0x00;
+            data[1] = 0x80;
+            data[2] = 0x00;
+            data[3] = 0x01; // Serial len
+            data[4] = '0';
+            *len = 5;
+            scsi->tx_done = true;
+            return true;
+        }
+    }; break;
+    case SCSI_READ_FORMAT_CAPACITIES: {
+        FURI_LOG_D(TAG, "SCSI_READ_FORMAT_CAPACITIES");
+        if(cap < 12) {
+            return false;
+        }
+        uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx);
+        uint32_t block_size = SCSI_BLOCK_SIZE;
+        // Capacity List Header
+        data[0] = 0;
+        data[1] = 0;
+        data[2] = 0;
+        data[3] = 8;
+
+        // Capacity Descriptor
+        data[4] = (n_blocks - 1) >> 24;
+        data[5] = (n_blocks - 1) >> 16;
+        data[6] = (n_blocks - 1) >> 8;
+        data[7] = (n_blocks - 1) & 0xFF;
+        data[8] = 0x02; // Formatted media
+        data[9] = block_size >> 16;
+        data[10] = block_size >> 8;
+        data[11] = block_size & 0xFF;
+        *len = 12;
+        scsi->tx_done = true;
+        return true;
+    }; break;
+    case SCSI_READ_CAPACITY_10: {
+        FURI_LOG_D(TAG, "SCSI_READ_CAPACITY_10");
+        if(cap < 8) return false;
+        uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx);
+        uint32_t block_size = SCSI_BLOCK_SIZE;
+        data[0] = (n_blocks - 1) >> 24;
+        data[1] = (n_blocks - 1) >> 16;
+        data[2] = (n_blocks - 1) >> 8;
+        data[3] = (n_blocks - 1) & 0xFF;
+        data[4] = block_size >> 24;
+        data[5] = block_size >> 16;
+        data[6] = block_size >> 8;
+        data[7] = block_size & 0xFF;
+        *len = 8;
+        scsi->tx_done = true;
+        return true;
+    }; break;
+    case SCSI_MODE_SENSE_6: {
+        FURI_LOG_D(TAG, "SCSI_MODE_SENSE_6 %lu", cap);
+        if(cap < 4) return false;
+        data[0] = 3; // mode data length (len - 1)
+        data[1] = 0; // medium type
+        data[2] = 0; // device-specific parameter
+        data[3] = 0; // block descriptor length
+        *len = 4;
+        scsi->tx_done = true;
+        return true;
+    }; break;
+    case SCSI_READ_10: {
+        uint32_t block_size = SCSI_BLOCK_SIZE;
+        bool result =
+            scsi->fn.read(scsi->fn.ctx, scsi->read_10.lba, scsi->read_10.count, data, len, cap);
+        *len -= *len % block_size;
+        uint16_t blocks = *len / block_size;
+        scsi->read_10.lba += blocks;
+        scsi->read_10.count -= blocks;
+        if(!scsi->read_10.count) {
+            scsi->tx_done = true;
+        }
+        return result;
+    }; break;
+    default: {
+        FURI_LOG_W(TAG, "unexpected scsi tx data cmd=%02X", scsi->cmd[0]);
+        scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
+        scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
+        return false;
+    }; break;
+    }
+}
+
+bool scsi_cmd_end(SCSISession* scsi) {
+    FURI_LOG_T(TAG, "END %02X", scsi->cmd[0]);
+    uint8_t* cmd = scsi->cmd;
+    uint8_t len = scsi->cmd_len;
+    scsi->cmd = NULL;
+    scsi->cmd_len = 0;
+    switch(cmd[0]) {
+    case SCSI_WRITE_10:
+        return scsi->rx_done;
+
+    case SCSI_REQUEST_SENSE:
+    case SCSI_INQUIRY:
+    case SCSI_READ_FORMAT_CAPACITIES:
+    case SCSI_READ_CAPACITY_10:
+    case SCSI_MODE_SENSE_6:
+    case SCSI_READ_10:
+        return scsi->tx_done;
+
+    case SCSI_TEST_UNIT_READY: {
+        FURI_LOG_D(TAG, "SCSI_TEST_UNIT_READY");
+        return true;
+    }; break;
+    case SCSI_PREVENT_MEDIUM_REMOVAL: {
+        if(len < 6) return false;
+        bool prevent = cmd[5];
+        FURI_LOG_D(TAG, "SCSI_PREVENT_MEDIUM_REMOVAL prevent=%d", prevent);
+        return !prevent;
+    }; break;
+    case SCSI_START_STOP_UNIT: {
+        if(len < 6) return false;
+        bool eject = (cmd[4] & 2) != 0;
+        bool start = (cmd[4] & 1) != 0;
+        FURI_LOG_D(TAG, "SCSI_START_STOP_UNIT eject=%d start=%d", eject, start);
+        if(eject) {
+            scsi->fn.eject(scsi->fn.ctx);
+        }
+        return true;
+    }; break;
+    default: {
+        FURI_LOG_W(TAG, "unexpected scsi cmd=%02X", cmd[0]);
+        scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
+        scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
+        return false;
+    }; break;
+    }
+}

+ 56 - 0
mass_storage/helpers/mass_storage_scsi.h

@@ -0,0 +1,56 @@
+#pragma once
+
+#include <furi.h>
+
+#define SCSI_BLOCK_SIZE (0x200UL)
+
+#define SCSI_SK_ILLEGAL_REQUEST (5)
+
+#define SCSI_ASC_INVALID_COMMAND_OPERATION_CODE (0x20)
+#define SCSI_ASC_LBA_OOB (0x21)
+#define SCSI_ASC_INVALID_FIELD_IN_CDB (0x24)
+
+typedef struct {
+    void* ctx;
+    bool (*read)(
+        void* ctx,
+        uint32_t lba,
+        uint16_t count,
+        uint8_t* out,
+        uint32_t* out_len,
+        uint32_t out_cap);
+    bool (*write)(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len);
+    uint32_t (*num_blocks)(void* ctx);
+    void (*eject)(void* ctx);
+} SCSIDeviceFunc;
+
+typedef struct {
+    SCSIDeviceFunc fn;
+
+    uint8_t* cmd;
+    uint8_t cmd_len;
+    bool rx_done;
+    bool tx_done;
+
+    uint8_t sk; // sense key
+    uint8_t asc; // additional sense code
+
+    // command-specific data
+    // valid from cmd_start to cmd_end
+    union {
+        struct {
+            uint16_t count;
+            uint32_t lba;
+        } read_10; // SCSI_READ_10
+
+        struct {
+            uint16_t count;
+            uint32_t lba;
+        } write_10; // SCSI_WRITE_10
+    };
+} SCSISession;
+
+bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len);
+bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len);
+bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap);
+bool scsi_cmd_end(SCSISession* scsi);

+ 481 - 0
mass_storage/helpers/mass_storage_usb.c

@@ -0,0 +1,481 @@
+#include "mass_storage_usb.h"
+#include <furi_hal.h>
+
+#define TAG "MassStorageUsb"
+
+#define USB_MSC_RX_EP (0x01)
+#define USB_MSC_TX_EP (0x82)
+
+#define USB_MSC_RX_EP_SIZE (64UL)
+#define USB_MSC_TX_EP_SIZE (64UL)
+
+#define USB_MSC_BOT_GET_MAX_LUN (0xFE)
+#define USB_MSC_BOT_RESET (0xFF)
+
+#define CBW_SIG (0x43425355)
+#define CBW_FLAGS_DEVICE_TO_HOST (0x80)
+
+#define CSW_SIG (0x53425355)
+#define CSW_STATUS_OK (0)
+#define CSW_STATUS_NOK (1)
+#define CSW_STATUS_PHASE_ERROR (2)
+
+// must be SCSI_BLOCK_SIZE aligned
+// larger than 0x10000 exceeds size_t, storage_file_* ops fail
+#define USB_MSC_BUF_MAX (0x10000UL - SCSI_BLOCK_SIZE)
+
+static usbd_respond usb_ep_config(usbd_device* dev, uint8_t cfg);
+static usbd_respond usb_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback);
+
+typedef enum {
+    EventExit = 1 << 0,
+    EventReset = 1 << 1,
+    EventRxTx = 1 << 2,
+
+    EventAll = EventExit | EventReset | EventRxTx,
+} MassStorageEvent;
+
+typedef struct {
+    uint32_t sig;
+    uint32_t tag;
+    uint32_t len;
+    uint8_t flags;
+    uint8_t lun;
+    uint8_t cmd_len;
+    uint8_t cmd[16];
+} __attribute__((packed)) CBW;
+
+typedef struct {
+    uint32_t sig;
+    uint32_t tag;
+    uint32_t residue;
+    uint8_t status;
+} __attribute__((packed)) CSW;
+
+struct MassStorageUsb {
+    FuriHalUsbInterface usb;
+    FuriHalUsbInterface* usb_prev;
+
+    FuriThread* thread;
+    usbd_device* dev;
+    SCSIDeviceFunc fn;
+};
+
+static int32_t mass_thread_worker(void* context) {
+    MassStorageUsb* mass = context;
+    usbd_device* dev = mass->dev;
+    SCSISession scsi = {
+        .fn = mass->fn,
+    };
+    CBW cbw = {0};
+    CSW csw = {0};
+    uint8_t* buf = NULL;
+    uint32_t buf_len = 0, buf_cap = 0, buf_sent = 0;
+    enum {
+        StateReadCBW,
+        StateReadData,
+        StateWriteData,
+        StateBuildCSW,
+        StateWriteCSW,
+    } state = StateReadCBW;
+    while(true) {
+        uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, FuriWaitForever);
+        if(flags & EventExit) {
+            FURI_LOG_D(TAG, "exit");
+            break;
+        }
+        if(flags & EventReset) {
+            FURI_LOG_D(TAG, "reset");
+            scsi.sk = 0;
+            scsi.asc = 0;
+            memset(&cbw, 0, sizeof(cbw));
+            memset(&csw, 0, sizeof(csw));
+            if(buf) {
+                free(buf);
+                buf = NULL;
+            }
+            buf_len = buf_cap = buf_sent = 0;
+            state = StateReadCBW;
+        }
+        if(flags & EventRxTx) do {
+                switch(state) {
+                case StateReadCBW: {
+                    FURI_LOG_T(TAG, "StateReadCBW");
+                    int32_t len = usbd_ep_read(dev, USB_MSC_RX_EP, &cbw, sizeof(cbw));
+                    if(len <= 0) {
+                        FURI_LOG_T(TAG, "cbw not ready");
+                        break;
+                    }
+                    if(len != sizeof(cbw) || cbw.sig != CBW_SIG) {
+                        FURI_LOG_W(TAG, "bad cbw sig=%08lx", cbw.sig);
+                        usbd_ep_stall(dev, USB_MSC_TX_EP);
+                        usbd_ep_stall(dev, USB_MSC_RX_EP);
+                        continue;
+                    }
+                    if(!scsi_cmd_start(&scsi, cbw.cmd, cbw.cmd_len)) {
+                        FURI_LOG_W(TAG, "bad cmd");
+                        usbd_ep_stall(dev, USB_MSC_RX_EP);
+                        csw.sig = CSW_SIG;
+                        csw.tag = cbw.tag;
+                        csw.status = CSW_STATUS_NOK;
+                        state = StateWriteCSW;
+                        continue;
+                    }
+                    if(cbw.flags & CBW_FLAGS_DEVICE_TO_HOST) {
+                        buf_len = 0;
+                        buf_sent = 0;
+                        state = StateWriteData;
+                    } else {
+                        buf_len = 0;
+                        state = StateReadData;
+                    }
+                    continue;
+                }; break;
+                case StateReadData: {
+                    FURI_LOG_T(TAG, "StateReadData %lu/%lu", buf_len, cbw.len);
+                    if(!cbw.len) {
+                        state = StateBuildCSW;
+                        continue;
+                    }
+                    uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
+                    if(buf_clamp > buf_cap) {
+                        FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
+                        if(buf) {
+                            free(buf);
+                        }
+                        buf_cap = buf_clamp;
+                        buf = malloc(buf_cap);
+                    }
+                    if(buf_len < buf_clamp) {
+                        int32_t len =
+                            usbd_ep_read(dev, USB_MSC_RX_EP, buf + buf_len, buf_clamp - buf_len);
+                        if(len < 0) {
+                            FURI_LOG_T(TAG, "rx not ready %ld", len);
+                            break;
+                        }
+                        FURI_LOG_T(TAG, "clamp %lu len %ld", buf_clamp, len);
+                        buf_len += len;
+                    }
+                    if(buf_len == buf_clamp) {
+                        if(!scsi_cmd_rx_data(&scsi, buf, buf_len)) {
+                            FURI_LOG_W(TAG, "short rx");
+                            usbd_ep_stall(dev, USB_MSC_RX_EP);
+                            csw.sig = CSW_SIG;
+                            csw.tag = cbw.tag;
+                            csw.status = CSW_STATUS_NOK;
+                            csw.residue = cbw.len;
+                            state = StateWriteCSW;
+                            continue;
+                        }
+                        cbw.len -= buf_len;
+                        buf_len = 0;
+                    }
+                    continue;
+                }; break;
+                case StateWriteData: {
+                    FURI_LOG_T(TAG, "StateWriteData %lu", cbw.len);
+                    if(!cbw.len) {
+                        state = StateBuildCSW;
+                        continue;
+                    }
+                    uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
+                    if(buf_clamp > buf_cap) {
+                        FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
+                        if(buf) {
+                            free(buf);
+                        }
+                        buf_cap = buf_clamp;
+                        buf = malloc(buf_cap);
+                    }
+                    if(!buf_len && !scsi_cmd_tx_data(&scsi, buf, &buf_len, buf_clamp)) {
+                        FURI_LOG_W(TAG, "short tx");
+                        // usbd_ep_stall(dev, USB_MSC_TX_EP);
+                        state = StateBuildCSW;
+                        continue;
+                    }
+                    int32_t len = usbd_ep_write(
+                        dev,
+                        USB_MSC_TX_EP,
+                        buf + buf_sent,
+                        MIN(USB_MSC_TX_EP_SIZE, buf_len - buf_sent));
+                    if(len < 0) {
+                        FURI_LOG_T(TAG, "tx not ready %ld", len);
+                        break;
+                    }
+                    buf_sent += len;
+                    if(buf_sent == buf_len) {
+                        cbw.len -= buf_len;
+                        buf_len = 0;
+                        buf_sent = 0;
+                    }
+                    continue;
+                }; break;
+                case StateBuildCSW: {
+                    FURI_LOG_T(TAG, "StateBuildCSW");
+                    csw.sig = CSW_SIG;
+                    csw.tag = cbw.tag;
+                    if(scsi_cmd_end(&scsi)) {
+                        csw.status = CSW_STATUS_OK;
+                    } else {
+                        csw.status = CSW_STATUS_NOK;
+                    }
+                    csw.residue = cbw.len;
+                    state = StateWriteCSW;
+                    continue;
+                }; break;
+                case StateWriteCSW: {
+                    FURI_LOG_T(TAG, "StateWriteCSW");
+                    if(csw.status) {
+                        FURI_LOG_W(
+                            TAG,
+                            "csw sig=%08lx tag=%08lx residue=%08lx status=%02x",
+                            csw.sig,
+                            csw.tag,
+                            csw.residue,
+                            csw.status);
+                    }
+                    int32_t len = usbd_ep_write(dev, USB_MSC_TX_EP, &csw, sizeof(csw));
+                    if(len < 0) {
+                        FURI_LOG_T(TAG, "csw not ready");
+                        break;
+                    }
+                    if(len != sizeof(csw)) {
+                        FURI_LOG_W(TAG, "bad csw write %ld", len);
+                        usbd_ep_stall(dev, USB_MSC_TX_EP);
+                        break;
+                    }
+                    memset(&cbw, 0, sizeof(cbw));
+                    memset(&csw, 0, sizeof(csw));
+                    state = StateReadCBW;
+                    continue;
+                }; break;
+                }
+                break;
+            } while(true);
+    }
+    if(buf) {
+        free(buf);
+    }
+    return 0;
+}
+
+// needed in usb_deinit, usb_suspend, usb_rxtx_ep_callback, usb_control,
+// where if_ctx isn't passed
+static MassStorageUsb* mass_cur = NULL;
+
+static void usb_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) {
+    UNUSED(intf);
+    MassStorageUsb* mass = ctx;
+    mass_cur = mass;
+    mass->dev = dev;
+
+    usbd_reg_config(dev, usb_ep_config);
+    usbd_reg_control(dev, usb_control);
+    usbd_connect(dev, true);
+
+    mass->thread = furi_thread_alloc();
+    furi_thread_set_name(mass->thread, "MassStorageUsb");
+    furi_thread_set_stack_size(mass->thread, 1024);
+    furi_thread_set_context(mass->thread, ctx);
+    furi_thread_set_callback(mass->thread, mass_thread_worker);
+    furi_thread_start(mass->thread);
+}
+
+static void usb_deinit(usbd_device* dev) {
+    usbd_reg_config(dev, NULL);
+    usbd_reg_control(dev, NULL);
+
+    MassStorageUsb* mass = mass_cur;
+    if(!mass || mass->dev != dev) {
+        FURI_LOG_E(TAG, "deinit mass_cur leak");
+        return;
+    }
+    mass_cur = NULL;
+
+    furi_assert(mass->thread);
+    furi_thread_flags_set(furi_thread_get_id(mass->thread), EventExit);
+    furi_thread_join(mass->thread);
+    furi_thread_free(mass->thread);
+    mass->thread = NULL;
+
+    free(mass->usb.str_prod_descr);
+    mass->usb.str_prod_descr = NULL;
+    free(mass->usb.str_serial_descr);
+    mass->usb.str_serial_descr = NULL;
+    free(mass);
+}
+
+static void usb_wakeup(usbd_device* dev) {
+    UNUSED(dev);
+}
+
+static void usb_suspend(usbd_device* dev) {
+    MassStorageUsb* mass = mass_cur;
+    if(!mass || mass->dev != dev) return;
+    furi_thread_flags_set(furi_thread_get_id(mass->thread), EventReset);
+}
+
+static void usb_rxtx_ep_callback(usbd_device* dev, uint8_t event, uint8_t ep) {
+    UNUSED(ep);
+    UNUSED(event);
+    MassStorageUsb* mass = mass_cur;
+    if(!mass || mass->dev != dev) return;
+    furi_thread_flags_set(furi_thread_get_id(mass->thread), EventRxTx);
+}
+
+static usbd_respond usb_ep_config(usbd_device* dev, uint8_t cfg) {
+    switch(cfg) {
+    case 0: // deconfig
+        usbd_ep_deconfig(dev, USB_MSC_RX_EP);
+        usbd_ep_deconfig(dev, USB_MSC_TX_EP);
+        usbd_reg_endpoint(dev, USB_MSC_RX_EP, NULL);
+        usbd_reg_endpoint(dev, USB_MSC_TX_EP, NULL);
+        return usbd_ack;
+    case 1: // config
+        usbd_ep_config(
+            dev, USB_MSC_RX_EP, USB_EPTYPE_BULK /* | USB_EPTYPE_DBLBUF*/, USB_MSC_RX_EP_SIZE);
+        usbd_ep_config(
+            dev, USB_MSC_TX_EP, USB_EPTYPE_BULK /* | USB_EPTYPE_DBLBUF*/, USB_MSC_TX_EP_SIZE);
+        usbd_reg_endpoint(dev, USB_MSC_RX_EP, usb_rxtx_ep_callback);
+        usbd_reg_endpoint(dev, USB_MSC_TX_EP, usb_rxtx_ep_callback);
+        return usbd_ack;
+    }
+    return usbd_fail;
+}
+
+static usbd_respond usb_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) {
+    UNUSED(callback);
+    if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) !=
+       (USB_REQ_INTERFACE | USB_REQ_CLASS)) {
+        return usbd_fail;
+    }
+    switch(req->bRequest) {
+    case USB_MSC_BOT_GET_MAX_LUN: {
+        static uint8_t max_lun = 0;
+        dev->status.data_ptr = &max_lun;
+        dev->status.data_count = 1;
+        return usbd_ack;
+    }; break;
+    case USB_MSC_BOT_RESET: {
+        MassStorageUsb* mass = mass_cur;
+        if(!mass || mass->dev != dev) return usbd_fail;
+        furi_thread_flags_set(furi_thread_get_id(mass->thread), EventReset);
+        return usbd_ack;
+    }; break;
+    }
+    return usbd_fail;
+}
+
+static const struct usb_string_descriptor dev_manuf_desc = USB_STRING_DESC("Flipper Devices Inc.");
+
+struct MassStorageDescriptor {
+    struct usb_config_descriptor config;
+    struct usb_interface_descriptor intf;
+    struct usb_endpoint_descriptor ep_rx;
+    struct usb_endpoint_descriptor ep_tx;
+} __attribute__((packed));
+
+static const struct usb_device_descriptor usb_mass_dev_descr = {
+    .bLength = sizeof(struct usb_device_descriptor),
+    .bDescriptorType = USB_DTYPE_DEVICE,
+    .bcdUSB = VERSION_BCD(2, 0, 0),
+    .bDeviceClass = USB_CLASS_PER_INTERFACE,
+    .bDeviceSubClass = USB_SUBCLASS_NONE,
+    .bDeviceProtocol = USB_PROTO_NONE,
+    .bMaxPacketSize0 = 8, // USB_EP0_SIZE
+    .idVendor = 0x0483,
+    .idProduct = 0x5720,
+    .bcdDevice = VERSION_BCD(1, 0, 0),
+    .iManufacturer = 1, // UsbDevManuf
+    .iProduct = 2, // UsbDevProduct
+    .iSerialNumber = 3, // UsbDevSerial
+    .bNumConfigurations = 1,
+};
+
+static const struct MassStorageDescriptor usb_mass_cfg_descr = {
+    .config =
+        {
+            .bLength = sizeof(struct usb_config_descriptor),
+            .bDescriptorType = USB_DTYPE_CONFIGURATION,
+            .wTotalLength = sizeof(struct MassStorageDescriptor),
+            .bNumInterfaces = 1,
+            .bConfigurationValue = 1,
+            .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,
+            .bAlternateSetting = 0,
+            .bNumEndpoints = 2,
+            .bInterfaceClass = USB_CLASS_MASS_STORAGE,
+            .bInterfaceSubClass = 0x06, // scsi transparent
+            .bInterfaceProtocol = 0x50, // bulk only
+            .iInterface = NO_DESCRIPTOR,
+        },
+    .ep_rx =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = USB_MSC_RX_EP,
+            .bmAttributes = USB_EPTYPE_BULK,
+            .wMaxPacketSize = USB_MSC_RX_EP_SIZE,
+            .bInterval = 0,
+        },
+    .ep_tx =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = USB_MSC_TX_EP,
+            .bmAttributes = USB_EPTYPE_BULK,
+            .wMaxPacketSize = USB_MSC_TX_EP_SIZE,
+            .bInterval = 0,
+        },
+};
+
+MassStorageUsb* mass_storage_usb_start(const char* filename, SCSIDeviceFunc fn) {
+    MassStorageUsb* mass = malloc(sizeof(MassStorageUsb));
+    mass->usb_prev = furi_hal_usb_get_config();
+    mass->usb.init = usb_init;
+    mass->usb.deinit = usb_deinit;
+    mass->usb.wakeup = usb_wakeup;
+    mass->usb.suspend = usb_suspend;
+    mass->usb.dev_descr = (struct usb_device_descriptor*)&usb_mass_dev_descr;
+    mass->usb.str_manuf_descr = (void*)&dev_manuf_desc;
+    mass->usb.str_prod_descr = NULL;
+    mass->usb.str_serial_descr = NULL;
+    mass->usb.cfg_descr = (void*)&usb_mass_cfg_descr;
+
+    const char* name = furi_hal_version_get_device_name_ptr();
+    if(!name) name = "Flipper Zero";
+    size_t len = strlen(name);
+    struct usb_string_descriptor* str_prod_descr = malloc(len * 2 + 2);
+    str_prod_descr->bLength = len * 2 + 2;
+    str_prod_descr->bDescriptorType = USB_DTYPE_STRING;
+    for(uint8_t i = 0; i < len; i++) str_prod_descr->wString[i] = name[i];
+    mass->usb.str_prod_descr = str_prod_descr;
+
+    len = strlen(filename);
+    struct usb_string_descriptor* str_serial_descr = malloc(len * 2 + 2);
+    str_serial_descr->bLength = len * 2 + 2;
+    str_serial_descr->bDescriptorType = USB_DTYPE_STRING;
+    for(uint8_t i = 0; i < len; i++) str_serial_descr->wString[i] = filename[i];
+    mass->usb.str_serial_descr = str_serial_descr;
+
+    mass->fn = fn;
+    if(!furi_hal_usb_set_config(&mass->usb, mass)) {
+        FURI_LOG_E(TAG, "USB locked, cannot start Mass Storage");
+        free(mass->usb.str_prod_descr);
+        free(mass->usb.str_serial_descr);
+        free(mass);
+        return NULL;
+    }
+    return mass;
+}
+
+void mass_storage_usb_stop(MassStorageUsb* mass) {
+    furi_hal_usb_set_config(mass->usb_prev, NULL);
+}

+ 9 - 0
mass_storage/helpers/mass_storage_usb.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include <storage/storage.h>
+#include "mass_storage_scsi.h"
+
+typedef struct MassStorageUsb MassStorageUsb;
+
+MassStorageUsb* mass_storage_usb_start(const char* filename, SCSIDeviceFunc fn);
+void mass_storage_usb_stop(MassStorageUsb* mass);

+ 136 - 0
mass_storage/mass_storage_app.c

@@ -0,0 +1,136 @@
+#include "mass_storage_app_i.h"
+#include <furi.h>
+#include <storage/storage.h>
+#include <lib/toolbox/path.h>
+
+static bool mass_storage_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    MassStorageApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool mass_storage_app_back_event_callback(void* context) {
+    furi_assert(context);
+    MassStorageApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+static void mass_storage_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    MassStorageApp* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+void mass_storage_app_show_loading_popup(MassStorageApp* app, bool show) {
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}
+
+MassStorageApp* mass_storage_app_alloc(char* arg) {
+    MassStorageApp* app = malloc(sizeof(MassStorageApp));
+    app->file_path = furi_string_alloc();
+
+    if(arg != NULL) {
+        furi_string_set_str(app->file_path, arg);
+    } else {
+        furi_string_set_str(app->file_path, MASS_STORAGE_APP_PATH_FOLDER);
+    }
+
+    app->gui = furi_record_open(RECORD_GUI);
+    app->fs_api = furi_record_open(RECORD_STORAGE);
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    app->scene_manager = scene_manager_alloc(&mass_storage_scene_handlers, app);
+
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, mass_storage_app_tick_event_callback, 500);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, mass_storage_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, mass_storage_app_back_event_callback);
+
+    app->mass_storage_view = mass_storage_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        MassStorageAppViewWork,
+        mass_storage_get_view(app->mass_storage_view));
+
+    app->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, MassStorageAppViewTextInput, text_input_get_view(app->text_input));
+
+    app->loading = loading_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, MassStorageAppViewLoading, loading_get_view(app->loading));
+
+    app->variable_item_list = variable_item_list_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        MassStorageAppViewStart,
+        variable_item_list_get_view(app->variable_item_list));
+
+    app->widget = widget_alloc();
+    view_dispatcher_add_view(
+        app->view_dispatcher, MassStorageAppViewWidget, widget_get_view(app->widget));
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    if(storage_file_exists(app->fs_api, furi_string_get_cstr(app->file_path))) {
+        if(!furi_hal_usb_is_locked()) {
+            scene_manager_next_scene(app->scene_manager, MassStorageSceneWork);
+        } else {
+            scene_manager_next_scene(app->scene_manager, MassStorageSceneUsbLocked);
+        }
+    } else {
+        scene_manager_next_scene(app->scene_manager, MassStorageSceneStart);
+    }
+
+    return app;
+}
+
+void mass_storage_app_free(MassStorageApp* app) {
+    furi_assert(app);
+
+    // Views
+    view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewWork);
+    view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewTextInput);
+    view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewStart);
+    view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewLoading);
+    view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewWidget);
+
+    mass_storage_free(app->mass_storage_view);
+    text_input_free(app->text_input);
+    variable_item_list_free(app->variable_item_list);
+    loading_free(app->loading);
+    widget_free(app->widget);
+
+    // View dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+
+    furi_string_free(app->file_path);
+
+    // Close records
+    furi_record_close(RECORD_GUI);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+
+    free(app);
+}
+
+int32_t mass_storage_app(void* p) {
+    MassStorageApp* mass_storage_app = mass_storage_app_alloc((char*)p);
+    view_dispatcher_run(mass_storage_app->view_dispatcher);
+    mass_storage_app_free(mass_storage_app);
+    return 0;
+}

+ 11 - 0
mass_storage/mass_storage_app.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct MassStorageApp MassStorageApp;
+
+#ifdef __cplusplus
+}
+#endif

+ 67 - 0
mass_storage/mass_storage_app_i.h

@@ -0,0 +1,67 @@
+#pragma once
+
+#include "mass_storage_app.h"
+#include "scenes/mass_storage_scene.h"
+#include "helpers/mass_storage_usb.h"
+
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <dialogs/dialogs.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/loading.h>
+#include <gui/modules/widget.h>
+#include <storage/storage.h>
+#include "views/mass_storage_view.h"
+#include <mass_storage_icons.h>
+
+#define MASS_STORAGE_APP_PATH_FOLDER STORAGE_APP_DATA_PATH_PREFIX
+#define MASS_STORAGE_APP_EXTENSION ".img"
+#define MASS_STORAGE_FILE_NAME_LEN 40
+
+struct MassStorageApp {
+    Gui* gui;
+    Storage* fs_api;
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+    Widget* widget;
+    DialogsApp* dialogs;
+    TextInput* text_input;
+    VariableItemList* variable_item_list;
+    Loading* loading;
+
+    FuriString* file_path;
+    File* file;
+    MassStorage* mass_storage_view;
+
+    FuriMutex* usb_mutex;
+    MassStorageUsb* usb;
+
+    char new_file_name[MASS_STORAGE_FILE_NAME_LEN + 1];
+    uint32_t new_file_size;
+
+    uint32_t bytes_read, bytes_written;
+};
+
+typedef enum {
+    MassStorageAppViewStart,
+    MassStorageAppViewTextInput,
+    MassStorageAppViewWork,
+    MassStorageAppViewLoading,
+    MassStorageAppViewWidget,
+} MassStorageAppView;
+
+enum MassStorageCustomEvent {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    MassStorageCustomEventReserved = 100,
+
+    MassStorageCustomEventEject,
+    MassStorageCustomEventFileSelect,
+    MassStorageCustomEventNewImage,
+    MassStorageCustomEventNameInput,
+};
+
+void mass_storage_app_show_loading_popup(MassStorageApp* app, bool show);

+ 30 - 0
mass_storage/scenes/mass_storage_scene.c

@@ -0,0 +1,30 @@
+#include "mass_storage_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const mass_storage_scene_on_enter_handlers[])(void*) = {
+#include "mass_storage_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const mass_storage_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "mass_storage_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const mass_storage_scene_on_exit_handlers[])(void* context) = {
+#include "mass_storage_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers mass_storage_scene_handlers = {
+    .on_enter_handlers = mass_storage_scene_on_enter_handlers,
+    .on_event_handlers = mass_storage_scene_on_event_handlers,
+    .on_exit_handlers = mass_storage_scene_on_exit_handlers,
+    .scene_num = MassStorageSceneNum,
+};

+ 29 - 0
mass_storage/scenes/mass_storage_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) MassStorageScene##id,
+typedef enum {
+#include "mass_storage_scene_config.h"
+    MassStorageSceneNum,
+} MassStorageScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers mass_storage_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "mass_storage_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "mass_storage_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "mass_storage_scene_config.h"
+#undef ADD_SCENE

+ 5 - 0
mass_storage/scenes/mass_storage_scene_config.h

@@ -0,0 +1,5 @@
+ADD_SCENE(mass_storage, start, Start)
+ADD_SCENE(mass_storage, file_select, FileSelect)
+ADD_SCENE(mass_storage, work, Work)
+ADD_SCENE(mass_storage, file_name, FileName)
+ADD_SCENE(mass_storage, usb_locked, UsbLocked)

+ 87 - 0
mass_storage/scenes/mass_storage_scene_file_name.c

@@ -0,0 +1,87 @@
+#include "../mass_storage_app_i.h"
+
+#define WRITE_BUF_LEN 4096
+
+static void mass_storage_file_name_text_callback(void* context) {
+    furi_assert(context);
+
+    MassStorageApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, MassStorageCustomEventNameInput);
+}
+
+static bool mass_storage_create_image(Storage* storage, const char* file_path, uint32_t size) {
+    FURI_LOG_I("TAG", "Creating image %s, len:%lu", file_path, size);
+    File* file = storage_file_alloc(storage);
+
+    bool success = false;
+    uint8_t* buffer = malloc(WRITE_BUF_LEN);
+    do {
+        if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) break;
+        if(!storage_file_seek(file, size, true)) break;
+        if(!storage_file_seek(file, 0, true)) break;
+        // Zero out first 4k - partition table and adjacent data
+        if(!storage_file_write(file, buffer, WRITE_BUF_LEN)) break;
+
+        success = true;
+    } while(false);
+
+    free(buffer);
+    storage_file_close(file);
+    storage_file_free(file);
+    return success;
+}
+
+void mass_storage_scene_file_name_on_enter(void* context) {
+    MassStorageApp* app = context;
+
+    text_input_set_header_text(app->text_input, "Enter image name");
+    ValidatorIsFile* validator_is_file =
+        validator_is_file_alloc_init(MASS_STORAGE_APP_PATH_FOLDER, MASS_STORAGE_APP_EXTENSION, "");
+    text_input_set_validator(app->text_input, validator_is_file_callback, validator_is_file);
+
+    text_input_set_result_callback(
+        app->text_input,
+        mass_storage_file_name_text_callback,
+        app,
+        app->new_file_name,
+        MASS_STORAGE_FILE_NAME_LEN,
+        true);
+    view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewTextInput);
+}
+
+bool mass_storage_scene_file_name_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(event);
+    MassStorageApp* app = context;
+
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MassStorageCustomEventNameInput) {
+            mass_storage_app_show_loading_popup(app, true);
+            furi_string_printf(
+                app->file_path,
+                "%s/%s%s",
+                MASS_STORAGE_APP_PATH_FOLDER,
+                app->new_file_name,
+                MASS_STORAGE_APP_EXTENSION);
+            if(mass_storage_create_image(
+                   app->fs_api, furi_string_get_cstr(app->file_path), app->new_file_size)) {
+                if(!furi_hal_usb_is_locked()) {
+                    scene_manager_next_scene(app->scene_manager, MassStorageSceneWork);
+                } else {
+                    scene_manager_next_scene(app->scene_manager, MassStorageSceneUsbLocked);
+                }
+            } // TODO: error message screen
+        }
+    }
+    return consumed;
+}
+
+void mass_storage_scene_file_name_on_exit(void* context) {
+    UNUSED(context);
+    MassStorageApp* app = context;
+    void* validator_context = text_input_get_validator_callback_context(app->text_input);
+    text_input_set_validator(app->text_input, NULL, NULL);
+    validator_is_file_free(validator_context);
+    text_input_reset(app->text_input);
+}

+ 42 - 0
mass_storage/scenes/mass_storage_scene_file_select.c

@@ -0,0 +1,42 @@
+#include "../mass_storage_app_i.h"
+#include "furi_hal_power.h"
+
+static bool mass_storage_file_select(MassStorageApp* mass_storage) {
+    furi_assert(mass_storage);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(
+        &browser_options, MASS_STORAGE_APP_EXTENSION, &I_mass_storage_10px);
+    browser_options.base_path = MASS_STORAGE_APP_PATH_FOLDER;
+    browser_options.hide_ext = false;
+
+    // Input events and views are managed by file_select
+    bool res = dialog_file_browser_show(
+        mass_storage->dialogs, mass_storage->file_path, mass_storage->file_path, &browser_options);
+    return res;
+}
+
+void mass_storage_scene_file_select_on_enter(void* context) {
+    MassStorageApp* mass_storage = context;
+
+    if(mass_storage_file_select(mass_storage)) {
+        if(!furi_hal_usb_is_locked()) {
+            scene_manager_next_scene(mass_storage->scene_manager, MassStorageSceneWork);
+        } else {
+            scene_manager_next_scene(mass_storage->scene_manager, MassStorageSceneUsbLocked);
+        }
+    } else {
+        scene_manager_previous_scene(mass_storage->scene_manager);
+    }
+}
+
+bool mass_storage_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    // MassStorageApp* mass_storage = context;
+    return false;
+}
+
+void mass_storage_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 74 - 0
mass_storage/scenes/mass_storage_scene_start.c

@@ -0,0 +1,74 @@
+#include "../mass_storage_app_i.h"
+
+static const struct {
+    char* name;
+    uint32_t value;
+} image_size[] = {
+    {"1.44M", 1440 * 1024},
+    {"2M", 2 * 1024 * 1024},
+    {"4M", 4 * 1024 * 1024},
+    {"8M", 8 * 1024 * 1024},
+    {"16M", 16 * 1024 * 1024},
+    {"32M", 32 * 1024 * 1024},
+    {"64M", 64 * 1024 * 1024},
+    {"128M", 128 * 1024 * 1024},
+    {"256M", 256 * 1024 * 1024},
+    {"512M", 512 * 1024 * 1024},
+    {"700M", 700 * 1024 * 1024},
+    {"1G", 1024 * 1024 * 1024},
+    {"2G", 2u * 1024 * 1024 * 1024},
+};
+
+static void mass_storage_item_select(void* context, uint32_t index) {
+    MassStorageApp* app = context;
+    if(index == 0) {
+        view_dispatcher_send_custom_event(app->view_dispatcher, MassStorageCustomEventFileSelect);
+    } else {
+        view_dispatcher_send_custom_event(app->view_dispatcher, MassStorageCustomEventNewImage);
+    }
+}
+
+static void mass_storage_image_size(VariableItem* item) {
+    MassStorageApp* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, image_size[index].name);
+    app->new_file_size = image_size[index].value;
+}
+
+void mass_storage_scene_start_on_enter(void* context) {
+    MassStorageApp* app = context;
+
+    VariableItem* item =
+        variable_item_list_add(app->variable_item_list, "Select disk image", 0, NULL, NULL);
+
+    item = variable_item_list_add(
+        app->variable_item_list, "New image", COUNT_OF(image_size), mass_storage_image_size, app);
+
+    variable_item_list_set_enter_callback(app->variable_item_list, mass_storage_item_select, app);
+
+    variable_item_set_current_value_index(item, 2);
+    variable_item_set_current_value_text(item, image_size[2].name);
+    app->new_file_size = image_size[2].value;
+    view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewStart);
+}
+
+bool mass_storage_scene_start_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    MassStorageApp* app = context;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MassStorageCustomEventFileSelect) {
+            scene_manager_next_scene(app->scene_manager, MassStorageSceneFileSelect);
+        } else if(event.event == MassStorageCustomEventNewImage) {
+            scene_manager_next_scene(app->scene_manager, MassStorageSceneFileName);
+        }
+    }
+    return false;
+}
+
+void mass_storage_scene_start_on_exit(void* context) {
+    UNUSED(context);
+    MassStorageApp* app = context;
+    variable_item_list_reset(app->variable_item_list);
+}

+ 40 - 0
mass_storage/scenes/mass_storage_scene_usb_locked.c

@@ -0,0 +1,40 @@
+#include "../mass_storage_app_i.h"
+
+void mass_storage_scene_usb_locked_on_enter(void* context) {
+    MassStorageApp* app = context;
+
+    widget_add_icon_element(app->widget, 78, 0, &I_ActiveConnection_50x64);
+    widget_add_string_multiline_element(
+        app->widget, 3, 2, AlignLeft, AlignTop, FontPrimary, "Connection\nis active!");
+    widget_add_string_multiline_element(
+        app->widget,
+        3,
+        30,
+        AlignLeft,
+        AlignTop,
+        FontSecondary,
+        "Disconnect from\nPC or phone to\nuse this function.");
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewWidget);
+}
+
+bool mass_storage_scene_usb_locked_on_event(void* context, SceneManagerEvent event) {
+    MassStorageApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_search_and_switch_to_previous_scene(
+            app->scene_manager, MassStorageSceneFileSelect);
+        if(!consumed) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, MassStorageSceneStart);
+        }
+    }
+
+    return consumed;
+}
+
+void mass_storage_scene_usb_locked_on_exit(void* context) {
+    MassStorageApp* app = context;
+    widget_reset(app->widget);
+}

+ 137 - 0
mass_storage/scenes/mass_storage_scene_work.c

@@ -0,0 +1,137 @@
+#include "../mass_storage_app_i.h"
+#include "../views/mass_storage_view.h"
+#include "../helpers/mass_storage_usb.h"
+#include <lib/toolbox/path.h>
+
+#define TAG "MassStorageSceneWork"
+
+static bool file_read(
+    void* ctx,
+    uint32_t lba,
+    uint16_t count,
+    uint8_t* out,
+    uint32_t* out_len,
+    uint32_t out_cap) {
+    MassStorageApp* app = ctx;
+    FURI_LOG_T(TAG, "file_read lba=%08lX count=%04X out_cap=%08lX", lba, count, out_cap);
+    if(!storage_file_seek(app->file, lba * SCSI_BLOCK_SIZE, true)) {
+        FURI_LOG_W(TAG, "seek failed");
+        return false;
+    }
+    uint16_t clamp = MIN(out_cap, count * SCSI_BLOCK_SIZE);
+    *out_len = storage_file_read(app->file, out, clamp);
+    FURI_LOG_T(TAG, "%lu/%lu", *out_len, count * SCSI_BLOCK_SIZE);
+    app->bytes_read += *out_len;
+    return *out_len == clamp;
+}
+
+static bool file_write(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len) {
+    MassStorageApp* app = ctx;
+    FURI_LOG_T(TAG, "file_write lba=%08lX count=%04X len=%08lX", lba, count, len);
+    if(len != count * SCSI_BLOCK_SIZE) {
+        FURI_LOG_W(TAG, "bad write params count=%u len=%lu", count, len);
+        return false;
+    }
+    if(!storage_file_seek(app->file, lba * SCSI_BLOCK_SIZE, true)) {
+        FURI_LOG_W(TAG, "seek failed");
+        return false;
+    }
+    app->bytes_written += len;
+    return storage_file_write(app->file, buf, len) == len;
+}
+
+static uint32_t file_num_blocks(void* ctx) {
+    MassStorageApp* app = ctx;
+    return storage_file_size(app->file) / SCSI_BLOCK_SIZE;
+}
+
+static void file_eject(void* ctx) {
+    MassStorageApp* app = ctx;
+    FURI_LOG_D(TAG, "EJECT");
+    view_dispatcher_send_custom_event(app->view_dispatcher, MassStorageCustomEventEject);
+}
+
+bool mass_storage_scene_work_on_event(void* context, SceneManagerEvent event) {
+    MassStorageApp* app = context;
+    bool consumed = false;
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MassStorageCustomEventEject) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, MassStorageSceneFileSelect);
+            if(!consumed) {
+                consumed = scene_manager_search_and_switch_to_previous_scene(
+                    app->scene_manager, MassStorageSceneStart);
+            }
+        }
+    } else if(event.type == SceneManagerEventTypeTick) {
+        mass_storage_set_stats(app->mass_storage_view, app->bytes_read, app->bytes_written);
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_search_and_switch_to_previous_scene(
+            app->scene_manager, MassStorageSceneFileSelect);
+        if(!consumed) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, MassStorageSceneStart);
+        }
+    }
+    return consumed;
+}
+
+void mass_storage_scene_work_on_enter(void* context) {
+    MassStorageApp* app = context;
+    app->bytes_read = app->bytes_written = 0;
+
+    if(!storage_file_exists(app->fs_api, furi_string_get_cstr(app->file_path))) {
+        scene_manager_search_and_switch_to_previous_scene(
+            app->scene_manager, MassStorageSceneStart);
+        return;
+    }
+
+    mass_storage_app_show_loading_popup(app, true);
+
+    app->usb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+
+    FuriString* file_name = furi_string_alloc();
+    path_extract_filename(app->file_path, file_name, true);
+
+    mass_storage_set_file_name(app->mass_storage_view, file_name);
+    app->file = storage_file_alloc(app->fs_api);
+    furi_assert(storage_file_open(
+        app->file,
+        furi_string_get_cstr(app->file_path),
+        FSAM_READ | FSAM_WRITE,
+        FSOM_OPEN_EXISTING));
+
+    SCSIDeviceFunc fn = {
+        .ctx = app,
+        .read = file_read,
+        .write = file_write,
+        .num_blocks = file_num_blocks,
+        .eject = file_eject,
+    };
+
+    app->usb = mass_storage_usb_start(furi_string_get_cstr(file_name), fn);
+
+    furi_string_free(file_name);
+
+    mass_storage_app_show_loading_popup(app, false);
+    view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewWork);
+}
+
+void mass_storage_scene_work_on_exit(void* context) {
+    MassStorageApp* app = context;
+    mass_storage_app_show_loading_popup(app, true);
+
+    if(app->usb_mutex) {
+        furi_mutex_free(app->usb_mutex);
+        app->usb_mutex = NULL;
+    }
+    if(app->usb) {
+        mass_storage_usb_stop(app->usb);
+        app->usb = NULL;
+    }
+    if(app->file) {
+        storage_file_free(app->file);
+        app->file = NULL;
+    }
+    mass_storage_app_show_loading_popup(app, false);
+}

+ 122 - 0
mass_storage/views/mass_storage_view.c

@@ -0,0 +1,122 @@
+#include "mass_storage_view.h"
+#include "../mass_storage_app_i.h"
+#include <gui/elements.h>
+
+struct MassStorage {
+    View* view;
+};
+
+typedef struct {
+    FuriString *file_name, *status_string;
+    uint32_t read_speed, write_speed;
+    uint32_t bytes_read, bytes_written;
+    uint32_t update_time;
+} MassStorageModel;
+
+static void append_suffixed_byte_count(FuriString* string, uint32_t count) {
+    if(count < 1024) {
+        furi_string_cat_printf(string, "%luB", count);
+    } else if(count < 1024 * 1024) {
+        furi_string_cat_printf(string, "%luK", count / 1024);
+    } else if(count < 1024 * 1024 * 1024) {
+        furi_string_cat_printf(string, "%.3fM", (double)count / (1024 * 1024));
+    } else {
+        furi_string_cat_printf(string, "%.3fG", (double)count / (1024 * 1024 * 1024));
+    }
+}
+
+static void mass_storage_draw_callback(Canvas* canvas, void* _model) {
+    MassStorageModel* model = _model;
+
+    canvas_draw_icon(canvas, 8, 14, &I_Drive_112x35);
+
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str_aligned(
+        canvas, canvas_width(canvas) / 2, 0, AlignCenter, AlignTop, "USB Mass Storage");
+
+    canvas_set_font(canvas, FontSecondary);
+    elements_string_fit_width(canvas, model->file_name, 89 - 2);
+    canvas_draw_str_aligned(
+        canvas, 50, 23, AlignCenter, AlignBottom, furi_string_get_cstr(model->file_name));
+
+    furi_string_set_str(model->status_string, "R:");
+    append_suffixed_byte_count(model->status_string, model->bytes_read);
+    if(model->read_speed) {
+        furi_string_cat_str(model->status_string, "; ");
+        append_suffixed_byte_count(model->status_string, model->read_speed);
+        furi_string_cat_str(model->status_string, "ps");
+    }
+    canvas_draw_str(canvas, 12, 34, furi_string_get_cstr(model->status_string));
+
+    furi_string_set_str(model->status_string, "W:");
+    append_suffixed_byte_count(model->status_string, model->bytes_written);
+    if(model->write_speed) {
+        furi_string_cat_str(model->status_string, "; ");
+        append_suffixed_byte_count(model->status_string, model->write_speed);
+        furi_string_cat_str(model->status_string, "ps");
+    }
+    canvas_draw_str(canvas, 12, 44, furi_string_get_cstr(model->status_string));
+}
+
+MassStorage* mass_storage_alloc() {
+    MassStorage* mass_storage = malloc(sizeof(MassStorage));
+
+    mass_storage->view = view_alloc();
+    view_allocate_model(mass_storage->view, ViewModelTypeLocking, sizeof(MassStorageModel));
+    with_view_model(
+        mass_storage->view,
+        MassStorageModel * model,
+        {
+            model->file_name = furi_string_alloc();
+            model->status_string = furi_string_alloc();
+        },
+        false);
+    view_set_context(mass_storage->view, mass_storage);
+    view_set_draw_callback(mass_storage->view, mass_storage_draw_callback);
+
+    return mass_storage;
+}
+
+void mass_storage_free(MassStorage* mass_storage) {
+    furi_assert(mass_storage);
+    with_view_model(
+        mass_storage->view,
+        MassStorageModel * model,
+        {
+            furi_string_free(model->file_name);
+            furi_string_free(model->status_string);
+        },
+        false);
+    view_free(mass_storage->view);
+    free(mass_storage);
+}
+
+View* mass_storage_get_view(MassStorage* mass_storage) {
+    furi_assert(mass_storage);
+    return mass_storage->view;
+}
+
+void mass_storage_set_file_name(MassStorage* mass_storage, FuriString* name) {
+    furi_assert(name);
+    with_view_model(
+        mass_storage->view,
+        MassStorageModel * model,
+        { furi_string_set(model->file_name, name); },
+        true);
+}
+
+void mass_storage_set_stats(MassStorage* mass_storage, uint32_t read, uint32_t written) {
+    with_view_model(
+        mass_storage->view,
+        MassStorageModel * model,
+        {
+            uint32_t now = furi_get_tick();
+            model->read_speed = (read - model->bytes_read) * 1000 / (now - model->update_time);
+            model->write_speed =
+                (written - model->bytes_written) * 1000 / (now - model->update_time);
+            model->bytes_read = read;
+            model->bytes_written = written;
+            model->update_time = now;
+        },
+        true);
+}

+ 15 - 0
mass_storage/views/mass_storage_view.h

@@ -0,0 +1,15 @@
+#pragma once
+
+#include <gui/view.h>
+
+typedef struct MassStorage MassStorage;
+
+MassStorage* mass_storage_alloc();
+
+void mass_storage_free(MassStorage* mass_storage);
+
+View* mass_storage_get_view(MassStorage* mass_storage);
+
+void mass_storage_set_file_name(MassStorage* mass_storage, FuriString* name);
+
+void mass_storage_set_stats(MassStorage* mass_storage, uint32_t read, uint32_t written);