mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-05-13 22:28:36 -07:00
Merge upstream mass storage changes
This commit is contained in:
@@ -7,7 +7,8 @@
|
|||||||
#define SCSI_TEST_UNIT_READY (0x00)
|
#define SCSI_TEST_UNIT_READY (0x00)
|
||||||
#define SCSI_REQUEST_SENSE (0x03)
|
#define SCSI_REQUEST_SENSE (0x03)
|
||||||
#define SCSI_INQUIRY (0x12)
|
#define SCSI_INQUIRY (0x12)
|
||||||
#define SCSI_READ_CAPACITY_6 (0x25)
|
#define SCSI_READ_FORMAT_CAPACITIES (0x23)
|
||||||
|
#define SCSI_READ_CAPACITY_10 (0x25)
|
||||||
#define SCSI_MODE_SENSE_6 (0x1A)
|
#define SCSI_MODE_SENSE_6 (0x1A)
|
||||||
#define SCSI_READ_10 (0x28)
|
#define SCSI_READ_10 (0x28)
|
||||||
#define SCSI_PREVENT_MEDIUM_REMOVAL (0x1E)
|
#define SCSI_PREVENT_MEDIUM_REMOVAL (0x1E)
|
||||||
@@ -20,7 +21,7 @@ bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len) {
|
|||||||
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// FURI_LOG_I(TAG, "START %02x", cmd[0]);
|
FURI_LOG_T(TAG, "START %02X", cmd[0]);
|
||||||
scsi->cmd = cmd;
|
scsi->cmd = cmd;
|
||||||
scsi->cmd_len = len;
|
scsi->cmd_len = len;
|
||||||
scsi->rx_done = false;
|
scsi->rx_done = false;
|
||||||
@@ -30,14 +31,14 @@ bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len) {
|
|||||||
if(len < 10) return false;
|
if(len < 10) return false;
|
||||||
scsi->write_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
|
scsi->write_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
|
||||||
scsi->write_10.count = cmd[7] << 8 | cmd[8];
|
scsi->write_10.count = cmd[7] << 8 | cmd[8];
|
||||||
FURI_LOG_I(TAG, "SCSI_WRITE_10 %08lx %04x", scsi->write_10.lba, scsi->write_10.count);
|
FURI_LOG_D(TAG, "SCSI_WRITE_10 %08lX %04X", scsi->write_10.lba, scsi->write_10.count);
|
||||||
return true;
|
return true;
|
||||||
}; break;
|
}; break;
|
||||||
case SCSI_READ_10: {
|
case SCSI_READ_10: {
|
||||||
if(len < 10) return false;
|
if(len < 10) return false;
|
||||||
scsi->read_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
|
scsi->read_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
|
||||||
scsi->read_10.count = cmd[7] << 8 | cmd[8];
|
scsi->read_10.count = cmd[7] << 8 | cmd[8];
|
||||||
FURI_LOG_I(TAG, "SCSI_READ_10 %08lx %04x", scsi->read_10.lba, scsi->read_10.count);
|
FURI_LOG_D(TAG, "SCSI_READ_10 %08lX %04X", scsi->read_10.lba, scsi->read_10.count);
|
||||||
return true;
|
return true;
|
||||||
}; break;
|
}; break;
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ 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_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len) {
|
||||||
// FURI_LOG_I(TAG, "RX %02x len %d", scsi->cmd[0], len);
|
FURI_LOG_T(TAG, "RX %02X len %lu", scsi->cmd[0], len);
|
||||||
if(scsi->rx_done) return false;
|
if(scsi->rx_done) return false;
|
||||||
switch(scsi->cmd[0]) {
|
switch(scsi->cmd[0]) {
|
||||||
case SCSI_WRITE_10: {
|
case SCSI_WRITE_10: {
|
||||||
@@ -61,7 +62,7 @@ bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len) {
|
|||||||
return result;
|
return result;
|
||||||
}; break;
|
}; break;
|
||||||
default: {
|
default: {
|
||||||
FURI_LOG_W(TAG, "unexpected scsi rx data cmd=%02x", scsi->cmd[0]);
|
FURI_LOG_W(TAG, "unexpected scsi rx data cmd=%02X", scsi->cmd[0]);
|
||||||
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
||||||
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
||||||
return false;
|
return false;
|
||||||
@@ -70,11 +71,11 @@ 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_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap) {
|
||||||
// FURI_LOG_I(TAG, "TX %02x cap %d", scsi->cmd[0], cap);
|
FURI_LOG_T(TAG, "TX %02X cap %lu", scsi->cmd[0], cap);
|
||||||
if(scsi->tx_done) return false;
|
if(scsi->tx_done) return false;
|
||||||
switch(scsi->cmd[0]) {
|
switch(scsi->cmd[0]) {
|
||||||
case SCSI_REQUEST_SENSE: {
|
case SCSI_REQUEST_SENSE: {
|
||||||
FURI_LOG_I(TAG, "SCSI_REQUEST_SENSE");
|
FURI_LOG_D(TAG, "SCSI_REQUEST_SENSE");
|
||||||
if(cap < 18) return false;
|
if(cap < 18) return false;
|
||||||
memset(data, 0, cap);
|
memset(data, 0, cap);
|
||||||
data[0] = 0x70; // fixed format sense data
|
data[0] = 0x70; // fixed format sense data
|
||||||
@@ -102,31 +103,73 @@ bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t
|
|||||||
return true;
|
return true;
|
||||||
}; break;
|
}; break;
|
||||||
case SCSI_INQUIRY: {
|
case SCSI_INQUIRY: {
|
||||||
FURI_LOG_I(TAG, "SCSI_INQUIRY");
|
FURI_LOG_D(TAG, "SCSI_INQUIRY");
|
||||||
if(scsi->cmd_len < 5) return false;
|
if(scsi->cmd_len < 5) return false;
|
||||||
|
|
||||||
if(cap < 36) return false;
|
if(cap < 36) return false;
|
||||||
|
|
||||||
bool evpd = scsi->cmd[1] & 1;
|
bool evpd = scsi->cmd[1] & 1;
|
||||||
uint8_t page_code = scsi->cmd[2];
|
uint8_t page_code = scsi->cmd[2];
|
||||||
// uint16_t alloc_len = scsi->cmd[3] << 8 | scsi->cmd[4];
|
if(evpd == 0) {
|
||||||
if(evpd) return false;
|
if(page_code != 0) return false;
|
||||||
if(page_code) return false;
|
|
||||||
data[0] = 0x00; // device type: direct access block device
|
data[0] = 0x00; // device type: direct access block device
|
||||||
data[1] = 0x80; // removable: true
|
data[1] = 0x80; // removable: true
|
||||||
data[2] = 0x04; // version
|
data[2] = 0x04; // version
|
||||||
data[3] = 0x02; // response data format
|
data[3] = 0x02; // response data format
|
||||||
data[4] = 31; // additional length (len - 5)
|
data[4] = 31; // additional length (len - 5)
|
||||||
data[5] = 0; // flags
|
data[5] = 0; // flags
|
||||||
data[6] = 0; // flags
|
data[6] = 0; // flags
|
||||||
data[7] = 0; // flags
|
data[7] = 0; // flags
|
||||||
memcpy(data + 8, "Flipper ", 8); // vendor id
|
memcpy(data + 8, "Flipper ", 8); // vendor id
|
||||||
memcpy(data + 16, "Mass Storage ", 16); // product id
|
memcpy(data + 16, "Mass Storage ", 16); // product id
|
||||||
memcpy(data + 32, "0001", 4); // product revision level
|
memcpy(data + 32, "0001", 4); // product revision level
|
||||||
*len = 36;
|
*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;
|
scsi->tx_done = true;
|
||||||
return true;
|
return true;
|
||||||
}; break;
|
}; break;
|
||||||
case SCSI_READ_CAPACITY_6: {
|
case SCSI_READ_CAPACITY_10: {
|
||||||
FURI_LOG_I(TAG, "SCSI_READ_CAPACITY_6");
|
FURI_LOG_D(TAG, "SCSI_READ_CAPACITY_10");
|
||||||
if(cap < 8) return false;
|
if(cap < 8) return false;
|
||||||
uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx);
|
uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx);
|
||||||
uint32_t block_size = SCSI_BLOCK_SIZE;
|
uint32_t block_size = SCSI_BLOCK_SIZE;
|
||||||
@@ -143,7 +186,7 @@ bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t
|
|||||||
return true;
|
return true;
|
||||||
}; break;
|
}; break;
|
||||||
case SCSI_MODE_SENSE_6: {
|
case SCSI_MODE_SENSE_6: {
|
||||||
FURI_LOG_I(TAG, "SCSI_MODE_SENSE_6 %lu", cap);
|
FURI_LOG_D(TAG, "SCSI_MODE_SENSE_6 %lu", cap);
|
||||||
if(cap < 4) return false;
|
if(cap < 4) return false;
|
||||||
data[0] = 3; // mode data length (len - 1)
|
data[0] = 3; // mode data length (len - 1)
|
||||||
data[1] = 0; // medium type
|
data[1] = 0; // medium type
|
||||||
@@ -167,7 +210,7 @@ bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t
|
|||||||
return result;
|
return result;
|
||||||
}; break;
|
}; break;
|
||||||
default: {
|
default: {
|
||||||
FURI_LOG_W(TAG, "unexpected scsi tx data cmd=%02x", scsi->cmd[0]);
|
FURI_LOG_W(TAG, "unexpected scsi tx data cmd=%02X", scsi->cmd[0]);
|
||||||
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
||||||
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
||||||
return false;
|
return false;
|
||||||
@@ -176,7 +219,7 @@ bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool scsi_cmd_end(SCSISession* scsi) {
|
bool scsi_cmd_end(SCSISession* scsi) {
|
||||||
// FURI_LOG_I(TAG, "END %02x", scsi->cmd[0]);
|
FURI_LOG_T(TAG, "END %02X", scsi->cmd[0]);
|
||||||
uint8_t* cmd = scsi->cmd;
|
uint8_t* cmd = scsi->cmd;
|
||||||
uint8_t len = scsi->cmd_len;
|
uint8_t len = scsi->cmd_len;
|
||||||
scsi->cmd = NULL;
|
scsi->cmd = NULL;
|
||||||
@@ -187,33 +230,34 @@ bool scsi_cmd_end(SCSISession* scsi) {
|
|||||||
|
|
||||||
case SCSI_REQUEST_SENSE:
|
case SCSI_REQUEST_SENSE:
|
||||||
case SCSI_INQUIRY:
|
case SCSI_INQUIRY:
|
||||||
case SCSI_READ_CAPACITY_6:
|
case SCSI_READ_FORMAT_CAPACITIES:
|
||||||
|
case SCSI_READ_CAPACITY_10:
|
||||||
case SCSI_MODE_SENSE_6:
|
case SCSI_MODE_SENSE_6:
|
||||||
case SCSI_READ_10:
|
case SCSI_READ_10:
|
||||||
return scsi->tx_done;
|
return scsi->tx_done;
|
||||||
|
|
||||||
case SCSI_TEST_UNIT_READY: {
|
case SCSI_TEST_UNIT_READY: {
|
||||||
FURI_LOG_I(TAG, "SCSI_TEST_UNIT_READY");
|
FURI_LOG_D(TAG, "SCSI_TEST_UNIT_READY");
|
||||||
return true;
|
return true;
|
||||||
}; break;
|
}; break;
|
||||||
case SCSI_PREVENT_MEDIUM_REMOVAL: {
|
case SCSI_PREVENT_MEDIUM_REMOVAL: {
|
||||||
if(len < 6) return false;
|
if(len < 6) return false;
|
||||||
bool prevent = cmd[5];
|
bool prevent = cmd[5];
|
||||||
FURI_LOG_I(TAG, "SCSI_PREVENT_MEDIUM_REMOVAL prevent=%d", prevent);
|
FURI_LOG_D(TAG, "SCSI_PREVENT_MEDIUM_REMOVAL prevent=%d", prevent);
|
||||||
return !prevent;
|
return !prevent;
|
||||||
}; break;
|
}; break;
|
||||||
case SCSI_START_STOP_UNIT: {
|
case SCSI_START_STOP_UNIT: {
|
||||||
if(len < 6) return false;
|
if(len < 6) return false;
|
||||||
bool eject = (cmd[4] & 2) != 0;
|
bool eject = (cmd[4] & 2) != 0;
|
||||||
bool start = (cmd[4] & 1) != 0;
|
bool start = (cmd[4] & 1) != 0;
|
||||||
FURI_LOG_I(TAG, "SCSI_START_STOP_UNIT eject=%d start=%d", eject, start);
|
FURI_LOG_D(TAG, "SCSI_START_STOP_UNIT eject=%d start=%d", eject, start);
|
||||||
if(eject) {
|
if(eject) {
|
||||||
scsi->fn.eject(scsi->fn.ctx);
|
scsi->fn.eject(scsi->fn.ctx);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}; break;
|
}; break;
|
||||||
default: {
|
default: {
|
||||||
FURI_LOG_W(TAG, "unexpected scsi cmd=%02x", cmd[0]);
|
FURI_LOG_W(TAG, "unexpected scsi cmd=%02X", cmd[0]);
|
||||||
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
||||||
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ typedef struct {
|
|||||||
uint8_t cmd_len;
|
uint8_t cmd_len;
|
||||||
uint8_t cmd[16];
|
uint8_t cmd[16];
|
||||||
} __attribute__((packed)) CBW;
|
} __attribute__((packed)) CBW;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t sig;
|
uint32_t sig;
|
||||||
uint32_t tag;
|
uint32_t tag;
|
||||||
@@ -80,11 +81,11 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
while(true) {
|
while(true) {
|
||||||
uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, FuriWaitForever);
|
uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, FuriWaitForever);
|
||||||
if(flags & EventExit) {
|
if(flags & EventExit) {
|
||||||
FURI_LOG_I(TAG, "exit");
|
FURI_LOG_D(TAG, "exit");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(flags & EventReset) {
|
if(flags & EventReset) {
|
||||||
FURI_LOG_I(TAG, "reset");
|
FURI_LOG_D(TAG, "reset");
|
||||||
scsi.sk = 0;
|
scsi.sk = 0;
|
||||||
scsi.asc = 0;
|
scsi.asc = 0;
|
||||||
memset(&cbw, 0, sizeof(cbw));
|
memset(&cbw, 0, sizeof(cbw));
|
||||||
@@ -99,10 +100,10 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
if(flags & EventRxTx) do {
|
if(flags & EventRxTx) do {
|
||||||
switch(state) {
|
switch(state) {
|
||||||
case StateReadCBW: {
|
case StateReadCBW: {
|
||||||
// FURI_LOG_I(TAG, "StateReadCBW");
|
FURI_LOG_T(TAG, "StateReadCBW");
|
||||||
int32_t len = usbd_ep_read(dev, USB_MSC_RX_EP, &cbw, sizeof(cbw));
|
int32_t len = usbd_ep_read(dev, USB_MSC_RX_EP, &cbw, sizeof(cbw));
|
||||||
if(len <= 0) {
|
if(len <= 0) {
|
||||||
// FURI_LOG_I(TAG, "cbw not ready");
|
FURI_LOG_T(TAG, "cbw not ready");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(len != sizeof(cbw) || cbw.sig != CBW_SIG) {
|
if(len != sizeof(cbw) || cbw.sig != CBW_SIG) {
|
||||||
@@ -131,14 +132,14 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
continue;
|
continue;
|
||||||
}; break;
|
}; break;
|
||||||
case StateReadData: {
|
case StateReadData: {
|
||||||
// FURI_LOG_I(TAG, "StateReadData %d/%d", buf_len, cbw.len);
|
FURI_LOG_T(TAG, "StateReadData %lu/%lu", buf_len, cbw.len);
|
||||||
if(!cbw.len) {
|
if(!cbw.len) {
|
||||||
state = StateBuildCSW;
|
state = StateBuildCSW;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
|
uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
|
||||||
if(buf_clamp > buf_cap) {
|
if(buf_clamp > buf_cap) {
|
||||||
FURI_LOG_I(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
|
FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
|
||||||
if(buf) {
|
if(buf) {
|
||||||
free(buf);
|
free(buf);
|
||||||
}
|
}
|
||||||
@@ -149,10 +150,10 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
int32_t len =
|
int32_t len =
|
||||||
usbd_ep_read(dev, USB_MSC_RX_EP, buf + buf_len, buf_clamp - buf_len);
|
usbd_ep_read(dev, USB_MSC_RX_EP, buf + buf_len, buf_clamp - buf_len);
|
||||||
if(len < 0) {
|
if(len < 0) {
|
||||||
// FURI_LOG_I(TAG, "rx not ready %d", len);
|
FURI_LOG_T(TAG, "rx not ready %ld", len);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// FURI_LOG_I(TAG, "clamp %ld len %d", buf_clamp, len);
|
FURI_LOG_T(TAG, "clamp %lu len %ld", buf_clamp, len);
|
||||||
buf_len += len;
|
buf_len += len;
|
||||||
}
|
}
|
||||||
if(buf_len == buf_clamp) {
|
if(buf_len == buf_clamp) {
|
||||||
@@ -172,14 +173,14 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
continue;
|
continue;
|
||||||
}; break;
|
}; break;
|
||||||
case StateWriteData: {
|
case StateWriteData: {
|
||||||
// FURI_LOG_I(TAG, "StateWriteData %d", cbw.len);
|
FURI_LOG_T(TAG, "StateWriteData %lu", cbw.len);
|
||||||
if(!cbw.len) {
|
if(!cbw.len) {
|
||||||
state = StateBuildCSW;
|
state = StateBuildCSW;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
|
uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
|
||||||
if(buf_clamp > buf_cap) {
|
if(buf_clamp > buf_cap) {
|
||||||
FURI_LOG_I(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
|
FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
|
||||||
if(buf) {
|
if(buf) {
|
||||||
free(buf);
|
free(buf);
|
||||||
}
|
}
|
||||||
@@ -198,7 +199,7 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
buf + buf_sent,
|
buf + buf_sent,
|
||||||
MIN(USB_MSC_TX_EP_SIZE, buf_len - buf_sent));
|
MIN(USB_MSC_TX_EP_SIZE, buf_len - buf_sent));
|
||||||
if(len < 0) {
|
if(len < 0) {
|
||||||
// FURI_LOG_I(TAG, "tx not ready %d", len);
|
FURI_LOG_T(TAG, "tx not ready %ld", len);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
buf_sent += len;
|
buf_sent += len;
|
||||||
@@ -210,7 +211,7 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
continue;
|
continue;
|
||||||
}; break;
|
}; break;
|
||||||
case StateBuildCSW: {
|
case StateBuildCSW: {
|
||||||
// FURI_LOG_I(TAG, "StateBuildCSW");
|
FURI_LOG_T(TAG, "StateBuildCSW");
|
||||||
csw.sig = CSW_SIG;
|
csw.sig = CSW_SIG;
|
||||||
csw.tag = cbw.tag;
|
csw.tag = cbw.tag;
|
||||||
if(scsi_cmd_end(&scsi)) {
|
if(scsi_cmd_end(&scsi)) {
|
||||||
@@ -223,7 +224,7 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
continue;
|
continue;
|
||||||
}; break;
|
}; break;
|
||||||
case StateWriteCSW: {
|
case StateWriteCSW: {
|
||||||
// FURI_LOG_I(TAG, "StateWriteCSW");
|
FURI_LOG_T(TAG, "StateWriteCSW");
|
||||||
if(csw.status) {
|
if(csw.status) {
|
||||||
FURI_LOG_W(
|
FURI_LOG_W(
|
||||||
TAG,
|
TAG,
|
||||||
@@ -235,7 +236,7 @@ static int32_t mass_thread_worker(void* context) {
|
|||||||
}
|
}
|
||||||
int32_t len = usbd_ep_write(dev, USB_MSC_TX_EP, &csw, sizeof(csw));
|
int32_t len = usbd_ep_write(dev, USB_MSC_TX_EP, &csw, sizeof(csw));
|
||||||
if(len < 0) {
|
if(len < 0) {
|
||||||
// FURI_LOG_I(TAG, "csw not ready");
|
FURI_LOG_T(TAG, "csw not ready");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(len != sizeof(csw)) {
|
if(len != sizeof(csw)) {
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ static void mass_storage_app_tick_event_callback(void* context) {
|
|||||||
scene_manager_handle_tick_event(app->scene_manager);
|
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* mass_storage_app_alloc(char* arg) {
|
||||||
MassStorageApp* app = malloc(sizeof(MassStorageApp));
|
MassStorageApp* app = malloc(sizeof(MassStorageApp));
|
||||||
app->file_path = furi_string_alloc();
|
app->file_path = furi_string_alloc();
|
||||||
@@ -36,7 +49,6 @@ MassStorageApp* mass_storage_app_alloc(char* arg) {
|
|||||||
|
|
||||||
app->gui = furi_record_open(RECORD_GUI);
|
app->gui = furi_record_open(RECORD_GUI);
|
||||||
app->fs_api = furi_record_open(RECORD_STORAGE);
|
app->fs_api = furi_record_open(RECORD_STORAGE);
|
||||||
app->notifications = furi_record_open(RECORD_NOTIFICATION);
|
|
||||||
app->dialogs = furi_record_open(RECORD_DIALOGS);
|
app->dialogs = furi_record_open(RECORD_DIALOGS);
|
||||||
|
|
||||||
app->view_dispatcher = view_dispatcher_alloc();
|
app->view_dispatcher = view_dispatcher_alloc();
|
||||||
@@ -76,6 +88,10 @@ MassStorageApp* mass_storage_app_alloc(char* arg) {
|
|||||||
view_dispatcher_add_view(
|
view_dispatcher_add_view(
|
||||||
app->view_dispatcher, MassStorageAppViewPopup, popup_get_view(app->popup));
|
app->view_dispatcher, MassStorageAppViewPopup, popup_get_view(app->popup));
|
||||||
|
|
||||||
|
app->loading = loading_alloc();
|
||||||
|
view_dispatcher_add_view(
|
||||||
|
app->view_dispatcher, MassStorageAppViewLoading, loading_get_view(app->loading));
|
||||||
|
|
||||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||||
|
|
||||||
scene_manager_set_scene_state(
|
scene_manager_set_scene_state(
|
||||||
@@ -94,15 +110,18 @@ void mass_storage_app_free(MassStorageApp* app) {
|
|||||||
|
|
||||||
// Views
|
// Views
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewWork);
|
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewWork);
|
||||||
mass_storage_free(app->mass_storage_view);
|
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewVarItemList);
|
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewVarItemList);
|
||||||
variable_item_list_free(app->var_item_list);
|
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewSubmenu);
|
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewSubmenu);
|
||||||
submenu_free(app->submenu);
|
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewTextInput);
|
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewTextInput);
|
||||||
text_input_free(app->text_input);
|
|
||||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewPopup);
|
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewPopup);
|
||||||
|
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewLoading);
|
||||||
|
|
||||||
|
mass_storage_free(app->mass_storage_view);
|
||||||
|
variable_item_list_free(app->var_item_list);
|
||||||
|
submenu_free(app->submenu);
|
||||||
|
text_input_free(app->text_input);
|
||||||
popup_free(app->popup);
|
popup_free(app->popup);
|
||||||
|
loading_free(app->loading);
|
||||||
|
|
||||||
// View dispatcher
|
// View dispatcher
|
||||||
view_dispatcher_free(app->view_dispatcher);
|
view_dispatcher_free(app->view_dispatcher);
|
||||||
@@ -113,7 +132,6 @@ void mass_storage_app_free(MassStorageApp* app) {
|
|||||||
// Close records
|
// Close records
|
||||||
furi_record_close(RECORD_GUI);
|
furi_record_close(RECORD_GUI);
|
||||||
furi_record_close(RECORD_STORAGE);
|
furi_record_close(RECORD_STORAGE);
|
||||||
furi_record_close(RECORD_NOTIFICATION);
|
|
||||||
furi_record_close(RECORD_DIALOGS);
|
furi_record_close(RECORD_DIALOGS);
|
||||||
|
|
||||||
free(app);
|
free(app);
|
||||||
|
|||||||
@@ -9,16 +9,16 @@
|
|||||||
#include <gui/view_dispatcher.h>
|
#include <gui/view_dispatcher.h>
|
||||||
#include <gui/scene_manager.h>
|
#include <gui/scene_manager.h>
|
||||||
#include <dialogs/dialogs.h>
|
#include <dialogs/dialogs.h>
|
||||||
#include <notification/notification_messages.h>
|
|
||||||
#include <gui/modules/widget.h>
|
|
||||||
#include <gui/modules/variable_item_list.h>
|
#include <gui/modules/variable_item_list.h>
|
||||||
#include <gui/modules/submenu.h>
|
#include <gui/modules/submenu.h>
|
||||||
#include <gui/modules/text_input.h>
|
#include <gui/modules/text_input.h>
|
||||||
#include <gui/modules/popup.h>
|
#include <gui/modules/popup.h>
|
||||||
|
#include <gui/modules/loading.h>
|
||||||
#include <storage/storage.h>
|
#include <storage/storage.h>
|
||||||
#include "views/mass_storage_view.h"
|
#include "views/mass_storage_view.h"
|
||||||
|
|
||||||
#define MASS_STORAGE_APP_PATH_FOLDER STORAGE_APP_DATA_PATH_PREFIX
|
#define MASS_STORAGE_APP_PATH_FOLDER STORAGE_APP_DATA_PATH_PREFIX
|
||||||
|
#define MASS_STORAGE_APP_EXTENSION ".img"
|
||||||
#define MASS_STORAGE_FILE_NAME_LEN 40
|
#define MASS_STORAGE_FILE_NAME_LEN 40
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -32,14 +32,12 @@ struct MassStorageApp {
|
|||||||
Storage* fs_api;
|
Storage* fs_api;
|
||||||
ViewDispatcher* view_dispatcher;
|
ViewDispatcher* view_dispatcher;
|
||||||
SceneManager* scene_manager;
|
SceneManager* scene_manager;
|
||||||
NotificationApp* notifications;
|
|
||||||
DialogsApp* dialogs;
|
DialogsApp* dialogs;
|
||||||
Widget* widget;
|
|
||||||
MassStorage* mass_storage_view;
|
|
||||||
VariableItemList* var_item_list;
|
VariableItemList* var_item_list;
|
||||||
Submenu* submenu;
|
Submenu* submenu;
|
||||||
TextInput* text_input;
|
TextInput* text_input;
|
||||||
Popup* popup;
|
Popup* popup;
|
||||||
|
Loading* loading;
|
||||||
|
|
||||||
uint32_t create_image_size;
|
uint32_t create_image_size;
|
||||||
SizeUnit create_size_unit;
|
SizeUnit create_size_unit;
|
||||||
@@ -47,16 +45,19 @@ struct MassStorageApp {
|
|||||||
|
|
||||||
FuriString* file_path;
|
FuriString* file_path;
|
||||||
File* file;
|
File* file;
|
||||||
|
MassStorage* mass_storage_view;
|
||||||
|
|
||||||
FuriMutex* usb_mutex;
|
FuriMutex* usb_mutex;
|
||||||
MassStorageUsb* usb;
|
MassStorageUsb* usb;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
MassStorageAppViewError,
|
|
||||||
MassStorageAppViewWork,
|
|
||||||
MassStorageAppViewVarItemList,
|
MassStorageAppViewVarItemList,
|
||||||
MassStorageAppViewSubmenu,
|
MassStorageAppViewSubmenu,
|
||||||
MassStorageAppViewTextInput,
|
MassStorageAppViewTextInput,
|
||||||
MassStorageAppViewPopup,
|
MassStorageAppViewPopup,
|
||||||
|
MassStorageAppViewLoading,
|
||||||
|
MassStorageAppViewWork,
|
||||||
} MassStorageAppView;
|
} MassStorageAppView;
|
||||||
|
|
||||||
|
void mass_storage_app_show_loading_popup(MassStorageApp* app, bool show);
|
||||||
|
|||||||
@@ -100,14 +100,7 @@ bool mass_storage_scene_create_image_on_event(void* context, SceneManagerEvent e
|
|||||||
scene_manager_next_scene(app->scene_manager, MassStorageSceneCreateImageName);
|
scene_manager_next_scene(app->scene_manager, MassStorageSceneCreateImageName);
|
||||||
break;
|
break;
|
||||||
case VarItemListIndexCreate: {
|
case VarItemListIndexCreate: {
|
||||||
popup_set_header(app->popup, "Creating Image...", 64, 32, AlignCenter, AlignCenter);
|
mass_storage_app_show_loading_popup(app, true);
|
||||||
popup_set_text(app->popup, "", 0, 0, AlignLeft, AlignBottom);
|
|
||||||
popup_set_callback(app->popup, NULL);
|
|
||||||
popup_set_context(app->popup, NULL);
|
|
||||||
popup_set_timeout(app->popup, 0);
|
|
||||||
popup_disable_timeout(app->popup);
|
|
||||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewPopup);
|
|
||||||
|
|
||||||
bool default_name = !strnlen(app->create_name, sizeof(app->create_name));
|
bool default_name = !strnlen(app->create_name, sizeof(app->create_name));
|
||||||
if(default_name) {
|
if(default_name) {
|
||||||
snprintf(
|
snprintf(
|
||||||
@@ -117,7 +110,12 @@ bool mass_storage_scene_create_image_on_event(void* context, SceneManagerEvent e
|
|||||||
app->create_image_size,
|
app->create_image_size,
|
||||||
size_unit_names[app->create_size_unit]);
|
size_unit_names[app->create_size_unit]);
|
||||||
}
|
}
|
||||||
furi_string_printf(app->file_path, APP_DATA_PATH("%s.img"), app->create_name);
|
furi_string_printf(
|
||||||
|
app->file_path,
|
||||||
|
"%s/%s%s",
|
||||||
|
MASS_STORAGE_APP_PATH_FOLDER,
|
||||||
|
app->create_name,
|
||||||
|
MASS_STORAGE_APP_EXTENSION);
|
||||||
|
|
||||||
app->file = storage_file_alloc(app->fs_api);
|
app->file = storage_file_alloc(app->fs_api);
|
||||||
const char* error = NULL;
|
const char* error = NULL;
|
||||||
@@ -136,6 +134,7 @@ bool mass_storage_scene_create_image_on_event(void* context, SceneManagerEvent e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage_file_free(app->file);
|
storage_file_free(app->file);
|
||||||
|
mass_storage_app_show_loading_popup(app, false);
|
||||||
|
|
||||||
if(error) {
|
if(error) {
|
||||||
popup_set_header(
|
popup_set_header(
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ void mass_storage_scene_create_image_name_on_enter(void* context) {
|
|||||||
MassStorageApp* app = context;
|
MassStorageApp* app = context;
|
||||||
TextInput* text_input = app->text_input;
|
TextInput* text_input = app->text_input;
|
||||||
|
|
||||||
text_input_set_header_text(text_input, "Leave empty for default");
|
text_input_set_header_text(text_input, "Image name, empty = default");
|
||||||
|
|
||||||
text_input_set_minimum_length(text_input, 0);
|
text_input_set_minimum_length(text_input, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ void mass_storage_scene_start_on_enter(void* context) {
|
|||||||
|
|
||||||
submenu_add_item(
|
submenu_add_item(
|
||||||
submenu,
|
submenu,
|
||||||
"Emulate Image",
|
"Select Disc Image",
|
||||||
MassStorageSceneFileSelect,
|
MassStorageSceneFileSelect,
|
||||||
mass_storage_scene_start_submenu_callback,
|
mass_storage_scene_start_submenu_callback,
|
||||||
app);
|
app);
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ static bool file_read(
|
|||||||
uint32_t* out_len,
|
uint32_t* out_len,
|
||||||
uint32_t out_cap) {
|
uint32_t out_cap) {
|
||||||
MassStorageApp* app = ctx;
|
MassStorageApp* app = ctx;
|
||||||
// FURI_LOG_I(TAG, "file_read lba=%08lx count=%04x out_cap=%04x", lba, count, out_cap);
|
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)) {
|
if(!storage_file_seek(app->file, lba * SCSI_BLOCK_SIZE, true)) {
|
||||||
FURI_LOG_W(TAG, "seek failed");
|
FURI_LOG_W(TAG, "seek failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
uint16_t clamp = MIN(out_cap, count * SCSI_BLOCK_SIZE);
|
uint16_t clamp = MIN(out_cap, count * SCSI_BLOCK_SIZE);
|
||||||
*out_len = storage_file_read(app->file, out, clamp);
|
*out_len = storage_file_read(app->file, out, clamp);
|
||||||
// FURI_LOG_I(TAG, "%d/%d", *out_len, count * SCSI_BLOCK_SIZE);
|
FURI_LOG_T(TAG, "%lu/%lu", *out_len, count * SCSI_BLOCK_SIZE);
|
||||||
return *out_len == clamp;
|
return *out_len == clamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool file_write(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len) {
|
static bool file_write(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len) {
|
||||||
MassStorageApp* app = ctx;
|
MassStorageApp* app = ctx;
|
||||||
// FURI_LOG_I(TAG, "file_write lba=%08lx count=%04x len=%04x", lba, count, len);
|
FURI_LOG_T(TAG, "file_write lba=%08lX count=%04X len=%08lX", lba, count, len);
|
||||||
if(len != count * SCSI_BLOCK_SIZE) {
|
if(len != count * SCSI_BLOCK_SIZE) {
|
||||||
FURI_LOG_W(TAG, "bad write params count=%u len=%lu", count, len);
|
FURI_LOG_W(TAG, "bad write params count=%u len=%lu", count, len);
|
||||||
return false;
|
return false;
|
||||||
@@ -45,7 +45,7 @@ static uint32_t file_num_blocks(void* ctx) {
|
|||||||
|
|
||||||
static void file_eject(void* ctx) {
|
static void file_eject(void* ctx) {
|
||||||
MassStorageApp* app = ctx;
|
MassStorageApp* app = ctx;
|
||||||
FURI_LOG_I(TAG, "EJECT");
|
FURI_LOG_D(TAG, "EJECT");
|
||||||
furi_check(furi_mutex_acquire(app->usb_mutex, FuriWaitForever) == FuriStatusOk);
|
furi_check(furi_mutex_acquire(app->usb_mutex, FuriWaitForever) == FuriStatusOk);
|
||||||
mass_storage_usb_stop(app->usb);
|
mass_storage_usb_stop(app->usb);
|
||||||
app->usb = NULL;
|
app->usb = NULL;
|
||||||
@@ -54,19 +54,38 @@ static void file_eject(void* ctx) {
|
|||||||
|
|
||||||
bool mass_storage_scene_work_on_event(void* context, SceneManagerEvent event) {
|
bool mass_storage_scene_work_on_event(void* context, SceneManagerEvent event) {
|
||||||
MassStorageApp* app = context;
|
MassStorageApp* app = context;
|
||||||
|
bool consumed = false;
|
||||||
if(event.type == SceneManagerEventTypeTick) {
|
if(event.type == SceneManagerEventTypeTick) {
|
||||||
bool ejected;
|
bool ejected;
|
||||||
furi_check(furi_mutex_acquire(app->usb_mutex, FuriWaitForever) == FuriStatusOk);
|
furi_check(furi_mutex_acquire(app->usb_mutex, FuriWaitForever) == FuriStatusOk);
|
||||||
ejected = app->usb == NULL;
|
ejected = app->usb == NULL;
|
||||||
furi_check(furi_mutex_release(app->usb_mutex) == FuriStatusOk);
|
furi_check(furi_mutex_release(app->usb_mutex) == FuriStatusOk);
|
||||||
if(ejected) scene_manager_previous_scene(app->scene_manager);
|
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 false;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void mass_storage_scene_work_on_enter(void* context) {
|
void mass_storage_scene_work_on_enter(void* context) {
|
||||||
MassStorageApp* app = 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);
|
app->usb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||||
|
|
||||||
FuriString* file_name = furi_string_alloc();
|
FuriString* file_name = furi_string_alloc();
|
||||||
@@ -92,13 +111,18 @@ void mass_storage_scene_work_on_enter(void* context) {
|
|||||||
|
|
||||||
furi_string_free(file_name);
|
furi_string_free(file_name);
|
||||||
|
|
||||||
|
mass_storage_app_show_loading_popup(app, false);
|
||||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewWork);
|
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewWork);
|
||||||
}
|
}
|
||||||
|
|
||||||
void mass_storage_scene_work_on_exit(void* context) {
|
void mass_storage_scene_work_on_exit(void* context) {
|
||||||
MassStorageApp* app = context;
|
MassStorageApp* app = context;
|
||||||
|
mass_storage_app_show_loading_popup(app, true);
|
||||||
|
|
||||||
furi_mutex_free(app->usb_mutex);
|
if(app->usb_mutex) {
|
||||||
|
furi_mutex_free(app->usb_mutex);
|
||||||
|
app->usb_mutex = NULL;
|
||||||
|
}
|
||||||
if(app->usb) {
|
if(app->usb) {
|
||||||
mass_storage_usb_stop(app->usb);
|
mass_storage_usb_stop(app->usb);
|
||||||
app->usb = NULL;
|
app->usb = NULL;
|
||||||
@@ -107,4 +131,5 @@ void mass_storage_scene_work_on_exit(void* context) {
|
|||||||
storage_file_free(app->file);
|
storage_file_free(app->file);
|
||||||
app->file = NULL;
|
app->file = NULL;
|
||||||
}
|
}
|
||||||
|
mass_storage_app_show_loading_popup(app, false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user