Skip to content
249 changes: 249 additions & 0 deletions gcs/src/components/config/serialPorts.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Serial Ports Configuration Page
import { useEffect, useMemo } from "react"
import {
Table,
Select,
MultiSelect,
ScrollArea,
Text,
Tooltip,
} from "@mantine/core"
import { useListState } from "@mantine/hooks"

// Custom components, helpers and data
import apmParamDefsCopter from "../../../data/gen_apm_params_def_copter.json"
import apmParamDefsPlane from "../../../data/gen_apm_params_def_plane.json"
import { dec2bin } from "../../helpers/dataFormatters"

import { useSelector, useDispatch } from "react-redux"
import { selectAircraftType } from "../../redux/slices/droneInfoSlice"
import {
emitGetSerialPortsConfig,
emitSetSerialPortConfigParam,
selectSerialPortsConfig,
} from "../../redux/slices/configSlice"
import { emitSetState } from "../../redux/slices/droneConnectionSlice"
import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice"

// Bitmask Select component for OPTIONS field.
function OptionsBitmaskSelect({ value, onChange, options }) {
const [selected, selectedHandler] = useListState([])

useEffect(() => {
parseBitmask(value)
}, [value])

function parseBitmask(bitmaskToParse) {
const binaryString = dec2bin(bitmaskToParse)
const selectedArray = []

binaryString
.split("")
.reverse()
.forEach((bit, index) => {
if (bit === "1") {
selectedArray.push(`${index}`)
}
})

Comment thread
Kwash67 marked this conversation as resolved.
selectedHandler.setState(selectedArray)
}

function createBitmask(value) {
const initialValue = 0
const bitmask = value.reduce(
(accumulator, currentValue) => accumulator + 2 ** parseInt(currentValue),
initialValue,
)
selectedHandler.setState(value)
onChange(bitmask)
}

const data = useMemo(
() =>
options
? Object.keys(options).map((key) => ({
value: `${key}`,
label: `${options[key]}`,
}))
: [],
[options],
)

return (
<ScrollArea.Autosize className="max-h-24 min-w-[200px]">
<MultiSelect
value={selected}
onChange={createBitmask}
data={data}
placeholder="Select options"
/>
</ScrollArea.Autosize>
)
}

export default function SerialPorts() {
const dispatch = useDispatch()
const aircraftType = useSelector(selectAircraftType)
const serialPortsConfig = useSelector(selectSerialPortsConfig)
const connected = useSelector(selectConnectedToDrone)

// Helper to get paramDef for a given param_id
function getParamDef(param_id) {
if (aircraftType === 1) return apmParamDefsPlane[param_id]
if (aircraftType === 2) return apmParamDefsCopter[param_id]
return undefined
}
Comment thread
Kwash67 marked this conversation as resolved.

// Helper to handle param change
function handleParamChange(param_id, value) {
// Guard against null/undefined values from Mantine components
if (value === null || value === undefined) {
return
}
dispatch(
emitSetSerialPortConfigParam({
param_id,
value: parseInt(value),
Comment thread
Kwash67 marked this conversation as resolved.
}),
)
}

// Build serial port rows
const serialPortRows = useMemo(
() =>
Object.entries(serialPortsConfig).map(([key, config]) => ({
number: Number(key.split("_")[1]),
protocol: config.protocol,
baud: config.baud,
options: config.options,
})),
[serialPortsConfig],
)

useEffect(() => {
if (connected) {
dispatch(emitSetState("config.serial_ports"))
dispatch(emitGetSerialPortsConfig())
}
}, [connected, dispatch])

return (
<div className="p-4 overflow-auto">
<Table withRowBorders={false} className="!w-fit">
<Table.Thead>
<Table.Tr>
<Table.Th>Port</Table.Th>
<Table.Th>Baud Rate</Table.Th>
<Table.Th>Protocol</Table.Th>
<Table.Th>Options</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{serialPortRows.map((port) => {
const num = port.number
const protocolParam = `SERIAL${num}_PROTOCOL`
const baudParam = `SERIAL${num}_BAUD`
const optionsParam = `SERIAL${num}_OPTIONS`

const protocolDef = getParamDef(protocolParam)
const baudDef = getParamDef(baudParam)
const optionsDef = getParamDef(optionsParam)

const protocolOptions = protocolDef?.Values
? Object.entries(protocolDef.Values).map(([value, label]) => ({
value,
label: `${value}: ${label}`,
}))
: []

// Baud rate handling
const baudOptions = baudDef?.Values
? Object.entries(baudDef.Values).map(([value, label]) => ({
value,
label: `${value}: ${label}`,
}))
: []

return (
<Table.Tr key={num} className="h-12">
<Table.Td>
<Tooltip
label={protocolDef?.DisplayName || `Serial Port ${num}`}
position="top-start"
>
<Text fw={500}>SERIAL{num}</Text>
</Tooltip>
</Table.Td>
<Table.Td>
<Tooltip
label={
<ScrollArea.Autosize className="max-h-48 max-w-80">
<div>
{baudDef?.Description || "Baud rate selection"}
</div>
</ScrollArea.Autosize>
}
position="top"
multiline
>
<Select
data={baudOptions}
value={port.baud?.toString() || ""}
placeholder="Select baud"
className="min-w-[120px]"
onChange={(val) => handleParamChange(baudParam, val)}
/>
</Tooltip>
</Table.Td>
<Table.Td>
<Tooltip
label={
<ScrollArea.Autosize className="max-h-48 max-w-80">
{protocolDef?.Description || "Protocol selection"}
</ScrollArea.Autosize>
}
position="top"
multiline
>
<Select
data={protocolOptions}
value={port.protocol?.toString() || ""}
placeholder="Select protocol"
className="min-w-[180px]"
onChange={(val) => handleParamChange(protocolParam, val)}
/>
</Tooltip>
</Table.Td>
<Table.Td>
<Tooltip
label={
<ScrollArea.Autosize className="max-h-48 max-w-80">
{optionsDef?.Description || "Serial port options"}
</ScrollArea.Autosize>
}
position="top"
multiline
>
<div>
<OptionsBitmaskSelect
value={port.options || 0}
onChange={(val) => handleParamChange(optionsParam, val)}
options={optionsDef?.Bitmask}
/>
</div>
</Tooltip>
</Table.Td>
</Table.Tr>
)
})}
</Table.Tbody>
</Table>

<Text size="md" fw={500} mt="md" c="dimmed">
Note: Changes to the serial port settings will not take effect until the
board is rebooted.
</Text>
</div>
)
}
7 changes: 7 additions & 0 deletions gcs/src/config.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useDispatch, useSelector } from "react-redux"
import Ftp from "./components/config/ftp"
import { selectActiveTab, setActiveTab } from "./redux/slices/configSlice"
import { selectConnectedToDrone } from "./redux/slices/droneConnectionSlice"
import SerialPorts from "./components/config/serialPorts"
import ServoOutput from "./components/config/servoOutput"

