diff --git a/Version.tsx b/Version.tsx index d9b9b6f..d8abb7f 100644 --- a/Version.tsx +++ b/Version.tsx @@ -1,2 +1,2 @@ // version.tsx -export const VERSION = "2.1.0a"; +export const VERSION = "2.2.0a"; diff --git a/public/assets/dark/Webserialdark.svg b/public/assets/dark/Webserialdark.svg new file mode 100644 index 0000000..a174e54 --- /dev/null +++ b/public/assets/dark/Webserialdark.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/public/assets/dark/indexDBdark.svg b/public/assets/dark/indexDBdark.svg new file mode 100644 index 0000000..1fcb776 --- /dev/null +++ b/public/assets/dark/indexDBdark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/public/assets/light/indexDBlight.svg b/public/assets/light/indexDBlight.svg new file mode 100644 index 0000000..9d9eac8 --- /dev/null +++ b/public/assets/light/indexDBlight.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/public/assets/light/serialdevicelight.svg b/public/assets/light/serialdevicelight.svg new file mode 100644 index 0000000..9661ddd --- /dev/null +++ b/public/assets/light/serialdevicelight.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/components/Connection.tsx b/src/components/Connection.tsx index 4b873bf..70a15a9 100644 --- a/src/components/Connection.tsx +++ b/src/components/Connection.tsx @@ -2,6 +2,7 @@ import React, { useState, useRef, useCallback, useEffect } from "react"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; +import { Notch } from './filters'; import { Cable, @@ -17,6 +18,8 @@ import { Minus, ZoomIn, // For magnify/zoom in functionality ZoomOut, // For zoom out functionality + CircleOff, + ReplaceAll, } from "lucide-react"; import { BoardsList } from "./boards"; import { toast } from "sonner"; @@ -54,6 +57,7 @@ const Connection: React.FC = ({ onPauseChange, datastream, Connection, + selectedBits, setSelectedBits, isDisplay, setIsDisplay, @@ -67,6 +71,7 @@ const Connection: React.FC = ({ const isRecordingRef = useRef(false); // Ref to track if the device is recording const [isEndTimePopoverOpen, setIsEndTimePopoverOpen] = useState(false); const [detectedBits, setDetectedBits] = useState(null); // State to store the detected bits + const detectedBitsRef = React.useRef(null); const [isRecordButtonDisabled, setIsRecordButtonDisabled] = useState(false); // New state variable const [datasets, setDatasets] = useState([]); // State to store the recorded datasets const [hasData, setHasData] = useState(false); @@ -91,6 +96,8 @@ const Connection: React.FC = ({ ); 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; @@ -188,6 +195,7 @@ const Connection: React.FC = ({ if (board) { 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 } @@ -253,6 +261,20 @@ const Connection: React.FC = ({ toast.error("Failed to connect to device."); } }; + const sample = useCallback((bits: BitSelection | null): number => { + if (bits === null) { + return 0; // Default value for null input + } + console.log(bits); + switch (bits) { + case "fourteen": + return 1; + case "ten": + return 2; + default: + return 0; // Fallback value for unexpected cases + } + }, []); const disconnectDevice = async (): Promise => { try { @@ -299,6 +321,43 @@ const Connection: React.FC = ({ Connection(false); } }; + const appliedFiltersRef = React.useRef<{ [key: number]: number }>({}); + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + + const removeFilter = (channelIndex: number) => { + delete appliedFiltersRef.current[channelIndex]; // Remove the filter for the channel + forceUpdate(); // Trigger re-render + console.log(`Filter removed from Channel ${channelIndex}`); + }; + + // Function to handle frequency selection + const handleFrequencySelection = (channelIndex: number, frequency: number) => { + appliedFiltersRef.current[channelIndex] = frequency; // Update the filter for the channel + forceUpdate(); //Trigger re-render + console.log( + `Channel ${channelIndex} selected with frequency ${frequency}Hz` + ); + }; + + // Function to set the same filter for all channels + const applyFilterToAllChannels = (channels: number[], frequency: number) => { + channels.forEach((channelIndex) => { + appliedFiltersRef.current[channelIndex] = frequency; // Set the filter for the channel + }); + forceUpdate(); // Trigger re-render + console.log( + `Filter set to ${frequency}Hz for all channels: ${channels.join(", ")}` + ); + }; + + // Function to remove the filter for all channels + const removeFilterFromAllChannels = (channels: number[]) => { + channels.forEach((channelIndex) => { + delete appliedFiltersRef.current[channelIndex]; // Remove the filter for the channel + }); + forceUpdate(); // Trigger re-render + console.log(`Filters removed from all channels: ${channels.join(", ")}`); + }; // Function to read data from a connected device and process it const readData = async (): Promise => { @@ -309,6 +368,14 @@ const Connection: React.FC = ({ const SYNC_BYTE2 = 0x7c; // Second synchronization byte const END_BYTE = 0x01; // End byte to signify the end of a packet let previousCounter: number | null = null; // Variable to store the previous counter value for loss detection + const notchFilters = [ + new Notch(), // Notch_1 + new Notch(), // Notch_2 + new Notch(), // Notch_3 + new Notch(), // Notch_4 + new Notch(), // Notch_5 + new Notch(), // Notch_6 + ]; try { // Loop while the device is connected @@ -351,12 +418,18 @@ const Connection: React.FC = ({ const packet = buffer.slice(syncIndex, syncIndex + PACKET_LENGTH); // Extract the packet from the buffer const channelData: number[] = []; // Array to store the extracted channel data for (let channel = 0; channel < NUM_CHANNELS; channel++) { - // Loop through each channel in the packet - const highByte = packet[channel * 2 + HEADER_LENGTH]; // Extract the high byte for the channel - const lowByte = packet[channel * 2 + HEADER_LENGTH + 1]; // Extract the low byte for the channel - const value = (highByte << 8) | lowByte; // Combine high and low bytes to get the channel value - channelData.push(value); // Convert the value to string and store it in the array + const highByte = packet[channel * 2 + HEADER_LENGTH]; + const lowByte = packet[channel * 2 + HEADER_LENGTH + 1]; + const value = (highByte << 8) | lowByte; + channelData.push( + notchFilters[channel].process( + value, + appliedFiltersRef.current[channel], + sample(detectedBitsRef.current) + ) + ); } + const counter = packet[2]; // Extract the counter value from the packet channelData.push(counter); // Add the counter to the channel data datastream(channelData); // Pass the channel data to the LineData function for further processing @@ -469,9 +542,8 @@ const Connection: React.FC = ({ if (minutes === 0) { return `${seconds} second${seconds !== 1 ? "s" : ""}`; } - return `${minutes} minute${minutes !== 1 ? "s" : ""} ${seconds} second${ - seconds !== 1 ? "s" : "" - }`; + return `${minutes} minute${minutes !== 1 ? "s" : ""} ${seconds} second${seconds !== 1 ? "s" : "" + }`; }; // Updated stopRecording function @@ -735,353 +807,466 @@ const Connection: React.FC = ({ toast.error("Failed to save data. Please try again."); } }; -// bg-gray-100 text-white p-2 flex-none flex items-center justify-center + // bg-gray-100 text-white p-2 flex-none flex items-center justify-center return (
- {/* Left-aligned section */} -
- {isRecordingRef.current && ( -
- - -
- + {isRecordingRef.current && ( +
+ + +
+ + + + + +
+
+ Set End Time (minutes)
- )} - - - -
-
- Set End Time (minutes) -
-
- {[1, 10, 20, 30].map((time) => ( +
+ {[1, 10, 20, 30].map((time) => ( + + ))} +
+
+ + e.key === "Enter" && handleCustomTimeSet() + } + onChange={handleCustomTimeChange} + className="w-20" + /> - ))} -
-
- - e.key === "Enter" && handleCustomTimeSet() - } - onChange={handleCustomTimeChange} - className="w-20" - /> - +
-
-
- -
-
- )} -
- - {/* Center-aligned buttons */} -
- {/* Connection button with tooltip */} - - - - - - -

