Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SerialProcessor.tsx #78

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

yousef7474
Copy link

@yousef7474 yousef7474 commented Feb 19, 2025

add another column for time taken

Summary by CodeRabbit

  • New Features
    • Introduced a new user interface component that allows serial port connection for real-time data recording.
    • Enables users to start/stop data collection and automatically download the recorded data in CSV format.
    • Provides status updates on connection progress and sample counts during data processing.

Copy link

coderabbitai bot commented Feb 19, 2025

Walkthrough

This pull request introduces a new React component, SerialProcessor, that manages serial port communication. It handles connecting to a serial port, recording data via packet processing, and exporting the recorded information as a CSV file. The component leverages hooks to manage state, provides error handling for serial operations, and renders a simple user interface with buttons for connecting, starting/stopping recording, and exporting data.

Changes

File(s) Change Summary
src/components/SerialProcessor.tsx Added new React component implementing serial port connection, data recording with packet processing, and CSV export.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SerialProcessor
  participant SerialPort

  User->>SerialProcessor: Click "Connect"
  SerialProcessor->>SerialPort: connectSerial()
  SerialPort-->>SerialProcessor: Connection response

  User->>SerialProcessor: Click "Start Recording"
  SerialProcessor->>SerialPort: send START command
  SerialPort-->>SerialProcessor: Stream data
  Note right of SerialProcessor: Process incoming packets

  User->>SerialProcessor: Click "Stop Recording"
  SerialProcessor->>SerialPort: send STOP command

  User->>SerialProcessor: Click "Save to CSV"
  SerialProcessor->>User: Trigger CSV file download
Loading

Possibly related PRs

  • SerialProcessor.tsx #77: Adjustments on the SerialProcessor component targeting data recording and processing functionalities, showing a direct code-level connection.

Poem

I hop through code with cheer so bright,
In serial streams I find my light.
Recording data, byte by byte,
Saving CSVs in pure delight.
Hopping through logic, swift and keen—
A rabbit’s code is fit for the scene! 🐇💻


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/components/SerialProcessor.tsx (3)

5-10: Add JSDoc documentation for the Sample interface.

Consider adding documentation to explain the purpose and units of each field, especially for timing-related fields like TimeDelta and timestamp.

+/**
+ * Represents a single sample of data from the serial port.
+ * @interface Sample
+ * @property {number} Counter - Sequential counter for the sample
+ * @property {number} Channel1 - Normalized channel value (0-1)
+ * @property {number} [TimeDelta] - Time difference from previous sample in milliseconds
+ * @property {number} timestamp - Raw timestamp from the device
+ */
 interface Sample {
   Counter: number;
   Channel1: number;
   TimeDelta?: number;
   timestamp: number;
 }

111-133: Improve CSV export robustness.

Consider the following improvements:

  1. Add error handling for file operations
  2. Use more descriptive filename format
  3. Clean up URL object after use
 const saveToCSV = () => {
+  let url: string | null = null;
   try {
     const csvContent = [
       // CSV Header
       ['Counter', 'Channel1', 'TimeDelta'].join(','),
       // CSV Data
       ...data.map(sample => 
         [
           sample.Counter,
           sample.Channel1.toFixed(6),
           sample.TimeDelta?.toFixed(3)
         ].join(',')
       )
     ].join('\n');

     const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
-    const url = URL.createObjectURL(blob);
+    url = URL.createObjectURL(blob);
     const link = document.createElement('a');
     link.setAttribute('href', url);
-    link.setAttribute('download', `ChordsWeb${new Date().toISOString().slice(0,-5).replace(/[:-]/g, '')}.csv`);
+    const timestamp = new Date().toISOString()
+      .replace(/[:.]/g, '-')
+      .replace('T', '_')
+      .slice(0, -1);
+    link.setAttribute('download', `chords_web_recording_${timestamp}.csv`);
     document.body.appendChild(link);
     link.click();
     document.body.removeChild(link);
+  } catch (error) {
+    console.error('Error saving CSV:', error);
+    // TODO: Add user-friendly error notification
+  } finally {
+    if (url) {
+      URL.revokeObjectURL(url);
+    }
   }
 };

135-167: Improve component accessibility.

Add ARIA labels and roles to improve accessibility:

