Skip to content

Commit a624de2

Browse files
author
Ritika Mishra
committed
improved code to fix issues and working
1 parent dea795d commit a624de2

File tree

2 files changed

+147
-71
lines changed

2 files changed

+147
-71
lines changed

src/app/serial-plotter/page.tsx

Lines changed: 115 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useState, useRef, useCallback } from "react";
44
import { WebglPlot, WebglLine, ColorRGBA } from "webgl-plot";
55
import { Button } from "@/components/ui/button";
66
import Navbar from "@/components/Navbar";
7+
import { toast } from "sonner";
78

89
interface 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}

src/components/Colors.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,15 @@ export const darkThemeColors = customColorsArray.map(hex => lightenColor(hex, 0.
6868

6969
// Helper function to retrieve a color based on channel index and theme
7070
export function getCustomColor(index: number, theme: 'light' | 'dark'): string {
71+
if (!Number.isInteger(index) || index < 0) {
72+
throw new Error('Index must be a non-negative integer');
73+
}
74+
7175
const colors = theme === 'dark' ? darkThemeColors : lightThemeColors;
7276
return colors[index % colors.length];
7377
}
7478

79+
7580
// ColorRGBA class definition for plotting
7681
export class ColorRGBA {
7782
constructor(
@@ -82,14 +87,36 @@ export class ColorRGBA {
8287
) { }
8388
}
8489

85-
// Plot line color function (ensure proper indexing)
90+
const LIGHT_THEME_ALPHA = 0.8;
91+
const DARK_THEME_ALPHA = 1.0;
92+
const DARKEN_FACTOR = 0.5; // Adjust to control darkness for light theme
93+
94+
function hexToRGB(hex: string): { r: number; g: number; b: number } {
95+
return {
96+
r: parseInt(hex.slice(1, 3), 16) / 255,
97+
g: parseInt(hex.slice(3, 5), 16) / 255,
98+
b: parseInt(hex.slice(5, 7), 16) / 255,
99+
};
100+
}
101+
86102
export const getLineColor = (channelNumber: number, theme: 'light' | 'dark'): ColorRGBA => {
103+
if (!Number.isInteger(channelNumber) || channelNumber < 1) {
104+
throw new Error('Channel number must be a positive integer');
105+
}
106+
87107
// Convert 1-indexed channel number to 0-indexed index
88108
const index = channelNumber - 1;
89109
const hex = getCustomColor(index, theme);
90-
const r = parseInt(hex.slice(1, 3), 16) / 255;
91-
const g = parseInt(hex.slice(3, 5), 16) / 255;
92-
const b = parseInt(hex.slice(5, 7), 16) / 255;
93-
const alpha = theme === 'dark' ? 1 : 0.8;
110+
let { r, g, b } = hexToRGB(hex);
111+
const alpha = theme === 'dark' ? DARK_THEME_ALPHA : LIGHT_THEME_ALPHA;
112+
113+
// Apply darkening factor for light theme
114+
if (theme === 'light') {
115+
r *= DARKEN_FACTOR;
116+
g *= DARKEN_FACTOR;
117+
b *= DARKEN_FACTOR;
118+
}
119+
94120
return new ColorRGBA(r, g, b, alpha);
95121
};
122+

0 commit comments

Comments
 (0)