Skip to content

Commit f24536e

Browse files
author
Ritika Mishra
committed
updated plotter
1 parent 905ba96 commit f24536e

File tree

1 file changed

+129
-97
lines changed

1 file changed

+129
-97
lines changed

src/app/serial-plotter/page.tsx

Lines changed: 129 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)