├── run.py └── OpenBCIBoard.py /run.py: -------------------------------------------------------------------------------- 1 | from OpenBCIBoard import OpenBCIBoard 2 | from OpenBCIBoard import OpenBCISample 3 | import time 4 | import threading 5 | import random 6 | from queue import Queue 7 | import numpy 8 | import matplotlib.pyplot as plt 9 | import csv 10 | #import pylab 11 | #import pyqtgraph as pg 12 | #import pyqtgraph.multiprocess as mp 13 | 14 | 15 | #data = [] 16 | 17 | #def handle_sample(sample, out_q): 18 | # data = sample.channel_data[7] 19 | # out_q.put(data) 20 | # test = out_q.get(data) 21 | # print(test) 22 | 23 | def receiver(in_q): 24 | while True: 25 | sample = in_q.get() 26 | #print(data.id) 27 | data.append(sample.channel_data[7]) 28 | 29 | def plotter(in_q): 30 | data = [] 31 | #plt.figure() 32 | #graph = plt.plot([]) 33 | #plt.ion() 34 | #plt.show() 35 | while True: 36 | sample = in_q.get() #id, channel_data 37 | #data.append(sample.id) 38 | print(sample.id, sample.channel_data) 39 | #print(range(len(data))) 40 | #plt.pause(1) 41 | #graph.set_xdata(range(len(data))) 42 | #graph.set_ydata(numpy.asarray(data)) 43 | #plt.draw() 44 | 45 | def csvwriter(in_q): 46 | print("---- Writing to signal.csv") 47 | c = csv.writer(open('signal.csv', 'w'), delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) 48 | #c = csv.writer(open('signal.csv', 'w'), delimiter=',', escapechar=';', quoting=csv.QUOTE_NONE) 49 | while True: 50 | sample = in_q.get() 51 | c.writerow([sample.id, sample.channel_data[0], sample.channel_data[1], sample.channel_data[2], sample.channel_data[3], sample.channel_data[4], sample.channel_data[5], sample.channel_data[6], sample.channel_data[7]]) 52 | 53 | 54 | if __name__ == '__main__': 55 | board = OpenBCIBoard() 56 | #board.print_register_settings() 57 | q = Queue() 58 | 59 | #t1 = threading.Thread(target=board.start_streaming(handle_sample, q)) 60 | t1 = threading.Thread(target=board.start_streaming, args=(q, )) 61 | #t2 = threading.Thread(target=receiver, args=(q, )) 62 | #t2 = threading.Thread(target=plotter, args=(q, )) 63 | t2 = threading.Thread(target=csvwriter, args=(q, )) 64 | #t1.daemon = True 65 | t1.start() 66 | t2.start() 67 | 68 | # plt.figure() 69 | # ln, = plt.plot([]) 70 | # plt.ion() 71 | # plt.show() 72 | # 73 | # plt.ion() 74 | # while True: 75 | # plt.pause(1) 76 | # ln.set_xdata(range(len(data))) 77 | # ln.set_ydata(data) 78 | # plt.draw() 79 | -------------------------------------------------------------------------------- /OpenBCIBoard.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core OpenBCI object for handling connections and samples from the board. 3 | 4 | EXAMPLE USE: 5 | 6 | def handle_sample(sample): 7 | print(sample.channels) 8 | 9 | board = OpenBCIBoard() 10 | board.print_register_settings() 11 | board.start(handle_sample) 12 | 13 | NOTE: If daisy modules is enabled, the callback will occur every two samples, hence "packet_id" will only contain even numbers. As a side effect, the sampling rate will be divided by 2. 14 | 15 | FIXME: at the moment we can just force daisy mode, do not check that the module is detected. 16 | 17 | 18 | """ 19 | import serial 20 | import struct 21 | import numpy as np 22 | import time 23 | import timeit 24 | import atexit 25 | import logging 26 | import threading 27 | #from queue import Queue 28 | import sys 29 | import pdb 30 | import glob 31 | 32 | SAMPLE_RATE = 250.0 # Hz 33 | START_BYTE = 0xA0 # start of data packet 34 | END_BYTE = 0xC0 # end of data packet 35 | ADS1299_Vref = 4.5 #reference voltage for ADC in ADS1299. set by its hardware 36 | ADS1299_gain = 24.0 #assumed gain setting for ADS1299. set by its Arduino code 37 | scale_fac_uVolts_per_count = ADS1299_Vref/float((pow(2,23)-1))/ADS1299_gain*1000000. 38 | scale_fac_accel_G_per_count = 0.002 /(pow(2,4)) #assume set to +/4G, so 2 mG 39 | ''' 40 | #Commands for in SDK http://docs.openbci.com/software/01-Open BCI_SDK: 41 | 42 | command_stop = "s"; 43 | command_startText = "x"; 44 | command_startBinary = "b"; 45 | command_startBinary_wAux = "n"; 46 | command_startBinary_4chan = "v"; 47 | command_activateFilters = "F"; 48 | command_deactivateFilters = "g"; 49 | command_deactivate_channel = {"1", "2", "3", "4", "5", "6", "7", "8"}; 50 | command_activate_channel = {"q", "w", "e", "r", "t", "y", "u", "i"}; 51 | command_activate_leadoffP_channel = {"!", "@", "#", "$", "%", "^", "&", "*"}; //shift + 1-8 52 | command_deactivate_leadoffP_channel = {"Q", "W", "E", "R", "T", "Y", "U", "I"}; //letters (plus shift) right below 1-8 53 | command_activate_leadoffN_channel = {"A", "S", "D", "F", "G", "H", "J", "K"}; //letters (plus shift) below the letters below 1-8 54 | command_deactivate_leadoffN_channel = {"Z", "X", "C", "V", "B", "N", "M", "<"}; //letters (plus shift) below the letters below the letters below 1-8 55 | command_biasAuto = "`"; 56 | command_biasFixed = "~"; 57 | ''' 58 | 59 | class OpenBCIBoard(object): 60 | """ 61 | 62 | Handle a connection to an OpenBCI board. 63 | 64 | Args: 65 | port: The port to connect to. 66 | baud: The baud of the serial connection. 67 | daisy: Enable or disable daisy module and 16 chans readings 68 | """ 69 | 70 | def __init__(self, port=None, baud=115200, filter_data=True, 71 | scaled_output=True, daisy=False, log=True, timeout=None): 72 | self.log = log # print_incoming_text needs log 73 | self.streaming = False 74 | self.baudrate = baud 75 | self.timeout = timeout 76 | # self.q = Queue() 77 | if not port: 78 | port = self.find_port() 79 | self.port = port 80 | print("Connecting to V3 at port %s" %(port)) 81 | self.ser = serial.Serial(port= port, baudrate = baud, timeout=timeout) 82 | 83 | print("Serial established...") 84 | 85 | time.sleep(2) 86 | #Initialize 32-bit board, doesn't affect 8bit board 87 | self.ser.write(b'v'); 88 | 89 | 90 | #wait for device to be ready 91 | time.sleep(1) 92 | self.print_incoming_text() 93 | 94 | self.streaming = False 95 | self.filtering_data = filter_data 96 | self.scaling_output = scaled_output 97 | self.eeg_channels_per_sample = 8 # number of EEG channels per sample *from the board* 98 | self.aux_channels_per_sample = 3 # number of AUX channels per sample *from the board* 99 | self.read_state = 0 100 | self.daisy = daisy 101 | self.last_odd_sample = OpenBCISample(-1, [], []) # used for daisy 102 | self.log_packet_count = 0 103 | self.attempt_reconnect = False 104 | self.last_reconnect = 0 105 | self.reconnect_freq = 5 106 | self.packets_dropped = 0 107 | 108 | #Disconnects from board when terminated 109 | atexit.register(self.disconnect) 110 | 111 | def getSampleRate(self): 112 | if self.daisy: 113 | return SAMPLE_RATE/2 114 | else: 115 | return SAMPLE_RATE 116 | 117 | def getNbEEGChannels(self): 118 | if self.daisy: 119 | return self.eeg_channels_per_sample*2 120 | else: 121 | return self.eeg_channels_per_sample 122 | 123 | def getNbAUXChannels(self): 124 | return self.aux_channels_per_sample 125 | 126 | # def start_streaming(self, callback, out_q, lapse=-1): 127 | def start_streaming(self, out_q, lapse=-1): 128 | """ 129 | Start handling streaming data from the board. Call a provided callback 130 | for every single sample that is processed (every two samples with daisy module). 131 | 132 | Args: 133 | callback: A callback function -- or a list of functions -- that will receive a single argument of the 134 | OpenBCISample object captured. 135 | """ 136 | if not self.streaming: 137 | self.ser.write(b'b') 138 | self.streaming = True 139 | 140 | start_time = timeit.default_timer() 141 | 142 | # Enclose callback funtion in a list if it comes alone 143 | #if not isinstance(callback, list): 144 | # callback = [callback] 145 | 146 | 147 | #Initialize check connection 148 | self.check_connection() 149 | 150 | while self.streaming: 151 | 152 | # read current sample 153 | sample = self._read_serial_binary() 154 | # if a daisy module is attached, wait to concatenate two samples (main board + daisy) before passing it to callback 155 | if self.daisy: 156 | # odd sample: daisy sample, save for later 157 | if ~sample.id % 2: 158 | self.last_odd_sample = sample 159 | # even sample: concatenate and send if last sample was the fist part, otherwise drop the packet 160 | elif sample.id - 1 == self.last_odd_sample.id: 161 | # the aux data will be the average between the two samples, as the channel samples themselves have been averaged by the board 162 | avg_aux_data = list((np.array(sample.aux_data) + np.array(self.last_odd_sample.aux_data))/2) 163 | whole_sample = OpenBCISample(sample.id, sample.channel_data + self.last_odd_sample.channel_data, avg_aux_data) 164 | out_q.put(whole_sample) 165 | else: 166 | out_q.put(sample) 167 | #print(sample.id) 168 | 169 | if(lapse > 0 and timeit.default_timer() - start_time > lapse): 170 | self.stop(); 171 | if self.log: 172 | self.log_packet_count = self.log_packet_count + 1; 173 | 174 | 175 | """ 176 | PARSER: 177 | Parses incoming data packet into OpenBCISample. 178 | Incoming Packet Structure: 179 | Start Byte(1)|Sample ID(1)|Channel Data(24)|Aux Data(6)|End Byte(1) 180 | 0xA0|0-255|8, 3-byte signed ints|3 2-byte signed ints|0xC0 181 | 182 | """ 183 | def _read_serial_binary(self, max_bytes_to_skip=3000): 184 | def read(n): 185 | b = self.ser.read(n) 186 | if not b: 187 | self.warn('Device appears to be stalled. Quitting...') 188 | sys.exit() 189 | raise Exception('Device Stalled') 190 | sys.exit() 191 | return '\xFF' 192 | else: 193 | return b 194 | 195 | for rep in range(max_bytes_to_skip): 196 | 197 | #---------Start Byte & ID--------- 198 | if self.read_state == 0: 199 | 200 | b = read(1) 201 | 202 | if struct.unpack('B', b)[0] == START_BYTE: 203 | if(rep != 0): 204 | self.warn('Skipped %d bytes before start found' %(rep)) 205 | rep = 0; 206 | packet_id = struct.unpack('B', read(1))[0] #packet id goes from 0-255 207 | log_bytes_in = str(packet_id); 208 | 209 | self.read_state = 1 210 | 211 | #---------Channel Data--------- 212 | elif self.read_state == 1: 213 | channel_data = [] 214 | for c in range(self.eeg_channels_per_sample): 215 | 216 | #3 byte ints 217 | literal_read = read(3) 218 | 219 | unpacked = struct.unpack('3B', literal_read) 220 | log_bytes_in = log_bytes_in + '|' + str(literal_read); 221 | 222 | #3byte int in 2s compliment 223 | if (unpacked[0] >= 127): 224 | pre_fix = bytes(bytearray.fromhex('FF')) 225 | else: 226 | pre_fix = bytes(bytearray.fromhex('00')) 227 | 228 | 229 | literal_read = pre_fix + literal_read; 230 | 231 | #unpack little endian(>) signed integer(i) (makes unpacking platform independent) 232 | myInt = struct.unpack('>i', literal_read)[0] 233 | 234 | if self.scaling_output: 235 | channel_data.append(myInt*scale_fac_uVolts_per_count) 236 | else: 237 | channel_data.append(myInt) 238 | 239 | self.read_state = 2; 240 | 241 | #---------Accelerometer Data--------- 242 | elif self.read_state == 2: 243 | aux_data = [] 244 | for a in range(self.aux_channels_per_sample): 245 | 246 | #short = h 247 | acc = struct.unpack('>h', read(2))[0] 248 | log_bytes_in = log_bytes_in + '|' + str(acc); 249 | 250 | if self.scaling_output: 251 | aux_data.append(acc*scale_fac_accel_G_per_count) 252 | else: 253 | aux_data.append(acc) 254 | 255 | self.read_state = 3; 256 | #---------End Byte--------- 257 | elif self.read_state == 3: 258 | val = struct.unpack('B', read(1))[0] 259 | log_bytes_in = log_bytes_in + '|' + str(val); 260 | self.read_state = 0 #read next packet 261 | if (val == END_BYTE): 262 | sample = OpenBCISample(packet_id, channel_data, aux_data) 263 | self.packets_dropped = 0 264 | return sample 265 | else: 266 | self.warn("ID:<%d> instead of <%s>" 267 | %(packet_id, val, END_BYTE)) 268 | logging.debug(log_bytes_in); 269 | self.packets_dropped = self.packets_dropped + 1 270 | 271 | """ 272 | 273 | Clean Up (atexit) 274 | 275 | """ 276 | def stop(self): 277 | print("Stopping streaming...\nWait for buffer to flush...") 278 | self.streaming = False 279 | self.ser.write(b's') 280 | if self.log: 281 | logging.warning('sent : stopped streaming') 282 | 283 | def disconnect(self): 284 | if(self.streaming == True): 285 | self.stop() 286 | if (self.ser.isOpen()): 287 | print("Closing Serial...") 288 | self.ser.close() 289 | logging.warning('serial closed') 290 | 291 | 292 | """ 293 | 294 | SETTINGS AND HELPERS 295 | 296 | """ 297 | def warn(self, text): 298 | if self.log: 299 | #log how many packets where sent succesfully in between warnings 300 | if self.log_packet_count: 301 | logging.info('Data packets received:'+str(self.log_packet_count)) 302 | self.log_packet_count = 0; 303 | logging.warning(text) 304 | print("Warning: %s" % text) 305 | 306 | 307 | def print_incoming_text(self): 308 | """ 309 | 310 | When starting the connection, print all the debug data until 311 | we get to a line with the end sequence '$$$'. 312 | 313 | """ 314 | line = '' 315 | #Wait for device to send data 316 | time.sleep(1) 317 | 318 | if self.ser.inWaiting(): 319 | line = '' 320 | c = '' 321 | #Look for end sequence $$$ 322 | while '$$$' not in line: 323 | c = self.ser.read().decode('utf-8') 324 | line += c 325 | print(line); 326 | else: 327 | self.warn("No Message") 328 | 329 | def openbci_id(self, serial): 330 | """ 331 | 332 | When automatically detecting port, parse the serial return for the "OpenBCI" ID. 333 | 334 | """ 335 | line = '' 336 | #Wait for device to send data 337 | time.sleep(2) 338 | 339 | if serial.inWaiting(): 340 | line = '' 341 | c = '' 342 | #Look for end sequence $$$ 343 | while '$$$' not in line: 344 | c = serial.read().decode('utf-8') 345 | line += c 346 | if "OpenBCI" in line: 347 | return True 348 | return False 349 | 350 | def print_register_settings(self): 351 | self.ser.write(b'?') 352 | time.sleep(0.5) 353 | self.print_incoming_text(); 354 | #DEBBUGING: Prints individual incoming bytes 355 | def print_bytes_in(self): 356 | if not self.streaming: 357 | self.ser.write(b'b') 358 | self.streaming = True 359 | while self.streaming: 360 | print(struct.unpack('B',self.ser.read())[0]); 361 | 362 | '''Incoming Packet Structure: 363 | Start Byte(1)|Sample ID(1)|Channel Data(24)|Aux Data(6)|End Byte(1) 364 | 0xA0|0-255|8, 3-byte signed ints|3 2-byte signed ints|0xC0''' 365 | 366 | def print_packets_in(self): 367 | while self.streaming: 368 | b = struct.unpack('B', self.ser.read())[0]; 369 | 370 | if b == START_BYTE: 371 | self.attempt_reconnect = False 372 | if skipped_str: 373 | logging.debug('SKIPPED\n' + skipped_str + '\nSKIPPED') 374 | skipped_str = '' 375 | 376 | packet_str = "%03d"%(b) + '|'; 377 | b = struct.unpack('B', self.ser.read())[0]; 378 | packet_str = packet_str + "%03d"%(b) + '|'; 379 | 380 | #data channels 381 | for i in range(24-1): 382 | b = struct.unpack('B', self.ser.read())[0]; 383 | packet_str = packet_str + '.' + "%03d"%(b); 384 | 385 | b = struct.unpack('B', self.ser.read())[0]; 386 | packet_str = packet_str + '.' + "%03d"%(b) + '|'; 387 | 388 | #aux channels 389 | for i in range(6-1): 390 | b = struct.unpack('B', self.ser.read())[0]; 391 | packet_str = packet_str + '.' + "%03d"%(b); 392 | 393 | b = struct.unpack('B', self.ser.read())[0]; 394 | packet_str = packet_str + '.' + "%03d"%(b) + '|'; 395 | 396 | #end byte 397 | b = struct.unpack('B', self.ser.read())[0]; 398 | 399 | #Valid Packet 400 | if b == END_BYTE: 401 | packet_str = packet_str + '.' + "%03d"%(b) + '|VAL'; 402 | print(packet_str) 403 | #logging.debug(packet_str) 404 | 405 | #Invalid Packet 406 | else: 407 | packet_str = packet_str + '.' + "%03d"%(b) + '|INV'; 408 | #Reset 409 | self.attempt_reconnect = True 410 | 411 | 412 | else: 413 | print(b) 414 | if b == END_BYTE: 415 | skipped_str = skipped_str + '|END|' 416 | else: 417 | skipped_str = skipped_str + "%03d"%(b) + '.' 418 | 419 | if self.attempt_reconnect and (timeit.default_timer()-self.last_reconnect) > self.reconnect_freq: 420 | self.last_reconnect = timeit.default_timer() 421 | self.warn('Reconnecting') 422 | self.reconnect() 423 | 424 | 425 | 426 | def check_connection(self, interval = 2, max_packets_to_skip=10): 427 | #check number of dropped packages and establish connection problem if too large 428 | if self.packets_dropped > max_packets_to_skip: 429 | #if error, attempt to reconect 430 | self.reconnect() 431 | # check again again in 2 seconds 432 | threading.Timer(interval, self.check_connection).start() 433 | 434 | def reconnect(self): 435 | self.packets_dropped = 0 436 | self.warn('Reconnecting') 437 | self.stop() 438 | time.sleep(0.5) 439 | self.ser.write(b'v') 440 | time.sleep(0.5) 441 | self.ser.write(b'b') 442 | time.sleep(0.5) 443 | self.streaming = True 444 | #self.attempt_reconnect = False 445 | 446 | 447 | #Adds a filter at 60hz to cancel out ambient electrical noise 448 | def enable_filters(self): 449 | self.ser.write(b'f') 450 | self.filtering_data = True; 451 | 452 | def disable_filters(self): 453 | self.ser.write(b'g') 454 | self.filtering_data = False; 455 | 456 | def test_signal(self, signal): 457 | if signal == 0: 458 | self.ser.write(b'0') 459 | self.warn("Connecting all pins to ground") 460 | elif signal == 1: 461 | self.ser.write(b'p') 462 | self.warn("Connecting all pins to Vcc") 463 | elif signal == 2: 464 | self.ser.write(b'-') 465 | self.warn("Connecting pins to low frequency 1x amp signal") 466 | elif signal == 3: 467 | self.ser.write(b'=') 468 | self.warn("Connecting pins to high frequency 1x amp signal") 469 | elif signal == 4: 470 | self.ser.write(b'[') 471 | self.warn("Connecting pins to low frequency 2x amp signal") 472 | elif signal == 5: 473 | self.ser.write(b']') 474 | self.warn("Connecting pins to high frequency 2x amp signal") 475 | else: 476 | self.warn("%s is not a known test signal. Valid signals go from 0-5" %(signal)) 477 | 478 | def set_channel(self, channel, toggle_position): 479 | #Commands to set toggle to on position 480 | if toggle_position == 1: 481 | if channel is 1: 482 | self.ser.write(b'!') 483 | if channel is 2: 484 | self.ser.write(b'@') 485 | if channel is 3: 486 | self.ser.write(b'#') 487 | if channel is 4: 488 | self.ser.write(b'$') 489 | if channel is 5: 490 | self.ser.write(b'%') 491 | if channel is 6: 492 | self.ser.write(b'^') 493 | if channel is 7: 494 | self.ser.write(b'&') 495 | if channel is 8: 496 | self.ser.write(b'*') 497 | if channel is 9 and self.daisy: 498 | self.ser.write(b'Q') 499 | if channel is 10 and self.daisy: 500 | self.ser.write(b'W') 501 | if channel is 11 and self.daisy: 502 | self.ser.write(b'E') 503 | if channel is 12 and self.daisy: 504 | self.ser.write(b'R') 505 | if channel is 13 and self.daisy: 506 | self.ser.write(b'T') 507 | if channel is 14 and self.daisy: 508 | self.ser.write(b'Y') 509 | if channel is 15 and self.daisy: 510 | self.ser.write(b'U') 511 | if channel is 16 and self.daisy: 512 | self.ser.write(b'I') 513 | #Commands to set toggle to off position 514 | elif toggle_position == 0: 515 | if channel is 1: 516 | self.ser.write(b'1') 517 | if channel is 2: 518 | self.ser.write(b'2') 519 | if channel is 3: 520 | self.ser.write(b'3') 521 | if channel is 4: 522 | self.ser.write(b'4') 523 | if channel is 5: 524 | self.ser.write(b'5') 525 | if channel is 6: 526 | self.ser.write(b'6') 527 | if channel is 7: 528 | self.ser.write(b'7') 529 | if channel is 8: 530 | self.ser.write(b'8') 531 | if channel is 9 and self.daisy: 532 | self.ser.write(b'q') 533 | if channel is 10 and self.daisy: 534 | self.ser.write(b'w') 535 | if channel is 11 and self.daisy: 536 | self.ser.write(b'e') 537 | if channel is 12 and self.daisy: 538 | self.ser.write(b'r') 539 | if channel is 13 and self.daisy: 540 | self.ser.write(b't') 541 | if channel is 14 and self.daisy: 542 | self.ser.write(b'y') 543 | if channel is 15 and self.daisy: 544 | self.ser.write(b'u') 545 | if channel is 16 and self.daisy: 546 | self.ser.write(b'i') 547 | 548 | def find_port(self): 549 | # Finds the serial port names 550 | if sys.platform.startswith('win'): 551 | ports = ['COM%s' % (i+1) for i in range(256)] 552 | elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): 553 | ports = glob.glob('/dev/ttyUSB*') 554 | elif sys.platform.startswith('darwin'): 555 | ports = glob.glob('/dev/tty.usbserial*') 556 | else: 557 | raise EnvironmentError('Error finding ports on your operating system') 558 | openbci_port = '' 559 | for port in ports: 560 | try: 561 | s = serial.Serial(port= port, baudrate = self.baudrate, timeout=self.timeout) 562 | s.write(b'v') 563 | openbci_serial = self.openbci_id(s) 564 | s.close() 565 | if openbci_serial: 566 | openbci_port = port; 567 | except (OSError, serial.SerialException): 568 | pass 569 | if openbci_port == '': 570 | raise OSError('Cannot find OpenBCI port') 571 | else: 572 | return openbci_port 573 | 574 | class OpenBCISample(object): 575 | """Object encapulsating a single sample from the OpenBCI board.""" 576 | def __init__(self, packet_id, channel_data, aux_data): 577 | self.id = packet_id; 578 | self.channel_data = channel_data; 579 | self.aux_data = aux_data; 580 | 581 | 582 | --------------------------------------------------------------------------------