diff --git a/CHANGELOG.md b/CHANGELOG.md index 892b8d6a2..47646b90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# v5.0.5 + +### Improvements +* Implement Focus Widget using BrainFlow Metrics! #924 +* Throw a popup if users are are running an old version of Windows operating system. GUI v5 supports 64-bit Windows 8, 8.1, and 10. #964 +* Throw a popup if Windows users are using 32-bit Java and Processing. #964 +* Set Networking Widget default baud rate for Serial output to 57600 + +### Bug Fixes +* Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off +* Gracefully handle cases when Cyton or Cyton+Daisy users want to use 8 or 16 channels #954 +* Update Save Session Settings success message. Session settings are no longer auto-loaded on Session start. #969 +* Session settings are no longer auto-saved when system is halted #969 + # v5.0.4 ### Improvements diff --git a/Networking-Test-Kit/LSL/brainflow_lsl_cyton_accel.py b/Networking-Test-Kit/LSL/brainflow_lsl_cyton_accel.py new file mode 100644 index 000000000..e1f2ce737 --- /dev/null +++ b/Networking-Test-Kit/LSL/brainflow_lsl_cyton_accel.py @@ -0,0 +1,67 @@ +import argparse +import time +import numpy as np +import brainflow +from brainflow.board_shim import BoardShim, BrainFlowInputParams, LogLevels, BoardIds +from brainflow.data_filter import DataFilter, FilterTypes, AggOperations +from pylsl import StreamInfo, StreamOutlet + +#from queue import Queue +BoardShim.enable_dev_board_logger() +params = BrainFlowInputParams() +params.serial_port = '/dev/cu.usbserial-DM00D7TW' +board = BoardShim(BoardIds.CYTON_BOARD.value, params) # added cyton board id here +srate = board.get_sampling_rate(BoardIds.CYTON_BOARD.value) +board.prepare_session() +board.start_stream() +eeg_chan = BoardShim.get_eeg_channels(BoardIds.CYTON_BOARD.value) +aux_chan = BoardShim.get_accel_channels(BoardIds.CYTON_BOARD.value) + +print('EEG channels:') +print(eeg_chan) +print('Accelerometer channels') +print(aux_chan) + +# define lsl streams + +# Defining stream info: +name = 'OpenBCIEEG' +ID = 'OpenBCIEEG' +channels = 8 +sample_rate = 250 +datatype = 'float32' +streamType = 'EEG' + +print(f"Creating LSL stream for EEG. \nName: {name}\nID: {ID}\n") +info_eeg = StreamInfo(name, streamType, channels, sample_rate, datatype, ID) +chns = info_eeg.desc().append_child("channels") +for label in ["AFp1", "AFp2", "C3", "C4", "P7", "P8", "O1", "O2"]: + ch = chns.append_child("channel") + ch.append_child_value("label", label) +info_aux = StreamInfo('OpenBCIAUX', 'AUX', 3, 250, 'float32', 'OpenBCItestAUX') +chns = info_aux.desc().append_child("channels") +for label in ["X", "Y", "Z"]: + ch = chns.append_child("channel") + ch.append_child_value("label", label) +outlet_aux = StreamOutlet(info_aux) +outlet_eeg = StreamOutlet(info_eeg) + +# construct a numpy array that contains only eeg channels and aux channels with correct scaling +# this streams to lsl +while True: + data = board.get_board_data() # this gets data continiously + # don't send empty data + if len(data[0]) < 1 : continue + eeg_data = data[eeg_chan] + aux_data = data[aux_chan] + #print(scaled_eeg_data) + #print(scaled_aux_data) + #print('------------------------------------------------------------------------------------------') + eegchunk = [] + for i in range(len(eeg_data[0])): + eegchunk.append((eeg_data[:,i]).tolist()) #scale data here + outlet_eeg.push_chunk(eegchunk) + auxchunk = [] + for i in range(len(aux_data[0])): + auxchunk.append((aux_data[:,i]).tolist()) #scale data here + outlet_aux.push_chunk(auxchunk) \ No newline at end of file diff --git a/Networking-Test-Kit/splitLargeCSV.py b/Networking-Test-Kit/splitLargeCSV.py new file mode 100644 index 000000000..e385ebd4a --- /dev/null +++ b/Networking-Test-Kit/splitLargeCSV.py @@ -0,0 +1,40 @@ + +import os +import csv + + +def split(filehandler, delimiter=',', row_limit=4000000, + output_name_template='output_%s.csv', output_path='.', keep_headers=True): + + reader = csv.reader(filehandler, delimiter=delimiter) + current_piece = 1 + current_out_path = os.path.join( + output_path, + output_name_template % current_piece + ) + current_out_writer = csv.writer(open(current_out_path, 'w', newline=''), delimiter=delimiter) + current_limit = row_limit + headers = [] + if keep_headers: + for i in range (5): + headerRow = next(reader); + headers.append(headerRow) + current_out_writer.writerow(headerRow) + + for i, row in enumerate(reader): + if i + 1 > current_limit: + current_piece += 1 + current_limit = row_limit * current_piece + current_out_path = os.path.join( + output_path, + output_name_template % current_piece + ) + current_out_writer = csv.writer(open(current_out_path, 'w', newline=''), delimiter=delimiter) + if keep_headers: + for headerRowI in headers: + current_out_writer.writerow(headerRowI) + print (headerRowI) + print (i) + current_out_writer.writerow(row) + +split(open('OpenBCI-RAW-2021-04-05_21-49-12.txt', 'r')); \ No newline at end of file diff --git a/OpenBCI_GUI/ADS1299SettingsController.pde b/OpenBCI_GUI/ADS1299SettingsController.pde index 97dace96a..687701c66 100644 --- a/OpenBCI_GUI/ADS1299SettingsController.pde +++ b/OpenBCI_GUI/ADS1299SettingsController.pde @@ -218,6 +218,7 @@ class ADS1299SettingsController { resizeDropdowns(chanBar_h); resizeCustomCommandUI(); + } //Returns true if board and UI are in sync @@ -382,10 +383,10 @@ class ADS1299SettingsController { } private void createCustomCommandUI() { - final Textfield _tf = hwsCp5.addTextfield("customCommand") + customCommandTF = hwsCp5.addTextfield("customCommand") .setPosition(0, 0) .setCaptionLabel("") - .setSize(10, 10) + .setSize(120, 20) .setFont(f2) .setFocus(false) .setColor(color(26, 26, 26)) @@ -398,22 +399,21 @@ class ADS1299SettingsController { .align(5, 10, 20, 40) .setAutoClear(false) //Don't clear textfield when pressing Enter key ; - _tf.setDescription("Type a custom command and Send to board."); + customCommandTF.setDescription("Type a custom command and Send to board."); //Clear textfield on double click - _tf.onDoublePress(new CallbackListener() { + customCommandTF.onDoublePress(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { output("[ExpertMode] Enter the custom command you would like to send to the board."); - _tf.clear(); + customCommandTF.clear(); } }); - _tf.addCallback(new CallbackListener() { + customCommandTF.addCallback(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { if ((theEvent.getAction() == ControlP5.ACTION_BROADCAST) || (theEvent.getAction() == ControlP5.ACTION_LEAVE)) { - _tf.setFocus(false); + customCommandTF.setFocus(false); } } }); - customCommandTF = _tf; sendCustomCmdButton = createButton(hwsCp5, "sendCustomCommand", "Send Custom Command", 0, 0, 10, 10); sendCustomCmdButton.setBorderColor(OBJECT_BORDER_GREY); @@ -434,7 +434,7 @@ class ADS1299SettingsController { resizeCustomCommandUI(); } - private void resizeCustomCommandUI() { + public void resizeCustomCommandUI() { customCmdUI_x = x; customCmdUI_w = w + 1; int tf_w = Math.round(button_w * 1.8); @@ -445,7 +445,8 @@ class ADS1299SettingsController { //int tf_w = Math.round((customCmdUI_w - padding_3*2) * .75); int tf_h = commandBarH - padding_3*2; customCommandTF.setPosition(tf_x, tf_y); - customCommandTF.setSize(tf_w, tf_h); + customCommandTF.setWidth(tf_w); + customCommandTF.setHeight(tf_h); int but_x = tf_x + customCommandTF.getWidth() + padding_3; sendCustomCmdButton.setPosition(but_x, tf_y); sendCustomCmdButton.setSize(but_w, tf_h - 1); diff --git a/OpenBCI_GUI/DataProcessing.pde b/OpenBCI_GUI/DataProcessing.pde index 6d39cbb52..49bcdcd1b 100644 --- a/OpenBCI_GUI/DataProcessing.pde +++ b/OpenBCI_GUI/DataProcessing.pde @@ -33,7 +33,6 @@ void processNewData() { //update the data buffers for (int Ichan=0; Ichan < channelCount; Ichan++) { - for(int i = 0; i < getCurrentBoardBufferSize(); i++) { dataProcessingRawBuffer[Ichan][i] = (float)currentData.get(i)[exgChannels[Ichan]]; } diff --git a/OpenBCI_GUI/Extras.pde b/OpenBCI_GUI/Extras.pde index 50a407345..6076e5abe 100644 --- a/OpenBCI_GUI/Extras.pde +++ b/OpenBCI_GUI/Extras.pde @@ -26,6 +26,22 @@ private boolean isMac() { return !isWindows() && !isLinux(); } +//BrainFlow only supports Windows 8 and 10. This will help with OpenBCI support tickets. #964 +private void checkIsOldVersionOfWindowsOS() { + boolean isOld = SystemUtils.IS_OS_WINDOWS_7 || SystemUtils.IS_OS_WINDOWS_VISTA || SystemUtils.IS_OS_WINDOWS_XP; + if (isOld) { + PopupMessage msg = new PopupMessage("Old Windows OS Detected", "OpenBCI GUI v5 and BrainFlow are made for 64-bit Windows 8, 8.1, and 10. Please update your OS, computer, or revert to GUI v4.2.0."); + } +} + +//Sanity check for 64-bit Java for Windows users #964 +private void checkIs64BitJava() { + boolean is64Bit = System.getProperty("sun.arch.data.model").indexOf("64") >= 0; + if (!is64Bit) { + PopupMessage msg = new PopupMessage("32-bit Java Detected", "OpenBCI GUI v5 and BrainFlow are made for 64-bit Java (Windows, Linux, and Mac). Please update your OS, computer, Processing IDE, or revert to GUI v4 or earlier."); + } +} + //compute the standard deviation float std(float[] data) { diff --git a/OpenBCI_GUI/FocusEnums.pde b/OpenBCI_GUI/FocusEnums.pde new file mode 100644 index 000000000..e6bfe7dd7 --- /dev/null +++ b/OpenBCI_GUI/FocusEnums.pde @@ -0,0 +1,185 @@ +/// Here are the enums used by the Focus Widget, found in W_Focus.pde + +// color enums +public enum FocusColors { + GREEN, CYAN, ORANGE +} + +interface FocusEnum { + public int getIndex(); + public String getString(); +} + +public enum FocusXLim implements FocusEnum +{ + FIVE (0, 5, "5 sec"), + TEN (1, 10, "10 sec"), + TWENTY (2, 20, "20 sec"), + THIRTY (3, 30, "30 sec"); + + private int index; + private int value; + private String label; + + private static FocusXLim[] vals = values(); + + FocusXLim(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } +} + +public enum FocusMetric implements FocusEnum +{ + CONCENTRATION (0, "Concentration", BrainFlowMetrics.CONCENTRATION, "Concentrating"), + RELAXATION (1, "Relaxation", BrainFlowMetrics.RELAXATION, "Relaxing"); + + private int index; + private String label; + private BrainFlowMetrics metric; + private String idealState; + + private static FocusMetric[] vals = values(); + + FocusMetric(int _index, String _label, BrainFlowMetrics _metric, String _idealState) { + this.index = _index; + this.label = _label; + this.metric = _metric; + this.idealState = _idealState; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public BrainFlowMetrics getMetric() { + return metric; + } + + public String getIdealStateString() { + return idealState; + } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } +} + +public enum FocusClassifier implements FocusEnum +{ + REGRESSION (0, "Regression", BrainFlowClassifiers.REGRESSION), + KNN (1, "KNN", BrainFlowClassifiers.KNN), + SVM (2, "SVM", BrainFlowClassifiers.SVM), + LDA (3, "LDA", BrainFlowClassifiers.LDA); + + private int index; + private int value; + private String label; + private BrainFlowClassifiers classifier; + + private static FocusClassifier[] vals = values(); + + FocusClassifier(int _index, String _label, BrainFlowClassifiers _classifier) { + this.index = _index; + this.label = _label; + this.classifier = _classifier; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public BrainFlowClassifiers getClassifier() { + return classifier; + } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } +} + +public enum FocusThreshold implements FocusEnum +{ + FIVE_TENTHS (0, .5, "0.5"), + SIX_TENTHS (1, .6, "0.6"), + SEVEN_TENTHS (2, .7, "0.7"), + EIGHT_TENTHS (3, .8, "0.8"), + NINE_TENTHS (4, .9, "0.9"); + + private int index; + private float value; + private String label; + + private static FocusThreshold[] vals = values(); + + FocusThreshold(int _index, float _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public float getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/Info.plist.tmpl b/OpenBCI_GUI/Info.plist.tmpl index 26c6b4ca7..0db5432a2 100644 --- a/OpenBCI_GUI/Info.plist.tmpl +++ b/OpenBCI_GUI/Info.plist.tmpl @@ -21,9 +21,9 @@ CFBundleShortVersionString - 4 + 5 CFBundleVersion - 5.0.4 + 5.0.5 CFBundleSignature ???? NSHumanReadableCopyright @@ -32,7 +32,7 @@ Copyright © 2021 OpenBCI CFBundleGetInfoString - March 2021 + May 2021 @@jvm_runtime@@ diff --git a/OpenBCI_GUI/Interactivity.pde b/OpenBCI_GUI/Interactivity.pde index 3a187b7b8..efe377a3b 100644 --- a/OpenBCI_GUI/Interactivity.pde +++ b/OpenBCI_GUI/Interactivity.pde @@ -106,14 +106,14 @@ void parseKey(char val) { ///////////////////// Save User settings lowercase n case 'n': - println("Save key pressed!"); + println("Interactivity: 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."); + outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory settings."); return; ///////////////////// Load User settings uppercase N case 'N': - println("Load key pressed!"); + println("Interactivity: Load key pressed!"); settings.loadKeyPressed(); return; diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index e2d4b2486..0462d5822 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -51,11 +51,6 @@ import oscP5.*; // for OSC import hypermedia.net.*; //for UDP import java.nio.ByteBuffer; //for UDP import edu.ucsd.sccn.LSL; //for LSL -//These are used by LSL -//import com.sun.jna.Library; -//import com.sun.jna.Native; -//import com.sun.jna.Platform; -//import com.sun.jna.Pointer; import com.fazecast.jSerialComm.*; //Helps distinguish serial ports on Windows import org.apache.commons.lang3.time.StopWatch; import http.requests.*; @@ -65,8 +60,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.4"; -String localGUIVersionDate = "March 2021"; +String localGUIVersionString = "v5.0.5"; +String localGUIVersionDate = "May 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; @@ -363,18 +358,22 @@ void setup() { System.setOut(outputStream); System.setErr(outputStream); - String osName = "Operating System: "; + StringBuilder osName = new StringBuilder("Operating System: "); if (isLinux()) { - osName += "Linux"; + osName.append("Linux"); } else if (isWindows()) { - osName += "Windows"; + osName.append("Windows"); + //Throw a popup if we detect an incompatible version of Windows. Fixes #964. Found in Extras.pde. + checkIsOldVersionOfWindowsOS(); + //This is an edge case when using 32-bit Processing Java on Windows. Throw a popup if detected. + checkIs64BitJava(); } else if (isMac()) { - osName += "Mac"; + osName.append("Mac"); } println("Console Log Started at Local Time: " + directoryManager.getFileNameDateTime()); println("Screen Resolution: " + displayWidth + " X " + displayHeight); - println(osName); + println(osName.toString()); println("Welcome to the Processing-based OpenBCI GUI!"); //Welcome line. println("For more information, please visit: https://openbci.github.io/Documentation/docs/06Software/01-OpenBCISoftware/GUIDocs"); @@ -571,6 +570,27 @@ void initSystem() { // initialize the chosen board boolean success = currentBoard.initialize(); abandonInit = !success; // abandon if init fails + + //Handle edge cases for Cyton and Cyton+Daisy users immediately after board is initialized. Fixes #954 + if (eegDataSource == DATASOURCE_CYTON) { + println("OpenBCI_GUI: Configuring Cyton Channel Count..."); + if (currentBoard instanceof BoardCytonSerial) { + Pair res = ((BoardBrainFlow)currentBoard).sendCommand("c"); + //println(res.getKey().booleanValue(), res.getValue()); + if (res.getValue().startsWith("daisy removed")) { + println("OpenBCI_GUI: Daisy is physically attached, using Cyton 8 Channels instead."); + } + } else if (currentBoard instanceof BoardCytonSerialDaisy) { + Pair res = ((BoardBrainFlow)currentBoard).sendCommand("C"); + //println(res.getKey().booleanValue(), res.getValue()); + if (res.getValue().startsWith("no daisy to attach")) { + haltSystem(); + outputError("User selected Cyton+Daisy, but no Daisy is attached. Please change Channel Count to 8 Channels."); + controlPanel.open(); + return; + } + } + } updateToNChan(currentBoard.getNumEXGChannels()); @@ -724,7 +744,7 @@ void stopRunning() { //halt the data collection void haltSystem() { - if (!systemHasHalted) { //prevents system from halting more than once\ + if (!systemHasHalted) { //prevents system from halting more than once println("openBCI_GUI: haltSystem: Halting system for reconfiguration of settings..."); //Reset the text for the Start Session buttonscreen. Skip when reiniting board while already in playback mode session. @@ -736,20 +756,16 @@ void haltSystem() { w_networking.stopNetwork(); println("openBCI_GUI: haltSystem: Network streams stopped"); } + + if (w_focus != null) { + w_focus.endSession(); + } stopRunning(); //stop data transfer topNav.resetStartStopButton(); topNav.destroySmoothingButton(); //Destroy this button if exists and make null, will be re-init if needed next time session starts - //Save a snapshot of User's GUI settings if the system is stopped, or halted. This will be loaded on next Start System. - //This method establishes default and user settings for all data modes - if (systemMode == SYSTEMMODE_POSTINIT && - eegDataSource != DATASOURCE_GALEA && - eegDataSource != DATASOURCE_STREAMING) { - settings.save(settings.getPath("User", eegDataSource, nchan)); - } - //reset connect loadStrings openBCI_portName = "N/A"; // Fixes inability to reconnect after halding JAM 1/2017 ganglion_portName = ""; @@ -887,7 +903,7 @@ void updateToNChan(int _nchan) { nchan = _nchan; settings.slnchan = _nchan; //used in SoftwareSettings.pde only fftBuff = new FFT[nchan]; //reinitialize the FFT buffer - println("Channel count set to " + str(nchan)); + println("OpenBCI_GUI: Channel count set to " + str(nchan)); } void introAnimation() { diff --git a/OpenBCI_GUI/SessionSettings.pde b/OpenBCI_GUI/SessionSettings.pde index 7fb6c8968..a6fb91bfa 100644 --- a/OpenBCI_GUI/SessionSettings.pde +++ b/OpenBCI_GUI/SessionSettings.pde @@ -145,7 +145,7 @@ class SessionSettings { //Used to set text in dropdown menus when loading Networking settings String[] nwProtocolArray = {"Serial", "LSL", "UDP", "OSC"}; - String[] nwDataTypesArray = {"None", "TimeSeries", "FFT", "EMG", "BandPower", "Accel/Aux", "Pulse"}; + String[] nwDataTypesArray = {"None", "TimeSeries", "Focus", "EMG", "BandPower", "Accel/Aux", "FFT", "Pulse"}; String[] nwBaudRatesArray = {"57600", "115200", "250000", "500000"}; //Used to set text in dropdown menus when loading Analog Read settings @@ -1171,7 +1171,7 @@ void saveConfigFile(File selection) { println("SessionSettings: saveConfigFile: User selected " + selection.getAbsolutePath()); settings.saveDialogName = selection.getAbsolutePath(); settings.save(settings.saveDialogName); //save current settings to JSON file in SavedData - outputSuccess("Settings Saved! The GUI will now load with these settings. Click \"Default\" to revert to factory settings."); //print success message to screen + outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory settings."); //print success message to screen settings.saveDialogName = null; //reset this variable for future use } } diff --git a/OpenBCI_GUI/W_Accelerometer.pde b/OpenBCI_GUI/W_Accelerometer.pde index fd6df170f..705766baa 100644 --- a/OpenBCI_GUI/W_Accelerometer.pde +++ b/OpenBCI_GUI/W_Accelerometer.pde @@ -55,7 +55,7 @@ class W_Accelerometer extends Widget { //Default dropdown settings settings.accVertScaleSave = 0; - settings.accHorizScaleSave = 0; + settings.accHorizScaleSave = 3; //Make dropdowns addDropdown("accelVertScale", "Vert Scale", Arrays.asList(settings.accVertScaleArray), settings.accVertScaleSave); @@ -69,8 +69,8 @@ class W_Accelerometer extends Widget { //create our channel bar and populate our accelerometerBar array! accelerometerBar = new AccelerometerBar(_parent, accelXyzLimit, accelGraphX, accelGraphY, accelGraphWidth, accelGraphHeight); - accelerometerBar.adjustTimeAxis(w_timeSeries.getTSHorizScale().getValue()); //sync horiz axis to Time Series by default - accelerometerBar.adjustVertScale(yLimOptions[0]); + accelerometerBar.adjustTimeAxis(xLimOptions[settings.accHorizScaleSave]); + accelerometerBar.adjustVertScale(yLimOptions[settings.accVertScaleSave]); createAccelModeButton("accelModeButton", "Turn Accel. Off", (int)(x + 3), (int)(y + 3 - navHeight), 120, navHeight - 6, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); } diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde new file mode 100644 index 000000000..8a1e282eb --- /dev/null +++ b/OpenBCI_GUI/W_Focus.pde @@ -0,0 +1,537 @@ +//////////////////////////////////////////////////// +// // +// W_focus.pde (ie "Focus Widget") // +// Enums can be found in FocusEnums.pde // +// // +// // +// Created by: Richard Waltman, March 2021 // +// // +//////////////////////////////////////////////////// + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.Pair; + +import brainflow.BoardIds; +import brainflow.BoardShim; +import brainflow.BrainFlowClassifiers; +import brainflow.BrainFlowInputParams; +import brainflow.BrainFlowMetrics; +import brainflow.BrainFlowModelParams; +import brainflow.DataFilter; +import brainflow.LogLevels; +import brainflow.MLModel; + +class W_Focus extends Widget { + + //to see all core variables/methods of the Widget class, refer to Widget.pde + //put your custom variables here... + private ControlP5 focus_cp5; + private Button widgetTemplateButton; + private ChannelSelect focusChanSelect; + private boolean prevChanSelectIsVisible = false; + + private Grid dataGrid; + private final int numTableRows = 6; + private final int numTableColumns = 2; + private final int tableWidth = 142; + private int tableHeight = 0; + private int cellHeight = 10; + private DecimalFormat df = new DecimalFormat("#.0000"); + + private final int PAD_FIVE = 5; + private final int PAD_TWO = 2; + private final int METRIC_DROPDOWN_W = 100; + private final int CLASSIFIER_DROPDOWN_W = 80; + + private FocusBar focusBar; + private float focusBarHardYAxisLimit = 1.05f; //Provide slight "breathing room" to avoid GPlot error when metric value == 1.0 + FocusXLim xLimit = FocusXLim.TEN; + FocusMetric focusMetric = FocusMetric.RELAXATION; + FocusClassifier focusClassifier = FocusClassifier.REGRESSION; + FocusThreshold focusThreshold = FocusThreshold.EIGHT_TENTHS; + private FocusColors focusColors = FocusColors.GREEN; + + int[] exgChannels; + int channelCount; + double[][] dataArray; + + MLModel mlModel; + private double metricPrediction = 0d; + private boolean predictionExceedsThreshold = false; + + private float xc, yc, wc, hc; // status circle center xy, width and height + private int graphX, graphY, graphW, graphH; + private int graphPadding = 30; + private color cBack, cDark, cMark, cFocus, cWave, cPanel; + + W_Focus(PApplet _parent) { + super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + + //Add channel select dropdown to this widget + focusChanSelect = new ChannelSelect(pApplet, this, x, y, w, navH, "FocusChannelSelect"); + focusChanSelect.activateAllButtons(); + + exgChannels = currentBoard.getEXGChannels(); + channelCount = currentBoard.getNumEXGChannels(); + dataArray = new double[channelCount][]; + + // initialize graphics parameters + onColorChange(); + + //This is the protocol for setting up dropdowns. + dropdownWidth = 60; //Override the default dropdown width for this widget + addDropdown("focusMetricDropdown", "Metric", FocusMetric.getEnumStringsAsList(), focusMetric.getIndex()); + addDropdown("focusClassifierDropdown", "Classifier", FocusClassifier.getEnumStringsAsList(), focusClassifier.getIndex()); + addDropdown("focusThresholdDropdown", "Threshold", FocusThreshold.getEnumStringsAsList(), focusThreshold.getIndex()); + addDropdown("focusWindowDropdown", "Window", FocusXLim.getEnumStringsAsList(), xLimit.getIndex()); + + + //Create data table + dataGrid = new Grid(numTableRows, numTableColumns, cellHeight); + dataGrid.setTableFontAndSize(p6, 10); + dataGrid.setDrawTableBorder(true); + dataGrid.setString("Metric Value", 0, 0); + dataGrid.setString("Delta (1.5-4Hz)", 1, 0); + dataGrid.setString("Theta (4-8Hz)", 2, 0); + dataGrid.setString("Alpha (7.5-13Hz)", 3, 0); + dataGrid.setString("Beta (13-30Hz)", 4, 0); + dataGrid.setString("Gamma (30-45Hz)", 5, 0); + + //Instantiate local cp5 for this box. This allows extra control of drawing cp5 elements specifically inside this class. + focus_cp5 = new ControlP5(ourApplet); + focus_cp5.setGraphics(ourApplet, 0,0); + focus_cp5.setAutoDraw(false); + + //create our focus graph + updateGraphDims(); + focusBar = new FocusBar(_parent, xLimit.getValue(), focusBarHardYAxisLimit, graphX, graphY, graphW, graphH); + + initBrainFlowMetric(); + } + + public void update() { + super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + + //Update channel checkboxes and active channels + focusChanSelect.update(x, y, w); + + //Flex the Gplot graph when channel select dropdown is open/closed + if (focusChanSelect.isVisible() != prevChanSelectIsVisible) { + channelSelectFlexWidgetUI(); + prevChanSelectIsVisible = focusChanSelect.isVisible(); + } + + if (currentBoard.isStreaming()) { + metricPrediction = updateFocusState(); + dataGrid.setString(df.format(metricPrediction), 0, 1); + focusBar.update(metricPrediction); + predictionExceedsThreshold = metricPrediction > focusThreshold.getValue(); + } + + //put your code here... + } + + public void draw() { + super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + //remember to refer to x,y,w,h which are the positioning variables of the Widget class + + //Draw data table + dataGrid.draw(); + + drawStatusCircle(); + + if (false) { + //Draw some guides to help develop this widget faster + pushStyle(); + stroke(0); + //Main guides + line(x, y+(h/2), x+w, y+(h/2)); + line(x+(w/2), y, x+(w/2), y+(h/2)); + //Top left container center + line(x+(w/4), y, x+(w/4), y+(h/2)); + line(x, y+(h/4), x+(w/2), y+(h/4)); + popStyle(); + } + + //This draws all cp5 objects in the local instance + focus_cp5.draw(); + + //Draw the graph + focusBar.draw(); + + focusChanSelect.draw(); + } + + public void screenResized() { + super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + + //Very important to allow users to interact with objects after app resize + focus_cp5.setGraphics(ourApplet, 0, 0); + + resizeTable(); + + //We need to set the position of our Cp5 object after the screen is resized + //widgetTemplateButton.setPosition(x + w/2 - widgetTemplateButton.getWidth()/2, y + h/2 - widgetTemplateButton.getHeight()/2); + + updateStatusCircle(); + + updateGraphDims(); + focusBar.screenResized(graphX, graphY, graphW, graphH); + focusChanSelect.screenResized(pApplet); + + //Custom resize these dropdowns due to longer text strings as options + cp5_widget.get(ScrollableList.class, "focusMetricDropdown").setWidth(METRIC_DROPDOWN_W); + cp5_widget.get(ScrollableList.class, "focusMetricDropdown").setPosition( + x0 + w0 - (dropdownWidth*2) - METRIC_DROPDOWN_W - CLASSIFIER_DROPDOWN_W - (PAD_TWO*4), + navH + y0 + PAD_TWO + ); + cp5_widget.get(ScrollableList.class, "focusClassifierDropdown").setWidth(CLASSIFIER_DROPDOWN_W); + cp5_widget.get(ScrollableList.class, "focusClassifierDropdown").setPosition( + x0 + w0 - (dropdownWidth*2) - CLASSIFIER_DROPDOWN_W - (PAD_TWO*3), + navH + y0 + PAD_TWO + ); + } + + void mousePressed() { + super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + focusChanSelect.mousePressed(this.dropdownIsActive); //Calls channel select mousePressed and checks if clicked + } + + private void resizeTable() { + int extraPadding = focusChanSelect.isVisible() ? navHeight : 0; + float upperLeftContainerW = w/2; + float upperLeftContainerH = h/2; + //float min = min(upperLeftContainerW, upperLeftContainerH); + int tx = x + int(upperLeftContainerW); + int ty = y + PAD_FIVE + extraPadding; + int tw = int(upperLeftContainerW) - PAD_FIVE*2; + //tableHeight = tw; + dataGrid.setDim(tx, ty, tw); + dataGrid.setTableHeight(int(upperLeftContainerH - PAD_FIVE*2)); + dataGrid.dynamicallySetTextVerticalPadding(0, 0); + dataGrid.setHorizontalCenterTextInCells(true); + } + + private void updateStatusCircle() { + float upperLeftContainerW = w/2; + float upperLeftContainerH = h/2; + float min = min(upperLeftContainerW, upperLeftContainerH); + xc = x + w/4; + yc = y + h/4; + wc = min * (3f/5); + hc = wc; + } + + private void updateGraphDims() { + graphW = int(w - PAD_FIVE*4); + graphH = int(h/2 - graphPadding - PAD_FIVE*2); + graphX = x + PAD_FIVE*2; + graphY = int(y + h/2); + } + + //Core method to fetch and process data + //Returns a metric value from 0. to 1. When there is an error, returns -1. + private double updateFocusState() { + try { + int windowSize = currentBoard.getSampleRate() * xLimit.getValue(); + // getData in GUI returns data in shape ndatapoints x nchannels, in BrainFlow its transposed + List currentData = currentBoard.getData(windowSize); + + if (currentData.size() != windowSize || focusChanSelect.activeChan.size() <= 0) { + return -1.0; + } + + for (int i = 0; i < channelCount; i++) { + dataArray[i] = new double[windowSize]; + for (int j = 0; j < currentData.size(); j++) { + dataArray[i][j] = currentData.get(j)[exgChannels[i]]; + } + } + + int[] channelsInDataArray = ArrayUtils.toPrimitive( + focusChanSelect.activeChan.toArray( + new Integer[focusChanSelect.activeChan.size()] + )); + + //Full Source Code for this method: https://github.com/brainflow-dev/brainflow/blob/c5f0ad86683e6eab556e30965befb7c93e389a3b/src/data_handler/data_handler.cpp#L1115 + Pair bands = DataFilter.get_avg_band_powers (dataArray, channelsInDataArray, currentBoard.getSampleRate(), true); + double[] featureVector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); + + //Left array is Averages, right array is Standard Deviations. Update values using Averages. + updateBandPowerTableValues(bands.getLeft()); + + //Keep this here + double prediction = mlModel.predict(featureVector); + //println("Concentration: " + prediction); + + return prediction; + + } catch (BrainFlowError e) { + e.printStackTrace(); + println("Error updating focus state!"); + return -1d; + } + } + + private void updateBandPowerTableValues(double[] bandPowers) { + for (int i = 0; i < bandPowers.length; i++) { + dataGrid.setString(df.format(bandPowers[i]), 1 + i, 1); + } + } + + private void drawStatusCircle() { + color fillColor; + color strokeColor; + StringBuilder sb = new StringBuilder(""); + if (predictionExceedsThreshold) { + fillColor = cFocus; + strokeColor = cFocus; + } else { + fillColor = cDark; + strokeColor = cDark; + sb.append("Not "); + } + sb.append(focusMetric.getIdealStateString()); + //Draw status graphic + pushStyle(); + noStroke(); + fill(fillColor); + stroke(strokeColor); + ellipseMode(CENTER); + ellipse(xc, yc, wc, hc); + noStroke(); + textAlign(CENTER); + text(sb.toString(), xc, yc + hc/2 + 16); + popStyle(); + } + + private void initBrainFlowMetric() { + BrainFlowModelParams modelParams = new BrainFlowModelParams( + focusMetric.getMetric().get_code(), + focusClassifier.getClassifier().get_code() + ); + mlModel = new MLModel (modelParams); + try { + mlModel.prepare(); + } catch (BrainFlowError e) { + e.printStackTrace(); + } + } + + //Called on haltSystem() when GUI exits or session stops + public void endSession() { + try { + mlModel.release(); + } catch (BrainFlowError e) { + e.printStackTrace(); + } + } + + private void onColorChange() { + switch(focusColors) { + case GREEN: + cBack = #ffffff; //white + cDark = #3068a6; //medium/dark blue + cMark = #4d91d9; //lighter blue + cFocus = #b8dc69; //theme green + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + case ORANGE: + cBack = #ffffff; //white + cDark = #377bc4; //medium/dark blue + cMark = #5e9ee2; //lighter blue + cFocus = #fcce51; //orange + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + case CYAN: + cBack = #ffffff; //white + cDark = #377bc4; //medium/dark blue + cMark = #5e9ee2; //lighter blue + cFocus = #91f4fc; //cyan + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + } + } + + void channelSelectFlexWidgetUI() { + focusBar.setPlotPosAndOuterDim(focusChanSelect.isVisible()); + int factor = focusChanSelect.isVisible() ? 1 : -1; + yc += navHeight * factor; + resizeTable(); + } + + public void setFocusHorizScale(int n) { + xLimit = xLimit.values()[n]; + focusBar.adjustTimeAxis(xLimit.getValue()); + } + + public void setMetric(int n) { + focusMetric = focusMetric.values()[n]; + endSession(); + initBrainFlowMetric(); + } + + public void setClassifier(int n) { + focusClassifier = focusClassifier.values()[n]; + endSession(); + initBrainFlowMetric(); + } + + public void setThreshold(int n) { + focusThreshold = focusThreshold.values()[n]; + } + + public int getMetricExceedsThreshold() { + return predictionExceedsThreshold ? 1 : 0; + } +}; //end of class + +//The following global functions are used by the Focus widget dropdowns. This method is the least amount of code. +public void focusWindowDropdown(int n) { + w_focus.setFocusHorizScale(n); +} + +public void focusMetricDropdown(int n) { + w_focus.setMetric(n); +} + +public void focusClassifierDropdown(int n) { + w_focus.setClassifier(n); +} + +public void focusThresholdDropdown(int n) { + w_focus.setThreshold(n); +} + +//This class contains the time series plot for the focus metric over time +class FocusBar { + int x, y, w, h; + int focusBarPadding = 30; + int xOffset; + final int nPoints = 30 * 1000; + + GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Series trace + LinkedList fifoList; + LinkedList fifoTimeList; + + int numSeconds; + color channelColor; //color of plot trace + + FocusBar(PApplet _parent, int xLimit, float yLimit, int _x, int _y, int _w, int _h) { //channel number, x/y location, height, width + x = _x; + y = _y; + w = _w; + h = _h; + if (eegDataSource == DATASOURCE_CYTON) { + xOffset = 22; + } else { + xOffset = 0; + } + numSeconds = xLimit; + + plot = new GPlot(_parent); + plot.setPos(x + 36 + 4 + xOffset, y); //match Accelerometer plot position with Time Series + plot.setDim(w - 36 - 4 - xOffset, h); + plot.setMar(0f, 0f, 0f, 0f); + plot.setLineColor((int)channelColors[(NUM_ACCEL_DIMS)%8]); + plot.setXLim(-numSeconds,0); //set the horizontal scale + plot.setYLim(0, yLimit); //change this to adjust vertical scale + //plot.setPointSize(2); + plot.setPointColor(0); + plot.getXAxis().setAxisLabelText("Time (s)"); + plot.getYAxis().setAxisLabelText("Metric Value"); + plot.setAllFontProperties("Arial", 0, 14); + plot.getXAxis().getAxisLabel().setOffset(float(22)); + plot.getYAxis().getAxisLabel().setOffset(float(focusBarPadding)); + + adjustTimeAxis(numSeconds); + + initArrays(); + + //set the plot points for X, Y, and Z axes + plot.addLayer("layer 1", new GPointsArray(30)); + plot.getLayer("layer 1").setLineColor(ACCEL_X_COLOR); + } + + private void initArrays() { + fifoList = new LinkedList(); + fifoTimeList = new LinkedList(); + for (int i = 0; i < nPoints; i++) { + fifoList.add(0f); + fifoTimeList.add(0f); + } + } + + public void update(double val) { + updateGPlotPoints(val); + } + + public void draw() { + plot.beginDraw(); + plot.drawBox(); //we won't draw this eventually ... + plot.drawGridLines(2); + plot.drawLines(); //Draw a Line graph! + //plot.drawPoints(); //Used to draw Points instead of Lines + plot.drawYAxis(); + plot.drawXAxis(); + plot.getXAxis().draw(); + plot.endDraw(); + } + + public void adjustTimeAxis(int _newTimeSize) { + numSeconds = _newTimeSize; + plot.setXLim(-_newTimeSize,0); + initArrays(); + //Set the number of axis divisions... + if (_newTimeSize > 1) { + plot.getXAxis().setNTicks(_newTimeSize); + }else{ + plot.getXAxis().setNTicks(10); + } + } + + //Used to update the Points within the graph + private void updateGPlotPoints(double val) { + float timerVal = (float)millis() / 1000.0; + fifoTimeList.removeFirst(); + fifoTimeList.addLast(timerVal); + fifoList.removeFirst(); + fifoList.addLast((float)val); + + int stopId = 0; + for (stopId = nPoints - 1; stopId > 0; stopId--) { + if (timerVal - fifoTimeList.get(stopId) > numSeconds) { + break; + } + } + int size = nPoints - 1 - stopId; + GPointsArray focusPoints = new GPointsArray(size); + for (int i = 0; i < size; i++) { + focusPoints.set(i, fifoTimeList.get(i + stopId) - timerVal, fifoList.get(i + stopId), ""); + } + plot.setPoints(focusPoints, "layer 1"); + } + + public void screenResized(int _x, int _y, int _w, int _h) { + x = _x; + y = _y; + w = _w; + h = _h; + //reposition & resize the plot + plot.setPos(x + 36 + 4 + xOffset, y); + plot.setDim(w - 36 - 4 - xOffset, h); + + } + + public void setPlotPosAndOuterDim(boolean chanSelectIsVisible) { + int _y = chanSelectIsVisible ? y + 22 : y; + int _h = chanSelectIsVisible ? h - 22 : h; + //reposition & resize the plot + plot.setPos(x + 36 + 4 + xOffset, _y); + plot.setDim(w - 36 - 4 - xOffset, _h); + } + +}; //end of class \ No newline at end of file diff --git a/OpenBCI_GUI/W_Networking.pde b/OpenBCI_GUI/W_Networking.pde index 4387aee77..fdc55a46e 100644 --- a/OpenBCI_GUI/W_Networking.pde +++ b/OpenBCI_GUI/W_Networking.pde @@ -148,7 +148,7 @@ class W_Networking extends Widget { if (eegDataSource != DATASOURCE_CYTON) { dataTypes.remove("Pulse"); } - defaultBaud = "115200"; + defaultBaud = "57600"; baudRates = Arrays.asList(settings.nwBaudRatesArray); protocolMode = "Serial"; //default to Serial addDropdown("Protocol", "Protocol", Arrays.asList(settings.nwProtocolArray), protocolIndex); @@ -1046,6 +1046,8 @@ class W_Networking extends Widget { private int getDataTypeNumChanLSL(String dataType) { if (dataType.equals("TimeSeries")) { return currentBoard.getNumEXGChannels(); + } else if (dataType.equals("Focus")) { + return 1; } else if (dataType.equals("FFT")) { return 125; } else if (dataType.equals("EMG")) { @@ -1389,6 +1391,8 @@ class Stream extends Thread { 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("Focus")) { + return w_networking.newDataToSend; } else if (this.dataType.equals("FFT")) { return w_networking.newDataToSend; } else if (this.dataType.equals("EMG")) { @@ -1406,6 +1410,8 @@ class Stream extends Thread { void setDataFalse() { if (this.dataType.equals("TimeSeries")) { w_networking.newDataToSend = false; + } else if (this.dataType.equals("Focus")) { + w_networking.newDataToSend = false; } else if (this.dataType.equals("FFT")) { w_networking.newDataToSend = false; } else if (this.dataType.equals("EMG")) { @@ -1422,6 +1428,8 @@ class Stream extends Thread { void sendData() { if (this.dataType.equals("TimeSeries")) { sendTimeSeriesData(); + } else if (this.dataType.equals("Focus")) { + sendFocusData(); } else if (this.dataType.equals("FFT")) { sendFFTData(); } else if (this.dataType.equals("EMG")) { @@ -1583,6 +1591,50 @@ class Stream extends Thread { } } + //Send out 1 or 0 as an integer over all networking data types for "Focus" data + //Filters do not apply to this data type + void sendFocusData() { + final int IS_METRIC = w_focus.getMetricExceedsThreshold(); + // OSC + if (this.protocol.equals("OSC")) { + msg.clearArguments(); + //ADD Focus Data + msg.add(IS_METRIC); + try { + this.osc.send(msg,this.netaddress); + } catch (Exception e) { + println(e.getMessage()); + } + // UDP + } else if (this.protocol.equals("UDP")) { + StringBuilder sb = new StringBuilder("{\"type\":\"focus\",\"data\":"); + sb.append(str(IS_METRIC)); + sb.append("}\r\n"); + try { + this.udp.send(sb.toString(), this.ip, this.port); + } catch (Exception e) { + println(e.getMessage()); + } + // LSL + } else if (this.protocol.equals("LSL")) { + dataToSend[0] = (float)IS_METRIC; + // Add timestamp to LSL Stream + outlet_data.push_sample(dataToSend, System.currentTimeMillis()); + // Serial + } else if (this.protocol.equals("Serial")) { // Send NORMALIZED EMG CHANNEL Data over Serial ... %%%%% + StringBuilder sb = new StringBuilder(); + sb.append(IS_METRIC); + sb.append("\n"); + try { + //println("SerialMessage: " + serialMessage); + this.serial_networking.write(sb.toString()); + } catch (Exception e) { + println("Networking Serial: Focus Error"); + println(e.getMessage()); + } + } + } + void sendFFTData() { // UNFILTERED & FILTERED ... influenced globally by the FFT filters dropdown //EEG/FFT readings above 125Hz don't typically travel through the skull diff --git a/OpenBCI_GUI/W_TimeSeries.pde b/OpenBCI_GUI/W_TimeSeries.pde index 51679735f..c1a3e2811 100644 --- a/OpenBCI_GUI/W_TimeSeries.pde +++ b/OpenBCI_GUI/W_TimeSeries.pde @@ -614,8 +614,8 @@ class ChannelBar { } private void updatePlotPoints() { - autoscaleMax = 0; - autoscaleMin = 0; + autoscaleMax = -Float.MAX_VALUE; + autoscaleMin = Float.MAX_VALUE; // update data in plot if (dataProcessingFilteredBuffer[channelIndex].length >= nPoints) { for (int i = dataProcessingFilteredBuffer[channelIndex].length - nPoints; i < dataProcessingFilteredBuffer[channelIndex].length; i++) { @@ -744,14 +744,15 @@ class ChannelBar { public void applyAutoscale() { //Do this once a second for all TimeSeries ChannelBars to save on resources - boolean doAutoscale = millis() > previousMillis + 1000; + int newMillis = millis(); + boolean doAutoscale = newMillis > previousMillis + 1000; if (isAutoscale && currentBoard.isStreaming() && doAutoscale) { - previousMillis = millis(); - float limit = Math.max(abs(autoscaleMin), autoscaleMax); - limit = Math.max(limit, 5); - plot.setYLim(-limit, limit); //<---- This is a very expensive method. Here is the bottleneck. - customYLim(yAxisMin, (int)-limit); - customYLim(yAxisMax, (int)limit); + autoscaleMin = (int) Math.floor(autoscaleMin); + autoscaleMax = (int) Math.ceil(autoscaleMax); + previousMillis = newMillis; + plot.setYLim(autoscaleMin, autoscaleMax); //<---- This is a very expensive method. Here is the bottleneck. + customYLim(yAxisMin, (int)autoscaleMin); + customYLim(yAxisMax, (int)autoscaleMax); } } diff --git a/OpenBCI_GUI/Widget.pde b/OpenBCI_GUI/Widget.pde index d4f6434d5..b342fcbe9 100644 --- a/OpenBCI_GUI/Widget.pde +++ b/OpenBCI_GUI/Widget.pde @@ -73,7 +73,6 @@ class Widget{ fill(200, 200, 200); rect(x0, y0+navH, w0, navH); //button bar popStyle(); - } public void addDropdown(String _id, String _title, List _items, int _defaultItem){ @@ -192,23 +191,21 @@ class Widget{ textAlign(CENTER, BOTTOM); fill(OPENBCI_DARKBLUE); for(int i = 0; i < dropdowns.size(); i++){ - int dropdownPos = dropdowns.size() - i; - // text(dropdowns.get(i).title, x+w-(dropdownWidth*(dropdownPos+1))-(2*(dropdownPos+1))+dropdownWidth/2, y+(navH-2)); - text(dropdowns.get(i).title, x0+w0-(dropdownWidth*(dropdownPos))-(2*(dropdownPos+1))+dropdownWidth/2, y0+(navH-2)); + int dropdownPos = dropdowns.size() - i; + int _width = cp5_widget.getController(dropdowns.get(i).id).getWidth(); + int _x = int(cp5_widget.getController(dropdowns.get(i).id).getPosition()[0]); + text(dropdowns.get(i).title, _x+_width/2, y0+(navH-2)); } popStyle(); } public void mouseDragged(){ - } public void mousePressed(){ - } public void mouseReleased(){ - } public void screenResized(){ @@ -292,7 +289,7 @@ class Widget{ previousDropdownIsActive = dropdownIsActive; } } -}; +}; //end of base Widget class /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -319,23 +316,18 @@ class NavBarDropdown{ } void update(){ - } void draw(){ - } void screenResized(){ - } void mousePressed(){ - } void mouseReleased(){ - } String returnDefaultAsString(){ @@ -365,7 +357,6 @@ void WidgetSelector(int n){ wm.widgets.get(n).setIsActive(true);//activate the new widget wm.widgets.get(n).setContainer(theContainer);//map it to the current container - //set the text of the widgetSelector to the newly selected widget } // This is a helpful class that will add a channel select feature to a Widget @@ -597,4 +588,4 @@ class ChannelSelect { } } -} //end of ChannelSelect class +} //end of ChannelSelect class \ No newline at end of file diff --git a/OpenBCI_GUI/WidgetManager.pde b/OpenBCI_GUI/WidgetManager.pde index 4d7c94d2b..04e9f98dc 100644 --- a/OpenBCI_GUI/WidgetManager.pde +++ b/OpenBCI_GUI/WidgetManager.pde @@ -24,6 +24,7 @@ W_DigitalRead w_digitalRead; W_playback w_playback; W_Spectrogram w_spectrogram; W_PacketLoss w_packetLoss; +W_Focus w_focus; //ADD YOUR WIDGET TO WIDGETS OF WIDGETMANAGER void setupWidgets(PApplet _this, ArrayList w){ @@ -63,6 +64,12 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_ganglionImpedance, w); } + //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 + w_focus = new W_Focus(_this); + w_focus.setTitle("Focus Widget"); + addWidget(w_focus, w); + // println(" setupWidgets focus widget -- " + millis()); + //Cyton/Synthetic Widget_3, Ganglion/Playback Widget_4 w_networking = new W_Networking(_this); w_networking.setTitle("Networking"); @@ -126,13 +133,8 @@ void setupWidgets(PApplet _this, ArrayList w){ w_template1.setTitle("Widget Template 1"); addWidget(w_template1, w); - /* - //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 - w_focus = new W_Focus(_this); - w_focus.setTitle("Focus Widget"); - addWidget(w_focus, w); - // println(" setupWidgets focus widget -- " + millis()); - */ + + }