@@ -31,9 +31,10 @@ const SerialPlotter = () => {
3131 const separateCanvasRefs = useRef < ( HTMLCanvasElement | null ) [ ] > ( Array ( maxChannels ) . fill ( null ) ) ;
3232 const separateWglpRefs = useRef < ( WebglPlot | null ) [ ] > ( Array ( maxChannels ) . fill ( null ) ) ;
3333 const separateLinesRefs = useRef < ( WebglLine | null ) [ ] > ( Array ( maxChannels ) . fill ( null ) ) ;
34- const [ awaitingCommand , setAwaitingCommand ] = useState ( false ) ;
35- const [ commandInput , setCommandInput ] = useState ( "whoru" ) ;
36-
34+ const [ showCommandInput , setShowCommandInput ] = useState ( false ) ;
35+ const [ command , setCommand ] = useState ( "" ) ;
36+ const [ boardName , setBoardName ] = useState < string | null > ( null ) ;
37+ const [ writer , setWriter ] = useState < WritableStreamDefaultWriter | null > ( null ) ;
3738
3839 useEffect ( ( ) => {
3940 if ( rawDataRef . current ) {
@@ -124,38 +125,50 @@ const SerialPlotter = () => {
124125 const serialReader = serialPort . readable ?. getReader ( ) ;
125126 if ( ! serialReader ) return ;
126127 setReader ( serialReader ) ;
128+
127129 let buffer = "" ;
130+ let receivedData = false ;
131+
132+ // Timeout: If no data in 3 sec, show command input
133+ setTimeout ( ( ) => {
134+ if ( ! receivedData ) {
135+ setShowCommandInput ( true ) ;
136+ }
137+ } , 3000 ) ;
128138
129139 while ( true ) {
130140 const { value, done } = await serialReader . read ( ) ;
131141 if ( done ) break ;
132142 if ( value ) {
143+ receivedData = true ;
144+ setShowCommandInput ( false ) ;
145+
133146 buffer += new TextDecoder ( ) . decode ( value ) ;
134147 const lines = buffer . split ( "\n" ) ;
135- buffer = lines . pop ( ) || "" ; // Keep any incomplete line for next read
148+ buffer = lines . pop ( ) || "" ; // Store incomplete line for next read
136149
137150 let newData : DataPoint [ ] = [ ] ;
138-
139151 lines . forEach ( ( line ) => {
140- // Store raw data for display
141152 setRawData ( ( prev ) => {
142153 const newRawData = prev . split ( "\n" ) . concat ( line . trim ( ) . replace ( / \s + / g, " " ) ) ;
143154 return newRawData . slice ( - maxRawDataLines ) . join ( "\n" ) ;
144155 } ) ;
145156
146- // Convert the line into an array of numbers
147- const values = line . trim ( ) . split ( / \s + / ) . map ( parseFloat ) . filter ( ( v ) => ! isNaN ( v ) ) ;
157+ if ( line . includes ( "BOARD:" ) ) {
158+ setBoardName ( line . split ( ":" ) [ 1 ] . trim ( ) ) ;
159+ setShowCommandInput ( true ) ; // Ensure input is still shown even after getting the board name
160+ }
148161
162+ // Convert data to numbers
163+ const values = line . trim ( ) . split ( / \s + / ) . map ( parseFloat ) . filter ( ( v ) => ! isNaN ( v ) ) ;
149164 if ( values . length > 0 ) {
150165 newData . push ( { time : Date . now ( ) , values } ) ;
151166
152- // Update the number of detected channels dynamically
153167 setSelectedChannels ( ( prevChannels ) => {
154168 const detectedChannels = values . length ;
155- if ( prevChannels . length !== detectedChannels ) {
156- return Array . from ( { length : detectedChannels } , ( _ , i ) => i ) ;
157- }
158- return prevChannels ;
169+ return prevChannels . length !== detectedChannels
170+ ? Array . from ( { length : detectedChannels } , ( _ , i ) => i )
171+ : prevChannels ;
159172 } ) ;
160173 }
161174 } ) ;
@@ -176,8 +189,6 @@ const SerialPlotter = () => {
176189 }
177190 } ;
178191
179-
180-
181192 useEffect ( ( ) => {
182193 if ( ! showSeparate ) {
183194 separateWglpRefs . current = Array ( maxChannels ) . fill ( null ) ; // Reset separate plots
@@ -188,7 +199,7 @@ const SerialPlotter = () => {
188199 const canvas = separateCanvasRefs . current [ index ] ;
189200 if ( canvas ) {
190201 canvas . width = canvas . clientWidth ;
191- canvas . height = 500 ;
202+ canvas . height = 100 ;
192203
193204 const wglp = new WebglPlot ( canvas ) ;
194205 separateWglpRefs . current [ index ] = wglp ;
@@ -202,6 +213,7 @@ const SerialPlotter = () => {
202213 }
203214 } ) ;
204215 } , [ selectedChannels , showSeparate ] ) ;
216+
205217 useEffect ( ( ) => {
206218 separateCanvasRefs . current = separateCanvasRefs . current . slice ( 0 , selectedChannels . length ) ;
207219 separateWglpRefs . current = separateWglpRefs . current . slice ( 0 , selectedChannels . length ) ;
@@ -303,96 +315,116 @@ const SerialPlotter = () => {
303315 setIsConnected ( false ) ;
304316 } ;
305317
306- return (
307- < div className = "w-full max-w-8xl mx-auto p-6 border rounded-2xl shadow-xl" >
308- < h1 className = "text-3xl font-bold text-center mb-6" > Chords Serial Plotter & Monitor </ h1 >
309-
310- < div className = "flex justify-center flex-wrap gap-4 mb-6" >
311- < Button onClick = { connectToSerial } disabled = { isConnected } className = "px-6 py-3 text-lg font-semibold" >
312- { isConnected ? "Connected" : "Connect Serial" }
313- </ Button >
314- < Button onClick = { disconnectSerial } disabled = { ! isConnected } className = "px-6 py-3 text-lg font-semibold" >
315- Disconnect
316- </ Button >
317- < label className = "flex items-center space-x-2 bg-gray-300 p-2 rounded" >
318- < Checkbox checked = { showCombined } onCheckedChange = { ( checked ) => setShowCombined ( ! ! checked ) } />
319- < span > Show Combined Graph</ span >
320- </ label >
321- < label className = "flex items-center space-x-2 bg-gray-300 p-2 rounded" >
322- < Checkbox checked = { showSeparate } onCheckedChange = { ( checked ) => setShowSeparate ( ! ! checked ) } />
323- < span > Show Separate Graphs</ span >
324- </ label >
325- </ div >
326-
327- { /* Zoom Control */ }
328- < div className = "w-full flex justify-center items-center mb-6 p-2" >
329- < label className = "mr-4" > Zoom:</ label >
330- < input
331- type = "range"
332- min = "0.1"
333- max = "5"
334- step = "0.1"
335- value = { zoomFactor }
336- className = "w-1/2"
337- onChange = { ( e ) => {
338- const newZoom = parseFloat ( e . target . value ) ;
339- setZoomFactor ( newZoom ) ;
340- linesRef . current . forEach ( ( line ) => {
341- if ( line ) line . scaleY = newZoom ;
342- } ) ;
343- separateLinesRefs . current . forEach ( ( line ) => {
344- if ( line ) line . scaleY = newZoom ;
345- } ) ;
346- wglpRef . current ?. update ( ) ;
347- separateWglpRefs . current . forEach ( ( wglp ) => wglp ?. update ( ) ) ;
348- } }
349- />
350- < span className = "ml-4" > { zoomFactor . toFixed ( 1 ) } x</ span >
351- </ div >
318+ const sendCommand = async ( ) => {
319+ if ( ! port ?. writable || ! command . trim ( ) ) return ;
320+
321+ try {
322+ const writer = port . writable . getWriter ( ) ; // Get writer
323+ await writer . write ( new TextEncoder ( ) . encode ( command + "\n" ) ) ;
324+ writer . releaseLock ( ) ; // Release writer after writing
352325
326+ } catch ( err ) {
327+ console . error ( "Error sending command:" , err ) ;
328+ }
329+ } ;
330+
331+ return (
332+ < div className = "w-full mx-auto p-4 border rounded-2xl shadow-xl flex flex-col gap-4 " >
333+ < h1 className = "text-2xl font-bold text-center" > Chords Serial Plotter & Monitor </ h1 >
334+
335+ < div className = "flex justify-center flex-wrap gap-2" >
336+ < Button onClick = { connectToSerial } disabled = { isConnected } className = "px-4 py-2 text-sm font-semibold" >
337+ { isConnected ? "Connected" : "Connect Serial" }
338+ </ Button >
339+ < Button onClick = { disconnectSerial } disabled = { ! isConnected } className = "px-4 py-2 text-sm font-semibold" >
340+ Disconnect
341+ </ Button >
342+ < label className = "flex items-center space-x-1 bg-gray-300 p-1 rounded" >
343+ < Checkbox checked = { showCombined } onCheckedChange = { ( checked ) => {
344+ setShowCombined ( ! ! checked ) ;
345+ setShowSeparate ( ! checked ) ;
346+ } } />
347+ < span className = "text-xs" > Show Combined</ span >
348+ </ label >
349+ < label className = "flex items-center space-x-1 bg-gray-300 p-1 rounded" >
350+ < Checkbox checked = { showSeparate } onCheckedChange = { ( checked ) => {
351+ setShowSeparate ( ! ! checked ) ;
352+ setShowCombined ( ! checked ) ;
353+ } } />
354+ < span className = "text-xs" > Show Separate</ span >
355+ </ label >
356+ </ div >
357+
358+ { /* Zoom Control */ }
359+ < div className = "w-full flex justify-center items-center p-1" >
360+ < label className = "mr-2 text-sm" > Zoom:</ label >
361+ < input
362+ type = "range"
363+ min = "0.1"
364+ max = "5"
365+ step = "0.1"
366+ value = { zoomFactor }
367+ className = "w-1/3"
368+ onChange = { ( e ) => {
369+ const newZoom = parseFloat ( e . target . value ) ;
370+ setZoomFactor ( newZoom ) ;
371+ linesRef . current . forEach ( ( line ) => {
372+ if ( line ) line . scaleY = newZoom ;
373+ } ) ;
374+ separateLinesRefs . current . forEach ( ( line ) => {
375+ if ( line ) line . scaleY = newZoom ;
376+ } ) ;
377+ wglpRef . current ?. update ( ) ;
378+ separateWglpRefs . current . forEach ( ( wglp ) => wglp ?. update ( ) ) ;
379+ } }
380+ />
381+ < span className = "ml-2 text-sm" > { zoomFactor . toFixed ( 1 ) } x</ span >
382+ </ div >
383+
384+ { /* Graph Container */ }
385+ < div className = "w-full gap-2"
386+ style = { { gridTemplateColumns : `repeat(${ selectedChannels . length >= 2 ? 2 : 1 } ` } } >
353387 { /* Combined Canvas */ }
354388 { showCombined && (
355- < div className = "w-full border rounded-xl shadow-lg bg-[#1a1a2e] p-4 mb-6 " >
356- < h2 className = "text-lg font-semibold text-center mb-2 text-white" > Combined Plot</ h2 >
357- < canvas ref = { canvasRef } className = "w-full h-[300px ] rounded-xl" />
389+ < div className = "border rounded-xl shadow-lg bg-[#1a1a2e] p-2 w-full " >
390+ < h2 className = "text-sm font-semibold text-center mb-1 text-white" > Combined Plot</ h2 >
391+ < canvas ref = { canvasRef } className = "w-full h-[150px ] rounded-xl" />
358392 </ div >
359393 ) }
360-
394+
361395 { /* Separate Canvases */ }
362- { showSeparate && (
363- < div className = { `grid gap-4 ${ selectedChannels . length % 2 === 1 ? "grid-cols-1" : "grid-cols-2" } ` } >
364- { selectedChannels . map ( ( index , i ) => (
365- < div
366- key = { index }
367- className = "border rounded-xl shadow-lg bg-[#1a1a2e] p-4 w-full"
368- style = { { gridColumn : selectedChannels . length % 2 === 1 && i === selectedChannels . length - 1 ? "span 2 w-10px" : "span 1" } }
369- >
370- < h2 className = "text-lg font-semibold text-center mb-2 text-white" >
371- Channel { index + 1 }
372- </ h2 >
373- < canvas
374- ref = { ( el ) => {
375- separateCanvasRefs . current [ index ] = el ;
376- } }
377- className = "w-full h-[300px] rounded-xl"
378- />
379- </ div >
380- ) ) }
396+ { showSeparate && selectedChannels . map ( ( index ) => (
397+ < div key = { index } className = "border rounded-xl shadow-lg bg-[#1a1a2e] p-2 w-full" >
398+ < h2 className = "text-sm font-semibold text-center mb-1 text-white" > Channel { index + 1 } </ h2 >
399+ < canvas ref = { ( el ) => { separateCanvasRefs . current [ index ] = el ; } }
400+ className = "w-full h-[100px] rounded-xl" />
401+ </ div >
402+ ) ) }
403+ </ div >
404+
405+ { /* Raw Data Output / Command Input */ }
406+ < div ref = { rawDataRef } className = "w-full py-2 border rounded-xl shadow-lg bg-[#1a1a2e] text-white overflow-auto h-40" >
407+ < h2 className = "text-sm font-semibold text-center mb-1" >
408+ { boardName ? `Connected to: ${ boardName } ` : "Raw Data Output" }
409+ </ h2 >
410+
411+ { showCommandInput ? (
412+ < div className = "flex items-center space-x-1 p-1" >
413+ < input
414+ type = "text"
415+ value = { command }
416+ onChange = { ( e ) => setCommand ( e . target . value ) }
417+ placeholder = "Enter command (WHORU, START)"
418+ className = "w-full p-1 rounded bg-gray-800 text-white border border-gray-600 text-xs"
419+ />
420+ < Button onClick = { sendCommand } className = "px-2 py-1 text-xs font-semibold" > Send</ Button >
381421 </ div >
422+ ) : (
423+ < pre className = "text-xs whitespace-pre-wrap break-words" > { rawData } </ pre >
382424 ) }
383-
384- { /* Raw Data Output */ }
385- < div
386- ref = { rawDataRef }
387- className = "w-full mt-6 py-4 border rounded-xl shadow-lg bg-[#1a1a2e] text-white overflow-auto h-40"
388- >
389- < h2 className = "text-xl font-semibold text-center mb-2" > Raw Data Output:</ h2 >
390- < pre className = "text-sm whitespace-pre-wrap break-words" > { rawData } </ pre >
391- </ div >
392425 </ div >
393-
394-
395-
426+ </ div >
427+
396428 ) ;
397429} ;
398430
0 commit comments