{isConnected ? "Disconnect Device" : "Connect Device"}

-
-
-
- {/* Autoscale/Bit selection */} - {isConnected && ( - - -
- {/* Decrease Canvas Button */} - - - - - -

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

-
-
- - - - {/* Toggle All Channels Button */} - - - - - -

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

-
-
- - - - {/* Increase Canvas Button */} - - - - - -

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

-
-
+ +
-
-
- )} +
+ )} +
- {/* Display (Play/Pause) button with tooltip */} - {isConnected && ( + {/* Center-aligned buttons */} +
+ {/* Connection button with tooltip */} - -

- {isDisplay ? "Pause Data Display" : "Resume Data Display"} -

+

{isConnected ? "Disconnect Device" : "Connect Device"}

- )} + {/* Autoscale/Bit selection */} + {isConnected && ( + + +
+ {/* Decrease Canvas Button */} + + + + + +

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

+
+
- {/* Record button with tooltip */} - {isConnected && ( - - - - - - -

- {!isRecordingRef.current - ? "Start Recording" - : "Stop Recording"} -

-
-
-
- )} + - {/* Save/Delete data buttons with tooltip */} - {isConnected && ( - -
- {hasData && datasets.length === 1 && ( - - - - - -

Save Data as CSV

-
-
- )} + {/* Toggle All Channels Button */} + + + + + +

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

+
+
+ + - + {/* Increase Canvas Button */} + + + + + +

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

+
+
+
+ +
+ )} + {/* Display (Play/Pause) button with tooltip */} + {isConnected && ( + - -

Save Recording

+

+ {isDisplay ? "Pause Data Display" : "Resume Data Display"} +

+
+ )} + {/* Record button with tooltip */} + {isConnected && ( + -

Delete Recording

+

+ {!isRecordingRef.current + ? "Start Recording" + : "Stop Recording"} +

-
-
- )} - - {/* Canvas control buttons with tooltip */} - {isConnected && ( - - -
- {/* Decrease Canvas Button */} - - - - - -

- {canvasCount === 1 - ? "At Least One Canvas Required" - : "Decrease Channel"} -

-
-
+ + )} + + {/* Save/Delete data buttons with tooltip */} + {isConnected && ( + +
+ {hasData && datasets.length === 1 && ( + + + + + +

Save Data as CSV

+
+
+ )} - {/* Toggle All Channels Button */} -

- {showAllChannels - ? "Hide All Channels" - : "Show All Channels"} -

+

Save Recording

- - - {/* Increase Canvas Button */} -

- {canvasCount >= 6 - ? "Maximum Channels Reached" - : "Increase Channel"} -

+

Delete Recording

- -
- )} + + )} + {isConnected && ( + + + + + +
+
+ {/* Filter Name */} +
+ {/* Buttons */} +
+ + + +
+
+
+ {["CH1", "CH2", "CH3", "CH4", "CH5", "CH6"].map((filterName, index) => ( +
+ {/* Filter Name */} +
{filterName}
+ + {/* Buttons */} +
+ + + +
+
+ ))} +
+
+
+ +
+ )} + + + {/* Canvas control buttons with tooltip */} + {isConnected && ( + + +
+ {/* Decrease Canvas Button */} + + + + + +

+ {canvasCount === 1 + ? "At Least One Canvas Required" + : "Decrease Channel"} +

+
+
+ + + + {/* Toggle All Channels Button */} + + + + + +

+ {showAllChannels + ? "Hide All Channels" + : "Show All Channels"} +

+
+
+ + + + {/* Increase Canvas Button */} + + + + + +

+ {canvasCount >= 6 + ? "Maximum Channels Reached" + : "Increase Channel"} +

+
+
+
+
+
+ )} +
-
); }; diff --git a/src/components/LandingComp/TechStack.tsx b/src/components/LandingComp/TechStack.tsx index e4e131b..65b2d3e 100644 --- a/src/components/LandingComp/TechStack.tsx +++ b/src/components/LandingComp/TechStack.tsx @@ -43,8 +43,8 @@ const Stack = () => { name: "Web Serial API", logo: theme === "light" - ? "./assets/dark/favicon.ico" - : "./assets/light/favicon.ico", + ? "./assets/dark/Webserialdark.svg" + : "./assets/light/serialdevicelight.svg", url: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API", description: "For connecting to the serial port of the device.", }, @@ -52,8 +52,8 @@ const Stack = () => { name: "IndexedDB API", logo: theme === "light" - ? "./assets/dark/favicon.ico" - : "./assets/light/favicon.ico", + ? "./assets/dark/indexDBdark.svg" + : "./assets/light/indexDBlight.svg", url: "https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API", description: "IndexedDB is a low-level API for client-side storage.", }, @@ -124,4 +124,4 @@ const Stack = () => { ); }; -export default Stack; +export default Stack; \ No newline at end of file diff --git a/src/components/filters.tsx b/src/components/filters.tsx new file mode 100644 index 0000000..6def59c --- /dev/null +++ b/src/components/filters.tsx @@ -0,0 +1,111 @@ +// 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 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; + + 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; + } + + // Method to apply the filter + process(input: number, type: number, sample: number): number { + if(!type) return input; + let output = input; + switch (sample) { + case 1: // 500Hz + switch (type) { + case 1: // Notch Sampling rate: 500.0 Hz, frequency: [48.0, 52.0] Hz. + this.x_1 = output - (-1.56858163 * this.z1_1) - (0.96424138 * this.z2_1); + output = 0.96508099 * this.x_1 + -1.56202714 * this.z1_1 + 0.96508099 * this.z2_1; + this.z2_1 = this.z1_1; + this.z1_1 = this.x_1; + + // Second filter section + this.x_2 = output - (-1.61100358 * this.z1_2) - (0.96592171 * this.z2_2); + output = 1.0 * this.x_2 + -1.61854514 * this.z1_2 + 1.0 * this.z2_2; + this.z2_2 = this.z1_2; + this.z1_2 = this.x_2; + break; + case 2: // Notch Sampling rate: 500.0 Hz, frequency: [58.0, 62.0] Hz. + this.x_1 = output - (-1.40810535 * this.z1_1) - (0.96443153 * this.z2_1); + output = 0.96508099 * this.x_1 + (-1.40747202 * this.z1_1) + (0.96508099 * this.z2_1); + this.z2_1 = this.z1_1; + this.z1_1 = this.x_1; + + // Second filter section + this.x_2 = output - (-1.45687509 * this.z1_2) - (0.96573127 * this.z2_2); + output = 1.00000000 * this.x_2 + (-1.45839783 * this.z1_2) + (1.00000000 * this.z2_2); + this.z2_2 = this.z1_2; + this.z1_2 = this.x_2; + break; + default: + break; + } + break; + + case 2: // 250Hz + switch (type) { + case 1: // Notch Sampling rate: 250.0 Hz, frequency: [48.0, 52.0] Hz. + this.x_1 = output - (-0.53127491 * this.z1_1) - (0.93061518 * this.z2_1); + output = 0.93137886 * this.x_1 + (-0.57635175 * this.z1_1) + 0.93137886 * this.z2_1; + this.z2_1 = this.z1_1; + this.z1_1 = this.x_1; + + // Second filter section + this.x_2 = output - (-0.66243374 * this.z1_2) - (0.93214913 * this.z2_2); + output = 1.00000000 * this.x_2 + (-0.61881558 * this.z1_2) + 1.00000000 * this.z2_2; + this.z2_2 = this.z1_2; + this.z1_2 = this.x_2; + break; + + case 2: // Notch Sampling rate: 250.0 Hz, frequency: [58.0, 62.0] Hz. + this.x_1 = output - (-0.05269865 * this.z1_1) - (0.93123336 * this.z2_1); + output = 0.93137886 * this.x_1 + (-0.11711144 * this.z1_1) + 0.93137886 * this.z2_1; + this.z2_1 = this.z1_1; + this.z1_1 = this.x_1; + + // Second filter section + this.x_2 = output - (-0.18985625 * this.z1_2) - (0.93153034 * this.z2_2); + output = 1.00000000 * this.x_2 + (-0.12573985 * this.z1_2) + 1.00000000 * this.z2_2; + this.z2_2 = this.z1_2; + this.z1_2 = this.x_2; + break; + + default: + break; + } + break; + + default: + break; + } + + return output; + } +} \ No newline at end of file