-    <div className="p-4">
+    <div className="p-4" role="region" aria-label="Serial Data Recording">
       <div className="flex gap-4 mb-4">
         <Button 
           onClick={connectSerial}
           disabled={isConnected}
           className="bg-primary text-primary-foreground hover:bg-primary/90"
+          aria-label={isConnected ? "Already connected to serial port" : "Connect to serial port"}
         >
           Connect Serial
         </Button>
         <Button 
           onClick={isRecording ? stopRecording : startRecording}
           disabled={!isConnected}
           className={isRecording ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : "bg-primary text-primary-foreground hover:bg-primary/90"}
+          aria-label={isRecording ? "Stop recording data" : "Start recording data"}
         >
           {isRecording ? 'Stop Recording' : 'Start Recording'}
         </Button>
         <Button 
           onClick={saveToCSV}
           disabled={data.length === 0}
           className="bg-secondary text-secondary-foreground hover:bg-secondary/90"
+          aria-label={data.length === 0 ? "No data to save" : "Save data as CSV"}
         >
           Save CSV
         </Button>
       </div>
-      <div className="text-sm text-muted-foreground">
+      <div className="text-sm text-muted-foreground" role="status" aria-live="polite">
         {isConnected ? 'Connected to serial port' : 'Not connected'}
         <br />
         Samples recorded: {data.length}
       </div>
     </div>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f17b65f and be6fdee.

📒 Files selected for processing (1)
  • src/components/SerialProcessor.tsx (1 hunks)

