From 97f8d649a577600be6a38b29c77749955475865a Mon Sep 17 00:00:00 2001 From: Aman Maheshwari Date: Tue, 3 Feb 2026 15:55:07 +0530 Subject: [PATCH 1/4] Add compatibility with new NPG-Lite BEAST firmware --- src/app/npg-lite/page.tsx | 351 +++++++++++++++++++++++++++++--------- tsconfig.json | 4 +- 2 files changed, 273 insertions(+), 82 deletions(-) diff --git a/src/app/npg-lite/page.tsx b/src/app/npg-lite/page.tsx index 94067f5..f1f6e34 100644 --- a/src/app/npg-lite/page.tsx +++ b/src/app/npg-lite/page.tsx @@ -4,6 +4,7 @@ import React, { useRef, useState, useCallback, + useMemo, } from "react"; import { Separator } from "@/components/ui/separator"; import { Input } from "@/components/ui/input"; @@ -41,11 +42,30 @@ import { Eye, BicepsFlexed, Settings, - Loader + Loader, + Battery, + BatteryCharging, + BatteryLow, + BatteryMedium, + BatteryFull } from "lucide-react"; import { lightThemeColors, darkThemeColors, getCustomColor } from '@/components/Colors'; import { useTheme } from "next-themes"; +// Device configuration interface +interface DeviceConfig { + maxChannels: number; + sampleLength: number; + hasBattery: boolean; + name: string; +} + +const defaultConfig: DeviceConfig = { + maxChannels: 3, + sampleLength: 7, + hasBattery: false, + name: "" +}; const NPG_Ble = () => { const isRecordingRef = useRef(false); // Ref to track if the device is recording @@ -73,9 +93,22 @@ const NPG_Ble = () => { const linesRef = useRef([]); const sweepPositions = useRef(new Array(6).fill(0)); // Array for sweep positions const currentSweepPos = useRef(new Array(6).fill(0)); // Array for sweep positions - const maxCanvasElementCountRef = useRef(3); - const channelNames = Array.from({ length: maxCanvasElementCountRef.current }, (_, i) => `CH${i + 1}`); - let numChannels = 3; + + // Centralized device configuration + const deviceConfigRef = useRef(defaultConfig); + const [deviceConfig, setDeviceConfig] = useState(defaultConfig); + + // State for UI updates only + const [batteryLevel, setBatteryLevel] = useState(null); // Battery level percentage for UI + const [deviceName, setDeviceName] = useState(""); // Store device name for UI + const [refreshKey, setRefreshKey] = useState(0); // Force re-render when config changes + + // Derive channel names from device config + const channelNames = useMemo(() => + Array.from({ length: deviceConfig.maxChannels }, (_, i) => `CH${i + 1}`), + [deviceConfig.maxChannels] + ); + const [selectedChannels, setSelectedChannels] = useState([1]); const [manuallySelected, setManuallySelected] = useState(false); // New state to track manual selection const { theme } = useTheme(); // Current theme of the app @@ -93,24 +126,22 @@ const NPG_Ble = () => { const fillingindex = useRef(0); // Initialize useRef with 0 const MAX_BUFFER_SIZE = 500; const pauseRef = useRef(true); + const togglePause = () => { const newPauseState = !isDisplay; setIsDisplay(newPauseState); pauseRef.current = newPauseState; }; const samplesReceivedRef = useRef(0); + const createCanvasElements = () => { const container = canvasContainerRef.current; if (!container) { return; // Exit if the ref is null } - // Ensure dataPointCount is calculated from current sampling rate and timeBase - const dpCount = samplingrateref.current * timeBase; - dataPointCountRef.current = dpCount; - - currentSweepPos.current = new Array(numChannels).fill(0); - sweepPositions.current = new Array(numChannels).fill(0); + currentSweepPos.current = new Array(deviceConfig.maxChannels).fill(0); + sweepPositions.current = new Array(deviceConfig.maxChannels).fill(0); // Clear existing child elements while (container.firstChild) { @@ -192,10 +223,10 @@ const NPG_Ble = () => { wglp.gScaleY = Zoom; - const line = new WebglLine(getLineColor(channelNumber, theme), dpCount); + const line = new WebglLine(getLineColor(channelNumber, theme), dataPointCountRef.current); wglp.gOffsetY = 0; line.offsetY = 0; - line.lineSpaceX(-1, 2 / dpCount); + line.lineSpaceX(-1, 2 / dataPointCountRef.current); wglp.addLine(line); newLines.push(line); @@ -222,7 +253,7 @@ const NPG_Ble = () => { const handleSelectAllToggle = () => { - const enabledChannels = Array.from({ length: maxCanvasElementCountRef.current }, (_, i) => i + 1); + const enabledChannels = Array.from({ length: deviceConfig.maxChannels }, (_, i) => i + 1); if (!isAllEnabledChannelSelected) { // Programmatic selection of all channels @@ -231,13 +262,11 @@ const NPG_Ble = () => { } else { // RESET functionality const savedchannels = JSON.parse(localStorage.getItem('savedchannels') || '[]'); - let initialSelectedChannelsRefs: number[] = []; // Default to channel 1 if no saved channels are found + let initialSelectedChannelsRefs: number[] = [1]; // Default to channel 1 // Get the saved channels for the device initialSelectedChannelsRefs = [1]; // Load saved channels or default to [1] - - // Set the channels back to saved values setSelectedChannels(initialSelectedChannelsRefs); // Reset to saved channels } @@ -252,9 +281,11 @@ const NPG_Ble = () => { useEffect(() => { createCanvasElements(); setRefresh(r => r + 1); - }, [numChannels, theme, timeBase, selectedChannels, Zoom, isConnected]); + }, [deviceConfig.maxChannels, theme, timeBase, selectedChannels, Zoom, isConnected, refreshKey]); + useEffect(() => { selectedChannelsRef.current = selectedChannels; + canvasElementCountRef.current = selectedChannels.length; }, [selectedChannels]); //filters @@ -317,9 +348,11 @@ const NPG_Ble = () => { }); forceUpdate(); // Trigger re-render }; + useEffect(() => { dataPointCountRef.current = (samplingrateref.current * timeBase); }, [timeBase]); + const zoomRef = useRef(Zoom); useEffect(() => { @@ -329,31 +362,109 @@ const NPG_Ble = () => { const SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b"; const DATA_CHAR_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8"; const CONTROL_CHAR_UUID = "0000ff01-0000-1000-8000-00805f9b34fb"; + const BATTERY_CHAR_UUID = "f633d0ec-46b4-43c1-a39f-1ca06d0602e1"; // Battery characteristic UUID - const SINGLE_SAMPLE_LEN = 7; // Each sample is 10 bytes - const BLOCK_COUNT = 10; // 10 samples batched per notification - const NEW_PACKET_LEN = SINGLE_SAMPLE_LEN * BLOCK_COUNT; // 100 bytes + let prevSampleCounter: number | null = null; + let channelData: number[] = []; + // Initialize filters with device config + const notchFiltersRef = useRef(Array.from({ length: deviceConfig.maxChannels }, () => new Notch())); + const exgFiltersRef = useRef(Array.from({ length: deviceConfig.maxChannels }, () => new EXGFilter())); + const pointoneFilterRef = useRef(Array.from({ length: deviceConfig.maxChannels }, () => new HighPassFilter())); + // Update filters when device config changes + useEffect(() => { + const config = deviceConfigRef.current; - let prevSampleCounter: number | null = null; - let channelData: number[] = []; - const notchFiltersRef = useRef(Array.from({ length: maxCanvasElementCountRef.current }, () => new Notch())); - const exgFiltersRef = useRef(Array.from({ length: maxCanvasElementCountRef.current }, () => new EXGFilter())); - const pointoneFilterRef = useRef(Array.from({ length: maxCanvasElementCountRef.current }, () => new HighPassFilter())); - notchFiltersRef.current.forEach((filter) => { - filter.setbits(samplingrateref.current); - }); - exgFiltersRef.current.forEach((filter) => { - filter.setbits("12", samplingrateref.current); - }); - pointoneFilterRef.current.forEach((filter) => { - filter.setSamplingRate(samplingrateref.current); - }); + notchFiltersRef.current = Array.from({ length: config.maxChannels }, () => new Notch()); + exgFiltersRef.current = Array.from({ length: config.maxChannels }, () => new EXGFilter()); + pointoneFilterRef.current = Array.from({ length: config.maxChannels }, () => new HighPassFilter()); + + notchFiltersRef.current.forEach((filter) => { + filter.setbits(samplingrateref.current); + }); + exgFiltersRef.current.forEach((filter) => { + filter.setbits("12", samplingrateref.current); + }); + pointoneFilterRef.current.forEach((filter) => { + filter.setSamplingRate(samplingrateref.current); + }); + }, [deviceConfig]); + + // Function to update device configuration based on name + const updateDeviceConfiguration = (name: string) => { + let newConfig: DeviceConfig; + + if (name.includes("3CH")) { + newConfig = { + maxChannels: 3, + sampleLength: 7, // 3 channels * 2 bytes + 1 byte counter = 7 bytes + hasBattery: true, + name + }; + console.log("3CH device connected"); + } else if (name.includes("6CH")) { + newConfig = { + maxChannels: 6, + sampleLength: 13, // 6 channels * 2 bytes + 1 byte counter = 13 bytes + hasBattery: true, + name + }; + console.log("6CH device connected"); + console.log(newConfig); + } else { + newConfig = { + maxChannels: 3, + sampleLength: 7, + hasBattery: false, + name + }; + console.log("Unknown device, defaulting to 3CH configuration"); + } + + // Update both ref and state + deviceConfigRef.current = newConfig; + setDeviceConfig(newConfig); + setDeviceName(name); + + // Reinitialize filters with new channel count + notchFiltersRef.current = Array.from({ length: newConfig.maxChannels }, () => new Notch()); + exgFiltersRef.current = Array.from({ length: newConfig.maxChannels }, () => new EXGFilter()); + pointoneFilterRef.current = Array.from({ length: newConfig.maxChannels }, () => new HighPassFilter()); + + // Initialize filters + notchFiltersRef.current.forEach((filter) => { + filter.setbits(samplingrateref.current); + }); + exgFiltersRef.current.forEach((filter) => { + filter.setbits("12", samplingrateref.current); + }); + pointoneFilterRef.current.forEach((filter) => { + filter.setSamplingRate(samplingrateref.current); + }); + + // Reset UI states + setSelectedChannels([1]); + setManuallySelected(false); + setBatteryLevel(null); + + // Force re-render to update UI with new channel count + setRefreshKey(prev => prev + 1); + }; + + const BLOCK_COUNT = 10; // 10 samples batched per notification + // Dynamic packet length based on current device config + const packetLengthRef = useRef(0); + useEffect(() => { + packetLengthRef.current = deviceConfigRef.current.sampleLength * BLOCK_COUNT; + }, [deviceConfig]); // Inside your component const processSample = useCallback((dataView: DataView): void => { - if (dataView.byteLength !== SINGLE_SAMPLE_LEN) { + const config = deviceConfigRef.current; + console.log(config); + + if (dataView.byteLength !== config.sampleLength) { console.log("Unexpected sample length: " + dataView.byteLength); return; } @@ -372,11 +483,16 @@ const NPG_Ble = () => { channelData.push(sampleCounter); - for (let channel = 0; channel < numChannels; channel++) { + // Process the correct number of channels based on device configuration + + for (let channel = 0; channel < config.maxChannels; channel++) { const sample = dataView.getInt16(1 + (channel * 2), false); channelData.push( notchFiltersRef.current[channel].process( - exgFiltersRef.current[channel].process(pointoneFilterRef.current[channel].process(sample), appliedEXGFiltersRef.current[channel]), + exgFiltersRef.current[channel].process( + pointoneFilterRef.current[channel].process(sample), + appliedEXGFiltersRef.current[channel] + ), appliedFiltersRef.current[channel] ) ); @@ -412,7 +528,7 @@ const NPG_Ble = () => { channelData = []; samplesReceivedRef.current += 1; }, [ - canvasElementCountRef.current, selectedChannels, timeBase + canvasElementCountRef.current, selectedChannels, timeBase, deviceConfig ]); interface BluetoothRemoteGATTCharacteristicExtended extends EventTarget { @@ -425,62 +541,98 @@ const NPG_Ble = () => { console.log("Received event with no value."); return; } - if (currentSweepPos.current.length !== numChannels || !pauseRef.current) { - currentSweepPos.current = new Array(numChannels).fill(0); - sweepPositions.current = new Array(numChannels).fill(0); + + const config = deviceConfigRef.current; + console.log(config); + const currentPacketLength = packetLengthRef.current; + + console.log(config); + console.log("Current packet length:", currentPacketLength); if (currentSweepPos.current.length !== config.maxChannels || !pauseRef.current) { + currentSweepPos.current = new Array(config.maxChannels).fill(0); + sweepPositions.current = new Array(config.maxChannels).fill(0); } const value = target.value; - if (value.byteLength === NEW_PACKET_LEN) { - for (let i = 0; i < NEW_PACKET_LEN; i += SINGLE_SAMPLE_LEN) { - const sampleBuffer = value.buffer.slice(i, i + SINGLE_SAMPLE_LEN); + if (value.byteLength === currentPacketLength) { + for (let i = 0; i < currentPacketLength; i += config.sampleLength) { + const sampleBuffer = value.buffer.slice(i, i + config.sampleLength); const sampleDataView = new DataView(sampleBuffer); processSample(sampleDataView); } - } else if (value.byteLength === SINGLE_SAMPLE_LEN) { + } else if (value.byteLength === config.sampleLength) { processSample(new DataView(value.buffer)); } else { console.log("Unexpected packet length: " + value.byteLength); } } + const connectedDeviceRef = useRef(null); // UseRef for device tracking + const batteryCharacteristicRef = useRef(null); // Ref for battery characteristic + async function connectBLE(): Promise { try { - setIsLoading(true); const nav = navigator as any; if (!nav.bluetooth) { console.log("Web Bluetooth API is not available in this browser."); + setIsLoading(false); return; } + const device = await nav.bluetooth.requestDevice({ filters: [{ namePrefix: "NPG" }], optionalServices: [SERVICE_UUID], }); + + // Update configuration based on device name + updateDeviceConfiguration(device.name || ""); + const server = await device.gatt?.connect(); if (!server) { + setIsLoading(false); return; } + connectedDeviceRef.current = device; const service = await server.getPrimaryService(SERVICE_UUID); const controlChar = await service.getCharacteristic(CONTROL_CHAR_UUID); const dataChar = await service.getCharacteristic(DATA_CHAR_UUID); + + // Try to get battery characteristic if device supports it + if (deviceConfigRef.current.hasBattery) { + try { + const batteryChar = await service.getCharacteristic(BATTERY_CHAR_UUID); + batteryCharacteristicRef.current = batteryChar; + await batteryChar.startNotifications(); + + batteryChar.addEventListener("characteristicvaluechanged", (event: any) => { + const target = event.target as BluetoothRemoteGATTCharacteristicExtended; + if (target.value && target.value.byteLength === 1) { + const batteryValue = target.value.getUint8(0); + setBatteryLevel(batteryValue); + } + }); + } catch (error) { + console.log("Battery characteristic not available:", error); + } + } + const encoder = new TextEncoder(); await controlChar.writeValue(encoder.encode("START")); await dataChar.startNotifications(); dataChar.addEventListener("characteristicvaluechanged", handleNotification); setIsConnected(true); + setIsLoading(false); + setInterval(() => { if (samplesReceivedRef.current === 0) { disconnect(); - window.location.reload(); } samplesReceivedRef.current = 0; }, 1000); } catch (error) { console.log("Error: " + (error instanceof Error ? error.message : error)); setIsLoading(false); - } } @@ -496,10 +648,11 @@ const NPG_Ble = () => { return; } - if (!server.connected) { connectedDeviceRef.current = null; setIsConnected(false); + setBatteryLevel(null); + setDeviceName(""); return; } @@ -508,10 +661,23 @@ const NPG_Ble = () => { await dataChar.stopNotifications(); dataChar.removeEventListener("characteristicvaluechanged", handleNotification); + // Stop battery notifications if enabled + if (batteryCharacteristicRef.current) { + await batteryCharacteristicRef.current.stopNotifications(); + batteryCharacteristicRef.current = null; + } + server.disconnect(); // Disconnect the device connectedDeviceRef.current = null; // Clear the global reference setIsConnected(false); + setBatteryLevel(null); + setDeviceName(""); + + // Reset device configuration to defaults + deviceConfigRef.current = defaultConfig; + setDeviceConfig(defaultConfig); + setRefreshKey(prev => prev + 1); } catch (error) { console.log("Error during disconnection: " + (error instanceof Error ? error.message : error)); } @@ -527,10 +693,6 @@ const NPG_Ble = () => { } }; - useEffect(() => { - canvasElementCountRef.current = selectedChannels.length; - }, [selectedChannels]); - const setSelectedChannelsInWorker = (selectedChannels: number[]) => { if (!workerRef.current) { initializeWorker(); @@ -704,6 +866,7 @@ const NPG_Ble = () => { currentFileNameRef.current = filename; } }; + const stopRecording = async () => { if (!recordingStartTimeRef.current) { toast.error("Recording start time was not captured."); @@ -723,6 +886,7 @@ const NPG_Ble = () => { // Call fetchData after stopping the recording fetchData(); }; + const getFileCountFromIndexedDB = async (): Promise => { if (!workerRef.current) { initializeWorker(); @@ -767,6 +931,7 @@ const NPG_Ble = () => { // Clear the input field after handling setCustomTimeInput(""); }; + const formatTime = (milliseconds: number): string => { const date = new Date(milliseconds); const hours = String(date.getUTCHours()).padStart(2, '0'); @@ -777,7 +942,7 @@ const NPG_Ble = () => { useEffect(() => { - const enabledChannels = Array.from({ length: maxCanvasElementCountRef.current }, (_, i) => i + 1); + const enabledChannels = Array.from({ length: deviceConfig.maxChannels }, (_, i) => i + 1); const allSelected = selectedChannels.length === enabledChannels.length; const onlyOneLeft = selectedChannels.length === enabledChannels.length - 1; @@ -786,10 +951,11 @@ const NPG_Ble = () => { // Update the "Select All" button state setIsAllEnabledChannelSelected(allSelected); - }, [selectedChannels, maxCanvasElementCountRef.current, manuallySelected]); + }, [selectedChannels, deviceConfig.maxChannels, manuallySelected, refreshKey]); const toggleChannel = (channelIndex: number) => { setSelectedChannels((prevSelected) => { + setManuallySelected(true); const updatedChannels = prevSelected.includes(channelIndex) ? prevSelected.filter((ch) => ch !== channelIndex) : [...prevSelected, channelIndex]; @@ -802,12 +968,8 @@ const NPG_Ble = () => { return sortedChannels; }); - - setManuallySelected(true); }; - - const updatePlots = useCallback( (data: number[], Zoom: number) => { // Access the latest selectedChannels via the ref @@ -829,6 +991,7 @@ const NPG_Ble = () => { console.warn(`WebglPlot instance at index ${index} is undefined.`); } }); + linesRef.current.forEach((line, i) => { if (!line) { console.warn(`Line at index ${i} is undefined.`); @@ -844,7 +1007,6 @@ const NPG_Ble = () => { const channelData = data[channelNumber]; - // Ensure sweepPositions.current[i] is initialized if (sweepPositions.current[i] === undefined) { sweepPositions.current[i] = 0; @@ -904,6 +1066,24 @@ const NPG_Ble = () => { }, [animate, Zoom]); + // Function to get battery icon based on level + const getBatteryIcon = (level: number | null) => { + if (level === null) return null; + + if (level <= 20) return ; + if (level <= 50) return ; + if (level <= 80) return ; + return ; + }; + + // Function to get battery color based on level + const getBatteryColor = (level: number | null) => { + if (level === null) return "text-gray-500"; + + if (level <= 20) return "text-red-500"; + if (level <= 50) return "text-yellow-500"; + return "text-green-500"; + }; return ( @@ -911,7 +1091,8 @@ const NPG_Ble = () => {
-
+
@@ -989,6 +1170,8 @@ const NPG_Ble = () => { )} + + {/* Center-aligned buttons */} @@ -1029,6 +1212,7 @@ const NPG_Ble = () => { +
@@ -1172,7 +1356,7 @@ const NPG_Ble = () => {
diff --git a/tsconfig.json b/tsconfig.json index f381505..2ca745a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { @@ -42,4 +42,4 @@ "exclude": [ "node_modules" ] -} +} From a7e8cc04058fae0224790e882512034873ee128b Mon Sep 17 00:00:00 2001 From: Aman Maheshwari Date: Mon, 9 Feb 2026 15:43:12 +0530 Subject: [PATCH 2/4] Resolve infinite disconnect loop in BLE reconnection and add battery toast --- src/app/npg-lite/page.tsx | 249 +++++++++++++++++++++++++++++++------- tailwind.config.ts | 6 + 2 files changed, 212 insertions(+), 43 deletions(-) diff --git a/src/app/npg-lite/page.tsx b/src/app/npg-lite/page.tsx index f1f6e34..e7a9154 100644 --- a/src/app/npg-lite/page.tsx +++ b/src/app/npg-lite/page.tsx @@ -44,10 +44,10 @@ import { Settings, Loader, Battery, - BatteryCharging, BatteryLow, BatteryMedium, - BatteryFull + BatteryFull, + BatteryWarning } from "lucide-react"; import { lightThemeColors, darkThemeColors, getCustomColor } from '@/components/Colors'; import { useTheme } from "next-themes"; @@ -69,6 +69,7 @@ const defaultConfig: DeviceConfig = { const NPG_Ble = () => { const isRecordingRef = useRef(false); // Ref to track if the device is recording + const isOldfirmwareRef = useRef(false); // Ref to track if the device has old firmware const [isDisplay, setIsDisplay] = useState(true); // Display state const [isRecord, setIsrecord] = useState(true); // Display state const [isEndTimePopoverOpen, setIsEndTimePopoverOpen] = useState(false); @@ -402,6 +403,7 @@ const NPG_Ble = () => { hasBattery: true, name }; + isOldfirmwareRef.current = false; // Mark as new firmware if 3CH device is detected console.log("3CH device connected"); } else if (name.includes("6CH")) { newConfig = { @@ -411,6 +413,7 @@ const NPG_Ble = () => { name }; console.log("6CH device connected"); + isOldfirmwareRef.current = false; // Mark as new firmware if 6CH device is detected console.log(newConfig); } else { newConfig = { @@ -420,6 +423,7 @@ const NPG_Ble = () => { name }; console.log("Unknown device, defaulting to 3CH configuration"); + isOldfirmwareRef.current = true; // Mark as old firmware if device name doesn't match known patterns } // Update both ref and state @@ -569,6 +573,9 @@ const NPG_Ble = () => { const connectedDeviceRef = useRef(null); // UseRef for device tracking const batteryCharacteristicRef = useRef(null); // Ref for battery characteristic + const disconnectIntervalRef = useRef(null); + + // Update the connectBLE function async function connectBLE(): Promise { try { setIsLoading(true); @@ -624,8 +631,21 @@ const NPG_Ble = () => { setIsConnected(true); setIsLoading(false); - setInterval(() => { + // Clear any existing interval first + if (disconnectIntervalRef.current) { + clearInterval(disconnectIntervalRef.current); + disconnectIntervalRef.current = null; + } + + // Create new interval for monitoring samples + disconnectIntervalRef.current = setInterval(() => { if (samplesReceivedRef.current === 0) { + console.log("No samples received in 1 second, disconnecting..."); + // Clear the interval before calling disconnect + if (disconnectIntervalRef.current) { + clearInterval(disconnectIntervalRef.current); + disconnectIntervalRef.current = null; + } disconnect(); } samplesReceivedRef.current = 0; @@ -636,50 +656,117 @@ const NPG_Ble = () => { } } + async function disconnect(): Promise { try { + setIsLoading(true); // Show loading while disconnecting + if (disconnectIntervalRef.current) { + clearInterval(disconnectIntervalRef.current); + disconnectIntervalRef.current = null; + } + + // Clear any existing timeouts/intervals + if (samplesReceivedRef.current > 0) { + // Clear the interval that checks for samples + const checkInterval = setInterval(() => { }); + clearInterval(checkInterval); + } + if (!connectedDeviceRef.current) { console.log("No connected device to disconnect."); + setIsLoading(false); return; } const server = connectedDeviceRef.current.gatt; if (!server) { + setIsLoading(false); return; } - if (!server.connected) { + try { + // Only try to stop notifications if server is connected + if (server.connected) { + const service = await server.getPrimaryService(SERVICE_UUID); + + // Stop data notifications + try { + const dataChar = await service.getCharacteristic(DATA_CHAR_UUID); + await dataChar.stopNotifications(); + dataChar.removeEventListener("characteristicvaluechanged", handleNotification); + } catch (error) { + console.log("Error stopping data notifications:", error); + } + + // Stop battery notifications if enabled + if (batteryCharacteristicRef.current) { + try { + await batteryCharacteristicRef.current.stopNotifications(); + batteryCharacteristicRef.current.removeEventListener("characteristicvaluechanged", handleBatteryUpdate); + } catch (error) { + console.log("Error stopping battery notifications:", error); + } + batteryCharacteristicRef.current = null; + } + + // Send stop command if control characteristic exists + try { + const controlChar = await service.getCharacteristic(CONTROL_CHAR_UUID); + const encoder = new TextEncoder(); + await controlChar.writeValue(encoder.encode("STOP")); + } catch (error) { + console.log("Error sending STOP command:", error); + } + + // Disconnect from device + server.disconnect(); + } + } catch (error) { + console.log("Error during cleanup:", error); + } finally { + // Reset all states and refs connectedDeviceRef.current = null; setIsConnected(false); setBatteryLevel(null); setDeviceName(""); - return; - } - const service = await server.getPrimaryService(SERVICE_UUID); - const dataChar = await service.getCharacteristic(DATA_CHAR_UUID); - await dataChar.stopNotifications(); - dataChar.removeEventListener("characteristicvaluechanged", handleNotification); + // Reset device configuration to defaults + deviceConfigRef.current = defaultConfig; + setDeviceConfig(defaultConfig); - // Stop battery notifications if enabled - if (batteryCharacteristicRef.current) { - await batteryCharacteristicRef.current.stopNotifications(); - batteryCharacteristicRef.current = null; - } + // Reset sample tracking + prevSampleCounter = null; + channelData = []; + samplesReceivedRef.current = 0; + + // Reset recording state + isRecordingRef.current = false; + setIsrecord(true); + setRecordingElapsedTime(0); + + // Reset UI states + setIsDisplay(true); + pauseRef.current = true; - server.disconnect(); // Disconnect the device + // Clear buffers + recordingBuffers.forEach(buffer => buffer.length = 0); + activeBufferIndex = 0; + fillingindex.current = 0; - connectedDeviceRef.current = null; // Clear the global reference - setIsConnected(false); - setBatteryLevel(null); - setDeviceName(""); + // Clear lines and sweep positions + linesRef.current = []; + sweepPositions.current = new Array(6).fill(0); + currentSweepPos.current = new Array(6).fill(0); - // Reset device configuration to defaults - deviceConfigRef.current = defaultConfig; - setDeviceConfig(defaultConfig); - setRefreshKey(prev => prev + 1); + // Force re-render + setRefreshKey(prev => prev + 1); + + setIsLoading(false); + console.log("Disconnected successfully"); + } } catch (error) { console.log("Error during disconnection: " + (error instanceof Error ? error.message : error)); + setIsLoading(false); } } @@ -1068,24 +1155,42 @@ const NPG_Ble = () => { // Function to get battery icon based on level const getBatteryIcon = (level: number | null) => { - if (level === null) return null; - - if (level <= 20) return ; - if (level <= 50) return ; - if (level <= 80) return ; - return ; + if (level === null) return ; + + if (level < 10) return ; + if (level <= 20.0 && level > 10.0) return ; + if (level <= 70.0 && level > 20.0) return ; + if (level > 70) return ; + return ; }; + useEffect(() => { + if (batteryLevel === null) return; + if (batteryLevel < 10) { + toast.error("Very low battery! Please recharge immediately."); + } else if (batteryLevel === 20) { + toast.warning("Battery is low at " + batteryLevel + "%. Consider recharging soon."); + } else if (batteryLevel === 70) { + toast.success("Battery level is " + batteryLevel + "%."); + } else if (batteryLevel === 99) { + toast.success("Battery fully charged."); + } + + }, [batteryLevel]); + // Function to get battery color based on level const getBatteryColor = (level: number | null) => { - if (level === null) return "text-gray-500"; + if (level === null) return "text-red-500"; - if (level <= 20) return "text-red-500"; - if (level <= 50) return "text-yellow-500"; + if (level <= 20.0 && level > 10.0) return "text-red-500"; + if (level <= 70.0 && level > 20.0) return "text-orange-500"; + if (level > 70) return "text-green-500"; return "text-green-500"; }; - return (
@@ -1568,11 +1673,11 @@ const NPG_Ble = () => { - - +
{/* Channel Selection */} @@ -1731,14 +1836,72 @@ const NPG_Ble = () => { {/* Battery display when connected and has battery */} - {isConnected && deviceConfig.hasBattery && batteryLevel !== null && ( -
-
+ + + +
-
- )} + + + + + + + {!isConnected ? ( +
+

Device Not Connected!

+

Please connect your device to view battery status.

+
+ ) : ( + <> + {isOldfirmwareRef.current ? ( +
+

Old Firmware Detected

+ +

+ Update firmware using + NPG Lite Flasher + . +

+
+ ) : ( + <> + {batteryLevel === null ? ( +
+

Calibrating...

+
+ ) : ( +
+
+ {getBatteryIcon(batteryLevel)} + {batteryLevel}% +
+ + {batteryLevel < 10 && ( + + Low Battery + + )} +
+ )} + + )} + + )} + + +
+ +
diff --git a/tailwind.config.ts b/tailwind.config.ts index d7a802a..2228735 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -76,6 +76,10 @@ const config = { sm: "calc(var(--radius) - 4px)", }, keyframes: { + blink: { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '0.5' }, + }, "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, @@ -101,6 +105,8 @@ const config = { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", "typewriter": 'typewriter 5s steps(50) 1s infinite forwards, infinite', + 'blink': 'blink 1s ease-in-out infinite', + 'pulse-fast': 'pulse 0.5s ease-in-out infinite', }, }, }, From edddfcc6784f20936d184f14025976ed0fb877c6 Mon Sep 17 00:00:00 2001 From: Aman Maheshwari Date: Mon, 9 Feb 2026 15:59:13 +0530 Subject: [PATCH 3/4] Implement recurring low battery notifications --- src/app/npg-lite/page.tsx | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/app/npg-lite/page.tsx b/src/app/npg-lite/page.tsx index e7a9154..9eda778 100644 --- a/src/app/npg-lite/page.tsx +++ b/src/app/npg-lite/page.tsx @@ -698,17 +698,6 @@ const NPG_Ble = () => { console.log("Error stopping data notifications:", error); } - // Stop battery notifications if enabled - if (batteryCharacteristicRef.current) { - try { - await batteryCharacteristicRef.current.stopNotifications(); - batteryCharacteristicRef.current.removeEventListener("characteristicvaluechanged", handleBatteryUpdate); - } catch (error) { - console.log("Error stopping battery notifications:", error); - } - batteryCharacteristicRef.current = null; - } - // Send stop command if control characteristic exists try { const controlChar = await service.getCharacteristic(CONTROL_CHAR_UUID); @@ -1157,7 +1146,7 @@ const NPG_Ble = () => { const getBatteryIcon = (level: number | null) => { if (level === null) return ; - if (level < 10) return ; @@ -1169,8 +1158,19 @@ const NPG_Ble = () => { useEffect(() => { if (batteryLevel === null) return; - if (batteryLevel < 10) { + + let intervalId: number | undefined; + + // Handle battery level checks + if (batteryLevel <= 10) { + // Show immediate notification toast.error("Very low battery! Please recharge immediately."); + + // Set up interval to show notification every minute + intervalId = window.setInterval(() => { + toast.error("Very low battery! Please recharge immediately."); + }, 60000); // 60000ms = 1 minute + } else if (batteryLevel === 20) { toast.warning("Battery is low at " + batteryLevel + "%. Consider recharging soon."); } else if (batteryLevel === 70) { @@ -1179,6 +1179,13 @@ const NPG_Ble = () => { toast.success("Battery fully charged."); } + // Cleanup function to clear interval when batteryLevel changes or component unmounts + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + }, [batteryLevel]); // Function to get battery color based on level From 0eac4b7e01009efa2f7ec313297ca9174124cbb4 Mon Sep 17 00:00:00 2001 From: Aman Maheshwari Date: Mon, 9 Feb 2026 16:03:16 +0530 Subject: [PATCH 4/4] WIP --- src/app/npg-lite/page.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/app/npg-lite/page.tsx b/src/app/npg-lite/page.tsx index 9eda778..6267f48 100644 --- a/src/app/npg-lite/page.tsx +++ b/src/app/npg-lite/page.tsx @@ -404,7 +404,6 @@ const NPG_Ble = () => { name }; isOldfirmwareRef.current = false; // Mark as new firmware if 3CH device is detected - console.log("3CH device connected"); } else if (name.includes("6CH")) { newConfig = { maxChannels: 6, @@ -412,9 +411,7 @@ const NPG_Ble = () => { hasBattery: true, name }; - console.log("6CH device connected"); isOldfirmwareRef.current = false; // Mark as new firmware if 6CH device is detected - console.log(newConfig); } else { newConfig = { maxChannels: 3, @@ -422,7 +419,6 @@ const NPG_Ble = () => { hasBattery: false, name }; - console.log("Unknown device, defaulting to 3CH configuration"); isOldfirmwareRef.current = true; // Mark as old firmware if device name doesn't match known patterns } @@ -466,7 +462,6 @@ const NPG_Ble = () => { // Inside your component const processSample = useCallback((dataView: DataView): void => { const config = deviceConfigRef.current; - console.log(config); if (dataView.byteLength !== config.sampleLength) { console.log("Unexpected sample length: " + dataView.byteLength); @@ -547,15 +542,8 @@ const NPG_Ble = () => { } const config = deviceConfigRef.current; - console.log(config); const currentPacketLength = packetLengthRef.current; - console.log(config); - console.log("Current packet length:", currentPacketLength); if (currentSweepPos.current.length !== config.maxChannels || !pauseRef.current) { - currentSweepPos.current = new Array(config.maxChannels).fill(0); - sweepPositions.current = new Array(config.maxChannels).fill(0); - } - const value = target.value; if (value.byteLength === currentPacketLength) { for (let i = 0; i < currentPacketLength; i += config.sampleLength) { @@ -751,7 +739,6 @@ const NPG_Ble = () => { setRefreshKey(prev => prev + 1); setIsLoading(false); - console.log("Disconnected successfully"); } } catch (error) { console.log("Error during disconnection: " + (error instanceof Error ? error.message : error));