From 545ff8c841f15010541a094bd01fb9ef41e96d20 Mon Sep 17 00:00:00 2001 From: jLynx Date: Sun, 14 Jan 2024 11:43:22 +1300 Subject: [PATCH 01/12] Can now display and navigate folder structure --- package-lock.json | 52 ++++- package.json | 3 + src/app/components/Controller/Controller.tsx | 180 +++++++++++++++++- .../FileStructure/FileStructure.tsx | 11 ++ 4 files changed, 240 insertions(+), 6 deletions(-) create mode 100644 src/app/components/FileStructure/FileStructure.tsx diff --git a/package-lock.json b/package-lock.json index 50cadf1..5fef651 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.1.0", "dependencies": { "@ducanh2912/next-pwa": "^10.0.2", + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@types/w3c-web-serial": "^1.0.6", "eslint-plugin-tailwindcss": "^3.13.1", "install": "^0.13.0", @@ -1900,6 +1903,51 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", + "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -8883,7 +8931,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -8960,8 +9007,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/read-cache": { "version": "1.0.0", diff --git a/package.json b/package.json index 7b4b8cf..21aadfc 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ }, "dependencies": { "@ducanh2912/next-pwa": "^10.0.2", + "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-solid-svg-icons": "^6.5.1", + "@fortawesome/react-fontawesome": "^0.2.0", "@types/w3c-web-serial": "^1.0.6", "eslint-plugin-tailwindcss": "^3.13.1", "install": "^0.13.0", diff --git a/src/app/components/Controller/Controller.tsx b/src/app/components/Controller/Controller.tsx index 430a8cc..d7ab9c6 100644 --- a/src/app/components/Controller/Controller.tsx +++ b/src/app/components/Controller/Controller.tsx @@ -1,6 +1,19 @@ "use client"; -import React, { ChangeEvent, useEffect, useRef, useState } from "react"; +import { + faFile, + faFolder, + faFolderOpen, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { + ChangeEvent, + ReactNode, + useEffect, + useRef, + useState, +} from "react"; +import { FileStructure } from "../FileStructure/FileStructure"; import HotkeyButton from "../HotkeyButton/HotkeyButton"; import { useSerial } from "../SerialLoader/SerialLoader"; import { DataPacket } from "../SerialProvider/SerialProvider"; @@ -13,6 +26,7 @@ const Controller = () => { const [command, setCommand] = useState(""); const [autoUpdateFrame, setAutoUpdateFrame] = useState(true); const [loadingFrame, setLoadingFrame] = useState(true); + const [dirStructure, setDirStructure] = useState(); const canvasRef = useRef(null); const started = useRef(false); @@ -59,12 +73,54 @@ const Controller = () => { if (serial.isOpen && !serial.isReading && !started.current) { started.current = true; serial.startReading(); - write(setDeviceTime(), false); - write("screenframeshort", false); + // await write(setDeviceTime(), false); + + const initSerialSetupCalls = async () => { + await write(setDeviceTime(), false); + + await fetchFolderStructure(); + + await write("screenframeshort", false); + }; + + const fetchFolderStructure = async () => { + const rootStructure = await write(`ls /`, false, true); // get the children directories + + if (rootStructure.response) { + const rootItems = rootStructure.response.split("\r\n").slice(1, -1); + + const fileStructures = parseDirectories(rootItems); + // console.log(fileStructures, rootStructure, rootItems); + setDirStructure(fileStructures); + } + }; + + initSerialSetupCalls(); + + // await write("screenframeshort", false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [serial]); + // Helper function to parse the directories into FileStructure + const parseDirectories = ( + dirList: string[], + parentPath: string = "/" + ): FileStructure[] => { + return dirList.map((path) => { + const isFolder = path.endsWith("/"); + const name = isFolder ? path.slice(0, -1) : path; + + return { + name, + path: parentPath, + type: isFolder ? "folder" : "file", + children: isFolder ? [] : undefined, + isOpen: false, + }; + }); + }; + const renderFrame = () => { const width = 241; const height = 321; @@ -327,10 +383,128 @@ const Controller = () => { // } }; + const updateDirectoryStructure = ( + structure: FileStructure[], + targetFolder: FileStructure, + newChildren: FileStructure[] + ): FileStructure[] => { + return structure.map((folder) => { + if (folder.name === targetFolder.name) { + return { ...folder, children: newChildren, isOpen: !folder.isOpen }; + } + + if (folder.children) { + return { + ...folder, + children: updateDirectoryStructure( + folder.children, + targetFolder, + newChildren + ), + }; + } + + return folder; + }); + }; + + const FolderToggle = ({ + folder, + indent, + }: { + folder: FileStructure; + indent: number; + }) => { + const toggleFolder = async () => { + let fileStructures: FileStructure[] = folder.children || []; + if (!folder.isOpen) { + const childDirs = await write( + `ls ${folder.path + folder.name}`, + false, + true + ); + + if (childDirs.response) { + const childItems = childDirs.response.split("\r\n").slice(1, -1); + fileStructures = parseDirectories( + childItems, + `${folder.path}${folder.name}/` + ); + } + } + setDirStructure( + (prevState) => + prevState && + updateDirectoryStructure(prevState, folder, fileStructures) + ); + }; + + return ( +
+
+ +

{folder.name}

