Skip to content

Commit a3b9541

Browse files
committed
Chords working code according to new format(End byte is removed, 4th byte contains num_channels & adc_resolution).The issue is that now the data count for the very 1st second is high then expected.
1 parent 2d0db10 commit a3b9541

File tree

1 file changed

+353
-0
lines changed

1 file changed

+353
-0
lines changed

working.py

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
from pylsl import StreamInfo, StreamOutlet # For LSL (Lab Streaming Layer) to stream data
2+
import argparse # For command-line argument parsing
3+
import serial # For serial communication with Arduino
4+
import time # For time-related functions
5+
import csv # For handling CSV file operations
6+
from datetime import datetime # For getting current timestamps
7+
import serial.tools.list_ports # To list available serial ports
8+
import numpy as np # For handling numeric arrays
9+
import sys
10+
import signal
11+
12+
# Initialize global variables for tracking and processing data
13+
total_packet_count = 0 # Total packets received in the last second
14+
cumulative_packet_count = 0 # Total packets received in the last 10 minutes
15+
start_time = None # Track the start time for packet counting
16+
last_ten_minute_time = None # Track the last 10-minute interval
17+
previous_sample_number = None # Store the previous sample number for detecting missing samples
18+
missing_samples = 0 # Count of missing samples due to packet loss
19+
buffer = bytearray() # Buffer for storing incoming raw data from Arduino
20+
samples_per_second = 0 # Number of samples received per second
21+
retry_limit = 4
22+
23+
# Initialize gloabal variables for Arduino Board
24+
board = "" # Variable for Connected Arduino Board
25+
supported_boards = {"UNO-R3":250, "UNO-R4":500, "UNO-CLONE" : 250 ,"RPI-PICO-RP2040":500, "GIGA-R1":500} # Boards : Sampling rate
26+
27+
# Initialize gloabal variables for Incoming Data
28+
SYNC_BYTE1 = 0xc7 # First byte of sync marker
29+
SYNC_BYTE2 = 0x7c # Second byte of sync marker
30+
HEADER_LENGTH = 4 #Length of the Packet Header
31+
32+
## Initialize gloabal variables for Output
33+
lsl_outlet = None # Placeholder for LSL stream outlet
34+
verbose = False # Flag for verbose output mode
35+
csv_filename = None # Store CSV filename
36+
csv_file = None
37+
ser = None
38+
packet_length = 0
39+
num_channels = 0
40+
41+
def decode_byte(data):
42+
# Extract the channel count (high nibble)
43+
num_channels = ((data >> 4) & 0x0F) + 1
44+
45+
# Extract the ADC resolution (low nibble)
46+
adc_resolution = (data & 0x0F) + 10
47+
48+
return num_channels, adc_resolution
49+
50+
def send_command(ser, command):
51+
ser.flushInput() # Clear the input buffer
52+
ser.flushOutput() # Clear the output buffer
53+
ser.write(f"{command}\n".encode()) # Send command
54+
time.sleep(0.1) # Wait briefly to ensure Arduino processes the command
55+
response = ser.readline().decode('utf-8', errors='ignore').strip() # Read response
56+
return response
57+
58+
def connect_hardware(port, baudrate, timeout=1):
59+
global board, sampling_rate, num_channels, packet_length, buffer, data, mid_value
60+
try:
61+
ser = serial.Serial(port, baudrate=baudrate, timeout=timeout) # Open serial port
62+
print(f"Attempting to connect to hardware on {port} at {baudrate} baud.")
63+
64+
# Check board type
65+
response = None
66+
retry_counter = 0
67+
while retry_counter < retry_limit:
68+
ser.write(b'WHORU\n') # Send command to identify the board
69+
response = ser.readline().strip().decode() # Read response
70+
if response in supported_boards:
71+
board = response
72+
print(f"Board detected: {board}")
73+
sampling_rate = supported_boards[board]
74+
break
75+
retry_counter += 1
76+
77+
if response not in supported_boards:
78+
print("Error: Unable to detect a supported board!")
79+
ser.close()
80+
return None
81+
82+
# Initialize variables
83+
buffer = bytearray()
84+
sync_detected = False
85+
send_command(ser, 'START') # Start streaming
86+
87+
while not sync_detected:
88+
raw_data = ser.read(ser.in_waiting or 1)
89+
if not raw_data:
90+
send_command(ser, 'START') # Resend start command if no data received
91+
continue
92+
93+
buffer.extend(raw_data) # Add raw data to the buffer
94+
95+
# Find synchronization bytes
96+
sync_index = buffer.find(bytes([SYNC_BYTE1, SYNC_BYTE2]))
97+
if sync_index != -1 and len(buffer) >= sync_index + HEADER_LENGTH:
98+
header = buffer[sync_index:sync_index + HEADER_LENGTH]
99+
if header[0] == SYNC_BYTE1 and header[1] == SYNC_BYTE2:
100+
sync_detected = True
101+
102+
# Decode header information
103+
num_channels, adc_resolution = decode_byte(header[3])
104+
print(f"Number of channels: {num_channels}")
105+
packet_length = (2 * num_channels) + HEADER_LENGTH
106+
print(f"Packet length: {packet_length}")
107+
108+
adc_max_value = (2 ** adc_resolution) - 1
109+
mid_value = adc_max_value // 2
110+
111+
# Remove processed data from the buffer
112+
buffer = buffer[sync_index + HEADER_LENGTH:]
113+
print("Synchronization successful. Starting data processing.")
114+
return ser
115+
116+
print("Error: Failed to synchronize with the board.")
117+
ser.close()
118+
return None
119+
120+
except (OSError, serial.SerialException) as e:
121+
print(f"Error: {e}")
122+
return None
123+
124+
# Function to automatically detect the Arduino's serial port
125+
def detect_hardware(baudrate, timeout=1):
126+
ports = serial.tools.list_ports.comports() # List available serial ports
127+
ser = None
128+
for port in ports: # Iterate through each port
129+
ser = connect_hardware(port.device, baudrate)
130+
if ser is not None:
131+
return ser
132+
print("Unable to detect hardware!") # Notify if no Arduino is found
133+
return None # Return None if not found
134+
135+
def read_arduino_data(ser, csv_writer=None, inverted=False):
136+
global total_packet_count, cumulative_packet_count, previous_sample_number, missing_samples, buffer, data, num_channels, packet_length
137+
138+
raw_data = ser.read(ser.in_waiting or 1)
139+
if raw_data == b'':
140+
send_command(ser, 'START')
141+
buffer.extend(raw_data)
142+
143+
while len(buffer) >= packet_length: # Continue processing if the buffer has at least one full packet
144+
sync_index = buffer.find(bytes([SYNC_BYTE1, SYNC_BYTE2]))
145+
packet = buffer[sync_index:sync_index + packet_length] # Extract the packet
146+
data = np.zeros((num_channels, 2000))
147+
if len(packet) == packet_length and packet[0] == SYNC_BYTE1 and packet[1] == SYNC_BYTE2:
148+
if start_time is None:
149+
start_timer() # Start timers for logging
150+
151+
# Extract and process packet data
152+
counter = packet[2]
153+
if previous_sample_number is not None and counter != (previous_sample_number + 1) % 256:
154+
missing_samples += (counter - previous_sample_number - 1) % 256 # Calculate missing samples
155+
if verbose:
156+
print(f"Error: Expected counter {previous_sample_number + 1} but received {counter}. Missing samples: {missing_samples}")
157+
158+
previous_sample_number = counter
159+
total_packet_count += 1
160+
cumulative_packet_count += 1
161+
162+
# Extract channel datae
163+
channel_data = []
164+
for channel in range(num_channels):
165+
high_byte = packet[HEADER_LENGTH + 2 * channel]
166+
low_byte = packet[HEADER_LENGTH + 2 * channel + 1]
167+
value = (high_byte << 8) | low_byte
168+
if inverted: # Apply inversion if the flag is set
169+
if value > mid_value:
170+
inverted_data = (mid_value) - abs(mid_value - value)
171+
elif value < mid_value:
172+
inverted_data = (mid_value) + abs(mid_value - value)
173+
else:
174+
inverted_data = value
175+
channel_data.append(float(inverted_data)) # Append the inverted value
176+
else:
177+
channel_data.append(float(value)) # Convert to float and add to channel data
178+
179+
if csv_writer:
180+
csv_writer.writerow([counter] + channel_data)
181+
if lsl_outlet:
182+
lsl_outlet.push_sample(channel_data)
183+
184+
data = np.roll(data, -1, axis=1)
185+
data[:, -1] = channel_data
186+
187+
del buffer[: packet_length]
188+
else:
189+
del buffer[:sync_index + 1]
190+
191+
# Function to start timers for logging data
192+
def start_timer():
193+
global start_time, last_ten_minute_time, total_packet_count, cumulative_packet_count
194+
current_time = time.time() # Get the current time
195+
start_time = current_time # Set the start time for packet counting
196+
last_ten_minute_time = current_time # Set the start time for 10-minute interval logging
197+
total_packet_count = 0 # Initialize total packet count
198+
cumulative_packet_count = 0 # Initialize cumulative packet count
199+
200+
# Function to log data every second
201+
def log_one_second_data(verbose=False):
202+
global total_packet_count, samples_per_second
203+
samples_per_second = total_packet_count # Update the samples per second
204+
if verbose:
205+
print(f"Data count for the last second: {total_packet_count} samples, Missing samples: {missing_samples}") # Print verbose output
206+
total_packet_count = 0 # Reset total packet count for the next second
207+
208+
# Function to log data for 10-minute intervals
209+
def log_ten_minute_data(verbose=False):
210+
global cumulative_packet_count, last_ten_minute_time
211+
if verbose:
212+
print(f"Total data count after 10 minutes: {cumulative_packet_count}") # Print cumulative data count
213+
sampling_rate = cumulative_packet_count / (10 * 60) # Calculate sampling rate
214+
print(f"Sampling rate: {sampling_rate:.2f} samples/second") # Print sampling rate
215+
expected_sampling_rate = supported_boards[board] # Expected sampling rate
216+
drift = ((sampling_rate - expected_sampling_rate) / expected_sampling_rate) * 3600 # Calculate drift
217+
print(f"Drift: {drift:.2f} seconds/hour") # Print drift
218+
cumulative_packet_count = 0 # Reset cumulative packet count
219+
last_ten_minute_time = time.time() # Update the last 10-minute interval start time
220+
221+
# Main function to parse command-line arguments and handle data acquisition
222+
def parse_data(ser, lsl_flag=False, csv_flag=False, verbose=False, run_time=None, inverted= False):
223+
global total_packet_count, cumulative_packet_count, start_time, lsl_outlet, last_ten_minute_time, csv_filename, num_channels
224+
225+
csv_writer = None # Placeholder for CSV writer
226+
csv_file = None
227+
228+
# Start LSL streaming if requested
229+
if lsl_flag:
230+
lsl_stream_info = StreamInfo('BioAmpDataStream', 'EXG', num_channels, supported_boards[board], 'float32', 'UpsideDownLabs') # Define LSL stream info
231+
lsl_outlet = StreamOutlet(lsl_stream_info) # Create LSL outlet
232+
print("LSL stream started") # Notify user
233+
234+
if csv_flag:
235+
csv_filename = f"chordspy-{datetime.now().strftime('%Y%m%d-%H%M%S')}.csv" # Create timestamped filename
236+
print(f"CSV recording started. Data will be saved to {csv_filename}") # Notify user
237+
238+
try:
239+
csv_file = open(csv_filename, mode='w', newline='') if csv_flag else None # Open CSV file if logging is
240+
if csv_file:
241+
csv_writer = csv.writer(csv_file) # Create CSV writer
242+
csv_writer.writerow([f"Arduino Board: {board}"])
243+
csv_writer.writerow([f"Sampling Rate (samples per second): {supported_boards[board]}"])
244+
csv_writer.writerow([]) # Blank row for separation
245+
csv_writer.writerow(['Counter', 'Channel1', 'Channel2', 'Channel3', 'Channel4', 'Channel5', 'Channel6']) # Write header
246+
247+
while True:
248+
read_arduino_data(ser, csv_writer, inverted=inverted) # Read and process data from Arduino
249+
if(start_time is not None):
250+
current_time = time.time() # Get the current time
251+
elapsed_time = current_time - start_time # Time elapsed since the last second
252+
elapsed_since_last_10_minutes = current_time - last_ten_minute_time # Time elapsed since the last 10-minute interval
253+
254+
if elapsed_time >= 1:
255+
log_one_second_data(verbose)
256+
start_time = current_time
257+
258+
if elapsed_since_last_10_minutes >= 600:
259+
log_ten_minute_data(verbose)
260+
261+
if run_time and current_time >= end_time:
262+
print("Runtime Over, Sending STOP Command...")
263+
send_command(ser, 'STOP')
264+
break
265+
266+
except KeyboardInterrupt:
267+
print("Process interrupted by user")
268+
269+
finally:
270+
cleanup()
271+
272+
print(f"Total missing samples: {missing_samples}")
273+
sys.exit(0)
274+
275+
def cleanup():
276+
global ser, lsl_outlet, csv_file
277+
278+
# Close the serial connection first
279+
try:
280+
if ser is not None and ser.is_open:
281+
send_command(ser, 'STOP') # Ensure the STOP command is sent
282+
time.sleep(1)
283+
ser.reset_input_buffer() # Clear the input buffer
284+
ser.reset_output_buffer() # Clear the output buffer
285+
ser.close() # Close the serial port
286+
print("Serial connection closed.")
287+
else:
288+
print("Serial connection is not open.")
289+
except Exception as e:
290+
print(f"Error while closing serial connection: {e}")
291+
292+
# Close the LSL stream if it exists
293+
try:
294+
if lsl_outlet:
295+
print("Closing LSL Stream.")
296+
lsl_outlet = None # Cleanup LSL outlet
297+
except Exception as e:
298+
print(f"Error while closing LSL stream: {e}")
299+
300+
# Close the CSV file if it exists
301+
try:
302+
if csv_file:
303+
csv_file.close() # Close the CSV file
304+
print("CSV recording saved.")
305+
except Exception as e:
306+
print(f"Error while closing CSV file: {e}")
307+
308+
print("Cleanup completed, exiting program.")
309+
print(f"Total missing samples: {missing_samples}")
310+
sys.exit(0)
311+
312+
def signal_handler(sig, frame):
313+
cleanup()
314+
315+
# Main entry point of the script
316+
def main():
317+
global verbose,ser
318+
parser = argparse.ArgumentParser(description="Upside Down Labs - Chords-Python Tool",allow_abbrev = False) # Create argument parser
319+
parser.add_argument('-p', '--port', type=str, help="Specify the COM port") # Port argument
320+
parser.add_argument('-b', '--baudrate', type=int, default=230400, help="Set baud rate for the serial communication") # Baud rate
321+
parser.add_argument('--csv', action='store_true', help="Create and write to a CSV file") # CSV logging flag
322+
parser.add_argument('--lsl', action='store_true', help="Start LSL stream") # LSL streaming flag
323+
parser.add_argument('-v', '--verbose', action='store_true', help="Enable verbose output with statistical data") # Verbose flag
324+
parser.add_argument('-t', '--time', type=int, help="Run the program for a specified number of seconds and then exit") #set time
325+
parser.add_argument('--inverted', action='store_true', help="Invert the signal before streaming LSL and logging") # Inverted flag
326+
327+
args = parser.parse_args() # Parse command-line arguments
328+
verbose = args.verbose # Set verbose mode
329+
330+
# Register the signal handler to handle Ctrl+C
331+
signal.signal(signal.SIGINT, signal_handler)
332+
333+
# Check if any logging or GUI options are selected, else show help
334+
if not args.csv and not args.lsl:
335+
parser.print_help() # Print help if no options are selected
336+
return
337+
338+
if args.port:
339+
print("trying to connect to port:", args.port)
340+
ser = connect_hardware(port=args.port, baudrate=args.baudrate)
341+
else:
342+
ser = detect_hardware(baudrate=args.baudrate)
343+
344+
if ser is None:
345+
print("Arduino port not specified or detected. Exiting.") # Notify if no port is available
346+
return
347+
348+
# Start data acquisition
349+
parse_data(ser, lsl_flag=args.lsl, csv_flag=args.csv, verbose=args.verbose, run_time=args.time, inverted=args.inverted)
350+
351+
# Run the main function if this script is executed
352+
if __name__ == "__main__":
353+
main()

0 commit comments

Comments
 (0)