From aa7d423393a18eeb65a48eaef6c03a40b85aaf3b Mon Sep 17 00:00:00 2001 From: Aman Maheshwari Date: Fri, 6 Dec 2024 16:17:18 +0530 Subject: [PATCH] Added frame buffer feature --- Version.tsx | 2 - package.json | 2 +- src/components/Canvas.tsx | 133 +++++++++++++---- src/components/Connection.tsx | 250 ++++++++++++++++---------------- src/components/Contributors.tsx | 6 +- src/components/DataPass.tsx | 24 +-- src/components/boards.ts | 5 + src/components/filters.tsx | 26 +--- 8 files changed, 258 insertions(+), 190 deletions(-) delete mode 100644 Version.tsx diff --git a/Version.tsx b/Version.tsx deleted file mode 100644 index d8abb7f..0000000 --- a/Version.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// version.tsx -export const VERSION = "2.2.0a"; diff --git a/package.json b/package.json index 75f867a..d207404 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Chords", - "version": "0.1.0", + "version": "2.2.0a", "private": true, "scripts": { "dev": "next dev", diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 9e4739c..2c68481 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -16,10 +16,8 @@ interface CanvasProps { isDisplay: boolean; canvasCount?: number; Zoom: number; -} -interface Batch { - time: number; - values: number[]; + currentSnapshot: number; + snapShotRef: React.MutableRefObject; } const Canvas = forwardRef( @@ -30,6 +28,8 @@ const Canvas = forwardRef( isDisplay, canvasCount = 6, // default value in case not provided Zoom, + currentSnapshot, + snapShotRef, }: CanvasProps, ref ) => { @@ -45,6 +45,13 @@ const Canvas = forwardRef( const sweepPositions = useRef(new Array(6).fill(0)); // Array for sweep positions const currentSweepPos = useRef(new Array(6).fill(0)); // Array for sweep positions let numX: number; + const array3DRef = useRef( + Array.from({ length: 6 }, () => + Array.from({ length: 6 }, () => Array()) + ) + ); + const activebuffer = useRef(0); // Initialize useRef with 0 + const indicesRef = useRef([]); // Use `useRef` for indices const getpoints = useCallback((bits: BitSelection): number => { switch (bits) { @@ -57,6 +64,41 @@ const Canvas = forwardRef( } }, []); numX = getpoints(selectedBits); + const prevCanvasCountRef = useRef(canvasCount); + + const processIncomingData = (incomingData: number[]) => { + for (let i = 0; i < canvasCount; i++) { + + if (prevCanvasCountRef.current !== canvasCount) { + // Clear the entire buffer if canvasCount changes + for (let bufferIndex = 0; bufferIndex < 6; bufferIndex++) { + array3DRef.current[bufferIndex] = Array.from({ length: canvasCount }, () => []); + snapShotRef.current[bufferIndex] = false; + } + prevCanvasCountRef.current = canvasCount; + } + if (array3DRef.current[activebuffer.current][i].length >= numX) { + array3DRef.current[activebuffer.current][i] = []; + } + array3DRef.current[activebuffer.current][i].push(incomingData[i]); + + if (array3DRef.current[activebuffer.current][i].length < numX && !pauseRef.current) { + array3DRef.current[activebuffer.current][i] = []; + } + } + + + if (array3DRef.current[activebuffer.current][0].length >= numX) { + snapShotRef.current[activebuffer.current] = true; + activebuffer.current = (activebuffer.current + 1) % 6; + snapShotRef.current[activebuffer.current] = false; + } + indicesRef.current = []; + for (let i = 1; i < 6; i++) { + indicesRef.current.push((activebuffer.current - i + 6) % 6); + } + }; + useEffect(() => { setNumChannels(canvasCount); }, [canvasCount]); @@ -66,11 +108,14 @@ const Canvas = forwardRef( () => ({ updateData(data: number[]) { // Reset the sweep positions if the number of channels has changed - if (currentSweepPos.current.length !== numChannels) { + if (currentSweepPos.current.length !== numChannels || !pauseRef.current) { currentSweepPos.current = new Array(numChannels).fill(0); sweepPositions.current = new Array(numChannels).fill(0); } - updatePlots(data, Zoom); + if (pauseRef.current) { + processIncomingData(data); + updatePlots(data, Zoom); + } if (previousCounter !== null) { // If there was a previous counter value const expectedCounter: number = (previousCounter + 1) % 256; // Calculate the expected counter value @@ -136,7 +181,7 @@ const Canvas = forwardRef( // Append grid lines to the wrapper canvasWrapper.appendChild(gridLineX); } - const horizontalline=50; + const horizontalline = 50; for (let j = 1; j < horizontalline; j++) { const gridLineY = document.createElement("div"); gridLineY.className = "absolute bg-[rgb(128,128,128)]"; @@ -158,8 +203,8 @@ const Canvas = forwardRef( const canvas = document.createElement("canvas"); canvas.id = `canvas${i + 1}`; - canvas.width = canvasContainerRef.current.clientWidth ; - const canvasHeight = (canvasContainerRef.current.clientHeight / numChannels) ; + canvas.width = canvasContainerRef.current.clientWidth; + const canvasHeight = (canvasContainerRef.current.clientHeight / numChannels); canvas.height = canvasHeight; canvas.className = "w-full h-full block rounded-xl"; @@ -259,31 +304,63 @@ const Canvas = forwardRef( createCanvases(); }, [numChannels, theme]); - 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) { + if (!pauseRef.current) { + // If paused, show the buffered data (this part runs when paused) + updatePlotSnapshot(currentSnapshot); + } else { + // If not paused, continue with normal updates (e.g., real-time plotting) wglPlots.forEach((wglp) => wglp.update()); - requestAnimationFrame(animate); + requestAnimationFrame(animate); // Continue the animation loop } - }, [wglPlots, pauseRef]); + }, [currentSnapshot, numX, pauseRef.current, wglPlots, Zoom]); + + + const updatePlotSnapshot = (currentSnapshot: number) => { + for (let i = 0; i < canvasCount; i++) { + wglPlots.forEach((wglp, index) => { + if (wglp) { + try { + wglp.gScaleY = Zoom; // Adjust the zoom value + } catch (error) { + console.error( + `Error setting gScaleY for WebglPlot instance at index ${index}:`, + error + ); + } + } else { + console.warn(`WebglPlot instance at index ${index} is undefined.`); + } + }); + if ( + array3DRef.current && + indicesRef.current && + indicesRef.current[currentSnapshot] !== undefined && + array3DRef.current[indicesRef.current[currentSnapshot]] !== undefined + ) { + const yArray = new Float32Array(array3DRef.current[indicesRef.current[currentSnapshot]][i]); + // Check if the line exists + const line = linesRef.current[i]; + if (line) { + line.shiftAdd(yArray); // Efficiently add new points + } else { + console.error(`Line at index ${i} is undefined or null.`); + } + + } else { + console.warn("One of the references is undefined or invalid"); + } + - useEffect(() => { - if (pauseRef.current) { - requestAnimationFrame(animate); } - }, [pauseRef.current, animate]); + wglPlots.forEach((wglp) => wglp.update()); // Redraw the plots + }; + + useEffect(() => { + requestAnimationFrame(animate); + + }, [animate]); useEffect(() => { const handleResize = () => { diff --git a/src/components/Connection.tsx b/src/components/Connection.tsx index 55352b2..51e23c8 100644 --- a/src/components/Connection.tsx +++ b/src/components/Connection.tsx @@ -24,6 +24,8 @@ import { Brain, Eye, BicepsFlexed, + ArrowRightToLine, + ArrowLeftToLine, } from "lucide-react"; import { BoardsList } from "./boards"; import { toast } from "sonner"; @@ -54,7 +56,11 @@ interface ConnectionProps { canvasCount: number; channelCount: number; SetZoom: React.Dispatch>; + SetcurrentSnapshot: React.Dispatch>; + currentSnapshot: number; Zoom: number; + snapShotRef: React.RefObject; + } const Connection: React.FC = ({ @@ -67,6 +73,9 @@ const Connection: React.FC = ({ setIsDisplay, setCanvasCount, canvasCount, + SetcurrentSnapshot, + currentSnapshot, + snapShotRef, SetZoom, Zoom, }) => { @@ -99,14 +108,15 @@ const Connection: React.FC = ({ null ); const buffer: number[] = []; // Buffer to store incoming data - const bufferdRef = useRef([[], []]); // Two buffers: [0] and [1] const [isFilterPopoverOpen, setIsFilterPopoverOpen] = useState(false); - const filterRef = useRef(null); const togglePause = () => { const newPauseState = !isDisplay; setIsDisplay(newPauseState); onPauseChange(newPauseState); // Notify parent about the change + SetcurrentSnapshot(0); + setClickCount(0); + }; const increaseCanvas = () => { if (canvasCount < 6) { @@ -114,6 +124,33 @@ const Connection: React.FC = ({ } }; + const [clickCount, setClickCount] = useState(0); // Track how many times the left arrow is clicked + + const enabledClicks = (snapShotRef.current?.filter(Boolean).length ?? 0)-1; + + // Enable/Disable left arrow button + const handlePrevSnapshot = () => { + if (clickCount < enabledClicks) { + setClickCount(clickCount + 1); + } + + if (currentSnapshot < 4) { + SetcurrentSnapshot(currentSnapshot + 1); + } + }; + + // Handle right arrow click (reset count and disable button if needed) + const handleNextSnapshot = () => { + if (clickCount>0) { + setClickCount(clickCount-1); // Reset count after right arrow click + } + if (currentSnapshot > 0) { + SetcurrentSnapshot(currentSnapshot - 1); + } + }; + + + const decreaseCanvas = () => { if (canvasCount > 1) { setCanvasCount(canvasCount - 1); // Decrease canvas count but not below 1 @@ -167,7 +204,7 @@ const Connection: React.FC = ({ } } }; - + const handleCustomTimeChange = (e: React.ChangeEvent) => { // Function to handle the custom time input change const value = e.target.value.replace(/[^0-9]/g, ""); @@ -200,7 +237,7 @@ const Connection: React.FC = ({ setifBits(board.bits as BitSelection); setSelectedBits(board.bits as BitSelection); detectedBitsRef.current = board.bits as BitSelection; - return (<>{board.name}
Product ID: {info.usbProductId}); // Return the board name and product ID + return (<>{board.name}
Product ID: {info.usbProductId}); // Return the board name and product ID } setDetectedBits(null); @@ -217,151 +254,93 @@ const Connection: React.FC = ({ } }; - - // const connectToDevice = async () => { - // try { - // // Disconnect any currently open port - // if (portRef.current && portRef.current.readable) { - // await disconnectDevice(); - // } - - // // Retrieve saved port information from localStorage - // const savedPort = localStorage.getItem('lastdevice'); - // let port: SerialPort | null = null; - - // if (savedPort) { - // const savedPorts = JSON.parse(savedPort); - - // // Attempt to get the matching port based on saved info - // const ports = await navigator.serial.getPorts(); - // port = ports.find(p => { - // const info = p.getInfo(); - // return info.usbVendorId === savedPorts.usbVendorId && info.usbProductId === savedPorts.usbProductId; - // })|| null; - - // if (port) { - // await port.open({ baudRate: 230400}); - // } - // } - - // if (!port) { - // // If no saved port or no matching port found, prompt user to select a port - // port = await navigator.serial.requestPort(); - // await port.open({ baudRate: 230400 }); - // } - - // // If port is successfully connected - // Connection(true); - // setIsConnected(true); - // onPauseChange(true); - // setIsDisplay(true); - // isConnectedRef.current = true; - // portRef.current = port; - - // // Save the necessary information (usbVendorId, usbProductId, baudRate) to localStorage - // const portInfo = await port.getInfo(); - // localStorage.setItem('lastdevice', JSON.stringify({ - // usbVendorId: portInfo.usbVendorId, - // usbProductId: portInfo.usbProductId, - // baudRate: 230400 - // })); - - // toast.success("Connection Successful", { - // description: ( - //
- //

