Skip to content

Commit 5313607

Browse files
authored
Merge pull request #985 from OpenBCI/development
GUI 5.0.6 - July 2021
2 parents d3e635d + 9b24951 commit 5313607

File tree

15 files changed

+361
-35
lines changed

15 files changed

+361
-35
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# v5.0.6
2+
3+
### Improvements
4+
* Add Auditory Feedback to the Focus Widget Fixes #709
5+
6+
### Bug Fixes
7+
* Fix drawing error in Control Panel WiFi Shield static IP Textfield
8+
* Accomodate high-dpi screens Fixes #968
9+
* Add Arduino Focus Fan example to networking test kit on GitHub repo
10+
* Allow synthetic square wave expert mode keyboard shortcut for Cyton and Ganglion Fixes #976
11+
112
# v5.0.5
213

314
### Improvements
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/////////////////////////////////////////////////////////////////////////////////////////
2+
// OpenBCI_GUI to Arduino via Serial: Focus Fan! //
3+
// //
4+
// - The Arduino Built-In LED blinks when the user is Focused //
5+
// - A button on pin 7 toggles motor speed: Full, Medium, Low, and Off //
6+
// //
7+
// Tested 7/13/2021 using iMac, Arduino Pro Mini, OpenBCI_GUI 5.0.5 //
8+
// Uses https://learn.adafruit.com/adafruit-arduino-lesson-15-dc-motor-reversing/ //
9+
// and https://docs.openbci.com/Tutorials/17-Arduino_Focus_Example //
10+
/////////////////////////////////////////////////////////////////////////////////////////
11+
12+
const byte numChars = 32;
13+
char receivedChars[numChars]; // an array to store the received data
14+
String previousData = "";
15+
boolean newData = false;
16+
boolean isFocused = false;
17+
boolean lastFocusState = false;
18+
19+
int enablePin = 11;
20+
int in1Pin = 10;
21+
int in2Pin = 9;
22+
int buttonPin = 7;
23+
int ledPin = 13;
24+
25+
int buttonPushCounter = 0; // counter for the number of button presses
26+
int buttonState = 0; // current state of the button
27+
int lastButtonState = 0; // previous state of the button
28+
29+
void setup() {
30+
pinMode(in1Pin, OUTPUT);
31+
pinMode(in2Pin, OUTPUT);
32+
pinMode(enablePin, OUTPUT);
33+
pinMode(buttonPin, INPUT_PULLUP);
34+
35+
Serial.begin(57600);
36+
pinMode(LED_BUILTIN, OUTPUT);
37+
Serial.println("<Arduino is ready>");
38+
}
39+
40+
void loop() {
41+
recvWithEndMarker();
42+
showNewData();
43+
44+
handleButtonState();
45+
//check for state change
46+
if (isFocused) {
47+
setMotor(getMotorPower(), true);
48+
} else {
49+
setMotor(0, true);
50+
}
51+
lastFocusState = isFocused;
52+
}
53+
54+
//Recieve data and look for the endMarker '\n' (new line)
55+
void recvWithEndMarker() {
56+
static byte ndx = 0;
57+
char endMarker = '\n';
58+
char rc;
59+
60+
while (Serial.available() > 0 && newData == false) {
61+
rc = Serial.read();
62+
63+
if (rc != endMarker) {
64+
receivedChars[ndx] = rc;
65+
ndx++;
66+
if (ndx >= numChars) {
67+
ndx = numChars - 1;
68+
}
69+
}
70+
else {
71+
receivedChars[ndx] = '\0'; // terminate the string
72+
ndx = 0;
73+
newData = true;
74+
}
75+
}
76+
}
77+
78+
void showNewData() {
79+
if (newData == true) {
80+
//Convert char array into string
81+
String s = receivedChars;
82+
//Only perform an action when the incoming data changes
83+
if (!s.equals(previousData)) {
84+
//Check if the string is "true" or "false"
85+
if (s.equals("0")) {
86+
Serial.println("Input: FALSE");
87+
isFocused = false;
88+
digitalWrite(LED_BUILTIN, LOW);
89+
} else if (s.equals("1")) {
90+
Serial.println("Input: TRUE");
91+
digitalWrite(LED_BUILTIN, HIGH);
92+
isFocused = true;
93+
} else {
94+
//Otherwise print the incoming with no action
95+
Serial.println("This just in ... " + s);
96+
}
97+
}
98+
newData = false;
99+
previousData = s;
100+
}
101+
}
102+
103+
void setMotor(int speed, boolean reverse) {
104+
analogWrite(enablePin, speed);
105+
digitalWrite(in1Pin, !reverse);
106+
digitalWrite(in2Pin, reverse);
107+
}
108+
109+
void handleButtonState () {
110+
buttonState = digitalRead(buttonPin);
111+
112+
// compare the buttonState to its previous state
113+
if (buttonState != lastButtonState) {
114+
// if the state has changed, increment the counter
115+
if (buttonState == HIGH) {
116+
// if the current state is HIGH then the button went from off to on:
117+
buttonPushCounter++;
118+
//Serial.println("on");
119+
} else {
120+
// if the current state is LOW then the button went from on to off:
121+
//Serial.println("off");
122+
}
123+
// Delay a little bit to avoid bouncing
124+
delay(50);
125+
}
126+
// save the current state as the last state, for next time through the loop
127+
lastButtonState = buttonState;
128+
}
129+
130+
int getMotorPower() {
131+
//Toggle the fan power between four settings, shown below
132+
//Default: Full Power
133+
int power;
134+
switch (buttonPushCounter % 4) {
135+
case 0: //Every 4 clicks reverts to full power
136+
power = 255; //Full power
137+
break;
138+
case 1:
139+
power = 180; //Medium power
140+
break;
141+
case 2:
142+
power = 90; //Low power
143+
break;
144+
case 3:
145+
power = 0; //Motor off
146+
break;
147+
}
148+
return power;
149+
}

OpenBCI_GUI/AuditoryNeurofeedback.pde

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//Used in the Focus Widget to provide auditory neurofeedback
2+
//Adjust amplitude of calming audio samples using normalized band power data or predicted metric
3+
4+
Minim minim;
5+
FilePlayer[] auditoryNfbFilePlayers;
6+
ddf.minim.ugens.Gain[] auditoryNfbGains;
7+
AudioOutput audioOutput;
8+
9+
//Pre-load audio files into memory in delayedSetup for best app performance and no waiting
10+
void asyncLoadAudioFiles() {
11+
final int _numSoundFiles = 5;
12+
minim = new Minim(this);
13+
auditoryNfbFilePlayers = new FilePlayer[_numSoundFiles];
14+
auditoryNfbGains = new ddf.minim.ugens.Gain[_numSoundFiles];
15+
audioOutput = minim.getLineOut();
16+
println("OpenBCI_GUI: AuditoryFeedback: Loading Audio...");
17+
for (int i = 0; i < _numSoundFiles; i++) {
18+
//Use large buffer size and cache files in memory
19+
auditoryNfbFilePlayers[i] = new FilePlayer( minim.loadFileStream("bp" + (i+1) + ".mp3", 2048, true) );
20+
auditoryNfbGains[i] = new ddf.minim.ugens.Gain(-15.0f);
21+
auditoryNfbFilePlayers[i].patch(auditoryNfbGains[i]).patch(audioOutput);
22+
}
23+
println("OpenBCI_GUI: AuditoryFeedback: Done Loading Audio!");
24+
}
25+
26+
class AuditoryNeurofeedback {
27+
28+
private int x, y, w, h;
29+
private ControlP5 localCP5;
30+
public Button startStopButton;
31+
public Button modeButton;
32+
private boolean usingBandPowers = false;
33+
//There will always be 5 band powers, and 5 possible concurrent audio files for playback
34+
private final int NUM_SOUND_FILES = auditoryNfbFilePlayers.length;
35+
private final float MIN_GAIN = -42.0;
36+
private final float MAX_GAIN = -7.0;
37+
private final int MAX_BUTTON_W = 120;
38+
private int buttonW = 120;
39+
private int buttonH;
40+
41+
AuditoryNeurofeedback(int _x, int _y, int _w, int _h) {
42+
localCP5 = new ControlP5(ourApplet);
43+
localCP5.setGraphics(ourApplet, 0,0);
44+
localCP5.setAutoDraw(false);
45+
buttonH = _h;
46+
createStartStopButton(_x, _y, buttonW, buttonH);
47+
createModeButton(_x, _y, buttonW, buttonH);
48+
}
49+
50+
//Use band powers or prediction value to control volume of each sound file
51+
public void update(double[] bandPowers, float predictionVal) {
52+
if (usingBandPowers) {
53+
for (int i = 0; i < NUM_SOUND_FILES; i++) {
54+
float gain = map((float)bandPowers[i], 0.1, .7, MIN_GAIN + 20f, MAX_GAIN);
55+
auditoryNfbGains[i].setValue(gain);
56+
}
57+
} else {
58+
float gain = map(predictionVal, 0.0, 1.0, MIN_GAIN, MAX_GAIN);
59+
for (int i = 0; i < NUM_SOUND_FILES; i++) {
60+
auditoryNfbGains[i].setValue(gain);
61+
}
62+
}
63+
}
64+
65+
public void draw() {
66+
localCP5.draw();
67+
}
68+
69+
public void screenResized(int _x, int _y, int _w, int _h) {
70+
localCP5.setGraphics(ourApplet, 0, 0);
71+
buttonW = (_w - 6) / 2;
72+
buttonW = buttonW > MAX_BUTTON_W ? MAX_BUTTON_W : buttonW;
73+
startStopButton.setPosition(_x - buttonW - 3, _y);
74+
startStopButton.setSize(buttonW, _h);
75+
modeButton.setPosition(_x + 3, _y);
76+
modeButton.setSize(buttonW, _h);
77+
}
78+
79+
public void killAudio() {
80+
for (int i = 0; i < NUM_SOUND_FILES; i++) {
81+
auditoryNfbFilePlayers[i].pause();
82+
auditoryNfbFilePlayers[i].rewind();
83+
}
84+
}
85+
86+
private void createStartStopButton(int _x, int _y, int _w, int _h) {
87+
//This is a generalized createButton method that allows us to save code by using a few patterns and method overloading
88+
startStopButton = createButton(localCP5, "startStopButton", "Turn Audio On", _x, _y, _w, _h, p5, 12, colorNotPressed, OPENBCI_DARKBLUE);
89+
//Set the border color explicitely
90+
startStopButton.setBorderColor(OBJECT_BORDER_GREY);
91+
//For this button, only call the callback listener on mouse release
92+
startStopButton.onRelease(new CallbackListener() {
93+
public void controlEvent(CallbackEvent theEvent) {
94+
//If using a TopNav object, ignore interaction with widget object (ex. widgetTemplateButton)
95+
if (!topNav.configSelector.isVisible && !topNav.layoutSelector.isVisible) {
96+
if (auditoryNfbFilePlayers[0].isPlaying()) {
97+
killAudio();
98+
startStopButton.getCaptionLabel().setText("Turn Audio On");
99+
} else {
100+
for (int i = 0; i < NUM_SOUND_FILES; i++) {
101+
auditoryNfbFilePlayers[i].loop();
102+
}
103+
startStopButton.getCaptionLabel().setText("Turn Audio Off");
104+
}
105+
}
106+
}
107+
});
108+
startStopButton.setDescription("Start and Stop Auditory Feedback.");
109+
}
110+
111+
private void createModeButton(int _x, int _y, int _w, int _h) {
112+
//This is a generalized createButton method that allows us to save code by using a few patterns and method overloading
113+
modeButton = createButton(localCP5, "modeButton", "Use Band Powers", _x, _y, _w, _h, p5, 12, colorNotPressed, OPENBCI_DARKBLUE);
114+
//Set the border color explicitely
115+
modeButton.setBorderColor(OBJECT_BORDER_GREY);
116+
//For this button, only call the callback listener on mouse release
117+
modeButton.onRelease(new CallbackListener() {
118+
public void controlEvent(CallbackEvent theEvent) {
119+
//If using a TopNav object, ignore interaction with widget object (ex. widgetTemplateButton)
120+
if (!topNav.configSelector.isVisible && !topNav.layoutSelector.isVisible) {
121+
String s = !usingBandPowers ? "Use Metric" : "Use Band Powers";
122+
modeButton.getCaptionLabel().setText(s);
123+
usingBandPowers = !usingBandPowers;
124+
}
125+
}
126+
});
127+
modeButton.setDescription("Change Auditory Feedback mode. Use the Metric to control all notes at once, or use Band Powers to control certain notes of the chord.");
128+
}
129+
130+
}

