diff --git a/OpenBCI_GUI/ADS1299SettingsController.pde b/OpenBCI_GUI/ADS1299SettingsController.pde index b2f9b69fc..6ee38e984 100644 --- a/OpenBCI_GUI/ADS1299SettingsController.pde +++ b/OpenBCI_GUI/ADS1299SettingsController.pde @@ -2,7 +2,7 @@ import org.apache.commons.lang3.tuple.Pair; class ADS1299SettingsController { - private PApplet _parentApplet; + private PApplet parentApplet; private boolean isVisible = false; protected int x, y, w, h; protected final int PADDING_3 = 3; @@ -64,16 +64,16 @@ class ADS1299SettingsController { protected int channelCount; protected List activeChannels; - ADS1299SettingsController(PApplet _parent, List _activeChannels, int _x, int _y, int _w, int _h, int _channelBarHeight) { + ADS1299SettingsController(PApplet _parentApplet, List _activeChannels, int _x, int _y, int _w, int _h, int _channelBarHeight) { x = _x; y = _y; w = _w; h = _h; channelBarHeight = _channelBarHeight; - _parentApplet = _parent; - hwsCp5 = new ControlP5(_parentApplet); - hwsCp5.setGraphics(_parentApplet, 0,0); + this.parentApplet = _parentApplet; + hwsCp5 = new ControlP5(parentApplet); + hwsCp5.setGraphics(parentApplet, 0,0); hwsCp5.setAutoDraw(false); int colOffset = (w / CONTROL_BUTTON_COUNT) / 2; @@ -180,7 +180,7 @@ class ADS1299SettingsController { toggleWidthAndHeight = DEFAULT_TOGGLE_WIDTH; } - hwsCp5.setGraphics(_parentApplet, 0, 0); + hwsCp5.setGraphics(parentApplet, 0, 0); int colOffset = (w / CONTROL_BUTTON_COUNT) / 2; int button_y = y + h + PADDING_3; @@ -233,7 +233,7 @@ class ADS1299SettingsController { dropdownY = int(y + (channelBarHeight * rowCount) + ((channelBarHeight - dropdownH) / 2)); final int buttonXIncrement = spaceBetweenButtons + dropdownW; - int toggleX = dropdownX + (dropdownW / 2) - (toggleWidthAndHeight); + int toggleX = dropdownX + (dropdownW / 2) - (toggleWidthAndHeight / 2); channelSelectToggles[i].setPosition(toggleX, dropdownY); channelSelectToggles[i].setSize(toggleWidthAndHeight, toggleWidthAndHeight); @@ -750,8 +750,9 @@ void loadHardwareSettings(File selection) { if (((ADS1299SettingsBoard)currentBoard).getADS1299Settings().loadSettingsValues(selection.getAbsolutePath())) { outputSuccess("Hardware Settings Loaded!"); for (int i = 0; i < globalChannelCount; i++) { - w_timeSeries.adsSettingsController.updateChanSettingsDropdowns(i, currentBoard.isEXGChannelActive(i)); - w_timeSeries.adsSettingsController.updateHasUnappliedSettings(i); + W_TimeSeries timeSeriesWidget = widgetManager.getTimeSeriesWidget(); + timeSeriesWidget.adsSettingsController.updateChanSettingsDropdowns(i, currentBoard.isEXGChannelActive(i)); + timeSeriesWidget.adsSettingsController.updateHasUnappliedSettings(i); } } else { outputError("Failed to load Hardware Settings."); diff --git a/OpenBCI_GUI/AccelerometerEnums.pde b/OpenBCI_GUI/AccelerometerEnums.pde new file mode 100644 index 000000000..72074988c --- /dev/null +++ b/OpenBCI_GUI/AccelerometerEnums.pde @@ -0,0 +1,84 @@ +public enum AccelerometerVerticalScale implements IndexingInterface +{ + AUTO (0, 0, "Auto"), + ONE_G (1, 1, "1 g"), + TWO_G (2, 2, "2 g"), + FOUR_G (3, 4, "4 g"); + + private int index; + private int value; + private String label; + + AccelerometerVerticalScale(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 int getHighestValue() { + int highestValue = 0; + for (AccelerometerVerticalScale scale : values()) { + if (scale.getValue() > highestValue) { + highestValue = scale.getValue(); + } + } + return highestValue; + } +} + +public enum AccelerometerHorizontalScale implements IndexingInterface +{ + ONE_SEC (1, 1, "1 sec"), + THREE_SEC (2, 3, "3 sec"), + FIVE_SEC (3, 5, "5 sec"), + TEN_SEC (4, 10, "10 sec"), + TWENTY_SEC (5, 20, "20 sec"); + + private int index; + private int value; + private String label; + + AccelerometerHorizontalScale(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 int getHighestValue() { + int highestValue = 0; + for (AccelerometerHorizontalScale scale : values()) { + if (scale.getValue() > highestValue) { + highestValue = scale.getValue(); + } + } + return highestValue; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/AnalogReadEnums.pde b/OpenBCI_GUI/AnalogReadEnums.pde new file mode 100644 index 000000000..eb943fe08 --- /dev/null +++ b/OpenBCI_GUI/AnalogReadEnums.pde @@ -0,0 +1,67 @@ +public enum AnalogReadVerticalScale implements IndexingInterface +{ + AUTO (0, 0, "Auto"), + FIFTY (1, 50, "50 uV"), + ONE_HUNDRED (2, 100, "100 uV"), + TWO_HUNDRED (3, 200, "200 uV"), + FOUR_HUNDRED (4, 400, "400 uV"), + ONE_THOUSAND_FIFTY (5, 1050, "1050 uV"), + TEN_THOUSAND (6, 10000, "10000 uV"); + + private int index; + private int value; + private String label; + + AnalogReadVerticalScale(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 enum AnalogReadHorizontalScale implements IndexingInterface +{ + ONE_SEC (1, 1, "1 sec"), + THREE_SEC (2, 3, "3 sec"), + FIVE_SEC (3, 5, "5 sec"), + TEN_SEC (4, 10, "10 sec"), + TWENTY_SEC (5, 20, "20 sec"); + + private int index; + private int value; + private String label; + + AnalogReadHorizontalScale(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; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/BandPowerEnums.pde b/OpenBCI_GUI/BandPowerEnums.pde new file mode 100644 index 000000000..c0597ffb1 --- /dev/null +++ b/OpenBCI_GUI/BandPowerEnums.pde @@ -0,0 +1,56 @@ + +public enum BPLogLin implements IndexingInterface { + LOG (0, "Log"), + LINEAR (1, "Linear"); + + private int index; + private String label; + + BPLogLin(int _index, String _label) { + this.index = _index; + this.label = _label; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum BPVerticalScale implements IndexingInterface { + SCALE_10 (0, 10, "10 uV"), + SCALE_50 (1, 50, "50 uV"), + SCALE_100 (2, 100, "100 uV"), + SCALE_500 (3, 500, "500 uV"), + SCALE_1000 (4, 1000, "1000 uV"), + SCALE_1500 (5, 1500, "1500 uV"); + + private int index; + private final int value; + private String label; + + BPVerticalScale(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; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/ChannelSelect.pde b/OpenBCI_GUI/ChannelSelect.pde index d82aa4606..963101cfd 100644 --- a/OpenBCI_GUI/ChannelSelect.pde +++ b/OpenBCI_GUI/ChannelSelect.pde @@ -10,7 +10,7 @@ class ChannelSelect { protected boolean channelSelectHover; protected boolean isVisible; - ChannelSelect(PApplet _parent, int _x, int _y, int _w, int _navH) { + ChannelSelect(PApplet _parentApplet, int _x, int _y, int _w, int _navH) { x = _x; y = _y; w = _w; @@ -18,8 +18,8 @@ class ChannelSelect { navH = _navH; //setup for checkboxes - cp5_chanSelect = new ControlP5(_parent); - cp5_chanSelect.setGraphics(_parent, 0, 0); + cp5_chanSelect = new ControlP5(_parentApplet); + cp5_chanSelect.setGraphics(_parentApplet, 0, 0); cp5_chanSelect.setAutoDraw(false); //draw only when specified } @@ -66,8 +66,8 @@ class ChannelSelect { popStyle(); } - public void screenResized(PApplet _parent) { - cp5_chanSelect.setGraphics(_parent, 0, 0); + public void screenResized(PApplet _parentApplet) { + cp5_chanSelect.setGraphics(_parentApplet, 0, 0); } public void mousePressed(boolean dropdownIsActive) { @@ -111,8 +111,8 @@ class ExGChannelSelect extends ChannelSelect { protected List channelButtons; private List activeChannels = new ArrayList(); - ExGChannelSelect(PApplet _parent, int _x, int _y, int _w, int _navH) { - super(_parent, _x, _y, _w, _navH); + ExGChannelSelect(PApplet _parentApplet, int _x, int _y, int _w, int _navH) { + super(_parentApplet, _x, _y, _w, _navH); createButtons(); } @@ -232,6 +232,17 @@ class ExGChannelSelect extends ChannelSelect { return activeChannels; } + public void updateChannelSelection(List channels) { + // First deactivate all channels + deactivateAllButtons(); + + // Then activate only the selected channels + for (Integer channel : channels) { + if (channel >= 0 && channel < channelButtons.size()) { + setToggleState(channel, true); // Changed from toggleButton + } + } + } } class DualChannelSelector { @@ -282,8 +293,8 @@ class DualExGChannelSelect extends ExGChannelSelect { DualChannelSelector dualChannelSelector; - DualExGChannelSelect(PApplet _parent, int _x, int _y, int _w, int _navH, boolean isFirstRow) { - super(_parent, _x, _y, _w, _navH); + DualExGChannelSelect(PApplet _parentApplet, int _x, int _y, int _w, int _navH, boolean isFirstRow) { + super(_parentApplet, _x, _y, _w, _navH); dualChannelSelector = new DualChannelSelector(isFirstRow); } diff --git a/OpenBCI_GUI/Containers.pde b/OpenBCI_GUI/Containers.pde index 1dbd200e4..bd1bdbe71 100644 --- a/OpenBCI_GUI/Containers.pde +++ b/OpenBCI_GUI/Containers.pde @@ -80,8 +80,8 @@ void drawContainers() { if (widthOfLastScreen_C != width || heightOfLastScreen_C != height) { setupContainers(); //setupVizs(); //container extension example (more below) - settings.widthOfLastScreen = width; - settings.heightOfLastScreen = height; + sessionSettings.widthOfLastScreen = width; + sessionSettings.heightOfLastScreen = height; } } diff --git a/OpenBCI_GUI/ControlPanel.pde b/OpenBCI_GUI/ControlPanel.pde index cc5a30bd6..e50e9ea1e 100644 --- a/OpenBCI_GUI/ControlPanel.pde +++ b/OpenBCI_GUI/ControlPanel.pde @@ -266,7 +266,7 @@ class ControlPanel { sb.append("OpenBCISession_"); sb.append(dataLogger.getSessionName()); sb.append(File.separator); - settings.setSessionPath(sb.toString()); + dataLogger.setSessionPath(sb.toString()); } public void setBrainFlowStreamerOutput() { @@ -364,7 +364,7 @@ class DataSourceBox { Map bob = sourceList.getItem(int(sourceList.getValue())); String str = (String)bob.get("headline"); // Get the text displayed in the MenuList int newDataSource = (int)bob.get("value"); - settings.controlEventDataSource = str; //Used for output message on system start + sessionSettings.controlEventDataSource = str; //Used for output message on system start eegDataSource = newDataSource; //Reset protocol @@ -910,10 +910,8 @@ class SessionDataBox { createODFButton("odfButton", "OpenBCI", dataLogger.getDataLoggerOutputFormat(), x + padding, y + padding*2 + 18 + 58, (w-padding*3)/2, 24); createBDFButton("bdfButton", "BDF+", dataLogger.getDataLoggerOutputFormat(), x + padding*2 + (w-padding*3)/2, y + padding*2 + 18 + 58, (w-padding*3)/2, 24); - createMaxDurationDropdown("maxFileDuration", Arrays.asList(settings.fileDurations)); - - - + List fileDurationList = EnumHelper.getEnumStrings(OdfFileDuration.class); + createMaxDurationDropdown("maxFileDuration", fileDurationList, odfFileDuration); } public void update() { @@ -1012,10 +1010,10 @@ class SessionDataBox { }); } - private void createMaxDurationDropdown(String name, List _items){ + private void createMaxDurationDropdown(String name, List _items, OdfFileDuration defaultValue) { maxDurationDropdown = sessionData_cp5.addScrollableList(name) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(150) //.setColorBackground(OPENBCI_BLUE) // text field bg color .setColorValueLabel(OPENBCI_DARKBLUE) // text color @@ -1033,7 +1031,7 @@ class SessionDataBox { maxDurationDropdown .getCaptionLabel() //the caption label is the text object in the primary bar .toUpperCase(false) //DO NOT AUTOSET TO UPPERCASE!!! - .setText(settings.fileDurations[settings.defaultOBCIMaxFileSize]) + .setText(defaultValue.getString()) .setFont(p4) .setSize(14) .getStyle() //need to grab style before affecting the paddingTop @@ -1042,7 +1040,7 @@ class SessionDataBox { maxDurationDropdown .getValueLabel() //the value label is connected to the text objects in the dropdown item bars .toUpperCase(false) //DO NOT AUTOSET TO UPPERCASE!!! - .setText(settings.fileDurations[settings.defaultOBCIMaxFileSize]) + .setText(defaultValue.getString()) .setFont(h5) .setSize(12) //set the font size of the item bars to 14pt .getStyle() //need to grab style before affecting the paddingTop @@ -1052,7 +1050,7 @@ class SessionDataBox { public void controlEvent(CallbackEvent theEvent) { if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { int n = (int)(theEvent.getController()).getValue(); - settings.setLogFileDurationChoice(n); + dataLogger.setLogFileDurationChoice(n); println("ControlPanel: Chosen Recording Duration: " + n); } else if (theEvent.getAction() == ControlP5.ACTION_ENTER) { lockOutsideElements(true); @@ -1775,7 +1773,7 @@ class BrainFlowStreamerBox { private void createDropdown(String name){ bfFileSaveOption = bfStreamerCp5.addScrollableList(name) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(150) .setSize(167, (dataWriterBfEnum.values().length + 1) * 24) .setBarHeight(24) //height of top/primary bar @@ -2164,7 +2162,7 @@ class SDBox { sdList = cp5_sdBox.addScrollableList(name) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(150) .setSize(w - padding*2, 2*24)//temporary size .setBarHeight(24) //height of top/primary bar @@ -2506,10 +2504,12 @@ class InitBox { //creates new data file name so that you don't accidentally overwrite the old one controlPanel.dataLogBoxCyton.setSessionTextfieldText(directoryManager.getFileNameDateTime()); controlPanel.dataLogBoxGanglion.setSessionTextfieldText(directoryManager.getFileNameDateTime()); - w_focus.killAuditoryFeedback(); - w_marker.disposeUdpMarkerReceiver(); + W_Focus focusWidget = (W_Focus) widgetManager.getWidget("W_Focus"); + W_Marker markerWidget = (W_Marker) widgetManager.getWidget("W_Marker"); + focusWidget.killAuditoryFeedback(); + markerWidget.disposeUdpMarkerReceiver(); haltSystem(); - wm.setAllWidgetsNull(); + widgetManager.setAllWidgetsNull(); } } diff --git a/OpenBCI_GUI/CytonElectrodeStatus.pde b/OpenBCI_GUI/CytonElectrodeStatus.pde index 5772b5174..e3b5588af 100644 --- a/OpenBCI_GUI/CytonElectrodeStatus.pde +++ b/OpenBCI_GUI/CytonElectrodeStatus.pde @@ -346,7 +346,8 @@ class CytonElectrodeStatus { final int _chan = channelNumber - 1; final int curMillis = millis(); println("CytonElectrodeTestButton: Toggling Impedance on ~~ " + electrodeLocation); - w_cytonImpedance.toggleImpedanceOnElectrode(!cytonBoard.isCheckingImpedanceNorP(_chan, is_N_Pin), _chan, is_N_Pin, curMillis); + W_CytonImpedance cytonImpedanceWidget = (W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance"); + cytonImpedanceWidget.toggleImpedanceOnElectrode(!cytonBoard.isCheckingImpedanceNorP(_chan, is_N_Pin), _chan, is_N_Pin, curMillis); } }); testing_button.setDescription("Click to toggle impedance check for this ADS pin."); diff --git a/OpenBCI_GUI/CytonImpedanceEnums.pde b/OpenBCI_GUI/CytonImpedanceEnums.pde index 21713bf9d..313b8d9de 100644 --- a/OpenBCI_GUI/CytonImpedanceEnums.pde +++ b/OpenBCI_GUI/CytonImpedanceEnums.pde @@ -6,7 +6,6 @@ public enum CytonSignalCheckMode implements IndexingInterface private int index; private String label; - private static CytonSignalCheckMode[] vals = values(); CytonSignalCheckMode(int _index, String _label) { this.index = _index; @@ -26,14 +25,6 @@ public enum CytonSignalCheckMode implements IndexingInterface public boolean getIsImpedanceMode() { return label.equals("Impedance"); } - - private static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum CytonImpedanceLabels implements IndexingInterface @@ -45,7 +36,6 @@ public enum CytonImpedanceLabels implements IndexingInterface private int index; private String label; private boolean boolean_value; - private static CytonImpedanceLabels[] vals = values(); CytonImpedanceLabels(int _index, String _label) { this.index = _index; @@ -65,14 +55,6 @@ public enum CytonImpedanceLabels implements IndexingInterface public boolean getIsAnatomicalName() { return label.equals("Anatomical"); } - - private static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum CytonImpedanceInterval implements IndexingInterface @@ -80,14 +62,12 @@ public enum CytonImpedanceInterval implements IndexingInterface FOUR (0, 4000, "4 sec"), FIVE (1, 5000, "5 sec"), SEVEN (2, 7000, "7 sec"), - TEN (3, 10000, "10 sec") - ; + TEN (3, 10000, "10 sec"); private int index; private int value; private String label; private boolean boolean_value; - private static CytonImpedanceInterval[] vals = values(); CytonImpedanceInterval(int _index, int _val, String _label) { this.index = _index; @@ -108,12 +88,4 @@ public enum CytonImpedanceInterval implements IndexingInterface public int getValue() { return value; } - - private static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } \ No newline at end of file diff --git a/OpenBCI_GUI/DataLogger.pde b/OpenBCI_GUI/DataLogger.pde index 9656face3..9aac64fc7 100644 --- a/OpenBCI_GUI/DataLogger.pde +++ b/OpenBCI_GUI/DataLogger.pde @@ -8,6 +8,10 @@ class DataLogger { public final int OUTPUT_SOURCE_ODF = 1; // The OpenBCI CSV Data Format public final int OUTPUT_SOURCE_BDF = 2; // The BDF data format http://www.biosemi.com/faq/file_format.htm private int outputDataSource; + private String sessionPath = ""; + private boolean logFileIsOpen = false; + private long logFileStartTime; + private long logFileMaxDurationNano = -1; DataLogger() { //Default to OpenBCI CSV Data Format @@ -33,7 +37,7 @@ class DataLogger { private void saveNewData() { //If data is available, save to playback file... - if(!settings.isLogFileOpen()) { + if(!isLogFileOpen()) { return; } @@ -54,21 +58,21 @@ class DataLogger { } public void limitRecordingFileDuration() { - if (settings.isLogFileOpen() && outputDataSource == OUTPUT_SOURCE_ODF && settings.maxLogTimeReached()) { + if (isLogFileOpen() && outputDataSource == OUTPUT_SOURCE_ODF && maxLogTimeReached()) { println("DataLogging: Max recording duration reached for OpenBCI data format. Creating a new recording file in the session folder."); closeLogFile(); openNewLogFile(directoryManager.getFileNameDateTime()); - settings.setLogFileStartTime(System.nanoTime()); + setLogFileStartTime(System.nanoTime()); } } public void onStartStreaming() { if (outputDataSource > OUTPUT_SOURCE_NONE && eegDataSource != DATASOURCE_PLAYBACKFILE) { //open data file if it has not already been opened - if (!settings.isLogFileOpen()) { + if (!isLogFileOpen()) { openNewLogFile(directoryManager.getFileNameDateTime()); } - settings.setLogFileStartTime(System.nanoTime()); + setLogFileStartTime(System.nanoTime()); } //Print BrainFlow Streamer Info here after ODF and BDF println @@ -111,7 +115,7 @@ class DataLogger { // Do nothing... break; } - settings.setLogFileIsOpen(true); + setLogFileIsOpen(true); } /** @@ -159,7 +163,7 @@ class DataLogger { // Do nothing... break; } - settings.setLogFileIsOpen(false); + setLogFileIsOpen(false); } private void closeLogFileBDF() { @@ -188,22 +192,61 @@ class DataLogger { sessionName = s; } - public final String getSessionName() { + public String getSessionName() { return sessionName; } + + + public void setSessionPath (String _path) { + sessionPath = _path; + } + + public String getSessionPath() { + return sessionPath; + } + public void setBfWriterFolder(String _folderName, String _folderPath) { fileWriterBF.setBrainFlowStreamerFolderName(_folderName, _folderPath); } public void setBfWriterDefaultFolder() { - if (settings.getSessionPath() != "") { - settings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + sessionName); + if (getSessionPath() != "") { + setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + sessionName); } - fileWriterBF.setBrainFlowStreamerFolderName(sessionName, settings.getSessionPath()); + fileWriterBF.setBrainFlowStreamerFolderName(sessionName, getSessionPath()); } public String getBfWriterFilePath() { return fileWriterBF.getBrainFlowStreamerRecordingFileName(); } + + + private void setLogFileIsOpen(boolean _toggle) { + logFileIsOpen = _toggle; + } + + private boolean isLogFileOpen() { + return logFileIsOpen; + } + + private void setLogFileStartTime(long _time) { + logFileStartTime = _time; + verbosePrint("Settings: LogFileStartTime = " + _time); + } + + public void setLogFileDurationChoice(int n) { + int fileDurationMinutes = odfFileDuration.values()[n].getValue(); + logFileMaxDurationNano = fileDurationMinutes * 1000000000L * 60; + println("Settings: LogFileMaxDuration = " + fileDurationMinutes + " minutes"); + } + + //Only called during live mode && using OpenBCI Data Format + private boolean maxLogTimeReached() { + if (logFileMaxDurationNano < 0) { + return false; + } else { + return (System.nanoTime() - logFileStartTime) > (logFileMaxDurationNano); + } + } }; \ No newline at end of file diff --git a/OpenBCI_GUI/DataProcessing.pde b/OpenBCI_GUI/DataProcessing.pde index d7045876f..3dcd513d5 100644 --- a/OpenBCI_GUI/DataProcessing.pde +++ b/OpenBCI_GUI/DataProcessing.pde @@ -28,24 +28,24 @@ void processNewData() { } //update the data buffers - for (int Ichan=0; Ichan < channelCount; Ichan++) { + for (int channel=0; channel < channelCount; channel++) { for(int i = 0; i < getCurrentBoardBufferSize(); i++) { - dataProcessingRawBuffer[Ichan][i] = (float)currentData.get(i)[exgChannels[Ichan]]; + dataProcessingRawBuffer[channel][i] = (float)currentData.get(i)[exgChannels[channel]]; } - dataProcessingFilteredBuffer[Ichan] = dataProcessingRawBuffer[Ichan].clone(); + dataProcessingFilteredBuffer[channel] = dataProcessingRawBuffer[channel].clone(); } //apply additional processing for the time-domain montage plot (ie, filtering) dataProcessing.process(dataProcessingFilteredBuffer, fftBuff); //look to see if the latest data is railed so that we can notify the user on the GUI - for (int Ichan=0; Ichan < globalChannelCount; Ichan++) is_railed[Ichan].update(dataProcessingRawBuffer[Ichan], Ichan); + for (int channel=0; channel < globalChannelCount; channel++) is_railed[channel].update(dataProcessingRawBuffer[channel], channel); //compute the electrode impedance. Do it in a very simple way [rms to amplitude, then uVolt to Volt, then Volt/Amp to Ohm] - for (int Ichan=0; Ichan < globalChannelCount; Ichan++) { + for (int channel=0; channel < globalChannelCount; channel++) { // Calculate the impedance - float impedance = (sqrt(2.0)*dataProcessing.data_std_uV[Ichan]*1.0e-6) / BoardCytonConstants.leadOffDrive_amps; + float impedance = (sqrt(2.0)*dataProcessing.data_std_uV[channel]*1.0e-6) / BoardCytonConstants.leadOffDrive_amps; // Subtract the 2.2kOhm resistor impedance -= BoardCytonConstants.series_resistor_ohms; // Verify the impedance is not less than 0 @@ -54,25 +54,24 @@ void processNewData() { impedance = 0; } // Store to the global variable - data_elec_imp_ohm[Ichan] = impedance; + data_elec_imp_ohm[channel] = impedance; } } -void initializeFFTObjects(ddf.minim.analysis.FFT[] fftBuff, float[][] dataProcessingRawBuffer, int Nfft, float fs_Hz) { - +void initializeFFTObjects(ddf.minim.analysis.FFT[] fftBuff, float[][] dataProcessingRawBuffer, int fftPointCount, float fs_Hz) { float[] fooData; - for (int Ichan=0; Ichan < globalChannelCount; Ichan++) { + for (int channel=0; channel < globalChannelCount; channel++) { //make the FFT objects...Following "SoundSpectrum" example that came with the Minim library - fftBuff[Ichan].window(ddf.minim.analysis.FFT.HAMMING); + fftBuff[channel].window(ddf.minim.analysis.FFT.HAMMING); //do the FFT on the initial data - if (isFFTFiltered == true) { - fooData = dataProcessingFilteredBuffer[Ichan]; //use the filtered data for the FFT + if (globalFFTSettings.getDataIsFiltered()) { + fooData = dataProcessingFilteredBuffer[channel]; //use the filtered data for the FFT } else { - fooData = dataProcessingRawBuffer[Ichan]; //use the raw data for the FFT + fooData = dataProcessingRawBuffer[channel]; //use the raw data for the FFT } - fooData = Arrays.copyOfRange(fooData, fooData.length-Nfft, fooData.length); - fftBuff[Ichan].forward(fooData); //compute FFT on this channel of data + fooData = Arrays.copyOfRange(fooData, fooData.length-fftPointCount, fooData.length); + fftBuff[channel].forward(fooData); //compute FFT on this channel of data } } @@ -112,37 +111,37 @@ class DataProcessing { } //Process data on a channel-by-channel basis - private synchronized void processChannel(int Ichan, float[][] data_forDisplay_uV, float[] prevFFTdata) { - int Nfft = getNfftSafe(); + private synchronized void processChannel(int channel, float[][] data_forDisplay_uV, float[] prevFFTdata) { + int fftPointCount = getNumFFTPoints(); double foo; // Filter the data in the time domain // TODO: Use double arrays here and convert to float only to plot data. // ^^^ This might not feasible or meaningful performance improvement. I looked into it a while ago and it seems we need floats for the FFT library also. -RW 2022) try { - double[] tempArray = floatToDoubleArray(data_forDisplay_uV[Ichan]); + double[] tempArray = floatToDoubleArray(data_forDisplay_uV[channel]); //Apply BandStop filter if the filter should be active on this channel - if (filterSettings.values.bandStopFilterActive[Ichan].isActive()) { + if (filterSettings.values.bandStopFilterActive[channel].isActive()) { DataFilter.perform_bandstop( tempArray, currentBoard.getSampleRate(), - filterSettings.values.bandStopStartFreq[Ichan], - filterSettings.values.bandStopStopFreq[Ichan], - filterSettings.values.bandStopFilterOrder[Ichan].getValue(), - filterSettings.values.bandStopFilterType[Ichan].getValue(), + filterSettings.values.bandStopStartFreq[channel], + filterSettings.values.bandStopStopFreq[channel], + filterSettings.values.bandStopFilterOrder[channel].getValue(), + filterSettings.values.bandStopFilterType[channel].getValue(), 1.0); } //Apply BandPass filter if the filter should be active on this channel - if (filterSettings.values.bandPassFilterActive[Ichan].isActive()) { + if (filterSettings.values.bandPassFilterActive[channel].isActive()) { DataFilter.perform_bandpass( tempArray, currentBoard.getSampleRate(), - filterSettings.values.bandPassStartFreq[Ichan], - filterSettings.values.bandPassStopFreq[Ichan], - filterSettings.values.bandPassFilterOrder[Ichan].getValue(), - filterSettings.values.bandPassFilterType[Ichan].getValue(), + filterSettings.values.bandPassStartFreq[channel], + filterSettings.values.bandPassStopFreq[channel], + filterSettings.values.bandPassFilterOrder[channel].getValue(), + filterSettings.values.bandPassFilterType[channel].getValue(), 1.0); } @@ -190,66 +189,68 @@ class DataProcessing { break; } - doubleToFloatArray(tempArray, data_forDisplay_uV[Ichan]); + doubleToFloatArray(tempArray, data_forDisplay_uV[channel]); } catch (BrainFlowError e) { e.printStackTrace(); } //compute the standard deviation of the filtered signal...this is for the head plot - float[] fooData_filt = dataProcessingFilteredBuffer[Ichan]; //use the filtered data + float[] fooData_filt = dataProcessingFilteredBuffer[channel]; //use the filtered data fooData_filt = Arrays.copyOfRange(fooData_filt, fooData_filt.length-((int)fs_Hz), fooData_filt.length); //just grab the most recent second of data - data_std_uV[Ichan]=std(fooData_filt); //compute the standard deviation for the whole array "fooData_filt" + data_std_uV[channel] = std(fooData_filt); //compute the standard deviation for the whole array "fooData_filt" //copy the previous FFT data...enables us to apply some smoothing to the FFT data - for (int I=0; I < fftBuff[Ichan].specSize(); I++) { - prevFFTdata[I] = fftBuff[Ichan].getBand(I); //copy the old spectrum values + for (int I=0; I < fftBuff[channel].specSize(); I++) { + prevFFTdata[I] = fftBuff[channel].getBand(I); //copy the old spectrum values } //prepare the data for the new FFT float[] fooData; - if (isFFTFiltered == true) { - fooData = dataProcessingFilteredBuffer[Ichan]; //use the filtered data for the FFT + if (globalFFTSettings.getDataIsFiltered()) { + fooData = dataProcessingFilteredBuffer[channel]; //use the filtered data for the FFT } else { - fooData = dataProcessingRawBuffer[Ichan]; //use the raw data for the FFT + fooData = dataProcessingRawBuffer[channel]; //use the raw data for the FFT } - fooData = Arrays.copyOfRange(fooData, fooData.length-Nfft, fooData.length); //trim to grab just the most recent block of data + fooData = Arrays.copyOfRange(fooData, fooData.length-fftPointCount, fooData.length); //trim to grab just the most recent block of data float meanData = mean(fooData); //compute the mean for (int I=0; I < fooData.length; I++) fooData[I] -= meanData; //remove the mean (for a better looking FFT //compute the FFT - fftBuff[Ichan].forward(fooData); //compute FFT on this channel of data + fftBuff[channel].forward(fooData); //compute FFT on this channel of data // FFT ref: https://www.mathworks.com/help/matlab/ref/fft.html // first calculate double-sided FFT amplitude spectrum - for (int I=0; I <= Nfft/2; I++) { - fftBuff[Ichan].setBand(I, (float)(fftBuff[Ichan].getBand(I) / Nfft)); + for (int I=0; I <= fftPointCount/2; I++) { + fftBuff[channel].setBand(I, (float)(fftBuff[channel].getBand(I) / fftPointCount)); } // then convert into single-sided FFT spectrum: DC & Nyquist (i=0 & i=N/2) remain the same, others multiply by two. - for (int I=1; I < Nfft/2; I++) { - fftBuff[Ichan].setBand(I, (float)(fftBuff[Ichan].getBand(I) * 2)); + for (int I=1; I < fftPointCount/2; I++) { + fftBuff[channel].setBand(I, (float)(fftBuff[channel].getBand(I) * 2)); } //average the FFT with previous FFT data so that it makes it smoother in time double min_val = 0.01d; - for (int I=0; I < fftBuff[Ichan].specSize(); I++) { //loop over each fft bin + float smoothingFactor = globalFFTSettings.getSmoothingFactor().getValue(); + for (int I=0; I < fftBuff[channel].specSize(); I++) { //loop over each fft bin if (prevFFTdata[I] < min_val) prevFFTdata[I] = (float)min_val; //make sure we're not too small for the log calls - foo = fftBuff[Ichan].getBand(I); + foo = fftBuff[channel].getBand(I); if (foo < min_val) foo = min_val; //make sure this value isn't too small if (true) { //smooth in dB power space - foo = (1.0d-smoothFac[smoothFac_ind]) * java.lang.Math.log(java.lang.Math.pow(foo, 2)); - foo += smoothFac[smoothFac_ind] * java.lang.Math.log(java.lang.Math.pow((double)prevFFTdata[I], 2)); + foo = (1.0d - smoothingFactor) * java.lang.Math.log(java.lang.Math.pow(foo, 2)); + foo += smoothingFactor * java.lang.Math.log(java.lang.Math.pow((double)prevFFTdata[I], 2)); foo = java.lang.Math.sqrt(java.lang.Math.exp(foo)); //average in dB space } else { + //LEGACY CODE -- NOT USED //smooth (average) in linear power space - foo = (1.0d-smoothFac[smoothFac_ind]) * java.lang.Math.pow(foo, 2); - foo+= smoothFac[smoothFac_ind] * java.lang.Math.pow((double)prevFFTdata[I], 2); + foo = (1.0d - smoothingFactor) * java.lang.Math.pow(foo, 2); + foo+= smoothingFactor * java.lang.Math.pow((double)prevFFTdata[I], 2); // take sqrt to be back into uV_rtHz foo = java.lang.Math.sqrt(foo); } - fftBuff[Ichan].setBand(I, (float)foo); //put the smoothed data back into the fftBuff data holder for use by everyone else - // fftBuff[Ichan].setBand(I, 1.0f); // test + fftBuff[channel].setBand(I, (float)foo); //put the smoothed data back into the fftBuff data holder for use by everyone else + // fftBuff[channel].setBand(I, 1.0f); // test } //end loop over FFT bins // calculate single-sided psd by single-sided FFT amplitude spectrum @@ -260,22 +261,22 @@ class DataProcessing { for (int i = 0; i < processing_band_low_Hz.length; i++) { float sum = 0; // int binNum = 0; - for (int Ibin = 0; Ibin <= Nfft/2; Ibin ++) { // loop over FFT bins - float FFT_freq_Hz = fftBuff[Ichan].indexToFreq(Ibin); // center frequency of this bin + for (int Ibin = 0; Ibin <= fftPointCount/2; Ibin ++) { // loop over FFT bins + float FFT_freq_Hz = fftBuff[channel].indexToFreq(Ibin); // center frequency of this bin float psdx = 0; // if the frequency matches a band if (FFT_freq_Hz >= processing_band_low_Hz[i] && FFT_freq_Hz < processing_band_high_Hz[i]) { - if (Ibin != 0 && Ibin != Nfft/2) { - psdx = fftBuff[Ichan].getBand(Ibin) * fftBuff[Ichan].getBand(Ibin) * Nfft/currentBoard.getSampleRate() / 4; + if (Ibin != 0 && Ibin != fftPointCount/2) { + psdx = fftBuff[channel].getBand(Ibin) * fftBuff[channel].getBand(Ibin) * fftPointCount/currentBoard.getSampleRate() / 4; } else { - psdx = fftBuff[Ichan].getBand(Ibin) * fftBuff[Ichan].getBand(Ibin) * Nfft/currentBoard.getSampleRate(); + psdx = fftBuff[channel].getBand(Ibin) * fftBuff[channel].getBand(Ibin) * fftPointCount/currentBoard.getSampleRate(); } sum += psdx; // binNum ++; } } - avgPowerInBins[Ichan][i] = sum; // total power in a band + avgPowerInBins[channel][i] = sum; // total power in a band // println(i, binNum, sum); } } @@ -284,8 +285,8 @@ class DataProcessing { float prevFFTdata[] = new float[fftBuff[0].specSize()]; - for (int Ichan=0; Ichan < globalChannelCount; Ichan++) { - processChannel(Ichan, data_forDisplay_uV, prevFFTdata); + for (int channel=0; channel < globalChannelCount; channel++) { + processChannel(channel, data_forDisplay_uV, prevFFTdata); } //end the loop over channels. for (int i = 0; i < processing_band_low_Hz.length; i++) { @@ -297,35 +298,17 @@ class DataProcessing { headWidePower[i] = sum/globalChannelCount; // averaging power over all channels } - // Calculate data used for Headplot - // Find strongest channel - int refChanInd = findMax(data_std_uV); - //println("EEG_Processing: strongest chan (one referenced) = " + (refChanInd+1)); - float[] refData_uV = dataProcessingFilteredBuffer[refChanInd]; //use the filtered data - refData_uV = Arrays.copyOfRange(refData_uV, refData_uV.length-((int)fs_Hz), refData_uV.length); //just grab the most recent second of data - // Compute polarity of each channel - for (int Ichan=0; Ichan < globalChannelCount; Ichan++) { - float[] fooData_filt = dataProcessingFilteredBuffer[Ichan]; //use the filtered data - fooData_filt = Arrays.copyOfRange(fooData_filt, fooData_filt.length-((int)fs_Hz), fooData_filt.length); //just grab the most recent second of data - float dotProd = calcDotProduct(fooData_filt, refData_uV); - if (dotProd >= 0.0f) { - polarity[Ichan]=1.0; - } else { - polarity[Ichan]=-1.0; - } - } - ///////////////////////////////////////////////////////////// // Compute widget values independent of widgets being open // // -RW #1094 // ///////////////////////////////////////////////////////////// emgSettings.values.process(dataProcessingFilteredBuffer); - w_focus.updateFocusWidgetData(); - w_bandPower.updateBandPowerWidgetData(); - w_emgJoystick.updateEmgJoystickWidgetData(); + ((W_Focus) widgetManager.getWidget("W_Focus")).updateFocusWidgetData(); + ((W_BandPower) widgetManager.getWidget("W_BandPower")).updateBandPowerWidgetData(); + ((W_EmgJoystick) widgetManager.getWidget("W_EmgJoystick")).updateEmgJoystickWidgetData(); if (currentBoard instanceof BoardCyton) { - if (w_pulseSensor != null) { - w_pulseSensor.updatePulseSensorWidgetData(); + if (widgetManager.getWidgetExists("W_PulseSensor")) { + ((W_PulseSensor) widgetManager.getWidget("W_PulseSensor")).updatePulseSensorWidgetData(); } } @@ -380,7 +363,7 @@ class DataProcessing { private void clearCalculatedMetricWidgets() { println("Clearing calculated metric widgets"); - w_spectrogram.clear(); - w_focus.clear(); + ((W_Spectrogram) widgetManager.getWidget("W_Spectrogram")).clear(); + ((W_Focus) widgetManager.getWidget("W_Focus")).clear(); } } \ No newline at end of file diff --git a/OpenBCI_GUI/DataWriterBF.pde b/OpenBCI_GUI/DataWriterBF.pde index 83f28f11c..6f7cbbed2 100644 --- a/OpenBCI_GUI/DataWriterBF.pde +++ b/OpenBCI_GUI/DataWriterBF.pde @@ -48,7 +48,8 @@ public class DataWriterBF { } public void setBrainFlowStreamerFolderName(String _folderName, String _folderPath) { - //settings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); + //FIX ME ? + //sessionSettings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); folderName = _folderName; folderPath = _folderPath; diff --git a/OpenBCI_GUI/DataWriterODF.pde b/OpenBCI_GUI/DataWriterODF.pde index 7d7e03bcf..e4c313247 100644 --- a/OpenBCI_GUI/DataWriterODF.pde +++ b/OpenBCI_GUI/DataWriterODF.pde @@ -7,8 +7,8 @@ public class DataWriterODF { protected String headerFirstLineString = "%OpenBCI Raw EXG Data"; DataWriterODF(String _sessionName, String _fileName) { - settings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); - fname = settings.getSessionPath(); + dataLogger.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); + fname = dataLogger.getSessionPath(); fname += fileNamePrependString; fname += _fileName; fname += ".txt"; @@ -21,8 +21,8 @@ public class DataWriterODF { DataWriterODF(String _sessionName, String _fileName, String _fileNamePrependString, String _headerFirstLineString) { fileNamePrependString = _fileNamePrependString; headerFirstLineString = _headerFirstLineString; - settings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); - fname = settings.getSessionPath(); + dataLogger.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); + fname = dataLogger.getSessionPath(); fname += fileNamePrependString; fname += _fileName; fname += ".txt"; diff --git a/OpenBCI_GUI/EmgJoystickEnums.pde b/OpenBCI_GUI/EmgJoystickEnums.pde new file mode 100644 index 000000000..902739a2d --- /dev/null +++ b/OpenBCI_GUI/EmgJoystickEnums.pde @@ -0,0 +1,98 @@ + +public enum EmgJoystickSmoothing implements IndexingInterface +{ + OFF (0, "Off", 0f), + POINT_9 (1, "0.9", .9f), + POINT_95 (2, "0.95", .95f), + POINT_98 (3, "0.98", .98f), + POINT_99 (4, "0.99", .99f), + POINT_999 (5, "0.999", .999f), + POINT_9999 (6, "0.9999", .9999f); + + private int index; + private String name; + private float value; + + EmgJoystickSmoothing(int index, String name, float value) { + this.index = index; + this.name = name; + this.value = value; + } + + @Override + public int getIndex() { + return index; + } + + @Override + public String getString() { + return name; + } + + public float getValue() { + return value; + } +} + +public class EMGJoystickInput implements IndexingInterface{ + private int index; + private String name; + private int value; + + EMGJoystickInput(int index, String name, int value) { + this.index = index; + this.name = name; + this.value = value; + } + + @Override + public int getIndex() { + return index; + } + + @Override + public String getString() { + return name; + } + + public int getValue() { + return value; + } +} + +public class EMGJoystickInputs { + private final int NUM_EMG_INPUTS = 4; + private final EMGJoystickInput[] VALUES; + private final EMGJoystickInput[] INPUTS = new EMGJoystickInput[NUM_EMG_INPUTS]; + + EMGJoystickInputs(int numExGChannels) { + VALUES = new EMGJoystickInput[numExGChannels]; + for (int i = 0; i < numExGChannels; i++) { + VALUES[i] = new EMGJoystickInput(i, "Channel " + (i + 1), i); + } + } + + public EMGJoystickInput[] getValues() { + return VALUES; + } + + public EMGJoystickInput[] getInputs() { + return INPUTS; + } + + public EMGJoystickInput getInput(int index) { + return INPUTS[index]; + } + + public void setInputToChannel(int inputNumber, int channel) { + if (inputNumber < 0 || inputNumber >= NUM_EMG_INPUTS) { + println("Invalid input number: " + inputNumber); + return; + } + if (channel < 0 || channel >= VALUES.length) { + println("Invalid channel: " + channel); + return; + } + INPUTS[inputNumber] = VALUES[channel]; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/EmgSettings.pde b/OpenBCI_GUI/EmgSettings.pde index ada23f214..ffa778357 100644 --- a/OpenBCI_GUI/EmgSettings.pde +++ b/OpenBCI_GUI/EmgSettings.pde @@ -11,57 +11,35 @@ class EmgSettings { values = new EmgSettingsValues(); } - public boolean loadSettingsValues(String filename) { + public String getJson() { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + return gson.toJson(values); + } + + public boolean loadSettingsFromJson(String json) { try { - File file = new File(filename); - StringBuilder fileContents = new StringBuilder((int)file.length()); - Scanner scanner = new Scanner(file); - while(scanner.hasNextLine()) { - fileContents.append(scanner.nextLine() + System.lineSeparator()); - } Gson gson = new Gson(); - EmgSettingsValues tempValues = gson.fromJson(fileContents.toString(), EmgSettingsValues.class); + EmgSettingsValues tempValues = gson.fromJson(json, EmgSettingsValues.class); + + // Validate channel count matches if (tempValues.window.length != channelCount) { - outputError("Emg Settings: Loaded EMG Settings file has different number of channels than the current board."); + outputError("Emg Settings: Loaded EMG Settings JSON has different number of channels than the current board."); return false; } - //Explicitely copy values over to avoid reference issues - //(e.g. values = tempValues "nukes" the old values object) + + // Explicitly copy values to avoid reference issues values.window = tempValues.window; values.uvLimit = tempValues.uvLimit; values.creepIncreasing = tempValues.creepIncreasing; values.creepDecreasing = tempValues.creepDecreasing; values.minimumDeltaUV = tempValues.minimumDeltaUV; values.lowerThresholdMinimum = tempValues.lowerThresholdMinimum; + + settingsWereLoaded = true; return true; - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); - File f = new File(filename); - if (f.exists()) { - if (f.delete()) { - outputError("Emg Settings: Could not load EMG settings from disk. Deleting this file..."); - } else { - outputError("Emg Settings: Error deleting old/broken EMG settings file! Please make sure the GUI has proper read/write permissions."); - } - } - return false; - } - } - - public String getJson() { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - return gson.toJson(values); - } - - public boolean saveToFile(String filename) { - String json = getJson(); - try { - FileWriter writer = new FileWriter(filename); - writer.write(json); - writer.close(); - return true; - } catch (IOException e) { - e.printStackTrace(); + outputError("EmgSettings: Could not load EMG settings from JSON string."); return false; } } @@ -76,39 +54,6 @@ class EmgSettings { return channelCount; } - //Avoid error with popup being in another thread. - public void storeSettings() { - StringBuilder settingsFilename = new StringBuilder(directoryManager.getSettingsPath()); - settingsFilename.append("EmgSettings"); - settingsFilename.append("_"); - settingsFilename.append(getChannelCount()); - settingsFilename.append("Channels.json"); - String filename = settingsFilename.toString(); - File fileToSave = new File(filename); - FileChooser chooser = new FileChooser( - FileChooserMode.SAVE, - "storeEmgSettings", - fileToSave, - "Save EMG settings to file"); - } - - //Avoid error with popup being in another thread. - public void loadSettings() { - StringBuilder settingsFilename = new StringBuilder(directoryManager.getSettingsPath()); - settingsFilename.append("EmgSettings"); - settingsFilename.append("_"); - settingsFilename.append(getChannelCount()); - settingsFilename.append("Channels.json"); - String filename = settingsFilename.toString(); - File fileToLoad = new File(filename); - FileChooser chooser = new FileChooser( - FileChooserMode.LOAD, - "loadEmgSettings", - fileToLoad, - "Select EMG settings file to load"); - - } - public boolean getSettingsWereLoaded() { return settingsWereLoaded; } @@ -116,29 +61,4 @@ class EmgSettings { public void setSettingsWereLoaded(boolean settingsWereLoaded) { this.settingsWereLoaded = settingsWereLoaded; } -} - -//Used by button in the EMG UI. Must be global and public. Called in above loadSettings method. -public void loadEmgSettings(File selection) { - if (selection == null) { - output("EMG Settings file not selected."); - } else { - if (dataProcessing.emgSettings.loadSettingsValues(selection.getAbsolutePath())) { - outputSuccess("EMG Settings Loaded!"); - dataProcessing.emgSettings.setSettingsWereLoaded(true); - } - } -} - -//Used by button in the EMG UI. Must be global and public. Called in above storeSettings method. -public void storeEmgSettings(File selection) { - if (selection == null) { - output("EMG Settings file not selected."); - } else { - if (dataProcessing.emgSettings.saveToFile(selection.getAbsolutePath())) { - outputSuccess("EMG Settings Saved!"); - } else { - outputError("Failed to save EMG Settings."); - } - } } \ No newline at end of file diff --git a/OpenBCI_GUI/EmgSettingsUI.pde b/OpenBCI_GUI/EmgSettingsUI.pde index 7e3991faa..a949ba773 100644 --- a/OpenBCI_GUI/EmgSettingsUI.pde +++ b/OpenBCI_GUI/EmgSettingsUI.pde @@ -27,11 +27,6 @@ class EmgSettingsUI extends PApplet implements Runnable { private boolean isFixedHeight; private int fixedHeight; private int[] dropdownYPositions; - private final int NUM_FOOTER_OBJECTS = 3; - private final int FOOTER_OBJECT_WIDTH = 45; - private final int FOOTER_OBJECT_HEIGHT = 26; - private int footerObjY; - private int[] footerObjX = new int[NUM_FOOTER_OBJECTS]; private final color HEADER_COLOR = OPENBCI_BLUE; private final color BACKGROUND_COLOR = GREY_235; @@ -61,10 +56,6 @@ class EmgSettingsUI extends PApplet implements Runnable { private String[] channelLabels; - private Button saveButton; - private Button loadButton; - private Button defaultButton; - @Override public void run() { PApplet.runSketch(new String[] {HEADER_MESSAGE}, this); @@ -235,17 +226,6 @@ class EmgSettingsUI extends PApplet implements Runnable { } private void createAllUIObjects() { - final int HALF_FOOTER_HEIGHT = (FOOTER_PADDING + (DROPDOWN_SPACER * 2)) / 2; - footerObjY = y + h - HALF_FOOTER_HEIGHT - (FOOTER_OBJECT_HEIGHT / 2); - int middle = x + w / 2; - int halfObjWidth = FOOTER_OBJECT_WIDTH / 2; - footerObjX[0] = middle - halfObjWidth - PADDING_12 - FOOTER_OBJECT_WIDTH; - footerObjX[1] = middle - halfObjWidth; - footerObjX[2] = middle + halfObjWidth + PADDING_12; - createEmgSettingsSaveButton("saveEmgSettingsButton", "Save", footerObjX[0], footerObjY, FOOTER_OBJECT_WIDTH, FOOTER_OBJECT_HEIGHT); - createEmgSettingsLoadButton("loadEmgSettingsButton", "Load", footerObjX[1], footerObjY, FOOTER_OBJECT_WIDTH, FOOTER_OBJECT_HEIGHT); - createEmgSettingsDefaultButton("defaultEmgSettingsButton", "Reset", footerObjX[2], footerObjY, FOOTER_OBJECT_WIDTH, FOOTER_OBJECT_HEIGHT); - channelLabels = new String[channelCount]; for (int i = 0; i < channelCount; i++) { channelLabels[i] = "Channel " + (i+1); @@ -369,36 +349,6 @@ class EmgSettingsUI extends PApplet implements Runnable { } } - private void createEmgSettingsSaveButton(String name, String text, int _x, int _y, int _w, int _h) { - saveButton = createButton(emgCp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - saveButton.setBorderColor(OBJECT_BORDER_GREY); - saveButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - dataProcessing.emgSettings.storeSettings(); - } - }); - } - - private void createEmgSettingsLoadButton(String name, String text, int _x, int _y, int _w, int _h) { - loadButton = createButton(emgCp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - loadButton.setBorderColor(OBJECT_BORDER_GREY); - loadButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - dataProcessing.emgSettings.loadSettings(); - } - }); - } - - private void createEmgSettingsDefaultButton(String name, String text, int _x, int _y, int _w, int _h) { - defaultButton = createButton(emgCp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - defaultButton.setBorderColor(OBJECT_BORDER_GREY); - defaultButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - dataProcessing.emgSettings.revertAllChannelsToDefaultValues(); - } - }); - } - private void updateCp5Objects() { for (int i = 0; i < channelCount; i++) { //Fetch values from the EmgSettingsValues object diff --git a/OpenBCI_GUI/EnumHelper.pde b/OpenBCI_GUI/EnumHelper.pde new file mode 100644 index 000000000..68c9e8ae5 --- /dev/null +++ b/OpenBCI_GUI/EnumHelper.pde @@ -0,0 +1,28 @@ +//Used for Widget Dropdown Enums +interface IndexingInterface { + public int getIndex(); + public String getString(); +} + +/** + * Helper class for working with IndexingInterface enums + */ +public static class EnumHelper { + /** + * Generic method to get enum strings as a list + */ + public static List getListAsStrings(T[] values) { + List enumStrings = new ArrayList<>(); + for (T enumValue : values) { + enumStrings.add(enumValue.getString()); + } + return enumStrings; + } + + /** + * Get list of strings for an enum class that implements IndexingInterface + */ + public static & IndexingInterface> List getEnumStrings(Class enumClass) { + return getListAsStrings(enumClass.getEnumConstants()); + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/Extras.pde b/OpenBCI_GUI/Extras.pde index f37587d10..c6d664cc7 100644 --- a/OpenBCI_GUI/Extras.pde +++ b/OpenBCI_GUI/Extras.pde @@ -416,6 +416,11 @@ void doubleToFloatArray(double[] array, float[] res) { } } +public double convertByteArrayToDouble(byte[] array) { + ByteBuffer buffer = ByteBuffer.wrap(array); + return buffer.getDouble(); +} + // shortens a string to a given width by adding [...] in the middle // make sure to pass the right font for accurate sizing String shortenString(String str, float maxWidth, PFont font) { diff --git a/OpenBCI_GUI/FFTEnums.pde b/OpenBCI_GUI/FFTEnums.pde new file mode 100644 index 000000000..ce1b1e775 --- /dev/null +++ b/OpenBCI_GUI/FFTEnums.pde @@ -0,0 +1,185 @@ + +public class GlobalFFTSettings { + public FFTSmoothingFactor smoothingFactor = FFTSmoothingFactor.SMOOTH_90; + public FFTFilteredEnum dataIsFiltered = FFTFilteredEnum.FILTERED; + + GlobalFFTSettings() { + // Constructor + } + + public void setSmoothingFactor(FFTSmoothingFactor factor) { + this.smoothingFactor = factor; + } + + public FFTSmoothingFactor getSmoothingFactor() { + return smoothingFactor; + } + + public void setFilteredEnum(FFTFilteredEnum filteredEnum) { + this.dataIsFiltered = filteredEnum; + } + + public FFTFilteredEnum getFilteredEnum() { + return dataIsFiltered; + } + + public boolean getDataIsFiltered() { + return dataIsFiltered == FFTFilteredEnum.FILTERED; + } +} + + +// Used by FFT Widget, Band Power Widget, and Head Plot Widget +public enum FFTSmoothingFactor implements IndexingInterface { + NONE (0, 0.0f, "O.O"), + SMOOTH_50 (1, 0.5f, "0.5"), + SMOOTH_75 (2, 0.75f, "0.75"), + SMOOTH_90 (3, 0.9f, "0.9"), + SMOOTH_95 (4, 0.95f, "0.95"), + SMOOTH_98 (5, 0.98f, "0.98"), + SMOOTH_99 (6, 0.99f, "0.99"), + SMOOTH_999 (7, 0.999f, "0.999"); + + private int index; + private final float value; + private String label; + + FFTSmoothingFactor(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; + } +} + +// Used by FFT Widget and Band Power Widget +public enum FFTFilteredEnum implements IndexingInterface { + FILTERED (0, "Filtered"), + UNFILTERED (1, "Unfilt."); + + private int index; + private String label; + + FFTFilteredEnum(int index, String label) { + this.index = index; + this.label = label; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum FFTMaxFrequency implements IndexingInterface { + MAX_20 (0, 20, "20 Hz"), + MAX_40 (1, 40, "40 Hz"), + MAX_60 (2, 60, "60 Hz"), + MAX_100 (3, 100, "100 Hz"), + MAX_120 (4, 120, "120 Hz"), + MAX_250 (5, 250, "250 Hz"), + MAX_500 (6, 500, "500 Hz"), + MAX_800 (7, 800, "800 Hz"); + + private int index; + private final int value; + private String label; + + FFTMaxFrequency(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 int getHighestFrequency() { + return MAX_800.getValue(); + } +} + +public enum FFTVerticalScale implements IndexingInterface { + SCALE_10 (0, 10, "10 uV"), + SCALE_50 (1, 50, "50 uV"), + SCALE_100 (2, 100, "100 uV"), + SCALE_500 (3, 500, "500 uV"), + SCALE_1000 (3, 1000, "1000 uV"), + SCALE_1500 (4, 1500, "1500 uV"); + + private int index; + private final int value; + private String label; + + FFTVerticalScale(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 enum GraphLogLin implements IndexingInterface { + LOG (0, "Log"), + LIN (1, "Linear"); + + private int index; + private String label; + + GraphLogLin(int index, String label) { + this.index = index; + this.label = label; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/FifoChannelBar.pde b/OpenBCI_GUI/FifoChannelBar.pde index 846fc8fae..f9debcc13 100644 --- a/OpenBCI_GUI/FifoChannelBar.pde +++ b/OpenBCI_GUI/FifoChannelBar.pde @@ -25,7 +25,7 @@ class FifoChannelBar { private GPlotAutoscaler gplotAutoscaler = new GPlotAutoscaler(); - FifoChannelBar(PApplet _parent, String yAxisLabel, int xLimit, float yLimit, int _x, int _y, int _w, int _h, color lineColor, int _layerCount, int _samplingRate, int _totalBufferSeconds) { + FifoChannelBar(PApplet _parentApplet, String yAxisLabel, int xLimit, float yLimit, int _x, int _y, int _w, int _h, color lineColor, int _layerCount, int _samplingRate, int _totalBufferSeconds) { x = _x; y = _y; w = _w; @@ -42,7 +42,7 @@ class FifoChannelBar { isOpenness = true; } - plot = new GPlot(_parent); + plot = new GPlot(_parentApplet); plot.setPos(x + 36 + 4 + xOffset, y); plot.setDim(w - 36 - 4 - xOffset, h); plot.setMar(0f, 0f, 0f, 0f); @@ -77,12 +77,12 @@ class FifoChannelBar { valueTextBox.setVisible(false); } - FifoChannelBar(PApplet _parent, String yAxisLabel, int xLimit, int _x, int _y, int _w, int _h, color lineColor, int layerCount, int _totalBufferSeconds) { - this(_parent, yAxisLabel, xLimit, 1, _x, _y, _w, _h, lineColor, layerCount, 200, _totalBufferSeconds); + FifoChannelBar(PApplet _parentApplet, String yAxisLabel, int xLimit, int _x, int _y, int _w, int _h, color lineColor, int layerCount, int _totalBufferSeconds) { + this(_parentApplet, yAxisLabel, xLimit, 1, _x, _y, _w, _h, lineColor, layerCount, 200, _totalBufferSeconds); } - FifoChannelBar(PApplet _parent, String yAxisLabel, int xLimit, float yLimit, int _x, int _y, int _w, int _h, color lineColor, int _totalBufferSeconds) { - this(_parent, yAxisLabel, xLimit, yLimit, _x, _y, _w, _h, lineColor, 1, 200, _totalBufferSeconds); + FifoChannelBar(PApplet _parentApplet, String yAxisLabel, int xLimit, float yLimit, int _x, int _y, int _w, int _h, color lineColor, int _totalBufferSeconds) { + this(_parentApplet, yAxisLabel, xLimit, yLimit, _x, _y, _w, _h, lineColor, 1, 200, _totalBufferSeconds); } private void initArrays() { diff --git a/OpenBCI_GUI/FileDurationEnum.pde b/OpenBCI_GUI/FileDurationEnum.pde new file mode 100644 index 000000000..acfd6dfcc --- /dev/null +++ b/OpenBCI_GUI/FileDurationEnum.pde @@ -0,0 +1,32 @@ +public enum OdfFileDuration implements IndexingInterface { + FIVE_MINUTES (0, 5, "5 Minutes"), + FIFTEEN_MINUTES (1, 15, "15 Minutes"), + THIRTY_MINUTES (2, 30, "30 Minutes"), + SIXTY_MINUTES (3, 60, "60 Minutes"), + ONE_HUNDRED_TWENTY_MINUTES (4, 120, "120 Minutes"), + NO_LIMIT (5, -1, "No Limit"); + + private int index; + private int duration; + private String label; + + OdfFileDuration(int _index, int _duration, String _label) { + this.index = _index; + this.duration = _duration; + this.label = _label; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public int getValue() { + return duration; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/FilterSettings.pde b/OpenBCI_GUI/FilterSettings.pde index fff293147..a627e325e 100644 --- a/OpenBCI_GUI/FilterSettings.pde +++ b/OpenBCI_GUI/FilterSettings.pde @@ -92,46 +92,19 @@ class FilterSettings { defaultValues = new FilterSettingsValues(channelCount); } - public boolean loadSettingsValues(String filename) { - try { - File file = new File(filename); - StringBuilder fileContents = new StringBuilder((int)file.length()); - Scanner scanner = new Scanner(file); - while(scanner.hasNextLine()) { - fileContents.append(scanner.nextLine() + System.lineSeparator()); - } - Gson gson = new Gson(); - values = gson.fromJson(fileContents.toString(), FilterSettingsValues.class); - return true; - } catch (IOException e) { - e.printStackTrace(); - File f = new File(filename); - if (f.exists()) { - if (f.delete()) { - println("FilterSettings: Could not load filter settings from disk. Deleting this file..."); - } else { - println("FilterSettings: Error deleting old/broken filter settings file! Please make sure the GUI has proper read/write permissions."); - } - } - return false; - } - } - public String getJson() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); return gson.toJson(values); } - public boolean saveToFile(String filename) { - String json = getJson(); + public void loadSettingsFromJson(String json) { try { - FileWriter writer = new FileWriter(filename); - writer.write(json); - writer.close(); - return true; - } catch (IOException e) { - e.printStackTrace(); - return false; + Gson gson = new Gson(); + values = gson.fromJson(json, FilterSettingsValues.class); + filterSettingsWereLoadedFromFile = true; + } catch (Exception e) { + e.printStackTrace(); + println("FilterSettings: Could not load filter settings from JSON string."); } } @@ -143,62 +116,4 @@ class FilterSettings { public int getChannelCount() { return channelCount; } - - //Avoid error with popup being in another thread. - public void storeSettings() { - StringBuilder settingsFilename = new StringBuilder(directoryManager.getSettingsPath()); - settingsFilename.append("FilterSettings"); - settingsFilename.append("_"); - settingsFilename.append(getChannelCount()); - settingsFilename.append("Channels.json"); - String filename = settingsFilename.toString(); - File fileToSave = new File(filename); - FileChooser chooser = new FileChooser( - FileChooserMode.SAVE, - "storeFilterSettings", - fileToSave, - "Save filter settings to file"); - } - //Avoid error with popup being in another thread. - public void loadSettings() { - StringBuilder settingsFilename = new StringBuilder(directoryManager.getSettingsPath()); - settingsFilename.append("FilterSettings"); - settingsFilename.append("_"); - settingsFilename.append(getChannelCount()); - settingsFilename.append("Channels.json"); - String filename = settingsFilename.toString(); - File fileToLoad = new File(filename); - FileChooser chooser = new FileChooser( - FileChooserMode.LOAD, - "loadFilterSettings", - fileToLoad, - "Select settings file to load"); - } -} - -//Used by button in the Filter UI. Must be global and public. -public void loadFilterSettings(File selection) { - if (selection == null) { - output("Filters Settings file not selected."); - } else { - if (filterSettings.loadSettingsValues(selection.getAbsolutePath())) { - outputSuccess("Filter Settings Loaded!"); - filterSettingsWereLoadedFromFile = true; - } else { - outputError("Failed to load Filter Settings. The old/broken file has been deleted."); - } - } -} - -//Used by button in the Filter UI. Must be global and public. -public void storeFilterSettings(File selection) { - if (selection == null) { - output("Filter Settings file not selected."); - } else { - if (filterSettings.saveToFile(selection.getAbsolutePath())) { - outputSuccess("Filter Settings Saved!"); - } else { - outputError("Failed to save Filter Settings."); - } - } } \ No newline at end of file diff --git a/OpenBCI_GUI/FilterUI.pde b/OpenBCI_GUI/FilterUI.pde index ba3bae2dd..2e1c86b26 100644 --- a/OpenBCI_GUI/FilterUI.pde +++ b/OpenBCI_GUI/FilterUI.pde @@ -25,12 +25,9 @@ class FilterUIPopup extends PApplet implements Runnable { private final int HALF_OBJ_WIDTH = HEADER_OBJ_WIDTH/2; private final int NUM_HEADER_OBJECTS = 4; private final int NUM_COLUMNS = 5; - private final int NUM_FOOTER_OBJECTS = 3; private int[] headerObjX = new int[NUM_HEADER_OBJECTS]; private final int HEADER_OBJ_Y = SM_SPACER; private int[] columnObjX = new int[NUM_COLUMNS]; - private int footerObjY = 0; - private int[] footerObjX = new int[NUM_FOOTER_OBJECTS]; private String message = "Sample text string"; private String headerMessage = "Filters"; @@ -50,9 +47,6 @@ class FilterUIPopup extends PApplet implements Runnable { private ScrollableList bfGlobalFilterDropdown; private ScrollableList bfEnvironmentalNoiseDropdown; - private Button saveButton; - private Button loadButton; - private Button defaultButton; private Button masterOnOffButton; private Textfield masterFirstColumnTextfield; @@ -116,7 +110,7 @@ class FilterUIPopup extends PApplet implements Runnable { filterSettingsWereModifiedFadeCounter = new int[numChans]; fixedWidth = (HEADER_OBJ_WIDTH * 6) + SM_SPACER*5; - maxHeight = HEADER_HEIGHT*3 + SM_SPACER*(numChans+5) + uiObjectHeight*(numChans+2) + EXPANDER_HEIGHT; + maxHeight = HEADER_HEIGHT*3 + SM_SPACER*(numChans+5) + uiObjectHeight*(numChans+1) + EXPANDER_HEIGHT; shortHeight = HEADER_HEIGHT*2 + SM_SPACER*(1+5) + uiObjectHeight*(1+2) + LG_SPACER + EXPANDER_HEIGHT; variableHeight = maxHeight; //Include spacer on the outside left and right of all columns. Used to draw visual feedback @@ -307,11 +301,7 @@ class FilterUIPopup extends PApplet implements Runnable { } private void createAllCp5Objects() { - calculateXYForHeaderColumnsAndFooter(); - - createFilterSettingsSaveButton("saveFilterSettingsButton", "Save", footerObjX[0], footerObjY, HEADER_OBJ_WIDTH, uiObjectHeight); - createFilterSettingsLoadButton("loadFilterSettingsButton", "Load", footerObjX[1], footerObjY, HEADER_OBJ_WIDTH, uiObjectHeight); - createFilterSettingsDefaultButton("defaultFilterSettingsButton", "Reset", footerObjX[2], footerObjY, HEADER_OBJ_WIDTH, uiObjectHeight); + calculateXYForHeaderColumns(); createOnOffButtons(); createTextfields(); @@ -334,7 +324,7 @@ class FilterUIPopup extends PApplet implements Runnable { bfEnvironmentalNoiseDropdown.getCaptionLabel().setText(filterSettings.values.globalEnvFilter.getString()); } - private void calculateXYForHeaderColumnsAndFooter() { + private void calculateXYForHeaderColumns() { middle = width / 2; headerObjX[0] = middle - SM_SPACER*2 - HEADER_OBJ_WIDTH*2; @@ -348,17 +338,12 @@ class FilterUIPopup extends PApplet implements Runnable { columnObjX[3] = middle + HALF_OBJ_WIDTH + LG_SPACER; columnObjX[4] = middle + HALF_OBJ_WIDTH + LG_SPACER*2 + HEADER_OBJ_WIDTH; - footerObjX[0] = middle - HALF_OBJ_WIDTH - LG_SPACER - HEADER_OBJ_WIDTH; - footerObjX[1] = middle - HALF_OBJ_WIDTH; - footerObjX[2] = middle + HALF_OBJ_WIDTH + LG_SPACER; - setFooterObjYPosition(filterSettings.values.filterChannelSelect); - expanderLineOneEnd = middle - expanderBreakMiddle/2; expanderLineTwoStart = middle + expanderBreakMiddle/2; } public void arrangeAllObjectsXY() { - calculateXYForHeaderColumnsAndFooter(); + calculateXYForHeaderColumns(); bfGlobalFilterDropdown.setPosition(headerObjX[1], HEADER_OBJ_Y); bfEnvironmentalNoiseDropdown.setPosition(headerObjX[3], HEADER_OBJ_Y); @@ -400,10 +385,6 @@ class FilterUIPopup extends PApplet implements Runnable { filterTypeDropdowns[chan].setPosition(columnObjX[3], rowY); filterOrderDropdowns[chan].setPosition(filterOrderDropdownNewX, rowY); } - - saveButton.setPosition(footerObjX[0], footerObjY); - loadButton.setPosition(footerObjX[1], footerObjY); - defaultButton.setPosition(footerObjX[2], footerObjY); } // Master method to update objects from the FilterSettings Class @@ -920,37 +901,6 @@ class FilterUIPopup extends PApplet implements Runnable { cp5ElementsToCheck.add(masterFilterOrderDropdown); } - private void createFilterSettingsSaveButton(String name, String text, int _x, int _y, int _w, int _h) { - saveButton = createButton(cp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - saveButton.setBorderColor(OBJECT_BORDER_GREY); - saveButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - filterSettings.storeSettings(); - } - }); - } - - private void createFilterSettingsLoadButton(String name, String text, int _x, int _y, int _w, int _h) { - loadButton = createButton(cp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - loadButton.setBorderColor(OBJECT_BORDER_GREY); - loadButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - filterSettings.loadSettings(); - } - }); - } - - private void createFilterSettingsDefaultButton(String name, String text, int _x, int _y, int _w, int _h) { - defaultButton = createButton(cp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - defaultButton.setBorderColor(OBJECT_BORDER_GREY); - defaultButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - filterSettings.revertAllChannelsToDefaultValues(); - filterSettingsWereLoadedFromFile = true; - } - }); - } - private void createMasterOnOffButton(String name, final String text, int _x, int _y, int _w, int _h) { masterOnOffButton = createButton(cp5, name, text, _x, _y, _w, _h, 0, h2, 16, SUBNAV_LIGHTBLUE, WHITE, BUTTON_HOVER, BUTTON_PRESSED, (Integer) null, -2); masterOnOffButton.setCircularButton(true); @@ -1002,25 +952,9 @@ class FilterUIPopup extends PApplet implements Runnable { filterOrderDropdowns[chan].setVisible(showAllChannels); } - setFooterObjYPosition(myEnum); - saveButton.setPosition(footerObjX[0], footerObjY); - loadButton.setPosition(footerObjX[1], footerObjY); - needToResizePopup = true; } - private void setFooterObjYPosition(FilterChannelSelect myEnum) { - boolean showAllChannels = myEnum == FilterChannelSelect.CUSTOM_CHANNELS; - int numChans = filterSettings.getChannelCount(); - int footerMaxHeightY = HEADER_HEIGHT*2 + SM_SPACER*(numChans+4) + uiObjectHeight*(numChans+1) + LG_SPACER*2 + EXPANDER_HEIGHT; - int footerMinHeightY = HEADER_HEIGHT*2 + SM_SPACER*4 + uiObjectHeight + LG_SPACER + EXPANDER_HEIGHT; - footerObjY = showAllChannels ? footerMaxHeightY : footerMinHeightY; - - if (!EXPANDER_IS_USED) { - footerObjY -= EXPANDER_HEIGHT + SM_SPACER; - } - } - private void filterSettingWasModifiedOnChannel(int chan) { filterSettingsWereModified[chan] = true; filterSettingsWereModifiedFadeCounter[chan] = millis(); diff --git a/OpenBCI_GUI/FocusEnums.pde b/OpenBCI_GUI/FocusEnums.pde index 7fa92c97f..59ce3191c 100644 --- a/OpenBCI_GUI/FocusEnums.pde +++ b/OpenBCI_GUI/FocusEnums.pde @@ -14,7 +14,6 @@ public enum FocusXLim implements IndexingInterface private int index; private int value; private String label; - private static FocusXLim[] vals = values(); FocusXLim(int _index, int _value, String _label) { this.index = _index; @@ -35,14 +34,6 @@ public enum FocusXLim implements IndexingInterface public int getIndex() { return index; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum FocusMetric implements IndexingInterface @@ -54,7 +45,6 @@ public enum FocusMetric implements IndexingInterface 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; @@ -80,14 +70,6 @@ public enum FocusMetric implements IndexingInterface public String getIdealStateString() { return idealState; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum FocusClassifier implements IndexingInterface @@ -99,8 +81,6 @@ public enum FocusClassifier implements IndexingInterface private String label; private BrainFlowClassifiers classifier; - private static FocusClassifier[] vals = values(); - FocusClassifier(int _index, String _label, BrainFlowClassifiers _classifier) { this.index = _index; this.label = _label; @@ -120,14 +100,6 @@ public enum FocusClassifier implements IndexingInterface public BrainFlowClassifiers getClassifier() { return classifier; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum FocusThreshold implements IndexingInterface @@ -142,8 +114,6 @@ public enum FocusThreshold implements IndexingInterface private float value; private String label; - private static FocusThreshold[] vals = values(); - FocusThreshold(int _index, float _value, String _label) { this.index = _index; this.value = _value; @@ -163,12 +133,4 @@ public enum FocusThreshold implements IndexingInterface public int getIndex() { return index; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } \ No newline at end of file diff --git a/OpenBCI_GUI/Interactivity.pde b/OpenBCI_GUI/Interactivity.pde index 9145bf914..eba496f1c 100644 --- a/OpenBCI_GUI/Interactivity.pde +++ b/OpenBCI_GUI/Interactivity.pde @@ -101,14 +101,14 @@ void parseKey(char val) { ///////////////////// Save User settings lowercase n case 'n': println("Interactivity: Save key pressed!"); - settings.save(settings.getPath("User", eegDataSource, globalChannelCount)); - outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory settings."); + sessionSettings.save(sessionSettings.getPath("User", eegDataSource, globalChannelCount)); + outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory sessionSettings."); return; ///////////////////// Load User settings uppercase N case 'N': println("Interactivity: Load key pressed!"); - settings.loadKeyPressed(); + sessionSettings.loadKeyPressed(); return; case '?': @@ -213,19 +213,20 @@ void parseKey(char val) { // Fixes #976. These keyboard shortcuts enable synthetic square waves on Ganglion and Cyton if (currentBoard instanceof BoardGanglion || currentBoard instanceof BoardCyton) { if (val == '[' || val == ']') { - println("Expert Mode: '" + val + "' pressed. Sending to Ganglion..."); + println("Expert Mode: '" + val + "' pressed. Sending to board..."); Boolean success = ((Board)currentBoard).sendCommand(str(val)).getKey(); if (success) { - outputSuccess("Expert Mode: Success sending '" + val + "' to Ganglion!"); + outputSuccess("Expert Mode: Success sending '" + val + "' to board!"); } else { - outputWarn("Expert Mode: Error sending '" + val + "' to Ganglion. Try again with data stream stopped."); + outputWarn("Expert Mode: Error sending '" + val + "' to board. Try again with data stream stopped."); } return; } } // Check for software marker keyboard shortcuts - if (w_marker.checkForMarkerKeyPress(val)) { + W_Marker markerWidget = (W_Marker) widgetManager.getWidget("W_Marker"); + if (markerWidget.checkForMarkerKeyPress(val)) { return; } @@ -241,7 +242,7 @@ void mouseDragged() { //calling mouse dragged inly outside of Control Panel if (controlPanel.isOpen == false) { - wm.mouseDragged(); + widgetManager.mouseDragged(); } } } @@ -261,7 +262,7 @@ synchronized void mousePressed() { if (controlPanel.isOpen == false) { //was the stopButton pressed? - wm.mousePressed(); + widgetManager.mousePressed(); } } @@ -295,7 +296,7 @@ synchronized void mouseReleased() { if (systemMode >= SYSTEMMODE_POSTINIT) { // GUIWidgets_mouseReleased(); // to replace GUI_Manager version (above) soon... cdr 7/25/16 - wm.mouseReleased(); + widgetManager.mouseReleased(); } } diff --git a/OpenBCI_GUI/Layout.pde b/OpenBCI_GUI/Layout.pde new file mode 100644 index 000000000..8dcbc732f --- /dev/null +++ b/OpenBCI_GUI/Layout.pde @@ -0,0 +1,26 @@ + +//The Layout class is an organizational tool. A layout consists of a combination of containers (found in Container.pde). +class Layout { + + Container[] myContainers; + int[] containerInts; + + Layout(int[] _myContainers){ //when creating a new layout, you pass in the integer #s of the containers you want as part of the layout ... so if I pass in the array {5}, my layout is 1 container that takes up the whole GUI body + //constructor stuff + myContainers = new Container[_myContainers.length]; //make the myContainers array equal to the size of the incoming array of ints + containerInts = new int[_myContainers.length]; + for(int i = 0; i < _myContainers.length; i++){ + myContainers[i] = container[_myContainers[i]]; + containerInts[i] = _myContainers[i]; + } + } + + Container getContainer(int _numContainer){ + if(_numContainer < myContainers.length){ + return myContainers[_numContainer]; + } else{ + println("Widget Manager: Tried to return a non-existant container..."); + return myContainers[myContainers.length-1]; + } + } +}; \ No newline at end of file diff --git a/OpenBCI_GUI/MarkerEnums.pde b/OpenBCI_GUI/MarkerEnums.pde new file mode 100644 index 000000000..e80b592b3 --- /dev/null +++ b/OpenBCI_GUI/MarkerEnums.pde @@ -0,0 +1,64 @@ +public enum MarkerWindow implements IndexingInterface +{ + FIVE (0, 5, "5 sec"), + TEN (1, 10, "10 sec"), + TWENTY (2, 20, "20 sec"); + + private int index; + private int value; + private String label; + + MarkerWindow(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 enum MarkerVertScale implements IndexingInterface +{ + AUTO (0, 0, "Auto"), + TWO (1, 2, "2"), + FOUR (2, 4, "4"), + EIGHT (3, 8, "8"), + TEN (4, 10, "10"), + TWENTY (6, 20, "20"); + + private int index; + private int value; + private String label; + + MarkerVertScale(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; + } +} diff --git a/OpenBCI_GUI/NetworkingDataAccumulator.pde b/OpenBCI_GUI/NetworkingDataAccumulator.pde index 5c9d2074e..ec4477a9d 100644 --- a/OpenBCI_GUI/NetworkingDataAccumulator.pde +++ b/OpenBCI_GUI/NetworkingDataAccumulator.pde @@ -294,7 +294,7 @@ public class NetworkingDataAccumulator { } public float[] getNormalizedBandPowerData() { - return w_bandPower.getNormalizedBPSelectedChannels(); + return ((W_BandPower) widgetManager.getWidget("W_BandPower")).getNormalizedBPSelectedChannels(); } public float[] getEmgNormalizedValues() { @@ -302,18 +302,18 @@ public class NetworkingDataAccumulator { } public int getPulseSensorBPM() { - return w_pulseSensor.getBPM(); + return ((W_PulseSensor) widgetManager.getWidget("W_PulseSensor")).getBPM(); } public int getPulseSensorIBI() { - return w_pulseSensor.getIBI(); + return ((W_PulseSensor) widgetManager.getWidget("W_PulseSensor")).getIBI(); } public int getFocusValueExceedsThreshold() { - return w_focus.getMetricExceedsThreshold(); + return ((W_Focus) widgetManager.getWidget("W_Focus")).getMetricExceedsThreshold(); } public float[] getEMGJoystickXY() { - return w_emgJoystick.getJoystickXY(); + return ((W_EmgJoystick) widgetManager.getWidget("W_EmgJoystick")).getJoystickXY(); } } \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkingEnums.pde b/OpenBCI_GUI/NetworkingEnums.pde index 49a2dea9f..e03b0cccb 100644 --- a/OpenBCI_GUI/NetworkingEnums.pde +++ b/OpenBCI_GUI/NetworkingEnums.pde @@ -4,21 +4,21 @@ public enum NetworkProtocol implements IndexingInterface { LSL (2, "LSL"), SERIAL (3, "Serial"); - private final int INDEX; - private final String NAME; + private int index; + private String label; private static final NetworkProtocol[] VALUES = values(); - NetworkProtocol(int index, String name) { - INDEX = index; - NAME = name; + NetworkProtocol(int index, String label) { + this.index = index; + this.label = label; } public int getIndex() { - return INDEX; + return index; } public String getString() { - return NAME; + return label; } public static NetworkProtocol getByIndex(int _index) { @@ -38,15 +38,6 @@ public enum NetworkProtocol implements IndexingInterface { } return null; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : VALUES) { - enumStrings.add(val.getString()); - } - enumStrings.remove("Serial"); // #354 - return enumStrings; - } } public enum NetworkDataType implements IndexingInterface { @@ -63,33 +54,33 @@ public enum NetworkDataType implements IndexingInterface { EMG_JOYSTICK (9, "EMGJoystick", "emgJoystick", "emg-joystick"), MARKER (10, "Marker", "marker", "marker"); - private final int INDEX; - private final String NAME; - private final String UDP_KEY; - private final String OSC_KEY; + private int index; + private String label; + private String udpKey; + private String oscKey; private static final NetworkDataType[] VALUES = values(); - NetworkDataType(int index, String name, String udpKey, String oscKey) { - INDEX = index; - NAME = name; - UDP_KEY = udpKey; - OSC_KEY = oscKey; + NetworkDataType(int index, String label, String udpKey, String oscKey) { + this.index = index; + this.label = label; + this.udpKey = udpKey; + this.oscKey = oscKey; } public int getIndex() { - return INDEX; + return index; } public String getString() { - return NAME; + return label; } public String getUDPKey() { - return UDP_KEY; + return udpKey; } public String getOSCKey() { - return OSC_KEY; + return oscKey; } public static NetworkDataType getByString(String _name) { diff --git a/OpenBCI_GUI/NetworkingUI.pde b/OpenBCI_GUI/NetworkingUI.pde index 15f43cba0..c19b0f6f8 100644 --- a/OpenBCI_GUI/NetworkingUI.pde +++ b/OpenBCI_GUI/NetworkingUI.pde @@ -490,6 +490,7 @@ class NetworkingUI extends PApplet implements Runnable { } private void createProtocolDropdown() { + List protocolList = EnumHelper.getEnumStrings(NetworkProtocol.class); protocolDropdown = nwCp5.addScrollableList("networkingProtocolDropdown") .setOpen(false) .setOutlineColor(OPENBCI_DARKBLUE) @@ -499,10 +500,10 @@ class NetworkingUI extends PApplet implements Runnable { .setColorForeground(color(125)) // border color when not selected .setColorActive(BUTTON_PRESSED) // border color when selected // .setColorCursor(color(26,26,26)) - .setSize(TEXTFIELD_WIDTH, (NetworkProtocol.getEnumStringsAsList().size() + 1) * (ITEM_HEIGHT))// + maxFreqList.size()) + .setSize(TEXTFIELD_WIDTH, (protocolList.size() + 1) * (ITEM_HEIGHT))// + maxFreqList.size()) .setBarHeight(ITEM_HEIGHT) // height of top/primary bar .setItemHeight(ITEM_HEIGHT) // height of all item/dropdown bars - .addItems(NetworkProtocol.getEnumStringsAsList()) // used to be .addItems(maxFreqList) + .addItems(protocolList) // used to be .addItems(maxFreqList) .setVisible(true); protocolDropdown.getCaptionLabel() // the caption label is the text object in the primary bar .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index f2ea0f895..b6cb0cbc1 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -121,6 +121,7 @@ String sdData_fname = "N/A"; //only used if loading input data from a sd file DataSource currentBoard = new BoardNull(); DataLogger dataLogger = new DataLogger(); +OdfFileDuration odfFileDuration = OdfFileDuration.SIXTY_MINUTES; // Intialize interface protocols InterfaceSerial iSerial = new InterfaceSerial(); //This is messy, half-deprecated code. See comments in InterfaceSerial.pde - Nov. 2020 @@ -256,6 +257,8 @@ final color SIGNAL_CHECK_YELLOW = color(221, 178, 13); //Same color as yellow ch final color SIGNAL_CHECK_YELLOW_LOWALPHA = color(221, 178, 13, 150); final color SIGNAL_CHECK_RED = BOLD_RED; final color SIGNAL_CHECK_RED_LOWALPHA = color(224, 56, 45, 150); +public CColor dropdownColorsGlobal = new CColor(); + //Channel Colors -- Defaulted to matching the OpenBCI electrode ribbon cable //Channel Colors -- Defaulted to matching the OpenBCI electrode ribbon cable @@ -275,12 +278,10 @@ final int COLOR_SCHEME_ALTERNATIVE_A = 2; // int COLOR_SCHEME_ALTERNATIVE_B = 3; int colorScheme = COLOR_SCHEME_ALTERNATIVE_A; -WidgetManager wm; -boolean wmVisible = true; -CColor cp5_colors; +WidgetManager widgetManager; //Global variable for general navigation bar height -final int navHeight = 22; +final int NAV_HEIGHT = 22; ButtonHelpText buttonHelpText; @@ -293,19 +294,19 @@ public final static String stopButton_pressToStop_txt = "Stop Data Stream"; public final static String stopButton_pressToStart_txt = "Start Data Stream"; DirectoryManager directoryManager; -SessionSettings settings; +SessionSettings sessionSettings; GuiSettings guiSettings; DataProcessing dataProcessing; FilterSettings filterSettings; -NetworkingUI networkUI; FilterUIPopup filterUI; +NetworkingUI networkUI; DeveloperCommandPopup developerCommandPopup; final int navBarHeight = 32; TopNav topNav; ddf.minim.analysis.FFT[] fftBuff = new ddf.minim.analysis.FFT[globalChannelCount]; //from the minim library -boolean isFFTFiltered = true; //yes by default ... this is used in dataProcessing.pde to determine which uV array feeds the FFT calculation +GlobalFFTSettings globalFFTSettings; StringBuilder globalScreenResolution; StringBuilder globalScreenDPI; @@ -342,6 +343,9 @@ void settings() { } void setup() { + + ourApplet = this; + frameRate(120); copyPaste = new CopyPaste(); @@ -378,6 +382,13 @@ void setup() { openbciLogoCog = loadImage("obci-logo-blu-cog.png"); + dropdownColorsGlobal.setActive((int)BUTTON_PRESSED); //bg color of box when pressed + dropdownColorsGlobal.setForeground((int)BUTTON_HOVER); //when hovering over any box (primary or dropdown) + dropdownColorsGlobal.setBackground((int)color(255)); //bg color of boxes (including primary) + dropdownColorsGlobal.setCaptionLabel((int)color(1, 18, 41)); //color of text in primary box + // dropdownColorsGlobal.setValueLabel((int)color(1, 18, 41)); //color of text in all dropdown boxes + dropdownColorsGlobal.setValueLabel((int)color(100)); //color of text in all dropdown boxes + // check if the current directory is writable File dummy = new File(sketchPath()); if (!dummy.canWrite()) { @@ -428,13 +439,10 @@ void setup() { // Copy sample data to the Users' Documents folder + create Recordings folder directoryManager.init(); - settings = new SessionSettings(); + sessionSettings = new SessionSettings(); guiSettings = new GuiSettings(directoryManager.getSettingsPath()); userPlaybackHistoryFile = directoryManager.getSettingsPath()+"UserPlaybackHistory.json"; - //open window - ourApplet = this; - // Bug #426: If setup takes too long, JOGL will time out waiting for the GUI to draw something. // moving the setup to a separate thread solves this. We just have to make sure not to // start drawing until delayed setup is done. @@ -445,8 +453,8 @@ void delayedSetup() { smooth(); //turn this off if it's too slow surface.setResizable(true); //updated from frame.setResizable in Processing 2 - settings.widthOfLastScreen = width; //for screen resizing (Thank's Tao) - settings.heightOfLastScreen = height; + sessionSettings.widthOfLastScreen = width; //for screen resizing (Thank's Tao) + sessionSettings.heightOfLastScreen = height; setupContainers(); @@ -533,8 +541,8 @@ synchronized void draw() { dataProcessing.networkingDataAccumulator.compareAndSetNetworkingFrameLocks(); } } else if (systemMode == SYSTEMMODE_INTROANIMATION) { - if (settings.introAnimationInit == 0) { - settings.introAnimationInit = millis(); + if (sessionSettings.introAnimationInit == 0) { + sessionSettings.introAnimationInit = millis(); } else { introAnimation(); } @@ -743,19 +751,19 @@ void initSystem() { topNav.controlPanelCollapser.setOff(); verbosePrint("OpenBCI_GUI: initSystem: -- Init 4 -- " + millis()); - wm = new WidgetManager(this); + widgetManager = new WidgetManager(); verbosePrint("OpenBCI_GUI: initSystem: -- Init 5 -- " + millis()); //don't save default session settings for StreamingBoard if (eegDataSource != DATASOURCE_STREAMING) { //Init software settings: create default settings file that is datasource unique - settings.init(); + sessionSettings.init(); verbosePrint("OpenBCI_GUI: initSystem: Session settings initialized"); } if (guiSettings.getAutoLoadSessionSettings()) { - settings.autoLoadSessionSettings(); + sessionSettings.autoLoadSessionSettings(); verbosePrint("OpenBCI_GUI: initSystem: User default session settings automatically loaded"); } @@ -803,7 +811,7 @@ public int getDownsampledBufferSize() { * @description Get the correct points of FFT based on sampling rate * @returns `int` - Points of FFT. 125Hz, 200Hz, 250Hz -> 256points. 1000Hz -> 1024points. 1600Hz -> 2048 points. */ -int getNfftSafe() { +int getNumFFTPoints() { int sampleRate = currentBoard.getSampleRate(); switch (sampleRate) { case 500: @@ -837,15 +845,17 @@ void initCoreDataObjects() { void initFFTObjectsAndBuffer() { //initialize the FFT objects - for (int Ichan=0; Ichan < globalChannelCount; Ichan++) { - // verbosePrint("Init FFT Buff – " + Ichan); - fftBuff[Ichan] = new ddf.minim.analysis.FFT(getNfftSafe(), currentBoard.getSampleRate()); + for (int channel = 0; channel < globalChannelCount; channel++) { + // verbosePrint("Init FFT Buff – " + channel); + fftBuff[channel] = new ddf.minim.analysis.FFT(getNumFFTPoints(), currentBoard.getSampleRate()); } //make the FFT objects + globalFFTSettings = new GlobalFFTSettings(); + //Attempt initialization. If error, print to console and exit function. //Fixes GUI crash when trying to load outdated recordings try { - initializeFFTObjects(fftBuff, dataProcessingRawBuffer, getNfftSafe(), currentBoard.getSampleRate()); + initializeFFTObjects(fftBuff, dataProcessingRawBuffer, getNumFFTPoints(), currentBoard.getSampleRate()); } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); outputError("Playback file load error. Try using a more recent recording."); @@ -861,8 +871,9 @@ void startRunning() { output("Data stream started."); // todo: this should really be some sort of signal that listeners can register for "OnStreamStarted" // close hardware settings if user starts streaming - if (w_timeSeries.getAdsSettingsVisible()) { - w_timeSeries.closeADSSettings(); + W_TimeSeries timeSeriesWidget = widgetManager.getTimeSeriesWidget(); + if (timeSeriesWidget.getAdsSettingsVisible()) { + timeSeriesWidget.closeADSSettings(); } try { streamTimeElapsed.reset(); @@ -918,8 +929,8 @@ void haltSystem() { } } - if (w_focus != null) { - w_focus.endSession(); + if (widgetManager.getWidgetExists("W_Focus")) { + ((W_Focus) widgetManager.getWidget("W_Focus")).endSession(); } stopRunning(); @@ -968,10 +979,10 @@ void systemUpdate() { // for updating data values and variables //updates while in system control panel before START SYSTEM controlPanel.update(); - if (settings.widthOfLastScreen != width || settings.heightOfLastScreen != height) { + if (sessionSettings.widthOfLastScreen != width || sessionSettings.heightOfLastScreen != height) { topNav.screenHasBeenResized(width, height); - settings.widthOfLastScreen = width; - settings.heightOfLastScreen = height; + sessionSettings.widthOfLastScreen = width; + sessionSettings.heightOfLastScreen = height; //println("W = " + width + " || H = " + height); } } @@ -980,23 +991,23 @@ void systemUpdate() { // for updating data values and variables //alternative component listener function (line 177 mouseReleased- 187 frame.addComponentListener) for processing 3, //Component listener doesn't seem to work, so staying with this method for now - if (settings.widthOfLastScreen != width || settings.heightOfLastScreen != height) { - settings.screenHasBeenResized = true; - settings.timeOfLastScreenResize = millis(); - settings.widthOfLastScreen = width; - settings.heightOfLastScreen = height; + if (sessionSettings.widthOfLastScreen != width || sessionSettings.heightOfLastScreen != height) { + sessionSettings.screenHasBeenResized = true; + sessionSettings.timeOfLastScreenResize = millis(); + sessionSettings.widthOfLastScreen = width; + sessionSettings.heightOfLastScreen = height; } //re-initialize GUI if screen has been resized and it's been more than 1/2 seccond (to prevent reinitialization of GUI from happening too often) - if (settings.screenHasBeenResized && settings.timeOfLastScreenResize + 500 > millis()) { + if (sessionSettings.screenHasBeenResized && sessionSettings.timeOfLastScreenResize + 500 > millis()) { ourApplet = this; //reset PApplet... topNav.screenHasBeenResized(width, height); - wm.screenResized(); - settings.screenHasBeenResized = false; + widgetManager.screenResized(); + sessionSettings.screenHasBeenResized = false; } - if (wm.isWMInitialized) { - wm.update(); + if (widgetManager != null) { + widgetManager.update(); } } } @@ -1008,7 +1019,7 @@ void systemDraw() { //for drawing to the screen //background(255); //clear the screen if (systemMode >= SYSTEMMODE_POSTINIT) { - wm.draw(); + widgetManager.draw(); drawContainers(); } @@ -1062,7 +1073,6 @@ void systemInitSession() { //Global function to update the number of channels void updateGlobalChannelCount(int _channelCount) { globalChannelCount = _channelCount; - settings.sessionSettingsChannelCount = _channelCount; //used in SoftwareSettings.pde only fftBuff = new ddf.minim.analysis.FFT[globalChannelCount]; //reinitialize the FFT buffer println("OpenBCI_GUI: Channel count set to " + str(globalChannelCount)); } @@ -1074,8 +1084,8 @@ void introAnimation() { int t1 = 0; float transparency = 0; - if (millis() >= settings.introAnimationInit) { - transparency = map(millis() - settings.introAnimationInit, t1, settings.introAnimationDuration, 0, 255); + if (millis() >= sessionSettings.introAnimationInit) { + transparency = map(millis() - sessionSettings.introAnimationInit, t1, sessionSettings.INTRO_ANIMATION_DURATION, 0, 255); verbosePrint(String.valueOf(transparency)); tint(255, transparency); //draw OpenBCI Logo Front & Center @@ -1089,7 +1099,7 @@ void introAnimation() { } //Exit intro animation when the duration has expired AND the Control Panel is ready - if ((millis() >= settings.introAnimationInit + settings.introAnimationDuration) + if ((millis() >= sessionSettings.introAnimationInit + sessionSettings.INTRO_ANIMATION_DURATION) && controlPanel != null) { systemMode = SYSTEMMODE_PREINIT; controlPanel.open(); diff --git a/OpenBCI_GUI/PopupMessageHardwareSettings.pde b/OpenBCI_GUI/PopupMessageHardwareSettings.pde index f3ba3473b..bb2100d5c 100644 --- a/OpenBCI_GUI/PopupMessageHardwareSettings.pde +++ b/OpenBCI_GUI/PopupMessageHardwareSettings.pde @@ -92,7 +92,7 @@ class PopupMessageHardwareSettings extends PopupMessage { myButton.onPress(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { topNav.dataStreamTogglePressed(); - w_timeSeries.setAdsSettingsVisible(true); + widgetManager.getTimeSeriesWidget().setAdsSettingsVisible(true); Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); frame.dispose(); exit(); diff --git a/OpenBCI_GUI/SessionSettings.pde b/OpenBCI_GUI/SessionSettings.pde index 446370204..f8d1bf644 100644 --- a/OpenBCI_GUI/SessionSettings.pde +++ b/OpenBCI_GUI/SessionSettings.pde @@ -1,1066 +1,432 @@ -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -/* -// This sketch saves and loads User Settings that appear during Sessions. -// -- All Time Series widget settings in Live, Playback, and Synthetic modes -// -- All FFT widget settings -// -- Default Layout, Board Mode, and other Global Settings -// -- Networking Mode and All settings for active networking protocol -// -- Accelerometer, Analog Read, Head Plot, Band Power, and Spectrogram -// -- Widget/Container Pairs -// -- OpenBCI Data Format Settings (.txt and .csv) -// Created: Richard Waltman - May/June 2018 -// -// -- Start System first! -// -- Lowercase 'n' to Save -// -- Capital 'N' to Load -// -- Functions saveGUIsettings() and loadGUISettings() are called: -// - during system initialization between checkpoints 4 and 5 -// - in Interactivty.pde with the rest of the keyboard shortcuts -// - in TopNav.pde when "Config" --> "Save Settings" || "Load Settings" is clicked -// -- This allows User to store snapshots of most GUI settings in Users/.../Documents/OpenBCI_GUI/Settings/ -// -- After loading, only a few actions are required: start/stop the data stream and networking streams, open/close serial port -// -// Tips on adding a new setting: -// -- figure out if the setting is Global, in an existing widget, or in a new class or widget -// -- read the comments -// -- once you find the right place to add your setting, you can copy the surrounding style -// -- uses JSON keys -// -- Example2: GUI version and settings version -// -- Requires new JSON key 'version` and settingsVersion -// -*/ -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Refactored: Richard Waltman, April 2025 -///////////////////////////////// -// SessionSettings Class // -///////////////////////////////// class SessionSettings { - //Current version to save to JSON - String settingsVersion = "3.0.0"; - //for screen resizing - boolean screenHasBeenResized = false; - float timeOfLastScreenResize = 0; - int widthOfLastScreen = 0; - int heightOfLastScreen = 0; - //default layout variables - int currentLayout; - //Used to time the GUI intro animation - int introAnimationInit = 0; - final int introAnimationDuration = 2500; - //Max File Size #461, default option 4 -> 60 minutes - public final String[] fileDurations = {"5 Minutes", "15 minutes", "30 Minutes", "60 Minutes", "120 Minutes", "No Limit"}; - public final int[] fileDurationInts = {5, 15, 30, 60, 120, -1}; - public final int defaultOBCIMaxFileSize = 3; //4th option from the above list - private boolean logFileIsOpen = false; - private long logFileStartTime; - private long logFileMaxDurationNano = -1; - //this is a global CColor that determines the style of all widget dropdowns ... this should go in WidgetManager.pde - CColor dropdownColors = new CColor(); - ///These `Save` vars are set to default when each widget instantiates - ///and updated every time user selects from dropdown - //Accelerometer settings - int accVertScaleSave; - int accHorizScaleSave; - //FFT plot settings, - int fftMaxFrqSave; - int fftMaxuVSave; - int fftLogLinSave; - int fftSmoothingSave; - int fftFilterSave; - //Analog Read settings - int arVertScaleSave; - int arHorizScaleSave; - //Headplot settings - int hpIntensitySave; - int hpPolaritySave; - int hpContoursSave; - int hpSmoothingSave; - //Used to check if a playback file has data - int minNumRowsPlaybackFile = int(currentBoard.getSampleRate()); - //Spectrogram Widget settings - int spectMaxFrqSave; - int spectSampleRateSave; - int spectLogLinSave; - - //default configuration settings file location and file name variables - private String sessionPath = ""; - final String[] userSettingsFiles = { - "CytonUserSettings.json", - "DaisyUserSettings.json", - "GanglionUserSettings.json", - "PlaybackUserSettings.json", - "SynthFourUserSettings.json", - "SynthEightUserSettings.json", - "SynthSixteenUserSettings.json" - }; - final String[] defaultSettingsFiles = { - "CytonDefaultSettings.json", - "DaisyDefaultSettings.json", - "GanglionDefaultSettings.json", - "PlaybackDefaultSettings.json", - "SynthFourDefaultSettings.json", - "SynthEightDefaultSettings.json", - "SynthSixteenDefaultSettings.json" - }; - - //Used to set text in dropdown menus when loading FFT settings - String[] fftMaxFrqArray = {"20 Hz", "40 Hz", "60 Hz", "100 Hz", "120 Hz", "250 Hz", "500 Hz", "800 Hz"}; - String[] fftVertScaleArray = {"10 uV", "50 uV", "100 uV", "1000 uV"}; - String[] fftLogLinArray = {"Log", "Linear"}; //share this with spectrogram also - String[] fftSmoothingArray = {"0.0", "0.5", "0.75", "0.9", "0.95", "0.98", "0.99", "0.999"}; - String[] fftFilterArray = {"Filtered", "Unfilt."}; - - //Used to set text in dropdown menus when loading Accelerometer settings - String[] accVertScaleArray = {"Auto","1 g", "2 g"}; - String[] accHorizScaleArray = {"Sync", "1 sec", "3 sec", "5 sec", "10 sec", "20 sec"}; - - //Used to set text in dropdown menus when loading Analog Read settings - String[] arVertScaleArray = {"Auto", "50", "100", "200", "400", "1000", "10000"}; - String[] arHorizScaleArray = {"Sync", "1 sec", "3 sec", "5 sec", "10 sec", "20 sec"}; - - //Used to set text in dropdown menus when loading Head Plot settings - String[] hpIntensityArray = {"4x", "2x", "1x", "0.5x", "0.2x", "0.02x"}; - String[] hpPolarityArray = {"+/-", " + "}; - String[] hpContoursArray = {"ON", "OFF"}; - String[] hpSmoothingArray = {"0.0", "0.5", "0.75", "0.9", "0.95", "0.98"}; - - //Used to set text in dropdown menus when loading Spectrogram Setings - String[] spectMaxFrqArray = {"20 Hz", "40 Hz", "60 Hz", "100 Hz", "120 Hz", "250 Hz"}; - String[] spectSampleRateArray = {"30 Min.", "6 Min.", "3 Min.", "1.5 Min.", "1 Min."}; - - //Load Accel. dropdown variables - int loadAccelVertScale; - int loadAccelHorizScale; - - //Load Analog Read dropdown variables - int loadAnalogReadVertScale; - int loadAnalogReadHorizScale; - - //Load FFT dropdown variables - int fftMaxFrqLoad; - int fftMaxuVLoad; - int fftLogLinLoad; - int fftSmoothingLoad; - int fftFilterLoad; - - //Load Headplot dropdown variables - int hpIntensityLoad; - int hpPolarityLoad; - int hpContoursLoad; - int hpSmoothingLoad; - - //Band Power widget settings - //smoothing and filter dropdowns are linked to FFT, so no need to save again - List loadBPActiveChans = new ArrayList(); - int loadBPAutoClean; - int loadBPAutoCleanThreshold; - int loadBPAutoCleanTimer; - - //Spectrogram widget settings - List loadSpectActiveChanTop = new ArrayList(); - List loadSpectActiveChanBot = new ArrayList(); - int spectMaxFrqLoad; - int spectSampleRateLoad; - int spectLogLinLoad; - - //Networking Settings save/load variables - JSONObject loadNetworkingSettings; - - //EMG Widget - List loadEmgActiveChannels = new ArrayList(); - - //EMG Joystick Widget - int loadEmgJoystickSmoothing; - List loadEmgJoystickInputs = new ArrayList(); - - //Marker Widget - private int loadMarkerWindow; - private int loadMarkerVertScale; - - //Focus Widget - private int loadFocusMetric; - private int loadFocusThreshold; - private int loadFocusWindow; - - //Primary JSON objects for saving and loading data + // Current version and configuration + private String settingsVersion = "5.0.0"; + public int currentLayout; + + // Screen resizing variables + public boolean screenHasBeenResized = false; + public float timeOfLastScreenResize = 0; + public int widthOfLastScreen = 0, heightOfLastScreen = 0; + + // Animation timer + public int introAnimationInit = 0; + public final int INTRO_ANIMATION_DURATION = 2500; + + // JSON data for saving/loading private JSONObject saveSettingsJSONData; private JSONObject loadSettingsJSONData; - - private final String kJSONKeyDataInfo = "dataInfo"; - private final String kJSONKeyTimeSeries = "timeSeries"; - private final String kJSONKeySettings = "settings"; - private final String kJSONKeyFFT = "fft"; - private final String kJSONKeyAccel = "accelerometer"; - private final String kJSONKeyNetworking = "networking"; - private final String kJSONKeyHeadplot = "headplot"; - private final String kJSONKeyBandPower = "bandPower"; - private final String kJSONKeyWidget = "widget"; - private final String kJSONKeyVersion = "version"; - private final String kJSONKeySpectrogram = "spectrogram"; - private final String kJSONKeyEmg = "emg"; - private final String kJSONKeyEmgJoystick = "emgJoystick"; - private final String kJSONKeyMarker = "marker"; - private final String kJSONKeyFocus = "focus"; - - //used only in this class to count the number of channels being used while saving/loading, this gets updated in updateGlobalChannelCount whenever the number of channels being used changes - int sessionSettingsChannelCount; - int numChanloaded; + + // Dialog control variables + String saveDialogName; + String loadDialogName; + String controlEventDataSource; + + // Error handling boolean chanNumError = false; - int numLoadedWidgets; - String [] loadedWidgetsArray; - int loadFramerate; - int loadDatasource; boolean dataSourceError = false; - - String saveDialogName; //Used when Save button is pressed - String loadDialogName; //Used when Load button is pressed - String controlEventDataSource; //Used for output message on system start - Boolean errorUserSettingsNotFound = false; //For error catching + boolean errorUserSettingsNotFound = false; + boolean loadErrorCytonEvent = false; int loadErrorTimerStart; - int loadErrorTimeWindow = 5000; //Time window in milliseconds to apply channel settings to Cyton board. This is to avoid a GUI crash at ~ 4500-5000 milliseconds. - Boolean loadErrorCytonEvent = false; - final int initTimeoutThreshold = 12000; //Timeout threshold in milliseconds + int loadErrorTimeWindow = 5000; + final int initTimeoutThreshold = 12000; + + // Constants for JSON keys + private final String + KEY_GLOBAL = "globalSettings", + KEY_VERSION = "guiVersion", + KEY_SETTINGS_VERSION = "sessionSettingsVersion", + KEY_CHANNELS = "channelCount", + KEY_DATA_SOURCE = "dataSource", + KEY_SMOOTHING = "dataSmoothing", + KEY_LAYOUT = "widgetLayout", + KEY_NETWORKING = "networking", + KEY_CONTAINERS = "widgetContainerSettings", + KEY_WIDGET_SETTINGS = "widgetSettings", + KEY_FILTER_SETTINGS = "filterSettings", + KEY_EMG_SETTINGS = "emgSettings"; + + // File paths configuration + private final String[][] SETTING_FILES = { + {"CytonUserSettings.json", "CytonDefaultSettings.json"}, + {"DaisyUserSettings.json", "DaisyDefaultSettings.json"}, + {"GanglionUserSettings.json", "GanglionDefaultSettings.json"}, + {"PlaybackUserSettings.json", "PlaybackDefaultSettings.json"}, + {"SynthFourUserSettings.json", "SynthFourDefaultSettings.json"}, + {"SynthEightUserSettings.json", "SynthEightDefaultSettings.json"}, + {"SynthSixteenUserSettings.json", "SynthSixteenDefaultSettings.json"} + }; + private final int FILE_USER = 0, FILE_DEFAULT = 1; - SessionSettings() { - //Instantiated on app start in OpenBCI_GUI.pde - dropdownColors.setActive((int)BUTTON_PRESSED); //bg color of box when pressed - dropdownColors.setForeground((int)BUTTON_HOVER); //when hovering over any box (primary or dropdown) - dropdownColors.setBackground((int)color(255)); //bg color of boxes (including primary) - dropdownColors.setCaptionLabel((int)color(1, 18, 41)); //color of text in primary box - // dropdownColors.setValueLabel((int)color(1, 18, 41)); //color of text in all dropdown boxes - dropdownColors.setValueLabel((int)color(100)); //color of text in all dropdown boxes - - setLogFileDurationChoice(defaultOBCIMaxFileSize); - } - - /////////////////////////////////// - // OpenBCI Data Format Functions // - /////////////////////////////////// - - public void setLogFileIsOpen (boolean _toggle) { - logFileIsOpen = _toggle; - } - - public boolean isLogFileOpen() { - return logFileIsOpen; - } - - public void setLogFileStartTime(long _time) { - logFileStartTime = _time; - verbosePrint("Settings: LogFileStartTime = " + _time); - } - - public void setLogFileDurationChoice(int choice) { - logFileMaxDurationNano = fileDurationInts[choice] * 1000000000L * 60; - println("Settings: LogFileMaxDuration = " + fileDurationInts[choice] + " minutes"); - } - - //Only called during live mode && using OpenBCI Data Format - public boolean maxLogTimeReached() { - if (logFileMaxDurationNano < 0) { - return false; - } else { - return (System.nanoTime() - logFileStartTime) > (logFileMaxDurationNano); - } - } - - public void setSessionPath (String _path) { - sessionPath = _path; - } - - public String getSessionPath() { - //println("SESSIONPATH==",sessionPath, millis()); - return sessionPath; - } - - //////////////////////////////////////////////////////////////// - // Init GUI Software Settings // - // // - // - Called during system initialization in OpenBCI_GUI.pde // - //////////////////////////////////////////////////////////////// + /** + * Initialize settings during system startup + */ void init() { - String defaultSettingsFileToSave = getPath("Default", eegDataSource, globalChannelCount); - int defaultNumChanLoaded = 0; - int defaultLoadedDataSource = 0; - String defaultSettingsVersion = ""; - String defaultGUIVersion = ""; - - //Take a snapshot of the default GUI settings on every system init + String defaultFile = getPath("Default", eegDataSource, globalChannelCount); println("InitSettings: Saving Default Settings to file!"); try { - this.save(defaultSettingsFileToSave); //to avoid confusion with save() image + save(defaultFile); } catch (Exception e) { outputError("Failed to save Default Settings during Init. Please submit an Issue on GitHub."); e.printStackTrace(); } } - /////////////////////////////// - // Save GUI Settings // - /////////////////////////////// - void save(String saveGUISettingsFileLocation) { - - //Set up a JSON array + /** + * Save current settings to a file + */ + void save(String saveFilePath) { + // Create main JSON object and global settings saveSettingsJSONData = new JSONObject(); - - //Save the number of channels being used and eegDataSource in the first object - JSONObject saveNumChannelsData = new JSONObject(); - saveNumChannelsData.setInt("Channels", sessionSettingsChannelCount); - saveNumChannelsData.setInt("Data Source", eegDataSource); - //println("Settings: NumChan: " + sessionSettingsChannelCount); - saveSettingsJSONData.setJSONObject(kJSONKeyDataInfo, saveNumChannelsData); - - //Make a new JSON Object for Time Series Settings - JSONObject saveTSSettings = new JSONObject(); - saveTSSettings.setInt("Time Series Vert Scale", w_timeSeries.getTSVertScale().getIndex()); - saveTSSettings.setInt("Time Series Horiz Scale", w_timeSeries.getTSHorizScale().getIndex()); - saveTSSettings.setInt("Time Series Label Mode", w_timeSeries.getTSLabelMode().getIndex()); - //Save data from the Active channel checkBoxes - JSONArray saveActiveChanTS = new JSONArray(); - int numActiveTSChan = w_timeSeries.tsChanSelect.getActiveChannels().size(); - for (int i = 0; i < numActiveTSChan; i++) { - int activeChannel = w_timeSeries.tsChanSelect.getActiveChannels().get(i); - saveActiveChanTS.setInt(i, activeChannel); - } - saveTSSettings.setJSONArray("activeChannels", saveActiveChanTS); - saveSettingsJSONData.setJSONObject(kJSONKeyTimeSeries, saveTSSettings); - - //Make a second JSON object within our JSONArray to store Global settings for the GUI - JSONObject saveGlobalSettings = new JSONObject(); - saveGlobalSettings.setInt("Current Layout", currentLayout); - saveGlobalSettings.setInt("Analog Read Vert Scale", arVertScaleSave); - saveGlobalSettings.setInt("Analog Read Horiz Scale", arHorizScaleSave); + JSONObject globalSettings = new JSONObject(); + + // Add global settings + globalSettings.setString(KEY_VERSION, localGUIVersionString); + globalSettings.setString(KEY_SETTINGS_VERSION, settingsVersion); + globalSettings.setInt(KEY_CHANNELS, globalChannelCount); + globalSettings.setInt(KEY_DATA_SOURCE, eegDataSource); + globalSettings.setInt(KEY_LAYOUT, currentLayout); + + // Add data smoothing setting if applicable if (currentBoard instanceof SmoothingCapableBoard) { - saveGlobalSettings.setBoolean("Data Smoothing", ((SmoothingCapableBoard)currentBoard).getSmoothingActive()); - } - saveSettingsJSONData.setJSONObject(kJSONKeySettings, saveGlobalSettings); - - /////Setup JSON Object for gui version and settings Version - JSONObject saveVersionInfo = new JSONObject(); - saveVersionInfo.setString("gui", localGUIVersionString); - saveVersionInfo.setString("settings", settingsVersion); - saveSettingsJSONData.setJSONObject(kJSONKeyVersion, saveVersionInfo); - - ///////////////////////////////////////////////Setup new JSON object to save FFT settings - JSONObject saveFFTSettings = new JSONObject(); - - //Save FFT_Max Freq Setting. The max frq variable is updated every time the user selects a dropdown in the FFT widget - saveFFTSettings.setInt("FFT_Max Freq", fftMaxFrqSave); - //Save FFT_Max uV Setting. The max uV variable is updated also when user selects dropdown in the FFT widget - saveFFTSettings.setInt("FFT_Max uV", fftMaxuVSave); - //Save FFT_LogLin Setting. Same thing happens for LogLin - saveFFTSettings.setInt("FFT_LogLin", fftLogLinSave); - //Save FFT_Smoothing Setting - saveFFTSettings.setInt("FFT_Smoothing", fftSmoothingSave); - //Save FFT_Filter Setting - if (isFFTFiltered == true) fftFilterSave = 0; - if (isFFTFiltered == false) fftFilterSave = 1; - saveFFTSettings.setInt("FFT_Filter", fftFilterSave); - //Set the FFT JSON Object - saveSettingsJSONData.setJSONObject(kJSONKeyFFT, saveFFTSettings); //next object will be set to sessionSettingsChannelCount+3, etc. - - ///////////////////////////////////////////////Setup new JSON object to save Accelerometer settings - if (w_accelerometer != null) { - JSONObject saveAccSettings = new JSONObject(); - saveAccSettings.setInt("Accelerometer Vert Scale", accVertScaleSave); - saveAccSettings.setInt("Accelerometer Horiz Scale", accHorizScaleSave); - saveSettingsJSONData.setJSONObject(kJSONKeyAccel, saveAccSettings); - } - - ///////////////////////////////////////////////Save Networking settings - String nwSettingsValues = dataProcessing.networkingSettings.getJson(); - JSONObject saveNetworkingSettings = parseJSONObject(nwSettingsValues); - saveSettingsJSONData.setJSONObject(kJSONKeyNetworking, saveNetworkingSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Headplot settings - if (w_headPlot != null) { - JSONObject saveHeadplotSettings = new JSONObject(); - - //Save Headplot Intesity - saveHeadplotSettings.setInt("HP_intensity", hpIntensitySave); - //Save Headplot Polarity - saveHeadplotSettings.setInt("HP_polarity", hpPolaritySave); - //Save Headplot contours - saveHeadplotSettings.setInt("HP_contours", hpContoursSave); - //Save Headplot Smoothing Setting - saveHeadplotSettings.setInt("HP_smoothing", hpSmoothingSave); - //Set the Headplot JSON Object - saveSettingsJSONData.setJSONObject(kJSONKeyHeadplot, saveHeadplotSettings); - } - - ///////////////////////////////////////////////Setup new JSON object to save Band Power settings - JSONObject saveBPSettings = new JSONObject(); - - //Save data from the Active channel checkBoxes - JSONArray saveActiveChanBP = new JSONArray(); - int numActiveBPChan = w_bandPower.bpChanSelect.getActiveChannels().size(); - for (int i = 0; i < numActiveBPChan; i++) { - int activeChannel = w_bandPower.bpChanSelect.getActiveChannels().get(i); - saveActiveChanBP.setInt(i, activeChannel); + globalSettings.setBoolean(KEY_SMOOTHING, + ((SmoothingCapableBoard)currentBoard).getSmoothingActive()); } - saveBPSettings.setJSONArray("activeChannels", saveActiveChanBP); - saveBPSettings.setInt("bpAutoClean", w_bandPower.getAutoClean().getIndex()); - saveBPSettings.setInt("bpAutoCleanThreshold", w_bandPower.getAutoCleanThreshold().getIndex()); - saveBPSettings.setInt("bpAutoCleanTimer", w_bandPower.getAutoCleanTimer().getIndex()); - saveSettingsJSONData.setJSONObject(kJSONKeyBandPower, saveBPSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Spectrogram settings - JSONObject saveSpectrogramSettings = new JSONObject(); - //Save data from the Active channel checkBoxes - Top - JSONArray saveActiveChanSpectTop = new JSONArray(); - int numActiveSpectChanTop = w_spectrogram.spectChanSelectTop.getActiveChannels().size(); - for (int i = 0; i < numActiveSpectChanTop; i++) { - int activeChannel = w_spectrogram.spectChanSelectTop.getActiveChannels().get(i); - saveActiveChanSpectTop.setInt(i, activeChannel); - } - saveSpectrogramSettings.setJSONArray("activeChannelsTop", saveActiveChanSpectTop); - //Save data from the Active channel checkBoxes - Bottom - JSONArray saveActiveChanSpectBot = new JSONArray(); - int numActiveSpectChanBot = w_spectrogram.spectChanSelectBot.getActiveChannels().size(); - for (int i = 0; i < numActiveSpectChanBot; i++) { - int activeChannel = w_spectrogram.spectChanSelectBot.getActiveChannels().get(i); - saveActiveChanSpectBot.setInt(i, activeChannel); - } - saveSpectrogramSettings.setJSONArray("activeChannelsBot", saveActiveChanSpectBot); - //Save Spectrogram_Max Freq Setting. The max frq variable is updated every time the user selects a dropdown in the spectrogram widget - saveSpectrogramSettings.setInt("Spectrogram_Max Freq", spectMaxFrqSave); - saveSpectrogramSettings.setInt("Spectrogram_Sample Rate", spectSampleRateSave); - saveSpectrogramSettings.setInt("Spectrogram_LogLin", spectLogLinSave); - saveSettingsJSONData.setJSONObject(kJSONKeySpectrogram, saveSpectrogramSettings); - - ///////////////////////////////////////////////Setup new JSON object to save EMG Settings - JSONObject saveEMGSettings = new JSONObject(); - - //Save data from the Active channel checkBoxes - JSONArray saveActiveChanEMG = new JSONArray(); - int numActiveEMGChan = w_emg.emgChannelSelect.getActiveChannels().size(); - for (int i = 0; i < numActiveEMGChan; i++) { - int activeChannel = w_emg.emgChannelSelect.getActiveChannels().get(i); - saveActiveChanEMG.setInt(i, activeChannel); - } - saveEMGSettings.setJSONArray("activeChannels", saveActiveChanEMG); - saveSettingsJSONData.setJSONObject(kJSONKeyEmg, saveEMGSettings); - - ///////////////////////////////////////////////Setup new JSON object to save EMG Joystick Settings - JSONObject saveEmgJoystickSettings = new JSONObject(); - saveEmgJoystickSettings.setInt("smoothing", w_emgJoystick.joystickSmoothing.getIndex()); - JSONArray saveEmgJoystickInputs = new JSONArray(); - for (int i = 0; i < w_emgJoystick.getNumEMGInputs(); i++) { - saveEmgJoystickInputs.setInt(i, w_emgJoystick.emgJoystickInputs.getInput(i).getIndex()); - } - saveEmgJoystickSettings.setJSONArray("joystickInputs", saveEmgJoystickInputs); - saveSettingsJSONData.setJSONObject(kJSONKeyEmgJoystick, saveEmgJoystickSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Marker Widget Settings - JSONObject saveMarkerSettings = new JSONObject(); - saveMarkerSettings.setInt("markerWindow", w_marker.getMarkerWindow().getIndex()); - saveMarkerSettings.setInt("markerVertScale", w_marker.getMarkerVertScale().getIndex()); - saveSettingsJSONData.setJSONObject(kJSONKeyMarker, saveMarkerSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Marker Widget Settings - JSONObject saveFocusSettings = new JSONObject(); - saveFocusSettings.setInt("focusMetric", w_focus.getFocusMetric().getIndex()); - saveFocusSettings.setInt("focusThreshold", w_focus.getFocusThreshold().getIndex()); - saveFocusSettings.setInt("focusWindow", w_focus.getFocusWindow().getIndex()); - saveSettingsJSONData.setJSONObject(kJSONKeyFocus, saveFocusSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Widgets Active in respective Containers - JSONObject saveWidgetSettings = new JSONObject(); + + // Add all settings to the main JSON object + saveSettingsJSONData.setJSONObject(KEY_GLOBAL, globalSettings); + saveSettingsJSONData.setJSONObject(KEY_NETWORKING, + parseJSONObject(dataProcessing.networkingSettings.getJson())); + saveSettingsJSONData.setJSONObject(KEY_CONTAINERS, saveWidgetContainerPositions()); + saveSettingsJSONData.setJSONObject(KEY_WIDGET_SETTINGS, + parseJSONObject(widgetManager.getWidgetSettingsAsJson())); + saveSettingsJSONData.setJSONObject(KEY_FILTER_SETTINGS, + parseJSONObject(filterSettings.getJson())); + saveSettingsJSONData.setJSONObject(KEY_EMG_SETTINGS, + parseJSONObject(dataProcessing.emgSettings.getJson())); + + // Save to file + saveJSONObject(saveSettingsJSONData, saveFilePath); + } + /** + * Save widget container positions + */ + private JSONObject saveWidgetContainerPositions() { + JSONObject widgetLayout = new JSONObject(); int numActiveWidgets = 0; - //Save what Widgets are active and respective Container number (see Containers.pde) - for (int i = 0; i < wm.widgets.size(); i++) { //increment through all widgets - if (wm.widgets.get(i).getIsActive()) { //If a widget is active... - numActiveWidgets++; //increment numActiveWidgets - //println("Widget" + i + " is active"); - // activeWidgets.add(i); //keep track of the active widget - int containerCountsave = wm.widgets.get(i).currentContainer; - //println("Widget " + i + " is in Container " + containerCountsave); - saveWidgetSettings.setInt("Widget_"+i, containerCountsave); - } else if (!wm.widgets.get(i).getIsActive()) { //If a widget is not active... - saveWidgetSettings.remove("Widget_"+i); //remove non-active widget from JSON - //println("widget"+i+" is not active"); + + // Save active widgets and their container positions + for (int i = 0; i < widgetManager.widgets.size(); i++) { + Widget widget = widgetManager.widgets.get(i); + if (widget.getIsActive()) { + numActiveWidgets++; + widgetLayout.setInt("Widget_" + i, widget.currentContainer); } } + println("SessionSettings: " + numActiveWidgets + " active widgets saved!"); - //Print what widgets are in the containers used by current layout for only the number of active widgets - //for (int i = 0; i < numActiveWidgets; i++) { - //int containerCounter = wm.layouts.get(currentLayout).containerInts[i]; - //println("Container " + containerCounter + " is available"); //For debugging - //} - saveSettingsJSONData.setJSONObject(kJSONKeyWidget, saveWidgetSettings); - - ///////////////////////////////////////////////////////////////////////////////// - ///ADD more global settings above this line in the same formats as above///////// - - //Let's save the JSON array to a file! - saveJSONObject(saveSettingsJSONData, saveGUISettingsFileLocation); - - } //End of Save GUI Settings function - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Load GUI Settings // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - void load(String loadGUISettingsFileLocation) throws Exception { - //Load all saved User Settings from a JSON file if it exists - loadSettingsJSONData = loadJSONObject(loadGUISettingsFileLocation); - - verbosePrint(loadSettingsJSONData.toString()); + return widgetLayout; + } - //Check the number of channels saved to json first! - JSONObject loadDataSettings = loadSettingsJSONData.getJSONObject(kJSONKeyDataInfo); - numChanloaded = loadDataSettings.getInt("Channels"); - //Print error if trying to load a different number of channels - if (numChanloaded != sessionSettingsChannelCount) { - println("Channels being loaded from " + loadGUISettingsFileLocation + " don't match channels being used!"); + /** + * Load settings from a file + */ + void load(String loadFilePath) throws Exception { + // Load and parse JSON data + loadSettingsJSONData = loadJSONObject(loadFilePath); + JSONObject globalSettings = loadSettingsJSONData.getJSONObject(KEY_GLOBAL); + + // Validate settings match current configuration + validateSettings(globalSettings); + + // Apply settings in order + currentLayout = globalSettings.getInt(KEY_LAYOUT); + applyDataSmoothingSettings(globalSettings); + applyNetworkingSettings(); + applyWidgetLayout(); + applyWidgetSettings(); + applyFilterSettings(); + applyEmgSettings(); + } + + /** + * Validate that loaded settings are compatible with current configuration + */ + private void validateSettings(JSONObject globalSettings) throws Exception { + // Check channel count match + int loadedChannels = globalSettings.getInt(KEY_CHANNELS); + if (loadedChannels != globalChannelCount) { chanNumError = true; - throw new Exception(); - } else { - chanNumError = false; + throw new Exception("Channel count mismatch"); } - //Check the Data Source integer next: Cyton = 0, Ganglion = 1, Playback = 2, Synthetic = 3 - loadDatasource = loadDataSettings.getInt("Data Source"); - verbosePrint("loadGUISettings: Data source loaded: " + loadDatasource + ". Current data source: " + eegDataSource); - //Print error if trying to load a different data source (ex. Live != Synthetic) - if (loadDatasource != eegDataSource) { - println("Data source being loaded from " + loadGUISettingsFileLocation + " doesn't match current data source."); + chanNumError = false; + + // Check data source match + int loadedDataSource = globalSettings.getInt(KEY_DATA_SOURCE); + if (loadedDataSource != eegDataSource) { dataSourceError = true; - throw new Exception(); - } else { - dataSourceError = false; - } - - //get the global settings JSON object - JSONObject loadGlobalSettings = loadSettingsJSONData.getJSONObject(kJSONKeySettings); - //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"); - //Load more global settings after this line, if needed - Boolean loadDataSmoothingSetting = (currentBoard instanceof SmoothingCapableBoard) ? loadGlobalSettings.getBoolean("Data Smoothing") : null; - - //get the FFT settings - JSONObject loadFFTSettings = loadSettingsJSONData.getJSONObject(kJSONKeyFFT); - fftMaxFrqLoad = loadFFTSettings.getInt("FFT_Max Freq"); - fftMaxuVLoad = loadFFTSettings.getInt("FFT_Max uV"); - fftLogLinLoad = loadFFTSettings.getInt("FFT_LogLin"); - fftSmoothingLoad = loadFFTSettings.getInt("FFT_Smoothing"); - fftFilterLoad = loadFFTSettings.getInt("FFT_Filter"); - - //get the Accelerometer settings - if (w_accelerometer != null) { - JSONObject loadAccSettings = loadSettingsJSONData.getJSONObject(kJSONKeyAccel); - loadAccelVertScale = loadAccSettings.getInt("Accelerometer Vert Scale"); - loadAccelHorizScale = loadAccSettings.getInt("Accelerometer Horiz Scale"); - } - - //get the Networking Settings - loadNetworkingSettings = loadSettingsJSONData.getJSONObject(kJSONKeyNetworking); - - //get the Headplot settings - if (w_headPlot != null) { - JSONObject loadHeadplotSettings = loadSettingsJSONData.getJSONObject(kJSONKeyHeadplot); - hpIntensityLoad = loadHeadplotSettings.getInt("HP_intensity"); - hpPolarityLoad = loadHeadplotSettings.getInt("HP_polarity"); - hpContoursLoad = loadHeadplotSettings.getInt("HP_contours"); - hpSmoothingLoad = loadHeadplotSettings.getInt("HP_smoothing"); - } - - //Get Band Power widget settings - loadBPActiveChans.clear(); - JSONObject loadBPSettings = loadSettingsJSONData.getJSONObject(kJSONKeyBandPower); - JSONArray loadBPChan = loadBPSettings.getJSONArray("activeChannels"); - for (int i = 0; i < loadBPChan.size(); i++) { - loadBPActiveChans.add(loadBPChan.getInt(i)); - } - loadBPAutoClean = loadBPSettings.getInt("bpAutoClean"); - loadBPAutoCleanThreshold = loadBPSettings.getInt("bpAutoCleanThreshold"); - loadBPAutoCleanTimer = loadBPSettings.getInt("bpAutoCleanTimer"); - //println("Settings: band power active chans loaded = " + loadBPActiveChans ); - - try { - //Get Spectrogram widget settings - loadSpectActiveChanTop.clear(); - loadSpectActiveChanBot.clear(); - JSONObject loadSpectSettings = loadSettingsJSONData.getJSONObject(kJSONKeySpectrogram); - JSONArray loadSpectChanTop = loadSpectSettings.getJSONArray("activeChannelsTop"); - for (int i = 0; i < loadSpectChanTop.size(); i++) { - loadSpectActiveChanTop.add(loadSpectChanTop.getInt(i)); - } - JSONArray loadSpectChanBot = loadSpectSettings.getJSONArray("activeChannelsBot"); - for (int i = 0; i < loadSpectChanBot.size(); i++) { - loadSpectActiveChanBot.add(loadSpectChanBot.getInt(i)); - } - spectMaxFrqLoad = loadSpectSettings.getInt("Spectrogram_Max Freq"); - spectSampleRateLoad = loadSpectSettings.getInt("Spectrogram_Sample Rate"); - spectLogLinLoad = loadSpectSettings.getInt("Spectrogram_LogLin"); - //println(loadSpectActiveChanTop, loadSpectActiveChanBot); - } catch (Exception e) { - e.printStackTrace(); - } - - //Get EMG widget settings - loadEmgActiveChannels.clear(); - JSONObject loadEmgSettings = loadSettingsJSONData.getJSONObject(kJSONKeyEmg); - JSONArray loadEmgChan = loadEmgSettings.getJSONArray("activeChannels"); - for (int i = 0; i < loadEmgChan.size(); i++) { - loadEmgActiveChannels.add(loadEmgChan.getInt(i)); - } - - //Get EMG Joystick widget settings - JSONObject loadEmgJoystickSettings = loadSettingsJSONData.getJSONObject(kJSONKeyEmgJoystick); - loadEmgJoystickSmoothing = loadEmgJoystickSettings.getInt("smoothing"); - loadEmgJoystickInputs.clear(); - JSONArray loadJoystickInputsJson = loadEmgJoystickSettings.getJSONArray("joystickInputs"); - for (int i = 0; i < loadJoystickInputsJson.size(); i++) { - loadEmgJoystickInputs.add(loadJoystickInputsJson.getInt(i)); - } - - //Get Marker widget settings - JSONObject loadMarkerSettings = loadSettingsJSONData.getJSONObject(kJSONKeyMarker); - loadMarkerWindow = loadMarkerSettings.getInt("markerWindow"); - loadMarkerVertScale = loadMarkerSettings.getInt("markerVertScale"); - - //Get Focus widget settings - JSONObject loadFocusSettings = loadSettingsJSONData.getJSONObject(kJSONKeyFocus); - loadFocusMetric = loadFocusSettings.getInt("focusMetric"); - loadFocusThreshold = loadFocusSettings.getInt("focusThreshold"); - loadFocusWindow = loadFocusSettings.getInt("focusWindow"); - - //get the Widget/Container settings - JSONObject loadWidgetSettings = loadSettingsJSONData.getJSONObject(kJSONKeyWidget); - //Apply Layout directly before loading and applying widgets to containers - wm.setNewContainerLayout(currentLayout); - verbosePrint("LoadGUISettings: Layout " + currentLayout + " Loaded!"); - numLoadedWidgets = loadWidgetSettings.size(); - - - //int numActiveWidgets = 0; //reset the counter - for (int w = 0; w < wm.widgets.size(); w++) { //increment through all widgets - if (wm.widgets.get(w).getIsActive()) { //If a widget is active... - verbosePrint("Deactivating widget [" + w + "]"); - wm.widgets.get(w).setIsActive(false); - //numActiveWidgets++; //counter the number of de-activated widgets - } + throw new Exception("Data source mismatch"); } - - //Store the Widget number keys from JSON to a string array - loadedWidgetsArray = (String[]) loadWidgetSettings.keys().toArray(new String[loadWidgetSettings.size()]); - //printArray(loadedWidgetsArray); - int widgetToActivate = 0; - for (int w = 0; w < numLoadedWidgets; w++) { - String [] loadWidgetNameNumber = split(loadedWidgetsArray[w], '_'); - //Store the value of the widget to be activated - widgetToActivate = Integer.valueOf(loadWidgetNameNumber[1]); - //Load the container for the current widget[w] - int containerToApply = loadWidgetSettings.getInt(loadedWidgetsArray[w]); - - wm.widgets.get(widgetToActivate).setIsActive(true);//activate the new widget - wm.widgets.get(widgetToActivate).setContainer(containerToApply);//map it to the container that was loaded! - println("LoadGUISettings: Applied Widget " + widgetToActivate + " to Container " + containerToApply); - }//end case for all widget/container settings - - ///////////////////////////////////////////////////////////// - // Load more widget settings above this line as above // - ///////////////////////////////////////////////////////////// - - ///////////////////////////////////////////////////////////// - // Apply Settings below this line // - ///////////////////////////////////////////////////////////// - - //Apply Data Smoothing for capable boards - if (currentBoard instanceof SmoothingCapableBoard) { - ((SmoothingCapableBoard)currentBoard).setSmoothingActive(loadDataSmoothingSetting); + dataSourceError = false; + } + + /** + * Apply data smoothing settings if available + */ + private void applyDataSmoothingSettings(JSONObject globalSettings) { + if (currentBoard instanceof SmoothingCapableBoard && + globalSettings.hasKey(KEY_SMOOTHING)) { + + ((SmoothingCapableBoard)currentBoard).setSmoothingActive( + globalSettings.getBoolean(KEY_SMOOTHING)); topNav.updateSmoothingButtonText(); } - - //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(); - - //Apply Time Series Settings Last!!! - loadApplyTimeSeriesSettings(); - - if (w_headPlot != null) { - //Force headplot to redraw if it is active - int hpWidgetNumber; - if (eegDataSource == DATASOURCE_GANGLION) { - hpWidgetNumber = 6; - } else { - hpWidgetNumber = 5; - } - if (wm.widgets.get(hpWidgetNumber).getIsActive()) { - w_headPlot.headPlot.setPositionSize(w_headPlot.headPlot.hp_x, w_headPlot.headPlot.hp_y, w_headPlot.headPlot.hp_w, w_headPlot.headPlot.hp_h, w_headPlot.headPlot.hp_win_x, w_headPlot.headPlot.hp_win_y); - println("Headplot is active: Redrawing"); - } - } - } //end of loadGUISettings - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void loadApplyWidgetDropdownText() { - - ////////Apply Time Series dropdown settings in loadApplyTimeSeriesSettings() instead of here - - ////////Apply FFT settings - MaxFreq(fftMaxFrqLoad); //This changes the back-end - w_fft.cp5_widget.getController("MaxFreq").getCaptionLabel().setText(fftMaxFrqArray[fftMaxFrqLoad]); //This changes front-end... etc. - - VertScale(fftMaxuVLoad); - w_fft.cp5_widget.getController("VertScale").getCaptionLabel().setText(fftVertScaleArray[fftMaxuVLoad]); - - LogLin(fftLogLinLoad); - w_fft.cp5_widget.getController("LogLin").getCaptionLabel().setText(fftLogLinArray[fftLogLinLoad]); - - Smoothing(fftSmoothingLoad); - w_fft.cp5_widget.getController("Smoothing").getCaptionLabel().setText(fftSmoothingArray[fftSmoothingLoad]); - - UnfiltFilt(fftFilterLoad); - w_fft.cp5_widget.getController("UnfiltFilt").getCaptionLabel().setText(fftFilterArray[fftFilterLoad]); - - ////////Apply Accelerometer settings; - if (w_accelerometer != null) { - accelVertScale(loadAccelVertScale); - w_accelerometer.cp5_widget.getController("accelVertScale").getCaptionLabel().setText(accVertScaleArray[loadAccelVertScale]); - - accelDuration(loadAccelHorizScale); - w_accelerometer.cp5_widget.getController("accelDuration").getCaptionLabel().setText(accHorizScaleArray[loadAccelHorizScale]); - } - - ////////Apply Anolog Read dropdowns to Live Cyton Only - if (eegDataSource == DATASOURCE_CYTON) { - ////////Apply Analog Read settings - VertScale_AR(loadAnalogReadVertScale); - w_analogRead.cp5_widget.getController("VertScale_AR").getCaptionLabel().setText(arVertScaleArray[loadAnalogReadVertScale]); - - Duration_AR(loadAnalogReadHorizScale); - w_analogRead.cp5_widget.getController("Duration_AR").getCaptionLabel().setText(arHorizScaleArray[loadAnalogReadHorizScale]); - } - - ////////////////////////////Apply Headplot settings - if (w_headPlot != null) { - Intensity(hpIntensityLoad); - w_headPlot.cp5_widget.getController("Intensity").getCaptionLabel().setText(hpIntensityArray[hpIntensityLoad]); - - Polarity(hpPolarityLoad); - w_headPlot.cp5_widget.getController("Polarity").getCaptionLabel().setText(hpPolarityArray[hpPolarityLoad]); - - ShowContours(hpContoursLoad); - w_headPlot.cp5_widget.getController("ShowContours").getCaptionLabel().setText(hpContoursArray[hpContoursLoad]); - - SmoothingHeadPlot(hpSmoothingLoad); - w_headPlot.cp5_widget.getController("SmoothingHeadPlot").getCaptionLabel().setText(hpSmoothingArray[hpSmoothingLoad]); - - //Force redraw headplot on load. Fixes issue where heaplot draws outside of the widget. - w_headPlot.headPlot.setPositionSize(w_headPlot.headPlot.hp_x, w_headPlot.headPlot.hp_y, w_headPlot.headPlot.hp_w, w_headPlot.headPlot.hp_h, w_headPlot.headPlot.hp_win_x, w_headPlot.headPlot.hp_win_y); - } - - ////////////////////////////Apply Band Power settings - try { - //apply channel checkbox settings - w_bandPower.bpChanSelect.deactivateAllButtons();; - for (int i = 0; i < loadBPActiveChans.size(); i++) { - w_bandPower.bpChanSelect.setToggleState(loadBPActiveChans.get(i), true); - } - } catch (Exception e) { - println("Settings: Exception caught applying band power settings " + e); - } - verbosePrint("Settings: Band Power Active Channels: " + loadBPActiveChans); - w_bandPower.setAutoClean(loadBPAutoClean); - w_bandPower.cp5_widget.getController("bpAutoCleanDropdown").getCaptionLabel().setText(w_bandPower.getAutoClean().getString()); - w_bandPower.setAutoCleanThreshold(loadBPAutoCleanThreshold); - w_bandPower.cp5_widget.getController("bpAutoCleanThresholdDropdown").getCaptionLabel().setText(w_bandPower.getAutoCleanThreshold().getString()); - w_bandPower.setAutoCleanTimer(loadBPAutoCleanTimer); - w_bandPower.cp5_widget.getController("bpAutoCleanTimerDropdown").getCaptionLabel().setText(w_bandPower.getAutoCleanTimer().getString()); - - ////////////////////////////Apply Spectrogram settings - //Apply Max Freq dropdown - SpectrogramMaxFreq(spectMaxFrqLoad); - w_spectrogram.cp5_widget.getController("SpectrogramMaxFreq").getCaptionLabel().setText(spectMaxFrqArray[spectMaxFrqLoad]); - SpectrogramSampleRate(spectSampleRateLoad); - w_spectrogram.cp5_widget.getController("SpectrogramSampleRate").getCaptionLabel().setText(spectSampleRateArray[spectSampleRateLoad]); - SpectrogramLogLin(spectLogLinLoad); - w_spectrogram.cp5_widget.getController("SpectrogramLogLin").getCaptionLabel().setText(fftLogLinArray[spectLogLinLoad]); - try { - //apply channel checkbox settings - w_spectrogram.spectChanSelectTop.deactivateAllButtons(); - w_spectrogram.spectChanSelectBot.deactivateAllButtons(); - //close channel select when loading to prevent UI issues - w_spectrogram.spectChanSelectTop.setIsVisible(false); - w_spectrogram.spectChanSelectBot.setIsVisible(false); - for (int i = 0; i < loadSpectActiveChanTop.size(); i++) { - w_spectrogram.spectChanSelectTop.setToggleState(loadSpectActiveChanTop.get(i), true); - } - for (int i = 0; i < loadSpectActiveChanBot.size(); i++) { - w_spectrogram.spectChanSelectBot.setToggleState(loadSpectActiveChanBot.get(i), true); - } - w_spectrogram.screenResized(); - } catch (Exception e) { - println("Settings: Exception caught applying spectrogram settings channel bar " + e); - } - println("Settings: Spectrogram Active Channels: TOP - " + loadSpectActiveChanTop + " || BOT - " + loadSpectActiveChanBot); - - ///////////Apply Networking Settings - String nwSettingsString = loadNetworkingSettings.toString(); - dataProcessing.networkingSettings.loadJson(nwSettingsString); + } + + /** + * Apply networking settings + */ + private void applyNetworkingSettings() { + dataProcessing.networkingSettings.loadJson( + loadSettingsJSONData.getJSONObject(KEY_NETWORKING).toString()); + } + + /** + * Apply widget layout and container positions + */ + private void applyWidgetLayout() { + // Set layout first + widgetManager.setNewContainerLayout(currentLayout); - ////////////////////////////Apply EMG widget settings - try { - //apply channel checkbox settings - w_emg.emgChannelSelect.deactivateAllButtons();; - for (int i = 0; i < loadEmgActiveChannels.size(); i++) { - w_emg.emgChannelSelect.setToggleState(loadEmgActiveChannels.get(i), true); - } - } catch (Exception e) { - println("Settings: Exception caught applying EMG widget settings " + e); + // Deactivate all widgets initially + for (Widget widget : widgetManager.widgets) { + widget.setIsActive(false); } - verbosePrint("Settings: EMG Widget Active Channels: " + loadEmgActiveChannels); - - ////////////////////////////Apply EMG Joystick settings - w_emgJoystick.setJoystickSmoothing(loadEmgJoystickSmoothing); - w_emgJoystick.cp5_widget.getController("emgJoystickSmoothingDropdown").getCaptionLabel() - .setText(EmgJoystickSmoothing.getEnumStringsAsList().get(loadEmgJoystickSmoothing)); - try { - for (int i = 0; i < loadEmgJoystickInputs.size(); i++) { - w_emgJoystick.updateJoystickInput(i, loadEmgJoystickInputs.get(i)); - } - } catch (Exception e) { - println("Settings: Exception caught applying EMG Joystick settings " + e); + + // Get widget container settings + JSONObject containerSettings = loadSettingsJSONData.getJSONObject(KEY_CONTAINERS); + + // Activate widgets and set containers + // Fix: Properly handle keys as a Set from containerSettings.keys() + for (Object keyObj : containerSettings.keys()) { + String key = keyObj.toString(); + String[] keyParts = split(key, '_'); + int widgetIndex = Integer.valueOf(keyParts[1]); + int containerIndex = containerSettings.getInt(key); + + Widget widget = widgetManager.widgets.get(widgetIndex); + widget.setIsActive(true); + widget.setContainer(containerIndex); } + } + + /** + * Apply individual widget settings + */ + private void applyWidgetSettings() { + widgetManager.loadWidgetSettingsFromJson( + loadSettingsJSONData.getJSONObject(KEY_WIDGET_SETTINGS).toString()); + } - ////////////////////////////Apply Marker Widget settings - w_marker.setMarkerWindow(loadMarkerWindow); - w_marker.cp5_widget.getController("markerWindowDropdown").getCaptionLabel().setText(w_marker.getMarkerWindow().getString()); - w_marker.setMarkerVertScale(loadMarkerVertScale); - w_marker.cp5_widget.getController("markerVertScaleDropdown").getCaptionLabel().setText(w_marker.getMarkerVertScale().getString()); - - ////////////////////////////Apply Focus Widget settings - w_focus.setMetric(loadFocusMetric); - w_focus.cp5_widget.getController("focusMetricDropdown").getCaptionLabel().setText(w_focus.getFocusMetric().getString()); - w_focus.setThreshold(loadFocusThreshold); - w_focus.cp5_widget.getController("focusThresholdDropdown").getCaptionLabel().setText(w_focus.getFocusThreshold().getString()); - w_focus.setFocusHorizScale(loadFocusWindow); - w_focus.cp5_widget.getController("focusWindowDropdown").getCaptionLabel().setText(w_focus.getFocusWindow().getString()); - - //////////////////////////////////////////////////////////// - // Apply more loaded widget settings above this line // - - } //end of loadApplyWidgetDropdownText() + private void applyFilterSettings() { + JSONObject filterSettingsJSON = loadSettingsJSONData.getJSONObject(KEY_FILTER_SETTINGS); + String filterSettingsString = filterSettingsJSON.toString(); + filterSettings.loadSettingsFromJson(filterSettingsString); + } - private void loadApplyTimeSeriesSettings() { + private void applyEmgSettings() { + JSONObject emgSettingsJSON = loadSettingsJSONData.getJSONObject(KEY_EMG_SETTINGS); + String emgSettingsString = emgSettingsJSON.toString(); + dataProcessing.emgSettings.loadSettingsFromJson(emgSettingsString); + } - JSONObject loadTimeSeriesSettings = loadSettingsJSONData.getJSONObject(kJSONKeyTimeSeries); - ////////Apply Time Series widget settings - w_timeSeries.setTSVertScale(loadTimeSeriesSettings.getInt("Time Series Vert Scale")); - w_timeSeries.cp5_widget.getController("VertScale_TS").getCaptionLabel().setText(w_timeSeries.getTSVertScale().getString()); //changes front-end + /** + * Get the appropriate settings file path based on mode and configuration + */ + String getPath(String mode, int dataSource, int channelCount) { + // Determine which settings file to use + int modeIndex = mode.equals("Default") ? FILE_DEFAULT : FILE_USER; + int fileIndex; - w_timeSeries.setTSHorizScale(loadTimeSeriesSettings.getInt("Time Series Horiz Scale")); - w_timeSeries.cp5_widget.getController("Duration").getCaptionLabel().setText(w_timeSeries.getTSHorizScale().getString()); - - w_timeSeries.setTSLabelMode(loadTimeSeriesSettings.getInt("Time Series Label Mode")); - w_timeSeries.cp5_widget.getController("LabelMode_TS").getCaptionLabel().setText(w_timeSeries.getTSLabelMode().getString()); - - JSONArray loadTSChan = loadTimeSeriesSettings.getJSONArray("activeChannels"); - w_timeSeries.tsChanSelect.deactivateAllButtons(); - try { - for (int i = 0; i < loadTSChan.size(); i++) { - w_timeSeries.tsChanSelect.setToggleState(loadTSChan.getInt(i), true); + if (dataSource == DATASOURCE_CYTON) { + fileIndex = (channelCount == CYTON_CHANNEL_COUNT) ? 0 : 1; + } else if (dataSource == DATASOURCE_GANGLION) { + fileIndex = 2; + } else if (dataSource == DATASOURCE_PLAYBACKFILE) { + fileIndex = 3; + } else if (dataSource == DATASOURCE_SYNTHETIC) { + if (channelCount == GANGLION_CHANNEL_COUNT) { + fileIndex = 4; + } else if (channelCount == CYTON_CHANNEL_COUNT) { + fileIndex = 5; + } else { + fileIndex = 6; } - } catch (Exception e) { - println("Settings: Exception caught applying time series settings " + e); + } else { + return "Error"; } - verbosePrint("Settings: Time Series Active Channels: " + loadBPActiveChans); - - } //end loadApplyTimeSeriesSettings + + return directoryManager.getSettingsPath() + SETTING_FILES[fileIndex][modeIndex]; + } /** - * @description Used in TopNav when user clicks ClearSettings->AreYouSure->Yes - * @params none - * Output Success message to bottom of GUI when done - */ + * Clear all settings files + */ void clearAll() { - for (File file: new File(directoryManager.getSettingsPath()).listFiles()) - if (!file.isDirectory()) + // Delete all settings files + for (File file : new File(directoryManager.getSettingsPath()).listFiles()) { + if (!file.isDirectory()) { file.delete(); + } + } + + // Clear playback history controlPanel.recentPlaybackBox.rpb_cp5.get(ScrollableList.class, "recentPlaybackFilesCP").clear(); controlPanel.recentPlaybackBox.shortFileNames.clear(); controlPanel.recentPlaybackBox.longFilePaths.clear(); + outputSuccess("All settings have been cleared!"); } /** - * @description Used in System Init, TopNav, and Interactivity - * @params mode="User"or"Default", dataSource, and number of channels - * @returns {String} - filePath or Error if mode not specified correctly - */ - String getPath(String _mode, int dataSource, int _channelCount) { - String filePath = directoryManager.getSettingsPath(); - String[] fileNames = new String[7]; - if (_mode.equals("Default")) { - fileNames = defaultSettingsFiles; - } else if (_mode.equals("User")) { - fileNames = userSettingsFiles; - } else { - filePath = "Error"; - } - if (!filePath.equals("Error")) { - if (dataSource == DATASOURCE_CYTON) { - filePath += (_channelCount == CYTON_CHANNEL_COUNT) ? - fileNames[0] : - fileNames[1]; - } else if (dataSource == DATASOURCE_GANGLION) { - filePath += fileNames[2]; - } else if (dataSource == DATASOURCE_PLAYBACKFILE) { - filePath += fileNames[3]; - } else if (dataSource == DATASOURCE_SYNTHETIC) { - if (_channelCount == GANGLION_CHANNEL_COUNT) { - filePath += fileNames[4]; - } else if (_channelCount == CYTON_CHANNEL_COUNT) { - filePath += fileNames[5]; - } else { - filePath += fileNames[6]; - } - } - } - return filePath; - } - + * Handle key press to load settings + */ void loadKeyPressed() { loadErrorTimerStart = millis(); - String settingsFileToLoad = getPath("User", eegDataSource, globalChannelCount); + String settingsFile = getPath("User", eegDataSource, globalChannelCount); + try { - load(settingsFileToLoad); + load(settingsFile); errorUserSettingsNotFound = false; + outputSuccess("Settings Loaded!"); } catch (Exception e) { - //println(e.getMessage()); - e.printStackTrace(); - println(settingsFileToLoad + " not found or other error. Save settings with keyboard 'n' or using dropdown menu."); errorUserSettingsNotFound = true; + handleLoadError(settingsFile); } - //Output message when Loading settings is complete - String err = null; - if (chanNumError == false && dataSourceError == false && errorUserSettingsNotFound == false && loadErrorCytonEvent == false) { - outputSuccess("Settings Loaded!"); - } else if (chanNumError) { - err = "Invalid number of channels"; + } + + /** + * Handle errors when loading settings + */ + private void handleLoadError(String settingsFile) { + if (chanNumError) { + outputError("Settings Error: Channel Number Mismatch"); } else if (dataSourceError) { - err = "Invalid data source"; - } else if (errorUserSettingsNotFound) { - err = settingsFileToLoad + " not found."; - } - - //Only try to delete file for SettingsNotFound/Broken settings - if (err != null && (!chanNumError && !dataSourceError)) { - println("Load Settings Error: " + err); - File f = new File(settingsFileToLoad); - if (f.exists()) { - if (f.delete()) { - outputError("Found old/broken GUI settings. Please reconfigure the GUI and save new settings."); - } else { - outputError("SessionSettings: Error deleting old/broken settings file..."); - } + outputError("Settings Error: Data Source Mismatch"); + } else { + File f = new File(settingsFile); + if (f.exists() && f.delete()) { + outputError("Found old/broken GUI settings. Please reconfigure the GUI and save new settings."); + } else if (f.exists()) { + outputError("Error deleting old/broken settings file."); } } } + /** + * Handle save button press + */ void saveButtonPressed() { if (saveDialogName == null) { - File fileToSave = dataFile(settings.getPath("User", eegDataSource, globalChannelCount)); - FileChooser chooser = new FileChooser( - FileChooserMode.SAVE, - "saveConfigFile", - fileToSave, - "Save settings to file"); + // Open file chooser dialog + File fileToSave = dataFile(getPath("User", eegDataSource, globalChannelCount)); + new FileChooser(FileChooserMode.SAVE, "saveConfigFile", fileToSave, + "Save settings to file"); } else { - println("saveSettingsFileName = " + saveDialogName); saveDialogName = null; } } + /** + * Handle load button press + */ void loadButtonPressed() { - //Select file to load from dialog box if (loadDialogName == null) { - FileChooser chooser = new FileChooser( - FileChooserMode.LOAD, - "loadConfigFile", - new File(directoryManager.getGuiDataPath() + "Settings"), - "Select a settings file to load"); + // Open file chooser dialog + new FileChooser(FileChooserMode.LOAD, "loadConfigFile", + new File(directoryManager.getGuiDataPath() + "Settings"), + "Select a settings file to load"); saveDialogName = null; } else { - println("loadSettingsFileName = " + loadDialogName); loadDialogName = null; } } + /** + * Reset to default settings + */ void defaultButtonPressed() { - //Revert GUI to default settings that were flashed on system start! - String defaultSettingsFileToLoad = getPath("Default", eegDataSource, globalChannelCount); + String defaultFile = getPath("Default", eegDataSource, globalChannelCount); try { - //Load all saved User Settings from a JSON file to see if it exists - JSONObject loadDefaultSettingsJSONData = loadJSONObject(defaultSettingsFileToLoad); - this.load(defaultSettingsFileToLoad); + // Check if default settings exist and load them + loadJSONObject(defaultFile); + load(defaultFile); outputSuccess("Default Settings Loaded!"); } catch (Exception e) { outputError("Default Settings Error: Valid Default Settings will be saved next system start."); - File f = new File(defaultSettingsFileToLoad); - if (f.exists()) { - if (f.delete()) { - println("SessionSettings: Old/Broken Default Settings file succesfully deleted."); - } else { - println("SessionSettings: Error deleting Default Settings file..."); - } + File f = new File(defaultFile); + if (f.exists() && !f.delete()) { + println("SessionSettings: Error deleting Default Settings file..."); } } } + /** + * Auto-load settings at startup + */ public void autoLoadSessionSettings() { loadKeyPressed(); } +} -} //end of Software Settings class - -////////////////////////////////////////// -// Global Functions // -// Called by Buttons with the same name // -////////////////////////////////////////// -// Select file to save custom settings using dropdown in TopNav.pde +/** + * Process file selection for saving settings + */ void saveConfigFile(File selection) { if (selection == null) { - println("SessionSettings: saveConfigFile: Window was closed or the user hit cancel."); - } else { - 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! 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 + return; } + + sessionSettings.save(selection.getAbsolutePath()); + outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key."); + sessionSettings.saveDialogName = null; } -// Select file to load custom settings using dropdown in TopNav.pde + +/** + * Process file selection for loading settings + */ void loadConfigFile(File selection) { if (selection == null) { - println("SessionSettings: loadConfigFile: Window was closed or the user hit cancel."); + return; + } + + try { + sessionSettings.load(selection.getAbsolutePath()); + if (!sessionSettings.chanNumError && !sessionSettings.dataSourceError && + !sessionSettings.loadErrorCytonEvent) { + outputSuccess("Settings Loaded!"); + } + } catch (Exception e) { + handleLoadConfigError(selection); + } + + sessionSettings.loadDialogName = null; +} + +/** + * Handle errors in loadConfigFile + */ +void handleLoadConfigError(File selection) { + if (sessionSettings.chanNumError) { + outputError("Settings Error: Channel Number Mismatch Detected"); + } else if (sessionSettings.dataSourceError) { + outputError("Settings Error: Data Source Mismatch Detected"); } else { - println("SessionSettings: loadConfigFile: User selected " + selection.getAbsolutePath()); - //output("You have selected \"" + selection.getAbsolutePath() + "\" to Load custom settings."); - settings.loadDialogName = selection.getAbsolutePath(); - try { - settings.load(settings.loadDialogName); //load settings from JSON file in /data/ - //Output success message when Loading settings is complete without errors - if (settings.chanNumError == false - && settings.dataSourceError == false - && settings.loadErrorCytonEvent == false) { - outputSuccess("Settings Loaded!"); - } - } catch (Exception e) { - println("SessionSettings: Incompatible settings file or other error"); - if (settings.chanNumError == true) { - outputError("Settings Error: Channel Number Mismatch Detected"); - } else if (settings.dataSourceError == true) { - outputError("Settings Error: Data Source Mismatch Detected"); - } else { - outputError("Error trying to load settings file, possibly from previous GUI. Removing old settings."); - if (selection.exists()) selection.delete(); - } + outputError("Error trying to load settings file, possibly from previous GUI."); + if (selection.exists()) { + selection.delete(); } - settings.loadDialogName = null; //reset this variable for future use } } \ No newline at end of file diff --git a/OpenBCI_GUI/SignalCheckThresholds.pde b/OpenBCI_GUI/SignalCheckThresholds.pde index 549f3023c..f24a9bea0 100644 --- a/OpenBCI_GUI/SignalCheckThresholds.pde +++ b/OpenBCI_GUI/SignalCheckThresholds.pde @@ -127,10 +127,11 @@ class SignalCheckThresholdUI { valuePercentage = val; } else { if (currentBoard instanceof BoardCyton) { + W_CytonImpedance cytonImpedanceWidget = (W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance"); if (name == "errorThreshold") { - w_cytonImpedance.updateElectrodeStatusYellowThreshold((double)val); + cytonImpedanceWidget.updateElectrodeStatusYellowThreshold((double)val); } else { - w_cytonImpedance.updateElectrodeStatusGreenThreshold((double)val); + cytonImpedanceWidget.updateElectrodeStatusGreenThreshold((double)val); } } valuekOhms = val; diff --git a/OpenBCI_GUI/SpectrogramEnums.pde b/OpenBCI_GUI/SpectrogramEnums.pde new file mode 100644 index 000000000..7b1a6d86e --- /dev/null +++ b/OpenBCI_GUI/SpectrogramEnums.pde @@ -0,0 +1,82 @@ +public enum SpectrogramMaxFrequency implements IndexingInterface { + MAX_20 (0, 20, "20 Hz", new int[]{20, 15, 10, 5, 0, 5, 10, 15, 20}), + MAX_40 (1, 40, "40 Hz", new int[]{40, 30, 20, 10, 0, 10, 20, 30, 40}), + MAX_60 (2, 60, "60 Hz", new int[]{60, 45, 30, 15, 0, 15, 30, 45, 60}), + MAX_100 (3, 100, "100 Hz", new int[]{100, 75, 50, 25, 0, 25, 50, 75, 100}), + MAX_120 (4, 120, "120 Hz", new int[]{120, 90, 60, 30, 0, 30, 60, 90, 120}), + MAX_250 (5, 250, "250 Hz", new int[]{250, 188, 125, 63, 0, 63, 125, 188, 250}); + + private int index; + private final int value; + private String label; + private final int[] axisLabels; + + SpectrogramMaxFrequency(int index, int value, String label, int[] axisLabels) { + this.index = index; + this.value = value; + this.label = label; + this.axisLabels = axisLabels; + } + + public int getValue() { + return value; + } + + public int[] getAxisLabels() { + return axisLabels; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum SpectrogramWindowSize implements IndexingInterface { + ONE_MINUTE (0, 1f, "1 Min.", new float[]{1, .5, 0}, 25), + ONE_MINUTE_THIRTY (1, 1.5f, "1.5 Min.", new float[]{1.5, 1, .5, 0}, 50), + THREE_MINUTES (2, 3f, "3 Min.", new float[]{3, 2, 1, 0}, 100), + SIX_MINUTES (3, 6f, "6 Min.", new float[]{6, 5, 4, 3, 2, 1, 0}, 200), + THIRTY_MINUTES (4, 30f, "30 Min.", new float[]{30, 25, 20, 15, 10, 5, 0}, 1000); + + private int index; + private final float value; + private String label; + private final float[] axisLabels; + private final int scrollSpeed; + + SpectrogramWindowSize(int index, float value, String label, float[] axisLabels, int scrollSpeed) { + this.index = index; + this.value = value; + this.label = label; + this.axisLabels = axisLabels; + this.scrollSpeed = scrollSpeed; + } + + public float getValue() { + return value; + } + + public float[] getAxisLabels() { + return axisLabels; + } + + public int getScrollSpeed() { + return scrollSpeed; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} diff --git a/OpenBCI_GUI/TimeSeriesEnums.pde b/OpenBCI_GUI/TimeSeriesEnums.pde new file mode 100644 index 000000000..a45a63323 --- /dev/null +++ b/OpenBCI_GUI/TimeSeriesEnums.pde @@ -0,0 +1,100 @@ +public enum TimeSeriesXLim implements IndexingInterface +{ + ONE (0, 1, "1 sec"), + THREE (1, 3, "3 sec"), + FIVE (2, 5, "5 sec"), + TEN (3, 10, "10 sec"), + TWENTY (4, 20, "20 sec"); + + private int index; + private int value; + private String label; + + TimeSeriesXLim(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 enum TimeSeriesYLim implements IndexingInterface +{ + AUTO (0, 0, "Auto"), + UV_10(1, 10, "10 uV"), + UV_25(2, 25, "25 uV"), + UV_50 (3, 50, "50 uV"), + UV_100 (4, 100, "100 uV"), + UV_200 (5, 200, "200 uV"), + UV_400 (6, 400, "400 uV"), + UV_1000 (7, 1000, "1000 uV"), + UV_10000 (8, 10000, "10000 uV"); + + private int index; + private int value; + private String label; + + TimeSeriesYLim(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 enum TimeSeriesLabelMode implements IndexingInterface +{ + OFF (0, 0, "Off"), + MINIMAL (1, 1, "Minimal"), + ON (2, 2, "On"); + + private int index; + private int value; + private String label; + + TimeSeriesLabelMode(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; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/TimeSeriesWidgetHelperClasses.pde b/OpenBCI_GUI/TimeSeriesWidgetHelperClasses.pde new file mode 100644 index 000000000..994cba5c4 --- /dev/null +++ b/OpenBCI_GUI/TimeSeriesWidgetHelperClasses.pde @@ -0,0 +1,718 @@ + +//======================================================================================================================== +// CHANNEL BAR CLASS -- Implemented by Time Series Widget Class +//======================================================================================================================== +//this class contains the plot and buttons for a single channel of the Time Series widget +//one of these will be created for each channel (4, 8, or 16) +class ChannelBar { + + int channelIndex; //duh + String channelString; + int x, y, w, h; + int defaultH; + ControlP5 cbCp5; + Button onOffButton; + int onOff_diameter; + int yScaleButton_h; + int yScaleButton_w; + Button yScaleButton_pos; + Button yScaleButton_neg; + int yAxisLabel_h; + private TextBox yAxisMax; + private TextBox yAxisMin; + + int yAxisUpperLim; + int yAxisLowerLim; + int uiSpaceWidth; + int padding_4 = 4; + int minimumChannelHeight; + int plotBottomWellH = 35; + + GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Se ries trace + GPointsArray channelPoints; + int nPoints; + int numSeconds; + float timeBetweenPoints; + private GPlotAutoscaler gplotAutoscaler; + + color channelColor; //color of plot trace + + TextBox voltageValue; + TextBox impValue; + + boolean drawVoltageValue; + + ChannelBar(PApplet _parentApplet, int _channelIndex, int _x, int _y, int _w, int _h, PImage expand_default, PImage expand_hover, PImage expand_active, PImage contract_default, PImage contract_hover, PImage contract_active) { + + cbCp5 = new ControlP5(ourApplet); + cbCp5.setGraphics(ourApplet, x, y); + cbCp5.setAutoDraw(false); //Setting this saves code as cp5 elements will only be drawn/visible when [cp5].draw() is called + + channelIndex = _channelIndex; + channelString = str(channelIndex + 1); + + x = _x; + y = _y; + w = _w; + h = _h; + defaultH = h; + + onOff_diameter = h > 26 ? 26 : h - 2; + createOnOffButton("onOffButton"+channelIndex, channelString, x + 6, y + int(h/2) - int(onOff_diameter/2), onOff_diameter, onOff_diameter); + + //Create GPlot for this Channel + uiSpaceWidth = 36 + padding_4; + yAxisUpperLim = 200; + yAxisLowerLim = -200; + numSeconds = 5; + plot = new GPlot(_parentApplet); + plot.setPos(x + uiSpaceWidth, y); + plot.setDim(w - uiSpaceWidth, h); + plot.setMar(0f, 0f, 0f, 0f); + plot.setLineColor((int)channelColors[channelIndex%8]); + plot.setXLim(-5,0); + plot.setYLim(yAxisLowerLim, yAxisUpperLim); + plot.setPointSize(2); + plot.setPointColor(0); + plot.setAllFontProperties("Arial", 0, 14); + plot.getXAxis().setFontColor(OPENBCI_DARKBLUE); + plot.getXAxis().setLineColor(OPENBCI_DARKBLUE); + plot.getXAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); + if (channelIndex == globalChannelCount-1) { + plot.getXAxis().setAxisLabelText("Time (s)"); + plot.getXAxis().getAxisLabel().setOffset(plotBottomWellH/2 + 5f); + } + gplotAutoscaler = new GPlotAutoscaler(); + + //Fill the GPlot with initial data + nPoints = nPointsBasedOnDataSource(); + channelPoints = new GPointsArray(nPoints); + timeBetweenPoints = (float)numSeconds / (float)nPoints; + for (int i = 0; i < nPoints; i++) { + float time = -(float)numSeconds + (float)i*timeBetweenPoints; + float filt_uV_value = 0.0; //0.0 for all points to start + GPoint tempPoint = new GPoint(time, filt_uV_value); + channelPoints.set(i, tempPoint); + } + plot.setPoints(channelPoints); //set the plot with 0.0 for all channelPoints to start + + //Create a UI to custom scale the Y axis for this channel + yScaleButton_w = 18; + yScaleButton_h = 18; + yAxisLabel_h = 12; + int padding = 2; + yAxisMax = new TextBox("+"+yAxisUpperLim+"uV", x + uiSpaceWidth + padding, y + int(padding*1.5), OPENBCI_DARKBLUE, color(255,255,255,175), LEFT, TOP); + yAxisMin = new TextBox(yAxisLowerLim+"uV", x + uiSpaceWidth + padding, y + h - yAxisLabel_h - padding_4, OPENBCI_DARKBLUE, color(255,255,255,175), LEFT, TOP); + customYLim(yAxisMax, yAxisUpperLim); + customYLim(yAxisMin, yAxisLowerLim); + yScaleButton_neg = createYScaleButton(channelIndex, false, "decreaseYscale", "-T", x + uiSpaceWidth + padding, y + w/2 - yScaleButton_h/2, yScaleButton_w, yScaleButton_h, contract_default, contract_hover, contract_active); + yScaleButton_pos = createYScaleButton(channelIndex, true, "increaseYscale", "+T", x + uiSpaceWidth + padding*2 + yScaleButton_w, y + w/2 - yScaleButton_h/2, yScaleButton_w, yScaleButton_h, expand_default, expand_hover, expand_active); + + //Create textBoxes to display the current values + impValue = new TextBox("", x + uiSpaceWidth + (int)plot.getDim()[0], y + padding, OPENBCI_DARKBLUE, color(255,255,255,175), RIGHT, TOP); + voltageValue = new TextBox("", x + uiSpaceWidth + (int)plot.getDim()[0] - padding, y + h, OPENBCI_DARKBLUE, color(255,255,255,175), RIGHT, BOTTOM); + drawVoltageValue = true; + + //Establish a minimumChannelHeight + minimumChannelHeight = padding_4 + yAxisLabel_h*2; + } + + void update(boolean hardwareSettingsAreOpen, TimeSeriesLabelMode _labelMode) { + + //Reusable variables + String fmt; float val; + + //Update the voltage value TextBox + val = dataProcessing.data_std_uV[channelIndex]; + voltageValue.string = String.format(getFmt(val),val) + " uVrms"; + if (is_railed != null) { + voltageValue.setText(is_railed[channelIndex].notificationString + voltageValue.string); + voltageValue.setTextColor(OPENBCI_DARKBLUE); + color bgColor = color(255,255,255,175); // Default white background for voltage TextBox + if (is_railed[channelIndex].is_railed) { + bgColor = SIGNAL_CHECK_RED_LOWALPHA; + } else if (is_railed[channelIndex].is_railed_warn) { + bgColor = SIGNAL_CHECK_YELLOW_LOWALPHA; + } + voltageValue.setBackgroundColor(bgColor); + } + + //update the impedance values + val = data_elec_imp_ohm[channelIndex]/1000; + fmt = String.format(getFmt(val),val) + " kOhm"; + if (is_railed != null && is_railed[channelIndex].is_railed == true) { + fmt = "RAILED - " + fmt; + } + impValue.setText(fmt); + + // update data in plot + updatePlotPoints(); + + if (currentBoard.isEXGChannelActive(channelIndex)) { + onOffButton.setColorBackground(channelColors[channelIndex%8]); // power down == false, set color to vibrant + } + else { + onOffButton.setColorBackground(50); // power down == true, set to grey + } + + //Hide yAxisButtons when hardware settings are open, using autoscale, and labels are turn on + boolean b = !hardwareSettingsAreOpen + && h > minimumChannelHeight + && !gplotAutoscaler.getEnabled() + && _labelMode == TimeSeriesLabelMode.ON; + yScaleButton_pos.setVisible(b); + yScaleButton_neg.setVisible(b); + yScaleButton_pos.setUpdate(b); + yScaleButton_neg.setUpdate(b); + b = !hardwareSettingsAreOpen + && h > minimumChannelHeight + && _labelMode == TimeSeriesLabelMode.ON; + yAxisMin.setVisible(b); + yAxisMax.setVisible(b); + voltageValue.setVisible(_labelMode != TimeSeriesLabelMode.OFF); + } + + private String getFmt(float val) { + String fmt; + if (val > 100.0f) { + fmt = "%.0f"; + } else if (val > 10.0f) { + fmt = "%.1f"; + } else { + fmt = "%.2f"; + } + return fmt; + } + + private void updatePlotPoints() { + float[][] buffer = downsampledFilteredBuffer.getBuffer(); + final int bufferSize = buffer[channelIndex].length; + final int startIndex = bufferSize - nPoints; + for (int i = startIndex; i < bufferSize; i++) { + int adjustedIndex = i - startIndex; + float time = -(float)numSeconds + (float)(adjustedIndex)*timeBetweenPoints; + float filt_uV_value = buffer[channelIndex][i]; + channelPoints.set(adjustedIndex, time, filt_uV_value, ""); + } + plot.setPoints(channelPoints); + + gplotAutoscaler.update(plot, channelPoints); + + if (gplotAutoscaler.getEnabled()) { + float[] minMax = gplotAutoscaler.getMinMax(); + customYLim(yAxisMin, (int)minMax[0]); + customYLim(yAxisMax, (int)minMax[1]); + } + } + + public void draw(boolean hardwareSettingsAreOpen) { + + plot.beginDraw(); + plot.drawBox(); + plot.drawGridLines(GPlot.VERTICAL); + try { + plot.drawLines(); + } catch (NullPointerException e) { + e.printStackTrace(); + println("PLOT ERROR ON CHANNEL " + channelIndex); + + } + //Draw the x axis label on the bottom channel bar, hide if hardware settings are open + if (isBottomChannel() && !hardwareSettingsAreOpen) { + plot.drawXAxis(); + plot.getXAxis().draw(); + } + plot.endDraw(); + + //draw channel holder background + pushStyle(); + stroke(OPENBCI_BLUE_ALPHA50); + noFill(); + rect(x,y,w,h); + popStyle(); + + //draw channelBar separator line in the middle of INTER_CHANNEL_BAR_SPACE + if (!isBottomChannel()) { + pushStyle(); + stroke(OPENBCI_DARKBLUE); + strokeWeight(1); + int separator_y = y + h + int(widgetManager.getTimeSeriesWidget().INTER_CHANNEL_BAR_SPACE / 2); + line(x, separator_y, x + w, separator_y); + popStyle(); + } + + //draw impedance values in time series also for each channel + drawVoltageValue = true; + if (currentBoard instanceof ImpedanceSettingsBoard) { + if (((ImpedanceSettingsBoard)currentBoard).isCheckingImpedance(channelIndex)) { + impValue.draw(); + drawVoltageValue = false; + } + } + + if (drawVoltageValue) { + voltageValue.draw(); + } + + try { + cbCp5.draw(); + } catch (NullPointerException e) { + e.printStackTrace(); + println("CP5 ERROR ON CHANNEL " + channelIndex); + } + + yAxisMin.draw(); + yAxisMax.draw(); + } + + private int nPointsBasedOnDataSource() { + return (numSeconds * currentBoard.getSampleRate()) / getDownsamplingFactor(); + } + + public void adjustTimeAxis(int _newTimeSize) { + numSeconds = _newTimeSize; + plot.setXLim(-_newTimeSize,0); + + nPoints = nPointsBasedOnDataSource(); + channelPoints = new GPointsArray(nPoints); + timeBetweenPoints = (float)numSeconds / (float)nPoints; + if (_newTimeSize > 1) { + plot.getXAxis().setNTicks(_newTimeSize); //sets the number of axis divisions... + }else{ + plot.getXAxis().setNTicks(10); + } + + updatePlotPoints(); + } + + public void adjustVertScale(int _vertScaleValue) { + boolean enableAutoscale = _vertScaleValue == 0; + gplotAutoscaler.setEnabled(enableAutoscale); + if (enableAutoscale) { + return; + } + yAxisLowerLim = -_vertScaleValue; + yAxisUpperLim = _vertScaleValue; + plot.setYLim(yAxisLowerLim, yAxisUpperLim); + //Update button text + customYLim(yAxisMin, yAxisLowerLim); + customYLim(yAxisMax, yAxisUpperLim); + } + + //Update yAxis text and responsively size Textfield + private void customYLim(TextBox tb, int limit) { + StringBuilder s = new StringBuilder(limit > 0 ? "+" : ""); + s.append(limit); + s.append("uV"); + tb.setText(s.toString()); + } + + public void resize(int _x, int _y, int _w, int _h) { + x = _x; + y = _y; + w = _w; + h = _h; + + //reposition & resize the plot + int plotW = w - uiSpaceWidth; + plot.setPos(x + uiSpaceWidth, y); + plot.setDim(plotW, h); + + int padding = 2; + voltageValue.setPosition(x + uiSpaceWidth + (w - uiSpaceWidth) - padding, y + h); + impValue.setPosition(x + uiSpaceWidth + (int)plot.getDim()[0], y + padding); + + yAxisMax.setPosition(x + uiSpaceWidth + padding, y + int(padding*1.5) - 2); + yAxisMin.setPosition(x + uiSpaceWidth + padding, y + h - yAxisLabel_h - padding - 1); + + final int yAxisLabelWidth = yAxisMax.getWidth(); + int yScaleButtonX = x + uiSpaceWidth + padding_4; + int yScaleButtonY = y + h/2 - yScaleButton_h/2; + boolean enoughSpaceBetweenAxisLabels = h > yScaleButton_h + yAxisLabel_h*2 + 2; + yScaleButtonX += enoughSpaceBetweenAxisLabels ? 0 : yAxisLabelWidth; + yScaleButton_neg.setPosition(yScaleButtonX, yScaleButtonY); + yScaleButtonX += yScaleButton_w + padding; + yScaleButton_pos.setPosition(yScaleButtonX, yScaleButtonY); + + onOff_diameter = h > 26 ? 26 : h - 2; + onOffButton.setSize(onOff_diameter, onOff_diameter); + onOffButton.setPosition(x + 6, y + int(h/2) - int(onOff_diameter/2)); + } + + public void updateCP5(PApplet _parentApplet) { + cbCp5.setGraphics(_parentApplet, 0, 0); + } + + private boolean isBottomChannel() { + int numActiveChannels = widgetManager.getTimeSeriesWidget().tsChanSelect.getActiveChannels().size(); + boolean isLastChannel = channelIndex == widgetManager.getTimeSeriesWidget().tsChanSelect.getActiveChannels().get(numActiveChannels - 1); + return isLastChannel; + } + + public void mousePressed() { + } + + public void mouseReleased() { + } + + private void createOnOffButton(String name, String text, int _x, int _y, int _w, int _h) { + onOffButton = createButton(cbCp5, name, text, _x, _y, _w, _h, 0, h2, 16, channelColors[channelIndex%8], WHITE, BUTTON_HOVER, BUTTON_PRESSED, (Integer) null, -2); + onOffButton.setCircularButton(true); + onOffButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + boolean newState = !currentBoard.isEXGChannelActive(channelIndex); + println("[" + channelString + "] onOff released - " + (newState ? "On" : "Off")); + currentBoard.setEXGChannelActive(channelIndex, newState); + if (currentBoard instanceof ADS1299SettingsBoard) { + W_TimeSeries timeSeriesWidget = widgetManager.getTimeSeriesWidget(); + timeSeriesWidget.adsSettingsController.updateChanSettingsDropdowns(channelIndex, currentBoard.isEXGChannelActive(channelIndex)); + boolean hasUnappliedChanges = currentBoard.isEXGChannelActive(channelIndex) != newState; + timeSeriesWidget.adsSettingsController.setHasUnappliedSettings(channelIndex, hasUnappliedChanges); + } + } + }); + onOffButton.setDescription("Click to toggle channel " + channelString + "."); + } + + private Button createYScaleButton(int chan, boolean shouldIncrease, String bName, String bText, int _x, int _y, int _w, int _h, PImage _default, PImage _hover, PImage _active) { + _default.resize(_w, _h); + _hover.resize(_w, _h); + _active.resize(_w, _h); + final Button myButton = cbCp5.addButton(bName) + .setPosition(_x, _y) + .setSize(_w, _h) + .setColorLabel(color(255)) + .setColorForeground(OPENBCI_BLUE) + .setColorBackground(color(144, 100)) + .setImages(_default, _hover, _active) + ; + myButton.onClick(new yScaleButtonCallbackListener(chan, shouldIncrease)); + return myButton; + } + + private class yScaleButtonCallbackListener implements CallbackListener { + private int channel; + private boolean increase; + private final int hardLimit = 10; + private int yLimOption = TimeSeriesYLim.UV_200.getValue(); + //private int delta = 0; //value to change limits by + + yScaleButtonCallbackListener(int theChannel, boolean isIncrease) { + channel = theChannel; + increase = isIncrease; + } + public void controlEvent(CallbackEvent theEvent) { + verbosePrint("A button was pressed for channel " + (channel+1) + ". Should we increase (or decrease?): " + increase); + + int inc = increase ? 1 : -1; + int factor = yAxisUpperLim > 25 || (yAxisUpperLim == 25 && increase) ? 25 : 5; + int n = (int)(log10(abs(yAxisLowerLim))) * factor * inc; + yAxisLowerLim -= n; + n = (int)(log10(yAxisUpperLim)) * factor * inc; + yAxisUpperLim += n; + + yAxisLowerLim = yAxisLowerLim <= -hardLimit ? yAxisLowerLim : -hardLimit; + yAxisUpperLim = yAxisUpperLim >= hardLimit ? yAxisUpperLim : hardLimit; + plot.setYLim(yAxisLowerLim, yAxisUpperLim); + //Update button text + customYLim(yAxisMin, yAxisLowerLim); + customYLim(yAxisMax, yAxisUpperLim); + } + } +}; + +//======================================================================================================================== +// END OF -- CHANNEL BAR CLASS +//======================================================================================================================== + + +//========================== PLAYBACKSLIDER ========================== +class PlaybackScrollbar { + private final float ps_Padding = 40.0; //used to make room for skip to start button + private int x, y, w, h; + private int swidth, sheight; // width and height of bar + private float xpos, ypos; // x and y position of bar + private float spos; // x position of slider + private float sposMin, sposMax; // max and min values of slider + private boolean over; // is the mouse over the slider? + private boolean locked; + private ControlP5 pbsb_cp5; + private Button skipToStartButton; + private int skipToStart_diameter; + private String currentAbsoluteTimeToDisplay = ""; + private String currentTimeInSecondsToDisplay = ""; + private FileBoard fileBoard; + + private final DateFormat currentTimeFormatShort = new SimpleDateFormat("mm:ss"); + private final DateFormat currentTimeFormatLong = new SimpleDateFormat("HH:mm:ss"); + private final DateFormat timeStampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + PlaybackScrollbar (int _x, int _y, int _w, int _h, float xp, float yp, int sw, int sh) { + x = _x; + y = _y; + w = _w; + h = _h; + swidth = sw; + sheight = sh; + xpos = xp + ps_Padding; //lots of padding to make room for button + ypos = yp-sheight/2; + spos = xpos; + sposMin = xpos; + sposMax = xpos + swidth - sheight/2; + + pbsb_cp5 = new ControlP5(ourApplet); + pbsb_cp5.setGraphics(ourApplet, 0,0); + pbsb_cp5.setAutoDraw(false); + + //Let's make a button to return to the start of playback!! + skipToStart_diameter = 25; + createSkipToStartButton("skipToStartButton", "", int(xp) + int(skipToStart_diameter*.5), int(yp) + int(sh/2) - skipToStart_diameter, skipToStart_diameter, skipToStart_diameter); + + fileBoard = (FileBoard)currentBoard; + } + + private void createSkipToStartButton(String name, String text, int _x, int _y, int _w, int _h) { + skipToStartButton = createButton(pbsb_cp5, name, text, _x, _y, _w, _h, 0, p5, 12, GREY_235, OPENBCI_DARKBLUE, BUTTON_HOVER, BUTTON_PRESSED, (Integer)null, 0); + PImage defaultImage = loadImage("skipToStart_default-30x26.png"); + skipToStartButton.setImage(defaultImage); + skipToStartButton.setForceDrawBackground(true); + skipToStartButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + skipToStartButtonAction(); + } + }); + skipToStartButton.setDescription("Click to go back to the beginning of the file."); + } + + /////////////// Update loop for PlaybackScrollbar + void update() { + checkMouseOver(); // check if mouse is over + + if (mousePressed && over) { + locked = true; + } + if (!mousePressed) { + locked = false; + } + //if the slider is being used, update new position based on user mouseX + if (locked) { + spos = constrain(mouseX-sheight/2, sposMin, sposMax); + scrubToPosition(); + } + else { + updateCursor(); + } + + // update timestamp + currentAbsoluteTimeToDisplay = getAbsoluteTimeToDisplay(); + + //update elapsed time to display + currentTimeInSecondsToDisplay = getCurrentTimeToDisplaySeconds(); + + } //end update loop for PlaybackScrollbar + + void updateCursor() { + float currentSample = float(fileBoard.getCurrentSample()); + float totalSamples = float(fileBoard.getTotalSamples()); + float currentPlaybackPos = currentSample / totalSamples; + + spos = lerp(sposMin, sposMax, currentPlaybackPos); + } + + void scrubToPosition() { + int totalSamples = fileBoard.getTotalSamples(); + int newSamplePos = floor(totalSamples * getCursorPercentage()); + + fileBoard.goToIndex(newSamplePos); + dataProcessing.updateEntireDownsampledBuffer(); + dataProcessing.clearCalculatedMetricWidgets(); + } + + float getCursorPercentage() { + return (spos - sposMin) / (sposMax - sposMin); + } + + String getAbsoluteTimeToDisplay() { + List currentData = currentBoard.getData(1); + if (currentData.get(0).length == 0) { + return ""; + } + int timeStampChan = currentBoard.getTimestampChannel(); + long timestampMS = (long)(currentData.get(0)[timeStampChan] * 1000.0); + if (timestampMS == 0) { + return ""; + } + + return timeStampFormat.format(new Date(timestampMS)); + } + + String getCurrentTimeToDisplaySeconds() { + double totalMillis = fileBoard.getTotalTimeSeconds() * 1000.0; + double currentMillis = fileBoard.getCurrentTimeSeconds() * 1000.0; + + String totalTimeStr = formatCurrentTime(totalMillis); + String currentTimeStr = formatCurrentTime(currentMillis); + + return currentTimeStr + " / " + totalTimeStr; + } + + String formatCurrentTime(double millis) { + DateFormat formatter = currentTimeFormatShort; + if (millis >= 3600000.0) { // bigger than 60 minutes + formatter = currentTimeFormatLong; + } + + return formatter.format(new Date((long)millis)); + } + + //checks if mouse is over the playback scrollbar + private void checkMouseOver() { + if (mouseX > xpos && mouseX < xpos+swidth && + mouseY > ypos && mouseY < ypos+sheight) { + if (!over) { + onMouseEnter(); + } + } + else { + if (over) { + onMouseExit(); + } + } + } + + // called when the mouse enters the playback scrollbar + private void onMouseEnter() { + over = true; + cursor(HAND); //changes cursor icon to a hand + } + + private void onMouseExit() { + over = false; + cursor(ARROW); + } + + void draw() { + pushStyle(); + + fill(GREY_235); + stroke(OPENBCI_BLUE); + rect(x, y, w, h); + + //draw the playback slider inside the playback sub-widget + noStroke(); + fill(GREY_200); + rect(xpos, ypos, swidth, sheight); + + //select color for playback indicator + if (over || locked) { + fill(OPENBCI_DARKBLUE); + } else { + fill(102, 102, 102); + } + //draws playback position indicator + rect(spos, ypos, sheight/2, sheight); + + //draw current timestamp and X of Y Seconds above scrollbar + textFont(p2, 18); + fill(OPENBCI_DARKBLUE); + textAlign(LEFT, TOP); + float textHeight = textAscent() - textDescent(); + float textY = y - textHeight - 10; + float tw = textWidth(currentAbsoluteTimeToDisplay); + text(currentAbsoluteTimeToDisplay, xpos + swidth - tw, textY); + text(currentTimeInSecondsToDisplay, xpos, textY); + + popStyle(); + + pbsb_cp5.draw(); + } + + void screenResized(int _x, int _y, int _w, int _h, float _pbx, float _pby, float _pbw, float _pbh) { + x = _x; + y = _y; + w = _w; + h = _h; + swidth = int(_pbw); + sheight = int(_pbh); + xpos = _pbx + ps_Padding; //add lots of padding for use + ypos = _pby - sheight/2; + sposMin = xpos; + sposMax = xpos + swidth - sheight/2; + //update the position of the playback indicator us + //newspos = updatePos(); + + pbsb_cp5.setGraphics(ourApplet, 0, 0); + + skipToStartButton.setPosition( + int(_pbx) + int(skipToStart_diameter*.5), + int(_pby) - int(skipToStart_diameter*.5) + ); + } + + //This function scrubs to the beginning of the playback file + //Useful to 'reset' the scrollbar before loading a new playback file + void skipToStartButtonAction() { + fileBoard.goToIndex(0); + dataProcessing.updateEntireDownsampledBuffer(); + dataProcessing.clearCalculatedMetricWidgets(); + } + +};//end PlaybackScrollbar class + +//========================== TimeDisplay ========================== +class TimeDisplay { + int swidth, sheight; // width and height of bar + float xpos, ypos; // x and y position of bar + String currentAbsoluteTimeToDisplay = ""; + Boolean updatePosition = false; + LocalDateTime time; + + TimeDisplay (float xp, float yp, int sw, int sh) { + swidth = sw; + sheight = sh; + xpos = xp; //lots of padding to make room for button + ypos = yp; + currentAbsoluteTimeToDisplay = fetchCurrentTimeString(); + } + + /////////////// Update loop for TimeDisplay when data stream is running + void update() { + if (currentBoard.isStreaming()) { + //Fetch Local time + try { + currentAbsoluteTimeToDisplay = fetchCurrentTimeString(); + } catch (NullPointerException e) { + println("TimeDisplay: Timestamp error..."); + e.printStackTrace(); + } + + } + } //end update loop for TimeDisplay + + void draw() { + pushStyle(); + //draw current timestamp at the bottom of the Widget container + if (!currentAbsoluteTimeToDisplay.equals(null)) { + int fontSize = 17; + textFont(p2, fontSize); + fill(OPENBCI_DARKBLUE); + float tw = textWidth(currentAbsoluteTimeToDisplay); + text(currentAbsoluteTimeToDisplay, xpos + swidth - tw, ypos); + text(streamTimeElapsed.toString(), xpos + 10, ypos); + } + popStyle(); + } + + void screenResized(float _x, float _y, float _w, float _h) { + swidth = int(_w); + sheight = int(_h); + xpos = _x; + ypos = _y; + } + + String fetchCurrentTimeString() { + time = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); + return time.format(formatter); + } +};//end TimeDisplay class diff --git a/OpenBCI_GUI/TopNav.pde b/OpenBCI_GUI/TopNav.pde index dabd4e12a..36be7ca7d 100644 --- a/OpenBCI_GUI/TopNav.pde +++ b/OpenBCI_GUI/TopNav.pde @@ -563,10 +563,10 @@ class TopNav { public void dataStreamTogglePressed() { //Exit method if doing Cyton impedance check. Avoids a BrainFlow error. - if (currentBoard instanceof BoardCyton && w_cytonImpedance != null) { + if (currentBoard instanceof BoardCyton && widgetManager.getWidgetExists("W_CytonImpedance")) { Integer checkingImpOnChan = ((ImpedanceSettingsBoard)currentBoard).isCheckingImpedanceOnChannel(); - //println("isCheckingImpedanceOnAnythingEZCHECK==",w_cytonImpedance.isCheckingImpedanceOnAnything); - if (checkingImpOnChan != null || w_cytonImpedance.cytonMasterImpedanceCheckIsActive() || w_cytonImpedance.isCheckingImpedanceOnAnything) { + W_CytonImpedance cytonImpedanceWidget = (W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance"); + if (checkingImpOnChan != null || cytonImpedanceWidget.cytonMasterImpedanceCheckIsActive() || cytonImpedanceWidget.getIsCheckingImpedanceOnAnything()) { PopupMessage msg = new PopupMessage("Busy Checking Impedance", "Please turn off impedance check to begin recording the data stream."); println("OpenBCI_GUI::Cyton: Please turn off impedance check to begin recording the data stream."); return; @@ -696,20 +696,8 @@ class LayoutSelector { void toggleVisibility() { isVisible = !isVisible; - if (isVisible) { - //the very convoluted way of locking all controllers of a single controlP5 instance... - for (int i = 0; i < wm.widgets.size(); i++) { - for (int j = 0; j < wm.widgets.get(i).cp5_widget.getAll().size(); j++) { - wm.widgets.get(i).cp5_widget.getController(wm.widgets.get(i).cp5_widget.getAll().get(j).getAddress()).lock(); - } - } - } else { - //the very convoluted way of unlocking all controllers of a single controlP5 instance... - for (int i = 0; i < wm.widgets.size(); i++) { - for (int j = 0; j < wm.widgets.get(i).cp5_widget.getAll().size(); j++) { - wm.widgets.get(i).cp5_widget.getController(wm.widgets.get(i).cp5_widget.getAll().get(j).getAddress()).unlock(); - } - } + if (widgetManager != null) { + widgetManager.lockCp5ObjectsInAllWidgets(isVisible); } } @@ -728,8 +716,8 @@ class LayoutSelector { public void controlEvent(CallbackEvent theEvent) { output("Layout [" + (layoutNumber) + "] selected."); toggleVisibility(); //shut layoutSelector if something is selected - wm.setNewContainerLayout(layoutNumber); //have WidgetManager update Layout and active widgets - settings.currentLayout = layoutNumber; //copy this value to be used when saving Layout setting + widgetManager.setNewContainerLayout(layoutNumber); //have WidgetManager update Layout and active widgets + sessionSettings.currentLayout = layoutNumber; //copy this value to be used when saving Layout setting } }); layoutOptions.add(tempLayoutButton); @@ -880,23 +868,9 @@ class ConfigSelector { void toggleVisibility() { isVisible = !isVisible; - if (systemMode >= SYSTEMMODE_POSTINIT) { - if (isVisible) { - //the very convoluted way of locking all controllers of a single controlP5 instance... - for (int i = 0; i < wm.widgets.size(); i++) { - for (int j = 0; j < wm.widgets.get(i).cp5_widget.getAll().size(); j++) { - wm.widgets.get(i).cp5_widget.getController(wm.widgets.get(i).cp5_widget.getAll().get(j).getAddress()).lock(); - } - } - clearAllSettingsPressed = false; - } else { - //the very convoluted way of unlocking all controllers of a single controlP5 instance... - for (int i = 0; i < wm.widgets.size(); i++) { - for (int j = 0; j < wm.widgets.get(i).cp5_widget.getAll().size(); j++) { - wm.widgets.get(i).cp5_widget.getController(wm.widgets.get(i).cp5_widget.getAll().get(j).getAddress()).unlock(); - } - } - } + if (widgetManager != null) { + widgetManager.lockCp5ObjectsInAllWidgets(isVisible); + clearAllSettingsPressed = !isVisible; } //When closed by any means and confirmation buttons are open... @@ -980,7 +954,7 @@ class ConfigSelector { saveSessionSettings.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { toggleVisibility(); - settings.saveButtonPressed(); + sessionSettings.saveButtonPressed(); } }); saveSessionSettings.setDescription("Expert Mode enables advanced keyboard shortcuts and access to all GUI features."); @@ -991,7 +965,7 @@ class ConfigSelector { loadSessionSettings.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { toggleVisibility(); - settings.loadButtonPressed(); + sessionSettings.loadButtonPressed(); } }); loadSessionSettings.setDescription("Expert Mode enables advanced keyboard shortcuts and access to all GUI features."); @@ -1002,7 +976,7 @@ class ConfigSelector { defaultSessionSettings.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { toggleVisibility(); - settings.defaultButtonPressed(); + sessionSettings.defaultButtonPressed(); } }); defaultSessionSettings.setDescription("Expert Mode enables advanced keyboard shortcuts and access to all GUI features."); @@ -1043,7 +1017,7 @@ class ConfigSelector { //Shorten height of this box h -= margin*4 + b_h*3; //User has selected Are You Sure?->Yes - settings.clearAll(); + sessionSettings.clearAll(); guiSettings.resetAllSettings(); clearAllSettingsPressed = false; //Stop the system if the user clears all settings @@ -1188,22 +1162,8 @@ class TutorialSelector { void toggleVisibility() { isVisible = !isVisible; - if (systemMode >= SYSTEMMODE_POSTINIT) { - if (isVisible) { - //the very convoluted way of locking all controllers of a single controlP5 instance... - for (int i = 0; i < wm.widgets.size(); i++) { - for (int j = 0; j < wm.widgets.get(i).cp5_widget.getAll().size(); j++) { - wm.widgets.get(i).cp5_widget.getController(wm.widgets.get(i).cp5_widget.getAll().get(j).getAddress()).lock(); - } - } - } else { - //the very convoluted way of unlocking all controllers of a single controlP5 instance... - for (int i = 0; i < wm.widgets.size(); i++) { - for (int j = 0; j < wm.widgets.get(i).cp5_widget.getAll().size(); j++) { - wm.widgets.get(i).cp5_widget.getController(wm.widgets.get(i).cp5_widget.getAll().get(j).getAddress()).unlock(); - } - } - } + if (widgetManager != null) { + widgetManager.lockCp5ObjectsInAllWidgets(isVisible); } } diff --git a/OpenBCI_GUI/W_Accelerometer.pde b/OpenBCI_GUI/W_Accelerometer.pde index 46b444eeb..1fc78c17d 100644 --- a/OpenBCI_GUI/W_Accelerometer.pde +++ b/OpenBCI_GUI/W_Accelerometer.pde @@ -11,7 +11,7 @@ // //////////////////////////////////////////////////// -class W_Accelerometer extends Widget { +class W_Accelerometer extends WidgetWithSettings { //To see all core variables/methods of the Widget class, refer to Widget.pde color graphStroke = color(210); color graphBG = color(245); @@ -19,21 +19,17 @@ class W_Accelerometer extends Widget { color strokeColor = color(138, 146, 153); color eggshell = color(255, 253, 248); - //Graphing variables - int[] xLimOptions = {0, 1, 3, 5, 10, 20}; //number of seconds (x axis of graph) - int[] yLimOptions = {0, 1, 2}; - float accelXyzLimit = 4.0; //hard limit on all accel values - int accelHorizLimit = 20; - float[] lastAccelVals; - AccelerometerBar accelerometerBar; - //Bottom xyz graph + AccelerometerBar accelerometerBar; int accelGraphWidth; int accelGraphHeight; int accelGraphX; int accelGraphY; int accPadding = 30; final int PAD_FIVE = 5; + private AccelerometerVerticalScale verticalScale = AccelerometerVerticalScale.AUTO; + private AccelerometerHorizontalScale horizontalScale = AccelerometerHorizontalScale.FIVE_SEC; + float[] lastDataSampleValues; //Circular 3d xyz graph float polarWindowX; @@ -41,67 +37,68 @@ class W_Accelerometer extends Widget { int polarWindowWidth; int polarWindowHeight; float polarCorner; - - float yMaxMin; + private float polarYMaxMin; boolean accelInitHasOccured = false; private Button accelModeButton; private AccelerometerCapableBoard accelBoard; - W_Accelerometer(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_Accelerometer() { + super(); + widgetTitle = "Accelerometer"; accelBoard = (AccelerometerCapableBoard)currentBoard; - - //Default dropdown settings - settings.accVertScaleSave = 0; - settings.accHorizScaleSave = 3; - - //Make dropdowns - addDropdown("accelVertScale", "Vert Scale", Arrays.asList(settings.accVertScaleArray), settings.accVertScaleSave); - addDropdown("accelDuration", "Window", Arrays.asList(settings.accHorizScaleArray), settings.accHorizScaleSave); - + setGraphDimensions(); - yMaxMin = adjustYMaxMinBasedOnSource(); + polarYMaxMin = adjustYMaxMinBasedOnSource(); //XYZ buffer for bottom graph - lastAccelVals = new float[NUM_ACCEL_DIMS]; + lastDataSampleValues = new float[NUM_ACCEL_DIMS]; + + //Create our channel bar and populate our accelerometerBar array + accelerometerBar = new AccelerometerBar(ourApplet, verticalScale.getHighestValue(), accelGraphX, accelGraphY, accelGraphWidth, accelGraphHeight); + accelerometerBar.adjustTimeAxis(horizontalScale.getValue()); + accelerometerBar.adjustVertScale(verticalScale.getValue()); - //create our channel bar and populate our accelerometerBar array! - accelerometerBar = new AccelerometerBar(_parent, accelXyzLimit, accelGraphX, accelGraphY, accelGraphWidth, accelGraphHeight); - accelerometerBar.adjustTimeAxis(xLimOptions[settings.accHorizScaleSave]); - accelerometerBar.adjustVertScale(yLimOptions[settings.accVertScaleSave]); + createAccelModeButton("accelModeButton", "Turn Accel. Off", (int)(x + 1), (int)(y0 + NAV_HEIGHT + 1), 120, NAV_HEIGHT - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + } + + @Override void initWidgetSettings() { + super.initWidgetSettings(); + widgetSettings.set(AccelerometerVerticalScale.class, AccelerometerVerticalScale.AUTO) + .set(AccelerometerHorizontalScale.class, AccelerometerHorizontalScale.FIVE_SEC) + .saveDefaults(); + initDropdown(AccelerometerVerticalScale.class, "accelerometerVerticalScaleDropdown", "Vert Scale"); + initDropdown(AccelerometerHorizontalScale.class, "accelerometerHorizontalScaleDropdown", "Window"); + } - createAccelModeButton("accelModeButton", "Turn Accel. Off", (int)(x + 1), (int)(y0 + navHeight + 1), 120, navHeight - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + @Override void applySettings() { + updateDropdownLabel(AccelerometerVerticalScale.class, "accelerometerVerticalScaleDropdown"); + updateDropdownLabel(AccelerometerHorizontalScale.class, "accelerometerHorizontalScaleDropdown"); } float adjustYMaxMinBasedOnSource() { float _yMaxMin; if (eegDataSource == DATASOURCE_CYTON) { _yMaxMin = 4.0; - }else if (eegDataSource == DATASOURCE_GANGLION || globalChannelCount == 4) { + } else if (eegDataSource == DATASOURCE_GANGLION || globalChannelCount == 4) { _yMaxMin = 2.0; - accelXyzLimit = 2.0; - }else{ + } else { _yMaxMin = 4.0; } return _yMaxMin; } - int nPointsBasedOnDataSource() { - return accelHorizLimit * ((AccelerometerCapableBoard)currentBoard).getAccelSampleRate(); - } - void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); if (accelBoard.isAccelerometerActive()) { //update the line graph and corresponding gplot points accelerometerBar.update(); //update the current Accelerometer values - lastAccelVals = accelerometerBar.getLastAccelVals(); + lastDataSampleValues = accelerometerBar.getLastAccelVals(); } //ignore top left button interaction when widgetSelector dropdown is active @@ -117,11 +114,11 @@ class W_Accelerometer extends Widget { } public float getLastAccelVal(int val) { - return lastAccelVals[val]; + return lastDataSampleValues[val]; } void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); pushStyle(); @@ -171,20 +168,20 @@ class W_Accelerometer extends Widget { int prevY = y; int prevW = w; int prevH = h; - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); setGraphDimensions(); //resize the accelerometer line graph accelerometerBar.screenResized(accelGraphX, accelGraphY, accelGraphWidth, accelGraphHeight); //bar x, bar y, bar w, bar h //update the position of the accel mode button - accelModeButton.setPosition((int)(x0 + 1), (int)(y0 + navHeight + 1)); + accelModeButton.setPosition((int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1)); } void mousePressed() { - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); } void mouseReleased() { - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + super.mouseReleased(); } private void createAccelModeButton(String name, String text, int _x, int _y, int _w, int _h, PFont _font, int _fontSize, color _bg, color _textColor) { @@ -197,11 +194,11 @@ class W_Accelerometer extends Widget { output("Starting to read accelerometer"); accelModeButton.getCaptionLabel().setText("Turn Accel. Off"); if (currentBoard instanceof DigitalCapableBoard) { - w_digitalRead.toggleDigitalReadButton(false); + ((W_DigitalRead) widgetManager.getWidget("W_DigitalRead")).toggleDigitalReadButton(false); } if (currentBoard instanceof AnalogCapableBoard) { - w_pulseSensor.toggleAnalogReadButton(false); - w_analogRead.toggleAnalogReadButton(false); + ((W_PulseSensor) widgetManager.getWidget("W_PulseSensor")).toggleAnalogReadButton(false); + ((W_AnalogRead) widgetManager.getWidget("W_AnalogRead")).toggleAnalogReadButton(false); } ///Hide button when set On for Cyton board only. This is a special case for Cyton board Aux mode behavior. See BoardCyton.pde for more info. if ((currentBoard instanceof BoardCyton)) { @@ -230,9 +227,9 @@ class W_Accelerometer extends Widget { //Draw the current accelerometer values as text void drawAccValues() { - float displayX = (float)lastAccelVals[0]; - float displayY = (float)lastAccelVals[1]; - float displayZ = (float)lastAccelVals[2]; + float displayX = (float)lastDataSampleValues[0]; + float displayY = (float)lastDataSampleValues[1]; + float displayZ = (float)lastDataSampleValues[2]; textAlign(LEFT,CENTER); textFont(h1,20); fill(ACCEL_X_COLOR); @@ -245,18 +242,18 @@ class W_Accelerometer extends Widget { //Draw the current accelerometer values as a 3D graph void draw3DGraph() { - float displayX = (float)lastAccelVals[0]; - float displayY = (float)lastAccelVals[1]; - float displayZ = (float)lastAccelVals[2]; + float displayX = (float)lastDataSampleValues[0]; + float displayY = (float)lastDataSampleValues[1]; + float displayZ = (float)lastDataSampleValues[2]; noFill(); strokeWeight(3); stroke(ACCEL_X_COLOR); - line(polarWindowX, polarWindowY, polarWindowX+map(displayX, -yMaxMin, yMaxMin, -polarWindowWidth/2, polarWindowWidth/2), polarWindowY); + line(polarWindowX, polarWindowY, polarWindowX+map(displayX, -polarYMaxMin, polarYMaxMin, -polarWindowWidth/2, polarWindowWidth/2), polarWindowY); stroke(ACCEL_Y_COLOR); - line(polarWindowX, polarWindowY, polarWindowX+map((sqrt(2)*displayY/2), -yMaxMin, yMaxMin, -polarWindowWidth/2, polarWindowWidth/2), polarWindowY+map((sqrt(2)*displayY/2), -yMaxMin, yMaxMin, polarWindowWidth/2, -polarWindowWidth/2)); + line(polarWindowX, polarWindowY, polarWindowX+map((sqrt(2)*displayY/2), -polarYMaxMin, polarYMaxMin, -polarWindowWidth/2, polarWindowWidth/2), polarWindowY+map((sqrt(2)*displayY/2), -polarYMaxMin, polarYMaxMin, polarWindowWidth/2, -polarWindowWidth/2)); stroke(ACCEL_Z_COLOR); - line(polarWindowX, polarWindowY, polarWindowX, polarWindowY+map(displayZ, -yMaxMin, yMaxMin, polarWindowWidth/2, -polarWindowWidth/2)); + line(polarWindowX, polarWindowY, polarWindowX, polarWindowY+map(displayZ, -polarYMaxMin, polarYMaxMin, polarWindowWidth/2, -polarWindowWidth/2)); strokeWeight(1); } @@ -277,25 +274,26 @@ class W_Accelerometer extends Widget { } } -};//end W_Accelerometer class + public void setVerticalScale(int n) { + widgetSettings.setByIndex(AccelerometerVerticalScale.class, n); + int verticalScaleValue = widgetSettings.get(AccelerometerVerticalScale.class).getValue(); + accelerometerBar.adjustVertScale(verticalScaleValue); + } -//These functions are activated when an item from the corresponding dropdown is selected -void accelVertScale(int n) { - settings.accVertScaleSave = n; - w_accelerometer.accelerometerBar.adjustVertScale(w_accelerometer.yLimOptions[n]); -} + public void setHorizontalScale(int n) { + widgetSettings.setByIndex(AccelerometerHorizontalScale.class, n); + int horizontalScaleValue = widgetSettings.get(AccelerometerHorizontalScale.class).getValue(); + accelerometerBar.adjustTimeAxis(horizontalScaleValue); + } -//triggered when there is an event in the Duration Dropdown -void accelDuration(int n) { - settings.accHorizScaleSave = n; +}; - //Sync the duration of Time Series, Accelerometer, and Analog Read(Cyton Only) - if (n == 0) { - w_accelerometer.accelerometerBar.adjustTimeAxis(w_timeSeries.getTSHorizScale().getValue()); - } else { - //set accelerometer x axis to the duration selected from dropdown - w_accelerometer.accelerometerBar.adjustTimeAxis(w_accelerometer.xLimOptions[n]); - } +public void accelerometerVerticalScaleDropdown(int n) { + ((W_Accelerometer) widgetManager.getWidget("W_Accelerometer")).setVerticalScale(n); +} + +public void accelerometerHorizontalScaleDropdown(int n) { + ((W_Accelerometer) widgetManager.getWidget("W_Accelerometer")).setHorizontalScale(n); } //======================================================================================================================== @@ -329,7 +327,7 @@ class AccelerometerBar { private AccelerometerCapableBoard accelBoard; - AccelerometerBar(PApplet _parent, float accelXyzLimit, int _x, int _y, int _w, int _h) { //channel number, x/y location, height, width + AccelerometerBar(PApplet _parentApplet, float _yLimit, int _x, int _y, int _w, int _h) { //channel number, x/y location, height, width // This widget is only instantiated when the board is accel capable, so we don't need to check accelBoard = (AccelerometerCapableBoard)currentBoard; @@ -339,13 +337,13 @@ class AccelerometerBar { w = _w; h = _h; - plot = new GPlot(_parent); + plot = new GPlot(_parentApplet); plot.setPos(x + 36 + 4, y); //match Accelerometer plot position with Time Series plot.setDim(w - 36 - 4, 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(-accelXyzLimit, accelXyzLimit); //change this to adjust vertical scale + plot.setYLim(-_yLimit, _yLimit); //change this to adjust vertical scale //plot.setPointSize(2); plot.setPointColor(0); plot.getXAxis().setAxisLabelText("Time (s)"); diff --git a/OpenBCI_GUI/W_AnalogRead.pde b/OpenBCI_GUI/W_AnalogRead.pde index aa5ce88ee..d2b6685af 100644 --- a/OpenBCI_GUI/W_AnalogRead.pde +++ b/OpenBCI_GUI/W_AnalogRead.pde @@ -1,94 +1,80 @@ - -//////////////////////////////////////////////////// -// -// W_AnalogRead is used to visiualze analog voltage values -// -// Created: AJ Keller -// -// -///////////////////////////////////////////////////, - -class W_AnalogRead extends Widget { - - //to see all core variables/methods of the Widget class, refer to Widget.pde - //put your custom variables here... - - private int numAnalogReadBars; - float xF, yF, wF, hF; - float arPadding; - float ar_x, ar_y, ar_h, ar_w; // values for actual time series chart (rectangle encompassing all analogReadBars) - float plotBottomWell; - float playbackWidgetHeight; - int analogReadBarHeight; - - AnalogReadBar[] analogReadBars; - - int[] xLimOptions = {0, 1, 3, 5, 10, 20}; // number of seconds (x axis of graph) - int[] yLimOptions = {0, 50, 100, 200, 400, 1000, 10000}; // 0 = Autoscale ... everything else is uV +//////////////////////////////////////////////////////////////////////// +// // +// W_AnalogRead is used to visualize analog voltage values // +// // +// Created: AJ Keller // +// Refactored: Richard Waltman, April 2025 // +// // +// // +//////////////////////////////////////////////////////////////////////// + +class W_AnalogRead extends WidgetWithSettings { + private float arPadding; + // values for actual time series chart (rectangle encompassing all analogReadBars) + private float ar_x, ar_y, ar_h, ar_w; + private float plotBottomWell; + private float playbackWidgetHeight; + private int analogReadBarHeight; + + private final int NUM_ANALOG_READ_BARS = 3; + private AnalogReadBar[] analogReadBars; private boolean allowSpillover = false; - //Initial dropdown settings - private int arInitialVertScaleIndex = 5; - private int arInitialHorizScaleIndex = 0; - private Button analogModeButton; private AnalogCapableBoard analogBoard; - W_AnalogRead(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_AnalogRead() { + super(); + widgetTitle = "AnalogRead"; analogBoard = (AnalogCapableBoard)currentBoard; - //Analog Read settings - settings.arVertScaleSave = 5; //updates in VertScale_AR() - settings.arHorizScaleSave = 0; //updates in Duration_AR() - - //This is the protocol for setting up dropdowns. - //Note that these 3 dropdowns correspond to the 3 global functions below - //You just need to make sure the "id" (the 1st String) has the same name as the corresponding function - addDropdown("VertScale_AR", "Vert Scale", Arrays.asList(settings.arVertScaleArray), arInitialVertScaleIndex); - addDropdown("Duration_AR", "Window", Arrays.asList(settings.arHorizScaleArray), arInitialHorizScaleIndex); - // addDropdown("Spillover", "Spillover", Arrays.asList("False", "True"), 0); - - //set number of analog reads - numAnalogReadBars = 3; - - xF = float(x); //float(int( ... is a shortcut for rounding the float down... so that it doesn't creep into the 1px margin - yF = float(y); - wF = float(w); - hF = float(h); - plotBottomWell = 45.0; //this appears to be an arbitrary vertical space adds GPlot leaves at bottom, I derived it through trial and error arPadding = 10.0; - ar_x = xF + arPadding; - ar_y = yF + (arPadding); - ar_w = wF - arPadding*2; - ar_h = hF - playbackWidgetHeight - plotBottomWell - (arPadding*2); - analogReadBarHeight = int(ar_h/numAnalogReadBars); + ar_x = float(x) + arPadding; + ar_y = float(y) + (arPadding); + ar_w = float(w) - arPadding*2; + ar_h = float(h) - playbackWidgetHeight - plotBottomWell - (arPadding*2); - analogReadBars = new AnalogReadBar[numAnalogReadBars]; + analogReadBars = new AnalogReadBar[NUM_ANALOG_READ_BARS]; + analogReadBarHeight = int(ar_h / analogReadBars.length); //create our channel bars and populate our analogReadBars array! - for(int i = 0; i < numAnalogReadBars; i++) { + for(int i = 0; i < analogReadBars.length; i++) { int analogReadBarY = int(ar_y) + i*(analogReadBarHeight); //iterate through bar locations - AnalogReadBar tempBar = new AnalogReadBar(_parent, i+5, int(ar_x), analogReadBarY, int(ar_w), analogReadBarHeight); //int _channelNumber, int _x, int _y, int _w, int _h + AnalogReadBar tempBar = new AnalogReadBar(ourApplet, i+5, int(ar_x), analogReadBarY, int(ar_w), analogReadBarHeight); //int _channelNumber, int _x, int _y, int _w, int _h analogReadBars[i] = tempBar; - analogReadBars[i].adjustVertScale(yLimOptions[arInitialVertScaleIndex]); - //sync horiz axis to Time Series by default - analogReadBars[i].adjustTimeAxis(w_timeSeries.getTSHorizScale().getValue()); } + + int verticalScaleValue = widgetSettings.get(AnalogReadVerticalScale.class).getValue(); + int horizontalScaleValue = widgetSettings.get(AnalogReadHorizontalScale.class).getValue(); + applyVerticalScale(verticalScaleValue); + applyHorizontalScale(horizontalScaleValue); - createAnalogModeButton("analogModeButton", "Turn Analog Read On", (int)(x0 + 1), (int)(y0 + navHeight + 1), 128, navHeight - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + createAnalogModeButton("analogModeButton", "Turn Analog Read On", (int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1), 128, NAV_HEIGHT - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); } - public int getNumAnalogReads() { - return numAnalogReadBars; + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + widgetSettings.set(AnalogReadVerticalScale.class, AnalogReadVerticalScale.ONE_THOUSAND_FIFTY) + .set(AnalogReadHorizontalScale.class, AnalogReadHorizontalScale.FIVE_SEC) + .saveDefaults(); + + initDropdown(AnalogReadVerticalScale.class, "analogReadVerticalScaleDropdown", "Vert Scale"); + initDropdown(AnalogReadHorizontalScale.class, "analogReadHorizontalScaleDropdown", "Window"); } - void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + @Override + protected void applySettings() { + updateDropdownLabel(AnalogReadVerticalScale.class, "analogReadVerticalScaleDropdown"); + updateDropdownLabel(AnalogReadHorizontalScale.class, "analogReadHorizontalScaleDropdown"); + } + + public void update() { + super.update(); if (currentBoard instanceof DataSourcePlayback) { if (((DataSourcePlayback)currentBoard) instanceof AnalogCapableBoard @@ -98,7 +84,7 @@ class W_AnalogRead extends Widget { } //update channel bars ... this means feeding new EEG data into plots - for(int i = 0; i < numAnalogReadBars; i++) { + for(int i = 0; i < analogReadBars.length; i++) { analogReadBars[i].update(); } @@ -114,45 +100,40 @@ class W_AnalogRead extends Widget { } } - void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + public void draw() { + super.draw(); //remember to refer to x,y,w,h which are the positioning variables of the Widget class if (analogBoard.isAnalogActive()) { - for(int i = 0; i < numAnalogReadBars; i++) { + for(int i = 0; i < analogReadBars.length; i++) { analogReadBars[i].draw(); } } } - void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + public void screenResized() { + super.screenResized(); - xF = float(x); //float(int( ... is a shortcut for rounding the float down... so that it doesn't creep into the 1px margin - yF = float(y); - wF = float(w); - hF = float(h); + ar_x = float(x) + arPadding; + ar_y = float(y) + (arPadding); + ar_w = float(w) - arPadding*2; + ar_h = float(h) - playbackWidgetHeight - plotBottomWell - (arPadding*2); + analogReadBarHeight = int(ar_h/analogReadBars.length); - ar_x = xF + arPadding; - ar_y = yF + (arPadding); - ar_w = wF - arPadding*2; - ar_h = hF - playbackWidgetHeight - plotBottomWell - (arPadding*2); - analogReadBarHeight = int(ar_h/numAnalogReadBars); - - for(int i = 0; i < numAnalogReadBars; i++) { + for(int i = 0; i < analogReadBars.length; i++) { int analogReadBarY = int(ar_y) + i*(analogReadBarHeight); //iterate through bar locations analogReadBars[i].screenResized(int(ar_x), analogReadBarY, int(ar_w), analogReadBarHeight); //bar x, bar y, bar w, bar h } - analogModeButton.setPosition((int)(x0 + 1), (int)(y0 + navHeight + 1)); + analogModeButton.setPosition((int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1)); } - void mousePressed() { - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + public void mousePressed() { + super.mousePressed(); } - void mouseReleased() { - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + public void mouseReleased() { + super.mouseReleased(); } private void createAnalogModeButton(String name, String text, int _x, int _y, int _w, int _h, PFont _font, int _fontSize, color _bg, color _textColor) { @@ -164,16 +145,16 @@ class W_AnalogRead extends Widget { analogBoard.setAnalogActive(true); analogModeButton.getCaptionLabel().setText("Turn Analog Read Off"); output("Starting to read analog inputs on pin marked A5 (D11), A6 (D12) and A7 (D13)"); - w_pulseSensor.toggleAnalogReadButton(true); - w_accelerometer.accelBoardSetActive(false); - w_digitalRead.toggleDigitalReadButton(false); + ((W_PulseSensor) widgetManager.getWidget("W_Accelerometer")).toggleAnalogReadButton(true); + ((W_Accelerometer) widgetManager.getWidget("W_Accelerometer")).accelBoardSetActive(false); + ((W_DigitalRead) widgetManager.getWidget("W_Accelerometer")).toggleDigitalReadButton(false); } else { analogBoard.setAnalogActive(false); analogModeButton.getCaptionLabel().setText("Turn Analog Read On"); output("Starting to read accelerometer"); - w_accelerometer.accelBoardSetActive(true); - w_digitalRead.toggleDigitalReadButton(false); - w_pulseSensor.toggleAnalogReadButton(false); + ((W_Accelerometer) widgetManager.getWidget("W_Accelerometer")).accelBoardSetActive(true); + ((W_DigitalRead) widgetManager.getWidget("W_Accelerometer")).toggleDigitalReadButton(false); + ((W_PulseSensor) widgetManager.getWidget("W_Accelerometer")).toggleAnalogReadButton(false); } } }); @@ -190,30 +171,38 @@ class W_AnalogRead extends Widget { analogModeButton.setOff(); } } -}; -//These functions need to be global! These functions are activated when an item from the corresponding dropdown is selected -void VertScale_AR(int n) { - settings.arVertScaleSave = n; - for(int i = 0; i < w_analogRead.numAnalogReadBars; i++) { - w_analogRead.analogReadBars[i].adjustVertScale(w_analogRead.yLimOptions[n]); + public void setVerticalScale(int n) { + widgetSettings.setByIndex(AnalogReadVerticalScale.class, n); + int verticalScaleValue = widgetSettings.get(AnalogReadVerticalScale.class).getValue(); + applyVerticalScale(verticalScaleValue); } -} -//triggered when there is an event in the LogLin Dropdown -void Duration_AR(int n) { - // println("adjust duration to: " + w_analogRead.analogReadBars[i].adjustTimeAxis(n)); - //set analog read x axis to the duration selected from dropdown - settings.arHorizScaleSave = n; + public void setHorizontalScale(int n) { + widgetSettings.setByIndex(AnalogReadHorizontalScale.class, n); + int horizontalScaleValue = widgetSettings.get(AnalogReadHorizontalScale.class).getValue(); + applyHorizontalScale(horizontalScaleValue); + } - //Sync the duration of Time Series, Accelerometer, and Analog Read(Cyton Only) - for(int i = 0; i < w_analogRead.numAnalogReadBars; i++) { - if (n == 0) { - w_analogRead.analogReadBars[i].adjustTimeAxis(w_timeSeries.getTSHorizScale().getValue()); - } else { - w_analogRead.analogReadBars[i].adjustTimeAxis(w_analogRead.xLimOptions[n]); + private void applyVerticalScale(int value) { + for(int i = 0; i < analogReadBars.length; i++) { + analogReadBars[i].adjustVertScale(value); } } + + private void applyHorizontalScale(int value) { + for(int i = 0; i < analogReadBars.length; i++) { + analogReadBars[i].adjustTimeAxis(value); + } + } +}; + +public void analogReadVerticalScaleDropdown(int n) { + ((W_AnalogRead) widgetManager.getWidget("W_AnalogRead")).setVerticalScale(n); +} + +public void analogReadHorizontalScaleDropdown(int n) { + ((W_AnalogRead) widgetManager.getWidget("W_AnalogRead")).setHorizontalScale(n); } //======================================================================================================================== @@ -247,7 +236,7 @@ class AnalogReadBar{ private AnalogCapableBoard analogBoard; - AnalogReadBar(PApplet _parent, int _analogInputPin, int _x, int _y, int _w, int _h) { // channel number, x/y location, height, width + AnalogReadBar(PApplet _parentApplet, int _analogInputPin, int _x, int _y, int _w, int _h) { // channel number, x/y location, height, width analogInputPin = _analogInputPin; int digitalPinNum = 0; @@ -271,7 +260,7 @@ class AnalogReadBar{ h = _h; numSeconds = 20; - plot = new GPlot(_parent); + plot = new GPlot(_parentApplet); plot.setPos(x + 36 + 4, y); plot.setDim(w - 36 - 4, h); plot.setMar(0f, 0f, 0f, 0f); diff --git a/OpenBCI_GUI/W_BandPower.pde b/OpenBCI_GUI/W_BandPower.pde index 6bede4e4d..7043a04f6 100644 --- a/OpenBCI_GUI/W_BandPower.pde +++ b/OpenBCI_GUI/W_BandPower.pde @@ -10,133 +10,11 @@ // // // Created by: Wangshu Sun, May 2017 // // Modified by: Richard Waltman, March 2022 // +// Refactored by: Richard Waltman, April 2025 // // // //////////////////////////////////////////////////////////////////////////////////////////////////////// -public enum BPAutoClean implements IndexingInterface -{ - ON (0, "On"), - OFF (1, "Off"); - - private int index; - private String label; - private static BPAutoClean[] vals = values(); - - BPAutoClean(int _index, String _label) { - this.index = _index; - this.label = _label; - } - - @Override - public String getString() { - return label; - } - - @Override - public int getIndex() { - return index; - } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -public enum BPAutoCleanThreshold implements IndexingInterface -{ - FORTY (0, 40f, "40 uV"), - FIFTY (1, 50f, "50 uV"), - SIXTY (2, 60f, "60 uV"), - SEVENTY (3, 70f, "70 uV"), - EIGHTY (4, 80f, "80 uV"), - NINETY (5, 90f, "90 uV"), - ONE_HUNDRED(6, 100f, "100 uV"); - - private int index; - private float value; - private String label; - private static BPAutoCleanThreshold[] vals = values(); - - BPAutoCleanThreshold(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 (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -public enum BPAutoCleanTimer implements IndexingInterface -{ - HALF_SECOND (0, 500, ".5 sec"), - ONE_SECOND (1, 1000, "1 sec"), - THREE_SECONDS (2, 2000, "3 sec"), - FIVE_SECONDS (3, 5000, "5 sec"), - TEN_SECONDS (4, 10000, "10 sec"), - TWENTY_SECONDS (5, 20000, "20 sec"), - THIRTY_SECONDS(6, 30000, "30 sec"); - - private int index; - private float value; - private String label; - private static BPAutoCleanTimer[] vals = values(); - - BPAutoCleanTimer(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 (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -class W_BandPower extends Widget { - - // indexes +class W_BandPower extends WidgetWithSettings { private final int DELTA = 0; // 1-4 Hz private final int THETA = 1; // 4-8 Hz private final int ALPHA = 2; // 8-13 Hz @@ -151,44 +29,63 @@ class W_BandPower extends Widget { public ExGChannelSelect bpChanSelect; private boolean prevChanSelectIsVisible = false; - private List cp5ElementsToCheck = new ArrayList(); + private List cp5ElementsToCheck; - BPAutoClean bpAutoClean = BPAutoClean.OFF; - BPAutoCleanThreshold bpAutoCleanThreshold = BPAutoCleanThreshold.FIFTY; - BPAutoCleanTimer bpAutoCleanTimer = BPAutoCleanTimer.THREE_SECONDS; - int[] autoCleanTimers; - boolean[] previousThresholdCrossed; + W_BandPower() { + super(); + widgetTitle = "Band Power"; - W_BandPower(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + createPlot(); + } - autoCleanTimers = new int[currentBoard.getNumEXGChannels()]; - previousThresholdCrossed = new boolean[currentBoard.getNumEXGChannels()]; + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + widgetSettings.set(BPVerticalScale.class, BPVerticalScale.SCALE_100) + .set(GraphLogLin.class, GraphLogLin.LOG) + .set(FFTSmoothingFactor.class, globalFFTSettings.getSmoothingFactor()) + .set(FFTFilteredEnum.class, globalFFTSettings.getFilteredEnum()); + + initDropdown(BPVerticalScale.class, "bandPowerVerticalScaleDropdown", "Max uV"); + initDropdown(GraphLogLin.class, "bandPowerLogLinDropdown", "Log/Lin"); + initDropdown(FFTSmoothingFactor.class, "bandPowerSmoothingDropdown", "Smooth"); + initDropdown(FFTFilteredEnum.class, "bandPowerDataFilteringDropdown", "Filters"); - //Add channel select dropdown to this widget - bpChanSelect = new ExGChannelSelect(pApplet, x, y, w, navH); + bpChanSelect = new ExGChannelSelect(ourApplet, x, y, w, navH); bpChanSelect.activateAllButtons(); - + cp5ElementsToCheck = new ArrayList(); cp5ElementsToCheck.addAll(bpChanSelect.getCp5ElementsForOverlapCheck()); - - //Add settings dropdowns - //Note: This is the correct way to create a dropdown using an enum -RW - addDropdown("bpAutoCleanDropdown", "AutoClean", bpAutoClean.getEnumStringsAsList(), bpAutoClean.getIndex()); - addDropdown("bpAutoCleanThresholdDropdown", "Threshold", bpAutoCleanThreshold.getEnumStringsAsList(), bpAutoCleanThreshold.getIndex()); - addDropdown("bpAutoCleanTimerDropdown", "Timer", bpAutoCleanTimer.getEnumStringsAsList(), bpAutoCleanTimer.getIndex()); - //Note: This is a legacy way to create a dropdown which is sloppy and disorganized -RW - //These two dropdowns also have to mirror the settings in the FFT widget - addDropdown("Smoothing", "Smooth", Arrays.asList(settings.fftSmoothingArray), smoothFac_ind); //smoothFac_ind is a global variable at the top of W_HeadPlot.pde - addDropdown("UnfiltFilt", "Filters?", Arrays.asList(settings.fftFilterArray), settings.fftFilterSave); + saveActiveChannels(bpChanSelect.getActiveChannels()); + widgetSettings.saveDefaults(); + } + + @Override + protected void applySettings() { + updateDropdownLabel(BPVerticalScale.class, "bandPowerVerticalScaleDropdown"); + updateDropdownLabel(GraphLogLin.class, "bandPowerLogLinDropdown"); + updateDropdownLabel(FFTSmoothingFactor.class, "bandPowerSmoothingDropdown"); + updateDropdownLabel(FFTFilteredEnum.class, "bandPowerDataFilteringDropdown"); + applyActiveChannels(bpChanSelect); + applyVerticalScale(); + applyPlotLogScale(); + } + + @Override + protected void updateChannelSettings() { + if (bpChanSelect != null) { + saveActiveChannels(bpChanSelect.getActiveChannels()); + } + } + private void createPlot() { // Setup for the BandPower plot - bp_plot = new GPlot(_parent, x, y-navHeight, w, h+navHeight); - // bp_plot.setPos(x, y+navHeight); + bp_plot = new GPlot(ourApplet, x, y-NAV_HEIGHT, w, h+NAV_HEIGHT); + // bp_plot.setPos(x, y+NAV_HEIGHT); bp_plot.setDim(w, h); bp_plot.setLogScale("y"); - bp_plot.setYLim(0.1, 100); + bp_plot.setYLim(0.1, 100); // Lower limit must be > 0 for log scale bp_plot.setXLim(0, 5); - bp_plot.getYAxis().setNTicks(9); + bp_plot.getYAxis().setNTicks(4); bp_plot.getXAxis().setNTicks(0); bp_plot.getTitle().setTextAlignment(LEFT); bp_plot.getTitle().setRelativePos(0); @@ -221,13 +118,11 @@ class W_BandPower extends Widget { ); //setting color of text label for each histogram bar on the x axis bp_plot.getHistogram().setFontColor(OPENBCI_DARKBLUE); + applyPlotLogScale(); } public void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - - // If enabled, automatically turn channels on or off in ExGChannelSelect for this widget - autoCleanByEnableDisableChannels(); + super.update(); //Update channel checkboxes and active channels bpChanSelect.update(x, y, w); @@ -252,7 +147,7 @@ class W_BandPower extends Widget { } public void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); pushStyle(); //remember to refer to x,y,w,h which are the positioning variables of the Widget class @@ -268,22 +163,22 @@ class W_BandPower extends Widget { //for this widget need to redraw the grey bar, bc the FFT plot covers it up... fill(200, 200, 200); - rect(x, y - navHeight, w, navHeight); //button bar + rect(x, y - NAV_HEIGHT, w, NAV_HEIGHT); //button bar popStyle(); bpChanSelect.draw(); } public void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); flexGPlotSizeAndPosition(); - bpChanSelect.screenResized(pApplet); + bpChanSelect.screenResized(ourApplet); } public void mousePressed() { - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); bpChanSelect.mousePressed(this.dropdownIsActive); //Calls channel select mousePressed and checks if clicked } @@ -301,72 +196,17 @@ class W_BandPower extends Widget { return normalizedBandPowers; } - private void autoCleanByEnableDisableChannels() { - if (bpAutoClean == BPAutoClean.OFF) { - return; - } - - int numChannels = currentBoard.getNumEXGChannels(); - for (int i = 0; i < numChannels; i++) { - float uvrms = dataProcessing.data_std_uV[i]; - boolean thresholdCrossed = uvrms > bpAutoCleanThreshold.getValue(); - - int currentMillis = millis(); - - //Check for state change. Reset timer on either state. - if (thresholdCrossed != previousThresholdCrossed[i]) { - previousThresholdCrossed[i] = thresholdCrossed; - autoCleanTimers[i] = currentMillis; - } - - //Auto-disable a channel if it's above the threshold and has been for the timer duration - boolean timerDurationExceeded = currentMillis - autoCleanTimers[i] > bpAutoCleanTimer.getValue(); - if (timerDurationExceeded) { - boolean enableChannel = !thresholdCrossed; - bpChanSelect.setToggleState(i, enableChannel); - } - } - } - - public BPAutoClean getAutoClean() { - return bpAutoClean; - } - - public BPAutoCleanThreshold getAutoCleanThreshold() { - return bpAutoCleanThreshold; - } - - public BPAutoCleanTimer getAutoCleanTimer() { - return bpAutoCleanTimer; - } - - public void setAutoClean(int n) { - bpAutoClean = bpAutoClean.values()[n]; - Arrays.fill(previousThresholdCrossed, false); - Arrays.fill(autoCleanTimers, 0); - } - - public void setAutoCleanThreshold(int n) { - bpAutoCleanThreshold = bpAutoCleanThreshold.values()[n]; - } - - public void setAutoCleanTimer(int n) { - bpAutoCleanTimer = bpAutoCleanTimer.values()[n]; - } //Called in DataProcessing.pde to update data even if widget is closed public void updateBandPowerWidgetData() { float normalizingSum = 0; for (int i = 0; i < NUM_BANDS; i++) { float sum = 0; - for (int j = 0; j < bpChanSelect.getActiveChannels().size(); j++) { int chan = bpChanSelect.getActiveChannels().get(j); sum += dataProcessing.avgPowerInBins[chan][i]; } - activePower[i] = sum / bpChanSelect.getActiveChannels().size(); - normalizingSum += activePower[i]; } @@ -374,16 +214,61 @@ class W_BandPower extends Widget { normalizedBandPowers[i] = activePower[i] / normalizingSum; } } + + public void setVerticalScale(int n) { + widgetSettings.setByIndex(BPVerticalScale.class, n); + applyVerticalScale(); + } + + public void setLogLin(int n) { + widgetSettings.setByIndex(GraphLogLin.class, n); + applyPlotLogScale(); + } + + private void applyVerticalScale() { + BPVerticalScale scale = widgetSettings.get(BPVerticalScale.class); + int scaleValue = scale.getValue(); + bp_plot.setYLim(0.1, scaleValue); // Lower limit must be > 0 for log scale + } + + private void applyPlotLogScale() { + GraphLogLin logLin = widgetSettings.get(GraphLogLin.class); + if (logLin == GraphLogLin.LOG) { + bp_plot.setLogScale("y"); + } else { + bp_plot.setLogScale(""); + } + } + + public void setSmoothingDropdownFrontend(FFTSmoothingFactor _smoothingFactor) { + widgetSettings.set(FFTSmoothingFactor.class, _smoothingFactor); + updateDropdownLabel(FFTSmoothingFactor.class, "bandPowerSmoothingDropdown"); + } + + public void setFilteringDropdownFrontend(FFTFilteredEnum _filteredEnum) { + widgetSettings.set(FFTFilteredEnum.class, _filteredEnum); + updateDropdownLabel(FFTFilteredEnum.class, "bandPowerDataFilteringDropdown"); + } }; -public void bpAutoCleanDropdown(int n) { - w_bandPower.setAutoClean(n); +public void bandPowerVerticalScaleDropdown(int n) { + ((W_BandPower) widgetManager.getWidget("W_BandPower")).setVerticalScale(n); } -public void bpAutoCleanThresholdDropdown(int n) { - w_bandPower.setAutoCleanThreshold(n); +public void bandPowerLogLinDropdown(int n) { + ((W_BandPower) widgetManager.getWidget("W_BandPower")).setLogLin(n); } -public void bpAutoCleanTimerDropdown(int n) { - w_bandPower.setAutoCleanTimer(n); +public void bandPowerSmoothingDropdown(int n) { + globalFFTSettings.setSmoothingFactor(FFTSmoothingFactor.values()[n]); + FFTSmoothingFactor smoothingFactor = globalFFTSettings.getSmoothingFactor(); + ((W_BandPower) widgetManager.getWidget("W_BandPower")).setSmoothingDropdownFrontend(smoothingFactor); + ((W_Fft) widgetManager.getWidget("W_Fft")).setSmoothingDropdownFrontend(smoothingFactor); } + +public void bandPowerDataFilteringDropdown(int n) { + globalFFTSettings.setFilteredEnum(FFTFilteredEnum.values()[n]); + FFTFilteredEnum filteredEnum = globalFFTSettings.getFilteredEnum(); + ((W_BandPower) widgetManager.getWidget("W_BandPower")).setFilteringDropdownFrontend(filteredEnum); + ((W_Fft) widgetManager.getWidget("W_Fft")).setFilteringDropdownFrontend(filteredEnum); +} \ No newline at end of file diff --git a/OpenBCI_GUI/W_CytonImpedance.pde b/OpenBCI_GUI/W_CytonImpedance.pde index f6df10ad1..20843268e 100644 --- a/OpenBCI_GUI/W_CytonImpedance.pde +++ b/OpenBCI_GUI/W_CytonImpedance.pde @@ -48,7 +48,7 @@ class W_CytonImpedance extends Widget { private int prevMasterCheckCounter = -1; private int numElectrodesToMasterCheck = 0; private int prevMasterCheckMillis = 0; //Used for simple timer - public boolean isCheckingImpedanceOnAnything = false; //This is more reliable than waiting to see if the Board is checking impedance + private boolean isCheckingImpedanceOnAnything = false; //This is more reliable than waiting to see if the Board is checking impedance private SignalCheckThresholdUI errorThreshold; private SignalCheckThresholdUI warningThreshold; @@ -56,8 +56,9 @@ class W_CytonImpedance extends Widget { private int thresholdTFWidth = 60; //Hard-code this value since there are deep errors with controlp5.textfield.setSize() and creating new graphics in this class - RW 12/13/2021 - W_CytonImpedance(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_CytonImpedance() { + super(); + widgetTitle = "Cyton Signal"; cytonBoard = (BoardCyton) currentBoard; @@ -69,10 +70,14 @@ class W_CytonImpedance extends Widget { threshold_ui_cp5.setGraphics(ourApplet, 0,0); threshold_ui_cp5.setAutoDraw(false); - addDropdown("CytonImpedance_MasterCheckInterval", "Interval", masterCheckInterval.getEnumStringsAsList(), masterCheckInterval.getIndex()); + List intervalList = EnumHelper.getEnumStrings(CytonImpedanceInterval.class); + List labelList = EnumHelper.getEnumStrings(CytonImpedanceLabels.class); + List modeList = EnumHelper.getEnumStrings(CytonSignalCheckMode.class); + + addDropdown("CytonImpedance_MasterCheckInterval", "Interval", intervalList, masterCheckInterval.getIndex()); dropdownWidth = 85; //Override the widget header dropdown width to fit "impedance" mode - addDropdown("CytonImpedance_LabelMode", "Labels", labelMode.getEnumStringsAsList(), labelMode.getIndex()); - addDropdown("CytonImpedance_Mode", "Mode", signalCheckMode.getEnumStringsAsList(), signalCheckMode.getIndex()); + addDropdown("CytonImpedance_LabelMode", "Labels", labelList, labelMode.getIndex()); + addDropdown("CytonImpedance_Mode", "Mode", modeList, signalCheckMode.getIndex()); footerHeight = navH/2; @@ -94,14 +99,14 @@ class W_CytonImpedance extends Widget { //Init the electrode map and fill and create signal check buttons initCytonImpedanceMap(); - cytonResetAllChannels = createCytonResetChannelsButton("cytonResetAllChannels", "Reset Channels", (int)(x0 + 1), (int)(y0 + navHeight + 1), 90, navHeight - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); - cytonImpedanceMasterCheck = createCytonImpMasterCheckButton("cytonImpedanceMasterCheck", "Check All Channels", (int)(x0 + 1 + padding_3 + 90), (int)(y0 + navHeight + 1), 120, navHeight - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + cytonResetAllChannels = createCytonResetChannelsButton("cytonResetAllChannels", "Reset Channels", (int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1), 90, NAV_HEIGHT - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + cytonImpedanceMasterCheck = createCytonImpMasterCheckButton("cytonImpedanceMasterCheck", "Check All Channels", (int)(x0 + 1 + padding_3 + 90), (int)(y0 + NAV_HEIGHT + 1), 120, NAV_HEIGHT - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); errorThreshold = new SignalCheckThresholdUI(threshold_ui_cp5, "errorThreshold", x + tableWidth + padding, y + h - navH, thresholdTFWidth, thresholdTFHeight, SIGNAL_CHECK_RED, signalCheckMode); warningThreshold = new SignalCheckThresholdUI(threshold_ui_cp5, "warningThreshold", x + tableWidth + padding, y + h - navH/2, thresholdTFWidth, thresholdTFHeight, SIGNAL_CHECK_YELLOW, signalCheckMode); } public void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); if (is_railed == null) { return; @@ -131,7 +136,7 @@ class W_CytonImpedance extends Widget { } public void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); dataGrid.draw(); @@ -176,7 +181,7 @@ class W_CytonImpedance extends Widget { } public void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); int overrideDropdownWidth = 64; cp5_widget.get(ScrollableList.class, "CytonImpedance_MasterCheckInterval").setWidth(overrideDropdownWidth); @@ -184,11 +189,11 @@ class W_CytonImpedance extends Widget { //**IMPORTANT FOR CP5**// //This makes the cp5 objects within the widget scale properly - imp_buttons_cp5.setGraphics(pApplet, 0, 0); - threshold_ui_cp5.setGraphics(pApplet, 0, 0); + imp_buttons_cp5.setGraphics(ourApplet, 0, 0); + threshold_ui_cp5.setGraphics(ourApplet, 0, 0); - cytonResetAllChannels.setPosition((int)(x0 + 1), (int)(y0 + navHeight + 1)); - cytonImpedanceMasterCheck.setPosition((int)(x0 + 1 + padding_3 + 90), (int)(y0 + navHeight + 1)); + cytonResetAllChannels.setPosition((int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1)); + cytonImpedanceMasterCheck.setPosition((int)(x0 + 1 + padding_3 + 90), (int)(y0 + NAV_HEIGHT + 1)); resizeTable(); @@ -242,11 +247,11 @@ class W_CytonImpedance extends Widget { } public void mousePressed(){ - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); } public void mouseReleased(){ - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + super.mouseReleased(); } private void initCytonImpedanceMap() { @@ -297,8 +302,9 @@ class W_CytonImpedance extends Widget { cytonImpedanceMasterCheck.setOff(); } else if (signalCheckMode == CytonSignalCheckMode.IMPEDANCE) { //Attempt to close Hardware Settings view. Also, throws a popup if there are unsent changes. - if (w_timeSeries.getAdsSettingsVisible()) { - w_timeSeries.closeADSSettings(); + W_TimeSeries timeSeriesWidget = widgetManager.getTimeSeriesWidget(); + if (timeSeriesWidget.getAdsSettingsVisible()) { + timeSeriesWidget.closeADSSettings(); } //Clear the cells and show buttons instead for (int i = 1; i < numTableRows; i++) { @@ -512,8 +518,9 @@ class W_CytonImpedance extends Widget { && checkingOtherChan_isNpin.equals(e.getIsNPin())) { //println("TOGGLE OFF", e.getGUIChannelNumber(), e.getIsNPin(), "TOGGLE TO ==", false); e.overrideTestingButtonSwitch(false); - w_timeSeries.adsSettingsController.updateChanSettingsDropdowns(checkingOtherChan-1, cytonBoard.isEXGChannelActive(checkingOtherChan-1)); - w_timeSeries.adsSettingsController.setHasUnappliedSettings(checkingOtherChan-1, false); + W_TimeSeries timeSeriesWidget = widgetManager.getTimeSeriesWidget(); + timeSeriesWidget.adsSettingsController.updateChanSettingsDropdowns(checkingOtherChan-1, cytonBoard.isEXGChannelActive(checkingOtherChan-1)); + timeSeriesWidget.adsSettingsController.setHasUnappliedSettings(checkingOtherChan-1, false); } } @@ -532,8 +539,9 @@ class W_CytonImpedance extends Widget { cytonImpedanceMasterCheck.setOff(); } else { //If successful, update the front end components to reflect the new state - w_timeSeries.adsSettingsController.updateChanSettingsDropdowns(checkingChanX, cytonBoard.isEXGChannelActive(checkingChanX)); - w_timeSeries.adsSettingsController.setHasUnappliedSettings(checkingChanX, false); + W_TimeSeries timeSeriesWidget = widgetManager.getTimeSeriesWidget(); + timeSeriesWidget.adsSettingsController.updateChanSettingsDropdowns(checkingChanX, cytonBoard.isEXGChannelActive(checkingChanX)); + timeSeriesWidget.adsSettingsController.setHasUnappliedSettings(checkingChanX, false); } boolean shouldBeOn = toggle && response; @@ -685,7 +693,7 @@ class W_CytonImpedance extends Widget { // Update ADS1299 settings to default but don't commit. Instead, sent "d" command twice. cytonBoard.getADS1299Settings().revertAllChannelsToDefaultValues(); - w_timeSeries.adsSettingsController.updateAllChanSettingsDropdowns(); + widgetManager.getTimeSeriesWidget().adsSettingsController.updateAllChanSettingsDropdowns(); timeElapsed = millis() - timeElapsed; StringBuilder sb = new StringBuilder("Cyton Impedance Check: Hard reset to default board mode took -- "); @@ -730,18 +738,22 @@ class W_CytonImpedance extends Widget { cytonElectrodeStatus[i].updateYellowThreshold(_d); } } + + public boolean getIsCheckingImpedanceOnAnything() { + return isCheckingImpedanceOnAnything; + } }; //These functions need to be global! These functions are activated when an item from the corresponding dropdown is selected //Update: It's not worth the trouble to implement a callback listener in the widget for this specifc kind of dropdown. Keep using this pattern for widget Nav dropdowns. - February 2021 RW void CytonImpedance_Mode(int n) { - w_cytonImpedance.setSignalCheckMode(n); + ((W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance")).setSignalCheckMode(n); } void CytonImpedance_LabelMode(int n) { - w_cytonImpedance.setShowAnatomicalName(n); + ((W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance")).setShowAnatomicalName(n); } void CytonImpedance_MasterCheckInterval(int n) { - w_cytonImpedance.setMasterCheckInterval(n); + ((W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance")).setMasterCheckInterval(n); } \ No newline at end of file diff --git a/OpenBCI_GUI/W_DigitalRead.pde b/OpenBCI_GUI/W_DigitalRead.pde index 69ea7f653..4e6272427 100644 --- a/OpenBCI_GUI/W_DigitalRead.pde +++ b/OpenBCI_GUI/W_DigitalRead.pde @@ -10,37 +10,31 @@ class W_DigitalRead extends Widget { private int numDigitalReadDots; - float xF, yF, wF, hF; - int dot_padding; - //values for actual time series chart (rectangle encompassing all digitalReadDots) - float dot_x, dot_y, dot_h, dot_w; - float plotBottomWell; - float playbackWidgetHeight; - int digitalReaddotHeight; + private int dot_padding; + private float dot_x, dot_y, dot_h, dot_w; + private float plotBottomWell; + private float playbackWidgetHeight; + private int digitalReaddotHeight; - DigitalReadDot[] digitalReadDots; + private DigitalReadDot[] digitalReadDots; private Button digitalModeButton; private DigitalCapableBoard digitalBoard; - W_DigitalRead(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_DigitalRead() { + super(); + widgetTitle = "Digital Read"; digitalBoard = (DigitalCapableBoard)currentBoard; numDigitalReadDots = 5; - xF = float(x); //float(int( ... is a shortcut for rounding the float down... so that it doesn't creep into the 1px margin - yF = float(y); - wF = float(w); - hF = float(h); - dot_padding = 10; - dot_x = xF + dot_padding; - dot_y = yF + (dot_padding); - dot_w = wF - dot_padding*2; - dot_h = hF - playbackWidgetHeight - plotBottomWell - (dot_padding*2); + dot_x = float(x) + dot_padding; + dot_y = float(y) + (dot_padding); + dot_w = float(w) - dot_padding*2; + dot_h = float(h) - playbackWidgetHeight - plotBottomWell - (dot_padding*2); digitalReaddotHeight = int(dot_h/numDigitalReadDots); digitalReadDots = new DigitalReadDot[numDigitalReadDots]; @@ -61,11 +55,11 @@ class W_DigitalRead extends Widget { } else { digitalPin = 18; } - DigitalReadDot tempDot = new DigitalReadDot(_parent, digitalPin, digitalReaddotX, digitalReaddotY, int(dot_w), digitalReaddotHeight, dot_padding); + DigitalReadDot tempDot = new DigitalReadDot(ourApplet, digitalPin, digitalReaddotX, digitalReaddotY, int(dot_w), digitalReaddotHeight, dot_padding); digitalReadDots[i] = tempDot; } - createDigitalModeButton("digitalModeButton", "Turn Digital Read On", (int)(x0 + 1), (int)(y0 + navHeight + 1), 128, navHeight - 3, p5, 12, buttonsLightBlue, WHITE); + createDigitalModeButton("digitalModeButton", "Turn Digital Read On", (int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1), 128, NAV_HEIGHT - 3, p5, 12, buttonsLightBlue, WHITE); } public int getNumDigitalReads() { @@ -73,7 +67,7 @@ class W_DigitalRead extends Widget { } public void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); if (currentBoard instanceof DataSourcePlayback) { if (((DataSourcePlayback)currentBoard) instanceof DigitalCapableBoard @@ -100,7 +94,7 @@ class W_DigitalRead extends Widget { } public void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); //draw channel bars if (digitalBoard.isDigitalActive()) { @@ -111,42 +105,37 @@ class W_DigitalRead extends Widget { } public void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) - - xF = float(x); //float(int( ... is a shortcut for rounding the float down... so that it doesn't creep into the 1px margin - yF = float(y); - wF = float(w); - hF = float(h); + super.screenResized(); - if (wF > hF) { - digitalReaddotHeight = int(hF/(numDigitalReadDots+1)); + if (w > h) { + digitalReaddotHeight = int(h/(numDigitalReadDots+1)); } else { - digitalReaddotHeight = int(wF/(numDigitalReadDots+1)); + digitalReaddotHeight = int(w/(numDigitalReadDots+1)); } if (numDigitalReadDots == 3) { - digitalReadDots[0].screenResized(x+int(wF*(1.0/3.0)), y+int(hF*(1.0/3.0)), digitalReaddotHeight, digitalReaddotHeight); //bar x, bar y, bar w, bar h - digitalReadDots[1].screenResized(x+int(wF/2), y+int(hF/2), digitalReaddotHeight, digitalReaddotHeight); //bar x, bar y, bar w, bar h - digitalReadDots[2].screenResized(x+int(wF*(2.0/3.0)), y+int(hF*(2.0/3.0)), digitalReaddotHeight, digitalReaddotHeight); //bar x, bar y, bar w, bar h + digitalReadDots[0].screenResized(x+int(w*(1.0/3.0)), y+int(h*(1.0/3.0)), digitalReaddotHeight, digitalReaddotHeight); //bar x, bar y, bar w, bar h + digitalReadDots[1].screenResized(x+int(w/2), y+int(h/2), digitalReaddotHeight, digitalReaddotHeight); //bar x, bar y, bar w, bar h + digitalReadDots[2].screenResized(x+int(w*(2.0/3.0)), y+int(h*(2.0/3.0)), digitalReaddotHeight, digitalReaddotHeight); //bar x, bar y, bar w, bar h } else { int y_pad = y + dot_padding; - digitalReadDots[0].screenResized(x+int(wF*(1.0/8.0)), y_pad+int(hF*(1.0/8.0)), digitalReaddotHeight, digitalReaddotHeight); - digitalReadDots[2].screenResized(x+int(wF/2), y_pad+int(hF/2), digitalReaddotHeight, digitalReaddotHeight); - digitalReadDots[4].screenResized(x+int(wF*(7.0/8.0)), y_pad+int(hF*(7.0/8.0)), digitalReaddotHeight, digitalReaddotHeight); - digitalReadDots[1].screenResized(digitalReadDots[0].dotX+int(wF*(3.0/16.0)), digitalReadDots[0].dotY+int(hF*(3.0/16.0)), digitalReaddotHeight, digitalReaddotHeight); - digitalReadDots[3].screenResized(digitalReadDots[2].dotX+int(wF*(3.0/16.0)), digitalReadDots[2].dotY+int(hF*(3.0/16.0)), digitalReaddotHeight, digitalReaddotHeight); + digitalReadDots[0].screenResized(x+int(w*(1.0/8.0)), y_pad+int(h*(1.0/8.0)), digitalReaddotHeight, digitalReaddotHeight); + digitalReadDots[2].screenResized(x+int(w/2), y_pad+int(h/2), digitalReaddotHeight, digitalReaddotHeight); + digitalReadDots[4].screenResized(x+int(w*(7.0/8.0)), y_pad+int(h*(7.0/8.0)), digitalReaddotHeight, digitalReaddotHeight); + digitalReadDots[1].screenResized(digitalReadDots[0].dotX+int(w*(3.0/16.0)), digitalReadDots[0].dotY+int(h*(3.0/16.0)), digitalReaddotHeight, digitalReaddotHeight); + digitalReadDots[3].screenResized(digitalReadDots[2].dotX+int(w*(3.0/16.0)), digitalReadDots[2].dotY+int(h*(3.0/16.0)), digitalReaddotHeight, digitalReaddotHeight); } - digitalModeButton.setPosition((int)(x0 + 1), (int)(y0 + navHeight + 1)); + digitalModeButton.setPosition((int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1)); } public void mousePressed() { - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); } public void mouseReleased() { - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + super.mouseReleased(); } private void createDigitalModeButton(String name, String text, int _x, int _y, int _w, int _h, PFont _font, int _fontSize, color _bg, color _textColor) { @@ -158,16 +147,16 @@ class W_DigitalRead extends Widget { digitalBoard.setDigitalActive(true); digitalModeButton.getCaptionLabel().setText("Turn Digital Read Off"); output("Starting to read digital inputs on pin marked D11, D12, D13, D17 and D18"); - w_accelerometer.accelBoardSetActive(false); - w_analogRead.toggleAnalogReadButton(false); - w_pulseSensor.toggleAnalogReadButton(false); + ((W_Accelerometer) widgetManager.getWidget("W_Accelerometer")).accelBoardSetActive(false); + ((W_AnalogRead) widgetManager.getWidget("W_AnalogRead")).toggleAnalogReadButton(false); + ((W_PulseSensor) widgetManager.getWidget("W_Accelerometer")).toggleAnalogReadButton(false); } else { digitalBoard.setDigitalActive(false); digitalModeButton.getCaptionLabel().setText("Turn Digital Read On"); output("Starting to read accelerometer"); - w_accelerometer.accelBoardSetActive(true); - w_analogRead.toggleAnalogReadButton(false); - w_pulseSensor.toggleAnalogReadButton(false); + ((W_Accelerometer) widgetManager.getWidget("W_Accelerometer")).accelBoardSetActive(true); + ((W_AnalogRead) widgetManager.getWidget("W_AnalogRead")).toggleAnalogReadButton(false); + ((W_PulseSensor) widgetManager.getWidget("W_Accelerometer")).toggleAnalogReadButton(false); } } }); @@ -217,7 +206,7 @@ class DigitalReadDot{ DigitalCapableBoard digitalBoard; - DigitalReadDot(PApplet _parent, int _digitalInputPin, int _x, int _y, int _w, int _h, int _padding) { // channel number, x/y location, height, width + DigitalReadDot(PApplet _parentApplet, int _digitalInputPin, int _x, int _y, int _w, int _h, int _padding) { // channel number, x/y location, height, width digitalBoard = (DigitalCapableBoard)currentBoard; diff --git a/OpenBCI_GUI/W_EMG.pde b/OpenBCI_GUI/W_EMG.pde index 905898ef4..f53a6bc82 100644 --- a/OpenBCI_GUI/W_EMG.pde +++ b/OpenBCI_GUI/W_EMG.pde @@ -13,27 +13,16 @@ // TODO: Add dynamic threshold functionality //////////////////////////////////////////////////////////////////////////////// -class W_emg extends Widget { - PApplet parent; - +class W_Emg extends WidgetWithSettings { private ControlP5 emgCp5; private Button emgSettingsButton; private final int EMG_SETTINGS_BUTTON_WIDTH = 125; private List cp5ElementsToCheck; - public ExGChannelSelect emgChannelSelect; - W_emg (PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - parent = _parent; - - cp5ElementsToCheck = new ArrayList(); - - //Add channel select dropdown to this widget - emgChannelSelect = new ExGChannelSelect(pApplet, x, y, w, navH); - emgChannelSelect.activateAllButtons(); - - cp5ElementsToCheck.addAll(emgChannelSelect.getCp5ElementsForOverlapCheck()); + W_Emg () { + super(); + widgetTitle = "EMG"; emgCp5 = new ControlP5(ourApplet); emgCp5.setGraphics(ourApplet, 0,0); @@ -43,8 +32,31 @@ class W_emg extends Widget { cp5ElementsToCheck.add((controlP5.Controller) emgSettingsButton); } + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + emgChannelSelect = new ExGChannelSelect(ourApplet, x, y, w, navH); + emgChannelSelect.activateAllButtons(); + cp5ElementsToCheck = new ArrayList(); + cp5ElementsToCheck.addAll(emgChannelSelect.getCp5ElementsForOverlapCheck()); + saveActiveChannels(emgChannelSelect.getActiveChannels()); + widgetSettings.saveDefaults(); + } + + @Override + protected void applySettings() { + applyActiveChannels(emgChannelSelect); + } + + @Override + protected void updateChannelSettings() { + if (emgChannelSelect != null) { + saveActiveChannels(emgChannelSelect.getActiveChannels()); + } + } + public void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); lockElementsOnOverlapCheck(cp5ElementsToCheck); //Update channel checkboxes and active channels @@ -60,7 +72,7 @@ class W_emg extends Widget { } public void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); drawEmgVisualizations(); @@ -71,14 +83,14 @@ class W_emg extends Widget { } public void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); emgCp5.setGraphics(ourApplet, 0, 0); emgSettingsButton.setPosition(x0 + w - EMG_SETTINGS_BUTTON_WIDTH - 2, y0 + navH + 1); - emgChannelSelect.screenResized(pApplet); + emgChannelSelect.screenResized(ourApplet); } public void mousePressed() { - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); //Calls channel select mousePressed and checks if clicked emgChannelSelect.mousePressed(this.dropdownIsActive); } diff --git a/OpenBCI_GUI/W_EMGJoystick.pde b/OpenBCI_GUI/W_EMGJoystick.pde index dabef103f..203d1e8ba 100644 --- a/OpenBCI_GUI/W_EMGJoystick.pde +++ b/OpenBCI_GUI/W_EMGJoystick.pde @@ -9,8 +9,7 @@ // // ///////////////////////////////////////////////////////////////////////////////////////////////////////// -class W_EMGJoystick extends Widget { - +class W_EmgJoystick extends WidgetWithSettings { private ControlP5 emgCp5; private Button emgSettingsButton; private List cp5ElementsToCheck; @@ -47,8 +46,6 @@ class W_EMGJoystick extends Widget { private String[] plotChannelLabels = new String[NUM_EMG_INPUTS]; - public EmgJoystickSmoothing joystickSmoothing = EmgJoystickSmoothing.POINT_9; - private int DROPDOWN_HEIGHT = navH - 4; private int DROPDOWN_WIDTH = 80; private int DROPDOWN_SPACER = 10; @@ -71,8 +68,9 @@ class W_EMGJoystick extends Widget { private PImage yPositiveInputLabelImage = loadImage("EMG_Joystick/UP_100x100.png"); private PImage yNegativeInputLabelImage = loadImage("EMG_Joystick/DOWN_100x100.png"); - W_EMGJoystick(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_EmgJoystick() { + super(); + widgetTitle = "EMG Joystick"; emgCp5 = new ControlP5(ourApplet); emgCp5.setGraphics(ourApplet, 0,0); @@ -96,18 +94,29 @@ class W_EMGJoystick extends Widget { plotChannelLabels[i] = Integer.toString(emgJoystickInputs.getInput(i).getIndex() + 1); } - addDropdown("emgJoystickSmoothingDropdown", "Smoothing", joystickSmoothing.getEnumStringsAsList(), joystickSmoothing.getIndex()); - createInputDropdowns(); } + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + widgetSettings.set(EmgJoystickSmoothing.class, EmgJoystickSmoothing.POINT_9); + initDropdown(EmgJoystickSmoothing.class, "emgJoystickSmoothingDropdown", "Smoothing"); + widgetSettings.saveDefaults(); + } + + @Override + protected void applySettings() { + updateDropdownLabel(EmgJoystickSmoothing.class, "emgJoystickSmoothingDropdown"); + } + public void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); lockElementsOnOverlapCheck(cp5ElementsToCheck); } public void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); drawJoystickXYGraph(); @@ -122,7 +131,7 @@ class W_EMGJoystick extends Widget { } public void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); emgCp5.setGraphics(ourApplet, 0, 0); emgSettingsButton.setPosition(x0 + 1, y0 + navH + 1); @@ -232,7 +241,7 @@ class W_EMGJoystick extends Widget { joystickRawX = unitCircleXY[0]; joystickRawY = unitCircleXY[1]; //Lerp the joystick values to smooth them out - float amount = 1.0f - joystickSmoothing.getValue(); + float amount = 1.0f - widgetSettings.get(EmgJoystickSmoothing.class).getValue(); joystickRawX = lerp(previousJoystickRawX, joystickRawX, amount); joystickRawY = lerp(previousJoystickRawY, joystickRawY, amount); } @@ -314,7 +323,7 @@ class W_EMGJoystick extends Widget { } public void setJoystickSmoothing(int n) { - joystickSmoothing = joystickSmoothing.values()[n]; + widgetSettings.setByIndex(EmgJoystickSmoothing.class, n); } private void createEmgSettingsButton() { @@ -461,116 +470,5 @@ class W_EMGJoystick extends Widget { }; public void emgJoystickSmoothingDropdown(int n) { - w_emgJoystick.setJoystickSmoothing(n); -} - -public enum EmgJoystickSmoothing implements IndexingInterface -{ - OFF (0, "Off", 0f), - POINT_9 (1, "0.9", .9f), - POINT_95 (2, "0.95", .95f), - POINT_98 (3, "0.98", .98f), - POINT_99 (4, "0.99", .99f), - POINT_999 (5, "0.999", .999f), - POINT_9999 (6, "0.9999", .9999f); - - private int index; - private String name; - private float value; - private static EmgJoystickSmoothing[] vals = values(); - - EmgJoystickSmoothing(int index, String name, float value) { - this.index = index; - this.name = name; - this.value = value; - } - - public int getIndex() { - return index; - } - - public String getString() { - return name; - } - - public float getValue() { - return value; - } - - private static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -public class EMGJoystickInput { - private int index; - private String name; - private int value; - - EMGJoystickInput(int index, String name, int value) { - this.index = index; - this.name = name; - this.value = value; - } - - public int getIndex() { - return index; - } - - public String getString() { - return name; - } - - public int getValue() { - return value; - } -} - -public class EMGJoystickInputs { - private final int NUM_EMG_INPUTS = 4; - private final EMGJoystickInput[] VALUES; - private final EMGJoystickInput[] INPUTS = new EMGJoystickInput[NUM_EMG_INPUTS]; - - EMGJoystickInputs(int numExGChannels) { - VALUES = new EMGJoystickInput[numExGChannels]; - for (int i = 0; i < numExGChannels; i++) { - VALUES[i] = new EMGJoystickInput(i, "Channel " + (i + 1), i); - } - } - - public EMGJoystickInput[] getValues() { - return VALUES; - } - - public EMGJoystickInput[] getInputs() { - return INPUTS; - } - - public EMGJoystickInput getInput(int index) { - return INPUTS[index]; - } - - public void setInputToChannel(int inputNumber, int channel) { - if (inputNumber < 0 || inputNumber >= NUM_EMG_INPUTS) { - println("Invalid input number: " + inputNumber); - return; - } - if (channel < 0 || channel >= VALUES.length) { - println("Invalid channel: " + channel); - return; - } - INPUTS[inputNumber] = VALUES[channel]; - } - - public List getValueStringsAsList() { - List enumStrings = new ArrayList(); - for (EMGJoystickInput val : VALUES) { - enumStrings.add(val.getString()); - } - return enumStrings; - } + ((W_EmgJoystick) widgetManager.getWidget("W_EmgJoystick")).setJoystickSmoothing(n); } \ No newline at end of file diff --git a/OpenBCI_GUI/W_FFT.pde b/OpenBCI_GUI/W_FFT.pde index d564f8fee..fdf7ba292 100644 --- a/OpenBCI_GUI/W_FFT.pde +++ b/OpenBCI_GUI/W_FFT.pde @@ -5,113 +5,136 @@ // It extends the Widget class // // Conor Russomanno, November 2016 +// Refactored: Richard Waltman, March 2025 // // Requires the plotting library from grafica ... // replacing the old gwoptics (which is now no longer supported) // /////////////////////////////////////////////////// -class W_fft extends Widget { - +class W_Fft extends WidgetWithSettings { public ExGChannelSelect fftChanSelect; - boolean prevChanSelectIsVisible = false; - - GPlot fft_plot; //create an fft plot for each active channel - GPointsArray[] fft_points; + private boolean prevChanSelectIsVisible = false; - int[] xLimOptions = {20, 40, 60, 100, 120, 250, 500, 800}; - int[] yLimOptions = {10, 50, 100, 1000}; + private GPlot fftPlot; //create an fft plot for each active channel + private GPointsArray[] fftGplotPoints; - int xLim = xLimOptions[2]; //maximum value of x axis ... in this case 20 Hz, 40 Hz, 60 Hz, 120 Hz - int xMax = xLimOptions[xLimOptions.length-1]; //maximum possible frequency in FFT - int FFT_indexLim = int(1.0*xMax*(getNfftSafe()/currentBoard.getSampleRate())); // maxim value of FFT index - int yLim = yLimOptions[2]; //maximum value of y axis ... 100 uV + private int fftFrequencyLimit; - List cp5ElementsToCheck = new ArrayList(); + private List cp5ElementsToCheck; - W_fft(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_Fft() { + super(); + widgetTitle = "FFT Plot"; - //Default FFT plot settings - settings.fftMaxFrqSave = 2; - settings.fftMaxuVSave = 2; - settings.fftLogLinSave = 0; - settings.fftSmoothingSave = 3; - settings.fftFilterSave = 0; + fftGplotPoints = new GPointsArray[globalChannelCount]; + initializeFFTPlot(); + } - //Instantiate Channel Select Class - fftChanSelect = new ExGChannelSelect(pApplet, x, y, w, navH); - fftChanSelect.activateAllButtons(); + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + widgetSettings.set(FFTMaxFrequency.class, FFTMaxFrequency.MAX_60) + .set(FFTVerticalScale.class, FFTVerticalScale.SCALE_100) + .set(GraphLogLin.class, GraphLogLin.LOG) + .set(FFTSmoothingFactor.class, globalFFTSettings.getSmoothingFactor()) + .set(FFTFilteredEnum.class, globalFFTSettings.getFilteredEnum()); + + initDropdown(FFTMaxFrequency.class, "fftMaxFrequencyDropdown", "Max Hz"); + initDropdown(FFTVerticalScale.class, "fftVerticalScaleDropdown", "Max uV"); + initDropdown(GraphLogLin.class, "GraphLogLinDropdown", "Log/Lin"); + initDropdown(FFTSmoothingFactor.class, "fftSmoothingDropdown", "Smooth"); + initDropdown(FFTFilteredEnum.class, "fftFilteringDropdown", "Filters"); + fftChanSelect = new ExGChannelSelect(ourApplet, x, y, w, navH); + fftChanSelect.activateAllButtons(); + cp5ElementsToCheck = new ArrayList(); cp5ElementsToCheck.addAll(fftChanSelect.getCp5ElementsForOverlapCheck()); + saveActiveChannels(fftChanSelect.getActiveChannels()); + widgetSettings.saveDefaults(); + + int maxFrequencyHighestValue = ((FFTMaxFrequency) widgetSettings.get(FFTMaxFrequency.class)).getHighestFrequency(); + fftFrequencyLimit = int(1.0 * maxFrequencyHighestValue * (getNumFFTPoints() / currentBoard.getSampleRate())); + } - //This is the protocol for setting up dropdowns. - //Note that these 3 dropdowns correspond to the 3 global functions below - //You just need to make sure the "id" (the 1st String) has the same name as the corresponding function - addDropdown("MaxFreq", "Max Freq", Arrays.asList(settings.fftMaxFrqArray), settings.fftMaxFrqSave); - addDropdown("VertScale", "Max uV", Arrays.asList(settings.fftVertScaleArray), settings.fftMaxuVSave); - addDropdown("LogLin", "Log/Lin", Arrays.asList(settings.fftLogLinArray), settings.fftLogLinSave); - addDropdown("Smoothing", "Smooth", Arrays.asList(settings.fftSmoothingArray), smoothFac_ind); //smoothFac_ind is a global variable at the top of W_HeadPlot.pde - addDropdown("UnfiltFilt", "Filters?", Arrays.asList(settings.fftFilterArray), settings.fftFilterSave); + @Override + protected void applySettings() { + updateDropdownLabel(FFTMaxFrequency.class, "fftMaxFrequencyDropdown"); + updateDropdownLabel(FFTVerticalScale.class, "fftVerticalScaleDropdown"); + updateDropdownLabel(GraphLogLin.class, "GraphLogLinDropdown"); + updateDropdownLabel(FFTSmoothingFactor.class, "fftSmoothingDropdown"); + updateDropdownLabel(FFTFilteredEnum.class, "fftFilteringDropdown"); + applyActiveChannels(fftChanSelect); + applyMaxFrequency(); + applyVerticalScale(); + setPlotLogScale(); + FFTSmoothingFactor smoothingFactor = widgetSettings.get(FFTSmoothingFactor.class); + FFTFilteredEnum filteredEnum = widgetSettings.get(FFTFilteredEnum.class); + globalFFTSettings.setSmoothingFactor(smoothingFactor); + globalFFTSettings.setFilteredEnum(filteredEnum); + } - fft_points = new GPointsArray[globalChannelCount]; - // println("fft_points.length: " + fft_points.length); - initializeFFTPlot(_parent); + @Override + protected void updateChannelSettings() { + if (fftChanSelect != null) { + saveActiveChannels(fftChanSelect.getActiveChannels()); + } } - void initializeFFTPlot(PApplet _parent) { + + private void initializeFFTPlot() { //setup GPlot for FFT - fft_plot = new GPlot(_parent, x, y-navHeight, w, h+navHeight); //based on container dimensions - fft_plot.setAllFontProperties("Arial", 0, 14); - fft_plot.getXAxis().setAxisLabelText("Frequency (Hz)"); - fft_plot.getYAxis().setAxisLabelText("Amplitude (uV)"); - fft_plot.setMar(60, 70, 40, 30); //{ bot=60, left=70, top=40, right=30 } by default - String logScale = settings.fftLogLinSave == 0 ? "y" : ""; - fft_plot.setLogScale(logScale); - - fft_plot.setYLim(0.1, yLim); - //int _nTicks = int(yLim/10 - 1); //number of axis subdivisions + fftPlot = new GPlot(ourApplet, x, y-NAV_HEIGHT, w, h+NAV_HEIGHT); + fftPlot.setAllFontProperties("Arial", 0, 14); + fftPlot.getXAxis().setAxisLabelText("Frequency (Hz)"); + fftPlot.getYAxis().setAxisLabelText("Amplitude (uV)"); + fftPlot.setMar(60, 70, 40, 30); //{ bot=60, left=70, top=40, right=30 } by default + setPlotLogScale(); + + int verticalScaleValue = widgetSettings.get(FFTVerticalScale.class).getValue(); + int maxFrequencyValue = widgetSettings.get(FFTMaxFrequency.class).getValue(); + fftPlot.setYLim(0.1, verticalScaleValue); int _nTicks = 10; - fft_plot.getYAxis().setNTicks(_nTicks); //sets the number of axis divisions... - fft_plot.setXLim(0.1, xLim); - fft_plot.getYAxis().setDrawTickLabels(true); - fft_plot.setPointSize(2); - fft_plot.setPointColor(0); - fft_plot.getXAxis().setFontColor(OPENBCI_DARKBLUE); - fft_plot.getXAxis().setLineColor(OPENBCI_DARKBLUE); - fft_plot.getXAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); - fft_plot.getYAxis().setFontColor(OPENBCI_DARKBLUE); - fft_plot.getYAxis().setLineColor(OPENBCI_DARKBLUE); - fft_plot.getYAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); + fftPlot.getYAxis().setNTicks(_nTicks); //sets the number of axis divisions... + fftPlot.setXLim(0.1, maxFrequencyValue); + fftPlot.getYAxis().setDrawTickLabels(true); + fftPlot.setPointSize(2); + fftPlot.setPointColor(0); + fftPlot.getXAxis().setFontColor(OPENBCI_DARKBLUE); + fftPlot.getXAxis().setLineColor(OPENBCI_DARKBLUE); + fftPlot.getXAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); + fftPlot.getYAxis().setFontColor(OPENBCI_DARKBLUE); + fftPlot.getYAxis().setLineColor(OPENBCI_DARKBLUE); + fftPlot.getYAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); //setup points of fft point arrays - for (int i = 0; i < fft_points.length; i++) { - fft_points[i] = new GPointsArray(FFT_indexLim); + for (int i = 0; i < fftGplotPoints.length; i++) { + fftGplotPoints[i] = new GPointsArray(fftFrequencyLimit); } //fill fft point arrays - for (int i = 0; i < fft_points.length; i++) { //loop through each channel - for (int j = 0; j < FFT_indexLim; j++) { + for (int i = 0; i < fftGplotPoints.length; i++) { //loop through each channel + for (int j = 0; j < fftFrequencyLimit; j++) { GPoint temp = new GPoint(j, 0); - fft_points[i].set(j, temp); + fftGplotPoints[i].set(j, temp); } } //map fft point arrays to fft plots - fft_plot.setPoints(fft_points[0]); + fftPlot.setPoints(fftGplotPoints[0]); } void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - float sr = currentBoard.getSampleRate(); - int nfft = getNfftSafe(); + super.update(); + float sampleRate = currentBoard.getSampleRate(); + int fftPointCount = getNumFFTPoints(); //update the points of the FFT channel arrays for all channels - for (int i = 0; i < fft_points.length; i++) { - for (int j = 0; j < FFT_indexLim + 2; j++) { //loop through frequency domain data, and store into points array - GPoint powerAtBin = new GPoint((1.0*sr/nfft)*j, fftBuff[i].getBand(j)); - fft_points[i].set(j, powerAtBin); + for (int i = 0; i < fftGplotPoints.length; i++) { + for (int j = 0; j < fftFrequencyLimit + 2; j++) { //loop through frequency domain data, and store into points array + GPoint powerAtBin = new GPoint((1.0*sampleRate/fftPointCount)*j, fftBuff[i].getBand(j)); + fftGplotPoints[i].set(j, powerAtBin); } } @@ -130,32 +153,32 @@ class W_fft extends Widget { } void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); //remember to refer to x,y,w,h which are the positioning variables of the Widget class pushStyle(); //draw FFT Graph w/ all plots noStroke(); - fft_plot.beginDraw(); - fft_plot.drawBackground(); - fft_plot.drawBox(); - fft_plot.drawXAxis(); - fft_plot.drawYAxis(); - fft_plot.drawGridLines(GPlot.BOTH); + fftPlot.beginDraw(); + fftPlot.drawBackground(); + fftPlot.drawBox(); + fftPlot.drawXAxis(); + fftPlot.drawYAxis(); + fftPlot.drawGridLines(GPlot.BOTH); //Update and draw active channels that have been selected via channel select for this widget for (int j = 0; j < fftChanSelect.getActiveChannels().size(); j++) { int chan = fftChanSelect.getActiveChannels().get(j); - fft_plot.setLineColor((int)channelColors[chan % 8]); + fftPlot.setLineColor((int)channelColors[chan % 8]); //remap fft point arrays to fft plots - fft_plot.setPoints(fft_points[chan]); - fft_plot.drawLines(); + fftPlot.setPoints(fftGplotPoints[chan]); + fftPlot.drawLines(); } - fft_plot.endDraw(); + fftPlot.endDraw(); //for this widget need to redraw the grey bar, bc the FFT plot covers it up... fill(200, 200, 200); - rect(x, y - navHeight, w, navHeight); //button bar + rect(x, y - NAV_HEIGHT, w, NAV_HEIGHT); //button bar popStyle(); @@ -163,82 +186,100 @@ class W_fft extends Widget { } void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); flexGPlotSizeAndPosition(); - fftChanSelect.screenResized(pApplet); + fftChanSelect.screenResized(ourApplet); } void mousePressed(){ - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); fftChanSelect.mousePressed(this.dropdownIsActive); //Calls channel select mousePressed and checks if clicked } void mouseReleased(){ - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + super.mouseReleased(); } void flexGPlotSizeAndPosition() { if (fftChanSelect.isVisible()) { - fft_plot.setPos(x, y + fftChanSelect.getHeight() - navH); - fft_plot.setOuterDim(w, h - fftChanSelect.getHeight() + navH); + fftPlot.setPos(x, y + fftChanSelect.getHeight() - navH); + fftPlot.setOuterDim(w, h - fftChanSelect.getHeight() + navH); } else { - fft_plot.setPos(x, y - navH); - fft_plot.setOuterDim(w, h + navH); + fftPlot.setPos(x, y - navH); + fftPlot.setOuterDim(w, h + navH); } } -}; -//These functions need to be global! These functions are activated when an item from the corresponding dropdown is selected -//triggered when there is an event in the MaxFreq. Dropdown -void MaxFreq(int n) { - /* request the selected item based on index n */ - w_fft.fft_plot.setXLim(0.1, w_fft.xLimOptions[n]); //update the xLim of the FFT_Plot - settings.fftMaxFrqSave = n; //save the xLim to variable for save/load settings -} + private void applyMaxFrequency() { + int maxFrequencyValue = widgetSettings.get(FFTMaxFrequency.class).getValue(); + fftPlot.setXLim(0.1, maxFrequencyValue); + } -//triggered when there is an event in the VertScale Dropdown -void VertScale(int n) { + private void applyVerticalScale() { + int verticalScaleValue = widgetSettings.get(FFTVerticalScale.class).getValue(); + fftPlot.setYLim(0.1, verticalScaleValue); + } - w_fft.fft_plot.setYLim(0.1, w_fft.yLimOptions[n]); //update the yLim of the FFT_Plot - settings.fftMaxuVSave = n; //save the yLim to variable for save/load settings -} + public void setMaxFrequency(int n) { + widgetSettings.setByIndex(FFTMaxFrequency.class, n); + applyMaxFrequency(); + } + + public void setVerticalScale(int n) { + widgetSettings.setByIndex(FFTVerticalScale.class, n); + applyVerticalScale(); + } + + public void setLogLin(int n) { + widgetSettings.setByIndex(GraphLogLin.class, n); + setPlotLogScale(); + } + + private void setPlotLogScale() { + GraphLogLin logLin = widgetSettings.get(GraphLogLin.class); + if (logLin == GraphLogLin.LOG) { + fftPlot.setLogScale("y"); + } else { + fftPlot.setLogScale(""); + } + } + + public void setSmoothingDropdownFrontend(FFTSmoothingFactor _smoothingFactor) { + widgetSettings.set(FFTSmoothingFactor.class, _smoothingFactor); + updateDropdownLabel(FFTSmoothingFactor.class, "fftSmoothingDropdown"); + } -//triggered when there is an event in the LogLin Dropdown -void LogLin(int n) { - if (n==0) { - w_fft.fft_plot.setLogScale("y"); - //store the current setting to save - settings.fftLogLinSave = 0; - } else { - w_fft.fft_plot.setLogScale(""); - //store the current setting to save - settings.fftLogLinSave = 1; + public void setFilteringDropdownFrontend(FFTFilteredEnum _filteredEnum) { + widgetSettings.set(FFTFilteredEnum.class, _filteredEnum); + updateDropdownLabel(FFTFilteredEnum.class, "fftFilteringDropdown"); } +}; + +//These functions need to be global! These functions are activated when an item from the corresponding dropdown is selected +public void fftMaxFrequencyDropdown(int n) { + ((W_Fft) widgetManager.getWidget("W_Fft")).setMaxFrequency(n); } -//triggered when there is an event in the Smoothing Dropdown -void Smoothing(int n) { - smoothFac_ind = n; - settings.fftSmoothingSave = n; - //since this function is called by both the BandPower and FFT Widgets the dropdown needs to be updated in both - w_fft.cp5_widget.getController("Smoothing").getCaptionLabel().setText(settings.fftSmoothingArray[n]); - w_bandPower.cp5_widget.getController("Smoothing").getCaptionLabel().setText(settings.fftSmoothingArray[n]); +public void fftVerticalScaleDropdown(int n) { + ((W_Fft) widgetManager.getWidget("W_Fft")).setVerticalScale(n); +} +public void GraphLogLinDropdown(int n) { + ((W_Fft) widgetManager.getWidget("W_Fft")).setLogLin(n); } -//triggered when there is an event in the UnfiltFilt Dropdown -void UnfiltFilt(int n) { - settings.fftFilterSave = n; - if (n==0) { - //have FFT use filtered data -- default - isFFTFiltered = true; - } else { - //have FFT use unfiltered data - isFFTFiltered = false; - } - //since this function is called by both the BandPower and FFT Widgets the dropdown needs to be updated in both - w_fft.cp5_widget.getController("UnfiltFilt").getCaptionLabel().setText(settings.fftFilterArray[n]); - w_bandPower.cp5_widget.getController("UnfiltFilt").getCaptionLabel().setText(settings.fftFilterArray[n]); +public void fftSmoothingDropdown(int n) { + globalFFTSettings.setSmoothingFactor(FFTSmoothingFactor.values()[n]); + FFTSmoothingFactor smoothingFactor = globalFFTSettings.getSmoothingFactor(); + ((W_BandPower) widgetManager.getWidget("W_BandPower")).setSmoothingDropdownFrontend(smoothingFactor); + ((W_Fft) widgetManager.getWidget("W_Fft")).setSmoothingDropdownFrontend(smoothingFactor); } + +public void fftFilteringDropdown(int n) { + globalFFTSettings.setFilteredEnum(FFTFilteredEnum.values()[n]); + FFTFilteredEnum filteredEnum = globalFFTSettings.getFilteredEnum(); + ((W_BandPower) widgetManager.getWidget("W_BandPower")).setFilteringDropdownFrontend(filteredEnum); + ((W_Fft) widgetManager.getWidget("W_Fft")).setFilteringDropdownFrontend(filteredEnum); +} \ No newline at end of file diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 678b5357d..fae7599eb 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -21,7 +21,7 @@ import brainflow.DataFilter; import brainflow.LogLevels; import brainflow.MLModel; -class W_Focus extends Widget { +class W_Focus extends WidgetWithSettings { private ExGChannelSelect focusChanSelect; private boolean prevChanSelectIsVisible = false; @@ -43,11 +43,6 @@ class W_Focus extends Widget { private FifoChannelBar focusBar; private float focusBarHardYAxisLimit = 1.05f; //Provide slight "breathing room" to avoid GPlot error when metric value == 1.0 - private FocusXLim xLimit = FocusXLim.TEN; - private FocusMetric focusMetric = FocusMetric.RELAXATION; - private FocusClassifier focusClassifier = FocusClassifier.REGRESSION; - private FocusThreshold focusThreshold = FocusThreshold.EIGHT_TENTHS; - private FocusColors focusColors = FocusColors.GREEN; private int[] exgChannels; private int channelCount; @@ -62,17 +57,12 @@ class W_Focus extends Widget { private final int GRAPH_PADDING = 30; private color cBack, cDark, cMark, cFocus, cWave, cPanel; - List cp5ElementsToCheck = new ArrayList(); + List cp5ElementsToCheck; - 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 ExGChannelSelect(pApplet, x, y, w, navH); - focusChanSelect.activateAllButtons(); + W_Focus() { + super(); + widgetTitle = "Focus"; - cp5ElementsToCheck.addAll(focusChanSelect.getCp5ElementsForOverlapCheck()); - auditoryNeurofeedback = new AuditoryNeurofeedback(x + PAD_FIVE, y + PAD_FIVE, w/2 - PAD_FIVE*2, navBarHeight/2); cp5ElementsToCheck.add((controlP5.Controller)auditoryNeurofeedback.startStopButton); cp5ElementsToCheck.add((controlP5.Controller)auditoryNeurofeedback.modeButton); @@ -83,12 +73,6 @@ class W_Focus extends Widget { // 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("focusThresholdDropdown", "Threshold", focusThreshold.getEnumStringsAsList(), focusThreshold.getIndex()); - addDropdown("focusWindowDropdown", "Window", xLimit.getEnumStringsAsList(), xLimit.getIndex()); //Create data table dataGrid = new Grid(NUM_TABLE_ROWS, NUM_TABLE_COLUMNS, cellHeight); @@ -103,13 +87,60 @@ class W_Focus extends Widget { //create our focus graph updateGraphDims(); - focusBar = new FifoChannelBar(_parent, "Metric Value", xLimit.getValue(), focusBarHardYAxisLimit, graphX, graphY, graphW, graphH, ACCEL_X_COLOR, FocusXLim.TWENTY.getValue()); + int xLimitValue = widgetSettings.get(FocusXLim.class).getValue(); + focusBar = new FifoChannelBar(ourApplet, "Metric Value", xLimitValue, focusBarHardYAxisLimit, graphX, graphY, graphW, graphH, ACCEL_X_COLOR, FocusXLim.TWENTY.getValue()); initBrainFlowMetric(); } + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + + widgetSettings.set(FocusXLim.class, FocusXLim.TEN) + .set(FocusMetric.class, FocusMetric.RELAXATION) + .set(FocusClassifier.class, FocusClassifier.REGRESSION) + .set(FocusThreshold.class, FocusThreshold.EIGHT_TENTHS) + .set(FocusColors.class, FocusColors.GREEN); + + dropdownWidth = 60; //Override the default dropdown width for this widget + initDropdown(FocusMetric.class, "focusMetricDropdown", "Metric"); + initDropdown(FocusThreshold.class, "focusThresholdDropdown", "Threshold"); + initDropdown(FocusXLim.class, "focusWindowDropdown", "Window"); + + //Add channel select dropdown to this widget + cp5ElementsToCheck = new ArrayList(); + focusChanSelect = new ExGChannelSelect(ourApplet, x, y, w, navH); + focusChanSelect.activateAllButtons(); + saveActiveChannels(focusChanSelect.getActiveChannels()); + cp5ElementsToCheck.addAll(focusChanSelect.getCp5ElementsForOverlapCheck()); + + widgetSettings.saveDefaults(); + } + + @Override + protected void applySettings() { + //Apply settings to dropdowns + updateDropdownLabel(FocusXLim.class, "focusWindowDropdown"); + updateDropdownLabel(FocusMetric.class, "focusMetricDropdown"); + updateDropdownLabel(FocusThreshold.class, "focusThresholdDropdown"); + applyHorizontalScale(); + initBrainFlowMetric(); + + //Apply settings to channel select dropdown + applyActiveChannels(focusChanSelect); + } + + @Override + protected void updateChannelSettings() { + //Save active channels to settings + if (focusChanSelect != null) { + saveActiveChannels(focusChanSelect.getActiveChannels()); + } + } + public void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); //Update channel checkboxes and active channels focusChanSelect.update(x, y, w); @@ -129,7 +160,7 @@ class W_Focus extends Widget { } public void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); //remember to refer to x,y,w,h which are the positioning variables of the Widget class //Draw data table @@ -159,7 +190,7 @@ class W_Focus extends Widget { } public void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); resizeTable(); @@ -168,7 +199,7 @@ class W_Focus extends Widget { updateGraphDims(); focusBar.screenResized(graphX, graphY, graphW, graphH); - focusChanSelect.screenResized(pApplet); + focusChanSelect.screenResized(ourApplet); //Custom resize these dropdowns due to longer text strings as options cp5_widget.get(ScrollableList.class, "focusMetricDropdown").setWidth(METRIC_DROPDOWN_W); @@ -179,12 +210,12 @@ class W_Focus extends Widget { } void mousePressed() { - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); focusChanSelect.mousePressed(this.dropdownIsActive); //Calls channel select mousePressed and checks if clicked } private void resizeTable() { - int extraPadding = focusChanSelect.isVisible() ? navHeight : 0; + int extraPadding = focusChanSelect.isVisible() ? NAV_HEIGHT : 0; float upperLeftContainerW = w/2; float upperLeftContainerH = h/2; //float min = min(upperLeftContainerW, upperLeftContainerH); @@ -199,9 +230,9 @@ class W_Focus extends Widget { } private void updateAuditoryNeurofeedbackPosition() { - int extraPadding = focusChanSelect.isVisible() ? navHeight : 0; + int extraPadding = focusChanSelect.isVisible() ? NAV_HEIGHT : 0; int subContainerMiddleX = x + w/4; - auditoryNeurofeedback.screenResized(subContainerMiddleX, (int)(y + h/2 - navHeight + extraPadding), w/2 - PAD_FIVE*2, navBarHeight/2); + auditoryNeurofeedback.screenResized(subContainerMiddleX, (int)(y + h/2 - NAV_HEIGHT + extraPadding), w/2 - PAD_FIVE*2, navBarHeight/2); } private void updateStatusCircle() { @@ -209,7 +240,7 @@ class W_Focus extends Widget { float upperLeftContainerH = h/2; float min = min(upperLeftContainerW, upperLeftContainerH); xc = x + w/4; - yc = y + h/4 - navHeight; + yc = y + h/4 - NAV_HEIGHT; wc = min * (3f/5); hc = wc; } @@ -225,7 +256,8 @@ class W_Focus extends Widget { //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(); + int xLimitValue = widgetSettings.get(FocusXLim.class).getValue(); + int windowSize = currentBoard.getSampleRate() * xLimitValue; // getData in GUI returns data in shape ndatapoints x nchannels, in BrainFlow its transposed List currentData = currentBoard.getData(windowSize); @@ -286,6 +318,7 @@ class W_Focus extends Widget { strokeColor = cDark; sb.append("Not "); } + FocusMetric focusMetric = widgetSettings.get(FocusMetric.class); sb.append(focusMetric.getIdealStateString()); //Draw status graphic pushStyle(); @@ -301,6 +334,11 @@ class W_Focus extends Widget { } private void initBrainFlowMetric() { + if (mlModel != null) { + endSession(); + } + FocusMetric focusMetric = widgetSettings.get(FocusMetric.class); + FocusClassifier focusClassifier = widgetSettings.get(FocusClassifier.class); BrainFlowModelParams modelParams = new BrainFlowModelParams( focusMetric.getMetric().get_code(), focusClassifier.getClassifier().get_code() @@ -323,6 +361,7 @@ class W_Focus extends Widget { } private void onColorChange() { + FocusColors focusColors = widgetSettings.get(FocusColors.class); switch(focusColors) { case GREEN: cBack = #ffffff; //white @@ -354,30 +393,33 @@ class W_Focus extends Widget { void channelSelectFlexWidgetUI() { focusBar.setPlotPositionAndOuterDimensions(focusChanSelect.isVisible()); int factor = focusChanSelect.isVisible() ? 1 : -1; - yc += navHeight * factor; + yc += NAV_HEIGHT * factor; resizeTable(); updateAuditoryNeurofeedbackPosition(); } - public void setFocusHorizScale(int n) { - xLimit = xLimit.values()[n]; - focusBar.adjustTimeAxis(xLimit.getValue()); + public void setFocusHorizontalScale(int n) { + widgetSettings.setByIndex(FocusXLim.class, n); + applyHorizontalScale(); } public void setMetric(int n) { - focusMetric = focusMetric.values()[n]; - endSession(); + widgetSettings.setByIndex(FocusMetric.class, n); initBrainFlowMetric(); } public void setClassifier(int n) { - focusClassifier = focusClassifier.values()[n]; - endSession(); + widgetSettings.setByIndex(FocusClassifier.class, n); initBrainFlowMetric(); } + private void applyHorizontalScale() { + int windowValue = widgetSettings.get(FocusXLim.class).getValue(); + focusBar.adjustTimeAxis(windowValue); + } + public void setThreshold(int n) { - focusThreshold = focusThreshold.values()[n]; + widgetSettings.setByIndex(FocusThreshold.class, n); } public int getMetricExceedsThreshold() { @@ -391,19 +433,8 @@ class W_Focus extends Widget { //Called in DataProcessing.pde to update data even if widget is closed public void updateFocusWidgetData() { metricPrediction = updateFocusState(); - predictionExceedsThreshold = metricPrediction > focusThreshold.getValue(); - } - - public FocusMetric getFocusMetric() { - return focusMetric; - } - - public FocusThreshold getFocusThreshold() { - return focusThreshold; - } - - public FocusXLim getFocusWindow() { - return xLimit; + float focusThresholdValue = widgetSettings.get(FocusThreshold.class).getValue(); + predictionExceedsThreshold = metricPrediction > focusThresholdValue; } public void clear() { @@ -412,17 +443,17 @@ class W_Focus extends Widget { dataGrid.setString(df.format(metricPrediction), 0, 1); focusBar.update(metricPrediction); } -}; //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); + ((W_Focus) widgetManager.getWidget("W_Focus")).setFocusHorizontalScale(n); } public void focusMetricDropdown(int n) { - w_focus.setMetric(n); + ((W_Focus) widgetManager.getWidget("W_Focus")).setMetric(n); } public void focusThresholdDropdown(int n) { - w_focus.setThreshold(n); + ((W_Focus) widgetManager.getWidget("W_Focus")).setThreshold(n); } diff --git a/OpenBCI_GUI/W_GanglionImpedance.pde b/OpenBCI_GUI/W_GanglionImpedance.pde index b073e0a7a..107806795 100644 --- a/OpenBCI_GUI/W_GanglionImpedance.pde +++ b/OpenBCI_GUI/W_GanglionImpedance.pde @@ -1,32 +1,21 @@ - -//////////////////////////////////////////////////// -// -// W_template.pde (ie "Widget Template") -// -// This is a Template Widget, intended to be used as a starting point for OpenBCI Community members that want to develop their own custom widgets! -// Good luck! If you embark on this journey, please let us know. Your contributions are valuable to everyone! -// -// Created by: Conor Russomanno, November 2016 -// -///////////////////////////////////////////////////, - - class W_GanglionImpedance extends Widget { + Button startStopCheck; int padding = 24; - W_GanglionImpedance(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_GanglionImpedance() { + super(); + widgetTitle = "Ganglion Signal"; - createStartStopCheck("startStopCheck", "Start Impedance Check", x + padding, y + padding, 200, navHeight, p4, 14, colorNotPressed, OPENBCI_DARKBLUE); + createStartStopCheck("startStopCheck", "Start Impedance Check", x + padding, y + padding, 200, NAV_HEIGHT, p4, 14, colorNotPressed, OPENBCI_DARKBLUE); } void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); } void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); //remember to refer to x,y,w,h which are the positioning variables of the Widget class pushStyle(); @@ -80,18 +69,10 @@ class W_GanglionImpedance extends Widget { } void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); startStopCheck.setPosition(x + padding, y + padding); } - void mousePressed(){ - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) - } - - void mouseReleased(){ - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) - } - private void createStartStopCheck(String name, String text, int _x, int _y, int _w, int _h, PFont _font, int _fontSize, color _bg, color _textColor) { startStopCheck = createButton(cp5_widget, name, text, _x, _y, _w, _h, _font, _fontSize, _bg, _textColor); startStopCheck.onRelease(new CallbackListener() { diff --git a/OpenBCI_GUI/W_HeadPlot.pde b/OpenBCI_GUI/W_HeadPlot.pde deleted file mode 100644 index d7578b3f6..000000000 --- a/OpenBCI_GUI/W_HeadPlot.pde +++ /dev/null @@ -1,1279 +0,0 @@ - -//////////////////////////////////////////////////// -// -// W_template.pde (ie "Widget Template") -// -// This is a Template Widget, intended to be used as a starting point for OpenBCI Community members that want to develop their own custom widgets! -// Good luck! If you embark on this journey, please let us know. Your contributions are valuable to everyone! -// -// Created by: Conor Russomanno, November 2016 -// Based on code written by: Chip Audette, Oct 2013 -// -///////////////////////////////////////////////////, - - -float[] smoothFac = new float[]{0.0, 0.5, 0.75, 0.9, 0.95, 0.98, 0.99, 0.999}; //used by FFT & Headplot -int smoothFac_ind = 3; //initial index into the smoothFac array = 0.75 to start .. used by FFT & Head Plots - -class W_HeadPlot extends Widget { - HeadPlot headPlot; - - W_HeadPlot(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - - //Headplot settings - settings.hpIntensitySave = 2; - settings.hpPolaritySave = 0; - settings.hpContoursSave = 0; - settings.hpSmoothingSave = 3; - //This is the protocol for setting up dropdowns. - //Note that these 3 dropdowns correspond to the 3 global functions below - //You just need to make sure the "id" (the 1st String) has the same name as the corresponding function - // addDropdown("Ten20", "Layout", Arrays.asList("10-20", "5-10"), 0); - // addDropdown("Headset", "Headset", Arrays.asList("None", "Mark II", "Mark III", "Mark IV "), 0); - addDropdown("Intensity", "Intensity", Arrays.asList("4x", "2x", "1x", "0.5x", "0.2x", "0.02x"), vertScaleFactor_ind); - addDropdown("Polarity", "Polarity", Arrays.asList("+/-", " + "), settings.hpPolaritySave); - addDropdown("ShowContours", "Contours", Arrays.asList("ON", "OFF"), settings.hpContoursSave); - addDropdown("SmoothingHeadPlot", "Smooth", Arrays.asList(settings.fftSmoothingArray), smoothFac_ind); - //Initialize the headplot - updateHeadPlot(); - } - - void updateHeadPlot() { - headPlot = new HeadPlot(x, y, w, h, win_w, win_h); - //FROM old Gui_Manager - headPlot.setIntensityData_byRef(dataProcessing.data_std_uV, is_railed); - headPlot.setPolarityData_byRef(dataProcessing.polarity); - setSmoothFac(smoothFac[smoothFac_ind]); - } - - void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - headPlot.update(); - } - - void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) - headPlot.draw(); //draw the actual headplot - } - - void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) - headPlot.hp_x = x; - headPlot.hp_y = y; - headPlot.hp_w = w; - headPlot.hp_h = h; - headPlot.hp_win_x = x; - headPlot.hp_win_y = y; - - thread("doHardCalcs"); - } - - void mousePressed(){ - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) - headPlot.mousePressed(); - } - - void mouseReleased(){ - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) - headPlot.mouseReleased(); - } - - void mouseDragged(){ - super.mouseDragged(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) - headPlot.mouseDragged(); - } - - //add custom class functions here - void setSmoothFac(float fac) { - headPlot.smooth_fac = fac; - } -}; - -//triggered when there is an event in the Polarity Dropdown -void Polarity(int n) { - - if (n==0) { - w_headPlot.headPlot.use_polarity = true; - } else { - w_headPlot.headPlot.use_polarity = false; - } - settings.hpPolaritySave = n; -} - -void ShowContours(int n){ - if(n==0){ - //turn headplot contours on - w_headPlot.headPlot.drawHeadAsContours = true; - } else if(n==1){ - //turn headplot contours off - w_headPlot.headPlot.drawHeadAsContours = false; - } - settings.hpContoursSave = n; -} - -//triggered when there is an event in the SmoothingHeadPlot Dropdown -void SmoothingHeadPlot(int n) { - w_headPlot.setSmoothFac(smoothFac[n]); - settings.hpSmoothingSave = n; -} - -void Intensity(int n){ - vertScaleFactor_ind = n; - updateVertScale(); - settings.hpIntensitySave = n; -} - -// ----- these variable/methods are used for adjusting the intensity factor of the headplot opacity --------------------------------------------------------------------------------------------------------- -float default_vertScale_uV = 200.0; //this defines the Y-scale on the montage plots...this is the vertical space between traces -float[] vertScaleFactor = { 0.25f, 0.5f, 1.0f, 2.0f, 5.0f, 50.0f}; -int vertScaleFactor_ind = 2; -float vertScale_uV = default_vertScale_uV; - -void setVertScaleFactor_ind(int ind) { - vertScaleFactor_ind = max(0,ind); - if (ind >= vertScaleFactor.length) vertScaleFactor_ind = 0; - updateVertScale(); -} - -void updateVertScale() { - vertScale_uV = default_vertScale_uV * vertScaleFactor[vertScaleFactor_ind]; - w_headPlot.headPlot.setMaxIntensity_uV(vertScale_uV); -} - -void doHardCalcs() { - if (!w_headPlot.headPlot.threadLock) { - w_headPlot.headPlot.threadLock = true; - w_headPlot.headPlot.setPositionSize(w_headPlot.headPlot.hp_x, w_headPlot.headPlot.hp_y, w_headPlot.headPlot.hp_w, w_headPlot.headPlot.hp_h, w_headPlot.headPlot.hp_win_x, w_headPlot.headPlot.hp_win_y); - w_headPlot.headPlot.hardCalcsDone = true; - w_headPlot.headPlot.threadLock = false; - } -} - -//--------------------------------------------------------------------------------------------------------------------------------------- - -////////////////////////////////////////////////////////////// -// -// HeadPlot Class -// -// This class creates and manages the head-shaped plot used by the GUI. -// The head includes circles representing the different EEG electrodes. -// The color (brightness) of the electrodes can be adjusted so that the -// electrodes' brightness values dynamically reflect the intensity of the -// EEG signal. All EEG processing must happen outside of this class. -// -// Created by: Chip Audette 2013 -// -/////////////////////////////////////////////////////////////// - -// Note: This routine uses aliasing to know which data should be used to -// set the brightness of the electrodes. - -class HeadPlot { - private float rel_posX, rel_posY, rel_width, rel_height; - private int circ_x, circ_y, circ_diam; - private int earL_x, earL_y, earR_x, earR_y, ear_width, ear_height; - private int[] nose_x, nose_y; - private float[][] electrode_xy; - private float[] ref_electrode_xy; - private float[][][] electrode_color_weightFac; - private int[][] electrode_rgb; - private float[][] headVoltage; - private int elec_diam; - PFont font; - public float[] intensity_data_uV; - public float[] polarity_data; - private DataStatus[] is_railed; - private float intense_min_uV=0.0f, intense_max_uV=1.0f, assumed_railed_voltage_uV=1.0f; - private float log10_intense_min_uV = 0.0f, log10_intense_max_uV=1.0; - PImage headImage; - private int image_x, image_y; - public boolean drawHeadAsContours; - private boolean plot_color_as_log = true; - public float smooth_fac = 0.0f; - private boolean use_polarity = true; - private int mouse_over_elec_index = -1; - private boolean isDragging = false; - private float drag_x, drag_y; - public int hp_win_x = 0; - public int hp_win_y = 0; - public int hp_x = 0; - public int hp_y = 0; - public int hp_w = 0; - public int hp_h = 0; - public boolean hardCalcsDone = false; - public boolean threadLock = false; - - HeadPlot(int _x, int _y, int _w, int _h, int _win_x, int _win_y) { - final int n_elec = globalChannelCount; //set number of electrodes using the global globalChannelCount variable - nose_x = new int[3]; - nose_y = new int[3]; - electrode_xy = new float[n_elec][2]; //x-y position of electrodes (pixels?) - ref_electrode_xy = new float[2]; //x-y position of reference electrode - electrode_rgb = new int[3][n_elec]; //rgb color for each electrode - font = p5; - drawHeadAsContours = true; //set this to be false for slower computers - - hp_x = _x; - hp_y = _y; - hp_w = _w; - hp_h = _h; - hp_win_x = _win_x; - hp_win_y = _win_y; - setMaxIntensity_uV(200.0f); //default intensity scaling for electrodes - } - - public void setPositionSize(int _x, int _y, int _w, int _h, int _win_x, int _win_y) { - float percentMargin = 0.1; - _x = _x + (int)(float(_w)*percentMargin); - _y = _y + (int)(float(_h)*percentMargin)-navHeight/2; - _w = (int)(float(_w)-(2*(float(_w)*percentMargin))); - _h = (int)(float(_h)-(2*(float(_h)*percentMargin))); - - rel_posX = float(_x)/_win_x; - rel_posY = float(_y)/_win_y; - rel_width = float(_w)/_win_x; - rel_height = float(_h)/_win_y; - setWindowDimensions(_win_x, _win_y); - } - - public void setIntensityData_byRef(float[] data, DataStatus[] is_rail) { - intensity_data_uV = data; //simply alias the data held externally. DOES NOT COPY THE DATA ITSEF! IT'S SIMPLY LINKED! - is_railed = is_rail; - } - - public void setPolarityData_byRef(float[] data) { - polarity_data = data;//simply alias the data held externally. DOES NOT COPY THE DATA ITSEF! IT'S SIMPLY LINKED! - } - - public String getUsePolarityTrueFalse() { - if (use_polarity) { - return "True"; - } else { - return "False"; - } - } - - public void setMaxIntensity_uV(float val_uV) { - intense_max_uV = val_uV; - intense_min_uV = intense_max_uV / 200.0 * 5.0f; //set to 200, get 5 - assumed_railed_voltage_uV = intense_max_uV; - - log10_intense_max_uV = log10(intense_max_uV); - log10_intense_min_uV = log10(intense_min_uV); - } - - public void set_plotColorAsLog(boolean state) { - plot_color_as_log = state; - } - - //this method defines all locations of all the subcomponents - public void setWindowDimensions(int win_width, int win_height) { - final int n_elec = electrode_xy.length; - - //define the head itself - float nose_relLen = 0.075f; - float nose_relWidth = 0.05f; - float nose_relGutter = 0.02f; - float ear_relLen = 0.15f; - float ear_relWidth = 0.075; - - float square_width = min(rel_width*(float)win_width, - rel_height*(float)win_height); //choose smaller of the two - - float total_width = square_width; - float total_height = square_width; - float nose_width = total_width * nose_relWidth; - float nose_height = total_height * nose_relLen; - ear_width = (int)(ear_relWidth * total_width); - ear_height = (int)(ear_relLen * total_height); - int circ_width_foo = (int)(total_width - 2.f*((float)ear_width)/2.0f); - int circ_height_foo = (int)(total_height - nose_height); - circ_diam = min(circ_width_foo, circ_height_foo); - - //locations: circle center, measured from upper left - circ_x = (int)((rel_posX+0.5f*rel_width)*(float)win_width); //center of head - circ_y = (int)((rel_posY+0.5*rel_height)*(float)win_height + nose_height); //center of head - - //locations: ear centers, measured from upper left - earL_x = circ_x - circ_diam/2; - earR_x = circ_x + circ_diam/2; - earL_y = circ_y; - earR_y = circ_y; - - //locations nose vertexes, measured from upper left - nose_x[0] = circ_x - (int)((nose_relWidth/2.f)*(float)win_width); - nose_x[1] = circ_x + (int)((nose_relWidth/2.f)*(float)win_width); - nose_x[2] = circ_x; - nose_y[0] = circ_y - (int)((float)circ_diam/2.0f - nose_relGutter*(float)win_height); - nose_y[1] = nose_y[0]; - nose_y[2] = circ_y - (int)((float)circ_diam/2.0f + nose_height); - - - //define the electrode positions as the relative position [-1.0 +1.0] within the head - //remember that negative "Y" is up and positive "Y" is down - float elec_relDiam = 0.12f; //was 0.1425 prior to 2014-03-23 - elec_diam = (int)(elec_relDiam*((float)circ_diam)); - setElectrodeLocations(n_elec, elec_relDiam); - - //define image to hold all of this - image_x = int(round(circ_x - 0.5*circ_diam - 0.5*ear_width)); - image_y = nose_y[2]; - headImage = createImage(int(total_width), int(total_height), ARGB); - - //initialize the image - for (int Iy=0; Iy < headImage.height; Iy++) { - for (int Ix = 0; Ix < headImage.width; Ix++) { - headImage.set(Ix, Iy, WHITE); - } - } - - //define the weighting factors to go from the electrode voltages - //outward to the full the contour plot - if (false) { - //here is a simple distance-based algorithm that works every time, though - //is not really physically accurate. It looks decent enough - computePixelWeightingFactors(); - } else { - //here is the better solution that is more physical. It involves an iterative - //solution, which could be really slow or could fail. If it does poorly, - //switch to using the algorithm above. - int n_wide_full = int(total_width); - int n_tall_full = int(total_height); - computePixelWeightingFactors_multiScale(n_wide_full, n_tall_full); - } - } //end of method - - - private void setElectrodeLocations(int n_elec, float elec_relDiam) { - //try loading the positions from a file - int n_elec_to_load = n_elec+1; //load the n_elec plus the reference electrode - Table elec_relXY = new Table(); - String default_fname = "electrode_positions_default.txt"; - //String default_fname = "electrode_positions_12elec_scalp9.txt"; - try { - elec_relXY = loadTable(default_fname, "header,csv"); //try loading the default file - } - catch (NullPointerException e) { - }; - - //get the default locations if the file didn't exist - if ((elec_relXY == null) || (elec_relXY.getRowCount() < n_elec_to_load)) { - println("headPlot: electrode position file not found or was wrong size: " + default_fname); - println(" : using defaults..."); - elec_relXY = createDefaultElectrodeLocations(default_fname, elec_relDiam); - } - - //define the actual locations of the electrodes in pixels - for (int i=0; i < min(electrode_xy.length, elec_relXY.getRowCount()); i++) { - electrode_xy[i][0] = circ_x+(int)(elec_relXY.getFloat(i, 0)*((float)circ_diam)); - electrode_xy[i][1] = circ_y+(int)(elec_relXY.getFloat(i, 1)*((float)circ_diam)); - } - - //the referenece electrode is last in the file - ref_electrode_xy[0] = circ_x+(int)(elec_relXY.getFloat(elec_relXY.getRowCount()-1, 0)*((float)circ_diam)); - ref_electrode_xy[1] = circ_y+(int)(elec_relXY.getFloat(elec_relXY.getRowCount()-1, 1)*((float)circ_diam)); - } - - private Table createDefaultElectrodeLocations(String fname, float elec_relDiam) { - - //regular electrodes - float[][] elec_relXY = new float[16][2]; - elec_relXY[0][0] = -0.125f; - elec_relXY[0][1] = -0.5f + elec_relDiam*(0.5f+0.2f); //FP1 - elec_relXY[1][0] = -elec_relXY[0][0]; - elec_relXY[1][1] = elec_relXY[0][1]; //FP2 - - elec_relXY[2][0] = -0.2f; - elec_relXY[2][1] = 0f; //C3 - elec_relXY[3][0] = -elec_relXY[2][0]; - elec_relXY[3][1] = elec_relXY[2][1]; //C4 - - elec_relXY[4][0] = -0.3425f; - elec_relXY[4][1] = 0.27f; //T5 (aka P7) - elec_relXY[5][0] = -elec_relXY[4][0]; - elec_relXY[5][1] = elec_relXY[4][1]; //T6 (aka P8) - - elec_relXY[6][0] = -0.125f; - elec_relXY[6][1] = +0.5f - elec_relDiam*(0.5f+0.2f); //O1 - elec_relXY[7][0] = -elec_relXY[6][0]; - elec_relXY[7][1] = elec_relXY[6][1]; //O2 - - elec_relXY[8][0] = elec_relXY[4][0]; - elec_relXY[8][1] = -elec_relXY[4][1]; //F7 - elec_relXY[9][0] = -elec_relXY[8][0]; - elec_relXY[9][1] = elec_relXY[8][1]; //F8 - - elec_relXY[10][0] = -0.18f; - elec_relXY[10][1] = -0.15f; //C3 - elec_relXY[11][0] = -elec_relXY[10][0]; - elec_relXY[11][1] = elec_relXY[10][1]; //C4 - - elec_relXY[12][0] = -0.5f +elec_relDiam*(0.5f+0.15f); - elec_relXY[12][1] = 0f; //T3 (aka T7?) - elec_relXY[13][0] = -elec_relXY[12][0]; - elec_relXY[13][1] = elec_relXY[12][1]; //T4 (aka T8) - - elec_relXY[14][0] = elec_relXY[10][0]; - elec_relXY[14][1] = -elec_relXY[10][1]; //CP3 - elec_relXY[15][0] = -elec_relXY[14][0]; - elec_relXY[15][1] = elec_relXY[14][1]; //CP4 - - //reference electrode - float[] ref_elec_relXY = new float[2]; - ref_elec_relXY[0] = 0.0f; - ref_elec_relXY[1] = 0.0f; - - //put it all into a table - Table table_elec_relXY = new Table(); - table_elec_relXY.addColumn("X", Table.FLOAT); - table_elec_relXY.addColumn("Y", Table.FLOAT); - for (int I = 0; I < elec_relXY.length; I++) { - table_elec_relXY.addRow(); - table_elec_relXY.setFloat(I, "X", elec_relXY[I][0]); - table_elec_relXY.setFloat(I, "Y", elec_relXY[I][1]); - } - - //last one is the reference electrode - table_elec_relXY.addRow(); - table_elec_relXY.setFloat(table_elec_relXY.getRowCount()-1, "X", ref_elec_relXY[0]); - table_elec_relXY.setFloat(table_elec_relXY.getRowCount()-1, "Y", ref_elec_relXY[1]); - - //try writing it to a file - String full_fname = "Data\\" + fname; - try { - saveTable(table_elec_relXY, full_fname, "csv"); - } - catch (NullPointerException e) { - println("headPlot: createDefaultElectrodeLocations: could not write file to " + full_fname); - }; - - //return - return table_elec_relXY; - } //end of method - - //Here, we do a two-step solution to get the weighting factors. - //We do a coarse grid first. We do our iterative solution on the coarse grid. - //Then, we formulate the full resolution fine grid. We interpolate these points - //from the data resulting from the coarse grid. - private void computePixelWeightingFactors_multiScale(int n_wide_full, int n_tall_full) { - int n_elec = electrode_xy.length; - - //define the coarse grid data structures and pixel locations - int decimation = 10; - int n_wide_small = n_wide_full / decimation + 1; - int n_tall_small = n_tall_full / decimation + 1; - float weightFac[][][] = new float[n_elec][n_wide_small][n_tall_small]; - int pixelAddress[][][] = new int[n_wide_small][n_tall_small][2]; - for (int Ix=0; Ix= 0.0. If it isn't, it was a - //quantization problem. let's clean it up. - for (int Ielec=0; Ielec -1) { - //we are! set the weightFac to reflect this electrode only - for (int Ielec=0; Ielec= 0) && (Iy_test < n_tall)) { - for (Ix_test=Ix-step; Ix_test<=Ix+step; Ix_test++) { - if ((Ix_test >=0) && (Ix_test < n_wide)) { - anyWithinBounds=true; - if (weightFac[Ix_test][Iy_test] >= 0.0) { - sum += weightFac[Ix_test][Iy_test]; - n_sum++; - } - } - } - } - - //along the right - Ix_test = Ix + step; - if ((Ix_test >= 0) && (Ix_test < n_wide)) { - for (Iy_test=Iy-step; Iy_test<=Iy+step; Iy_test++) { - if ((Iy_test >=0) && (Iy_test < n_tall)) { - anyWithinBounds=true; - if (weightFac[Ix_test][Iy_test] >= 0.0) { - sum += weightFac[Ix_test][Iy_test]; - n_sum++; - } - } - } - } - //along the bottom - Iy_test = Iy - step; - if ((Iy_test >= 0) && (Iy_test < n_tall)) { - for (Ix_test=Ix-step; Ix_test<=Ix+step; Ix_test++) { - if ((Ix_test >=0) && (Ix_test < n_wide)) { - anyWithinBounds=true; - if (weightFac[Ix_test][Iy_test] >= 0.0) { - sum += weightFac[Ix_test][Iy_test]; - n_sum++; - } - } - } - } - - //along the left - Ix_test = Ix - step; - if ((Ix_test >= 0) && (Ix_test < n_wide)) { - for (Iy_test=Iy-step; Iy_test<=Iy+step; Iy_test++) { - if ((Iy_test >=0) && (Iy_test < n_tall)) { - anyWithinBounds=true; - if (weightFac[Ix_test][Iy_test] >= 0.0) { - sum += weightFac[Ix_test][Iy_test]; - n_sum++; - } - } - } - } - - if (n_sum > 0) { - //some good pixels were found, so we have our answer - new_weightFac = sum / n_sum; //complete the averaging process - done = true; //we're done - } else { - //we did not find any good pixels. Step outward one more pixel and repeat the search - step++; //step outwward - if (anyWithinBounds) { //did the last iteration have some pixels that were at least within the domain - //some pixels were within the domain, so we have space to try again - done = false; - } else { - //no pixels were within the domain. We're out of space. We're done. - done = true; - } - } - } - return new_weightFac; //good or bad, return our new value - } - - private void computeWeightFactorsGivenOneElectrode_iterative(int toPixels[][][][], int toElectrodes[][][], int Ielec, float pixelVal[][][]) { - //Approach: pretend that one electrode is set to 1.0 and that all other electrodes are set to 0.0. - //Assume all of the pixels start at zero. Then, begin the simulation as if it were a transient - //solution where energy is coming in from the connections. Any excess energy will accumulate - //and cause the local pixel's value to increase. Iterate until the pixel values stabalize. - - int n_wide = toPixels.length; - int n_tall = toPixels[0].length; - int n_dir = toPixels[0][0].length; - float prevVal[][] = new float[n_wide][n_tall]; - float total, dVal; - int Ix_targ, Iy_targ; - float min_val=0.0f, max_val=0.0f; - boolean anyConnections = false; - int pixel_step = 1; - - //initialize all pixels to zero - //for (int Ix=0; Ix dVal_threshold)) { - //increment the counter - iter_count++; - - //reset our test value to a large value - max_dVal = 0.0f; - - //reset other values that I'm using for debugging - min_val = 1000.0f; //init to a big val - max_val = -1000.f; //init to a small val - - //copy current values - for (int Ix=0; Ix -1) { - Ix_targ = toPixels[Ix][Iy][Idir][0]; //x index of target pixel - Iy_targ = toPixels[Ix][Iy][Idir][1]; //y index of target pixel - total += (prevVal[Ix_targ][Iy_targ]-prevVal[Ix][Iy]); //difference relative to target pixel - anyConnections = true; - } - //do we connect to an electrode? - if (toElectrodes[Ix][Iy][Idir] > -1) { - //do we connect to the electrode that we're stimulating - if (toElectrodes[Ix][Iy][Idir] == Ielec) { - //yes, this is the active high one - total += (1.0-prevVal[Ix][Iy]); //difference relative to HIGH electrode - } else { - //no, this is a low one - total += (0.0-prevVal[Ix][Iy]); //difference relative to the LOW electrode - } - anyConnections = true; - } - } - - //compute the new pixel value - //if (numConnections[Ix][Iy] > 0) { - if (anyConnections) { - - //dVal = change_fac * (total - float(numConnections[Ix][Iy])*prevVal[Ix][Iy]); - dVal = change_fac * total; - pixelVal[Ielec][Ix][Iy] = prevVal[Ix][Iy] + dVal; - - //is this our worst change in value? - max_dVal = max(max_dVal, abs(dVal)); - - //update our other debugging values, too - min_val = min(min_val, pixelVal[Ielec][Ix][Iy]); - max_val = max(max_val, pixelVal[Ielec][Ix][Iy]); - } else { - pixelVal[Ielec][Ix][Iy] = -1.0; //means that there are no connections - } - } - } - //println("headPlot: computeWeightFactor: Ielec " + Ielec + ", iter = " + iter_count + ", max_dVal = " + max_dVal); - } - //println("headPlot: computeWeightFactor: Ielec " + Ielec + ", solution complete with " + iter_count + " iterations. min and max vals = " + min_val + ", " + max_val); - if (iter_count >= lim_iter_count) println("headPlot: computeWeightFactor: Ielec " + Ielec + ", solution complete with " + iter_count + " iterations. max_dVal = " + max_dVal); - } //end of method - - private void makeAllTheConnections(boolean withinHead[][], int withinElectrode[][], int toPixels[][][][], int toElectrodes[][][]) { - - int n_wide = toPixels.length; - int n_tall = toPixels[0].length; - int n_elec = electrode_xy.length; - int curPixel, Ipix, Ielec; - int n_pixels = n_wide * n_tall; - int Ix_try, Iy_try; - - //loop over every pixel in the image - for (int Iy=0; Iy < n_tall; Iy++) { - for (int Ix=0; Ix < n_wide; Ix++) { - - //loop over the four connections: left, right, up, down - for (int Idirection = 0; Idirection < 4; Idirection++) { - - Ix_try = -1; - Iy_try=-1; //nonsense values - switch (Idirection) { - case 0: - Ix_try = Ix-1; - Iy_try = Iy; //left - break; - case 1: - Ix_try = Ix+1; - Iy_try = Iy; //right - break; - case 2: - Ix_try = Ix; - Iy_try = Iy-1; //up - break; - case 3: - Ix_try = Ix; - Iy_try = Iy+1; //down - break; - } - - //initalize to no connection - toPixels[Ix][Iy][Idirection][0] = -1; - toPixels[Ix][Iy][Idirection][1] = -1; - toElectrodes[Ix][Iy][Idirection] = -1; - - //does the target pixel exist - if ((Ix_try >= 0) && (Ix_try < n_wide) && (Iy_try >= 0) && (Iy_try < n_tall)) { - //is the target pixel an electrode - if (withinElectrode[Ix_try][Iy_try] >= 0) { - //the target pixel is within an electrode - toElectrodes[Ix][Iy][Idirection] = withinElectrode[Ix_try][Iy_try]; - } else { - //the target pixel is not within an electrode. is it within the head? - if (withinHead[Ix_try][Iy_try]) { - toPixels[Ix][Iy][Idirection][0] = Ix_try; //save the address of the target pixel - toPixels[Ix][Iy][Idirection][1] = Iy_try; //save the address of the target pixel - } - } - } - } //end loop over direction of the target pixel - } //end loop over Ix - } //end loop over Iy - } // end of method - - private void whereAreThePixels(int pixelAddress[][][], boolean[][] withinHead, int[][] withinElectrode) { - int n_wide = pixelAddress.length; - int n_tall = pixelAddress[0].length; - int n_elec = electrode_xy.length; - int pixel_x, pixel_y; - int withinElecInd=-1; - float dist; - float elec_radius = 0.5*elec_diam; - - for (int Iy=0; Iy < n_tall; Iy++) { - //pixel_y = image_y + Iy; - for (int Ix = 0; Ix < n_wide; Ix++) { - //pixel_x = image_x + Ix; - - pixel_x = pixelAddress[Ix][Iy][0]+image_x; - pixel_y = pixelAddress[Ix][Iy][1]+image_y; - - //is it within the head - withinHead[Ix][Iy] = isPixelInsideHead(pixel_x, pixel_y); - - //compute distances of this pixel to each electrode - withinElecInd = -1; //reset for this pixel - for (int Ielec=0; Ielec < n_elec; Ielec++) { - //compute distance - dist = max(1.0, calcDistance(pixel_x, pixel_y, electrode_xy[Ielec][0], electrode_xy[Ielec][1])); - if (dist < elec_radius) withinElecInd = Ielec; - } - withinElectrode[Ix][Iy] = withinElecInd; //-1 means not inside an electrode - } //close Ix loop - } //close Iy loop - - //ensure that each electrode is at at least one pixel - for (int Ielec=0; Ielec= 0.0) { //zero and positive values are inside the head - //it is inside the head. set the color based on the electrodes - headImage.set(Ix, Iy, calcPixelColor(Ix, Iy)); - } else { //negative values are outside of the head - //pixel is outside the head. set to black. - headImage.set(Ix, Iy, WHITE); - } - } - } - } - - private void convertVoltagesToHeadImage() { - for (int Iy=0; Iy < headImage.height; Iy++) { - for (int Ix = 0; Ix < headImage.width; Ix++) { - //is this pixel inside the head? - if (electrode_color_weightFac[0][Ix][Iy] >= 0.0) { //zero and positive values are inside the head - //it is inside the head. set the color based on the electrodes - headVoltage[Ix][Iy] = calcPixelVoltage(Ix, Iy, headVoltage[Ix][Iy]); - headImage.set(Ix, Iy, calcPixelColor(headVoltage[Ix][Iy])); - } else { //negative values are outside of the head - //pixel is outside the head. set to black. - headVoltage[Ix][Iy] = -1.0; - headImage.set(Ix, Iy, WHITE); - } - } - } - } - - private float calcPixelVoltage(int pixel_Ix, int pixel_Iy, float prev_val) { - float weight, elec_volt; - int n_elec = electrode_xy.length; - float voltage = 0.0f; - float low = intense_min_uV; - float high = intense_max_uV; - - for (int Ielec=0; Ielec 0.0f) voltage = smooth_fac*prev_val + (1.0-smooth_fac)*voltage; - - return voltage; - } - - - private color calcPixelColor(float pixel_volt_uV) { - // float new_rgb[] = {255.0, 0.0, 0.0}; //init to red - //224, 56, 45 - float new_rgb[] = {224.0, 56.0, 45.0}; //init to red - // float new_rgb[] = {0.0, 255.0, 0.0}; //init to red - //54, 87, 158 - if (pixel_volt_uV < 0.0) { - //init to blue instead - new_rgb[0]=54.0; - new_rgb[1]=87.0; - new_rgb[2]=158.0; - // new_rgb[0]=0.0; - // new_rgb[1]=0.0; - // new_rgb[2]=255.0; - } - float val; - - - float intensity = constrain(abs(pixel_volt_uV), intense_min_uV, intense_max_uV); - if (plot_color_as_log) { - intensity = map(log10(intensity), - log10_intense_min_uV, - log10_intense_max_uV, - 0.0f, 1.0f); - } else { - intensity = map(intensity, - intense_min_uV, - intense_max_uV, - 0.0f, 1.0f); - } - - //make the intensity fade NOT from black->color, but from white->color - for (int i=0; i < 3; i++) { - val = ((float)new_rgb[i]) / 255.f; - new_rgb[i] = ((val + (1.0f - val)*(1.0f-intensity))*255.f); //adds in white at low intensity. no white at high intensity - new_rgb[i] = constrain(new_rgb[i], 0.0, 255.0); - } - - //quantize the color to make contour-style plot? - if (true) quantizeColor(new_rgb); - - return color(int(new_rgb[0]), int(new_rgb[1]), int(new_rgb[2]), 255); - } - - private void quantizeColor(float new_rgb[]) { - int n_colors = 12; - int ticks_per_color = 256 / (n_colors+1); - for (int Irgb=0; Irgb<3; Irgb++) new_rgb[Irgb] = min(255.0, float(int(new_rgb[Irgb]/ticks_per_color))*ticks_per_color); - } - - - //compute the color of the pixel given the location - private color calcPixelColor(int pixel_Ix, int pixel_Iy) { - float weight; - - //compute the weighted average using the precomputed factors - float new_rgb[] = {0.0, 0.0, 0.0}; //init to zeros - for (int Ielec=0; Ielec < electrode_xy.length; Ielec++) { - //int Ielec = 0; - weight = electrode_color_weightFac[Ielec][pixel_Ix][pixel_Iy]; - for (int Irgb=0; Irgb<3; Irgb++) { - new_rgb[Irgb] += weight*electrode_rgb[Irgb][Ielec]; - } - } - - //quantize the color to make contour-style plot? - if (true) quantizeColor(new_rgb); - - return color(int(new_rgb[0]), int(new_rgb[1]), int(new_rgb[2]), 255); - } - - private float calcDistance(int x, int y, float ref_x, float ref_y) { - float dx = float(x) - ref_x; - float dy = float(y) - ref_y; - return sqrt(dx*dx + dy*dy); - } - - //compute color for the electrode value - private void updateElectrodeColors() { - int rgb[] = new int[]{255, 0, 0}; //color for the electrode when fully light - float intensity; - float val; - int new_rgb[] = new int[3]; - float low = intense_min_uV; - float high = intense_max_uV; - float log_low = log10_intense_min_uV; - float log_high = log10_intense_max_uV; - for (int Ielec=0; Ielec < electrode_xy.length; Ielec++) { - intensity = constrain(intensity_data_uV[Ielec], low, high); - if (plot_color_as_log) { - intensity = map(log10(intensity), log_low, log_high, 0.0f, 1.0f); - } else { - intensity = map(intensity, low, high, 0.0f, 1.0f); - } - - //make the intensity fade NOT from black->color, but from white->color - for (int i=0; i < 3; i++) { - val = ((float)rgb[i]) / 255.f; - new_rgb[i] = (int)((val + (1.0f - val)*(1.0f-intensity))*255.f); //adds in white at low intensity. no white at high intensity - new_rgb[i] = constrain(new_rgb[i], 0, 255); - } - - //change color to dark RED if railed - if (is_railed[Ielec].is_railed) new_rgb = new int[]{127, 0, 0}; - - //set the electrode color - electrode_rgb[0][Ielec] = new_rgb[0]; - electrode_rgb[1][Ielec] = new_rgb[1]; - electrode_rgb[2][Ielec] = new_rgb[2]; - } - } - - private boolean isMouseOverElectrode(int n){ - float elec_mouse_x_dist = electrode_xy[n][0] - mouseX; - float elec_mouse_y_dist = electrode_xy[n][1] - mouseY; - return elec_mouse_x_dist * elec_mouse_x_dist + elec_mouse_y_dist * elec_mouse_y_dist < elec_diam * elec_diam / 4; - } - - private boolean isDraggedElecInsideHead() { - int dx = mouseX - circ_x; - int dy = mouseY - circ_y; - return dx * dx + dy * dy < (circ_diam - elec_diam) * (circ_diam - elec_diam) / 4; - } - - void mousePressed() { - if (mouse_over_elec_index > -1) { - isDragging = true; - drag_x = mouseX - electrode_xy[mouse_over_elec_index][0]; - drag_y = mouseY - electrode_xy[mouse_over_elec_index][1]; - } else { - isDragging = false; - } - } - - void mouseDragged() { - if (isDragging && mouse_over_elec_index > -1 && isDraggedElecInsideHead()) { - electrode_xy[mouse_over_elec_index][0] = mouseX - drag_x; - electrode_xy[mouse_over_elec_index][1] = mouseY - drag_y; - } - } - - void mouseReleased() { - isDragging = false; - } - - public boolean isPixelInsideHead(int pixel_x, int pixel_y) { - int dx = pixel_x - circ_x; - int dy = pixel_y - circ_y; - float r = sqrt(float(dx*dx) + float(dy*dy)); - if (r <= 0.5*circ_diam) { - return true; - } else { - return false; - } - } - - public void update() { - //do this when new data is available - if (!hardCalcsDone) { - thread("doHardCalcs"); - } - - //update electrode colors - updateElectrodeColors(); - - if (false) { - //update the head image - if (drawHeadAsContours) updateHeadImage(); - } else { - //update head voltages - if (!threadLock && hardCalcsDone) { - convertVoltagesToHeadImage(); - } - } - } - - public void draw() { - - if (!hardCalcsDone) { - return; - } - - pushStyle(); - smooth(); - //draw head parts - fill(WHITE); - stroke(GREY_125); - triangle(nose_x[0], nose_y[0], nose_x[1], nose_y[1], nose_x[2], nose_y[2]); //nose - ellipse(earL_x, earL_y, ear_width, ear_height); //little circle for the ear - ellipse(earR_x, earR_y, ear_width, ear_height); //little circle for the ear - - //draw head itself - fill(WHITE); //fill in a white head - strokeWeight(1); - ellipse(circ_x, circ_y, circ_diam, circ_diam); //big circle for the head - if (drawHeadAsContours) { - //add the contnours - image(headImage, image_x, image_y); - noFill(); //overlay a circle as an outline, but no fill - strokeWeight(1); - ellipse(circ_x, circ_y, circ_diam, circ_diam); //big circle for the head - } - - //draw electrodes on the head - if (!isDragging) { - mouse_over_elec_index = -1; - } - for (int Ielec=0; Ielec < electrode_xy.length; Ielec++) { - if (drawHeadAsContours) { - noFill(); //make transparent to allow color to come through from below - } else { - fill(electrode_rgb[0][Ielec], electrode_rgb[1][Ielec], electrode_rgb[2][Ielec]); - } - if (!isDragging && isMouseOverElectrode(Ielec)) { - //electrode with a bigger index gets priority in dragging - mouse_over_elec_index = Ielec; - strokeWeight(2); - } else if (mouse_over_elec_index == Ielec) { - strokeWeight(2); - } else{ - strokeWeight(1); - } - ellipse(electrode_xy[Ielec][0], electrode_xy[Ielec][1], elec_diam, elec_diam); //electrode circle - } - - //add labels to electrodes - fill(OPENBCI_DARKBLUE); - textFont(font); - textAlign(CENTER, CENTER); - for (int i=0; i < electrode_xy.length; i++) { - //text(Integer.toString(i),electrode_xy[i][0], electrode_xy[i][1]); - text(i+1, electrode_xy[i][0], electrode_xy[i][1]); - } - text("R", ref_electrode_xy[0], ref_electrode_xy[1]); - - popStyle(); - } //end of draw method -}; diff --git a/OpenBCI_GUI/W_Marker.pde b/OpenBCI_GUI/W_Marker.pde index 7eaaedb08..9aa472ed9 100644 --- a/OpenBCI_GUI/W_Marker.pde +++ b/OpenBCI_GUI/W_Marker.pde @@ -8,8 +8,7 @@ // // ////////////////////////////////////////////////////// -class W_Marker extends Widget { - +class W_Marker extends WidgetWithSettings { private ControlP5 localCP5; private List cp5ElementsToCheckForOverlap; @@ -25,8 +24,10 @@ class W_Marker extends Widget { private Textfield markerReceiveIPTextfield; private Textfield markerReceivePortTextfield; - private String markerReceiveIP = "127.0.0.1"; - private int markerReceivePort = 12340; + private final String KEY_MARKER_RECEIVE_IP = "markerReceiveIPTextfield"; + private final String KEY_MARKER_RECEIVE_PORT = "markerReceivePortTextfield"; + private final String DEFAULT_RECEIVER_IP = "127.0.0.1"; + private final int DEFAULT_RECEIVER_PORT = 12340; private final int MARKER_RECEIVE_TEXTFIELD_WIDTH = 108; private final int MARKER_RECEIVE_TEXTFIELD_HEIGHT = 22; @@ -42,11 +43,9 @@ class W_Marker extends Widget { private int PAD_FIVE = 5; private int GRAPH_PADDING = 30; - private MarkerVertScale markerVertScale = MarkerVertScale.EIGHT; - private MarkerWindow markerWindow = MarkerWindow.FIVE; - - W_Marker(PApplet _parent){ - super(_parent); + W_Marker() { + super(); + widgetTitle = "Marker"; //Instantiate local cp5 for this box. This allows extra control of drawing cp5 elements specifically inside this class. localCP5 = new ControlP5(ourApplet); @@ -54,12 +53,11 @@ class W_Marker extends Widget { localCP5.setAutoDraw(false); createMarkerButtons(); - - addDropdown("markerVertScaleDropdown", "Vert Scale", markerVertScale.getEnumStringsAsList(), markerVertScale.getIndex()); - addDropdown("markerWindowDropdown", "Window", markerWindow.getEnumStringsAsList(), markerWindow.getIndex()); updateGraphDims(); - markerBar = new MarkerBar(_parent, MAX_NUMBER_OF_MARKER_BUTTONS, markerWindow.getValue(), markerVertScale.getValue(), graphX, graphY, graphW, graphH); + MarkerWindow markerWindow = widgetSettings.get(MarkerWindow.class); + MarkerVertScale markerVertScale = widgetSettings.get(MarkerVertScale.class); + markerBar = new MarkerBar(ourApplet, MAX_NUMBER_OF_MARKER_BUTTONS, markerWindow.getValue(), markerVertScale.getValue(), graphX, graphY, graphW, graphH); grid = new Grid(MARKER_UI_GRID_ROWS, MARKER_UI_GRID_COLUMNS, MARKER_UI_GRID_CELL_HEIGHT); grid.setDrawTableBorder(false); @@ -80,6 +78,57 @@ class W_Marker extends Widget { cp5ElementsToCheckForOverlap.add(markerReceiveToggle); } + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + widgetSettings.set(MarkerVertScale.class, MarkerVertScale.EIGHT) + .set(MarkerWindow.class, MarkerWindow.FIVE) + .setObject(KEY_MARKER_RECEIVE_IP, DEFAULT_RECEIVER_IP) + .setObject(KEY_MARKER_RECEIVE_PORT, DEFAULT_RECEIVER_PORT) + .saveDefaults(); + + initDropdown(MarkerVertScale.class, "markerVerticalScaleDropdown", "Vert Scale"); + initDropdown(MarkerWindow.class, "markerWindowDropdown", "Window"); + } + + @Override + protected void applySettings() { + updateDropdownLabel(MarkerVertScale.class, "markerVerticalScaleDropdown"); + updateDropdownLabel(MarkerWindow.class, "markerWindowDropdown"); + + applyVerticalScale(); + applyWindow(); + + String ipValue = widgetSettings.getObject(KEY_MARKER_RECEIVE_IP, DEFAULT_RECEIVER_IP).toString(); + String portValue = widgetSettings.getObject(KEY_MARKER_RECEIVE_PORT, DEFAULT_RECEIVER_PORT).toString(); + + markerReceiveIPTextfield.setText(ipValue); + markerReceivePortTextfield.setText(portValue); + } + + @Override + protected void saveSettings() { + // Call the parent method to handle default saving behavior + super.saveSettings(); + + // Save our marker-specific settings + saveMarkerSettings(); + } + + private void saveMarkerSettings() { + // Get the current values from textfields + String currentIP = markerReceiveIPTextfield.getText(); + String currentPort = markerReceivePortTextfield.getText(); + + // Clean up the values + currentIP = getIpAddrFromStr(currentIP); + Integer currentPortInt = Integer.parseInt(dropNonPrintableChars(currentPort)); + + // Save values to widget settings + widgetSettings.setObject(KEY_MARKER_RECEIVE_IP, currentIP); + widgetSettings.setObject(KEY_MARKER_RECEIVE_PORT, currentPortInt); + } + public void update(){ super.update(); markerBar.update(); @@ -88,7 +137,6 @@ class W_Marker extends Widget { textfieldUpdateHelper.checkTextfield(markerReceivePortTextfield); lockElementsOnOverlapCheck(cp5ElementsToCheckForOverlap); - } public void draw(){ @@ -207,8 +255,8 @@ class W_Marker extends Widget { } private void createMarkerReceiveUI() { - markerReceiveIPTextfield = createTextfield("markerReceiveIPTextfield", markerReceiveIP); - markerReceivePortTextfield = createTextfield("markerReceivePortTextfield", Integer.toString(markerReceivePort)); + markerReceiveIPTextfield = createTextfield(KEY_MARKER_RECEIVE_IP, DEFAULT_RECEIVER_IP); + markerReceivePortTextfield = createTextfield(KEY_MARKER_RECEIVE_PORT, Integer.toString(DEFAULT_RECEIVER_PORT)); createMarkerReceiveToggle(); } @@ -241,6 +289,8 @@ class W_Marker extends Widget { public void controlEvent(CallbackEvent theEvent) { if (theEvent.getAction() == ControlP5.ACTION_BROADCAST && myTextfield.getText().equals("")) { resetMarkerReceiveTextfield(myTextfield); + } else if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + saveMarkerSettings(); } } }); @@ -250,16 +300,17 @@ class W_Marker extends Widget { if (!myTextfield.isActive() && myTextfield.getText().equals("")) { resetMarkerReceiveTextfield(myTextfield); } + saveMarkerSettings(); } }); return myTextfield; } private void resetMarkerReceiveTextfield(Textfield tf) { - if (tf.getName().equals("markerReceiveIPTextfield")) { - tf.setText(markerReceiveIP); - } else if (tf.getName().equals("markerReceivePortTextfield")) { - tf.setText(Integer.toString(markerReceivePort)); + if (tf.getName().equals(KEY_MARKER_RECEIVE_IP)) { + tf.setText(DEFAULT_RECEIVER_IP); + } else if (tf.getName().equals(KEY_MARKER_RECEIVE_PORT)) { + tf.setText(Integer.toString(DEFAULT_RECEIVER_PORT)); } } @@ -288,24 +339,24 @@ class W_Marker extends Widget { } private void initUdpMarkerReceiver() { - markerReceiveIP = getIpAddrFromStr(markerReceiveIPTextfield.getText()); - markerReceivePort = Integer.parseInt(dropNonPrintableChars(markerReceivePortTextfield.getText())); + String currentIP = getIpAddrFromStr(markerReceiveIPTextfield.getText()); + Integer currentPort = Integer.parseInt(dropNonPrintableChars(markerReceivePortTextfield.getText())); disposeUdpMarkerReceiver(); - udpReceiver = new UDP(ourApplet, markerReceivePort, markerReceiveIP); + udpReceiver = new UDP(ourApplet, currentPort, currentIP); udpReceiver.listen(true); udpReceiver.broadcast(false); udpReceiver.log(false); udpReceiver.setReceiveHandler("receiveMarkerViaUdp"); - outputSuccess("Marker Widget: Listening for markers on " + markerReceiveIP + ":" + markerReceivePort); + outputSuccess("Marker Widget: Listening for markers on " + currentIP + ":" + currentPort); } public void disposeUdpMarkerReceiver() { if (udpReceiver != null) { udpReceiver.close(); udpReceiver.dispose(); - println("Marker Widget: Stopped listening for markers on " + markerReceiveIP + ":" + markerReceivePort); + println("Marker Widget: Stopped listening for markers"); } } @@ -330,32 +381,46 @@ class W_Marker extends Widget { } public void setMarkerWindow(int n) { - markerWindow = markerWindow.values()[n]; - markerBar.adjustTimeAxis(markerWindow.getValue()); + widgetSettings.setByIndex(MarkerWindow.class, n); + applyWindow(); } - public void setMarkerVertScale(int n) { - markerVertScale = markerVertScale.values()[n]; - markerBar.adjustYAxis(markerVertScale.getValue()); + public void setMarkerVerticalScale(int n) { + widgetSettings.setByIndex(MarkerVertScale.class, n); + applyVerticalScale(); } - public MarkerWindow getMarkerWindow() { - return markerWindow; + private void applyWindow() { + int markerWindowValue = widgetSettings.get(MarkerWindow.class).getValue(); + markerBar.adjustTimeAxis(markerWindowValue); } - public MarkerVertScale getMarkerVertScale() { - return markerVertScale; + private void applyVerticalScale() { + int markerVertScaleValue = widgetSettings.get(MarkerVertScale.class).getValue(); + markerBar.adjustYAxis(markerVertScaleValue); } +}; - public String getMarkerReceiveIP() { - return getIpAddrFromStr(markerReceiveIPTextfield.getText()); - } - public String getMarkerReceivePort() { - return dropNonPrintableChars(markerReceivePortTextfield.getText()); - } +//The following global functions are used by the Marker widget dropdowns. This method is the least amount of code. +public void markerWindowDropdown(int n) { + W_Marker markerWidget = (W_Marker) widgetManager.getWidget("W_Marker"); + markerWidget.setMarkerWindow(n); +} -}; +public void markerVerticalScaleDropdown(int n) { + W_Marker markerWidget = (W_Marker) widgetManager.getWidget("W_Marker"); + markerWidget.setMarkerVerticalScale(n); +} + +//Custom UDP receive handler for receiving markers from external sources +public void receiveMarkerViaUdp( byte[] data, String ip, int port ) { + double markerValue = convertByteArrayToDouble(data); + //String message = Double.toString(markerValue); + //println( "received: \""+message+"\" from "+ip+" on port "+port ); + W_Marker markerWidget = (W_Marker) widgetManager.getWidget("W_Marker"); + markerWidget.insertMarkerFromExternal(markerValue); +} //This class contains the time series plot for displaying the markers over time class MarkerBar { @@ -383,7 +448,7 @@ class MarkerBar { private float autoscaleMax; private int previousMillis = 0; - MarkerBar(PApplet _parent, int _yAxisMax, int markerWindow, float yLimit, int _x, int _y, int _w, int _h) { //channel number, x/y location, height, width + MarkerBar(PApplet _parentApplet, int _yAxisMax, int markerWindow, float yLimit, int _x, int _y, int _w, int _h) { //channel number, x/y location, height, width yAxisMax = _yAxisMax; numSeconds = markerWindow; @@ -395,7 +460,7 @@ class MarkerBar { w = _w; h = _h; - plot = new GPlot(_parent); + plot = new GPlot(_parentApplet); plot.setPos(x + 36 + 4, y); //match marker plot position with Time Series plot.setDim(w - 36 - 4, h); plot.setMar(0f, 0f, 0f, 0f); @@ -510,107 +575,3 @@ class MarkerBar { } }; -public enum MarkerWindow implements IndexingInterface -{ - FIVE (0, 5, "5 sec"), - TEN (1, 10, "10 sec"), - TWENTY (2, 20, "20 sec"); - - private int index; - private int value; - private String label; - private static MarkerWindow[] vals = values(); - - MarkerWindow(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 (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -public enum MarkerVertScale implements IndexingInterface -{ - AUTO (0, 0, "Auto"), - TWO (1, 2, "2"), - FOUR (2, 4, "4"), - EIGHT (3, 8, "8"), - TEN (4, 10, "10"), - TWENTY (6, 20, "20"); - - private int index; - private int value; - private String label; - private static MarkerVertScale[] vals = values(); - - MarkerVertScale(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 (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -//The following global functions are used by the Marker widget dropdowns. This method is the least amount of code. -public void markerWindowDropdown(int n) { - w_marker.setMarkerWindow(n); -} - -public void markerVertScaleDropdown(int n) { - w_marker.setMarkerVertScale(n); -} - -//Custom UDP receive handler for receiving markers from external sources -public void receiveMarkerViaUdp( byte[] data, String ip, int port ) { - double markerValue = convertByteArrayToDouble(data); - //String message = Double.toString(markerValue); - //println( "received: \""+message+"\" from "+ip+" on port "+port ); - w_marker.insertMarkerFromExternal(markerValue); -} - -public double convertByteArrayToDouble(byte[] array) { - ByteBuffer buffer = ByteBuffer.wrap(array); - return buffer.getDouble(); -} \ No newline at end of file diff --git a/OpenBCI_GUI/W_PacketLoss.pde b/OpenBCI_GUI/W_PacketLoss.pde index ae8c0d943..f8d8be27d 100644 --- a/OpenBCI_GUI/W_PacketLoss.pde +++ b/OpenBCI_GUI/W_PacketLoss.pde @@ -37,8 +37,9 @@ class W_PacketLoss extends Widget { private CalculationWindowSize tableWindowSize = CalculationWindowSize.SECONDS10; - W_PacketLoss(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_PacketLoss() { + super(); + widgetTitle = "Packet Loss"; dataGrid = new Grid(5/*numRows*/, 4/*numCols*/, CELL_HEIGHT); packetLossTracker = ((Board)currentBoard).getPacketLossTracker(); @@ -63,7 +64,7 @@ class W_PacketLoss extends Widget { tableDropdown = cp5_widget.addScrollableList("TableTimeWindow") .setDrawOutline(false) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(OBJECT_BORDER_GREY) .setBarHeight(CELL_HEIGHT) //height of top/primary bar .setItemHeight(CELL_HEIGHT) //height of all item/dropdown bars @@ -102,7 +103,7 @@ class W_PacketLoss extends Widget { } public void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); lastMillisPacketRecord = packetLossTracker.getCumulativePacketRecordForLast(tableWindowSize.getMilliseconds()); @@ -129,7 +130,7 @@ class W_PacketLoss extends Widget { } public void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); pushStyle(); fill(OPENBCI_DARKBLUE); @@ -142,17 +143,17 @@ class W_PacketLoss extends Widget { } public void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); resizeGrid(); } public void mousePressed(){ - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); } public void mouseReleased(){ - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + super.mouseReleased(); } diff --git a/OpenBCI_GUI/W_Playback.pde b/OpenBCI_GUI/W_Playback.pde index b200dffca..a5feb34c2 100644 --- a/OpenBCI_GUI/W_Playback.pde +++ b/OpenBCI_GUI/W_Playback.pde @@ -21,20 +21,21 @@ class W_playback extends Widget { private boolean menuHasUpdated = false; - W_playback(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_playback() { + super(); + widgetTitle = "Playback History"; - cp5_playback = new ControlP5(pApplet); + cp5_playback = new ControlP5(ourApplet); cp5_playback.setGraphics(ourApplet, 0,0); cp5_playback.setAutoDraw(false); int initialWidth = w - padding*2; createPlaybackMenuList(cp5_playback, "playbackMenuList", x + padding/2, y + 2, initialWidth, h - padding*2, p3); - createSelectPlaybackFileButton("selectPlaybackFile_Session", "Select Playback File", x + w/2 - (padding*2), y - navHeight + 2, 200, navHeight - 6); + createSelectPlaybackFileButton("selectPlaybackFile_Session", "Select Playback File", x + w/2 - (padding*2), y - NAV_HEIGHT + 2, 200, NAV_HEIGHT - 6); } void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); if (!menuHasUpdated) { refreshPlaybackList(); menuHasUpdated = true; @@ -56,7 +57,7 @@ class W_playback extends Widget { } void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); //x,y,w,h are the positioning variables of the Widget class pushStyle(); @@ -77,14 +78,14 @@ class W_playback extends Widget { } //end draw loop void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); //**IMPORTANT FOR CP5**// //This makes the cp5 objects within the widget scale properly - cp5_playback.setGraphics(pApplet, 0, 0); + cp5_playback.setGraphics(ourApplet, 0, 0); //Resize and position cp5 objects within this widget - selectPlaybackFileButton.setPosition(x + w - selectPlaybackFileButton.getWidth() - 2, y - navHeight + 2); + selectPlaybackFileButton.setPosition(x + w - selectPlaybackFileButton.getWidth() - 2, y - NAV_HEIGHT + 2); playbackMenuList.setPosition(x + padding/2, y + 2); playbackMenuList.setSize(w - padding*2, h - padding*2); diff --git a/OpenBCI_GUI/W_PulseSensor.pde b/OpenBCI_GUI/W_PulseSensor.pde index 1881847f5..f8344b07c 100644 --- a/OpenBCI_GUI/W_PulseSensor.pde +++ b/OpenBCI_GUI/W_PulseSensor.pde @@ -9,7 +9,6 @@ //////////////////////////////////////////////////// class W_PulseSensor extends Widget { - //to see all core variables/methods of the Widget class, refer to Widget.pde //put your custom variables here... private color graphStroke = #d2d2d2; @@ -62,8 +61,9 @@ class W_PulseSensor extends Widget { private AnalogCapableBoard analogBoard; - W_PulseSensor(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_PulseSensor() { + super(); + widgetTitle = "Pulse Sensor"; analogBoard = (AnalogCapableBoard)currentBoard; @@ -76,11 +76,11 @@ class W_PulseSensor extends Widget { setPulseWidgetVariables(); initializePulseFinderVariables(); - createAnalogModeButton("pulseSensorAnalogModeButton", "Turn Analog Read On", (int)(x0 + 1), (int)(y0 + navHeight + 1), 128, navHeight - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + createAnalogModeButton("pulseSensorAnalogModeButton", "Turn Analog Read On", (int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1), 128, NAV_HEIGHT - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); } public void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); if(currentBoard instanceof DataSourcePlayback) { if (((DataSourcePlayback)currentBoard) instanceof AnalogCapableBoard @@ -104,7 +104,7 @@ class W_PulseSensor extends Widget { } public void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); //remember to refer to x,y,w,h which are the positioning variables of the Widget class pushStyle(); @@ -128,10 +128,10 @@ class W_PulseSensor extends Widget { } public void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); setPulseWidgetVariables(); - analogModeButton.setPosition((int)(x0 + 1), (int)(y0 + navHeight + 1)); + analogModeButton.setPosition((int)(x0 + 1), (int)(y0 + NAV_HEIGHT + 1)); } private void createAnalogModeButton(String name, String text, int _x, int _y, int _w, int _h, PFont _font, int _fontSize, color _bg, color _textColor) { @@ -143,16 +143,16 @@ class W_PulseSensor extends Widget { analogBoard.setAnalogActive(true); analogModeButton.getCaptionLabel().setText("Turn Analog Read Off"); output("Starting to read analog inputs on pin marked D11."); - w_analogRead.toggleAnalogReadButton(true); - w_accelerometer.accelBoardSetActive(false); - w_digitalRead.toggleDigitalReadButton(false); + ((W_AnalogRead) widgetManager.getWidget("W_AnalogRead")).toggleAnalogReadButton(true); + ((W_Accelerometer) widgetManager.getWidget("W_Accelerometer")).accelBoardSetActive(false); + ((W_DigitalRead) widgetManager.getWidget("W_Accelerometer")).toggleDigitalReadButton(false); } else { analogBoard.setAnalogActive(false); analogModeButton.getCaptionLabel().setText("Turn Analog Read On"); output("Starting to read accelerometer"); - w_analogRead.toggleAnalogReadButton(false); - w_accelerometer.accelBoardSetActive(true); - w_digitalRead.toggleDigitalReadButton(false); + ((W_AnalogRead) widgetManager.getWidget("W_AnalogRead")).toggleAnalogReadButton(false); + ((W_Accelerometer) widgetManager.getWidget("W_Accelerometer")).accelBoardSetActive(true); + ((W_DigitalRead) widgetManager.getWidget("W_Accelerometer")).toggleDigitalReadButton(false); } } }); diff --git a/OpenBCI_GUI/W_Spectrogram.pde b/OpenBCI_GUI/W_Spectrogram.pde index 1d349b0de..83ab17302 100644 --- a/OpenBCI_GUI/W_Spectrogram.pde +++ b/OpenBCI_GUI/W_Spectrogram.pde @@ -3,76 +3,49 @@ // W_Spectrogram.pde // // // // // -// Created by: Richard Waltman, September 2019 // +// Created by: Richard Waltman, September 2019 // +// Refactored by: Richard Waltman, April 2025 // // // ////////////////////////////////////////////////////// -class W_Spectrogram extends Widget { - - //to see all core variables/methods of the Widget class, refer to Widget.pde - public ExGChannelSelect spectChanSelectTop; - public ExGChannelSelect spectChanSelectBot; +class W_Spectrogram extends WidgetWithSettings { + private ExGChannelSelect spectChanSelectTop; + private ExGChannelSelect spectChanSelectBot; private boolean chanSelectWasOpen = false; - List cp5ElementsToCheck = new ArrayList(); - - int xPos = 0; - int hueLimit = 160; - - PImage dataImg; - int dataImageW = 1800; - int dataImageH = 200; - int prevW = 0; - int prevH = 0; - float scaledWidth; - float scaledHeight; - int graphX = 0; - int graphY = 0; - int graphW = 0; - int graphH = 0; - int midLineY = 0; + private List cp5ElementsToCheck; + + private int xPos = 0; + private int hueLimit = 160; + + private PImage dataImg; + private int dataImageW = 1800; + private int dataImageH = 200; + private int prevW = 0; + private int prevH = 0; + private float scaledWidth; + private float scaledHeight; + private int graphX = 0; + private int graphY = 0; + private int graphW = 0; + private int graphH = 0; + private int midLineY = 0; private int lastShift = 0; private int scrollSpeed = 25; // == 40Hz private boolean wasRunning = false; - int paddingLeft = 54; - int paddingRight = 26; - int paddingTop = 8; - int paddingBottom = 50; - int numHorizAxisDivs = 2; // == 40Hz - int numVertAxisDivs = 8; - final int[][] vertAxisLabels = { - {20, 15, 10, 5, 0, 5, 10, 15, 20}, - {40, 30, 20, 10, 0, 10, 20, 30, 40}, - {60, 45, 30, 15, 0, 15, 30, 45, 60}, - {100, 75, 50, 25, 0, 25, 50, 75, 100}, - {120, 90, 60, 30, 0, 30, 60, 90, 120}, - {250, 188, 125, 63, 0, 63, 125, 188, 250} - }; - int[] vertAxisLabel; - final float[][] horizAxisLabels = { - {30, 25, 20, 15, 10, 5, 0}, - {6, 5, 4, 3, 2, 1, 0}, - {3, 2, 1, 0}, - {1.5, 1, .5, 0}, - {1, .5, 0} - }; - float[] horizAxisLabel; - StringList horizAxisLabelStrings; - - float[] topFFTAvg; - float[] botFFTAvg; - - W_Spectrogram(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - - //Add channel select dropdown to this widget - spectChanSelectTop = new DualExGChannelSelect(pApplet, x, y, w, navH, true); - spectChanSelectBot = new DualExGChannelSelect(pApplet, x, y + navH, w, navH, false); - activateDefaultChannels(); + private int paddingLeft = 54; + private int paddingRight = 26; + private int paddingTop = 8; + private int paddingBottom = 50; + private StringList horizontalAxisLabelStrings; - cp5ElementsToCheck.addAll(spectChanSelectTop.getCp5ElementsForOverlapCheck()); - cp5ElementsToCheck.addAll(spectChanSelectBot.getCp5ElementsForOverlapCheck()); + private float[] topFFTAvg; + private float[] botFFTAvg; + + W_Spectrogram() { + super(); + widgetTitle = "Spectrogram"; xPos = w - 1; //draw on the right, and shift pixels to the left prevW = w; @@ -81,64 +54,78 @@ class W_Spectrogram extends Widget { graphY = y + paddingTop; graphW = w - paddingRight - paddingLeft; graphH = h - paddingBottom - paddingTop; - - settings.spectMaxFrqSave = 2; - settings.spectSampleRateSave = 4; - settings.spectLogLinSave = 1; - vertAxisLabel = vertAxisLabels[settings.spectMaxFrqSave]; - horizAxisLabel = horizAxisLabels[settings.spectSampleRateSave]; - horizAxisLabelStrings = new StringList(); + //Fetch/calculate the time strings for the horizontal axis ticks - fetchTimeStrings(numHorizAxisDivs); - - //This is the protocol for setting up dropdowns. - //Note that these 3 dropdowns correspond to the 3 global functions below - //You just need to make sure the "id" (the 1st String) has the same name as the corresponding function - addDropdown("SpectrogramMaxFreq", "Max Freq", Arrays.asList(settings.spectMaxFrqArray), settings.spectMaxFrqSave); - addDropdown("SpectrogramSampleRate", "Window", Arrays.asList(settings.spectSampleRateArray), settings.spectSampleRateSave); - addDropdown("SpectrogramLogLin", "Log/Lin", Arrays.asList(settings.fftLogLinArray), settings.spectLogLinSave); + horizontalAxisLabelStrings = fetchTimeStrings(); //Resize the height of the data image using default - dataImageH = vertAxisLabel[0] * 2; + SpectrogramMaxFrequency maxFrequency = widgetSettings.get(SpectrogramMaxFrequency.class); + dataImageH = maxFrequency.getAxisLabels()[0] * 2; //Create image using correct dimensions! Fixes bug where image size and labels do not align on session start. dataImg = createImage(dataImageW, dataImageH, RGB); } - void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + widgetSettings.set(SpectrogramMaxFrequency.class, SpectrogramMaxFrequency.MAX_60) + .set(SpectrogramWindowSize.class, SpectrogramWindowSize.ONE_MINUTE) + .set(GraphLogLin.class, GraphLogLin.LIN); - //Update channel checkboxes, active channels, and position - spectChanSelectTop.update(x, y, w); - int chanSelectBotYOffset; - chanSelectBotYOffset = navH; - spectChanSelectBot.update(x, y + chanSelectBotYOffset, w); + initDropdown(SpectrogramMaxFrequency.class, "spectrogramMaxFrequencyDropdown", "Max Hz"); + initDropdown(SpectrogramWindowSize.class, "spectrogramWindowDropdown", "Window"); + initDropdown(GraphLogLin.class, "spectrogramLogLinDropdown", "Log/Lin"); + + spectChanSelectTop = new DualExGChannelSelect(ourApplet, x, y, w, navH, true); + spectChanSelectBot = new DualExGChannelSelect(ourApplet, x, y + navH, w, navH, false); + activateDefaultChannels(); + updateChannelSettings(); - //Let the top channel select open the bottom one also so we can open both with 1 button - if (chanSelectWasOpen != spectChanSelectTop.isVisible()) { - spectChanSelectBot.setIsVisible(spectChanSelectTop.isVisible()); - chanSelectWasOpen = spectChanSelectTop.isVisible(); - } + cp5ElementsToCheck = new ArrayList(); + cp5ElementsToCheck.addAll(spectChanSelectTop.getCp5ElementsForOverlapCheck()); + cp5ElementsToCheck.addAll(spectChanSelectBot.getCp5ElementsForOverlapCheck()); + } - //Allow spectrogram to flex size and position depending on if the channel select is open - flexSpectrogramSizeAndPosition(); + @Override + protected void applySettings() { + updateDropdownLabel(SpectrogramMaxFrequency.class, "spectrogramMaxFrequencyDropdown"); + updateDropdownLabel(SpectrogramWindowSize.class, "spectrogramWindowDropdown"); + updateDropdownLabel(GraphLogLin.class, "spectrogramLogLinDropdown"); + applyMaxFrequency(); + applyWindowSize(); + applyChannelSettings(); + } - if (spectChanSelectTop.isVisible()) { - lockElementsOnOverlapCheck(cp5ElementsToCheck); + private void applyChannelSettings() { + // Apply saved channel selections if available + if (hasNamedChannels("top")) { + applyNamedChannels("top", spectChanSelectTop); } - if (currentBoard.isStreaming()) { - //Make sure we are always draw new pixels on the right - xPos = dataImg.width - 1; - //Fetch/calculate the time strings for the horizontal axis ticks - fetchTimeStrings(numHorizAxisDivs); + if (hasNamedChannels("bottom")) { + applyNamedChannels("bottom", spectChanSelectBot); } + } + + @Override + protected void updateChannelSettings() { + // Save current channel selections before saving settings + saveChannelSettings(); + } + + private void saveChannelSettings() { + saveNamedChannels("top", spectChanSelectTop.getActiveChannels()); + saveNamedChannels("bottom", spectChanSelectBot.getActiveChannels()); + } + + @Override + public void update(){ + super.update(); + + //Update channel checkboxes, active channels, and position + updateUIState(); - //State change check - if (currentBoard.isStreaming() && !wasRunning) { - onStartRunning(); - } else if (!currentBoard.isStreaming() && wasRunning) { - onStopRunning(); - } + checkBoardStreamingState(); } private void onStartRunning() { @@ -150,185 +137,226 @@ class W_Spectrogram extends Widget { wasRunning = false; } - public void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) - - //put your code here... //remember to refer to x,y,w,h which are the positioning variables of the Widget class + @Override + public void draw() { + super.draw(); - //Scale the dataImage to fit in inside the widget float scaleW = float(graphW) / dataImageW; float scaleH = float(graphH) / dataImageH; + // Draw black background + drawBackground(); + + // Update spectrogram data if streaming + if (currentBoard.isStreaming()) { + updateSpectrogramData(); + } + + // Display the spectrogram image + displaySpectrogramImage(scaleW, scaleH); + + // Draw UI elements + spectChanSelectTop.draw(); + spectChanSelectBot.draw(); + drawAxes(scaleW, scaleH); + drawCenterLine(); + } + + private void drawBackground() { pushStyle(); fill(0); - rect(x, y, w, h); //draw a black background for the widget + rect(x, y, w, h); popStyle(); + } - //draw the spectrogram if the widget is open, and update pixels if board is streaming data - if (currentBoard.isStreaming()) { - pushStyle(); - dataImg.loadPixels(); - - //Shift all pixels to the left! (every scrollspeed ms) - if(millis() - lastShift > scrollSpeed) { - for (int r = 0; r < dataImg.height; r++) { - if (r != 0) { - arrayCopy(dataImg.pixels, dataImg.width * r, dataImg.pixels, dataImg.width * r - 1, dataImg.width); - } else { - //When there would be an ArrayOutOfBoundsException, account for it! - arrayCopy(dataImg.pixels, dataImg.width * (r + 1), dataImg.pixels, r * dataImg.width, dataImg.width); - } - } + private void displaySpectrogramImage(float scaleW, float scaleH) { + pushMatrix(); + translate(graphX, graphY); + scale(scaleW, scaleH); + image(dataImg, 0, 0); + popMatrix(); + } - lastShift += scrollSpeed; - } - //for (int i = 0; i < fftLin_L.specSize() - 80; i++) { - for (int i = 0; i <= dataImg.height/2; i++) { - //LEFT SPECTROGRAM ON TOP - float hueValue = hueLimit - map((fftAvgs(spectChanSelectTop.getActiveChannels(), i)*32), 0, 256, 0, hueLimit); - if (settings.spectLogLinSave == 0) { - hueValue = map(log10(hueValue), 0, 2, 0, hueLimit); - } - // colorMode is HSB, the range for hue is 256, for saturation is 100, brightness is 100. - colorMode(HSB, 256, 100, 100); - // color for stroke is specified as hue, saturation, brightness. - stroke(int(hueValue), 100, 80); - // plot a point using the specified stroke - //point(xPos, i); - int loc = xPos + ((dataImg.height/2 - i) * dataImg.width); - if (loc >= dataImg.width * dataImg.height) loc = dataImg.width * dataImg.height - 1; - try { - dataImg.pixels[loc] = color(int(hueValue), 100, 80); - } catch (Exception e) { - println("Major drawing error Spectrogram Left image!"); - } + private void updateSpectrogramData() { + pushStyle(); + dataImg.loadPixels(); + + // Shift pixels to the left if needed + shiftPixelsLeft(); + + // Calculate and draw new data points + drawSpectrogramPoints(); + + dataImg.updatePixels(); + popStyle(); + } - //RIGHT SPECTROGRAM ON BOTTOM - hueValue = hueLimit - map((fftAvgs(spectChanSelectBot.getActiveChannels(), i)*32), 0, 256, 0, hueLimit); - if (settings.spectLogLinSave == 0) { - hueValue = map(log10(hueValue), 0, 2, 0, hueLimit); - } - // colorMode is HSB, the range for hue is 256, for saturation is 100, brightness is 100. - colorMode(HSB, 256, 100, 100); - // color for stroke is specified as hue, saturation, brightness. - stroke(int(hueValue), 100, 80); - int y_offset = -1; - // Pixel = X + ((Y + Height/2) * Width) - loc = xPos + ((i + dataImg.height/2 + y_offset) * dataImg.width); - if (loc >= dataImg.width * dataImg.height) loc = dataImg.width * dataImg.height - 1; - try { - dataImg.pixels[loc] = color(int(hueValue), 100, 80); - } catch (Exception e) { - println("Major drawing error Spectrogram Right image!"); + private void shiftPixelsLeft() { + if (millis() - lastShift > scrollSpeed) { + for (int r = 0; r < dataImg.height; r++) { + if (r != 0) { + arrayCopy(dataImg.pixels, dataImg.width * r, dataImg.pixels, dataImg.width * r - 1, dataImg.width); + } else { + arrayCopy(dataImg.pixels, dataImg.width * (r + 1), dataImg.pixels, r * dataImg.width, dataImg.width); } } - dataImg.updatePixels(); - popStyle(); + lastShift += scrollSpeed; } + } + + private void drawSpectrogramPoints() { + GraphLogLin logLin = widgetSettings.get(GraphLogLin.class); - pushMatrix(); - translate(graphX, graphY); - scale(scaleW, scaleH); - image(dataImg, 0, 0); - popMatrix(); + for (int i = 0; i <= dataImg.height/2; i++) { + // Draw top spectrogram (left channels) + drawSpectrogramPoint(spectChanSelectTop.getActiveChannels(), i, dataImg.height/2 - i, logLin); + + // Draw bottom spectrogram (right channels) + int y_offset = -1; + drawSpectrogramPoint(spectChanSelectBot.getActiveChannels(), i, i + dataImg.height/2 + y_offset, logLin); + } + } - spectChanSelectTop.draw(); - spectChanSelectBot.draw(); - drawAxes(scaleW, scaleH); - drawCenterLine(); + private void drawSpectrogramPoint(List channels, int freqBand, int yPosition, GraphLogLin logLin) { + float hueValue = hueLimit - map((fftAvgs(channels, freqBand)*32), 0, 256, 0, hueLimit); + + if (logLin == GraphLogLin.LOG) { + hueValue = map(log10(hueValue), 0, 2, 0, hueLimit); + } + + colorMode(HSB, 256, 100, 100); + stroke(int(hueValue), 100, 80); + + int loc = xPos + (yPosition * dataImg.width); + if (loc >= dataImg.width * dataImg.height) { + loc = dataImg.width * dataImg.height - 1; + } + + try { + dataImg.pixels[loc] = color(int(hueValue), 100, 80); + } catch (Exception e) { + println("Major drawing error in Spectrogram at position: " + yPosition); + } } + @Override public void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); - spectChanSelectTop.screenResized(pApplet); - spectChanSelectBot.screenResized(pApplet); + spectChanSelectTop.screenResized(ourApplet); + spectChanSelectBot.screenResized(ourApplet); graphX = x + paddingLeft; graphY = y + paddingTop; graphW = w - paddingRight - paddingLeft; graphH = h - paddingBottom - paddingTop; } - void mousePressed(){ - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + @Override + public void mousePressed(){ + super.mousePressed(); spectChanSelectTop.mousePressed(this.dropdownIsActive); //Calls channel select mousePressed and checks if clicked spectChanSelectBot.mousePressed(this.dropdownIsActive); } - void mouseReleased(){ - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + @Override + public void mouseReleased(){ + super.mouseReleased(); + } + private void drawAxes(float scaledW, float scaledH) { + drawSpectrogramBorder(scaledW, scaledH); + drawHorizontalAxisAndLabels(scaledW, scaledH); + drawVerticalAxisAndLabels(scaledW, scaledH); + drawYAxisLabel(); + drawColorScaleReference(); } - void drawAxes(float scaledW, float scaledH) { - + private void drawSpectrogramBorder(float scaledW, float scaledH) { pushStyle(); - fill(255); - textSize(14); - //draw horizontal axis label - text("Time", x + w/2 - textWidth("Time")/3, y + h - 9); - noFill(); - stroke(255); - strokeWeight(2); - //draw rectangle around the spectrogram - rect(graphX, graphY, scaledW * dataImageW, scaledH * dataImageH); + fill(255); + textSize(14); + text("Time", x + w/2 - textWidth("Time")/3, y + h - 9); + noFill(); + stroke(255); + strokeWeight(2); + rect(graphX, graphY, scaledW * dataImageW, scaledH * dataImageH); popStyle(); + } + private void drawHorizontalAxisAndLabels(float scaledW, float scaledH) { pushStyle(); - //draw horizontal axis ticks from left to right - int tickMarkSize = 7; //in pixels - float horizAxisX = graphX; - float horizAxisY = graphY + scaledH * dataImageH; - stroke(255); - fill(255); - strokeWeight(2); - textSize(11); - for (int i = 0; i <= numHorizAxisDivs; i++) { - float offset = scaledW * dataImageW * (float(i) / numHorizAxisDivs); - line(horizAxisX + offset, horizAxisY, horizAxisX + offset, horizAxisY + tickMarkSize); - if (horizAxisLabelStrings.get(i) != null) { - text(horizAxisLabelStrings.get(i), horizAxisX + offset - (int)textWidth(horizAxisLabelStrings.get(i))/2, horizAxisY + tickMarkSize * 3); - } + int tickMarkSize = 7; + float horizontalAxisX = graphX; + float horizontalAxisY = graphY + scaledH * dataImageH; + stroke(255); + fill(255); + strokeWeight(2); + textSize(11); + + SpectrogramWindowSize windowSize = widgetSettings.get(SpectrogramWindowSize.class); + int horizontalAxisDivCount = windowSize.getAxisLabels().length; + + for (int i = 0; i < horizontalAxisDivCount; i++) { + float offset = scaledW * dataImageW * (float(i) / horizontalAxisDivCount); + line(horizontalAxisX + offset, horizontalAxisY, horizontalAxisX + offset, horizontalAxisY + tickMarkSize); + + if (horizontalAxisLabelStrings.get(i) != null) { + text(horizontalAxisLabelStrings.get(i), + horizontalAxisX + offset - (int)textWidth(horizontalAxisLabelStrings.get(i))/2, + horizontalAxisY + tickMarkSize * 3); } + } popStyle(); - + } + + private void drawYAxisLabel() { pushStyle(); - pushMatrix(); - rotate(radians(-90)); - textSize(14); - int yAxisLabelOffset = spectChanSelectTop.isVisible() ? (int)textWidth("Frequency (Hz)") / 4 : 0; - translate(-h/2 - textWidth("Frequency (Hz)")/4, 20); - fill(255); - // Draw y axis label only when channel select is not visible due to overlap - if (!spectChanSelectTop.isVisible()) { - text("Frequency (Hz)", -y - yAxisLabelOffset, x); - } - popMatrix(); + pushMatrix(); + rotate(radians(-90)); + textSize(14); + int yAxisLabelOffset = spectChanSelectTop.isVisible() ? (int)textWidth("Frequency (Hz)") / 4 : 0; + translate(-h/2 - textWidth("Frequency (Hz)")/4, 20); + fill(255); + + if (!spectChanSelectTop.isVisible()) { + text("Frequency (Hz)", -y - yAxisLabelOffset, x); + } + + popMatrix(); popStyle(); + } + private void drawVerticalAxisAndLabels(float scaledW, float scaledH) { pushStyle(); - //draw vertical axis ticks from top to bottom - float vertAxisX = graphX; - float vertAxisY = graphY; - stroke(255); - fill(255); - textSize(12); - strokeWeight(2); - for (int i = 0; i <= numVertAxisDivs; i++) { - float offset = scaledH * dataImageH * (float(i) / numVertAxisDivs); - //if (i <= numVertAxisDivs/2) offset -= 2; - line(vertAxisX, vertAxisY + offset, vertAxisX - tickMarkSize, vertAxisY + offset); - if (vertAxisLabel[i] == 0) midLineY = int(vertAxisY + offset); - offset += paddingTop/2; - text(vertAxisLabel[i], vertAxisX - tickMarkSize*2 - textWidth(Integer.toString(vertAxisLabel[i])), vertAxisY + offset); + float verticalAxisX = graphX; + float verticalAxisY = graphY; + int tickMarkSize = 7; + stroke(255); + fill(255); + textSize(12); + strokeWeight(2); + + SpectrogramMaxFrequency maxFrequency = widgetSettings.get(SpectrogramMaxFrequency.class); + int verticalAxisDivCount = maxFrequency.getAxisLabels().length - 1; + + for (int i = 0; i < verticalAxisDivCount; i++) { + float offset = scaledH * dataImageH * (float(i) / verticalAxisDivCount); + line(verticalAxisX, verticalAxisY + offset, verticalAxisX - tickMarkSize, verticalAxisY + offset); + + if (maxFrequency.getAxisLabels()[i] == 0) { + midLineY = int(verticalAxisY + offset); } + + offset += paddingTop/2; + text(maxFrequency.getAxisLabels()[i], + verticalAxisX - tickMarkSize*2 - textWidth(Integer.toString(maxFrequency.getAxisLabels()[i])), + verticalAxisY + offset); + } popStyle(); - - drawColorScaleReference(); } - void drawCenterLine() { + private void drawCenterLine() { //draw a thick line down the middle to separate the two plots pushStyle(); stroke(255); @@ -337,7 +365,7 @@ class W_Spectrogram extends Widget { popStyle(); } - void drawColorScaleReference() { + private void drawColorScaleReference() { int colorScaleHeight = 128; //Dynamically scale the Log/Lin amplitude-to-color reference line. If it won't fit, don't draw it. if (graphH < colorScaleHeight) { @@ -346,11 +374,12 @@ class W_Spectrogram extends Widget { return; } } + GraphLogLin logLin = widgetSettings.get(GraphLogLin.class); pushStyle(); //draw color scale reference to the right of the spectrogram for (int i = 0; i < colorScaleHeight; i++) { float hueValue = hueLimit - map(i * 2, 0, colorScaleHeight*2, 0, hueLimit); - if (settings.spectLogLinSave == 0) { + if (logLin == GraphLogLin.LOG) { hueValue = map(log(hueValue) / log(10), 0, 2, 0, hueLimit); } //println(hueValue); @@ -364,7 +393,7 @@ class W_Spectrogram extends Widget { popStyle(); } - void activateDefaultChannels() { + private void activateDefaultChannels() { int[] topChansToActivate; int[] botChansToActivate; @@ -389,7 +418,7 @@ class W_Spectrogram extends Widget { } } - void flexSpectrogramSizeAndPosition() { + private void flexSpectrogramSizeAndPosition() { int flexHeight = spectChanSelectTop.getHeight() + spectChanSelectBot.getHeight(); if (spectChanSelectTop.isVisible()) { graphY = y + paddingTop + flexHeight; @@ -400,11 +429,11 @@ class W_Spectrogram extends Widget { } } - void setScrollSpeed(int i) { + public void setScrollSpeed(int i) { scrollSpeed = i; } - float fftAvgs(List _activeChan, int freqBand) { + private float fftAvgs(List _activeChan, int freqBand) { float sum = 0f; for (int i = 0; i < _activeChan.size(); i++) { sum += fftBuff[_activeChan.get(i)].getBand(freqBand); @@ -412,23 +441,23 @@ class W_Spectrogram extends Widget { return sum / _activeChan.size(); } - void fetchTimeStrings(int numAxisTicks) { - horizAxisLabelStrings.clear(); + private StringList fetchTimeStrings() { + StringList output = new StringList(); LocalDateTime time; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - if (getCurrentTimeStamp() == 0) { time = LocalDateTime.now(); } else { time = LocalDateTime.ofInstant(Instant.ofEpochMilli(getCurrentTimeStamp()), TimeZone.getDefault().toZoneId()); } - - for (int i = 0; i <= numAxisTicks; i++) { - long l = (long)(horizAxisLabel[i] * 60f); + SpectrogramWindowSize windowSize = widgetSettings.get(SpectrogramWindowSize.class); + for (int i = 0; i < windowSize.getAxisLabels().length; i++) { + long l = (long)(windowSize.getAxisLabels()[i] * 60f); LocalDateTime t = time.minus(l, ChronoUnit.SECONDS); - horizAxisLabelStrings.append(t.format(formatter)); + output.append(t.format(formatter)); } + return output; } //Identical to the method in TimeSeries, but allows spectrogram to get the data directly from the playback data in the background @@ -446,49 +475,100 @@ class W_Spectrogram extends Widget { public void clear() { // Set all pixels to black (or any other background color you want to clear with) - for (int i = 0; i < w_spectrogram.dataImg.pixels.length; i++) { + for (int i = 0; i < dataImg.pixels.length; i++) { dataImg.pixels[i] = color(0); // Black background } } + + public void setLogLin(int n) { + widgetSettings.setByIndex(GraphLogLin.class, n); + } + + public void setMaxFrequency(int n) { + widgetSettings.setByIndex(SpectrogramMaxFrequency.class, n); + applyMaxFrequency(); + } + + public void setWindowSize(int n) { + widgetSettings.setByIndex(SpectrogramWindowSize.class, n); + applyWindowSize(); + } + + private void applyMaxFrequency() { + SpectrogramMaxFrequency maxFrequency = widgetSettings.get(SpectrogramMaxFrequency.class); + // Resize the height of the data image + dataImageH = maxFrequency.getAxisLabels()[0] * 2; + // Overwrite the existing image + dataImg = createImage(dataImageW, dataImageH, RGB); + } + + private void applyWindowSize() { + SpectrogramWindowSize windowSize = widgetSettings.get(SpectrogramWindowSize.class); + setScrollSpeed(windowSize.getScrollSpeed()); + horizontalAxisLabelStrings.clear(); + horizontalAxisLabelStrings = fetchTimeStrings(); + dataImg = createImage(dataImageW, dataImageH, RGB); + } + + private void resetSpectrogramImage() { + // Create a new image with the current settings + dataImg = createImage(dataImageW, dataImageH, RGB); + } + + private void updateTimeAxisLabels() { + horizontalAxisLabelStrings.clear(); + horizontalAxisLabelStrings = fetchTimeStrings(); + } + + private void checkBoardStreamingState() { + if (currentBoard.isStreaming()) { + // Update position for new data points + xPos = dataImg.width - 1; + // Update time axis labels + updateTimeAxisLabels(); + } + + // State change detection + if (currentBoard.isStreaming() && !wasRunning) { + onStartRunning(); + } else if (!currentBoard.isStreaming() && wasRunning) { + onStopRunning(); + } + } + + private void updateUIState() { + spectChanSelectTop.update(x, y, w); + int chanSelectBotYOffset = navH; + spectChanSelectBot.update(x, y + chanSelectBotYOffset, w); + + // Synchronize visibility between top and bottom channel selectors + synchronizeChannelSelectors(); + + // Update flexible layout based on channel selector visibility + flexSpectrogramSizeAndPosition(); + + // Handle UI element overlap checking + if (spectChanSelectTop.isVisible()) { + lockElementsOnOverlapCheck(cp5ElementsToCheck); + } + } + + private void synchronizeChannelSelectors() { + if (chanSelectWasOpen != spectChanSelectTop.isVisible()) { + spectChanSelectBot.setIsVisible(spectChanSelectTop.isVisible()); + chanSelectWasOpen = spectChanSelectTop.isVisible(); + } + } }; -//These functions need to be global! These functions are activated when an item from the corresponding dropdown is selected -//triggered when there is an event in the Spectrogram Widget MaxFreq. Dropdown -void SpectrogramMaxFreq(int n) { - settings.spectMaxFrqSave = n; - //reset the vertical axis labels - w_spectrogram.vertAxisLabel = w_spectrogram.vertAxisLabels[n]; - //Resize the height of the data image - w_spectrogram.dataImageH = w_spectrogram.vertAxisLabel[0] * 2; - //overwrite the existing image because the sample rate is about to change - w_spectrogram.dataImg = createImage(w_spectrogram.dataImageW, w_spectrogram.dataImageH, RGB); +public void spectrogramMaxFrequencyDropdown(int n) { + ((W_Spectrogram) widgetManager.getWidget("W_Spectrogram")).setMaxFrequency(n); } -void SpectrogramSampleRate(int n) { - settings.spectSampleRateSave = n; - //overwrite the existing image because the sample rate is about to change - w_spectrogram.dataImg = createImage(w_spectrogram.dataImageW, w_spectrogram.dataImageH, RGB); - w_spectrogram.horizAxisLabel = w_spectrogram.horizAxisLabels[n]; - if (n == 0) { - w_spectrogram.numHorizAxisDivs = 6; - w_spectrogram.setScrollSpeed(1000); - } else if (n == 1) { - w_spectrogram.numHorizAxisDivs = 6; - w_spectrogram.setScrollSpeed(200); - } else if (n == 2) { - w_spectrogram.numHorizAxisDivs = 3; - w_spectrogram.setScrollSpeed(100); - } else if (n == 3) { - w_spectrogram.numHorizAxisDivs = 3; - w_spectrogram.setScrollSpeed(50); - } else if (n == 4) { - w_spectrogram.numHorizAxisDivs = 2; - w_spectrogram.setScrollSpeed(25); - } - w_spectrogram.horizAxisLabelStrings.clear(); - w_spectrogram.fetchTimeStrings(w_spectrogram.numHorizAxisDivs); +public void spectrogramWindowDropdown(int n) { + ((W_Spectrogram) widgetManager.getWidget("W_Spectrogram")).setWindowSize(n); } -void SpectrogramLogLin(int n) { - settings.spectLogLinSave = n; -} \ No newline at end of file +public void spectrogramLogLinDropdown(int n) { + ((W_Spectrogram) widgetManager.getWidget("W_Spectrogram")).setLogLin(n); +} diff --git a/OpenBCI_GUI/W_Template.pde b/OpenBCI_GUI/W_Template.pde index 2d55fc3d4..78dd3a64f 100644 --- a/OpenBCI_GUI/W_Template.pde +++ b/OpenBCI_GUI/W_Template.pde @@ -1,92 +1,123 @@ - -//////////////////////////////////////////////////// -// -// W_template.pde (ie "Widget Template") -// -// This is a Template Widget, intended to be used as a starting point for OpenBCI Community members that want to develop their own custom widgets! -// Good luck! If you embark on this journey, please let us know. Your contributions are valuable to everyone! -// -// Created by: Conor Russomanno, November 2016 -// -///////////////////////////////////////////////////, - -class W_template extends Widget { - - //to see all core variables/methods of the Widget class, refer to Widget.pde - //put your custom variables here... - ControlP5 localCP5; - Button widgetTemplateButton; - - W_template(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - - //This is the protocol for setting up dropdowns. - //Note that these 3 dropdowns correspond to the 3 global functions below - //You just need to make sure the "id" (the 1st String) has the same name as the corresponding function - addDropdown("Dropdown1", "Drop 1", Arrays.asList("A", "B"), 0); - addDropdown("Dropdown2", "Drop 2", Arrays.asList("C", "D", "E"), 1); - addDropdown("Dropdown3", "Drop 3", Arrays.asList("F", "G", "H", "I"), 3); - - - //Instantiate local cp5 for this box. This allows extra control of drawing cp5 elements specifically inside this class. +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// +//// //// +//// W_Template.pde (ie "Widget Template") //// +//// //// +//// This is a Template Widget, intended to be //// +//// used as a starting point for OpenBCI //// +//// Community members that want to develop //// +//// their own custom widgets! //// +//// //// +//// Good luck! If you embark on this journey, //// +//// please let us know. Your contributions //// +//// are valuable to everyone! //// +//// //// +//// Created: Conor Russomanno, November 2016 //// +//// Refactored: Richard Waltman, April 2025 //// +//// //// +//////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////// + +class W_Template extends WidgetWithSettings { + + // To see all core variables/methods of the Widget class, refer to Widget.pde + // Put your custom variables here! Make sure to declare them as private by default. + // In Java, if you need to access a variable from another class, you should create a getter/setter methods. + // Example: public int getMyVariable(){ return myVariable; } + // Example: public void setMyVariable(int myVariable){ this.myVariable = myVariable; } + private ControlP5 localCP5; + private Button widgetTemplateButton; + + W_Template() { + // Call super() first! This sets up the widget and allows you to use all the methods in the Widget class. + super(); + // Set the title of the widget. This is what will be displayed in the GUI. + widgetTitle = "Widget Template"; + + // Instantiate local cp5 for this box. This allows extra control of drawing cp5 elements specifically inside this class. localCP5 = new ControlP5(ourApplet); localCP5.setGraphics(ourApplet, 0,0); localCP5.setAutoDraw(false); createWidgetTemplateButton(); - + } + + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + // Widget Settings are used to store the state of the widget. + // This is where you can set the default values for your dropdowns and other settings. + widgetSettings.set(TemplateDropdown1.class, TemplateDropdown1.ITEM_A) + .set(TemplateDropdown2.class, TemplateDropdown2.ITEM_C) + .set(TemplateDropdown3.class, TemplateDropdown3.ITEM_F) + .saveDefaults(); + + // This is the protocol for setting up dropdowns. + // Note that these 3 dropdowns correspond to the 3 global functions below. + // You just need to make sure the "id" (the 1st String) has the same name as the corresponding function. + // Arguments: Class, String id, String label + initDropdown(TemplateDropdown1.class, "widgetTemplateDropdown1", "Drop 1"); + initDropdown(TemplateDropdown2.class, "widgetTemplateDropdown2", "Drop 2"); + initDropdown(TemplateDropdown3.class, "widgetTemplateDropdown3", "Drop 3"); + } + + @Override + protected void applySettings() { + updateDropdownLabel(TemplateDropdown1.class, "widgetTemplateDropdown1"); + updateDropdownLabel(TemplateDropdown2.class, "widgetTemplateDropdown2"); + updateDropdownLabel(TemplateDropdown3.class, "widgetTemplateDropdown3"); } public void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); - //put your code here... + // put your code here... } public void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); - //remember to refer to x,y,w,h which are the positioning variables of the Widget class + // remember to refer to x,y,w,h which are the positioning variables of the Widget class - //This draws all cp5 objects in the local instance + // This draws all cp5 objects in the local instance localCP5.draw(); } public void screenResized(){ - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); - //Very important to allow users to interact with objects after app resize + // Very important to allow users to interact with objects after app resize localCP5.setGraphics(ourApplet, 0, 0); - //We need to set the position of our Cp5 object after the screen is resized + // 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); } public void mousePressed(){ - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) - //Since GUI v5, these methods should not really be used. - //Instead, use ControlP5 objects and callbacks. - //Example: createWidgetTemplateButton() found below + super.mousePressed(); + // Since GUI v5, these methods should not really be used. + // Instead, use ControlP5 objects and callbacks. + // Example: createWidgetTemplateButton() found below } public void mouseReleased(){ - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) - //Since GUI v5, these methods should not really be used. + super.mouseReleased(); + // Since GUI v5, these methods should not really be used. } - //When creating new UI objects, follow this rough pattern. - //Using custom methods like this allows us to condense the code required to create new objects. - //You can find more detailed examples in the Control Panel, where there are many UI objects with varying functionality. + // When creating new UI objects, follow this rough pattern. + // Using custom methods like this allows us to condense the code required to create new objects. + // You can find more detailed examples in the Control Panel, where there are many UI objects with varying functionality. private void createWidgetTemplateButton() { - //This is a generalized createButton method that allows us to save code by using a few patterns and method overloading - widgetTemplateButton = createButton(localCP5, "widgetTemplateButton", "Design Your Own Widget!", x + w/2, y + h/2, 200, navHeight, p4, 14, colorNotPressed, OPENBCI_DARKBLUE); - //Set the border color explicitely + // This is a generalized createButton method that allows us to save code by using a few patterns and method overloading + widgetTemplateButton = createButton(localCP5, "widgetTemplateButton", "Design Your Own Widget!", x + w/2, y + h/2, 200, NAV_HEIGHT, p4, 14, colorNotPressed, OPENBCI_DARKBLUE); + // Set the border color explicitely widgetTemplateButton.setBorderColor(OBJECT_BORDER_GREY); - //For this button, only call the callback listener on mouse release + // For this button, only call the callback listener on mouse release widgetTemplateButton.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - //If using a TopNav object, ignore interaction with widget object (ex. widgetTemplateButton) + // If using a TopNav object, ignore interaction with widget object (ex. widgetTemplateButton) if (!topNav.configSelector.isVisible && !topNav.layoutSelector.isVisible) { openURLInBrowser("https://docs.openbci.com/Software/OpenBCISoftware/GUIWidgets/#custom-widget"); } @@ -95,35 +126,44 @@ class W_template extends Widget { widgetTemplateButton.setDescription("Here is the description for this UI object. It will fade in as help text when hovering over the object."); } - //add custom functions here + // add custom functions here private void customFunction(){ - //this is a fake function... replace it with something relevant to this widget + // this is a fake function... replace it with something relevant to this widget } -}; + public void setDropdown1(int n){ + widgetSettings.setByIndex(TemplateDropdown1.class, n); + println("Item " + (n+1) + " selected from Dropdown 1"); + if(n == 0){ + println("Item A selected from Dropdown 1"); + } else if(n == 1){ + println("Item B selected from Dropdown 1"); + } + } -/** -These functions (e.g. Dropdown1()) are global! They are activated when an item from the -corresponding dropdown is selected. While it's true they could be defined in the class above -with a CallbackListener, it's not worth the trouble (and the sheer amount of duplicated code) -for this specific kind of dropdown in each widget. In some widgets, you will see that we simply -use these global methods to call a method in the widget class. This is the best pattern to follow -due to the limitations of the ControlP5 library. -**/ -void Dropdown1(int n){ - println("Item " + (n+1) + " selected from Dropdown 1"); - if(n==0){ - //do this - } else if(n==1){ - //do this instead + public void setDropdown2(int n) { + widgetSettings.setByIndex(TemplateDropdown2.class, n); + println("Item " + (n+1) + " selected from Dropdown 2"); } -} -void Dropdown2(int n){ - println("Item " + (n+1) + " selected from Dropdown 2"); + public void setDropdown3(int n) { + widgetSettings.setByIndex(TemplateDropdown3.class, n); + println("Item " + (n+1) + " selected from Dropdown 3"); + } +}; + + +public void widgetTemplateDropdown1(int n) { + // This is the callback function for the first dropdown. It will be called when the user selects an item from the dropdown. + // The parameter "n" is the index of the selected item. + ((W_Template) widgetManager.getWidget("W_Template")).setDropdown1(n); } -void Dropdown3(int n){ - println("Item " + (n+1) + " selected from Dropdown 3"); +public void widgetTemplateDropdown2(int n) { + ((W_Template) widgetManager.getWidget("W_Template")).setDropdown2(n); } + +public void widgetTemplateDropdown3(int n) { + ((W_Template) widgetManager.getWidget("W_Template")).setDropdown3(n); +} \ No newline at end of file diff --git a/OpenBCI_GUI/W_TimeSeries.pde b/OpenBCI_GUI/W_TimeSeries.pde index 61d63b3e8..26a1d4043 100644 --- a/OpenBCI_GUI/W_TimeSeries.pde +++ b/OpenBCI_GUI/W_TimeSeries.pde @@ -11,137 +11,7 @@ import org.apache.commons.lang3.math.NumberUtils; -public enum TimeSeriesXLim implements IndexingInterface -{ - ONE (0, 1, "1 sec"), - THREE (1, 3, "3 sec"), - FIVE (2, 5, "5 sec"), - TEN (3, 10, "10 sec"), - TWENTY (4, 20, "20 sec"); - - private int index; - private int value; - private String label; - private static TimeSeriesXLim[] vals = values(); - - TimeSeriesXLim(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 (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -public enum TimeSeriesYLim implements IndexingInterface -{ - AUTO (0, 0, "Auto"), - UV_10(1, 10, "10 uV"), - UV_25(2, 25, "25 uV"), - UV_50 (3, 50, "50 uV"), - UV_100 (4, 100, "100 uV"), - UV_200 (5, 200, "200 uV"), - UV_400 (6, 400, "400 uV"), - UV_1000 (7, 1000, "1000 uV"), - UV_10000 (8, 10000, "10000 uV"); - - private int index; - private int value; - private String label; - private static TimeSeriesYLim[] vals = values(); - - TimeSeriesYLim(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 (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -public enum TimeSeriesLabelMode implements IndexingInterface -{ - OFF (0, 0, "Off"), - MINIMAL (1, 1, "Minimal"), - ON (2, 2, "On"); - - private int index; - private int value; - private String label; - private static TimeSeriesLabelMode[] vals = values(); - - TimeSeriesLabelMode(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 (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - -class W_timeSeries extends Widget { - //to see all core variables/methods of the Widget class, refer to Widget.pde - //put your custom variables here... +class W_TimeSeries extends WidgetWithSettings { private int numChannelBars; private float xF, yF, wF, hF; private float ts_padding; @@ -161,10 +31,6 @@ class W_timeSeries extends Widget { private PlaybackScrollbar scrollbar; private TimeDisplay timeDisplay; - TimeSeriesXLim xLimit = TimeSeriesXLim.FIVE; - TimeSeriesYLim yLimit = TimeSeriesYLim.AUTO; - TimeSeriesLabelMode labelMode = TimeSeriesLabelMode.MINIMAL; - private PImage expand_default; private PImage expand_hover; private PImage expand_active; @@ -177,20 +43,15 @@ class W_timeSeries extends Widget { private boolean allowSpillover = false; private boolean hasScrollbar = true; //used to turn playback scrollbar widget on/off - List cp5ElementsToCheck = new ArrayList(); + List cp5ElementsToCheck; - W_timeSeries(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + W_TimeSeries() { + super(); + widgetTitle = "Time Series"; - tscp5 = new ControlP5(_parent); - tscp5.setGraphics(_parent, 0, 0); + tscp5 = new ControlP5(ourApplet); + tscp5.setGraphics(ourApplet, 0, 0); tscp5.setAutoDraw(false); - - tsChanSelect = new ExGChannelSelect(pApplet, x, y, w, navH); - //Activate all channels in channelSelect by default for this widget - tsChanSelect.activateAllButtons(); - - cp5ElementsToCheck.addAll(tsChanSelect.getCp5ElementsForOverlapCheck()); xF = float(x); //float(int( ... is a shortcut for rounding the float down... so that it doesn't creep into the 1px margin yF = float(y); @@ -205,13 +66,8 @@ class W_timeSeries extends Widget { ts_h = hF - playbackWidgetHeight - plotBottomWell - (ts_padding*2); numChannelBars = globalChannelCount; //set number of channel bars = to current globalChannelCount of system (4, 8, or 16) - //This is a newer protocol for setting up dropdowns. - addDropdown("VertScale_TS", "Vert Scale", yLimit.getEnumStringsAsList(), yLimit.getIndex()); - addDropdown("Duration", "Window", xLimit.getEnumStringsAsList(), xLimit.getIndex()); - addDropdown("LabelMode_TS", "Labels", labelMode.getEnumStringsAsList(), labelMode.getIndex()); - //Instantiate scrollbar if using playback mode and scrollbar feature in use - if((currentBoard instanceof FileBoard) && hasScrollbar) { + if ((currentBoard instanceof FileBoard) && hasScrollbar) { playbackWidgetHeight = 30.0; int _x = floor(xF) - 1; int _y = int(ts_y + ts_h + playbackWidgetHeight + 5); @@ -239,11 +95,12 @@ class W_timeSeries extends Widget { channelBarHeight = int(ts_h/numChannelBars); channelBars = new ChannelBar[numChannelBars]; //create our channel bars and populate our channelBars array! - for(int i = 0; i < numChannelBars; i++) { + for (int i = 0; i < numChannelBars; i++) { int channelBarY = int(ts_y) + i*(channelBarHeight); //iterate through bar locations - ChannelBar tempBar = new ChannelBar(_parent, i, int(ts_x), channelBarY, int(ts_w), channelBarHeight, expand_default, expand_hover, expand_active, contract_default, contract_hover, contract_active); + ChannelBar tempBar = new ChannelBar(ourApplet, i, int(ts_x), channelBarY, int(ts_w), channelBarHeight, expand_default, expand_hover, expand_active, contract_default, contract_hover, contract_active); channelBars[i] = tempBar; } + applyVerticalScaleToChannelBars(); int x_hsc = int(channelBars[0].plot.getPos()[0] + 2); int y_hsc = int(channelBars[0].plot.getPos()[1]); @@ -251,16 +108,63 @@ class W_timeSeries extends Widget { int h_hsc = channelBarHeight * numChannelBars; if (currentBoard instanceof ADS1299SettingsBoard) { - hwSettingsButton = createHSCButton("HardwareSettings", "Hardware Settings", (int)(x0 + 80), (int)(y0 + navHeight + 1), 120, navHeight - 3); + hwSettingsButton = createHSCButton("HardwareSettings", "Hardware Settings", (int)(x0 + 80), (int)(y0 + NAV_HEIGHT + 1), 120, NAV_HEIGHT - 3); cp5ElementsToCheck.add((controlP5.Controller)hwSettingsButton); - adsSettingsController = new ADS1299SettingsController(_parent, tsChanSelect.getActiveChannels(), x_hsc, y_hsc, w_hsc, h_hsc, channelBarHeight); + adsSettingsController = new ADS1299SettingsController(ourApplet, tsChanSelect.getActiveChannels(), x_hsc, y_hsc, w_hsc, h_hsc, channelBarHeight); } + } + + @Override + protected void initWidgetSettings() { + super.initWidgetSettings(); + // Store default values for widget settings + widgetSettings.set(TimeSeriesYLim.class, TimeSeriesYLim.AUTO) + .set(TimeSeriesXLim.class, TimeSeriesXLim.FIVE) + .set(TimeSeriesLabelMode.class, TimeSeriesLabelMode.MINIMAL); + + // Initialize the dropdowns with these settings + initDropdown(TimeSeriesYLim.class, "timeSeriesVerticalScaleDropdown", "Vert Scale"); + initDropdown(TimeSeriesXLim.class, "timeSeriesHorizontalScaleDropdown", "Window"); + initDropdown(TimeSeriesLabelMode.class, "timeSeriesLabelModeDropdown", "Labels"); - setTSVertScale(yLimit.getIndex()); + // Initialize the channel select feature for this widget + tsChanSelect = new ExGChannelSelect(ourApplet, x, y, w, navH); + + // Activate all channels in channelSelect by default for this widget + tsChanSelect.activateAllButtons(); + + // Check and lock channel select if a dropdown that overlaps it is open + cp5ElementsToCheck = new ArrayList(); + cp5ElementsToCheck.addAll(tsChanSelect.getCp5ElementsForOverlapCheck()); + + // Save the active channels to the widget settings + saveActiveChannels(tsChanSelect.getActiveChannels()); + + // Save the current settings to the widget settings + widgetSettings.saveDefaults(); + } + + @Override + protected void applySettings() { + // Update dropdown labels to match current settings + updateDropdownLabel(TimeSeriesYLim.class, "timeSeriesVerticalScaleDropdown"); + updateDropdownLabel(TimeSeriesXLim.class, "timeSeriesHorizontalScaleDropdown"); + updateDropdownLabel(TimeSeriesLabelMode.class, "timeSeriesLabelModeDropdown"); + applyVerticalScaleToChannelBars(); + applyHorizontalScaleToChannelBars(); + applyActiveChannels(tsChanSelect); + } + + @Override + protected void updateChannelSettings() { + // Just save the current active channels when saving settings + if (tsChanSelect != null) { + saveActiveChannels(tsChanSelect.getActiveChannels()); + } } void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + super.update(); // offset based on whether channel select or hardware settings are open or not int chanSelectOffset = tsChanSelect.isVisible() ? tsChanSelect.getHeight() : 0; @@ -277,13 +181,13 @@ class W_timeSeries extends Widget { tsChanSelect.update(x, y, w); //Update and resize all active channels - for(int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { + for (int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { int activeChannel = tsChanSelect.getActiveChannels().get(i); int channelBarY = int(ts_y + chanSelectOffset) + i*(channelBarHeight); //iterate through bar locations //To make room for channel bar separator, subtract space between channel bars from height int cb_h = channelBarHeight - INTER_CHANNEL_BAR_SPACE; channelBars[activeChannel].resize(int(ts_x), channelBarY, int(ts_w), cb_h); - channelBars[activeChannel].update(getAdsSettingsVisible(), labelMode); + channelBars[activeChannel].update(getAdsSettingsVisible(), widgetSettings.get(TimeSeriesLabelMode.class)); } //Responsively size and update the HardwareSettingsController @@ -295,7 +199,7 @@ class W_timeSeries extends Widget { } //Update Playback scrollbar and/or display time - if((currentBoard instanceof FileBoard) && hasScrollbar) { + if ((currentBoard instanceof FileBoard) && hasScrollbar) { //scrub playback file scrollbar.update(); } else { @@ -306,13 +210,13 @@ class W_timeSeries extends Widget { } void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + super.draw(); //remember to refer to x,y,w,h which are the positioning variables of the Widget class //draw channel bars for (int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { int activeChannel = tsChanSelect.getActiveChannels().get(i); - channelBars[activeChannel].draw(getAdsSettingsVisible(), labelMode); + channelBars[activeChannel].draw(getAdsSettingsVisible()); } //Display playback scrollbar, timeDisplay, or ADSSettingsController depending on data source @@ -334,12 +238,12 @@ class W_timeSeries extends Widget { } void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + super.screenResized(); //Very important to allow users to interact with objects after app resize tscp5.setGraphics(ourApplet, 0,0); - tsChanSelect.screenResized(pApplet); + tsChanSelect.screenResized(ourApplet); xF = float(x); //float(int( ... is a shortcut for rounding the float down... so that it doesn't creep into the 1px margin yF = float(y); @@ -352,7 +256,7 @@ class W_timeSeries extends Widget { ts_h = hF - playbackWidgetHeight - plotBottomWell - (ts_padding*2); ////Resize the playback slider if using playback mode, or resize timeDisplay div at the bottom of timeSeries - if((currentBoard instanceof FileBoard) && hasScrollbar) { + if ((currentBoard instanceof FileBoard) && hasScrollbar) { int _x = floor(xF) - 1; int _y = y + h - int(playbackWidgetHeight); int _w = int(wF) + 1; @@ -377,39 +281,39 @@ class W_timeSeries extends Widget { cb.updateCP5(ourApplet); } - for(int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { + for (int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { int activeChannel = tsChanSelect.getActiveChannels().get(i); int channelBarY = int(ts_y + chanSelectOffset) + i*(channelBarHeight); //iterate through bar locations channelBars[activeChannel].resize(int(ts_x), channelBarY, int(ts_w), channelBarHeight); //bar x, bar y, bar w, bar h } if (currentBoard instanceof ADS1299SettingsBoard) { - hwSettingsButton.setPosition(x0 + 80, (int)(y0 + navHeight + 1)); + hwSettingsButton.setPosition(x0 + 80, (int)(y0 + NAV_HEIGHT + 1)); } } void mousePressed() { - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + super.mousePressed(); tsChanSelect.mousePressed(this.dropdownIsActive); //Calls channel select mousePressed and checks if clicked - for(int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { + for (int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { int activeChannel = tsChanSelect.getActiveChannels().get(i); channelBars[activeChannel].mousePressed(); } } void mouseReleased() { - super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + super.mouseReleased(); - for(int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { + for (int i = 0; i < tsChanSelect.getActiveChannels().size(); i++) { int activeChannel = tsChanSelect.getActiveChannels().get(i); channelBars[activeChannel].mouseReleased(); } } public void setAdsSettingsVisible(boolean visible) { - if(!(currentBoard instanceof ADS1299SettingsBoard)) { + if (!(currentBoard instanceof ADS1299SettingsBoard)) { return; } @@ -456,781 +360,44 @@ class W_timeSeries extends Widget { return myButton; } - public TimeSeriesYLim getTSVertScale() { - return yLimit; - } - - public TimeSeriesXLim getTSHorizScale() { - return xLimit; - } - - public TimeSeriesLabelMode getTSLabelMode() { - return labelMode; - } - - public void setTSVertScale(int n) { - yLimit = yLimit.values()[n]; + private void applyVerticalScaleToChannelBars() { + int verticalScaleValue = widgetSettings.get(TimeSeriesYLim.class).getValue(); for (int i = 0; i < numChannelBars; i++) { - channelBars[i].adjustVertScale(yLimit.getValue()); + channelBars[i].adjustVertScale(verticalScaleValue); } } - public void setTSHorizScale(int n) { - xLimit = xLimit.values()[n]; + private void applyHorizontalScaleToChannelBars() { + int horizontalScaleValue = widgetSettings.get(TimeSeriesXLim.class).getValue(); for (int i = 0; i < numChannelBars; i++) { - channelBars[i].adjustTimeAxis(xLimit.getValue()); - } - } - - public void setTSLabelMode(int n) { - labelMode = labelMode.values()[n]; - } -}; - -//These functions are activated when an item from the corresponding dropdown is selected -void VertScale_TS(int n) { - w_timeSeries.setTSVertScale(n); -} - -//triggered when there is an event in the Duration Dropdown -void Duration(int n) { - w_timeSeries.setTSHorizScale(n); - - int newDuration = w_timeSeries.getTSHorizScale().getValue(); - //If selected by user, sync the duration of Time Series, Accelerometer, and Analog Read(Cyton Only) - if (currentBoard instanceof AccelerometerCapableBoard) { - if (settings.accHorizScaleSave == 0) { - //set accelerometer x axis to the duration selected from dropdown - w_accelerometer.accelerometerBar.adjustTimeAxis(newDuration); - } - } - if (currentBoard instanceof AnalogCapableBoard) { - if (settings.arHorizScaleSave == 0) { - //set analog read x axis to the duration selected from dropdown - for(int i = 0; i < w_analogRead.numAnalogReadBars; i++) { - w_analogRead.analogReadBars[i].adjustTimeAxis(newDuration); - } - } - } -} - -void LabelMode_TS(int n) { - w_timeSeries.setTSLabelMode(n); -} - -//======================================================================================================================== -// CHANNEL BAR CLASS -- Implemented by Time Series Widget Class -//======================================================================================================================== -//this class contains the plot and buttons for a single channel of the Time Series widget -//one of these will be created for each channel (4, 8, or 16) -class ChannelBar { - - int channelIndex; //duh - String channelString; - int x, y, w, h; - int defaultH; - ControlP5 cbCp5; - Button onOffButton; - int onOff_diameter; - int yScaleButton_h; - int yScaleButton_w; - Button yScaleButton_pos; - Button yScaleButton_neg; - int yAxisLabel_h; - private TextBox yAxisMax; - private TextBox yAxisMin; - - int yAxisUpperLim; - int yAxisLowerLim; - int uiSpaceWidth; - int padding_4 = 4; - int minimumChannelHeight; - int plotBottomWellH = 35; - - GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Se ries trace - GPointsArray channelPoints; - int nPoints; - int numSeconds; - float timeBetweenPoints; - private GPlotAutoscaler gplotAutoscaler; - - color channelColor; //color of plot trace - - TextBox voltageValue; - TextBox impValue; - - boolean drawVoltageValue; - - ChannelBar(PApplet _parent, int _channelIndex, int _x, int _y, int _w, int _h, PImage expand_default, PImage expand_hover, PImage expand_active, PImage contract_default, PImage contract_hover, PImage contract_active) { - - cbCp5 = new ControlP5(ourApplet); - cbCp5.setGraphics(ourApplet, x, y); - cbCp5.setAutoDraw(false); //Setting this saves code as cp5 elements will only be drawn/visible when [cp5].draw() is called - - channelIndex = _channelIndex; - channelString = str(channelIndex + 1); - - x = _x; - y = _y; - w = _w; - h = _h; - defaultH = h; - - onOff_diameter = h > 26 ? 26 : h - 2; - createOnOffButton("onOffButton"+channelIndex, channelString, x + 6, y + int(h/2) - int(onOff_diameter/2), onOff_diameter, onOff_diameter); - - //Create GPlot for this Channel - uiSpaceWidth = 36 + padding_4; - yAxisUpperLim = 200; - yAxisLowerLim = -200; - numSeconds = 5; - plot = new GPlot(_parent); - plot.setPos(x + uiSpaceWidth, y); - plot.setDim(w - uiSpaceWidth, h); - plot.setMar(0f, 0f, 0f, 0f); - plot.setLineColor((int)channelColors[channelIndex%8]); - plot.setXLim(-5,0); - plot.setYLim(yAxisLowerLim, yAxisUpperLim); - plot.setPointSize(2); - plot.setPointColor(0); - plot.setAllFontProperties("Arial", 0, 14); - plot.getXAxis().setFontColor(OPENBCI_DARKBLUE); - plot.getXAxis().setLineColor(OPENBCI_DARKBLUE); - plot.getXAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); - if(channelIndex == globalChannelCount-1) { - plot.getXAxis().setAxisLabelText("Time (s)"); - plot.getXAxis().getAxisLabel().setOffset(plotBottomWellH/2 + 5f); - } - gplotAutoscaler = new GPlotAutoscaler(); - - //Fill the GPlot with initial data - nPoints = nPointsBasedOnDataSource(); - channelPoints = new GPointsArray(nPoints); - timeBetweenPoints = (float)numSeconds / (float)nPoints; - for (int i = 0; i < nPoints; i++) { - float time = -(float)numSeconds + (float)i*timeBetweenPoints; - float filt_uV_value = 0.0; //0.0 for all points to start - GPoint tempPoint = new GPoint(time, filt_uV_value); - channelPoints.set(i, tempPoint); - } - plot.setPoints(channelPoints); //set the plot with 0.0 for all channelPoints to start - - //Create a UI to custom scale the Y axis for this channel - yScaleButton_w = 18; - yScaleButton_h = 18; - yAxisLabel_h = 12; - int padding = 2; - yAxisMax = new TextBox("+"+yAxisUpperLim+"uV", x + uiSpaceWidth + padding, y + int(padding*1.5), OPENBCI_DARKBLUE, color(255,255,255,175), LEFT, TOP); - yAxisMin = new TextBox(yAxisLowerLim+"uV", x + uiSpaceWidth + padding, y + h - yAxisLabel_h - padding_4, OPENBCI_DARKBLUE, color(255,255,255,175), LEFT, TOP); - customYLim(yAxisMax, yAxisUpperLim); - customYLim(yAxisMin, yAxisLowerLim); - yScaleButton_neg = createYScaleButton(channelIndex, false, "decreaseYscale", "-T", x + uiSpaceWidth + padding, y + w/2 - yScaleButton_h/2, yScaleButton_w, yScaleButton_h, contract_default, contract_hover, contract_active); - yScaleButton_pos = createYScaleButton(channelIndex, true, "increaseYscale", "+T", x + uiSpaceWidth + padding*2 + yScaleButton_w, y + w/2 - yScaleButton_h/2, yScaleButton_w, yScaleButton_h, expand_default, expand_hover, expand_active); - - //Create textBoxes to display the current values - impValue = new TextBox("", x + uiSpaceWidth + (int)plot.getDim()[0], y + padding, OPENBCI_DARKBLUE, color(255,255,255,175), RIGHT, TOP); - voltageValue = new TextBox("", x + uiSpaceWidth + (int)plot.getDim()[0] - padding, y + h, OPENBCI_DARKBLUE, color(255,255,255,175), RIGHT, BOTTOM); - drawVoltageValue = true; - - //Establish a minimumChannelHeight - minimumChannelHeight = padding_4 + yAxisLabel_h*2; - } - - void update(boolean hardwareSettingsAreOpen, TimeSeriesLabelMode _labelMode) { - - //Reusable variables - String fmt; float val; - - //Update the voltage value TextBox - val = dataProcessing.data_std_uV[channelIndex]; - voltageValue.string = String.format(getFmt(val),val) + " uVrms"; - if (is_railed != null) { - voltageValue.setText(is_railed[channelIndex].notificationString + voltageValue.string); - voltageValue.setTextColor(OPENBCI_DARKBLUE); - color bgColor = color(255,255,255,175); // Default white background for voltage TextBox - if (is_railed[channelIndex].is_railed) { - bgColor = SIGNAL_CHECK_RED_LOWALPHA; - } else if (is_railed[channelIndex].is_railed_warn) { - bgColor = SIGNAL_CHECK_YELLOW_LOWALPHA; - } - voltageValue.setBackgroundColor(bgColor); - } - - //update the impedance values - val = data_elec_imp_ohm[channelIndex]/1000; - fmt = String.format(getFmt(val),val) + " kOhm"; - if (is_railed != null && is_railed[channelIndex].is_railed == true) { - fmt = "RAILED - " + fmt; - } - impValue.setText(fmt); - - // update data in plot - updatePlotPoints(); - - if(currentBoard.isEXGChannelActive(channelIndex)) { - onOffButton.setColorBackground(channelColors[channelIndex%8]); // power down == false, set color to vibrant - } - else { - onOffButton.setColorBackground(50); // power down == true, set to grey - } - - //Hide yAxisButtons when hardware settings are open, using autoscale, and labels are turn on - boolean b = !hardwareSettingsAreOpen - && h > minimumChannelHeight - && !gplotAutoscaler.getEnabled() - && _labelMode == TimeSeriesLabelMode.ON; - yScaleButton_pos.setVisible(b); - yScaleButton_neg.setVisible(b); - yScaleButton_pos.setUpdate(b); - yScaleButton_neg.setUpdate(b); - b = !hardwareSettingsAreOpen - && h > minimumChannelHeight - && _labelMode == TimeSeriesLabelMode.ON; - yAxisMin.setVisible(b); - yAxisMax.setVisible(b); - voltageValue.setVisible(_labelMode != TimeSeriesLabelMode.OFF); - } - - private String getFmt(float val) { - String fmt; - if (val > 100.0f) { - fmt = "%.0f"; - } else if (val > 10.0f) { - fmt = "%.1f"; - } else { - fmt = "%.2f"; - } - return fmt; - } - - private void updatePlotPoints() { - float[][] buffer = downsampledFilteredBuffer.getBuffer(); - final int bufferSize = buffer[channelIndex].length; - final int startIndex = bufferSize - nPoints; - for (int i = startIndex; i < bufferSize; i++) { - int adjustedIndex = i - startIndex; - float time = -(float)numSeconds + (float)(adjustedIndex)*timeBetweenPoints; - float filt_uV_value = buffer[channelIndex][i]; - channelPoints.set(adjustedIndex, time, filt_uV_value, ""); - } - plot.setPoints(channelPoints); - - gplotAutoscaler.update(plot, channelPoints); - - if (gplotAutoscaler.getEnabled()) { - float[] minMax = gplotAutoscaler.getMinMax(); - customYLim(yAxisMin, (int)minMax[0]); - customYLim(yAxisMax, (int)minMax[1]); + channelBars[i].adjustTimeAxis(horizontalScaleValue); } } - public void draw(boolean hardwareSettingsAreOpen, TimeSeriesLabelMode _labelMode) { - - plot.beginDraw(); - plot.drawBox(); - plot.drawGridLines(GPlot.VERTICAL); - try { - plot.drawLines(); - } catch (NullPointerException e) { - e.printStackTrace(); - println("PLOT ERROR ON CHANNEL " + channelIndex); - - } - //Draw the x axis label on the bottom channel bar, hide if hardware settings are open - if (isBottomChannel() && !hardwareSettingsAreOpen) { - plot.drawXAxis(); - plot.getXAxis().draw(); - } - plot.endDraw(); - - //draw channel holder background - pushStyle(); - stroke(OPENBCI_BLUE_ALPHA50); - noFill(); - rect(x,y,w,h); - popStyle(); - - //draw channelBar separator line in the middle of INTER_CHANNEL_BAR_SPACE - if (!isBottomChannel()) { - pushStyle(); - stroke(OPENBCI_DARKBLUE); - strokeWeight(1); - int separator_y = y + h + int(w_timeSeries.INTER_CHANNEL_BAR_SPACE/2); - line(x, separator_y, x + w, separator_y); - popStyle(); - } - - //draw impedance values in time series also for each channel - drawVoltageValue = true; - if (currentBoard instanceof ImpedanceSettingsBoard) { - if(((ImpedanceSettingsBoard)currentBoard).isCheckingImpedance(channelIndex)) { - impValue.draw(); - drawVoltageValue = false; - } - } - - if (drawVoltageValue) { - voltageValue.draw(); - } - - try { - cbCp5.draw(); - } catch (NullPointerException e) { - e.printStackTrace(); - println("CP5 ERROR ON CHANNEL " + channelIndex); - } - - yAxisMin.draw(); - yAxisMax.draw(); - } - - private int nPointsBasedOnDataSource() { - return (numSeconds * currentBoard.getSampleRate()) / getDownsamplingFactor(); - } - - public void adjustTimeAxis(int _newTimeSize) { - numSeconds = _newTimeSize; - plot.setXLim(-_newTimeSize,0); - - nPoints = nPointsBasedOnDataSource(); - channelPoints = new GPointsArray(nPoints); - timeBetweenPoints = (float)numSeconds / (float)nPoints; - if(_newTimeSize > 1) { - plot.getXAxis().setNTicks(_newTimeSize); //sets the number of axis divisions... - }else{ - plot.getXAxis().setNTicks(10); - } - - updatePlotPoints(); - } - - public void adjustVertScale(int _vertScaleValue) { - boolean enableAutoscale = _vertScaleValue == 0; - gplotAutoscaler.setEnabled(enableAutoscale); - if (enableAutoscale) { - return; - } - yAxisLowerLim = -_vertScaleValue; - yAxisUpperLim = _vertScaleValue; - plot.setYLim(yAxisLowerLim, yAxisUpperLim); - //Update button text - customYLim(yAxisMin, yAxisLowerLim); - customYLim(yAxisMax, yAxisUpperLim); - } - - //Update yAxis text and responsively size Textfield - private void customYLim(TextBox tb, int limit) { - StringBuilder s = new StringBuilder(limit > 0 ? "+" : ""); - s.append(limit); - s.append("uV"); - tb.setText(s.toString()); - } - - public void resize(int _x, int _y, int _w, int _h) { - x = _x; - y = _y; - w = _w; - h = _h; - - //reposition & resize the plot - int plotW = w - uiSpaceWidth; - plot.setPos(x + uiSpaceWidth, y); - plot.setDim(plotW, h); - - int padding = 2; - voltageValue.setPosition(x + uiSpaceWidth + (w - uiSpaceWidth) - padding, y + h); - impValue.setPosition(x + uiSpaceWidth + (int)plot.getDim()[0], y + padding); - - yAxisMax.setPosition(x + uiSpaceWidth + padding, y + int(padding*1.5) - 2); - yAxisMin.setPosition(x + uiSpaceWidth + padding, y + h - yAxisLabel_h - padding - 1); - - final int yAxisLabelWidth = yAxisMax.getWidth(); - int yScaleButtonX = x + uiSpaceWidth + padding_4; - int yScaleButtonY = y + h/2 - yScaleButton_h/2; - boolean enoughSpaceBetweenAxisLabels = h > yScaleButton_h + yAxisLabel_h*2 + 2; - yScaleButtonX += enoughSpaceBetweenAxisLabels ? 0 : yAxisLabelWidth; - yScaleButton_neg.setPosition(yScaleButtonX, yScaleButtonY); - yScaleButtonX += yScaleButton_w + padding; - yScaleButton_pos.setPosition(yScaleButtonX, yScaleButtonY); - - onOff_diameter = h > 26 ? 26 : h - 2; - onOffButton.setSize(onOff_diameter, onOff_diameter); - onOffButton.setPosition(x + 6, y + int(h/2) - int(onOff_diameter/2)); - } - - public void updateCP5(PApplet _parent) { - cbCp5.setGraphics(_parent, 0, 0); + public void setVerticalScale(int n) { + widgetSettings.setByIndex(TimeSeriesYLim.class, n); + applyVerticalScaleToChannelBars(); } - private boolean isBottomChannel() { - int numActiveChannels = w_timeSeries.tsChanSelect.getActiveChannels().size(); - boolean isLastChannel = channelIndex == w_timeSeries.tsChanSelect.getActiveChannels().get(numActiveChannels - 1); - return isLastChannel; + public void setHorizontalScale(int n) { + widgetSettings.setByIndex(TimeSeriesXLim.class, n); + applyHorizontalScaleToChannelBars(); } - public void mousePressed() { - } - - public void mouseReleased() { - } - - private void createOnOffButton(String name, String text, int _x, int _y, int _w, int _h) { - onOffButton = createButton(cbCp5, name, text, _x, _y, _w, _h, 0, h2, 16, channelColors[channelIndex%8], WHITE, BUTTON_HOVER, BUTTON_PRESSED, (Integer) null, -2); - onOffButton.setCircularButton(true); - onOffButton.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - boolean newState = !currentBoard.isEXGChannelActive(channelIndex); - println("[" + channelString + "] onOff released - " + (newState ? "On" : "Off")); - currentBoard.setEXGChannelActive(channelIndex, newState); - if (currentBoard instanceof ADS1299SettingsBoard) { - w_timeSeries.adsSettingsController.updateChanSettingsDropdowns(channelIndex, currentBoard.isEXGChannelActive(channelIndex)); - boolean hasUnappliedChanges = currentBoard.isEXGChannelActive(channelIndex) != newState; - w_timeSeries.adsSettingsController.setHasUnappliedSettings(channelIndex, hasUnappliedChanges); - } - } - }); - onOffButton.setDescription("Click to toggle channel " + channelString + "."); - } - - private Button createYScaleButton(int chan, boolean shouldIncrease, String bName, String bText, int _x, int _y, int _w, int _h, PImage _default, PImage _hover, PImage _active) { - _default.resize(_w, _h); - _hover.resize(_w, _h); - _active.resize(_w, _h); - final Button myButton = cbCp5.addButton(bName) - .setPosition(_x, _y) - .setSize(_w, _h) - .setColorLabel(color(255)) - .setColorForeground(OPENBCI_BLUE) - .setColorBackground(color(144, 100)) - .setImages(_default, _hover, _active) - ; - myButton.onClick(new yScaleButtonCallbackListener(chan, shouldIncrease)); - return myButton; - } - - private class yScaleButtonCallbackListener implements CallbackListener { - private int channel; - private boolean increase; - private final int hardLimit = 10; - private int yLimOption = TimeSeriesYLim.UV_200.getValue(); - //private int delta = 0; //value to change limits by - - yScaleButtonCallbackListener(int theChannel, boolean isIncrease) { - channel = theChannel; - increase = isIncrease; - } - public void controlEvent(CallbackEvent theEvent) { - verbosePrint("A button was pressed for channel " + (channel+1) + ". Should we increase (or decrease?): " + increase); - - int inc = increase ? 1 : -1; - int factor = yAxisUpperLim > 25 || (yAxisUpperLim == 25 && increase) ? 25 : 5; - int n = (int)(log10(abs(yAxisLowerLim))) * factor * inc; - yAxisLowerLim -= n; - n = (int)(log10(yAxisUpperLim)) * factor * inc; - yAxisUpperLim += n; - - yAxisLowerLim = yAxisLowerLim <= -hardLimit ? yAxisLowerLim : -hardLimit; - yAxisUpperLim = yAxisUpperLim >= hardLimit ? yAxisUpperLim : hardLimit; - plot.setYLim(yAxisLowerLim, yAxisUpperLim); - //Update button text - customYLim(yAxisMin, yAxisLowerLim); - customYLim(yAxisMax, yAxisUpperLim); - } + public void setLabelMode(int n) { + widgetSettings.setByIndex(TimeSeriesLabelMode.class, n); } }; -//======================================================================================================================== -// END OF -- CHANNEL BAR CLASS -//======================================================================================================================== - - -//========================== PLAYBACKSLIDER ========================== -class PlaybackScrollbar { - private final float ps_Padding = 40.0; //used to make room for skip to start button - private int x, y, w, h; - private int swidth, sheight; // width and height of bar - private float xpos, ypos; // x and y position of bar - private float spos; // x position of slider - private float sposMin, sposMax; // max and min values of slider - private boolean over; // is the mouse over the slider? - private boolean locked; - private ControlP5 pbsb_cp5; - private Button skipToStartButton; - private int skipToStart_diameter; - private String currentAbsoluteTimeToDisplay = ""; - private String currentTimeInSecondsToDisplay = ""; - private FileBoard fileBoard; - - private final DateFormat currentTimeFormatShort = new SimpleDateFormat("mm:ss"); - private final DateFormat currentTimeFormatLong = new SimpleDateFormat("HH:mm:ss"); - private final DateFormat timeStampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - PlaybackScrollbar (int _x, int _y, int _w, int _h, float xp, float yp, int sw, int sh) { - x = _x; - y = _y; - w = _w; - h = _h; - swidth = sw; - sheight = sh; - xpos = xp + ps_Padding; //lots of padding to make room for button - ypos = yp-sheight/2; - spos = xpos; - sposMin = xpos; - sposMax = xpos + swidth - sheight/2; - - pbsb_cp5 = new ControlP5(ourApplet); - pbsb_cp5.setGraphics(ourApplet, 0,0); - pbsb_cp5.setAutoDraw(false); - - //Let's make a button to return to the start of playback!! - skipToStart_diameter = 25; - createSkipToStartButton("skipToStartButton", "", int(xp) + int(skipToStart_diameter*.5), int(yp) + int(sh/2) - skipToStart_diameter, skipToStart_diameter, skipToStart_diameter); - - fileBoard = (FileBoard)currentBoard; - } - - private void createSkipToStartButton(String name, String text, int _x, int _y, int _w, int _h) { - skipToStartButton = createButton(pbsb_cp5, name, text, _x, _y, _w, _h, 0, p5, 12, GREY_235, OPENBCI_DARKBLUE, BUTTON_HOVER, BUTTON_PRESSED, (Integer)null, 0); - PImage defaultImage = loadImage("skipToStart_default-30x26.png"); - skipToStartButton.setImage(defaultImage); - skipToStartButton.setForceDrawBackground(true); - skipToStartButton.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - skipToStartButtonAction(); - } - }); - skipToStartButton.setDescription("Click to go back to the beginning of the file."); - } - - /////////////// Update loop for PlaybackScrollbar - void update() { - checkMouseOver(); // check if mouse is over - - if (mousePressed && over) { - locked = true; - } - if (!mousePressed) { - locked = false; - } - //if the slider is being used, update new position based on user mouseX - if (locked) { - spos = constrain(mouseX-sheight/2, sposMin, sposMax); - scrubToPosition(); - } - else { - updateCursor(); - } - - // update timestamp - currentAbsoluteTimeToDisplay = getAbsoluteTimeToDisplay(); - - //update elapsed time to display - currentTimeInSecondsToDisplay = getCurrentTimeToDisplaySeconds(); - - } //end update loop for PlaybackScrollbar - - void updateCursor() { - float currentSample = float(fileBoard.getCurrentSample()); - float totalSamples = float(fileBoard.getTotalSamples()); - float currentPlaybackPos = currentSample / totalSamples; - - spos = lerp(sposMin, sposMax, currentPlaybackPos); - } - - void scrubToPosition() { - int totalSamples = fileBoard.getTotalSamples(); - int newSamplePos = floor(totalSamples * getCursorPercentage()); - - fileBoard.goToIndex(newSamplePos); - dataProcessing.updateEntireDownsampledBuffer(); - dataProcessing.clearCalculatedMetricWidgets(); - } - - float getCursorPercentage() { - return (spos - sposMin) / (sposMax - sposMin); - } - - String getAbsoluteTimeToDisplay() { - List currentData = currentBoard.getData(1); - if (currentData.get(0).length == 0) { - return ""; - } - int timeStampChan = currentBoard.getTimestampChannel(); - long timestampMS = (long)(currentData.get(0)[timeStampChan] * 1000.0); - if(timestampMS == 0) { - return ""; - } - - return timeStampFormat.format(new Date(timestampMS)); - } - - String getCurrentTimeToDisplaySeconds() { - double totalMillis = fileBoard.getTotalTimeSeconds() * 1000.0; - double currentMillis = fileBoard.getCurrentTimeSeconds() * 1000.0; - - String totalTimeStr = formatCurrentTime(totalMillis); - String currentTimeStr = formatCurrentTime(currentMillis); - - return currentTimeStr + " / " + totalTimeStr; - } - - String formatCurrentTime(double millis) { - DateFormat formatter = currentTimeFormatShort; - if (millis >= 3600000.0) { // bigger than 60 minutes - formatter = currentTimeFormatLong; - } - - return formatter.format(new Date((long)millis)); - } - - //checks if mouse is over the playback scrollbar - private void checkMouseOver() { - if (mouseX > xpos && mouseX < xpos+swidth && - mouseY > ypos && mouseY < ypos+sheight) { - if(!over) { - onMouseEnter(); - } - } - else { - if (over) { - onMouseExit(); - } - } - } - - // called when the mouse enters the playback scrollbar - private void onMouseEnter() { - over = true; - cursor(HAND); //changes cursor icon to a hand - } - - private void onMouseExit() { - over = false; - cursor(ARROW); - } - - void draw() { - pushStyle(); - - fill(GREY_235); - stroke(OPENBCI_BLUE); - rect(x, y, w, h); - - //draw the playback slider inside the playback sub-widget - noStroke(); - fill(GREY_200); - rect(xpos, ypos, swidth, sheight); - - //select color for playback indicator - if (over || locked) { - fill(OPENBCI_DARKBLUE); - } else { - fill(102, 102, 102); - } - //draws playback position indicator - rect(spos, ypos, sheight/2, sheight); - - //draw current timestamp and X of Y Seconds above scrollbar - textFont(p2, 18); - fill(OPENBCI_DARKBLUE); - textAlign(LEFT, TOP); - float textHeight = textAscent() - textDescent(); - float textY = y - textHeight - 10; - float tw = textWidth(currentAbsoluteTimeToDisplay); - text(currentAbsoluteTimeToDisplay, xpos + swidth - tw, textY); - text(currentTimeInSecondsToDisplay, xpos, textY); - - popStyle(); - - pbsb_cp5.draw(); - } - - void screenResized(int _x, int _y, int _w, int _h, float _pbx, float _pby, float _pbw, float _pbh) { - x = _x; - y = _y; - w = _w; - h = _h; - swidth = int(_pbw); - sheight = int(_pbh); - xpos = _pbx + ps_Padding; //add lots of padding for use - ypos = _pby - sheight/2; - sposMin = xpos; - sposMax = xpos + swidth - sheight/2; - //update the position of the playback indicator us - //newspos = updatePos(); - - pbsb_cp5.setGraphics(ourApplet, 0, 0); - - skipToStartButton.setPosition( - int(_pbx) + int(skipToStart_diameter*.5), - int(_pby) - int(skipToStart_diameter*.5) - ); - } - - //This function scrubs to the beginning of the playback file - //Useful to 'reset' the scrollbar before loading a new playback file - void skipToStartButtonAction() { - fileBoard.goToIndex(0); - dataProcessing.updateEntireDownsampledBuffer(); - dataProcessing.clearCalculatedMetricWidgets(); - } - -};//end PlaybackScrollbar class - -//========================== TimeDisplay ========================== -class TimeDisplay { - int swidth, sheight; // width and height of bar - float xpos, ypos; // x and y position of bar - String currentAbsoluteTimeToDisplay = ""; - Boolean updatePosition = false; - LocalDateTime time; - - TimeDisplay (float xp, float yp, int sw, int sh) { - swidth = sw; - sheight = sh; - xpos = xp; //lots of padding to make room for button - ypos = yp; - currentAbsoluteTimeToDisplay = fetchCurrentTimeString(); - } - - /////////////// Update loop for TimeDisplay when data stream is running - void update() { - if (currentBoard.isStreaming()) { - //Fetch Local time - try { - currentAbsoluteTimeToDisplay = fetchCurrentTimeString(); - } catch (NullPointerException e) { - println("TimeDisplay: Timestamp error..."); - e.printStackTrace(); - } - - } - } //end update loop for TimeDisplay - - void draw() { - pushStyle(); - //draw current timestamp at the bottom of the Widget container - if (!currentAbsoluteTimeToDisplay.equals(null)) { - int fontSize = 17; - textFont(p2, fontSize); - fill(OPENBCI_DARKBLUE); - float tw = textWidth(currentAbsoluteTimeToDisplay); - text(currentAbsoluteTimeToDisplay, xpos + swidth - tw, ypos); - text(streamTimeElapsed.toString(), xpos + 10, ypos); - } - popStyle(); - } +//These functions are activated when an item from the corresponding dropdown is selected +void timeSeriesVerticalScaleDropdown(int n) { + widgetManager.getTimeSeriesWidget().setVerticalScale(n); +} - void screenResized(float _x, float _y, float _w, float _h) { - swidth = int(_w); - sheight = int(_h); - xpos = _x; - ypos = _y; - } +void timeSeriesHorizontalScaleDropdown(int n) { + widgetManager.getTimeSeriesWidget().setHorizontalScale(n); +} - String fetchCurrentTimeString() { - time = LocalDateTime.now(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); - return time.format(formatter); - } -};//end TimeDisplay class +void timeSeriesLabelModeDropdown(int n) { + widgetManager.getTimeSeriesWidget().setLabelMode(n); +} diff --git a/OpenBCI_GUI/Widget.pde b/OpenBCI_GUI/Widget.pde index 82bb4ad66..4a0abafc3 100644 --- a/OpenBCI_GUI/Widget.pde +++ b/OpenBCI_GUI/Widget.pde @@ -7,29 +7,21 @@ // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -//Used for Widget Dropdown Enums -interface IndexingInterface { - public int getIndex(); - public String getString(); -} - -class Widget{ - - protected PApplet pApplet; +class Widget { + protected String widgetTitle = "Widget"; //default name of the widget protected int x0, y0, w0, h0; //true x,y,w,h of container protected int x, y, w, h; //adjusted x,y,w,h of white space `blank rectangle` under the nav... private int currentContainer; //this determines where the widget is located ... based on the x/y/w/h of the parent container + protected ControlP5 cp5_widget; + private ArrayList dropdowns; protected boolean dropdownIsActive = false; private boolean previousDropdownIsActive = false; private boolean previousTopNavDropdownMenuIsOpen = false; private boolean widgetSelectorIsActive = false; - private ArrayList dropdowns; - protected ControlP5 cp5_widget; - protected String widgetTitle = "No Title Set"; //used to limit the size of the widget selector, forces a scroll bar to show and allows us to add even more widgets in the future private final float widgetDropdownScaling = .90; private boolean isWidgetActive = false; @@ -41,16 +33,17 @@ class Widget{ protected int dropdownWidth = 64; private boolean initialResize = false; //used to properly resize the widgetSelector when loading default settings - Widget(PApplet _parent){ - pApplet = _parent; - cp5_widget = new ControlP5(pApplet); + Widget() { + cp5_widget = new ControlP5(ourApplet); cp5_widget.setAutoDraw(false); //this prevents the cp5 object from drawing automatically (if it is set to true it will be drawn last, on top of all other GUI stuff... not good) dropdowns = new ArrayList(); - //setup dropdown menus currentContainer = 5; //central container by default mapToCurrentContainer(); + } + public String getWidgetTitle() { + return widgetTitle; } public boolean getIsActive() { @@ -88,12 +81,12 @@ class Widget{ } public void setupWidgetSelectorDropdown(ArrayList _widgetOptions){ - cp5_widget.setColor(settings.dropdownColors); + cp5_widget.setColor(dropdownColorsGlobal); ScrollableList scrollList = cp5_widget.addScrollableList("WidgetSelector") .setPosition(x0+2, y0+2) //upper left corner // .setFont(h2) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(OBJECT_BORDER_GREY) //.setSize(widgetSelectorWidth, int(h0 * widgetDropdownScaling) )// + maxFreqList.size()) //.setSize(widgetSelectorWidth, (NUM_WIDGETS_TO_SHOW+1)*(navH-4) )// + maxFreqList.size()) @@ -123,7 +116,7 @@ class Widget{ } public void setupNavDropdowns(){ - cp5_widget.setColor(settings.dropdownColors); + cp5_widget.setColor(dropdownColorsGlobal); // println("Setting up dropdowns..."); for(int i = 0; i < dropdowns.size(); i++){ int dropdownPos = dropdowns.size() - i; @@ -132,7 +125,7 @@ class Widget{ .setPosition(x0+w0-(dropdownWidth*(dropdownPos))-(2*(dropdownPos)), y0 + navH + 2) //float right .setFont(h5) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(OBJECT_BORDER_GREY) .setSize(dropdownWidth, (dropdowns.get(i).items.size()+1)*(navH-4) )// + maxFreqList.size()) .setBarHeight(navH-4) @@ -219,10 +212,6 @@ class Widget{ mapToCurrentContainer(); } - public void setTitle(String _widgetTitle){ - widgetTitle = _widgetTitle; - } - public void setContainer(int _currentContainer){ currentContainer = _currentContainer; mapToCurrentContainer(); @@ -233,8 +222,8 @@ class Widget{ private void resizeWidgetSelector() { int dropdownsItemsToShow = int((h0 * widgetDropdownScaling) / (navH - 4)); widgetSelectorHeight = (dropdownsItemsToShow + 1) * (navH - 4); - if (wm != null) { - int maxDropdownHeight = (wm.widgetOptions.size() + 1) * (navH - 4); + if (widgetManager != null) { + int maxDropdownHeight = (widgetManager.getWidgetCount() + 1) * (navH - 4); if (widgetSelectorHeight > maxDropdownHeight) widgetSelectorHeight = maxDropdownHeight; } @@ -258,7 +247,7 @@ class Widget{ h = h0 - navH*2; //This line resets the origin for all cp5 elements under "cp5_widget" when the screen is resized, otherwise there will be drawing errors - cp5_widget.setGraphics(pApplet, 0, 0); + cp5_widget.setGraphics(ourApplet, 0, 0); if (cp5_widget.getController("WidgetSelector") != null) { resizeWidgetSelector(); @@ -268,7 +257,7 @@ class Widget{ for(int i = 0; i < dropdowns.size(); i++){ int dropdownPos = dropdowns.size() - i; cp5_widget.getController(dropdowns.get(i).id) - //.setPosition(w-(dropdownWidth*dropdownPos)-(2*(dropdownPos+1)), navHeight+(y+2)) // float left + //.setPosition(w-(dropdownWidth*dropdownPos)-(2*(dropdownPos+1)), NAV_HEIGHT+(y+2)) // float left .setPosition(x0+w0-(dropdownWidth*(dropdownPos))-(2*(dropdownPos)), navH +(y0+2)) //float right //.setSize(dropdownWidth, (maxFreqList.size()+1)*(navBarHeight-4)) ; @@ -318,6 +307,190 @@ class Widget{ } }; //end of base Widget class +abstract class WidgetWithSettings extends Widget { + protected WidgetSettings widgetSettings; + + WidgetWithSettings() { + super(); + // Create settings with the widget's title + widgetSettings = new WidgetSettings(getWidgetTitle()); + // Initialize settings with default values + initWidgetSettings(); + } + + /** + * Initialize widget settings with default values + * Override this method in widget subclasses to set custom defaults + */ + protected void initWidgetSettings() { + // Default implementation is empty + // Child classes should override this to add their specific settings + } + + /** + * Apply current settings to the widget UI + * Override this method in subclasses to update UI elements based on settings + */ + protected abstract void applySettings(); + + /** + * Get the settings object for this widget + * @return WidgetSettings object for this widget + */ + public WidgetSettings getSettings() { + return widgetSettings; + } + + /** + * Convert widget settings to JSON string + * @return JSON representation of settings + */ + public String settingsToJSON() { + // Call saveSettings to ensure all widget settings are up-to-date before serializing + saveSettings(); + return widgetSettings.toJSON(); + } + + /** + * Load settings from JSON string + * @param jsonString JSON string containing settings + * @return true if settings were loaded successfully, false otherwise + */ + public boolean loadSettingsFromJSON(String jsonString) { + boolean success = widgetSettings.loadFromJSON(jsonString); + if (success) { + applySettings(); + } + return success; + } + + /** + * Helper method to initialize a dropdown with values from an enum + * @param enumClass Enum class to get values from + * @param id ID for the dropdown controller + * @param label Label to display above the dropdown + */ + protected & IndexingInterface> void initDropdown(Class enumClass, String id, String label) { + T currentValue = widgetSettings.get(enumClass); + int currentIndex = currentValue != null ? currentValue.getIndex() : 0; + List options = EnumHelper.getEnumStrings(enumClass); + addDropdown(id, label, options, currentIndex); + } + + /** + * Helper method to update a dropdown label with current setting value + * @param enumClass Enum class to get the current value from + * @param controllerId ID of the controller to update + */ + protected & IndexingInterface> void updateDropdownLabel(Class enumClass, String controllerId) { + T currentValue = widgetSettings.get(enumClass); + if (currentValue != null) { + String value = currentValue.getString(); + cp5_widget.getController(controllerId).getCaptionLabel().setText(value); + } + } + + /** + * Save active channel selection to widget settings + * @param channels List of selected channel indices + */ + protected void saveActiveChannels(List channels) { + widgetSettings.setActiveChannels(channels); + println(widgetTitle + ": Saved " + channels.size() + " active channels"); + } + + /** + * Apply saved active channel selection to a channel select component + * @param channelSelect The channel select component to update + * @return true if channels were loaded and applied, false otherwise + */ + protected boolean applyActiveChannels(ExGChannelSelect channelSelect) { + List savedChannels = widgetSettings.getActiveChannels(); + if (!savedChannels.isEmpty()) { + channelSelect.updateChannelSelection(savedChannels); + return true; + } + return false; + } + + /** + * Get the list of active channels from settings + * @return List of active channel indices, or empty list if none are saved + */ + protected List getActiveChannels() { + return widgetSettings.getActiveChannels(); + } + + /** + * Check if active channels are defined in settings + * @return true if active channels are defined, false otherwise + */ + protected boolean hasActiveChannels() { + return widgetSettings.hasActiveChannels(); + } + + /** + * Update channel settings from any channel selectors before saving + * Each widget class should override this if it has channel selectors + */ + protected void updateChannelSettings() { + // Default implementation does nothing + // Override in widgets that have channel selectors + } + + /** + * Save active channel selection to widget settings with a specific name + * @param name Identifier for this channel selection (e.g., "top", "bottom") + * @param channels List of selected channel indices + */ + protected void saveNamedChannels(String name, List channels) { + widgetSettings.setNamedChannels(name, channels); + println(widgetTitle + ": Saved " + channels.size() + " channels for " + name); + } + + /** + * Apply saved named channel selection to a channel select component + * @param name Identifier for the channel selection + * @param channelSelect The channel select component to update + * @return true if channels were loaded and applied, false otherwise + */ + protected boolean applyNamedChannels(String name, ExGChannelSelect channelSelect) { + List savedChannels = widgetSettings.getNamedChannels(name); + if (!savedChannels.isEmpty()) { + channelSelect.updateChannelSelection(savedChannels); + return true; + } + return false; + } + + /** + * Get the list of active channels for a named selection from settings + * @param name Identifier for the channel selection + * @return List of active channel indices, or empty list if none are saved + */ + protected List getNamedChannels(String name) { + return widgetSettings.getNamedChannels(name); + } + + /** + * Check if a named channel selection is defined in settings + * @param name Identifier for the channel selection + * @return true if the named channel selection is defined, false otherwise + */ + protected boolean hasNamedChannels(String name) { + return widgetSettings.hasNamedChannels(name); + } + + /** + * Save settings before serializing to JSON + * Default implementation - for channels only + * Child classes can override this to save additional settings + */ + protected void saveSettings() { + updateChannelSettings(); + } +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // NavBarDropdown is a single dropdown item in any instance of a Widget @@ -367,23 +540,23 @@ class NavBarDropdown{ void WidgetSelector(int n){ println("New widget [" + n + "] selected for container..."); //find out if the widget you selected is already active - boolean isSelectedWidgetActive = wm.widgets.get(n).getIsActive(); + boolean isSelectedWidgetActive = widgetManager.widgets.get(n).getIsActive(); //find out which widget & container you are currently in... int theContainer = -1; - for(int i = 0; i < wm.widgets.size(); i++){ - if(wm.widgets.get(i).isMouseHere()){ - theContainer = wm.widgets.get(i).currentContainer; //keep track of current container (where mouse is...) + for(int i = 0; i < widgetManager.widgets.size(); i++){ + if(widgetManager.widgets.get(i).isMouseHere()){ + theContainer = widgetManager.widgets.get(i).currentContainer; //keep track of current container (where mouse is...) if(isSelectedWidgetActive){ //if the selected widget was already active - wm.widgets.get(i).setContainer(wm.widgets.get(n).currentContainer); //just switch the widget locations (ie swap containers) + widgetManager.widgets.get(i).setContainer(widgetManager.widgets.get(n).currentContainer); //just switch the widget locations (ie swap containers) } else{ - wm.widgets.get(i).setIsActive(false); //deactivate the current widget (if it is different than the one selected) + widgetManager.widgets.get(i).setIsActive(false); //deactivate the current widget (if it is different than the one selected) } } } - wm.widgets.get(n).setIsActive(true);//activate the new widget - wm.widgets.get(n).setContainer(theContainer);//map it to the current container + widgetManager.widgets.get(n).setIsActive(true);//activate the new widget + widgetManager.widgets.get(n).setContainer(theContainer);//map it to the current container } diff --git a/OpenBCI_GUI/WidgetManager.pde b/OpenBCI_GUI/WidgetManager.pde index 5678e5c44..9363c9b81 100644 --- a/OpenBCI_GUI/WidgetManager.pde +++ b/OpenBCI_GUI/WidgetManager.pde @@ -3,403 +3,379 @@ //======================================================================================== /* Notes: - - In this file all you have to do is MAKE YOUR WIDGET GLOBALLY, and then ADD YOUR WIDGET TO WIDGETS OF WIDGETMANAGER in the setupWidgets() function below - - the order in which they are added will effect the order in which they appear in the GUI and in the WidgetSelector dropdown menu of each widget - - use the WidgetTemplate.pde file as a starting point for creating new widgets (also check out W_timeSeries.pde, W_fft.pde, and W_HeadPlot.pde) + - The order in which they are added will effect the order in which they appear in the GUI and in the WidgetSelector dropdown menu of each widget. + - Use the WidgetTemplate.pde file as a starting point for creating new widgets. + - Also, check out W_TimeSeries.pde, W_Fft.pde, and W_Accelerometer.pde for examples. */ - -// MAKE YOUR WIDGET GLOBALLY -W_timeSeries w_timeSeries; -W_fft w_fft; -W_BandPower w_bandPower; -W_Accelerometer w_accelerometer; -W_CytonImpedance w_cytonImpedance; -W_GanglionImpedance w_ganglionImpedance; -W_HeadPlot w_headPlot; -W_template w_template1; -W_emg w_emg; -W_PulseSensor w_pulseSensor; -W_AnalogRead w_analogRead; -W_DigitalRead w_digitalRead; -W_playback w_playback; -W_Spectrogram w_spectrogram; -W_Focus w_focus; -W_EMGJoystick w_emgJoystick; -W_Marker w_marker; -W_PacketLoss w_packetLoss; - //======================================================================================== //======================================================================================== //======================================================================================== -class WidgetManager{ - - //this holds all of the widgets ... when creating/adding new widgets, we will add them to this ArrayList (below) - ArrayList widgets; - ArrayList widgetOptions; //List of Widget Titles, used to populate cp5 widgetSelector dropdown of all widgets - - //Variables for - int currentContainerLayout; //this is the Layout structure for the main body of the GUI ... refer to [PUT_LINK_HERE] for layouts/numbers image - ArrayList layouts = new ArrayList(); //this holds all of the different layouts ... +class WidgetManager { + //This holds all of the widgets. When creating/adding new widgets, we will add them to this ArrayList (below) + private ArrayList widgets; + private int currentContainerLayout; //This is the Layout structure for the main body of the GUI + private ArrayList layouts = new ArrayList(); //This holds all of the different layouts ... - public boolean isWMInitialized = false; private boolean visible = true; - WidgetManager(PApplet _this){ + WidgetManager() { widgets = new ArrayList(); - widgetOptions = new ArrayList(); - isWMInitialized = false; //DO NOT re-order the functions below setupLayouts(); - setupWidgets(_this); + setupWidgets(); setupWidgetSelectorDropdowns(); - if(globalChannelCount == 4 && eegDataSource == DATASOURCE_GANGLION) { + if ((globalChannelCount == 4 && eegDataSource == DATASOURCE_GANGLION) || eegDataSource == DATASOURCE_PLAYBACKFILE) { currentContainerLayout = 1; - settings.currentLayout = 1; // used for save/load settings - setNewContainerLayout(currentContainerLayout); //sets and fills layout with widgets in order of widget index, to reorganize widget index, reorder the creation in setupWidgets() - } else if (eegDataSource == DATASOURCE_PLAYBACKFILE) { - currentContainerLayout = 1; - settings.currentLayout = 1; // used for save/load settings - setNewContainerLayout(currentContainerLayout); //sets and fills layout with widgets in order of widget index, to reorganize widget index, reorder the creation in setupWidgets() + sessionSettings.currentLayout = 1; } else { currentContainerLayout = 4; //default layout ... tall container left and 2 shorter containers stacked on the right - settings.currentLayout = 4; // used for save/load settings - setNewContainerLayout(currentContainerLayout); //sets and fills layout with widgets in order of widget index, to reorganize widget index, reorder the creation in setupWidgets() + sessionSettings.currentLayout = 4; } - - isWMInitialized = true; + + //Set and fill layout with widgets in order of widget index + setNewContainerLayout(currentContainerLayout); } - void setupWidgets(PApplet _this) { - // println(" setupWidgets start -- " + millis()); + private void setupWidgets() { - w_timeSeries = new W_timeSeries(_this); - w_timeSeries.setTitle("Time Series"); - widgets.add(w_timeSeries); + widgets.add(new W_TimeSeries()); - w_fft = new W_fft(_this); - w_fft.setTitle("FFT Plot"); - widgets.add(w_fft); + widgets.add(new W_Fft()); - boolean showAccelerometerWidget = currentBoard instanceof AccelerometerCapableBoard; - if (showAccelerometerWidget) { - w_accelerometer = new W_Accelerometer(_this); - w_accelerometer.setTitle("Accelerometer"); - widgets.add(w_accelerometer); + if (currentBoard instanceof AccelerometerCapableBoard) { + widgets.add(new W_Accelerometer()); } if (currentBoard instanceof BoardCyton) { - w_cytonImpedance = new W_CytonImpedance(_this); - w_cytonImpedance.setTitle("Cyton Signal"); - widgets.add(w_cytonImpedance); + widgets.add(new W_CytonImpedance()); } - if (currentBoard instanceof DataSourcePlayback && w_playback == null) { - w_playback = new W_playback(_this); - w_playback.setTitle("Playback History"); - widgets.add(w_playback); + if (currentBoard instanceof DataSourcePlayback) { + widgets.add(new W_playback()); } - //only instantiate this widget if you are using a Ganglion board for live streaming - if(globalChannelCount == 4 && currentBoard instanceof BoardGanglion){ - //If using Ganglion, this is Widget_3 - w_ganglionImpedance = new W_GanglionImpedance(_this); - w_ganglionImpedance.setTitle("Ganglion Signal"); - widgets.add(w_ganglionImpedance); + if (globalChannelCount == 4 && currentBoard instanceof BoardGanglion) { + widgets.add(new W_GanglionImpedance()); } - w_focus = new W_Focus(_this); - w_focus.setTitle("Focus"); - widgets.add(w_focus); - - w_bandPower = new W_BandPower(_this); - w_bandPower.setTitle("Band Power"); - widgets.add(w_bandPower); + widgets.add(new W_Focus()); - w_headPlot = new W_HeadPlot(_this); - w_headPlot.setTitle("Head Plot"); - widgets.add(w_headPlot); + widgets.add(new W_BandPower()); - w_emg = new W_emg(_this); - w_emg.setTitle("EMG"); - widgets.add(w_emg); + widgets.add(new W_Emg()); - w_emgJoystick = new W_EMGJoystick(_this); - w_emgJoystick.setTitle("EMG Joystick"); - widgets.add(w_emgJoystick); - - w_spectrogram = new W_Spectrogram(_this); - w_spectrogram.setTitle("Spectrogram"); - widgets.add(w_spectrogram); - - if (currentBoard instanceof AnalogCapableBoard){ - w_pulseSensor = new W_PulseSensor(_this); - w_pulseSensor.setTitle("Pulse Sensor"); - widgets.add(w_pulseSensor); + widgets.add(new W_EmgJoystick()); + + widgets.add(new W_Spectrogram()); + + if (currentBoard instanceof AnalogCapableBoard) { + widgets.add(new W_PulseSensor()); } if (currentBoard instanceof DigitalCapableBoard) { - w_digitalRead = new W_DigitalRead(_this); - w_digitalRead.setTitle("Digital Read"); - widgets.add(w_digitalRead); + widgets.add(new W_DigitalRead()); } if (currentBoard instanceof AnalogCapableBoard) { - w_analogRead = new W_AnalogRead(_this); - w_analogRead.setTitle("Analog Read"); - widgets.add(w_analogRead); + widgets.add(new W_AnalogRead()); } if (currentBoard instanceof Board) { - w_packetLoss = new W_PacketLoss(_this); - w_packetLoss.setTitle("Packet Loss"); - widgets.add(w_packetLoss); + widgets.add(new W_PacketLoss()); } - w_marker = new W_Marker(_this); - w_marker.setTitle("Marker"); - widgets.add(w_marker); + widgets.add(new W_Marker()); //DEVELOPERS: Here is an example widget with the essentials/structure in place - w_template1 = new W_template(_this); - w_template1.setTitle("Widget Template 1"); - widgets.add(w_template1); - } - - - public boolean isVisible() { - return visible; - } - - public void setVisible(boolean _visible) { - visible = _visible; + widgets.add(new W_Template()); } - void setupWidgetSelectorDropdowns(){ - //create the widgetSelector dropdown of each widget - //println("widgets.size() = " + widgets.size()); - //create list of WidgetTitles.. we will use this to populate the dropdown (widget selector) of each widget - for(int i = 0; i < widgets.size(); i++){ - widgetOptions.add(widgets.get(i).widgetTitle); + private void setupWidgetSelectorDropdowns() { + // Create a temporary list of widget titles for dropdown setup + ArrayList widgetTitles = new ArrayList(); + + // Populate the titles list by calling getWidgetTitle() on each widget + for (Widget widget : widgets) { + widgetTitles.add(widget.getWidgetTitle()); } - //println("widgetOptions.size() = " + widgetOptions.size()); - for(int i = 0; i activeWidgets = new ArrayList(); - for(int i = 0; i < widgets.size(); i++){ - if(widgets.get(i).getIsActive()){ - numActiveWidgets++; //increment numActiveWidgets - // activeWidgets.add(i); //keep track of the active widget + for (Widget widget : widgets) { + if (widget.getIsActive()) { + numActiveWidgets++; } } - if(numActiveWidgets > numActiveWidgetsNeeded){ //if there are more active widgets than needed - //shut some down + if (numActiveWidgets > numActiveWidgetsNeeded) { + // Need to deactivate some widgets int numToShutDown = numActiveWidgets - numActiveWidgetsNeeded; int counter = 0; println("Widget Manager: Powering " + numToShutDown + " widgets down, and remapping."); - for(int i = widgets.size()-1; i >= 0; i--){ - if(widgets.get(i).getIsActive() && counter < numToShutDown){ + + // Deactivate widgets starting from the end + for (int i = widgets.size()-1; i >= 0 && counter < numToShutDown; i--) { + if (widgets.get(i).getIsActive()) { verbosePrint("Widget Manager: Deactivating widget [" + i + "]"); widgets.get(i).setIsActive(false); counter++; } } - //and map active widgets - counter = 0; - for(int i = 0; i < widgets.size(); i++){ - if(widgets.get(i).getIsActive()){ - widgets.get(i).setContainer(layouts.get(_newLayout).containerInts[counter]); - counter++; - } - } + // Map active widgets to containers + mapActiveWidgetsToContainers(_newLayout); - } else if(numActiveWidgetsNeeded > numActiveWidgets){ //if there are less active widgets than needed - //power some up + } else if (numActiveWidgetsNeeded > numActiveWidgets) { + // Need to activate more widgets int numToPowerUp = numActiveWidgetsNeeded - numActiveWidgets; int counter = 0; verbosePrint("Widget Manager: Powering " + numToPowerUp + " widgets up, and remapping."); - for(int i = 0; i < widgets.size(); i++){ - if(!widgets.get(i).getIsActive() && counter < numToPowerUp){ + + // Activate widgets from the beginning + for (int i = 0; i < widgets.size() && counter < numToPowerUp; i++) { + if (!widgets.get(i).getIsActive()) { verbosePrint("Widget Manager: Activating widget [" + i + "]"); widgets.get(i).setIsActive(true); counter++; } } - //and map active widgets - counter = 0; - for(int i = 0; i < widgets.size(); i++){ - if(widgets.get(i).getIsActive()){ - widgets.get(i).setContainer(layouts.get(_newLayout).containerInts[counter]); - // widgets.get(i).screenResized(); // do this to make sure the container is updated - counter++; - } - } + // Map active widgets to containers + mapActiveWidgetsToContainers(_newLayout); - } else{ //if there are the same amount - //simply remap active widgets + } else { + // Same number of active widgets as needed, just remap verbosePrint("Widget Manager: Remapping widgets."); - int counter = 0; - for(int i = 0; i < widgets.size(); i++){ - if(widgets.get(i).getIsActive()){ - widgets.get(i).setContainer(layouts.get(_newLayout).containerInts[counter]); - counter++; - } + mapActiveWidgetsToContainers(_newLayout); + } + } + + // Helper method to map active widgets to containers + private void mapActiveWidgetsToContainers(int layoutIndex) { + int counter = 0; + for (Widget widget : widgets) { + if (widget.getIsActive()) { + widget.setContainer(layouts.get(layoutIndex).containerInts[counter]); + counter++; } } } public void setAllWidgetsNull() { widgets.clear(); - w_timeSeries = null; - w_fft = null; - w_bandPower = null; - w_accelerometer = null; - w_cytonImpedance = null; - w_ganglionImpedance = null; - w_headPlot = null; - w_template1 = null; - w_emg = null; - w_pulseSensor = null; - w_analogRead = null; - w_digitalRead = null; - w_playback = null; - w_spectrogram = null; - w_packetLoss = null; - w_focus = null; - w_emgJoystick = null; - w_marker = null; - println("Widget Manager: All widgets set to null."); } -}; -//the Layout class is an orgnanizational tool ... a layout consists of a combination of containers ... refer to Container.pde -class Layout{ + // Useful in places like TopNav which overlap widget dropdowns + public void lockCp5ObjectsInAllWidgets(boolean lock) { + for (int i = 0; i < widgets.size(); i++) { + ControlP5 cp5Instance = widgets.get(i).cp5_widget; + List controllerList = cp5Instance.getAll(); + + for (int j = 0; j < controllerList.size(); j++) { + controlP5.Controller controller = (controlP5.Controller)controllerList.get(j); + controller.setLock(lock); + } + } + } - Container[] myContainers; - int[] containerInts; + public Widget getWidget(String className) { + for (Widget widget : widgets) { + String widgetClassName = widget.getClass().getSimpleName(); + if (widgetClassName.equals(className)) { + return widget; + } + } + return null; + } - Layout(int[] _myContainers){ //when creating a new layout, you pass in the integer #s of the containers you want as part of the layout ... so if I pass in the array {5}, my layout is 1 container that takes up the whole GUI body - //constructor stuff - myContainers = new Container[_myContainers.length]; //make the myContainers array equal to the size of the incoming array of ints - containerInts = new int[_myContainers.length]; - for(int i = 0; i < _myContainers.length; i++){ - myContainers[i] = container[_myContainers[i]]; - containerInts[i] = _myContainers[i]; + public boolean getWidgetExists(String className) { + return getWidget(className) != null; + } + + public W_TimeSeries getTimeSeriesWidget() { + return (W_TimeSeries) getWidget("W_TimeSeries"); + } + + public int getWidgetCount() { + return widgets.size(); + } + + public String getWidgetSettingsAsJson() { + StringBuilder allWidgetSettings = new StringBuilder(); + allWidgetSettings.append("{"); + boolean firstWidget = true; + + for (Widget widget : widgets) { + if (!(widget instanceof WidgetWithSettings)) { + continue; + } + + WidgetWithSettings widgetWithSettings = (WidgetWithSettings) widget; + + // Call updateChannelSettings to ensure channel selections are saved + widgetWithSettings.updateChannelSettings(); + + String widgetTitle = widget.getWidgetTitle(); + WidgetSettings widgetSettings = widgetWithSettings.getSettings(); + String json = widgetSettings.toJSON(); + + // Only add comma if not the first widget + if (!firstWidget) { + allWidgetSettings.append(", "); + } else { + firstWidget = false; + } + + allWidgetSettings.append("\"").append(widgetTitle).append("\": "); + allWidgetSettings.append(json); } + + allWidgetSettings.append("}"); + return allWidgetSettings.toString(); } - Container getContainer(int _numContainer){ - if(_numContainer < myContainers.length){ - return myContainers[_numContainer]; - } else{ - println("Widget Manager: Tried to return a non-existant container..."); - return myContainers[myContainers.length-1]; + public void loadWidgetSettingsFromJson(String widgetSettingsJson) { + JSONObject json = parseJSONObject(widgetSettingsJson); + if (json == null) { + println("WidgetManager:loadWidgetSettingsFromJson: Failed to parse JSON string."); + return; + } + + for (Widget widget : widgets) { + if (!(widget instanceof WidgetWithSettings)) { + continue; + } + + WidgetWithSettings widgetWithSettings = (WidgetWithSettings) widget; + String widgetTitle = widget.getWidgetTitle(); + if (!json.hasKey(widgetTitle)) { + println("WidgetManager:loadWidgetSettingsFromJson: No settings found for " + widgetTitle); + continue; + } + + String settingsJson = json.getString(widgetTitle, ""); + WidgetSettings widgetSettings = widgetWithSettings.getSettings(); + boolean success = widgetSettings.loadFromJSON(settingsJson); + if (!success) { + println("WidgetManager:loadWidgetSettingsFromJson: Failed to load settings for " + widgetTitle); + continue; + } + widgetWithSettings.applySettings(); } } }; \ No newline at end of file diff --git a/OpenBCI_GUI/WidgetSettings.pde b/OpenBCI_GUI/WidgetSettings.pde new file mode 100644 index 000000000..2a0236337 --- /dev/null +++ b/OpenBCI_GUI/WidgetSettings.pde @@ -0,0 +1,521 @@ +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Unified settings storage for widgets that handles enum settings, channel selections, + * and other types of settings with JSON serialization + */ +class WidgetSettings { + private String widgetName; + // Enum settings + private HashMap> enumSettings; + private HashMap> defaults; + // Channel settings + private HashMap> channelSettings; + private HashMap> defaultChannelSettings; + // Other settings + private HashMap otherSettings; + private HashMap defaultOtherSettings; + + public static final String KEY_ACTIVE_CHANNELS = "activeChannels"; + + public WidgetSettings(String widgetName) { + this.widgetName = widgetName; + this.enumSettings = new HashMap>(); + this.defaults = new HashMap>(); + this.channelSettings = new HashMap>(); + this.defaultChannelSettings = new HashMap>(); + this.otherSettings = new HashMap(); + this.defaultOtherSettings = new HashMap(); + } + + // + // ENUM SETTINGS + // + + /** + * Store a setting using enum class as key + * @return this WidgetSettings instance for method chaining + */ + public > WidgetSettings set(Class enumClass, T value) { + enumSettings.put(enumClass.getName(), value); + return this; + } + + /** + * Store a setting using the enum class and index + * Useful for setting values from UI components like dropdowns + * + * @param enumClass The enum class to look up values + * @param index The index of the enum constant to set + * @return this WidgetSettings instance for method chaining + */ + public > WidgetSettings setByIndex(Class enumClass, int index) { + T[] enumConstants = enumClass.getEnumConstants(); + + // Check if index is valid + if (index >= 0 && index < enumConstants.length) { + // Get the enum value at the specified index + T value = enumConstants[index]; + // Set it using the regular set method + set(enumClass, value); + } else { + // Index was out of bounds + println("Warning: Invalid index " + index + " for enum " + enumClass.getName()); + } + + return this; + } + + /** + * Get a setting using enum class as key + */ + public > T get(Class enumClass, T defaultValue) { + String key = enumClass.getName(); + if (enumSettings.containsKey(key)) { + Object value = enumSettings.get(key); + if (value != null && enumClass.isInstance(value)) { + return enumClass.cast(value); + } + } + return defaultValue; + } + + /** + * Get a setting using enum class as key (returns null if not found) + */ + public > T get(Class enumClass) { + return get(enumClass, null); + } + + // + // CHANNEL SETTINGS + // + + /** + * Store active channels + * @param channels List of selected channel indices + * @return this WidgetSettings instance for method chaining + */ + public WidgetSettings setActiveChannels(List channels) { + // Create a copy to prevent external modification + channelSettings.put(KEY_ACTIVE_CHANNELS, new ArrayList(channels)); + return this; + } + + /** + * Get active channels + * @return List of selected channel indices or empty list if not found + */ + public List getActiveChannels() { + if (channelSettings.containsKey(KEY_ACTIVE_CHANNELS)) { + // Return a copy to prevent external modification + return new ArrayList(channelSettings.get(KEY_ACTIVE_CHANNELS)); + } + return new ArrayList(); // Empty list if not found + } + + /** + * Check if active channels exist + * @return true if active channels exist, false otherwise + */ + public boolean hasActiveChannels() { + return channelSettings.containsKey(KEY_ACTIVE_CHANNELS) && + !channelSettings.get(KEY_ACTIVE_CHANNELS).isEmpty(); + } + + /** + * Store active channels with a specific name identifier + * @param name Identifier for this channel selection (e.g., "top", "bottom") + * @param channels List of selected channel indices + * @return this WidgetSettings instance for method chaining + */ + public WidgetSettings setNamedChannels(String name, List channels) { + // Create a copy to prevent external modification + channelSettings.put(name, new ArrayList(channels)); + return this; + } + + /** + * Get active channels for a specific named selection + * @param name Identifier for the channel selection + * @return List of selected channel indices or empty list if not found + */ + public List getNamedChannels(String name) { + if (channelSettings.containsKey(name)) { + // Return a copy to prevent external modification + return new ArrayList(channelSettings.get(name)); + } + return new ArrayList(); // Empty list if not found + } + + /** + * Check if a named channel selection exists + * @param name Identifier for the channel selection + * @return true if the named selection exists, false otherwise + */ + public boolean hasNamedChannels(String name) { + return channelSettings.containsKey(name) && + !channelSettings.get(name).isEmpty(); + } + + // + // OTHER SETTINGS + // + + /** + * Store a generic object setting with the given key + * @param key Name of the setting + * @param value Value to store + * @return this WidgetSettings instance for method chaining + */ + public WidgetSettings setObject(String key, T value) { + otherSettings.put(key, value); + return this; + } + + /** + * Get a generic object setting by key + * @param key Name of the setting to retrieve + * @param defaultValue Value to return if setting doesn't exist + * @return The stored value or the default if not found + */ + @SuppressWarnings("unchecked") + public T getObject(String key, T defaultValue) { + if (otherSettings.containsKey(key)) { + try { + return (T) otherSettings.get(key); + } catch (ClassCastException e) { + println("Type mismatch for setting " + key + ": " + e.getMessage()); + } + } + return defaultValue; + } + + /** + * Check if a generic object setting exists + * @param key Name of the setting to check + * @return true if the setting exists, false otherwise + */ + public boolean hasObject(String key) { + return otherSettings.containsKey(key); + } + + // + // DEFAULT HANDLING + // + + /** + * Save current settings as defaults + * @return this WidgetSettings instance for method chaining + */ + public WidgetSettings saveDefaults() { + // Save enum defaults + defaults = new HashMap>(enumSettings); + + // Save channel defaults + defaultChannelSettings = new HashMap>(); + saveDefaultChannels(); + + // Save other settings defaults + defaultOtherSettings = new HashMap(otherSettings); + + return this; + } + + // Helper method for saving default channel settings + private void saveDefaultChannels() { + for (String key : channelSettings.keySet()) { + defaultChannelSettings.put(key, new ArrayList(channelSettings.get(key))); + } + } + + /** + * Restore to default settings + * @return this WidgetSettings instance for method chaining + */ + public WidgetSettings restoreDefaults() { + // Restore enum settings + enumSettings = new HashMap>(defaults); + + // Restore channel settings + restoreDefaultChannels(); + + // Restore other settings + otherSettings = new HashMap(defaultOtherSettings); + + return this; + } + + // Helper method for restoring default channel settings + private void restoreDefaultChannels() { + channelSettings = new HashMap>(); + + for (String key : defaultChannelSettings.keySet()) { + channelSettings.put(key, new ArrayList(defaultChannelSettings.get(key))); + } + } + + // + // SERIALIZATION + // + + /** + * Convert settings to JSON string + */ + public String toJSON() { + JSONObject json = new JSONObject(); + json.setString("widgetTitle", widgetName); + + // Serialize settings + serializeEnumSettings(json); + serializeChannelSettings(json); + serializeOtherSettings(json); + + return json.toString(); + } + + // Helper method for enum serialization + private void serializeEnumSettings(JSONObject json) { + if (enumSettings.isEmpty()) { + return; + } + + JSONArray enumItems = new JSONArray(); + int i = 0; + for (String key : enumSettings.keySet()) { + Enum value = enumSettings.get(key); + JSONObject item = new JSONObject(); + item.setString("class", key); + item.setString("value", value.name()); + enumItems.setJSONObject(i++, item); + } + json.setJSONArray("enumSettings", enumItems); + } + + // Helper method for channel serialization + private void serializeChannelSettings(JSONObject json) { + if (channelSettings.isEmpty()) { + return; + } + + JSONObject channelsJson = new JSONObject(); + for (String key : channelSettings.keySet()) { + List channels = channelSettings.get(key); + JSONArray channelArray = new JSONArray(); + + for (int i = 0; i < channels.size(); i++) { + channelArray.setInt(i, channels.get(i)); + } + channelsJson.setJSONArray(key, channelArray); + } + json.setJSONObject("channelSettings", channelsJson); + } + + // Helper method for other settings serialization + private void serializeOtherSettings(JSONObject json) { + if (otherSettings.isEmpty()) { + return; + } + + JSONObject othersJson = new JSONObject(); + + for (String key : otherSettings.keySet()) { + Object value = otherSettings.get(key); + + // Handle basic types that JSONObject supports + if (value instanceof String) { + othersJson.setString(key, (String)value); + } else if (value instanceof Integer) { + othersJson.setInt(key, (Integer)value); + } else if (value instanceof Float) { + othersJson.setFloat(key, (Float)value); + } else if (value instanceof Boolean) { + othersJson.setBoolean(key, (Boolean)value); + } else { + println("WARNING: Couldn't save setting '" + key + "' with value type " + + (value != null ? value.getClass().getName() : "null")); + } + } + + if (othersJson.size() > 0) { + json.setJSONObject("otherSettings", othersJson); + } + } + + /** + * Attempts to load settings from a JSON string + * @param jsonString The JSON string containing settings + * @return true if settings were loaded successfully, false otherwise + */ + public boolean loadFromJSON(String jsonString) { + try { + JSONObject json = parseJSONObject(jsonString); + if (json == null) { + return false; + } + + validateWidgetName(json); + + boolean enumSuccess = loadEnumSettings(json); + boolean channelSuccess = loadChannelSettings(json); + boolean otherSuccess = loadOtherSettings(json); + + return enumSuccess || channelSuccess || otherSuccess; + } catch (Exception e) { + println("Error parsing JSON: " + e.getMessage()); + return false; + } + } + + // Helper method to validate widget name + private void validateWidgetName(JSONObject json) { + String loadedWidget = json.getString("widgetTitle", ""); + if (!loadedWidget.equals(widgetName)) { + println("Warning: Widget mismatch. Expected: " + widgetName + ", Found: " + loadedWidget); + } + } + + // Helper method to load enum settings + private boolean loadEnumSettings(JSONObject json) { + if (!json.hasKey("enumSettings")) { + return false; + } + + JSONArray enumItems = json.getJSONArray("enumSettings"); + if (enumItems == null) { + return false; + } + + boolean anySuccess = false; + for (int i = 0; i < enumItems.size(); i++) { + JSONObject item = enumItems.getJSONObject(i); + if (item == null) { + continue; + } + + String className = item.getString("class", null); + String valueName = item.getString("value", null); + + if (className == null || valueName == null) { + continue; + } + + anySuccess |= loadSingleEnum(className, valueName); + } + + return anySuccess; + } + + // Helper method to load a single enum value + private boolean loadSingleEnum(String className, String valueName) { + try { + Class enumClass = Class.forName(className); + if (!enumClass.isEnum()) { + return false; + } + + @SuppressWarnings("unchecked") + Enum enumValue = Enum.valueOf((Class)enumClass, valueName); + enumSettings.put(className, enumValue); + return true; + } catch (Exception e) { + println("Error loading enum setting: " + e.getMessage()); + return false; + } + } + + // Helper method to load channel settings + private boolean loadChannelSettings(JSONObject json) { + if (!json.hasKey("channelSettings")) { + return false; + } + + JSONObject channelsJson = json.getJSONObject("channelSettings"); + // Fixed this line + if (channelsJson == null || channelsJson.size() == 0) { + return false; + } + + // Clear existing settings only when we have valid data + channelSettings.clear(); + + boolean anySuccess = false; + for (Object key : channelsJson.keys()) { + String channelKey = key.toString(); + JSONArray channelArray = channelsJson.getJSONArray(channelKey); + + if (channelArray == null) { + continue; + } + + List channels = new ArrayList(); + for (int i = 0; i < channelArray.size(); i++) { + channels.add(channelArray.getInt(i)); + } + + channelSettings.put(channelKey, channels); + anySuccess = true; + } + + return anySuccess; + } + + // Helper method to load other settings - simplified version + private boolean loadOtherSettings(JSONObject json) { + if (!json.hasKey("otherSettings")) { + return false; + } + + JSONObject othersJson = json.getJSONObject("otherSettings"); + if (othersJson == null || othersJson.size() == 0) { + return false; + } + + boolean anySuccess = false; + + // Get all keys + for (Object keyObj : othersJson.keys()) { + String key = keyObj.toString(); + Object value = null; + + // Try each type in sequence + value = tryLoadAnyType(othersJson, key); + if (value != null) { + otherSettings.put(key, value); + anySuccess = true; + } else { + println("Could not determine type for key: " + key); + } + } + + return anySuccess; + } + + /** + * Try to load a value from JSON as any supported type + */ + private Object tryLoadAnyType(JSONObject json, String key) { + // Try as String + try { + return json.getString(key); + } catch (Exception e) { /* Not a string */ } + + // Try as Integer + try { + return json.getInt(key); + } catch (Exception e) { /* Not an integer */ } + + // Try as Float + try { + return json.getFloat(key); + } catch (Exception e) { /* Not a float */ } + + // Try as Boolean + try { + return json.getBoolean(key); + } catch (Exception e) { /* Not a boolean */ } + + return null; // No type worked + } +} diff --git a/OpenBCI_GUI/WidgetTemplateEnums.pde b/OpenBCI_GUI/WidgetTemplateEnums.pde new file mode 100644 index 000000000..ac73027b0 --- /dev/null +++ b/OpenBCI_GUI/WidgetTemplateEnums.pde @@ -0,0 +1,92 @@ +public enum TemplateDropdown1 implements IndexingInterface +{ + ITEM_A (0, 0, "Item A"), + ITEM_B (1, 1, "Item B"); + + private int index; + private int value; + private String label; + + TemplateDropdown1(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 enum TemplateDropdown2 implements IndexingInterface +{ + ITEM_C (0, 0, "Item C"), + ITEM_D (1, 1, "Item D"), + ITEM_E (2, 2, "Item E"); + + private int index; + private int value; + private String label; + + TemplateDropdown2(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 enum TemplateDropdown3 implements IndexingInterface +{ + ITEM_F (0, 0, "Item F"), + ITEM_G (1, 1, "Item G"), + ITEM_H (2, 2, "Item H"), + ITEM_I (3, 3, "Item I"); + + private int index; + private int value; + private String label; + + TemplateDropdown3(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; + } +} \ No newline at end of file