From cb839777cf889c24a07c9a1f9817a4a761d29ea4 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Wed, 8 Feb 2023 17:16:15 -0600 Subject: [PATCH 1/8] Refactor creation and playback of OpenBCI CSV files Supports Cyton mode only in this PR --- OpenBCI_GUI/DataLogger.pde | 20 +- OpenBCI_GUI/DataSourcePlayback.pde | 304 +++------- OpenBCI_GUI/DataSourcePlaybackCyton.pde | 109 ++++ OpenBCI_GUI/DataWriterAuxODF.pde | 71 +-- OpenBCI_GUI/DataWriterODF.pde | 152 ++--- OpenBCI_GUI/Extras.pde | 80 ++- OpenBCI_GUI/FileBoard.pde | 28 +- OpenBCI_GUI/OpenBCI_GUI.pde | 7 +- OpenBCI_GUI/SignalCheckThresholds.pde | 284 ++++----- OpenBCI_GUI/W_AnalogRead.pde | 7 +- OpenBCI_GUI/W_DigitalRead.pde | 7 +- OpenBCI_GUI/W_PulseSensor.pde | 765 ++++++++++++------------ 12 files changed, 928 insertions(+), 906 deletions(-) create mode 100644 OpenBCI_GUI/DataSourcePlaybackCyton.pde diff --git a/OpenBCI_GUI/DataLogger.pde b/OpenBCI_GUI/DataLogger.pde index 8dc022891..968c8eab3 100644 --- a/OpenBCI_GUI/DataLogger.pde +++ b/OpenBCI_GUI/DataLogger.pde @@ -145,15 +145,19 @@ class DataLogger { closeLogFile(); } //open the new file - fileWriterODF = new DataWriterODF(sessionName, _fileName); - if (currentBoard instanceof AuxDataBoard) { - if (fileWriterAuxODF != null) - fileWriterAuxODF.closeFile(); - fileWriterAuxODF = new DataWriterAuxODF(sessionName, _fileName); - } + try { + fileWriterODF = new DataWriterODF(sessionName, _fileName); + if (currentBoard instanceof AuxDataBoard) { + if (fileWriterAuxODF != null) + fileWriterAuxODF.closeFile(); + fileWriterAuxODF = new DataWriterAuxODF(sessionName, _fileName); + } - output_fname = fileWriterODF.fname; - println("OpenBCI_GUI: openNewLogFile: opened ODF output file: " + output_fname); //Print filename of new ODF file to console + output_fname = fileWriterODF.fname; + println("OpenBCI_GUI: openNewLogFile: opened ODF output file: " + output_fname); //Print filename of new ODF file to console + } catch (Exception e) { + e.printStackTrace(); + } } private void closeLogFile() { diff --git a/OpenBCI_GUI/DataSourcePlayback.pde b/OpenBCI_GUI/DataSourcePlayback.pde index 31c372700..67b323cb7 100644 --- a/OpenBCI_GUI/DataSourcePlayback.pde +++ b/OpenBCI_GUI/DataSourcePlayback.pde @@ -1,35 +1,34 @@ -class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, AnalogCapableBoard, DigitalCapableBoard, EDACapableBoard, PPGCapableBoard, BatteryInfoCapableBoard, FileBoard { - private String playbackFilePath; - private ArrayList rawData; - private int currentSample; - private int timeOfLastUpdateMS; +abstract class DataSourcePlayback implements DataSource, FileBoard { + private String playbackFilePathExg; + private ArrayList rawDataExg; + private int currentSampleExg; + private int timeOfLastUpdateMSExg; private String underlyingClassName; - private Integer batteryChannelCache = null; - private int numNewSamplesThisFrame; + private int numNewSamplesThisFrameExg; private boolean initialized = false; private boolean streaming = false; - private Board underlyingBoard = null; - private int sampleRate = -1; - private int numChannels = 0; // use it instead getTotalChannelCount() method for old playback files + public Board underlyingBoard = null; + private int sampleRateExg = -1; + private int numChannelsExg = 0; // use it instead getTotalChannelCount() method for old playback files DataSourcePlayback(String filePath) { - playbackFilePath = filePath; + playbackFilePathExg = filePath; } @Override public boolean initialize() { - currentSample = 0; - String[] lines = loadStrings(playbackFilePath); + currentSampleExg = 0; + String[] lines = loadStrings(playbackFilePathExg); - if(!parseHeader(lines)) { + if(!parseExgHeader(lines)) { return false; } if(!instantiateUnderlyingBoard()) { return false; } - if(!parseData(lines)) { + if(!parseExgData(lines)) { return false; } @@ -41,7 +40,7 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo initialized = false; } - protected boolean parseHeader(String[] lines) { + protected boolean parseExgHeader(String[] lines) { for (String line : lines) { if (!line.startsWith("%")) { break; // reached end of header @@ -61,7 +60,7 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo int endIndex = line.indexOf("Hz") - 1; String hzString = line.substring(startIndex, endIndex); - sampleRate = Integer.parseInt(hzString); + sampleRateExg = Integer.parseInt(hzString); } // used to figure out the underlying board type @@ -71,7 +70,7 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo } } - boolean success = sampleRate > 0 && underlyingClassName != ""; + boolean success = sampleRateExg > 0 && underlyingClassName != ""; if(!success) { outputError("Playback file does not contain the required header data."); } @@ -95,7 +94,7 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo return underlyingBoard != null; } - protected boolean parseData(String[] lines) { + protected boolean parseExgData(String[] lines) { int dataStart; // set data start to first line of data (skip header) for (dataStart = 0; dataStart < lines.length; dataStart++) { @@ -107,21 +106,21 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo } int dataLength = lines.length - dataStart; - rawData = new ArrayList(dataLength); + rawDataExg = new ArrayList(dataLength); for (int iData=0; iData list = getData(numNewSamplesThisFrame); - for (int i = 0; i < numNewSamplesThisFrame; i++) { - for (int j = 0; j < numChannels; j++) { + double[][] array = new double[numChannelsExg][numNewSamplesThisFrameExg]; + List list = getData(numNewSamplesThisFrameExg); + for (int i = 0; i < numNewSamplesThisFrameExg; i++) { + for (int j = 0; j < numChannelsExg; j++) { array[j][i] = list.get(i)[j]; } } @@ -275,14 +244,14 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo @Override public List getData(int maxSamples) { - int firstSample = max(0, currentSample - maxSamples); - List result = rawData.subList(firstSample, currentSample); + int firstSample = max(0, currentSampleExg - maxSamples); + List result = rawDataExg.subList(firstSample, currentSampleExg); // if needed, pad the beginning of the array with empty data - if (maxSamples > currentSample) { - int sampleDiff = maxSamples - currentSample; + if (maxSamples > currentSampleExg) { + int sampleDiff = maxSamples - currentSampleExg; - double[] emptyData = new double[numChannels]; + double[] emptyData = new double[numChannelsExg]; ArrayList newResult = new ArrayList(maxSamples); for (int i=0; i= getTotalSamples(); } - @Override - public void setPPGActive(boolean active) { - // nothing - } +} - @Override - public int[] getPPGChannels() { - if (underlyingBoard instanceof PPGCapableBoard) { - return ((PPGCapableBoard)underlyingBoard).getPPGChannels(); +public DataSourcePlayback getDataSourcePlaybackClassFromFile(String path) { + verbosePrint("Checking " + path + " for underlying board class."); + String strCurrentLine; + int lineCounter = 0; + int maxLinesToCheck = 4; + String infoToCheck = "%Board = "; + String underlyingBoardClassName = ""; + BufferedReader reader = createBufferedReader(path); + try { + while (lineCounter < maxLinesToCheck) { + strCurrentLine = reader.readLine(); + verbosePrint(strCurrentLine); + if (strCurrentLine.startsWith(infoToCheck)) { + String[] splitCurrentLine = split(strCurrentLine, "OpenBCI_GUI$"); + underlyingBoardClassName = splitCurrentLine[1]; + } + lineCounter++; } - - return new int[0]; - } - - @Override - public Integer getBatteryChannel() { - if (batteryChannelCache == null && underlyingBoard instanceof BatteryInfoCapableBoard) { - try { - batteryChannelCache = BoardShim.get_battery_channel(((BoardBrainFlow)underlyingBoard).getBoardIdInt()); - } catch (BrainFlowError e) { - e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (reader != null) { + reader.close(); } + } catch (IOException ex) { + ex.printStackTrace(); } - - return batteryChannelCache; - } - - @Override - public boolean endOfFileReached() { - return currentSample >= getTotalSamples(); - } - - @Override - public List getDataWithBatteryInfo(int maxSamples) { - return getData(maxSamples); - } - - @Override - public List getDataWithAnalog(int maxSamples) { - return getData(maxSamples); } - - @Override - public List getDataWithDigital(int maxSamples) { - return getData(maxSamples); - } - - @Override - public List getDataWithAccel(int maxSamples) { - return getData(maxSamples); - } - - @Override - public List getDataWithPPG(int maxSamples) { - return getData(maxSamples); - } - - @Override - public List getDataWithEDA(int maxSamples) { - return getData(maxSamples); + + switch (underlyingBoardClassName) { + case ("BoardCytonSerial"): + return new DataSourcePlaybackCyton(path); + default: + return null; } - } diff --git a/OpenBCI_GUI/DataSourcePlaybackCyton.pde b/OpenBCI_GUI/DataSourcePlaybackCyton.pde new file mode 100644 index 000000000..21bb120f0 --- /dev/null +++ b/OpenBCI_GUI/DataSourcePlaybackCyton.pde @@ -0,0 +1,109 @@ +class DataSourcePlaybackCyton extends DataSourcePlayback implements AccelerometerCapableBoard, AnalogCapableBoard, DigitalCapableBoard, FileBoard { + + DataSourcePlaybackCyton(String filePath) { + super(filePath); + } + + @Override + public int getAccelSampleRate() { + return getSampleRate(); + } + + @Override + public int getAnalogSampleRate() { + return getSampleRate(); + } + + @Override + public int getDigitalSampleRate() { + return getSampleRate(); + } + + @Override + public boolean isAccelerometerActive() { + return underlyingBoard instanceof AccelerometerCapableBoard; + } + + @Override + public void setAccelerometerActive(boolean active) { + // nothing + } + + @Override + public boolean canDeactivateAccelerometer() { + return false; + } + + @Override + public int[] getAccelerometerChannels() { + if (underlyingBoard instanceof AccelerometerCapableBoard) { + return ((AccelerometerCapableBoard)underlyingBoard).getAccelerometerChannels(); + } + + return new int[0]; + } + + @Override + public boolean isAnalogActive() { + return underlyingBoard instanceof AnalogCapableBoard; + } + + @Override + public void setAnalogActive(boolean active) { + // nothing + } + + @Override + public boolean canDeactivateAnalog() { + return false; + } + + @Override + public int[] getAnalogChannels() { + if (underlyingBoard instanceof AnalogCapableBoard) { + return ((AnalogCapableBoard)underlyingBoard).getAnalogChannels(); + } + + return new int[0]; + } + + @Override + public boolean isDigitalActive() { + return underlyingBoard instanceof DigitalCapableBoard; + } + + @Override + public void setDigitalActive(boolean active) { + // nothing + } + + @Override + public boolean canDeactivateDigital() { + return false; + } + + @Override + public int[] getDigitalChannels() { + if (underlyingBoard instanceof DigitalCapableBoard) { + return ((DigitalCapableBoard)underlyingBoard).getDigitalChannels(); + } + + return new int[0]; + } + + @Override + public List getDataWithAnalog(int maxSamples) { + return getData(maxSamples); + } + + @Override + public List getDataWithDigital(int maxSamples) { + return getData(maxSamples); + } + + @Override + public List getDataWithAccel(int maxSamples) { + return getData(maxSamples); + } + +} diff --git a/OpenBCI_GUI/DataWriterAuxODF.pde b/OpenBCI_GUI/DataWriterAuxODF.pde index 924dcd369..86b552d94 100644 --- a/OpenBCI_GUI/DataWriterAuxODF.pde +++ b/OpenBCI_GUI/DataWriterAuxODF.pde @@ -1,67 +1,30 @@ - -// todo refactor to avoid copypaste with DataWriterODF -//write data to a text file -public class DataWriterAuxODF { - private PrintWriter output; - private String fname; - private int rowsWritten; - private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); - - private AuxDataBoard streamingBoard; +public class DataWriterAuxODF extends DataWriterODF { + protected String fileNamePrependString = "OpenBCI-RAW-Aux-"; + protected String headerFirstLineString = "%OpenBCI Raw Aux Data"; //variation on constructor to have custom name DataWriterAuxODF(String _sessionName, String _fileName) { - streamingBoard = (AuxDataBoard)currentBoard; - fname = settings.getSessionPath(); - fname += "OpenBCI-RAW-Aux-"; - fname += _fileName; - fname += ".txt"; - output = createWriter(fname); //open the file - writeHeader(); //add the header - rowsWritten = 0; //init the counter + super(_sessionName, _fileName); } - - public void writeHeader() { - output.println("%OpenBCI Raw Aux Data"); - output.println("%Number of channels = " + streamingBoard.getNumAuxChannels()); - output.println("%Sample Rate = " + streamingBoard.getAuxSampleRate() + " Hz"); - output.println("%Board = " + streamingBoard.getClass().getName()); - - String[] colNames = streamingBoard.getAuxChannelNames(); - - for (int i=0; i allData = currentBoard.getData(PulseBuffSize); - int[] analogChannels = analogBoard.getAnalogChannels(); - - for (int i=0; i < PulseBuffSize; i++ ) { - int signal = (int)(allData.get(i)[analogChannels[0]]); - //processSignal(signal); - PulseWaveY[i] = signal; - } - - double[][] frameData = currentBoard.getFrameData(); - for (int i = 0; i < frameData[0].length; i++) - { - int signal = (int)(frameData[analogChannels[0]][i]); - processSignal(signal); - } - - //ignore top left button interaction when widgetSelector dropdown is active - List cp5ElementsToCheck = new ArrayList(); - cp5ElementsToCheck.add((controlP5.Controller)analogModeButton); - lockElementsOnOverlapCheck(cp5ElementsToCheck); - - if (!analogBoard.canDeactivateAnalog()) { - analogModeButton.setLock(true); - analogModeButton.getCaptionLabel().setText("Analog Read On"); - analogModeButton.setColorBackground(BUTTON_LOCKED_GREY); - } - } - - private void updateOnOffButton() { - if (analogBoard.isAnalogActive()) { - - analogModeButton.setOn(); - } - else { - - analogModeButton.setOff(); - } - } - - void addBPM(int bpm) { - for(int i=0; i (IBI/5)*3){ // avoid dichrotic noise by waiting 3/5 of last IBI - if (sample < T){ // T is the trough - T = sample; // keep track of lowest point in pulse wave - } - } - - if(sample > thresh && sample > P){ // thresh condition helps avoid noise - P = sample; // P is the peak - } // keep track of highest point in pulse wave - - // NOW IT'S TIME TO LOOK FOR THE HEART BEAT - // signal surges up in value every time there is a pulse - if (N > 250){ // avoid high frequency noise - if ( (sample > thresh) && (Pulse == false) && (N > (IBI/5)*3) ){ - Pulse = true; // set the Pulse flag when we think there is a pulse - IBI = sampleCounter - lastBeatTime; // measure time between beats in mS - lastBeatTime = sampleCounter; // keep track of time for next pulse - - if(secondBeat){ // if this is the second beat, if secondBeat == TRUE - secondBeat = false; // clear secondBeat flag - for(int i=0; i<=9; i++){ // seed the running total to get a realisitic BPM at startup - rate[i] = IBI; - } - } - - if(firstBeat){ // if it's the first time we found a beat, if firstBeat == TRUE - firstBeat = false; // clear firstBeat flag - secondBeat = true; // set the second beat flag - // sei(); // enable interrupts again - return; // IBI value is unreliable so discard it - } - - - // keep a running total of the last 10 IBI values - int runningTotal = 0; // clear the runningTotal variable - - for(int i=0; i<=8; i++){ // shift data in the rate array - rate[i] = rate[i+1]; // and drop the oldest IBI value - runningTotal += rate[i]; // add up the 9 oldest IBI values - } - - rate[9] = IBI; // add the latest IBI to the rate array - runningTotal += rate[9]; // add the latest IBI to runningTotal - runningTotal /= 10; // average the last 10 IBI values - BPM = 60000/runningTotal; // how many beats can fit into a minute? that's BPM! - BPM = constrain(BPM,0,200); - addBPM(BPM); - } - } - - if (sample < thresh && Pulse == true){ // when the values are going down, the beat is over - // digitalWrite(blinkPin,LOW); // turn off pin 13 LED - Pulse = false; // reset the Pulse flag so we can do it again - amp = P - T; // get amplitude of the pulse wave - thresh = amp/2 + T; // set thresh at 50% of the amplitude - P = thresh; // reset these for next time - T = thresh; - } - - if (N > 2500){ // if 2.5 seconds go by without a beat - thresh = 530; // set thresh default - P = 512; // set P default - T = 512; // set T default - lastBeatTime = sampleCounter; // bring the lastBeatTime up to date - firstBeat = true; // set these to avoid noise - secondBeat = false; // when we get the heartbeat back - } - - // sei(); // enable interrupts when youre done! - }// end processSignal - - -}; + +//////////////////////////////////////////////////// +// +// W_PulseSensor.pde +// +// Created: Joel Murphy, Spring 2017 +// +///////////////////////////////////////////////////, + +class W_PulseSensor extends Widget { + + //to see all core variables/methods of the Widget class, refer to Widget.pde + //put your custom variables here... + + + color graphStroke = #d2d2d2; + color graphBG = #f5f5f5; + color textColor = #000000; + +// Pulse Sensor Visualizer Stuff + int count = 0; + int heart = 0; + int PulseBuffSize = 3*currentBoard.getSampleRate(); // Originally 400 + int BPMbuffSize = 100; + + int PulseWindowWidth; + int PulseWindowHeight; + int PulseWindowX; + int PulseWindowY; + int BPMwindowWidth; + int BPMwindowHeight; + int BPMwindowX; + int BPMwindowY; + int BPMposX; + int BPMposY; + int IBIposX; + int IBIposY; + int padding = 15; + color eggshell; + color pulseWave; + int[] PulseWaveY; // HOLDS HEARTBEAT WAVEFORM DATA + int[] BPMwaveY; // HOLDS BPM WAVEFORM DATA + boolean rising; + + // Synthetic Wave Generator Stuff + float theta; // Start angle at 0 + float amplitude; // Height of wave + int syntheticMultiplier; + long thisTime; + long thatTime; + int refreshRate; + + // Pulse Sensor Beat Finder Stuff + // ASSUMES 250Hz SAMPLE RATE + int[] rate; // array to hold last ten IBI values + int sampleCounter; // used to determine pulse timing + int lastBeatTime; // used to find IBI + int P =512; // used to find peak in pulse wave, seeded + int T = 512; // used to find trough in pulse wave, seeded + int thresh = 530; // used to find instant moment of heart beat, seeded + int amp = 0; // used to hold amplitude of pulse waveform, seeded + boolean firstBeat = true; // used to seed rate array so we startup with reasonable BPM + boolean secondBeat = false; // used to seed rate array so we startup with reasonable BPM + int BPM; // int that holds raw Analog in 0. updated every 2mS + int Signal; // holds the incoming raw data + int IBI = 600; // int that holds the time interval between beats! Must be seeded! + boolean Pulse = false; // "True" when User's live heartbeat is detected. "False" when not a "live beat". + int lastProcessedDataPacketInd = 0; + private Button analogModeButton; + + private AnalogCapableBoard analogBoard; + + W_PulseSensor(PApplet _parent){ + super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + + analogBoard = (AnalogCapableBoard)currentBoard; + + // Pulse Sensor Stuff + eggshell = color(255, 253, 248); + pulseWave = BOLD_RED; + + PulseWaveY = new int[PulseBuffSize]; + BPMwaveY = new int[BPMbuffSize]; + rate = new int[10]; + setPulseWidgetVariables(); + initializePulseFinderVariables(); + + createAnalogModeButton("pulseSensorAnalogModeButton", "Turn Analog Read On", (int)(x0 + 1), (int)(y0 + navHeight + 1), 128, navHeight - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + } + + void update(){ + super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + + if(currentBoard instanceof DataSourcePlayback) { + if (((DataSourcePlayback)currentBoard) instanceof AnalogCapableBoard + && (!((AnalogCapableBoard)currentBoard).isAnalogActive())) { + return; + } + } + + List allData = currentBoard.getData(PulseBuffSize); + int[] analogChannels = analogBoard.getAnalogChannels(); + + for (int i=0; i < PulseBuffSize; i++ ) { + int signal = (int)(allData.get(i)[analogChannels[0]]); + //processSignal(signal); + PulseWaveY[i] = signal; + } + + double[][] frameData = currentBoard.getFrameData(); + for (int i = 0; i < frameData[0].length; i++) + { + int signal = (int)(frameData[analogChannels[0]][i]); + processSignal(signal); + } + + //ignore top left button interaction when widgetSelector dropdown is active + List cp5ElementsToCheck = new ArrayList(); + cp5ElementsToCheck.add((controlP5.Controller)analogModeButton); + lockElementsOnOverlapCheck(cp5ElementsToCheck); + + if (!analogBoard.canDeactivateAnalog()) { + analogModeButton.setLock(true); + analogModeButton.getCaptionLabel().setText("Analog Read On"); + analogModeButton.setColorBackground(BUTTON_LOCKED_GREY); + } + } + + private void updateOnOffButton() { + if (analogBoard.isAnalogActive()) { + + analogModeButton.setOn(); + } + else { + + analogModeButton.setOff(); + } + } + + void addBPM(int bpm) { + for(int i=0; i (IBI/5)*3){ // avoid dichrotic noise by waiting 3/5 of last IBI + if (sample < T){ // T is the trough + T = sample; // keep track of lowest point in pulse wave + } + } + + if(sample > thresh && sample > P){ // thresh condition helps avoid noise + P = sample; // P is the peak + } // keep track of highest point in pulse wave + + // NOW IT'S TIME TO LOOK FOR THE HEART BEAT + // signal surges up in value every time there is a pulse + if (N > 250){ // avoid high frequency noise + if ( (sample > thresh) && (Pulse == false) && (N > (IBI/5)*3) ){ + Pulse = true; // set the Pulse flag when we think there is a pulse + IBI = sampleCounter - lastBeatTime; // measure time between beats in mS + lastBeatTime = sampleCounter; // keep track of time for next pulse + + if(secondBeat){ // if this is the second beat, if secondBeat == TRUE + secondBeat = false; // clear secondBeat flag + for(int i=0; i<=9; i++){ // seed the running total to get a realisitic BPM at startup + rate[i] = IBI; + } + } + + if(firstBeat){ // if it's the first time we found a beat, if firstBeat == TRUE + firstBeat = false; // clear firstBeat flag + secondBeat = true; // set the second beat flag + // sei(); // enable interrupts again + return; // IBI value is unreliable so discard it + } + + + // keep a running total of the last 10 IBI values + int runningTotal = 0; // clear the runningTotal variable + + for(int i=0; i<=8; i++){ // shift data in the rate array + rate[i] = rate[i+1]; // and drop the oldest IBI value + runningTotal += rate[i]; // add up the 9 oldest IBI values + } + + rate[9] = IBI; // add the latest IBI to the rate array + runningTotal += rate[9]; // add the latest IBI to runningTotal + runningTotal /= 10; // average the last 10 IBI values + BPM = 60000/runningTotal; // how many beats can fit into a minute? that's BPM! + BPM = constrain(BPM,0,200); + addBPM(BPM); + } + } + + if (sample < thresh && Pulse == true){ // when the values are going down, the beat is over + // digitalWrite(blinkPin,LOW); // turn off pin 13 LED + Pulse = false; // reset the Pulse flag so we can do it again + amp = P - T; // get amplitude of the pulse wave + thresh = amp/2 + T; // set thresh at 50% of the amplitude + P = thresh; // reset these for next time + T = thresh; + } + + if (N > 2500){ // if 2.5 seconds go by without a beat + thresh = 530; // set thresh default + P = 512; // set P default + T = 512; // set T default + lastBeatTime = sampleCounter; // bring the lastBeatTime up to date + firstBeat = true; // set these to avoid noise + secondBeat = false; // when we get the heartbeat back + } + + // sei(); // enable interrupts when youre done! + }// end processSignal + + +}; From 67a5e0cb3126cc2ef403ba102e2ec78fc9b6317f Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Wed, 8 Feb 2023 17:46:20 -0600 Subject: [PATCH 2/8] Remove try/catch in DataLogger.pde --- OpenBCI_GUI/DataLogger.pde | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/OpenBCI_GUI/DataLogger.pde b/OpenBCI_GUI/DataLogger.pde index 968c8eab3..8dc022891 100644 --- a/OpenBCI_GUI/DataLogger.pde +++ b/OpenBCI_GUI/DataLogger.pde @@ -145,19 +145,15 @@ class DataLogger { closeLogFile(); } //open the new file - try { - fileWriterODF = new DataWriterODF(sessionName, _fileName); - if (currentBoard instanceof AuxDataBoard) { - if (fileWriterAuxODF != null) - fileWriterAuxODF.closeFile(); - fileWriterAuxODF = new DataWriterAuxODF(sessionName, _fileName); - } - - output_fname = fileWriterODF.fname; - println("OpenBCI_GUI: openNewLogFile: opened ODF output file: " + output_fname); //Print filename of new ODF file to console - } catch (Exception e) { - e.printStackTrace(); + fileWriterODF = new DataWriterODF(sessionName, _fileName); + if (currentBoard instanceof AuxDataBoard) { + if (fileWriterAuxODF != null) + fileWriterAuxODF.closeFile(); + fileWriterAuxODF = new DataWriterAuxODF(sessionName, _fileName); } + + output_fname = fileWriterODF.fname; + println("OpenBCI_GUI: openNewLogFile: opened ODF output file: " + output_fname); //Print filename of new ODF file to console } private void closeLogFile() { From 71c514bc9a53a3175696dc19ed13b822d03ccbc8 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Mon, 13 Feb 2023 16:21:18 -0600 Subject: [PATCH 3/8] Add classes and methods for all DataSourcePlayback boards --- OpenBCI_GUI/BoardBrainFlowSynthetic.pde | 104 +-- OpenBCI_GUI/DataSourcePlayback.pde | 9 + OpenBCI_GUI/DataSourcePlaybackGanglion.pde | 41 ++ OpenBCI_GUI/DataSourcePlaybackSynthetic.pde | 53 ++ OpenBCI_GUI/W_Playback.pde | 722 ++++++++++---------- 5 files changed, 465 insertions(+), 464 deletions(-) create mode 100644 OpenBCI_GUI/DataSourcePlaybackGanglion.pde create mode 100644 OpenBCI_GUI/DataSourcePlaybackSynthetic.pde diff --git a/OpenBCI_GUI/BoardBrainFlowSynthetic.pde b/OpenBCI_GUI/BoardBrainFlowSynthetic.pde index 75f597212..b77c3f2f6 100644 --- a/OpenBCI_GUI/BoardBrainFlowSynthetic.pde +++ b/OpenBCI_GUI/BoardBrainFlowSynthetic.pde @@ -2,13 +2,9 @@ import brainflow.*; import org.apache.commons.lang3.tuple.Pair; -class BoardBrainFlowSynthetic extends BoardBrainFlow -implements AccelerometerCapableBoard, PPGCapableBoard, EDACapableBoard, BatteryInfoCapableBoard { +class BoardBrainFlowSynthetic extends BoardBrainFlow implements AccelerometerCapableBoard { private int[] accelChannelsCache = null; - private int[] edaChannelsCache = null; - private int[] ppgChannelsCache = null; - private Integer batteryChannelCache = null; private int numChannels = 0; private volatile boolean[] activeChannels = null; @@ -97,80 +93,12 @@ implements AccelerometerCapableBoard, PPGCapableBoard, EDACapableBoard, BatteryI return accelChannelsCache; } - @Override - public boolean isPPGActive() { - return true; - } - - @Override - public void setPPGActive(boolean active) { - outputWarn("PPG is always active for BrainflowSyntheticBoard"); - } - - @Override - public int[] getPPGChannels() { - if(ppgChannelsCache == null) { - try { - ppgChannelsCache = BoardShim.get_ppg_channels(getBoardIdInt()); - - } catch (BrainFlowError e) { - e.printStackTrace(); - } - } - - return ppgChannelsCache; - } - - @Override - public boolean isEDAActive() { - return true; - } - - @Override - public void setEDAActive(boolean active) { - outputWarn("EDA is always active for BrainflowSyntheticBoard"); - } - - @Override - public int[] getEDAChannels() { - if (edaChannelsCache == null) { - try { - edaChannelsCache = BoardShim.get_eda_channels(getBoardIdInt()); - - } catch (BrainFlowError e) { - e.printStackTrace(); - } - } - - return edaChannelsCache; - } - - @Override - public Integer getBatteryChannel() { - if (batteryChannelCache == null) { - try { - batteryChannelCache = BoardShim.get_battery_channel(getBoardIdInt()); - } catch (BrainFlowError e) { - e.printStackTrace(); - - } - } - - return batteryChannelCache; - } @Override protected void addChannelNamesInternal(String[] channelNames) { - for (int i=0; i getDataWithPPG(int maxSamples) { - return getData(maxSamples); - } - - @Override - public List getDataWithEDA(int maxSamples) { - return getData(maxSamples); - } - - @Override - public List getDataWithBatteryInfo(int maxSamples) { - return getData(maxSamples); - } - @Override public int getAccelSampleRate() { return getSampleRate(); } - - @Override - public int getPPGSampleRate() { - return getSampleRate(); - } - - @Override - public int getEDASampleRate() { - return getSampleRate(); - } - - @Override - public int getBatteryInfoSampleRate() { - return getSampleRate(); - } }; diff --git a/OpenBCI_GUI/DataSourcePlayback.pde b/OpenBCI_GUI/DataSourcePlayback.pde index 67b323cb7..5ef3feee8 100644 --- a/OpenBCI_GUI/DataSourcePlayback.pde +++ b/OpenBCI_GUI/DataSourcePlayback.pde @@ -303,7 +303,16 @@ public DataSourcePlayback getDataSourcePlaybackClassFromFile(String path) { switch (underlyingBoardClassName) { case ("BoardCytonSerial"): + case ("BoardCytonSerialDaisy"): + case ("BoardCytonWifi"): + case ("BoardCytonWifiDaisy"): return new DataSourcePlaybackCyton(path); + case ("BoardGanglionBLE"): + case ("BoardGanglionNative"): + case ("BoardGanglionWifi"): + return new DataSourcePlaybackGanglion(path); + case ("BoardBrainFlowSynthetic"): + return new DataSourcePlaybackSynthetic(path); default: return null; } diff --git a/OpenBCI_GUI/DataSourcePlaybackGanglion.pde b/OpenBCI_GUI/DataSourcePlaybackGanglion.pde new file mode 100644 index 000000000..57a581740 --- /dev/null +++ b/OpenBCI_GUI/DataSourcePlaybackGanglion.pde @@ -0,0 +1,41 @@ +class DataSourcePlaybackGanglion extends DataSourcePlayback implements AccelerometerCapableBoard, FileBoard { + + DataSourcePlaybackGanglion(String filePath) { + super(filePath); + } + + @Override + public int getAccelSampleRate() { + return getSampleRate(); + } + + @Override + public boolean isAccelerometerActive() { + return underlyingBoard instanceof AccelerometerCapableBoard; + } + + @Override + public void setAccelerometerActive(boolean active) { + // nothing + } + + @Override + public boolean canDeactivateAccelerometer() { + return false; + } + + @Override + public int[] getAccelerometerChannels() { + if (underlyingBoard instanceof AccelerometerCapableBoard) { + return ((AccelerometerCapableBoard)underlyingBoard).getAccelerometerChannels(); + } + + return new int[0]; + } + + @Override + public List getDataWithAccel(int maxSamples) { + return getData(maxSamples); + } + +} diff --git a/OpenBCI_GUI/DataSourcePlaybackSynthetic.pde b/OpenBCI_GUI/DataSourcePlaybackSynthetic.pde new file mode 100644 index 000000000..01ce69ef4 --- /dev/null +++ b/OpenBCI_GUI/DataSourcePlaybackSynthetic.pde @@ -0,0 +1,53 @@ +class DataSourcePlaybackSynthetic extends DataSourcePlayback implements AccelerometerCapableBoard, FileBoard { + + DataSourcePlaybackSynthetic(String filePath) { + super(filePath); + } + + protected boolean instantiateUnderlyingBoard() { + try { + underlyingBoard = new BoardBrainFlowSynthetic(nchan); + } catch (Exception e) { + println(e.getMessage()); + e.printStackTrace(); + return false; + } + + return underlyingBoard != null; + } + + @Override + public int getAccelSampleRate() { + return getSampleRate(); + } + + @Override + public boolean isAccelerometerActive() { + return underlyingBoard instanceof AccelerometerCapableBoard; + } + + @Override + public void setAccelerometerActive(boolean active) { + // nothing + } + + @Override + public boolean canDeactivateAccelerometer() { + return false; + } + + @Override + public int[] getAccelerometerChannels() { + if (underlyingBoard instanceof AccelerometerCapableBoard) { + return ((AccelerometerCapableBoard)underlyingBoard).getAccelerometerChannels(); + } + + return new int[0]; + } + + @Override + public List getDataWithAccel(int maxSamples) { + return getData(maxSamples); + } + +} diff --git a/OpenBCI_GUI/W_Playback.pde b/OpenBCI_GUI/W_Playback.pde index 410b8e3f9..0acafd855 100644 --- a/OpenBCI_GUI/W_Playback.pde +++ b/OpenBCI_GUI/W_Playback.pde @@ -1,361 +1,361 @@ - -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// W_playback.pde (ie "Playback History") -// -// Allow user to load playback files from within GUI without having to restart the system -// Created: Richard Waltman - August 2018 -///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -import java.io.FileReader; - -class W_playback extends Widget { - //allow access to dataProcessing - DataProcessing dataProcessing; - //Set up variables for Playback widget - ControlP5 cp5_playback; - Button selectPlaybackFileButton; - MenuList playbackMenuList; - //Used for spacing - int padding = 10; - List cp5ElementsToCheck = new ArrayList(); - - private boolean menuHasUpdated = false; - - W_playback(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - - cp5_playback = new ControlP5(pApplet); - cp5_playback.setGraphics(ourApplet, 0,0); - cp5_playback.setAutoDraw(false); - - int initialWidth = w - padding*2; - createPlaybackMenuList(cp5_playback, "playbackMenuList", x + padding/2, y + 2, initialWidth, h - padding*2, p3); - createSelectPlaybackFileButton("selectPlaybackFile_Session", "Select Playback File", x + w/2 - (padding*2), y - navHeight + 2, 200, navHeight - 6); - } - - void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - if (!menuHasUpdated) { - refreshPlaybackList(); - menuHasUpdated = true; - } - //Lock the MenuList if Widget selector is open, otherwise update - if (cp5_widget.get(ScrollableList.class, "WidgetSelector").isOpen() || topNav.getDropdownMenuIsOpen()) { - if (!playbackMenuList.isLock()) { - playbackMenuList.lock(); - playbackMenuList.setUpdate(false); - } - } else { - if (playbackMenuList.isLock()) { - playbackMenuList.unlock(); - playbackMenuList.setUpdate(true); - } - playbackMenuList.updateMenu(); - } - lockElementsOnOverlapCheck(cp5ElementsToCheck); - } - - void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) - - //x,y,w,h are the positioning variables of the Widget class - pushStyle(); - fill(boxColor); - stroke(boxStrokeColor); - strokeWeight(1); - rect(x-1, y, w+1, h); - //Add text if needed - /* - fill(OPENBCI_DARKBLUE); - textFont(h3, 16); - textAlign(LEFT, TOP); - text("PLAYBACK FILE", x + padding, y + padding); - */ - popStyle(); - - cp5_playback.draw(); - } //end draw loop - - void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) - - //**IMPORTANT FOR CP5**// - //This makes the cp5 objects within the widget scale properly - cp5_playback.setGraphics(pApplet, 0, 0); - - //Resize and position cp5 objects within this widget - selectPlaybackFileButton.setPosition(x + w - selectPlaybackFileButton.getWidth() - 2, y - navHeight + 2); - - playbackMenuList.setPosition(x + padding/2, y + 2); - playbackMenuList.setSize(w - padding*2, h - padding*2); - refreshPlaybackList(); - } - - public void refreshPlaybackList() { - - File f = new File(userPlaybackHistoryFile); - if (!f.exists()) { - println("OpenBCI_GUI::RefreshPlaybackList: Playback history file not found."); - return; - } - - try { - playbackMenuList.items.clear(); - loadPlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray loadPlaybackHistoryJSONArray = loadPlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - //println("Array Size:" + loadPlaybackHistoryJSONArray.size()); - int currentFileNameToDraw = 0; - for (int i = loadPlaybackHistoryJSONArray.size() - 1; i >= 0; i--) { //go through array in reverse since using append - JSONObject loadRecentPlaybackFile = loadPlaybackHistoryJSONArray.getJSONObject(i); - int fileNumber = loadRecentPlaybackFile.getInt("recentFileNumber"); - String shortFileName = loadRecentPlaybackFile.getString("id"); - String longFilePath = loadRecentPlaybackFile.getString("filePath"); - - int totalPadding = padding + playbackMenuList.padding; - shortFileName = shortenString(shortFileName, w-totalPadding*2.f, p4); - //add as an item in the MenuList - playbackMenuList.addItem(shortFileName, Integer.toString(fileNumber), longFilePath); - currentFileNameToDraw++; - } - playbackMenuList.updateMenu(); - } catch (NullPointerException e) { - println("PlaybackWidget: Playback history file not found."); - } - } - - private void createSelectPlaybackFileButton(String name, String text, int _x, int _y, int _w, int _h) { - selectPlaybackFileButton = createButton(cp5_playback, name, text, _x, _y, _w, _h); - selectPlaybackFileButton.setBorderColor(OBJECT_BORDER_GREY); - selectPlaybackFileButton.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - output("Select a file for playback"); - selectInput("Select a pre-recorded file for playback:", "playbackSelectedWidgetButton"); - } - }); - selectPlaybackFileButton.setDescription("Click to open a dialog box to select an OpenBCI playback file (.txt or .csv)."); - cp5ElementsToCheck.add((controlP5.Controller)selectPlaybackFileButton); - } - - private void createPlaybackMenuList(ControlP5 _cp5, String name, int _x, int _y, int _w, int _h, PFont font) { - playbackMenuList = new MenuList(_cp5, name, _w, _h, font); - playbackMenuList.setPosition(_x, _y); - playbackMenuList.addCallback(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { - //Check to make sure value of clicked item is in valid range. Fixes #480 - float valueOfItem = playbackMenuList.getValue(); - if (valueOfItem < 0 || valueOfItem > (playbackMenuList.items.size() - 1) ) { - //println("CP: No such item " + value + " found in list."); - } else { - Map m = playbackMenuList.getItem(int(valueOfItem)); - //println("got a menu event from item " + value + " : " + m); - userSelectedPlaybackMenuList(m.get("copy").toString(), int(valueOfItem)); - } - } - } - }); - playbackMenuList.scrollerLength = 40; - } -}; //end Playback widget class - -////////////////////////////////////// -// GLOBAL FUNCTIONS BELOW THIS LINE // -////////////////////////////////////// - -//Called when user selects a playback file from controlPanel dialog box -void playbackFileSelected(File selection) { - if (selection == null) { - println("DataLogging: playbackSelected: Window was closed or the user hit cancel."); - } else { - println("DataLogging: playbackSelected: User selected " + selection.getAbsolutePath()); - //Set the name of the file - playbackFileSelected(selection.getAbsolutePath(), selection.getName()); - } -} - - -//Activated when user selects a file using the "Select Playback File" button in PlaybackHistory -void playbackSelectedWidgetButton(File selection) { - if (selection == null) { - println("W_Playback: playbackSelected: Window was closed or the user hit cancel."); - } else { - println("W_Playback: playbackSelected: User selected " + selection.getAbsolutePath()); - if (playbackFileSelected(selection.getAbsolutePath(), selection.getName())) { - // restart the session with the new file - requestReinit(); - } - } -} - -//Activated when user selects a file using the recent file MenuList -void userSelectedPlaybackMenuList (String filePath, int listItem) { - if (new File(filePath).isFile()) { - playbackFileFromList(filePath, listItem); - // restart the session with the new file - requestReinit(); - } else { - verbosePrint("Playback: " + filePath); - outputError("Playback: Selected file does not exist. Try another file or clear settings to remove this entry."); - } -} - -//Called when user selects a playback file from a list -void playbackFileFromList (String longName, int listItem) { - String shortName = ""; - //look at the JSON file to set the range menu using number of recent file entries - try { - savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - JSONObject playbackFile = recentFilesArray.getJSONObject(-listItem + recentFilesArray.size() - 1); - shortName = playbackFile.getString("id"); - playbackHistoryFileExists = true; - } catch (NullPointerException e) { - //println("Playback history JSON file does not exist. Load first file to make it."); - playbackHistoryFileExists = false; - } - playbackFileSelected(longName, shortName); -} - -//Handles the work for the above cases -boolean playbackFileSelected (String longName, String shortName) { - playbackData_fname = longName; - playbackData_ShortName = shortName; - //Process the playback file, check if SD card file or something else - try { - BufferedReader brTest = new BufferedReader(new FileReader(longName)); - String line = brTest.readLine(); - if (line.equals("%OpenBCI Raw EEG Data")) { - verbosePrint("PLAYBACK: Found OpenBCI Header in File!"); - sdData_fname = "N/A"; - for (int i = 0; i < 3; i++) { - line = brTest.readLine(); - verbosePrint("PLAYBACK: " + line); - } - if (!line.startsWith("%Board")) { - playbackData_fname = "N/A"; - playbackData_ShortName = "N/A"; - outputError("Found GUI v4 or earlier file. Please convert this file using the provided Python script."); - PopupMessage msg = new PopupMessage("GUI v4 to v5 File Converter", "Found GUI v4 or earlier file. Please convert this file using the provided Python script. Press the button below to access this open-source fix.", "LINK", "https://github.com/OpenBCI/OpenBCI_GUI/tree/development/tools"); - return false; - } - } else if (line.equals("%STOP AT")) { - verbosePrint("PLAYBACK: Found SD File Header in File!"); - playbackData_fname = "N/A"; - sdData_fname = longName; - } else { - outputError("ERROR: Tried to load an unsupported file for playback! Please try a valid file."); - playbackData_fname = "N/A"; - playbackData_ShortName = "N/A"; - sdData_fname = "N/A"; - return false; - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - return false; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - - //Output new playback settings to GUI as success - outputSuccess("You have selected \"" - + shortName + "\" for playback."); - - File f = new File(userPlaybackHistoryFile); - if (!f.exists()) { - println("OpenBCI_GUI::playbackFileSelected: Playback history file not found."); - playbackHistoryFileExists = false; - } else { - try { - savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - playbackHistoryFileExists = true; - } catch (RuntimeException e) { - outputError("Found an error in UserPlaybackHistory.json. Deleting this file. Please, Restart the GUI."); - File file = new File(userPlaybackHistoryFile); - if (!file.isDirectory()) { - file.delete(); - } - } - } - - //add playback file that was processed to the JSON history - savePlaybackFileToHistory(longName); - return true; -} - -void savePlaybackFileToHistory(String fileName) { - int maxNumHistoryFiles = 36; - if (playbackHistoryFileExists) { - println("Found user playback history file!"); - savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - //println("ARRAYSIZE-Check1: " + int(recentFilesArray.size())); - //Recent file has recentFileNumber=0, and appears at the end of the JSON array - //check if already in the list, if so, remove from the list - removePlaybackFileFromHistory(recentFilesArray, playbackData_fname); - //next, increment fileNumber of all current entries +1 - for (int i = 0; i < recentFilesArray.size(); i++) { - JSONObject playbackFile = recentFilesArray.getJSONObject(i); - playbackFile.setInt("recentFileNumber", recentFilesArray.size()-i); - //println(recentFilesArray.size()-i); - playbackFile.setString("id", playbackFile.getString("id")); - playbackFile.setString("filePath", playbackFile.getString("filePath")); - recentFilesArray.setJSONObject(i, playbackFile); - } - //println("ARRAYSIZE-Check2: " + int(recentFilesArray.size())); - //append selected playback file to position 1 at the end of the JSONArray - JSONObject mostRecentFile = new JSONObject(); - mostRecentFile.setInt("recentFileNumber", 0); - mostRecentFile.setString("id", playbackData_ShortName); - mostRecentFile.setString("filePath", playbackData_fname); - recentFilesArray.append(mostRecentFile); - //remove entries greater than max num files - if (recentFilesArray.size() >= maxNumHistoryFiles) { - for (int i = 0; i <= recentFilesArray.size()-maxNumHistoryFiles; i++) { - recentFilesArray.remove(i); - println("ARRAY INDEX " + i + " REMOVED----"); - } - } - //println("ARRAYSIZE-Check3: " + int(recentFilesArray.size())); - //printArray(recentFilesArray); - - //save the JSON array and file - savePlaybackHistoryJSON.setJSONArray("playbackFileHistory", recentFilesArray); - saveJSONObject(savePlaybackHistoryJSON, userPlaybackHistoryFile); - - } else if (!playbackHistoryFileExists) { - println("Playback history file not found. making a new one."); - //do this if the file does not exist - JSONObject newHistoryFile; - newHistoryFile = new JSONObject(); - JSONArray newHistoryFileArray = new JSONArray(); - //save selected playback file to position 1 in recent file history - JSONObject mostRecentFile = new JSONObject(); - mostRecentFile.setInt("recentFileNumber", 0); - mostRecentFile.setString("id", playbackData_ShortName); - mostRecentFile.setString("filePath", playbackData_fname); - newHistoryFileArray.setJSONObject(0, mostRecentFile); - //newHistoryFile.setJSONArray("") - - //save the JSON array and file - newHistoryFile.setJSONArray("playbackFileHistory", newHistoryFileArray); - saveJSONObject(newHistoryFile, userPlaybackHistoryFile); - - //now the file exists! - println("Playback history JSON has been made!"); - playbackHistoryFileExists = true; - } -} - -void removePlaybackFileFromHistory(JSONArray array, String _filePath) { - //check if already in the list, if so, remove from the list - for (int i = 0; i < array.size(); i++) { - JSONObject playbackFile = array.getJSONObject(i); - //println("CHECKING " + i + " : " + playbackFile.getString("id") + " == " + fileName + " ?"); - if (playbackFile.getString("filePath").equals(_filePath)) { - array.remove(i); - //println("REMOVED: " + fileName); - } - } -} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// W_playback.pde (ie "Playback History") +// +// Allow user to load playback files from within GUI without having to restart the system +// Created: Richard Waltman - August 2018 +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import java.io.FileReader; + +class W_playback extends Widget { + //allow access to dataProcessing + DataProcessing dataProcessing; + //Set up variables for Playback widget + ControlP5 cp5_playback; + Button selectPlaybackFileButton; + MenuList playbackMenuList; + //Used for spacing + int padding = 10; + List cp5ElementsToCheck = new ArrayList(); + + private boolean menuHasUpdated = false; + + W_playback(PApplet _parent) { + super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + + cp5_playback = new ControlP5(pApplet); + cp5_playback.setGraphics(ourApplet, 0,0); + cp5_playback.setAutoDraw(false); + + int initialWidth = w - padding*2; + createPlaybackMenuList(cp5_playback, "playbackMenuList", x + padding/2, y + 2, initialWidth, h - padding*2, p3); + createSelectPlaybackFileButton("selectPlaybackFile_Session", "Select Playback File", x + w/2 - (padding*2), y - navHeight + 2, 200, navHeight - 6); + } + + void update() { + super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + if (!menuHasUpdated) { + refreshPlaybackList(); + menuHasUpdated = true; + } + //Lock the MenuList if Widget selector is open, otherwise update + if (cp5_widget.get(ScrollableList.class, "WidgetSelector").isOpen() || topNav.getDropdownMenuIsOpen()) { + if (!playbackMenuList.isLock()) { + playbackMenuList.lock(); + playbackMenuList.setUpdate(false); + } + } else { + if (playbackMenuList.isLock()) { + playbackMenuList.unlock(); + playbackMenuList.setUpdate(true); + } + playbackMenuList.updateMenu(); + } + lockElementsOnOverlapCheck(cp5ElementsToCheck); + } + + void draw() { + super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + + //x,y,w,h are the positioning variables of the Widget class + pushStyle(); + fill(boxColor); + stroke(boxStrokeColor); + strokeWeight(1); + rect(x-1, y, w+1, h); + //Add text if needed + /* + fill(OPENBCI_DARKBLUE); + textFont(h3, 16); + textAlign(LEFT, TOP); + text("PLAYBACK FILE", x + padding, y + padding); + */ + popStyle(); + + cp5_playback.draw(); + } //end draw loop + + void screenResized() { + super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + + //**IMPORTANT FOR CP5**// + //This makes the cp5 objects within the widget scale properly + cp5_playback.setGraphics(pApplet, 0, 0); + + //Resize and position cp5 objects within this widget + selectPlaybackFileButton.setPosition(x + w - selectPlaybackFileButton.getWidth() - 2, y - navHeight + 2); + + playbackMenuList.setPosition(x + padding/2, y + 2); + playbackMenuList.setSize(w - padding*2, h - padding*2); + refreshPlaybackList(); + } + + public void refreshPlaybackList() { + + File f = new File(userPlaybackHistoryFile); + if (!f.exists()) { + println("OpenBCI_GUI::RefreshPlaybackList: Playback history file not found."); + return; + } + + try { + playbackMenuList.items.clear(); + loadPlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray loadPlaybackHistoryJSONArray = loadPlaybackHistoryJSON.getJSONArray("playbackFileHistory"); + //println("Array Size:" + loadPlaybackHistoryJSONArray.size()); + int currentFileNameToDraw = 0; + for (int i = loadPlaybackHistoryJSONArray.size() - 1; i >= 0; i--) { //go through array in reverse since using append + JSONObject loadRecentPlaybackFile = loadPlaybackHistoryJSONArray.getJSONObject(i); + int fileNumber = loadRecentPlaybackFile.getInt("recentFileNumber"); + String shortFileName = loadRecentPlaybackFile.getString("id"); + String longFilePath = loadRecentPlaybackFile.getString("filePath"); + + int totalPadding = padding + playbackMenuList.padding; + shortFileName = shortenString(shortFileName, w-totalPadding*2.f, p4); + //add as an item in the MenuList + playbackMenuList.addItem(shortFileName, Integer.toString(fileNumber), longFilePath); + currentFileNameToDraw++; + } + playbackMenuList.updateMenu(); + } catch (NullPointerException e) { + println("PlaybackWidget: Playback history file not found."); + } + } + + private void createSelectPlaybackFileButton(String name, String text, int _x, int _y, int _w, int _h) { + selectPlaybackFileButton = createButton(cp5_playback, name, text, _x, _y, _w, _h); + selectPlaybackFileButton.setBorderColor(OBJECT_BORDER_GREY); + selectPlaybackFileButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + output("Select a file for playback"); + selectInput("Select a pre-recorded file for playback:", "playbackSelectedWidgetButton"); + } + }); + selectPlaybackFileButton.setDescription("Click to open a dialog box to select an OpenBCI playback file (.txt or .csv)."); + cp5ElementsToCheck.add((controlP5.Controller)selectPlaybackFileButton); + } + + private void createPlaybackMenuList(ControlP5 _cp5, String name, int _x, int _y, int _w, int _h, PFont font) { + playbackMenuList = new MenuList(_cp5, name, _w, _h, font); + playbackMenuList.setPosition(_x, _y); + playbackMenuList.addCallback(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + //Check to make sure value of clicked item is in valid range. Fixes #480 + float valueOfItem = playbackMenuList.getValue(); + if (valueOfItem < 0 || valueOfItem > (playbackMenuList.items.size() - 1) ) { + //println("CP: No such item " + value + " found in list."); + } else { + Map m = playbackMenuList.getItem(int(valueOfItem)); + //println("got a menu event from item " + value + " : " + m); + userSelectedPlaybackMenuList(m.get("copy").toString(), int(valueOfItem)); + } + } + } + }); + playbackMenuList.scrollerLength = 40; + } +}; //end Playback widget class + +////////////////////////////////////// +// GLOBAL FUNCTIONS BELOW THIS LINE // +////////////////////////////////////// + +//Called when user selects a playback file from controlPanel dialog box +void playbackFileSelected(File selection) { + if (selection == null) { + println("DataLogging: playbackSelected: Window was closed or the user hit cancel."); + } else { + println("DataLogging: playbackSelected: User selected " + selection.getAbsolutePath()); + //Set the name of the file + playbackFileSelected(selection.getAbsolutePath(), selection.getName()); + } +} + + +//Activated when user selects a file using the "Select Playback File" button in PlaybackHistory +void playbackSelectedWidgetButton(File selection) { + if (selection == null) { + println("W_Playback: playbackSelected: Window was closed or the user hit cancel."); + } else { + println("W_Playback: playbackSelected: User selected " + selection.getAbsolutePath()); + if (playbackFileSelected(selection.getAbsolutePath(), selection.getName())) { + // restart the session with the new file + requestReinit(); + } + } +} + +//Activated when user selects a file using the recent file MenuList +void userSelectedPlaybackMenuList (String filePath, int listItem) { + if (new File(filePath).isFile()) { + playbackFileFromList(filePath, listItem); + // restart the session with the new file + requestReinit(); + } else { + verbosePrint("Playback: " + filePath); + outputError("Playback: Selected file does not exist. Try another file or clear settings to remove this entry."); + } +} + +//Called when user selects a playback file from a list +void playbackFileFromList (String longName, int listItem) { + String shortName = ""; + //look at the JSON file to set the range menu using number of recent file entries + try { + savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); + JSONObject playbackFile = recentFilesArray.getJSONObject(-listItem + recentFilesArray.size() - 1); + shortName = playbackFile.getString("id"); + playbackHistoryFileExists = true; + } catch (NullPointerException e) { + //println("Playback history JSON file does not exist. Load first file to make it."); + playbackHistoryFileExists = false; + } + playbackFileSelected(longName, shortName); +} + +//Handles the work for the above cases +boolean playbackFileSelected (String longName, String shortName) { + playbackData_fname = longName; + playbackData_ShortName = shortName; + //Process the playback file, check if SD card file or something else + try { + BufferedReader brTest = new BufferedReader(new FileReader(longName)); + String line = brTest.readLine(); + if (line.equals("%OpenBCI Raw EEG Data") || line.equals("%OpenBCI Raw EXG Data")) { + verbosePrint("PLAYBACK: Found OpenBCI Header in File!"); + sdData_fname = "N/A"; + for (int i = 0; i < 3; i++) { + line = brTest.readLine(); + verbosePrint("PLAYBACK: " + line); + } + if (!line.startsWith("%Board")) { + playbackData_fname = "N/A"; + playbackData_ShortName = "N/A"; + outputError("Found GUI v4 or earlier file. Please convert this file using the provided Python script."); + PopupMessage msg = new PopupMessage("GUI v4 to v5 File Converter", "Found GUI v4 or earlier file. Please convert this file using the provided Python script. Press the button below to access this open-source fix.", "LINK", "https://github.com/OpenBCI/OpenBCI_GUI/tree/development/tools"); + return false; + } + } else if (line.equals("%STOP AT")) { + verbosePrint("PLAYBACK: Found SD File Header in File!"); + playbackData_fname = "N/A"; + sdData_fname = longName; + } else { + outputError("ERROR: Tried to load an unsupported file for playback! Please try a valid file."); + playbackData_fname = "N/A"; + playbackData_ShortName = "N/A"; + sdData_fname = "N/A"; + return false; + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return false; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + //Output new playback settings to GUI as success + outputSuccess("You have selected \"" + + shortName + "\" for playback."); + + File f = new File(userPlaybackHistoryFile); + if (!f.exists()) { + println("OpenBCI_GUI::playbackFileSelected: Playback history file not found."); + playbackHistoryFileExists = false; + } else { + try { + savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); + playbackHistoryFileExists = true; + } catch (RuntimeException e) { + outputError("Found an error in UserPlaybackHistory.json. Deleting this file. Please, Restart the GUI."); + File file = new File(userPlaybackHistoryFile); + if (!file.isDirectory()) { + file.delete(); + } + } + } + + //add playback file that was processed to the JSON history + savePlaybackFileToHistory(longName); + return true; +} + +void savePlaybackFileToHistory(String fileName) { + int maxNumHistoryFiles = 36; + if (playbackHistoryFileExists) { + println("Found user playback history file!"); + savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); + //println("ARRAYSIZE-Check1: " + int(recentFilesArray.size())); + //Recent file has recentFileNumber=0, and appears at the end of the JSON array + //check if already in the list, if so, remove from the list + removePlaybackFileFromHistory(recentFilesArray, playbackData_fname); + //next, increment fileNumber of all current entries +1 + for (int i = 0; i < recentFilesArray.size(); i++) { + JSONObject playbackFile = recentFilesArray.getJSONObject(i); + playbackFile.setInt("recentFileNumber", recentFilesArray.size()-i); + //println(recentFilesArray.size()-i); + playbackFile.setString("id", playbackFile.getString("id")); + playbackFile.setString("filePath", playbackFile.getString("filePath")); + recentFilesArray.setJSONObject(i, playbackFile); + } + //println("ARRAYSIZE-Check2: " + int(recentFilesArray.size())); + //append selected playback file to position 1 at the end of the JSONArray + JSONObject mostRecentFile = new JSONObject(); + mostRecentFile.setInt("recentFileNumber", 0); + mostRecentFile.setString("id", playbackData_ShortName); + mostRecentFile.setString("filePath", playbackData_fname); + recentFilesArray.append(mostRecentFile); + //remove entries greater than max num files + if (recentFilesArray.size() >= maxNumHistoryFiles) { + for (int i = 0; i <= recentFilesArray.size()-maxNumHistoryFiles; i++) { + recentFilesArray.remove(i); + println("ARRAY INDEX " + i + " REMOVED----"); + } + } + //println("ARRAYSIZE-Check3: " + int(recentFilesArray.size())); + //printArray(recentFilesArray); + + //save the JSON array and file + savePlaybackHistoryJSON.setJSONArray("playbackFileHistory", recentFilesArray); + saveJSONObject(savePlaybackHistoryJSON, userPlaybackHistoryFile); + + } else if (!playbackHistoryFileExists) { + println("Playback history file not found. making a new one."); + //do this if the file does not exist + JSONObject newHistoryFile; + newHistoryFile = new JSONObject(); + JSONArray newHistoryFileArray = new JSONArray(); + //save selected playback file to position 1 in recent file history + JSONObject mostRecentFile = new JSONObject(); + mostRecentFile.setInt("recentFileNumber", 0); + mostRecentFile.setString("id", playbackData_ShortName); + mostRecentFile.setString("filePath", playbackData_fname); + newHistoryFileArray.setJSONObject(0, mostRecentFile); + //newHistoryFile.setJSONArray("") + + //save the JSON array and file + newHistoryFile.setJSONArray("playbackFileHistory", newHistoryFileArray); + saveJSONObject(newHistoryFile, userPlaybackHistoryFile); + + //now the file exists! + println("Playback history JSON has been made!"); + playbackHistoryFileExists = true; + } +} + +void removePlaybackFileFromHistory(JSONArray array, String _filePath) { + //check if already in the list, if so, remove from the list + for (int i = 0; i < array.size(); i++) { + JSONObject playbackFile = array.getJSONObject(i); + //println("CHECKING " + i + " : " + playbackFile.getString("id") + " == " + fileName + " ?"); + if (playbackFile.getString("filePath").equals(_filePath)) { + array.remove(i); + //println("REMOVED: " + fileName); + } + } +} From 7dcd64d4a9faa133b677f50da992c97a5d1d15ee Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Tue, 14 Mar 2023 12:17:59 -0500 Subject: [PATCH 4/8] Remove BatteryInfo, EDA, and PPG boards from this repo --- OpenBCI_GUI/BatteryInfoCapableBoard.pde | 9 --------- OpenBCI_GUI/EDACapableBoard.pde | 13 ------------- OpenBCI_GUI/PPGCapableBoard.pde | 13 ------------- 3 files changed, 35 deletions(-) delete mode 100644 OpenBCI_GUI/BatteryInfoCapableBoard.pde delete mode 100644 OpenBCI_GUI/EDACapableBoard.pde delete mode 100644 OpenBCI_GUI/PPGCapableBoard.pde diff --git a/OpenBCI_GUI/BatteryInfoCapableBoard.pde b/OpenBCI_GUI/BatteryInfoCapableBoard.pde deleted file mode 100644 index 53deb4305..000000000 --- a/OpenBCI_GUI/BatteryInfoCapableBoard.pde +++ /dev/null @@ -1,9 +0,0 @@ - -interface BatteryInfoCapableBoard { - - public Integer getBatteryChannel(); - - public List getDataWithBatteryInfo(int maxSamples); - - public int getBatteryInfoSampleRate(); -}; diff --git a/OpenBCI_GUI/EDACapableBoard.pde b/OpenBCI_GUI/EDACapableBoard.pde deleted file mode 100644 index 36df4d642..000000000 --- a/OpenBCI_GUI/EDACapableBoard.pde +++ /dev/null @@ -1,13 +0,0 @@ - -interface EDACapableBoard { - - public boolean isEDAActive(); - - public void setEDAActive(boolean active); - - public int[] getEDAChannels(); - - public List getDataWithEDA(int maxSamples); - - public int getEDASampleRate(); -}; diff --git a/OpenBCI_GUI/PPGCapableBoard.pde b/OpenBCI_GUI/PPGCapableBoard.pde deleted file mode 100644 index 942b2dd58..000000000 --- a/OpenBCI_GUI/PPGCapableBoard.pde +++ /dev/null @@ -1,13 +0,0 @@ - -interface PPGCapableBoard { - - public boolean isPPGActive(); - - public void setPPGActive(boolean active); - - public int[] getPPGChannels(); - - public List getDataWithPPG(int maxSamples); - - public int getPPGSampleRate(); -}; From f007e5a940ca0e4923aebf65b3c4a36b263343ba Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 5 May 2023 13:50:48 -0500 Subject: [PATCH 5/8] Fix line endings in datawriterodf, fileboard, signalcheckthresholds, w_playback, and w_pulsesensor --- OpenBCI_GUI/DataWriterODF.pde | 170 +++--- OpenBCI_GUI/FileBoard.pde | 28 +- OpenBCI_GUI/SignalCheckThresholds.pde | 284 +++++----- OpenBCI_GUI/W_Playback.pde | 722 ++++++++++++------------ OpenBCI_GUI/W_PulseSensor.pde | 766 +++++++++++++------------- 5 files changed, 984 insertions(+), 986 deletions(-) diff --git a/OpenBCI_GUI/DataWriterODF.pde b/OpenBCI_GUI/DataWriterODF.pde index 7e0df3f6a..d92c6da18 100644 --- a/OpenBCI_GUI/DataWriterODF.pde +++ b/OpenBCI_GUI/DataWriterODF.pde @@ -1,85 +1,85 @@ -public class DataWriterODF { - private PrintWriter output; - private String fname; - private int rowsWritten; - private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); - protected String fileNamePrependString = "OpenBCI-RAW-"; - protected String headerFirstLineString = "%OpenBCI Raw EXG Data"; - - //variation on constructor to have custom name - DataWriterODF(String _sessionName, String _fileName) { - settings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); - fname = settings.getSessionPath(); - fname += fileNamePrependString; - fname += _fileName; - fname += ".txt"; - output = createWriter(fname); //open the file - writeHeader(); //add the header - rowsWritten = 0; //init the counter - } - - public void writeHeader() { - output.println(headerFirstLineString); - output.println("%Number of channels = " + getNumberOfChannels()); - output.println("%Sample Rate = " + getSamplingRate() + " Hz"); - output.println("%Board = " + getUnderlyingBoardClass()); - - String[] colNames = getChannelNames(); - - for (int i=0; i cp5ElementsToCheck = new ArrayList(); - - private boolean menuHasUpdated = false; - - W_playback(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - - cp5_playback = new ControlP5(pApplet); - cp5_playback.setGraphics(ourApplet, 0,0); - cp5_playback.setAutoDraw(false); - - int initialWidth = w - padding*2; - createPlaybackMenuList(cp5_playback, "playbackMenuList", x + padding/2, y + 2, initialWidth, h - padding*2, p3); - createSelectPlaybackFileButton("selectPlaybackFile_Session", "Select Playback File", x + w/2 - (padding*2), y - navHeight + 2, 200, navHeight - 6); - } - - void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - if (!menuHasUpdated) { - refreshPlaybackList(); - menuHasUpdated = true; - } - //Lock the MenuList if Widget selector is open, otherwise update - if (cp5_widget.get(ScrollableList.class, "WidgetSelector").isOpen() || topNav.getDropdownMenuIsOpen()) { - if (!playbackMenuList.isLock()) { - playbackMenuList.lock(); - playbackMenuList.setUpdate(false); - } - } else { - if (playbackMenuList.isLock()) { - playbackMenuList.unlock(); - playbackMenuList.setUpdate(true); - } - playbackMenuList.updateMenu(); - } - lockElementsOnOverlapCheck(cp5ElementsToCheck); - } - - void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) - - //x,y,w,h are the positioning variables of the Widget class - pushStyle(); - fill(boxColor); - stroke(boxStrokeColor); - strokeWeight(1); - rect(x-1, y, w+1, h); - //Add text if needed - /* - fill(OPENBCI_DARKBLUE); - textFont(h3, 16); - textAlign(LEFT, TOP); - text("PLAYBACK FILE", x + padding, y + padding); - */ - popStyle(); - - cp5_playback.draw(); - } //end draw loop - - void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) - - //**IMPORTANT FOR CP5**// - //This makes the cp5 objects within the widget scale properly - cp5_playback.setGraphics(pApplet, 0, 0); - - //Resize and position cp5 objects within this widget - selectPlaybackFileButton.setPosition(x + w - selectPlaybackFileButton.getWidth() - 2, y - navHeight + 2); - - playbackMenuList.setPosition(x + padding/2, y + 2); - playbackMenuList.setSize(w - padding*2, h - padding*2); - refreshPlaybackList(); - } - - public void refreshPlaybackList() { - - File f = new File(userPlaybackHistoryFile); - if (!f.exists()) { - println("OpenBCI_GUI::RefreshPlaybackList: Playback history file not found."); - return; - } - - try { - playbackMenuList.items.clear(); - loadPlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray loadPlaybackHistoryJSONArray = loadPlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - //println("Array Size:" + loadPlaybackHistoryJSONArray.size()); - int currentFileNameToDraw = 0; - for (int i = loadPlaybackHistoryJSONArray.size() - 1; i >= 0; i--) { //go through array in reverse since using append - JSONObject loadRecentPlaybackFile = loadPlaybackHistoryJSONArray.getJSONObject(i); - int fileNumber = loadRecentPlaybackFile.getInt("recentFileNumber"); - String shortFileName = loadRecentPlaybackFile.getString("id"); - String longFilePath = loadRecentPlaybackFile.getString("filePath"); - - int totalPadding = padding + playbackMenuList.padding; - shortFileName = shortenString(shortFileName, w-totalPadding*2.f, p4); - //add as an item in the MenuList - playbackMenuList.addItem(shortFileName, Integer.toString(fileNumber), longFilePath); - currentFileNameToDraw++; - } - playbackMenuList.updateMenu(); - } catch (NullPointerException e) { - println("PlaybackWidget: Playback history file not found."); - } - } - - private void createSelectPlaybackFileButton(String name, String text, int _x, int _y, int _w, int _h) { - selectPlaybackFileButton = createButton(cp5_playback, name, text, _x, _y, _w, _h); - selectPlaybackFileButton.setBorderColor(OBJECT_BORDER_GREY); - selectPlaybackFileButton.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - output("Select a file for playback"); - selectInput("Select a pre-recorded file for playback:", "playbackSelectedWidgetButton"); - } - }); - selectPlaybackFileButton.setDescription("Click to open a dialog box to select an OpenBCI playback file (.txt or .csv)."); - cp5ElementsToCheck.add((controlP5.Controller)selectPlaybackFileButton); - } - - private void createPlaybackMenuList(ControlP5 _cp5, String name, int _x, int _y, int _w, int _h, PFont font) { - playbackMenuList = new MenuList(_cp5, name, _w, _h, font); - playbackMenuList.setPosition(_x, _y); - playbackMenuList.addCallback(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { - //Check to make sure value of clicked item is in valid range. Fixes #480 - float valueOfItem = playbackMenuList.getValue(); - if (valueOfItem < 0 || valueOfItem > (playbackMenuList.items.size() - 1) ) { - //println("CP: No such item " + value + " found in list."); - } else { - Map m = playbackMenuList.getItem(int(valueOfItem)); - //println("got a menu event from item " + value + " : " + m); - userSelectedPlaybackMenuList(m.get("copy").toString(), int(valueOfItem)); - } - } - } - }); - playbackMenuList.scrollerLength = 40; - } -}; //end Playback widget class - -////////////////////////////////////// -// GLOBAL FUNCTIONS BELOW THIS LINE // -////////////////////////////////////// - -//Called when user selects a playback file from controlPanel dialog box -void playbackFileSelected(File selection) { - if (selection == null) { - println("DataLogging: playbackSelected: Window was closed or the user hit cancel."); - } else { - println("DataLogging: playbackSelected: User selected " + selection.getAbsolutePath()); - //Set the name of the file - playbackFileSelected(selection.getAbsolutePath(), selection.getName()); - } -} - - -//Activated when user selects a file using the "Select Playback File" button in PlaybackHistory -void playbackSelectedWidgetButton(File selection) { - if (selection == null) { - println("W_Playback: playbackSelected: Window was closed or the user hit cancel."); - } else { - println("W_Playback: playbackSelected: User selected " + selection.getAbsolutePath()); - if (playbackFileSelected(selection.getAbsolutePath(), selection.getName())) { - // restart the session with the new file - requestReinit(); - } - } -} - -//Activated when user selects a file using the recent file MenuList -void userSelectedPlaybackMenuList (String filePath, int listItem) { - if (new File(filePath).isFile()) { - playbackFileFromList(filePath, listItem); - // restart the session with the new file - requestReinit(); - } else { - verbosePrint("Playback: " + filePath); - outputError("Playback: Selected file does not exist. Try another file or clear settings to remove this entry."); - } -} - -//Called when user selects a playback file from a list -void playbackFileFromList (String longName, int listItem) { - String shortName = ""; - //look at the JSON file to set the range menu using number of recent file entries - try { - savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - JSONObject playbackFile = recentFilesArray.getJSONObject(-listItem + recentFilesArray.size() - 1); - shortName = playbackFile.getString("id"); - playbackHistoryFileExists = true; - } catch (NullPointerException e) { - //println("Playback history JSON file does not exist. Load first file to make it."); - playbackHistoryFileExists = false; - } - playbackFileSelected(longName, shortName); -} - -//Handles the work for the above cases -boolean playbackFileSelected (String longName, String shortName) { - playbackData_fname = longName; - playbackData_ShortName = shortName; - //Process the playback file, check if SD card file or something else - try { - BufferedReader brTest = new BufferedReader(new FileReader(longName)); - String line = brTest.readLine(); - if (line.equals("%OpenBCI Raw EEG Data") || line.equals("%OpenBCI Raw EXG Data")) { - verbosePrint("PLAYBACK: Found OpenBCI Header in File!"); - sdData_fname = "N/A"; - for (int i = 0; i < 3; i++) { - line = brTest.readLine(); - verbosePrint("PLAYBACK: " + line); - } - if (!line.startsWith("%Board")) { - playbackData_fname = "N/A"; - playbackData_ShortName = "N/A"; - outputError("Found GUI v4 or earlier file. Please convert this file using the provided Python script."); - PopupMessage msg = new PopupMessage("GUI v4 to v5 File Converter", "Found GUI v4 or earlier file. Please convert this file using the provided Python script. Press the button below to access this open-source fix.", "LINK", "https://github.com/OpenBCI/OpenBCI_GUI/tree/development/tools"); - return false; - } - } else if (line.equals("%STOP AT")) { - verbosePrint("PLAYBACK: Found SD File Header in File!"); - playbackData_fname = "N/A"; - sdData_fname = longName; - } else { - outputError("ERROR: Tried to load an unsupported file for playback! Please try a valid file."); - playbackData_fname = "N/A"; - playbackData_ShortName = "N/A"; - sdData_fname = "N/A"; - return false; - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - return false; - } catch (IOException e) { - e.printStackTrace(); - return false; - } - - //Output new playback settings to GUI as success - outputSuccess("You have selected \"" - + shortName + "\" for playback."); - - File f = new File(userPlaybackHistoryFile); - if (!f.exists()) { - println("OpenBCI_GUI::playbackFileSelected: Playback history file not found."); - playbackHistoryFileExists = false; - } else { - try { - savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - playbackHistoryFileExists = true; - } catch (RuntimeException e) { - outputError("Found an error in UserPlaybackHistory.json. Deleting this file. Please, Restart the GUI."); - File file = new File(userPlaybackHistoryFile); - if (!file.isDirectory()) { - file.delete(); - } - } - } - - //add playback file that was processed to the JSON history - savePlaybackFileToHistory(longName); - return true; -} - -void savePlaybackFileToHistory(String fileName) { - int maxNumHistoryFiles = 36; - if (playbackHistoryFileExists) { - println("Found user playback history file!"); - savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - //println("ARRAYSIZE-Check1: " + int(recentFilesArray.size())); - //Recent file has recentFileNumber=0, and appears at the end of the JSON array - //check if already in the list, if so, remove from the list - removePlaybackFileFromHistory(recentFilesArray, playbackData_fname); - //next, increment fileNumber of all current entries +1 - for (int i = 0; i < recentFilesArray.size(); i++) { - JSONObject playbackFile = recentFilesArray.getJSONObject(i); - playbackFile.setInt("recentFileNumber", recentFilesArray.size()-i); - //println(recentFilesArray.size()-i); - playbackFile.setString("id", playbackFile.getString("id")); - playbackFile.setString("filePath", playbackFile.getString("filePath")); - recentFilesArray.setJSONObject(i, playbackFile); - } - //println("ARRAYSIZE-Check2: " + int(recentFilesArray.size())); - //append selected playback file to position 1 at the end of the JSONArray - JSONObject mostRecentFile = new JSONObject(); - mostRecentFile.setInt("recentFileNumber", 0); - mostRecentFile.setString("id", playbackData_ShortName); - mostRecentFile.setString("filePath", playbackData_fname); - recentFilesArray.append(mostRecentFile); - //remove entries greater than max num files - if (recentFilesArray.size() >= maxNumHistoryFiles) { - for (int i = 0; i <= recentFilesArray.size()-maxNumHistoryFiles; i++) { - recentFilesArray.remove(i); - println("ARRAY INDEX " + i + " REMOVED----"); - } - } - //println("ARRAYSIZE-Check3: " + int(recentFilesArray.size())); - //printArray(recentFilesArray); - - //save the JSON array and file - savePlaybackHistoryJSON.setJSONArray("playbackFileHistory", recentFilesArray); - saveJSONObject(savePlaybackHistoryJSON, userPlaybackHistoryFile); - - } else if (!playbackHistoryFileExists) { - println("Playback history file not found. making a new one."); - //do this if the file does not exist - JSONObject newHistoryFile; - newHistoryFile = new JSONObject(); - JSONArray newHistoryFileArray = new JSONArray(); - //save selected playback file to position 1 in recent file history - JSONObject mostRecentFile = new JSONObject(); - mostRecentFile.setInt("recentFileNumber", 0); - mostRecentFile.setString("id", playbackData_ShortName); - mostRecentFile.setString("filePath", playbackData_fname); - newHistoryFileArray.setJSONObject(0, mostRecentFile); - //newHistoryFile.setJSONArray("") - - //save the JSON array and file - newHistoryFile.setJSONArray("playbackFileHistory", newHistoryFileArray); - saveJSONObject(newHistoryFile, userPlaybackHistoryFile); - - //now the file exists! - println("Playback history JSON has been made!"); - playbackHistoryFileExists = true; - } -} - -void removePlaybackFileFromHistory(JSONArray array, String _filePath) { - //check if already in the list, if so, remove from the list - for (int i = 0; i < array.size(); i++) { - JSONObject playbackFile = array.getJSONObject(i); - //println("CHECKING " + i + " : " + playbackFile.getString("id") + " == " + fileName + " ?"); - if (playbackFile.getString("filePath").equals(_filePath)) { - array.remove(i); - //println("REMOVED: " + fileName); - } - } -} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// W_playback.pde (ie "Playback History") +// +// Allow user to load playback files from within GUI without having to restart the system +// Created: Richard Waltman - August 2018 +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +import java.io.FileReader; + +class W_playback extends Widget { + //allow access to dataProcessing + DataProcessing dataProcessing; + //Set up variables for Playback widget + ControlP5 cp5_playback; + Button selectPlaybackFileButton; + MenuList playbackMenuList; + //Used for spacing + int padding = 10; + List cp5ElementsToCheck = new ArrayList(); + + private boolean menuHasUpdated = false; + + W_playback(PApplet _parent) { + super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + + cp5_playback = new ControlP5(pApplet); + cp5_playback.setGraphics(ourApplet, 0,0); + cp5_playback.setAutoDraw(false); + + int initialWidth = w - padding*2; + createPlaybackMenuList(cp5_playback, "playbackMenuList", x + padding/2, y + 2, initialWidth, h - padding*2, p3); + createSelectPlaybackFileButton("selectPlaybackFile_Session", "Select Playback File", x + w/2 - (padding*2), y - navHeight + 2, 200, navHeight - 6); + } + + void update() { + super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + if (!menuHasUpdated) { + refreshPlaybackList(); + menuHasUpdated = true; + } + //Lock the MenuList if Widget selector is open, otherwise update + if (cp5_widget.get(ScrollableList.class, "WidgetSelector").isOpen() || topNav.getDropdownMenuIsOpen()) { + if (!playbackMenuList.isLock()) { + playbackMenuList.lock(); + playbackMenuList.setUpdate(false); + } + } else { + if (playbackMenuList.isLock()) { + playbackMenuList.unlock(); + playbackMenuList.setUpdate(true); + } + playbackMenuList.updateMenu(); + } + lockElementsOnOverlapCheck(cp5ElementsToCheck); + } + + void draw() { + super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) + + //x,y,w,h are the positioning variables of the Widget class + pushStyle(); + fill(boxColor); + stroke(boxStrokeColor); + strokeWeight(1); + rect(x-1, y, w+1, h); + //Add text if needed + /* + fill(OPENBCI_DARKBLUE); + textFont(h3, 16); + textAlign(LEFT, TOP); + text("PLAYBACK FILE", x + padding, y + padding); + */ + popStyle(); + + cp5_playback.draw(); + } //end draw loop + + void screenResized() { + super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) + + //**IMPORTANT FOR CP5**// + //This makes the cp5 objects within the widget scale properly + cp5_playback.setGraphics(pApplet, 0, 0); + + //Resize and position cp5 objects within this widget + selectPlaybackFileButton.setPosition(x + w - selectPlaybackFileButton.getWidth() - 2, y - navHeight + 2); + + playbackMenuList.setPosition(x + padding/2, y + 2); + playbackMenuList.setSize(w - padding*2, h - padding*2); + refreshPlaybackList(); + } + + public void refreshPlaybackList() { + + File f = new File(userPlaybackHistoryFile); + if (!f.exists()) { + println("OpenBCI_GUI::RefreshPlaybackList: Playback history file not found."); + return; + } + + try { + playbackMenuList.items.clear(); + loadPlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray loadPlaybackHistoryJSONArray = loadPlaybackHistoryJSON.getJSONArray("playbackFileHistory"); + //println("Array Size:" + loadPlaybackHistoryJSONArray.size()); + int currentFileNameToDraw = 0; + for (int i = loadPlaybackHistoryJSONArray.size() - 1; i >= 0; i--) { //go through array in reverse since using append + JSONObject loadRecentPlaybackFile = loadPlaybackHistoryJSONArray.getJSONObject(i); + int fileNumber = loadRecentPlaybackFile.getInt("recentFileNumber"); + String shortFileName = loadRecentPlaybackFile.getString("id"); + String longFilePath = loadRecentPlaybackFile.getString("filePath"); + + int totalPadding = padding + playbackMenuList.padding; + shortFileName = shortenString(shortFileName, w-totalPadding*2.f, p4); + //add as an item in the MenuList + playbackMenuList.addItem(shortFileName, Integer.toString(fileNumber), longFilePath); + currentFileNameToDraw++; + } + playbackMenuList.updateMenu(); + } catch (NullPointerException e) { + println("PlaybackWidget: Playback history file not found."); + } + } + + private void createSelectPlaybackFileButton(String name, String text, int _x, int _y, int _w, int _h) { + selectPlaybackFileButton = createButton(cp5_playback, name, text, _x, _y, _w, _h); + selectPlaybackFileButton.setBorderColor(OBJECT_BORDER_GREY); + selectPlaybackFileButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + output("Select a file for playback"); + selectInput("Select a pre-recorded file for playback:", "playbackSelectedWidgetButton"); + } + }); + selectPlaybackFileButton.setDescription("Click to open a dialog box to select an OpenBCI playback file (.txt or .csv)."); + cp5ElementsToCheck.add((controlP5.Controller)selectPlaybackFileButton); + } + + private void createPlaybackMenuList(ControlP5 _cp5, String name, int _x, int _y, int _w, int _h, PFont font) { + playbackMenuList = new MenuList(_cp5, name, _w, _h, font); + playbackMenuList.setPosition(_x, _y); + playbackMenuList.addCallback(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + //Check to make sure value of clicked item is in valid range. Fixes #480 + float valueOfItem = playbackMenuList.getValue(); + if (valueOfItem < 0 || valueOfItem > (playbackMenuList.items.size() - 1) ) { + //println("CP: No such item " + value + " found in list."); + } else { + Map m = playbackMenuList.getItem(int(valueOfItem)); + //println("got a menu event from item " + value + " : " + m); + userSelectedPlaybackMenuList(m.get("copy").toString(), int(valueOfItem)); + } + } + } + }); + playbackMenuList.scrollerLength = 40; + } +}; //end Playback widget class + +////////////////////////////////////// +// GLOBAL FUNCTIONS BELOW THIS LINE // +////////////////////////////////////// + +//Called when user selects a playback file from controlPanel dialog box +void playbackFileSelected(File selection) { + if (selection == null) { + println("DataLogging: playbackSelected: Window was closed or the user hit cancel."); + } else { + println("DataLogging: playbackSelected: User selected " + selection.getAbsolutePath()); + //Set the name of the file + playbackFileSelected(selection.getAbsolutePath(), selection.getName()); + } +} + + +//Activated when user selects a file using the "Select Playback File" button in PlaybackHistory +void playbackSelectedWidgetButton(File selection) { + if (selection == null) { + println("W_Playback: playbackSelected: Window was closed or the user hit cancel."); + } else { + println("W_Playback: playbackSelected: User selected " + selection.getAbsolutePath()); + if (playbackFileSelected(selection.getAbsolutePath(), selection.getName())) { + // restart the session with the new file + requestReinit(); + } + } +} + +//Activated when user selects a file using the recent file MenuList +void userSelectedPlaybackMenuList (String filePath, int listItem) { + if (new File(filePath).isFile()) { + playbackFileFromList(filePath, listItem); + // restart the session with the new file + requestReinit(); + } else { + verbosePrint("Playback: " + filePath); + outputError("Playback: Selected file does not exist. Try another file or clear settings to remove this entry."); + } +} + +//Called when user selects a playback file from a list +void playbackFileFromList (String longName, int listItem) { + String shortName = ""; + //look at the JSON file to set the range menu using number of recent file entries + try { + savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); + JSONObject playbackFile = recentFilesArray.getJSONObject(-listItem + recentFilesArray.size() - 1); + shortName = playbackFile.getString("id"); + playbackHistoryFileExists = true; + } catch (NullPointerException e) { + //println("Playback history JSON file does not exist. Load first file to make it."); + playbackHistoryFileExists = false; + } + playbackFileSelected(longName, shortName); +} + +//Handles the work for the above cases +boolean playbackFileSelected (String longName, String shortName) { + playbackData_fname = longName; + playbackData_ShortName = shortName; + //Process the playback file, check if SD card file or something else + try { + BufferedReader brTest = new BufferedReader(new FileReader(longName)); + String line = brTest.readLine(); + if (line.equals("%OpenBCI Raw EEG Data") || line.equals("%OpenBCI Raw EXG Data")) { + verbosePrint("PLAYBACK: Found OpenBCI Header in File!"); + sdData_fname = "N/A"; + for (int i = 0; i < 3; i++) { + line = brTest.readLine(); + verbosePrint("PLAYBACK: " + line); + } + if (!line.startsWith("%Board")) { + playbackData_fname = "N/A"; + playbackData_ShortName = "N/A"; + outputError("Found GUI v4 or earlier file. Please convert this file using the provided Python script."); + PopupMessage msg = new PopupMessage("GUI v4 to v5 File Converter", "Found GUI v4 or earlier file. Please convert this file using the provided Python script. Press the button below to access this open-source fix.", "LINK", "https://github.com/OpenBCI/OpenBCI_GUI/tree/development/tools"); + return false; + } + } else if (line.equals("%STOP AT")) { + verbosePrint("PLAYBACK: Found SD File Header in File!"); + playbackData_fname = "N/A"; + sdData_fname = longName; + } else { + outputError("ERROR: Tried to load an unsupported file for playback! Please try a valid file."); + playbackData_fname = "N/A"; + playbackData_ShortName = "N/A"; + sdData_fname = "N/A"; + return false; + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return false; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + //Output new playback settings to GUI as success + outputSuccess("You have selected \"" + + shortName + "\" for playback."); + + File f = new File(userPlaybackHistoryFile); + if (!f.exists()) { + println("OpenBCI_GUI::playbackFileSelected: Playback history file not found."); + playbackHistoryFileExists = false; + } else { + try { + savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); + playbackHistoryFileExists = true; + } catch (RuntimeException e) { + outputError("Found an error in UserPlaybackHistory.json. Deleting this file. Please, Restart the GUI."); + File file = new File(userPlaybackHistoryFile); + if (!file.isDirectory()) { + file.delete(); + } + } + } + + //add playback file that was processed to the JSON history + savePlaybackFileToHistory(longName); + return true; +} + +void savePlaybackFileToHistory(String fileName) { + int maxNumHistoryFiles = 36; + if (playbackHistoryFileExists) { + println("Found user playback history file!"); + savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); + JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); + //println("ARRAYSIZE-Check1: " + int(recentFilesArray.size())); + //Recent file has recentFileNumber=0, and appears at the end of the JSON array + //check if already in the list, if so, remove from the list + removePlaybackFileFromHistory(recentFilesArray, playbackData_fname); + //next, increment fileNumber of all current entries +1 + for (int i = 0; i < recentFilesArray.size(); i++) { + JSONObject playbackFile = recentFilesArray.getJSONObject(i); + playbackFile.setInt("recentFileNumber", recentFilesArray.size()-i); + //println(recentFilesArray.size()-i); + playbackFile.setString("id", playbackFile.getString("id")); + playbackFile.setString("filePath", playbackFile.getString("filePath")); + recentFilesArray.setJSONObject(i, playbackFile); + } + //println("ARRAYSIZE-Check2: " + int(recentFilesArray.size())); + //append selected playback file to position 1 at the end of the JSONArray + JSONObject mostRecentFile = new JSONObject(); + mostRecentFile.setInt("recentFileNumber", 0); + mostRecentFile.setString("id", playbackData_ShortName); + mostRecentFile.setString("filePath", playbackData_fname); + recentFilesArray.append(mostRecentFile); + //remove entries greater than max num files + if (recentFilesArray.size() >= maxNumHistoryFiles) { + for (int i = 0; i <= recentFilesArray.size()-maxNumHistoryFiles; i++) { + recentFilesArray.remove(i); + println("ARRAY INDEX " + i + " REMOVED----"); + } + } + //println("ARRAYSIZE-Check3: " + int(recentFilesArray.size())); + //printArray(recentFilesArray); + + //save the JSON array and file + savePlaybackHistoryJSON.setJSONArray("playbackFileHistory", recentFilesArray); + saveJSONObject(savePlaybackHistoryJSON, userPlaybackHistoryFile); + + } else if (!playbackHistoryFileExists) { + println("Playback history file not found. making a new one."); + //do this if the file does not exist + JSONObject newHistoryFile; + newHistoryFile = new JSONObject(); + JSONArray newHistoryFileArray = new JSONArray(); + //save selected playback file to position 1 in recent file history + JSONObject mostRecentFile = new JSONObject(); + mostRecentFile.setInt("recentFileNumber", 0); + mostRecentFile.setString("id", playbackData_ShortName); + mostRecentFile.setString("filePath", playbackData_fname); + newHistoryFileArray.setJSONObject(0, mostRecentFile); + //newHistoryFile.setJSONArray("") + + //save the JSON array and file + newHistoryFile.setJSONArray("playbackFileHistory", newHistoryFileArray); + saveJSONObject(newHistoryFile, userPlaybackHistoryFile); + + //now the file exists! + println("Playback history JSON has been made!"); + playbackHistoryFileExists = true; + } +} + +void removePlaybackFileFromHistory(JSONArray array, String _filePath) { + //check if already in the list, if so, remove from the list + for (int i = 0; i < array.size(); i++) { + JSONObject playbackFile = array.getJSONObject(i); + //println("CHECKING " + i + " : " + playbackFile.getString("id") + " == " + fileName + " ?"); + if (playbackFile.getString("filePath").equals(_filePath)) { + array.remove(i); + //println("REMOVED: " + fileName); + } + } +} diff --git a/OpenBCI_GUI/W_PulseSensor.pde b/OpenBCI_GUI/W_PulseSensor.pde index ee75d6f8b..c010db895 100644 --- a/OpenBCI_GUI/W_PulseSensor.pde +++ b/OpenBCI_GUI/W_PulseSensor.pde @@ -1,384 +1,382 @@ - -//////////////////////////////////////////////////// -// -// W_PulseSensor.pde -// -// Created: Joel Murphy, Spring 2017 -// -///////////////////////////////////////////////////, - -class W_PulseSensor extends Widget { - - //to see all core variables/methods of the Widget class, refer to Widget.pde - //put your custom variables here... - - - color graphStroke = #d2d2d2; - color graphBG = #f5f5f5; - color textColor = #000000; - -// Pulse Sensor Visualizer Stuff - int count = 0; - int heart = 0; - int PulseBuffSize = 3*currentBoard.getSampleRate(); // Originally 400 - int BPMbuffSize = 100; - - int PulseWindowWidth; - int PulseWindowHeight; - int PulseWindowX; - int PulseWindowY; - int BPMwindowWidth; - int BPMwindowHeight; - int BPMwindowX; - int BPMwindowY; - int BPMposX; - int BPMposY; - int IBIposX; - int IBIposY; - int padding = 15; - color eggshell; - color pulseWave; - int[] PulseWaveY; // HOLDS HEARTBEAT WAVEFORM DATA - int[] BPMwaveY; // HOLDS BPM WAVEFORM DATA - boolean rising; - - // Synthetic Wave Generator Stuff - float theta; // Start angle at 0 - float amplitude; // Height of wave - int syntheticMultiplier; - long thisTime; - long thatTime; - int refreshRate; - - // Pulse Sensor Beat Finder Stuff - // ASSUMES 250Hz SAMPLE RATE - int[] rate; // array to hold last ten IBI values - int sampleCounter; // used to determine pulse timing - int lastBeatTime; // used to find IBI - int P =512; // used to find peak in pulse wave, seeded - int T = 512; // used to find trough in pulse wave, seeded - int thresh = 530; // used to find instant moment of heart beat, seeded - int amp = 0; // used to hold amplitude of pulse waveform, seeded - boolean firstBeat = true; // used to seed rate array so we startup with reasonable BPM - boolean secondBeat = false; // used to seed rate array so we startup with reasonable BPM - int BPM; // int that holds raw Analog in 0. updated every 2mS - int Signal; // holds the incoming raw data - int IBI = 600; // int that holds the time interval between beats! Must be seeded! - boolean Pulse = false; // "True" when User's live heartbeat is detected. "False" when not a "live beat". - int lastProcessedDataPacketInd = 0; - private Button analogModeButton; - - private AnalogCapableBoard analogBoard; - - W_PulseSensor(PApplet _parent){ - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - - analogBoard = (AnalogCapableBoard)currentBoard; - - // Pulse Sensor Stuff - eggshell = color(255, 253, 248); - pulseWave = BOLD_RED; - - PulseWaveY = new int[PulseBuffSize]; - BPMwaveY = new int[BPMbuffSize]; - rate = new int[10]; - setPulseWidgetVariables(); - initializePulseFinderVariables(); - - createAnalogModeButton("pulseSensorAnalogModeButton", "Turn Analog Read On", (int)(x0 + 1), (int)(y0 + navHeight + 1), 128, navHeight - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); - } - - void update(){ - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - - if(currentBoard instanceof DataSourcePlayback) { - if (((DataSourcePlayback)currentBoard) instanceof AnalogCapableBoard - && (!((AnalogCapableBoard)currentBoard).isAnalogActive())) { - return; - } - } - - List allData = currentBoard.getData(PulseBuffSize); - int[] analogChannels = analogBoard.getAnalogChannels(); - - for (int i=0; i < PulseBuffSize; i++ ) { - int signal = (int)(allData.get(i)[analogChannels[0]]); - //processSignal(signal); - PulseWaveY[i] = signal; - } - - double[][] frameData = currentBoard.getFrameData(); - for (int i = 0; i < frameData[0].length; i++) - { - int signal = (int)(frameData[analogChannels[0]][i]); - processSignal(signal); - } - - //ignore top left button interaction when widgetSelector dropdown is active - List cp5ElementsToCheck = new ArrayList(); - cp5ElementsToCheck.add((controlP5.Controller)analogModeButton); - lockElementsOnOverlapCheck(cp5ElementsToCheck); - - if (!analogBoard.canDeactivateAnalog()) { - analogModeButton.setLock(true); - analogModeButton.getCaptionLabel().setText("Analog Read On"); - analogModeButton.setColorBackground(BUTTON_LOCKED_GREY); - } - } - - private void updateOnOffButton() { - if (analogBoard.isAnalogActive()) { - - analogModeButton.setOn(); - } - else { - - analogModeButton.setOff(); - } - } - - void addBPM(int bpm) { - for(int i=0; i (IBI/5)*3){ // avoid dichrotic noise by waiting 3/5 of last IBI - if (sample < T){ // T is the trough - T = sample; // keep track of lowest point in pulse wave - } - } - - if(sample > thresh && sample > P){ // thresh condition helps avoid noise - P = sample; // P is the peak - } // keep track of highest point in pulse wave - - // NOW IT'S TIME TO LOOK FOR THE HEART BEAT - // signal surges up in value every time there is a pulse - if (N > 250){ // avoid high frequency noise - if ( (sample > thresh) && (Pulse == false) && (N > (IBI/5)*3) ){ - Pulse = true; // set the Pulse flag when we think there is a pulse - IBI = sampleCounter - lastBeatTime; // measure time between beats in mS - lastBeatTime = sampleCounter; // keep track of time for next pulse - - if(secondBeat){ // if this is the second beat, if secondBeat == TRUE - secondBeat = false; // clear secondBeat flag - for(int i=0; i<=9; i++){ // seed the running total to get a realisitic BPM at startup - rate[i] = IBI; - } - } - - if(firstBeat){ // if it's the first time we found a beat, if firstBeat == TRUE - firstBeat = false; // clear firstBeat flag - secondBeat = true; // set the second beat flag - // sei(); // enable interrupts again - return; // IBI value is unreliable so discard it - } - - - // keep a running total of the last 10 IBI values - int runningTotal = 0; // clear the runningTotal variable - - for(int i=0; i<=8; i++){ // shift data in the rate array - rate[i] = rate[i+1]; // and drop the oldest IBI value - runningTotal += rate[i]; // add up the 9 oldest IBI values - } - - rate[9] = IBI; // add the latest IBI to the rate array - runningTotal += rate[9]; // add the latest IBI to runningTotal - runningTotal /= 10; // average the last 10 IBI values - BPM = 60000/runningTotal; // how many beats can fit into a minute? that's BPM! - BPM = constrain(BPM,0,200); - addBPM(BPM); - } - } - - if (sample < thresh && Pulse == true){ // when the values are going down, the beat is over - // digitalWrite(blinkPin,LOW); // turn off pin 13 LED - Pulse = false; // reset the Pulse flag so we can do it again - amp = P - T; // get amplitude of the pulse wave - thresh = amp/2 + T; // set thresh at 50% of the amplitude - P = thresh; // reset these for next time - T = thresh; - } - - if (N > 2500){ // if 2.5 seconds go by without a beat - thresh = 530; // set thresh default - P = 512; // set P default - T = 512; // set T default - lastBeatTime = sampleCounter; // bring the lastBeatTime up to date - firstBeat = true; // set these to avoid noise - secondBeat = false; // when we get the heartbeat back - } - - // sei(); // enable interrupts when youre done! - }// end processSignal - - -}; + +//////////////////////////////////////////////////// +// +// W_PulseSensor.pde +// +// Created: Joel Murphy, Spring 2017 +// +///////////////////////////////////////////////////, + +class W_PulseSensor extends Widget { + + //to see all core variables/methods of the Widget class, refer to Widget.pde + //put your custom variables here... + color graphStroke = #d2d2d2; + color graphBG = #f5f5f5; + color textColor = #000000; + + // Pulse Sensor Visualizer Stuff + int count = 0; + int heart = 0; + int PulseBuffSize = 3*currentBoard.getSampleRate(); // Originally 400 + int BPMbuffSize = 100; + + int PulseWindowWidth; + int PulseWindowHeight; + int PulseWindowX; + int PulseWindowY; + int BPMwindowWidth; + int BPMwindowHeight; + int BPMwindowX; + int BPMwindowY; + int BPMposX; + int BPMposY; + int IBIposX; + int IBIposY; + int padding = 15; + color eggshell; + color pulseWave; + int[] PulseWaveY; // HOLDS HEARTBEAT WAVEFORM DATA + int[] BPMwaveY; // HOLDS BPM WAVEFORM DATA + boolean rising; + + // Synthetic Wave Generator Stuff + float theta; // Start angle at 0 + float amplitude; // Height of wave + int syntheticMultiplier; + long thisTime; + long thatTime; + int refreshRate; + + // Pulse Sensor Beat Finder Stuff + // ASSUMES 250Hz SAMPLE RATE + int[] rate; // array to hold last ten IBI values + int sampleCounter; // used to determine pulse timing + int lastBeatTime; // used to find IBI + int P =512; // used to find peak in pulse wave, seeded + int T = 512; // used to find trough in pulse wave, seeded + int thresh = 530; // used to find instant moment of heart beat, seeded + int amp = 0; // used to hold amplitude of pulse waveform, seeded + boolean firstBeat = true; // used to seed rate array so we startup with reasonable BPM + boolean secondBeat = false; // used to seed rate array so we startup with reasonable BPM + int BPM; // int that holds raw Analog in 0. updated every 2mS + int Signal; // holds the incoming raw data + int IBI = 600; // int that holds the time interval between beats! Must be seeded! + boolean Pulse = false; // "True" when User's live heartbeat is detected. "False" when not a "live beat". + int lastProcessedDataPacketInd = 0; + private Button analogModeButton; + + private AnalogCapableBoard analogBoard; + + W_PulseSensor(PApplet _parent){ + super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) + + analogBoard = (AnalogCapableBoard)currentBoard; + + // Pulse Sensor Stuff + eggshell = color(255, 253, 248); + pulseWave = BOLD_RED; + + PulseWaveY = new int[PulseBuffSize]; + BPMwaveY = new int[BPMbuffSize]; + rate = new int[10]; + setPulseWidgetVariables(); + initializePulseFinderVariables(); + + createAnalogModeButton("pulseSensorAnalogModeButton", "Turn Analog Read On", (int)(x0 + 1), (int)(y0 + navHeight + 1), 128, navHeight - 3, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + } + + void update(){ + super.update(); //calls the parent update() method of Widget (DON'T REMOVE) + + if(currentBoard instanceof DataSourcePlayback) { + if (((DataSourcePlayback)currentBoard) instanceof AnalogCapableBoard + && (!((AnalogCapableBoard)currentBoard).isAnalogActive())) { + return; + } + } + + List allData = currentBoard.getData(PulseBuffSize); + int[] analogChannels = analogBoard.getAnalogChannels(); + + for (int i=0; i < PulseBuffSize; i++ ) { + int signal = (int)(allData.get(i)[analogChannels[0]]); + //processSignal(signal); + PulseWaveY[i] = signal; + } + + double[][] frameData = currentBoard.getFrameData(); + for (int i = 0; i < frameData[0].length; i++) + { + int signal = (int)(frameData[analogChannels[0]][i]); + processSignal(signal); + } + + //ignore top left button interaction when widgetSelector dropdown is active + List cp5ElementsToCheck = new ArrayList(); + cp5ElementsToCheck.add((controlP5.Controller)analogModeButton); + lockElementsOnOverlapCheck(cp5ElementsToCheck); + + if (!analogBoard.canDeactivateAnalog()) { + analogModeButton.setLock(true); + analogModeButton.getCaptionLabel().setText("Analog Read On"); + analogModeButton.setColorBackground(BUTTON_LOCKED_GREY); + } + } + + private void updateOnOffButton() { + if (analogBoard.isAnalogActive()) { + + analogModeButton.setOn(); + } + else { + + analogModeButton.setOff(); + } + } + + void addBPM(int bpm) { + for(int i=0; i (IBI/5)*3){ // avoid dichrotic noise by waiting 3/5 of last IBI + if (sample < T){ // T is the trough + T = sample; // keep track of lowest point in pulse wave + } + } + + if(sample > thresh && sample > P){ // thresh condition helps avoid noise + P = sample; // P is the peak + } // keep track of highest point in pulse wave + + // NOW IT'S TIME TO LOOK FOR THE HEART BEAT + // signal surges up in value every time there is a pulse + if (N > 250){ // avoid high frequency noise + if ( (sample > thresh) && (Pulse == false) && (N > (IBI/5)*3) ){ + Pulse = true; // set the Pulse flag when we think there is a pulse + IBI = sampleCounter - lastBeatTime; // measure time between beats in mS + lastBeatTime = sampleCounter; // keep track of time for next pulse + + if(secondBeat){ // if this is the second beat, if secondBeat == TRUE + secondBeat = false; // clear secondBeat flag + for(int i=0; i<=9; i++){ // seed the running total to get a realisitic BPM at startup + rate[i] = IBI; + } + } + + if(firstBeat){ // if it's the first time we found a beat, if firstBeat == TRUE + firstBeat = false; // clear firstBeat flag + secondBeat = true; // set the second beat flag + // sei(); // enable interrupts again + return; // IBI value is unreliable so discard it + } + + + // keep a running total of the last 10 IBI values + int runningTotal = 0; // clear the runningTotal variable + + for(int i=0; i<=8; i++){ // shift data in the rate array + rate[i] = rate[i+1]; // and drop the oldest IBI value + runningTotal += rate[i]; // add up the 9 oldest IBI values + } + + rate[9] = IBI; // add the latest IBI to the rate array + runningTotal += rate[9]; // add the latest IBI to runningTotal + runningTotal /= 10; // average the last 10 IBI values + BPM = 60000/runningTotal; // how many beats can fit into a minute? that's BPM! + BPM = constrain(BPM,0,200); + addBPM(BPM); + } + } + + if (sample < thresh && Pulse == true){ // when the values are going down, the beat is over + // digitalWrite(blinkPin,LOW); // turn off pin 13 LED + Pulse = false; // reset the Pulse flag so we can do it again + amp = P - T; // get amplitude of the pulse wave + thresh = amp/2 + T; // set thresh at 50% of the amplitude + P = thresh; // reset these for next time + T = thresh; + } + + if (N > 2500){ // if 2.5 seconds go by without a beat + thresh = 530; // set thresh default + P = 512; // set P default + T = 512; // set T default + lastBeatTime = sampleCounter; // bring the lastBeatTime up to date + firstBeat = true; // set these to avoid noise + secondBeat = false; // when we get the heartbeat back + } + + // sei(); // enable interrupts when youre done! + }// end processSignal + + +}; From 489c27af30a9af34e76d9e9718a51a44555aee48 Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Fri, 5 May 2023 15:08:42 -0500 Subject: [PATCH 6/8] Add comment clarifying usage of MenuList Class in CustomCp5Classes.pde --- OpenBCI_GUI/CustomCp5Classes.pde | 1 + 1 file changed, 1 insertion(+) diff --git a/OpenBCI_GUI/CustomCp5Classes.pde b/OpenBCI_GUI/CustomCp5Classes.pde index ced72f01b..0bcbc8c57 100644 --- a/OpenBCI_GUI/CustomCp5Classes.pde +++ b/OpenBCI_GUI/CustomCp5Classes.pde @@ -130,6 +130,7 @@ class ButtonHelpText{ //////////////////////////////////////////////////////////////////////////////////// // MENULIST CLASS // // Based on ControlP5 Processing Library example, written by Andreas Schlegel // +// WARNING: This class does not respond well to being resized -RW 5/5/23 // //////////////////////////////////////////////////////////////////////////////////// public class MenuList extends controlP5.Controller { From 131188660943e12c168bc647174f7ec30a23fb2d Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Tue, 9 May 2023 16:44:36 -0500 Subject: [PATCH 7/8] Fix bug in names of SessionSettings getPath() method --- OpenBCI_GUI/SessionSettings.pde | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OpenBCI_GUI/SessionSettings.pde b/OpenBCI_GUI/SessionSettings.pde index 865dfb2d3..e528f9a4a 100644 --- a/OpenBCI_GUI/SessionSettings.pde +++ b/OpenBCI_GUI/SessionSettings.pde @@ -1027,14 +1027,14 @@ class SessionSettings { } else if (dataSource == DATASOURCE_GANGLION) { filePath += fileNames[2]; } else if (dataSource == DATASOURCE_PLAYBACKFILE) { - filePath += fileNames[4]; + filePath += fileNames[3]; } else if (dataSource == DATASOURCE_SYNTHETIC) { if (_nchan == NCHAN_GANGLION) { - filePath += fileNames[5]; + filePath += fileNames[4]; } else if (_nchan == NCHAN_CYTON) { - filePath += fileNames[6]; + filePath += fileNames[5]; } else { - filePath += fileNames[7]; + filePath += fileNames[6]; } } } From a83527f0a731f98a3097097d13323a9f022c83af Mon Sep 17 00:00:00 2001 From: Richard Waltman Date: Mon, 15 May 2023 13:34:32 -0500 Subject: [PATCH 8/8] Update changelog and bump to version 5.1.1-alpha.3 --- CHANGELOG.md | 1 + OpenBCI_GUI/OpenBCI_GUI.pde | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8356817b..2ccf3d8b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Improvements - Update to BrainFlow 5.6.1 - Add feature to connect to Ganglion using Native Bluetooth #1080 +- Refactor the creation and playback of OpenBCI GUI CSV files #1119 # v5.1.0 diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index cf47d2786..8569825ca 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -61,8 +61,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.1.1-alpha.2"; -String localGUIVersionDate = "February 2023"; +String localGUIVersionString = "v5.1.1-alpha.3"; +String localGUIVersionDate = "May 2023"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; Boolean guiIsUpToDate;