Skip to content

Commit d79c70c

Browse files
authored
Load params from a file (#818)
* Add functionality to load parameters from a file * Add loading params to modified params list * Address copilot review comments * Fix bugs in params controller setParam function * Address copilot review comments * Fix pytest
1 parent d42846e commit d79c70c

11 files changed

Lines changed: 348 additions & 35 deletions

File tree

gcs/electron/main.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import registerVibeStatusIPC, {
4141
destroyVibeStatusWindow,
4242
} from "./modules/vibeStatusWindow"
4343
import registerVideoIPC, { destroyVideoWindow } from "./modules/videoWindow"
44-
44+
import { readParamsFile } from "./utils/paramsFile"
4545
// The built directory structure
4646
//
4747
// ├─┬─┬ dist
@@ -487,6 +487,42 @@ app.whenReady().then(() => {
487487
return result
488488
})
489489

490+
ipcMain.handle("params:load-params-from-file", async (event) => {
491+
const window = BrowserWindow.fromWebContents(event.sender)
492+
if (!window) {
493+
throw new Error("No active window found")
494+
}
495+
496+
const { canceled, filePaths } = await dialog.showOpenDialog({
497+
properties: ["openFile"],
498+
filters: [
499+
{ name: "Param File", extensions: ["param"] },
500+
{ name: "All Files", extensions: ["*"] },
501+
],
502+
})
503+
504+
if (!canceled && filePaths.length > 0) {
505+
const filePath = filePaths[0]
506+
try {
507+
const params = readParamsFile(filePath)
508+
return {
509+
success: true,
510+
path: filePath,
511+
name: path.basename(filePath),
512+
params: params,
513+
}
514+
} catch (err) {
515+
console.error("Error reading param file:", err)
516+
return {
517+
success: false,
518+
error: err instanceof Error ? err.message : "Unknown error",
519+
}
520+
}
521+
}
522+
523+
return null
524+
})
525+
490526
ipcMain.handle("app:get-node-env", () =>
491527
app.isPackaged ? "production" : "development",
492528
)

gcs/electron/preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const ALLOWED_INVOKE_CHANNELS = [
3232
"app:open-ekf-status-window",
3333
"app:update-vibe-status",
3434
"app:open-vibe-status-window",
35+
"params:load-params-from-file",
3536
]
3637

3738
const ALLOWED_SEND_CHANNELS = [

gcs/electron/utils/paramsFile.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { readFileSync } from "fs"
2+
3+
export const readParamsFile = (filePath: string): Record<string, number> => {
4+
try {
5+
const fileContents = readFileSync(filePath, "utf-8")
6+
const lines = fileContents.split("\n")
7+
const params: Record<string, number> = {}
8+
9+
// Params are stored as key,value pairs on each line
10+
lines.forEach((line) => {
11+
const trimmedLine = line.trim()
12+
if (trimmedLine && !trimmedLine.startsWith("#")) {
13+
const [key, value] = trimmedLine.split(",")
14+
if (key && value) {
15+
params[key.trim()] = parseFloat(value.trim())
16+
}
17+
}
18+
})
19+
20+
return params
21+
} catch (error) {
22+
console.error("Error reading params file:", error)
23+
throw error
24+
}
25+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
Modal that pops up when loading params from a file to show the user the param diff
3+
*/
4+
5+
// 3rd party imports
6+
import { Button, Modal, Table, TextInput } from "@mantine/core"
7+
8+
// Redux
9+
import { useState } from "react"
10+
import { useDispatch, useSelector } from "react-redux"
11+
import { showSuccessNotification } from "../../helpers/notification"
12+
import {
13+
appendModifiedParams,
14+
selectLoadedFileName,
15+
selectLoadedParams,
16+
selectLoadParamsFileModalOpen,
17+
setLoadParamsFileModalOpen,
18+
} from "../../redux/slices/paramsSlice"
19+
20+
export default function LoadParamsFileModal() {
21+
const dispatch = useDispatch()
22+
const opened = useSelector(selectLoadParamsFileModalOpen)
23+
const loadedFileName = useSelector(selectLoadedFileName)
24+
const loadedParams = useSelector(selectLoadedParams)
25+
26+
const [paramSearchValue, setParamSearchValue] = useState("")
27+
28+
function acceptLoadedParams() {
29+
dispatch(
30+
appendModifiedParams(
31+
loadedParams.map((param) => ({
32+
param_id: param.id,
33+
param_value: param.newValue,
34+
param_type: param.type,
35+
initial_value: param.oldValue,
36+
})),
37+
),
38+
)
39+
dispatch(setLoadParamsFileModalOpen(false))
40+
showSuccessNotification(
41+
`Successfully loaded ${loadedParams.length} parameters from ${loadedFileName}`,
42+
)
43+
}
44+
45+
return (
46+
<Modal
47+
opened={opened}
48+
onClose={() => dispatch(setLoadParamsFileModalOpen(false))}
49+
title={`Load params from ${loadedFileName}`}
50+
closeOnClickOutside={false}
51+
closeOnEscape={false}
52+
centered
53+
overlayProps={{
54+
backgroundOpacity: 0.55,
55+
blur: 3,
56+
}}
57+
size="auto"
58+
>
59+
<div className="flex flex-col items-center justify-center gap-4 w-full">
60+
<TextInput
61+
placeholder="Search parameters"
62+
value={paramSearchValue}
63+
onChange={(event) => setParamSearchValue(event.currentTarget.value)}
64+
className="w-full"
65+
/>
66+
<Table.ScrollContainer maxHeight={600} className="w-full">
67+
<Table striped highlightOnHover withColumnBorders>
68+
<Table.Thead>
69+
<Table.Tr>
70+
<Table.Th className="w-56">Parameter</Table.Th>
71+
<Table.Th className="w-40">Current value</Table.Th>
72+
<Table.Th className="w-40">New value</Table.Th>
73+
</Table.Tr>
74+
</Table.Thead>
75+
<Table.Tbody>
76+
{loadedParams
77+
.filter((param) =>
78+
param.id
79+
.toLowerCase()
80+
.includes(paramSearchValue.toLowerCase()),
81+
)
82+
.sort((a, b) => a.id.localeCompare(b.id))
83+
.map((param) => (
84+
<Table.Tr key={param.id}>
85+
<Table.Td>{param.id}</Table.Td>
86+
<Table.Td>{param.oldValue}</Table.Td>
87+
<Table.Td>{param.newValue}</Table.Td>
88+
</Table.Tr>
89+
))}
90+
</Table.Tbody>
91+
</Table>
92+
</Table.ScrollContainer>
93+
<Button onClick={acceptLoadedParams} color="green">
94+
Load params
95+
</Button>
96+
</div>
97+
</Modal>
98+
)
99+
}

gcs/src/components/params/paramsToolbar.jsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
IconPower,
1515
IconRefresh,
1616
IconTool,
17+
IconUpload,
1718
} from "@tabler/icons-react"
1819

1920
// Styling imports
@@ -41,7 +42,7 @@ import {
4142
toggleShowModifiedParams,
4243
} from "../../redux/slices/paramsSlice.js"
4344

44-
export default function ParamsToolbar() {
45+
export default function ParamsToolbar({ loadParamsFromFile }) {
4546
const dispatch = useDispatch()
4647
const modifiedParams = useSelector(selectModifiedParams)
4748
const showModifiedParams = useSelector(selectShowModifiedParams)
@@ -144,6 +145,15 @@ export default function ParamsToolbar() {
144145
>
145146
Save params to file
146147
</Button>
148+
149+
<Button
150+
size="sm"
151+
rightSection={<IconUpload size={14} />}
152+
onClick={loadParamsFromFile}
153+
color={tailwindColors.blue[600]}
154+
>
155+
Load params from file
156+
</Button>
147157
</div>
148158
)
149159
}

gcs/src/components/params/rowItem.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ const RowItem = memo(({ index, style }) => {
8989
<ValueInput index={index} paramDef={paramDef} className="grow" />
9090
{hasBeenModified && (
9191
<Tooltip
92-
label={`Reset to previous value of ${paramPreviousValue.param_value}`}
92+
label={`Reset to previous value of ${paramPreviousValue?.param_value}`}
9393
>
9494
<ActionIcon
9595
size="lg"

gcs/src/helpers/mavlinkConstants.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,3 +519,20 @@ export const EKF_STATUS_WARNING_LEVEL = 0.5
519519
export const EKF_STATUS_DANGER_LEVEL = 0.8
520520
export const VIBE_STATUS_WARNING_LEVEL = 30
521521
export const VIBE_STATUS_DANGER_LEVEL = 60
522+
523+
export const EXCLUDE_PARAMS_LOAD = [
524+
"ARSPD_OFFSET",
525+
"CMD_INDEX",
526+
"CMD_TOTAL",
527+
"FENCE_TOTAL",
528+
"FORMAT_VERSION",
529+
"GND_ABS_PRESS",
530+
"GND_TEMP",
531+
"LOG_LASTFILE",
532+
"MIS_TOTAL",
533+
"SYSID_SW_MREV",
534+
"SYS_NUM_RESETS",
535+
"STAT_BOOTCNT",
536+
"STAT_RESET",
537+
"STAT_RUNTIME",
538+
]

gcs/src/params.jsx

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import { Row } from "./components/params/row.jsx"
2222

2323
// Redux
2424
import { useDispatch, useSelector } from "react-redux"
25+
import LoadParamsFileModal from "./components/params/loadParamsFileModal.jsx"
26+
import { EXCLUDE_PARAMS_LOAD } from "./helpers/mavlinkConstants.js"
27+
import { showErrorNotification } from "./helpers/notification.js"
2528
import { selectConnectedToDrone } from "./redux/slices/droneConnectionSlice.js"
2629
import {
2730
emitRefreshParams,
@@ -36,9 +39,22 @@ import {
3639
selectShownParams,
3740
setFetchingVars,
3841
setHasFetchedOnce,
42+
setLoadedFileName,
43+
setLoadedParams,
44+
setLoadParamsFileModalOpen,
3945
setShownParams,
4046
} from "./redux/slices/paramsSlice.js"
4147

48+
function cleanFloat(value, decimals = 5) {
49+
if (typeof value === "number") {
50+
return Number(value.toFixed(decimals))
51+
}
52+
if (!isNaN(value)) {
53+
return Number(parseFloat(value).toFixed(decimals))
54+
}
55+
return value
56+
}
57+
4258
export default function Params() {
4359
const dispatch = useDispatch()
4460
const connected = useSelector(selectConnectedToDrone)
@@ -58,12 +74,6 @@ export default function Params() {
5874
const fetchingVars = useSelector(selectFetchingVars)
5975
const fetchingVarsProgress = useSelector(selectFetchingVarsProgress)
6076

61-
function fetchParams() {
62-
dispatch(setFetchingVars(true))
63-
dispatch(emitRefreshParams())
64-
dispatch(setHasFetchedOnce(true))
65-
}
66-
6777
// Reset state if we loose connection
6878
useEffect(() => {
6979
if (!connected) {
@@ -89,9 +99,68 @@ export default function Params() {
8999
dispatch(setShownParams(filteredParams))
90100
}, [debouncedSearchValue, showModifiedParams, params, modifiedParams])
91101

102+
function fetchParams() {
103+
dispatch(setFetchingVars(true))
104+
dispatch(emitRefreshParams())
105+
dispatch(setHasFetchedOnce(true))
106+
}
107+
108+
async function loadParamsFromFile() {
109+
const result = await window.ipcRenderer.invoke(
110+
"params:load-params-from-file",
111+
)
112+
if (!result) {
113+
return
114+
}
115+
116+
if (result.success) {
117+
dispatch(setLoadedFileName(result.name))
118+
119+
// Only keep params that are different to the current ones
120+
const loadedParamsList = []
121+
for (const [key, value] of Object.entries(result.params)) {
122+
if (EXCLUDE_PARAMS_LOAD.includes(key)) {
123+
continue
124+
}
125+
126+
const existingParam = params.find((param) => param.param_id === key)
127+
const cleanedNewValue = cleanFloat(value)
128+
129+
if (existingParam) {
130+
const cleanedOldValue = cleanFloat(existingParam.param_value)
131+
132+
if (cleanedOldValue !== cleanedNewValue) {
133+
loadedParamsList.push({
134+
id: key,
135+
oldValue: cleanedOldValue,
136+
newValue: cleanedNewValue,
137+
type: existingParam.param_type,
138+
})
139+
}
140+
} else {
141+
loadedParamsList.push({
142+
id: key,
143+
oldValue: null,
144+
newValue: cleanedNewValue,
145+
type: null,
146+
})
147+
}
148+
}
149+
dispatch(setLoadedParams(loadedParamsList))
150+
dispatch(setLoadParamsFileModalOpen(true))
151+
} else {
152+
showErrorNotification(
153+
`Error loading params from file: ${
154+
result.error || "Please try again."
155+
}`,
156+
)
157+
}
158+
}
159+
92160
return (
93161
<Layout currentPage="params">
94162
<AutopilotRebootModal />
163+
<LoadParamsFileModal />
95164

96165
{connected ? (
97166
<>
@@ -112,7 +181,7 @@ export default function Params() {
112181

113182
{Object.keys(params).length > 0 && !fetchingVars && (
114183
<div className="w-full h-full contents">
115-
<ParamsToolbar />
184+
<ParamsToolbar loadParamsFromFile={loadParamsFromFile} />
116185

117186
<div className="h-full w-2/3 mx-auto">
118187
<AutoSizer>

0 commit comments

Comments
 (0)