Comment on lines +12 to +18
const SerialProcessor = () => {
const [port, setPort] = useState<SerialPort | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [data, setData] = useState<Sample[]>([]);
const [lastTimestamp, setLastTimestamp] = useState<number | null>(null);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add type definition for SerialPort.

The SerialPort type is used but not imported or defined. Consider importing it from the Web Serial API types.

+/// <reference types="w3c-web-serial" />
 import React, { useState, useEffect, useCallback } from 'react';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const SerialProcessor = () => {
const [port, setPort] = useState<SerialPort | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [data, setData] = useState<Sample[]>([]);
const [lastTimestamp, setLastTimestamp] = useState<number | null>(null);
/// <reference types="w3c-web-serial" />
import React, { useState, useEffect, useCallback } from 'react';
const SerialProcessor = () => {
const [port, setPort] = useState<SerialPort | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [data, setData] = useState<Sample[]>([]);
const [lastTimestamp, setLastTimestamp] = useState<number | null>(null);
// ... rest of the component logic
};

Comment on lines +19 to +28
const connectSerial = async () => {
try {
const selectedPort = await navigator.serial.requestPort();
await selectedPort.open({ baudRate: 230400 });
setPort(selectedPort);
setIsConnected(true);
} catch (error) {
console.error('Error connecting to serial:', error);
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and add cleanup.

  1. Add user-friendly error messages
  2. Implement cleanup on component unmount
 const connectSerial = async () => {
   try {
     const selectedPort = await navigator.serial.requestPort();
     await selectedPort.open({ baudRate: 230400 });
     setPort(selectedPort);
     setIsConnected(true);
   } catch (error) {
-    console.error('Error connecting to serial:', error);
+    const message = error instanceof Error ? error.message : 'Unknown error';
+    console.error('Error connecting to serial:', message);
+    // TODO: Add user-friendly error notification
   }
 };

+useEffect(() => {
+  return () => {
+    if (port) {
+      port.close().catch(console.error);
+    }
+  };
+}, [port]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const connectSerial = async () => {
try {
const selectedPort = await navigator.serial.requestPort();
await selectedPort.open({ baudRate: 230400 });
setPort(selectedPort);
setIsConnected(true);
} catch (error) {
console.error('Error connecting to serial:', error);
}
};
const connectSerial = async () => {
try {
const selectedPort = await navigator.serial.requestPort();
await selectedPort.open({ baudRate: 230400 });
setPort(selectedPort);
setIsConnected(true);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Error connecting to serial:', message);
// TODO: Add user-friendly error notification
}
};
useEffect(() => {
return () => {
if (port) {
port.close().catch(console.error);
}
};
}, [port]);

Comment on lines +30 to +61
const extractTimestamp = (packet: Uint8Array): number => {
const timeIndex = 3 + (6 * 2); // HEADER_LEN + (NUM_CHANNELS * 2)
return (packet[timeIndex] << 24) |
(packet[timeIndex + 1] << 16) |
(packet[timeIndex + 2] << 8) |
packet[timeIndex + 3];
};

const processPacket = (packet: Uint8Array): Sample | null => {
if (packet.length < 20) return null; // Minimum packet size check

// Check sync bytes
if (packet[0] !== 0xC7 || packet[1] !== 0x7C) return null;

const timestamp = extractTimestamp(packet);
const counter = packet[2];

// Process Channel1 (assuming 14-bit ADC)
const channel1High = packet[3];
const channel1Low = packet[4];
const channel1Value = ((channel1High << 8) | channel1Low) / 16384.0; // Normalize to 0-1

const timeDelta = lastTimestamp ? (timestamp - lastTimestamp) / 1000 : 0;
setLastTimestamp(timestamp);

return {
Counter: counter,
Channel1: channel1Value,
TimeDelta: timeDelta,
timestamp: timestamp
};
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve packet processing robustness.

  1. Define constants for packet structure
  2. Add validation for channel value range
  3. Consider using DataView for safer binary parsing
+const PACKET_CONSTANTS = {
+  SYNC_BYTE1: 0xC7,
+  SYNC_BYTE2: 0x7C,
+  MIN_PACKET_SIZE: 20,
+  HEADER_LEN: 3,
+  CHANNELS: 6,
+  TIMESTAMP_SIZE: 4
+} as const;

 const extractTimestamp = (packet: Uint8Array): number => {
-  const timeIndex = 3 + (6 * 2);
+  const timeIndex = PACKET_CONSTANTS.HEADER_LEN + (PACKET_CONSTANTS.CHANNELS * 2);
   return (packet[timeIndex] << 24) | 
          (packet[timeIndex + 1] << 16) | 
          (packet[timeIndex + 2] << 8) | 
          packet[timeIndex + 3];
 };

 const processPacket = (packet: Uint8Array): Sample | null => {
-  if (packet.length < 20) return null;
+  if (packet.length < PACKET_CONSTANTS.MIN_PACKET_SIZE) return null;

   // Check sync bytes
-  if (packet[0] !== 0xC7 || packet[1] !== 0x7C) return null;
+  if (packet[0] !== PACKET_CONSTANTS.SYNC_BYTE1 || 
+      packet[1] !== PACKET_CONSTANTS.SYNC_BYTE2) return null;

   const timestamp = extractTimestamp(packet);
   const counter = packet[2];
   
-  // Process Channel1 (assuming 14-bit ADC)
-  const channel1High = packet[3];
-  const channel1Low = packet[4];
-  const channel1Value = ((channel1High << 8) | channel1Low) / 16384.0;
+  // Use DataView for safer binary parsing
+  const view = new DataView(packet.buffer);
+  const channel1Raw = view.getUint16(3, false); // big-endian
+  const channel1Value = channel1Raw / 16384.0;
+
+  // Validate channel value
+  if (channel1Value < 0 || channel1Value > 1) {
+    console.warn(`Invalid channel value: ${channel1Value}`);
+    return null;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const extractTimestamp = (packet: Uint8Array): number => {
const timeIndex = 3 + (6 * 2); // HEADER_LEN + (NUM_CHANNELS * 2)
return (packet[timeIndex] << 24) |
(packet[timeIndex + 1] << 16) |
(packet[timeIndex + 2] << 8) |
packet[timeIndex + 3];
};
const processPacket = (packet: Uint8Array): Sample | null => {
if (packet.length < 20) return null; // Minimum packet size check
// Check sync bytes
if (packet[0] !== 0xC7 || packet[1] !== 0x7C) return null;
const timestamp = extractTimestamp(packet);
const counter = packet[2];
// Process Channel1 (assuming 14-bit ADC)
const channel1High = packet[3];
const channel1Low = packet[4];
const channel1Value = ((channel1High << 8) | channel1Low) / 16384.0; // Normalize to 0-1
const timeDelta = lastTimestamp ? (timestamp - lastTimestamp) / 1000 : 0;
setLastTimestamp(timestamp);
return {
Counter: counter,
Channel1: channel1Value,
TimeDelta: timeDelta,
timestamp: timestamp
};
};
const PACKET_CONSTANTS = {
SYNC_BYTE1: 0xC7,
SYNC_BYTE2: 0x7C,
MIN_PACKET_SIZE: 20,
HEADER_LEN: 3,
CHANNELS: 6,
TIMESTAMP_SIZE: 4
} as const;
const extractTimestamp = (packet: Uint8Array): number => {
const timeIndex = PACKET_CONSTANTS.HEADER_LEN + (PACKET_CONSTANTS.CHANNELS * 2);
return (packet[timeIndex] << 24) |
(packet[timeIndex + 1] << 16) |
(packet[timeIndex + 2] << 8) |
packet[timeIndex + 3];
};
const processPacket = (packet: Uint8Array): Sample | null => {
if (packet.length < PACKET_CONSTANTS.MIN_PACKET_SIZE) return null;
// Check sync bytes
if (packet[0] !== PACKET_CONSTANTS.SYNC_BYTE1 ||
packet[1] !== PACKET_CONSTANTS.SYNC_BYTE2) return null;
const timestamp = extractTimestamp(packet);
const counter = packet[2];
// Use DataView for safer binary parsing
const view = new DataView(packet.buffer);
const channel1Raw = view.getUint16(3, false); // big-endian
const channel1Value = channel1Raw / 16384.0;
// Validate channel value
if (channel1Value < 0 || channel1Value > 1) {
console.warn(`Invalid channel value: ${channel1Value}`);
return null;
}
const timeDelta = lastTimestamp ? (timestamp - lastTimestamp) / 1000 : 0;
setLastTimestamp(timestamp);
return {
Counter: counter,
Channel1: channel1Value,
TimeDelta: timeDelta,
timestamp: timestamp
};
};

Comment on lines +63 to +109
const startRecording = async () => {
if (!port) return;

setIsRecording(true);
setData([]);
setLastTimestamp(null);

try {
// Send START command
const encoder = new TextEncoder();
const writer = port.writable.getWriter();
await writer.write(encoder.encode('START\n'));
writer.releaseLock();

// Start reading
const reader = port.readable.getReader();

while (isRecording) {
const { value, done } = await reader.read();
if (done) break;

const sample = processPacket(value);
if (sample) {
setData(prev => [...prev, sample]);
}
}

reader.releaseLock();
} catch (error) {
console.error('Error during recording:', error);
}
};

const stopRecording = async () => {
if (!port) return;

setIsRecording(false);

try {
const encoder = new TextEncoder();
const writer = port.writable.getWriter();
await writer.write(encoder.encode('STOP\n'));
writer.releaseLock();
} catch (error) {
console.error('Error stopping recording:', error);
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential infinite loop and improve resource handling.

The current implementation has several issues:

  1. The while loop might not break if state updates are missed
  2. Resources aren't properly released on errors
 const startRecording = async () => {
   if (!port) return;

   setIsRecording(true);
   setData([]);
   setLastTimestamp(null);

+  let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
+  let writer: WritableStreamDefaultWriter<Uint8Array> | null = null;

   try {
     // Send START command
     const encoder = new TextEncoder();
-    const writer = port.writable.getWriter();
+    writer = port.writable.getWriter();
     await writer.write(encoder.encode('START\n'));
-    writer.releaseLock();
+    await writer.close();
+    writer = null;

     // Start reading
-    const reader = port.readable.getReader();
+    reader = port.readable.getReader();
     
-    while (isRecording) {
+    // Use AbortController for safe cancellation
+    const abortController = new AbortController();
+    const signal = abortController.signal;
+
+    const recordingPromise = async () => {
+      while (!signal.aborted) {
         const { value, done } = await reader.read();
         if (done) break;
         
         const sample = processPacket(value);
         if (sample) {
           setData(prev => [...prev, sample]);
         }
-      }
+      }
+    };

-    reader.releaseLock();
+    await recordingPromise();
   } catch (error) {
     console.error('Error during recording:', error);
+    setIsRecording(false);
   } finally {
+    if (writer) {
+      try {
+        await writer.close();
+      } catch (e) {
+        console.error('Error closing writer:', e);
+      }
+      writer.releaseLock();
+    }
+    if (reader) {
+      try {
+        await reader.cancel();
+      } catch (e) {
+        console.error('Error cancelling reader:', e);
+      }
+      reader.releaseLock();
+    }
   }
 };

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant