Skip to content
Merged
45 changes: 23 additions & 22 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
{
"cSpell.words": [
"ardupilot",
"ARSP",
"centered",
"chancount",
"Crosshair",
"falcongrey",
"falconred",
"frametype",
"frametypename",
"maplibre",
"maptilers",
"mavutil",
"Motortestpanel",
"pymavlink",
"PYQT",
"RSSI",
"serialutil",
"SITL",
"statustext",
"SUAS"
]
"cSpell.words": [
"ardupilot",
"ARSP",
"centered",
"chancount",
"Crosshair",
"falcongrey",
"falconred",
"frametype",
"frametypename",
"maplibre",
"maptilers",
"mavutil",
"Motortestpanel",
"pymavlink",
"PYQT",
"RSSI",
"serialutil",
"SITL",
"statustext",
"SUAS"
],
"mypy-type-checker.args": ["--config-file=/mypy.ini"]
}
21 changes: 21 additions & 0 deletions gcs/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,27 @@ app.whenReady().then(() => {
return result
})

ipcMain.handle(
"app:save-file",
async (
_event,
{ filePath, content }: { filePath: string; content: number[] },
) => {
try {
// Convert number array to Buffer for fs.writeFileSync
const buffer = Buffer.from(content)
fs.writeFileSync(filePath, buffer as unknown as string)
return { success: true }
} catch (err) {
console.error("Error saving file:", err)
return {
success: false,
error: err instanceof Error ? err.message : "Unknown error",
}
}
},
)

