diff --git a/.github/workflows/mergenovadev.yml b/.github/workflows/mergenovadev.yml deleted file mode 100644 index d8e0f1f93..000000000 --- a/.github/workflows/mergenovadev.yml +++ /dev/null @@ -1,20 +0,0 @@ - -on: - push: - branches: [ development ] - -jobs: - sync-branch: - runs-on: ubuntu-latest - steps: - - name: Branch Merge - uses: everlytic/branch-merge@1.1.0 - with: - github_token: ${{ github.token }} - # Branch name or Ref that you wish to merge into the target_branch. - source_ref: development - # Branch you are merging the source ref into. - target_branch: novaxr-dev - # Template to generate the commit message, see README.md for more info - commit_message_template: Merged {source_ref} into {target_branch}. - diff --git a/.gitignore b/.gitignore index c2b853dcd..2dbd1d293 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ libBoardController.so libDataHandler.so libGanglionLib.so libGanglionScan.so +libunicorn.so diff --git a/.travis.yml b/.travis.yml index bd88487eb..c81e354aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ jobs: branches: only: - master - - novaxr-dev before_install: - if [ "$TRAVIS_OS_NAME" = osx ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then openssl aes-256-cbc -K $encrypted_2f5d2771e3cb_key -iv $encrypted_2f5d2771e3cb_iv -in release_script/mac_only/Certificates.p12.enc -out release_script/mac_only/Certificates.p12 -d; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e53da4b0..892b8d6a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# v5.0.4 + +### Improvements +* Add Copy/Paste for all textfields on all OS #940 +* Update BrainFlow library to version that includes a marker channel +* Handle paths with spaces on Linux Standalone GUI #916 +* Allow Expert Ganglion Users to send square wave commands via keyboard #950 +* Show Send Custom Hardware Command UI for Cyton Expert Mode in Hardware Settings +* Improve Hardware Setting UX/UI for ADS1299 boards #954 + +### Bug Fixes +* Clean up GUI code to fix Processing/JVM memory issue causing crash #955 +* Avoid playback history file not found exception #959 +* Fix issue with Spectrogram Widget data image default height +* Fix issue with Accelerometer Widget graph default vertical scale +* Fix text drawing in wrong spot in Session Data box in Control Panel + # v5.0.3 ### Improvements diff --git a/Networking-Test-Kit/LSL/brainflow_lsl.py b/Networking-Test-Kit/LSL/brainflow_lsl.py new file mode 100644 index 000000000..9a819f5a5 --- /dev/null +++ b/Networking-Test-Kit/LSL/brainflow_lsl.py @@ -0,0 +1,147 @@ +############################################################################# +## BrainFlow + LSL ## +## Use BrainFlow to read data from board send it as an LSL stream ## +############################################################################# + +# Install dependencies with: +# pip install --upgrade numpy brainflow pylsl + +# Here are example commands using Cyton and get_exg_channels()from BrainFlow. This has only been tested with Cyton + Dongle, for now. + +# Mac: +# python3 Networking-Test-Kit/LSL/brainflow_lsl.py --board-id 2 --serial-port /dev/cu.usbserial-DM00D7TW --name test --data-type EXG --channel-names 1,2,3,4,5,6,7,8 --uid brainflow + +# Windows: +# python3 Networking-Test-Kit/LSL/brainflow_lsl.py --board-id 2 --serial-port COM3 --name test --data-type EXG --channel-names 1,2,3,4,5,6,7,8 --uid brainflow + +import argparse +import time +import numpy as np + +from queue import Queue + +import brainflow +from brainflow.board_shim import BoardShim, BrainFlowInputParams +from brainflow.data_filter import DataFilter, FilterTypes, AggOperations + +from pylsl import StreamInfo, StreamOutlet, local_clock + +def channel_select(board, board_id, data_type): + switcher = { + 'EXG': board.get_exg_channels(board_id), + # can add more + } + + return switcher.get(data_type, "error") + +def main(): + BoardShim.enable_dev_board_logger() + + parser = argparse.ArgumentParser() + + # brainflow params - use docs to check which parameters are required for specific board, e.g. for Cyton set serial port + parser.add_argument('--timeout', type=int, help='timeout for device discovery or connection', required=False, default=0) + parser.add_argument('--ip-address', type=str, help='ip address', required=False, default='') + parser.add_argument('--board-id', type=int, help='board id, check docs to get a list of supported boards', required=True) + parser.add_argument('--serial-port', type=str, help='serial port', required=False, default='') + parser.add_argument('--streamer-params', type=str, help='streamer params', required=False, default='') + + # LSL params + parser.add_argument('--name', type=str, help='name', required=True) + parser.add_argument('--data-type', type=str, help='data type', required=True) + parser.add_argument('--channel-names', type=str, help='channel names', required=True) + parser.add_argument('--uid', type=str, help='uid', required=True) + + args = parser.parse_args() + + # brainflow initialization + params = BrainFlowInputParams() + params.serial_port = args.serial_port + params.ip_address = args.ip_address + board = BoardShim(args.board_id, params) + + # LSL initialization + channel_names = args.channel_names.split(',') + n_channels = len(channel_names) + srate = board.get_sampling_rate(args.board_id) + info = StreamInfo(args.name, args.data_type, n_channels, srate, 'double64', args.uid) + outlet = StreamOutlet(info) + fw_delay = 0 + + # prepare session + board.prepare_session() + + # send commands to the board for every channel. Cyton has 8 Channels. Here, we turn off every channel except for 1 and 8. + # This is here for testing purposes. + #board.config_board("x1000110X") #Lower the gain to 1x on channel 1 + #board.config_board("x1061000X") + #board.config_board("x2161000X") + #board.config_board("x3161000X") + #board.config_board("x4161000X") + #board.config_board("x5161000X") + #board.config_board("x6161000X") + #board.config_board("x7161000X") + #board.config_board("x8060110X") + + # start stream + board.start_stream(45000, args.streamer_params) + time.sleep(1) + start_time = local_clock() + sent_samples = 0 + queue = Queue(maxsize = 5*srate) + chans = channel_select(board, args.board_id, args.data_type) + + # Vars for filters + applyBandStop = True + applyBandPass = True + bandStopFrequency = 60.0 + bp_lowerBound = 5.0 + bp_upperBound = 50.0 + bp_centerFreq = (bp_upperBound + bp_lowerBound) / 2.0; + bp_bandWidth = bp_upperBound - bp_lowerBound + + + # read data with brainflow and send it via LSL + print("Now sending data...") + while True: + data = board.get_board_data()[chans] + + # It's best to apply filters on the receiving end, but this is here just for testing purposes. + """ + for chan in range(len(chans)): + if applyBandStop: + DataFilter.perform_bandstop(data[chan], + BoardShim.get_sampling_rate(args.board_id), + bandStopFrequency, + 4.0, + 2, + FilterTypes.BUTTERWORTH.value, + 0); + if applyBandPass: + DataFilter.perform_bandpass( + data[chan], + BoardShim.get_sampling_rate(args.board_id), + bp_centerFreq, + bp_bandWidth, + 2, + FilterTypes.BUTTERWORTH.value, + 0); + """ + + for i in range(len(data[0])): + queue.put(data[:,i].tolist()) + elapsed_time = local_clock() - start_time + required_samples = int(srate * elapsed_time) - sent_samples + if required_samples > 0 and queue.qsize() >= required_samples: + mychunk = [] + + for i in range(required_samples): + mychunk.append(queue.get()) + stamp = local_clock() - fw_delay + outlet.push_chunk(mychunk, stamp) + sent_samples += required_samples + time.sleep(1) + + +if __name__ == "__main__": + main() diff --git a/Networking-Test-Kit/LSL/lslStreamTest.py b/Networking-Test-Kit/LSL/lslStreamTest.py index 79baf2a19..6fce85048 100644 --- a/Networking-Test-Kit/LSL/lslStreamTest.py +++ b/Networking-Test-Kit/LSL/lslStreamTest.py @@ -15,20 +15,25 @@ def testLSLSamplingRate(): start = time.time() - numSamples = 0 + totalNumSamples = 0 + validSamples = 0 numChunks = 0 while time.time() <= start + duration: # get chunks of samples samples, timestamp = inlet.pull_chunk() - if timestamp: + if samples: numChunks += 1 print( len(samples) ) - numSamples += len(samples) + totalNumSamples += len(samples) # print(samples); + for sample in samples: + print(sample) + validSamples += 1 - print( "Number of Chunks == {}".format(numChunks) ) - print( "Avg Sampling Rate == {}".format(numSamples / duration) ) + print( "Number of Chunks and Samples == {} , {}".format(numChunks, totalNumSamples) ) + print( "Valid Samples and Duration == {} / {}".format(validSamples, duration) ) + print( "Avg Sampling Rate == {}".format(validSamples / duration) ) testLSLSamplingRate() \ No newline at end of file diff --git a/Networking-Test-Kit/LSL/lslStreamTest_Chan1Pulse.py b/Networking-Test-Kit/LSL/lslStreamTest_Chan1Pulse.py new file mode 100644 index 000000000..4a742a7bf --- /dev/null +++ b/Networking-Test-Kit/LSL/lslStreamTest_Chan1Pulse.py @@ -0,0 +1,58 @@ +"""Example program to show how to read a multi-channel time series from LSL.""" +import time +from pylsl import StreamInlet, resolve_stream +from time import sleep +import numpy as np +import matplotlib.pyplot as plt +from matplotlib import style +from collections import deque + +# first resolve an EEG stream on the lab network +print("looking for an EEG stream...") +streams = resolve_stream('type', 'EXG') + +# create a new inlet to read from the stream +inlet = StreamInlet(streams[0]) +duration = 10 + +sleep(0) + +def testLSLSamplingRate(): + start = time.time() + numSamples = 0 + numChunks = 0 + + while time.time() <= start + duration: + # get chunks of samples + chunk, timestamp = inlet.pull_chunk() + if timestamp: + numChunks += 1 + for sample in chunk: + numSamples += 1 + + print( "Number of Chunks == {}".format(numChunks) ) + print( "Avg Sampling Rate == {}".format(numSamples / duration) ) + + +# testLSLSamplingRate() + +print("gathering data to plot...") + +def testLSLPulseData(): + start = time.time() + raw_pulse_signal = [] + + while time.time() <= start + duration: + chunk, timestamp = inlet.pull_chunk() + if timestamp: + for sample in chunk: + print(sample) + raw_pulse_signal.append(sample[0]) + + # print(raw_pulse_signal) + print( "Avg Sampling Rate == {}".format(len(raw_pulse_signal) / duration) ) + plt.plot(raw_pulse_signal) + plt.ylabel('raw analog signal') + plt.show() + +testLSLPulseData() \ No newline at end of file diff --git a/OpenBCI_GUI/ADS1299SettingsController.pde b/OpenBCI_GUI/ADS1299SettingsController.pde index e715824d1..97dace96a 100644 --- a/OpenBCI_GUI/ADS1299SettingsController.pde +++ b/OpenBCI_GUI/ADS1299SettingsController.pde @@ -93,6 +93,8 @@ class ADS1299SettingsController { if (tfactive) { textFieldIsActive = true; } + + copyPaste.checkForCopyPaste(customCommandTF); } public void draw() { @@ -104,12 +106,14 @@ class ADS1299SettingsController { stroke(OBJECT_BORDER_GREY); fill(0, 0, 0, 100); rect(x, y - columnLabelH, w, columnLabelH); + popStyle(); //background pushStyle(); noStroke(); fill(0, 0, 0, 100); rect(x, y, w + 1, h); + popStyle(); gainLabel.draw(); inputTypeLabel.draw(); @@ -130,26 +134,27 @@ class ADS1299SettingsController { fill(color(57, 128, 204, 190)); //light blue from TopNav //fill(color(245, 64, 64, 180)); //light red rect(x, y + chanBar_h * i, w, chanBar_h); + popStyle(); } } + boolean showCustomCommandUI = settings.expertModeToggle; + //Draw background behind command buttons pushStyle(); fill(0, 0, 0, 100); rect(x, y + h, w + 1, commandBarH); - - boolean showCustomCommandUI = settings.expertModeToggle && !(currentBoard instanceof BoardCyton); - customCommandTF.setVisible(showCustomCommandUI); - sendCustomCmdButton.setVisible(showCustomCommandUI); if (showCustomCommandUI) { rect(customCmdUI_x, y + h + commandBarH, customCmdUI_w, commandBarH); //keep above style for other command buttons } + popStyle(); + + customCommandTF.setVisible(showCustomCommandUI); + sendCustomCmdButton.setVisible(showCustomCommandUI); //Draw cp5 objects on top of everything hwsCp5.draw(); } - - popStyle(); } private void resizeDropdowns(int _channelBarHeight) { @@ -274,20 +279,26 @@ class ADS1299SettingsController { sendButton.setDescription("Send hardware settings to the board."); sendButton.onClick(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - - boolean[] sendCommandSuccess = ((ADS1299SettingsBoard)currentBoard).getADS1299Settings().commitAll(); - boolean noErrors = true; - for (int i = 0; i < sendCommandSuccess.length; i++) { - if (!sendCommandSuccess[i]) { - noErrors = false; - } else { - hasUnappliedChanges[i] = false; - boardSettings.saveLastValues(i); + boolean noErrors = true; + boolean atLeastOneChannelHasChanged = false; + + for (int i = 0; i < channelCount; i++) { + if (hasUnappliedChanges[i]) { + boolean sendCommandSuccess = ((ADS1299SettingsBoard)currentBoard).getADS1299Settings().commit(i); + if (!sendCommandSuccess) { + noErrors = false; + } else { + setHasUnappliedSettings(i, false); + atLeastOneChannelHasChanged = true; + boardSettings.saveLastValues(i); + } } } - if (noErrors) { + if (!atLeastOneChannelHasChanged) { + output("No new settings to send to board."); + } else if (noErrors) { output("Hardware Settings sent to board!"); } else { PopupMessage msg = new PopupMessage("Error", "Failed to send one or more Hardware Settings to board. Check hardware and battery level. Cyton users, check that your dongle is connected with blue light shining."); @@ -440,6 +451,20 @@ class ADS1299SettingsController { sendCustomCmdButton.setSize(but_w, tf_h - 1); } + private void updateHasUnappliedSettings(int _channel) { + hasUnappliedChanges[_channel] = !boardSettings.equalsLastValues(_channel); + } + + public void updateHasUnappliedSettings() { + for (int i : activeChannels) { + updateHasUnappliedSettings(i); + } + } + + public void setHasUnappliedSettings(int _channel, boolean b) { + hasUnappliedChanges[_channel] = b; + } + public void updateChanSettingsDropdowns(int chan, boolean isActive) { color darkNotActive = color(57); color c = isActive ? color(255) : darkNotActive; @@ -467,7 +492,7 @@ class ADS1299SettingsController { srb1Lists[chan].setColorBackground(c); srb1Lists[chan].setLock(!isActive); - hasUnappliedChanges[chan] = false; + } private class SLCallbackListener implements CallbackListener { @@ -487,29 +512,24 @@ class ADS1299SettingsController { if (myEnum instanceof Gain) { //verbosePrint("HardwareSettings: previousVal == " + boardSettings.previousValues.gain[channel]); - hasUnappliedChanges[channel] = (Gain)myEnum != boardSettings.values.gain[channel]; boardSettings.values.gain[channel] = (Gain)myEnum; } else if (myEnum instanceof InputType) { - hasUnappliedChanges[channel] = (InputType)myEnum != boardSettings.values.inputType[channel]; boardSettings.values.inputType[channel] = (InputType)myEnum; } else if (myEnum instanceof Bias) { - hasUnappliedChanges[channel] = (Bias)myEnum != boardSettings.values.bias[channel]; boardSettings.values.bias[channel] = (Bias)myEnum; _bgColor = (Bias)myEnum == Bias.INCLUDE ? yesOnColor : noOffColor; (theEvent.getController()).setColorBackground(_bgColor); } else if (myEnum instanceof Srb2) { - hasUnappliedChanges[channel] = (Srb2)myEnum != boardSettings.values.srb2[channel]; boardSettings.values.srb2[channel] = (Srb2)myEnum; _bgColor = (Srb2)myEnum == Srb2.CONNECT ? yesOnColor : noOffColor; (theEvent.getController()).setColorBackground(_bgColor); } else if (myEnum instanceof Srb1) { - hasUnappliedChanges[channel] = (Srb1)myEnum != boardSettings.values.srb1[channel]; boardSettings.values.srb1[channel] = (Srb1)myEnum; _bgColor = (Srb1)myEnum == Srb1.CONNECT ? yesOnColor : noOffColor; (theEvent.getController()).setColorBackground(_bgColor); } - hasUnappliedChanges[channel] = !boardSettings.equalsLastValues(channel); + updateHasUnappliedSettings(channel); } } } @@ -524,6 +544,7 @@ void loadHardwareSettings(File selection) { outputSuccess("Hardware Settings Loaded!"); for (int i = 0; i < nchan; i++) { w_timeSeries.adsSettingsController.updateChanSettingsDropdowns(i, currentBoard.isEXGChannelActive(i)); + w_timeSeries.adsSettingsController.updateHasUnappliedSettings(i); } } else { outputError("Failed to load Hardware Settings."); diff --git a/OpenBCI_GUI/ControlPanel.pde b/OpenBCI_GUI/ControlPanel.pde index 005ee0d50..9e00d2949 100644 --- a/OpenBCI_GUI/ControlPanel.pde +++ b/OpenBCI_GUI/ControlPanel.pde @@ -179,10 +179,6 @@ class ControlPanel { public void draw() { - pushStyle(); - - noStroke(); - initBox.draw(); if (systemMode == 10) { @@ -272,8 +268,6 @@ class ControlPanel { text(stopInstructions, x + globalPadding*2, y + globalPadding*3, w - globalPadding*4, dataSourceBox.h - globalPadding*4); popStyle(); } - - popStyle(); } public void hideRadioPopoutBox() { @@ -340,11 +334,11 @@ class DataSourceBox { sourceList.setPosition(_x, _y); // sourceList.itemHeight = 28; // sourceList.padding = 9; - sourceList.addItem("CYTON (live)", DATASOURCE_CYTON); - sourceList.addItem("GANGLION (live)", DATASOURCE_GANGLION); if (galeaEnabled) { sourceList.addItem("GALEA (live)", DATASOURCE_GALEA); } + sourceList.addItem("CYTON (live)", DATASOURCE_CYTON); + sourceList.addItem("GANGLION (live)", DATASOURCE_GANGLION); sourceList.addItem("PLAYBACK (from file)", DATASOURCE_PLAYBACKFILE); sourceList.addItem("SYNTHETIC (algorithmic)", DATASOURCE_SYNTHETIC); sourceList.addItem("STREAMING (from external)", DATASOURCE_STREAMING); @@ -788,6 +782,7 @@ class WifiBox { public void update() { wifiList.updateMenu(); + copyPaste.checkForCopyPaste(staticIPAddressTF); } public void draw() { @@ -829,6 +824,7 @@ class WifiBox { textFont(h3, 16); textAlign(LEFT, TOP); text(boardIpInfo, x + w/2 - textWidth(boardIpInfo)/2, y + h - padding - 46); + popStyle(); if (wifiIsRefreshing){ //Display spinning cog gif @@ -1183,7 +1179,7 @@ class SessionDataBox { } public void update() { - + copyPaste.checkForCopyPaste(sessionNameTextfield); } public void draw() { @@ -1217,9 +1213,7 @@ class SessionDataBox { fill(OPENBCI_DARKBLUE); maxDurationDropdown.setPosition(x + maxDurTextWidth, int(outputODF.getPosition()[1]) + 24 + padding); //Carefully draw some text to the left of above dropdown, otherwise this text moves when changing WiFi mode - int extraPadding = (controlPanel.getWifiSearchStyle() == controlPanel.WIFI_STATIC) || selectedProtocol != BoardProtocol.WIFI - ? 20 - : 5; + int extraPadding = 20; fill(OPENBCI_DARKBLUE); textFont(p4, 14); text("Max File Duration", maxDurText_x, y + h - 24 - padding + extraPadding); @@ -1879,6 +1873,15 @@ class RecentPlaybackBox { private void getRecentPlaybackFiles() { int numFilesToShow = 10; + + File f = new File(userPlaybackHistoryFile); + if (!f.exists()) { + println("OpenBCI_GUI::Control Panel: Playback history file not found."); + recentPlaybackFilesHaveUpdated = true; + playbackHistoryFileExists = false; + return; + } + try { JSONObject playbackHistory = loadJSONObject(userPlaybackHistoryFile); JSONArray recentFilesArray = playbackHistory.getJSONArray("playbackFileHistory"); @@ -1902,7 +1905,7 @@ class RecentPlaybackBox { playbackHistoryFileExists = true; } catch (Exception e) { - println("OpenBCI_GUI::Control Panel: Playback history file not found or other error."); + println("OpenBCI_GUI::Control Panel: Other error! Please submit an issue on Github and share this console log."); println(e.getMessage()); playbackHistoryFileExists = false; } @@ -1968,7 +1971,8 @@ class GaleaBox { private final String boxLabel = "GALEA CONFIG"; private final String ipAddressLabel = "IP Address"; private final String sampleRateLabel = "Sample Rate"; - private String ipAddress = "192.168.4.1"; + //private String ipAddress = "192.168.4.1"; + private String ipAddress = "127.0.0.1"; //For use with testing emulator private ControlP5 localCP5; private Textfield ipAddressTF; private ScrollableList srList; @@ -1991,7 +1995,7 @@ class GaleaBox { } public void update() { - // nothing + copyPaste.checkForCopyPaste(ipAddressTF); } public void draw() { @@ -2204,7 +2208,8 @@ class StreamingBoardBox { } public void update() { - // nothing + copyPaste.checkForCopyPaste(ipAddress); + copyPaste.checkForCopyPaste(port); } public void draw() { diff --git a/OpenBCI_GUI/DataSourcePlayback.pde b/OpenBCI_GUI/DataSourcePlayback.pde index b70b1258a..3362ad7d2 100644 --- a/OpenBCI_GUI/DataSourcePlayback.pde +++ b/OpenBCI_GUI/DataSourcePlayback.pde @@ -12,6 +12,7 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo private Board underlyingBoard = null; private int sampleRate = -1; + private int numChannels = 0; // use it instead getTotalChannelCount() method for old playback files DataSourcePlayback(String filePath) { playbackFilePath = filePath; @@ -111,9 +112,13 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo for (int iData=0; iData list = getData(numNewSamplesThisFrame); for (int i = 0; i < numNewSamplesThisFrame; i++) { - for (int j = 0; j < getTotalChannelCount(); j++) { + for (int j = 0; j < numChannels; j++) { array[j][i] = list.get(i)[j]; } } @@ -245,7 +252,7 @@ class DataSourcePlayback implements DataSource, AccelerometerCapableBoard, Analo if (maxSamples > currentSample) { int sampleDiff = maxSamples - currentSample; - double[] emptyData = new double[getTotalChannelCount()]; + double[] emptyData = new double[numChannels]; ArrayList newResult = new ArrayList(maxSamples); for (int i=0; i= getTotalSamples(); } -} \ No newline at end of file +} diff --git a/OpenBCI_GUI/Extras.pde b/OpenBCI_GUI/Extras.pde index 72e677d1e..50a407345 100644 --- a/OpenBCI_GUI/Extras.pde +++ b/OpenBCI_GUI/Extras.pde @@ -75,6 +75,19 @@ String dropNonPrintableChars(String myString) return newString.toString(); } +float getFontStringHeight(PFont _font, String string) { + float minY = Float.MAX_VALUE; + float maxY = Float.NEGATIVE_INFINITY; + for (Character c : string.toCharArray()) { + PShape character = _font.getShape(c); // create character vector + for (int i = 0; i < character.getVertexCount(); i++) { + minY = min(character.getVertex(i).y, minY); + maxY = max(character.getVertex(i).y, maxY); + } + } + return maxY - minY; +} + ////////////////////////////////////////////////// // // Some functions to implement some math and some filtering. These functions @@ -300,6 +313,9 @@ class DataStatus { public color getColor() { return colorIndicator; } + public double getPercentage() { + return percentage; + } }; class FilterConstants { @@ -395,11 +411,14 @@ class TextBox { fill(backgroundColor); rect(xbox,ybox,w,h); } + popStyle(); + //draw the text itself pushStyle(); noStroke(); fill(textColor); textAlign(alignH,alignV); + textFont(font); text(string,x,y); strokeWeight(1); popStyle(); diff --git a/OpenBCI_GUI/GClip.pde b/OpenBCI_GUI/GClip.pde new file mode 100644 index 000000000..1cf20861d --- /dev/null +++ b/OpenBCI_GUI/GClip.pde @@ -0,0 +1,185 @@ +/* +// EXAMPLE USAGE -- WORKS!!!!! + + String n1, n2; + // String to copy to clipboard + n1 = "Harry Potter"; + // Copy to clipboard + //GClip.copy(n1); + + // Get clipboard contents into another string + n2 = GClip.paste(); + // Display new string + println(n2); +/* + + +/* +Part of the GUI for Processing library + http://www.lagers.org.uk/g4p/index.html + http://gui4processing.googlecode.com/svn/trunk/ + + Copyright (c) 2008-09 Peter Lager + +The actual code to create the clipbaord, copy and paste were +taken taken from a similar GUI library Interfascia ALPHA 002 -- +http://superstable.net/interfascia/ produced by Brenden Berg +The main change is to provide static copy and paste methods to +separate the clipboard logic from the component logic and provide +global access. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General +Public License along with this library; if not, write to the +Free Software Foundation, Inc., 59 Temple Place, Suite 330, +Boston, MA 02111-1307 USA +*/ + +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.ClipboardOwner; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; + +/* +* I wanted to implement copying and pasting to the clipboard using static +* methods to simplify the sharing of a single clipboard over all classes. +* The need to implement the ClipboardOwner interface requires an object so +* this class creates an object the first time an attempt to copy or paste +* is used. +* +* All methods are private except copy() and paste() - lostOwnership() +* has to be public because of the Clipboard owner interface. +* +* @author Peter Lager +* +*/ + +/** +* This provides clipboard functionality for text and is currently only used by the +* GTextField class. +* +* @author Peter Lager +*/ +public static class GClip implements ClipboardOwner { + /** + * Static reference to enforce singleton pattern + */ + private static GClip gclip = null; + + /** + * Class attribute to reference the programs clipboard + */ + private Clipboard clipboard = null; + + /** + * Copy a string to the clipboard + * @param chars + */ + public static boolean copy(String chars) { + if (gclip == null) gclip = new GClip(); + return gclip.copyString(chars); + } + + /** + * Get a string from the clipboard + * @return the string on the clipboard + */ + public static String paste() { + if (gclip == null) gclip = new GClip(); + return gclip.pasteString(); + } + + /** + * Ctor is private so clipboard is only created when a copy or paste is + * attempted and one does not exist already. + */ + private GClip() { + if (clipboard == null) { + makeClipboardObject(); + } + } + + /** + * If security permits use the system clipboard otherwise create + * our own application clipboard. + */ + private void makeClipboardObject() { + SecurityManager security = System.getSecurityManager(); + if (security != null) { + try { + ///security.checkPermission(AWTPermission("accessEventQueue")); + clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + } catch (SecurityException e) { + clipboard = new Clipboard("Application Clipboard"); + } + } + else { + try { + clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + } catch (Exception e) { + // THIS IS DUMB - true but is there another way - I think not + } + } + } + + /** + * Copy a string to the clipboard. If the Clipboard has not been created + * then create it. + * @return true for a successful copy to clipboard + */ + private boolean copyString(String chars) { + if (clipboard == null) { + makeClipboardObject(); + } + if (clipboard != null) { + StringSelection fieldContent = new StringSelection (chars); + clipboard.setContents (fieldContent, this); + return true; + } + return false; + } + + /** + * Gets a string from the clipboard. If there is no Clipboard + * then create it. + * @return + */ + private String pasteString() { + // If there is no clipboard then there is nothing to paste + if (clipboard == null) { + makeClipboardObject(); + return ""; + } + // We have a clipboard so get the string if we can + Transferable clipboardContent = clipboard.getContents(this); + if ((clipboardContent != null) && + (clipboardContent.isDataFlavorSupported(DataFlavor.stringFlavor))) { + try { + String tempString; + tempString = (String) clipboardContent.getTransferData(DataFlavor.stringFlavor); + return tempString; + } catch (Exception e) { + e.printStackTrace (); + } + } + return ""; + } + + /** + * Reqd by ClipboardOwner interface + */ + public void lostOwnership(Clipboard clipboard, Transferable contents) { + + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/Grid.pde b/OpenBCI_GUI/Grid.pde index 48b95ccbb..53d40d1a7 100644 --- a/OpenBCI_GUI/Grid.pde +++ b/OpenBCI_GUI/Grid.pde @@ -6,9 +6,17 @@ class Grid { private int[] colOffset; private int[] rowOffset; private int rowHeight; + private boolean horizontallyCenterTextInCells = false; + private boolean drawTableBorder = false; private int x, y, w; - private final int pad = 5; + private int pad_horiz = 5; + private int pad_vert = 5; + + private PFont tableFont = p5; + private int tableFontSize = 12; + + private color[][] textColors; private String[][] strings; @@ -21,13 +29,18 @@ class Grid { rowOffset = new int[numRows]; strings = new String[numRows][numCols]; + textColors = new color[numRows][numCols]; + + color defaultTextColor = OPENBCI_DARKBLUE; + for (color[] row: textColors) { + Arrays.fill(row, defaultTextColor); + } } public void draw() { pushStyle(); textAlign(LEFT); stroke(0); - fill(0, 0, 0, 255); textFont(p5, 12); // draw row lines @@ -44,10 +57,18 @@ class Grid { for (int row = 0; row < numRows; row++) { for (int col = 0; col < numCols; col++) { if (strings[row][col] != null) { - text(strings[row][col], x + colOffset[col] + pad, y + rowOffset[row] - pad); + fill(textColors[row][col]); + textAlign(horizontallyCenterTextInCells ? CENTER : LEFT); + text(strings[row][col], x + colOffset[col] + pad_horiz, y + rowOffset[row] - pad_vert); } } } + + if (drawTableBorder) { + noFill(); + stroke(0); + rect(x, y, w, rowOffset[numRows - 1]); + } popStyle(); } @@ -81,4 +102,40 @@ class Grid { public void setString(String s, int row, int col) { strings[row][col] = s; } + + public void setTableFontAndSize(PFont _font, int _fontSize) { + tableFont = _font; + tableFontSize = _fontSize; + } + + public void setRowHeight(int _height) { + rowHeight = _height; + } + + //This overrides the rowHeight and rowOffset when setting the total height of the Grid. + public void setTableHeight(int _height) { + rowHeight = _height / numRows; + for (int i = 0; i < numRows; i++) { + rowOffset[i] = rowHeight * (i + 1); + } + } + + public void setTextColor(color c, int row, int col) { + textColors[row][col] = c; + } + + //Change vertical padding for all cells based on the string/text height from a given cell + public void dynamicallySetTextVerticalPadding(int row, int col) { + float _textH = getFontStringHeight(tableFont, strings[row][col]); + pad_vert = int( (rowHeight - _textH) / 2); //Force round down here + } + + public void setHorizontalCenterTextInCells(boolean b) { + horizontallyCenterTextInCells = b; + pad_horiz = b ? getCellDims(0,0).w/2 : 5; + } + + public void setDrawTableBorder(boolean b) { + drawTableBorder = b; + } } \ No newline at end of file diff --git a/OpenBCI_GUI/Info.plist.tmpl b/OpenBCI_GUI/Info.plist.tmpl index e814301d7..26c6b4ca7 100644 --- a/OpenBCI_GUI/Info.plist.tmpl +++ b/OpenBCI_GUI/Info.plist.tmpl @@ -23,7 +23,7 @@ CFBundleShortVersionString 4 CFBundleVersion - 5.0.3 + 5.0.4 CFBundleSignature ???? NSHumanReadableCopyright @@ -32,7 +32,7 @@ Copyright © 2021 OpenBCI CFBundleGetInfoString - January 2021 + March 2021 @@jvm_runtime@@ diff --git a/OpenBCI_GUI/Interactivity.pde b/OpenBCI_GUI/Interactivity.pde index 6f4dff755..3a187b7b8 100644 --- a/OpenBCI_GUI/Interactivity.pde +++ b/OpenBCI_GUI/Interactivity.pde @@ -23,6 +23,11 @@ synchronized void keyPressed() { //note that the Processing variable "keyCode" is the keypress as a JAVA keycode. This differs from ASCII //println("OpenBCI_GUI: keyPressed: key = " + key + ", int(key) = " + int(key) + ", keyCode = " + keyCode); + //Check for Copy/Paste text keyboard shortcuts before anything else. + if (copyPaste.checkIfPressedAllOS()) { + return; + } + boolean anyActiveTextfields = isNetworkingTextActive() || textFieldIsActive; if(!controlPanel.isOpen && !anyActiveTextfields){ //don't parse the key if the control panel is open @@ -38,6 +43,10 @@ synchronized void keyPressed() { } } +synchronized void keyReleased() { + copyPaste.checkIfReleasedAllOS(); +} + void parseKey(char val) { //assumes that val is a usual printable ASCII character (ASCII 32 through 126) switch (val) { @@ -213,6 +222,19 @@ void parseKey(char val) { } } + if (currentBoard instanceof BoardGanglion) { + if (val == '[' || val == ']') { + println("Expert Mode: '" + val + "' pressed. Sending to Ganglion..."); + Boolean success = ((Board)currentBoard).sendCommand(str(val)).getKey(); + if (success) { + outputSuccess("Expert Mode: Success sending '" + val + "' to Ganglion!"); + } else { + outputWarn("Expert Mode: Error sending '" + val + "' to Ganglion. Try again with data stream stopped."); + } + return; + } + } + if (currentBoard instanceof Board) { output("Expert Mode: '" + key + "' pressed. This is not assigned or applicable to current setup."); //((Board)currentBoard).sendCommand(str(key)); @@ -298,3 +320,127 @@ void openURLInBrowser(String _url){ println("Error launching url in browser: " + _url); } } + +//////////////////////////////////////////////////////////////// +// GUI CopyPaste // +// Custom class used by the GUI to handle both Copy and Paste // +// Use standard copy and paste keyboard shortcuts for all OS // +// // +// Copy ControlP5 Textfield Text // +// - Windows and Linux: Control + C // +// - Mac: Command + C // +// Paste Text into Textfield // +// - Windows and Linux: Control + V // +// - Mac: Command + V // +//////////////////////////////////////////////////////////////// +class CopyPaste { + + private final int CMD_CNTL_KEYCODE = (isLinux() || isWindows()) ? 17 : 157; + private final int C_KEYCODE = 67; + private final int V_KEYCODE = 86; + private boolean commandControlPressed; + private boolean copyPressed; + private String value; + + CopyPaste () { + + } + + public boolean checkIfPressedAllOS() { + //This logic mimics the behavior of copy/paste in Mac OS X, and applied to all. + if (keyCode == CMD_CNTL_KEYCODE) { + commandControlPressed = true; + //println("KEYBOARD SHORTCUT: COMMAND PRESSED"); + return true; + } + + if (commandControlPressed && keyCode == V_KEYCODE) { + //println("KEYBOARD SHORTCUT: PASTE PRESSED"); + // Get clipboard contents + String s = GClip.paste(); + //println("FROM CLIPBOARD ~~ " + s); + // Assign to stored value + value = s; + return true; + } + + if (commandControlPressed && keyCode == C_KEYCODE) { + //println("KEYBOARD SHORTCUT: COPY PRESSED"); + copyPressed = true; + return true; + } + + return false; + } + + public void checkIfReleasedAllOS() { + if (keyCode == CMD_CNTL_KEYCODE) { + commandControlPressed = false; + } + } + + //Pull stored value from this class and set to null, otherwise return null. + private String pullValue() { + if (value == null) { + return value; + } + String s = value; + value = null; + return s; + } + + private void checkForPaste(Textfield tf) { + if (value == null) { + return; + } + + if (tf.isFocus()) { + StringBuilder status = new StringBuilder("OpenBCI_GUI: User pasted text from the clipboard into "); + status.append(tf.toString()); + println(status); + StringBuilder sb = new StringBuilder(); + String existingText = dropNonPrintableChars(tf.getText()); + String val = pullValue(); + //println("EXISTING TEXT =="+ existingText+ "__end. VALUE ==" + val + "__end."); + + // On Mac, Remove 'v' character from the end of the existing text + existingText = existingText.length() > 0 && isMac() ? existingText.substring(0, existingText.length() - 1) : existingText; + + sb.append(existingText); + sb.append(val); + //The 'v' character does make it to the textfield, but this is immediately overwritten here. + tf.setText(sb.toString()); + } + } + + private void checkForCopy(Textfield tf) { + if (!copyPressed) { + return; + } + + if (tf.isFocus()) { + String s = dropNonPrintableChars(tf.getText()); + if (s.length() == 0) { + return; + } + StringBuilder status = new StringBuilder("OpenBCI_GUI: User copied text from "); + status.append(tf.toString()); + status.append(" to the clipboard"); + println(status); + //println("FOUND TEXT =="+ s+"__end."); + if (isMac()) { + //Remove the 'c' character that was just typed in the textfield + s = s.substring(0, s.length() - 1); + tf.setText(s); + //println("MAC FIXED TEXT =="+ s+"__end."); + } + boolean b = GClip.copy(s); + copyPressed = false; + } + } + + public void checkForCopyPaste(Textfield tf) { + checkForPaste(tf); + checkForCopy(tf); + } +} diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index 880cb5327..e2d4b2486 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -8,6 +8,7 @@ // Modified (v2.0): Conor Russomanno & Joel Murphy (AJ Keller helped too), June 2016 // Modified (v3.0) AJ Keller (Conor Russomanno & Joel Murphy & Wangshu), September 2017 // Modified (v4.0) AJ Keller (Richard Waltman), September 2018 +// Modified (v5.0) Richard Waltman, August 2020 // // Requires gwoptics graphing library for processing. Built on V0.5.0 // http://www.gwoptics.org/processing/gwoptics_p5lib/ @@ -64,13 +65,15 @@ import http.requests.*; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v5.0.3"; -String localGUIVersionDate = "January 2021"; +String localGUIVersionString = "v5.0.4"; +String localGUIVersionDate = "March 2021"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; PApplet ourApplet; +CopyPaste copyPaste; + //used to switch between application states final int SYSTEMMODE_INTROANIMATION = -10; final int SYSTEMMODE_PREINIT = 0; @@ -315,6 +318,8 @@ void settings() { void setup() { frameRate(120); + copyPaste = new CopyPaste(); + //V1 FONTS f1 = createFont("fonts/Raleway-SemiBold.otf", 16); f2 = createFont("fonts/Raleway-Regular.otf", 15); @@ -395,19 +400,6 @@ void delayedSetup() { settings.heightOfLastScreen = height; setupContainers(); - - //listen for window resize ... used to adjust elements in application - //Doesn't seem to work... - frame.addComponentListener(new ComponentAdapter() { - public void componentResized(ComponentEvent e) { - if (e.getSource().equals(frame)) { - settings.screenHasBeenResized = true; - settings.timeOfLastScreenResize = millis(); - // initializeGUI(); - } - } - } - ); fontInfo = new PlotFontInfo(); helpWidget = new HelpWidget(0, win_h - 30, win_w, 30); diff --git a/OpenBCI_GUI/W_Accelerometer.pde b/OpenBCI_GUI/W_Accelerometer.pde index 4ceb9d74d..fd6df170f 100644 --- a/OpenBCI_GUI/W_Accelerometer.pde +++ b/OpenBCI_GUI/W_Accelerometer.pde @@ -70,6 +70,7 @@ class W_Accelerometer extends Widget { //create our channel bar and populate our accelerometerBar array! accelerometerBar = new AccelerometerBar(_parent, accelXyzLimit, accelGraphX, accelGraphY, accelGraphWidth, accelGraphHeight); accelerometerBar.adjustTimeAxis(w_timeSeries.getTSHorizScale().getValue()); //sync horiz axis to Time Series by default + accelerometerBar.adjustVertScale(yLimOptions[0]); createAccelModeButton("accelModeButton", "Turn Accel. Off", (int)(x + 3), (int)(y + 3 - navHeight), 120, navHeight - 6, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); } @@ -140,10 +141,13 @@ class W_Accelerometer extends Widget { if (accelBoard.isAccelerometerActive()) { drawAccValues(); draw3DGraph(); - accelerometerBar.draw(); } popStyle(); + + if (accelBoard.isAccelerometerActive()) { + accelerometerBar.draw(); + } } void setGraphDimensions() { diff --git a/OpenBCI_GUI/W_AuraAux.pde b/OpenBCI_GUI/W_AuraAux.pde deleted file mode 100644 index 796d8ed1e..000000000 --- a/OpenBCI_GUI/W_AuraAux.pde +++ /dev/null @@ -1,487 +0,0 @@ -import java.util.*; - -class W_AuraAux extends Widget { - private EDACapableBoard edaBoard; - private PPGCapableBoard ppgBoard; - private BatteryInfoCapableBoard batteryBoard; - - private int numAuxReadBars = 3; - private float xF, yF, wF, hF; - private float arPadding; - private float ar_x, ar_y, ar_h, ar_w; // values for rectangle encompassing all auxReadBars - private float plotBottomWell; - private int channelBarHeight; - private int batteryMeterH = 24; - - private PPGReadBar ppgReadBar1; - private PPGReadBar ppgReadBar2; - private EDAReadBar edaReadBar; - private BatteryMeter batteryMeter; - - public int[] xLimOptions = {1, 3, 5, 10, 20}; // number of seconds (x axis of graph) - public int[] yLimOptions = {0, 50, 100, 200, 400, 1000, 10000}; // 0 = Autoscale ... everything else is uV - private String[] vertScaleOptions = {"Auto", "50", "100", "200", "400", "1000", "10000"}; - private String[] horizScaleOptions = {"1 sec", "3 sec", "5 sec", "10 sec", "20 sec"}; - private boolean allowSpillover = false; - - //Initial dropdown settings - private int naInitialVertScaleIndex = 0; - private int naInitialHorizScaleIndex = 2; - - W_AuraAux(PApplet _parent) { - super(_parent); //calls the parent CONSTRUCTOR method of Widget (DON'T REMOVE) - - // todo add check that current board implements these interfaces before casting - // otherwise should throw and exception and maybe popup message - edaBoard = (EDACapableBoard) currentBoard; - ppgBoard = (PPGCapableBoard) currentBoard; - batteryBoard = (BatteryInfoCapableBoard) currentBoard; - - addDropdown("VertScale_AuraAux", "Vert Scale", Arrays.asList(vertScaleOptions), naInitialVertScaleIndex); - addDropdown("Duration_AuraAux", "Window", Arrays.asList(horizScaleOptions), naInitialHorizScaleIndex); - - xF = float(x); //float(int( ... is a shortcut for rounding the float down... so that it doesn't creep into the 1px margin - yF = float(y); - wF = float(w); - hF = float(h); - - plotBottomWell = 40.0; // Allow space at the bottom for X axis and X axis label - arPadding = 10.0; - ar_x = xF + arPadding; - ar_y = yF + (arPadding*2) + batteryMeterH; - ar_w = wF - arPadding*2; - ar_h = hF - plotBottomWell - (arPadding*2); - channelBarHeight = (int)(ar_h/numAuxReadBars); - - batteryMeter = new BatteryMeter(_parent, batteryBoard, "Battery", int(xF), int(yF), int(wF), batteryMeterH, int(arPadding)); - - int counter = 0; - ppgReadBar1 = new PPGReadBar(_parent, counter+1, ppgBoard, 0, "PPG_1", false, int(ar_x), int(ar_y) + counter*(channelBarHeight), int(ar_w), channelBarHeight, plotBottomWell); - counter++; - ppgReadBar2 = new PPGReadBar(_parent, counter+1, ppgBoard, 1, "PPG_2", false, int(ar_x), int(ar_y) + counter*(channelBarHeight), int(ar_w), channelBarHeight, plotBottomWell); - counter++; - edaReadBar = new EDAReadBar(_parent, counter+1, edaBoard, "EDA", true, int(ar_x), int(ar_y) + counter*(channelBarHeight), int(ar_w), channelBarHeight, plotBottomWell); - - adjustTimeAxisAllPlots(xLimOptions[naInitialHorizScaleIndex]); - adjustVertScaleAllPlots(yLimOptions[naInitialVertScaleIndex]); - } - - public void update() { - super.update(); //calls the parent update() method of Widget (DON'T REMOVE) - batteryMeter.update(); - //Feed new data into plots - ppgReadBar1.update(); - ppgReadBar2.update(); - edaReadBar.update(); - } - - public void draw() { - super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) - batteryMeter.draw(); - ppgReadBar1.draw(); - ppgReadBar2.draw(); - edaReadBar.draw(); - } - - public void screenResized() { - super.screenResized(); //calls the parent screenResized() method of Widget (DON'T REMOVE) - - xF = float(x); //float(int( ... is a shortcut for rounding the float down... so that it doesn't creep into the 1px margin - yF = float(y); - wF = float(w); - hF = float(h); - - ar_x = xF + arPadding; - ar_y = yF + (arPadding*2) + batteryMeterH; - ar_w = wF - arPadding*2; - ar_h = hF - plotBottomWell - (arPadding*3) - batteryMeterH; - channelBarHeight = (int)(ar_h/numAuxReadBars); - - batteryMeter.screenResized(int(xF), int(yF), int(wF), batteryMeterH); - - int counter = 0; - ppgReadBar1.screenResized(int(ar_x), int(ar_y) + counter*(channelBarHeight), int(ar_w), channelBarHeight); - counter++; - ppgReadBar2.screenResized(int(ar_x), int(ar_y) + counter*(channelBarHeight), int(ar_w), channelBarHeight); - counter++; - edaReadBar.screenResized(int(ar_x), int(ar_y) + counter*(channelBarHeight), int(ar_w), channelBarHeight); - } - - public void adjustTimeAxisAllPlots(int _newTimeSize) { - ppgReadBar1.adjustTimeAxis(_newTimeSize); - ppgReadBar2.adjustTimeAxis(_newTimeSize); - edaReadBar.adjustTimeAxis(_newTimeSize); - } - - public void adjustVertScaleAllPlots(int _vertScaleValue) { - ppgReadBar1.adjustVertScale(_vertScaleValue); - ppgReadBar2.adjustVertScale(_vertScaleValue); - edaReadBar.adjustVertScale(_vertScaleValue); - } -}; - -//triggered when there is an event in the LogLin Dropdown -void Duration_AuraAux(int n) { - w_galeaAux.adjustTimeAxisAllPlots(w_galeaAux.xLimOptions[n]); -} - -//These functions need to be global! These functions are activated when an item from the corresponding dropdown is selected -//^^^not true. we can do this in the class above with a CallbackListener -void VertScale_AuraAux(int n) { - w_galeaAux.adjustVertScaleAllPlots(w_galeaAux.yLimOptions[n]); -} - -//======================================================================================================================== -// Analog Voltage BAR CLASS -- Implemented by Analog Read Widget Class -//======================================================================================================================== -//this class contains the plot and buttons for a single channel of the Time Series widget -//one of these will be created for each channel (4, 8, or 16) -abstract class AuxReadBar{ - - private int auxValuesPosition; - private String auxChanLabel; - private boolean isBottomBar = false; - private float plotBottomWellH; - private int channel = 0; //used to track Board channel number - private int x, y, w, h; - - private GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Series trace - private GPointsArray auxReadPoints; - private int nPoints; - private int numSeconds; - private float timeBetweenPoints; - - private color channelColor; //color of plot trace - - //When isAutoscale equals true, the y-axis of each channelBar will automatically update to scale to minimum and maximum value found in the plot - private boolean isAutoscale; - private int yLim = 200; - private int channelAverage = 0; - - private TextBox analogValue; - private TextBox analogPin; - - private boolean drawAnalogValue; - private int lastProcessedDataPacketInd = 0; - private int downscale = 1; // 1 means not downscale - - AuxReadBar(PApplet _parent, int targetSamplingRate, int auxChanNum, String label, boolean isBotBar, int _x, int _y, int _w, int _h, float _plotAxisH) { - - auxValuesPosition = auxChanNum; - auxChanLabel = label; - isBottomBar = isBotBar; - plotBottomWellH = _plotAxisH; - if (targetSamplingRate > 0) { - downscale = currentBoard.getSampleRate() / targetSamplingRate; - } - - x = _x; - y = _y; - w = _w; - h = _h; - - numSeconds = 20; - plot = new GPlot(_parent); - plot.setPos(x + 36 + 4, y); - plot.setDim(w - 36 - 4, h); - plot.setMar(0f, 0f, 0f, 0f); - plot.setLineColor((int)channelColors[(auxValuesPosition)%8]); - plot.setXLim(-5,0); - plot.setYLim(-yLim,yLim); - plot.setPointSize(2); - plot.setPointColor(0); - plot.setAllFontProperties("Arial", 0, 14); - plot.getXAxis().setAxisLabelText("Time (s)"); - plot.getXAxis().getAxisLabel().setOffset(plotBottomWellH/2 + 5f); - - initArrays(); - - analogValue = new TextBox("t", x + 36 + 4 + (w - 36 - 4) - 2, y + h); - analogValue.textColor = OPENBCI_DARKBLUE; - analogValue.alignH = RIGHT; - analogValue.drawBackground = true; - analogValue.backgroundColor = color(255,255,255,125); - - analogPin = new TextBox(auxChanLabel, x+3, y + h); - analogPin.textColor = OPENBCI_DARKBLUE; - analogPin.alignH = CENTER; - - drawAnalogValue = true; - - } - - private void initArrays() { - nPoints = nPointsBasedOnDataSource(); - timeBetweenPoints = (float)numSeconds / (float)nPoints; - auxReadPoints = new GPointsArray(nPoints); - - for (int i = 0; i < nPoints; i++) { - float time = calcTimeAxis(i); - float analog_value = 0.0; //0.0 for all points to start - auxReadPoints.set(i, time, analog_value, ""); - } - - plot.setPoints(auxReadPoints); //set the plot with 0.0 for all auxReadPoints to start - } - - public void update() { - // early out if unactive - if (!isBoardActive()) { - return; - } - - channel = getChannel(); - - // update data in plot - updatePlotPoints(); - - //Fetch the last value in the buffer to display on screen - float val = auxReadPoints.getLastPoint().getY(); - analogValue.string = String.format(getFmt(val),val); - } - - private String getFmt(float val) { - String fmt; - if (val > 100.0f) { - fmt = "%.0f"; - } else if (val > 10.0f) { - fmt = "%.1f"; - } else { - fmt = "%.2f"; - } - return fmt; - } - - private float calcTimeAxis(int sampleIndex) { - return -(float)numSeconds + (float)sampleIndex * timeBetweenPoints; - } - - private void updatePlotPoints() { - int totalPoints = numSeconds * currentBoard.getSampleRate(); - List allData = currentBoard.getData(totalPoints); - - float max = (float)allData.get(0)[channel]; - float min = (float)max; - - for (int i=0, counter=0; i < allData.size(); i+=downscale, counter++) { - float timey = calcTimeAxis(counter); - float value = (float)allData.get(i)[channel]; - auxReadPoints.set(counter, timey, value, ""); - - max = value > max ? value : max; - min = value < min ? value : min; - } - //When not using autoscale, center around average value - if (!isAutoscale) { - float avg = (min + max) / 2; - min = avg - yLim; - max = avg + yLim; - } - plot.setYLim(min, max); - plot.setPoints(auxReadPoints); - } - - public void draw() { - pushStyle(); - - //draw plot - stroke(31,69,110, 50); - fill(color(125,30,12,30)); - - rect(x + 36 + 4, y, w - 36 - 4, h); - - plot.beginDraw(); - plot.drawBox(); // we won't draw this eventually ... - plot.drawGridLines(0); - plot.drawLines(); - - if (isBottomBar) { //only draw the x axis label on the bottom channel bar - plot.drawXAxis(); - plot.getXAxis().draw(); - } - - plot.endDraw(); - - if(drawAnalogValue) { - analogValue.draw(); - analogPin.draw(); - } - - popStyle(); - } - - private int nPointsBasedOnDataSource() { - return (int) ((double)numSeconds * currentBoard.getSampleRate() / downscale); - } - - public void adjustTimeAxis(int _newTimeSize) { - numSeconds = _newTimeSize; - plot.setXLim(-_newTimeSize,0); - - initArrays(); - - //sets the number of axis divisions - plot.getXAxis().setNTicks(_newTimeSize > 1 ? _newTimeSize : 10); - } - - public void adjustVertScale(int _vertScaleValue) { - if(_vertScaleValue == 0) { - isAutoscale = true; - } else { - isAutoscale = false; - yLim = _vertScaleValue; - } - } - - public void screenResized(int _x, int _y, int _w, int _h) { - x = _x; - y = _y; - w = _w; - h = _h; - - plot.setPos(x + 36 + 4, y); - plot.setDim(w - 36 - 4, h); - - analogValue.x = x + 36 + 4 + (w - 36 - 4) - 2; - analogValue.y = y + h; - - analogPin.x = x + 14; - analogPin.y = y + int(h/2.0) + 7; - } - - protected abstract boolean isBoardActive(); - - protected abstract int getChannel(); -}; - - -class PPGReadBar extends AuxReadBar { - private PPGCapableBoard ppgBoard; - private int ppgChan; - - public PPGReadBar (PApplet _parent, int auxChanNum, PPGCapableBoard _ppgBoard, int _ppgChan, String _label, boolean isBotBar, int _x, int _y, int _w, int _h, float axisH) { - super(_parent, 50, auxChanNum, _label, isBotBar, _x, _y, _w, _h, axisH); - ppgBoard = _ppgBoard; - ppgChan = _ppgChan; - } - - @Override - protected boolean isBoardActive() { - return ppgBoard.isPPGActive(); - } - - @Override - protected int getChannel() { - return ppgBoard.getPPGChannels()[ppgChan]; //There are two ppg sensors - } -} - -class EDAReadBar extends AuxReadBar { - private EDACapableBoard edaBoard; - - public EDAReadBar (PApplet _parent, int auxChanNum, EDACapableBoard _edaBoard, String _label, boolean isBotBar, int _x, int _y, int _w, int _h, float axisH) { - super(_parent, 0, auxChanNum, _label, isBotBar, _x, _y, _w, _h, axisH); - edaBoard = _edaBoard; - } - - @Override - protected boolean isBoardActive() { - return edaBoard.isEDAActive(); - } - - @Override - protected int getChannel() { - return edaBoard.getEDAChannels()[0]; //There is one EDA sensor - } -} - -class BatteryMeter { - private int x, y, w, h, padding; - private BatteryInfoCapableBoard batteryBoard; - private String displayLabel; - private int nPoints; - private int meterX; - private int meterW = 200; - private int meterH; - private float meterIndicatorW; - private color green = color(0,255,100,100); - private color yellow = color(254,211,0,100); - private color red = color(255,0,0,100); - - public BatteryMeter (PApplet _parent, BatteryInfoCapableBoard _batteryBoard, String _label, int _x, int _y, int _w, int _h, int _padding) { - batteryBoard = _batteryBoard; - displayLabel = _label; - x = _x; - y = _y; - w = _w; - h = _h; - padding = _padding; - nPoints = nPointsBasedOnDataSource(); - meterH = h; - meterIndicatorW = meterW; - } - - public void update() { - meterIndicatorW = map(getBatteryValue(), 0, 100, 0, meterW); - } - - public void draw() { - meterX = x + w/2 - meterW/2; - String batteryLevel = Integer.toString(getBatteryValue()) + "%"; - - pushStyle(); - - stroke(0); - fill(color(0)); - text(displayLabel, meterX - padding - textWidth(displayLabel), y + padding + 4, 200, h); - text(batteryLevel, meterX + meterW + padding, y + padding + 4, 50, h); - - //Fill battery meter with level - noStroke(); - fill(getBatteryColor()); - rect(meterX, y + padding, meterIndicatorW, meterH); - - //Draw bounding box for meter with no fill on top of indicator rectangle - stroke(0); - noFill(); - rect(meterX, y + padding, meterW, meterH); - - popStyle(); - } - - private int nPointsBasedOnDataSource() { - return 1 * currentBoard.getSampleRate(); - } - - private Integer getChannel() { - return batteryBoard.getBatteryChannel(); - } - - private int getBatteryValue() { - if (getChannel() != null) { - List allData = currentBoard.getData(nPoints); - return (int)allData.get(nPoints-1)[getChannel()]; - } - - return 0; - } - - private color getBatteryColor() { - int val = getBatteryValue(); - if (val > 50) { - return green; - } else if (val <= 50 && val > 20) { - return yellow; - } else { - return red; - } - } - - public void screenResized(int _x, int _y, int _w, int _h) { - x = _x; - y = _y; - w = _w; - h = _h; - meterH = h; - } -} \ No newline at end of file diff --git a/OpenBCI_GUI/W_Networking.pde b/OpenBCI_GUI/W_Networking.pde index e82ded28b..4387aee77 100644 --- a/OpenBCI_GUI/W_Networking.pde +++ b/OpenBCI_GUI/W_Networking.pde @@ -234,10 +234,19 @@ class W_Networking extends Widget { if (protocolMode.equals("OSC")) { cp5ElementsAreActive = textfieldsAreActive(oscTextFieldNames); + for (int i = 0; i < oscTextFieldNames.length; i++) { + copyPaste.checkForCopyPaste(cp5_networking.get(Textfield.class, oscTextFieldNames[i])); + } } else if (protocolMode.equals("UDP")) { cp5ElementsAreActive = textfieldsAreActive(udpTextFieldNames); + for (int i = 0; i < udpTextFieldNames.length; i++) { + copyPaste.checkForCopyPaste(cp5_networking.get(Textfield.class, udpTextFieldNames[i])); + } } else if (protocolMode.equals("LSL")) { cp5ElementsAreActive = textfieldsAreActive(lslTextFieldNames); + for (int i = 0; i < lslTextFieldNames.length; i++) { + copyPaste.checkForCopyPaste(cp5_networking.get(Textfield.class, lslTextFieldNames[i])); + } } else { //For serial mode, disable fft output by switching to bandpower instead this.disableCertainOutputs((int)getCP5Map().get(datatypeNames[0])); diff --git a/OpenBCI_GUI/W_Playback.pde b/OpenBCI_GUI/W_Playback.pde index 39a361488..abb8f858d 100644 --- a/OpenBCI_GUI/W_Playback.pde +++ b/OpenBCI_GUI/W_Playback.pde @@ -90,6 +90,13 @@ class W_playback extends Widget { } 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); @@ -251,20 +258,24 @@ boolean playbackFileSelected (String longName, String shortName) { outputSuccess("You have selected \"" + shortName + "\" for playback."); - try { - savePlaybackHistoryJSON = loadJSONObject(userPlaybackHistoryFile); - JSONArray recentFilesArray = savePlaybackHistoryJSON.getJSONArray("playbackFileHistory"); - playbackHistoryFileExists = true; - } catch (NullPointerException e) { - //println("Playback history JSON file does not exist. Load first file to make it."); + File f = new File(userPlaybackHistoryFile); + if (!f.exists()) { + println("OpenBCI_GUI::playbackFileSelected: Playback history file not found."); playbackHistoryFileExists = false; - } 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(); + } 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; diff --git a/OpenBCI_GUI/W_Spectrogram.pde b/OpenBCI_GUI/W_Spectrogram.pde index 02a01321c..1c26db967 100644 --- a/OpenBCI_GUI/W_Spectrogram.pde +++ b/OpenBCI_GUI/W_Spectrogram.pde @@ -80,8 +80,6 @@ class W_Spectrogram extends Widget { graphW = w - paddingRight - paddingLeft; graphH = h - paddingBottom - paddingTop; - dataImg = createImage(dataImageW, dataImageH, RGB); - settings.spectMaxFrqSave = 1; settings.spectSampleRateSave = 2; settings.spectLogLinSave = 0; @@ -97,6 +95,11 @@ class W_Spectrogram extends Widget { addDropdown("SpectrogramMaxFreq", "Max Freq", Arrays.asList(settings.spectMaxFrqArray), settings.spectMaxFrqSave); addDropdown("SpectrogramSampleRate", "Samples", Arrays.asList(settings.spectSampleRateArray), settings.spectSampleRateSave); addDropdown("SpectrogramLogLin", "Log/Lin", Arrays.asList(settings.fftLogLinArray), settings.spectLogLinSave); + + //Resize the height of the data image using default + dataImageH = vertAxisLabel[0] * 2; + //Create image using correct dimensions! Fixes bug where image size and labels do not align on session start. + dataImg = createImage(dataImageW, dataImageH, RGB); } void update(){ @@ -278,7 +281,7 @@ class W_Spectrogram extends Widget { stroke(255); fill(255); strokeWeight(2); - textSize(10); + textSize(11); for (int i = 0; i <= numHorizAxisDivs; i++) { float offset = scaledW * dataImageW * (float(i) / numHorizAxisDivs); line(horizAxisX + offset, horizAxisY, horizAxisX + offset, horizAxisY + tickMarkSize); @@ -305,13 +308,14 @@ class W_Spectrogram extends Widget { float vertAxisY = graphY; stroke(255); fill(255); + textSize(12); strokeWeight(2); for (int i = 0; i <= numVertAxisDivs; i++) { float offset = scaledH * dataImageH * (float(i) / numVertAxisDivs); //if (i <= numVertAxisDivs/2) offset -= 2; line(vertAxisX, vertAxisY + offset, vertAxisX - tickMarkSize, vertAxisY + offset); if (vertAxisLabel[i] == 0) midLineY = int(vertAxisY + offset); - offset += paddingTop - 2; + offset += paddingTop/2; text(vertAxisLabel[i], vertAxisX - tickMarkSize*2 - textWidth(Integer.toString(vertAxisLabel[i])), vertAxisY + offset); } popStyle(); @@ -364,6 +368,10 @@ class W_Spectrogram extends Widget { for (int i = 0; i < topChansToActivate.length; i++) { spectChanSelectTop.setToggleState(topChansToActivate[i], true); + + } + + for (int i = 0; i < botChansToActivate.length; i++) { spectChanSelectBot.setToggleState(botChansToActivate[i], true); } } @@ -424,7 +432,7 @@ class W_Spectrogram extends Widget { //triggered when there is an event in the Spectrogram Widget MaxFreq. Dropdown void SpectrogramMaxFreq(int n) { settings.spectMaxFrqSave = n; - //reset the vertical axis labelss + //reset the vertical axis labels w_spectrogram.vertAxisLabel = w_spectrogram.vertAxisLabels[n]; //Resize the height of the data image w_spectrogram.dataImageH = w_spectrogram.vertAxisLabel[0] * 2; diff --git a/OpenBCI_GUI/W_TimeSeries.pde b/OpenBCI_GUI/W_TimeSeries.pde index bc7f132b3..51679735f 100644 --- a/OpenBCI_GUI/W_TimeSeries.pde +++ b/OpenBCI_GUI/W_TimeSeries.pde @@ -121,7 +121,6 @@ class W_timeSeries extends Widget { private ADS1299SettingsController adsSettingsController; private boolean allowSpillover = false; - private TextBox[] impValuesMontage; private boolean hasScrollbar = true; //used to turn playback scrollbar widget on/off W_timeSeries(PApplet _parent) { @@ -242,19 +241,19 @@ class W_timeSeries extends Widget { super.draw(); //calls the parent draw() method of Widget (DON'T REMOVE) //remember to refer to x,y,w,h which are the positioning variables of the Widget class - pushStyle(); //draw channel bars for (int i = 0; i < tsChanSelect.activeChan.size(); i++) { int activeChan = tsChanSelect.activeChan.get(i); channelBars[activeChan].draw(getAdsSettingsVisible()); } - popStyle(); //Display playback scrollbar, timeDisplay, or ADSSettingsController depending on data source if ((currentBoard instanceof FileBoard) && hasScrollbar) { //you will only ever see the playback widget in Playback Mode ... otherwise not visible + pushStyle(); fill(0,0,0,20); stroke(31,69,110); rect(xF, ts_y + ts_h + playbackWidgetHeight + 5, wF, playbackWidgetHeight); + popStyle(); scrollbar.draw(); } else if (currentBoard instanceof ADS1299SettingsBoard) { //Hide time display when ADSSettingsController is open for compatible boards @@ -269,8 +268,6 @@ class W_timeSeries extends Widget { tscp5.draw(); tsChanSelect.draw(); - - popStyle(); } void screenResized() { @@ -505,7 +502,7 @@ class ChannelBar { onOff_diameter = h > 26 ? 26 : h - 2; createOnOffButton("onOffButton"+channelIndex, channelString, x + 6, y + int(h/2) - int(onOff_diameter/2), onOff_diameter, onOff_diameter); - if(currentBoard instanceof ImpedanceSettingsBoard) { + if(currentBoard instanceof ImpedanceSettingsBoard && !(currentBoard instanceof BoardGalea)) { impButton_diameter = 22; createImpButton("impButton"+channelIndex, "\u2126", x + 36, y + int(h/2) - int(impButton_diameter/2), impButton_diameter, impButton_diameter); } else { @@ -598,6 +595,10 @@ class ChannelBar { if (yAxisMin.isFocus() || yAxisMax.isFocus()) { textFieldIsActive = true; } + + copyPaste.checkForCopyPaste(yAxisMax); + copyPaste.checkForCopyPaste(yAxisMin); + } private String getFmt(float val) { @@ -632,7 +633,6 @@ class ChannelBar { } public void draw(boolean hardwareSettingsAreOpen) { - pushStyle(); plot.beginDraw(); plot.drawBox(); @@ -656,6 +656,7 @@ class ChannelBar { stroke(31,69,110, 50); noFill(); rect(x,y,w,h); + popStyle(); //draw channelBar separator line in the middle of interChannelBarSpace if (!isBottomChannel()) { @@ -664,17 +665,23 @@ class ChannelBar { strokeWeight(1); int separator_y = y + h + int(w_timeSeries.interChannelBarSpace/2); line(x, separator_y, x + w, separator_y); + popStyle(); } //draw impedance check Button and values drawVoltageValue = true; - if (currentBoard instanceof ImpedanceSettingsBoard) { + if (currentBoard instanceof ImpedanceSettingsBoard && !(currentBoard instanceof BoardGalea)) { impCheckButton.setVisible(true); if(((ImpedanceSettingsBoard)currentBoard).isCheckingImpedance(channelIndex)) { impValue.draw(); drawVoltageValue = false; } } + + if (currentBoard instanceof BoardGalea && ((ImpedanceSettingsBoard)currentBoard).isCheckingImpedance(channelIndex)) { + impValue.draw(); + drawVoltageValue = false; + } if (drawVoltageValue) { voltageValue.draw(); @@ -688,7 +695,6 @@ class ChannelBar { yAxisMin.setVisible(b); yAxisMax.setVisible(b); - popStyle(); try { cbCp5.draw(); } catch (NullPointerException e) { @@ -784,7 +790,7 @@ class ChannelBar { onOffButton.setSize(onOff_diameter, onOff_diameter); onOffButton.setPosition(x + 6, y + int(h/2) - int(onOff_diameter/2)); - if(currentBoard instanceof ImpedanceSettingsBoard) { + if(currentBoard instanceof ImpedanceSettingsBoard && !(currentBoard instanceof BoardGalea)) { impCheckButton.setPosition(x + 36, y + int(h/2) - int(impButton_diameter/2)); } } @@ -808,10 +814,13 @@ class ChannelBar { onOffButton.setCircularButton(true); onOffButton.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - println("[" + channelString + "] onOff released"); - currentBoard.setEXGChannelActive(channelIndex, !currentBoard.isEXGChannelActive(channelIndex)); + boolean newState = !currentBoard.isEXGChannelActive(channelIndex); + println("[" + channelString + "] onOff released - " + (newState ? "On" : "Off")); + currentBoard.setEXGChannelActive(channelIndex, newState); if (currentBoard instanceof ADS1299SettingsBoard) { w_timeSeries.adsSettingsController.updateChanSettingsDropdowns(channelIndex, currentBoard.isEXGChannelActive(channelIndex)); + boolean hasUnappliedChanges = currentBoard.isEXGChannelActive(channelIndex) != newState; + w_timeSeries.adsSettingsController.setHasUnappliedSettings(channelIndex, hasUnappliedChanges); } } }); diff --git a/OpenBCI_GUI/Widget.pde b/OpenBCI_GUI/Widget.pde index 0a9b0b9a9..d4f6434d5 100644 --- a/OpenBCI_GUI/Widget.pde +++ b/OpenBCI_GUI/Widget.pde @@ -31,7 +31,7 @@ class Widget{ protected final int navH = 22; private int widgetSelectorWidth = 160; private int widgetSelectorHeight = 0; - private final int dropdownWidth = 64; + protected int dropdownWidth = 64; private boolean initialResize = false; //used to properly resize the widgetSelector when loading default settings Widget(PApplet _parent){ @@ -64,6 +64,7 @@ class Widget{ noStroke(); fill(255); rect(x,y-1,w,h+1); //draw white widget background + popStyle(); //draw nav bars and button bars pushStyle(); @@ -71,8 +72,8 @@ class Widget{ rect(x0, y0, w0, navH); //top bar fill(200, 200, 200); rect(x0, y0+navH, w0, navH); //button bar - popStyle(); + } public void addDropdown(String _id, String _title, List _items, int _defaultItem){ @@ -195,6 +196,7 @@ class Widget{ // text(dropdowns.get(i).title, x+w-(dropdownWidth*(dropdownPos+1))-(2*(dropdownPos+1))+dropdownWidth/2, y+(navH-2)); text(dropdowns.get(i).title, x0+w0-(dropdownWidth*(dropdownPos))-(2*(dropdownPos+1))+dropdownWidth/2, y0+(navH-2)); } + popStyle(); } public void mouseDragged(){ @@ -455,6 +457,7 @@ class ChannelSelect { rect(x,y,w,navH); } } + popStyle(); //Draw channel select buttons cp5_chanSelect.draw(); diff --git a/OpenBCI_GUI/WidgetManager.pde b/OpenBCI_GUI/WidgetManager.pde index 9410ca14b..4d7c94d2b 100644 --- a/OpenBCI_GUI/WidgetManager.pde +++ b/OpenBCI_GUI/WidgetManager.pde @@ -23,7 +23,6 @@ W_AnalogRead w_analogRead; W_DigitalRead w_digitalRead; W_playback w_playback; W_Spectrogram w_spectrogram; -W_AuraAux w_galeaAux; W_PacketLoss w_packetLoss; //ADD YOUR WIDGET TO WIDGETS OF WIDGETMANAGER @@ -56,13 +55,6 @@ void setupWidgets(PApplet _this, ArrayList w){ addWidget(w_playback, w); } - if (galeaEnabled && currentBoard instanceof PPGCapableBoard && currentBoard instanceof EDACapableBoard) { - //Galea_Widget_2 - w_galeaAux = new W_AuraAux(_this); - w_galeaAux.setTitle("Galea Aux"); - addWidget(w_galeaAux, w); - } - //only instantiate this widget if you are using a Ganglion board for live streaming if(nchan == 4 && currentBoard instanceof BoardGanglion){ //If using Ganglion, this is Widget_3 @@ -230,10 +222,8 @@ class WidgetManager{ if(visible){ for(int i = 0; i < widgets.size(); i++){ if(widgets.get(i).getIsActive()){ - pushStyle(); widgets.get(i).draw(); widgets.get(i).drawDropdowns(); - popStyle(); }else{ if(widgets.get(i).widgetTitle.equals("Networking")){ try{ diff --git a/OpenBCI_GUI/libraries/brainflow/library/brainflow.jar b/OpenBCI_GUI/libraries/brainflow/library/brainflow.jar index 01e269ad0..206af19d5 100644 Binary files a/OpenBCI_GUI/libraries/brainflow/library/brainflow.jar and b/OpenBCI_GUI/libraries/brainflow/library/brainflow.jar differ diff --git a/appveyor.yml b/appveyor.yml index 84deb3309..5cc9d373a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -52,8 +52,9 @@ deploy_script: - aws s3 rm s3://openbci-gui/%APPVEYOR_REPO_BRANCH%/latest --recursive --exclude "*" --include "openbcigui_*_windows64.zip" - aws s3 cp %APPVEYOR_BUILD_FOLDER%\. s3://openbci-gui/%APPVEYOR_REPO_BRANCH%/%GUI_VERSION_STRING%_%GUI_COMMIT_TIME% --recursive --exclude "*" --include "openbcigui_*_windows64.zip" - aws s3 cp %APPVEYOR_BUILD_FOLDER%\. s3://openbci-gui/%APPVEYOR_REPO_BRANCH%/latest --recursive --exclude "*" --include "openbcigui_*_windows64.zip" - # copy index.html back to s3 to refresh it and avoid it being deleted by the eviction policy + # copy index.html and list.js back to s3 to refresh it and avoid it being deleted by the eviction policy - aws s3 cp %APPVEYOR_BUILD_FOLDER%\release_script\index.html s3://openbci-gui/index.html + - aws s3 cp %APPVEYOR_BUILD_FOLDER%\release_script\list.js s3://openbci-gui/list.js notifications: - provider: Email diff --git a/release_script/make-release.py b/release_script/make-release.py index 658e77eb6..ea99780b3 100644 --- a/release_script/make-release.py +++ b/release_script/make-release.py @@ -19,6 +19,7 @@ import subprocess import argparse import requests +import fileinput from bs4 import BeautifulSoup ### Define platform-specific strings @@ -194,6 +195,21 @@ def package_app(sketch_dir, flavor, timestamp, windows_signing=False, windows_pf os.rename(build_dir, new_build_dir) build_dir = new_build_dir + # Allow GUI to launch from directory with spaces #916 + if LOCAL_OS == LINUX: + # Read in the file + with open(build_dir + '/OpenBCI_GUI', 'r') as file : + filedata = file.read() + + # Replace the target string + filedata = filedata.replace('$APPDIR/java/bin/java', '\"$APPDIR/java/bin/java\"') + + # Write the file out again + with open(build_dir + '/OpenBCI_GUI', 'w') as file: + file.write(filedata) + + print ( "Fixed issue on Linux when launching from directory with spaces.") + # delete source directory source_dir = os.path.join(build_dir, "source") try: