From a718fa63a3f822d4b00a850279e92357adbad478 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Thu, 18 Mar 2021 23:54:42 -0500 Subject: [PATCH 01/34] Make empty focus widget to start --- OpenBCI_GUI/W_Focus.pde | 105 ++++++ OpenBCI_GUI/W_Focus_Old.pde | 584 ++++++++++++++++++++++++++++++++++ OpenBCI_GUI/WidgetManager.pde | 4 +- 3 files changed, 691 insertions(+), 2 deletions(-) create mode 100644 OpenBCI_GUI/W_Focus.pde create mode 100644 OpenBCI_GUI/W_Focus_Old.pde diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde new file mode 100644 index 000000000..86c13da60 --- /dev/null +++ b/OpenBCI_GUI/W_Focus.pde @@ -0,0 +1,105 @@ + +//////////////////////////////////////////////////// +// +// 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_Focus 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_Focus(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. + localCP5 = new ControlP5(ourApplet); + localCP5.setGraphics(ourApplet, 0,0); + localCP5.setAutoDraw(false); + + createWidgetTemplateButton(); + + } + + public void update(){ + super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + + //put your code here... + } + + public void draw(){ + super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + + //remember to refer to x,y,w,h which are the positioning variables of the Widget class + + //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) + + //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 + 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 + } + + 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. + } + + //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 + widgetTemplateButton.setBorderColor(OBJECT_BORDER_GREY); + //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 (!topNav.configSelector.isVisible && !topNav.layoutSelector.isVisible) { + openURLInBrowser("https://openbci.github.io/Documentation/docs/06Software/01-OpenBCISoftware/GUIWidgets#custom-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 + private void customFunction(){ + //this is a fake function... replace it with something relevant to this widget + + } + +}; + diff --git a/OpenBCI_GUI/W_Focus_Old.pde b/OpenBCI_GUI/W_Focus_Old.pde new file mode 100644 index 000000000..b16541d99 --- /dev/null +++ b/OpenBCI_GUI/W_Focus_Old.pde @@ -0,0 +1,584 @@ + +//////////////////////////////////////////////////// +// +// W_focus.pde (ie "Focus Widget") +// +// +// Created by: Richard Waltman, March 2021 +// +///////////////////////////////////////////////////, + + +// color enums +public enum FocusColors { + GREEN, CYAN, ORANGE +} + +class W_Focus_Old extends Widget { + //to see all core variables/methods of the Widget class, refer to Widget.pde + Robot robot; // a key-stroking robot waiting for focused state + boolean enableKey = false; // enable key stroke by the robot + int keyNum = 0; // 0 - up arrow, 1 - Spacebar + boolean enableSerial = false; // send the Focused state to Arduino + + // output values + float alpha_avg = 0, beta_avg = 0; + boolean isFocused; + + // alpha, beta threshold default values + float alpha_thresh = 0.7, beta_thresh = 0.7, alpha_upper = 2, beta_upper = 2; + + // drawing parameters + boolean showAbout = false; + PFont myfont = createFont("fonts/Raleway-SemiBold.otf", 12); + PFont f = f1; //for widget title + + FocusColors focusColors = FocusColors.GREEN; + + color cBack, cDark, cMark, cFocus, cWave, cPanel; + + // float x, y, w, h; //widget topleft xy, width and height + float xc, yc, wc, hc; // crystal ball center xy, width and height + float wg, hg; //graph width, graph height + float wl; // line width + float xg1, yg1; //graph1 center xy + float xg2, yg2; //graph1 center xy + float rp; // padding radius + float rb; // button radius + float xb, yb; // button center xy + + // two sliders for alpha and one slider for beta + FocusSlider sliderAlphaMid, sliderBetaMid; + FocusSlider_Static sliderAlphaTop; + Button infoButton; + int infoButtonSize = 18; + + W_Focus_Old(PApplet _parent){ + super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + + // initialize graphics parameters + onColorChange(); + update_graphic_parameters(); + + // sliders + sliderAlphaMid = new FocusSlider(x + xg1 + wg * 0.8, y + yg1 + hg/2, y + yg1 - hg/2, alpha_thresh / alpha_upper); + sliderAlphaTop = new FocusSlider_Static(x + xg1 + wg * 0.8, y + yg1 + hg/2, y + yg1 - hg/2); + sliderBetaMid = new FocusSlider(x + xg2 + wg * 0.8, y + yg2 + hg/2, y + yg2 - hg/2, beta_thresh / beta_upper); + + ///Focus widget settings + //settings.focusThemeSave = 0; + //settings.focusKeySave = 0; + + //Dropdowns. + addDropdown("ChooseFocusColor", "Theme", Arrays.asList("Green", "Orange", "Cyan"), 0); + addDropdown("StrokeKeyWhenFocused", "KeyPress", Arrays.asList("OFF", "UP", "SPACE"), 0); + + //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); + */ + + } + + void onColorChange() { + switch(focusColors) { + case GREEN: + cBack = #ffffff; //white + cDark = #3068a6; //medium/dark blue + cMark = #4d91d9; //lighter blue + cFocus = #b8dc69; //theme green + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + case ORANGE: + cBack = #ffffff; //white + cDark = #377bc4; //medium/dark blue + cMark = #5e9ee2; //lighter blue + cFocus = #fcce51; //orange + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + case CYAN: + cBack = #ffffff; //white + cDark = #377bc4; //medium/dark blue + cMark = #5e9ee2; //lighter blue + cFocus = #91f4fc; //cyan + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + } + } + + void update(){ + super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + updateFocusState(); // focus calculation + invokeKeyStroke(); // robot keystroke + + // update sliders + sliderAlphaMid.update(); + sliderAlphaTop.update(); + sliderBetaMid.update(); + + // update threshold values + alpha_thresh = alpha_upper * sliderAlphaMid.getVal(); + beta_thresh = beta_upper * sliderBetaMid.getVal(); + + alpha_upper = sliderAlphaTop.getVal() * 2; + beta_upper = alpha_upper; + + sliderAlphaMid.setVal(alpha_thresh / alpha_upper); + sliderBetaMid.setVal(beta_thresh / beta_upper); + } + + void updateFocusState() { + // focus detection algorithm based on Jordan's clean mind: focus == high alpha average && low beta average + float FFT_freq_Hz, FFT_value_uV; + int alpha_count = 0, beta_count = 0; + + for (int Ichan=0; Ichan < 2; Ichan++) { // only consider first two channels + for (int Ibin=0; Ibin < fftBuff[Ichan].specSize(); Ibin++) { + FFT_freq_Hz = fftBuff[Ichan].indexToFreq(Ibin); + FFT_value_uV = fftBuff[Ichan].getBand(Ibin); + + if (FFT_freq_Hz >= 7.5 && FFT_freq_Hz <= 12.5) { //FFT bins in alpha range + alpha_avg += FFT_value_uV; + alpha_count ++; + } + else if (FFT_freq_Hz > 12.5 && FFT_freq_Hz <= 30) { //FFT bins in beta range + beta_avg += FFT_value_uV; + beta_count ++; + } + } + } + + alpha_avg = alpha_avg / alpha_count; // average uV per bin + beta_avg = beta_avg / beta_count; // average uV per bin + + // version 1 + if (alpha_avg > alpha_thresh && alpha_avg < alpha_upper && beta_avg < beta_thresh) { + isFocused = true; + } else { + isFocused = false; + } + } + + void invokeKeyStroke() { + /* + // robot keystroke + if (enableKey) { + if (keyNum == 0) { + if (isFocused) { + robot.keyPress(KeyEvent.VK_UP); //if you want to change to other key, google "java keyEvent" to see the full list + } + else { + robot.keyRelease(KeyEvent.VK_UP); + } + } + else if (keyNum == 1) { + if (isFocused) { + robot.keyPress(KeyEvent.VK_SPACE); //if you want to change to other key, google "java keyEvent" to see the full list + } + else { + robot.keyRelease(KeyEvent.VK_SPACE); + } + } + } + */ + } + + void draw(){ + super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + + //remember to refer to x,y,w,h which are the positioning variables of the Widget class + pushStyle(); + + //----------------- presettings before drawing Focus Viz -------------- + translate(x, y); + textAlign(CENTER, CENTER); + textFont(myfont); + + //----------------- draw background rectangle and panel ----------------- + fill(cBack); + noStroke(); + rect(0, 0, w, h); + + fill(cPanel); + noStroke(); + rect(rp, rp, w-rp*2, h-rp*2); + + //----------------- draw focus crystalball ----------------- + noStroke(); + if (isFocused) { + fill(cFocus); + stroke(cFocus); + } else { + fill(cDark); + } + ellipse(xc, yc, wc, hc); + noStroke(); + // draw focus label + if (isFocused) { + fill(cFocus); + text("focused!", xc, yc + hc/2 + 16); + } else { + fill(cMark); + text("not focused", xc, yc + hc/2 + 16); + } + + //----------------- draw alpha meter ----------------- + noStroke(); + fill(cDark); + rect(xg1 - wg/2, yg1 - hg/2, wg, hg); + + float hat = map(alpha_thresh, 0, alpha_upper, 0, hg); // alpha threshold height + stroke(cMark); + line(xg1 - wl/2, yg1 + hg/2, xg1 + wl/2, yg1 + hg/2); + line(xg1 - wl/2, yg1 - hg/2, xg1 + wl/2, yg1 - hg/2); + line(xg1 - wl/2, yg1 + hg/2 - hat, xg1 + wl/2, yg1 + hg/2 - hat); + + // draw alpha zone and text + noStroke(); + if (alpha_avg > alpha_thresh && alpha_avg < alpha_upper) { + fill(cFocus); + } else { + fill(cMark); + } + rect(xg1 - wg/2, yg1 - hg/2, wg, hg - hat); + text("alpha", xg1, yg1 + hg/2 + 16); + + // draw connection between two sliders + stroke(cMark); + line(xg1 + wg * 0.8, yg1 - hg/2 + 10, xg1 + wg * 0.8, yg1 + hg/2 - hat - 10); + + noStroke(); + fill(cMark); + text(String.format("%.01f", alpha_upper), xg1 - wl/2 - 14, yg1 - hg/2); + text(String.format("%.01f", alpha_thresh), xg1 - wl/2 - 14, yg1 + hg/2 - hat); + text("0.0", xg1 - wl/2 - 14, yg1 + hg/2); + + stroke(cWave); + strokeWeight(4); + float ha = map(alpha_avg, 0, alpha_upper, 0, hg); //alpha height + ha = constrain(ha, 0, hg); + line(xg1 - wl/2, yg1 + hg/2 - ha, xg1 + wl/2, yg1 + hg/2 - ha); + strokeWeight(1); + + //----------------- draw beta meter ----------------- + noStroke(); + fill(cDark); + rect(xg2 - wg/2, yg2 - hg/2, wg, hg); + + float hbt = map(beta_thresh, 0, beta_upper, 0, hg); // beta threshold height + stroke(cMark); + line(xg2 - wl/2, yg2 + hg/2, xg2 + wl/2, yg2 + hg/2); + line(xg2 - wl/2, yg2 - hg/2, xg2 + wl/2, yg2 - hg/2); + line(xg2 - wl/2, yg2 + hg/2 - hbt, xg2 + wl/2, yg2 + hg/2 - hbt); + + // draw beta zone and text + noStroke(); + if (beta_avg < beta_thresh) { + fill(cFocus); + } else { + fill(cMark); + } + rect(xg2 - wg/2, yg2 + hg/2 - hbt, wg, hbt); + text("beta", xg2, yg2 + hg/2 + 16); + + // draw connection between slider and bottom + stroke(cMark); + float yt = yg2 + hg/2 - hbt + 10; // y threshold + yt = constrain(yt, yg2 - hg/2 + 10, yg2 + hg/2); + line(xg2 + wg * 0.8, yg2 + hg/2, xg2 + wg * 0.8, yt); + + noStroke(); + fill(cMark); + text(String.format("%.01f", beta_upper), xg2 - wl/2 - 14, yg2 - hg/2); + text(String.format("%.01f", beta_thresh), xg2 - wl/2 - 14, yg2 + hg/2 - hbt); + text("0.0", xg2 - wl/2 - 14, yg2 + hg/2); + + stroke(cWave); + strokeWeight(4); + float hb = map(beta_avg, 0, beta_upper, 0, hg); //beta height + hb = constrain(hb, 0, hg); + line(xg2 - wl/2, yg2 + hg/2 - hb, xg2 + wl/2, yg2 + hg/2 - hb); + strokeWeight(1); + + translate(-x, -y); + + //------------------ draw sliders -------------------- + sliderAlphaMid.draw(); + sliderAlphaTop.draw(); + sliderBetaMid.draw(); + + //----------------- draw about button ----------------- + translate(x, y); + if (showAbout) { + stroke(cDark); + fill(cBack); + + rect(rp, rp, w-rp*2, h-rp*2); + textAlign(LEFT, TOP); + 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); + } + + /* + noStroke(); + fill(cDark); + ellipse(xb, yb, rb, rb); + fill(cBack); + textAlign(CENTER, CENTER); + if (showAbout) { + text("x", xb, yb); + } 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... + sliderAlphaMid.screenResized(x + xg1 + wg * 0.8, y + yg1 + hg/2, y + yg1 - hg/2); + sliderAlphaTop.screenResized(x + xg1 + wg * 0.8, y + yg1 + hg/2, y + yg1 - hg/2); + sliderBetaMid.screenResized(x + xg2 + wg * 0.8, y + yg2 + hg/2, y + yg2 - hg/2); + } + + void update_graphic_parameters () { + xc = w/4; + yc = h/2; + wc = w/4; + hc = w/4; + wg = 0.07*w; + hg = 0.64*h; + wl = 0.11*w; + xg1 = 0.6*w; + yg1 = 0.5*h; + xg2 = 0.83*w; + yg2 = 0.5*h; + rp = max(w*0.05, h*0.05); + rb = 20; + xb = w-rp; + yb = rp; + } + + void mousePressed(){ + super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + + // about button + if (!this.dropdownIsActive) { + if (dist(mouseX,mouseY,xb+x,yb+y) <= rb) { + showAbout = !showAbout; + } + } + + /* + if (infoButton.isMouseHere()) { + infoButton.setIsActive(true); + } + */ + + // sliders + sliderAlphaMid.mousePressed(); + sliderAlphaTop.mousePressed(); + sliderBetaMid.mousePressed(); + } + + 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(); + sliderBetaMid.mouseReleased(); + } + +}; + +/* ---------------------- Supporting Slider Classes ---------------------------*/ + +// abstract basic slider +public abstract class BasicSlider { + float x, y, w, h; // center x, y. w, h means width and height of triangle + float yBot, yTop; // y range. Notice val of top y is less than bottom y + boolean isPressed = false; + color cNormal = #CCCCCC; + color cPressed = #FF0000; + + BasicSlider(float _x, float _yBot, float _yTop) { + x = _x; + yBot = _yBot; + yTop = _yTop; + w = 10; + h = 10; + } + + // abstract functions + + abstract void update(); + abstract void screenResized(float _x, float _yBot, float _yTop); + abstract float getVal(); + abstract void setVal(float _val); + + // shared functions + + void draw() { + if (isPressed) fill(cPressed); + else fill(cNormal); + noStroke(); + triangle(x-w/2, y, x+w/2, y-h/2, x+w/2, y+h/2); + } + + void mousePressed() { + if (abs(mouseX - (x)) <= w/2 && abs(mouseY - y) <= h/2) { + isPressed = true; + } + } + + void mouseReleased() { + if (isPressed) { + isPressed = false; + } + } +} + +// middle slider that changes value and move +public class FocusSlider extends BasicSlider { + private float val = 0; // val = 0 ~ 1 -> yBot to yTop + final float valMin = 0; + final float valMax = 0.90; + FocusSlider(float _x, float _yBot, float _yTop, float _val) { + super(_x, _yBot, _yTop); + val = constrain(_val, valMin, valMax); + y = map(val, 0, 1, yBot, yTop); + } + + public void update() { + if (isPressed) { + float newVal = map(mouseY, yBot, yTop, 0, 1); + val = constrain(newVal, valMin, valMax); + y = map(val, 0, 1, yBot, yTop); + println("Focus: " + val); + } + } + + public void screenResized(float _x, float _yBot, float _yTop) { + x = _x; + yBot = _yBot; + yTop = _yTop; + y = map(val, 0, 1, yBot, yTop); + } + + public float getVal() { + return val; + } + + public void setVal(float _val) { + val = constrain(_val, valMin, valMax); + y = map(val, 0, 1, yBot, yTop); + } +} + +// top slider that changes value but doesn't move +public class FocusSlider_Static extends BasicSlider { + private float val = 0; // val = 0 ~ 1 -> yBot to yTop + final float valMin = 0.5; + final float valMax = 5.0; + FocusSlider_Static(float _x, float _yBot, float _yTop) { + super(_x, _yBot, _yTop); + val = 1; + y = yTop; + } + + public void update() { + if (isPressed) { + float diff = map(mouseY, yBot, yTop, -0.07, 0); + val = constrain(val + diff, valMin, valMax); + println("Focus: " + val); + } + } + + public void screenResized(float _x, float _yBot, float _yTop) { + x = _x; + yBot = _yBot; + yTop = _yTop; + y = yTop; + } + + public float getVal() { + return val; + } + + public void setVal(float _val) { + val = constrain(_val, valMin, valMax); + } + +} + +/* ---------------- Global Functions For Menu Entries --------------------*/ + +// //These functions need to be global! These functions are activated when an item from the corresponding dropdown is selected +void StrokeKeyWhenFocused(int n){ + /* + // println("Item " + (n+1) + " selected from Dropdown 1"); + if(n==0){ + //do this + w_focus.enableKey = false; + //println("The robot ignores focused state and will not press any key."); + } else if(n==1){ + //do this instead + w_focus.enableKey = true; + w_focus.keyNum = 0; + //println("The robot will keep pressing Arrow Up key when you are focused, and release the key when you lose focus."); + } else if(n==2){ + //do this instead + w_focus.enableKey = true; + w_focus.keyNum = 1; + //println("The robot will keep pressing Spacebar when you are focused, and release the key when you lose focus."); + } + //settings.focusKeySave = n; + //closeAllDropdowns(); // do this at the end of all widget-activated functions to ensure proper widget interactivity ... we want to make sure a click makes the menu close + */ +} + +void ChooseFocusColor(int n){ + /* + if(n==0){ + w_focus.focusColors = FocusColors.GREEN; + w_focus.onColorChange(); + } else if(n==1){ + w_focus.focusColors = FocusColors.ORANGE; + w_focus.onColorChange(); + } else if(n==2){ + w_focus.focusColors = FocusColors.CYAN; + w_focus.onColorChange(); + } + //settings.focusThemeSave = n; + //closeAllDropdowns(); + */ +} \ No newline at end of file diff --git a/OpenBCI_GUI/WidgetManager.pde b/OpenBCI_GUI/WidgetManager.pde index 4d7c94d2b..ed7f98dc5 100644 --- a/OpenBCI_GUI/WidgetManager.pde +++ b/OpenBCI_GUI/WidgetManager.pde @@ -24,6 +24,7 @@ W_DigitalRead w_digitalRead; W_playback w_playback; W_Spectrogram w_spectrogram; W_PacketLoss w_packetLoss; +W_Focus w_focus; //ADD YOUR WIDGET TO WIDGETS OF WIDGETMANAGER void setupWidgets(PApplet _this, ArrayList w){ @@ -126,13 +127,12 @@ void setupWidgets(PApplet _this, ArrayList w){ w_template1.setTitle("Widget Template 1"); addWidget(w_template1, w); - /* + //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 w_focus = new W_Focus(_this); w_focus.setTitle("Focus Widget"); addWidget(w_focus, w); // println(" setupWidgets focus widget -- " + millis()); - */ } From 3c4c012cad9cb27d09b941df4aff9fffb7504b5f Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 19 Mar 2021 15:35:35 -0500 Subject: [PATCH 02/34] WIP Front End - new Focus widget front-end layout --- Networking-Test-Kit/brainflow_metric.py | 80 +++++ OpenBCI_GUI/ADS1299SettingsController.pde | 21 +- OpenBCI_GUI/W_Accelerometer.pde | 5 +- OpenBCI_GUI/W_Focus.pde | 404 ++++++++++++++++++++-- OpenBCI_GUI/W_Focus_Old.pde | 3 +- OpenBCI_GUI/WidgetManager.pde | 12 +- 6 files changed, 471 insertions(+), 54 deletions(-) create mode 100644 Networking-Test-Kit/brainflow_metric.py diff --git a/Networking-Test-Kit/brainflow_metric.py b/Networking-Test-Kit/brainflow_metric.py new file mode 100644 index 000000000..955889630 --- /dev/null +++ b/Networking-Test-Kit/brainflow_metric.py @@ -0,0 +1,80 @@ +# python3 Networking-Test-Kit/brainflow_metric.py --board-id 2 --serial-port /dev/cu.usbserial-DM00D7TW + +import argparse +import time +import brainflow +import numpy as np + +from brainflow.board_shim import BoardShim, BrainFlowInputParams, LogLevels, BoardIds, BrainFlowError +from brainflow.data_filter import DataFilter, FilterTypes, AggOperations, WindowFunctions, DetrendOperations +from brainflow.ml_model import MLModel, BrainFlowMetrics, BrainFlowClassifiers, BrainFlowModelParams +from brainflow.exit_codes import * + + +def main(): + BoardShim.enable_board_logger() + DataFilter.enable_data_logger() + MLModel.enable_ml_logger() + + parser = argparse.ArgumentParser() + # use docs to check which parameters are required for specific board, e.g. for Cyton - set serial port + parser.add_argument('--timeout', type=int, help='timeout for device discovery or connection', required=False, + default=0) + parser.add_argument('--ip-port', type=int, help='ip port', required=False, default=0) + parser.add_argument('--ip-protocol', type=int, help='ip protocol, check IpProtocolType enum', required=False, + default=0) + parser.add_argument('--ip-address', type=str, help='ip address', required=False, default='') + parser.add_argument('--serial-port', type=str, help='serial port', required=False, default='') + parser.add_argument('--mac-address', type=str, help='mac address', required=False, default='') + parser.add_argument('--other-info', type=str, help='other info', required=False, default='') + parser.add_argument('--streamer-params', type=str, help='streamer params', required=False, default='') + parser.add_argument('--serial-number', type=str, help='serial number', required=False, default='') + parser.add_argument('--board-id', type=int, help='board id, check docs to get a list of supported boards', + required=True) + parser.add_argument('--file', type=str, help='file', required=False, default='') + args = parser.parse_args() + + params = BrainFlowInputParams() + params.ip_port = args.ip_port + params.serial_port = args.serial_port + params.mac_address = args.mac_address + params.other_info = args.other_info + params.serial_number = args.serial_number + params.ip_address = args.ip_address + params.ip_protocol = args.ip_protocol + params.timeout = args.timeout + params.file = args.file + + board = BoardShim(args.board_id, params) + master_board_id = board.get_board_id() + sampling_rate = BoardShim.get_sampling_rate(master_board_id) + board.prepare_session() + board.start_stream(45000, args.streamer_params) + BoardShim.log_message(LogLevels.LEVEL_INFO.value, 'start sleeping in the main thread') + time.sleep(5) # recommended window size for eeg metric calculation is at least 4 seconds, bigger is better + data = board.get_board_data() + board.stop_stream() + board.release_session() + + eeg_channels = BoardShim.get_eeg_channels(int(master_board_id)) + bands = DataFilter.get_avg_band_powers(data, eeg_channels, sampling_rate, True) + feature_vector = np.concatenate((bands[0], bands[1])) + print(feature_vector) + + # calc concentration + concentration_params = BrainFlowModelParams(BrainFlowMetrics.CONCENTRATION.value, BrainFlowClassifiers.KNN.value) + concentration = MLModel(concentration_params) + concentration.prepare() + print('Concentration: %f' % concentration.predict(feature_vector)) + concentration.release() + + # calc relaxation + relaxation_params = BrainFlowModelParams(BrainFlowMetrics.RELAXATION.value, BrainFlowClassifiers.REGRESSION.value) + relaxation = MLModel(relaxation_params) + relaxation.prepare() + print('Relaxation: %f' % relaxation.predict(feature_vector)) + relaxation.release() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/OpenBCI_GUI/ADS1299SettingsController.pde b/OpenBCI_GUI/ADS1299SettingsController.pde index 97dace96a..687701c66 100644 --- a/OpenBCI_GUI/ADS1299SettingsController.pde +++ b/OpenBCI_GUI/ADS1299SettingsController.pde @@ -218,6 +218,7 @@ class ADS1299SettingsController { resizeDropdowns(chanBar_h); resizeCustomCommandUI(); + } //Returns true if board and UI are in sync @@ -382,10 +383,10 @@ class ADS1299SettingsController { } private void createCustomCommandUI() { - final Textfield _tf = hwsCp5.addTextfield("customCommand") + customCommandTF = hwsCp5.addTextfield("customCommand") .setPosition(0, 0) .setCaptionLabel("") - .setSize(10, 10) + .setSize(120, 20) .setFont(f2) .setFocus(false) .setColor(color(26, 26, 26)) @@ -398,22 +399,21 @@ class ADS1299SettingsController { .align(5, 10, 20, 40) .setAutoClear(false) //Don't clear textfield when pressing Enter key ; - _tf.setDescription("Type a custom command and Send to board."); + customCommandTF.setDescription("Type a custom command and Send to board."); //Clear textfield on double click - _tf.onDoublePress(new CallbackListener() { + customCommandTF.onDoublePress(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { output("[ExpertMode] Enter the custom command you would like to send to the board."); - _tf.clear(); + customCommandTF.clear(); } }); - _tf.addCallback(new CallbackListener() { + customCommandTF.addCallback(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { if ((theEvent.getAction() == ControlP5.ACTION_BROADCAST) || (theEvent.getAction() == ControlP5.ACTION_LEAVE)) { - _tf.setFocus(false); + customCommandTF.setFocus(false); } } }); - customCommandTF = _tf; sendCustomCmdButton = createButton(hwsCp5, "sendCustomCommand", "Send Custom Command", 0, 0, 10, 10); sendCustomCmdButton.setBorderColor(OBJECT_BORDER_GREY); @@ -434,7 +434,7 @@ class ADS1299SettingsController { resizeCustomCommandUI(); } - private void resizeCustomCommandUI() { + public void resizeCustomCommandUI() { customCmdUI_x = x; customCmdUI_w = w + 1; int tf_w = Math.round(button_w * 1.8); @@ -445,7 +445,8 @@ class ADS1299SettingsController { //int tf_w = Math.round((customCmdUI_w - padding_3*2) * .75); int tf_h = commandBarH - padding_3*2; customCommandTF.setPosition(tf_x, tf_y); - customCommandTF.setSize(tf_w, tf_h); + customCommandTF.setWidth(tf_w); + customCommandTF.setHeight(tf_h); int but_x = tf_x + customCommandTF.getWidth() + padding_3; sendCustomCmdButton.setPosition(but_x, tf_y); sendCustomCmdButton.setSize(but_w, tf_h - 1); diff --git a/OpenBCI_GUI/W_Accelerometer.pde b/OpenBCI_GUI/W_Accelerometer.pde index 4ceb9d74d..4488a6e98 100644 --- a/OpenBCI_GUI/W_Accelerometer.pde +++ b/OpenBCI_GUI/W_Accelerometer.pde @@ -140,10 +140,13 @@ class W_Accelerometer extends Widget { if (accelBoard.isAccelerometerActive()) { drawAccValues(); draw3DGraph(); - accelerometerBar.draw(); } popStyle(); + + if (accelBoard.isAccelerometerActive()) { + accelerometerBar.draw(); + } } void setGraphDimensions() { diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 86c13da60..ab83cee1a 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -1,78 +1,182 @@ //////////////////////////////////////////////////// -// -// 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 -// -///////////////////////////////////////////////////, +// // +// W_focus.pde (ie "Focus Widget") // +// // +// // +// Created by: Richard Waltman, March 2021 // +// // +//////////////////////////////////////////////////// + +// color enums +public enum FocusColors { + GREEN, CYAN, ORANGE +} + +public enum FocusXLim implements TimeSeriesAxisEnum +{ + TEN (0, 10, "10 sec"), + TWENTY (1, 20, "20 sec"), + THIRTY (2, 30, "30 sec"), + SIXTY (3, 60, "60 sec"), + ONE_HUNDRED_TWENTY (4, 120, "120 sec"); + + private int index; + private int value; + private String label; + + FocusXLim(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + @Override + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} class W_Focus 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; + private ControlP5 focus_cp5; + private Button widgetTemplateButton; + + private FocusBar focusBar; + private float focusBarHardYAxisLimit = 1f; + FocusXLim xLimit = FocusXLim.TEN; + + private FocusColors focusColors = FocusColors.GREEN; + + private double metricPrediction = 0d; + private float xc, yc, wc, hc; // crystal ball center xy, width and height + private int graph_x, graph_y, graph_w, graph_h; + private int graph_pad = 30; + private color cBack, cDark, cMark, cFocus, cWave, cPanel; - W_Focus(PApplet _parent){ + W_Focus(PApplet _parent) { super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + + // initialize graphics parameters + onColorChange(); //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); - + addDropdown("FocusWindow", "Window", Arrays.asList("10 sec", "20 sec", "30 sec", "1 min" ), 0); + addDropdown("FocusMetric", "Metric", Arrays.asList("Concentration", "Relaxation"), 0); + addDropdown("FocusClassifier", "Classifier", Arrays.asList("Regression", "KNN", "SVM", "LDA"), 0); //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); + focus_cp5 = new ControlP5(ourApplet); + focus_cp5.setGraphics(ourApplet, 0,0); + focus_cp5.setAutoDraw(false); - createWidgetTemplateButton(); + //createWidgetTemplateButton(); + + //create our focus graph + update_graph_dims(); + focusBar = new FocusBar(_parent, focusBarHardYAxisLimit, graph_x, graph_y, graph_w, graph_h); + focusBar.adjustTimeAxis(w_timeSeries.getTSHorizScale().getValue()); //sync horiz axis to Time Series by default } - public void update(){ + public void update() { super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + metricPrediction = updateFocusState(); + + focusBar.update(); + //put your code here... } - public void draw(){ + public void draw() { super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) - //remember to refer to x,y,w,h which are the positioning variables of the Widget class + pushStyle(); + noStroke(); + if (metricPrediction > .5d) { + fill(cFocus); + stroke(cFocus); + ellipseMode(CENTER); + ellipse(xc, yc, wc, hc); + noStroke(); + textAlign(CENTER); + text("focused!", xc, yc + hc/2 + 16); + } else { + fill(cDark); + ellipseMode(CENTER); + ellipse(xc, yc, wc, hc); + noStroke(); + fill(cMark); + textAlign(CENTER); + text("not focused", xc, yc + hc/2 + 16); + } + popStyle(); + + + //Draw some guides to help develop this widget faster + pushStyle(); + stroke(0); + //Main guides + line(x, y+(h/2), x+w, y+(h/2)); + line(x+(w/2), y, x+(w/2), y+(h/2)); + //Top left container center + line(x+(w/4), y, x+(w/4), y+(h/2)); + line(x, y+(h/4), x+(w/2), y+(h/4)); + popStyle(); + //This draws all cp5 objects in the local instance - localCP5.draw(); + //focus_cp5.draw(); + + focusBar.draw(); } - public void screenResized(){ + public void screenResized() { super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) //Very important to allow users to interact with objects after app resize - localCP5.setGraphics(ourApplet, 0, 0); + focus_cp5.setGraphics(ourApplet, 0, 0); //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); + //widgetTemplateButton.setPosition(x + w/2 - widgetTemplateButton.getWidth()/2, y + h/2 - widgetTemplateButton.getHeight()/2); + + update_crystalball_dims(); + update_graph_dims(); + focusBar.screenResized(graph_x, graph_y, graph_w, graph_h); } - 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 + private void update_crystalball_dims() { + //Update "crystal ball" dimensions + float upperLeftContainerW = w/2; + float upperLeftContainerH = h/2; + float min = min(upperLeftContainerW, upperLeftContainerH); + xc = x + w/4; + yc = y + h/4; + wc = min * (3f/5); + hc = wc; } - 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. + private void update_graph_dims() { + graph_w = int(w - graph_pad*2); + graph_h = int(h/2 - navH*2); + graph_x = x + navH; + graph_y = int(y + h/2); } //When creating new UI objects, follow this rough pattern. @@ -80,7 +184,7 @@ class W_Focus extends Widget { //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); + widgetTemplateButton = createButton(focus_cp5, "widgetTemplateButton", "Design Your Own Widget!", x + w/2, y + h/2, 200, navHeight, 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 @@ -95,11 +199,237 @@ class W_Focus extends Widget { widgetTemplateButton.setDescription("Here is the description for this UI object. It will fade in as help text when hovering over the object."); } + private double updateFocusState() { + return 1d; + } + + private void onColorChange() { + switch(focusColors) { + case GREEN: + cBack = #ffffff; //white + cDark = #3068a6; //medium/dark blue + cMark = #4d91d9; //lighter blue + cFocus = #b8dc69; //theme green + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + case ORANGE: + cBack = #ffffff; //white + cDark = #377bc4; //medium/dark blue + cMark = #5e9ee2; //lighter blue + cFocus = #fcce51; //orange + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + case CYAN: + cBack = #ffffff; //white + cDark = #377bc4; //medium/dark blue + cMark = #5e9ee2; //lighter blue + cFocus = #91f4fc; //cyan + cWave = #ffdd3a; //yellow + cPanel = #f5f5f5; //little grey + break; + } + } + + public void setFocusHorizScale(int n) { + xLimit = xLimit.values()[n]; + focusBar.adjustTimeAxis(xLimit.getValue()); + } + //add custom functions here - private void customFunction(){ + private void customFunction() { //this is a fake function... replace it with something relevant to this widget } }; +class FocusBar { + //this class contains the plot for the 2d graph of accelerometer data + int x, y, w, h; + int focusBarPadding = 30; + int xOffset; + + GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Series trace + GPointsArray accelPointsX; + GPointsArray accelPointsY; + GPointsArray accelPointsZ; + + int nPoints; + int numSeconds = 20; //default to 20 seconds + float timeBetweenPoints; + float[] accelTimeArray; + int numSamplesToProcess; + float minX, minY, minZ; + float maxX, maxY, maxZ; + float minVal; + float maxVal; + final float autoScaleSpacing = 0.1; + + color channelColor; //color of plot trace + + boolean isAutoscale; //when isAutoscale equals true, the y-axis will automatically update to scale to the largest visible amplitude + int lastProcessedDataPacketInd = 0; + + private AccelerometerCapableBoard accelBoard; + + FocusBar(PApplet _parent, float accelXyzLimit, 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; + + x = _x; + y = _y; + w = _w; + h = _h; + if (eegDataSource == DATASOURCE_CYTON) { + xOffset = 22; + } else { + xOffset = 0; + } + + plot = new GPlot(_parent); + plot.setPos(x + 36 + 4 + xOffset, y); //match Accelerometer plot position with Time Series + plot.setDim(w - 36 - 4 - xOffset, h); + plot.setMar(0f, 0f, 0f, 0f); + plot.setLineColor((int)channelColors[(NUM_ACCEL_DIMS)%8]); + plot.setXLim(-numSeconds,0); //set the horizontal scale + plot.setYLim(0, accelXyzLimit); //change this to adjust vertical scale + //plot.setPointSize(2); + plot.setPointColor(0); + plot.getXAxis().setAxisLabelText("Time (s)"); + plot.getYAxis().setAxisLabelText("Metric Value"); + plot.setAllFontProperties("Arial", 0, 14); + plot.getXAxis().getAxisLabel().setOffset(float(22)); + plot.getYAxis().getAxisLabel().setOffset(float(focusBarPadding)); + + initArrays(); + + //set the plot points for X, Y, and Z axes + plot.addLayer("layer 1", accelPointsX); + plot.getLayer("layer 1").setLineColor(ACCEL_X_COLOR); + plot.addLayer("layer 2", accelPointsY); + plot.getLayer("layer 2").setLineColor(ACCEL_Y_COLOR); + plot.addLayer("layer 3", accelPointsZ); + plot.getLayer("layer 3").setLineColor(ACCEL_Z_COLOR); + } + + void initArrays() { + nPoints = nPointsBasedOnDataSource(); + timeBetweenPoints = (float)numSeconds / (float)nPoints; + + accelTimeArray = new float[nPoints]; + for (int i = 0; i < accelTimeArray.length; i++) { + accelTimeArray[i] = -(float)numSeconds + (float)i * timeBetweenPoints; + } + + float[] accelArrayX = new float[nPoints]; + float[] accelArrayY = new float[nPoints]; + float[] accelArrayZ = new float[nPoints]; + + //make a GPoint array using float arrays x[] and y[] instead of plain index points + accelPointsX = new GPointsArray(accelTimeArray, accelArrayX); + accelPointsY = new GPointsArray(accelTimeArray, accelArrayY); + accelPointsZ = new GPointsArray(accelTimeArray, accelArrayZ); + } + + //Used to update the accelerometerBar class + void update() { + updateGPlotPoints(); + + if (isAutoscale) { + autoScale(); + } + } + + void draw() { + plot.beginDraw(); + plot.drawBox(); //we won't draw this eventually ... + plot.drawGridLines(2); + plot.drawLines(); //Draw a Line graph! + //plot.drawPoints(); //Used to draw Points instead of Lines + plot.drawYAxis(); + plot.drawXAxis(); + plot.getXAxis().draw(); + plot.endDraw(); + } + + int nPointsBasedOnDataSource() { + return numSeconds * currentBoard.getSampleRate(); + } + + void adjustTimeAxis(int _newTimeSize) { + numSeconds = _newTimeSize; + plot.setXLim(-_newTimeSize,0); + + initArrays(); + + //Set the number of axis divisions... + if (_newTimeSize > 1) { + plot.getXAxis().setNTicks(_newTimeSize); + }else{ + plot.getXAxis().setNTicks(10); + } + } + + //Used to update the Points within the graph + void updateGPlotPoints() { + List allData = currentBoard.getData(nPoints); + int[] accelChannels = accelBoard.getAccelerometerChannels(); + + for (int i=0; i < nPoints; i++) { + accelPointsX.set(i, accelTimeArray[i], (float)allData.get(i)[accelChannels[0]], ""); + accelPointsY.set(i, accelTimeArray[i], (float)allData.get(i)[accelChannels[1]], ""); + accelPointsZ.set(i, accelTimeArray[i], (float)allData.get(i)[accelChannels[2]], ""); + } + + plot.setPoints(accelPointsX, "layer 1"); + plot.setPoints(accelPointsY, "layer 2"); + plot.setPoints(accelPointsZ, "layer 3"); + } + + float[] getLastAccelVals() { + float[] result = new float[NUM_ACCEL_DIMS]; + result[0] = accelPointsX.getY(nPoints-1); + result[1] = accelPointsY.getY(nPoints-1); + result[2] = accelPointsZ.getY(nPoints-1); + + return result; + } + + void adjustVertScale(int _vertScaleValue) { + if (_vertScaleValue == 0) { + isAutoscale = true; + } else { + isAutoscale = false; + plot.setYLim(-_vertScaleValue, _vertScaleValue); + } + } + + void autoScale() { + float[] minMaxVals = minMax(accelPointsX, accelPointsY, accelPointsZ); + plot.setYLim(minMaxVals[0] - autoScaleSpacing, minMaxVals[1] + autoScaleSpacing); + } + + float[] minMax(GPointsArray arrX, GPointsArray arrY, GPointsArray arrZ) { + float[] minMaxVals = {0.f, 0.f}; + for (int i = 0; i < arrX.getNPoints(); i++) { //go through the XYZ GPpointArrays for on-screen values + float[] vals = {arrX.getY(i), arrY.getY(i), arrZ.getY(i)}; + minMaxVals[0] = min(minMaxVals[0], min(vals)); //make room to see + minMaxVals[1] = max(minMaxVals[1], max(vals)); + } + return minMaxVals; + } + + void screenResized(int _x, int _y, int _w, int _h) { + x = _x; + y = _y; + w = _w; + h = _h; + //reposition & resize the plot + plot.setPos(x + 36 + 4 + xOffset, y); + plot.setDim(w - 36 - 4 - xOffset, h); + + } +}; //end of class \ No newline at end of file diff --git a/OpenBCI_GUI/W_Focus_Old.pde b/OpenBCI_GUI/W_Focus_Old.pde index b16541d99..dbc48e32d 100644 --- a/OpenBCI_GUI/W_Focus_Old.pde +++ b/OpenBCI_GUI/W_Focus_Old.pde @@ -8,11 +8,12 @@ // ///////////////////////////////////////////////////, - +/* // color enums public enum FocusColors { GREEN, CYAN, ORANGE } +*/ class W_Focus_Old extends Widget { //to see all core variables/methods of the Widget class, refer to Widget.pde diff --git a/OpenBCI_GUI/WidgetManager.pde b/OpenBCI_GUI/WidgetManager.pde index ed7f98dc5..b2d9ecbf8 100644 --- a/OpenBCI_GUI/WidgetManager.pde +++ b/OpenBCI_GUI/WidgetManager.pde @@ -36,6 +36,12 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_timeSeries, w); // println(" setupWidgets time series -- " + millis()); + //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 + w_focus = new W_Focus(_this); + w_focus.setTitle("Focus Widget"); + addWidget(w_focus, w); + // println(" setupWidgets focus widget -- " + millis()); + //Widget_1 w_fft = new W_fft(_this); w_fft.setTitle("FFT Plot"); @@ -128,11 +134,7 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_template1, w); - //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 - w_focus = new W_Focus(_this); - w_focus.setTitle("Focus Widget"); - addWidget(w_focus, w); - // println(" setupWidgets focus widget -- " + millis()); + } From 47a7375e7e51a6cb554dc957cb78a2ac6f17e4e1 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Sun, 21 Mar 2021 18:15:28 -0500 Subject: [PATCH 03/34] update Grid.pde --- OpenBCI_GUI/Grid.pde | 52 ++++++++++++++++++++++++++++++++++++++--- OpenBCI_GUI/W_Focus.pde | 23 +++++++++--------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/OpenBCI_GUI/Grid.pde b/OpenBCI_GUI/Grid.pde index 48b95ccbb..b406d171f 100644 --- a/OpenBCI_GUI/Grid.pde +++ b/OpenBCI_GUI/Grid.pde @@ -6,9 +6,16 @@ class Grid { private int[] colOffset; private int[] rowOffset; private int rowHeight; + private boolean horizontallyCenterTextInCells = false; private int x, y, w; - private final int pad = 5; + private int pad_horiz = 5; + private int pad_vert = 5; + + private PFont tableFont = p5; + private int tableFontSize = 12; + + private color[][] textColors; private String[][] strings; @@ -21,13 +28,18 @@ class Grid { rowOffset = new int[numRows]; strings = new String[numRows][numCols]; + textColors = new color[numRows][numCols]; + + color defaultTextColor = OPENBCI_DARKBLUE; + for (color[] row: textColors) { + Arrays.fill(row, defaultTextColor); + } } public void draw() { pushStyle(); textAlign(LEFT); stroke(0); - fill(0, 0, 0, 255); textFont(p5, 12); // draw row lines @@ -44,7 +56,9 @@ class Grid { for (int row = 0; row < numRows; row++) { for (int col = 0; col < numCols; col++) { if (strings[row][col] != null) { - text(strings[row][col], x + colOffset[col] + pad, y + rowOffset[row] - pad); + fill(textColors[row][col]); + textAlign(horizontallyCenterTextInCells ? CENTER : LEFT); + text(strings[row][col], x + colOffset[col] + pad_horiz, y + rowOffset[row] - pad_vert); } } } @@ -81,4 +95,36 @@ class Grid { public void setString(String s, int row, int col) { strings[row][col] = s; } + + public void setTableFontAndSize(PFont _font, int _fontSize) { + tableFont = _font; + tableFontSize = _fontSize; + } + + public void setRowHeight(int _height) { + rowHeight = _height; + } + + //This overrides the rowHeight and rowOffset when setting the total height of the Grid. + public void setTableHeight(int _height) { + rowHeight = _height / numRows; + for (int i = 0; i < numRows; i++) { + rowOffset[i] = rowHeight * (i + 1); + } + } + + public void setTextColor(color c, int row, int col) { + textColors[row][col] = c; + } + + //Change vertical padding for all cells based on the string/text height from a given cell + public void dynamicallySetTextVerticalPadding(int row, int col) { + float _textH = getFontStringHeight(tableFont, strings[row][col]); + pad_vert = int( (rowHeight - _textH) / 2); //Force round down here + } + + public void setHorizontalCenterTextInCells(boolean b) { + horizontallyCenterTextInCells = b; + pad_horiz = b ? getCellDims(0,0).w/2 : 5; + } } \ No newline at end of file diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index ab83cee1a..35995e105 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -128,17 +128,18 @@ class W_Focus extends Widget { } popStyle(); - - //Draw some guides to help develop this widget faster - pushStyle(); - stroke(0); - //Main guides - line(x, y+(h/2), x+w, y+(h/2)); - line(x+(w/2), y, x+(w/2), y+(h/2)); - //Top left container center - line(x+(w/4), y, x+(w/4), y+(h/2)); - line(x, y+(h/4), x+(w/2), y+(h/4)); - popStyle(); + if (false) { + //Draw some guides to help develop this widget faster + pushStyle(); + stroke(0); + //Main guides + line(x, y+(h/2), x+w, y+(h/2)); + line(x+(w/2), y, x+(w/2), y+(h/2)); + //Top left container center + line(x+(w/4), y, x+(w/4), y+(h/2)); + line(x, y+(h/4), x+(w/2), y+(h/4)); + popStyle(); + } //This draws all cp5 objects in the local instance //focus_cp5.draw(); From 8b63e5c3c678a5f4bc2e25b1f9fefb467c2943e5 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Sun, 21 Mar 2021 19:05:04 -0500 Subject: [PATCH 04/34] Add data grid to focus widget. Delete old focus widget. --- OpenBCI_GUI/Grid.pde | 11 + OpenBCI_GUI/W_Focus.pde | 72 +++-- OpenBCI_GUI/W_Focus_Old.pde | 585 ---------------------------------- OpenBCI_GUI/WidgetManager.pde | 12 +- 4 files changed, 68 insertions(+), 612 deletions(-) delete mode 100644 OpenBCI_GUI/W_Focus_Old.pde diff --git a/OpenBCI_GUI/Grid.pde b/OpenBCI_GUI/Grid.pde index b406d171f..53d40d1a7 100644 --- a/OpenBCI_GUI/Grid.pde +++ b/OpenBCI_GUI/Grid.pde @@ -7,6 +7,7 @@ class Grid { private int[] rowOffset; private int rowHeight; private boolean horizontallyCenterTextInCells = false; + private boolean drawTableBorder = false; private int x, y, w; private int pad_horiz = 5; @@ -62,6 +63,12 @@ class Grid { } } } + + if (drawTableBorder) { + noFill(); + stroke(0); + rect(x, y, w, rowOffset[numRows - 1]); + } popStyle(); } @@ -127,4 +134,8 @@ class Grid { horizontallyCenterTextInCells = b; pad_horiz = b ? getCellDims(0,0).w/2 : 5; } + + public void setDrawTableBorder(boolean b) { + drawTableBorder = b; + } } \ No newline at end of file diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 35995e105..c40d3d758 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -54,6 +54,15 @@ class W_Focus extends Widget { private ControlP5 focus_cp5; private Button widgetTemplateButton; + private Grid dataGrid; + private final int numTableRows = 6; + private final int numTableColumns = 2; + private final int tableWidth = 142; + private int tableHeight = 0; + private int cellHeight = 10; + + private final int padding_5 = 5; + private FocusBar focusBar; private float focusBarHardYAxisLimit = 1f; FocusXLim xLimit = FocusXLim.TEN; @@ -78,6 +87,13 @@ class W_Focus extends Widget { addDropdown("FocusWindow", "Window", Arrays.asList("10 sec", "20 sec", "30 sec", "1 min" ), 0); addDropdown("FocusMetric", "Metric", Arrays.asList("Concentration", "Relaxation"), 0); addDropdown("FocusClassifier", "Classifier", Arrays.asList("Regression", "KNN", "SVM", "LDA"), 0); + addDropdown("FocusThreshold", "Threshold", Arrays.asList("0.5", "0.6","0.7", "0.8", "0.9"), 0); + + //Create data table + dataGrid = new Grid(numTableRows, numTableColumns, cellHeight); + dataGrid.setTableFontAndSize(p6, 10); + dataGrid.setDrawTableBorder(true); + dataGrid.setString("Band Power", 0, 0); //Instantiate local cp5 for this box. This allows extra control of drawing cp5 elements specifically inside this class. focus_cp5 = new ControlP5(ourApplet); @@ -100,6 +116,11 @@ class W_Focus extends Widget { focusBar.update(); + + if (metricPrediction > .5d) { + + } + //put your code here... } @@ -107,25 +128,19 @@ class W_Focus extends Widget { super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) //remember to refer to x,y,w,h which are the positioning variables of the Widget class + //Draw data table + dataGrid.draw(); + + //Draw status graphic pushStyle(); noStroke(); - if (metricPrediction > .5d) { - fill(cFocus); - stroke(cFocus); - ellipseMode(CENTER); - ellipse(xc, yc, wc, hc); - noStroke(); - textAlign(CENTER); - text("focused!", xc, yc + hc/2 + 16); - } else { - fill(cDark); - ellipseMode(CENTER); - ellipse(xc, yc, wc, hc); - noStroke(); - fill(cMark); - textAlign(CENTER); - text("not focused", xc, yc + hc/2 + 16); - } + fill(cFocus); + stroke(cFocus); + ellipseMode(CENTER); + ellipse(xc, yc, wc, hc); + noStroke(); + textAlign(CENTER); + text("focused!", xc, yc + hc/2 + 16); popStyle(); if (false) { @@ -143,7 +158,8 @@ class W_Focus extends Widget { //This draws all cp5 objects in the local instance //focus_cp5.draw(); - + + //Draw the graph focusBar.draw(); } @@ -153,6 +169,8 @@ class W_Focus extends Widget { //Very important to allow users to interact with objects after app resize focus_cp5.setGraphics(ourApplet, 0, 0); + resizeTable(); + //We need to set the position of our Cp5 object after the screen is resized //widgetTemplateButton.setPosition(x + w/2 - widgetTemplateButton.getWidth()/2, y + h/2 - widgetTemplateButton.getHeight()/2); @@ -162,6 +180,20 @@ class W_Focus extends Widget { focusBar.screenResized(graph_x, graph_y, graph_w, graph_h); } + private void resizeTable() { + float upperLeftContainerW = w/2; + float upperLeftContainerH = h/2; + //float min = min(upperLeftContainerW, upperLeftContainerH); + int tx = x + int(upperLeftContainerW) + padding_5; + int ty = y + padding_5; + int tw = int(upperLeftContainerW) - padding_5*2; + //tableHeight = tw; + dataGrid.setDim(tx, ty, tw); + dataGrid.setTableHeight(int(upperLeftContainerH - padding_5*2)); + dataGrid.dynamicallySetTextVerticalPadding(0, 0); + dataGrid.setHorizontalCenterTextInCells(true); + } + private void update_crystalball_dims() { //Update "crystal ball" dimensions float upperLeftContainerW = w/2; @@ -175,8 +207,8 @@ class W_Focus extends Widget { private void update_graph_dims() { graph_w = int(w - graph_pad*2); - graph_h = int(h/2 - navH*2); - graph_x = x + navH; + graph_h = int(h/2 - padding_5); + graph_x = x + padding_5; graph_y = int(y + h/2); } diff --git a/OpenBCI_GUI/W_Focus_Old.pde b/OpenBCI_GUI/W_Focus_Old.pde deleted file mode 100644 index dbc48e32d..000000000 --- a/OpenBCI_GUI/W_Focus_Old.pde +++ /dev/null @@ -1,585 +0,0 @@ - -//////////////////////////////////////////////////// -// -// W_focus.pde (ie "Focus Widget") -// -// -// Created by: Richard Waltman, March 2021 -// -///////////////////////////////////////////////////, - -/* -// color enums -public enum FocusColors { - GREEN, CYAN, ORANGE -} -*/ - -class W_Focus_Old extends Widget { - //to see all core variables/methods of the Widget class, refer to Widget.pde - Robot robot; // a key-stroking robot waiting for focused state - boolean enableKey = false; // enable key stroke by the robot - int keyNum = 0; // 0 - up arrow, 1 - Spacebar - boolean enableSerial = false; // send the Focused state to Arduino - - // output values - float alpha_avg = 0, beta_avg = 0; - boolean isFocused; - - // alpha, beta threshold default values - float alpha_thresh = 0.7, beta_thresh = 0.7, alpha_upper = 2, beta_upper = 2; - - // drawing parameters - boolean showAbout = false; - PFont myfont = createFont("fonts/Raleway-SemiBold.otf", 12); - PFont f = f1; //for widget title - - FocusColors focusColors = FocusColors.GREEN; - - color cBack, cDark, cMark, cFocus, cWave, cPanel; - - // float x, y, w, h; //widget topleft xy, width and height - float xc, yc, wc, hc; // crystal ball center xy, width and height - float wg, hg; //graph width, graph height - float wl; // line width - float xg1, yg1; //graph1 center xy - float xg2, yg2; //graph1 center xy - float rp; // padding radius - float rb; // button radius - float xb, yb; // button center xy - - // two sliders for alpha and one slider for beta - FocusSlider sliderAlphaMid, sliderBetaMid; - FocusSlider_Static sliderAlphaTop; - Button infoButton; - int infoButtonSize = 18; - - W_Focus_Old(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - - // initialize graphics parameters - onColorChange(); - update_graphic_parameters(); - - // sliders - sliderAlphaMid = new FocusSlider(x + xg1 + wg * 0.8, y + yg1 + hg/2, y + yg1 - hg/2, alpha_thresh / alpha_upper); - sliderAlphaTop = new FocusSlider_Static(x + xg1 + wg * 0.8, y + yg1 + hg/2, y + yg1 - hg/2); - sliderBetaMid = new FocusSlider(x + xg2 + wg * 0.8, y + yg2 + hg/2, y + yg2 - hg/2, beta_thresh / beta_upper); - - ///Focus widget settings - //settings.focusThemeSave = 0; - //settings.focusKeySave = 0; - - //Dropdowns. - addDropdown("ChooseFocusColor", "Theme", Arrays.asList("Green", "Orange", "Cyan"), 0); - addDropdown("StrokeKeyWhenFocused", "KeyPress", Arrays.asList("OFF", "UP", "SPACE"), 0); - - //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); - */ - - } - - void onColorChange() { - switch(focusColors) { - case GREEN: - cBack = #ffffff; //white - cDark = #3068a6; //medium/dark blue - cMark = #4d91d9; //lighter blue - cFocus = #b8dc69; //theme green - cWave = #ffdd3a; //yellow - cPanel = #f5f5f5; //little grey - break; - case ORANGE: - cBack = #ffffff; //white - cDark = #377bc4; //medium/dark blue - cMark = #5e9ee2; //lighter blue - cFocus = #fcce51; //orange - cWave = #ffdd3a; //yellow - cPanel = #f5f5f5; //little grey - break; - case CYAN: - cBack = #ffffff; //white - cDark = #377bc4; //medium/dark blue - cMark = #5e9ee2; //lighter blue - cFocus = #91f4fc; //cyan - cWave = #ffdd3a; //yellow - cPanel = #f5f5f5; //little grey - break; - } - } - - void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - updateFocusState(); // focus calculation - invokeKeyStroke(); // robot keystroke - - // update sliders - sliderAlphaMid.update(); - sliderAlphaTop.update(); - sliderBetaMid.update(); - - // update threshold values - alpha_thresh = alpha_upper * sliderAlphaMid.getVal(); - beta_thresh = beta_upper * sliderBetaMid.getVal(); - - alpha_upper = sliderAlphaTop.getVal() * 2; - beta_upper = alpha_upper; - - sliderAlphaMid.setVal(alpha_thresh / alpha_upper); - sliderBetaMid.setVal(beta_thresh / beta_upper); - } - - void updateFocusState() { - // focus detection algorithm based on Jordan's clean mind: focus == high alpha average && low beta average - float FFT_freq_Hz, FFT_value_uV; - int alpha_count = 0, beta_count = 0; - - for (int Ichan=0; Ichan < 2; Ichan++) { // only consider first two channels - for (int Ibin=0; Ibin < fftBuff[Ichan].specSize(); Ibin++) { - FFT_freq_Hz = fftBuff[Ichan].indexToFreq(Ibin); - FFT_value_uV = fftBuff[Ichan].getBand(Ibin); - - if (FFT_freq_Hz >= 7.5 && FFT_freq_Hz <= 12.5) { //FFT bins in alpha range - alpha_avg += FFT_value_uV; - alpha_count ++; - } - else if (FFT_freq_Hz > 12.5 && FFT_freq_Hz <= 30) { //FFT bins in beta range - beta_avg += FFT_value_uV; - beta_count ++; - } - } - } - - alpha_avg = alpha_avg / alpha_count; // average uV per bin - beta_avg = beta_avg / beta_count; // average uV per bin - - // version 1 - if (alpha_avg > alpha_thresh && alpha_avg < alpha_upper && beta_avg < beta_thresh) { - isFocused = true; - } else { - isFocused = false; - } - } - - void invokeKeyStroke() { - /* - // robot keystroke - if (enableKey) { - if (keyNum == 0) { - if (isFocused) { - robot.keyPress(KeyEvent.VK_UP); //if you want to change to other key, google "java keyEvent" to see the full list - } - else { - robot.keyRelease(KeyEvent.VK_UP); - } - } - else if (keyNum == 1) { - if (isFocused) { - robot.keyPress(KeyEvent.VK_SPACE); //if you want to change to other key, google "java keyEvent" to see the full list - } - else { - robot.keyRelease(KeyEvent.VK_SPACE); - } - } - } - */ - } - - void draw(){ - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) - - //remember to refer to x,y,w,h which are the positioning variables of the Widget class - pushStyle(); - - //----------------- presettings before drawing Focus Viz -------------- - translate(x, y); - textAlign(CENTER, CENTER); - textFont(myfont); - - //----------------- draw background rectangle and panel ----------------- - fill(cBack); - noStroke(); - rect(0, 0, w, h); - - fill(cPanel); - noStroke(); - rect(rp, rp, w-rp*2, h-rp*2); - - //----------------- draw focus crystalball ----------------- - noStroke(); - if (isFocused) { - fill(cFocus); - stroke(cFocus); - } else { - fill(cDark); - } - ellipse(xc, yc, wc, hc); - noStroke(); - // draw focus label - if (isFocused) { - fill(cFocus); - text("focused!", xc, yc + hc/2 + 16); - } else { - fill(cMark); - text("not focused", xc, yc + hc/2 + 16); - } - - //----------------- draw alpha meter ----------------- - noStroke(); - fill(cDark); - rect(xg1 - wg/2, yg1 - hg/2, wg, hg); - - float hat = map(alpha_thresh, 0, alpha_upper, 0, hg); // alpha threshold height - stroke(cMark); - line(xg1 - wl/2, yg1 + hg/2, xg1 + wl/2, yg1 + hg/2); - line(xg1 - wl/2, yg1 - hg/2, xg1 + wl/2, yg1 - hg/2); - line(xg1 - wl/2, yg1 + hg/2 - hat, xg1 + wl/2, yg1 + hg/2 - hat); - - // draw alpha zone and text - noStroke(); - if (alpha_avg > alpha_thresh && alpha_avg < alpha_upper) { - fill(cFocus); - } else { - fill(cMark); - } - rect(xg1 - wg/2, yg1 - hg/2, wg, hg - hat); - text("alpha", xg1, yg1 + hg/2 + 16); - - // draw connection between two sliders - stroke(cMark); - line(xg1 + wg * 0.8, yg1 - hg/2 + 10, xg1 + wg * 0.8, yg1 + hg/2 - hat - 10); - - noStroke(); - fill(cMark); - text(String.format("%.01f", alpha_upper), xg1 - wl/2 - 14, yg1 - hg/2); - text(String.format("%.01f", alpha_thresh), xg1 - wl/2 - 14, yg1 + hg/2 - hat); - text("0.0", xg1 - wl/2 - 14, yg1 + hg/2); - - stroke(cWave); - strokeWeight(4); - float ha = map(alpha_avg, 0, alpha_upper, 0, hg); //alpha height - ha = constrain(ha, 0, hg); - line(xg1 - wl/2, yg1 + hg/2 - ha, xg1 + wl/2, yg1 + hg/2 - ha); - strokeWeight(1); - - //----------------- draw beta meter ----------------- - noStroke(); - fill(cDark); - rect(xg2 - wg/2, yg2 - hg/2, wg, hg); - - float hbt = map(beta_thresh, 0, beta_upper, 0, hg); // beta threshold height - stroke(cMark); - line(xg2 - wl/2, yg2 + hg/2, xg2 + wl/2, yg2 + hg/2); - line(xg2 - wl/2, yg2 - hg/2, xg2 + wl/2, yg2 - hg/2); - line(xg2 - wl/2, yg2 + hg/2 - hbt, xg2 + wl/2, yg2 + hg/2 - hbt); - - // draw beta zone and text - noStroke(); - if (beta_avg < beta_thresh) { - fill(cFocus); - } else { - fill(cMark); - } - rect(xg2 - wg/2, yg2 + hg/2 - hbt, wg, hbt); - text("beta", xg2, yg2 + hg/2 + 16); - - // draw connection between slider and bottom - stroke(cMark); - float yt = yg2 + hg/2 - hbt + 10; // y threshold - yt = constrain(yt, yg2 - hg/2 + 10, yg2 + hg/2); - line(xg2 + wg * 0.8, yg2 + hg/2, xg2 + wg * 0.8, yt); - - noStroke(); - fill(cMark); - text(String.format("%.01f", beta_upper), xg2 - wl/2 - 14, yg2 - hg/2); - text(String.format("%.01f", beta_thresh), xg2 - wl/2 - 14, yg2 + hg/2 - hbt); - text("0.0", xg2 - wl/2 - 14, yg2 + hg/2); - - stroke(cWave); - strokeWeight(4); - float hb = map(beta_avg, 0, beta_upper, 0, hg); //beta height - hb = constrain(hb, 0, hg); - line(xg2 - wl/2, yg2 + hg/2 - hb, xg2 + wl/2, yg2 + hg/2 - hb); - strokeWeight(1); - - translate(-x, -y); - - //------------------ draw sliders -------------------- - sliderAlphaMid.draw(); - sliderAlphaTop.draw(); - sliderBetaMid.draw(); - - //----------------- draw about button ----------------- - translate(x, y); - if (showAbout) { - stroke(cDark); - fill(cBack); - - rect(rp, rp, w-rp*2, h-rp*2); - textAlign(LEFT, TOP); - 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); - } - - /* - noStroke(); - fill(cDark); - ellipse(xb, yb, rb, rb); - fill(cBack); - textAlign(CENTER, CENTER); - if (showAbout) { - text("x", xb, yb); - } 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... - sliderAlphaMid.screenResized(x + xg1 + wg * 0.8, y + yg1 + hg/2, y + yg1 - hg/2); - sliderAlphaTop.screenResized(x + xg1 + wg * 0.8, y + yg1 + hg/2, y + yg1 - hg/2); - sliderBetaMid.screenResized(x + xg2 + wg * 0.8, y + yg2 + hg/2, y + yg2 - hg/2); - } - - void update_graphic_parameters () { - xc = w/4; - yc = h/2; - wc = w/4; - hc = w/4; - wg = 0.07*w; - hg = 0.64*h; - wl = 0.11*w; - xg1 = 0.6*w; - yg1 = 0.5*h; - xg2 = 0.83*w; - yg2 = 0.5*h; - rp = max(w*0.05, h*0.05); - rb = 20; - xb = w-rp; - yb = rp; - } - - void mousePressed(){ - super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) - - // about button - if (!this.dropdownIsActive) { - if (dist(mouseX,mouseY,xb+x,yb+y) <= rb) { - showAbout = !showAbout; - } - } - - /* - if (infoButton.isMouseHere()) { - infoButton.setIsActive(true); - } - */ - - // sliders - sliderAlphaMid.mousePressed(); - sliderAlphaTop.mousePressed(); - sliderBetaMid.mousePressed(); - } - - 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(); - sliderBetaMid.mouseReleased(); - } - -}; - -/* ---------------------- Supporting Slider Classes ---------------------------*/ - -// abstract basic slider -public abstract class BasicSlider { - float x, y, w, h; // center x, y. w, h means width and height of triangle - float yBot, yTop; // y range. Notice val of top y is less than bottom y - boolean isPressed = false; - color cNormal = #CCCCCC; - color cPressed = #FF0000; - - BasicSlider(float _x, float _yBot, float _yTop) { - x = _x; - yBot = _yBot; - yTop = _yTop; - w = 10; - h = 10; - } - - // abstract functions - - abstract void update(); - abstract void screenResized(float _x, float _yBot, float _yTop); - abstract float getVal(); - abstract void setVal(float _val); - - // shared functions - - void draw() { - if (isPressed) fill(cPressed); - else fill(cNormal); - noStroke(); - triangle(x-w/2, y, x+w/2, y-h/2, x+w/2, y+h/2); - } - - void mousePressed() { - if (abs(mouseX - (x)) <= w/2 && abs(mouseY - y) <= h/2) { - isPressed = true; - } - } - - void mouseReleased() { - if (isPressed) { - isPressed = false; - } - } -} - -// middle slider that changes value and move -public class FocusSlider extends BasicSlider { - private float val = 0; // val = 0 ~ 1 -> yBot to yTop - final float valMin = 0; - final float valMax = 0.90; - FocusSlider(float _x, float _yBot, float _yTop, float _val) { - super(_x, _yBot, _yTop); - val = constrain(_val, valMin, valMax); - y = map(val, 0, 1, yBot, yTop); - } - - public void update() { - if (isPressed) { - float newVal = map(mouseY, yBot, yTop, 0, 1); - val = constrain(newVal, valMin, valMax); - y = map(val, 0, 1, yBot, yTop); - println("Focus: " + val); - } - } - - public void screenResized(float _x, float _yBot, float _yTop) { - x = _x; - yBot = _yBot; - yTop = _yTop; - y = map(val, 0, 1, yBot, yTop); - } - - public float getVal() { - return val; - } - - public void setVal(float _val) { - val = constrain(_val, valMin, valMax); - y = map(val, 0, 1, yBot, yTop); - } -} - -// top slider that changes value but doesn't move -public class FocusSlider_Static extends BasicSlider { - private float val = 0; // val = 0 ~ 1 -> yBot to yTop - final float valMin = 0.5; - final float valMax = 5.0; - FocusSlider_Static(float _x, float _yBot, float _yTop) { - super(_x, _yBot, _yTop); - val = 1; - y = yTop; - } - - public void update() { - if (isPressed) { - float diff = map(mouseY, yBot, yTop, -0.07, 0); - val = constrain(val + diff, valMin, valMax); - println("Focus: " + val); - } - } - - public void screenResized(float _x, float _yBot, float _yTop) { - x = _x; - yBot = _yBot; - yTop = _yTop; - y = yTop; - } - - public float getVal() { - return val; - } - - public void setVal(float _val) { - val = constrain(_val, valMin, valMax); - } - -} - -/* ---------------- Global Functions For Menu Entries --------------------*/ - -// //These functions need to be global! These functions are activated when an item from the corresponding dropdown is selected -void StrokeKeyWhenFocused(int n){ - /* - // println("Item " + (n+1) + " selected from Dropdown 1"); - if(n==0){ - //do this - w_focus.enableKey = false; - //println("The robot ignores focused state and will not press any key."); - } else if(n==1){ - //do this instead - w_focus.enableKey = true; - w_focus.keyNum = 0; - //println("The robot will keep pressing Arrow Up key when you are focused, and release the key when you lose focus."); - } else if(n==2){ - //do this instead - w_focus.enableKey = true; - w_focus.keyNum = 1; - //println("The robot will keep pressing Spacebar when you are focused, and release the key when you lose focus."); - } - //settings.focusKeySave = n; - //closeAllDropdowns(); // do this at the end of all widget-activated functions to ensure proper widget interactivity ... we want to make sure a click makes the menu close - */ -} - -void ChooseFocusColor(int n){ - /* - if(n==0){ - w_focus.focusColors = FocusColors.GREEN; - w_focus.onColorChange(); - } else if(n==1){ - w_focus.focusColors = FocusColors.ORANGE; - w_focus.onColorChange(); - } else if(n==2){ - w_focus.focusColors = FocusColors.CYAN; - w_focus.onColorChange(); - } - //settings.focusThemeSave = n; - //closeAllDropdowns(); - */ -} \ No newline at end of file diff --git a/OpenBCI_GUI/WidgetManager.pde b/OpenBCI_GUI/WidgetManager.pde index b2d9ecbf8..ed7f98dc5 100644 --- a/OpenBCI_GUI/WidgetManager.pde +++ b/OpenBCI_GUI/WidgetManager.pde @@ -36,12 +36,6 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_timeSeries, w); // println(" setupWidgets time series -- " + millis()); - //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 - w_focus = new W_Focus(_this); - w_focus.setTitle("Focus Widget"); - addWidget(w_focus, w); - // println(" setupWidgets focus widget -- " + millis()); - //Widget_1 w_fft = new W_fft(_this); w_fft.setTitle("FFT Plot"); @@ -134,7 +128,11 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_template1, w); - + //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 + w_focus = new W_Focus(_this); + w_focus.setTitle("Focus Widget"); + addWidget(w_focus, w); + // println(" setupWidgets focus widget -- " + millis()); } From 452107c8f3cb4d472e1e06c47c222482442e1bcf Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Sun, 21 Mar 2021 20:11:18 -0500 Subject: [PATCH 05/34] Fix textbox text size issue after pushStyle/popStyle memory fix --- OpenBCI_GUI/Extras.pde | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenBCI_GUI/Extras.pde b/OpenBCI_GUI/Extras.pde index b2b37e6a4..50a407345 100644 --- a/OpenBCI_GUI/Extras.pde +++ b/OpenBCI_GUI/Extras.pde @@ -418,6 +418,7 @@ class TextBox { noStroke(); fill(textColor); textAlign(alignH,alignV); + textFont(font); text(string,x,y); strokeWeight(1); popStyle(); From b47b592a2ec804d0cdbef98feec42b4b7f5e4d8a Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Sun, 21 Mar 2021 21:34:19 -0500 Subject: [PATCH 06/34] focus wip --- OpenBCI_GUI/W_Focus.pde | 26 +++++++++++++++++++++----- OpenBCI_GUI/WidgetManager.pde | 12 +++++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index c40d3d758..5397dc6c1 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -13,7 +13,13 @@ public enum FocusColors { GREEN, CYAN, ORANGE } -public enum FocusXLim implements TimeSeriesAxisEnum +interface FocusEnum { + public int getIndex(); + public int getValue(); + public String getString(); +} + +public enum FocusXLim implements FocusEnum { TEN (0, 10, "10 sec"), TWENTY (1, 20, "20 sec"), @@ -25,6 +31,8 @@ public enum FocusXLim implements TimeSeriesAxisEnum private int value; private String label; + private static FocusXLim[] vals = values(); + FocusXLim(int _index, int _value, String _label) { this.index = _index; this.value = _value; @@ -45,6 +53,14 @@ public enum FocusXLim implements TimeSeriesAxisEnum public int getIndex() { return index; } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } } class W_Focus extends Widget { @@ -84,7 +100,7 @@ class W_Focus extends Widget { //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("FocusWindow", "Window", Arrays.asList("10 sec", "20 sec", "30 sec", "1 min" ), 0); + addDropdown("FocusWindow", "Window", FocusXLim.getEnumStringsAsList(), 0); addDropdown("FocusMetric", "Metric", Arrays.asList("Concentration", "Relaxation"), 0); addDropdown("FocusClassifier", "Classifier", Arrays.asList("Regression", "KNN", "SVM", "LDA"), 0); addDropdown("FocusThreshold", "Threshold", Arrays.asList("0.5", "0.6","0.7", "0.8", "0.9"), 0); @@ -206,9 +222,9 @@ class W_Focus extends Widget { } private void update_graph_dims() { - graph_w = int(w - graph_pad*2); - graph_h = int(h/2 - padding_5); - graph_x = x + padding_5; + graph_w = int(w - padding_5*4); + graph_h = int(h/2 - graph_pad - padding_5); + graph_x = x + padding_5*2; graph_y = int(y + h/2); } diff --git a/OpenBCI_GUI/WidgetManager.pde b/OpenBCI_GUI/WidgetManager.pde index ed7f98dc5..b2d9ecbf8 100644 --- a/OpenBCI_GUI/WidgetManager.pde +++ b/OpenBCI_GUI/WidgetManager.pde @@ -36,6 +36,12 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_timeSeries, w); // println(" setupWidgets time series -- " + millis()); + //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 + w_focus = new W_Focus(_this); + w_focus.setTitle("Focus Widget"); + addWidget(w_focus, w); + // println(" setupWidgets focus widget -- " + millis()); + //Widget_1 w_fft = new W_fft(_this); w_fft.setTitle("FFT Plot"); @@ -128,11 +134,7 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_template1, w); - //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 - w_focus = new W_Focus(_this); - w_focus.setTitle("Focus Widget"); - addWidget(w_focus, w); - // println(" setupWidgets focus widget -- " + millis()); + } From e9989d272701dddf7e5f459e66f68335534fcebe Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Sun, 21 Mar 2021 22:44:23 -0500 Subject: [PATCH 07/34] Implement BrainFlow EEG Metrics - first try - errors w/ bandpower --- Networking-Test-Kit/brainflow_metric.py | 80 --------------------- OpenBCI_GUI/W_Focus.pde | 58 ++++++++++++--- javaEEGMetrics.pde | 93 +++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 91 deletions(-) delete mode 100644 Networking-Test-Kit/brainflow_metric.py create mode 100644 javaEEGMetrics.pde diff --git a/Networking-Test-Kit/brainflow_metric.py b/Networking-Test-Kit/brainflow_metric.py deleted file mode 100644 index 955889630..000000000 --- a/Networking-Test-Kit/brainflow_metric.py +++ /dev/null @@ -1,80 +0,0 @@ -# python3 Networking-Test-Kit/brainflow_metric.py --board-id 2 --serial-port /dev/cu.usbserial-DM00D7TW - -import argparse -import time -import brainflow -import numpy as np - -from brainflow.board_shim import BoardShim, BrainFlowInputParams, LogLevels, BoardIds, BrainFlowError -from brainflow.data_filter import DataFilter, FilterTypes, AggOperations, WindowFunctions, DetrendOperations -from brainflow.ml_model import MLModel, BrainFlowMetrics, BrainFlowClassifiers, BrainFlowModelParams -from brainflow.exit_codes import * - - -def main(): - BoardShim.enable_board_logger() - DataFilter.enable_data_logger() - MLModel.enable_ml_logger() - - parser = argparse.ArgumentParser() - # use docs to check which parameters are required for specific board, e.g. for Cyton - set serial port - parser.add_argument('--timeout', type=int, help='timeout for device discovery or connection', required=False, - default=0) - parser.add_argument('--ip-port', type=int, help='ip port', required=False, default=0) - parser.add_argument('--ip-protocol', type=int, help='ip protocol, check IpProtocolType enum', required=False, - default=0) - parser.add_argument('--ip-address', type=str, help='ip address', required=False, default='') - parser.add_argument('--serial-port', type=str, help='serial port', required=False, default='') - parser.add_argument('--mac-address', type=str, help='mac address', required=False, default='') - parser.add_argument('--other-info', type=str, help='other info', required=False, default='') - parser.add_argument('--streamer-params', type=str, help='streamer params', required=False, default='') - parser.add_argument('--serial-number', type=str, help='serial number', required=False, default='') - parser.add_argument('--board-id', type=int, help='board id, check docs to get a list of supported boards', - required=True) - parser.add_argument('--file', type=str, help='file', required=False, default='') - args = parser.parse_args() - - params = BrainFlowInputParams() - params.ip_port = args.ip_port - params.serial_port = args.serial_port - params.mac_address = args.mac_address - params.other_info = args.other_info - params.serial_number = args.serial_number - params.ip_address = args.ip_address - params.ip_protocol = args.ip_protocol - params.timeout = args.timeout - params.file = args.file - - board = BoardShim(args.board_id, params) - master_board_id = board.get_board_id() - sampling_rate = BoardShim.get_sampling_rate(master_board_id) - board.prepare_session() - board.start_stream(45000, args.streamer_params) - BoardShim.log_message(LogLevels.LEVEL_INFO.value, 'start sleeping in the main thread') - time.sleep(5) # recommended window size for eeg metric calculation is at least 4 seconds, bigger is better - data = board.get_board_data() - board.stop_stream() - board.release_session() - - eeg_channels = BoardShim.get_eeg_channels(int(master_board_id)) - bands = DataFilter.get_avg_band_powers(data, eeg_channels, sampling_rate, True) - feature_vector = np.concatenate((bands[0], bands[1])) - print(feature_vector) - - # calc concentration - concentration_params = BrainFlowModelParams(BrainFlowMetrics.CONCENTRATION.value, BrainFlowClassifiers.KNN.value) - concentration = MLModel(concentration_params) - concentration.prepare() - print('Concentration: %f' % concentration.predict(feature_vector)) - concentration.release() - - # calc relaxation - relaxation_params = BrainFlowModelParams(BrainFlowMetrics.RELAXATION.value, BrainFlowClassifiers.REGRESSION.value) - relaxation = MLModel(relaxation_params) - relaxation.prepare() - print('Relaxation: %f' % relaxation.predict(feature_vector)) - relaxation.release() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 5397dc6c1..406a112e0 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -8,6 +8,19 @@ // // //////////////////////////////////////////////////// +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.Pair; + +import brainflow.BoardIds; +import brainflow.BoardShim; +import brainflow.BrainFlowClassifiers; +import brainflow.BrainFlowInputParams; +import brainflow.BrainFlowMetrics; +import brainflow.BrainFlowModelParams; +import brainflow.DataFilter; +import brainflow.LogLevels; +import brainflow.MLModel; + // color enums public enum FocusColors { GREEN, CYAN, ORANGE @@ -128,13 +141,10 @@ class W_Focus extends Widget { public void update() { super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - metricPrediction = updateFocusState(); - - focusBar.update(); - - - if (metricPrediction > .5d) { + if (currentBoard.isStreaming()) { + metricPrediction = updateFocusState(); + focusBar.update(); } //put your code here... @@ -200,7 +210,7 @@ class W_Focus extends Widget { float upperLeftContainerW = w/2; float upperLeftContainerH = h/2; //float min = min(upperLeftContainerW, upperLeftContainerH); - int tx = x + int(upperLeftContainerW) + padding_5; + int tx = x + int(upperLeftContainerW); int ty = y + padding_5; int tw = int(upperLeftContainerW) - padding_5*2; //tableHeight = tw; @@ -223,7 +233,7 @@ class W_Focus extends Widget { private void update_graph_dims() { graph_w = int(w - padding_5*4); - graph_h = int(h/2 - graph_pad - padding_5); + graph_h = int(h/2 - graph_pad - padding_5*2); graph_x = x + padding_5*2; graph_y = int(y + h/2); } @@ -248,8 +258,34 @@ class W_Focus extends Widget { widgetTemplateButton.setDescription("Here is the description for this UI object. It will fade in as help text when hovering over the object."); } + //Returns a metric value from 0. to 1. When there is an error, returns -1. private double updateFocusState() { - return 1d; + try { + int window_size = currentBoard.getSampleRate() * xLimit.getValue(); + List currentData = currentBoard.getData(window_size); + double[][] data = new double[currentData.size()][]; + if (currentData.size() == 0) { + println("OOPS!!!!"); + return -1d; + } + data = currentData.toArray(data); + println(data.length); + Pair bands = DataFilter.get_avg_band_powers (data, currentBoard.getEXGChannels(), currentBoard.getSampleRate(), false); + double[] feature_vector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); + BrainFlowModelParams model_params = new BrainFlowModelParams (BrainFlowMetrics.CONCENTRATION.get_code (), + BrainFlowClassifiers.REGRESSION.get_code ()); + MLModel concentration = new MLModel (model_params); + concentration.prepare (); + double prediction = concentration.predict (feature_vector); + println("Concentration: " + prediction); + concentration.release (); + return prediction; + + } catch (BrainFlowError e) { + e.printStackTrace(); + println("ERROR UPDATING FOCUS STATE!"); + return -1d; + } } private void onColorChange() { @@ -385,10 +421,10 @@ class FocusBar { //Used to update the accelerometerBar class void update() { - updateGPlotPoints(); + //updateGPlotPoints(); if (isAutoscale) { - autoScale(); + //autoScale(); } } diff --git a/javaEEGMetrics.pde b/javaEEGMetrics.pde new file mode 100644 index 000000000..7dcf35769 --- /dev/null +++ b/javaEEGMetrics.pde @@ -0,0 +1,93 @@ +package brainflow.examples; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.Pair; + +import brainflow.BoardIds; +import brainflow.BoardShim; +import brainflow.BrainFlowClassifiers; +import brainflow.BrainFlowInputParams; +import brainflow.BrainFlowMetrics; +import brainflow.BrainFlowModelParams; +import brainflow.DataFilter; +import brainflow.LogLevels; +import brainflow.MLModel; + +public class EEGMetrics +{ + + public static void main (String[] args) throws Exception + { + BoardShim.enable_board_logger (); + BrainFlowInputParams params = new BrainFlowInputParams (); + int board_id = parse_args (args, params); + BoardShim board_shim = new BoardShim (board_id, params); + int master_board_id = board_shim.get_board_id (); + int sampling_rate = BoardShim.get_sampling_rate (master_board_id); + int[] eeg_channels = BoardShim.get_eeg_channels (master_board_id); + + board_shim.prepare_session (); + board_shim.start_stream (3600); + BoardShim.log_message (LogLevels.LEVEL_INFO.get_code (), "Start sleeping in the main thread"); + // recommended window size for eeg metric calculation is at least 4 seconds, + // bigger is better + Thread.sleep (5000); + board_shim.stop_stream (); + double[][] data = board_shim.get_board_data (); + board_shim.release_session (); + + Pair bands = DataFilter.get_avg_band_powers (data, eeg_channels, sampling_rate, true); + double[] feature_vector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); + BrainFlowModelParams model_params = new BrainFlowModelParams (BrainFlowMetrics.CONCENTRATION.get_code (), + BrainFlowClassifiers.REGRESSION.get_code ()); + MLModel concentration = new MLModel (model_params); + concentration.prepare (); + System.out.print ("Concentration: " + concentration.predict (feature_vector)); + concentration.release (); + } + + private static int parse_args (String[] args, BrainFlowInputParams params) + { + int board_id = -1; + for (int i = 0; i < args.length; i++) + { + if (args[i].equals ("--ip-address")) + { + params.ip_address = args[i + 1]; + } + if (args[i].equals ("--serial-port")) + { + params.serial_port = args[i + 1]; + } + if (args[i].equals ("--ip-port")) + { + params.ip_port = Integer.parseInt (args[i + 1]); + } + if (args[i].equals ("--ip-protocol")) + { + params.ip_protocol = Integer.parseInt (args[i + 1]); + } + if (args[i].equals ("--other-info")) + { + params.other_info = args[i + 1]; + } + if (args[i].equals ("--board-id")) + { + board_id = Integer.parseInt (args[i + 1]); + } + if (args[i].equals ("--timeout")) + { + params.timeout = Integer.parseInt (args[i + 1]); + } + if (args[i].equals ("--serial-number")) + { + params.serial_number = args[i + 1]; + } + if (args[i].equals ("--file")) + { + params.file = args[i + 1]; + } + } + return board_id; + } +} \ No newline at end of file From 1064545a058b5eaf372d5c0b18384b23bdab42f8 Mon Sep 17 00:00:00 2001 From: retiutut Date: Tue, 6 Apr 2021 22:17:30 -0500 Subject: [PATCH 08/34] Add splitLargeCSV.py utility script --- Networking-Test-Kit/splitLargeCSV.py | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Networking-Test-Kit/splitLargeCSV.py diff --git a/Networking-Test-Kit/splitLargeCSV.py b/Networking-Test-Kit/splitLargeCSV.py new file mode 100644 index 000000000..e385ebd4a --- /dev/null +++ b/Networking-Test-Kit/splitLargeCSV.py @@ -0,0 +1,40 @@ + +import os +import csv + + +def split(filehandler, delimiter=',', row_limit=4000000, + output_name_template='output_%s.csv', output_path='.', keep_headers=True): + + reader = csv.reader(filehandler, delimiter=delimiter) + current_piece = 1 + current_out_path = os.path.join( + output_path, + output_name_template % current_piece + ) + current_out_writer = csv.writer(open(current_out_path, 'w', newline=''), delimiter=delimiter) + current_limit = row_limit + headers = [] + if keep_headers: + for i in range (5): + headerRow = next(reader); + headers.append(headerRow) + current_out_writer.writerow(headerRow) + + for i, row in enumerate(reader): + if i + 1 > current_limit: + current_piece += 1 + current_limit = row_limit * current_piece + current_out_path = os.path.join( + output_path, + output_name_template % current_piece + ) + current_out_writer = csv.writer(open(current_out_path, 'w', newline=''), delimiter=delimiter) + if keep_headers: + for headerRowI in headers: + current_out_writer.writerow(headerRowI) + print (headerRowI) + print (i) + current_out_writer.writerow(row) + +split(open('OpenBCI-RAW-2021-04-05_21-49-12.txt', 'r')); \ No newline at end of file From d39abed6be49c83b262dd0be3d8ee099254550a4 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 27 Apr 2021 19:22:56 +0300 Subject: [PATCH 09/34] fixing docus widget Signed-off-by: Andrey Parfenov --- OpenBCI_GUI/DataProcessing.pde | 1 - OpenBCI_GUI/W_Focus.pde | 35 ++++++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/OpenBCI_GUI/DataProcessing.pde b/OpenBCI_GUI/DataProcessing.pde index 6d39cbb52..49bcdcd1b 100644 --- a/OpenBCI_GUI/DataProcessing.pde +++ b/OpenBCI_GUI/DataProcessing.pde @@ -33,7 +33,6 @@ void processNewData() { //update the data buffers for (int Ichan=0; Ichan < channelCount; Ichan++) { - for(int i = 0; i < getCurrentBoardBufferSize(); i++) { dataProcessingRawBuffer[Ichan][i] = (float)currentData.get(i)[exgChannels[Ichan]]; } diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 406a112e0..1e548b0fc 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -260,24 +260,43 @@ class W_Focus extends Widget { //Returns a metric value from 0. to 1. When there is an error, returns -1. private double updateFocusState() { + // todo move concentration.prepare() and variable initialization outside from this method, it should be called only once! try { int window_size = currentBoard.getSampleRate() * xLimit.getValue(); + // getData in GUI returns data in shape nchannels x ndatapoints, in BrainFlow its transposed List currentData = currentBoard.getData(window_size); - double[][] data = new double[currentData.size()][]; - if (currentData.size() == 0) { - println("OOPS!!!!"); - return -1d; + if (currentData.size() != window_size) { + return -1.0; } - data = currentData.toArray(data); - println(data.length); - Pair bands = DataFilter.get_avg_band_powers (data, currentBoard.getEXGChannels(), currentBoard.getSampleRate(), false); + int[] exgChannels = currentBoard.getEXGChannels(); + int channelCount = currentBoard.getNumEXGChannels(); + int[] channelsInDataArray = new int[channelCount]; + //for (int i = 0; i < channelCount; i++) // use this line to use all channels + for (int i = 0; i < 3; i++) // temp to test - use first 3 channels + { + channelsInDataArray[i] = i; + } + double[][] data = new double[channelCount][]; + // todo preallocate this array outside from this method + for (int i = 0; i < channelCount; i++) { + data[i] = new double[window_size]; + for (int j = 0; j < currentData.size(); j++) { + data[i][j] = currentData.get(j)[exgChannels[i]]; + } + } + + Pair bands = DataFilter.get_avg_band_powers (data, channelsInDataArray, currentBoard.getSampleRate(), true); double[] feature_vector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); + + // todo move it away from here BrainFlowModelParams model_params = new BrainFlowModelParams (BrainFlowMetrics.CONCENTRATION.get_code (), - BrainFlowClassifiers.REGRESSION.get_code ()); + BrainFlowClassifiers.REGRESSION.get_code ()); MLModel concentration = new MLModel (model_params); concentration.prepare (); + double prediction = concentration.predict (feature_vector); println("Concentration: " + prediction); + // todo move it to smth like widget_close method concentration.release (); return prediction; From e1c778ee87e8d296cb046b01c073f127e475be98 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Tue, 27 Apr 2021 19:25:50 +0300 Subject: [PATCH 10/34] fix comment Signed-off-by: Andrey Parfenov --- OpenBCI_GUI/W_Focus.pde | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 1e548b0fc..aa6318ba2 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -263,7 +263,7 @@ class W_Focus extends Widget { // todo move concentration.prepare() and variable initialization outside from this method, it should be called only once! try { int window_size = currentBoard.getSampleRate() * xLimit.getValue(); - // getData in GUI returns data in shape nchannels x ndatapoints, in BrainFlow its transposed + // getData in GUI returns data in shape ndatapoints x nchannels, in BrainFlow its transposed List currentData = currentBoard.getData(window_size); if (currentData.size() != window_size) { return -1.0; From af899bccb8bf5e9fcf67095d29e325a67a0e63e5 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Tue, 27 Apr 2021 22:39:11 -0500 Subject: [PATCH 11/34] Focus Widget - Print Value to Table - Runs Slow! --- OpenBCI_GUI/OpenBCI_GUI.pde | 6 +- OpenBCI_GUI/W_Focus.pde | 196 ++++++++++++++++-------------------- 2 files changed, 89 insertions(+), 113 deletions(-) diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index ccd2a6782..4f80f2822 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -723,7 +723,7 @@ void stopRunning() { //halt the data collection void haltSystem() { - if (!systemHasHalted) { //prevents system from halting more than once\ + if (!systemHasHalted) { //prevents system from halting more than once println("openBCI_GUI: haltSystem: Halting system for reconfiguration of settings..."); //Reset the text for the Start Session buttonscreen. Skip when reiniting board while already in playback mode session. @@ -735,6 +735,10 @@ void haltSystem() { w_networking.stopNetwork(); println("openBCI_GUI: haltSystem: Network streams stopped"); } + + if (w_focus != null) { + w_focus.endSession(); + } stopRunning(); //stop data transfer diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index aa6318ba2..276537ff1 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -89,15 +89,18 @@ class W_Focus extends Widget { private final int tableWidth = 142; private int tableHeight = 0; private int cellHeight = 10; + private DecimalFormat df = new DecimalFormat("#.0000"); private final int padding_5 = 5; private FocusBar focusBar; private float focusBarHardYAxisLimit = 1f; - FocusXLim xLimit = FocusXLim.TEN; + FocusXLim xLimit = FocusXLim.TWENTY; private FocusColors focusColors = FocusColors.GREEN; + MLModel mlModel; + private double metricPrediction = 0d; private float xc, yc, wc, hc; // crystal ball center xy, width and height private int graph_x, graph_y, graph_w, graph_h; @@ -113,16 +116,16 @@ class W_Focus extends Widget { //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("FocusWindow", "Window", FocusXLim.getEnumStringsAsList(), 0); - addDropdown("FocusMetric", "Metric", Arrays.asList("Concentration", "Relaxation"), 0); - addDropdown("FocusClassifier", "Classifier", Arrays.asList("Regression", "KNN", "SVM", "LDA"), 0); - addDropdown("FocusThreshold", "Threshold", Arrays.asList("0.5", "0.6","0.7", "0.8", "0.9"), 0); + addDropdown("focusWindow", "Window", FocusXLim.getEnumStringsAsList(), 0); + addDropdown("focusMetric", "Metric", Arrays.asList("Concentration", "Relaxation"), 0); + addDropdown("focusClassifier", "Classifier", Arrays.asList("Regression", "KNN", "SVM", "LDA"), 0); + addDropdown("focusThreshold", "Threshold", Arrays.asList("0.5", "0.6","0.7", "0.8", "0.9"), 0); //Create data table dataGrid = new Grid(numTableRows, numTableColumns, cellHeight); dataGrid.setTableFontAndSize(p6, 10); dataGrid.setDrawTableBorder(true); - dataGrid.setString("Band Power", 0, 0); + dataGrid.setString("Metric Value", 0, 0); //Instantiate local cp5 for this box. This allows extra control of drawing cp5 elements specifically inside this class. focus_cp5 = new ControlP5(ourApplet); @@ -134,8 +137,8 @@ class W_Focus extends Widget { //create our focus graph update_graph_dims(); focusBar = new FocusBar(_parent, focusBarHardYAxisLimit, graph_x, graph_y, graph_w, graph_h); - focusBar.adjustTimeAxis(w_timeSeries.getTSHorizScale().getValue()); //sync horiz axis to Time Series by default - + + initBrainFlowMetric(BrainFlowMetrics.RELAXATION, BrainFlowClassifiers.LDA); } public void update() { @@ -144,7 +147,7 @@ class W_Focus extends Widget { if (currentBoard.isStreaming()) { metricPrediction = updateFocusState(); - focusBar.update(); + focusBar.update(metricPrediction); } //put your code here... @@ -238,26 +241,6 @@ class W_Focus extends Widget { graph_y = int(y + h/2); } - //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(focus_cp5, "widgetTemplateButton", "Design Your Own Widget!", x + w/2, y + h/2, 200, navHeight, 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 - widgetTemplateButton.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - //If using a TopNav object, ignore interaction with widget object (ex. widgetTemplateButton) - if (!topNav.configSelector.isVisible && !topNav.layoutSelector.isVisible) { - openURLInBrowser("https://openbci.github.io/Documentation/docs/06Software/01-OpenBCISoftware/GUIWidgets#custom-widget"); - } - } - }); - widgetTemplateButton.setDescription("Here is the description for this UI object. It will fade in as help text when hovering over the object."); - } - //Returns a metric value from 0. to 1. When there is an error, returns -1. private double updateFocusState() { // todo move concentration.prepare() and variable initialization outside from this method, it should be called only once! @@ -288,25 +271,44 @@ class W_Focus extends Widget { Pair bands = DataFilter.get_avg_band_powers (data, channelsInDataArray, currentBoard.getSampleRate(), true); double[] feature_vector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); - // todo move it away from here - BrainFlowModelParams model_params = new BrainFlowModelParams (BrainFlowMetrics.CONCENTRATION.get_code (), - BrainFlowClassifiers.REGRESSION.get_code ()); - MLModel concentration = new MLModel (model_params); - concentration.prepare (); + //Keep this here + double prediction = mlModel.predict(feature_vector); + //println("Concentration: " + prediction); + dataGrid.setString(df.format(prediction), 0, 1); - double prediction = concentration.predict (feature_vector); - println("Concentration: " + prediction); - // todo move it to smth like widget_close method - concentration.release (); return prediction; } catch (BrainFlowError e) { e.printStackTrace(); - println("ERROR UPDATING FOCUS STATE!"); + println("Error updating focus state!"); return -1d; } } + private void initBrainFlowMetric(BrainFlowMetrics metric, BrainFlowClassifiers classifier) { + BrainFlowModelParams model_params = new BrainFlowModelParams(metric.get_code(), classifier.get_code()); + mlModel = new MLModel (model_params); + try { + mlModel.prepare(); + } catch (BrainFlowError e) { + e.printStackTrace(); + } + } + + //Called on haltSystem() when GUI exits or session stops + public void endSession() { + try { + mlModel.release(); + } catch (BrainFlowError e) { + e.printStackTrace(); + } + } + + public void setFocusHorizScale(int n) { + xLimit = xLimit.values()[n]; + focusBar.adjustTimeAxis(xLimit.getValue()); + } + private void onColorChange() { switch(focusColors) { case GREEN: @@ -335,35 +337,23 @@ class W_Focus extends Widget { break; } } - - public void setFocusHorizScale(int n) { - xLimit = xLimit.values()[n]; - focusBar.adjustTimeAxis(xLimit.getValue()); - } - - //add custom functions here - private void customFunction() { - //this is a fake function... replace it with something relevant to this widget - - } - }; +//This class contains the time series plot for the focus metric over time class FocusBar { - //this class contains the plot for the 2d graph of accelerometer data int x, y, w, h; int focusBarPadding = 30; int xOffset; GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Series trace - GPointsArray accelPointsX; - GPointsArray accelPointsY; - GPointsArray accelPointsZ; + LinkedList fifo_list; + GPointsArray focusPoints; int nPoints; - int numSeconds = 20; //default to 20 seconds + int numSeconds = FocusXLim.TWENTY.getValue(); //default to 20 seconds float timeBetweenPoints; - float[] accelTimeArray; + float graph_timer; + float[] focusTimeArray; int numSamplesToProcess; float minX, minY, minZ; float maxX, maxY, maxZ; @@ -375,14 +365,8 @@ class FocusBar { boolean isAutoscale; //when isAutoscale equals true, the y-axis will automatically update to scale to the largest visible amplitude int lastProcessedDataPacketInd = 0; - - private AccelerometerCapableBoard accelBoard; FocusBar(PApplet _parent, float accelXyzLimit, 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; - x = _x; y = _y; w = _w; @@ -408,40 +392,31 @@ class FocusBar { plot.getXAxis().getAxisLabel().setOffset(float(22)); plot.getYAxis().getAxisLabel().setOffset(float(focusBarPadding)); + adjustTimeAxis(numSeconds); + initArrays(); //set the plot points for X, Y, and Z axes - plot.addLayer("layer 1", accelPointsX); + plot.addLayer("layer 1", focusPoints); plot.getLayer("layer 1").setLineColor(ACCEL_X_COLOR); - plot.addLayer("layer 2", accelPointsY); - plot.getLayer("layer 2").setLineColor(ACCEL_Y_COLOR); - plot.addLayer("layer 3", accelPointsZ); - plot.getLayer("layer 3").setLineColor(ACCEL_Z_COLOR); } void initArrays() { nPoints = nPointsBasedOnDataSource(); timeBetweenPoints = (float)numSeconds / (float)nPoints; - - accelTimeArray = new float[nPoints]; - for (int i = 0; i < accelTimeArray.length; i++) { - accelTimeArray[i] = -(float)numSeconds + (float)i * timeBetweenPoints; + focusTimeArray = new float[nPoints]; + fifo_list = new LinkedList(); + for (int i = 0; i < focusTimeArray.length; i++) { + focusTimeArray[i] = -(float)numSeconds + (float)i * timeBetweenPoints; + fifo_list.add(0f); } - - float[] accelArrayX = new float[nPoints]; - float[] accelArrayY = new float[nPoints]; - float[] accelArrayZ = new float[nPoints]; - - //make a GPoint array using float arrays x[] and y[] instead of plain index points - accelPointsX = new GPointsArray(accelTimeArray, accelArrayX); - accelPointsY = new GPointsArray(accelTimeArray, accelArrayY); - accelPointsZ = new GPointsArray(accelTimeArray, accelArrayZ); + float[] floatArray = ArrayUtils.toPrimitive(fifo_list.toArray(new Float[0]), 0.0F); + focusPoints = new GPointsArray(focusTimeArray, floatArray); } //Used to update the accelerometerBar class - void update() { - //updateGPlotPoints(); - + void update(double val) { + updateGPlotPoints(val); if (isAutoscale) { //autoScale(); } @@ -460,7 +435,7 @@ class FocusBar { } int nPointsBasedOnDataSource() { - return numSeconds * currentBoard.getSampleRate(); + return numSeconds * 30; } void adjustTimeAxis(int _newTimeSize) { @@ -478,30 +453,22 @@ class FocusBar { } //Used to update the Points within the graph - void updateGPlotPoints() { - List allData = currentBoard.getData(nPoints); - int[] accelChannels = accelBoard.getAccelerometerChannels(); - - for (int i=0; i < nPoints; i++) { - accelPointsX.set(i, accelTimeArray[i], (float)allData.get(i)[accelChannels[0]], ""); - accelPointsY.set(i, accelTimeArray[i], (float)allData.get(i)[accelChannels[1]], ""); - accelPointsZ.set(i, accelTimeArray[i], (float)allData.get(i)[accelChannels[2]], ""); - } - - plot.setPoints(accelPointsX, "layer 1"); - plot.setPoints(accelPointsY, "layer 2"); - plot.setPoints(accelPointsZ, "layer 3"); - } + void updateGPlotPoints(double val) { + + //if (graph_timer + timeBetweenPoints < millis()) { + graph_timer = millis(); + fifo_list.removeFirst(); + fifo_list.add((float)val); - float[] getLastAccelVals() { - float[] result = new float[NUM_ACCEL_DIMS]; - result[0] = accelPointsX.getY(nPoints-1); - result[1] = accelPointsY.getY(nPoints-1); - result[2] = accelPointsZ.getY(nPoints-1); + for (int i=0; i < nPoints; i++) { + focusPoints.set(i, focusTimeArray[i], fifo_list.get(i), ""); + } - return result; + plot.setPoints(focusPoints, "layer 1"); + //} } + /* void adjustVertScale(int _vertScaleValue) { if (_vertScaleValue == 0) { isAutoscale = true; @@ -510,18 +477,19 @@ class FocusBar { plot.setYLim(-_vertScaleValue, _vertScaleValue); } } + */ void autoScale() { - float[] minMaxVals = minMax(accelPointsX, accelPointsY, accelPointsZ); + float[] minMaxVals = minMax(focusPoints); plot.setYLim(minMaxVals[0] - autoScaleSpacing, minMaxVals[1] + autoScaleSpacing); } - float[] minMax(GPointsArray arrX, GPointsArray arrY, GPointsArray arrZ) { + float[] minMax(GPointsArray arr) { float[] minMaxVals = {0.f, 0.f}; - for (int i = 0; i < arrX.getNPoints(); i++) { //go through the XYZ GPpointArrays for on-screen values - float[] vals = {arrX.getY(i), arrY.getY(i), arrZ.getY(i)}; - minMaxVals[0] = min(minMaxVals[0], min(vals)); //make room to see - minMaxVals[1] = max(minMaxVals[1], max(vals)); + for (int i = 0; i < arr.getNPoints(); i++) { //go through the XYZ GPpointArrays for on-screen values + float val = arr.getY(i); + minMaxVals[0] = min(minMaxVals[0], val); //make room to see + minMaxVals[1] = max(minMaxVals[1], val); } return minMaxVals; } @@ -536,4 +504,8 @@ class FocusBar { plot.setDim(w - 36 - 4 - xOffset, h); } -}; //end of class \ No newline at end of file +}; //end of class + +public void focusWindow(int n) { + w_focus.setFocusHorizScale(n); +} \ No newline at end of file From 944f23e7c04b6654d2cede18a8da57bf4b2d0f7f Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Wed, 28 Apr 2021 22:04:15 -0500 Subject: [PATCH 12/34] Add FocusEnums.pde and make W_Focus dropdowns functional --- OpenBCI_GUI/FocusEnums.pde | 185 +++++++++++++++++++++++++++++++++++++ OpenBCI_GUI/W_Focus.pde | 171 +++++++++++++++++----------------- 2 files changed, 269 insertions(+), 87 deletions(-) create mode 100644 OpenBCI_GUI/FocusEnums.pde diff --git a/OpenBCI_GUI/FocusEnums.pde b/OpenBCI_GUI/FocusEnums.pde new file mode 100644 index 000000000..c4ade5907 --- /dev/null +++ b/OpenBCI_GUI/FocusEnums.pde @@ -0,0 +1,185 @@ +/// Here are the enums used by the Focus Widget, found in W_Focus.pde + +// color enums +public enum FocusColors { + GREEN, CYAN, ORANGE +} + +interface FocusEnum { + public int getIndex(); + public String getString(); +} + +public enum FocusXLim implements FocusEnum +{ + FIVE (0, 5, "5 sec"), + TEN (1, 10, "10 sec"), + TWENTY (2, 20, "20 sec"), + THIRTY (3, 30, "30 sec"); + + private int index; + private int value; + private String label; + + private static FocusXLim[] vals = values(); + + FocusXLim(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } +} + +public enum FocusMetric implements FocusEnum +{ + CONCENTRATION (0, "Concentration", BrainFlowMetrics.CONCENTRATION, "Concentrating"), + RELAXATION (1, "Relaxation", BrainFlowMetrics.RELAXATION, "Relaxed"); + + private int index; + private String label; + private BrainFlowMetrics metric; + private String idealState; + + private static FocusMetric[] vals = values(); + + FocusMetric(int _index, String _label, BrainFlowMetrics _metric, String _idealState) { + this.index = _index; + this.label = _label; + this.metric = _metric; + this.idealState = _idealState; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public BrainFlowMetrics getMetric() { + return metric; + } + + public String getIdealStateString() { + return idealState; + } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } +} + +public enum FocusClassifier implements FocusEnum +{ + REGRESSION (0, "Regression", BrainFlowClassifiers.REGRESSION), + KNN (1, "KNN", BrainFlowClassifiers.KNN), + SVM (2, "SVM", BrainFlowClassifiers.SVM), + LDA (3, "LDA", BrainFlowClassifiers.LDA); + + private int index; + private int value; + private String label; + private BrainFlowClassifiers classifier; + + private static FocusClassifier[] vals = values(); + + FocusClassifier(int _index, String _label, BrainFlowClassifiers _classifier) { + this.index = _index; + this.label = _label; + this.classifier = _classifier; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public BrainFlowClassifiers getClassifier() { + return classifier; + } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } +} + +public enum FocusThreshold implements FocusEnum +{ + FIVE_TENTHS (0, .5, "0.5"), + SIX_TENTHS (1, .6, "0.6"), + SEVEN_TENTHS (2, .7, "0.7"), + EIGHT_TENTHS (3, .8, "0.8"), + NINE_TENTHS (4, .9, "0.9"); + + private int index; + private float value; + private String label; + + private static FocusThreshold[] vals = values(); + + FocusThreshold(int _index, float _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public float getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public static List getEnumStringsAsList() { + List enumStrings = new ArrayList(); + for (FocusEnum val : vals) { + enumStrings.add(val.getString()); + } + return enumStrings; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 276537ff1..58fd045ae 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -2,6 +2,7 @@ //////////////////////////////////////////////////// // // // W_focus.pde (ie "Focus Widget") // +// Enums can be found in FocusEnums.pde // // // // // // Created by: Richard Waltman, March 2021 // @@ -21,61 +22,6 @@ import brainflow.DataFilter; import brainflow.LogLevels; import brainflow.MLModel; -// color enums -public enum FocusColors { - GREEN, CYAN, ORANGE -} - -interface FocusEnum { - public int getIndex(); - public int getValue(); - public String getString(); -} - -public enum FocusXLim implements FocusEnum -{ - TEN (0, 10, "10 sec"), - TWENTY (1, 20, "20 sec"), - THIRTY (2, 30, "30 sec"), - SIXTY (3, 60, "60 sec"), - ONE_HUNDRED_TWENTY (4, 120, "120 sec"); - - private int index; - private int value; - private String label; - - private static FocusXLim[] vals = values(); - - FocusXLim(int _index, int _value, String _label) { - this.index = _index; - this.value = _value; - this.label = _label; - } - - @Override - public int getValue() { - return value; - } - - @Override - public String getString() { - return label; - } - - @Override - public int getIndex() { - return index; - } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (FocusEnum val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } -} - class W_Focus extends Widget { //to see all core variables/methods of the Widget class, refer to Widget.pde @@ -96,12 +42,15 @@ class W_Focus extends Widget { private FocusBar focusBar; private float focusBarHardYAxisLimit = 1f; FocusXLim xLimit = FocusXLim.TWENTY; - + FocusMetric focusMetric = FocusMetric.RELAXATION; + FocusClassifier focusClassifier = FocusClassifier.REGRESSION; + FocusThreshold focusThreshold = FocusThreshold.EIGHT_TENTHS; private FocusColors focusColors = FocusColors.GREEN; MLModel mlModel; - private double metricPrediction = 0d; + private boolean predictionExceedsThreshold = false; + private float xc, yc, wc, hc; // crystal ball center xy, width and height private int graph_x, graph_y, graph_w, graph_h; private int graph_pad = 30; @@ -116,10 +65,10 @@ class W_Focus extends Widget { //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("focusWindow", "Window", FocusXLim.getEnumStringsAsList(), 0); - addDropdown("focusMetric", "Metric", Arrays.asList("Concentration", "Relaxation"), 0); - addDropdown("focusClassifier", "Classifier", Arrays.asList("Regression", "KNN", "SVM", "LDA"), 0); - addDropdown("focusThreshold", "Threshold", Arrays.asList("0.5", "0.6","0.7", "0.8", "0.9"), 0); + addDropdown("focusWindowDropdown", "Window", FocusXLim.getEnumStringsAsList(), xLimit.getIndex()); + addDropdown("focusMetricDropdown", "Metric", FocusMetric.getEnumStringsAsList(), focusMetric.getIndex()); + addDropdown("focusClassifierDropdown", "Classifier", FocusClassifier.getEnumStringsAsList(), focusClassifier.getIndex()); + addDropdown("focusThresholdDropdown", "Threshold", FocusThreshold.getEnumStringsAsList(), focusThreshold.getIndex()); //Create data table dataGrid = new Grid(numTableRows, numTableColumns, cellHeight); @@ -138,7 +87,7 @@ class W_Focus extends Widget { update_graph_dims(); focusBar = new FocusBar(_parent, focusBarHardYAxisLimit, graph_x, graph_y, graph_w, graph_h); - initBrainFlowMetric(BrainFlowMetrics.RELAXATION, BrainFlowClassifiers.LDA); + initBrainFlowMetric(); } public void update() { @@ -146,7 +95,7 @@ class W_Focus extends Widget { if (currentBoard.isStreaming()) { metricPrediction = updateFocusState(); - + predictionExceedsThreshold = metricPrediction > focusThreshold.getValue(); focusBar.update(metricPrediction); } @@ -160,17 +109,7 @@ class W_Focus extends Widget { //Draw data table dataGrid.draw(); - //Draw status graphic - pushStyle(); - noStroke(); - fill(cFocus); - stroke(cFocus); - ellipseMode(CENTER); - ellipse(xc, yc, wc, hc); - noStroke(); - textAlign(CENTER); - text("focused!", xc, yc + hc/2 + 16); - popStyle(); + drawStatusCircle(); if (false) { //Draw some guides to help develop this widget faster @@ -285,8 +224,37 @@ class W_Focus extends Widget { } } - private void initBrainFlowMetric(BrainFlowMetrics metric, BrainFlowClassifiers classifier) { - BrainFlowModelParams model_params = new BrainFlowModelParams(metric.get_code(), classifier.get_code()); + private void drawStatusCircle() { + color _fill; + color _stroke; + StringBuilder sb = new StringBuilder(""); + if (predictionExceedsThreshold) { + _fill = cFocus; + _stroke = cFocus; + } else { + _fill = cDark; + _stroke = cDark; + sb.append("Not "); + } + sb.append(focusMetric.getIdealStateString()); + //Draw status graphic + pushStyle(); + noStroke(); + fill(_fill); + stroke(_stroke); + ellipseMode(CENTER); + ellipse(xc, yc, wc, hc); + noStroke(); + textAlign(CENTER); + text(sb.toString(), xc, yc + hc/2 + 16); + popStyle(); + } + + private void initBrainFlowMetric() { + BrainFlowModelParams model_params = new BrainFlowModelParams( + focusMetric.getMetric().get_code(), + focusClassifier.getClassifier().get_code() + ); mlModel = new MLModel (model_params); try { mlModel.prepare(); @@ -304,11 +272,6 @@ class W_Focus extends Widget { } } - public void setFocusHorizScale(int n) { - xLimit = xLimit.values()[n]; - focusBar.adjustTimeAxis(xLimit.getValue()); - } - private void onColorChange() { switch(focusColors) { case GREEN: @@ -337,7 +300,45 @@ class W_Focus extends Widget { break; } } -}; + + public void setFocusHorizScale(int n) { + xLimit = xLimit.values()[n]; + focusBar.adjustTimeAxis(xLimit.getValue()); + } + + public void setMetric(int n) { + focusMetric = focusMetric.values()[n]; + endSession(); + initBrainFlowMetric(); + } + + public void setClassifier(int n) { + focusClassifier = focusClassifier.values()[n]; + endSession(); + initBrainFlowMetric(); + } + + public void setThreshold(int n) { + focusThreshold = focusThreshold.values()[n]; + } +}; //end of class + +//The following global functions are used by the Focus widget dropdowns. This method is the least amount of code. +public void focusWindowDropdown(int n) { + w_focus.setFocusHorizScale(n); +} + +public void focusMetricDropdown(int n) { + w_focus.setMetric(n); +} + +public void focusClassifierDropdown(int n) { + w_focus.setClassifier(n); +} + +public void focusThresholdDropdown(int n) { + w_focus.setThreshold(n); +} //This class contains the time series plot for the focus metric over time class FocusBar { @@ -504,8 +505,4 @@ class FocusBar { plot.setDim(w - 36 - 4 - xOffset, h); } -}; //end of class - -public void focusWindow(int n) { - w_focus.setFocusHorizScale(n); -} \ No newline at end of file +}; //end of class \ No newline at end of file From edcd01704aa5833f31ff129f42216b4fc93f2e28 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Thu, 29 Apr 2021 15:49:17 -0500 Subject: [PATCH 13/34] Focus Widget Channel Select - WIP - Broken - No Internet push --- CHANGELOG.md | 5 +++++ OpenBCI_GUI/OpenBCI_GUI.pde | 4 ++-- OpenBCI_GUI/W_Focus.pde | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8e19ed7..fad56d621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v5.0.5 + +### Improvements +* Implement Focus Widget using BrainFlow Metrics + # v5.0.4 ### Improvements diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index 4f80f2822..cf091e099 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -64,8 +64,8 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.4-alpha.3"; -String localGUIVersionDate = "March 2021"; +String localGUIVersionString = "v5.0.5-alpha.1"; +String localGUIVersionDate = "April 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 58fd045ae..532396aff 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -28,6 +28,7 @@ class W_Focus extends Widget { //put your custom variables here... private ControlP5 focus_cp5; private Button widgetTemplateButton; + private ChannelSelect focusChanSelect; private Grid dataGrid; private final int numTableRows = 6; @@ -59,6 +60,10 @@ class W_Focus extends Widget { W_Focus(PApplet _parent) { super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + //Add channel select dropdown to this widget + focusChanSelect = new ChannelSelect(pApplet, this, x, y, w, navH, "Focus_Channels"); + focusChanSelect.activateAllButtons(); + // initialize graphics parameters onColorChange(); From c5bc141d0148fc1740c71fec431e2c60c994cc99 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 30 Apr 2021 12:20:11 -0500 Subject: [PATCH 14/34] Fix AutoVertScale by default in accelerometer widget --- .../LSL/brainflow_lsl_cyton_accel.py | 67 +++++++++++++++++++ OpenBCI_GUI/W_Accelerometer.pde | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 Networking-Test-Kit/LSL/brainflow_lsl_cyton_accel.py diff --git a/Networking-Test-Kit/LSL/brainflow_lsl_cyton_accel.py b/Networking-Test-Kit/LSL/brainflow_lsl_cyton_accel.py new file mode 100644 index 000000000..e1f2ce737 --- /dev/null +++ b/Networking-Test-Kit/LSL/brainflow_lsl_cyton_accel.py @@ -0,0 +1,67 @@ +import argparse +import time +import numpy as np +import brainflow +from brainflow.board_shim import BoardShim, BrainFlowInputParams, LogLevels, BoardIds +from brainflow.data_filter import DataFilter, FilterTypes, AggOperations +from pylsl import StreamInfo, StreamOutlet + +#from queue import Queue +BoardShim.enable_dev_board_logger() +params = BrainFlowInputParams() +params.serial_port = '/dev/cu.usbserial-DM00D7TW' +board = BoardShim(BoardIds.CYTON_BOARD.value, params) # added cyton board id here +srate = board.get_sampling_rate(BoardIds.CYTON_BOARD.value) +board.prepare_session() +board.start_stream() +eeg_chan = BoardShim.get_eeg_channels(BoardIds.CYTON_BOARD.value) +aux_chan = BoardShim.get_accel_channels(BoardIds.CYTON_BOARD.value) + +print('EEG channels:') +print(eeg_chan) +print('Accelerometer channels') +print(aux_chan) + +# define lsl streams + +# Defining stream info: +name = 'OpenBCIEEG' +ID = 'OpenBCIEEG' +channels = 8 +sample_rate = 250 +datatype = 'float32' +streamType = 'EEG' + +print(f"Creating LSL stream for EEG. \nName: {name}\nID: {ID}\n") +info_eeg = StreamInfo(name, streamType, channels, sample_rate, datatype, ID) +chns = info_eeg.desc().append_child("channels") +for label in ["AFp1", "AFp2", "C3", "C4", "P7", "P8", "O1", "O2"]: + ch = chns.append_child("channel") + ch.append_child_value("label", label) +info_aux = StreamInfo('OpenBCIAUX', 'AUX', 3, 250, 'float32', 'OpenBCItestAUX') +chns = info_aux.desc().append_child("channels") +for label in ["X", "Y", "Z"]: + ch = chns.append_child("channel") + ch.append_child_value("label", label) +outlet_aux = StreamOutlet(info_aux) +outlet_eeg = StreamOutlet(info_eeg) + +# construct a numpy array that contains only eeg channels and aux channels with correct scaling +# this streams to lsl +while True: + data = board.get_board_data() # this gets data continiously + # don't send empty data + if len(data[0]) < 1 : continue + eeg_data = data[eeg_chan] + aux_data = data[aux_chan] + #print(scaled_eeg_data) + #print(scaled_aux_data) + #print('------------------------------------------------------------------------------------------') + eegchunk = [] + for i in range(len(eeg_data[0])): + eegchunk.append((eeg_data[:,i]).tolist()) #scale data here + outlet_eeg.push_chunk(eegchunk) + auxchunk = [] + for i in range(len(aux_data[0])): + auxchunk.append((aux_data[:,i]).tolist()) #scale data here + outlet_aux.push_chunk(auxchunk) \ No newline at end of file diff --git a/OpenBCI_GUI/W_Accelerometer.pde b/OpenBCI_GUI/W_Accelerometer.pde index 4488a6e98..999f1b8a4 100644 --- a/OpenBCI_GUI/W_Accelerometer.pde +++ b/OpenBCI_GUI/W_Accelerometer.pde @@ -69,7 +69,7 @@ class W_Accelerometer extends Widget { //create our channel bar and populate our accelerometerBar array! accelerometerBar = new AccelerometerBar(_parent, accelXyzLimit, accelGraphX, accelGraphY, accelGraphWidth, accelGraphHeight); - accelerometerBar.adjustTimeAxis(w_timeSeries.getTSHorizScale().getValue()); //sync horiz axis to Time Series by default + accelerometerBar.adjustVertScale(yLimOptions[0]); createAccelModeButton("accelModeButton", "Turn Accel. Off", (int)(x + 3), (int)(y + 3 - navHeight), 120, navHeight - 6, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); } From 784f8275b9613cd790da2d01aaf60d32d5d2d8bb Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 30 Apr 2021 20:04:18 -0500 Subject: [PATCH 15/34] Focus Widget Functional Channel Select -RW --- OpenBCI_GUI/W_Focus.pde | 72 +++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 532396aff..fe2540619 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -29,6 +29,7 @@ class W_Focus extends Widget { private ControlP5 focus_cp5; private Button widgetTemplateButton; private ChannelSelect focusChanSelect; + private boolean prevChanSelectIsVisible = false; private Grid dataGrid; private final int numTableRows = 6; @@ -98,6 +99,15 @@ class W_Focus extends Widget { public void update() { super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + //Update channel checkboxes and active channels + focusChanSelect.update(x, y, w); + + //Flex the Gplot graph when channel select dropdown is open/closed + if (focusChanSelect.isVisible() != prevChanSelectIsVisible) { + channelSelectFlexWidgetUI(); + prevChanSelectIsVisible = focusChanSelect.isVisible(); + } + if (currentBoard.isStreaming()) { metricPrediction = updateFocusState(); predictionExceedsThreshold = metricPrediction > focusThreshold.getValue(); @@ -134,6 +144,8 @@ class W_Focus extends Widget { //Draw the graph focusBar.draw(); + + focusChanSelect.draw(); } public void screenResized() { @@ -151,14 +163,21 @@ class W_Focus extends Widget { update_graph_dims(); focusBar.screenResized(graph_x, graph_y, graph_w, graph_h); + focusChanSelect.screenResized(pApplet); + } + + void mousePressed() { + super.mousePressed(); //calls the parent mousePressed() method of Widget (DON'T REMOVE) + focusChanSelect.mousePressed(this.dropdownIsActive); //Calls channel select mousePressed and checks if clicked } private void resizeTable() { + int extraPadding = focusChanSelect.isVisible() ? navHeight : 0; float upperLeftContainerW = w/2; float upperLeftContainerH = h/2; //float min = min(upperLeftContainerW, upperLeftContainerH); int tx = x + int(upperLeftContainerW); - int ty = y + padding_5; + int ty = y + padding_5 + extraPadding; int tw = int(upperLeftContainerW) - padding_5*2; //tableHeight = tw; dataGrid.setDim(tx, ty, tw); @@ -192,16 +211,25 @@ class W_Focus extends Widget { int window_size = currentBoard.getSampleRate() * xLimit.getValue(); // getData in GUI returns data in shape ndatapoints x nchannels, in BrainFlow its transposed List currentData = currentBoard.getData(window_size); - if (currentData.size() != window_size) { + if (currentData.size() != window_size || focusChanSelect.activeChan.size() <= 0) { return -1.0; } int[] exgChannels = currentBoard.getEXGChannels(); int channelCount = currentBoard.getNumEXGChannels(); int[] channelsInDataArray = new int[channelCount]; - //for (int i = 0; i < channelCount; i++) // use this line to use all channels - for (int i = 0; i < 3; i++) // temp to test - use first 3 channels + /* + for (int j = 0; j < bpChanSelect.activeChan.size(); j++) { + int chan = bpChanSelect.activeChan.get(j); + */ + + int activeChanCounter = 0; + for (int i = 0; (i < channelCount) && (activeChanCounter < focusChanSelect.activeChan.size()); i++) // use this line to use all channels { - channelsInDataArray[i] = i; + int chan = focusChanSelect.activeChan.get(activeChanCounter); + if (i == chan) { + channelsInDataArray[i] = i; + activeChanCounter++; + } } double[][] data = new double[channelCount][]; // todo preallocate this array outside from this method @@ -306,6 +334,13 @@ class W_Focus extends Widget { } } + void channelSelectFlexWidgetUI() { + focusBar.setPlotPosAndOuterDim(focusChanSelect.isVisible()); + int factor = focusChanSelect.isVisible() ? 1 : -1; + yc += navHeight * factor; + resizeTable(); + } + public void setFocusHorizScale(int n) { xLimit = xLimit.values()[n]; focusBar.adjustTimeAxis(xLimit.getValue()); @@ -407,7 +442,7 @@ class FocusBar { plot.getLayer("layer 1").setLineColor(ACCEL_X_COLOR); } - void initArrays() { + private void initArrays() { nPoints = nPointsBasedOnDataSource(); timeBetweenPoints = (float)numSeconds / (float)nPoints; focusTimeArray = new float[nPoints]; @@ -421,14 +456,14 @@ class FocusBar { } //Used to update the accelerometerBar class - void update(double val) { + public void update(double val) { updateGPlotPoints(val); if (isAutoscale) { //autoScale(); } } - void draw() { + public void draw() { plot.beginDraw(); plot.drawBox(); //we won't draw this eventually ... plot.drawGridLines(2); @@ -440,11 +475,11 @@ class FocusBar { plot.endDraw(); } - int nPointsBasedOnDataSource() { + private int nPointsBasedOnDataSource() { return numSeconds * 30; } - void adjustTimeAxis(int _newTimeSize) { + public void adjustTimeAxis(int _newTimeSize) { numSeconds = _newTimeSize; plot.setXLim(-_newTimeSize,0); @@ -459,7 +494,7 @@ class FocusBar { } //Used to update the Points within the graph - void updateGPlotPoints(double val) { + private void updateGPlotPoints(double val) { //if (graph_timer + timeBetweenPoints < millis()) { graph_timer = millis(); @@ -485,12 +520,12 @@ class FocusBar { } */ - void autoScale() { + private void autoScale() { float[] minMaxVals = minMax(focusPoints); plot.setYLim(minMaxVals[0] - autoScaleSpacing, minMaxVals[1] + autoScaleSpacing); } - float[] minMax(GPointsArray arr) { + private float[] minMax(GPointsArray arr) { float[] minMaxVals = {0.f, 0.f}; for (int i = 0; i < arr.getNPoints(); i++) { //go through the XYZ GPpointArrays for on-screen values float val = arr.getY(i); @@ -500,7 +535,7 @@ class FocusBar { return minMaxVals; } - void screenResized(int _x, int _y, int _w, int _h) { + public void screenResized(int _x, int _y, int _w, int _h) { x = _x; y = _y; w = _w; @@ -510,4 +545,13 @@ class FocusBar { plot.setDim(w - 36 - 4 - xOffset, h); } + + public void setPlotPosAndOuterDim(boolean chanSelectIsVisible) { + int _y = chanSelectIsVisible ? y + 22 : y; + int _h = chanSelectIsVisible ? h - 22 : h; + //reposition & resize the plot + plot.setPos(x + 36 + 4 + xOffset, _y); + plot.setDim(w - 36 - 4 - xOffset, _h); + } + }; //end of class \ No newline at end of file From 67f79ca46cde9467e136899fb78804138fb16862 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Tue, 4 May 2021 20:23:34 -0500 Subject: [PATCH 16/34] Code Review 1 - Focus Widget 2021 --- OpenBCI_GUI/FocusEnums.pde | 2 +- OpenBCI_GUI/W_Focus.pde | 197 ++++++++++++++++--------------------- OpenBCI_GUI/Widget.pde | 21 ++-- javaEEGMetrics.pde | 93 ----------------- 4 files changed, 93 insertions(+), 220 deletions(-) delete mode 100644 javaEEGMetrics.pde diff --git a/OpenBCI_GUI/FocusEnums.pde b/OpenBCI_GUI/FocusEnums.pde index c4ade5907..e6bfe7dd7 100644 --- a/OpenBCI_GUI/FocusEnums.pde +++ b/OpenBCI_GUI/FocusEnums.pde @@ -55,7 +55,7 @@ public enum FocusXLim implements FocusEnum public enum FocusMetric implements FocusEnum { CONCENTRATION (0, "Concentration", BrainFlowMetrics.CONCENTRATION, "Concentrating"), - RELAXATION (1, "Relaxation", BrainFlowMetrics.RELAXATION, "Relaxed"); + RELAXATION (1, "Relaxation", BrainFlowMetrics.RELAXATION, "Relaxing"); private int index; private String label; diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index fe2540619..3dfe09d49 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -1,4 +1,3 @@ - //////////////////////////////////////////////////// // // // W_focus.pde (ie "Focus Widget") // @@ -39,42 +38,53 @@ class W_Focus extends Widget { private int cellHeight = 10; private DecimalFormat df = new DecimalFormat("#.0000"); - private final int padding_5 = 5; + private final int PAD_FIVE = 5; + private final int PAD_TWO = 2; + private final int METRIC_DROPDOWN_W = 100; + private final int CLASSIFIER_DROPDOWN_W = 80; private FocusBar focusBar; private float focusBarHardYAxisLimit = 1f; - FocusXLim xLimit = FocusXLim.TWENTY; + FocusXLim xLimit = FocusXLim.TEN; FocusMetric focusMetric = FocusMetric.RELAXATION; FocusClassifier focusClassifier = FocusClassifier.REGRESSION; FocusThreshold focusThreshold = FocusThreshold.EIGHT_TENTHS; private FocusColors focusColors = FocusColors.GREEN; + int[] exgChannels; + int channelCount; + double[][] dataArray; + MLModel mlModel; private double metricPrediction = 0d; private boolean predictionExceedsThreshold = false; - private float xc, yc, wc, hc; // crystal ball center xy, width and height - private int graph_x, graph_y, graph_w, graph_h; - private int graph_pad = 30; + private float xc, yc, wc, hc; // status circle center xy, width and height + private int graphX, graphY, graphW, graphH; + private int graphPadding = 30; private color cBack, cDark, cMark, cFocus, cWave, cPanel; W_Focus(PApplet _parent) { super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) //Add channel select dropdown to this widget - focusChanSelect = new ChannelSelect(pApplet, this, x, y, w, navH, "Focus_Channels"); + focusChanSelect = new ChannelSelect(pApplet, this, x, y, w, navH, "FocusChannelSelect"); focusChanSelect.activateAllButtons(); + exgChannels = currentBoard.getEXGChannels(); + channelCount = currentBoard.getNumEXGChannels(); + dataArray = new double[channelCount][]; + // initialize graphics parameters onColorChange(); //This is the protocol for setting up dropdowns. - //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("focusWindowDropdown", "Window", FocusXLim.getEnumStringsAsList(), xLimit.getIndex()); + dropdownWidth = 60; //Override the default dropdown width for this widget addDropdown("focusMetricDropdown", "Metric", FocusMetric.getEnumStringsAsList(), focusMetric.getIndex()); addDropdown("focusClassifierDropdown", "Classifier", FocusClassifier.getEnumStringsAsList(), focusClassifier.getIndex()); addDropdown("focusThresholdDropdown", "Threshold", FocusThreshold.getEnumStringsAsList(), focusThreshold.getIndex()); + addDropdown("focusWindowDropdown", "Window", FocusXLim.getEnumStringsAsList(), xLimit.getIndex()); + //Create data table dataGrid = new Grid(numTableRows, numTableColumns, cellHeight); @@ -87,11 +97,9 @@ class W_Focus extends Widget { focus_cp5.setGraphics(ourApplet, 0,0); focus_cp5.setAutoDraw(false); - //createWidgetTemplateButton(); - //create our focus graph - update_graph_dims(); - focusBar = new FocusBar(_parent, focusBarHardYAxisLimit, graph_x, graph_y, graph_w, graph_h); + updateGraphDims(); + focusBar = new FocusBar(_parent, focusBarHardYAxisLimit, graphX, graphY, graphW, graphH); initBrainFlowMetric(); } @@ -110,8 +118,9 @@ class W_Focus extends Widget { if (currentBoard.isStreaming()) { metricPrediction = updateFocusState(); - predictionExceedsThreshold = metricPrediction > focusThreshold.getValue(); + dataGrid.setString(df.format(metricPrediction), 0, 1); focusBar.update(metricPrediction); + predictionExceedsThreshold = metricPrediction > focusThreshold.getValue(); } //put your code here... @@ -140,7 +149,7 @@ class W_Focus extends Widget { } //This draws all cp5 objects in the local instance - //focus_cp5.draw(); + focus_cp5.draw(); //Draw the graph focusBar.draw(); @@ -159,11 +168,23 @@ class W_Focus extends Widget { //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); - update_crystalball_dims(); + updateStatusCircle(); - update_graph_dims(); - focusBar.screenResized(graph_x, graph_y, graph_w, graph_h); + updateGraphDims(); + focusBar.screenResized(graphX, graphY, graphW, graphH); focusChanSelect.screenResized(pApplet); + + //Custom resize these dropdowns due to longer text strings as options + cp5_widget.get(ScrollableList.class, "focusMetricDropdown").setWidth(METRIC_DROPDOWN_W); + cp5_widget.get(ScrollableList.class, "focusMetricDropdown").setPosition( + x0 + w0 - (dropdownWidth*2) - METRIC_DROPDOWN_W - CLASSIFIER_DROPDOWN_W - (PAD_TWO*4), + navH + y0 + PAD_TWO + ); + cp5_widget.get(ScrollableList.class, "focusClassifierDropdown").setWidth(CLASSIFIER_DROPDOWN_W); + cp5_widget.get(ScrollableList.class, "focusClassifierDropdown").setPosition( + x0 + w0 - (dropdownWidth*2) - CLASSIFIER_DROPDOWN_W - (PAD_TWO*3), + navH + y0 + PAD_TWO + ); } void mousePressed() { @@ -177,17 +198,16 @@ class W_Focus extends Widget { float upperLeftContainerH = h/2; //float min = min(upperLeftContainerW, upperLeftContainerH); int tx = x + int(upperLeftContainerW); - int ty = y + padding_5 + extraPadding; - int tw = int(upperLeftContainerW) - padding_5*2; + int ty = y + PAD_FIVE + extraPadding; + int tw = int(upperLeftContainerW) - PAD_FIVE*2; //tableHeight = tw; dataGrid.setDim(tx, ty, tw); - dataGrid.setTableHeight(int(upperLeftContainerH - padding_5*2)); + dataGrid.setTableHeight(int(upperLeftContainerH - PAD_FIVE*2)); dataGrid.dynamicallySetTextVerticalPadding(0, 0); dataGrid.setHorizontalCenterTextInCells(true); } - private void update_crystalball_dims() { - //Update "crystal ball" dimensions + private void updateStatusCircle() { float upperLeftContainerW = w/2; float upperLeftContainerH = h/2; float min = min(upperLeftContainerW, upperLeftContainerH); @@ -197,56 +217,43 @@ class W_Focus extends Widget { hc = wc; } - private void update_graph_dims() { - graph_w = int(w - padding_5*4); - graph_h = int(h/2 - graph_pad - padding_5*2); - graph_x = x + padding_5*2; - graph_y = int(y + h/2); + private void updateGraphDims() { + graphW = int(w - PAD_FIVE*4); + graphH = int(h/2 - graphPadding - PAD_FIVE*2); + graphX = x + PAD_FIVE*2; + graphY = int(y + h/2); } + //Core method to fetch and process data //Returns a metric value from 0. to 1. When there is an error, returns -1. private double updateFocusState() { - // todo move concentration.prepare() and variable initialization outside from this method, it should be called only once! try { - int window_size = currentBoard.getSampleRate() * xLimit.getValue(); + int windowSize = currentBoard.getSampleRate() * xLimit.getValue(); // getData in GUI returns data in shape ndatapoints x nchannels, in BrainFlow its transposed - List currentData = currentBoard.getData(window_size); - if (currentData.size() != window_size || focusChanSelect.activeChan.size() <= 0) { + List currentData = currentBoard.getData(windowSize); + + if (currentData.size() != windowSize || focusChanSelect.activeChan.size() <= 0) { return -1.0; } - int[] exgChannels = currentBoard.getEXGChannels(); - int channelCount = currentBoard.getNumEXGChannels(); - int[] channelsInDataArray = new int[channelCount]; - /* - for (int j = 0; j < bpChanSelect.activeChan.size(); j++) { - int chan = bpChanSelect.activeChan.get(j); - */ - - int activeChanCounter = 0; - for (int i = 0; (i < channelCount) && (activeChanCounter < focusChanSelect.activeChan.size()); i++) // use this line to use all channels - { - int chan = focusChanSelect.activeChan.get(activeChanCounter); - if (i == chan) { - channelsInDataArray[i] = i; - activeChanCounter++; - } - } - double[][] data = new double[channelCount][]; - // todo preallocate this array outside from this method + for (int i = 0; i < channelCount; i++) { - data[i] = new double[window_size]; + dataArray[i] = new double[windowSize]; for (int j = 0; j < currentData.size(); j++) { - data[i][j] = currentData.get(j)[exgChannels[i]]; + dataArray[i][j] = currentData.get(j)[exgChannels[i]]; } } - Pair bands = DataFilter.get_avg_band_powers (data, channelsInDataArray, currentBoard.getSampleRate(), true); - double[] feature_vector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); + int[] channelsInDataArray = ArrayUtils.toPrimitive( + focusChanSelect.activeChan.toArray( + new Integer[focusChanSelect.activeChan.size()] + )); + + Pair bands = DataFilter.get_avg_band_powers (dataArray, channelsInDataArray, currentBoard.getSampleRate(), true); + double[] featureVector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); //Keep this here - double prediction = mlModel.predict(feature_vector); + double prediction = mlModel.predict(featureVector); //println("Concentration: " + prediction); - dataGrid.setString(df.format(prediction), 0, 1); return prediction; @@ -258,23 +265,23 @@ class W_Focus extends Widget { } private void drawStatusCircle() { - color _fill; - color _stroke; + color fillColor; + color strokeColor; StringBuilder sb = new StringBuilder(""); if (predictionExceedsThreshold) { - _fill = cFocus; - _stroke = cFocus; + fillColor = cFocus; + strokeColor = cFocus; } else { - _fill = cDark; - _stroke = cDark; + fillColor = cDark; + strokeColor = cDark; sb.append("Not "); } sb.append(focusMetric.getIdealStateString()); //Draw status graphic pushStyle(); noStroke(); - fill(_fill); - stroke(_stroke); + fill(fillColor); + stroke(strokeColor); ellipseMode(CENTER); ellipse(xc, yc, wc, hc); noStroke(); @@ -284,11 +291,11 @@ class W_Focus extends Widget { } private void initBrainFlowMetric() { - BrainFlowModelParams model_params = new BrainFlowModelParams( + BrainFlowModelParams modelParams = new BrainFlowModelParams( focusMetric.getMetric().get_code(), focusClassifier.getClassifier().get_code() ); - mlModel = new MLModel (model_params); + mlModel = new MLModel (modelParams); try { mlModel.prepare(); } catch (BrainFlowError e) { @@ -387,13 +394,13 @@ class FocusBar { int xOffset; GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Series trace - LinkedList fifo_list; + LinkedList fifoList; GPointsArray focusPoints; int nPoints; int numSeconds = FocusXLim.TWENTY.getValue(); //default to 20 seconds float timeBetweenPoints; - float graph_timer; + float graphTimer; float[] focusTimeArray; int numSamplesToProcess; float minX, minY, minZ; @@ -446,21 +453,17 @@ class FocusBar { nPoints = nPointsBasedOnDataSource(); timeBetweenPoints = (float)numSeconds / (float)nPoints; focusTimeArray = new float[nPoints]; - fifo_list = new LinkedList(); + fifoList = new LinkedList(); for (int i = 0; i < focusTimeArray.length; i++) { focusTimeArray[i] = -(float)numSeconds + (float)i * timeBetweenPoints; - fifo_list.add(0f); + fifoList.add(0f); } - float[] floatArray = ArrayUtils.toPrimitive(fifo_list.toArray(new Float[0]), 0.0F); + float[] floatArray = ArrayUtils.toPrimitive(fifoList.toArray(new Float[0]), 0.0F); focusPoints = new GPointsArray(focusTimeArray, floatArray); } - //Used to update the accelerometerBar class public void update(double val) { updateGPlotPoints(val); - if (isAutoscale) { - //autoScale(); - } } public void draw() { @@ -482,9 +485,7 @@ class FocusBar { public void adjustTimeAxis(int _newTimeSize) { numSeconds = _newTimeSize; plot.setXLim(-_newTimeSize,0); - initArrays(); - //Set the number of axis divisions... if (_newTimeSize > 1) { plot.getXAxis().setNTicks(_newTimeSize); @@ -495,46 +496,20 @@ class FocusBar { //Used to update the Points within the graph private void updateGPlotPoints(double val) { - - //if (graph_timer + timeBetweenPoints < millis()) { - graph_timer = millis(); - fifo_list.removeFirst(); - fifo_list.add((float)val); + //todo : important to align time with actual elapsed time! + //if (graphTimer + timeBetweenPoints < millis()) { + graphTimer = millis(); + fifoList.removeFirst(); + fifoList.addLast((float)val); for (int i=0; i < nPoints; i++) { - focusPoints.set(i, focusTimeArray[i], fifo_list.get(i), ""); + focusPoints.set(i, focusTimeArray[i], fifoList.get(i), ""); } plot.setPoints(focusPoints, "layer 1"); //} } - /* - void adjustVertScale(int _vertScaleValue) { - if (_vertScaleValue == 0) { - isAutoscale = true; - } else { - isAutoscale = false; - plot.setYLim(-_vertScaleValue, _vertScaleValue); - } - } - */ - - private void autoScale() { - float[] minMaxVals = minMax(focusPoints); - plot.setYLim(minMaxVals[0] - autoScaleSpacing, minMaxVals[1] + autoScaleSpacing); - } - - private float[] minMax(GPointsArray arr) { - float[] minMaxVals = {0.f, 0.f}; - for (int i = 0; i < arr.getNPoints(); i++) { //go through the XYZ GPpointArrays for on-screen values - float val = arr.getY(i); - minMaxVals[0] = min(minMaxVals[0], val); //make room to see - minMaxVals[1] = max(minMaxVals[1], val); - } - return minMaxVals; - } - public void screenResized(int _x, int _y, int _w, int _h) { x = _x; y = _y; diff --git a/OpenBCI_GUI/Widget.pde b/OpenBCI_GUI/Widget.pde index d4f6434d5..b342fcbe9 100644 --- a/OpenBCI_GUI/Widget.pde +++ b/OpenBCI_GUI/Widget.pde @@ -73,7 +73,6 @@ class Widget{ fill(200, 200, 200); rect(x0, y0+navH, w0, navH); //button bar popStyle(); - } public void addDropdown(String _id, String _title, List _items, int _defaultItem){ @@ -192,23 +191,21 @@ class Widget{ textAlign(CENTER, BOTTOM); fill(OPENBCI_DARKBLUE); for(int i = 0; i < dropdowns.size(); i++){ - int dropdownPos = dropdowns.size() - i; - // text(dropdowns.get(i).title, x+w-(dropdownWidth*(dropdownPos+1))-(2*(dropdownPos+1))+dropdownWidth/2, y+(navH-2)); - text(dropdowns.get(i).title, x0+w0-(dropdownWidth*(dropdownPos))-(2*(dropdownPos+1))+dropdownWidth/2, y0+(navH-2)); + int dropdownPos = dropdowns.size() - i; + int _width = cp5_widget.getController(dropdowns.get(i).id).getWidth(); + int _x = int(cp5_widget.getController(dropdowns.get(i).id).getPosition()[0]); + text(dropdowns.get(i).title, _x+_width/2, y0+(navH-2)); } popStyle(); } public void mouseDragged(){ - } public void mousePressed(){ - } public void mouseReleased(){ - } public void screenResized(){ @@ -292,7 +289,7 @@ class Widget{ previousDropdownIsActive = dropdownIsActive; } } -}; +}; //end of base Widget class /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -319,23 +316,18 @@ class NavBarDropdown{ } void update(){ - } void draw(){ - } void screenResized(){ - } void mousePressed(){ - } void mouseReleased(){ - } String returnDefaultAsString(){ @@ -365,7 +357,6 @@ void WidgetSelector(int n){ wm.widgets.get(n).setIsActive(true);//activate the new widget wm.widgets.get(n).setContainer(theContainer);//map it to the current container - //set the text of the widgetSelector to the newly selected widget } // This is a helpful class that will add a channel select feature to a Widget @@ -597,4 +588,4 @@ class ChannelSelect { } } -} //end of ChannelSelect class +} //end of ChannelSelect class \ No newline at end of file diff --git a/javaEEGMetrics.pde b/javaEEGMetrics.pde deleted file mode 100644 index 7dcf35769..000000000 --- a/javaEEGMetrics.pde +++ /dev/null @@ -1,93 +0,0 @@ -package brainflow.examples; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.tuple.Pair; - -import brainflow.BoardIds; -import brainflow.BoardShim; -import brainflow.BrainFlowClassifiers; -import brainflow.BrainFlowInputParams; -import brainflow.BrainFlowMetrics; -import brainflow.BrainFlowModelParams; -import brainflow.DataFilter; -import brainflow.LogLevels; -import brainflow.MLModel; - -public class EEGMetrics -{ - - public static void main (String[] args) throws Exception - { - BoardShim.enable_board_logger (); - BrainFlowInputParams params = new BrainFlowInputParams (); - int board_id = parse_args (args, params); - BoardShim board_shim = new BoardShim (board_id, params); - int master_board_id = board_shim.get_board_id (); - int sampling_rate = BoardShim.get_sampling_rate (master_board_id); - int[] eeg_channels = BoardShim.get_eeg_channels (master_board_id); - - board_shim.prepare_session (); - board_shim.start_stream (3600); - BoardShim.log_message (LogLevels.LEVEL_INFO.get_code (), "Start sleeping in the main thread"); - // recommended window size for eeg metric calculation is at least 4 seconds, - // bigger is better - Thread.sleep (5000); - board_shim.stop_stream (); - double[][] data = board_shim.get_board_data (); - board_shim.release_session (); - - Pair bands = DataFilter.get_avg_band_powers (data, eeg_channels, sampling_rate, true); - double[] feature_vector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); - BrainFlowModelParams model_params = new BrainFlowModelParams (BrainFlowMetrics.CONCENTRATION.get_code (), - BrainFlowClassifiers.REGRESSION.get_code ()); - MLModel concentration = new MLModel (model_params); - concentration.prepare (); - System.out.print ("Concentration: " + concentration.predict (feature_vector)); - concentration.release (); - } - - private static int parse_args (String[] args, BrainFlowInputParams params) - { - int board_id = -1; - for (int i = 0; i < args.length; i++) - { - if (args[i].equals ("--ip-address")) - { - params.ip_address = args[i + 1]; - } - if (args[i].equals ("--serial-port")) - { - params.serial_port = args[i + 1]; - } - if (args[i].equals ("--ip-port")) - { - params.ip_port = Integer.parseInt (args[i + 1]); - } - if (args[i].equals ("--ip-protocol")) - { - params.ip_protocol = Integer.parseInt (args[i + 1]); - } - if (args[i].equals ("--other-info")) - { - params.other_info = args[i + 1]; - } - if (args[i].equals ("--board-id")) - { - board_id = Integer.parseInt (args[i + 1]); - } - if (args[i].equals ("--timeout")) - { - params.timeout = Integer.parseInt (args[i + 1]); - } - if (args[i].equals ("--serial-number")) - { - params.serial_number = args[i + 1]; - } - if (args[i].equals ("--file")) - { - params.file = args[i + 1]; - } - } - return board_id; - } -} \ No newline at end of file From bd564e2f335c7498991e1dcdd565d2a27ce1bde6 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Tue, 4 May 2021 20:34:45 -0500 Subject: [PATCH 17/34] Bump version v5.0.5-alpha.2 --- OpenBCI_GUI/OpenBCI_GUI.pde | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index cf091e099..f2daae1ed 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -64,8 +64,8 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.5-alpha.1"; -String localGUIVersionDate = "April 2021"; +String localGUIVersionString = "v5.0.5-alpha.2"; +String localGUIVersionDate = "May 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; From 8160364673cf59e165597de884b0bf9a86a2d9aa Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Tue, 4 May 2021 20:46:39 -0500 Subject: [PATCH 18/34] Fix merge conflict in W_Accelerometer and default to 5 second window --- OpenBCI_GUI/W_Accelerometer.pde | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OpenBCI_GUI/W_Accelerometer.pde b/OpenBCI_GUI/W_Accelerometer.pde index 999f1b8a4..705766baa 100644 --- a/OpenBCI_GUI/W_Accelerometer.pde +++ b/OpenBCI_GUI/W_Accelerometer.pde @@ -55,7 +55,7 @@ class W_Accelerometer extends Widget { //Default dropdown settings settings.accVertScaleSave = 0; - settings.accHorizScaleSave = 0; + settings.accHorizScaleSave = 3; //Make dropdowns addDropdown("accelVertScale", "Vert Scale", Arrays.asList(settings.accVertScaleArray), settings.accVertScaleSave); @@ -69,7 +69,8 @@ class W_Accelerometer extends Widget { //create our channel bar and populate our accelerometerBar array! accelerometerBar = new AccelerometerBar(_parent, accelXyzLimit, accelGraphX, accelGraphY, accelGraphWidth, accelGraphHeight); - accelerometerBar.adjustVertScale(yLimOptions[0]); + accelerometerBar.adjustTimeAxis(xLimOptions[settings.accHorizScaleSave]); + accelerometerBar.adjustVertScale(yLimOptions[settings.accVertScaleSave]); createAccelModeButton("accelModeButton", "Turn Accel. Off", (int)(x + 3), (int)(y + 3 - navHeight), 120, navHeight - 6, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); } From 3404b3732ce7a42fc5bef577900e3f3654c53229 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Tue, 4 May 2021 22:11:48 -0500 Subject: [PATCH 19/34] Add Focus Networking data type - FocusWidget2021 --- OpenBCI_GUI/W_Focus.pde | 4 ++++ OpenBCI_GUI/W_Networking.pde | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 3dfe09d49..f6ec5ef23 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -368,6 +368,10 @@ class W_Focus extends Widget { public void setThreshold(int n) { focusThreshold = focusThreshold.values()[n]; } + + public int getMetricExceedsThreshold() { + return predictionExceedsThreshold ? 1 : 0; + } }; //end of class //The following global functions are used by the Focus widget dropdowns. This method is the least amount of code. diff --git a/OpenBCI_GUI/W_Networking.pde b/OpenBCI_GUI/W_Networking.pde index 4387aee77..edc096cbe 100644 --- a/OpenBCI_GUI/W_Networking.pde +++ b/OpenBCI_GUI/W_Networking.pde @@ -1422,6 +1422,8 @@ class Stream extends Thread { void sendData() { if (this.dataType.equals("TimeSeries")) { sendTimeSeriesData(); + } else if (this.dataType.equals("Focus")) { + sendFocusData(); } else if (this.dataType.equals("FFT")) { sendFFTData(); } else if (this.dataType.equals("EMG")) { @@ -1583,6 +1585,50 @@ class Stream extends Thread { } } + //Send out 1 or 0 as an integer over all networking data types for "Focus" data + //Filters do not apply to this data type + void sendFocusData() { + final int IS_METRIC = w_focus.getMetricExceedsThreshold(); + // OSC + if (this.protocol.equals("OSC")) { + msg.clearArguments(); + //ADD Focus Data + msg.add(IS_METRIC); + try { + this.osc.send(msg,this.netaddress); + } catch (Exception e) { + println(e.getMessage()); + } + // UDP + } else if (this.protocol.equals("UDP")) { + StringBuilder sb = new StringBuilder("{\"type\":\"focus\",\"data\":"); + sb.append(str(IS_METRIC)); + sb.append("]}\r\n"); + try { + this.udp.send(sb.toString(), this.ip, this.port); + } catch (Exception e) { + println(e.getMessage()); + } + // LSL + } else if (this.protocol.equals("LSL")) { + dataToSend[0] = IS_METRIC; + // Add timestamp to LSL Stream + outlet_data.push_sample(dataToSend, System.currentTimeMillis()); + // Serial + } else if (this.protocol.equals("Serial")) { // Send NORMALIZED EMG CHANNEL Data over Serial ... %%%%% + StringBuilder sb = new StringBuilder(); + sb.append(IS_METRIC); + sb.append("\n"); + try { + //println("SerialMessage: " + serialMessage); + this.serial_networking.write(sb.toString()); + } catch (Exception e) { + println("Networking Serial: Focus Error"); + println(e.getMessage()); + } + } + } + void sendFFTData() { // UNFILTERED & FILTERED ... influenced globally by the FFT filters dropdown //EEG/FFT readings above 125Hz don't typically travel through the skull From 3b38a7398910b15d8a9a8c58173227830e074db3 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Tue, 4 May 2021 23:12:02 -0500 Subject: [PATCH 20/34] Add focus networking output - add getDataTypeNumChan - Focus2021 --- OpenBCI_GUI/SessionSettings.pde | 2 +- OpenBCI_GUI/W_Networking.pde | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/OpenBCI_GUI/SessionSettings.pde b/OpenBCI_GUI/SessionSettings.pde index 7fb6c8968..2148f05a9 100644 --- a/OpenBCI_GUI/SessionSettings.pde +++ b/OpenBCI_GUI/SessionSettings.pde @@ -145,7 +145,7 @@ class SessionSettings { //Used to set text in dropdown menus when loading Networking settings String[] nwProtocolArray = {"Serial", "LSL", "UDP", "OSC"}; - String[] nwDataTypesArray = {"None", "TimeSeries", "FFT", "EMG", "BandPower", "Accel/Aux", "Pulse"}; + String[] nwDataTypesArray = {"None", "TimeSeries", "Focus", "EMG", "BandPower", "Accel/Aux", "FFT", "Pulse"}; String[] nwBaudRatesArray = {"57600", "115200", "250000", "500000"}; //Used to set text in dropdown menus when loading Analog Read settings diff --git a/OpenBCI_GUI/W_Networking.pde b/OpenBCI_GUI/W_Networking.pde index edc096cbe..64507c4ba 100644 --- a/OpenBCI_GUI/W_Networking.pde +++ b/OpenBCI_GUI/W_Networking.pde @@ -1046,6 +1046,8 @@ class W_Networking extends Widget { private int getDataTypeNumChanLSL(String dataType) { if (dataType.equals("TimeSeries")) { return currentBoard.getNumEXGChannels(); + } else if (dataType.equals("Focus")) { + return 1; } else if (dataType.equals("FFT")) { return 125; } else if (dataType.equals("EMG")) { @@ -1389,6 +1391,8 @@ class Stream extends Thread { Boolean checkForData() { //Try to remove these methods in next version of GUI if (this.dataType.equals("TimeSeries")) { return w_networking.newDataToSend; + } else if (this.dataType.equals("Focus")) { + return w_networking.newDataToSend; } else if (this.dataType.equals("FFT")) { return w_networking.newDataToSend; } else if (this.dataType.equals("EMG")) { @@ -1406,6 +1410,8 @@ class Stream extends Thread { void setDataFalse() { if (this.dataType.equals("TimeSeries")) { w_networking.newDataToSend = false; + } else if (this.dataType.equals("Focus")) { + w_networking.newDataToSend = false; } else if (this.dataType.equals("FFT")) { w_networking.newDataToSend = false; } else if (this.dataType.equals("EMG")) { @@ -1603,7 +1609,7 @@ class Stream extends Thread { } else if (this.protocol.equals("UDP")) { StringBuilder sb = new StringBuilder("{\"type\":\"focus\",\"data\":"); sb.append(str(IS_METRIC)); - sb.append("]}\r\n"); + sb.append("}\r\n"); try { this.udp.send(sb.toString(), this.ip, this.port); } catch (Exception e) { @@ -1611,7 +1617,7 @@ class Stream extends Thread { } // LSL } else if (this.protocol.equals("LSL")) { - dataToSend[0] = IS_METRIC; + dataToSend[0] = (float)IS_METRIC; // Add timestamp to LSL Stream outlet_data.push_sample(dataToSend, System.currentTimeMillis()); // Serial From d6aef8e731a26a819bcf400032228ff7df2369bf Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Wed, 5 May 2021 11:42:09 -0500 Subject: [PATCH 21/34] Fix focus widget default horizontal axis --- OpenBCI_GUI/W_Focus.pde | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index f6ec5ef23..43091bc55 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -99,7 +99,7 @@ class W_Focus extends Widget { //create our focus graph updateGraphDims(); - focusBar = new FocusBar(_parent, focusBarHardYAxisLimit, graphX, graphY, graphW, graphH); + focusBar = new FocusBar(_parent, xLimit.getValue(), focusBarHardYAxisLimit, graphX, graphY, graphW, graphH); initBrainFlowMetric(); } @@ -402,7 +402,7 @@ class FocusBar { GPointsArray focusPoints; int nPoints; - int numSeconds = FocusXLim.TWENTY.getValue(); //default to 20 seconds + int numSeconds; float timeBetweenPoints; float graphTimer; float[] focusTimeArray; @@ -418,7 +418,7 @@ class FocusBar { boolean isAutoscale; //when isAutoscale equals true, the y-axis will automatically update to scale to the largest visible amplitude int lastProcessedDataPacketInd = 0; - FocusBar(PApplet _parent, float accelXyzLimit, int _x, int _y, int _w, int _h) { //channel number, x/y location, height, width + FocusBar(PApplet _parent, int xLimit, float yLimit, int _x, int _y, int _w, int _h) { //channel number, x/y location, height, width x = _x; y = _y; w = _w; @@ -428,6 +428,7 @@ class FocusBar { } else { xOffset = 0; } + numSeconds = xLimit; plot = new GPlot(_parent); plot.setPos(x + 36 + 4 + xOffset, y); //match Accelerometer plot position with Time Series @@ -435,7 +436,7 @@ class FocusBar { plot.setMar(0f, 0f, 0f, 0f); plot.setLineColor((int)channelColors[(NUM_ACCEL_DIMS)%8]); plot.setXLim(-numSeconds,0); //set the horizontal scale - plot.setYLim(0, accelXyzLimit); //change this to adjust vertical scale + plot.setYLim(0, yLimit); //change this to adjust vertical scale //plot.setPointSize(2); plot.setPointColor(0); plot.getXAxis().setAxisLabelText("Time (s)"); From 7ba4404954b0fa597e9d4e7732270dd2cc916502 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 7 May 2021 17:50:18 -0500 Subject: [PATCH 22/34] Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off --- CHANGELOG.md | 3 +++ OpenBCI_GUI/OpenBCI_GUI.pde | 2 +- OpenBCI_GUI/W_TimeSeries.pde | 12 +++++------- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a13f4f03c..42b45a2da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ### Improvements * Implement Focus Widget using BrainFlow Metrics +### Bug Fixes +* Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off + # v5.0.4 ### Improvements diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index 5b301c44b..a9dd03801 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -65,7 +65,7 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.5-alpha.2"; +String localGUIVersionString = "v5.0.5-alpha.3"; String localGUIVersionDate = "May 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; diff --git a/OpenBCI_GUI/W_TimeSeries.pde b/OpenBCI_GUI/W_TimeSeries.pde index 51679735f..e43a62288 100644 --- a/OpenBCI_GUI/W_TimeSeries.pde +++ b/OpenBCI_GUI/W_TimeSeries.pde @@ -614,8 +614,8 @@ class ChannelBar { } private void updatePlotPoints() { - autoscaleMax = 0; - autoscaleMin = 0; + autoscaleMax = -Float.MAX_VALUE; + autoscaleMin = Float.MAX_VALUE; // update data in plot if (dataProcessingFilteredBuffer[channelIndex].length >= nPoints) { for (int i = dataProcessingFilteredBuffer[channelIndex].length - nPoints; i < dataProcessingFilteredBuffer[channelIndex].length; i++) { @@ -747,11 +747,9 @@ class ChannelBar { boolean doAutoscale = millis() > previousMillis + 1000; if (isAutoscale && currentBoard.isStreaming() && doAutoscale) { previousMillis = millis(); - float limit = Math.max(abs(autoscaleMin), autoscaleMax); - limit = Math.max(limit, 5); - plot.setYLim(-limit, limit); //<---- This is a very expensive method. Here is the bottleneck. - customYLim(yAxisMin, (int)-limit); - customYLim(yAxisMax, (int)limit); + plot.setYLim(autoscaleMin, autoscaleMax); //<---- This is a very expensive method. Here is the bottleneck. + customYLim(yAxisMin, (int)autoscaleMin); + customYLim(yAxisMax, (int)autoscaleMax); } } From c157bdc6999e5208cdca1a9dd3ce039217887e30 Mon Sep 17 00:00:00 2001 From: Andrey Parfenov Date: Mon, 10 May 2021 03:29:25 +0300 Subject: [PATCH 23/34] fix time in focus widget Signed-off-by: Andrey Parfenov --- OpenBCI_GUI/W_Focus.pde | 61 ++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 43091bc55..4f745622d 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -396,28 +396,15 @@ class FocusBar { int x, y, w, h; int focusBarPadding = 30; int xOffset; + final int nPoints = 30 * 1000; GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Series trace LinkedList fifoList; - GPointsArray focusPoints; + LinkedList fifoTimeList; - int nPoints; int numSeconds; - float timeBetweenPoints; - float graphTimer; - float[] focusTimeArray; - int numSamplesToProcess; - float minX, minY, minZ; - float maxX, maxY, maxZ; - float minVal; - float maxVal; - final float autoScaleSpacing = 0.1; - color channelColor; //color of plot trace - boolean isAutoscale; //when isAutoscale equals true, the y-axis will automatically update to scale to the largest visible amplitude - int lastProcessedDataPacketInd = 0; - FocusBar(PApplet _parent, int xLimit, float yLimit, int _x, int _y, int _w, int _h) { //channel number, x/y location, height, width x = _x; y = _y; @@ -450,21 +437,17 @@ class FocusBar { initArrays(); //set the plot points for X, Y, and Z axes - plot.addLayer("layer 1", focusPoints); + plot.addLayer("layer 1", new GPointsArray(30)); plot.getLayer("layer 1").setLineColor(ACCEL_X_COLOR); } private void initArrays() { - nPoints = nPointsBasedOnDataSource(); - timeBetweenPoints = (float)numSeconds / (float)nPoints; - focusTimeArray = new float[nPoints]; fifoList = new LinkedList(); - for (int i = 0; i < focusTimeArray.length; i++) { - focusTimeArray[i] = -(float)numSeconds + (float)i * timeBetweenPoints; + fifoTimeList = new LinkedList(); + for (int i = 0; i < nPoints; i++) { fifoList.add(0f); + fifoTimeList.add(0f); } - float[] floatArray = ArrayUtils.toPrimitive(fifoList.toArray(new Float[0]), 0.0F); - focusPoints = new GPointsArray(focusTimeArray, floatArray); } public void update(double val) { @@ -483,10 +466,6 @@ class FocusBar { plot.endDraw(); } - private int nPointsBasedOnDataSource() { - return numSeconds * 30; - } - public void adjustTimeAxis(int _newTimeSize) { numSeconds = _newTimeSize; plot.setXLim(-_newTimeSize,0); @@ -501,18 +480,24 @@ class FocusBar { //Used to update the Points within the graph private void updateGPlotPoints(double val) { - //todo : important to align time with actual elapsed time! - //if (graphTimer + timeBetweenPoints < millis()) { - graphTimer = millis(); - fifoList.removeFirst(); - fifoList.addLast((float)val); - - for (int i=0; i < nPoints; i++) { - focusPoints.set(i, focusTimeArray[i], fifoList.get(i), ""); + float timerVal = (float)millis() / 1000.0; + fifoTimeList.removeFirst(); + fifoTimeList.addLast(timerVal); + fifoList.removeFirst(); + fifoList.addLast((float)val); + + int stopId = 0; + for (stopId = nPoints - 1; stopId > 0; stopId--) { + if (timerVal - fifoTimeList.get(stopId) > numSeconds) { + break; } - - plot.setPoints(focusPoints, "layer 1"); - //} + } + int size = nPoints - 1 - stopId; + GPointsArray focusPoints = new GPointsArray(size); + for (int i = 0; i < size; i++) { + focusPoints.set(i, fifoTimeList.get(i + stopId) - timerVal, fifoList.get(i + stopId), ""); + } + plot.setPoints(focusPoints, "layer 1"); } public void screenResized(int _x, int _y, int _w, int _h) { From 6e1714bfa12523542856479304116fb50a983313 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Wed, 19 May 2021 19:41:06 -0500 Subject: [PATCH 24/34] Fix weird bug when autoscaling in time series --- OpenBCI_GUI/W_TimeSeries.pde | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/OpenBCI_GUI/W_TimeSeries.pde b/OpenBCI_GUI/W_TimeSeries.pde index e43a62288..c1a3e2811 100644 --- a/OpenBCI_GUI/W_TimeSeries.pde +++ b/OpenBCI_GUI/W_TimeSeries.pde @@ -744,9 +744,12 @@ class ChannelBar { public void applyAutoscale() { //Do this once a second for all TimeSeries ChannelBars to save on resources - boolean doAutoscale = millis() > previousMillis + 1000; + int newMillis = millis(); + boolean doAutoscale = newMillis > previousMillis + 1000; if (isAutoscale && currentBoard.isStreaming() && doAutoscale) { - previousMillis = millis(); + autoscaleMin = (int) Math.floor(autoscaleMin); + autoscaleMax = (int) Math.ceil(autoscaleMax); + previousMillis = newMillis; plot.setYLim(autoscaleMin, autoscaleMax); //<---- This is a very expensive method. Here is the bottleneck. customYLim(yAxisMin, (int)autoscaleMin); customYLim(yAxisMax, (int)autoscaleMax); From a13ba331dc6ea3dfac54e517234b1e0a5cb37f86 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Wed, 26 May 2021 20:37:55 -0500 Subject: [PATCH 25/34] Display Average Band Power data from BrainFlow in the new Focus Widget --- OpenBCI_GUI/W_Focus.pde | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index 4f745622d..c74b28da2 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -91,6 +91,11 @@ class W_Focus extends Widget { dataGrid.setTableFontAndSize(p6, 10); dataGrid.setDrawTableBorder(true); dataGrid.setString("Metric Value", 0, 0); + dataGrid.setString("Delta (1.5-4Hz)", 1, 0); + dataGrid.setString("Theta (4-8Hz)", 2, 0); + dataGrid.setString("Alpha (7.5-13Hz)", 3, 0); + dataGrid.setString("Beta (13-30Hz)", 4, 0); + dataGrid.setString("Gamma (30-45Hz)", 5, 0); //Instantiate local cp5 for this box. This allows extra control of drawing cp5 elements specifically inside this class. focus_cp5 = new ControlP5(ourApplet); @@ -248,9 +253,13 @@ class W_Focus extends Widget { new Integer[focusChanSelect.activeChan.size()] )); + //Full Source Code for this method: https://github.com/brainflow-dev/brainflow/blob/c5f0ad86683e6eab556e30965befb7c93e389a3b/src/data_handler/data_handler.cpp#L1115 Pair bands = DataFilter.get_avg_band_powers (dataArray, channelsInDataArray, currentBoard.getSampleRate(), true); double[] featureVector = ArrayUtils.addAll (bands.getLeft (), bands.getRight ()); + //Left array is Averages, right array is Standard Deviations. Update values using Averages. + updateBandPowerTableValues(bands.getLeft()); + //Keep this here double prediction = mlModel.predict(featureVector); //println("Concentration: " + prediction); @@ -264,6 +273,12 @@ class W_Focus extends Widget { } } + private void updateBandPowerTableValues(double[] bandPowers) { + for (int i = 0; i < bandPowers.length; i++) { + dataGrid.setString(df.format(bandPowers[i]), 1 + i, 1); + } + } + private void drawStatusCircle() { color fillColor; color strokeColor; From 208b0d682b550e7373177ebaac461abe53dfdf08 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 14:34:50 -0500 Subject: [PATCH 26/34] Clarify Windows OS requirements with popup. Fixes #964 --- CHANGELOG.md | 1 + OpenBCI_GUI/Extras.pde | 8 ++++++++ OpenBCI_GUI/OpenBCI_GUI.pde | 12 +++++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42b45a2da..b8d844d37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Improvements * Implement Focus Widget using BrainFlow Metrics +* Throw a popup if users are are running an old version of Windows operating system. GUI v5 supports 64-bit Windows 8, 8.1, and 10. ### Bug Fixes * Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off diff --git a/OpenBCI_GUI/Extras.pde b/OpenBCI_GUI/Extras.pde index 50a407345..a9089ce14 100644 --- a/OpenBCI_GUI/Extras.pde +++ b/OpenBCI_GUI/Extras.pde @@ -26,6 +26,14 @@ private boolean isMac() { return !isWindows() && !isLinux(); } +//BrainFlow only supports Windows 8 and 10. This will help with OpenBCI support tickets. +private void checkIsOldVersionOfWindowsOS() { + boolean isOld = SystemUtils.IS_OS_WINDOWS_7 || SystemUtils.IS_OS_WINDOWS_VISTA || SystemUtils.IS_OS_WINDOWS_XP; + if (isOld) { + PopupMessage msg = new PopupMessage("Old Windows OS Detected", "OpenBCI GUI v5 and BrainFlow are made for 64-bit Windows 8, 8.1, and 10. Please update your OS, computer, or revert to GUI v4.2.0."); + } +} + //compute the standard deviation float std(float[] data) { diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index a9dd03801..7a570dee6 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -363,18 +363,20 @@ void setup() { System.setOut(outputStream); System.setErr(outputStream); - String osName = "Operating System: "; + StringBuilder osName = new StringBuilder("Operating System: "); if (isLinux()) { - osName += "Linux"; + osName.append("Linux"); } else if (isWindows()) { - osName += "Windows"; + osName.append("Windows"); + //Throw a popup if we detect an incompatible version of Windows. Fixes #964. Found in Extras.pde. + checkIsOldVersionOfWindowsOS(); } else if (isMac()) { - osName += "Mac"; + osName.append("Mac"); } println("Console Log Started at Local Time: " + directoryManager.getFileNameDateTime()); println("Screen Resolution: " + displayWidth + " X " + displayHeight); - println(osName); + println(osName.toString()); println("Welcome to the Processing-based OpenBCI GUI!"); //Welcome line. println("For more information, please visit: https://openbci.github.io/Documentation/docs/06Software/01-OpenBCISoftware/GUIDocs"); From 9fa5f176977d235ed6391a4d676e5e269df3e5f4 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 15:50:43 -0500 Subject: [PATCH 27/34] Check for 64-bit Java on Windows #964 --- CHANGELOG.md | 5 +++-- OpenBCI_GUI/Extras.pde | 10 +++++++++- OpenBCI_GUI/OpenBCI_GUI.pde | 4 +++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d844d37..3d7a54773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # v5.0.5 ### Improvements -* Implement Focus Widget using BrainFlow Metrics -* Throw a popup if users are are running an old version of Windows operating system. GUI v5 supports 64-bit Windows 8, 8.1, and 10. +* Implement Focus Widget using BrainFlow Metrics! #924 +* Throw a popup if users are are running an old version of Windows operating system. GUI v5 supports 64-bit Windows 8, 8.1, and 10. #964 +* Throw a popup if Windows users are using 32-bit Java and Processing. #964 ### Bug Fixes * Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off diff --git a/OpenBCI_GUI/Extras.pde b/OpenBCI_GUI/Extras.pde index a9089ce14..6076e5abe 100644 --- a/OpenBCI_GUI/Extras.pde +++ b/OpenBCI_GUI/Extras.pde @@ -26,7 +26,7 @@ private boolean isMac() { return !isWindows() && !isLinux(); } -//BrainFlow only supports Windows 8 and 10. This will help with OpenBCI support tickets. +//BrainFlow only supports Windows 8 and 10. This will help with OpenBCI support tickets. #964 private void checkIsOldVersionOfWindowsOS() { boolean isOld = SystemUtils.IS_OS_WINDOWS_7 || SystemUtils.IS_OS_WINDOWS_VISTA || SystemUtils.IS_OS_WINDOWS_XP; if (isOld) { @@ -34,6 +34,14 @@ private void checkIsOldVersionOfWindowsOS() { } } +//Sanity check for 64-bit Java for Windows users #964 +private void checkIs64BitJava() { + boolean is64Bit = System.getProperty("sun.arch.data.model").indexOf("64") >= 0; + if (!is64Bit) { + PopupMessage msg = new PopupMessage("32-bit Java Detected", "OpenBCI GUI v5 and BrainFlow are made for 64-bit Java (Windows, Linux, and Mac). Please update your OS, computer, Processing IDE, or revert to GUI v4 or earlier."); + } +} + //compute the standard deviation float std(float[] data) { diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index 7a570dee6..233f5dc58 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -65,7 +65,7 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.5-alpha.3"; +String localGUIVersionString = "v5.0.5-alpha.4"; String localGUIVersionDate = "May 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; @@ -370,6 +370,8 @@ void setup() { osName.append("Windows"); //Throw a popup if we detect an incompatible version of Windows. Fixes #964. Found in Extras.pde. checkIsOldVersionOfWindowsOS(); + //This is an edge case when using 32-bit Processing Java on Windows. Throw a popup if detected. + checkIs64BitJava(); } else if (isMac()) { osName.append("Mac"); } From 5753e1e623813a2140d4a7c86a1bf0b33e5606ae Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 17:30:58 -0500 Subject: [PATCH 28/34] Gracefully handle cases when Cyton or Cyton+Daisy users want to use 8 or 16 channels #954 --- CHANGELOG.md | 1 + OpenBCI_GUI/OpenBCI_GUI.pde | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d7a54773..6d90e426a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Bug Fixes * Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off +* Gracefully handle cases when Cyton or Cyton+Daisy users want to use 8 or 16 channels #954 # v5.0.4 diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index 233f5dc58..7a1400a79 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -65,7 +65,7 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.5-alpha.4"; +String localGUIVersionString = "v5.0.5-alpha.5"; String localGUIVersionDate = "May 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; @@ -575,6 +575,27 @@ void initSystem() { // initialize the chosen board boolean success = currentBoard.initialize(); abandonInit = !success; // abandon if init fails + + //Handle edge cases for Cyton and Cyton+Daisy users immediately after board is initialized. Fixes #954 + if (eegDataSource == DATASOURCE_CYTON) { + println("OpenBCI_GUI: Configuring Cyton Channel Count..."); + if (currentBoard instanceof BoardCytonSerial) { + Pair res = ((BoardBrainFlow)currentBoard).sendCommand("c"); + //println(res.getKey().booleanValue(), res.getValue()); + if (res.getValue().startsWith("daisy removed")) { + println("OpenBCI_GUI: Daisy is physically attached, using Cyton 8 Channels instead."); + } + } else if (currentBoard instanceof BoardCytonSerialDaisy) { + Pair res = ((BoardBrainFlow)currentBoard).sendCommand("C"); + //println(res.getKey().booleanValue(), res.getValue()); + if (res.getValue().startsWith("no daisy to attach")) { + haltSystem(); + outputError("User selected Cyton+Daisy, but no Daisy is attached. Please change Channel Count to 8 Channels."); + controlPanel.open(); + return; + } + } + } updateToNChan(currentBoard.getNumEXGChannels()); @@ -895,7 +916,7 @@ void updateToNChan(int _nchan) { nchan = _nchan; settings.slnchan = _nchan; //used in SoftwareSettings.pde only fftBuff = new FFT[nchan]; //reinitialize the FFT buffer - println("Channel count set to " + str(nchan)); + println("OpenBCI_GUI: Channel count set to " + str(nchan)); } void introAnimation() { From b3c8a1df3f1a69b1da098d0a79bcaf4f3e9a66ff Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 17:46:25 -0500 Subject: [PATCH 29/34] Remove unused imports in openbci_gui.pde --- OpenBCI_GUI/OpenBCI_GUI.pde | 5 ----- 1 file changed, 5 deletions(-) diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index 7a1400a79..ea56e2302 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -51,11 +51,6 @@ import oscP5.*; // for OSC import hypermedia.net.*; //for UDP import java.nio.ByteBuffer; //for UDP import edu.ucsd.sccn.LSL; //for LSL -//These are used by LSL -//import com.sun.jna.Library; -//import com.sun.jna.Native; -//import com.sun.jna.Platform; -//import com.sun.jna.Pointer; import com.fazecast.jSerialComm.*; //Helps distinguish serial ports on Windows import org.apache.commons.lang3.time.StopWatch; import http.requests.*; From a6d07520db756b3c8d58a5ed0514292547cd3808 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 18:15:25 -0500 Subject: [PATCH 30/34] Address Issue #969 - Change Save Settings Success message --- CHANGELOG.md | 1 + OpenBCI_GUI/Interactivity.pde | 6 +++--- OpenBCI_GUI/SessionSettings.pde | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d90e426a..f4c91c0ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Bug Fixes * Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off * Gracefully handle cases when Cyton or Cyton+Daisy users want to use 8 or 16 channels #954 +* Update Save Session Settings success message. Session settings are no longer auto-loaded on Session start. #969 # v5.0.4 diff --git a/OpenBCI_GUI/Interactivity.pde b/OpenBCI_GUI/Interactivity.pde index 3a187b7b8..efe377a3b 100644 --- a/OpenBCI_GUI/Interactivity.pde +++ b/OpenBCI_GUI/Interactivity.pde @@ -106,14 +106,14 @@ void parseKey(char val) { ///////////////////// Save User settings lowercase n case 'n': - println("Save key pressed!"); + println("Interactivity: Save key pressed!"); settings.save(settings.getPath("User", eegDataSource, nchan)); - outputSuccess("Settings Saved! The GUI will now load with these settings. Click \"Default\" to revert to factory settings."); + outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory settings."); return; ///////////////////// Load User settings uppercase N case 'N': - println("Load key pressed!"); + println("Interactivity: Load key pressed!"); settings.loadKeyPressed(); return; diff --git a/OpenBCI_GUI/SessionSettings.pde b/OpenBCI_GUI/SessionSettings.pde index 2148f05a9..a6fb91bfa 100644 --- a/OpenBCI_GUI/SessionSettings.pde +++ b/OpenBCI_GUI/SessionSettings.pde @@ -1171,7 +1171,7 @@ void saveConfigFile(File selection) { println("SessionSettings: saveConfigFile: User selected " + selection.getAbsolutePath()); settings.saveDialogName = selection.getAbsolutePath(); settings.save(settings.saveDialogName); //save current settings to JSON file in SavedData - outputSuccess("Settings Saved! The GUI will now load with these settings. Click \"Default\" to revert to factory settings."); //print success message to screen + outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory settings."); //print success message to screen settings.saveDialogName = null; //reset this variable for future use } } From 97d71f763b35e1f5fd72c36a697604414afeb925 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 18:44:31 -0500 Subject: [PATCH 31/34] Focus widget debugging - Drop default serial baud to 57600 and add breathing room at upper limit of focus widget gplot graph --- OpenBCI_GUI/W_Focus.pde | 2 +- OpenBCI_GUI/W_Networking.pde | 2 +- OpenBCI_GUI/WidgetManager.pde | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/OpenBCI_GUI/W_Focus.pde b/OpenBCI_GUI/W_Focus.pde index c74b28da2..8a1e282eb 100644 --- a/OpenBCI_GUI/W_Focus.pde +++ b/OpenBCI_GUI/W_Focus.pde @@ -44,7 +44,7 @@ class W_Focus extends Widget { private final int CLASSIFIER_DROPDOWN_W = 80; private FocusBar focusBar; - private float focusBarHardYAxisLimit = 1f; + private float focusBarHardYAxisLimit = 1.05f; //Provide slight "breathing room" to avoid GPlot error when metric value == 1.0 FocusXLim xLimit = FocusXLim.TEN; FocusMetric focusMetric = FocusMetric.RELAXATION; FocusClassifier focusClassifier = FocusClassifier.REGRESSION; diff --git a/OpenBCI_GUI/W_Networking.pde b/OpenBCI_GUI/W_Networking.pde index 64507c4ba..fdc55a46e 100644 --- a/OpenBCI_GUI/W_Networking.pde +++ b/OpenBCI_GUI/W_Networking.pde @@ -148,7 +148,7 @@ class W_Networking extends Widget { if (eegDataSource != DATASOURCE_CYTON) { dataTypes.remove("Pulse"); } - defaultBaud = "115200"; + defaultBaud = "57600"; baudRates = Arrays.asList(settings.nwBaudRatesArray); protocolMode = "Serial"; //default to Serial addDropdown("Protocol", "Protocol", Arrays.asList(settings.nwProtocolArray), protocolIndex); diff --git a/OpenBCI_GUI/WidgetManager.pde b/OpenBCI_GUI/WidgetManager.pde index b2d9ecbf8..04e9f98dc 100644 --- a/OpenBCI_GUI/WidgetManager.pde +++ b/OpenBCI_GUI/WidgetManager.pde @@ -36,12 +36,6 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_timeSeries, w); // println(" setupWidgets time series -- " + millis()); - //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 - w_focus = new W_Focus(_this); - w_focus.setTitle("Focus Widget"); - addWidget(w_focus, w); - // println(" setupWidgets focus widget -- " + millis()); - //Widget_1 w_fft = new W_fft(_this); w_fft.setTitle("FFT Plot"); @@ -70,6 +64,12 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_ganglionImpedance, w); } + //Cyton Widget_12, Synthetic Widget_9, Ganglion/Playback Widget_10 + w_focus = new W_Focus(_this); + w_focus.setTitle("Focus Widget"); + addWidget(w_focus, w); + // println(" setupWidgets focus widget -- " + millis()); + //Cyton/Synthetic Widget_3, Ganglion/Playback Widget_4 w_networking = new W_Networking(_this); w_networking.setTitle("Networking"); From f65ded520abe3f2fb08b1e9e233ad7c1897d358e Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 19:03:51 -0500 Subject: [PATCH 32/34] Session settings are no longer auto-saved when system is halted Fixes #969 --- CHANGELOG.md | 1 + OpenBCI_GUI/OpenBCI_GUI.pde | 10 +--------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c91c0ab..3384e0379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off * Gracefully handle cases when Cyton or Cyton+Daisy users want to use 8 or 16 channels #954 * Update Save Session Settings success message. Session settings are no longer auto-loaded on Session start. #969 +* Session settings are no longer auto-saved when system is halted #969 # v5.0.4 diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index ea56e2302..ea4e4ffbe 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -60,7 +60,7 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.5-alpha.5"; +String localGUIVersionString = "v5.0.5-alpha.7"; String localGUIVersionDate = "May 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; @@ -766,14 +766,6 @@ void haltSystem() { topNav.resetStartStopButton(); topNav.destroySmoothingButton(); //Destroy this button if exists and make null, will be re-init if needed next time session starts - //Save a snapshot of User's GUI settings if the system is stopped, or halted. This will be loaded on next Start System. - //This method establishes default and user settings for all data modes - if (systemMode == SYSTEMMODE_POSTINIT && - eegDataSource != DATASOURCE_GALEA && - eegDataSource != DATASOURCE_STREAMING) { - settings.save(settings.getPath("User", eegDataSource, nchan)); - } - //reset connect loadStrings openBCI_portName = "N/A"; // Fixes inability to reconnect after halding JAM 1/2017 ganglion_portName = ""; From 2e0cf3d9e006bc933e24853dafa50baca37b4c18 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 19:16:55 -0500 Subject: [PATCH 33/34] Update change log to reflect networking serial output default baud rate --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3384e0379..47646b90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Implement Focus Widget using BrainFlow Metrics! #924 * Throw a popup if users are are running an old version of Windows operating system. GUI v5 supports 64-bit Windows 8, 8.1, and 10. #964 * Throw a popup if Windows users are using 32-bit Java and Processing. #964 +* Set Networking Widget default baud rate for Serial output to 57600 ### Bug Fixes * Fix Y axis Autoscale in TimeSeries when all values are less than zero. Example: Cyton with filters off From 1031ce1f0787f14e755395a272f8b696f6ee7b50 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 28 May 2021 19:38:07 -0500 Subject: [PATCH 34/34] Bump Version to 5.0.5 May 2021 --- OpenBCI_GUI/Info.plist.tmpl | 6 +++--- OpenBCI_GUI/OpenBCI_GUI.pde | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OpenBCI_GUI/Info.plist.tmpl b/OpenBCI_GUI/Info.plist.tmpl index 26c6b4ca7..0db5432a2 100644 --- a/OpenBCI_GUI/Info.plist.tmpl +++ b/OpenBCI_GUI/Info.plist.tmpl @@ -21,9 +21,9 @@ CFBundleShortVersionString - 4 + 5 CFBundleVersion - 5.0.4 + 5.0.5 CFBundleSignature ???? NSHumanReadableCopyright @@ -32,7 +32,7 @@ Copyright © 2021 OpenBCI CFBundleGetInfoString - March 2021 + May 2021 @@jvm_runtime@@ diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index ea4e4ffbe..0462d5822 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -60,7 +60,7 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.5-alpha.7"; +String localGUIVersionString = "v5.0.5"; String localGUIVersionDate = "May 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest";