diff --git a/gcs/src/components/fla/graph.jsx b/gcs/src/components/fla/graph.jsx
index 7f1895b38..33bcfc16f 100644
--- a/gcs/src/components/fla/graph.jsx
+++ b/gcs/src/components/fla/graph.jsx
@@ -252,37 +252,36 @@ export default function Graph({
const labelColor = backgroundColor.replace(/[^,]+(?=\))/, "1")
// Critical fix: Convert timestamp to Date object for time scale
- let xMinValue = flightMode.TimeUS;
- let xMaxValue = xMax;
+ let xMinValue = flightMode.TimeUS
+ let xMaxValue = xMax
// Check if we're using a time scale
- const isTimeScale = config.scales?.x?.type === "time";
-
+ const isTimeScale = config.scales?.x?.type === "time"
+
// Convert timestamps to Date objects when using time scale
if (isTimeScale) {
- xMinValue = new Date(flightMode.TimeUS);
-
+ xMinValue = new Date(flightMode.TimeUS)
+
// Handle xMax
if (nextFlightMode !== undefined) {
- xMaxValue = new Date(nextFlightMode.TimeUS);
+ xMaxValue = new Date(nextFlightMode.TimeUS)
} else {
// Stretch to the latest date
- let maxTime = 0;
+ let maxTime = 0
data.datasets.forEach((dataset) => {
dataset.data.forEach((point) => {
if (point.x > maxTime) {
- maxTime = point.x;
+ maxTime = point.x
}
- });
- });
- const maxDate = new Date(maxTime);
- xMaxValue = maxDate;
+ })
+ })
+ const maxDate = new Date(maxTime)
+ xMaxValue = maxDate
}
-
} else {
// For non-time scales, handle next flight mode
if (nextFlightMode !== undefined) {
- xMaxValue = nextFlightMode.TimeUS;
+ xMaxValue = nextFlightMode.TimeUS
}
}
diff --git a/gcs/src/components/settingsModal.jsx b/gcs/src/components/settingsModal.jsx
index 35642fd1b..2a552c9cb 100644
--- a/gcs/src/components/settingsModal.jsx
+++ b/gcs/src/components/settingsModal.jsx
@@ -114,9 +114,7 @@ function ExtendableNumberSetting({ settingName, range }) {
diff --git a/gcs/src/fla.jsx b/gcs/src/fla.jsx
index abd114169..b8d00c4f8 100644
--- a/gcs/src/fla.jsx
+++ b/gcs/src/fla.jsx
@@ -181,7 +181,7 @@ export default function FLA() {
setLoadingFile(true)
const result = await window.ipcRenderer.loadFile(file.path)
- let gpsOffset = null; // To store the offset between TimeUS and TimeUTC
+ let gpsOffset = null // To store the offset between TimeUS and TimeUTC
if (result.success) {
// Load messages into states
@@ -277,26 +277,32 @@ export default function FLA() {
let tempLoadedLogMessages = { ...loadedLogMessages }
- if ("GPS" in loadedLogMessages && result.logType === "dataflash" && gpsOffset === null) {
+ if (
+ "GPS" in loadedLogMessages &&
+ result.logType === "dataflash" &&
+ gpsOffset === null
+ ) {
const messageObj = tempLoadedLogMessages["GPS"][0] // Get the first GPS message
-
+
// Calculate the offset
if (messageObj.GWk !== undefined && messageObj.GMS !== undefined) {
- const utcTime = gpsToUTC(messageObj.GWk, messageObj.GMS);
- gpsOffset = utcTime.getTime() - messageObj.TimeUS/1000;
+ const utcTime = gpsToUTC(messageObj.GWk, messageObj.GMS)
+ gpsOffset = utcTime.getTime() - messageObj.TimeUS / 1000
}
// Loop through all messages and replace TimeUS with UTC
Object.keys(tempLoadedLogMessages).forEach((key) => {
if (key !== "format" && key !== "units") {
- tempLoadedLogMessages[key] = tempLoadedLogMessages[key].map((message) => {
- return {
- ...message,
- TimeUS: message.TimeUS/1000 + gpsOffset, // Add the new property
- };
- });
+ tempLoadedLogMessages[key] = tempLoadedLogMessages[key].map(
+ (message) => {
+ return {
+ ...message,
+ TimeUS: message.TimeUS / 1000 + gpsOffset, // Add the new property
+ }
+ },
+ )
}
- });
+ })
updateUtcAvailable(true)
updateFlightModeMessages(tempLoadedLogMessages.MODE)
}
@@ -318,7 +324,7 @@ export default function FLA() {
tempLoadedLogMessages[battName].push({
...battData,
name: battName,
- TimeUS: battData.TimeUS/1000 + gpsOffset
+ TimeUS: battData.TimeUS / 1000 + gpsOffset,
})
// Add filter state for new BATT
@@ -379,16 +385,16 @@ export default function FLA() {
function gpsToUTC(gpsWeek, gms, leapSeconds = 18) {
// GPS epoch starts at 1980-01-06 00:00:00 UTC
- const gpsEpoch = new Date(Date.UTC(1980, 0, 6));
-
+ const gpsEpoch = new Date(Date.UTC(1980, 0, 6))
+
// Calculate total milliseconds since Unix epoch
const totalMs =
gpsEpoch.getTime() +
gpsWeek * 604_800_000 + // Convert weeks to milliseconds
gms - // Add GPS milliseconds
- leapSeconds * 1_000; // Subtract leap seconds
-
- return new Date(totalMs);
+ leapSeconds * 1_000 // Subtract leap seconds
+
+ return new Date(totalMs)
}
// Get a list of the recent FGCS telemetry logs
@@ -984,9 +990,7 @@ export default function FLA() {
data={chartData}
events={logEvents}
flightModes={flightModeMessages}
- graphConfig={
- utcAvailable ? fgcsOptions: dataflashOptions
- }
+ graphConfig={utcAvailable ? fgcsOptions : dataflashOptions}
clearFilters={clearFilters}
canSavePreset={canSavePreset}
openPresetModal={open}
diff --git a/gcs/src/helpers/socket.js b/gcs/src/helpers/socket.js
index ddebf19fe..495dd203c 100644
--- a/gcs/src/helpers/socket.js
+++ b/gcs/src/helpers/socket.js
@@ -1,3 +1,4 @@
+"use client"
import { io } from "socket.io-client"
export const socket = io(import.meta.env.VITE_BACKEND_URL)
@@ -9,3 +10,24 @@ socket.on("connect", () => {
socket.on("disconnect", () => {
console.log("Disconnected from socket")
})
+
+class SocketConnection {
+ socket
+ socketEndpoint = import.meta.env.VITE_BACKEND_URL
+
+ constructor() {
+ this.socket = io(this.socketEndpoint)
+ }
+}
+
+let socketConnection = undefined
+
+class SocketFactory {
+ static create() {
+ if (!socketConnection) {
+ socketConnection = new SocketConnection()
+ }
+ return socketConnection
+ }
+}
+export default SocketFactory
diff --git a/gcs/src/redux/logAnalyserSlice.js b/gcs/src/redux/logAnalyserSlice.js
index 404fcdbba..3b0100a37 100644
--- a/gcs/src/redux/logAnalyserSlice.js
+++ b/gcs/src/redux/logAnalyserSlice.js
@@ -86,4 +86,4 @@ export const {
setCanSavePreset,
} = logAnalyserSlice.actions
-export default logAnalyserSlice.reducer
+export default logAnalyserSlice
diff --git a/gcs/src/redux/middleware/socketMiddleware.js b/gcs/src/redux/middleware/socketMiddleware.js
new file mode 100644
index 000000000..156282d2b
--- /dev/null
+++ b/gcs/src/redux/middleware/socketMiddleware.js
@@ -0,0 +1,316 @@
+// this redux middleware intercepts all actions from the
+
+// socket actions
+import {
+ initSocket,
+ socketConnected,
+ socketDisconnected,
+} from "../slices/socketSlice"
+
+// drone actions
+import {
+ emitGetCurrentMission,
+ emitGetHomePosition,
+ emitIsConnectedToDrone,
+ emitSetState,
+ setComPorts,
+ setConnected,
+ setConnecting,
+ setConnectionModal,
+ setConnectionStatus,
+ setFetchingComPorts,
+ setSelectedComPorts,
+} from "../slices/droneConnectionSlice"
+
+// socket factory
+import SocketFactory from "../../helpers/socket"
+import {
+ queueErrorNotification,
+ queueNotification,
+} from "../slices/notificationSlice"
+import { setCurrentMission, setHomePosition } from "../slices/missionSlice"
+import {
+ setDroneAircraftType,
+ setTelemetryData,
+ setAttitudeData,
+ setGpsData,
+ setNavControllerOutput,
+ setHeartbeatData,
+ setGpsRawIntData,
+ setBatteryData,
+ setOnboardControlSensorsEnabled,
+ setRSSIData,
+} from "../slices/droneInfoSlice"
+import { pushMessage } from "../slices/statusTextSlice.js"
+
+const SocketEvents = Object.freeze({
+ // socket.on events
+ Connect: "connect",
+ Disconnect: "disconnect",
+
+ // droneConnectionSlice
+ // getComPorts: "get_com_ports",
+ isConnectedToDrone: "is_connected_to_drone",
+ listComPorts: "list_com_ports",
+})
+
+const DroneSpecificSocketEvents = Object.freeze({
+ onArmDisarm: "arm_disarm",
+ onSetCurrentFlightMode: "set_current_flight_mode_result",
+ onNavResult: "nav_result",
+ onMissionControlResult: "mission_control_result",
+ onHomePositionResult: "home_position_result",
+ onIncomingMsg: "incoming_msg",
+})
+
+const socketMiddleware = (store) => {
+ let socket
+
+ const incomingMessageHandler = (msg) => {
+ switch (msg.mavpackettype) {
+ case "VFR_HUD":
+ store.dispatch(setTelemetryData(msg))
+ break
+ case "ATTITUDE":
+ store.dispatch(setAttitudeData(msg))
+ break
+ case "GLOBAL_POSITION_INT":
+ store.dispatch(setGpsData(msg))
+ break
+ case "NAV_CONTROLLER_OUTPUT":
+ store.dispatch(setNavControllerOutput(msg))
+ break
+ case "HEARTBEAT":
+ store.dispatch(setHeartbeatData(msg))
+ break
+ case "STATUSTEXT":
+ store.dispatch(pushMessage(msg))
+ break
+ case "SYS_STATUS":
+ store.dispatch(
+ setOnboardControlSensorsEnabled(msg.onboard_control_sensors_enabled),
+ )
+ break
+ case "GPS_RAW_INT":
+ store.dispatch(setGpsRawIntData(msg))
+ break
+ case "RC_CHANNELS":
+ // NOTE: UNABLE TO TEST IN SIMULATOR!
+ store.dispatch(setRSSIData(msg.rssi))
+ break
+ case "MISSION_CURRENT":
+ store.dispatch(setCurrentMission(msg))
+ break
+ case "BATTERY_STATUS":
+ store.dispatch(setBatteryData(msg))
+ break
+ }
+ }
+
+ return (next) => (action) => {
+ if (initSocket.match(action)) {
+ // client side execution
+ if (!socket && typeof window !== "undefined") {
+ socket = SocketFactory.create()
+
+ /*
+ ========================
+ = UNDERLYING CONNETION =
+ ========================
+ */
+
+ // handle socket connection events
+ // EXAMPLE SOCKET.ON EVENT
+ socket.socket.on(SocketEvents.Connect, () => {
+ // DISPATCH ALL ACTIONS HERE
+ // SINCE ITS MIDDLWARE, OTHER FUNCTIONS CAN ALSO BE CALLED
+ console.log(`Connected to socket from redux, ${socket.socket.id}`)
+ store.dispatch(socketConnected())
+ })
+
+ socket.socket.on(SocketEvents.Disconnect, () => {
+ console.log(`Disconnected from socket, ${socket.socket.id}`)
+ store.dispatch(socketDisconnected())
+ })
+
+ /*
+ ====================
+ = DRONE CONNETIONS =
+ ====================
+ */
+
+ socket.socket.on("is_connected_to_drone", (msg) => {
+ if (msg) {
+ store.dispatch(setConnected(true))
+ } else {
+ store.dispatch(setConnected(false))
+ store.dispatch(setConnecting(false))
+ // Get com ports?
+ // check if we're connected
+ // emit get_com_ports
+ }
+ })
+
+ socket.socket.on("connected", () => {
+ store.dispatch(setConnected(true))
+ })
+
+ socket.socket.on("disconnect", () => {
+ store.dispatch(setConnected(false))
+ store.dispatch(setConnecting(false))
+ })
+
+ // Fetch com ports and list them
+ socket.socket.on("list_com_ports", (msg) => {
+ store.dispatch(setFetchingComPorts(false))
+ store.dispatch(setComPorts(msg))
+ const possibleComPort = msg.find(
+ (port) =>
+ port.toLowerCase().includes("mavlink") ||
+ port.toLowerCase().includes("ardupilot"),
+ )
+ if (possibleComPort !== undefined) {
+ store.dispatch(setSelectedComPorts(possibleComPort))
+ } else if (msg.length > 0) {
+ store.dispatch(setSelectedComPorts(msg[0]))
+ }
+ })
+
+ // Flags that the drone is disconnected
+ socket.socket.on("disconnected_from_drone", () => {
+ store.dispatch(setConnected(false))
+ })
+
+ // Flags an error with the com port
+ socket.socket.on("connection_error", (msg) => {
+ console.log("Connection error: " + msg.message)
+ store.dispatch(queueErrorNotification(msg.message))
+ store.dispatch(setConnecting(false))
+ store.dispatch(setConnected(false))
+ })
+
+ // Flags that the drone is connected
+ socket.socket.on("connected_to_drone", (msg) => {
+ store.dispatch(setDroneAircraftType(msg.aircraft_type)) // There are two aircraftTypes, make sure to not use FLA one haha :D
+ if (msg.aircraft_type != 1 && msg.aircraft_type != 2) {
+ store.dispatch(
+ queueErrorNotification(
+ "Aircraft not of type quadcopter or plane",
+ ),
+ )
+ }
+ store.dispatch(setConnected(true))
+ store.dispatch(setConnecting(false))
+ store.dispatch(setConnectionModal(false))
+
+ store.dispatch(emitSetState({ state: "dashboard" })) // Potential issue with state?
+ store.dispatch(emitGetHomePosition())
+ store.dispatch(emitGetCurrentMission())
+ // socket.emit("set_state", { state: "dashboard" })
+ // socket.emit("get_home_position")
+ // socket.emit("get_current_mission")
+ })
+
+ // Setting connection status
+ socket.socket.on("drone_connect_status", (msg) => {
+ store.dispatch(setConnectionStatus(msg.message))
+ })
+ }
+ }
+
+ if (setConnected.match(action)) {
+ // Setup socket listeners on drone connection
+ if (action.payload) {
+ socket.socket.on(DroneSpecificSocketEvents.onArmDisarm, (msg) => {
+ if (!msg.success)
+ store.dispatch(
+ queueNotification({ type: "error", message: msg.message }),
+ )
+ })
+ socket.socket.on(
+ DroneSpecificSocketEvents.onSetCurrentFlightMode,
+ (msg) => {
+ store.dispatch(
+ queueNotification({
+ type: msg.success ? "success" : "error",
+ message: msg.message,
+ }),
+ )
+ },
+ )
+ socket.socket.on(DroneSpecificSocketEvents.onNavResult, (msg) => {
+ store.dispatch(
+ queueNotification({
+ type: msg.success ? "success" : "error",
+ message: msg.message,
+ }),
+ )
+ })
+ socket.socket.on(
+ DroneSpecificSocketEvents.onMissionControlResult,
+ (msg) => {
+ store.dispatch(
+ queueNotification({
+ type: msg.success ? "success" : "error",
+ message: msg.message,
+ }),
+ )
+ },
+ )
+ socket.socket.on(
+ DroneSpecificSocketEvents.onHomePositionResult,
+ (msg) => {
+ store.dispatch(
+ msg.success
+ ? setHomePosition(msg.data)
+ : queueNotification({ type: "error", message: msg.message }),
+ )
+ },
+ )
+ socket.socket.on(DroneSpecificSocketEvents.onIncomingMsg, (msg) => {
+ incomingMessageHandler(msg)
+ // if (incomingMessageHandler()[msg.mavpackettype] !== undefined) {
+ // incomingMessageHandler()[msg.mavpackettype](msg)
+ // Store packetType that has arrived
+ // const packetType = msg.mavpackettype
+
+ // Use functional form of setState to ensure the latest state is used
+ // setDisplayedData((prevDisplayedData) => {
+ // // Create a copy of displayedData to modify
+ // let updatedDisplayedData = [...prevDisplayedData]
+
+ // // Iterate over displayedData to find and update the matching item
+ // updatedDisplayedData = updatedDisplayedData.map((dataItem) => {
+ // if (dataItem.currently_selected.startsWith(packetType)) {
+ // const specificData = dataItem.currently_selected.split(".")[1]
+ // if (Object.prototype.hasOwnProperty.call(msg, specificData)) {
+ // return { ...dataItem, value: msg[specificData] }
+ // }
+ // }
+ // return dataItem
+ // })
+
+ // return updatedDisplayedData
+ // })
+ // }
+ })
+ } else {
+ Object.values(DroneSpecificSocketEvents).map((event) =>
+ socket.socket.off(event),
+ )
+ }
+ }
+
+ // these actions handle emitting based on UI events
+ // for each action type, emit socket and pass onto reducer
+ if (socket) {
+ if (emitIsConnectedToDrone.match(action)) {
+ socket.socket.emit("is_connected_to_drone")
+ }
+ }
+
+ next(action)
+ }
+}
+
+export default socketMiddleware
diff --git a/gcs/src/redux/slices/droneConnectionSlice.js b/gcs/src/redux/slices/droneConnectionSlice.js
new file mode 100644
index 000000000..f4160a42c
--- /dev/null
+++ b/gcs/src/redux/slices/droneConnectionSlice.js
@@ -0,0 +1,184 @@
+import { createSlice } from "@reduxjs/toolkit"
+import { socket } from "../../helpers/socket"
+
+export const ConnectionType = {
+ Serial: "serial",
+ Network: "network",
+}
+
+const initialState = {
+ // drone connection status
+ connecting: false,
+ connected: false,
+ connection_modal: false,
+ connection_status: null,
+
+ // aircraft type
+ aircraft_type: 0,
+
+ // drone connection parameters
+ wireless: true, // local
+ baudrate: "9600", // local
+ connection_type: ConnectionType.Serial, // local
+
+ // com ports
+ fetching_com_ports: false,
+ com_ports: [],
+ selected_com_ports: null,
+
+ // network parameters
+ network_type: "tcp", // local
+ ip: "127.0.0.1", // local
+ port: "5760", // local
+}
+
+const droneConnectionSlice = createSlice({
+ name: "droneConnection",
+ initialState,
+ reducers: {
+ // Setters
+ setConnecting: (state, action) => {
+ if (action.payload !== state.connecting) {
+ state.connecting = action.payload
+ }
+ },
+ setConnected: (state, action) => {
+ if (action.payload !== state.connected) {
+ state.connected = action.payload
+ }
+ },
+ setBaudrate: (state, action) => {
+ if (action.payload !== state.baudrate) {
+ state.baudrate = action.payload
+ }
+ },
+ setConnectionType: (state, action) => {
+ if (action.payload !== state.connection_type) {
+ state.connection_type = action.payload
+ }
+ },
+ setFetchingComPorts: (state, action) => {
+ if (action.payload !== state.fetching_com_ports) {
+ state.fetching_com_ports = action.payload
+ }
+ },
+ setComPorts: (state, action) => {
+ if (action.payload !== state.com_ports) {
+ state.com_ports = action.payload
+ }
+ },
+ setSelectedComPorts: (state, action) => {
+ if (action.payload !== state.selected_com_ports) {
+ state.selected_com_ports = action.payload
+ }
+ },
+ setNetworkType: (state, action) => {
+ if (action.payload !== state.network_type) {
+ state.network_type = action.payload
+ }
+ },
+ setIp: (state, action) => {
+ if (action.payload !== state.ip) {
+ state.ip = action.payload
+ }
+ },
+ setPort: (state, action) => {
+ if (action.payload !== state.port) {
+ state.port = action.payload
+ }
+ },
+ setConnectionModal: (state, action) => {
+ if (action.payload !== state.connection_modal) {
+ state.connection_modal = action.payload
+ }
+ },
+ setConnectionStatus: (state, action) => {
+ if (action.payload !== state.connection_status) {
+ state.connection_status = action.payload
+ }
+ },
+
+ // Emits
+ emitIsConnectedToDrone: () => {
+ socket.emit("is_connected_to_drone")
+ },
+ emitGetComPorts: () => {
+ socket.emit("get_com_ports")
+ },
+ emitDisconnectFromDrone: () => {
+ console.log("Disconnecting from drone")
+ socket.emit("disconnect_from_drone")
+ },
+ emitConnectToDrone: (_, action) => {
+ console.log("Attempting to connect to drone")
+ socket.emit("connect_to_drone", action.payload)
+ },
+ emitSetState: (_, action) => {
+ console.log(`Setting State to ${action.payload.state}`)
+ socket.emit("set_state", action.payload)
+ },
+ emitGetHomePosition: () => {
+ console.log("Getting home position")
+ socket.emit("get_home_position")
+ },
+ emitGetCurrentMission: () => {
+ console.log("Getting current mission")
+ socket.emit("get_current_mission")
+ },
+ },
+ selectors: {
+ selectConnecting: (state) => state.connecting,
+ selectConnectedToDrone: (state) => state.connected,
+ selectBaudrate: (state) => state.baudrate,
+ selectConnectionType: (state) => state.connection_type,
+ selectFetchingComPorts: (state) => state.fetching_com_ports,
+ selectComPorts: (state) => state.com_ports,
+ selectSelectedComPorts: (state) => state.selected_com_ports,
+ selectNetworkType: (state) => state.network_type,
+ selectIp: (state) => state.ip,
+ selectPort: (state) => state.port,
+ selectConnectionModal: (state) => state.connection_modal,
+ selectConnectionStatus: (state) => state.connection_status,
+ },
+})
+
+export const {
+ // Setters
+ setConnecting,
+ setConnected,
+ setBaudrate,
+ setConnectionType,
+ setFetchingComPorts,
+ setComPorts,
+ setSelectedComPorts,
+ setNetworkType,
+ setIp,
+ setPort,
+ setConnectionModal,
+ setConnectionStatus,
+
+ // Emitters
+ emitIsConnectedToDrone,
+ emitGetComPorts,
+ emitDisconnectFromDrone,
+ emitConnectToDrone,
+ emitSetState,
+ emitGetHomePosition,
+ emitGetCurrentMission,
+} = droneConnectionSlice.actions
+export const {
+ selectConnecting,
+ selectConnectedToDrone,
+ selectBaudrate,
+ selectConnectionType,
+ selectFetchingComPorts,
+ selectComPorts,
+ selectSelectedComPorts,
+ selectNetworkType,
+ selectIp,
+ selectPort,
+ selectConnectionModal,
+ selectConnectionStatus,
+} = droneConnectionSlice.selectors
+
+export default droneConnectionSlice
diff --git a/gcs/src/redux/slices/droneInfoSlice.js b/gcs/src/redux/slices/droneInfoSlice.js
new file mode 100644
index 000000000..72a55080c
--- /dev/null
+++ b/gcs/src/redux/slices/droneInfoSlice.js
@@ -0,0 +1,264 @@
+import { createSelector, createSlice } from "@reduxjs/toolkit"
+import {
+ COPTER_MODES_FLIGHT_MODE_MAP,
+ MAV_STATE,
+ PLANE_MODES_FLIGHT_MODE_MAP,
+} from "../../helpers/mavlinkConstants"
+import { defaultDataMessages } from "../../helpers/dashboardDefaultDataMessages"
+
+const droneInfoSlice = createSlice({
+ name: "droneInfo",
+ initialState: {
+ attitudeData: {
+ roll: 0.0,
+ pitch: 0.0,
+ yaw: 0.0,
+ },
+ telemetryData: {
+ airspeed: 0.0,
+ groundspeed: 0.0,
+ },
+ gpsData: {
+ lat: 0.0,
+ lon: 0.0,
+ hdg: 0.0,
+ alt: 0.0,
+ relativeAlt: 0.0,
+ },
+ navControllerData: {
+ navBearing: 0.0,
+ wpDist: 0.0,
+ },
+ heartbeatData: {
+ baseMode: 0,
+ customMode: 0,
+ systemStatus: 0,
+ },
+ onboardControlSensorsEnabled: 0,
+ gpsRawIntData: {
+ fixType: 0,
+ satellitesVisible: 0,
+ },
+ rssi: 0.0,
+ notificationSound: "",
+ aircraftType: 0, // TODO: This should be in local storage but I have no idea how :D,
+ batteryData: [],
+ extraDroneData: [
+ ...defaultDataMessages, // TODO: Should also be stored in local storage, values set to 0 on launch but actual messages stored
+ ],
+ },
+ reducers: {
+ setHeartbeatData: (state, action) => {
+ if (
+ action.payload.baseMode & 128 &&
+ !(state.heartbeatData.baseMode & 128)
+ )
+ state.notificationSound = "armed"
+ else if (
+ !(action.payload.baseMode & 128) &&
+ state.heartbeatData.baseMode & 128
+ )
+ state.notificationSound = "disarmed"
+ },
+ setBatteryData: (state, action) => {
+ const battery = state.batteryData.filter(
+ (battery) => battery.id == action.payload.id,
+ )[0]
+ if (battery) {
+ Object.assign(battery, action.payload)
+ } else {
+ state.batteryData.push(action.payload)
+ }
+ },
+ soundPlayed: (state) => {
+ state.notificationSound = ""
+ },
+ changeExtraData: (state, action) => {
+ state.extraDroneData[action.payload.index] = {
+ ...state.extraDroneData[action.payload.index],
+ ...action.payload.data,
+ }
+ },
+ setDroneAircraftType: (state, action) => {
+ if (action.payload !== state.aircraftType) {
+ state.aircraftType = action.payload
+ }
+ },
+ setTelemetryData: (state, action) => {
+ if (action.payload !== state.telemetryData) {
+ state.telemetryData = action.payload
+ }
+ },
+ setGpsData: (state, action) => {
+ if (action.payload !== state.gpsData) {
+ state.gpsData = action.payload
+ }
+ },
+ setAttitudeData: (state, action) => {
+ if (action.payload !== state.attitudeData) {
+ state.attitudeData = action.payload
+ }
+ },
+ setNavControllerOutput: (state, action) => {
+ if (action.payload !== state.navControllerData) {
+ state.navControllerData.wpDist = action.payload.wp_dist
+ state.navControllerData.navBearing = action.payload.nav_bearing
+ }
+ },
+ setGpsRawIntData: (state, action) => {
+ if (action.payload !== state.gpsRawIntData) {
+ state.gpsRawIntData = action.payload
+ }
+ },
+ setOnboardControlSensorsEnabled: (state, action) => {
+ if (action.payload !== state.onboardControlSensorsEnabled) {
+ state.onboardControlSensorsEnabled = action.payload
+ }
+ },
+ setRSSIData: (state, action) => {
+ if (action.payload !== state.rssi) {
+ state.rssi = action.payload
+ }
+ },
+ },
+ selectors: {
+ selectAttitude: (state) => state.attitudeData,
+ selectTelemetry: (state) => state.telemetryData,
+
+ selectGPS: (state) => state.gpsData,
+ selectHeading: (state) => state.gpsData.hdg / 100,
+
+ selectNavController: (state) => state.navControllerData,
+
+ selectHeartbeat: (state) => state.heartbeatData,
+ selectArmed: (state) => state.heartbeatData.baseMode & 128,
+ selectNotificationSound: (state) => state.notificationSound,
+ selectFlightMode: (state) => state.heartbeatData.customMode,
+ selectSystemStatus: (state) => MAV_STATE[state.heartbeatData.systemStatus],
+
+ selectPrearmEnabled: (state) =>
+ state.onboardControlSensorsEnabled & 268435456,
+
+ selectGPSRawInt: (state) => state.gpsRawIntData,
+ selectRSSI: (state) => state.rssi,
+ selectAircraftType: (state) => state.aircraftType,
+ selectBatteryData: (state) =>
+ state.batteryData.sort((b1, b2) => b1.id - b2.id),
+
+ selectExtraDroneData: (state) => state.extraDroneData,
+ selectStatusText: (state) => state.statusText,
+ },
+})
+
+export const {
+ setHeartbeatData,
+ soundPlayed,
+ changeExtraData,
+ setDroneAircraftType,
+ setTelemetryData,
+ setGpsData,
+ setAttitudeData,
+ setNavControllerOutput,
+ setGpsRawIntData,
+ setBatteryData,
+ setOnboardControlSensorsEnabled,
+ setRSSIData,
+} = droneInfoSlice.actions
+
+// Memoized selectors because redux is a bitch
+export const selectDroneCoords = createSelector(
+ [droneInfoSlice.selectors.selectGPS],
+ ({ lat, lon }) => {
+ return { lat: lat * 1e-7, lon: lon * 1e-7 }
+ },
+)
+
+export const selectAttitudeDeg = createSelector(
+ [droneInfoSlice.selectors.selectAttitude],
+ ({ roll, pitch, yaw }) => {
+ return {
+ roll: roll * (180 / Math.PI),
+ pitch: pitch * (180 / Math.PI),
+ yaw: yaw * (180 / Math.PI),
+ }
+ },
+)
+
+export const selectFlightModeString = createSelector(
+ [
+ droneInfoSlice.selectors.selectFlightMode,
+ droneInfoSlice.selectors.selectAircraftType,
+ ],
+ (flightMode, aircraftType) => {
+ //TODO: aircraftType should be in local storage apparently (for some reason?)
+ if (aircraftType === 1) {
+ return PLANE_MODES_FLIGHT_MODE_MAP[flightMode]
+ } else if (aircraftType === 2) {
+ return COPTER_MODES_FLIGHT_MODE_MAP[flightMode]
+ }
+ return "UNKNOWN"
+ },
+)
+
+export const selectAlt = createSelector(
+ [droneInfoSlice.selectors.selectGPS],
+ ({ alt, relativeAlt }) => {
+ return { alt: alt / 1000, relativeAlt: relativeAlt / 1000 }
+ },
+)
+
+// export function incomingMessageHandler(msg) {
+// switch (msg.mavpackettype) {
+// case "VFR_HUD":
+// console.log(msg)
+// setTelemetryData({ airspeed: msg.airspeed, groundspeed: msg.groundspeed });
+// break;
+// }
+// VFR_HUD: (msg) => setTelemetryData(msg),
+// BATTERY_STATUS: (msg) => {
+// const battery = localBatteryData.filter(
+// (battery) => battery.id == msg.id,
+// )[0]
+// if (battery) {
+// Object.assign(battery, msg)
+// } else {
+// localBatteryData.push(msg)
+// }
+// localBatteryData.sort((b1, b2) => b1.id - b2.id)
+// setBatteryData(localBatteryData)
+// },
+// ATTITUDE: (msg) => setAttitudeData(msg),
+// GLOBAL_POSITION_INT: (msg) => setGpsData(msg),
+// NAV_CONTROLLER_OUTPUT: (msg) => setNavControllerOutputData(msg),
+// HEARTBEAT: (msg) => {
+// if (msg.autopilot !== MAV_AUTOPILOT_INVALID) {
+// setHeartbeatData(msg)
+// }
+// },
+// STATUSTEXT: (msg) => statustextMessagesHandler.prepend(msg),
+// SYS_STATUS: (msg) => setSysStatusData(msg),
+// GPS_RAW_INT: (msg) => setGpsRawIntData(msg),
+// RC_CHANNELS: (msg) => setRCChannelsData(msg),
+// MISSION_CURRENT: (msg) => setCurrentMissionData(msg),
+// }
+
+export const {
+ selectAttitude,
+ selectTelemetry,
+ selectGPS,
+ selectNavController,
+ selectHeartbeat,
+ selectArmed,
+ selectPrearmEnabled,
+ selectGPSRawInt,
+ selectRSSI,
+ selectHeading,
+ selectSystemStatus,
+ selectNotificationSound,
+ selectFlightMode,
+ selectAircraftType,
+ selectBatteryData,
+ selectExtraDroneData,
+} = droneInfoSlice.selectors
+
+export default droneInfoSlice
diff --git a/gcs/src/redux/slices/missionSlice.js b/gcs/src/redux/slices/missionSlice.js
new file mode 100644
index 000000000..8ba3f95c2
--- /dev/null
+++ b/gcs/src/redux/slices/missionSlice.js
@@ -0,0 +1,66 @@
+import { createSelector, createSlice } from "@reduxjs/toolkit"
+import { FILTER_MISSION_ITEM_COMMANDS_LIST } from "../../helpers/mavlinkConstants"
+
+const missionInfoSlice = createSlice({
+ name: "missionInfo",
+ initialState: {
+ currentMission: {
+ missionState: 0,
+ total: 0,
+ seq: 0,
+ },
+ currentMissionItems: {
+ missionItems: [],
+ fenceItems: [],
+ rallyItems: [],
+ },
+ homePosition: {
+ lat: 0,
+ lon: 0,
+ alt: 0,
+ },
+ },
+ reducers: {
+ setCurrentMission: (state, action) => {
+ if (action.payload === state.currentMission) return
+ state.currentMission = action.payload
+ },
+ // TODO: on socket.on(current_mission)
+ setCurrentMissionItems: (state, action) => {
+ if (action.payload === state.currentMissionItems) return
+ state.currentMissionItems = action.payload
+ },
+ setHomePosition: (state, action) => {
+ if (action.payload === state.homePosition) return
+ state.homePosition = action.payload
+ },
+ },
+ selectors: {
+ selectCurrentMission: (state) => state.currentMission,
+ selectCurrentMissionItems: (state) => state.currentMissionItems,
+ selectHomePosition: (state) => state.homePosition,
+ },
+})
+
+// Memoization because redux doesn't like me
+export const selectFilteredMissionItems = createSelector(
+ [missionInfoSlice.selectors.selectCurrentMissionItems],
+ ({ missionItems }) => {
+ return missionItems.filter(
+ (missionItem) =>
+ !Object.values(FILTER_MISSION_ITEM_COMMANDS_LIST).includes(
+ missionItem.command,
+ ),
+ )
+ },
+)
+
+export const {
+ selectCurrentMission,
+ selectCurrentMissionItems,
+ selectHomePosition,
+} = missionInfoSlice.selectors
+export const { setCurrentMission, setCurrentMissionItems, setHomePosition } =
+ missionInfoSlice.actions
+
+export default missionInfoSlice
diff --git a/gcs/src/redux/slices/notificationSlice.js b/gcs/src/redux/slices/notificationSlice.js
new file mode 100644
index 000000000..9d6e63ba1
--- /dev/null
+++ b/gcs/src/redux/slices/notificationSlice.js
@@ -0,0 +1,35 @@
+import { createSlice } from "@reduxjs/toolkit"
+
+const notificationSlice = createSlice({
+ name: "notifications",
+ initialState: {
+ notifications: [],
+ },
+ reducers: {
+ notificationShown: (state) => {
+ state.notifications.shift()
+ },
+ queueNotification: (state, action) => {
+ state.notifications.push(action.payload)
+ },
+ queueErrorNotification: (state, action) => {
+ state.notifications.push({ type: "error", message: action.payload })
+ },
+ queueSuccessNotification: (state, action) => {
+ state.notifications.push({ type: "success", message: action.payload })
+ },
+ },
+ selectors: {
+ selectNotificationQueue: (state) => state.notifications,
+ },
+})
+
+export const {
+ notificationShown,
+ queueNotification,
+ queueErrorNotification,
+ queueSuccessNotification,
+} = notificationSlice.actions
+export const { selectNotificationQueue } = notificationSlice.selectors
+
+export default notificationSlice
diff --git a/gcs/src/redux/slices/socketSlice.js b/gcs/src/redux/slices/socketSlice.js
new file mode 100644
index 000000000..a7f351bb3
--- /dev/null
+++ b/gcs/src/redux/slices/socketSlice.js
@@ -0,0 +1,25 @@
+import { createSlice } from "@reduxjs/toolkit"
+
+const socketSlice = createSlice({
+ name: "socketConnection",
+ initialState: {
+ isConnected: false,
+ },
+ reducers: {
+ initSocket: (state) => state,
+ socketConnected: (state) => {
+ state.isConnected = true
+ },
+ socketDisconnected: (state) => {
+ state.isConnected = false
+ },
+ },
+ selectors: {
+ selectIsConnectedToSocket: (state) => state.isConnected,
+ },
+})
+
+export const { selectIsConnectedToSocket } = socketSlice.selectors
+export const { initSocket, socketConnected, socketDisconnected } =
+ socketSlice.actions
+export default socketSlice
diff --git a/gcs/src/redux/slices/statusTextSlice.js b/gcs/src/redux/slices/statusTextSlice.js
new file mode 100644
index 000000000..46935f770
--- /dev/null
+++ b/gcs/src/redux/slices/statusTextSlice.js
@@ -0,0 +1,21 @@
+import { createSlice } from "@reduxjs/toolkit"
+
+const statusTextSlice = createSlice({
+ name: "statustext",
+ initialState: { messages: [] },
+ reducers: {
+ pushMessage: (state, action) => {
+ state.messages.push(action.payload)
+ },
+ },
+ selectors: {
+ selectMessages: (state) => {
+ return state.messages
+ },
+ },
+})
+
+export const { pushMessage } = statusTextSlice.actions
+export const { selectMessages } = statusTextSlice.selectors
+
+export default statusTextSlice
diff --git a/gcs/src/redux/store.js b/gcs/src/redux/store.js
index e73518589..89dca63cf 100644
--- a/gcs/src/redux/store.js
+++ b/gcs/src/redux/store.js
@@ -1,13 +1,30 @@
-import { configureStore } from "@reduxjs/toolkit"
-import logAnalyserReducer from "./logAnalyserSlice"
+import { combineSlices, configureStore } from "@reduxjs/toolkit"
+import logAnalyserSlice from "./logAnalyserSlice"
+import socketSlice from "./slices/socketSlice"
+import droneInfoSlice from "./slices/droneInfoSlice"
+
+import socketMiddleware from "./middleware/socketMiddleware"
+import droneConnectionSlice from "./slices/droneConnectionSlice"
+import missionInfoSlice from "./slices/missionSlice"
+import statusTextSlice from "./slices/statusTextSlice"
+import notificationSlice from "./slices/notificationSlice"
+
+const rootReducer = combineSlices(
+ logAnalyserSlice,
+ socketSlice,
+ droneConnectionSlice,
+ droneInfoSlice,
+ missionInfoSlice,
+ statusTextSlice,
+ notificationSlice,
+)
export const store = configureStore({
- reducer: {
- logAnalyser: logAnalyserReducer,
- },
- middleware: (getDefaultMiddleware) =>
- getDefaultMiddleware({
+ reducer: rootReducer,
+ middleware: (getDefaultMiddleware) => {
+ return getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
- }),
+ }).concat([socketMiddleware])
+ },
})
diff --git a/gcs/src/redux/subscribers/localStorageSubscriber.js b/gcs/src/redux/subscribers/localStorageSubscriber.js
new file mode 100644
index 000000000..e8c8c43dc
--- /dev/null
+++ b/gcs/src/redux/subscribers/localStorageSubscriber.js
@@ -0,0 +1,34 @@
+// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+// !! THIS IS CURRENT BROKEN, I DON'T THINK WE NEED IT !!
+// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+// subscribe to store updates
+// and save to local storage any values that have changed
+
+// import store
+
+// store.subscribe(() => {
+// // create a mutable copy of the store
+// let store_mut = store.getState()
+
+// // droneConnection
+// // may need to serialize the stored variables
+
+// // PERFORMANCE WAY BE BAD
+// // POTENTIALLY ADD CHECK TO SEE IF THESE VARIABLES HAVE EVEN BEEN MODIFIED IN THE STORE
+// window.localStorage.setItem(
+// "wirelessConnection",
+// store_mut.droneConnection.wireless,
+// )
+// window.localStorage.setItem("baudrate", store_mut.droneConnection.baudrate)
+// window.localStorage.setItem(
+// "connectionType",
+// store_mut.droneConnection.connectionType,
+// )
+// window.localStorage.setItem(
+// "networkType",
+// store_mut.droneConnection.networkType,
+// )
+// window.localStorage.setItem("ip", store_mut.droneConnection.ip)
+// window.localStorage.setItem("port", store_mut.droneConnection.port)
+// })