Skip to content
37 changes: 29 additions & 8 deletions gcs/electron/fla.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,15 +453,15 @@ export default async function openFile(event, filePath) {
}

// on-demand retrieval of messages
export async function retrieveMessages(_event, requestedMessages) {
export async function getMessages(_event, requestedMessages) {
// each requestedMessage should be of the form `${requestedMessageName}/${requestedFieldName}`
// like ['ARM/ArmState', 'ARSP/Airspeed']

// for large log files, we need to consider decimation.

if (!logData) {
console.error(
"retrieveMessages: logData is null or undefined. Unable to retrieve messages.",
"getMessages: logData is null or undefined. Unable to retrieve messages.",
)
return {
success: false,
Expand Down Expand Up @@ -490,14 +490,35 @@ export async function retrieveMessages(_event, requestedMessages) {
const categoryName = label.slice(0, slash)
const fieldName = label.slice(slash + 1)

// Validate existence of requestedMessage in our log
const fmt = formatMessages[categoryName]
const hasField = fmt?.fields.includes(fieldName)

const series = logData[categoryName]

if (!hasField || !Array.isArray(series) || series.length === 0) {
// Skip unknown or unavailable labels
continue
}

const len = series.length
// use typed arrays to reduce IPC serialization overhead
// Time as Float64, values as Float32 for size efficiency
Comment thread
Kwash67 marked this conversation as resolved.
const x = new Float64Array(len)
const y = new Float32Array(len)

for (let j = 0; j < len; j++) {
const point = series[j]
// making sure all the entries are numbers
x[j] = typeof point.TimeUS === "number" ? point.TimeUS : 0
y[j] = typeof point[fieldName] === "number" ? point[fieldName] : 0
}

datasets.push({
label: label,
label,
yAxisID: getUnit(categoryName, fieldName, formatMessages, units),
// I guess this is the expensive part. We're looping through every data point
data: logData[categoryName].map((d) => ({
x: d.TimeUS,
y: d[fieldName],
})),
x,
y,
})
}

Expand Down
4 changes: 2 additions & 2 deletions gcs/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import packageInfo from "../package.json"
import openFile, {
clearRecentFiles,
getRecentFiles,
retrieveMessages,
getMessages,
// @ts-expect-error - no types available
} from "./fla"
import registerAboutIPC, {
Expand Down Expand Up @@ -475,7 +475,7 @@ app.whenReady().then(() => {
ipcMain.handle("fla:clear-recent-logs", clearRecentFiles)

// Load Messages on demand
ipcMain.handle("fla:get-messages", retrieveMessages)
ipcMain.handle("fla:get-messages", getMessages)

// Open native save dialog
ipcMain.handle("app:get-save-file-path", async (event, options) => {
Expand Down
71 changes: 71 additions & 0 deletions gcs/src/components/fla/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,74 @@ export const colorInputSwatch = [
"#fab005",
"#fd7e14",
]

// Fixed list of message labels to preload for FLA
export const PRELOAD_LABELS = {
dataflash: [
// Attitude
"ATT/DesRoll",
"ATT/Roll",
"ATT/DesPitch",
"ATT/Pitch",
"ATT/DesYaw",
"ATT/Yaw",

// Speed
"GPS/Spd",
"ARSP/Airspeed",

// Vibration
"VIBE/VibeX",
"VIBE/VibeY",
"VIBE/VibeZ",

// Battery
"BAT1/Volt",
"BAT1/Curr",

// Control tuning (copter/plane)
"CTUN/DAlt",
"CTUN/Alt",
"CTUN/BAlt",
"CTUN/DCRt",
"CTUN/CRt",
"CTUN/ThI",
"CTUN/ThO",

// Control tuning (quadplane)
"QTUN/DAlt",
"QTUN/Alt",
"QTUN/BAlt",
"QTUN/DCRt",
"QTUN/CRt",
"QTUN/ThI",
"QTUN/ThO",

// RC Inputs (first four)
"RCIN/C1",
"RCIN/C2",
"RCIN/C3",
"RCIN/C4",
],

fgcs_telemetry: [
// Attitude
"ATTITUDE/roll",
"ATTITUDE/pitch",

// Speed
"VFR_HUD/groundspeed",
"VFR_HUD/airspeed",

// Vibration
"VIBRATION/vibration_x",
"VIBRATION/vibration_y",
"VIBRATION/vibration_z",

// RC Inputs (first four)
"RC_CHANNELS/chan1_raw",
"RC_CHANNELS/chan2_raw",
"RC_CHANNELS/chan3_raw",
"RC_CHANNELS/chan4_raw",
],
}
61 changes: 37 additions & 24 deletions gcs/src/fla.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

// Base imports
import { useEffect, useMemo, useState } from "react"
import { useEffect, useMemo } from "react"

// 3rd Party Imports
import { useDispatch, useSelector } from "react-redux"
Expand All @@ -14,6 +14,7 @@ import { useDispatch, useSelector } from "react-redux"
import { hexToRgba } from "./components/fla/utils"

// Custom components and helpers
import { PRELOAD_LABELS } from "./components/fla/constants.js"
import { logEventIds } from "./components/fla/logEventIds.js"

import SelectFlightLog from "./components/fla/SelectFlightLog.jsx"
Expand Down Expand Up @@ -48,9 +49,6 @@ export default function FLA() {
const customColors = useSelector(selectCustomColors)
const baseChartData = useSelector(selectBaseChartData)

// Local states
const [chartData, setLocalChartData] = useState({ datasets: [] })

/**
* Dispatch the lightweight summary info to Redux
*/
Expand All @@ -76,13 +74,17 @@ export default function FLA() {
),
)
dispatch(setBaseChartData([]))
// Fire off preload in the background without blocking
const labelsToPreload = PRELOAD_LABELS[summary.logType]
if (labelsToPreload && labelsToPreload.length > 0) {
setTimeout(() => fetchData(labelsToPreload), 0)
Comment thread
Kwash67 marked this conversation as resolved.
}
}

// Close file
function closeLogFile() {
dispatch(setFile(null))
dispatch(setLogMessages(null))
setLocalChartData({ datasets: [] })
dispatch(setMessageFilters(null))
dispatch(setCustomColors({}))
dispatch(setUtcAvailable(false))
Expand All @@ -93,6 +95,30 @@ export default function FLA() {
dispatch(setBaseChartData([]))
}

async function fetchData(labelsToFetch) {
const newDatasets = await window.ipcRenderer.invoke(
"fla:get-messages",
labelsToFetch,
)
// Unpack and Cache
if (Array.isArray(newDatasets) && newDatasets.length > 0) {
const transformed = newDatasets.map((ds) => {
if (Array.isArray(ds?.data)) return ds
const len = Math.min(ds.x.length, ds.y.length)
const points = new Array(len)
for (let i = 0; i < len; i++) {
points[i] = { x: ds.x[i], y: ds.y[i] }
}
return { label: ds.label, yAxisID: ds.yAxisID, data: points }
})
// Deduplicate by label: new datasets override old ones
const existing = baseChartData || []
const newLabels = new Set(transformed.map((ds) => ds.label))
const filteredExisting = existing.filter((ds) => !newLabels.has(ds.label))
dispatch(setBaseChartData([...filteredExisting, ...transformed]))
}
}

// Step 1: Memoize the calculation of which labels are currently requested.
// This loop only runs when `messageFilters` changes.
const requestedLabels = useMemo(() => {
Expand Down Expand Up @@ -121,19 +147,9 @@ export default function FLA() {

if (labelsToFetch.length > 0) {
console.log("Cache miss. Fetching:", labelsToFetch)
const fetchMissingData = async () => {
const newDatasets = await window.ipcRenderer.invoke(
"fla:get-messages",
labelsToFetch,
)
if (newDatasets) {
// Dispatch to add the new data to our master cache in Redux
dispatch(setBaseChartData([...(baseChartData || []), ...newDatasets]))
}
}
fetchMissingData()
fetchData(labelsToFetch)
}
}, [requestedLabels, baseChartData, dispatch])
}, [requestedLabels, baseChartData])

// Step 3: Memoize the final chart data.
// This filters the master cache and applies colors. It only re-runs if
Expand All @@ -153,18 +169,15 @@ export default function FLA() {
})
}, [baseChartData, customColors, requestedLabels])

// Step 4: Update the chart's state.
// This is now very simple and just syncs the memoized data to the local state.
useEffect(() => {
setLocalChartData({ datasets: visibleDataWithColors })
}, [visibleDataWithColors])

return (
<Layout currentPage="fla">
{messageFilters === null ? (
<SelectFlightLog getLogSummary={getLogSummary} />
) : (
<MainDisplay closeLogFile={closeLogFile} chartData={chartData} />
<MainDisplay
closeLogFile={closeLogFile}
chartData={{ datasets: visibleDataWithColors }}
/>
)}
</Layout>
)
Expand Down