@@ -4,6 +4,7 @@ import { useEffect, useState, useRef, useCallback } from "react";
44import { WebglPlot , WebglLine , ColorRGBA } from "webgl-plot" ;
55import { Button } from "@/components/ui/button" ;
66import Navbar from "@/components/Navbar" ;
7+ import { toast } from "sonner" ;
78
89interface DataPoint {
910 time : number ;
@@ -36,24 +37,43 @@ const SerialPlotter = () => {
3637 const bitsref = useRef < number > ( 10 ) ;
3738 const channelsref = useRef < number > ( 1 ) ;
3839 const sweepPositions = useRef < number [ ] > ( new Array ( channelsref . current ) . fill ( 0 ) ) ; // Array for sweep positions
39- const [ isConnecting , setIsConnecting ] = useState ( false ) ;
4040
4141 useEffect ( ( ) => {
4242 if ( rawDataRef . current ) {
4343 rawDataRef . current . scrollTop = rawDataRef . current . scrollHeight ;
4444 }
45- } , [ rawData ] ) ;
45+ } , [ rawData ] ) ; // Runs when rawData updates
4646
4747 const maxRawDataLines = 1000 ; // Limit for raw data lines
4848
49- // ✅ RE-INITIALIZE WebGL when selectedChannels updates
49+ function testWebGLShaderSupport ( gl : WebGLRenderingContext ) {
50+ const vertexShader = gl . createShader ( gl . VERTEX_SHADER ) ;
51+ if ( ! vertexShader ) {
52+ console . error ( "Failed to create vertex shader" ) ;
53+ return false ;
54+ }
55+ gl . shaderSource ( vertexShader , "attribute vec4 position; void main() { gl_Position = position; }" ) ;
56+ gl . compileShader ( vertexShader ) ;
57+ if ( ! gl . getShaderParameter ( vertexShader , gl . COMPILE_STATUS ) ) {
58+ console . error ( "WebGL shader compilation failed:" , gl . getShaderInfoLog ( vertexShader ) ) ;
59+ return false ;
60+ }
61+ return true ;
62+ }
63+
5064 useEffect ( ( ) => {
5165 if ( ! canvasRef . current || selectedChannels . length === 0 ) return ;
5266
5367 const canvas = canvasRef . current ;
5468 canvas . width = canvas . clientWidth ;
5569 canvas . height = canvas . clientHeight ;
5670
71+ const gl = canvas . getContext ( "webgl" ) ;
72+ if ( ! gl || ! testWebGLShaderSupport ( gl ) ) {
73+ console . error ( "WebGL shader support check failed." ) ;
74+ return ;
75+ }
76+
5777 const wglp = new WebglPlot ( canvas ) ;
5878 wglpRef . current = wglp ;
5979
@@ -68,7 +88,8 @@ const SerialPlotter = () => {
6888 } ) ;
6989
7090 wglp . update ( ) ;
71- } , [ selectedChannels ] ) ; // ✅ Runs when channels are detected
91+ } , [ selectedChannels ] ) ;
92+
7293
7394 useEffect ( ( ) => {
7495 if ( ! canvasRef . current ) return ;
@@ -108,7 +129,6 @@ const SerialPlotter = () => {
108129 } ;
109130
110131 const connectToSerial = useCallback ( async ( ) => {
111- setIsConnecting ( true ) ; // Start showing "Connecting..."
112132 try {
113133 const ports = await ( navigator as any ) . serial . getPorts ( ) ;
114134 let selectedPort = ports . length > 0 ? ports [ 0 ] : null ;
@@ -118,9 +138,10 @@ const SerialPlotter = () => {
118138 }
119139
120140 await selectedPort . open ( { baudRate : baudRateref . current } ) ;
141+ setRawData ( "" ) ;
142+ setData ( [ ] ) ;
121143 setPort ( selectedPort ) ;
122144 setIsConnected ( true ) ;
123- setRawData ( "" ) ;
124145 wglpRef . current = null ;
125146 linesRef . current = [ ] ;
126147 selectedChannelsRef . current = [ ] ;
@@ -129,15 +150,16 @@ const SerialPlotter = () => {
129150 setTimeout ( ( ) => {
130151 sweepPositions . current = new Array ( 6 ) . fill ( 0 ) ;
131152 setShowPlotterData ( true ) ; // Show plotted data after 4 seconds
132- setIsConnecting ( false ) ; // Done "connecting"
133153 } , 4000 ) ;
134154 } catch ( err ) {
135155 console . error ( "Error connecting to serial:" , err ) ;
136- setIsConnecting ( false ) ;
137156 }
138157 } , [ baudRateref . current , setPort , setIsConnected , setRawData , wglpRef , linesRef ] ) ;
139158
140159 const readSerialData = async ( serialPort : SerialPort ) => {
160+ const READ_TIMEOUT = 5000 ;
161+ const BATCH_SIZE = 10 ;
162+
141163 try {
142164 const serialReader = serialPort . readable ?. getReader ( ) ;
143165 if ( ! serialReader ) return ;
@@ -146,85 +168,110 @@ const SerialPlotter = () => {
146168 let buffer = "" ;
147169 let receivedData = false ;
148170
149- // Timeout: If no data in 3 sec, show command input
150- setTimeout ( ( ) => {
171+ const timeoutId = setTimeout ( ( ) => {
151172 if ( ! receivedData ) {
152173 setShowCommandInput ( true ) ;
174+ console . warn ( "No data received within timeout period" ) ;
153175 }
154- } , 3000 ) ;
176+ } , READ_TIMEOUT ) ;
155177
156178 while ( true ) {
157- const { value, done } = await serialReader . read ( ) ;
158- if ( done ) break ;
159- if ( value ) {
160- receivedData = true ;
161- setShowCommandInput ( false ) ;
162-
163- buffer += new TextDecoder ( ) . decode ( value ) ;
164- const lines = buffer . split ( "\n" ) ;
165- buffer = lines . pop ( ) || "" ; // Store incomplete line for next read
166-
167- let newData : DataPoint [ ] = [ ] ;
168- lines . forEach ( ( line ) => {
169- setRawData ( ( prev ) => {
170- const newRawData = prev . split ( "\n" ) . concat ( line . trim ( ) . replace ( / \s + / g, " " ) ) ;
171- return newRawData . slice ( - maxRawDataLines ) . join ( "\n" ) ;
172- } ) ;
173-
174- // Detect Board Name
175- if ( line . includes ( "BOARD:" ) ) {
176- setBoardName ( line . split ( ":" ) [ 1 ] . trim ( ) ) ;
177- setShowCommandInput ( true ) ;
178- }
179-
180- // Convert to numeric data
181- const values = line . trim ( ) . split ( / \s + / ) . map ( parseFloat ) . filter ( ( v ) => ! isNaN ( v ) ) ;
182- if ( values . length > 0 ) {
183- newData . push ( { time : Date . now ( ) , values } ) ;
184- channelsref . current = values . length ;
185- // ✅ Ensure selectedChannels updates before plotting
186- setSelectedChannels ( ( prevChannels ) => {
187- if ( prevChannels . length !== values . length ) {
188- return Array . from ( { length : values . length } , ( _ , i ) => i ) ;
179+ try {
180+ const readPromise = serialReader . read ( ) ;
181+ const timeoutPromise = new Promise < never > ( ( _ , reject ) =>
182+ setTimeout ( ( ) => reject ( new Error ( "Read timeout" ) ) , READ_TIMEOUT )
183+ ) ;
184+
185+ const { value, done } = await Promise . race ( [ readPromise , timeoutPromise ] ) ;
186+ if ( done ) break ;
187+ if ( value ) {
188+ receivedData = true ;
189+ setShowCommandInput ( false ) ;
190+
191+ // Process data efficiently
192+ const decoder = new TextDecoder ( ) ;
193+ buffer += decoder . decode ( value , { stream : true } ) ;
194+ const lines = buffer . split ( "\n" ) ;
195+ buffer = lines . pop ( ) || "" ; // Store incomplete line for next read
196+
197+ let newData : DataPoint [ ] = [ ] ;
198+ for ( let i = 0 ; i < lines . length ; i += BATCH_SIZE ) {
199+ const batch = lines . slice ( i , i + BATCH_SIZE ) ;
200+ batch . forEach ( ( line ) => {
201+ setRawData ( ( prev ) => {
202+ const newRawData = prev . split ( "\n" ) . concat ( line . trim ( ) . replace ( / \s + / g, " " ) ) ;
203+ return newRawData . slice ( - maxRawDataLines ) . join ( "\n" ) ;
204+ } ) ;
205+
206+ // Detect Board Name
207+ if ( line . includes ( "BOARD:" ) ) {
208+ setBoardName ( line . split ( ":" ) [ 1 ] . trim ( ) ) ;
209+ setShowCommandInput ( true ) ;
189210 }
190211
191- return prevChannels ;
212+ // Convert to numeric data
213+ const values = line . trim ( ) . split ( / \s + / ) . map ( parseFloat ) . filter ( ( v ) => ! isNaN ( v ) ) ;
214+ if ( values . length > 0 ) {
215+ newData . push ( { time : Date . now ( ) , values } ) ;
216+ channelsref . current = values . length ;
217+
218+ setSelectedChannels ( ( prevChannels ) => {
219+ return prevChannels . length !== values . length
220+ ? Array . from ( { length : values . length } , ( _ , i ) => i )
221+ : prevChannels ;
222+ } ) ;
223+ }
192224 } ) ;
193225 }
194- } ) ;
195226
196- if ( newData . length > 0 ) {
197-
198- setData ( ( prev ) => [ ... prev , ... newData ] . slice ( - maxPoints ) ) ;
227+ if ( newData . length > 0 ) {
228+ setData ( ( prev ) => [ ... prev , ... newData ] . slice ( - maxPoints ) ) ;
229+ }
199230 }
231+ } catch ( error ) {
232+ console . error ( "Error reading serial data chunk:" , error ) ;
233+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ; // Short delay before retry
234+ continue ;
200235 }
201236 }
237+
238+ clearTimeout ( timeoutId ) ;
202239 serialReader . releaseLock ( ) ;
203240 } catch ( err ) {
204241 console . error ( "Error reading serial data:" , err ) ;
242+
243+ // Attempt to reconnect if still connected
244+ setTimeout ( ( ) => {
245+ if ( isConnected ) {
246+ toast ( "Attempting to reconnect..." ) ;
247+ connectToSerial ( ) ;
248+ }
249+ } , 5000 ) ;
205250 }
206251 } ;
207252
253+
208254 useEffect ( ( ) => {
209255 let isMounted = true ;
256+ let animationFrameId : number ;
210257
211258 const animate = ( ) => {
212259 if ( ! isMounted ) return ;
213- requestAnimationFrame ( animate ) ;
214- requestAnimationFrame ( ( ) => {
215- if ( wglpRef . current ) {
216- wglpRef . current . update ( ) ;
217- }
218- } ) ;
260+ if ( wglpRef . current ) {
261+ wglpRef . current . update ( ) ;
262+ }
263+ animationFrameId = requestAnimationFrame ( animate ) ;
219264 } ;
220265
221- requestAnimationFrame ( animate ) ; // Ensure continuous updates
266+ animationFrameId = requestAnimationFrame ( animate ) ;
222267
223268 return ( ) => {
224269 isMounted = false ;
270+ cancelAnimationFrame ( animationFrameId ) ;
225271 } ;
226272 } , [ ] ) ;
227273
274+
228275 useEffect ( ( ) => {
229276 const checkPortStatus = async ( ) => {
230277 if ( port ) {
@@ -306,7 +353,7 @@ const SerialPlotter = () => {
306353 await port . close ( ) ;
307354 setPort ( null ) ;
308355 }
309-
356+ setData ( [ ] ) ;
310357 setIsConnected ( false ) ;
311358 setShowPlotterData ( false ) ; // Hide plotted data on disconnect
312359
@@ -327,6 +374,7 @@ const SerialPlotter = () => {
327374 connectToSerial ( ) ; // Reconnect with the new baud rate
328375 } , 500 ) ;
329376 } ;
377+
330378 const sendCommand = async ( ) => {
331379 if ( ! port ?. writable || ! command . trim ( ) ) return ;
332380
@@ -352,13 +400,7 @@ const SerialPlotter = () => {
352400 < div className = "border rounded-xl shadow-lg bg-[#1a1a2e] p-2 w-full h-full flex flex-col" >
353401 { /* Canvas Container */ }
354402 < div className = "canvas-container w-full h-full flex items-center justify-center overflow-hidden" >
355- { ( isConnecting ) ? (
356- < div className = "w-full h-full rounded-xl bg-gray-800 flex items-center justify-center text-white" >
357- Connecting...
358- </ div >
359- ) : (
360- < canvas ref = { canvasRef } className = "w-full h-full rounded-xl" />
361- ) }
403+ < canvas ref = { canvasRef } className = "w-full h-full rounded-xl" />
362404 </ div >
363405
364406 </ div >
@@ -382,31 +424,38 @@ const SerialPlotter = () => {
382424 type = "text"
383425 value = { command }
384426 onChange = { ( e ) => setCommand ( e . target . value ) }
427+ onKeyDown = { ( e ) => {
428+ if ( e . key === "Enter" ) {
429+ e . preventDefault ( ) ; // Prevent default behavior (e.g., form submission)
430+ sendCommand ( ) ; // Call the send function
431+ }
432+ } }
385433 placeholder = "Enter command"
386434 className = "w-full p-2 text-xs font-semibold rounded bg-gray-800 text-white border border-gray-600"
387- style = { { height : ' 36px' } } // Ensure the height is consistent with buttons
435+ style = { { height : " 36px" } } // Ensure the height is consistent with buttons
388436 />
389437
390438 { /* Buttons (Shifted Left) */ }
391439 < div className = "flex items-center space-x-2 mr-auto" >
392440 < Button
393441 onClick = { sendCommand }
394442 className = "px-4 py-2 text-xs font-semibold bg-gray-500 rounded shadow-md hover:bg-gray-500 transition ml-2"
395- style = { { height : ' 36px' } } // Set height equal to the input box
443+ style = { { height : " 36px" } } // Set height equal to the input box
396444 >
397445 Send
398446 </ Button >
399447 < button
400448 onClick = { ( ) => setRawData ( "" ) }
401449 className = "px-4 py-2 text-xs bg-red-600 text-white rounded shadow-md hover:bg-red-700 transition"
402- style = { { height : ' 36px' } } // Set height equal to the input box
450+ style = { { height : " 36px" } } // Set height equal to the input box
403451 >
404452 Clear
405453 </ button >
406454 </ div >
407455 </ div >
408456
409457
458+
410459 { /* Data Display */ }
411460 < pre className = "text-xs whitespace-pre-wrap break-words px-4 pb-4 flex-grow overflow-auto rounded-xl" >
412461 { rawData }
0 commit comments