diff --git a/package.json b/package.json index 7211b5e4..1ea761c6 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@types/w3c-web-hid": "^1.0.1", "@types/w3c-web-serial": "^1.0.2", "@types/w3c-web-usb": "^1.0.5", + "@types/wicg-file-system-access": "^2020.9.7", "@typescript-eslint/eslint-plugin": "^4.10.0", "@typescript-eslint/parser": "^4.10.0", "babel-eslint": "^10.1.0", diff --git a/src/actions/firmware.action.ts b/src/actions/firmware.action.ts index efdf4f26..4e5f11b1 100644 --- a/src/actions/firmware.action.ts +++ b/src/actions/firmware.action.ts @@ -187,7 +187,7 @@ export const firmwareActionsThunk = { common.firmware.flashFirmwareDialog.bootloaderType!; const flashMode = common.firmware.flashFirmwareDialog.flashMode; - let flashBytes: Buffer; + let flashBytes: Buffer | undefined; if (flashMode === 'fetch_and_flash') { const definitionDocument = entities.keyboardDefinitionDocument!; sendEventToGoogleAnalytics('catalog/flash_firmware', { @@ -211,41 +211,25 @@ export const firmwareActionsThunk = { return; } const blob: Blob = fetchBlobResult.value.blob; - try { - flashBytes = intelHex.parse( - Buffer.from(new Uint8Array(await blob.arrayBuffer())) - ).data; - } catch (error) { - console.error(error); - dispatch( - NotificationActions.addError( - 'Reading the firmware binary failed.', - error - ) - ); - dispatch(FlashFirmwareDialogActions.appendLog(`Error: ${error}`)); - dispatch(FlashFirmwareDialogActions.updateFlashing(false)); + flashBytes = createFlashBytes( + Buffer.from(new Uint8Array(await blob.arrayBuffer())), + bootloaderType, + dispatch + ); + if (flashBytes === undefined) { return; } } else { - try { - flashBytes = intelHex.parse( - Buffer.from( - new Uint8Array( - common.firmware.uploadFirmwareDialog.firmwareFileBuffer! - ) - ) - ).data; - } catch (error) { - console.error(error); - dispatch( - NotificationActions.addError( - 'Reading the firmware binary failed.', - error + flashBytes = createFlashBytes( + Buffer.from( + new Uint8Array( + common.firmware.uploadFirmwareDialog.firmwareFileBuffer! ) - ); - dispatch(FlashFirmwareDialogActions.appendLog(`Error: ${error}`)); - dispatch(FlashFirmwareDialogActions.updateFlashing(false)); + ), + bootloaderType, + dispatch + ); + if (flashBytes === undefined) { return; } } @@ -307,3 +291,30 @@ export const firmwareActionsThunk = { } }, }; + +const createFlashBytes = ( + buffer: Buffer, + bootloaderType: IBootloaderType, + dispatch: ThunkDispatch +): Buffer | undefined => { + try { + switch (bootloaderType) { + case 'caterina': + case 'dfu': + return intelHex.parse(buffer).data; + case 'copy': + return buffer; + } + } catch (error) { + console.error(error); + dispatch( + NotificationActions.addError( + 'Creating the firmware binary failed.', + error + ) + ); + dispatch(FlashFirmwareDialogActions.appendLog(`Error: ${error}`)); + dispatch(FlashFirmwareDialogActions.updateFlashing(false)); + return undefined; + } +}; diff --git a/src/components/keyboards/editdefinition/firmwareform/FirmwareForm.tsx b/src/components/keyboards/editdefinition/firmwareform/FirmwareForm.tsx index bd5d0d16..d8b73f89 100644 --- a/src/components/keyboards/editdefinition/firmwareform/FirmwareForm.tsx +++ b/src/components/keyboards/editdefinition/firmwareform/FirmwareForm.tsx @@ -252,6 +252,7 @@ export default function FirmwareForm(props: FirmwareFormProps) { > caterina dfu + copy @@ -602,6 +603,7 @@ function EditDialog(props: IEditDialogProps) { > caterina dfu + copy ) : ( diff --git a/src/services/firmware/FirmwareWriterWebApiImpl.ts b/src/services/firmware/FirmwareWriterWebApiImpl.ts index 5ce0b6b7..ff3f0be5 100644 --- a/src/services/firmware/FirmwareWriterWebApiImpl.ts +++ b/src/services/firmware/FirmwareWriterWebApiImpl.ts @@ -11,6 +11,7 @@ import { IUsb } from './usb/Usb'; import WebUsb from './usb/WebUsb'; import { IBootloader } from './Bootloader'; import { DfuBootloader } from './dfu/DfuBootloader'; +import { WebFileSystem } from './copy/WebFileSystem'; const BAUD_RATE = 115200; const BUFFER_SIZE = 81920; @@ -55,6 +56,14 @@ export class FirmwareWriterWebApiImpl implements IFirmwareWriter { } const bootloader: IBootloader = createDfuBootloaderResult.bootloader!; return await bootloader.write(flashBytes, eepromBytes, progress, phase); + } else if (bootloaderType === 'copy') { + const fileSystem = new WebFileSystem(); + const openResult = await fileSystem.open(); + if (!openResult.success) { + return openResult; + } + phase('opened'); + return await fileSystem.write(flashBytes, eepromBytes, progress, phase); } else { throw new Error(`Unknown bootloader type: ${bootloaderType}`); } diff --git a/src/services/firmware/Types.ts b/src/services/firmware/Types.ts index ce9bd8de..90f9275c 100644 --- a/src/services/firmware/Types.ts +++ b/src/services/firmware/Types.ts @@ -32,7 +32,7 @@ export const MCU: IMcuMap = { }, }; -export const ALL_BOOTLOADER_TYPE = ['caterina', 'dfu'] as const; +export const ALL_BOOTLOADER_TYPE = ['caterina', 'dfu', 'copy'] as const; type bootloaderTypeTuple = typeof ALL_BOOTLOADER_TYPE; export type IBootloaderType = bootloaderTypeTuple[number]; diff --git a/src/services/firmware/copy/WebFileSystem.ts b/src/services/firmware/copy/WebFileSystem.ts new file mode 100644 index 00000000..41c078d7 --- /dev/null +++ b/src/services/firmware/copy/WebFileSystem.ts @@ -0,0 +1,84 @@ +import { IBootloader, IBootloaderReadResult } from '../Bootloader'; +import { + FirmwareWriterPhaseListener, + FirmwareWriterProgressListener, +} from '../FirmwareWriter'; +import { IResult } from '../Types'; + +export class WebFileSystem implements IBootloader { + private directoryHandle: any | undefined; + + constructor() { + this.directoryHandle = undefined; + } + + async open(): Promise { + try { + this.directoryHandle = await window.showDirectoryPicker({ + mode: 'readwrite', + }); + return { success: true }; + } catch (error) { + return { + success: false, + error: `Opening a directory failed: ${error}`, + cause: error, + }; + } + } + + async read( + // eslint-disable-next-line no-unused-vars + size: number, + // eslint-disable-next-line no-unused-vars + progress: FirmwareWriterProgressListener, + // eslint-disable-next-line no-unused-vars + phase: FirmwareWriterPhaseListener + ): Promise { + throw new Error('This method never be called.'); + } + + async verify( + // eslint-disable-next-line no-unused-vars + bytes: Uint8Array, + // eslint-disable-next-line no-unused-vars + progress: FirmwareWriterProgressListener, + // eslint-disable-next-line no-unused-vars + phase: FirmwareWriterPhaseListener + ): Promise { + throw new Error('This method never be called.'); + } + + async write( + flashBytes: Uint8Array, + // eslint-disable-next-line no-unused-vars + eepromBytes: Uint8Array | null, + progress: FirmwareWriterProgressListener, + phase: FirmwareWriterPhaseListener + ): Promise { + if (this.directoryHandle === undefined) { + throw new Error('A target directory is not opened.'); + } + try { + progress('Start writing firmware to a file.'); + const fileHandle = await this.directoryHandle.getFileHandle( + 'firmware.uf2', + { create: true } + ); + const writable = await fileHandle.createWritable(); + await writable.write(flashBytes); + await writable.close(); + progress('Writing firmware to a file is completed.'); + phase('wrote'); + this.directoryHandle = undefined; + phase('closed'); + return { success: true }; + } catch (error) { + return { + success: false, + error: `Writing firmware to a file failed: ${error}`, + cause: error, + }; + } + } +} diff --git a/yarn.lock b/yarn.lock index 3df86fbf..82f41749 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3839,6 +3839,11 @@ anymatch "^3.0.0" source-map "^0.6.0" +"@types/wicg-file-system-access@^2020.9.7": + version "2020.9.7" + resolved "https://registry.yarnpkg.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.7.tgz#8cd797c9b81ed876cf41158b10a634623831fccf" + integrity sha512-fjEImGBMKeoFE2pgX17XkNPAmgZdVs3MZcEAN7uW6vj2UxJR47WxKvmcR8TBgKpozbc1Gqxb8KaT8/VUgWEmTQ== + "@types/yargs-parser@*": version "20.2.1" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129"