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 92e07ee48..5e53da4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 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 +* Fix bugs found when loading Session Settings #942 + # v5.0.2 ### Improvements 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 f8a1f0df6..51109281c 100755 --- a/Networking-Test-Kit/UDP/udp_receive.py +++ b/Networking-Test-Kit/UDP/udp_receive.py @@ -10,10 +10,11 @@ # Print received message to console def print_message(*args): try: - obj = json.loads(args[0]) - print obj.get('data') + 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(e) # print("(%s) RECEIVED MESSAGE: " % time.time() + # ''.join(str(struct.unpack('>%df' % int(length), args[0])))) @@ -78,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/DataProcessing.pde b/OpenBCI_GUI/DataProcessing.pde index 01b203cf8..6d39cbb52 100644 --- a/OpenBCI_GUI/DataProcessing.pde +++ b/OpenBCI_GUI/DataProcessing.pde @@ -93,8 +93,8 @@ class DataProcessing { float data_std_uV[]; float polarity[]; boolean newDataToSend; - BandPassRanges bpRange = BandPassRanges.FiveToFifty; - BandStopRanges bsRange = BandStopRanges.Sixty; + public BandPassRanges bpRange = BandPassRanges.FiveToFifty; + public BandStopRanges bsRange = BandStopRanges.Sixty; final int[] processing_band_low_Hz = { 1, 4, 8, 13, 30 }; //lower bound for each frequency band of interest (2D classifier only) diff --git a/OpenBCI_GUI/FilterEnums.pde b/OpenBCI_GUI/FilterEnums.pde index 65cb449fd..26d7a9ec1 100644 --- a/OpenBCI_GUI/FilterEnums.pde +++ b/OpenBCI_GUI/FilterEnums.pde @@ -1,21 +1,32 @@ public enum BandStopRanges { - Sixty(60.0d), - Fifty(50.0d), - None(null); + Sixty(0, 60.0d), + Fifty(1, 50.0d), + None(2, null); + private int index; private Double freq; private static BandStopRanges[] vals = values(); - BandStopRanges(Double freq) { + BandStopRanges(int index, Double freq) { + this.index = index; this.freq = freq; } + + public int getIndex() { + return index; + } public Double getFreq() { return freq; } + public static BandStopRanges getByIndex(int i) + { + return vals[i]; + } + public BandStopRanges next() { return vals[(this.ordinal() + 1) % vals.length]; @@ -31,22 +42,28 @@ public enum BandStopRanges public enum BandPassRanges { - FiveToFifty(5.0d, 50.0d), - SevenToThirteen(7.0d, 13.0d), - FifteenToFifty(15.0d, 50.0d), - OneToFifty(1.0d, 50.0d), - OneToHundred(1.0d, 100.0d), - None(null, null); + FiveToFifty(0, 5.0d, 50.0d), + SevenToThirteen(1, 7.0d, 13.0d), + FifteenToFifty(2, 15.0d, 50.0d), + OneToFifty(3, 1.0d, 50.0d), + OneToHundred(4, 1.0d, 100.0d), + None(5, null, null); + private int index; private Double start; private Double stop; private static BandPassRanges[] vals = values(); - BandPassRanges(Double start, Double stop) { + BandPassRanges(int index, Double start, Double stop) { + this.index = index; this.start = start; this.stop = stop; } + + public int getIndex() { + return index; + } public Double getStart() { return start; @@ -56,6 +73,11 @@ public enum BandPassRanges return stop; } + public static BandPassRanges getByIndex(int i) + { + return vals[i]; + } + public BandPassRanges next() { return vals[(this.ordinal() + 1) % vals.length]; diff --git a/OpenBCI_GUI/Info.plist.tmpl b/OpenBCI_GUI/Info.plist.tmpl index 0f869a761..e814301d7 100644 --- a/OpenBCI_GUI/Info.plist.tmpl +++ b/OpenBCI_GUI/Info.plist.tmpl @@ -23,16 +23,16 @@ CFBundleShortVersionString 4 CFBundleVersion - 5.0.2 + 5.0.3 CFBundleSignature ???? NSHumanReadableCopyright MIT License - Copyright © 2020 OpenBCI + Copyright © 2021 OpenBCI CFBundleGetInfoString - December 2020 + January 2021 @@jvm_runtime@@ diff --git a/OpenBCI_GUI/Interactivity.pde b/OpenBCI_GUI/Interactivity.pde index 282612d99..6f4dff755 100644 --- a/OpenBCI_GUI/Interactivity.pde +++ b/OpenBCI_GUI/Interactivity.pde @@ -44,10 +44,10 @@ void parseKey(char val) { case ' ': // space to start/stop the stream topNav.stopButtonWasPressed(); - break; + return; case ',': drawContainers = !drawContainers; - break; + return; case '{': if(colorScheme == COLOR_SCHEME_DEFAULT){ colorScheme = COLOR_SCHEME_ALTERNATIVE_A; @@ -56,153 +56,167 @@ void parseKey(char val) { } //topNav.updateNavButtonsBasedOnColorScheme(); output("New Dark color scheme coming soon!"); - break; + return; - //deactivate channels 1-16 + //deactivate channels 1-4 case '1': currentBoard.setEXGChannelActive(1-1, false); - break; + return; case '2': currentBoard.setEXGChannelActive(2-1, false); - break; + return; case '3': currentBoard.setEXGChannelActive(3-1, false); - break; + return; case '4': currentBoard.setEXGChannelActive(4-1, false); - break; - case '5': - currentBoard.setEXGChannelActive(5-1, false); - break; - case '6': - currentBoard.setEXGChannelActive(6-1, false); - break; - case '7': - currentBoard.setEXGChannelActive(7-1, false); - break; - case '8': - currentBoard.setEXGChannelActive(8-1, false); - break; - case 'q': - currentBoard.setEXGChannelActive(9-1, false); - break; - case 'w': - currentBoard.setEXGChannelActive(10-1, false); - break; - case 'e': - currentBoard.setEXGChannelActive(11-1, false); - break; - case 'r': - currentBoard.setEXGChannelActive(12-1, false); - break; - case 't': - currentBoard.setEXGChannelActive(13-1, false); - break; - case 'y': - currentBoard.setEXGChannelActive(14-1, false); - break; - case 'u': - currentBoard.setEXGChannelActive(15-1, false); - break; - case 'i': - currentBoard.setEXGChannelActive(16-1, false); - break; + return; - //activate channels 1-16 + //activate channels 1-4 case '!': currentBoard.setEXGChannelActive(1-1, true); - break; + return; case '@': currentBoard.setEXGChannelActive(2-1, true); - break; + return; case '#': currentBoard.setEXGChannelActive(3-1, true); - break; + return; case '$': currentBoard.setEXGChannelActive(4-1, true); - break; - case '%': - currentBoard.setEXGChannelActive(5-1, true); - break; - case '^': - currentBoard.setEXGChannelActive(6-1, true); - break; - case '&': - currentBoard.setEXGChannelActive(7-1, true); - break; - case '*': - currentBoard.setEXGChannelActive(8-1, true); - break; - case 'Q': - currentBoard.setEXGChannelActive(9-1, true); - break; - case 'W': - currentBoard.setEXGChannelActive(10-1, true); - break; - case 'E': - currentBoard.setEXGChannelActive(11-1, true); - break; - case 'R': - currentBoard.setEXGChannelActive(12-1, true); - break; - case 'T': - currentBoard.setEXGChannelActive(13-1, true); - break; - case 'Y': - currentBoard.setEXGChannelActive(14-1, true); - break; - case 'U': - currentBoard.setEXGChannelActive(15-1, true); - break; - case 'I': - currentBoard.setEXGChannelActive(16-1, true); - break; + return; //other controls case 's': stopRunning(); - break; + return; case 'b': startRunning(); - break; + return; ///////////////////// Save User settings lowercase n case 'n': println("Save key pressed!"); settings.save(settings.getPath("User", eegDataSource, nchan)); outputSuccess("Settings Saved! The GUI will now load with these settings. Click \"Default\" to revert to factory settings."); - break; + return; ///////////////////// Load User settings uppercase N case 'N': println("Load key pressed!"); settings.loadKeyPressed(); - break; + return; case '?': if(currentBoard instanceof BoardCyton) { ((BoardCyton)currentBoard).printRegisters(); } - break; + return; case 'd': - break; + return; case 'm': String picfname = "OpenBCI-" + directoryManager.getFileNameDateTime() + ".jpg"; //println("OpenBCI_GUI: 'm' was pressed...taking screenshot:" + picfname); saveFrame(directoryManager.getGuiDataPath() + "Screenshots" + System.getProperty("file.separator") + picfname); // take a shot of that! output("Screenshot captured! Saved to /Documents/OpenBCI_GUI/Screenshots/" + picfname); - break; - + return; default: - if (currentBoard instanceof Board) { - println("Interactivity: '" + key + "' Pressed...sending to Board..."); - ((Board)currentBoard).sendCommand(str(key)); - } break; } + + if (nchan > 4) { + switch (val) { + case '5': + currentBoard.setEXGChannelActive(5-1, false); + return; + case '6': + currentBoard.setEXGChannelActive(6-1, false); + return; + case '7': + currentBoard.setEXGChannelActive(7-1, false); + return; + case '8': + currentBoard.setEXGChannelActive(8-1, false); + return; + case '%': + currentBoard.setEXGChannelActive(5-1, true); + return; + case '^': + currentBoard.setEXGChannelActive(6-1, true); + return; + case '&': + currentBoard.setEXGChannelActive(7-1, true); + return; + case '*': + currentBoard.setEXGChannelActive(8-1, true); + return; + default: + break; + } + } + + if (nchan > 8) { + switch (val) { + case 'q': + currentBoard.setEXGChannelActive(9-1, false); + return; + case 'w': + currentBoard.setEXGChannelActive(10-1, false); + return; + case 'e': + currentBoard.setEXGChannelActive(11-1, false); + return; + case 'r': + currentBoard.setEXGChannelActive(12-1, false); + return; + case 't': + currentBoard.setEXGChannelActive(13-1, false); + return; + case 'y': + currentBoard.setEXGChannelActive(14-1, false); + return; + case 'u': + currentBoard.setEXGChannelActive(15-1, false); + return; + case 'i': + currentBoard.setEXGChannelActive(16-1, false); + return; + case 'Q': + currentBoard.setEXGChannelActive(9-1, true); + return; + case 'W': + currentBoard.setEXGChannelActive(10-1, true); + return; + case 'E': + currentBoard.setEXGChannelActive(11-1, true); + return; + case 'R': + currentBoard.setEXGChannelActive(12-1, true); + return; + case 'T': + currentBoard.setEXGChannelActive(13-1, true); + return; + case 'Y': + currentBoard.setEXGChannelActive(14-1, true); + return; + case 'U': + currentBoard.setEXGChannelActive(15-1, true); + return; + case 'I': + currentBoard.setEXGChannelActive(16-1, true); + return; + default: + break; + } + } + + if (currentBoard instanceof Board) { + output("Expert Mode: '" + key + "' pressed. This is not assigned or applicable to current setup."); + //((Board)currentBoard).sendCommand(str(key)); + } } void mouseDragged() { diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index a48a81a8f..880cb5327 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -64,8 +64,8 @@ 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.2"; -String localGUIVersionDate = "December 2020"; +String localGUIVersionString = "v5.0.3"; +String localGUIVersionDate = "January 2021"; 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/SessionSettings.pde b/OpenBCI_GUI/SessionSettings.pde index fcf050386..7fb6c8968 100644 --- a/OpenBCI_GUI/SessionSettings.pde +++ b/OpenBCI_GUI/SessionSettings.pde @@ -61,9 +61,6 @@ class SessionSettings { CColor dropdownColors = new CColor(); ///These `Save` vars are set to default when each widget instantiates ///and updated every time user selects from dropdown - //Notch and Bandpass filter variables for save - int dataProcessingNotchSave = 0; - int dataProcessingBandpassSave = 3; //Accelerometer settings int accVertScaleSave; int accHorizScaleSave; @@ -171,11 +168,6 @@ class SessionSettings { String[] spectMaxFrqArray = {"20 Hz", "40 Hz", "60 Hz", "100 Hz", "120 Hz", "250 Hz"}; String[] spectSampleRateArray = {"1 Hz", "5 hz", "10 Hz", "20 Hz", "40 Hz"}; - //Load global settings variables - int loadLayoutSetting; - int loadNotchSetting; - int loadBandpassSetting; - //Load Accel. dropdown variables int loadAccelVertScale; int loadAccelHorizScale; @@ -376,10 +368,13 @@ class SessionSettings { JSONObject saveGlobalSettings = new JSONObject(); saveGlobalSettings.setBoolean("Expert Mode", expertModeToggle); saveGlobalSettings.setInt("Current Layout", currentLayout); - saveGlobalSettings.setInt("Notch", dataProcessingNotchSave); - saveGlobalSettings.setInt("Bandpass Filter", dataProcessingBandpassSave); + saveGlobalSettings.setInt("Notch", dataProcessing.bsRange.getIndex()); + saveGlobalSettings.setInt("Bandpass Filter", dataProcessing.bpRange.getIndex()); saveGlobalSettings.setInt("Analog Read Vert Scale", arVertScaleSave); saveGlobalSettings.setInt("Analog Read Horiz Scale", arHorizScaleSave); + if (currentBoard instanceof SmoothingCapableBoard) { + saveGlobalSettings.setBoolean("Data Smoothing", ((SmoothingCapableBoard)currentBoard).getSmoothingActive()); + } saveSettingsJSONData.setJSONObject(kJSONKeySettings, saveGlobalSettings); /////Setup JSON Object for gui version and settings Version @@ -585,15 +580,15 @@ class SessionSettings { //get the global settings JSON object JSONObject loadGlobalSettings = loadSettingsJSONData.getJSONObject(kJSONKeySettings); - loadLayoutSetting = loadGlobalSettings.getInt("Current Layout"); - loadNotchSetting = loadGlobalSettings.getInt("Notch"); - loadBandpassSetting = loadGlobalSettings.getInt("Bandpass Filter"); - Boolean loadExpertModeToggle = loadGlobalSettings.getBoolean("Expert Mode"); + //Store loaded layout to current layout variable + currentLayout = loadGlobalSettings.getInt("Current Layout"); loadAnalogReadVertScale = loadGlobalSettings.getInt("Analog Read Vert Scale"); loadAnalogReadHorizScale = loadGlobalSettings.getInt("Analog Read Horiz Scale"); - //Store loaded layout to current layout variable - currentLayout = loadLayoutSetting; //Load more global settings after this line, if needed + int loadNotchSetting = loadGlobalSettings.getInt("Notch"); + int loadBandpassSetting = loadGlobalSettings.getInt("Bandpass Filter"); + Boolean loadExpertModeToggle = loadGlobalSettings.getBoolean("Expert Mode"); + Boolean loadDataSmoothingSetting = (currentBoard instanceof SmoothingCapableBoard) ? loadGlobalSettings.getBoolean("Data Smoothing") : null; //get the FFT settings JSONObject loadFFTSettings = loadSettingsJSONData.getJSONObject(kJSONKeyFFT); @@ -717,8 +712,8 @@ class SessionSettings { //get the Widget/Container settings JSONObject loadWidgetSettings = loadSettingsJSONData.getJSONObject(kJSONKeyWidget); //Apply Layout directly before loading and applying widgets to containers - wm.setNewContainerLayout(loadLayoutSetting); - verbosePrint("LoadGUISettings: Layout " + loadLayoutSetting + " Loaded!"); + wm.setNewContainerLayout(currentLayout); + verbosePrint("LoadGUISettings: Layout " + currentLayout + " Loaded!"); numLoadedWidgets = loadWidgetSettings.size(); @@ -749,11 +744,28 @@ class SessionSettings { ///////////////////////////////////////////////////////////// // Load more widget settings above this line as above // + ///////////////////////////////////////////////////////////// - //}//end case for all objects in JSON + ///////////////////////////////////////////////////////////// + // Apply Settings below this line // + ///////////////////////////////////////////////////////////// + + //Apply notch + dataProcessing.bsRange = BandStopRanges.getByIndex(loadNotchSetting); + topNav.filtNotchButton.getCaptionLabel().setText("Notch\n" + dataProcessing.getShortNotchDescription()); + //Apply Bandpass filter + dataProcessing.bpRange = BandPassRanges.getByIndex(loadBandpassSetting); + topNav.filtBPButton.getCaptionLabel().setText("BP Filt\n" + dataProcessing.getShortFilterDescription()); + + //Apply Data Smoothing for capable boards + if (currentBoard instanceof SmoothingCapableBoard) { + ((SmoothingCapableBoard)currentBoard).setSmoothingActive(loadDataSmoothingSetting); + topNav.updateSmoothingButtonText(); + } //Apply Expert Mode toggle - topNav.configSelector.toggleExpertMode(loadExpertModeToggle); + //This should not be loaded with other session settings - RW Jan 2021 + //topNav.configSelector.toggleExpertMode(loadExpertModeToggle); //Load and apply all of the settings that are in dropdown menus. It's a bit much, so it has it's own function below. loadApplyWidgetDropdownText(); @@ -987,7 +999,7 @@ class SessionSettings { w_timeSeries.cp5_widget.getController("VertScale_TS").getCaptionLabel().setText(w_timeSeries.getTSVertScale().getString()); //changes front-end w_timeSeries.setTSHorizScale(loadTimeSeriesSettings.getInt("Time Series Horiz Scale")); - w_timeSeries.cp5_widget.getController("Duration").getCaptionLabel().setText(w_timeSeries.getTSVertScale().getString()); + w_timeSeries.cp5_widget.getController("Duration").getCaptionLabel().setText(w_timeSeries.getTSHorizScale().getString()); JSONArray loadTSChan = loadTimeSeriesSettings.getJSONArray("activeChannels"); w_timeSeries.tsChanSelect.deactivateAllButtons(); diff --git a/OpenBCI_GUI/TopNav.pde b/OpenBCI_GUI/TopNav.pde index 78b7acbe4..448dec2b9 100644 --- a/OpenBCI_GUI/TopNav.pde +++ b/OpenBCI_GUI/TopNav.pde @@ -398,6 +398,10 @@ class TopNav { return val; } + public void updateSmoothingButtonText() { + smoothingButton.getCaptionLabel().setText(getSmoothingString()); + } + private String getSmoothingString() { return ((SmoothingCapableBoard)currentBoard).getSmoothingActive() ? "Smoothing\n On" : "Smoothing\n Off"; } diff --git a/OpenBCI_GUI/W_Networking.pde b/OpenBCI_GUI/W_Networking.pde index 59f53c5ab..e82ded28b 100644 --- a/OpenBCI_GUI/W_Networking.pde +++ b/OpenBCI_GUI/W_Networking.pde @@ -13,7 +13,8 @@ // Created by: Gabriel Ibagon (github.com/gabrielibagon), January 2017 // /////////////////////////////////////////////////////////////////////////////// - +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; class W_Networking extends Widget { @@ -1201,12 +1202,15 @@ class Stream extends Thread { String streamName; int nChanLSL; int numChan = 0; + DecimalFormat threeDecimalPlaces; + DecimalFormat fourLeadingPlaces; Boolean isStreaming; Boolean newData = false; // Data buffers set dynamically in updateNumChan() int start; float[] dataToSend; + private double[][] previousFrameData; //OSC Objects OscP5 osc; @@ -1266,6 +1270,15 @@ class Stream extends Thread { this.filter = filter; this.isStreaming = false; updateNumChan(_nchan); + + //Force decimal formatting for all Locales + Locale currentLocale = Locale.getDefault(); + DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(currentLocale); + otherSymbols.setDecimalSeparator('.'); + otherSymbols.setGroupingSeparator(','); + threeDecimalPlaces = new DecimalFormat("0.000", otherSymbols); + fourLeadingPlaces = new DecimalFormat("####", otherSymbols); + if (this.dataType.equals("TimeSeries")) { buffer = ByteBuffer.allocate(4*numChan); } else { @@ -1336,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 { @@ -1356,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")) { @@ -1375,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; } @@ -1799,7 +1813,8 @@ class Stream extends Thread { String outputter = "{\"type\":\"accelerometer\",\"data\":["; for (int i = 0; i < NUM_ACCEL_DIMS; i++) { float accelData = w_accelerometer.getLastAccelVal(i); - String accelData_3dec = String.format("%.3f", accelData); + String accelData_3dec = threeDecimalPlaces.format(accelData); + //String accelData_3dec = String.format("%.3f", accelData); //This does not work in all international settings outputter += accelData_3dec; if (i != NUM_ACCEL_DIMS - 1) { outputter += ","; @@ -1853,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 = String.format("%04d", 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()); + } } } @@ -1981,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); @@ -1999,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 { @@ -2013,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); diff --git a/OpenBCI_GUI/W_Spectrogram.pde b/OpenBCI_GUI/W_Spectrogram.pde index c93b066c1..02a01321c 100644 --- a/OpenBCI_GUI/W_Spectrogram.pde +++ b/OpenBCI_GUI/W_Spectrogram.pde @@ -424,9 +424,6 @@ class W_Spectrogram extends Widget { //triggered when there is an event in the Spectrogram Widget MaxFreq. Dropdown void SpectrogramMaxFreq(int n) { settings.spectMaxFrqSave = n; - //Link the choices made in the FFT widget and the Spectrogram Widget for this parameter - MaxFreq(n); - w_fft.cp5_widget.getController("MaxFreq").getCaptionLabel().setText(settings.fftMaxFrqArray[n]); //reset the vertical axis labelss w_spectrogram.vertAxisLabel = w_spectrogram.vertAxisLabels[n]; //Resize the height of the data image