diff --git a/.gitignore b/.gitignore index 5ca027772..c2b853dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ application.* *.autosave .vscode/* temp/* +libBoardController.so +libDataHandler.so +libGanglionLib.so +libGanglionScan.so diff --git a/CHANGELOG.md b/CHANGELOG.md index 9446a351c..442eea2a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # v5.0.3 ### Improvements +* Increase sampling rate for Pulse data output in Networking Widget ### Bug Fixes +* Fix Pulse LSL output error #943 * Fix Accel/Aux UDP output #944 * Fix Expert Mode unplanned keyboard shortcuts crash GUI #941 diff --git a/Networking-Test-Kit/LSL/lslStreamTest_AnalogPinPulse.py b/Networking-Test-Kit/LSL/lslStreamTest_AnalogPinPulse.py new file mode 100644 index 000000000..91b58d1db --- /dev/null +++ b/Networking-Test-Kit/LSL/lslStreamTest_AnalogPinPulse.py @@ -0,0 +1,58 @@ +"""Example program to show how to read a multi-channel time series from LSL.""" +import time +from pylsl import StreamInlet, resolve_stream +from time import sleep +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import style +from collections import deque + +# first resolve an EEG stream on the lab network +print("looking for an EEG stream...") +streams = resolve_stream('type', 'EEG') + +# create a new inlet to read from the stream +inlet = StreamInlet(streams[0]) +duration = 10 + +sleep(0) + +def testLSLSamplingRate(): + start = time.time() + numSamples = 0 + numChunks = 0 + + while time.time() <= start + duration: + # get chunks of samples + chunk, timestamp = inlet.pull_chunk() + if timestamp: + numChunks += 1 + for sample in chunk: + numSamples += 1 + + print( "Number of Chunks == {}".format(numChunks) ) + print( "Avg Sampling Rate == {}".format(numSamples / duration) ) + + +testLSLSamplingRate() + +print("gathering data to plot...") + +def testLSLPulseData(): + start = time.time() + raw_pulse_signal = [] + + while time.time() <= start + duration: + chunk, timestamp = inlet.pull_chunk() + if timestamp: + for sample in chunk: + # print(sample) + raw_pulse_signal.append(sample[1]) + + print(raw_pulse_signal) + print( "Avg Sampling Rate == {}".format(len(raw_pulse_signal) / duration) ) + plt.plot(raw_pulse_signal) + plt.ylabel('raw analog signal') + plt.show() + +testLSLPulseData() \ No newline at end of file diff --git a/Networking-Test-Kit/UDP/udp_receive.py b/Networking-Test-Kit/UDP/udp_receive.py index 7133e6279..51109281c 100755 --- a/Networking-Test-Kit/UDP/udp_receive.py +++ b/Networking-Test-Kit/UDP/udp_receive.py @@ -79,9 +79,16 @@ def close_file(*args): # Receive messages print("Listening...") - while True: + start = time.time() + numSamples = 0 + duration = 10 + while time.time() <= start + duration: data, addr = sock.recvfrom(20000) # buffer size is 20000 bytes if args.option=="print": print_message(data) + numSamples += 1 elif args.option=="record": record_to_file(data) +print( "Samples == {}".format(numSamples) ) +print( "Duration == {}".format(duration) ) +print( "Avg Sampling Rate == {}".format(numSamples / duration) ) diff --git a/Networking-Test-Kit/UDP/udp_receive_pulse.py b/Networking-Test-Kit/UDP/udp_receive_pulse.py new file mode 100755 index 000000000..eec641d3f --- /dev/null +++ b/Networking-Test-Kit/UDP/udp_receive_pulse.py @@ -0,0 +1,106 @@ +import socket +import sys +import time +import argparse +import signal +import struct +import os +import json +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import style + +raw_pulse_signal = [] + +# Print received message to console +def print_message(*args): + try: + # print(args[0]) #added to see raw data + obj = json.loads(args[0].decode()) + print(obj.get('data')) + except BaseException as e: + print(e) + # print("(%s) RECEIVED MESSAGE: " % time.time() + + # ''.join(str(struct.unpack('>%df' % int(length), args[0])))) + +# Clean exit from print mode +def exit_print(signal, frame): + print("Closing listener") + sys.exit(0) + +# Record received message in text file +def record_to_file(*args): + textfile.write(str(time.time()) + ",") + textfile.write(''.join(str(struct.unpack('>%df' % length,args[0])))) + textfile.write("\n") + +# Save recording, clean exit from record mode +def close_file(*args): + print("\nFILE SAVED") + textfile.close() + sys.exit(0) + +if __name__ == "__main__": + # Collect command line arguments + parser = argparse.ArgumentParser() + parser.add_argument("--ip", + default="127.0.0.1", help="The ip to listen on") + parser.add_argument("--port", + type=int, default=12345, help="The port to listen on") + parser.add_argument("--address",default="/openbci", help="address to listen to") + parser.add_argument("--option",default="print",help="Debugger option") + parser.add_argument("--len",default=8,help="Debugger option") + args = parser.parse_args() + + # Set up necessary parameters from command line + length = args.len + if args.option=="print": + signal.signal(signal.SIGINT, exit_print) + elif args.option=="record": + i = 0 + while os.path.exists("udp_test%s.txt" % i): + i += 1 + filename = "udp_test%i.txt" % i + textfile = open(filename, "w") + textfile.write("time,address,messages\n") + textfile.write("-------------------------\n") + print("Recording to %s" % filename) + signal.signal(signal.SIGINT, close_file) + + # Connect to socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_address = (args.ip, args.port) + sock.bind(server_address) + + # Display socket attributes + print('--------------------') + print("-- UDP LISTENER -- ") + print('--------------------') + print("IP:", args.ip) + print("PORT:", args.port) + print('--------------------') + print("%s option selected" % args.option) + + # Receive messages + print("Listening...") + start = time.time() + numSamples = 0 + duration = 3 + + while time.time() <= start + duration: + data, addr = sock.recvfrom(20000) # buffer size is 20000 bytes + if args.option=="print": + print_message(data) + sample = json.loads(data.decode()).get('data')[1] + raw_pulse_signal.append(sample) + numSamples += 1 + elif args.option=="record": + record_to_file(data) + +print( "Samples == {}".format(numSamples) ) +print( "Duration == {}".format(duration) ) +print( "Avg Sampling Rate == {}".format(numSamples / duration) ) +plt.plot(raw_pulse_signal) +plt.ylabel('raw analog signal') +plt.show() diff --git a/OpenBCI_GUI/Info.plist.tmpl b/OpenBCI_GUI/Info.plist.tmpl index abc917b9d..9de05e0c6 100644 --- a/OpenBCI_GUI/Info.plist.tmpl +++ b/OpenBCI_GUI/Info.plist.tmpl @@ -23,7 +23,7 @@ CFBundleShortVersionString 4 CFBundleVersion - 5.0.3-alpha.2 + 5.0.3-alpha.3 CFBundleSignature ???? NSHumanReadableCopyright diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index f4557570f..abc488e47 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -64,7 +64,7 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.3-alpha.2"; +String localGUIVersionString = "v5.0.3-alpha.3"; String localGUIVersionDate = "January 2020"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; diff --git a/OpenBCI_GUI/W_Networking.pde b/OpenBCI_GUI/W_Networking.pde index cc34139d9..e82ded28b 100644 --- a/OpenBCI_GUI/W_Networking.pde +++ b/OpenBCI_GUI/W_Networking.pde @@ -1210,6 +1210,7 @@ class Stream extends Thread { // Data buffers set dynamically in updateNumChan() int start; float[] dataToSend; + private double[][] previousFrameData; //OSC Objects OscP5 osc; @@ -1348,7 +1349,7 @@ class Stream extends Thread { println(e.getMessage()); } } else { - if (checkForData()) { + if (checkForData()) { //This needs to be removed or modified in next version of the GUI sendData(); setDataFalse(); } else { @@ -1368,14 +1369,15 @@ class Stream extends Thread { println(e.getMessage()); } } else { - if (checkForData()) { + if (checkForData()) { //This needs to be removed or modified in next version of the GUI sendData(); + setDataFalse(); } } } } - Boolean checkForData() { + Boolean checkForData() { //Try to remove these methods in next version of GUI if (this.dataType.equals("TimeSeries")) { return w_networking.newDataToSend; } else if (this.dataType.equals("FFT")) { @@ -1387,7 +1389,7 @@ class Stream extends Thread { } else if (this.dataType.equals("Accel/Aux")) { return w_networking.newDataToSend; } else if (this.dataType.equals("Pulse")) { - return w_networking.newDataToSend; + return true; } return false; } @@ -1866,65 +1868,64 @@ class Stream extends Thread { final int NUM_ANALOG_READS = analogChannels.length; // UNFILTERED & FILTERED, Aux data is not affected by filters anyways - if (this.filter==false || this.filter==true) { - // OSC - if (this.protocol.equals("OSC")) { - for (int i = 0; i < NUM_ANALOG_READS; i++) { - msg.clearArguments(); - msg.add(i+1); - msg.add((int)lastSample[analogChannels[i]]); - try { - this.osc.send(msg,this.netaddress); - } catch (Exception e) { - println(e.getMessage()); - } - } - // UDP - } else if (this.protocol.equals("UDP")) { - String outputter = "{\"type\":\"auxiliary\",\"data\":["; - for (int i = 0; i < NUM_ANALOG_READS; i++) { - int auxData = (int)lastSample[analogChannels[i]]; - String auxData_formatted = fourLeadingPlaces.format(auxData); - outputter += auxData_formatted; - if (i != NUM_ANALOG_READS - 1) { - outputter += ","; - } else { - outputter += "]}\r\n"; - } - } + //if (this.filter==false || this.filter==true) { + // OSC + if (this.protocol.equals("OSC")) { + for (int i = 0; i < NUM_ANALOG_READS; i++) { + msg.clearArguments(); + msg.add(i+1); + msg.add((int)lastSample[analogChannels[i]]); try { - this.udp.send(outputter, this.ip, this.port); + this.osc.send(msg,this.netaddress); } catch (Exception e) { println(e.getMessage()); } - // LSL - } else if (this.protocol.equals("LSL")) { - for (int i = 0; i < NUM_ANALOG_READS; i++) { - dataToSend[i] = (int)lastSample[analogChannels[i]]; - } - // Add timestamp to LSL Stream - outlet_data.push_sample(dataToSend); - } else if (this.protocol.equals("Serial")) { - // Data Format: 0001,0002,0003\n or 0001,0002\n depending if Wifi Shield is used - // 5 chars per pin, including \n char for Z - serialMessage = ""; - for (int i = 0; i < NUM_ANALOG_READS; i++) { - int auxData = (int)lastSample[analogChannels[i]]; - String auxData_formatted = String.format("%04d", auxData); - serialMessage += auxData_formatted; - if (i != NUM_ANALOG_READS - 1) { - serialMessage += ","; - } else { - serialMessage += "\n"; - } + } + // UDP + } else if (this.protocol.equals("UDP")) { + String outputter = "{\"type\":\"auxiliary\",\"data\":["; + for (int i = 0; i < NUM_ANALOG_READS; i++) { + int auxData = (int)lastSample[analogChannels[i]]; + String auxData_formatted = fourLeadingPlaces.format(auxData); + outputter += auxData_formatted; + if (i != NUM_ANALOG_READS - 1) { + outputter += ","; + } else { + outputter += "]}\r\n"; } - try { - //println(serialMessage); - this.serial_networking.write(serialMessage); - } catch (Exception e) { - println(e.getMessage()); + } + try { + this.udp.send(outputter, this.ip, this.port); + } catch (Exception e) { + println(e.getMessage()); + } + // LSL + } else if (this.protocol.equals("LSL")) { + for (int i = 0; i < NUM_ANALOG_READS; i++) { + dataToSend[i] = (int)lastSample[analogChannels[i]]; + } + // Add timestamp to LSL Stream + outlet_data.push_sample(dataToSend); + } else if (this.protocol.equals("Serial")) { + // Data Format: 0001,0002,0003\n or 0001,0002\n depending if Wifi Shield is used + // 5 chars per pin, including \n char for Z + serialMessage = ""; + for (int i = 0; i < NUM_ANALOG_READS; i++) { + int auxData = (int)lastSample[analogChannels[i]]; + String auxData_formatted = String.format("%04d", auxData); + serialMessage += auxData_formatted; + if (i != NUM_ANALOG_READS - 1) { + serialMessage += ","; + } else { + serialMessage += "\n"; } } + try { + //println(serialMessage); + this.serial_networking.write(serialMessage); + } catch (Exception e) { + println(e.getMessage()); + } } } @@ -1994,15 +1995,29 @@ class Stream extends Thread { } ////////////////////////////////////// Stream pulse data from W_PulseSensor + //This data type is not affected by GUI filters + //JAN 2021 - Using this method to test refactoring Networking streaming void sendPulseData() { - if (this.filter==false || this.filter==true) { + //Get data from Board that + int numDataPoints = 3; + double[][] frameData = currentBoard.getFrameData(); + int[] analogChannels = ((AnalogCapableBoard)currentBoard).getAnalogChannels(); + + //Check for state change in the available frameData. This works, but maybe checkIfEnoughDataToSend could be used instead and be more accurate... + if (!frameData.equals(previousFrameData)) { + + previousFrameData = frameData; + // OSC if (this.protocol.equals("OSC")) { - //ADD BPM Data (BPM, Signal, IBI) - for (int i = 0; i < (w_pulsesensor.PulseWaveY.length); i++) {//This works + + for (int i = 0; i < frameData[0].length; i++) + { + int raw_signal = (int)(frameData[analogChannels[0]][i]); + //ADD BPM Data (BPM, Signal, IBI) msg.clearArguments(); //This belongs here msg.add(w_pulsesensor.BPM); //Add BPM first - msg.add(w_pulsesensor.PulseWaveY[i]); //Add Raw Signal second + msg.add(raw_signal); //Add Raw Signal second msg.add(w_pulsesensor.IBI); //Add IBI third //Message received in Max via OSC is a list of three integers without commas: 75 512 600 : BPM Signal IBI //println(" " + this.port + " ~~~~ " + w_pulsesensor.BPM + "," + w_pulsesensor.PulseWaveY[i] + "," + w_pulsesensor.IBI); @@ -2012,12 +2027,16 @@ class Stream extends Thread { println(e.getMessage()); } } + // UDP } else if (this.protocol.equals("UDP")) { //////////////////This needs to be checked - String outputter = "{\"type\":\"pulse\",\"data\":"; - for (int i = 0; i < (w_pulsesensor.PulseWaveY.length); i++) { + + for (int i = 0; i < frameData[0].length; i++) + { + String outputter = "{\"type\":\"pulse\",\"data\":["; + int raw_signal = (int)(frameData[analogChannels[0]][i]); outputter += str(w_pulsesensor.BPM) + ","; //Comma separated string output (BPM,Raw Signal,IBI) - outputter += str(w_pulsesensor.PulseWaveY[i]) + ","; + outputter += str(raw_signal) + ","; outputter += str(w_pulsesensor.IBI); outputter += "]}\r\n"; try { @@ -2026,25 +2045,31 @@ class Stream extends Thread { println(e.getMessage()); } } + // LSL } else if (this.protocol.equals("LSL")) { ///////////////////This needs to be checked - for (int i = 0; i < (w_pulsesensor.PulseWaveY.length); i++) { - dataToSend[0] = w_pulsesensor.BPM; //Array output - dataToSend[1] = w_pulsesensor.PulseWaveY[i]; - dataToSend[2] = w_pulsesensor.IBI; + + float[] _dataToSend = new float[frameData[0].length * numDataPoints]; + for (int i = 0; i < frameData[0].length; i++) + { + int raw_signal = (int)(frameData[analogChannels[0]][i]); + _dataToSend[numDataPoints*i] = w_pulsesensor.BPM; + _dataToSend[numDataPoints*i+1] = raw_signal; + _dataToSend[numDataPoints*i+2] = w_pulsesensor.IBI; } - // Add timestamp to LSL Stream - outlet_data.push_chunk(dataToSend); + // From LSLLink Library: The time stamps of other samples are automatically derived based on the sampling rate of the stream. + outlet_data.push_chunk(_dataToSend); + // Serial } else if (this.protocol.equals("Serial")) { // Send Pulse Data (BPM,Signal,IBI) over Serial - for (int i = 0; i < (w_pulsesensor.PulseWaveY.length); i++) { + + for (int i = 0; i < frameData[0].length; i++) + { serialMessage = ""; //clear message - int BPM = (w_pulsesensor.BPM); - int Signal = (w_pulsesensor.PulseWaveY[i]); - int IBI = (w_pulsesensor.IBI); - serialMessage += BPM + ","; //Comma separated string output (BPM,Raw Signal,IBI) - serialMessage += Signal + ","; - serialMessage += IBI; + int raw_signal = (int)(frameData[analogChannels[0]][i]); + serialMessage += w_pulsesensor.BPM + ","; //Comma separated string output (BPM,Raw Signal,IBI) + serialMessage += raw_signal + ","; + serialMessage += w_pulsesensor.IBI; try { println(serialMessage); this.serial_networking.write(serialMessage); diff --git a/OpenBCI_GUI/W_PulseSensor.pde b/OpenBCI_GUI/W_PulseSensor.pde index 25b67713a..0126d34ad 100644 --- a/OpenBCI_GUI/W_PulseSensor.pde +++ b/OpenBCI_GUI/W_PulseSensor.pde @@ -100,10 +100,17 @@ class W_PulseSensor extends Widget { for (int i=0; i < PulseBuffSize; i++ ) { int signal = (int)(allData.get(i)[analogChannels[0]]); - processSignal(signal); + //processSignal(signal); PulseWaveY[i] = signal; } + double[][] frameData = currentBoard.getFrameData(); + for (int i = 0; i < frameData[0].length; i++) + { + int signal = (int)(frameData[analogChannels[0]][i]); + processSignal(signal); + } + //ignore top left button interaction when widgetSelector dropdown is active lockElementOnOverlapCheck(analogModeButton);