├── .gitignore ├── screenshot.jpg ├── BrainGrapher ├── code │ └── controlP5.jar ├── Point.pde ├── Channel.pde ├── ConnectionLight.pde ├── Monitor.pde ├── BrainGrapher.pde └── Graph.pde └── README.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephfrazier/BrainGrapher/master/screenshot.jpg -------------------------------------------------------------------------------- /BrainGrapher/code/controlP5.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephfrazier/BrainGrapher/master/BrainGrapher/code/controlP5.jar -------------------------------------------------------------------------------- /BrainGrapher/Point.pde: -------------------------------------------------------------------------------- 1 | class Point { 2 | // Value object class to store time / value tuple. 3 | // One instance per data point per channel. 4 | long time; 5 | int value; 6 | 7 | Point(long _time, int _value) { 8 | time = _time; 9 | value = _value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BrainGrapher/Channel.pde: -------------------------------------------------------------------------------- 1 | class Channel { 2 | // Value object class to store EEG power information for each channel. 3 | // One instance per EEG channel. 4 | 5 | String name; 6 | int drawColor; 7 | String description; 8 | boolean graphMe; 9 | boolean relative; 10 | int maxValue; 11 | int minValue; 12 | ArrayList points; 13 | boolean allowGlobal; 14 | 15 | Channel(String _name, int _drawColor, String _description) { 16 | name = _name; 17 | drawColor = _drawColor; 18 | description = _description; 19 | allowGlobal = true; 20 | points = new ArrayList(); 21 | } 22 | 23 | void addDataPoint(int value) { 24 | long time = System.currentTimeMillis(); 25 | 26 | if (value > maxValue) maxValue = value; 27 | if (value < minValue) minValue = value; 28 | 29 | points.add(new Point(time, value)); 30 | } 31 | 32 | Point getLatestPoint() { 33 | if (points.size() > 0) { 34 | return (Point)points.get(points.size() - 1); 35 | } 36 | else { 37 | return new Point(0, 0); 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /BrainGrapher/ConnectionLight.pde: -------------------------------------------------------------------------------- 1 | class ConnectionLight { 2 | // View class to display EEG connection strength. 3 | // Used as a singleton. 4 | 5 | int x, y, diameter, latestConnectionValue; 6 | int currentColor = 0; 7 | int goodColor = color(0, 255, 0); 8 | int badColor = color(255, 255, 0); 9 | int noColor = color(255, 0, 0); 10 | Textlabel label; 11 | Textlabel packetsRecievedLabel; 12 | 13 | ConnectionLight(int _x, int _y, int _diameter) { 14 | x = _x; 15 | y = _y; 16 | diameter = _diameter; 17 | 18 | // Set up the text label 19 | label = new Textlabel(controlP5, "CONNECTION QUALITY", 32, 11, 200, 30); 20 | label.setMultiline(true); 21 | label.setColorValue(color(0)); 22 | 23 | packetsRecievedLabel = new Textlabel(controlP5, "PACKETS RECEIVED: 0", 5, 35, 200, 30); 24 | packetsRecievedLabel.setMultiline(false); 25 | packetsRecievedLabel.setColorValue(color(0)); 26 | } 27 | 28 | void update() { 29 | // Show red if no packets yet 30 | if (channels[0].points.size() == 0) { 31 | latestConnectionValue = 200; 32 | } 33 | else { 34 | latestConnectionValue = channels[0].getLatestPoint().value; 35 | } 36 | 37 | if (latestConnectionValue == 200) currentColor = noColor; 38 | if (latestConnectionValue < 200) currentColor = badColor; 39 | if (latestConnectionValue == 00) currentColor = goodColor; 40 | 41 | packetsRecievedLabel.setText("PACKETS RECIEVED: " + packetCount); 42 | 43 | } 44 | 45 | void draw() { 46 | pushMatrix(); 47 | translate(x, y); 48 | 49 | noStroke(); 50 | fill(255, 150); 51 | rect(0, 0, 132, 50); 52 | 53 | noStroke(); 54 | fill(currentColor); 55 | ellipseMode(CORNER); 56 | ellipse(5, 4, diameter, diameter); 57 | 58 | label.draw(); 59 | packetsRecievedLabel.draw(); 60 | popMatrix(); 61 | } 62 | } -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ##Processing Brain Grapher 2 | 3 | ####Overview 4 | 5 | 6 | 7 | This is a simple Processing application for graphing changes in brain waves over time. It's designed to read data from a hacked MindFlex EEG headset connected via USB. 8 | 9 | It's mostly a proof of concept, demonstrating how to parse serial packets from the Arduino Brain Library, monitor signal strength, etc. 10 | 11 | `BrainGrapher.pde` is the main project file. Open this in the Processing PDE to work with the project. 12 | 13 | You may need to modfiy the index value in the line `serial = new Serial(this, Serial.list()[0], 9600);` inside the app's `setup()` function file depending on which serial / USB port your Arduino is connected to. (Try ` Serial.list()[1]`, ` Serial.list()[2]`, ` Serial.list()[3]`, etc.) 14 | 15 | ####Repository Rename 16 | This project was formerly “Processing-Brain-Grapher” on GitHub, but was renamed to just “BrainGrapher” in 2014 for simplicity's sake. 17 | 18 | ####Dependencies 19 | - The core Processing project. Tested with [Processing 3.0.2](http://processing.org/download/). 20 | 21 | - Version 2.2.5 of the [ControlP5 GUI Library](http://www.sojamo.de/libraries/controlP5/) is included with this project in the `/code` folder. No installation is necessary. 22 | 23 | - If you're using this with a hacked MindFlex, you'll need the [Arduino Brain Library](https://github.com/kitschpatrol/Brain) installed and running on your Arduino. Additional instructions at [frontiernerds.com/brain-hack]](http://www.frontiernerds.com/brain-hack). 24 | 25 | 26 | ####Colophon 27 | Created by Eric Mika at NYU ITP in the spring of 2010. Revised in Spring 2012 to keep up with Processing and ControlP5 updates. Updated in early 2014 with bundled dependencies and more fixes for Control P5. Update in Spring 2016 with Processing 3 support. 28 | 29 | ####Contact 30 | Eric Mika 31 | eric@ericmika.com 32 | [https://github.com/kitschpatrol](https://github.com/kitschpatrol) 33 | [http://frontiernerds.com](http://frontiernerds.com) 34 | -------------------------------------------------------------------------------- /BrainGrapher/Monitor.pde: -------------------------------------------------------------------------------- 1 | class Monitor { 2 | // View class showing a bar-graph of each channel's 3 | // One instance per EEG channel. 4 | 5 | int x, y, w, h, currentValue, targetValue, backgroundColor; 6 | Channel sourceChannel; 7 | CheckBox showGraph; 8 | Textlabel label; 9 | Toggle toggle; 10 | 11 | Monitor(Channel _sourceChannel, int _x, int _y, int _w, int _h) { 12 | sourceChannel = _sourceChannel; 13 | x = _x; 14 | y = _y; 15 | w = _w; 16 | h = _h; 17 | currentValue = 0; 18 | backgroundColor = color(255); 19 | 20 | // Create GUI 21 | showGraph = controlP5.addCheckBox("showGraph" + sourceChannel.name, x + 16, y + 32); 22 | showGraph.addItem("GRAPH" + sourceChannel.name, 0); 23 | showGraph.activate(1); 24 | showGraph.setColorForeground(sourceChannel.drawColor); 25 | showGraph.setColorActive(color(180)); 26 | showGraph.setColorBackground(color(0)); 27 | 28 | toggle = showGraph.getItem(0); 29 | toggle.setLabel("GRAPH"); 30 | 31 | label = new Textlabel(controlP5, sourceChannel.name.toUpperCase(), x + 12, y + 15); 32 | label.setColorValue(0); 33 | } 34 | 35 | void update() { 36 | sourceChannel.graphMe = (showGraph.getItem(0).getValue() == 0); 37 | } 38 | 39 | void draw() { 40 | pushMatrix(); 41 | translate(x, y); 42 | 43 | // Background 44 | noStroke(); 45 | fill(backgroundColor); 46 | rect(0, 0, w, h); 47 | 48 | // Border line 49 | strokeWeight(1); 50 | stroke(220); 51 | line(w - 1, 0, w - 1, h); 52 | 53 | // Bar graph 54 | if (sourceChannel.points.size() > 0) { 55 | Point targetPoint = (Point)sourceChannel.points.get(sourceChannel.points.size() - 1); 56 | targetValue = round(map(targetPoint.value, sourceChannel.minValue, sourceChannel.maxValue, 0, h)); 57 | 58 | if ((scaleMode == "Global") && sourceChannel.allowGlobal) { 59 | targetValue = (int)map(targetPoint.value, 0, globalMax, 0, h); 60 | } 61 | 62 | // Calculate the new position on the way to the target with easing 63 | currentValue = currentValue + round(((float)(targetValue - currentValue) * .08)); 64 | 65 | // Bar 66 | noStroke(); 67 | fill(sourceChannel.drawColor); 68 | rect(0, h - currentValue, w, h); 69 | } 70 | 71 | // Draw the checkbox matte 72 | noStroke(); 73 | fill(240, 150); 74 | rect(10, 10, w - 20, 40); 75 | 76 | popMatrix(); 77 | label.draw(); 78 | } 79 | } -------------------------------------------------------------------------------- /BrainGrapher/BrainGrapher.pde: -------------------------------------------------------------------------------- 1 | // Main controller / model file for the the Processing Brain Grapher. 2 | 3 | // See README.markdown for more info. 4 | // See http://frontiernerds.com/brain-hack for a tutorial on getting started with the Arduino Brain Library and this Processing Brain Grapher. 5 | 6 | // Latest source code is on https://github.com/kitschpatrol/Processing-Brain-Grapher 7 | // Created by Eric Mika in Fall 2010, updates Spring 2012, early 2014, and Spring 2016. 8 | 9 | import processing.serial.*; 10 | import controlP5.*; 11 | 12 | ControlP5 controlP5; 13 | 14 | Serial serial; 15 | 16 | Channel[] channels = new Channel[11]; 17 | Monitor[] monitors = new Monitor[10]; 18 | Graph graph; 19 | ConnectionLight connectionLight; 20 | 21 | int packetCount = 0; 22 | int globalMax = 0; 23 | String scaleMode; 24 | 25 | void setup() { 26 | // Set up window 27 | size(1024, 768); 28 | frameRate(60); 29 | smooth(); 30 | surface.setTitle("Processing Brain Grapher"); 31 | 32 | // Set up serial connection 33 | println("Find your Arduino in the list below, note its [index]:\n"); 34 | 35 | for (int i = 0; i < Serial.list().length; i++) { 36 | println("[" + i + "] " + Serial.list()[i]); 37 | } 38 | 39 | // Put the index found above here: 40 | serial = new Serial(this, Serial.list()[0], 9600); 41 | 42 | serial.bufferUntil(10); 43 | 44 | // Set up the ControlP5 knobs and dials 45 | controlP5 = new ControlP5(this); 46 | 47 | controlP5.setColorValueLabel(color(0)); 48 | controlP5.setColorCaptionLabel(color(0)); 49 | controlP5.setColorBackground(color(0)); 50 | controlP5.disableShortcuts(); 51 | controlP5.setMouseWheelRotation(0); 52 | controlP5.setMoveable(false); 53 | 54 | // Create the channel objects 55 | channels[0] = new Channel("Signal Quality", color(0), ""); 56 | channels[1] = new Channel("Attention", color(100), ""); 57 | channels[2] = new Channel("Meditation", color(50), ""); 58 | channels[3] = new Channel("Delta", color(219, 211, 42), "Dreamless Sleep"); 59 | channels[4] = new Channel("Theta", color(245, 80, 71), "Drowsy"); 60 | channels[5] = new Channel("Low Alpha", color(237, 0, 119), "Relaxed"); 61 | channels[6] = new Channel("High Alpha", color(212, 0, 149), "Relaxed"); 62 | channels[7] = new Channel("Low Beta", color(158, 18, 188), "Alert"); 63 | channels[8] = new Channel("High Beta", color(116, 23, 190), "Alert"); 64 | channels[9] = new Channel("Low Gamma", color(39, 25, 159), "Multi-sensory processing"); 65 | channels[10] = new Channel("High Gamma", color(23, 26, 153), "???"); 66 | 67 | // Manual override for a couple of limits. 68 | channels[0].minValue = 0; 69 | channels[0].maxValue = 200; 70 | channels[1].minValue = 0; 71 | channels[1].maxValue = 100; 72 | channels[2].minValue = 0; 73 | channels[2].maxValue = 100; 74 | channels[0].allowGlobal = false; 75 | channels[1].allowGlobal = false; 76 | channels[2].allowGlobal = false; 77 | 78 | // Set up the monitors, skip the signal quality 79 | for (int i = 0; i < monitors.length; i++) { 80 | monitors[i] = new Monitor(channels[i + 1], i * (width / 10), height / 2, width / 10, height / 2); 81 | } 82 | 83 | monitors[monitors.length - 1].w += width % monitors.length; 84 | 85 | // Set up the graph 86 | graph = new Graph(0, 0, width, height / 2); 87 | 88 | // Set yup the connection light 89 | connectionLight = new ConnectionLight(width - 140, 10, 20); 90 | } 91 | 92 | void draw() { 93 | // Keep track of global maxima 94 | if (scaleMode == "Global" && (channels.length > 3)) { 95 | for (int i = 3; i < channels.length; i++) { 96 | if (channels[i].maxValue > globalMax) globalMax = channels[i].maxValue; 97 | } 98 | } 99 | 100 | // Clear the background 101 | background(255); 102 | 103 | // Update and draw the main graph 104 | graph.update(); 105 | graph.draw(); 106 | 107 | // Update and draw the connection light 108 | connectionLight.update(); 109 | connectionLight.draw(); 110 | 111 | // Update and draw the monitors 112 | for (int i = 0; i < monitors.length; i++) { 113 | monitors[i].update(); 114 | monitors[i].draw(); 115 | } 116 | } 117 | 118 | void serialEvent(Serial p) { 119 | // Split incoming packet on commas 120 | // See https://github.com/kitschpatrol/Arduino-Brain-Library/blob/master/README for information on the CSV packet format 121 | 122 | String incomingString = p.readString().trim(); 123 | print("Received string over serial: "); 124 | println(incomingString); 125 | 126 | String[] incomingValues = split(incomingString, ','); 127 | 128 | // Verify that the packet looks legit 129 | if (incomingValues.length > 1) { 130 | packetCount++; 131 | 132 | // Wait till the third packet or so to start recording to avoid initialization garbage. 133 | if (packetCount > 3) { 134 | 135 | for (int i = 0; i < incomingValues.length; i++) { 136 | String stringValue = incomingValues[i].trim(); 137 | 138 | int newValue = Integer.parseInt(stringValue); 139 | 140 | // Zero the EEG power values if we don't have a signal. 141 | // Can be useful to leave them in for development. 142 | if ((Integer.parseInt(incomingValues[0]) == 200) && (i > 2)) { 143 | newValue = 0; 144 | } 145 | 146 | channels[i].addDataPoint(newValue); 147 | } 148 | } 149 | } 150 | } 151 | 152 | 153 | // Utilities 154 | 155 | // Extend Processing's built-in map() function to support the Long datatype 156 | long mapLong(long x, long in_min, long in_max, long out_min, long out_max) { 157 | return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 158 | } 159 | 160 | // Extend Processing's built-in constrain() function to support the Long datatype 161 | long constrainLong(long value, long min_value, long max_value) { 162 | if (value > max_value) return max_value; 163 | if (value < min_value) return min_value; 164 | return value; 165 | } -------------------------------------------------------------------------------- /BrainGrapher/Graph.pde: -------------------------------------------------------------------------------- 1 | class Graph { 2 | // View class to draw a graph of the channel model's values over time. 3 | // Used as a singleton. 4 | 5 | int x, y, w, h, pixelsPerSecond, gridColor, gridX, originalW, originalX; 6 | long leftTime, rightTime, gridTime; 7 | boolean scrollGrid; 8 | String renderMode; 9 | float gridSeconds; 10 | Slider pixelSecondsSlider; 11 | RadioButton renderModeRadio; 12 | RadioButton scaleRadio; 13 | 14 | Graph(int _x, int _y, int _w, int _h) { 15 | x = _x; 16 | y = _y; 17 | w = _w; 18 | h = _h; 19 | 20 | pixelsPerSecond = 50; 21 | gridColor = color(0); 22 | gridSeconds = 1; // seconds per grid line 23 | scrollGrid = false; 24 | 25 | originalW = w; 26 | originalX = x; 27 | 28 | // Set up GUI controls 29 | pixelSecondsSlider = controlP5.addSlider("PIXELS PER SECOND", 10, width, 50, 16, 16, 100, 10); 30 | pixelSecondsSlider.setColorForeground(color(180)); 31 | pixelSecondsSlider.setColorActive(color(180)); 32 | pixelSecondsSlider.setColorValueLabel(color(255)); 33 | 34 | renderModeRadio = controlP5.addRadioButton("RENDER MODE", 16, 36); 35 | renderModeRadio.setColorForeground(color(255)); 36 | renderModeRadio.setColorActive(color(0)); 37 | renderModeRadio.setColorBackground(color(180)); 38 | renderModeRadio.setSpacingRow(4); 39 | renderModeRadio.addItem("Lines", 1); 40 | renderModeRadio.addItem("Curves", 2); 41 | renderModeRadio.addItem("Shaded", 3); 42 | renderModeRadio.addItem("Triangles", 4); 43 | 44 | renderModeRadio.activate(0); 45 | 46 | scaleRadio = controlP5.addRadioButton("SCALE MODE", 104, 36); 47 | scaleRadio.setColorForeground(color(255)); 48 | scaleRadio.setColorActive(color(0)); 49 | scaleRadio.setColorBackground(color(180)); 50 | scaleRadio.setSpacingRow(4); 51 | scaleRadio.addItem("Local Maximum", 1); 52 | scaleRadio.addItem("Global Maximum", 2); 53 | scaleRadio.activate(0); 54 | } 55 | 56 | void update() { 57 | // Set pixels per second from GUI slider 58 | pixelsPerSecond = round(pixelSecondsSlider.getValue()); 59 | 60 | // Set render mode from GUI radio buttons 61 | switch (round(renderModeRadio.getValue())) { 62 | case 1: 63 | renderMode = "Lines"; 64 | break; 65 | case 2: 66 | renderMode = "Curves"; 67 | break; 68 | case 3: 69 | renderMode = "Shaded"; 70 | break; 71 | case 4: 72 | renderMode = "Triangles"; 73 | break; 74 | } 75 | 76 | // Set scale mode from GUI radio buttons 77 | switch(round(scaleRadio.getValue())) { 78 | case 1: 79 | scaleMode = "Local"; 80 | break; 81 | case 2: 82 | scaleMode = "Global"; 83 | break; 84 | } 85 | 86 | // Smooth drawing kludge 87 | w = originalW; 88 | x = originalX; 89 | 90 | w += (pixelsPerSecond * 2); 91 | x -= pixelsPerSecond; 92 | 93 | // Figure out the left and right time bounds of the graph, based on 94 | // the pixels per second value 95 | rightTime = System.currentTimeMillis(); 96 | leftTime = rightTime - ((w / pixelsPerSecond) * 1000); 97 | } 98 | 99 | void draw() { 100 | pushMatrix(); 101 | translate(x, y); 102 | 103 | // Background 104 | 105 | 106 | 107 | fill(220); 108 | rect(0, 0, w, h); 109 | 110 | // Draw the background graph paper grid 111 | strokeWeight(1); 112 | stroke(255); 113 | 114 | if (scrollGrid) { 115 | // Start from the first whole second and work right 116 | gridTime = (rightTime / (long)(1000 * gridSeconds)) * (long)(1000 * gridSeconds); 117 | } 118 | else { 119 | gridTime = rightTime; 120 | } 121 | 122 | while (gridTime >= leftTime) { 123 | int gridX = (int)mapLong(gridTime, leftTime, rightTime, 0L, (long)w); 124 | line(gridX, 0, gridX, h); 125 | gridTime -= (long)(1000 * gridSeconds); 126 | } 127 | 128 | // Draw square horizontal grid for now 129 | int gridY = h; 130 | while (gridY >= 0) { 131 | gridY -= pixelsPerSecond * gridSeconds; 132 | line(0, gridY, w, gridY); 133 | } 134 | 135 | // Draw each channel 136 | noFill(); 137 | if (renderMode == "Shaded" || renderMode == "Triangles") noStroke(); 138 | if (renderMode == "Curves" || renderMode == "Lines") strokeWeight(2); 139 | 140 | for (int i = 0; i < channels.length; i++) { 141 | Channel thisChannel = channels[i]; 142 | 143 | if (thisChannel.graphMe) { 144 | // Draw the graph line 145 | if (renderMode == "Lines" || renderMode == "Curves") stroke(thisChannel.drawColor); 146 | 147 | if (renderMode == "Shaded" || renderMode == "Triangles") { 148 | noStroke(); 149 | fill(thisChannel.drawColor, 120); 150 | } 151 | 152 | if (renderMode == "Triangles") { 153 | beginShape(TRIANGLES); 154 | } 155 | else { 156 | beginShape(); 157 | } 158 | 159 | if (renderMode == "Curves" || renderMode == "Shaded") vertex(0, h); 160 | 161 | for (int j = 0; j < thisChannel.points.size(); j++) { 162 | Point thisPoint = (Point)thisChannel.points.get(j); 163 | 164 | // check bounds 165 | if ((thisPoint.time >= leftTime) && (thisPoint.time <= rightTime)) { 166 | 167 | int pointX = (int)mapLong(thisPoint.time, leftTime, rightTime, 0L, (long)w); 168 | int pointY = 0; 169 | 170 | if ((scaleMode == "Global") && (i > 2)) { 171 | // Global scale 172 | pointY = (int)map(thisPoint.value, 0, globalMax, h, 0); 173 | } 174 | else { 175 | // Local scale 176 | pointY = (int)map(thisPoint.value, thisChannel.minValue, thisChannel.maxValue, h, 0); 177 | } 178 | 179 | if (renderMode == "Curves") { 180 | curveVertex(pointX, pointY); 181 | } 182 | else { 183 | vertex(pointX, pointY); 184 | } 185 | } 186 | } 187 | } 188 | 189 | if (renderMode == "Curves" || renderMode == "Shaded") vertex(w, h); 190 | if (renderMode == "Lines" || renderMode == "Curves" || renderMode == "Triangles") endShape(); 191 | if (renderMode == "Shaded") endShape(CLOSE); 192 | } 193 | 194 | popMatrix(); 195 | 196 | // GUI background matte 197 | noStroke(); 198 | fill(255, 150); 199 | rect(10, 10, 195, 81); 200 | } 201 | } --------------------------------------------------------------------------------