Device: {formatPortInfo(portInfo)}

- //

Baud Rate: 230400

- //
- // ), - // }); - - // // Set up reader and writer for data transfer - // const reader = port.readable?.getReader(); - // readerRef.current = reader; - - // const writer = port.writable?.getWriter(); - // if (writer) { - // setTimeout(function () { - // writerRef.current = writer; - // const message = new TextEncoder().encode("START\n"); - // writerRef.current.write(message); - // }, 2000); - // } else { - // console.error("Writable stream not available"); - // } - - // readData(); - // await navigator.wakeLock.request("screen"); - - // } catch (error) { - // await disconnectDevice(); - // console.error("Error connecting to device:", error); - // toast.error("Failed to connect to device."); - // } - // }; - interface SavedDevice { usbVendorId: number; usbProductId: number; baudRate: number; } - + const connectToDevice = async () => { try { if (portRef.current && portRef.current.readable) { await disconnectDevice(); } - + const savedPorts: SavedDevice[] = JSON.parse(localStorage.getItem('savedDevices') || '[]'); let port: SerialPort | null = null; - + let baudRate = 230400; // Default baud rate + const ports = await navigator.serial.getPorts(); + if (savedPorts.length > 0) { port = ports.find(p => { const info = p.getInfo(); - return savedPorts.some((saved: SavedDevice) => - saved.usbVendorId === info.usbVendorId && saved.usbProductId === info.usbProductId + return savedPorts.some(saved => + saved.usbVendorId === (info.usbVendorId ?? 0) && saved.usbProductId === (info.usbProductId ?? 0) ); }) || null; } - + if (!port) { port = await navigator.serial.requestPort(); - await port.open({ baudRate: 230400 }); - const newPortInfo = await port.getInfo(); - if (!savedPorts.some(saved => saved.usbVendorId === newPortInfo.usbVendorId && saved.usbProductId === newPortInfo.usbProductId)) { + + + const usbVendorId = newPortInfo.usbVendorId ?? 0; + const usbProductId = newPortInfo.usbProductId ?? 0; + + // Check for specific usbProductId 29987 and set baud rate + if (usbProductId === 29987) { + baudRate = 115200; + + } + + const existingDevice = savedPorts.find(saved => + saved.usbVendorId === usbVendorId && saved.usbProductId === usbProductId + ); + + if (!existingDevice) { savedPorts.push({ - usbVendorId: newPortInfo.usbVendorId??0, - usbProductId: newPortInfo.usbProductId??0, - baudRate: 230400 + usbVendorId, + usbProductId, + baudRate }); localStorage.setItem('savedDevices', JSON.stringify(savedPorts)); + console.log(`New device saved: Vendor ${usbVendorId}, Product ${usbProductId}, Baud Rate ${baudRate}`); } + + await port.open({ baudRate }); } else { - await port.open({ baudRate: 230400 }); + const portInfo = port.getInfo(); + const usbProductId = portInfo.usbProductId ?? 0; + + // Check again if the port has productId 29987 + if (usbProductId === 29987) { + baudRate = 115200; + } + + await port.open({ baudRate }); } - + Connection(true); setIsConnected(true); onPauseChange(true); setIsDisplay(true); isConnectedRef.current = true; portRef.current = port; - + toast.success("Connection Successful", { description: (