OpenBCI_GUI/ControlPanel.pde

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -941,9 +941,15 @@ class WifiBox {
941941
.setColorCursor(color(26, 26, 26))
942942
.setText(wifi_ipAddress)
943943
.align(5, 10, 20, 40)
944-
.onDoublePress(cb)
945944
.setAutoClear(true)
946945
.setVisible(false);
946+
//Clear textfield on double click
947+
staticIPAddressTF.onDoublePress(new CallbackListener() {
948+
public void controlEvent(CallbackEvent theEvent) {
949+
output("WiFi Static IP: Enter your custom IP address for WiFi shield.");
950+
staticIPAddressTF.clear();
951+
}
952+
});
947953
}
948954

949955
public void setDefaultToDynamicIP() {
@@ -956,15 +962,8 @@ class WifiBox {
956962
}
957963

958964
private void setStaticIPTextfield(String text) {
959-
staticIPAddressTF.getCaptionLabel().setText(text);
965+
staticIPAddressTF.setText(text);
960966
}
961-
962-
//Clear text field on double-click
963-
CallbackListener cb = new CallbackListener() {
964-
public void controlEvent(CallbackEvent theEvent) {
965-
staticIPAddressTF.clear();
966-
}
967-
};
968967
};
969968

970969
class InterfaceBoxCyton {
@@ -2787,6 +2786,7 @@ class InitBox {
27872786
controlPanel.dataLogBoxGanglion.setSessionTextfieldText(directoryManager.getFileNameDateTime());
27882787
controlPanel.dataLogBoxGalea.setSessionTextfieldText(directoryManager.getFileNameDateTime());
27892788
controlPanel.wifiBox.setStaticIPTextfield(wifi_ipAddress);
2789+
w_focus.killAuditoryFeedback();
27902790
haltSystem();
27912791
}
27922792
}