export default function Config() {
Expand Down Expand Up @@ -55,6 +56,7 @@ export default function Config() {
<Tabs.Tab value="rc_calibration">RC Calibration</Tabs.Tab>
<Tabs.Tab value="flightmodes">Flight modes</Tabs.Tab>
<Tabs.Tab value="servo">Servo Output</Tabs.Tab>
<Tabs.Tab value="serial_ports">Serial Ports</Tabs.Tab>
<Tabs.Tab value="ftp">FTP</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="gripper">
Expand Down Expand Up @@ -82,6 +84,11 @@ export default function Config() {
<ServoOutput />
</div>
</Tabs.Panel>
<Tabs.Panel value="serial_ports">
<div className={paddingTop}>
<SerialPorts />
</div>
</Tabs.Panel>
<Tabs.Panel value="ftp">
<div className={paddingTop}>
<Ftp />
Expand Down
4 changes: 4 additions & 0 deletions gcs/src/helpers/dataFormatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export function coordToInt(val) {
return parseInt(val * 1e7)
}

export function dec2bin(dec) {
return (dec >>> 0).toString(2)
}

export const dataFormatters = {
"ATTITUDE.pitch": radToDeg,
"ATTITUDE.roll": radToDeg,
Expand Down
15 changes: 15 additions & 0 deletions gcs/src/redux/middleware/emitters.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
emitGetGripperConfig,
emitGetGripperEnabled,
emitGetRcConfig,
emitGetSerialPortsConfig,
emitGetServoConfig,
emitRefreshFlightModeData,
emitSetFlightMode,
Expand All @@ -16,6 +17,7 @@ import {
emitSetGripperDisabled,
emitSetGripperEnabled,
emitSetRcConfigParam,
emitSetSerialPortConfigParam,
emitSetServoConfigParam,
emitTestAllMotors,
emitTestMotorSequence,
Expand Down Expand Up @@ -481,6 +483,19 @@ export function handleEmitters(socket, store, action) {
})
},
},
{
emitter: emitGetSerialPortsConfig,
callback: () => socket.socket.emit("get_serial_ports_config"),
},
{
emitter: emitSetSerialPortConfigParam,
callback: () => {
socket.socket.emit("set_serial_port_config_param", {
param_id: action.payload.param_id,
value: action.payload.value,
})
},
},
{
emitter: emitListFiles,
callback: () => {
Expand Down
Loading
Loading