diff --git a/CHANGELOG.md b/CHANGELOG.md index 9240462be..de8f68d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,20 @@ # v4.1.6 -Use OpenBCIHub v2.0.9 please. +Use OpenBCIHub v2.1.0 please. ## Beta 0 ### Improvements +* Cyton+Dongle AutoConnect Button! * GUI error message when using old Cyton firmware #597 +* Update Focus widget help button +* Console Log window UI/UX update +* Add GUI Troublshooting Guide button to "Help" dropdown in TopNav.pde ### Bug Fixes * Cyton+WiFi unable to start session #555 #590 -* Networking start/stop stream #593 +* Networking: Start/Stop stream button behavior #593 +* Networking: Only show Pulse datatype for Cyton(Live) +* Show error when loading empty playback file and delete file from history # v4.1.5 Use OpenBCIHub v2.0.9 please. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da3dda6cc..013549cdb 100755 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,13 +18,13 @@ If you're new to Git and want to learn how to fork this repo, make your own addi ### Community -This project is maintained by the [OpenBCI](www.openbci.com) community. Join the [OpenBCI Forum](http://openbci.com/index.php/forum/), where discussions about OpenBCI takes place. +This project is maintained by the [OpenBCI](https://openbci.com) community. Join the [OpenBCI Forum](https://openbci.com/forum/), where discussions about OpenBCI takes place. ## How can I contribute? -This is currently a small, humble project so our contribution process is rather casual. If there's a feature you'd be interested in building, go ahead! Let us know on the [OpenBCI Forum](http://openbci.com/index.php/forum/) or [open an issue](../../issues) so others can follow along and we'll support you as much as we can. When you're finished submit a pull request to the master branch referencing the specific issue you addressed. +This is currently a small, humble project so our contribution process is rather casual. If there's a feature you'd be interested in building, go ahead! Let us know on the [OpenBCI Forum](https://openbci.com/forum/) or [open an issue](../../issues) so others can follow along and we'll support you as much as we can. When you're finished submit a pull request to the master branch referencing the specific issue you addressed. -If you find a bug, or have a suggestion on how to improve the project, just fill out a [Github issue](../../issues) +If you find a bug, or have a suggestion on how to improve the project, please fill out a [Github issue](../../issues). ### Steps to Contribute diff --git a/OpenBCI_GUI/ConsoleLog.pde b/OpenBCI_GUI/ConsoleLog.pde index 2f6d8c61f..3fb0d4e2b 100644 --- a/OpenBCI_GUI/ConsoleLog.pde +++ b/OpenBCI_GUI/ConsoleLog.pde @@ -17,6 +17,8 @@ import java.awt.Desktop; static class ConsoleWindow extends PApplet { private static ConsoleWindow instance = null; + PApplet logApplet; + private ControlP5 cp5; private Textarea consoleTextArea; private ClipHelper clipboardCopy; @@ -24,9 +26,14 @@ static class ConsoleWindow extends PApplet { private final int headerHeight = 42; private final int defaultWidth = 620; private final int defaultHeight = 500; - private final int buttonWidth = 170; + private final int buttonWidth = 142; private final int buttonHeight = 34; - private int previousWidth = defaultWidth; + + //for screen resizing + private boolean screenHasBeenResized = false; + private float timeOfLastScreenResize = 0; + private int widthOfLastScreen = defaultWidth; + private int heightOfLastScreen = defaultHeight; public static void display() { // enforce only one Console Window @@ -45,6 +52,9 @@ static class ConsoleWindow extends PApplet { } void setup() { + + logApplet = this; + surface.setAlwaysOnTop(true); surface.setResizable(true); @@ -53,6 +63,7 @@ static class ConsoleWindow extends PApplet { consoleTextArea = cp5.addTextarea("ConsoleWindow") .setPosition(0, headerHeight) + .setSize(width, height - headerHeight) .setFont(createFont("arial", 14)) .setLineHeight(18) .setColor(color(242)) @@ -65,13 +76,15 @@ static class ConsoleWindow extends PApplet { // register this console's Textarea with the output stream object outputStream.registerTextArea(consoleTextArea); - int cW = int(width/3); + int cW = int(width/4); int bX = int((cW - buttonWidth) / 2); createConsoleLogButton("openLogFileAsText", "Open Log as Text (F)", bX); bX += cW; - createConsoleLogButton("copyFullTextToClipboard", "Copy Full Log Text (C)", bX); + createConsoleLogButton("copyFullTextToClipboard", "Copy Full Text (C)", bX); bX += cW; createConsoleLogButton("copyLastLineToClipboard", "Copy Last Line (L)", bX); + bX += cW; + createConsoleLogButton("jumpToLastLine", "Jump to Last Line (J)", bX); } void createConsoleLogButton (String bName, String bText, int x) { @@ -84,21 +97,45 @@ static class ConsoleWindow extends PApplet { .setColorBackground(color(144, 100)); cp5.getController(bName) .getCaptionLabel() - .setFont(createFont("Arial",16,true)) + .setFont(createFont("Arial",14,true)) .toUpperCase(false) - .setSize(16) + .setSize(14) .setText(bText); } void draw() { - // dynamically resize text area to fit widget - consoleTextArea.setSize(width, height - headerHeight); - // update button positions when screen width changes - updateButtonPositions(); - clear(); scene(); cp5.draw(); + //checks if the screen is resized, similar to main GUI window + screenResized(); + } + + void screenResized() { + if (this.widthOfLastScreen != width || this.heightOfLastScreen != height) { + //println("ConsoleLog: RESIZED"); + this.screenHasBeenResized = true; + this.timeOfLastScreenResize = millis(); + this.widthOfLastScreen = width; + this.heightOfLastScreen = height; + } + if (this.screenHasBeenResized) { + //setGraphics() is very important, it lets the cp5 elements know where the origin is. + //Without this, cp5 elements won't work after screen is resized. + //This also happens in most widgets when the main GUI window is resized. + logApplet = this; + cp5.setGraphics(logApplet, 0, 0); + + imposeMinConsoleLogDimensions(); + // dynamically resize text area to fit widget + consoleTextArea.setSize(width, height - headerHeight); + // update button positions when screen width changes + updateButtonPositions(); + } + //re-initialize console log if screen has been resized and it's been more than 1 seccond (to prevent reinitialization happening too often) + if (this.screenHasBeenResized == true && (millis() - this.timeOfLastScreenResize) > 1000) { + this.screenHasBeenResized = false; + } } void scene() { @@ -110,14 +147,23 @@ static class ConsoleWindow extends PApplet { void keyReleased() { if (key == 'c') { copyFullTextToClipboard(); - } - - if (key == 'f') { + } else if (key == 'f') { openLogFileAsText(); + } else if (key == 'l') { + copyLastLineToClipboard(); + } else if (key == 'j' ) { + jumpToLastLine(); } + + } - if (key == 'l') { - copyLastLineToClipboard(); + void keyPressed() { + if (key == CODED) { + if (keyCode == UP) { + consoleTextArea.scrolled(-5); + } else if (keyCode == DOWN) { + consoleTextArea.scrolled(5); + } } } @@ -154,17 +200,30 @@ static class ConsoleWindow extends PApplet { println("ConsoleLog: Previous line copied to clipboard."); } + void jumpToLastLine() { + consoleTextArea.scroll(1.0); + } + void updateButtonPositions() { - if (width != previousWidth) { - int cW = width / 3; - int bX = (cW - 170) / 2; - int bY = 4; - cp5.getController("openLogFileAsText").setPosition(bX, bY); - bX += cW; - cp5.getController("copyFullTextToClipboard").setPosition(bX, bY); - bX += cW; - cp5.getController("copyLastLineToClipboard").setPosition(bX, bY); - previousWidth = width; + int cW = width / 4; + int bX = (cW - buttonWidth) / 2; + int bY = 4; + cp5.getController("openLogFileAsText").setPosition(bX, bY); + bX += cW; + cp5.getController("copyFullTextToClipboard").setPosition(bX, bY); + bX += cW; + cp5.getController("copyLastLineToClipboard").setPosition(bX, bY); + bX += cW; + cp5.getController("jumpToLastLine").setPosition(bX, bY); + } + + void imposeMinConsoleLogDimensions() { + //impose minimum gui dimensions + int minHeight = int(defaultHeight/2); + if (width < defaultWidth || height < minHeight) { + int _w = (width < defaultWidth) ? defaultWidth : width; + int _h = (height < minHeight) ? minHeight : height; + surface.setSize(_w, _h); } } diff --git a/OpenBCI_GUI/Containers.pde b/OpenBCI_GUI/Containers.pde index 37c1c61be..e80d0f8fd 100644 --- a/OpenBCI_GUI/Containers.pde +++ b/OpenBCI_GUI/Containers.pde @@ -81,8 +81,8 @@ void drawContainers() { println("OpenBCI_GUI: setup: RESIZED"); setupContainers(); //setupVizs(); //container extension example (more below) - widthOfLastScreen = width; - heightOfLastScreen = height; + settings.widthOfLastScreen = width; + settings.heightOfLastScreen = height; } } diff --git a/OpenBCI_GUI/ControlPanel.pde b/OpenBCI_GUI/ControlPanel.pde index 421701aed..64cdc1df2 100644 --- a/OpenBCI_GUI/ControlPanel.pde +++ b/OpenBCI_GUI/ControlPanel.pde @@ -223,20 +223,6 @@ public void controlEvent(ControlEvent theEvent) { output("Wifi Device Name = " + wifi_portName); } - /* - if (theEvent.isFrom("sdTimes")) { - Map bob = ((MenuList)theEvent.getController()).getItem(int(theEvent.getValue())); - sdSettingString = (String)bob.get("headline"); - sdSetting = int(theEvent.getValue()); - if (sdSetting != 0) { - output("OpenBCI microSD Setting = " + sdSettingString + " recording time"); - } else { - output("OpenBCI microSD Setting = " + sdSettingString); - } - verbosePrint("SD setting = " + sdSetting); - } - */ - if (theEvent.isFrom("sdTimes")) { Map bob = ((MenuList)theEvent.getController()).getItem(int(theEvent.getValue())); sdSettingString = (String)bob.get("headline"); @@ -314,6 +300,7 @@ class ControlPanel { //various control panel elements that are unique to specific datasources DataSourceBox dataSourceBox; SerialBox serialBox; + ComPortBox comPortBox; SessionDataBox dataLogBoxCyton; ChannelCountBox channelCountBox; InitBox initBox; @@ -380,7 +367,8 @@ class ControlPanel { sdConverterBox = new SDConverterBox(x + w, (playbackFileBox.y + playbackFileBox.h), playbackWidth, h, globalPadding); recentPlaybackBox = new RecentPlaybackBox(x + w, (sdConverterBox.y + sdConverterBox.h), playbackWidth, h, globalPadding); - rcBox = new RadioConfigBox(x+w, y, w, h, globalPadding); + comPortBox = new ComPortBox(x+w*2, y, w, h, globalPadding); + rcBox = new RadioConfigBox(x+w, y + comPortBox.h, w, h, globalPadding); channelPopup = new ChannelPopup(x+w, y, w, h, globalPadding); pollPopup = new PollPopup(x+w,y,w,h,globalPadding); @@ -448,6 +436,7 @@ class ControlPanel { sdBox.update(); rcBox.update(); + comPortBox.update(); wcBox.update(); initBox.update(); @@ -500,10 +489,11 @@ class ControlPanel { if (cyton.getInterface() == INTERFACE_SERIAL) { serialBox.y = interfaceBoxCyton.y + interfaceBoxCyton.h; serialBox.draw(); - dataLogBoxCyton.y = serialBox.y + serialBox.h; - cp5.get(MenuList.class, "serialList").setVisible(true); + dataLogBoxCyton.y = serialBox.y + serialBox.h; if (rcBox.isShowing) { + comPortBox.draw(); rcBox.draw(); + cp5.get(MenuList.class, "serialList").setVisible(true); if (channelPopup.wasClicked()) { channelPopup.draw(); cp5Popup.get(MenuList.class, "channelListCP").setVisible(true); @@ -653,11 +643,13 @@ class ControlPanel { public void hideRadioPopoutBox() { rcBox.isShowing = false; + comPortBox.isShowing = false; cp5Popup.hide(); // make sure to hide the controlP5 object cp5Popup.get(MenuList.class, "channelListCP").setVisible(false); cp5Popup.get(MenuList.class, "pollList").setVisible(false); + cp5.get(MenuList.class, "serialList").setVisible(false); // cp5Popup.hide(); // make sure to hide the controlP5 object - popOutRadioConfigButton.setString(">"); + popOutRadioConfigButton.setString("Manual >"); rcBox.print_onscreen(""); if (board != null) { board.stop(); @@ -791,6 +783,10 @@ class ControlPanel { refreshPort.setIsActive(true); refreshPort.wasPressed = true; } + if (serialBox.autoConnect.isMouseHere()) { + serialBox.autoConnect.setIsActive(true); + serialBox.autoConnect.wasPressed = true; + } } if (cyton.isWifi()) { @@ -1123,21 +1119,30 @@ class ControlPanel { //mouse released in control panel public void CPmouseReleased() { //verbosePrint("CPMouseReleased: CPmouseReleased start..."); - if(popOutRadioConfigButton.isMouseHere() && popOutRadioConfigButton.wasPressed){ + if (popOutRadioConfigButton.isMouseHere() && popOutRadioConfigButton.wasPressed) { popOutRadioConfigButton.wasPressed = false; popOutRadioConfigButton.setIsActive(false); if (cyton.isSerial()) { - if(rcBox.isShowing){ + if (rcBox.isShowing) { hideRadioPopoutBox(); - } - else{ + serialBox.autoConnect.setIgnoreHover(false); + serialBox.autoConnect.setColorNotPressed(255); + } else { rcBox.isShowing = true; rcBox.print_onscreen(rcBox.initial_message); - popOutRadioConfigButton.setString("<"); + popOutRadioConfigButton.setString("Manual <"); + serialBox.autoConnect.setIgnoreHover(true); + serialBox.autoConnect.setColorNotPressed(140); } } } + if (serialBox.autoConnect.isMouseHere() && serialBox.autoConnect.wasPressed) { + serialBox.autoConnect.wasPressed = false; + serialBox.autoConnect.setIsActive(false); + serialBox.attemptAutoConnectCyton(); + } + if (rcBox.isShowing) { if(getChannel.isMouseHere() && getChannel.wasPressed){ // if(board != null) // Radios_Config will handle creating the serial port JAM 1/2017 @@ -1630,6 +1635,11 @@ public void initButtonPressed(){ initSystemButton.wasPressed = false; initSystemButton.setIsActive(false); return; + } else if (playbackFileIsEmpty) { + outputError("Playback file appears empty. Try loading a different file."); + initSystemButton.wasPressed = false; + initSystemButton.setIsActive(false); + return; } else { //otherwise, initiate system! //verbosePrint("ControlPanel: CPmouseReleased: init"); initSystemButton.setString("STOP SESSION"); @@ -1658,7 +1668,7 @@ public void initButtonPressed(){ println("Static IP address of " + wifi_ipAddress); } midInit = true; - println("Calling initSystem()"); + println("initButtonPressed: Calling initSystem()"); try { initSystem(); //found in OpenBCI_GUI.pde } catch (Exception e) { @@ -1774,17 +1784,18 @@ class DataSourceBox { class SerialBox { int x, y, w, h, padding; //size and position + Button autoConnect; SerialBox(int _x, int _y, int _w, int _h, int _padding) { x = _x; y = _y; w = _w; - h = 140 + _padding; + h = 70; padding = _padding; - // autoconnect = new Button(x + padding, y + padding*3 + 4, w - padding*2, 24, "AUTOCONNECT AND START SYSTEM", fontInfo.buttonLabel_size); - refreshPort = new Button (x + padding, y + padding*4 + 72 + 8, w - padding*2, 24, "REFRESH LIST", fontInfo.buttonLabel_size); - popOutRadioConfigButton = new Button(x+padding + (w-padding*4), y + padding, 20,20,">",fontInfo.buttonLabel_size); + autoConnect = new Button(x + padding, y + padding*3 + 4, w - padding*3 - 70, 24, "AUTO", fontInfo.buttonLabel_size); + autoConnect.setHelpText("Attempt to auto-connect to Cyton. Try \"Manual\" if this does not work."); + popOutRadioConfigButton = new Button(x + w - 70 - padding, y + padding*3 + 4, 70, 24,"Manual >",fontInfo.buttonLabel_size); popOutRadioConfigButton.setHelpText("Having trouble connecting to Cyton? Click here to access Radio Configuration tools."); serialList = new MenuList(cp5, "serialList", w - padding*2, 72, p4); @@ -1809,16 +1820,92 @@ class SerialBox { fill(bgColor); textFont(h3, 16); textAlign(LEFT, TOP); - text("SERIAL/COM PORT", x + padding, y + padding); + text("SERIAL CONNECT", x + padding, y + padding); popStyle(); - refreshPort.draw(); if (cyton.isSerial()) { popOutRadioConfigButton.draw(); + autoConnect.draw(); + } + } + + public void attemptAutoConnectCyton() { + //Fetch the number of com ports... + int numComPorts = cp5.get(MenuList.class, "serialList").getListSize(); + String _regex = ""; + //Then look for matching cyton dongle + for (int i = 0; i < numComPorts; i++) { + String comPort = (String)cp5.get(MenuList.class, "serialList").getItem(i).get("headline"); + if (isMac()) { + _regex = "^/dev/tty.usbserial-DM.*$"; + } else if (isWindows()) { + _regex = "COM.*$"; + } else if (isLinux()) { + _regex = "^/dev/ttyUSB.*$"; + } + if (ableToConnect(comPort, _regex)) return; + } //end for loop for all com ports + + if (!openBCI_portName.equals("N/A")) { + outputError("Unable to auto-connect..."); + } + } //end attempAutoConnectCyton + + private boolean ableToConnect(String _comPort, String _regex) { + if (systemMode < SYSTEMMODE_POSTINIT) { + //There are quite a few serial ports on Linux, but not many that start with /dev/ttyUSB + String[] foundCytonPort = match(_comPort, _regex); + if (foundCytonPort != null) { // If not null, then a match was found + println("ControlPanel: Attempting to connect to " + _comPort); + openBCI_portName = foundCytonPort[0]; + initButtonPressed(); + if (systemMode == SYSTEMMODE_POSTINIT) return true; + } + return false; + } else { + return true; } } +}; - public void refreshSerialList() { +class ComPortBox { + int x, y, w, h, padding; //size and position + boolean isShowing; + + ComPortBox(int _x, int _y, int _w, int _h, int _padding) { + x = _x; + y = _y; + w = _w + 10; + h = 140 + _padding; + padding = _padding; + isShowing = false; + + refreshPort = new Button (x + padding, y + padding*4 + 72 + 8, w - padding*2, 24, "REFRESH LIST", fontInfo.buttonLabel_size); + serialList = new MenuList(cp5, "serialList", w - padding*2, 72, p4); + // println(w-padding*2); + serialList.setPosition(x + padding, y + padding*3 + 8); + serialPorts = Serial.list(); + for (int i = 0; i < serialPorts.length; i++) { + String tempPort = serialPorts[(serialPorts.length-1) - i]; //list backwards... because usually our port is at the bottom + serialList.addItem(makeItem(tempPort)); + } + } + + public void update() { + } + + public void draw() { + pushStyle(); + fill(boxColor); + stroke(boxStrokeColor); + strokeWeight(1); + rect(x, y, w, h); + fill(bgColor); + textFont(h3, 16); + textAlign(LEFT, TOP); + text("SERIAL/COM PORT", x + padding, y + padding); + refreshPort.draw(); + popStyle(); } }; @@ -2070,7 +2157,7 @@ class SessionDataBox { boolean dropdownWasClicked = false; SessionDataBox (int _x, int _y, int _w, int _h, int _padding, int _dataSource) { - odfModeHeight = bdfModeHeight + 24 + _padding - 3; + odfModeHeight = bdfModeHeight + 24 + _padding; x = _x; y = _y; w = _w; @@ -3007,7 +3094,7 @@ class RadioConfigBox { int x, y, w, h, padding; //size and position String initial_message = "Having trouble connecting to your Cyton? Try AutoScan!\n\nUse this tool to get Cyton status or change settings."; String last_message = initial_message; - boolean isShowing; + public boolean isShowing; RadioConfigBox(int _x, int _y, int _w, int _h, int _padding) { x = _x + _w; @@ -3194,7 +3281,6 @@ class ChannelPopup { textAlign(LEFT, TOP); text(title, x + padding, y + padding); popStyle(); - refreshPort.draw(); } public void setClicked(boolean click) { this.clicked = click; } @@ -3236,7 +3322,6 @@ class PollPopup { textAlign(LEFT, TOP); text("POLL SELECTION", x + padding, y + padding); popStyle(); - refreshPort.draw(); } public void setClicked(boolean click) { this.clicked = click; } @@ -3489,4 +3574,8 @@ public class MenuList extends controlP5.Controller { } return m; } + + int getListSize() { + return items.size(); + } }; diff --git a/OpenBCI_GUI/DataLogging.pde b/OpenBCI_GUI/DataLogging.pde index 2a82cac0b..b20559526 100644 --- a/OpenBCI_GUI/DataLogging.pde +++ b/OpenBCI_GUI/DataLogging.pde @@ -1395,8 +1395,7 @@ class Table_CSV extends Table { } } } - } - catch (Exception e) { + } catch (Exception e) { throw new RuntimeException("Error reading table on line " + row, e); } // shorten or lengthen based on what's left diff --git a/OpenBCI_GUI/Interactivity.pde b/OpenBCI_GUI/Interactivity.pde index 7137453e1..df7c61501 100644 --- a/OpenBCI_GUI/Interactivity.pde +++ b/OpenBCI_GUI/Interactivity.pde @@ -486,9 +486,9 @@ synchronized void mouseReleased() { redrawScreenNow = true; //command a redraw of the GUI whenever the mouse is released } - if (screenHasBeenResized) { + if (settings.screenHasBeenResized) { println("OpenBCI_GUI: mouseReleased: screen has been resized..."); - screenHasBeenResized = false; + settings.screenHasBeenResized = false; } } diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index d5f0414e5..be7006d37 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -198,15 +198,6 @@ boolean redrawScreenNow = true; int openBCI_byteCount = 0; StringBuilder board_message; -//for screen resizing -boolean screenHasBeenResized = false; -float timeOfLastScreenResize = 0; -float timeOfGUIreinitialize = 0; -int reinitializeGUIdelay = 125; -//Tao's variables -int widthOfLastScreen = 0; -int heightOfLastScreen = 0; - //set window size int win_x = 1024; //window width int win_y = 768; //window height @@ -239,9 +230,10 @@ PFont p6; //small Open Sans ButtonHelpText buttonHelpText; +//Used for playback file boolean has_processed = false; boolean isOldData = false; -//Used for playback file +boolean playbackFileIsEmpty = false; int indices = 0; //# columns used by a playback file determines number of channels final int totalColumns4ChanThresh = 10; @@ -265,7 +257,6 @@ int hubTimerCounter; //Count how many times GUI tries to connect to Hub int hubTimerLimit = 8; //Allow up to 8 tries for GUI to connect to Hub int hubTimerInterval = 2500; //try every 2.5 seconds, 8*2.5=20seconds - PApplet ourApplet; static CustomOutputStream outputStream; @@ -296,7 +287,7 @@ void settings() { size(976, 742, P2D); } else { //default 1024x768 resolution with 2D graphics - size(1024, 768, P2D); + size(win_x, win_y, P2D); } } @@ -376,8 +367,8 @@ void delayedSetup() { smooth(); //turn this off if it's too slow surface.setResizable(true); //updated from frame.setResizable in Processing 2 - widthOfLastScreen = width; //for screen resizing (Thank's Tao) - heightOfLastScreen = height; + settings.widthOfLastScreen = width; //for screen resizing (Thank's Tao) + settings.heightOfLastScreen = height; setupContainers(); @@ -386,8 +377,8 @@ void delayedSetup() { public void componentResized(ComponentEvent e) { if (e.getSource()==frame) { println("OpenBCI_GUI: setup: RESIZED"); - screenHasBeenResized = true; - timeOfLastScreenResize = millis(); + settings.screenHasBeenResized = true; + settings.timeOfLastScreenResize = millis(); // initializeGUI(); } } @@ -847,6 +838,7 @@ void initSystem() throws Exception { } else { outputError("Failed to connect. Check that the device is powered on and in range."); } + systemMode = SYSTEMMODE_PREINIT; controlPanel.open(); } @@ -1149,11 +1141,11 @@ void systemUpdate() { // for updating data values and variables //updates while in system control panel before START SYSTEM controlPanel.update(); - if (widthOfLastScreen != width || heightOfLastScreen != height) { + if (settings.widthOfLastScreen != width || settings.heightOfLastScreen != height) { imposeMinimumGUIDimensions(); topNav.screenHasBeenResized(width, height); - widthOfLastScreen = width; - heightOfLastScreen = height; + settings.widthOfLastScreen = width; + settings.heightOfLastScreen = height; //println("W = " + width + " || H = " + height); } } @@ -1200,25 +1192,25 @@ void systemUpdate() { // for updating data values and variables // gui.cc.update(); //update Channel Controller even when not updating certain parts of the GUI... (this is a bit messy...) //alternative component listener function (line 177 - 187 frame.addComponentListener) for processing 3, - if (widthOfLastScreen != width || heightOfLastScreen != height) { + if (settings.widthOfLastScreen != width || settings.heightOfLastScreen != height) { println("OpenBCI_GUI: setup: RESIZED"); - screenHasBeenResized = true; - timeOfLastScreenResize = millis(); - widthOfLastScreen = width; - heightOfLastScreen = height; + settings.screenHasBeenResized = true; + settings.timeOfLastScreenResize = millis(); + settings.widthOfLastScreen = width; + settings.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 (screenHasBeenResized) { + if (settings.screenHasBeenResized) { ourApplet = this; //reset PApplet... imposeMinimumGUIDimensions(); topNav.screenHasBeenResized(width, height); wm.screenResized(); } - if (screenHasBeenResized == true && (millis() - timeOfLastScreenResize) > reinitializeGUIdelay) { - screenHasBeenResized = false; + if (settings.screenHasBeenResized == true && (millis() - settings.timeOfLastScreenResize) > settings.reinitializeGUIdelay) { + settings.screenHasBeenResized = false; println("systemUpdate: reinitializing GUI"); - timeOfGUIreinitialize = millis(); + settings.timeOfGUIreinitialize = millis(); } if (wm.isWMInitialized) { @@ -1273,7 +1265,7 @@ void systemDraw() { //for drawing to the screen } //wait 1 second for GUI to reinitialize - if ((millis() - timeOfGUIreinitialize) > reinitializeGUIdelay) { + if ((millis() - settings.timeOfGUIreinitialize) > settings.reinitializeGUIdelay) { // println("attempting to draw GUI..."); try { // println("GUI DRAW!!! " + millis()); @@ -1281,8 +1273,8 @@ void systemDraw() { //for drawing to the screen wm.draw(); } catch (Exception e) { println(e.getMessage()); - reinitializeGUIdelay = reinitializeGUIdelay * 2; - println("OpenBCI_GUI: systemDraw: New GUI reinitialize delay = " + reinitializeGUIdelay); + settings.reinitializeGUIdelay = settings.reinitializeGUIdelay * 2; + println("OpenBCI_GUI: systemDraw: New GUI reinitialize delay = " + settings.reinitializeGUIdelay); } } else { //reinitializing GUI after resize diff --git a/OpenBCI_GUI/SoftwareSettings.pde b/OpenBCI_GUI/SoftwareSettings.pde index 1db88798f..20698f600 100644 --- a/OpenBCI_GUI/SoftwareSettings.pde +++ b/OpenBCI_GUI/SoftwareSettings.pde @@ -43,6 +43,13 @@ class SoftwareSettings { //impose minimum gui width and height in openBCI_GUI.pde int minGUIWidth = 705; int minGUIHeight = 400; + //for screen resizing + boolean screenHasBeenResized = false; + float timeOfLastScreenResize = 0; + float timeOfGUIreinitialize = 0; + int reinitializeGUIdelay = 125; + int widthOfLastScreen = 0; + int heightOfLastScreen = 0; //default layout variables int currentLayout; //Used to time the GUI intro animation @@ -104,6 +111,8 @@ class SoftwareSettings { int[] freqsSave; boolean[] channelActivitySave; int numSSVEPs; + //Used to check if a playback file has data + int minNumRowsPlaybackFile = int(getSampleRateSafe()); //default configuration settings file location and file name variables public final String guiDataPath = System.getProperty("user.home")+File.separator+"Documents"+File.separator+"OpenBCI_GUI"+File.separator; diff --git a/OpenBCI_GUI/TopNav.pde b/OpenBCI_GUI/TopNav.pde index ae47ca0d5..c96fb2087 100644 --- a/OpenBCI_GUI/TopNav.pde +++ b/OpenBCI_GUI/TopNav.pde @@ -1109,7 +1109,7 @@ class TutorialSelector { tutorialOptions.get(i).setIsActive(false); tutorialOptions.get(i).goToURL(); println("Attempting to use your default web browser to open " + tutorialOptions.get(i).myURL); - output("Layout [" + tutorialSelected + "] selected."); + //output("Help button [" + tutorialSelected + "] selected."); toggleVisibility(); //shut layoutSelector if something is selected //open corresponding link } @@ -1168,9 +1168,9 @@ class TutorialSelector { buttonNumber = 2; h = margin*(buttonNumber+2) + b_h*(buttonNumber+1); - tempTutorialButton = new Button(x + margin, y + margin*(buttonNumber+1) + b_h*(buttonNumber), b_w, b_h, "OpenBCI Forum"); + tempTutorialButton = new Button(x + margin, y + margin*(buttonNumber+1) + b_h*(buttonNumber), b_w, b_h, "Troubleshooting Guide"); tempTutorialButton.setFont(p5, 12); - tempTutorialButton.setURL("https://openbci.com/forum/"); + tempTutorialButton.setURL("https://docs.openbci.com/docs/10Troubleshooting/GUI_Troubleshooting"); tutorialOptions.add(tempTutorialButton); buttonNumber = 3; @@ -1179,5 +1179,12 @@ class TutorialSelector { tempTutorialButton.setFont(p5, 12); tempTutorialButton.setURL("https://openbci.github.io/Documentation/docs/06Software/01-OpenBCISoftware/GUIWidgets#custom-widget"); tutorialOptions.add(tempTutorialButton); + + buttonNumber = 4; + h = margin*(buttonNumber+2) + b_h*(buttonNumber+1); + tempTutorialButton = new Button(x + margin, y + margin*(buttonNumber+1) + b_h*(buttonNumber), b_w, b_h, "OpenBCI Forum"); + tempTutorialButton.setFont(p5, 12); + tempTutorialButton.setURL("https://openbci.com/forum/"); + tutorialOptions.add(tempTutorialButton); } } diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 6ec6bc5f8..9a2ae0ffc 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -56,6 +56,8 @@ class W_Focus extends Widget { // two sliders for alpha and one slider for beta FocusSlider sliderAlphaMid, sliderBetaMid; FocusSlider_Static sliderAlphaTop; + Button infoButton; + int infoButtonSize = 18; W_Focus(PApplet _parent){ super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) @@ -77,6 +79,15 @@ class W_Focus extends Widget { addDropdown("ChooseFocusColor", "Theme", Arrays.asList("Green", "Orange", "Cyan"), settings.focusThemeSave); addDropdown("StrokeKeyWhenFocused", "KeyPress", Arrays.asList("OFF", "UP", "SPACE"), settings.focusKeySave); + //More info button + infoButton = new Button(x + w - dropdownWidth * 2 - infoButtonSize - 10, y - navH + 2, infoButtonSize, infoButtonSize, "?", 14); + infoButton.setCornerRoundess((int)(navHeight-6)); + infoButton.setFont(p5,12); + infoButton.setColorNotPressed(color(57,128,204)); + infoButton.setFontColorNotActive(color(255)); + infoButton.setHelpText("Click this button to view details on the Focus Widget."); + infoButton.hasStroke(false); + // prepare simulate keystroking try { robot = new Robot(); @@ -326,7 +337,8 @@ class W_Focus extends Widget { fill(cDark); text("This widget recognizes a focused mental state by looking at alpha and beta wave levels on channel 1 & 2. For better result, try setting the smooth at 0.98 in FFT plot.\n\nThe algorithm thinks you are focused when the alpha level is between 0.7~2uV and the beta level is between 0~0.7 uV, otherwise it thinks you are not focused. It is designed based on Jordan Frand’s brainwave and tested on other subjects, and you can playback Jordan's file in W_Focus folder.\n\nYou can turn on KeyPress and use your focus play a game, so whenever you are focused, the specified UP arrow or SPACE key will be pressed down, otherwise it will be released. You can also try out the Arduino output feature, example and instructions are included in W_Focus folder. For more information, contact wangshu.sun@hotmail.com.", rp*1.5, rp*1.5, w-rp*3, h-rp*3); } - // draw the button that toggles information + + /* noStroke(); fill(cDark); ellipse(xb, yb, rb, rb); @@ -337,18 +349,21 @@ class W_Focus extends Widget { } else { text("?", xb, yb); } + */ //----------------- revert origin point of draw to default ----------------- translate(-x, -y); textAlign(LEFT, BASELINE); - + // draw the button that toggles information + infoButton.draw(); popStyle(); - } void screenResized(){ super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + infoButton.setPos(x + w - dropdownWidth * 2 - infoButtonSize - 10, y - navH + 2); + update_graphic_parameters(); //update sliders... @@ -385,6 +400,10 @@ class W_Focus extends Widget { } } + if (infoButton.isMouseHere()) { + infoButton.setIsActive(true); + } + // sliders sliderAlphaMid.mousePressed(); sliderAlphaTop.mousePressed(); @@ -394,6 +413,11 @@ class W_Focus extends Widget { void mouseReleased(){ super.mouseReleased(); //calls the parent mouseReleased() method of Widget (DON'T REMOVE) + if (infoButton.isActive && infoButton.isMouseHere()) { + showAbout = !showAbout; + } + infoButton.setIsActive(false); + // sliders sliderAlphaMid.mouseReleased(); sliderAlphaTop.mouseReleased(); diff --git a/OpenBCI_GUI/W_Networking.pde b/OpenBCI_GUI/W_Networking.pde index 84bca1c48..814ab971d 100644 --- a/OpenBCI_GUI/W_Networking.pde +++ b/OpenBCI_GUI/W_Networking.pde @@ -125,10 +125,13 @@ class W_Networking extends Widget { settings.nwDataType4 = 0; settings.nwSerialPort = "None"; settings.nwProtocolSave = protocolIndex; //save default protocol index, or 0, updates in the Protocol() function - - dataTypes = Arrays.asList(settings.nwDataTypesArray); //Add any new widgets capable of streaming here + + dataTypes = new LinkedList(Arrays.asList(settings.nwDataTypesArray)); //Add any new widgets capable of streaming here + //Only show pulse data type when using Cyton in Live + if (eegDataSource != DATASOURCE_CYTON) { + dataTypes.remove("Pulse"); + } defaultBaud = "115200"; - // baudRates = Arrays.asList("1200", "9600", "57600", "115200"); baudRates = Arrays.asList(settings.nwBaudRatesArray); protocolMode = "Serial"; //default to Serial addDropdown("Protocol", "Protocol", Arrays.asList(settings.nwProtocolArray), protocolIndex); diff --git a/OpenBCI_GUI/W_Playback.pde b/OpenBCI_GUI/W_Playback.pde index fbfad1f3a..f92326c4a 100644 --- a/OpenBCI_GUI/W_Playback.pde +++ b/OpenBCI_GUI/W_Playback.pde @@ -199,15 +199,23 @@ void playbackSelectedWidgetButton(File selection) { } else { println("W_Playback: playbackSelected: User selected " + selection.getAbsolutePath()); playbackFileSelected(selection.getAbsolutePath(), selection.getName()); - reInitAfterPlaybackSelected(); + if (playbackFileIsEmpty) { + haltLoadingFile(selection.getAbsolutePath()); + } else { + reInitAfterPlaybackSelected(); + } } } //Activated when user selects a file using the recent file MenuList -void userSelectedPlaybackMenuList (String fullPath, int listItem) { - if (new File(fullPath).isFile()) { - playbackFileSelected (fullPath, listItem); - reInitAfterPlaybackSelected(); +void userSelectedPlaybackMenuList (String filePath, int listItem) { + if (new File(filePath).isFile()) { + playbackFileSelected(filePath, listItem); + if (playbackFileIsEmpty) { + haltLoadingFile(filePath); + } else { + reInitAfterPlaybackSelected(); + } } else { outputError("W_Playback: Selected file does not exist. Try another file or clear settings to remove this entry."); } @@ -241,6 +249,10 @@ void playbackFileSelected(File selection) { println("DataLogging: playbackSelected: User selected " + selection.getAbsolutePath()); //Set the name of the file playbackFileSelected(selection.getAbsolutePath(), selection.getName()); + if (playbackFileIsEmpty) { + haltLoadingFile(selection.getAbsolutePath()); + return; + } } } @@ -259,6 +271,10 @@ void playbackFileSelected (String longName, int listItem) { playbackHistoryFileExists = false; } playbackFileSelected(longName, shortName); + if (playbackFileIsEmpty) { + haltLoadingFile(longName); + return; + } } //Handles the work for the above two cases @@ -267,6 +283,7 @@ void playbackFileSelected (String longName, String shortName) { playbackData_ShortName = shortName; //Process the playback file processNewPlaybackFile(); + if (playbackFileIsEmpty) return; //Determine the number of channels determineNumChanFromFile(playbackData_table); //Output new playback settings to GUI as success @@ -282,7 +299,7 @@ void playbackFileSelected (String longName, String shortName) { playbackHistoryFileExists = false; } //add playback file that was processed to the JSON history - savePlaybackFileToHistory(playbackData_ShortName); + savePlaybackFileToHistory(longName); } void processNewPlaybackFile() { //Also used in DataLogging.pde @@ -317,19 +334,36 @@ void determineNumChanFromFile(Table datatable) { void initPlaybackFileToTable() { //also used in OpenBCI_GUI.pde on system start //open and load the data file println("OpenBCI_GUI: initSystem: loading playback data from " + playbackData_fname); + playbackFileIsEmpty = false; //reset this flag each time playback data is loaded try { playbackData_table = new Table_CSV(playbackData_fname); //removing first column of data from data file...the first column is a time index and not eeg data playbackData_table.removeColumn(0); } catch (Exception e) { - println("OpenBCI_GUI: initSystem: could not open file for playback: " + playbackData_fname); - println(" : quitting..."); - hub.killAndShowMsg("Could not open file for playback: " + playbackData_fname); + println("initPlaybackFileToTable: Encountered an error while loading " + playbackData_fname); } println("OpenBCI_GUI: initSystem: loading complete. " + playbackData_table.getRowCount() + " rows of data, which is " + round(float(playbackData_table.getRowCount())/getSampleRateSafe()) + " seconds of EEG data"); + + //If a playback file has less than one second of data, throw an error using a flag + if (playbackData_table.getRowCount() <= settings.minNumRowsPlaybackFile) { + playbackFileIsEmpty = true; + } } +void haltLoadingFile(String _filePath) { + if (systemMode == SYSTEMMODE_POSTINIT) { + abandonInit = true; + initSystemButton.setString("START SESSION"); + controlPanel.open(); + haltSystem(); + } + //Go ahead and remove this file from the Playback History + JSONObject playbackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray recentFilesArray = playbackHistoryJSON.getJSONArray("playbackFileHistory"); + removePlaybackFileFromHistory(recentFilesArray, _filePath); + outputError("Playback file appears empty. Try loading a different file."); +} void reinitializeCoreDataAndFFTBuffer() { //println("Data Processing Number of Channels is: " + dataProcessing.nchan); @@ -366,7 +400,7 @@ void reinitializeCoreDataAndFFTBuffer() { } -void savePlaybackFileToHistory(String fileNameToAdd) { +void savePlaybackFileToHistory(String fileName) { int maxNumHistoryFiles = 36; if (playbackHistoryFileExists) { println("Found user playback history file!"); @@ -375,14 +409,7 @@ void savePlaybackFileToHistory(String fileNameToAdd) { //println("ARRAYSIZE-Check1: " + int(recentFilesArray.size())); //Recent file has recentFileNumber=0, and appears at the end of the JSON array //check if already in the list, if so, remove from the list - for (int i = 0; i < recentFilesArray.size(); i++) { - JSONObject playbackFile = recentFilesArray.getJSONObject(i); - //println("CHECKING " + i + " : " + playbackFile.getString("id") + " == " + fileNameToAdd + " ?"); - if (playbackFile.getString("id").equals(fileNameToAdd)) { - recentFilesArray.remove(i); - //println("REMOVED: " + fileNameToAdd); - } - } + removePlaybackFileFromHistory(recentFilesArray, playbackData_fname); //next, increment fileNumber of all current entries +1 for (int i = 0; i < recentFilesArray.size(); i++) { JSONObject playbackFile = recentFilesArray.getJSONObject(i); @@ -422,7 +449,7 @@ void savePlaybackFileToHistory(String fileNameToAdd) { //save selected playback file to position 1 in recent file history JSONObject mostRecentFile = new JSONObject(); mostRecentFile.setInt("recentFileNumber", 0); - mostRecentFile.setString("id", fileNameToAdd); + mostRecentFile.setString("id", playbackData_ShortName); mostRecentFile.setString("filePath", playbackData_fname); newHistoryFileArray.setJSONObject(0, mostRecentFile); //newHistoryFile.setJSONArray("") @@ -436,3 +463,15 @@ void savePlaybackFileToHistory(String fileNameToAdd) { playbackHistoryFileExists = true; } } + +void removePlaybackFileFromHistory(JSONArray array, String _filePath) { + //check if already in the list, if so, remove from the list + for (int i = 0; i < array.size(); i++) { + JSONObject playbackFile = array.getJSONObject(i); + //println("CHECKING " + i + " : " + playbackFile.getString("id") + " == " + fileName + " ?"); + if (playbackFile.getString("filePath").equals(_filePath)) { + array.remove(i); + //println("REMOVED: " + fileName); + } + } +} diff --git a/OpenBCI_GUI/W_SSVEP.pde b/OpenBCI_GUI/W_SSVEP.pde index ef30f369c..6f2e7827e 100644 --- a/OpenBCI_GUI/W_SSVEP.pde +++ b/OpenBCI_GUI/W_SSVEP.pde @@ -56,6 +56,7 @@ class W_SSVEP extends Widget { + "This widget is currently in beta mode and requires more input and testing from the OpenBCI Community."; int ssvepHelpTextFontSize = 16; Button infoButton; + int infoButtonSize = 18; W_SSVEP(PApplet _parent) { @@ -102,7 +103,7 @@ class W_SSVEP extends Widget { cp5_ssvep.setAutoDraw(false); showAbout = false; //set Default start value for showing about section as fault - infoButton = new Button(x + w - 80, y - navH + 2, 18, 18, "?", 14); + infoButton = new Button(x + w - dropdownWidth - infoButtonSize - 10, y - navH + 2, infoButtonSize, infoButtonSize, "?", 14); infoButton.setCornerRoundess((int)(navHeight-6)); infoButton.setFont(p5,12); infoButton.setColorNotPressed(color(57,128,204)); @@ -245,7 +246,7 @@ class W_SSVEP extends Widget { s = h; } - infoButton.setPos(x + w - 100, y - navH + 2); + infoButton.setPos(x + w - dropdownWidth - infoButtonSize - 10, y - navH + 2); setFreqDropdownSizes(); diff --git a/OpenBCI_GUI/W_TimeSeries.pde b/OpenBCI_GUI/W_TimeSeries.pde index 35bfebae6..5d9c0551f 100644 --- a/OpenBCI_GUI/W_TimeSeries.pde +++ b/OpenBCI_GUI/W_TimeSeries.pde @@ -420,7 +420,7 @@ class ChannelBar{ impButton_diameter = 22; impCheckButton = new Button (x + 36, y + int(h/2) - int(impButton_diameter/2), impButton_diameter, impButton_diameter, "\u2126", fontInfo.buttonLabel_size); impCheckButton.setHelpText("Click to toggle impedance check for channel " + channelNumber + "."); - impCheckButton.setFont(h2, 16); + impCheckButton.setFont(h3, 16); //override the default font and fontsize impCheckButton.setCircleButton(true); impCheckButton.setColorNotPressed(color(255)); //White background impCheckButton.textColorNotActive = color(0); //Black text diff --git a/README.md b/README.md index 3fbbe551b..afcdb4e40 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,11 @@ OpenGL acceleration is required. ## Troubleshooting -The OpenBCI Hub comes installed in recent versions of the GUI. When running the GUI from Processing code, [please follow these instructions](https://openbci.github.io/Documentation/docs/06Software/01-OpenBCISoftware/GUIDocs#install-openbci-hub-on-mac-linux-windows) for getting the **critical** piece of software called the OpenBCI HUB for Mac/Linux/Windows. Thanks and happy hacking! +- The OpenBCI Hub comes installed in recent versions of the GUI. When running the GUI from Processing code, [please follow these instructions](https://openbci.github.io/Documentation/docs/06Software/01-OpenBCISoftware/GUIDocs#install-openbci-hub-on-mac-linux-windows) for getting the **critical** piece of software called the OpenBCI HUB for Mac/Linux/Windows. Thanks and happy hacking! + +- If you are on a Mac and you seem to get a "spinning wheel of death" when trying to open a dialog box to view files (example "SELECT PLAYBACK FILE" button), [please update your Java Runtime Environment](https://www.java.com/en/download/). This happens because Java was not packaged with a version of the GUI producing this error. + +- For more on GUI troubleshooting, head over to the [GUI Troublshooting Doc](https://docs.openbci.com/docs/10Troubleshooting/GUI_Troubleshooting). ## License: