From ed0823c3820bb9a13262d6a764c4452e68bd0208 Mon Sep 17 00:00:00 2001 From: Nikolay Minaylov Date: Mon, 31 Jul 2023 14:31:56 +0300 Subject: [PATCH] [FL-3432] USB storage app (#1) * USB storage app * Fix missing SCSI commands * New image creation --- .gitignore | 3 + mass_storage/application.fam | 15 + mass_storage/assets/Drive_112x35.png | Bin 0 -> 3695 bytes mass_storage/assets/mass_storage_10px.png | Bin 0 -> 183 bytes mass_storage/helpers/mass_storage_scsi.c | 266 ++++++++++ mass_storage/helpers/mass_storage_scsi.h | 56 ++ mass_storage/helpers/mass_storage_usb.c | 482 ++++++++++++++++++ mass_storage/helpers/mass_storage_usb.h | 9 + mass_storage/mass_storage_app.c | 128 +++++ mass_storage/mass_storage_app.h | 11 + mass_storage/mass_storage_app_i.h | 60 +++ mass_storage/scenes/mass_storage_scene.c | 30 ++ mass_storage/scenes/mass_storage_scene.h | 29 ++ .../scenes/mass_storage_scene_config.h | 4 + .../scenes/mass_storage_scene_file_name.c | 85 +++ .../scenes/mass_storage_scene_file_select.c | 39 ++ .../scenes/mass_storage_scene_start.c | 67 +++ mass_storage/scenes/mass_storage_scene_work.c | 135 +++++ mass_storage/views/mass_storage_view.c | 67 +++ mass_storage/views/mass_storage_view.h | 13 + 20 files changed, 1499 insertions(+) create mode 100644 .gitignore create mode 100644 mass_storage/application.fam create mode 100644 mass_storage/assets/Drive_112x35.png create mode 100644 mass_storage/assets/mass_storage_10px.png create mode 100644 mass_storage/helpers/mass_storage_scsi.c create mode 100644 mass_storage/helpers/mass_storage_scsi.h create mode 100644 mass_storage/helpers/mass_storage_usb.c create mode 100644 mass_storage/helpers/mass_storage_usb.h create mode 100644 mass_storage/mass_storage_app.c create mode 100644 mass_storage/mass_storage_app.h create mode 100644 mass_storage/mass_storage_app_i.h create mode 100644 mass_storage/scenes/mass_storage_scene.c create mode 100644 mass_storage/scenes/mass_storage_scene.h create mode 100644 mass_storage/scenes/mass_storage_scene_config.h create mode 100644 mass_storage/scenes/mass_storage_scene_file_name.c create mode 100644 mass_storage/scenes/mass_storage_scene_file_select.c create mode 100644 mass_storage/scenes/mass_storage_scene_start.c create mode 100644 mass_storage/scenes/mass_storage_scene_work.c create mode 100644 mass_storage/views/mass_storage_view.c create mode 100644 mass_storage/views/mass_storage_view.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..b717f37381d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode +/*/dist +/*/.clang-format diff --git a/mass_storage/application.fam b/mass_storage/application.fam new file mode 100644 index 00000000000..402affc9c1a --- /dev/null +++ b/mass_storage/application.fam @@ -0,0 +1,15 @@ +App( + appid="mass_storage", + name="Mass Storage", + apptype=FlipperAppType.EXTERNAL, + entry_point="mass_storage_app", + requires=[ + "gui", + "dialogs", + ], + stack_size=2 * 1024, + order=20, + fap_icon="assets/mass_storage_10px.png", + fap_icon_assets="assets", + fap_category="USB", +) diff --git a/mass_storage/assets/Drive_112x35.png b/mass_storage/assets/Drive_112x35.png new file mode 100644 index 0000000000000000000000000000000000000000..6f7b9c8342be74c2231a2b0830b1af9c1f77122f GIT binary patch literal 3695 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 literal 0 HcmV?d00001 diff --git a/mass_storage/helpers/mass_storage_scsi.c b/mass_storage/helpers/mass_storage_scsi.c new file mode 100644 index 00000000000..c1efacf8e23 --- /dev/null +++ b/mass_storage/helpers/mass_storage_scsi.c @@ -0,0 +1,266 @@ +#include "mass_storage_scsi.h" + +#include + +#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; + } +} diff --git a/mass_storage/helpers/mass_storage_scsi.h b/mass_storage/helpers/mass_storage_scsi.h new file mode 100644 index 00000000000..a35d6aff322 --- /dev/null +++ b/mass_storage/helpers/mass_storage_scsi.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +#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); \ No newline at end of file diff --git a/mass_storage/helpers/mass_storage_usb.c b/mass_storage/helpers/mass_storage_usb.c new file mode 100644 index 00000000000..8e3e0f8756b --- /dev/null +++ b/mass_storage/helpers/mass_storage_usb.c @@ -0,0 +1,482 @@ +#include "mass_storage_usb.h" +#include + +#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); + // freed by usb_deinit asynchronously from usb thread +} diff --git a/mass_storage/helpers/mass_storage_usb.h b/mass_storage/helpers/mass_storage_usb.h new file mode 100644 index 00000000000..0f370f98ed6 --- /dev/null +++ b/mass_storage/helpers/mass_storage_usb.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#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); diff --git a/mass_storage/mass_storage_app.c b/mass_storage/mass_storage_app.c new file mode 100644 index 00000000000..a05c1d47be1 --- /dev/null +++ b/mass_storage/mass_storage_app.c @@ -0,0 +1,128 @@ +#include "mass_storage_app_i.h" +#include +#include +#include + +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) { + TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME); + + if(show) { + // Raise timer priority so that animations can play + vTaskPrioritySet(timer_task, configMAX_PRIORITIES - 1); + view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewLoading); + } else { + // Restore default timer priority + vTaskPrioritySet(timer_task, configTIMER_TASK_PRIORITY); + } +} + +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("gui"); + app->fs_api = furi_record_open("storage"); + app->dialogs = furi_record_open("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)); + + 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))) { + scene_manager_next_scene(app->scene_manager, MassStorageSceneWork); + } 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); + + 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); + + // 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("gui"); + furi_record_close("storage"); + furi_record_close("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; +} diff --git a/mass_storage/mass_storage_app.h b/mass_storage/mass_storage_app.h new file mode 100644 index 00000000000..820ad2b6c77 --- /dev/null +++ b/mass_storage/mass_storage_app.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct MassStorageApp MassStorageApp; + +#ifdef __cplusplus +} +#endif diff --git a/mass_storage/mass_storage_app_i.h b/mass_storage/mass_storage_app_i.h new file mode 100644 index 00000000000..715b2a95642 --- /dev/null +++ b/mass_storage/mass_storage_app_i.h @@ -0,0 +1,60 @@ +#pragma once + +#include "mass_storage_app.h" +#include "scenes/mass_storage_scene.h" +#include "helpers/mass_storage_usb.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "views/mass_storage_view.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; + 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; +}; + +typedef enum { + MassStorageAppViewStart, + MassStorageAppViewTextInput, + MassStorageAppViewWork, + MassStorageAppViewLoading, +} MassStorageAppView; + +enum MassStorageCustomEvent { + // Reserve first 100 events for button types and indexes, starting from 0 + MassStorageCustomEventReserved = 100, + + MassStorageCustomEventFileSelect, + MassStorageCustomEventNewImage, + MassStorageCustomEventNameInput, +}; + +void mass_storage_app_show_loading_popup(MassStorageApp* app, bool show); diff --git a/mass_storage/scenes/mass_storage_scene.c b/mass_storage/scenes/mass_storage_scene.c new file mode 100644 index 00000000000..bab24ca4004 --- /dev/null +++ b/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, +}; diff --git a/mass_storage/scenes/mass_storage_scene.h b/mass_storage/scenes/mass_storage_scene.h new file mode 100644 index 00000000000..d43bb4c978e --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// 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 diff --git a/mass_storage/scenes/mass_storage_scene_config.h b/mass_storage/scenes/mass_storage_scene_config.h new file mode 100644 index 00000000000..5798dd53e65 --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_config.h @@ -0,0 +1,4 @@ +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) diff --git a/mass_storage/scenes/mass_storage_scene_file_name.c b/mass_storage/scenes/mass_storage_scene_file_name.c new file mode 100644 index 00000000000..8acd4fad392 --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_file_name.c @@ -0,0 +1,85 @@ +#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 void 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); + + do { + if(!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) break; + uint32_t size_left = size; + uint8_t* buf = malloc(WRITE_BUF_LEN); + memset(buf, 0, WRITE_BUF_LEN); + while(size_left > 0) { + uint32_t wr_len = size_left; + if(wr_len > WRITE_BUF_LEN) { + wr_len = WRITE_BUF_LEN; + } + if(storage_file_write(file, buf, wr_len) != wr_len) break; + size_left -= wr_len; + } + free(buf); + + } while(false); + + storage_file_close(file); + storage_file_free(file); +} + +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); + mass_storage_create_image( + app->fs_api, furi_string_get_cstr(app->file_path), app->new_file_size); + scene_manager_next_scene(app->scene_manager, MassStorageSceneWork); + } + } + 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); +} diff --git a/mass_storage/scenes/mass_storage_scene_file_select.c b/mass_storage/scenes/mass_storage_scene_file_select.c new file mode 100644 index 00000000000..158490e562e --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_file_select.c @@ -0,0 +1,39 @@ +#include "../mass_storage_app_i.h" +#include "furi_hal_power.h" +#include + +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)) { + scene_manager_next_scene(mass_storage->scene_manager, MassStorageSceneWork); + } 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); +} diff --git a/mass_storage/scenes/mass_storage_scene_start.c b/mass_storage/scenes/mass_storage_scene_start.c new file mode 100644 index 00000000000..ab8687c867a --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_start.c @@ -0,0 +1,67 @@ +#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}, +}; + +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 disc 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); +} diff --git a/mass_storage/scenes/mass_storage_scene_work.c b/mass_storage/scenes/mass_storage_scene_work.c new file mode 100644 index 00000000000..ad3630a481d --- /dev/null +++ b/mass_storage/scenes/mass_storage_scene_work.c @@ -0,0 +1,135 @@ +#include "../mass_storage_app_i.h" +#include "../views/mass_storage_view.h" +#include "../helpers/mass_storage_usb.h" +#include + +#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); + 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; + } + 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"); + furi_check(furi_mutex_acquire(app->usb_mutex, FuriWaitForever) == FuriStatusOk); + mass_storage_usb_stop(app->usb); + app->usb = NULL; + furi_check(furi_mutex_release(app->usb_mutex) == FuriStatusOk); +} + +bool mass_storage_scene_work_on_event(void* context, SceneManagerEvent event) { + MassStorageApp* app = context; + bool consumed = false; + if(event.type == SceneManagerEventTypeTick) { + bool ejected; + furi_check(furi_mutex_acquire(app->usb_mutex, FuriWaitForever) == FuriStatusOk); + ejected = app->usb == NULL; + furi_check(furi_mutex_release(app->usb_mutex) == FuriStatusOk); + if(ejected) { + scene_manager_previous_scene(app->scene_manager); + consumed = true; + } + } 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; + + 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); +} diff --git a/mass_storage/views/mass_storage_view.c b/mass_storage/views/mass_storage_view.c new file mode 100644 index 00000000000..ad38f1849ca --- /dev/null +++ b/mass_storage/views/mass_storage_view.c @@ -0,0 +1,67 @@ +#include "mass_storage_view.h" +#include +#include + +struct MassStorage { + View* view; +}; + +typedef struct { + FuriString* file_name; +} MassStorageModel; + +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"); + + elements_string_fit_width(canvas, model->file_name, 87 - 2); + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 12, 25, "Disc image:"); + canvas_draw_str(canvas, 12, 40, furi_string_get_cstr(model->file_name)); +} + +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(); }, + 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); }, + 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); +} diff --git a/mass_storage/views/mass_storage_view.h b/mass_storage/views/mass_storage_view.h new file mode 100644 index 00000000000..96df01c8d5e --- /dev/null +++ b/mass_storage/views/mass_storage_view.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +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);