From 3606c32f6267a0b6bb40ea9f3653005ac26a8a08 Mon Sep 17 00:00:00 2001 From: ritika8081 Date: Thu, 3 Oct 2024 18:34:35 +0530 Subject: [PATCH] added canvas, updated indexed db logic and connect/disconnect logic --- src/components/Canvas.tsx | 439 ++++++++++++++++----------------- src/components/Connection.tsx | 447 +++++++++++++++------------------- 2 files changed, 413 insertions(+), 473 deletions(-) diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 939d5a6..0f8be81 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -4,11 +4,10 @@ import React, { useState, useCallback, useMemo, - useImperativeHandle, + useImperativeHandle, forwardRef, } from "react"; -import { SmoothieChart, TimeSeries } from "smoothie"; -import { useTheme } from "next-themes"; + import { BitSelection } from "./DataPass"; import { WebglPlot, ColorRGBA, WebglLine } from "webgl-plot"; @@ -18,257 +17,245 @@ interface CanvasProps { isDisplay: boolean; canvasCount?: number; Zoom: number; - } interface Batch { time: number; values: number[]; } -const Canvas= forwardRef( ({ - pauseRef, - selectedBits, - isDisplay, - canvasCount = 6, // default value in case not provided - Zoom, +const Canvas = forwardRef( + ( + { + pauseRef, + selectedBits, + isDisplay, + canvasCount = 6, // default value in case not provided + Zoom, }:CanvasProps, ref) => { - let previousCounter: number | null = null; // Variable to store the previous counter value for loss detection - const canvasContainerRef = useRef(null); - const [numChannels, setNumChannels] = useState(canvasCount); - const [canvases, setCanvases] = useState([]); - const [wglPlots, setWglPlots] = useState([]); - const [lines, setLines] = useState([]); - const linesRef = useRef([]); - const fps = 60; - const samplingRate = 500; // Set the sampling rate in Hz - const slidePoints = Math.floor(samplingRate / fps); // Set how many points to slide - let numX: number; - - const getpoints = useCallback((bits: BitSelection): number => { - switch (bits) { - case "ten": + let previousCounter: number | null = null; // Variable to store the previous counter value for loss detection + const canvasContainerRef = useRef(null); + const [numChannels, setNumChannels] = useState(canvasCount); + const [canvases, setCanvases] = useState([]); + const [wglPlots, setWglPlots] = useState([]); + const [lines, setLines] = useState([]); + const linesRef = useRef([]); + const fps = 60; + const samplingRate = 500; // Set the sampling rate in Hz + const slidePoints = Math.floor(samplingRate / fps); // Set how many points to slide + let numX: number; + + const getpoints = useCallback((bits: BitSelection): number => { + switch (bits) { + case "ten": return samplingRate*2; - case "fourteen": + case "fourteen": return samplingRate*4; - default: - return 0; // Or any other fallback value you'd like - } - }, []); + default: + return 0; // Or any other fallback value you'd like + } + }, []); numX=getpoints(selectedBits); - useEffect(() => { - setNumChannels(canvasCount); - }, [canvasCount]); - - useImperativeHandle(ref, () => ({ - updateData(data: number[]) { - updatePlots(data,Zoom); - if (previousCounter !== null) { - // If there was a previous counter value - const expectedCounter: number = (previousCounter + 1) % 256; // Calculate the expected counter value - if (data[6] !== expectedCounter) { - // Check for data loss by comparing the current counter with the expected counter - console.warn( - `Data loss detected in canvas! Previous counter: ${previousCounter}, Current counter: ${data[6]}` - ); + useEffect(() => { + setNumChannels(canvasCount); + }, [canvasCount]); + + useImperativeHandle( + ref, + () => ({ + updateData(data: number[]) { + updatePlots(data, Zoom); + if (previousCounter !== null) { + // If there was a previous counter value + const expectedCounter: number = (previousCounter + 1) % 256; // Calculate the expected counter value + if (data[6] !== expectedCounter) { + // Check for data loss by comparing the current counter with the expected counter + console.warn( + `Data loss detected in canvas! Previous counter: ${previousCounter}, Current counter: ${data[6]}` + ); + } + } + previousCounter = data[6]; // Update the previous counter with the current counter + }, + }), + [Zoom] + ); + + const createCanvases = () => { + if (!canvasContainerRef.current) return; + + // Clean up all existing canvases and their WebGL contexts + while (canvasContainerRef.current.firstChild) { + const firstChild = canvasContainerRef.current.firstChild; + + // Ensure it's an HTMLCanvasElement before trying to get the context + if (firstChild instanceof HTMLCanvasElement) { + const gl = firstChild.getContext("webgl"); + + // Lose the WebGL context if available + if (gl) { + const loseContext = gl.getExtension("WEBGL_lose_context"); + if (loseContext) { + loseContext.loseContext(); + } + } + + // Remove the canvas element from the container + canvasContainerRef.current.removeChild(firstChild); + } else { + // Remove the badge or any other non-canvas element + canvasContainerRef.current.removeChild(firstChild); } } - previousCounter =data[6]; // Update the previous counter with the current counter - } - }),[Zoom]); - - -const createCanvases = () => { - if (!canvasContainerRef.current) return; + // Clear the arrays holding canvases, WebGL plots, and lines + setCanvases([]); + setWglPlots([]); + linesRef.current = []; - // Clean up all existing canvases and their WebGL contexts - while (canvasContainerRef.current.firstChild) { - const canvas = canvasContainerRef.current.firstChild as HTMLCanvasElement; - const gl = canvas.getContext("webgl"); + const fixedCanvasWidth = canvasContainerRef.current.clientWidth; + const containerHeight = + canvasContainerRef.current.clientHeight || window.innerHeight - 50; + const canvasHeight = containerHeight / numChannels; - // Lose the WebGL context if available - if (gl) { - const loseContext = gl.getExtension("WEBGL_lose_context"); - if (loseContext) { - loseContext.loseContext(); - } - } - - // Remove the canvas element from the container - canvasContainerRef.current.removeChild(canvas); - } + const newCanvases: HTMLCanvasElement[] = []; + const newWglPlots: WebglPlot[] = []; + const newLines: WebglLine[] = []; - // Clear the arrays holding canvases, WebGL plots, and lines - setCanvases([]); - setWglPlots([]); - linesRef.current = []; + for (let i = 0; i < numChannels; i++) { + const canvas = document.createElement("canvas"); - const fixedCanvasWidth = canvasContainerRef.current.clientWidth; - const containerHeight = canvasContainerRef.current.clientHeight || window.innerHeight - 50; - const canvasHeight = containerHeight / numChannels; + canvas.width = fixedCanvasWidth; + canvas.height = canvasHeight; - const newCanvases: HTMLCanvasElement[] = []; - const newWglPlots: WebglPlot[] = []; - const newLines: WebglLine[] = []; + canvas.className = "border border-secondary-foreground w-full"; + canvas.style.height = `${canvasHeight}px`; + canvas.style.border = "0.5px solid #ccc"; - for (let i = 0; i < numChannels; i++) { - const canvas = document.createElement("canvas"); + // Create a badge for the channel number + const badge = document.createElement("div"); + badge.className = + "absolute top-1 left-1 transform -translate-y-1/20 translate-x-1/6 text-gray-500 text-sm rounded-full"; + badge.innerText = `CH${i + 1}`; // Display channel number starting from 1 - canvas.width = fixedCanvasWidth; - canvas.height = canvasHeight; + // Append the canvas and badge to the container + const canvasWrapper = document.createElement("div"); + canvasWrapper.className = "relative"; // Ensure the badge is positioned relative to the canvas + canvasWrapper.appendChild(canvas); + canvasWrapper.appendChild(badge); - canvas.className = "border border-secondary-foreground w-full"; - canvas.style.height = `${canvasHeight}px`; - canvas.style.border = "0.5px solid #ccc"; + canvasContainerRef.current.appendChild(canvasWrapper); // Append the wrapper to the container - canvasContainerRef.current.appendChild(canvas); - newCanvases.push(canvas); + newCanvases.push(canvas); - const wglp = new WebglPlot(canvas); - newWglPlots.push(wglp); + const wglp = new WebglPlot(canvas); + newWglPlots.push(wglp); - const line = new WebglLine(getRandomColor(i), numX); - line.lineSpaceX(-1, 2 / numX); - wglp.addLine(line); - newLines.push(line); - } - - linesRef.current = newLines; - setCanvases(newCanvases); - setWglPlots(newWglPlots); - setLines(newLines); -}; - - - - const getRandomColor = (i: number): ColorRGBA => { - // Define bright colors - const colors: ColorRGBA[] = [ - new ColorRGBA(1, 0.286, 0.529, 1), // Bright Pink - new ColorRGBA(0.475, 0.894, 0.952, 1), // Light Blue - new ColorRGBA(0, 1, 0.753, 1), // Bright Cyan - new ColorRGBA(0.431, 0.761, 0.031, 1), // Bright Green - new ColorRGBA(0.678, 0.286, 0.882, 1), // Bright Purple - new ColorRGBA(0.914, 0.361, 0.051, 1) // Bright Orange - ]; - - // Return color based on the index, cycling through if necessary - return colors[i % colors.length]; // Ensure to always return a valid ColorRGBA - }; - - // const updatePlots = useCallback((processedData: number[][]) => { - // wglPlots.forEach((wglp, index) => { - // if (wglp) { - // try { - // wglp.gScaleY = Zoom; // Adjust this value as needed - // } catch (error) { - // console.error(`Error setting gScaleY for WebglPlot instance at index ${index}:`, error); - // } - // } else { - // console.warn(`WebglPlot instance at index ${index} is undefined.`); - // } - // }); - - // linesRef.current.forEach((line, channelIndex) => { - // // Shift existing data points to make room for the 10 new points - // const bitsPoints = Math.pow(2, getValue(selectedBits)); // Adjust this according to your ADC resolution - // const yScale = 2 / bitsPoints; - - // // The 10 points for this particular channel - // const newPoints = processedData[channelIndex].map(value => (value - bitsPoints / 2) * yScale); - - // // Shift data points to the left by 10 positions - // // for (let j = newPoints.length; j < line.numPoints; j++) { - // // line.setY(j - newPoints.length, line.getY(j)); - // // } - // let yArray = new Float32Array(newPoints); - - // // for (let j = 0; j < newPoints.length; j++) { - // // line.setY(line.numPoints - newPoints.length + j, newPoints[j]); - // // } - // line.shiftAdd(yArray); - - // }); - // }, [linesRef, wglPlots, selectedBits,Zoom]); // Ensure dependencies are correctly referenced - - - const updatePlots = useCallback((data: number[],Zoom:number) => { - wglPlots.forEach((wglp, index) => { - if (wglp) { - try { - wglp.gScaleY = Zoom; // Adjust this value as needed - } catch (error) { - console.error(`Error setting gScaleY for WebglPlot instance at index ${index}:`, error); - } - } else { - console.warn(`WebglPlot instance at index ${index} is undefined.`); + const line = new WebglLine(getRandomColor(i), numX); + line.lineSpaceX(-1, 2 / numX); + wglp.addLine(line); + newLines.push(line); } - }); - linesRef.current.forEach((line, i) => { - // Shift the data points efficiently using a single operation - const bitsPoints = Math.pow(2, getValue(selectedBits)); // Adjust this according to your ADC resolution - const yScale = 2 / bitsPoints; - const chData = (data[i] - bitsPoints / 2) * yScale; - - for (let j = 1; j < line.numPoints; j++) { - line.setY(j - 1, line.getY(j)); - } - line.setY(line.numPoints - 1, chData); - }); - - }, [lines,wglPlots]); // Add dependencies here - - - - useEffect(() => { - createCanvases(); - }, [numChannels]); - - - - - const getValue = useCallback((bits: BitSelection): number => { - switch (bits) { - case "ten": - return 10; - case "twelve": - return 12; - case "fourteen": - return 14; - default: - return 0; // Or any other fallback value you'd like - } - }, []); - - - - - const animate = useCallback(() => { - if (pauseRef.current) { - wglPlots.forEach((wglp) => wglp.update()); - requestAnimationFrame(animate); - } - }, [wglPlots, pauseRef]); + linesRef.current = newLines; + setCanvases(newCanvases); + setWglPlots(newWglPlots); + setLines(newLines); + }; + + const getRandomColor = (i: number): ColorRGBA => { + // Define bright colors + const colors: ColorRGBA[] = [ + new ColorRGBA(1, 0.286, 0.529, 1), // Bright Pink + new ColorRGBA(0.475, 0.894, 0.952, 1), // Light Blue + new ColorRGBA(0, 1, 0.753, 1), // Bright Cyan + new ColorRGBA(0.431, 0.761, 0.031, 1), // Bright Green + new ColorRGBA(0.678, 0.286, 0.882, 1), // Bright Purple + new ColorRGBA(0.914, 0.361, 0.051, 1), // Bright Orange + ]; + + // Return color based on the index, cycling through if necessary + return colors[i % colors.length]; // Ensure to always return a valid ColorRGBA + }; + + const updatePlots = useCallback( + (data: number[], Zoom: number) => { + wglPlots.forEach((wglp, index) => { + if (wglp) { + try { + wglp.gScaleY = Zoom; // Adjust this value as needed + } catch (error) { + console.error( + `Error setting gScaleY for WebglPlot instance at index ${index}:`, + error + ); + } + } else { + console.warn(`WebglPlot instance at index ${index} is undefined.`); + } + }); + linesRef.current.forEach((line, i) => { + // Shift the data points efficiently using a single operation + const bitsPoints = Math.pow(2, getValue(selectedBits)); // Adjust this according to your ADC resolution + const yScale = 2 / bitsPoints; + const chData = (data[i] - bitsPoints / 2) * yScale; + + for (let j = 1; j < line.numPoints; j++) { + line.setY(j - 1, line.getY(j)); + } + line.setY(line.numPoints - 1, chData); + }); + }, + [lines, wglPlots] + ); // Add dependencies here + + useEffect(() => { + createCanvases(); + }, [numChannels]); + + const getValue = useCallback((bits: BitSelection): number => { + switch (bits) { + case "ten": + return 10; + case "twelve": + return 12; + case "fourteen": + return 14; + default: + return 0; // Or any other fallback value you'd like + } + }, []); - useEffect(() => { - if (pauseRef.current) { - requestAnimationFrame(animate); - } - }, [pauseRef.current, animate]); - + const animate = useCallback(() => { + if (pauseRef.current) { + wglPlots.forEach((wglp) => wglp.update()); + requestAnimationFrame(animate); + } + }, [wglPlots, pauseRef]); - return ( -
- {/* Canvas container taking 70% of the screen height */} -
-
-
-
-
-
- ); -}); + useEffect(() => { + if (pauseRef.current) { + requestAnimationFrame(animate); + } + }, [pauseRef.current, animate]); + + return ( +
+ {/* Canvas container taking 70% of the screen height */} +
+
+ {/* Badge for Channel Number */} + + +
+
+
+
+ ); + } +); Canvas.displayName = "Canvas"; export default Canvas; diff --git a/src/components/Connection.tsx b/src/components/Connection.tsx index 8e92483..e65ec48 100644 --- a/src/components/Connection.tsx +++ b/src/components/Connection.tsx @@ -18,7 +18,6 @@ import { Minus, ZoomIn, // For magnify/zoom in functionality ZoomOut, // For zoom out functionality - } from "lucide-react"; import { BoardsList } from "./boards"; import { toast } from "sonner"; @@ -29,13 +28,7 @@ import { TooltipProvider, TooltipTrigger, } from "./ui/tooltip"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "./ui/select"; + import { BitSelection } from "./DataPass"; import { Separator } from "./ui/separator"; @@ -45,6 +38,12 @@ import { PopoverTrigger, } from "../components/ui/popover"; +interface FormattedData { + timestamp: string; + counter: number | null; + [key: string]: number | null | string; +} + interface ConnectionProps { onPauseChange: (pause: boolean) => void; // Callback to pass pause state to parent dataSteam: (data: number[]) => void; @@ -56,7 +55,7 @@ interface ConnectionProps { setCanvasCount: React.Dispatch>; // Specify type for setCanvasCount canvasCount: number; channelCount: number; - SetZoom:React.Dispatch>; + SetZoom: React.Dispatch>; Zoom: number; } @@ -64,7 +63,6 @@ const Connection: React.FC = ({ onPauseChange, dataSteam, Connection, - selectedBits, setSelectedBits, isDisplay, setIsDisplay, @@ -84,12 +82,12 @@ const Connection: React.FC = ({ const [hasData, setHasData] = useState(false); const [recData, setrecData] = useState(false); const [elapsedTime, setElapsedTime] = useState(0); // State to store the recording duration - const timerIntervalRef = useRef(null); // Ref to store the timer interval + const timerIntervalRef = useRef(null); // Type for Node.js environment + const recordingIntervalRef = useRef(null); const [customTime, setCustomTime] = useState(""); // State to store the custom stop time input const endTimeRef = useRef(null); // Ref to store the end time of the recording const startTimeRef = useRef(null); // Ref to store the start time of the recording const bufferRef = useRef([]); // Ref to store the data temporary buffer during recording - const chartRef = useRef([]); // Define chartRef using useRef const portRef = useRef(null); // Ref to store the serial port const indexedDBRef = useRef(null); const [ifBits, setifBits] = useState("auto"); @@ -220,13 +218,17 @@ const Connection: React.FC = ({ const connectToDevice = async () => { try { - const port = await navigator.serial.requestPort(); // Request the serial port + if (portRef.current && portRef.current.readable) { + await disconnectDevice(); + } + + const port = await navigator.serial.requestPort(); await port.open({ baudRate: 230400 }); - Connection(true); // Set the connection state to true, enabling the data visualization + Connection(true); setIsConnected(true); isConnectedRef.current = true; portRef.current = port; - + toast.success("Connection Successful", { description: (
@@ -235,80 +237,75 @@ const Connection: React.FC = ({
), }); - - // Get the reader from the port + const reader = port.readable?.getReader(); readerRef.current = reader; - - // Get the writer from the port (check if it's available) + const writer = port.writable?.getWriter(); if (writer) { - setTimeout(function () { - writerRef.current = writer; - const message = new TextEncoder().encode("START\n"); - writerRef.current.write(message); - }, 2000); + writerRef.current = writer; + const message = new TextEncoder().encode("START\n"); + await writer.write(message); } else { console.error("Writable stream not available"); } - - // Start reading the data from the device + readData(); - - // Request the wake lock to keep the screen on + await navigator.wakeLock.request("screen"); } catch (error) { - // If there is an error during connection, disconnect the device - disconnectDevice(); - isConnectedRef.current = false; - setIsConnected(false); + await disconnectDevice(); console.error("Error connecting to device:", error); + toast.error("Failed to connect to device."); } }; - + const disconnectDevice = async (): Promise => { - // Function to disconnect the device try { - if (portRef.current && portRef.current.readable) { - // Check if the writer is available to send the STOP command + if (portRef.current) { if (writerRef.current) { - const stopMessage = new TextEncoder().encode("STOP\n"); // Prepare the STOP command - console.log(stopMessage); - await writerRef.current.write(stopMessage); // Send the STOP command to the device + const stopMessage = new TextEncoder().encode("STOP\n"); + try { + await writerRef.current.write(stopMessage); + } catch (error) { + console.error("Failed to send STOP command:", error); + } writerRef.current.releaseLock(); - writerRef.current = null; // Reset the writer reference + writerRef.current = null; } - - // Cancel the reader to stop data flow + if (readerRef.current) { - await readerRef.current.cancel(); // Cancel the reader + try { + await readerRef.current.cancel(); + } catch (error) { + console.error("Failed to cancel reader:", error); + } readerRef.current.releaseLock(); - readerRef.current = null; // Reset the reader reference + readerRef.current = null; + } + + if (portRef.current.readable) { + await portRef.current.close(); } - - await portRef.current.close(); // Close the port to disconnect the device portRef.current = null; - - // Notify the user of successful disconnection with a reconnect option + toast("Disconnected from device", { action: { label: "Reconnect", - onClick: () => connectToDevice(), // Reconnect when the "Reconnect" button is clicked + onClick: () => connectToDevice(), }, }); } } catch (error) { - // Handle any errors that occur during disconnection console.error("Error during disconnection:", error); } finally { - // Ensure the connection state is properly updated - setIsConnected(false); // Update state to indicate the device is disconnected - Connection(false); + setIsConnected(false); isConnectedRef.current = false; - isRecordingRef.current = false; // Ensure recording is stopped + isRecordingRef.current = false; + Connection(false); } }; - + // Function to read data from a connected device and process it const readData = async (): Promise => { const buffer: number[] = []; // Buffer to store incoming data @@ -404,32 +401,30 @@ const Connection: React.FC = ({ } }; - - // Function to add channel data to the buffer - const addToBuffer = (channelData: number[]) => { - if (channelData.length !== channelCount) { - console.error(`Expected ${channelCount} channels, but got ${channelData.length}`); - return; - } - - // Add the channel data to the buffer - bufferdRef.current.push(channelData); - - // Check if the buffer is full - if (bufferdRef.current.length === bufferSize) { - setBufferFull(true); - - // Process the buffer data (e.g., plot or save) - // console.log("Buffer is full and ready for use:", bufferdRef.current); - - // Reset the buffer for reuse - bufferdRef.current = []; - setBufferFull(false); // Reset the flag after processing - } -}; + // Function to add channel data to the buffer + const addToBuffer = (channelData: number[]) => { + if (channelData.length !== channelCount) { + console.error( + `Expected ${channelCount} channels, but got ${channelData.length}` + ); + return; + } + + // Add the channel data to the buffer + bufferdRef.current.push(channelData); + // Check if the buffer is full + if (bufferdRef.current.length === bufferSize) { + setBufferFull(true); - const convertToCSV = (data: any[]): string => { + // Reset the buffer for reuse + bufferdRef.current = []; + setBufferFull(false); // Reset the flag after processing + } + }; + + // Function to convert data to CSV format + const convertToCSV = (data: FormattedData[]): string => { if (data.length === 0) return ""; const header = Object.keys(data[0]); @@ -471,9 +466,16 @@ const Connection: React.FC = ({ // Handle any errors during the IndexedDB initialization console.error("Failed to initialize IndexedDB:", error); toast.error( - "Failed to initialize storage. Recording may not be saved." // Show an error message if initialization fails + "Failed to initialize storage. Recording may not be saved." ); } + + // Start reading and saving data + recordingIntervalRef.current = setInterval(() => { + const data = bufferRef.current; // Use bufferRef which stores actual data + saveDataDuringRecording(data); // Save the data to IndexedDB + bufferRef.current = []; // Clear the buffer after saving + }, 1000); // Save data every 1 second or adjust the interval as needed } } else { // Notify the user if no device is connected @@ -511,72 +513,67 @@ const Connection: React.FC = ({ timerIntervalRef.current = null; } - // Check if the start time was set correctly - if (startTimeRef.current === null) { - toast.error("Start time was not set properly."); - return; // Exit the function if start time is not valid - } + const endTime = new Date(); // Capture the end time + const recordedFilesCount = (await getAllDataFromIndexedDB()).length; - // Record the end time and format it for display - const endTime = new Date(); - const endTimeString = endTime.toLocaleTimeString(); - const startTimeString = new Date(startTimeRef.current).toLocaleTimeString(); + // Check if startTimeRef.current is not null before using it + if (startTimeRef.current !== null) { + // Format the start and end times as readable strings + const startTimeString = new Date( + startTimeRef.current + ).toLocaleTimeString(); + const endTimeString = endTime.toLocaleTimeString(); - // Calculate the recording duration in seconds - const durationInSeconds = Math.round( - (endTime.getTime() - startTimeRef.current) / 1000 - ); + // Calculate the duration of the recording + const durationInSeconds = Math.floor( + (endTime.getTime() - startTimeRef.current) / 1000 + ); - // If there is recorded data in the buffer, save it - if (bufferRef.current.length > 0) { - await saveDataDuringRecording(bufferRef.current); - bufferRef.current = []; // Clear the buffer after saving - } + // Close IndexedDB reference + if (indexedDBRef.current) { + indexedDBRef.current.close(); + indexedDBRef.current = null; // Reset the reference + } - // Retrieve all recorded data from IndexedDB - const allData = await getAllDataFromIndexedDB(); - setHasData(allData.length > 0); // Update state to reflect if data exists - const recordedFilesCount = allData.length; // Count of recorded files + const allData = await getAllDataFromIndexedDB(); + setHasData(allData.length > 0); - // Close the IndexedDB connection if it's open - if (indexedDBRef.current) { - indexedDBRef.current.close(); - indexedDBRef.current = null; // Reset the reference + // Display the toast with all the details + toast.success("Recording completed successfully", { + description: ( +
+

Start Time: {startTimeString}

+

End Time: {endTimeString}

+

Recording Duration: {formatDuration(durationInSeconds)}

+

Samples Recorded: {recordedFilesCount}

+
+ ), + }); + } else { + console.error("Start time is null. Unable to calculate duration."); + toast.error("Recording start time was not captured."); } - setrecData(false); // Reset recording data state - - // Display a success message with recording details - toast.success("Recording completed Successfully", { - description: ( -
-

Start Time: {startTimeString}

-

End Time: {endTimeString}

-

Recording Duration: {formatDuration(durationInSeconds)}

-

Samples Recorded: {recordedFilesCount}

-

Data saved to IndexedDB

-
- ), - }); - - // Reset recording states - isRecordingRef.current = false; // Indicate recording has stopped - startTimeRef.current = null; // Clear the start time - endTimeRef.current = null; // Clear the end time - setElapsedTime(0); // Reset the elapsed time display - - setIsRecordButtonDisabled(true); // Disable the record button - }; - - const checkDataAvailability = async () => { - const allData = await getAllDataFromIndexedDB(); - setHasData(allData.length > 0); + // Reset the recording state + isRecordingRef.current = false; + setElapsedTime(0); + setrecData(false); + setIsRecordButtonDisabled(true); }; // Call this function when your component mounts or when you suspect the data might change useEffect(() => { - checkDataAvailability(); - }, []); + const checkDataAndConnection = async () => { + // Check if data exists in IndexedDB + const allData = await getAllDataFromIndexedDB(); + setHasData(allData.length > 0); + + // Disable the record button if there is data in IndexedDB and device is connected + setIsRecordButtonDisabled(allData.length > 0 || !isConnected); + }; + + checkDataAndConnection(); + }, [isConnected]); // Add this function to save data to IndexedDB during recording const saveDataDuringRecording = async (data: number[][]) => { @@ -586,13 +583,18 @@ const Connection: React.FC = ({ const tx = indexedDBRef.current.transaction(["adcReadings"], "readwrite"); const store = tx.objectStore("adcReadings"); - // Dynamically create channel data based on the current canvas count + console.log(`Saving data for ${canvasCount} channels.`); + for (const row of data) { - const channels = row.slice(0, canvasCount); // Only include channels up to the current canvasCount + // Ensure all channels are present by filling missing values with null + const channels = row + .slice(0, canvasCount) + .map((value) => (value !== undefined ? value : null)); + await store.add({ timestamp: new Date().toISOString(), - channels: channels.map(Number), // Save channel data as an array of numbers - counter: Number(row[6]), // If you have a counter column + channels, // Save the array of channels + counter: row[6], // Adjust based on counter location }); } } catch (error) { @@ -601,6 +603,7 @@ const Connection: React.FC = ({ }; // Function to format time from seconds into a "MM:SS" string format + const formatTime = (seconds: number): string => { // Calculate the number of minutes by dividing seconds by 60 const mins = Math.floor(seconds / 60); @@ -651,7 +654,6 @@ const Connection: React.FC = ({ }; // Delete all data from IndexedDB - // Function to delete all data from the IndexedDB const deleteDataFromIndexedDB = async () => { try { // Initialize the IndexedDB @@ -659,35 +661,21 @@ const Connection: React.FC = ({ // Start a readwrite transaction on the "adcReadings" object store const tx = db.transaction(["adcReadings"], "readwrite"); - const store = tx.objectStore("adcReadings"); // Access the object store - - // Clear the store to remove all records - const clearRequest = store.clear(); - - // Event handler for successful clearing of the store - clearRequest.onsuccess = async () => { - console.log("All data deleted from IndexedDB"); // Log success to console - toast.success("Recorded file is deleted."); // Notify user of successful deletion - setOpen(false); // Close any open modal or dialog - - // Check if there is any data left in the database after deletion - const allData = await getAllDataFromIndexedDB(); - setHasData(allData.length > 0); // Update state to reflect if data exists + const store = tx.objectStore("adcReadings"); - setIsRecordButtonDisabled(false); // Enable the record button after deletion - }; + await store.clear(); + console.log("All data deleted from IndexedDB"); + toast.success("Recorded file is deleted."); - // Event handler for any errors that occur during the clear request - clearRequest.onerror = (error) => { - console.error("Error deleting data from IndexedDB:", error); // Log the error - toast.error("Failed to delete data. Please try again."); // Notify user of the failure - }; + // Check if there is any data left in the database after deletion + const allData = await getAllDataFromIndexedDB(); + setHasData(allData.length > 0); + setIsRecordButtonDisabled(false); } catch (error) { - // Handle any errors that occur during IndexedDB initialization - console.error("Error initializing IndexedDB for deletion:", error); + console.error("Error deleting data from IndexedDB:", error); + toast.error("Failed to delete data. Please try again."); } }; - // Function to retrieve all data from the IndexedDB const getAllDataFromIndexedDB = async (): Promise => { return new Promise(async (resolve, reject) => { @@ -717,50 +705,42 @@ const Connection: React.FC = ({ } }); }; - // Updated saveData function const saveData = async () => { try { - const allData = await getAllDataFromIndexedDB(); + const allData = await getAllDataFromIndexedDB(); // Fetch data from IndexedDB if (allData.length === 0) { toast.error("No data available to download."); return; } - // Dynamically format data based on the current canvasCount + // Ensure all channel data is formatted properly and missing data is handled const formattedData = allData.map((item) => { - const dynamicChannels: { [key: string]: number | null } = {}; // Define the type of dynamicChannels + const dynamicChannels: { [key: string]: number | null } = {}; // Assume channels are stored as an array in `item.channels` const channels = item.channels || []; - // Loop through the channels array based on canvasCount and log the channel data + // Loop through the channels array based on canvasCount for (let i = 0; i < canvasCount; i++) { - const channelKey = `channel_${i + 1}`; + const channelKey = `channel_${i + 1}`; // Create a dynamic key for each channel dynamicChannels[channelKey] = - channels[i] !== undefined ? channels[i] : null; // Access channels array - - // Log the value of each channel - console.log(`Channel ${i + 1} value:`, channels[i]); + channels[i] !== undefined ? channels[i] : null; // Handle missing data } return { timestamp: item.timestamp, ...dynamicChannels, // Spread the dynamic channels into the result object - counter: item.counter || null, // Include the counter if you have it + counter: item.counter || null, // Include the counter if available }; }); - console.log("Formatted data to be saved:", formattedData); - - setOpen(false); - // Convert the formatted data to CSV const csvData = convertToCSV(formattedData); const blob = new Blob([csvData], { type: "text/csv;charset=utf-8" }); - // Get the current date and time + // Get the current date and time for the filename const now = new Date(); const formattedTimestamp = `${now.getFullYear()}-${String( now.getMonth() + 1 @@ -772,12 +752,15 @@ const Connection: React.FC = ({ // Use the timestamp in the filename const filename = `recorded_data_${formattedTimestamp}.csv`; - saveAs(blob, filename); + saveAs(blob, filename); // Trigger download // Delete the data from IndexedDB after saving - await deleteDataFromIndexedDB(); - toast.success("Data downloaded and cleared from storage."); - setHasData(false); // Update state after data is deleted + await deleteDataFromIndexedDB(); // Clear the IndexedDB + toast.success("Data downloaded and cleared from storage."); // Success notification + + // Check if any data remains after deletion + const remainingData = await getAllDataFromIndexedDB(); + setHasData(remainingData.length > 0); // Update hasData state } catch (error) { console.error("Error saving data:", error); toast.error("Failed to save data. Please try again."); @@ -897,18 +880,13 @@ const Connection: React.FC = ({ -

- {Zoom === 1 - ? "We can't shrinkage" - : "Decrease Zoom"} -

+

{Zoom === 1 ? "We can't shrinkage" : "Decrease Zoom"}

@@ -925,11 +903,7 @@ const Connection: React.FC = ({ -

- {FullZoom - ? "Remove Full Zoom" - : "Full Zoom"} -

+

{FullZoom ? "Remove Full Zoom" : "Full Zoom"}

@@ -948,9 +922,7 @@ const Connection: React.FC = ({

- {Zoom >= 10 - ? "Maximum Zoom Reached" - : "Increase Zoom"} + {Zoom >= 10 ? "Maximum Zoom Reached" : "Increase Zoom"}

@@ -1031,54 +1003,35 @@ const Connection: React.FC = ({ - {hasData && datasets.length === 1 ? ( - - - - - -

Delete Data

-
-
- ) : ( - <> - - - - - -

Save Data as Zip

-
-
- - - - - - -

Delete All Data

-
-
- - )} + + + + + +

Save Data as Zip

+
+
+ + + + + + +

Delete All Data

+
+
)}