ipcMain.handle("params:load-params-from-file", async (event) => {
const window = BrowserWindow.fromWebContents(event.sender)
if (!window) {
Expand Down
1 change: 1 addition & 0 deletions gcs/electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const ALLOWED_INVOKE_CHANNELS = [
"fla:clear-recent-logs",
"fla:get-messages",
"app:get-save-file-path",
"app:save-file",
"app:get-node-env",
"app:get-version",
"app:is-mac",
Expand Down
168 changes: 128 additions & 40 deletions gcs/src/components/config/ftp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,26 @@ import { Button, Group, LoadingOverlay, Tree } from "@mantine/core"
import { IconFile, IconFolder, IconFolderOpen } from "@tabler/icons-react"
import { useEffect, useMemo } from "react"
import { useDispatch, useSelector } from "react-redux"
import {
showErrorNotification,
showSuccessNotification,
} from "../../helpers/notification"
import {
emitListFiles,
emitReadFile,
resetFiles,
selectFiles,
selectIsReadingFile,
selectLoadingListFiles,
selectReadFileData,
} from "../../redux/slices/ftpSlice"

export default function Ftp() {
const dispatch = useDispatch()
const files = useSelector(selectFiles)
const loadingListFiles = useSelector(selectLoadingListFiles)
const isReadingFile = useSelector(selectIsReadingFile)
const readFileData = useSelector(selectReadFileData)

const convertedFiles = useMemo(() => {
if (!files || files.length === 0) return []
Expand Down Expand Up @@ -52,6 +61,24 @@ export default function Ftp() {
})
}, [files])

const fileContentString = useMemo(() => {
if (readFileData) {
try {
const decoder = new TextDecoder("utf-8")
return {
success: true,
content: decoder.decode(new Uint8Array(readFileData.file_data)),
}
} catch (e) {
return {
success: false,
content: `Error decoding file content: ${e.message}`,
}
}
}
return null
}, [readFileData])

useEffect(() => {
if (files.length === 0) {
dispatch(emitListFiles({ path: "/" }))
Expand All @@ -63,53 +90,114 @@ export default function Ftp() {
if (node.children === undefined) {
dispatch(emitListFiles({ path: node.path }))
}
} else {
dispatch(emitReadFile({ path: node.path }))
}
}

async function downloadReadFile() {
if (fileContentString && fileContentString.success) {
const options = {
title: "Save file",
defaultPath: readFileData.file_name,
filters: [{ name: "All Files", extensions: ["*"] }],
}

const result = await window.ipcRenderer.invoke(
"app:get-save-file-path",
options,
)

if (!result.canceled && result.filePath) {
const saveResult = await window.ipcRenderer.invoke("app:save-file", {
filePath: result.filePath,
content: readFileData.file_data,
})

if (saveResult.success) {
showSuccessNotification(
`File saved successfully to: ${result.filePath}`,
)
} else {
showErrorNotification("Error saving file:", saveResult.error)
}
}
}
}

return (
<div className="flex flex-col gap-4 mx-4 relative w-fit">
<LoadingOverlay
visible={loadingListFiles}
zIndex={1000}
overlayProps={{ blur: 2 }}
/>

{loadingListFiles && <p>Loading files...</p>}

<Button
onClick={() => {
dispatch(resetFiles())
}}
w={"fit-content"}
>
Refresh files
</Button>
<Tree
data={convertedFiles}
renderNode={({ node, expanded, elementProps }) => (
<Group gap={5} {...elementProps} key={node.path}>
{node.is_dir ? (
<>
{expanded ? (
<IconFolderOpen size={20} />
) : (
<IconFolder size={20} />
)}
</>
) : (
<IconFile size={20} />
)}

<span
onClick={() => {
handleFileClick(node)
<div className="flex flex-row gap-4 p-4 w-full">
<div className="flex flex-col gap-4 relative flex-1">
<LoadingOverlay
visible={loadingListFiles}
zIndex={1000}
overlayProps={{ blur: 2 }}
/>
{loadingListFiles && <p>Loading files...</p>}
<Button
onClick={() => {
dispatch(resetFiles())
}}
w={"fit-content"}
>
Refresh files
</Button>
<Tree
data={convertedFiles}
renderNode={({ node, expanded, elementProps }) => (
<Group gap={5} {...elementProps} key={node.path}>
{node.is_dir ? (
<>
{expanded ? (
<IconFolderOpen size={20} />
) : (
<IconFolder size={20} />
)}
</>
) : (
<IconFile size={20} />
)}
<span
onClick={() => {
handleFileClick(node)
}}
>
{node.label}
</span>
</Group>
)}
/>
</div>
<div className="flex flex-col gap-4 flex-1">
{fileContentString !== null && (
<div className="flex flex-col relative gap-2">
<LoadingOverlay
visible={isReadingFile}
zIndex={1000}
overlayProps={{ blur: 2 }}
/>
<Button
disabled={!fileContentString || !fileContentString.success}
onClick={async () => {
await downloadReadFile()
}}
w={"fit-content"}
>
{node.label}
</span>
</Group>
Download
</Button>

<div className="p-4 border border-falcongrey-600 rounded bg-falcongrey-800">
{fileContentString.success ? (
<pre className="whitespace-pre-wrap break-all">
{fileContentString.content}
</pre>
) : (
<p className="text-red-500">{fileContentString.content}</p>
)}
</div>
</div>
)}
/>
</div>
</div>
)
}
16 changes: 15 additions & 1 deletion gcs/src/redux/middleware/emitters.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ import {
setCurrentPage,
setIsForwarding,
} from "../slices/droneConnectionSlice"
import { emitListFiles, setLoadingListFiles } from "../slices/ftpSlice"
import {
emitListFiles,
emitReadFile,
setIsReadingFile,
setLoadingListFiles,
} from "../slices/ftpSlice"
import {
emitControlMission,
emitExportMissionToFile,
Expand Down Expand Up @@ -383,6 +388,15 @@ export function handleEmitters(socket, store, action) {
store.dispatch(setLoadingListFiles(true))
},
},
{
emitter: emitReadFile,
callback: () => {
socket.socket.emit("read_file", {
path: action.payload.path,
})
store.dispatch(setIsReadingFile(true))
},
},
]

for (const { emitter, callback } of emitHandlers) {
Expand Down
15 changes: 15 additions & 0 deletions gcs/src/redux/middleware/socketMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ import {
import {
addFiles,
resetFiles,
setIsReadingFile,
setLoadingListFiles,
setReadFileData,
} from "../slices/ftpSlice.js"
import {
addIdToItem,
Expand Down Expand Up @@ -178,6 +180,7 @@ const ConfigSpecificSocketEvents = Object.freeze({

const FtpSpecificSocketEvents = Object.freeze({
onListFilesResult: "list_files_result",
onReadFileResult: "read_file_result",
})

const socketMiddleware = (store) => {
Expand Down Expand Up @@ -409,6 +412,8 @@ const socketMiddleware = (store) => {
store.dispatch(resetMessages())
store.dispatch(resetGpsTrack())
store.dispatch(resetFiles())
store.dispatch(setIsReadingFile(false))
store.dispatch(setReadFileData(null))
})

// Link stats
Expand Down Expand Up @@ -1091,6 +1096,16 @@ const socketMiddleware = (store) => {
showErrorNotification(msg.message)
}
})

socket.socket.on(FtpSpecificSocketEvents.onReadFileResult, (msg) => {
store.dispatch(setIsReadingFile(false))
if (msg.success) {
showSuccessNotification(msg.message)
store.dispatch(setReadFileData(msg.data))
} else {
showErrorNotification(msg.message)
}
})
} else {
// Turn off socket events
Object.values(DroneSpecificSocketEvents).map((event) =>
Expand Down
Loading