+
+ {folder.isOpen && + folder.children && + folder.children.map((file, index) => ( + + ))} +
+ ); + }; + + useEffect(() => { + console.log(dirStructure); + }, [dirStructure]); + + // File Component + const File = ({ file }: { file: FileStructure }) => ( +
+ +

{file.name}

+
+ ); + + // ListItem Component + const ListItem = ({ + item, + indent, + }: { + item: FileStructure; + indent: number; + }) => { + return ( +
+ {item.type === "folder" ? ( + + ) : ( +
+ +
+ )} +
+ ); + }; + return ( <>

Connected to HackRF!

+ +
+ {dirStructure && + dirStructure.map((file, index) => ( + + ))} +
+ {!serial.isReading && "Enable console for buttons to enable"}
Date: Sun, 14 Jan 2024 11:47:41 +1300 Subject: [PATCH 02/12] Added the ability to download a file --- src/app/components/Controller/Controller.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/components/Controller/Controller.tsx b/src/app/components/Controller/Controller.tsx index d7ab9c6..a8ef486 100644 --- a/src/app/components/Controller/Controller.tsx +++ b/src/app/components/Controller/Controller.tsx @@ -90,7 +90,6 @@ const Controller = () => { const rootItems = rootStructure.response.split("\r\n").slice(1, -1); const fileStructures = parseDirectories(rootItems); - // console.log(fileStructures, rootStructure, rootItems); setDirStructure(fileStructures); } }; @@ -466,7 +465,13 @@ const Controller = () => { // File Component const File = ({ file }: { file: FileStructure }) => ( -
+
{ + console.log(file.path + file.name); + downloadFile(file.path + file.name); + }} + >

{file.name}

From eaff3501b18b5b805827e7e5352e8a43f97e0483 Mon Sep 17 00:00:00 2001 From: jLynx Date: Sun, 14 Jan 2024 14:35:10 +1300 Subject: [PATCH 03/12] WIP --- src/app/components/Controller/Controller.tsx | 81 ++++++++++++-------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/src/app/components/Controller/Controller.tsx b/src/app/components/Controller/Controller.tsx index a8ef486..3dcb256 100644 --- a/src/app/components/Controller/Controller.tsx +++ b/src/app/components/Controller/Controller.tsx @@ -4,6 +4,7 @@ import { faFile, faFolder, faFolderOpen, + faUpload, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { @@ -23,11 +24,13 @@ const Controller = () => { const { serial, consoleMessage } = useSerial(); const [consoleMessageList, setConsoleMessageList] = useState(""); const [updateStatus, setUpdateStatus] = useState(""); + const [selectedUploadFolder, setSelectedUploadFolder] = useState("/"); const [command, setCommand] = useState(""); const [autoUpdateFrame, setAutoUpdateFrame] = useState(true); const [loadingFrame, setLoadingFrame] = useState(true); const [dirStructure, setDirStructure] = useState(); const canvasRef = useRef(null); + const fileInputRef = useRef(null); // Create a reference const started = useRef(false); @@ -73,7 +76,6 @@ const Controller = () => { if (serial.isOpen && !serial.isReading && !started.current) { started.current = true; serial.startReading(); - // await write(setDeviceTime(), false); const initSerialSetupCalls = async () => { await write(setDeviceTime(), false); @@ -95,8 +97,6 @@ const Controller = () => { }; initSerialSetupCalls(); - - // await write("screenframeshort", false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [serial]); @@ -302,7 +302,8 @@ const Controller = () => { URL.revokeObjectURL(url); }; - const onFileChange = (event: ChangeEvent) => { + const onFileChange = (event: ChangeEvent, path: string) => { + console.log("HIT!"); const fileList = event.target.files; if (!fileList) return; @@ -313,7 +314,8 @@ const Controller = () => { const arrayBuffer = reader.result; if (arrayBuffer instanceof ArrayBuffer) { let bytes = new Uint8Array(arrayBuffer); - uploadFile(file.name, bytes); + console.log(path + file.name); + // uploadFile(path + file.name, bytes); } }; @@ -423,6 +425,7 @@ const Controller = () => { true ); + // Currently dirs with spaces in them are not valid if (childDirs.response) { const childItems = childDirs.response.split("\r\n").slice(1, -1); fileStructures = parseDirectories( @@ -441,15 +444,29 @@ const Controller = () => { return (
+
+ +

{folder.name}

+
+ { + // e.stopPropagation(); + // e.preventDefault(); + setSelectedUploadFolder(folder.path + folder.name + "/"); + fileInputRef.current?.click(); + }} /> -

{folder.name}

+ {folder.isOpen && folder.children && folder.children.map((file, index) => ( @@ -502,14 +519,6 @@ const Controller = () => { <>

Connected to HackRF!

- -
- {dirStructure && - dirStructure.map((file, index) => ( - - ))} -
- {!serial.isReading && "Enable console for buttons to enable"}
{ ) : (
-
- {/* */} - {/* */} - Date: Sun, 14 Jan 2024 15:06:37 +1300 Subject: [PATCH 04/12] WIP make this code more readable --- src/app/components/Controller/Controller.tsx | 445 ++---------------- .../DeviceButtons/DeviceButtons.tsx | 89 ++++ .../components/FileBrowser/FileBrowser.tsx | 169 +++++++ .../FileStructure/FileStructure.tsx | 11 - src/app/utils/fileUtils.tsx | 30 ++ src/app/utils/serialUtils.tsx | 161 +++++++ 6 files changed, 476 insertions(+), 429 deletions(-) create mode 100644 src/app/components/DeviceButtons/DeviceButtons.tsx create mode 100644 src/app/components/FileBrowser/FileBrowser.tsx delete mode 100644 src/app/components/FileStructure/FileStructure.tsx create mode 100644 src/app/utils/fileUtils.tsx create mode 100644 src/app/utils/serialUtils.tsx diff --git a/src/app/components/Controller/Controller.tsx b/src/app/components/Controller/Controller.tsx index 3dcb256..e6cd660 100644 --- a/src/app/components/Controller/Controller.tsx +++ b/src/app/components/Controller/Controller.tsx @@ -1,23 +1,16 @@ "use client"; +import React, { ChangeEvent, useEffect, useRef, useState } from "react"; +import { parseDirectories } from "@/app/utils/fileUtils"; import { - faFile, - faFolder, - faFolderOpen, - faUpload, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { - ChangeEvent, - ReactNode, - useEffect, - useRef, - useState, -} from "react"; -import { FileStructure } from "../FileStructure/FileStructure"; + UploadFile, + Write, + downloadFileFromUrl, +} from "@/app/utils/serialUtils"; +import { DeviceButtons } from "../DeviceButtons/DeviceButtons"; +import { FileBrowser, FileStructure } from "../FileBrowser/FileBrowser"; import HotkeyButton from "../HotkeyButton/HotkeyButton"; import { useSerial } from "../SerialLoader/SerialLoader"; -import { DataPacket } from "../SerialProvider/SerialProvider"; import ToggleSwitch from "../ToggleSwitch/ToggleSwitch"; const Controller = () => { @@ -34,28 +27,8 @@ const Controller = () => { const started = useRef(false); - const write = async ( - command: string, - updateFrame: boolean, - awaitResponse: boolean = true - ) => { - let data: DataPacket = { - id: 0, - command: "", - response: null, - }; - if (awaitResponse) data = await serial.queueWriteAndResponse(command); - else serial.queueWrite(command); - if (updateFrame) { - serial.queueWrite("screenframeshort"); - setLoadingFrame(true); - } - - return data; - }; - const sendCommand = async () => { - await write(command, false); + await Write(command, false); setCommand(""); }; @@ -78,15 +51,15 @@ const Controller = () => { serial.startReading(); const initSerialSetupCalls = async () => { - await write(setDeviceTime(), false); + await Write(setDeviceTime(), false); await fetchFolderStructure(); - await write("screenframeshort", false); + await Write("screenframeshort", false); }; const fetchFolderStructure = async () => { - const rootStructure = await write(`ls /`, false, true); // get the children directories + const rootStructure = await Write(`ls /`, false, true); // get the children directories if (rootStructure.response) { const rootItems = rootStructure.response.split("\r\n").slice(1, -1); @@ -101,25 +74,6 @@ const Controller = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [serial]); - // Helper function to parse the directories into FileStructure - const parseDirectories = ( - dirList: string[], - parentPath: string = "/" - ): FileStructure[] => { - return dirList.map((path) => { - const isFolder = path.endsWith("/"); - const name = isFolder ? path.slice(0, -1) : path; - - return { - name, - path: parentPath, - type: isFolder ? "folder" : "file", - children: isFolder ? [] : undefined, - isOpen: false, - }; - }); - }; - const renderFrame = () => { const width = 241; const height = 321; @@ -178,128 +132,8 @@ const Controller = () => { e.preventDefault(); let key_code = e.key.length === 1 ? e.key.charCodeAt(0) : e.keyCode; const keyHex = key_code.toString(16).padStart(2, "0").toUpperCase(); - write(`keyboard ${keyHex}`, autoUpdateFrame); - } - }; - - const uploadFile = async (filePath: string, bytes: Uint8Array) => { - await write("fclose", false); - await write(`fopen ${filePath}`, false); - - await write(`fseek 0`, false); - - let blob = new Blob([bytes]); - const arrayBuffer = await blob.arrayBuffer(); - - const chunkSize = 100000; - - console.log("Total length: ", arrayBuffer.byteLength); - - let startTime = Date.now(); - let totalTime = 0; - - for (let i = 0; i < arrayBuffer.byteLength; i += chunkSize) { - const chunk = arrayBuffer.slice(i, i + chunkSize); - - await write(`fwb ${chunk.byteLength}`, false, true); - await serial.queueWriteAndResponseBinary(new Uint8Array(chunk)); - - // calculate elapsed time and average time per chunk - let elapsed = Date.now() - startTime; - totalTime += elapsed; - let avgTimePerChunk = totalTime / (i / chunkSize + 1); - - // estimate remaining time in seconds - let remainingChunks = (arrayBuffer.byteLength - i) / chunkSize; - let estRemainingTime = (remainingChunks * avgTimePerChunk) / 1000; - - console.log( - "Chunk done", - i, - arrayBuffer.byteLength, - ((i / arrayBuffer.byteLength) * 100).toFixed(2) + "%", - "Estimated time remaining: " + estRemainingTime.toFixed(0) + " seconds" - ); - setUpdateStatus( - `${((i / arrayBuffer.byteLength) * 100).toFixed( - 2 - )}% Estimated time remaining: ${estRemainingTime.toFixed(0)} seconds` - ); - - // reset start time for next iteration - startTime = Date.now(); + Write(`keyboard ${keyHex}`, autoUpdateFrame); } - console.log("FILE DONE"); - setUpdateStatus(`File upload complete!`); - - await write("fclose", false); - }; - - const downloadFile = async (filePath: string) => { - await write("fclose", false); - let sizeResponse = await write(`filesize ${filePath}`, false, true); - if (!sizeResponse.response) { - console.error("Error downloading (size) file"); - } - let size = parseInt(sizeResponse.response?.split("\r\n")[1] || "0"); - await write(`fopen ${filePath}`, false); - - await write(`fseek 0`, false); - - let rem = size; - let chunk = 62 * 15; - - let dataObject: Uint8Array = new Uint8Array(); - - while (rem > 0) { - if (rem < chunk) { - chunk = rem; - } - let lines = - (await write(`fread ${chunk.toString()}`, false, true)).response - ?.split("\r\n") - .slice(1) - .slice(0, -2) - .join("") || ""; - - let bArr = hexToBytes(lines); - rem -= bArr.length; - dataObject = new Uint8Array([...dataObject, ...Array.from(bArr)]); - } - downloadFileFromBytes( - dataObject, - filePath.substring(filePath.lastIndexOf("/") + 1) - ); - await write("fclose", false); - }; - - const hexToBytes = (hex: string) => { - let bytes = new Uint8Array(Math.ceil(hex.length / 2)); - for (let i = 0; i < bytes.length; i++) - bytes[i] = parseInt(hex.substr(i * 2, 2), 16); - return bytes; - }; - - const bytesToHex = (bytes: Uint8Array) => { - return bytes - .map((byte: any) => byte.toString(16).padStart(2, "0")) - .join(""); - }; - - const downloadFileFromBytes = ( - bytes: Uint8Array | string, - fileName: string = "output.txt" - ) => { - let blob = new Blob([bytes]); - let url = URL.createObjectURL(blob); - let a = document.createElement("a"); - a.style.display = "none"; - a.href = url; - a.download = fileName; // Filename - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); }; const onFileChange = (event: ChangeEvent, path: string) => { @@ -315,7 +149,7 @@ const Controller = () => { if (arrayBuffer instanceof ArrayBuffer) { let bytes = new Uint8Array(arrayBuffer); console.log(path + file.name); - // uploadFile(path + file.name, bytes); + UploadFile(path + file.name, bytes); } }; @@ -328,31 +162,6 @@ const Controller = () => { } }; - interface DownloadedFile { - blob: Blob; - filename: string; - } - - const downloadFileFromUrl = async (url: string): Promise => { - const response = await fetch(url); - - if (!response.ok) { - throw new Error("Network response was not ok"); - } - - const contentDispositionHeader = response.headers.get( - "Content-Disposition" - ); - const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; - let matches = contentDispositionHeader?.match(filenameRegex); - let filename = - matches && matches[1] ? matches[1].replace(/['"]/g, "") : "unknown.fail"; - - const blob = await response.blob(); - - return { blob, filename }; - }; - const flashLatestFirmware = async () => { const fileBlob = await downloadFileFromUrl( "https://hackrf.app/api/fetch_nightly_firmware" @@ -360,12 +169,12 @@ const Controller = () => { console.log("Downloading firmware update...", fileBlob.filename); - await uploadFile( + await UploadFile( `/FIRMWARE/${fileBlob.filename}`, new Uint8Array(await fileBlob.blob.arrayBuffer()) ); - await write(`flash /FIRMWARE/${fileBlob.filename}`, false, true); + await Write(`flash /FIRMWARE/${fileBlob.filename}`, false, true); console.log("DONE! firmware complete. Rebooting..."); alert("Firmware update complete! Please wait for your device to reboot."); }; @@ -384,137 +193,10 @@ const Controller = () => { // } }; - const updateDirectoryStructure = ( - structure: FileStructure[], - targetFolder: FileStructure, - newChildren: FileStructure[] - ): FileStructure[] => { - return structure.map((folder) => { - if (folder.name === targetFolder.name) { - return { ...folder, children: newChildren, isOpen: !folder.isOpen }; - } - - if (folder.children) { - return { - ...folder, - children: updateDirectoryStructure( - folder.children, - targetFolder, - newChildren - ), - }; - } - - return folder; - }); - }; - - const FolderToggle = ({ - folder, - indent, - }: { - folder: FileStructure; - indent: number; - }) => { - const toggleFolder = async () => { - let fileStructures: FileStructure[] = folder.children || []; - if (!folder.isOpen) { - const childDirs = await write( - `ls ${folder.path + folder.name}`, - false, - true - ); - - // Currently dirs with spaces in them are not valid - if (childDirs.response) { - const childItems = childDirs.response.split("\r\n").slice(1, -1); - fileStructures = parseDirectories( - childItems, - `${folder.path}${folder.name}/` - ); - } - } - setDirStructure( - (prevState) => - prevState && - updateDirectoryStructure(prevState, folder, fileStructures) - ); - }; - - return ( -
-
-
- -

{folder.name}

-
- - { - // e.stopPropagation(); - // e.preventDefault(); - setSelectedUploadFolder(folder.path + folder.name + "/"); - fileInputRef.current?.click(); - }} - /> -
- - {folder.isOpen && - folder.children && - folder.children.map((file, index) => ( - - ))} -
- ); - }; - useEffect(() => { console.log(dirStructure); }, [dirStructure]); - // File Component - const File = ({ file }: { file: FileStructure }) => ( -
{ - console.log(file.path + file.name); - downloadFile(file.path + file.name); - }} - > - -

{file.name}

-
- ); - - // ListItem Component - const ListItem = ({ - item, - indent, - }: { - item: FileStructure; - indent: number; - }) => { - return ( -
- {item.type === "folder" ? ( - - ) : ( -
- -
- )} -
- ); - }; - return ( <>
@@ -539,7 +221,7 @@ const Controller = () => { { - if (!autoUpdateFrame) write("screenframeshort", false); + if (!autoUpdateFrame) Write("screenframeshort", false); setAutoUpdateFrame(!autoUpdateFrame); }} /> @@ -549,7 +231,7 @@ const Controller = () => { onClickFunction={() => { if (!loadingFrame) { setLoadingFrame(true); - write("screenframeshort", false); + Write("screenframeshort", false); } }} className="h-6 w-6 bg-blue-500" @@ -572,87 +254,15 @@ const Controller = () => { const x = event.clientX - bounds.left; const y = event.clientY - bounds.top; - write(`touch ${x} ${y}`, autoUpdateFrame); + Write(`touch ${x} ${y}`, autoUpdateFrame); }} />
-
-
-
-
- write("button 2", autoUpdateFrame)} - className="h-16 w-16 bg-green-500" - shortcutKeys={"ArrowLeft"} - /> - - write("button 4", autoUpdateFrame)} - className="h-16 w-16 bg-green-500" - shortcutKeys={"ArrowUp"} - /> - write("button 5", autoUpdateFrame)} - className="h-16 w-16 bg-blue-500" - shortcutKeys={"Enter"} - /> - write("button 3", autoUpdateFrame)} - className="h-16 w-16 bg-green-500" - shortcutKeys={"ArrowDown"} - /> -
- write("button 1", autoUpdateFrame)} - className="h-16 w-16 bg-green-500" - shortcutKeys={"ArrowRight"} - /> - -
-
-
- write("button 6", autoUpdateFrame)} - className="h-16 w-16 bg-slate-400" - shortcutKeys={"mod+D"} - /> - -
-
+
{!serial.isReading ? ( @@ -680,7 +290,6 @@ const Controller = () => { }} /> {/*
diff --git a/src/app/components/DeviceButtons/DeviceButtons.tsx b/src/app/components/DeviceButtons/DeviceButtons.tsx new file mode 100644 index 0000000..e51c529 --- /dev/null +++ b/src/app/components/DeviceButtons/DeviceButtons.tsx @@ -0,0 +1,89 @@ +import { Write } from "@/app/utils/serialUtils"; +import HotkeyButton from "../HotkeyButton/HotkeyButton"; + +export const DeviceButtons = ({ + loadingFrame, + autoUpdateFrame, +}: { + loadingFrame: boolean; + autoUpdateFrame: boolean; +}) => { + return ( +
+
+
+
+ Write("button 2", autoUpdateFrame)} + className="h-16 w-16 bg-green-500" + shortcutKeys={"ArrowLeft"} + /> + + Write("button 4", autoUpdateFrame)} + className="h-16 w-16 bg-green-500" + shortcutKeys={"ArrowUp"} + /> + Write("button 5", autoUpdateFrame)} + className="h-16 w-16 bg-blue-500" + shortcutKeys={"Enter"} + /> + Write("button 3", autoUpdateFrame)} + className="h-16 w-16 bg-green-500" + shortcutKeys={"ArrowDown"} + /> +
+ Write("button 1", autoUpdateFrame)} + className="h-16 w-16 bg-green-500" + shortcutKeys={"ArrowRight"} + /> + +
+
+
+ Write("button 6", autoUpdateFrame)} + className="h-16 w-16 bg-slate-400" + shortcutKeys={"mod+D"} + /> + +
+
+ ); +}; diff --git a/src/app/components/FileBrowser/FileBrowser.tsx b/src/app/components/FileBrowser/FileBrowser.tsx new file mode 100644 index 0000000..8b380ac --- /dev/null +++ b/src/app/components/FileBrowser/FileBrowser.tsx @@ -0,0 +1,169 @@ +import { + faFolderOpen, + faFolder, + faUpload, + faFile, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Dispatch, RefObject, SetStateAction, useState } from "react"; +import { parseDirectories } from "@/app/utils/fileUtils"; +import { DownloadFile, Write } from "@/app/utils/serialUtils"; + +// Define FileType +export type FileType = "file" | "folder"; + +// Define file structure +export type FileStructure = { + name: string; + path: string; + type: FileType; + children?: FileStructure[]; + isOpen: boolean; +}; + +export const FileBrowser = ({ + fileInputRef, + setSelectedUploadFolder, +}: { + fileInputRef: RefObject; + setSelectedUploadFolder: Dispatch>; +}) => { + // const { serial, consoleMessage } = useSerial(); + const [dirStructure, setDirStructure] = useState(); + + const updateDirectoryStructure = ( + structure: FileStructure[], + targetFolder: FileStructure, + newChildren: FileStructure[] + ): FileStructure[] => { + return structure.map((folder) => { + if (folder.name === targetFolder.name) { + return { ...folder, children: newChildren, isOpen: !folder.isOpen }; + } + + if (folder.children) { + return { + ...folder, + children: updateDirectoryStructure( + folder.children, + targetFolder, + newChildren + ), + }; + } + + return folder; + }); + }; + + const FolderToggle = ({ + folder, + indent, + }: { + folder: FileStructure; + indent: number; + }) => { + const toggleFolder = async () => { + let fileStructures: FileStructure[] = folder.children || []; + if (!folder.isOpen) { + const childDirs = await Write( + `ls ${folder.path + folder.name}`, + false, + true + ); + + // Currently dirs with spaces in them are not valid + if (childDirs.response) { + const childItems = childDirs.response.split("\r\n").slice(1, -1); + fileStructures = parseDirectories( + childItems, + `${folder.path}${folder.name}/` + ); + } + } + setDirStructure( + (prevState) => + prevState && + updateDirectoryStructure(prevState, folder, fileStructures) + ); + }; + + return ( +
+
+
+ +

{folder.name}

+
+ + { + // e.stopPropagation(); + // e.preventDefault(); + setSelectedUploadFolder(folder.path + folder.name + "/"); + fileInputRef.current?.click(); + }} + /> +
+ + {folder.isOpen && + folder.children && + folder.children.map((file, index) => ( + + ))} +
+ ); + }; + + // File Component + const File = ({ file }: { file: FileStructure }) => ( +
{ + console.log(file.path + file.name); + DownloadFile(file.path + file.name); + }} + > + +

{file.name}

+
+ ); + + // ListItem Component + const ListItem = ({ + item, + indent, + }: { + item: FileStructure; + indent: number; + }) => { + return ( +
+ {item.type === "folder" ? ( + + ) : ( +
+ +
+ )} +
+ ); + }; + + return ( + <> + {dirStructure && + dirStructure.map((file, index) => ( + + ))} + + ); +}; diff --git a/src/app/components/FileStructure/FileStructure.tsx b/src/app/components/FileStructure/FileStructure.tsx deleted file mode 100644 index 1e9d971..0000000 --- a/src/app/components/FileStructure/FileStructure.tsx +++ /dev/null @@ -1,11 +0,0 @@ -// Define FileType -export type FileType = "file" | "folder"; - -// Define file structure -export type FileStructure = { - name: string; - path: string; - type: FileType; - children?: FileStructure[]; - isOpen: boolean; -}; diff --git a/src/app/utils/fileUtils.tsx b/src/app/utils/fileUtils.tsx new file mode 100644 index 0000000..934f8a1 --- /dev/null +++ b/src/app/utils/fileUtils.tsx @@ -0,0 +1,30 @@ +import { FileStructure } from "../components/FileBrowser/FileBrowser"; + +export const hexToBytes = (hex: string) => { + let bytes = new Uint8Array(Math.ceil(hex.length / 2)); + for (let i = 0; i < bytes.length; i++) + bytes[i] = parseInt(hex.substr(i * 2, 2), 16); + return bytes; +}; + +export const bytesToHex = (bytes: Uint8Array) => { + return bytes.map((byte: any) => byte.toString(16).padStart(2, "0")).join(""); +}; + +export const parseDirectories = ( + dirList: string[], + parentPath: string = "/" +): FileStructure[] => { + return dirList.map((path) => { + const isFolder = path.endsWith("/"); + const name = isFolder ? path.slice(0, -1) : path; + + return { + name, + path: parentPath, + type: isFolder ? "folder" : "file", + children: isFolder ? [] : undefined, + isOpen: false, + }; + }); +}; diff --git a/src/app/utils/serialUtils.tsx b/src/app/utils/serialUtils.tsx new file mode 100644 index 0000000..1e8c8f7 --- /dev/null +++ b/src/app/utils/serialUtils.tsx @@ -0,0 +1,161 @@ +import { hexToBytes } from "./fileUtils"; +import { useSerial } from "../components/SerialLoader/SerialLoader"; +import { DataPacket } from "../components/SerialProvider/SerialProvider"; + +interface DownloadedFile { + blob: Blob; + filename: string; +} + +export const Write = async ( + command: string, + updateFrame: boolean, + awaitResponse: boolean = true +) => { + const { serial, consoleMessage } = useSerial(); + + let data: DataPacket = { + id: 0, + command: "", + response: null, + }; + if (awaitResponse) data = await serial.queueWriteAndResponse(command); + else serial.queueWrite(command); + if (updateFrame) { + serial.queueWrite("screenframeshort"); + setLoadingFrame(true); + } + + return data; +}; + +export const DownloadFile = async (filePath: string) => { + const { serial, consoleMessage } = useSerial(); + + await Write("fclose", false); + let sizeResponse = await Write(`filesize ${filePath}`, false, true); + if (!sizeResponse.response) { + console.error("Error downloading (size) file"); + } + let size = parseInt(sizeResponse.response?.split("\r\n")[1] || "0"); + await Write(`fopen ${filePath}`, false); + + await Write(`fseek 0`, false); + + let rem = size; + let chunk = 62 * 15; + + let dataObject: Uint8Array = new Uint8Array(); + + while (rem > 0) { + if (rem < chunk) { + chunk = rem; + } + let lines = + (await Write(`fread ${chunk.toString()}`, false, true)).response + ?.split("\r\n") + .slice(1) + .slice(0, -2) + .join("") || ""; + + let bArr = hexToBytes(lines); + rem -= bArr.length; + dataObject = new Uint8Array([...dataObject, ...Array.from(bArr)]); + } + downloadFileFromBytes( + dataObject, + filePath.substring(filePath.lastIndexOf("/") + 1) + ); + await Write("fclose", false); +}; + +const downloadFileFromBytes = ( + bytes: Uint8Array | string, + fileName: string = "output.txt" +) => { + let blob = new Blob([bytes]); + let url = URL.createObjectURL(blob); + let a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = fileName; // Filename + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +}; + +export const downloadFileFromUrl = async ( + url: string +): Promise => { + const response = await fetch(url); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const contentDispositionHeader = response.headers.get("Content-Disposition"); + const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; + let matches = contentDispositionHeader?.match(filenameRegex); + let filename = + matches && matches[1] ? matches[1].replace(/['"]/g, "") : "unknown.fail"; + + const blob = await response.blob(); + + return { blob, filename }; +}; + +export const UploadFile = async (filePath: string, bytes: Uint8Array) => { + const { serial, consoleMessage } = useSerial(); + + await Write("fclose", false); + await Write(`fopen ${filePath}`, false); + + await Write(`fseek 0`, false); + + let blob = new Blob([bytes]); + const arrayBuffer = await blob.arrayBuffer(); + + const chunkSize = 100000; + + console.log("Total length: ", arrayBuffer.byteLength); + + let startTime = Date.now(); + let totalTime = 0; + + for (let i = 0; i < arrayBuffer.byteLength; i += chunkSize) { + const chunk = arrayBuffer.slice(i, i + chunkSize); + + await Write(`fwb ${chunk.byteLength}`, false, true); + await serial.queueWriteAndResponseBinary(new Uint8Array(chunk)); + + // calculate elapsed time and average time per chunk + let elapsed = Date.now() - startTime; + totalTime += elapsed; + let avgTimePerChunk = totalTime / (i / chunkSize + 1); + + // estimate remaining time in seconds + let remainingChunks = (arrayBuffer.byteLength - i) / chunkSize; + let estRemainingTime = (remainingChunks * avgTimePerChunk) / 1000; + + console.log( + "Chunk done", + i, + arrayBuffer.byteLength, + ((i / arrayBuffer.byteLength) * 100).toFixed(2) + "%", + "Estimated time remaining: " + estRemainingTime.toFixed(0) + " seconds" + ); + setUpdateStatus( + `${((i / arrayBuffer.byteLength) * 100).toFixed( + 2 + )}% Estimated time remaining: ${estRemainingTime.toFixed(0)} seconds` + ); + + // reset start time for next iteration + startTime = Date.now(); + } + console.log("FILE DONE"); + setUpdateStatus(`File upload complete!`); + + await Write("fclose", false); +}; From 1a6671b498093b4d5e3c045eda0637f3e0af8e6c Mon Sep 17 00:00:00 2001 From: jLynx Date: Sun, 14 Jan 2024 15:14:03 +1300 Subject: [PATCH 05/12] WIP make this code more readable --- src/app/components/Controller/Controller.tsx | 5 +++-- src/app/utils/serialUtils.tsx | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/components/Controller/Controller.tsx b/src/app/components/Controller/Controller.tsx index e6cd660..c94303f 100644 --- a/src/app/components/Controller/Controller.tsx +++ b/src/app/components/Controller/Controller.tsx @@ -149,7 +149,7 @@ const Controller = () => { if (arrayBuffer instanceof ArrayBuffer) { let bytes = new Uint8Array(arrayBuffer); console.log(path + file.name); - UploadFile(path + file.name, bytes); + UploadFile(path + file.name, bytes, setUpdateStatus); // ToDo: This should possibly be some sort of callback } }; @@ -171,7 +171,8 @@ const Controller = () => { await UploadFile( `/FIRMWARE/${fileBlob.filename}`, - new Uint8Array(await fileBlob.blob.arrayBuffer()) + new Uint8Array(await fileBlob.blob.arrayBuffer()), + setUpdateStatus ); await Write(`flash /FIRMWARE/${fileBlob.filename}`, false, true); diff --git a/src/app/utils/serialUtils.tsx b/src/app/utils/serialUtils.tsx index 1e8c8f7..955443c 100644 --- a/src/app/utils/serialUtils.tsx +++ b/src/app/utils/serialUtils.tsx @@ -1,3 +1,4 @@ +import { Dispatch, SetStateAction } from "react"; import { hexToBytes } from "./fileUtils"; import { useSerial } from "../components/SerialLoader/SerialLoader"; import { DataPacket } from "../components/SerialProvider/SerialProvider"; @@ -105,7 +106,11 @@ export const downloadFileFromUrl = async ( return { blob, filename }; }; -export const UploadFile = async (filePath: string, bytes: Uint8Array) => { +export const UploadFile = async ( + filePath: string, + bytes: Uint8Array, + setUpdateStatus: Dispatch> +) => { const { serial, consoleMessage } = useSerial(); await Write("fclose", false); From 8e971b5b0a44d2ec01de176e672fa49b0c0b62f1 Mon Sep 17 00:00:00 2001 From: jLynx Date: Sun, 14 Jan 2024 16:25:35 +1300 Subject: [PATCH 06/12] Finished refactoring --- src/app/components/Controller/Controller.tsx | 25 ++++--- .../DeviceButtons/DeviceButtons.tsx | 22 +++--- .../components/FileBrowser/FileBrowser.tsx | 13 ++-- src/app/utils/serialUtils.tsx | 67 ++++++++++--------- 4 files changed, 70 insertions(+), 57 deletions(-) diff --git a/src/app/components/Controller/Controller.tsx b/src/app/components/Controller/Controller.tsx index c94303f..be4d6a4 100644 --- a/src/app/components/Controller/Controller.tsx +++ b/src/app/components/Controller/Controller.tsx @@ -4,8 +4,8 @@ import React, { ChangeEvent, useEffect, useRef, useState } from "react"; import { parseDirectories } from "@/app/utils/fileUtils"; import { UploadFile, - Write, downloadFileFromUrl, + useWriteCommand, } from "@/app/utils/serialUtils"; import { DeviceButtons } from "../DeviceButtons/DeviceButtons"; import { FileBrowser, FileStructure } from "../FileBrowser/FileBrowser"; @@ -15,12 +15,13 @@ import ToggleSwitch from "../ToggleSwitch/ToggleSwitch"; const Controller = () => { const { serial, consoleMessage } = useSerial(); + const { write, loadingFrame, setLoadingFrame } = useWriteCommand(); + const [consoleMessageList, setConsoleMessageList] = useState(""); const [updateStatus, setUpdateStatus] = useState(""); const [selectedUploadFolder, setSelectedUploadFolder] = useState("/"); const [command, setCommand] = useState(""); const [autoUpdateFrame, setAutoUpdateFrame] = useState(true); - const [loadingFrame, setLoadingFrame] = useState(true); const [dirStructure, setDirStructure] = useState(); const canvasRef = useRef(null); const fileInputRef = useRef(null); // Create a reference @@ -28,7 +29,7 @@ const Controller = () => { const started = useRef(false); const sendCommand = async () => { - await Write(command, false); + await write(command, false); setCommand(""); }; @@ -51,15 +52,15 @@ const Controller = () => { serial.startReading(); const initSerialSetupCalls = async () => { - await Write(setDeviceTime(), false); + await write(setDeviceTime(), false); await fetchFolderStructure(); - await Write("screenframeshort", false); + await write("screenframeshort", false); }; const fetchFolderStructure = async () => { - const rootStructure = await Write(`ls /`, false, true); // get the children directories + const rootStructure = await write(`ls /`, false, true); // get the children directories if (rootStructure.response) { const rootItems = rootStructure.response.split("\r\n").slice(1, -1); @@ -132,7 +133,7 @@ const Controller = () => { e.preventDefault(); let key_code = e.key.length === 1 ? e.key.charCodeAt(0) : e.keyCode; const keyHex = key_code.toString(16).padStart(2, "0").toUpperCase(); - Write(`keyboard ${keyHex}`, autoUpdateFrame); + write(`keyboard ${keyHex}`, autoUpdateFrame); } }; @@ -175,7 +176,7 @@ const Controller = () => { setUpdateStatus ); - await Write(`flash /FIRMWARE/${fileBlob.filename}`, false, true); + await write(`flash /FIRMWARE/${fileBlob.filename}`, false, true); console.log("DONE! firmware complete. Rebooting..."); alert("Firmware update complete! Please wait for your device to reboot."); }; @@ -222,7 +223,7 @@ const Controller = () => { { - if (!autoUpdateFrame) Write("screenframeshort", false); + if (!autoUpdateFrame) write("screenframeshort", false); setAutoUpdateFrame(!autoUpdateFrame); }} /> @@ -232,7 +233,7 @@ const Controller = () => { onClickFunction={() => { if (!loadingFrame) { setLoadingFrame(true); - Write("screenframeshort", false); + write("screenframeshort", false); } }} className="h-6 w-6 bg-blue-500" @@ -255,7 +256,7 @@ const Controller = () => { const x = event.clientX - bounds.left; const y = event.clientY - bounds.top; - Write(`touch ${x} ${y}`, autoUpdateFrame); + write(`touch ${x} ${y}`, autoUpdateFrame); }} />
@@ -305,6 +306,8 @@ const Controller = () => {
diff --git a/src/app/components/DeviceButtons/DeviceButtons.tsx b/src/app/components/DeviceButtons/DeviceButtons.tsx index e51c529..c9c9e8f 100644 --- a/src/app/components/DeviceButtons/DeviceButtons.tsx +++ b/src/app/components/DeviceButtons/DeviceButtons.tsx @@ -1,4 +1,4 @@ -import { Write } from "@/app/utils/serialUtils"; +import { useWriteCommand } from "@/app/utils/serialUtils"; import HotkeyButton from "../HotkeyButton/HotkeyButton"; export const DeviceButtons = ({ @@ -8,6 +8,8 @@ export const DeviceButtons = ({ loadingFrame: boolean; autoUpdateFrame: boolean; }) => { + const { write } = useWriteCommand(); + return (
Write("button 2", autoUpdateFrame)} + onClickFunction={() => write("button 2", autoUpdateFrame)} className="h-16 w-16 bg-green-500" shortcutKeys={"ArrowLeft"} /> - ) : ( -
-
- { - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - }} - onChange={(e) => { - onFileChange(e, selectedUploadFolder); - }} - /> - {/* -