OpenBCI_GUI/DataProcessing.pde

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ void processNewData() {
6464
}
6565
}
6666

67-
void initializeFFTObjects(FFT[] fftBuff, float[][] dataProcessingRawBuffer, int Nfft, float fs_Hz) {
67+
void initializeFFTObjects(ddf.minim.analysis.FFT[] fftBuff, float[][] dataProcessingRawBuffer, int Nfft, float fs_Hz) {
6868

6969
float[] fooData;
7070
for (int Ichan=0; Ichan < nchan; Ichan++) {
7171
//make the FFT objects...Following "SoundSpectrum" example that came with the Minim library
72-
fftBuff[Ichan].window(FFT.HAMMING);
72+
fftBuff[Ichan].window(ddf.minim.analysis.FFT.HAMMING);
7373

7474
//do the FFT on the initial data
7575
if (isFFTFiltered == true) {
@@ -237,7 +237,7 @@ class DataProcessing {
237237
}
238238
}
239239

240-
public void process(float[][] data_forDisplay_uV, FFT[] fftData) { //holds the FFT (frequency spectrum) of the latest data
240+
public void process(float[][] data_forDisplay_uV, ddf.minim.analysis.FFT[] fftData) { //holds the FFT (frequency spectrum) of the latest data
241241

242242
float prevFFTdata[] = new float[fftBuff[0].specSize()];
243243

OpenBCI_GUI/Info.plist.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<key>CFBundleShortVersionString</key>
2424
<string>5</string>
2525
<key>CFBundleVersion</key>
26-
<string>5.0.5</string>
26+
<string>5.0.6</string>
2727
<key>CFBundleSignature</key>
2828
<string>????</string>
2929
<key>NSHumanReadableCopyright</key>
@@ -32,7 +32,7 @@
3232
Copyright © 2021 OpenBCI
3333
</string>
3434
<key>CFBundleGetInfoString</key>
35-
<string>May 2021</string>
35+
<string>July 2021</string>
3636
<!-- End of the set that can be customized -->
3737

3838
@@jvm_runtime@@

OpenBCI_GUI/Interactivity.pde

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,9 @@ void parseKey(char val) {
221221
break;
222222
}
223223
}
224-
225-
if (currentBoard instanceof BoardGanglion) {
224+
225+
// Fixes #976. These keyboard shortcuts enable synthetic square waves on Ganglion and Cyton
226+
if (currentBoard instanceof BoardGanglion || currentBoard instanceof BoardCyton) {
226227
if (val == '[' || val == ']') {
227228
println("Expert Mode: '" + val + "' pressed. Sending to Ganglion...");
228229
Boolean success = ((Board)currentBoard).sendCommand(str(val)).getKey();

0 commit comments

Comments
 (0)