Device: {formatPortInfo(port.getInfo())}

-

Baud Rate: 230400

+

Baud Rate: {baudRate}

), }); - const reader = port.readable?.getReader(); readerRef.current = reader; - + const writer = port.writable?.getWriter(); if (writer) { setTimeout(() => { @@ -372,18 +351,17 @@ const Connection: React.FC = ({ } else { console.error("Writable stream not available"); } - + readData(); await navigator.wakeLock.request("screen"); - + } catch (error) { await disconnectDevice(); console.error("Error connecting to device:", error); toast.error("Failed to connect to device."); } }; - - + const disconnectDevice = async (): Promise => { try { if (portRef.current) { @@ -394,25 +372,30 @@ const Connection: React.FC = ({ } catch (error) { console.error("Failed to send STOP command:", error); } - writerRef.current.releaseLock(); - writerRef.current = null; + if (writerRef.current) { + writerRef.current.releaseLock(); + writerRef.current = null; + } } - + snapShotRef.current?.fill(false); if (readerRef.current) { try { await readerRef.current.cancel(); } catch (error) { console.error("Failed to cancel reader:", error); } - readerRef.current.releaseLock(); - readerRef.current = null; + if (readerRef.current) { + readerRef.current.releaseLock(); + readerRef.current = null; + } } - + + // Close port if (portRef.current.readable) { await portRef.current.close(); } portRef.current = null; - + toast("Disconnected from device", { action: { label: "Reconnect", @@ -437,14 +420,14 @@ const Connection: React.FC = ({ const removeEXGFilter = (channelIndex: number) => { delete appliedEXGFiltersRef.current[channelIndex]; // Remove the filter for the channel forceEXGUpdate(); // Trigger re-render - + }; // Function to handle frequency selection const handleFrequencySelectionEXG = (channelIndex: number, frequency: number) => { appliedEXGFiltersRef.current[channelIndex] = frequency; // Update the filter for the channel forceEXGUpdate(); //Trigger re-render - + }; // Function to set the same filter for all channels @@ -453,7 +436,7 @@ const Connection: React.FC = ({ appliedEXGFiltersRef.current[channelIndex] = frequency; // Set the filter for the channel }); forceEXGUpdate(); // Trigger re-render - + }; @@ -1056,7 +1039,7 @@ const Connection: React.FC = ({ @@ -1074,7 +1057,6 @@ const Connection: React.FC = ({ @@ -1092,7 +1074,7 @@ const Connection: React.FC = ({ - + )} + {/* Record button with tooltip */} {isConnected && ( @@ -1162,7 +1161,7 @@ const Connection: React.FC = ({ {/* Save/Delete data buttons with tooltip */} {isConnected && ( -
+
{hasData && datasets.length === 1 && ( @@ -1180,7 +1179,6 @@ const Connection: React.FC = ({ )} - diff --git a/src/components/Contributors.tsx b/src/components/Contributors.tsx index b4e0eb8..66985e0 100644 --- a/src/components/Contributors.tsx +++ b/src/components/Contributors.tsx @@ -11,15 +11,15 @@ import { CardHeader, CardTitle, } from "../components/ui/card"; -import { Separator } from "@/components/ui/separator"; +import { Separator } from "../components/ui/separator"; import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar"; import { Dialog, DialogContent, DialogTrigger } from "../components/ui/dialog"; import { CircleAlert } from "lucide-react"; import { Button } from "../components/ui/button"; import Link from "next/link"; -import { VERSION } from "../../Version"; import Chords from "./LandingComp/Chords"; import { Badge } from "./ui/badge"; +import packageJson from '../../package.json'; const contributors = [ { @@ -72,7 +72,7 @@ export default function Contributors() { - v{VERSION} + v{packageJson.version}

Contributors

diff --git a/src/components/DataPass.tsx b/src/components/DataPass.tsx index 7397904..42196d2 100644 --- a/src/components/DataPass.tsx +++ b/src/components/DataPass.tsx @@ -2,7 +2,7 @@ import Connection from "./Connection"; import Steps from "./Steps"; -import React, { useState ,useCallback,useRef} from "react"; +import React, { useState, useCallback, useRef } from "react"; import Canvas from "./Canvas"; import Navbar from "./Navbar"; // Import the Navbar @@ -17,11 +17,12 @@ const DataPass = () => { const canvasRef = useRef(null); // Create a ref for the Canvas component let previousCounter: number | null = null; // Variable to store the previous counter value for loss detection const [Zoom, SetZoom] = useState(1); // Number of canvases + const [currentSnapshot, SetcurrentSnapshot] = useState(0); // Number of canvases const pauseRef = useRef(true); const handlePauseChange = (newPauseState: boolean) => { pauseRef.current = newPauseState; }; - + const snapShotRef = useRef(Array(6).fill(false)); const datastream = useCallback((data: number[]) => { if (canvasRef.current) { @@ -37,18 +38,20 @@ const DataPass = () => { ); } } - previousCounter =data[6]; // Update the previous counter with the current counter + previousCounter = data[6]; // Update the previous counter with the current counter }, []); return (
-
- +
+
{isConnected ? ( { )} { canvasCount={canvasCount} channelCount={channelCount} SetZoom={SetZoom} + SetcurrentSnapshot={SetcurrentSnapshot} + currentSnapshot={currentSnapshot} Zoom={Zoom} />
diff --git a/src/components/boards.ts b/src/components/boards.ts index 00b2a70..a53f95c 100644 --- a/src/components/boards.ts +++ b/src/components/boards.ts @@ -19,6 +19,11 @@ export const BoardsList = Object.freeze([ field_pid: "105", bits: "fourteen", }, + { + name: "Arduino UNO R4 WiFi", + field_pid: "4098", + bits: "fourteen", + }, { name: "Maker Uno", field_pid: "29987", diff --git a/src/components/filters.tsx b/src/components/filters.tsx index fbf0e8b..f64e40a 100644 --- a/src/components/filters.tsx +++ b/src/components/filters.tsx @@ -8,16 +8,6 @@ // // Note: // filter_gen.py provides C/C++ type functions which we have converted to TS -// TypeScript filter classes for Chords -// Made with <3 at Upside Down labs -// Author: Aman Maheshwari -// -// Reference: -// https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.butter.html -// https://courses.ideate.cmu.edu/16-223/f2020/Arduino/FilterDemos/filter_gen.py -// -// Note: -// filter_gen.py provides C/C++ type functions which we have converted to TS //Notch Filter 50Hz/60Hz export class EXGFilter { @@ -37,7 +27,6 @@ export class EXGFilter { // Initialize state variables this.z1 = 0; this.z2 = 0; - this.x1 = 0; this.x2 = 0; this.x3 = 0; @@ -46,8 +35,11 @@ export class EXGFilter { this.bitsPoints=0; this.yScale=0; } - //sample 1.500 2.250 - //TYPE 1.ECG + //sample- + //1.500 + //2.250 + //TYPE- + //1.ECG //2.EOG //3.EEG //4.EMG @@ -147,35 +139,27 @@ export class EXGFilter { break; default: break; - } return chData; } } - - export class Notch { // Properties to hold the state of the filter sections private z1_1: number; private z2_1: number; - private z1_2: number; private z2_2: number; - private x_1: number; private x_2: number; - private sample: string | null; constructor() { // Initialize state variables for both filter sections this.z1_1 = 0; this.z2_1 = 0; - this.z1_2 = 0; this.z2_2 = 0; - this.x_1 = 0; this.x_2 = 0; this.sample = null;