diff --git a/gui_dev/bun.lockb b/gui_dev/bun.lockb index 1b6429db..41c7846d 100755 Binary files a/gui_dev/bun.lockb and b/gui_dev/bun.lockb differ diff --git a/gui_dev/data_processor/Cargo.lock b/gui_dev/data_processor/Cargo.lock index 4c53c8bf..f23aa6c0 100644 --- a/gui_dev/data_processor/Cargo.lock +++ b/gui_dev/data_processor/Cargo.lock @@ -12,10 +12,12 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" name = "cbor_decoder" version = "0.1.0" dependencies = [ + "console_error_panic_hook", "serde", "serde-wasm-bindgen", "serde_cbor", "wasm-bindgen", + "web-sys", ] [[package]] @@ -24,6 +26,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "half" version = "1.8.3" @@ -213,3 +225,13 @@ name = "wasm-bindgen-shared" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] diff --git a/gui_dev/data_processor/Cargo.toml b/gui_dev/data_processor/Cargo.toml index 00ce1b70..0cf2b141 100644 --- a/gui_dev/data_processor/Cargo.toml +++ b/gui_dev/data_processor/Cargo.toml @@ -11,9 +11,11 @@ serde = { version = "1.0", features = ["derive"] } serde_cbor = "0.11" wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } serde-wasm-bindgen = "0.6.5" +web-sys = {version = "0.3.72" , features = ["console"]} +console_error_panic_hook = "0.1" [profile.release] opt-level = "z" # Optimize for size (can be adjusted to 3 for speed) -lto = true # Enable Link-Time Optimization -codegen-units = 1 # Single codegen unit improves optimizations +lto = true +codegen-units = 1 \ No newline at end of file diff --git a/gui_dev/data_processor/src/lib.rs b/gui_dev/data_processor/src/lib.rs index 67cebf80..d4ac3287 100644 --- a/gui_dev/data_processor/src/lib.rs +++ b/gui_dev/data_processor/src/lib.rs @@ -1,12 +1,81 @@ use wasm_bindgen::prelude::*; use serde_cbor::Value; use serde_wasm_bindgen::to_value; +use serde::Serialize; +use std::collections::{BTreeMap, BTreeSet}; #[wasm_bindgen] -pub fn decode_cbor(data: &[u8]) -> JsValue { - match serde_cbor::from_slice::(data) { - Ok(value) => to_value(&value).unwrap_or(JsValue::NULL), - Err(_) => JsValue::NULL, +pub fn process_cbor_data(data: &[u8]) -> JsValue { + match serde_cbor::from_slice::>(data) { + Ok(decoded_data) => { + let mut data_by_channel: BTreeMap = BTreeMap::new(); + let mut all_features_set: BTreeSet = BTreeSet::new(); + + for (key, value) in decoded_data { + let (channel_name, feature_name) = get_channel_and_feature(&key); + + if channel_name.is_empty() { + continue; + } + + if !feature_name.starts_with("fft_psd_") { + continue; + } + + let feature_number = &feature_name["fft_psd_".len()..]; + let feature_index = match feature_number.parse::() { + Ok(n) => n, + Err(_) => continue, + }; + + all_features_set.insert(feature_index); + + let channel_data = data_by_channel + .entry(channel_name.clone()) + .or_insert_with(|| ChannelData { + channel_name: channel_name.clone(), + feature_map: BTreeMap::new(), + }); + + channel_data.feature_map.insert(feature_index, value); + } + + let all_features: Vec = all_features_set.into_iter().collect(); + + let result = ProcessedData { + data_by_channel, + all_features, + }; + + to_value(&result).unwrap_or(JsValue::NULL) + } + Err(e) => { + // Optionally log the error for debugging + JsValue::NULL + }, + } +} + +fn get_channel_and_feature(key: &str) -> (String, String) { + // Adjusted to split at the "_fft_psd_" pattern + let pattern = "_fft_psd_"; + if let Some(pos) = key.find(pattern) { + let channel_name = &key[..pos]; + let feature_name = &key[pos + 1..]; // Skip the underscore + (channel_name.to_string(), feature_name.to_string()) + } else { + ("".to_string(), key.to_string()) } } +#[derive(Serialize)] +struct ChannelData { + channel_name: String, + feature_map: BTreeMap, +} + +#[derive(Serialize)] +struct ProcessedData { + data_by_channel: BTreeMap, + all_features: Vec, +} diff --git a/gui_dev/package.json b/gui_dev/package.json index 3e1c3c16..bc63014a 100644 --- a/gui_dev/package.json +++ b/gui_dev/package.json @@ -9,8 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@emotion/react": "^11.13.3", - "@emotion/styled": "^11.13.0", + "@emotion/react": "^11.13.5", + "@emotion/styled": "^11.13.5", "@mui/icons-material": "latest", "@mui/material": "latest", "cbor-js": "^0.1.0", @@ -29,11 +29,11 @@ "@babel/eslint-parser": "^7.25.9", "@babel/preset-env": "^7.26.0", "@babel/preset-react": "^7.25.9", - "@eslint/compat": "^1.2.2", + "@eslint/compat": "^1.2.3", "@vitejs/plugin-react": "^4.3.3", "@welldone-software/why-did-you-render": "^8.0.3", "babel-plugin-react-compiler": "latest", - "eslint": "^9.14.0", + "eslint": "^9.15.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-react": "^7.37.2", diff --git a/gui_dev/src/components/HeatmapGraph.jsx b/gui_dev/src/components/HeatmapGraph.jsx index 8fb7aa3d..fd651e6d 100644 --- a/gui_dev/src/components/HeatmapGraph.jsx +++ b/gui_dev/src/components/HeatmapGraph.jsx @@ -9,15 +9,13 @@ import { FormControlLabel, Radio, Checkbox, + Slider, } from '@mui/material'; import { CollapsibleBox } from './CollapsibleBox'; import { getChannelAndFeature } from './utils'; import { shallow } from 'zustand/shallow'; -const maxTimeWindow = 10; - export const HeatmapGraph = () => { - const channels = useSessionStore((state) => state.channels, shallow); const usedChannels = useMemo( @@ -30,35 +28,41 @@ export const HeatmapGraph = () => { [usedChannels] ); - const [selectedChannel, setSelectedChannel] = useState(''); // TODO: Switch this maybe multiple? + const [selectedChannel, setSelectedChannel] = useState(''); const [features, setFeatures] = useState([]); - const [fftFeatures, setFftFeatures] = useState([]); + const [fftFeatures, setFftFeatures] = useState([]); const [otherFeatures, setOtherFeatures] = useState([]); const [selectedFeatures, setSelectedFeatures] = useState([]); const [heatmapData, setHeatmapData] = useState({ x: [], z: [] }); const [isDataStale, setIsDataStale] = useState(false); const [lastDataTime, setLastDataTime] = useState(null); - const [lastDataTimestamp, setLastDataTimestamp] = useState(null); - const hasInitialized = useRef(Date.now()); const graphData = useSocketStore((state) => state.graphData); + const [maxDataPoints, setMaxDataPoints] = useState(100); + + const handleMaxDataPointsChange = (event, newValue) => { + setMaxDataPoints(newValue); + }; + const handleChannelToggle = (event) => { setSelectedChannel(event.target.value); }; - // Added handler for fft_psd_xyz features toggle TODO: also welch/ other features? const handleFftFeaturesToggle = () => { - const allFftFeaturesSelected = fftFeatures.every((feature) => selectedFeatures.includes(feature)); + const allFftFeaturesSelected = fftFeatures.every((feature) => + selectedFeatures.includes(feature) + ); if (allFftFeaturesSelected) { setSelectedFeatures((prevSelected) => prevSelected.filter((feature) => !fftFeatures.includes(feature)) ); } else { - setSelectedFeatures((prevSelected) => - [...prevSelected, ...fftFeatures.filter((feature) => !prevSelected.includes(feature))] - ); + setSelectedFeatures((prevSelected) => [ + ...prevSelected, + ...fftFeatures.filter((feature) => !prevSelected.includes(feature)), + ]); } }; @@ -86,47 +90,45 @@ export const HeatmapGraph = () => { const featureKeys = dataKeys.filter( (key) => key.startsWith(channelPrefix) && key !== 'time' ); - const newFeatures = featureKeys.map((key) => key.substring(channelPrefix.length)); + const newFeatures = featureKeys.map((key) => + key.substring(channelPrefix.length) + ); if (JSON.stringify(newFeatures) !== JSON.stringify(features)) { console.log('Updating features:', newFeatures); setFeatures(newFeatures); - // TODO: currently all psd selectable together. should we also do this for welch and other features? - const fftFeatures = newFeatures.filter((feature) => feature.startsWith('fft_psd_')); - const otherFeatures = newFeatures.filter((feature) => !feature.startsWith('fft_psd_')); + const fftFeatures = newFeatures.filter((feature) => + feature.startsWith('fft_psd_') + ); + const otherFeatures = newFeatures.filter( + (feature) => !feature.startsWith('fft_psd_') + ); setFftFeatures(fftFeatures); setOtherFeatures(otherFeatures); setSelectedFeatures(newFeatures); - setHeatmapData({ x: [], z: [] }); // Reset heatmap data when features change + setHeatmapData({ x: [], z: [] }); setIsDataStale(false); setLastDataTime(null); - setLastDataTimestamp(null); } }, [graphData, selectedChannel, features]); useEffect(() => { - if (!graphData || !selectedChannel || features.length === 0 || selectedFeatures.length === 0) return; - - // TODO: Always data in ms? (Time conversion here always necessary?) - // Timon: yes, let's define that the stream's time is always in ms - let timestamp = graphData.time; - if (timestamp === undefined) { - timestamp = (Date.now() - hasInitialized.current) / 1000; - } else { - timestamp = timestamp / 1000; - } + if ( + !graphData || + !selectedChannel || + features.length === 0 || + selectedFeatures.length === 0 + ) + return; setLastDataTime(Date.now()); setIsDataStale(false); - let x = [...heatmapData.x, timestamp]; - let z; - // Initialize 'z' for selected features if (heatmapData.z && heatmapData.z.length === selectedFeatures.length) { z = heatmapData.z.map((row) => [...row]); } else { @@ -137,27 +139,30 @@ export const HeatmapGraph = () => { const key = `${selectedChannel}_${featureName}`; const value = graphData[key]; const numericValue = typeof value === 'number' && !isNaN(value) ? value : 0; - z[idx].push(numericValue); - }); - - const currentTime = timestamp; - const minTime = currentTime - maxTimeWindow; // TODO: What should be the visible window frame? adjustable? 10s? - // Timon: Would be amazing if it's adjustable - const validIndices = x.reduce((indices, time, index) => { - if (time >= minTime) { - indices.push(index); + // Shift existing data to the left if necessary + if (z[idx].length >= maxDataPoints) { + z[idx].shift(); } - return indices; - }, []); - x = validIndices.map((index) => x[index]); - z = z.map((row) => validIndices.map((index) => row[index])); + // Append the new data + z[idx].push(numericValue); + }); + + // Update x based on the length of z[0] (assuming all rows are the same length) + const dataLength = z[0]?.length || 0; + const x = Array.from({ length: dataLength }, (_, i) => i); setHeatmapData({ x, z }); - }, [graphData, selectedChannel, features, selectedFeatures]); - - // Check if data is stale (no new data in the last second) -> TODO: Find better solution debug this + }, [ + graphData, + selectedChannel, + features, + selectedFeatures, + maxDataPoints, + ]); + + // Check if data is stale (no new data in the last second) useEffect(() => { if (!lastDataTime) return; @@ -172,27 +177,19 @@ export const HeatmapGraph = () => { return () => clearInterval(interval); }, [lastDataTime]); - // TODO: Adjustment of x-axis -> this currently is a bit buggy - const xRange = isDataStale && heatmapData.x.length > 0 - ? [heatmapData.x[0], heatmapData.x[heatmapData.x.length - 1]] - : undefined; - const layout = { - // title: { text: 'Heatmap', font: { color: '#f4f4f4' } }, - height: 600, + height: 350, paper_bgcolor: '#333', plot_bgcolor: '#333', autosize: true, xaxis: { - tickformat: '.2f', - title: { text: 'Time (s)', font: { color: '#f4f4f4' } }, + title: { text: 'Nr. of Samples', font: { color: '#f4f4f4' } }, color: '#cccccc', tickfont: { color: '#cccccc', }, automargin: false, - autorange: !isDataStale, - range: xRange, + // autorange: 'reversed' }, yaxis: { title: { text: 'Features', font: { color: '#f4f4f4' } }, @@ -215,15 +212,12 @@ export const HeatmapGraph = () => { - + {usedChannels.map((channel, index) => ( } // TODO: Should we make multiple selectable? // Timon: No, let's keep with one + control={} label={channel.name} /> ))} @@ -231,7 +225,7 @@ export const HeatmapGraph = () => { - + @@ -239,10 +233,16 @@ export const HeatmapGraph = () => { selectedFeatures.includes(feature))} + checked={fftFeatures.every((feature) => + selectedFeatures.includes(feature) + )} indeterminate={ - fftFeatures.some((feature) => selectedFeatures.includes(feature)) && - !fftFeatures.every((feature) => selectedFeatures.includes(feature)) + fftFeatures.some((feature) => + selectedFeatures.includes(feature) + ) && + !fftFeatures.every((feature) => + selectedFeatures.includes(feature) + ) } onChange={handleFftFeaturesToggle} color="primary" @@ -268,24 +268,42 @@ export const HeatmapGraph = () => { + + + Current Value: {maxDataPoints} + + + - {heatmapData.x.length > 0 && selectedFeatures.length > 0 && heatmapData.z.length > 0 && ( - - )} + {heatmapData.x.length > 0 && + selectedFeatures.length > 0 && + heatmapData.z.length > 0 && ( + + )} ); }; diff --git a/gui_dev/src/components/PSDGraph.jsx b/gui_dev/src/components/PSDGraph.jsx index f6e60161..97ab5dbf 100644 --- a/gui_dev/src/components/PSDGraph.jsx +++ b/gui_dev/src/components/PSDGraph.jsx @@ -9,11 +9,8 @@ import { Checkbox, } from "@mui/material"; import { CollapsibleBox } from "./CollapsibleBox"; -import { getChannelAndFeature } from "./utils"; import { shallow } from 'zustand/shallow'; -const defaultPsdData = { frequencies: [], powers: [] }; - const generateColors = (numColors) => { const colors = []; for (let i = 0; i < numColors; i++) { @@ -39,44 +36,21 @@ export const PSDGraph = () => { const [selectedChannels, setSelectedChannels] = useState([]); const hasInitialized = useRef(false); - const socketPsdData = useSocketStore((state) => state.graphData); + const psdProcessedData = useSocketStore((state) => state.psdProcessedData); + console.log(psdProcessedData); const psdData = useMemo(() => { - if (!socketPsdData) return []; - console.log("Socket PSD Data:", socketPsdData); - const dataByChannel = {}; - const allFeaturesSet = new Set(); - - Object.entries(socketPsdData).forEach(([key, value]) => { - const { channelName = '', featureName = '' } = getChannelAndFeature(availableChannels, key); - if (!channelName) return; - - if (!featureName.startsWith("fft_psd_")) return; - - const featureNumber = featureName.substring("fft_psd_".length); - const featureIndex = parseInt(featureNumber); - - if (isNaN(featureIndex)) return; - - allFeaturesSet.add(featureIndex); - - if (!dataByChannel[channelName]) { - dataByChannel[channelName] = { - channelName, - featureMap: {}, - }; - } - - dataByChannel[channelName].featureMap[featureIndex] = value; - }); + if (!psdProcessedData) return []; - const allFeatures = Array.from(allFeaturesSet).sort((a, b) => a - b); + const dataByChannel = psdProcessedData.data_by_channel || new Map(); + const allFeatures = psdProcessedData.all_features || []; const selectedData = selectedChannels.map((channelName) => { - const channelData = dataByChannel[channelName]; + const channelData = dataByChannel.get(channelName); if (channelData) { const values = allFeatures.map((featureIndex) => { - return channelData.featureMap[featureIndex] !== undefined ? channelData.featureMap[featureIndex] : null; + const value = channelData.feature_map.get(featureIndex); + return value !== undefined ? value : null; }); return { channelName, @@ -93,7 +67,7 @@ export const PSDGraph = () => { }); return selectedData; - }, [socketPsdData, selectedChannels, availableChannels]); + }, [psdProcessedData, selectedChannels]); const graphRef = useRef(null); const plotlyRef = useRef(null); @@ -148,8 +122,8 @@ export const PSDGraph = () => { const traces = psdData.map((data, idx) => ({ x: data.features, y: data.values, - type: "scatter", - mode: "lines+markers", + type: "scattergl", + mode: "lines", name: data.channelName, line: { simplify: false, color: colors[idx] }, })); diff --git a/gui_dev/src/components/RawDataGraph.jsx b/gui_dev/src/components/RawDataGraph.jsx index 202c3da6..23b4c336 100644 --- a/gui_dev/src/components/RawDataGraph.jsx +++ b/gui_dev/src/components/RawDataGraph.jsx @@ -51,7 +51,7 @@ export const RawDataGraph = ({ const graphRef = useRef(null); const plotlyRef = useRef(null); const [yAxisMaxValue, setYAxisMaxValue] = useState("Auto"); - const [maxDataPoints, setMaxDataPoints] = useState(400); + const [maxDataPoints, setMaxDataPoints] = useState(100); const layoutRef = useRef({ // title: { @@ -74,7 +74,7 @@ export const RawDataGraph = ({ font: { color: "#f4f4f4" }, }, color: "#cccccc", - autorange: "reversed", + // autorange: "reversed", }, yaxis: { // title: { @@ -117,6 +117,7 @@ export const RawDataGraph = ({ // Process incoming graphData to extract raw data for each channel -> TODO: Check later if this fits here better than socketStore useEffect(() => { + const startPSDGraph = performance.now(); if (!graphData || Object.keys(graphData).length === 0) return; const latestData = graphData; @@ -149,6 +150,8 @@ export const RawDataGraph = ({ return updatedRawData; }); + const endPSDGraph = performance.now(); + console.log("Time taken to process data: ", endPSDGraph - startPSDGraph); }, [graphData, availableChannels, maxDataPoints]); useEffect(() => { @@ -204,7 +207,7 @@ export const RawDataGraph = ({ return { x, y, - type: "scatter", + type: "scattergl", mode: "lines", name: channelName, line: { simplify: false, color: colors[idx] }, @@ -249,8 +252,8 @@ export const RawDataGraph = ({ {title} - - + + {/* TODO: Fix the typing errors -> How to solve this in jsx? */} @@ -270,7 +273,7 @@ export const RawDataGraph = ({ - + diff --git a/gui_dev/src/stores/socketStore.js b/gui_dev/src/stores/socketStore.js index 2fa0e2a9..44f258e1 100644 --- a/gui_dev/src/stores/socketStore.js +++ b/gui_dev/src/stores/socketStore.js @@ -1,15 +1,24 @@ import { createStore } from "./createStore"; import { getBackendURL } from "@/utils/getBackendURL"; -import CBOR from "cbor-js"; +import init, { process_cbor_data } from '../../data_processor/pkg/cbor_decoder.js'; const WEBSOCKET_URL = getBackendURL("/ws"); const RECONNECT_INTERVAL = 500; // ms +let wasmInitialized = false; + +async function initWasm() { + if (!wasmInitialized) { + await init(); + wasmInitialized = true; + } +} + export const useSocketStore = createStore("socket", (set, get) => ({ socket: null, status: "disconnected", // 'disconnected', 'connecting', 'connected' error: null, - graphData: [], + psdProcessedData: null, infoMessages: [], reconnectTimer: null, intentionalDisconnect: false, @@ -17,7 +26,6 @@ export const useSocketStore = createStore("socket", (set, get) => ({ setSocket: (socket) => set({ socket }), connectSocket: () => { - // Get current socket status and cancel if connecting or already connected const { socket, status, reconnectTimer } = get(); if (reconnectTimer) { @@ -26,10 +34,8 @@ export const useSocketStore = createStore("socket", (set, get) => ({ if (socket || status === "connecting" || status === "connected") return; - // Set socket status to connecting set({ status: "connecting", error: null, intentionalDisconnect: false }); - // Create new socket connection const newSocket = new WebSocket(WEBSOCKET_URL); newSocket.binaryType = "arraybuffer"; // Default is "blob" @@ -38,7 +44,6 @@ export const useSocketStore = createStore("socket", (set, get) => ({ set({ socket: newSocket, status: "connected", error: null }); }; - // Error event fires when the connection is closed due to error newSocket.onerror = (event) => { if (!get().intentionalDisconnect) { console.error("WebSocket error:", event); @@ -48,7 +53,6 @@ export const useSocketStore = createStore("socket", (set, get) => ({ socket: null, }); - // Attempt to reconnect after a delay get().setReconnectTimer(RECONNECT_INTERVAL); } }; @@ -63,14 +67,22 @@ export const useSocketStore = createStore("socket", (set, get) => ({ } }; - newSocket.onmessage = (event) => { + newSocket.onmessage = async (event) => { try { const arrayBuffer = event.data; - const decodedData = CBOR.decode(arrayBuffer); - // console.log("Decoded message from server:", decodedData); - set({graphData: decodedData}); + const uint8Array = new Uint8Array(arrayBuffer); + + // Ensure the WASM module is initialized + await initWasm(); + + // Process CBOR data using Rust module + const processedData = process_cbor_data(uint8Array); + + // Set processed data in store + set({ psdProcessedData: processedData }); + console.log("PSD processed data:", processedData); } catch (error) { - console.error("Failed to decode CBOR message:", error); + console.error("Failed to process CBOR message:", error); } };