diff --git a/gcs/electron/fla.js b/gcs/electron/fla.js index 3a17e0123..f1c7e5829 100644 --- a/gcs/electron/fla.js +++ b/gcs/electron/fla.js @@ -453,7 +453,7 @@ 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'] @@ -461,7 +461,7 @@ export async function retrieveMessages(_event, requestedMessages) { 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, @@ -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 + 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, }) } diff --git a/gcs/electron/main.ts b/gcs/electron/main.ts index ebb17a4ac..94566786b 100644 --- a/gcs/electron/main.ts +++ b/gcs/electron/main.ts @@ -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, { @@ -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) => { diff --git a/gcs/src/components/fla/constants.js b/gcs/src/components/fla/constants.js index 75f4220e1..8f0176961 100644 --- a/gcs/src/components/fla/constants.js +++ b/gcs/src/components/fla/constants.js @@ -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", + ], +} diff --git a/gcs/src/fla.jsx b/gcs/src/fla.jsx index e73dfec75..a36f2ddd1 100644 --- a/gcs/src/fla.jsx +++ b/gcs/src/fla.jsx @@ -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" @@ -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" @@ -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 */ @@ -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) + } } // Close file function closeLogFile() { dispatch(setFile(null)) dispatch(setLogMessages(null)) - setLocalChartData({ datasets: [] }) dispatch(setMessageFilters(null)) dispatch(setCustomColors({})) dispatch(setUtcAvailable(false)) @@ -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(() => { @@ -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 @@ -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 ( {messageFilters === null ? ( ) : ( - + )} )