diff --git a/.github/workflows/linux_build_deploy.yml b/.github/workflows/linux_build_deploy.yml index d17f71310..9ec711d64 100644 --- a/.github/workflows/linux_build_deploy.yml +++ b/.github/workflows/linux_build_deploy.yml @@ -1,18 +1,18 @@ name: Build GUI for Linux on: - workflow_dispatch: + workflow_dispatch: pull_request: branches: [master, development] push: branches: [master, development] - + permissions: id-token: write contents: read env: - AWS_REGION : us-east-1 + AWS_REGION: us-east-1 jobs: build: @@ -28,10 +28,10 @@ jobs: with: python-version: '3.9' cache: 'pip' - + - name: Install Python Dependencies run: pip install -r release/requirements.txt - + - name: Install Processing run: | mkdir -p $GITHUB_WORKSPACE/processing @@ -55,10 +55,10 @@ jobs: cp -a $GITHUB_WORKSPACE/OpenBCI_GUI/libraries/. $HOME/sketchbook/libraries/ # Unit tests cannot be run on Linux without attached display. - + - name: Build run: python $GITHUB_WORKSPACE/release/build.py - + - name: Package run: python $GITHUB_WORKSPACE/release/package.py @@ -67,17 +67,17 @@ jobs: with: role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} aws-region: ${{ env.AWS_REGION }} - + - name: Get Branch Names id: branch-name uses: tj-actions/branch-names@v7 - + - name: Store Build on AWS run: | cd $GITHUB_WORKSPACE ls CURRENT_BRANCH=${{ steps.branch-name.outputs.head_ref_branch }} echo $CURRENT_BRANCH - aws s3 rm s3://openbci-public-gui-v6/latest --recursive --exclude "*" --include "openbcigui_*_linux64.zip" - aws s3 cp $GITHUB_WORKSPACE/. s3://openbci-public-gui-v6/${CURRENT_BRANCH} --recursive --exclude "*" --include "openbcigui_*_linux64.zip" - aws s3 cp $GITHUB_WORKSPACE/. s3://openbci-public-gui-v6/latest --recursive --exclude "*" --include "openbcigui_*_linux64.zip" \ No newline at end of file + aws s3 rm s3://openbci-public-gui-v7/latest --recursive --exclude "*" --include "openbcigui_*_linux64.zip" + aws s3 cp $GITHUB_WORKSPACE/. s3://openbci-public-gui-v7/${CURRENT_BRANCH} --recursive --exclude "*" --include "openbcigui_*_linux64.zip" + aws s3 cp $GITHUB_WORKSPACE/. s3://openbci-public-gui-v7/latest --recursive --exclude "*" --include "openbcigui_*_linux64.zip" diff --git a/.github/workflows/mac_build_deploy.yml b/.github/workflows/mac_build_deploy.yml index e964a12b1..46e85ff62 100644 --- a/.github/workflows/mac_build_deploy.yml +++ b/.github/workflows/mac_build_deploy.yml @@ -6,13 +6,13 @@ on: branches: [master, development] push: branches: [master, development] - + permissions: id-token: write contents: read env: - AWS_REGION : us-east-1 + AWS_REGION: us-east-1 jobs: build: @@ -28,10 +28,10 @@ jobs: with: python-version: '3.9' cache: 'pip' - + - name: Install Python Dependencies run: pip install -r release/requirements.txt - + - name: Install Processing run: | curl -O -L --insecure https://github.com/processing/processing4/releases/download/processing-1292-4.2/processing-4.2-macos-x64.zip @@ -57,7 +57,7 @@ jobs: - name: Run Unit Tests run: python $GITHUB_WORKSPACE/GuiUnitTests/run-unittests.py - + - name: Decrypt Certificate run: | openssl version @@ -72,26 +72,35 @@ jobs: - name: Add OSX Signing Certificate to Keychain uses: apple-actions/import-codesign-certs@v2 - with: + with: p12-filepath: ${{ github.workspace }}/release/mac/certificate.p12 p12-password: ${{ secrets.CERTIFICATE_P12_PASSWORD }} - + - name: Build run: | python $GITHUB_WORKSPACE/release/build.py cp $GITHUB_WORKSPACE/OpenBCI_GUI/sketch.icns $GITHUB_WORKSPACE/application.macosx/OpenBCI_GUI.app/Contents/Resources/sketch.icns - name: Sign Build + if: ${{ false }} run: | codesign -f -v -s "Developer ID Application: OpenBCI, Inc. (3P82WRGLM8)" $GITHUB_WORKSPACE/application.macosx/OpenBCI_GUI.app + - name: Debug DMG Build Settings + run: | + echo "Examining license settings in dmgbuild_settings.py..." + cat release/mac/dmgbuild_settings.py + pip install --upgrade dmgbuild + - name: Create DMG run: | dmgbuild -s release/mac/dmgbuild_settings.py \ -D app=$GITHUB_WORKSPACE/application.macosx/OpenBCI_GUI.app \ + -D license_encoding=utf-8 \ OpenBCI_GUI $GITHUB_WORKSPACE/application.macosx.dmg - name: Sign DMG + if: ${{ false }} run: | codesign -f -v -s "Developer ID Application: OpenBCI, Inc. (3P82WRGLM8)" $GITHUB_WORKSPACE/application.macosx.dmg @@ -107,13 +116,13 @@ jobs: - name: Get Branch Names id: branch-name uses: tj-actions/branch-names@v7 - + - name: Store Build on AWS run: | cd $GITHUB_WORKSPACE ls CURRENT_BRANCH=${{ steps.branch-name.outputs.head_ref_branch }} echo $CURRENT_BRANCH - aws s3 rm s3://openbci-public-gui-v6/latest --recursive --exclude "*" --include "openbcigui_*_macosx.dmg" - aws s3 cp $GITHUB_WORKSPACE/. s3://openbci-public-gui-v6/${CURRENT_BRANCH} --recursive --exclude "*" --include "openbcigui_*_macosx.dmg" - aws s3 cp $GITHUB_WORKSPACE/. s3://openbci-public-gui-v6/latest --recursive --exclude "*" --include "openbcigui_*_macosx.dmg" \ No newline at end of file + aws s3 rm s3://openbci-public-gui-v7/latest --recursive --exclude "*" --include "openbcigui_*_macosx.dmg" + aws s3 cp $GITHUB_WORKSPACE/. s3://openbci-public-gui-v7/${CURRENT_BRANCH} --recursive --exclude "*" --include "openbcigui_*_macosx.dmg" + aws s3 cp $GITHUB_WORKSPACE/. s3://openbci-public-gui-v7/latest --recursive --exclude "*" --include "openbcigui_*_macosx.dmg" diff --git a/.github/workflows/windows_build_deploy.yml b/.github/workflows/windows_build_deploy.yml index 265a84c0f..4715ea0f0 100644 --- a/.github/workflows/windows_build_deploy.yml +++ b/.github/workflows/windows_build_deploy.yml @@ -12,7 +12,7 @@ permissions: contents: read env: - AWS_REGION : us-east-1 + AWS_REGION: us-east-1 jobs: build: @@ -20,81 +20,134 @@ jobs: runs-on: windows-latest steps: - - name: Clone Repository - uses: actions/checkout@v3 - - - name: Install Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: '3.9' - architecture: 'x64' - - - name: Install Processing - run: | - mkdir %GITHUB_WORKSPACE%\processing - cd %GITHUB_WORKSPACE%\processing - curl -O -L --insecure https://github.com/processing/processing4/releases/download/processing-1292-4.2/processing-4.2-windows-x64.zip - ls -l %GITHUB_WORKSPACE%\processing - unzip processing-4.2-windows-x64.zip - ls -l %GITHUB_WORKSPACE%\processing\processing-4.2 - mkdir %userprofile%\documents\processing\libraries - xcopy %GITHUB_WORKSPACE%\OpenBCI_GUI\libraries\* %userprofile%\documents\processing\libraries /s /i /q - ls -l %userprofile%\documents\processing\libraries - shell: cmd - - - name: Set Path - run: | - echo %GITHUB_WORKSPACE%\processing\processing-4.2>>%GITHUB_PATH% - echo C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64>>%GITHUB_PATH% - shell: cmd - - - name: Print Path - run: echo %PATH% - shell: cmd - - - name: Check processing-java Command - run: processing-java --help - shell: cmd - - - name: Run Unit Tests - run: python %GITHUB_WORKSPACE%\GuiUnitTests\run-unittests.py - shell: cmd - - - name: Build - run: python %GITHUB_WORKSPACE%\release\build.py - shell: cmd - - - name: Sign - run: | - dotnet tool install --global azuresigntool - mt -manifest %GITHUB_WORKSPACE%\release\windows\gui.manifest -outputresource:%GITHUB_WORKSPACE%\application.windows64\OpenBCI_GUI.exe;#1 - mt -manifest %GITHUB_WORKSPACE%\release\windows\java.manifest -outputresource:%GITHUB_WORKSPACE%\application.windows64\java\bin\java.exe;#1 - mt -manifest %GITHUB_WORKSPACE%\release\windows\javaw.manifest -outputresource:%GITHUB_WORKSPACE%\application.windows64\java\bin\javaw.exe;#1 - azuresigntool sign --azure-key-vault-url "${{ secrets.AZURE_KEY_VAULT_URI }}" --azure-key-vault-client-id "${{ secrets.AZURE_CLIENT_ID }}" --azure-key-vault-tenant-id "${{ secrets.AZURE_TENANT_ID }}" --azure-key-vault-client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" --azure-key-vault-certificate "${{ secrets.AZURE_CERT_NAME }}" --timestamp-rfc3161 http://timestamp.digicert.com --verbose %GITHUB_WORKSPACE%\application.windows64\OpenBCI_GUI.exe - shell: cmd - - - name: Package - run: python %GITHUB_WORKSPACE%\release\package.py - shell: cmd - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ env.AWS_REGION }} - - - name: Get Branch Names - id: branch-name - uses: tj-actions/branch-names@v7 - - - name: Store Build on AWS - run: | - cd ${{ github.workspace }} - ls - echo "${{ steps.branch-name.outputs.head_ref_branch }}" - set S3_BRANCH_FOLDER=s3://openbci-public-gui-v6/${{ steps.branch-name.outputs.head_ref_branch }} - echo %S3_BRANCH_FOLDER% - aws s3 rm s3://openbci-public-gui-v6/latest --recursive --exclude "*" --include "openbcigui_*_windows64.zip" - aws s3 cp ${{ github.workspace }}/. %S3_BRANCH_FOLDER% --recursive --exclude "*" --include "openbcigui_*_windows64.zip" - aws s3 cp ${{ github.workspace }}/. s3://openbci-public-gui-v6/latest --recursive --exclude "*" --include "openbcigui_*_windows64.zip" - shell: cmd \ No newline at end of file + - name: Clone Repository + uses: actions/checkout@v4 + + - name: Install Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: '3.9' + architecture: 'x64' + + - name: Install Processing + run: | + mkdir %GITHUB_WORKSPACE%\processing + cd %GITHUB_WORKSPACE%\processing + curl -O -L --insecure https://github.com/processing/processing4/releases/download/processing-1292-4.2/processing-4.2-windows-x64.zip + ls -l %GITHUB_WORKSPACE%\processing + unzip processing-4.2-windows-x64.zip + ls -l %GITHUB_WORKSPACE%\processing\processing-4.2 + mkdir %userprofile%\documents\processing\libraries + xcopy %GITHUB_WORKSPACE%\OpenBCI_GUI\libraries\* %userprofile%\documents\processing\libraries /s /i /q + ls -l %userprofile%\documents\processing\libraries + shell: cmd + + - name: Set Path + run: | + echo %GITHUB_WORKSPACE%\processing\processing-4.2>>%GITHUB_PATH% + echo C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64>>%GITHUB_PATH% + shell: cmd + + - name: Print Path + run: echo %PATH% + shell: cmd + + - name: Check processing-java Command + run: processing-java --help + shell: cmd + + - name: Update Processing Application Icon File + run: | + echo ${{ env.gui-icon-parent-folder }} + ls -l ${{ env.gui-icon-parent-folder }} + echo ${{ env.processing-icon-path }} + ls -l ${{ env.processing-icon-path }} + xcopy ${{ env.gui-icon-path }} ${{ env.processing-icon-path }} /i /y + env: + gui-icon-parent-folder: ${{ github.workspace }}\release\windows\icons\application + gui-icon-path: ${{ github.workspace }}\release\windows\icons\application\application.ico + processing-icon-path: ${{ github.workspace }}\processing\processing-4.2\modes\java\application + + - name: Update Processing Core Icon Files + run: | + ren ${{ env.processing-core-folder }}\core.jar core.zip + mkdir ${{ env.processing-core-icon-path }} + ls -l ${{ env.processing-core-folder }} + unzip ${{ env.processing-core-folder }}\core.zip -d ${{ env.processing-core-unzip-path }} + del ${{ env.processing-core-folder }}\core.zip + xcopy ${{ env.gui-core-icon-path }} ${{ env.processing-core-icon-path }} /e /i /y + cd ${{ env.processing-core-unzip-path }} + ls -l + 7z a -r core.zip * + ren core.zip core.jar + echo "Updated Processing Core Icon Files" + ls -l + move core.jar ${{ env.processing-core-updated-jar-path }} + cd ${{ env.processing-core-updated-jar-path }} + rmdir ${{ env.processing-core-icon-path }} /s /q + ls -l + env: + processing-core-folder: ${{ github.workspace }}\processing\processing-4.2\core\library\ + processing-core-unzip-path: ${{ github.workspace }}\processing\processing-4.2\core\library\core + gui-core-icon-path: ${{ github.workspace }}\release\windows\icons\core\* + processing-core-icon-path: ${{ github.workspace }}\processing\processing-4.2\core\library\core\icon + processing-core-updated-jar-path: ${{ github.workspace }}\processing\processing-4.2\core\library\ + shell: cmd + + - name: Run Unit Tests + run: python %GITHUB_WORKSPACE%\GuiUnitTests\run-unittests.py + shell: cmd + + - name: Build + run: python %GITHUB_WORKSPACE%\release\build.py + shell: cmd + + - name: Sign Executables + if: ${{ true }} + run: | + dotnet tool install --global azuresigntool + mt -manifest %GITHUB_WORKSPACE%\release\windows\gui.manifest -outputresource:%GITHUB_WORKSPACE%\application.windows64\OpenBCI_GUI.exe;#1 + mt -manifest %GITHUB_WORKSPACE%\release\windows\java.manifest -outputresource:%GITHUB_WORKSPACE%\application.windows64\java\bin\java.exe;#1 + mt -manifest %GITHUB_WORKSPACE%\release\windows\javaw.manifest -outputresource:%GITHUB_WORKSPACE%\application.windows64\java\bin\javaw.exe;#1 + azuresigntool sign --azure-key-vault-url "${{ secrets.AZURE_KEY_VAULT_URI }}" --azure-key-vault-client-id "${{ secrets.AZURE_CLIENT_ID }}" --azure-key-vault-tenant-id "${{ secrets.AZURE_TENANT_ID }}" --azure-key-vault-client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" --azure-key-vault-certificate "${{ secrets.AZURE_CERT_NAME }}" --timestamp-rfc3161 http://timestamp.digicert.com --verbose %GITHUB_WORKSPACE%\application.windows64\OpenBCI_GUI.exe + shell: cmd + + - name: Package + id: package + run: python %GITHUB_WORKSPACE%\release\package.py + shell: cmd + + - name: Sign MSI Package + if: ${{ true }} + run: | + azuresigntool sign --azure-key-vault-url "${{ secrets.AZURE_KEY_VAULT_URI }}" --azure-key-vault-client-id "${{ secrets.AZURE_CLIENT_ID }}" --azure-key-vault-tenant-id "${{ secrets.AZURE_TENANT_ID }}" --azure-key-vault-client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" --azure-key-vault-certificate "${{ secrets.AZURE_CERT_NAME }}" --timestamp-rfc3161 http://timestamp.digicert.com --verbose ${{ env.msi-path }} + env: + msi-path: ${{ steps.package.outputs.msi_path }} + shell: cmd + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ env.AWS_REGION }} + + - name: Get Branch Names + id: branch-name + uses: tj-actions/branch-names@v8 + + - name: Store Build on AWS + run: | + cd ${{ env.msi-path }} + ls + echo "${{ steps.branch-name.outputs.head_ref_branch }}" + set S3_BRANCH_FOLDER=s3://openbci-public-gui-v7/${{ steps.branch-name.outputs.head_ref_branch }} + echo Branch Folder: %S3_BRANCH_FOLDER% + set S3_LATEST_FOLDER=s3://openbci-public-gui-v7/latest + echo Latest Folder: %S3_LATEST_FOLDER% + aws s3 rm %S3_LATEST_FOLDER% --recursive --exclude "*" --include "*.msi" + aws s3 rm %S3_BRANCH_FOLDER% --recursive --exclude "*" --include "*.msi" + aws s3 cp . %S3_BRANCH_FOLDER% --recursive --exclude "*" --include "*.msi" + aws s3 cp . %S3_LATEST_FOLDER% --recursive --exclude "*" --include "*.msi" + env: + msi-path: ${{ github.workspace }}\release\wix\bin\Release\en-US\ + shell: cmd diff --git a/.github/workflows/windows_build_processing_3.yml b/.github/workflows/windows_build_processing_3.yml deleted file mode 100644 index 44aff598b..000000000 --- a/.github/workflows/windows_build_processing_3.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Build GUI for Windows - Processing 3 - -on: - workflow_dispatch: - pull_request: - branches: - - development - -jobs: - build: - name: Build for Windows - Processing 3 - runs-on: windows-latest - - steps: - - name: Clone Repository - uses: actions/checkout@v3 - - - name: Install Python 3.9 - uses: actions/setup-python@v4 - with: - python-version: '3.9' - architecture: 'x64' - cache: 'pip' - - - name: Set Path - run: echo %GITHUB_WORKSPACE%\processing\processing-3.5.3>>%GITHUB_PATH% - shell: cmd - - - name: Print Path - run: echo %PATH% - shell: cmd - - - name: Install Processing - run: | - mkdir processing - cd %GITHUB_WORKSPACE%\processing - curl -O -L --insecure https://download.processing.org/processing-3.5.3-windows64.zip - ls -l %GITHUB_WORKSPACE%\processing - unzip processing-3.5.3-windows64.zip - ls -l %GITHUB_WORKSPACE%\processing\processing-3.5.3 - mkdir %userprofile%\documents\processing\libraries - xcopy %GITHUB_WORKSPACE%\OpenBCI_GUI\libraries\* %userprofile%\documents\processing\libraries /s /i /q - ls -l %userprofile%\documents\processing\libraries - shell: cmd - - - name: Check processing-java - run: processing-java --help - shell: cmd - - - name: Build - run: | - python %GITHUB_WORKSPACE%\GuiUnitTests\run-unittests.py - python %GITHUB_WORKSPACE%\release\build.py - shell: cmd \ No newline at end of file diff --git a/.gitignore b/.gitignore index 93090df19..4035cbfe2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ OpenBCI_GUI/application.* OpenBCI_GUI_unittests/UNITTEST_FAILURE openbcigui_* application.* +!application.ico *.autosave .vscode/* temp/* @@ -27,3 +28,6 @@ libGanglionLib.so libGanglionScan.so libunicorn.so OpenBCI_GUI/out/ +release/wix/bin/* +release/wix/obj/* +UNITTEST_FAILURE \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..7ac9e67cf --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +**/*.wxs \ No newline at end of file diff --git a/Networking-Test-Kit/LSL/labrecorderXdfTest.py b/Networking-Test-Kit/LSL/labrecorderXdfTest.py new file mode 100644 index 000000000..b480da8e0 --- /dev/null +++ b/Networking-Test-Kit/LSL/labrecorderXdfTest.py @@ -0,0 +1,38 @@ +import pyxdf +import matplotlib.pyplot as plt + +# Load the XDF file +file_path = "PROVIDE THE PATH TO THE XDF FILE HERE" +data, header = pyxdf.load_xdf(file_path) + +# Find the EEG stream +eeg_stream = None +for stream in data: + if stream['info']['type'][0] == 'EXG': # Stream with LSL type 'EXG' + eeg_stream = stream + break + +if eeg_stream is None: + raise ValueError("No EEG stream found in the XDF file") + +# Extract time series and time stamps +time_series = eeg_stream['time_series'] +time_stamps = eeg_stream['time_stamps'] + +# Check the nominal sampling rate +nominal_sampling_rate = float(eeg_stream['info']['nominal_srate'][0]) +print(f"Nominal sampling rate: {nominal_sampling_rate} Hz") + +# Calculate the actual sampling rate +actual_sampling_rate = len(time_stamps) / (time_stamps[-1] - time_stamps[0]) +print(f"Actual sampling rate: {actual_sampling_rate:.2f} Hz") + +# Plot only channel 1 of the EEG data +plt.figure(figsize=(12, 6)) +plt.plot(time_stamps, time_series[:, 0], label='Channel 1') + +plt.xlabel('Time (s)') +plt.ylabel('Amplitude') +plt.title('EEG Time Series Data - Channel 1') +plt.legend() +plt.show() \ No newline at end of file diff --git a/Networking-Test-Kit/LSL/lslStreamTest.py b/Networking-Test-Kit/LSL/lslStreamTest.py index 0495f08bf..a7f6c7c29 100644 --- a/Networking-Test-Kit/LSL/lslStreamTest.py +++ b/Networking-Test-Kit/LSL/lslStreamTest.py @@ -5,7 +5,7 @@ # first resolve an EEG stream on the lab network print("looking for an EEG stream...") -streams = resolve_byprop('type', 'EEG') +streams = resolve_byprop('name', 'obci_stream_0') # create a new inlet to read from the stream inlet = StreamInlet(streams[0]) diff --git a/Networking-Test-Kit/LSL/lslStreamTestMetaData.py b/Networking-Test-Kit/LSL/lslStreamTestMetaData.py new file mode 100644 index 000000000..498a14496 --- /dev/null +++ b/Networking-Test-Kit/LSL/lslStreamTestMetaData.py @@ -0,0 +1,52 @@ +"""Example program to show how to read a multi-channel time series from LSL.""" +import time +from pylsl import StreamInlet, resolve_byprop +from time import sleep + +# first resolve an EEG stream on the lab network +print("looking for an EEG stream...") +streams = resolve_byprop('type', 'EEG') + +# create a new inlet to read from the stream +inlet = StreamInlet(streams[0]) +duration = 5 + +# get the full stream info (including custom meta-data) and dissect it +info = inlet.info() +print("The stream's XML meta-data is: ") +print(info.as_xml()) +print("The manufacturer is: %s" % info.desc().child_value("manufacturer")) +print("Cap circumference is: %s" % info.desc().child("cap").child_value("size")) +print("The channel labels are as follows:") +ch = info.desc().child("channels").child("channel") +for k in range(info.channel_count()): + print(" " + ch.child_value("label")) + ch = ch.next_sibling() + +sleep(1) + +def testLSLSamplingRate(): + start = time.time() + totalNumSamples = 0 + validSamples = 0 + numChunks = 0 + print( "Testing Sampling Rates..." ) + + while time.time() <= start + duration: + # get chunks of samples + chunk, timestamp = inlet.pull_chunk() + if chunk: + numChunks += 1 + # print( len(chunk) ) + totalNumSamples += len(chunk) + # print(chunk); + for sample in chunk: + print(sample) + validSamples += 1 + + 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_timeSeriesGraphed.py b/Networking-Test-Kit/LSL/lslStreamTest_timeSeriesGraphed.py new file mode 100644 index 000000000..8200311f6 --- /dev/null +++ b/Networking-Test-Kit/LSL/lslStreamTest_timeSeriesGraphed.py @@ -0,0 +1,90 @@ +import time +from pylsl import StreamInlet, resolve_byprop +from time import sleep +import pandas as pd +import matplotlib.pyplot as plt +from datetime import datetime + +duration_seconds = 11 +channel_to_plot = 0 +buffer = [] + +def test_lsl_sampling_rate(): + start = time.time() + total_samples_count = 0 + valid_samples_count = 0 + chunk_count = 0 + global previous_timestamp + previous_timestamp = 0 + global timestamps_out_of_order_counter + timestamps_out_of_order_counter = 0 + print( "Testing Sampling Rates..." ) + + while time.time() <= start + duration_seconds: + # get chunks of samples + chunk, timestamp = inlet.pull_chunk() + if chunk: + offset = inlet.time_correction() + print("Offset: " + str(offset)) + new_chunk_received_time = datetime.now() + print("\nNew chunk! -- Time: " + str(new_chunk_received_time)) + chunk_count += 1 + # print( len(chunk) ) + total_samples_count += len(chunk) + # print(chunk) + i = 0 + for sample in chunk: + # print(sample, timestamp[i]) + add_sample_to_buffer(buffer, timestamp[i] * 1000, sample[channel_to_plot]) + valid_samples_count += 1 + i += 1 + + print( "Number of Chunks and Samples == {} , {}".format(chunk_count, total_samples_count) ) + print( "Valid Samples and duration_seconds == {} / {}".format(valid_samples_count, duration_seconds) ) + print( "Avg Sampling Rate == {}".format(valid_samples_count / duration_seconds) ) + print( "Number of timestamps out of order == {}".format(timestamps_out_of_order_counter) ) + +# Function to add a new sample to the buffer +def add_sample_to_buffer(buffer, timestamp, value): + global new_timestamp + global previous_timestamp + global timestamps_out_of_order_counter + new_timestamp = timestamp + if new_timestamp < previous_timestamp: + print("Timestamps are not in order!") + timestamps_out_of_order_counter += 1 + previous_timestamp = new_timestamp + buffer.append({'Timestamp': timestamp, 'Value': value}) + print(f"Sample added to buffer: {timestamp}, {value}") + +# Function to convert buffer to DataFrame +def buffer_to_dataframe(buffer): + data = pd.DataFrame(buffer) + data['Timestamp'] = pd.to_datetime(data['Timestamp']) + return data + +# Function to plot the time series graph +def plot_time_series(data): + plt.figure(figsize=(10, 6)) + plt.plot(data['Timestamp'], data['Value'], marker='o', linestyle='-') + plt.title('Time Series Data') + plt.xlabel('Timestamp') + plt.ylabel('Value') + plt.grid(True) + plt.show() + +# first resolve an EEG stream on the lab network +print("looking for an EEG stream...") +streams = resolve_byprop('name', 'obci_stream_0') + +# create a new inlet to read from the stream +inlet = StreamInlet(streams[0]) + +sleep(1) + +test_lsl_sampling_rate() + +# Convert buffer to DataFrame and plot the time series +data = buffer_to_dataframe(buffer) +if not data.empty: + plot_time_series(data) diff --git a/Networking-Test-Kit/LSL/pylsl_receive_and_plot.py b/Networking-Test-Kit/LSL/pylsl_receive_and_plot.py new file mode 100644 index 000000000..0eda716d3 --- /dev/null +++ b/Networking-Test-Kit/LSL/pylsl_receive_and_plot.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python +""" +ReceiveAndPlot example for LSL + +This example shows data from all found outlets in realtime. +It illustrates the following use cases: +- efficiently pulling data, re-using buffers +- automatically discarding older samples +- online postprocessing +""" + +import math +from typing import List + +import numpy as np +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui + +import pylsl + +# Basic parameters for the plotting window +plot_duration = 5 # how many seconds of data to show +update_interval = 60 # ms between screen updates +pull_interval = 500 # ms between each pull operation + + +class Inlet: + """Base class to represent a plottable inlet""" + + def __init__(self, info: pylsl.StreamInfo): + # create an inlet and connect it to the outlet we found earlier. + # max_buflen is set so data older the plot_duration is discarded + # automatically and we only pull data new enough to show it + + # Also, perform online clock synchronization so all streams are in the + # same time domain as the local lsl_clock() + # (see https://labstreaminglayer.readthedocs.io/projects/liblsl/ref/enums.html#_CPPv414proc_clocksync) + # and dejitter timestamps + self.inlet = pylsl.StreamInlet( + info, + max_buflen=plot_duration, + processing_flags=pylsl.proc_clocksync | pylsl.proc_dejitter, + ) + # store the name and channel count + self.name = info.name() + self.channel_count = info.channel_count() + + def pull_and_plot(self, plot_time: float, plt: pg.PlotItem): + """Pull data from the inlet and add it to the plot. + :param plot_time: lowest timestamp that's still visible in the plot + :param plt: the plot the data should be shown on + """ + # We don't know what to do with a generic inlet, so we skip it. + pass + + +class DataInlet(Inlet): + """A DataInlet represents an inlet with continuous, multi-channel data that + should be plotted as multiple lines.""" + + dtypes = [[], np.float32, np.float64, None, np.int32, np.int16, np.int8, np.int64] + + def __init__(self, info: pylsl.StreamInfo, plt: pg.PlotItem): + super().__init__(info) + # calculate the size for our buffer, i.e. two times the displayed data + bufsize = ( + 2 * math.ceil(info.nominal_srate() * plot_duration), + info.channel_count(), + ) + self.buffer = np.empty(bufsize, dtype=self.dtypes[info.channel_format()]) + empty = np.array([]) + # create one curve object for each channel/line that will handle displaying the data + self.curves = [ + pg.PlotCurveItem(x=empty, y=empty, autoDownsample=True) + for _ in range(self.channel_count) + ] + for curve in self.curves: + plt.addItem(curve) + + def pull_and_plot(self, plot_time, plt): + # pull the data + _, ts = self.inlet.pull_chunk( + timeout=0.0, max_samples=self.buffer.shape[0], dest_obj=self.buffer + ) + # ts will be empty if no samples were pulled, a list of timestamps otherwise + if ts: + ts = np.asarray(ts) + y = self.buffer[0 : ts.size, :] + this_x = None + old_offset = 0 + new_offset = 0 + for ch_ix in range(self.channel_count): + # we don't pull an entire screen's worth of data, so we have to + # trim the old data and append the new data to it + old_x, old_y = self.curves[ch_ix].getData() + # the timestamps are identical for all channels, so we need to do + # this calculation only once + if ch_ix == 0: + # find the index of the first sample that's still visible, + # i.e. newer than the left border of the plot + old_offset = old_x.searchsorted(plot_time) + # same for the new data, in case we pulled more data than + # can be shown at once + new_offset = ts.searchsorted(plot_time) + # append new timestamps to the trimmed old timestamps + this_x = np.hstack((old_x[old_offset:], ts[new_offset:])) + # append new data to the trimmed old data + this_y = np.hstack((old_y[old_offset:], y[new_offset:, ch_ix] - ch_ix)) + # replace the old data + self.curves[ch_ix].setData(this_x, this_y) + + +class MarkerInlet(Inlet): + """A MarkerInlet shows events that happen sporadically as vertical lines""" + + def __init__(self, info: pylsl.StreamInfo): + super().__init__(info) + + def pull_and_plot(self, plot_time, plt): + # TODO: purge old markers + strings, timestamps = self.inlet.pull_chunk(0) + if timestamps: + for string, ts in zip(strings, timestamps): + plt.addItem( + pg.InfiniteLine(ts, angle=90, movable=False, label=string[0]) + ) + + +def main(): + # firstly resolve all streams that could be shown + inlets: List[Inlet] = [] + print("looking for streams") + streams = pylsl.resolve_streams() + + # Create the pyqtgraph window + pw = pg.plot(title="LSL Plot") + plt = pw.getPlotItem() + plt.enableAutoRange(x=False, y=True) + + # iterate over found streams, creating specialized inlet objects that will + # handle plotting the data + for info in streams: + if info.type() == "Markers": + if ( + info.nominal_srate() != pylsl.IRREGULAR_RATE + or info.channel_format() != pylsl.cf_string + ): + print("Invalid marker stream " + info.name()) + print("Adding marker inlet: " + info.name()) + inlets.append(MarkerInlet(info)) + elif ( + info.nominal_srate() != pylsl.IRREGULAR_RATE + and info.channel_format() != pylsl.cf_string + ): + print("Adding data inlet: " + info.name()) + inlets.append(DataInlet(info, plt)) + else: + print("Don't know what to do with stream " + info.name()) + + def scroll(): + """Move the view so the data appears to scroll""" + # We show data only up to a timepoint shortly before the current time + # so new data doesn't suddenly appear in the middle of the plot + fudge_factor = pull_interval * 0.002 + plot_time = pylsl.local_clock() + pw.setXRange(plot_time - plot_duration + fudge_factor, plot_time - fudge_factor) + + def update(): + # Read data from the inlet. Use a timeout of 0.0 so we don't block GUI interaction. + mintime = pylsl.local_clock() - plot_duration + # call pull_and_plot for each inlet. + # Special handling of inlet types (markers, continuous data) is done in + # the different inlet classes. + for inlet in inlets: + inlet.pull_and_plot(mintime, plt) + + # create a timer that will move the view every update_interval ms + update_timer = QtCore.QTimer() + update_timer.timeout.connect(scroll) + update_timer.start(update_interval) + + # create a timer that will pull and add new data occasionally + pull_timer = QtCore.QTimer() + pull_timer.timeout.connect(update) + pull_timer.start(pull_interval) + + import sys + + # Start Qt event loop unless running in interactive mode or using pyside. + if (sys.flags.interactive != 1) or not hasattr(QtCore, "PYQT_VERSION"): + QtGui.QGuiApplication.instance().exec() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Networking-Test-Kit/UDP/udp_receive.py b/Networking-Test-Kit/UDP/udp_receive.py index 51109281c..9e2aff20e 100755 --- a/Networking-Test-Kit/UDP/udp_receive.py +++ b/Networking-Test-Kit/UDP/udp_receive.py @@ -13,8 +13,13 @@ def print_message(*args): print(args[0]) #added to see raw data obj = json.loads(args[0].decode()) print(obj.get('data')) + if obj: + return True + else: + return False except BaseException as e: print(e) + return False # print("(%s) RECEIVED MESSAGE: " % time.time() + # ''.join(str(struct.unpack('>%df' % int(length), args[0])))) @@ -83,10 +88,10 @@ def close_file(*args): numSamples = 0 duration = 10 while time.time() <= start + duration: - data, addr = sock.recvfrom(20000) # buffer size is 20000 bytes + data, addr = sock.recvfrom(65507) # buffer size is 65507 bytes if args.option=="print": - print_message(data) - numSamples += 1 + if print_message(data): + numSamples += 1 elif args.option=="record": record_to_file(data) print( "Samples == {}".format(numSamples) ) diff --git a/Networking-Test-Kit/UDP/udp_receive_timeSeriesAux b/Networking-Test-Kit/UDP/udp_receive_timeSeriesAux new file mode 100644 index 000000000..730b97b22 --- /dev/null +++ b/Networking-Test-Kit/UDP/udp_receive_timeSeriesAux @@ -0,0 +1,102 @@ +import socket +import sys +import time +import argparse +import signal +import struct +import os +import json + +numSamples = 0 + +# Print received message to console +def print_message(*args): + try: + print(args[0]) #added to see raw data + obj = json.loads(args[0].decode()) + print(obj.get('data')) + sampleCountInData = len(obj.get('data')[0]) + global numSamples + numSamples += sampleCountInData + if obj: + return True + else: + return False + except BaseException as e: + print(e) + return False + # print("(%s) RECEIVED MESSAGE: " % time.time() + + # ''.join(str(struct.unpack('>%df' % int(length), args[0])))) + +# Clean exit from print mode +def exit_print(signal, frame): + print("Closing listener") + sys.exit(0) + +# Record received message in text file +def record_to_file(*args): + textfile.write(str(time.time()) + ",") + textfile.write(''.join(str(struct.unpack('>%df' % length,args[0])))) + textfile.write("\n") + +# Save recording, clean exit from record mode +def close_file(*args): + print("\nFILE SAVED") + textfile.close() + sys.exit(0) + +if __name__ == "__main__": + # Collect command line arguments + parser = argparse.ArgumentParser() + parser.add_argument("--ip", + default="127.0.0.1", help="The ip to listen on") + parser.add_argument("--port", + type=int, default=12353, help="The port to listen on") + parser.add_argument("--address",default="/openbci", help="address to listen to") + parser.add_argument("--option",default="print",help="Debugger option") + parser.add_argument("--len",default=8,help="Debugger option") + args = parser.parse_args() + + # Set up necessary parameters from command line + length = args.len + if args.option=="print": + signal.signal(signal.SIGINT, exit_print) + elif args.option=="record": + i = 0 + while os.path.exists("udp_test%s.txt" % i): + i += 1 + filename = "udp_test%i.txt" % i + textfile = open(filename, "w") + textfile.write("time,address,messages\n") + textfile.write("-------------------------\n") + print("Recording to %s" % filename) + signal.signal(signal.SIGINT, close_file) + + # Connect to socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_address = (args.ip, args.port) + sock.bind(server_address) + + # Display socket attributes + print('--------------------') + print("-- UDP LISTENER -- ") + print('--------------------') + print("IP:", args.ip) + print("PORT:", args.port) + print('--------------------') + print("%s option selected" % args.option) + + # Receive messages + print("Listening...") + start = time.time() + duration = 10 + while time.time() <= start + duration: + data, addr = sock.recvfrom(65507) # buffer size is 65507 bytes + if args.option=="print": + print_message(data) + elif args.option=="record": + record_to_file(data) +print( "Samples == {}".format(numSamples) ) +print( "Duration == {}".format(duration) ) +print( "Avg Sampling Rate == {}".format(numSamples / duration) ) diff --git a/Networking-Test-Kit/UDP/udp_send_marker.py b/Networking-Test-Kit/UDP/udp_send_marker.py index e8563e0c2..f4ee0996a 100644 --- a/Networking-Test-Kit/UDP/udp_send_marker.py +++ b/Networking-Test-Kit/UDP/udp_send_marker.py @@ -12,7 +12,7 @@ parser = argparse.ArgumentParser() parser.add_argument("--ip", default="127.0.0.1", help="The IP of the UDP server") - parser.add_argument("--port", type=int, default=12350, + parser.add_argument("--port", type=int, default=12340, help="The port the UDP server is sending to") args = parser.parse_args() diff --git a/OpenBCI_GUI/ADS1299SettingsBoard.pde b/OpenBCI_GUI/ADS1299SettingsBoard.pde index a05d385eb..5cba38a48 100644 --- a/OpenBCI_GUI/ADS1299SettingsBoard.pde +++ b/OpenBCI_GUI/ADS1299SettingsBoard.pde @@ -133,6 +133,7 @@ public class ADS1299SettingsValues { //Used for Channel On/Off to reflect what happens in Firmware public Bias[] previousBias; public Srb2[] previousSrb2; + public Srb1[] previousSrb1; public InputType[] previousInputType; public ADS1299SettingsValues() { @@ -186,6 +187,7 @@ class ADS1299Settings { values.previousBias = values.bias.clone(); values.previousSrb2 = values.srb2.clone(); + values.previousSrb1 = values.srb1.clone(); values.previousInputType = values.inputType.clone(); String currentVals = getJson(); @@ -237,14 +239,17 @@ class ADS1299Settings { if (active) { values.bias[chan] = values.previousBias[chan]; values.srb2[chan] = values.previousSrb2[chan]; + values.srb1[chan] = values.previousSrb1[chan]; values.inputType[chan] = values.previousInputType[chan]; } else { values.previousBias[chan] = values.bias[chan]; values.previousSrb2[chan] = values.srb2[chan]; + values.previousSrb1[chan] = values.srb1[chan]; values.previousInputType[chan] = values.inputType[chan]; values.bias[chan] = Bias.NO_INCLUDE; values.srb2[chan] = Srb2.DISCONNECT; + values.srb1[chan] = Srb1.DISCONNECT; values.inputType[chan] = InputType.SHORTED; } @@ -290,7 +295,6 @@ class ADS1299Settings { return board.sendCommand(sb.toString()).getKey().booleanValue(); } - //Return true if all commits are successful public void revertAllChannelsToDefaultValues() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); String defaultValsAsString = gson.toJson(defaultValues); @@ -311,7 +315,12 @@ class ADS1299Settings { previousValues.srb1[chan] = values.srb1[chan]; } - public void revertToLastValues(int chan) { + public boolean revertToLastValues(int chan) { + revertToLastValuesWithoutCommitting(chan); + return commit(chan); + } + + public void revertToLastValuesWithoutCommitting(int chan) { values.gain[chan] = previousValues.gain[chan]; values.inputType[chan] = previousValues.inputType[chan]; values.bias[chan] = previousValues.bias[chan]; @@ -337,6 +346,12 @@ class ADS1299Settings { vals.srb2[chan].ordinal(), vals.srb1[chan].ordinal()); return commandString; } + + public void saveDefaultValues() { + Gson gson = new Gson(); + String defaultValsAsString = gson.toJson(values); + defaultValues = gson.fromJson(defaultValsAsString, ADS1299SettingsValues.class); + } } interface ADS1299SettingsBoard { diff --git a/OpenBCI_GUI/ADS1299SettingsController.pde b/OpenBCI_GUI/ADS1299SettingsController.pde index a5f5d1c0c..6ee38e984 100644 --- a/OpenBCI_GUI/ADS1299SettingsController.pde +++ b/OpenBCI_GUI/ADS1299SettingsController.pde @@ -2,81 +2,104 @@ import org.apache.commons.lang3.tuple.Pair; class ADS1299SettingsController { - private PApplet _parentApplet; + private PApplet parentApplet; private boolean isVisible = false; - private int x, y, w, h; - private final int padding_3 = 3; - private final int navH = 22; + protected int x, y, w, h; + protected final int PADDING_3 = 3; + protected final int NAV_HEIGHT = 22; + private final int COLUMN_COUNT = 6; - private ControlP5 hwsCp5; - private final int numControlButtons = 3; + protected ControlP5 hwsCp5; + private final int CONTROL_BUTTON_COUNT = 4; private Button loadButton; private Button saveButton; + private Button resetButton; private Button sendButton; - private int button_w = 80; - private int button_h = navH; - private final int columnLabelH = navH; - private final int commandBarH = navH + padding_3 * 2; - private int chanBar_h; - - private int spaceBetweenButtons = 5; //space between buttons - - private TextBox gainLabel; + private int buttonWidth = 80; + private int buttonHeight = NAV_HEIGHT; + private final int DEFAULT_TOGGLE_WIDTH = 20; + private final int MINIMUM_TOGGLE_WIDTH = 12; + protected int toggleWidthAndHeight = DEFAULT_TOGGLE_WIDTH; + private final int COLUMN_LABEL_HEIGHT = NAV_HEIGHT; + protected final int CONTROLLER_HEADER_HEIGHT = (NAV_HEIGHT * 2) + (PADDING_3 * 2); + private final int COMMAND_BAR_HEIGHT = NAV_HEIGHT + PADDING_3 * 2; + protected int channelBarHeight; + + protected int spaceBetweenButtons = 5; + + protected TextBox channelSelectLabel; + protected TextBox gainLabel; private TextBox inputTypeLabel; - private TextBox biasLabel; + protected TextBox biasLabel; private TextBox srb2Label; private TextBox srb1Label; - private ScrollableList[] gainLists; - private ScrollableList[] inputTypeLists; - private ScrollableList[] biasLists; - private ScrollableList[] srb2Lists; - private ScrollableList[] srb1Lists; - private boolean[] hasUnappliedChanges; - private final color yesOnColor = #DFF2BF; - private final color noOffColor = #FFD2D2; - - private Textfield customCommandTF; - private Button sendCustomCmdButton; - private int customCmdUI_x; - private int customCmdUI_w; + protected Toggle toggleAllChannels; + protected ScrollableList gainListAll; + protected ScrollableList inputTypeListAll; + protected ScrollableList biasListAll; + protected ScrollableList srb2ListAll; + protected ScrollableList srb1ListAll; + protected Toggle[] channelSelectToggles; + protected ScrollableList[] gainLists; + protected ScrollableList[] inputTypeLists; + protected ScrollableList[] biasLists; + protected ScrollableList[] srb2Lists; + protected ScrollableList[] srb1Lists; + private boolean[] channelHasUnappliedChanges; + private boolean[] channelIsSelected; + protected final color YES_ON_COLOR = #DFF2BF; + protected final color NO_OFF_COLOR = #FFD2D2; + + protected Button openCustomCommandPopup; + private int customCommandUIX; + private int customCommandUIWidth; + protected int customCommandUIMiddle; + protected int customCommandObjectW; + protected int customCommandObjectY; + protected int customCommandObjectH; private ADS1299Settings boardSettings; - private int channelCount; - private List activeChannels; + protected int channelCount; + protected List activeChannels; - ADS1299SettingsController(PApplet _parent, List _activeChannels, int _x, int _y, int _w, int _h, int _channelBarHeight) { + ADS1299SettingsController(PApplet _parentApplet, List _activeChannels, int _x, int _y, int _w, int _h, int _channelBarHeight) { x = _x; y = _y; w = _w; h = _h; - chanBar_h = _channelBarHeight; + channelBarHeight = _channelBarHeight; - _parentApplet = _parent; - hwsCp5 = new ControlP5(_parentApplet); - hwsCp5.setGraphics(_parentApplet, 0,0); + this.parentApplet = _parentApplet; + hwsCp5 = new ControlP5(parentApplet); + hwsCp5.setGraphics(parentApplet, 0,0); hwsCp5.setAutoDraw(false); - int colOffset = (w / numControlButtons) / 2; - int button_y = y + h + padding_3; - createHWSettingsLoadButton("HardwareSettingsLoad", "Load", x + colOffset - button_w/2, button_y, button_w, button_h); - createHWSettingsSaveButton("HardwareSettingsSave", "Save", x + colOffset + (w/numControlButtons) - button_w/2, button_y, button_w, button_h); - createHWSettingsSendButton("HardwareSettingsSend", "Send", x + colOffset + (w/numControlButtons)*2 - button_w/2, button_y, button_w, button_h); + int colOffset = (w / CONTROL_BUTTON_COUNT) / 2; + int button_y = y + h + PADDING_3; + + createLoadButton("HardwareSettingsLoad", "Load", x + colOffset - buttonWidth/2, button_y, buttonWidth, buttonHeight); + createSaveButton("HardwareSettingsSave", "Save", x + colOffset + (w/CONTROL_BUTTON_COUNT) - buttonWidth/2, button_y, buttonWidth, buttonHeight); + createResetButton("HardwareSettingsReset", "Reset", x + colOffset + (w/CONTROL_BUTTON_COUNT)*2 - buttonWidth/2, button_y, buttonWidth, buttonHeight); + createSendButton("HardwareSettingsSend", "Send", x + colOffset + (w/CONTROL_BUTTON_COUNT)*3 - buttonWidth/2, button_y, buttonWidth, buttonHeight); activeChannels = _activeChannels; ADS1299SettingsBoard settingsBoard = (ADS1299SettingsBoard)currentBoard; boardSettings = settingsBoard.getADS1299Settings(); boardSettings.saveAllLastValues(); channelCount = currentBoard.getNumEXGChannels(); - hasUnappliedChanges = new boolean[channelCount]; - Arrays.fill(hasUnappliedChanges, Boolean.FALSE); + channelHasUnappliedChanges = new boolean[channelCount]; + Arrays.fill(channelHasUnappliedChanges, Boolean.FALSE); + channelIsSelected = new boolean[channelCount]; + Arrays.fill(channelIsSelected, Boolean.FALSE); //color labelBG = color(220); color labelBG = color(255,255,255,0); color labelTxt = OPENBCI_DARKBLUE; colOffset = (w / 5) / 2; - int label_y = y - 14 - padding_3; + int label_y = y - (NAV_HEIGHT * 2) - (PADDING_3 * 2); + channelSelectLabel = new TextBox("Select", x + colOffset, label_y, labelTxt, labelBG, 12, h5, CENTER, TOP); gainLabel = new TextBox("PGA Gain", x + colOffset, label_y, labelTxt, labelBG, 12, h5, CENTER, TOP); inputTypeLabel = new TextBox("Input Type", x + colOffset + (w/5), label_y, labelTxt, labelBG, 12, h5, CENTER, TOP); biasLabel = new TextBox("Bias Include", x + colOffset + (w/5)*2, label_y, labelTxt, labelBG, 12, h5, CENTER, TOP); @@ -84,17 +107,13 @@ class ADS1299SettingsController { srb1Label = new TextBox("SRB1", x + colOffset + (w/5)*4, label_y, labelTxt, labelBG, 12, h5, CENTER, TOP); createCustomCommandUI(); + resizeCustomCommandUI(); - createAllDropdowns(chanBar_h); + createUIObjects(); } public void update() { - boolean tfactive = customCommandTF.isFocus(); - if (tfactive) { - textFieldIsActive = true; - } - - textfieldUpdateHelper.checkTextfield(customCommandTF); + //Empty for now } public void draw() { @@ -105,7 +124,7 @@ class ADS1299SettingsController { //stroke(OPENBCI_BLUE_ALPHA50); stroke(OBJECT_BORDER_GREY); fill(GREY_100); - rect(x, y - columnLabelH, w, columnLabelH); + rect(x, y - CONTROLLER_HEADER_HEIGHT, w, CONTROLLER_HEADER_HEIGHT); popStyle(); //background @@ -115,66 +134,116 @@ class ADS1299SettingsController { rect(x, y, w + 1, h); popStyle(); - gainLabel.draw(); - inputTypeLabel.draw(); - biasLabel.draw(); - srb2Label.draw(); - srb1Label.draw(); + drawLabels(); - for (int i = 0; i < channelCount; i++) { - boolean b = activeChannels.contains(i); - gainLists[i].setVisible(b); - inputTypeLists[i].setVisible(b); - biasLists[i].setVisible(b); - srb2Lists[i].setVisible(b); - srb1Lists[i].setVisible(b); - - if (hasUnappliedChanges[i]) { - pushStyle(); - 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(); - } - } + setUIObjectVisibility(); + + drawChannelStatus(); boolean showCustomCommandUI = guiSettings.getExpertModeBoolean(); //Draw background behind command buttons pushStyle(); fill(GREY_100); - rect(x, y + h, w + 1, commandBarH); + rect(x, y + h, w + 1, COMMAND_BAR_HEIGHT); if (showCustomCommandUI) { - rect(customCmdUI_x, y + h + commandBarH, customCmdUI_w, commandBarH); //keep above style for other command buttons + rect(customCommandUIX, y + h + COMMAND_BAR_HEIGHT, customCommandUIWidth, COMMAND_BAR_HEIGHT); //keep above style for other command buttons } popStyle(); - customCommandTF.setVisible(showCustomCommandUI); - sendCustomCmdButton.setVisible(showCustomCommandUI); + hideShowCustomCommandUI(showCustomCommandUI); //Draw cp5 objects on top of everything hwsCp5.draw(); + + //Draw check marks on top of the toggle buttons + for (int i = 0; i < channelCount; i++) { + drawCheckMark(channelSelectToggles[i]); + } + //Draw check mark for All Channels toggle + drawCheckMark(toggleAllChannels); } } - private void resizeDropdowns(int _channelBarHeight) { + public void resize(int _x, int _y, int _w, int _h, int _channelBarHeight) { + x = _x; + y = _y; + w = _w; + h = _h; + channelBarHeight = _channelBarHeight; + if (channelBarHeight - 2 < DEFAULT_TOGGLE_WIDTH) { + toggleWidthAndHeight = channelBarHeight - 2; + if (toggleWidthAndHeight < MINIMUM_TOGGLE_WIDTH) { + toggleWidthAndHeight = MINIMUM_TOGGLE_WIDTH; + } + } else { + toggleWidthAndHeight = DEFAULT_TOGGLE_WIDTH; + } + + hwsCp5.setGraphics(parentApplet, 0, 0); + + int colOffset = (w / CONTROL_BUTTON_COUNT) / 2; + int button_y = y + h + PADDING_3; + loadButton.setPosition(x + colOffset - (buttonWidth / 2), button_y); + saveButton.setPosition(x + colOffset + (w/CONTROL_BUTTON_COUNT) - (buttonWidth / 2), button_y); + resetButton.setPosition(x + colOffset + ((w/CONTROL_BUTTON_COUNT) * 2) - (buttonWidth / 2), button_y); + sendButton.setPosition(x + colOffset + ((w/CONTROL_BUTTON_COUNT) * 3) - (buttonWidth / 2), button_y); + + updateLabelPositions(); + + resizeAndPositionUIObjects(); + + resizeCustomCommandUI(); + } + + + protected void resizeAndPositionUIObjects() { + int columnCount = getColumnCount(); int dropdownX = 0; int dropdownY = 0; - int dropdownW = int((w - (spaceBetweenButtons*6)) / 5); + int dropdownW = int((w - (spaceBetweenButtons * (columnCount + 1))) / columnCount); int dropdownH = 18; + int allChannelObjectsY = y - CONTROLLER_HEADER_HEIGHT + PADDING_3*2 + NAV_HEIGHT; + int allChannelObjectsX = x + spaceBetweenButtons; + int allChannelObjectsW = dropdownW; + int allChannelObjectsH = 5 * dropdownH; + int toggleAllX = allChannelObjectsX + (allChannelObjectsW / 2) - (toggleWidthAndHeight / 2); + toggleAllChannels.setPosition(toggleAllX, allChannelObjectsY); + toggleAllChannels.setSize(toggleWidthAndHeight, toggleWidthAndHeight); + allChannelObjectsX += dropdownW + spaceBetweenButtons; + gainListAll.setPosition(allChannelObjectsX, allChannelObjectsY); + gainListAll.setSize(allChannelObjectsW, allChannelObjectsH); + allChannelObjectsX += dropdownW + spaceBetweenButtons; + inputTypeListAll.setPosition(allChannelObjectsX, allChannelObjectsY); + inputTypeListAll.setSize(allChannelObjectsW, allChannelObjectsH); + allChannelObjectsX += dropdownW + spaceBetweenButtons; + biasListAll.setPosition(allChannelObjectsX, allChannelObjectsY); + biasListAll.setSize(allChannelObjectsW, allChannelObjectsH); + allChannelObjectsX += dropdownW + spaceBetweenButtons; + srb2ListAll.setPosition(allChannelObjectsX, allChannelObjectsY); + srb2ListAll.setSize(allChannelObjectsW, allChannelObjectsH); + allChannelObjectsX += dropdownW + spaceBetweenButtons; + srb1ListAll.setPosition(allChannelObjectsX, allChannelObjectsY); + srb1ListAll.setSize(allChannelObjectsW, allChannelObjectsH); + int rowCount = 0; for (int i : activeChannels) { dropdownX = x + spaceBetweenButtons; - dropdownY = int(y + ((_channelBarHeight)*rowCount) + (((_channelBarHeight)-dropdownH)/2)); + dropdownY = int(y + (channelBarHeight * rowCount) + ((channelBarHeight - dropdownH) / 2)); final int buttonXIncrement = spaceBetweenButtons + dropdownW; + int toggleX = dropdownX + (dropdownW / 2) - (toggleWidthAndHeight / 2); + channelSelectToggles[i].setPosition(toggleX, dropdownY); + channelSelectToggles[i].setSize(toggleWidthAndHeight, toggleWidthAndHeight); + + dropdownX += buttonXIncrement; gainLists[i].setPosition(dropdownX, dropdownY); - gainLists[i].setSize(dropdownW,5*dropdownH); //Only enough space for SelectedItem + 4 options in the latter channels + gainLists[i].setSize(dropdownW, 5 * dropdownH); //Only enough space for SelectedItem + 4 options in the latter channels dropdownX += buttonXIncrement; inputTypeLists[i].setPosition(dropdownX, dropdownY); - inputTypeLists[i].setSize(dropdownW,5*dropdownH); //Only enough space for SelectedItem + 4 options in the latter channels + inputTypeLists[i].setSize(dropdownW, 5 * dropdownH); //Only enough space for SelectedItem + 4 options in the latter channels dropdownX += buttonXIncrement; biasLists[i].setPosition(dropdownX, dropdownY); @@ -192,33 +261,20 @@ class ADS1299SettingsController { } } - public void resize(int _x, int _y, int _w, int _h, int _channelBarHeight) { - x = _x; - y = _y; - w = _w; - h = _h; - chanBar_h = _channelBarHeight; - - hwsCp5.setGraphics(_parentApplet, 0, 0); - - int colOffset = (w / numControlButtons) / 2; - int button_y = y + h + padding_3; - loadButton.setPosition(x + colOffset - button_w/2, button_y); - saveButton.setPosition(x + colOffset + (w/numControlButtons) - button_w/2, button_y); - sendButton.setPosition(x + colOffset + (w/numControlButtons)*2 - button_w/2, button_y); - - colOffset = (w / 5) / 2; - int label_y = y - 14 - padding_3; - gainLabel.setPosition(x + colOffset, label_y); - inputTypeLabel.setPosition(x + colOffset + (w/5), label_y); - biasLabel.setPosition(x + colOffset + (w/5)*2, label_y); - srb2Label.setPosition(x + colOffset + (w/5)*3, label_y); - srb1Label.setPosition(x + colOffset + (w/5)*4, label_y); - - resizeDropdowns(chanBar_h); + protected void updateLabelPositions() { + int columnCount = getColumnCount(); + int colOffset = (w / columnCount) / 2; + int label_y = y - CONTROLLER_HEADER_HEIGHT + PADDING_3; + channelSelectLabel.setPosition(x + colOffset, label_y); + gainLabel.setPosition(x + colOffset + (w /columnCount), label_y); + inputTypeLabel.setPosition(x + colOffset + (w / columnCount) * 2, label_y); + biasLabel.setPosition(x + colOffset + (w / columnCount) * 3, label_y); + srb2Label.setPosition(x + colOffset + (w / columnCount) * 4, label_y); + srb1Label.setPosition(x + colOffset + (w / columnCount) * 5, label_y); + } - resizeCustomCommandUI(); - + protected int getColumnCount() { + return COLUMN_COUNT; } //Returns true if board and UI are in sync @@ -228,8 +284,8 @@ class ADS1299SettingsController { if (!v) { boolean allChannelsInSync = true; - for (int i = 0; i < hasUnappliedChanges.length; i++) { - if (hasUnappliedChanges[i]) { + for (int i = 0; i < channelHasUnappliedChanges.length; i++) { + if (channelHasUnappliedChanges[i]) { allChannelsInSync = false; } } @@ -248,7 +304,11 @@ class ADS1299SettingsController { return isVisible; } - private void createHWSettingsLoadButton(String name, String text, int _x, int _y, int _w, int _h) { + protected void hideShowCustomCommandUI(boolean showUI) { + openCustomCommandPopup.setVisible(showUI); + } + + private void createLoadButton(String name, String text, int _x, int _y, int _w, int _h) { loadButton = createButton(hwsCp5, name, text, _x, _y, _w, _h); loadButton.setBorderColor(OBJECT_BORDER_GREY); loadButton.setDescription("Load hardware settings from file."); @@ -257,24 +317,47 @@ class ADS1299SettingsController { if (currentBoard.isStreaming()) { PopupMessage msg = new PopupMessage("Info", "Streaming needs to be stopped before loading hardware settings."); } else { - selectInput("Select settings file to load", "loadHardwareSettings"); + FileChooser chooser = new FileChooser( + FileChooserMode.LOAD, + "loadHardwareSettings", + new File(directoryManager.getGuiDataPath() + "Settings"), + "Select settings file to load"); } } }); } - private void createHWSettingsSaveButton(String name, String text, int _x, int _y, int _w, int _h) { + private void createSaveButton(String name, String text, int _x, int _y, int _w, int _h) { saveButton = createButton(hwsCp5, name, text, _x, _y, _w, _h); saveButton.setBorderColor(OBJECT_BORDER_GREY); saveButton.setDescription("Save hardware settings to file."); saveButton.onClick(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - selectOutput("Save settings to file", "storeHardwareSettings"); + FileChooser chooser = new FileChooser( + FileChooserMode.SAVE, + "storeHardwareSettings", + new File(directoryManager.getGuiDataPath() + "Settings"), + "Save settings to file"); } }); } - private void createHWSettingsSendButton(String name, String text, int _x, int _y, int _w, int _h) { + private void createResetButton(String name, String text, int _x, int _y, int _w, int _h) { + resetButton = createButton(hwsCp5, name, text, _x, _y, _w, _h); + resetButton.setBorderColor(OBJECT_BORDER_GREY); + resetButton.setDescription("Reset hardware settings to last saved values."); + resetButton.onClick(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + for (int i = 0; i < channelCount; i++) { + boardSettings.revertAllChannelsToDefaultValues(); + updateChanSettingsDropdowns(i, true); + } + output("Hardware Settings reset to last saved values."); + } + }); + } + + private void createSendButton(String name, String text, int _x, int _y, int _w, int _h) { sendButton = createButton(hwsCp5, name, text, _x, _y, _w, _h); sendButton.setBorderColor(OBJECT_BORDER_GREY); sendButton.setDescription("Send hardware settings to the board."); @@ -285,7 +368,7 @@ class ADS1299SettingsController { boolean atLeastOneChannelHasChanged = false; for (int i = 0; i < channelCount; i++) { - if (hasUnappliedChanges[i]) { + if (channelHasUnappliedChanges[i]) { boolean sendCommandSuccess = ((ADS1299SettingsBoard)currentBoard).getADS1299Settings().commit(i); if (!sendCommandSuccess) { noErrors = false; @@ -308,7 +391,7 @@ class ADS1299SettingsController { }); } - private ScrollableList createDropdown(int chanNum, String name, ADSSettingsEnum[] enumValues, ADSSettingsEnum e, color _backgroundColor) { + private ScrollableList createDropdown(String name, ADSSettingsEnum[] enumValues, ADSSettingsEnum e, color _backgroundColor) { int dropdownW = int((w - (spaceBetweenButtons*6)) / 5); int dropdownH = 18; ScrollableList list = hwsCp5.addScrollableList(name) @@ -346,14 +429,14 @@ class ADS1299SettingsController { .getStyle() //need to grab style before affecting the paddingTop .setPaddingTop(3) //4-pixel vertical offset to center text ; - list.addCallback(new SLCallbackListener(chanNum)); return list; } - private void createAllDropdowns(int _channelBarHeight) { + private void createUIObjects() { //the size and space of these buttons are dependendant on the size of the screen and full ChannelController verbosePrint("ChannelController: createChannelSettingButtons: creating channel setting buttons..."); + channelSelectToggles = new Toggle[channelCount]; gainLists = new ScrollableList[channelCount]; inputTypeLists = new ScrollableList[channelCount]; biasLists = new ScrollableList[channelCount]; @@ -363,97 +446,74 @@ class ADS1299SettingsController { //Init dropdowns in reverse so that chan 1 draws on top of chan 2, etc. for (int i = channelCount - 1; i >= 0; i--) { + channelSelectToggles[i] = createChannelSelectToggle(i, "channelSelectToggle_" + i); + _bgColor = #FFFFFF; - gainLists[i] = createDropdown(i, "gain_ch_"+(i+1), boardSettings.values.gain[i].values(), boardSettings.values.gain[i], _bgColor); - + gainLists[i] = createDropdown("gain_ch_" + i, boardSettings.values.gain[i].values(), boardSettings.values.gain[i], _bgColor); + gainLists[i].addCallback(new SLCallbackListener(i)); + _bgColor = #FFFFFF; - inputTypeLists[i] = createDropdown(i, "inputType_ch_"+(i+1), boardSettings.values.inputType[i].values(), boardSettings.values.inputType[i], _bgColor); - - _bgColor = boardSettings.values.bias[i] == Bias.INCLUDE ? yesOnColor : noOffColor; - biasLists[i] = createDropdown(i, "bias_ch_"+(i+1), boardSettings.values.bias[i].values(), boardSettings.values.bias[i], _bgColor); + inputTypeLists[i] = createDropdown("inputType_ch_" + i, boardSettings.values.inputType[i].values(), boardSettings.values.inputType[i], _bgColor); + inputTypeLists[i].addCallback(new SLCallbackListener(i)); - _bgColor = boardSettings.values.srb2[i] == Srb2.CONNECT ? yesOnColor : noOffColor; - srb2Lists[i] = createDropdown(i, "srb2_ch_"+(i+1), boardSettings.values.srb2[i].values(), boardSettings.values.srb2[i], _bgColor); + _bgColor = boardSettings.values.bias[i] == Bias.INCLUDE ? YES_ON_COLOR : NO_OFF_COLOR; + biasLists[i] = createDropdown("bias_ch_" + i, boardSettings.values.bias[i].values(), boardSettings.values.bias[i], _bgColor); + biasLists[i].addCallback(new SLCallbackListener(i)); - _bgColor = boardSettings.values.srb1[i] == Srb1.CONNECT ? yesOnColor : noOffColor; - srb1Lists[i] = createDropdown(i, "srb1_ch_"+(i+1), boardSettings.values.srb1[i].values(), boardSettings.values.srb1[i], _bgColor); - } + _bgColor = boardSettings.values.srb2[i] == Srb2.CONNECT ? YES_ON_COLOR : NO_OFF_COLOR; + srb2Lists[i] = createDropdown("srb2_ch_" + i, boardSettings.values.srb2[i].values(), boardSettings.values.srb2[i], _bgColor); + srb2Lists[i].addCallback(new SLCallbackListener(i)); - resizeDropdowns(_channelBarHeight); - } - - private void createCustomCommandUI() { - customCommandTF = hwsCp5.addTextfield("customCommand") - .setPosition(0, 0) - .setCaptionLabel("") - .setSize(120, 20) - .setFont(f2) - .setFocus(false) - .setColor(color(26, 26, 26)) - .setColorBackground(color(255, 255, 255)) // text field bg color - .setColorValueLabel(OPENBCI_DARKBLUE) // text color - .setColorForeground(OBJECT_BORDER_GREY) // border color when not selected - .setColorActive(isSelected_color) // border color when selected - .setColorCursor(color(26, 26, 26)) - .setText("") - .align(5, 10, 20, 40) - .setAutoClear(false) //Don't clear textfield when pressing Enter key - ; - customCommandTF.setDescription("Type a custom command and Send to board."); - //Clear textfield on double click - customCommandTF.onDoublePress(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - output("[ExpertMode] Enter the custom command you would like to send to the board."); - customCommandTF.clear(); - } - }); - customCommandTF.addCallback(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - if ((theEvent.getAction() == ControlP5.ACTION_BROADCAST) || (theEvent.getAction() == ControlP5.ACTION_LEAVE)) { - customCommandTF.setFocus(false); - } - } - }); + _bgColor = boardSettings.values.srb1[i] == Srb1.CONNECT ? YES_ON_COLOR : NO_OFF_COLOR; + srb1Lists[i] = createDropdown("srb1_ch_" + i, boardSettings.values.srb1[i].values(), boardSettings.values.srb1[i], _bgColor); + srb1Lists[i].addCallback(new SLCallbackListener(i)); + } + + _bgColor = #FFFFFF; + toggleAllChannels = createChannelSelectToggle(-1, "channelSelectToggle_all"); + gainListAll = createDropdown("gain_all", boardSettings.values.gain[0].values(), boardSettings.values.gain[0], _bgColor); + gainListAll.addCallback(new AllChannelSLCallbackListener()); + inputTypeListAll = createDropdown("inputType_all", boardSettings.values.inputType[0].values(), boardSettings.values.inputType[0], _bgColor); + inputTypeListAll.addCallback(new AllChannelSLCallbackListener()); + biasListAll = createDropdown("bias_all", boardSettings.values.bias[0].values(), boardSettings.values.bias[0], _bgColor); + biasListAll.addCallback(new AllChannelSLCallbackListener()); + srb2ListAll = createDropdown("srb2_all", boardSettings.values.srb2[0].values(), boardSettings.values.srb2[0], _bgColor); + srb2ListAll.addCallback(new AllChannelSLCallbackListener()); + srb1ListAll = createDropdown("srb1_all", boardSettings.values.srb1[0].values(), boardSettings.values.srb1[0], _bgColor); + srb1ListAll.addCallback(new AllChannelSLCallbackListener()); + + resizeAndPositionUIObjects(); + } - sendCustomCmdButton = createButton(hwsCp5, "sendCustomCommand", "Send Custom Command", 0, 0, 10, 10); - sendCustomCmdButton.setBorderColor(OBJECT_BORDER_GREY); - sendCustomCmdButton.getCaptionLabel().getStyle().setMarginLeft(1); - sendCustomCmdButton.onClick(new CallbackListener() { + protected void createCustomCommandUI() { + openCustomCommandPopup = createButton(hwsCp5, "openCustomCommandPopup", "Developer Commands", 0, 0, 10, 10); + openCustomCommandPopup.setBorderColor(OBJECT_BORDER_GREY); + openCustomCommandPopup.getCaptionLabel().getStyle().setMarginLeft(1); + openCustomCommandPopup.onClick(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - String text = dropNonPrintableChars(customCommandTF.getText()); - Pair res = ((BoardBrainFlow)currentBoard).sendCommand(text); - if (res.getKey().booleanValue()) { - outputSuccess("[ExpertMode] Success sending command to board: " + text); + if (!developerCommandPopupIsOpen) { + developerCommandPopup = new DeveloperCommandPopup(); } else { - outputError("[ExpertMode] Failure sending command to board: " + text); + developerCommandPopup.exitPopup(); + developerCommandPopup = null; } - println("ADSSettingsController: Response == " + res.getValue()); } }); - - resizeCustomCommandUI(); } public void resizeCustomCommandUI() { - customCmdUI_x = x; - customCmdUI_w = w + 1; - int tf_w = Math.round(button_w * 1.8); - int but_w = tf_w; - //customCmdUI_w = (int)Math.ceil(w * (2f/3f)) + 24; - int tf_x = customCmdUI_x + Math.round(customCmdUI_w / 2f) - Math.round((tf_w + but_w + padding_3) / 2) + padding_3; - int tf_y = y + h + commandBarH + padding_3; - //int tf_w = Math.round((customCmdUI_w - padding_3*2) * .75); - int tf_h = commandBarH - padding_3*2; - customCommandTF.setPosition(tf_x, tf_y); - customCommandTF.setWidth(tf_w); - customCommandTF.setHeight(tf_h); - int but_x = tf_x + customCommandTF.getWidth() + padding_3; - sendCustomCmdButton.setPosition(but_x, tf_y); - sendCustomCmdButton.setSize(but_w, tf_h - 1); + customCommandUIX = x; + customCommandUIWidth = w + 1; + customCommandUIMiddle = customCommandUIX + Math.round(customCommandUIWidth / 2f); + customCommandObjectW = Math.round(buttonWidth * 1.7); + customCommandObjectY = y + h + COMMAND_BAR_HEIGHT + PADDING_3; + customCommandObjectH = COMMAND_BAR_HEIGHT - (PADDING_3 * 2); + openCustomCommandPopup.setPosition(customCommandUIMiddle - (customCommandObjectW / 2), customCommandObjectY); + openCustomCommandPopup.setSize(customCommandObjectW, customCommandObjectH - 1); } private void updateHasUnappliedSettings(int _channel) { - hasUnappliedChanges[_channel] = !boardSettings.equalsLastValues(_channel); + channelHasUnappliedChanges[_channel] = !boardSettings.equalsLastValues(_channel); } public void updateHasUnappliedSettings() { @@ -463,35 +523,35 @@ class ADS1299SettingsController { } public void setHasUnappliedSettings(int _channel, boolean b) { - hasUnappliedChanges[_channel] = b; + channelHasUnappliedChanges[_channel] = b; } public void updateChanSettingsDropdowns(int chan, boolean isActive) { color darkNotActive = color(57); color c = isActive ? color(255) : darkNotActive; - + gainLists[chan].setValue(boardSettings.values.gain[chan].ordinal()); gainLists[chan].setColorBackground(c); gainLists[chan].setLock(!isActive); - + inputTypeLists[chan].setValue(boardSettings.values.inputType[chan].ordinal()); inputTypeLists[chan].setColorBackground(c); inputTypeLists[chan].setLock(!isActive); - - c = isActive ? (boardSettings.values.bias[chan] == Bias.INCLUDE ? yesOnColor : noOffColor) : darkNotActive; + + c = isActive ? (boardSettings.values.bias[chan] == Bias.INCLUDE ? YES_ON_COLOR : NO_OFF_COLOR) : darkNotActive; biasLists[chan].setValue(boardSettings.values.bias[chan].ordinal()); biasLists[chan].setColorBackground(c); biasLists[chan].setLock(!isActive); - - c = isActive ? (boardSettings.values.srb2[chan] == Srb2.CONNECT ? yesOnColor : noOffColor) : darkNotActive; + + c = isActive ? (boardSettings.values.srb2[chan] == Srb2.CONNECT ? YES_ON_COLOR : NO_OFF_COLOR) : darkNotActive; srb2Lists[chan].setValue(boardSettings.values.srb2[chan].ordinal()); srb2Lists[chan].setColorBackground(c); srb2Lists[chan].setLock(!isActive); - - c = isActive ? (boardSettings.values.srb1[chan] == Srb1.CONNECT ? yesOnColor : noOffColor) : darkNotActive; + + c = isActive ? (boardSettings.values.srb1[chan] == Srb1.CONNECT ? YES_ON_COLOR : NO_OFF_COLOR) : darkNotActive; srb1Lists[chan].setValue(boardSettings.values.srb1[chan].ordinal()); srb1Lists[chan].setColorBackground(c); - srb1Lists[chan].setLock(!isActive); + srb1Lists[chan].setLock(!isActive); } public void updateAllChanSettingsDropdowns() { @@ -508,37 +568,178 @@ class ADS1299SettingsController { channel = _i; } public void controlEvent(CallbackEvent theEvent) { - color _bgColor = #FFFFFF; //Selecting an item from ScrollableList triggers Broadcast - if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { int val = (int)(theEvent.getController()).getValue(); Map bob = ((ScrollableList)theEvent.getController()).getItem(val); ADSSettingsEnum myEnum = (ADSSettingsEnum)bob.get("value"); verbosePrint("HardwareSettings: " + (theEvent.getController()).getName() + " == " + myEnum.getName()); + updateBoardSettingsValues(channel, myEnum, theEvent.getController()); + } + } + } - if (myEnum instanceof Gain) { - //verbosePrint("HardwareSettings: previousVal == " + boardSettings.previousValues.gain[channel]); - boardSettings.values.gain[channel] = (Gain)myEnum; - } else if (myEnum instanceof InputType) { - boardSettings.values.inputType[channel] = (InputType)myEnum; - } else if (myEnum instanceof Bias) { - boardSettings.values.bias[channel] = (Bias)myEnum; - _bgColor = (Bias)myEnum == Bias.INCLUDE ? yesOnColor : noOffColor; - (theEvent.getController()).setColorBackground(_bgColor); - } else if (myEnum instanceof Srb2) { - boardSettings.values.srb2[channel] = (Srb2)myEnum; - _bgColor = (Srb2)myEnum == Srb2.CONNECT ? yesOnColor : noOffColor; - (theEvent.getController()).setColorBackground(_bgColor); - } else if (myEnum instanceof Srb1) { - boardSettings.values.srb1[channel] = (Srb1)myEnum; - _bgColor = (Srb1)myEnum == Srb1.CONNECT ? yesOnColor : noOffColor; - (theEvent.getController()).setColorBackground(_bgColor); + private class AllChannelSLCallbackListener implements CallbackListener { + + AllChannelSLCallbackListener() { + } + + public void controlEvent(CallbackEvent theEvent) { + //Selecting an item from ScrollableList triggers Broadcast + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + int val = (int)(theEvent.getController()).getValue(); + Map bob = ((ScrollableList)theEvent.getController()).getItem(val); + ADSSettingsEnum myEnum = (ADSSettingsEnum)bob.get("value"); + + for (int i = 0; i < channelIsSelected.length; i++) { + if (channelIsSelected[i]) { + updateBoardSettingsValues(i, myEnum, theEvent.getController()); + updateChanSettingsDropdowns(i, true); + } } + } + } + } + + private void updateBoardSettingsValues(int channel, ADSSettingsEnum myEnum, controlP5.Controller theController) { + color _bgColor = #FFFFFF; + if (myEnum instanceof Gain) { + //verbosePrint("HardwareSettings: previousVal == " + boardSettings.previousValues.gain[channel]); + boardSettings.values.gain[channel] = (Gain)myEnum; + } else if (myEnum instanceof InputType) { + boardSettings.values.inputType[channel] = (InputType)myEnum; + } else if (myEnum instanceof Bias) { + boardSettings.values.bias[channel] = (Bias)myEnum; + _bgColor = (Bias)myEnum == Bias.INCLUDE ? YES_ON_COLOR : NO_OFF_COLOR; + theController.setColorBackground(_bgColor); + } else if (myEnum instanceof Srb2) { + boardSettings.values.srb2[channel] = (Srb2)myEnum; + _bgColor = (Srb2)myEnum == Srb2.CONNECT ? YES_ON_COLOR : NO_OFF_COLOR; + theController.setColorBackground(_bgColor); + } else if (myEnum instanceof Srb1) { + boardSettings.values.srb1[channel] = (Srb1)myEnum; + _bgColor = (Srb1)myEnum == Srb1.CONNECT ? YES_ON_COLOR : NO_OFF_COLOR; + theController.setColorBackground(_bgColor); + updateAllSrb1Channels((Srb1) myEnum); + } + + updateHasUnappliedSettings(channel); + } - updateHasUnappliedSettings(channel); + private void updateAllSrb1Channels(Srb1 srb1State) { + boolean allOn = srb1State == Srb1.CONNECT; + for (int i = 0; i < channelCount; i++) { + if (boardSettings.values.srb1[i] != srb1State) { + boardSettings.values.srb1[i] = allOn ? Srb1.CONNECT : Srb1.DISCONNECT; + srb1Lists[i].setValue(allOn ? Srb1.CONNECT.ordinal() : Srb1.DISCONNECT.ordinal()); + srb1Lists[i].setColorBackground(allOn ? YES_ON_COLOR : NO_OFF_COLOR); + srb1Lists[i].setLock(false); } } } + + protected void setUIObjectVisibility() { + for (int i = 0; i < channelCount; i++) { + boolean b = activeChannels.contains(i); + channelSelectToggles[i].setVisible(b); + gainLists[i].setVisible(b); + inputTypeLists[i].setVisible(b); + biasLists[i].setVisible(b); + srb2Lists[i].setVisible(b); + srb1Lists[i].setVisible(b); + } + } + + protected void drawChannelStatus() { + for (int i = 0; i < channelCount; i++) { + if (channelHasUnappliedChanges[i]) { + pushStyle(); + fill(color(57, 128, 204, 190)); //light blue from TopNav + //fill(color(245, 64, 64, 180)); //light red + rect(x, y + channelBarHeight * i, w, channelBarHeight); + popStyle(); + } + } + } + + protected void drawLabels() { + channelSelectLabel.draw(); + gainLabel.draw(); + inputTypeLabel.draw(); + biasLabel.draw(); + srb2Label.draw(); + srb1Label.draw(); + } + + private Toggle createChannelSelectToggle(int _channel, String name) { + int _w = DEFAULT_TOGGLE_WIDTH; + int _h = DEFAULT_TOGGLE_WIDTH; + int _x = 0; + int _y = 0; + boolean _value = false; + final int channel = _channel; + + int _fontSize = 16; + Toggle thisToggle = hwsCp5.addToggle(name) + .setPosition(_x, _y) // temporary position + .setSize(_w, _h) + .setColorLabel(GREY_100) + .setColorForeground(color(120)) + .setColorBackground(color(150)) + .setColorActive(color(57, 128, 204)) + .setVisible(true) + .setValue(_value) + ; + thisToggle.getCaptionLabel() + .setFont(p3) + .toUpperCase(false) + .setSize(_fontSize) + .setText("") + .getStyle() //need to grab style before affecting margin and padding + .setMargin(0, 0, 0, 0) + .setPaddingLeft(0) + ; + thisToggle.onPress(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + boolean b = ((Toggle)theEvent.getController()).getBooleanValue(); + if (channel == -1) { + for (int i = 0; i < channelCount; i++) { + channelSelectToggles[i].setValue(b); + channelIsSelected[i] = b; + } + } else { + channelIsSelected[channel] = b; + } + } + }); + + if (checkMark_20x20 == null) { + checkMark_20x20 = loadImage("Checkmark_20x20.png"); + } + + if (checkMark_20x20 == null) { + println("Error: Could not load checkmark image"); + } + + return thisToggle; + } + + private void drawCheckMark(Toggle _toggle) { + float[] xy = _toggle.getPosition(); + if (_toggle.getBooleanValue()) { + pushStyle(); + image(checkMark_20x20, xy[0], xy[1], toggleWidthAndHeight, toggleWidthAndHeight); + popStyle(); + } + } + + public int getCommandBarHeight() { + return COMMAND_BAR_HEIGHT; + } + + public int getHeaderHeight() { + return CONTROLLER_HEADER_HEIGHT; + } }; void loadHardwareSettings(File selection) { @@ -548,9 +749,10 @@ void loadHardwareSettings(File selection) { if (currentBoard instanceof ADS1299SettingsBoard) { if (((ADS1299SettingsBoard)currentBoard).getADS1299Settings().loadSettingsValues(selection.getAbsolutePath())) { outputSuccess("Hardware Settings Loaded!"); - for (int i = 0; i < nchan; i++) { - w_timeSeries.adsSettingsController.updateChanSettingsDropdowns(i, currentBoard.isEXGChannelActive(i)); - w_timeSeries.adsSettingsController.updateHasUnappliedSettings(i); + for (int i = 0; i < globalChannelCount; i++) { + W_TimeSeries timeSeriesWidget = widgetManager.getTimeSeriesWidget(); + timeSeriesWidget.adsSettingsController.updateChanSettingsDropdowns(i, currentBoard.isEXGChannelActive(i)); + timeSeriesWidget.adsSettingsController.updateHasUnappliedSettings(i); } } else { outputError("Failed to load Hardware Settings."); diff --git a/OpenBCI_GUI/AccelerometerEnums.pde b/OpenBCI_GUI/AccelerometerEnums.pde new file mode 100644 index 000000000..72074988c --- /dev/null +++ b/OpenBCI_GUI/AccelerometerEnums.pde @@ -0,0 +1,84 @@ +public enum AccelerometerVerticalScale implements IndexingInterface +{ + AUTO (0, 0, "Auto"), + ONE_G (1, 1, "1 g"), + TWO_G (2, 2, "2 g"), + FOUR_G (3, 4, "4 g"); + + private int index; + private int value; + private String label; + + AccelerometerVerticalScale(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public int getHighestValue() { + int highestValue = 0; + for (AccelerometerVerticalScale scale : values()) { + if (scale.getValue() > highestValue) { + highestValue = scale.getValue(); + } + } + return highestValue; + } +} + +public enum AccelerometerHorizontalScale implements IndexingInterface +{ + ONE_SEC (1, 1, "1 sec"), + THREE_SEC (2, 3, "3 sec"), + FIVE_SEC (3, 5, "5 sec"), + TEN_SEC (4, 10, "10 sec"), + TWENTY_SEC (5, 20, "20 sec"); + + private int index; + private int value; + private String label; + + AccelerometerHorizontalScale(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public int getHighestValue() { + int highestValue = 0; + for (AccelerometerHorizontalScale scale : values()) { + if (scale.getValue() > highestValue) { + highestValue = scale.getValue(); + } + } + return highestValue; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/AnalogReadEnums.pde b/OpenBCI_GUI/AnalogReadEnums.pde new file mode 100644 index 000000000..eb943fe08 --- /dev/null +++ b/OpenBCI_GUI/AnalogReadEnums.pde @@ -0,0 +1,67 @@ +public enum AnalogReadVerticalScale implements IndexingInterface +{ + AUTO (0, 0, "Auto"), + FIFTY (1, 50, "50 uV"), + ONE_HUNDRED (2, 100, "100 uV"), + TWO_HUNDRED (3, 200, "200 uV"), + FOUR_HUNDRED (4, 400, "400 uV"), + ONE_THOUSAND_FIFTY (5, 1050, "1050 uV"), + TEN_THOUSAND (6, 10000, "10000 uV"); + + private int index; + private int value; + private String label; + + AnalogReadVerticalScale(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum AnalogReadHorizontalScale implements IndexingInterface +{ + ONE_SEC (1, 1, "1 sec"), + THREE_SEC (2, 3, "3 sec"), + FIVE_SEC (3, 5, "5 sec"), + TEN_SEC (4, 10, "10 sec"), + TWENTY_SEC (5, 20, "20 sec"); + + private int index; + private int value; + private String label; + + AnalogReadHorizontalScale(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/AuditoryNeurofeedback.pde b/OpenBCI_GUI/AuditoryNeurofeedback.pde index cb2686ca2..ff180624c 100644 --- a/OpenBCI_GUI/AuditoryNeurofeedback.pde +++ b/OpenBCI_GUI/AuditoryNeurofeedback.pde @@ -18,7 +18,7 @@ void asyncLoadAudioFiles() { for (int i = 0; i < _numSoundFiles; i++) { //Use large buffer size and cache files in memory try { - auditoryNfbFilePlayers[i] = new FilePlayer( minim.loadFileStream("bp" + (i+1) + ".mp3", 2048, true) ); + auditoryNfbFilePlayers[i] = new FilePlayer( minim.loadFileStream("Auditory_Neurofeedback/bp" + (i+1) + ".mp3", 2048, true) ); auditoryNfbGains[i] = new ddf.minim.ugens.Gain(-15.0f); auditoryNfbFilePlayers[i].patch(auditoryNfbGains[i]).patch(audioOutput); } catch (Exception e) { diff --git a/OpenBCI_GUI/AuxDataBoard.pde b/OpenBCI_GUI/AuxDataBoard.pde deleted file mode 100644 index c81dd45eb..000000000 --- a/OpenBCI_GUI/AuxDataBoard.pde +++ /dev/null @@ -1,15 +0,0 @@ - -interface AuxDataBoard { - - public List getAuxData(int maxSamples); - - public String[] getAuxChannelNames(); - - public double[][] getAuxFrameData(); - - public int getAuxSampleRate(); - - public int getNumAuxChannels(); - - public int getAuxTimestampChannel(); -}; diff --git a/OpenBCI_GUI/BandPowerEnums.pde b/OpenBCI_GUI/BandPowerEnums.pde new file mode 100644 index 000000000..c0597ffb1 --- /dev/null +++ b/OpenBCI_GUI/BandPowerEnums.pde @@ -0,0 +1,56 @@ + +public enum BPLogLin implements IndexingInterface { + LOG (0, "Log"), + LINEAR (1, "Linear"); + + private int index; + private String label; + + BPLogLin(int _index, String _label) { + this.index = _index; + this.label = _label; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum BPVerticalScale implements IndexingInterface { + SCALE_10 (0, 10, "10 uV"), + SCALE_50 (1, 50, "50 uV"), + SCALE_100 (2, 100, "100 uV"), + SCALE_500 (3, 500, "500 uV"), + SCALE_1000 (4, 1000, "1000 uV"), + SCALE_1500 (5, 1500, "1500 uV"); + + private int index; + private final int value; + private String label; + + BPVerticalScale(int index, int value, String label) { + this.index = index; + this.value = value; + this.label = label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/Board.pde b/OpenBCI_GUI/Board.pde index 3a5502046..aaef5f8a0 100644 --- a/OpenBCI_GUI/Board.pde +++ b/OpenBCI_GUI/Board.pde @@ -46,6 +46,10 @@ abstract class Board implements DataSource { dataThisFrame = getNewDataInternal(); + if (dataThisFrame == null) { + return; + } + for (int i = 0; i < dataThisFrame[0].length; i++) { double[] newEntry = new double[getTotalChannelCount()]; for (int j = 0; j < getTotalChannelCount(); j++) { @@ -87,6 +91,7 @@ abstract class Board implements DataSource { names[getTimestampChannel()] = "Timestamp"; names[getSampleIndexChannel()] = "Sample Index"; + names[getMarkerChannel()] = "Marker"; int[] exgChannels = getEXGChannels(); for (int i=0; i samplingRateCommands = new HashMap() {{ - put(16000, "~0"); - put(8000, "~1"); - put(4000, "~2"); - put(2000, "~3"); - put(1000, "~4"); - put(500, "~5"); - put(250, "~6"); - }}; - - public BoardCytonWifiBase() { - super(); - } - - public BoardCytonWifiBase(int samplingRate) { - super(); - samplingRateCache = samplingRate; - } - - @Override - public boolean initializeInternal() { - boolean res = super.initializeInternal(); - - if ((res) && (samplingRateCache > 0)){ - String command = samplingRateCommands.get(samplingRateCache); - sendCommand(command); - } - return res; - } - - @Override - protected PacketLossTracker setupPacketLossTracker() { - final int minSampleIndex = 0; - final int maxSampleIndex = 255; - return new PacketLossTracker(getSampleIndexChannel(), getTimestampChannel(), - minSampleIndex, maxSampleIndex); - } -}; - class CytonDefaultSettings extends ADS1299Settings { CytonDefaultSettings(Board theBoard) { super(theBoard); @@ -245,7 +174,7 @@ class CytonDefaultSettings extends ADS1299Settings { abstract class BoardCyton extends BoardBrainFlow implements ImpedanceSettingsBoard, AccelerometerCapableBoard, AnalogCapableBoard, DigitalCapableBoard, ADS1299SettingsBoard { - private final char[] channelSelectForSettings = {'1', '2', '3', '4', '5', '6', '7', '8', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I'}; + private final char[] channelIndentifierChars = {'1', '2', '3', '4', '5', '6', '7', '8', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I'}; private ADS1299Settings currentADS1299Settings; private boolean[] isCheckingImpedance; @@ -427,7 +356,7 @@ implements ImpedanceSettingsBoard, AccelerometerCapableBoard, AnalogCapableBoard } // for example: z 4 1 0 Z - String command = String.format("z%c%c%cZ", channelSelectForSettings[channel], p, n); + String command = String.format("z%c%c%cZ", channelIndentifierChars[channel], p, n); sendCommand(command); isCheckingImpedance[channel] = active; @@ -450,8 +379,8 @@ implements ImpedanceSettingsBoard, AccelerometerCapableBoard, AnalogCapableBoard currentADS1299Settings.values.gain[channel] = Gain.X1; currentADS1299Settings.values.inputType[channel] = InputType.NORMAL; - currentADS1299Settings.values.bias[channel] = Bias.INCLUDE; - currentADS1299Settings.values.srb2[channel] = Srb2.DISCONNECT; + currentADS1299Settings.values.bias[channel] = Bias.NO_INCLUDE; + currentADS1299Settings.values.srb2[channel] = Srb2.CONNECT; currentADS1299Settings.values.srb1[channel] = Srb1.DISCONNECT; fullCommand.append(currentADS1299Settings.getValuesString(channel, currentADS1299Settings.values)); @@ -470,7 +399,7 @@ implements ImpedanceSettingsBoard, AccelerometerCapableBoard, AnalogCapableBoard } // Format the impedance command string. Example: z 4 1 0 Z - String impedanceCommandString = String.format("z%c%c%cZ", channelSelectForSettings[channel], p, n); + String impedanceCommandString = String.format("z%c%c%cZ", channelIndentifierChars[channel], p, n); fullCommand.append(impedanceCommandString); final String commandToSend = fullCommand.toString(); @@ -556,7 +485,7 @@ implements ImpedanceSettingsBoard, AccelerometerCapableBoard, AnalogCapableBoard @Override public char getChannelSelector(int channel) { - return channelSelectForSettings[channel]; + return channelIndentifierChars[channel]; } public CytonBoardMode getBoardMode() { diff --git a/OpenBCI_GUI/BoardGanglion.pde b/OpenBCI_GUI/BoardGanglion.pde index 27a63aeec..20ab14a63 100644 --- a/OpenBCI_GUI/BoardGanglion.pde +++ b/OpenBCI_GUI/BoardGanglion.pde @@ -119,53 +119,6 @@ class BoardGanglionBLE extends BoardGanglion { } }; -class BoardGanglionWifi extends BoardGanglion { - // https://docs.openbci.com/docs/03Ganglion/GanglionSDK - private Map samplingRateCommands = new HashMap() {{ - put(25600, "~0"); - put(12800, "~1"); - put(6400, "~2"); - put(3200, "~3"); - put(1600, "~4"); - put(800, "~5"); - put(400, "~6"); - put(200, "~7"); - }}; - - public BoardGanglionWifi(String ipAddress, int samplingRate) { - super(); - this.ipAddress = ipAddress; - samplingRateCache = samplingRate; - } - - @Override - public boolean initializeInternal() - { - // turn on accel by default, or is it handled somewhere else? - boolean res = super.initializeInternal(); - - if ((res) && (samplingRateCache > 0)){ - String command = samplingRateCommands.get(samplingRateCache); - sendCommand(command); - } - - return res; - } - - @Override - public BoardIds getBoardId() { - return BoardIds.GANGLION_WIFI_BOARD; - } - - @Override - protected PacketLossTracker setupPacketLossTracker() { - final int minSampleIndex = 0; - final int maxSampleIndex = 200; - return new PacketLossTracker(getSampleIndexChannel(), getTimestampChannel(), - minSampleIndex, maxSampleIndex); - } -}; - abstract class BoardGanglion extends BoardBrainFlow implements AccelerometerCapableBoard { private final char[] deactivateChannelChars = {'1', '2', '3', '4', '5', '6', '7', '8', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i'}; @@ -308,4 +261,15 @@ abstract class BoardGanglion extends BoardBrainFlow implements AccelerometerCapa public int getAccelSampleRate() { return getSampleRate(); } + + @Override + public String[] getChannelNames() { + String[] output = super.getChannelNames(); + int[] resistanceChannels = getResistanceChannels(); + for (int i = 0; i < resistanceChannels.length - 1; i++) { + output[resistanceChannels[i]] = "Impedance Channel " + i; + } + output[resistanceChannels[resistanceChannels.length - 1]] = "Impedance Channel Reference"; + return output; + } }; diff --git a/OpenBCI_GUI/ChannelSelect.pde b/OpenBCI_GUI/ChannelSelect.pde new file mode 100644 index 000000000..963101cfd --- /dev/null +++ b/OpenBCI_GUI/ChannelSelect.pde @@ -0,0 +1,327 @@ +class ChannelSelect { + public ControlP5 cp5_chanSelect; + protected List cp5ElementsToCheck = new ArrayList(); + protected int x, y, w, h, navH; + private float tri_xpos = 0; + protected float chanSelectXPos = 0; + protected final int button_spacer = 10; + protected int offset; //offset on nav bar of checkboxes + protected int buttonW, buttonH; + protected boolean channelSelectHover; + protected boolean isVisible; + + ChannelSelect(PApplet _parentApplet, int _x, int _y, int _w, int _navH) { + x = _x; + y = _y; + w = _w; + h = _navH; + navH = _navH; + + //setup for checkboxes + cp5_chanSelect = new ControlP5(_parentApplet); + cp5_chanSelect.setGraphics(_parentApplet, 0, 0); + cp5_chanSelect.setAutoDraw(false); //draw only when specified + } + + public void update(int _x, int _y, int _w) { + x = _x; + y = _y; + w = _w; + if (mouseX > (chanSelectXPos) && mouseX < (tri_xpos + 10) && mouseY < (y - navH*0.25) && mouseY > (y - navH*0.65)) { + channelSelectHover = true; + } else { + channelSelectHover = false; + } + } + + public void draw() { + drawChannelSelectExpander(); + if (isVisible) { + cp5_chanSelect.draw(); + } + } + + protected void drawChannelSelectExpander() { + chanSelectXPos = x + 2; + pushStyle(); + noStroke(); + //change "Channels" text color and triangle color on hover + if (channelSelectHover) { + fill(OPENBCI_BLUE); + } else { + fill(OPENBCI_DARKBLUE); + } + textFont(p5, 12); + + text("Channels", chanSelectXPos, y - 6); + tri_xpos = x + textWidth("Channels") + 7; + + //draw triangle as pointing up or down, depending on if channel Select is active or closed + if (isVisible) { + triangle(tri_xpos, y - 13, tri_xpos + 6, y - 7, tri_xpos + 12, y - 13); + drawGrayBackground(x, y, w, navH); + } else { + triangle(tri_xpos, y - 7, tri_xpos + 6, y - 13, tri_xpos + 12, y - 7); + } + popStyle(); + } + + public void screenResized(PApplet _parentApplet) { + cp5_chanSelect.setGraphics(_parentApplet, 0, 0); + } + + public void mousePressed(boolean dropdownIsActive) { + if (!dropdownIsActive) { + if (mouseX > (chanSelectXPos) && mouseX < (tri_xpos + 10) && mouseY < (y - navH*0.25) && mouseY > (y - navH*0.65)) { + isVisible = !isVisible; + } + } + } + + protected int getMarginLeftOffset(int chan) { + return chan > 9 ? -9 : -6; + } + + public List getCp5ElementsForOverlapCheck() { + return cp5ElementsToCheck; + } + + public boolean isVisible() { + return isVisible; + } + + public void setIsVisible(boolean b) { + isVisible = b; + } + + public int getHeight() { + return h; + } + + public void drawGrayBackground(int _x, int _y, int _w, int _h) { + pushStyle(); + fill(200); + rect(_x, _y, _w, _h); + popStyle(); + } +} + +class ExGChannelSelect extends ChannelSelect { + + protected List channelButtons; + private List activeChannels = new ArrayList(); + + ExGChannelSelect(PApplet _parentApplet, int _x, int _y, int _w, int _navH) { + super(_parentApplet, _x, _y, _w, _navH); + createButtons(); + } + + public void draw() { + super.draw(); + + if (isVisible) { + drawExGChannelOnOffStatus(); + } + } + + public void update(int _x, int _y, int _w) { + super.update(_x, _y, _w); + updateChannelButtonPositions(); + } + + protected void drawExGChannelOnOffStatus() { + //Draw a border around toggle buttons to indicate if channel is on or off + pushStyle(); + int weight = 1; + strokeWeight(weight); + noFill(); + for (int i = 0; i < globalChannelCount; i++) { + color c = currentBoard.isEXGChannelActive(i) ? color(0,255,0,255) : color(255,0,0,255); + stroke(c); + float[] buttonXY = channelButtons.get(i).getPosition(); + rect(buttonXY[0] - weight, buttonXY[1] - weight, channelButtons.get(i).getWidth() + weight, channelButtons.get(i).getHeight() + weight); + } + popStyle(); + } + + protected void createButtons() { + channelButtons = new ArrayList(); + int numButtons = currentBoard.getNumEXGChannels(); + + int checkSize = navH - 6; + offset = (navH - checkSize)/2; + + channelSelectHover = false; + isVisible = false; + + buttonW = checkSize; + buttonH = buttonW; + + for (int i = 0; i < numButtons; i++) { + //start all items as invisible until user clicks dropdown to show checkboxes + channelButtons.add( + createToggle("ch"+(i+1), (i+1), x + (button_spacer*(i+2)) + (buttonW*i), y + offset, buttonW, buttonH) + ); + cp5ElementsToCheck.add((controlP5.Controller)channelButtons.get(i)); + } + } + + protected Toggle createToggle(String name, int chan, int _x, int _y, int _w, int _h) { + int _fontSize = 12; + Toggle myButton = cp5_chanSelect.addToggle(name) + .setPosition(_x, _y) + .setSize(_w, _h) + .setColorLabel(OPENBCI_DARKBLUE) + .setColorForeground(color(120)) + .setColorBackground(color(150)) + .setColorActive(color(57, 128, 204)) + .setVisible(true) + ; + myButton + .getCaptionLabel() + .setFont(createFont("Arial", _fontSize, true)) + .toUpperCase(false) + .setSize(_fontSize) + .setText(String.valueOf(chan)) + .getStyle() //need to grab style before affecting margin and padding + .setMargin(-_h - 3, 0, 0, getMarginLeftOffset(chan)) + .setPaddingLeft(10) + ; + myButton.onPress(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + int chan = Integer.parseInt(((Toggle)theEvent.getController()).getCaptionLabel().getText()) - 1; + boolean b = ((Toggle)theEvent.getController()).getBooleanValue(); + setToggleState(chan, b); + } + }); + return myButton; + } + + protected void updateChannelButtonPositions() { + for (int i = 0; i < currentBoard.getNumEXGChannels(); i++) { + channelButtons.get(i).setPosition(x + (button_spacer*(i+1)) + (buttonW*i), y + offset); + } + } + + public void deactivateAllButtons() { + for (int i = 0; i < globalChannelCount; i++) { + channelButtons.get(i).setState(false); + } + activeChannels.clear(); + } + + public void activateAllButtons() { + for (int i = 0; i < globalChannelCount; i++) { + channelButtons.get(i).setState(true); + activeChannels.add(i); + } + Collections.sort(activeChannels); + } + + public void setToggleState(Integer chan, boolean b) { + channelButtons.get(chan).setState(b); + if (b && !activeChannels.contains(chan)) { + activeChannels.add(chan); + } else if (!b && activeChannels.contains(chan)) { + activeChannels.remove(chan); + } + Collections.sort(activeChannels); + } + + public List getActiveChannels() { + return activeChannels; + } + + public void updateChannelSelection(List channels) { + // First deactivate all channels + deactivateAllButtons(); + + // Then activate only the selected channels + for (Integer channel : channels) { + if (channel >= 0 && channel < channelButtons.size()) { + setToggleState(channel, true); // Changed from toggleButton + } + } + } +} + +class DualChannelSelector { + private final int ROW_LABEL_WIDTH = 28; + private final int ROW_LABEL_SPACER = 4; + + private String firstRowLabel = "Top"; + private String secondRowLabel = "Bot"; + + private boolean isFirstRowChannelSelect = true; + + DualChannelSelector (boolean isFirstRow) { + isFirstRowChannelSelect = isFirstRow; + } + + public boolean getIsFirstRowChannelSelect() { + return isFirstRowChannelSelect; + } + + public void setFirstRowLabel(String s) { + firstRowLabel = s; + } + + public void setSecondRowLabel(String s) { + secondRowLabel = s; + } + + public void drawRowLabel(int _x, int _y, int _offset) { + pushStyle(); + fill(0); + textFont(p5, 12); + textAlign(CENTER, TOP); + String label = isFirstRowChannelSelect ? firstRowLabel : secondRowLabel; + text(label, _x + ROW_LABEL_SPACER + ROW_LABEL_WIDTH/2, _y + _offset); + popStyle(); + } + + public int getRowLabelWidth() { + return ROW_LABEL_WIDTH; + } + + public int getRowLabelSpacer() { + return ROW_LABEL_SPACER; + } +} + +class DualExGChannelSelect extends ExGChannelSelect { + + DualChannelSelector dualChannelSelector; + + DualExGChannelSelect(PApplet _parentApplet, int _x, int _y, int _w, int _navH, boolean isFirstRow) { + super(_parentApplet, _x, _y, _w, _navH); + dualChannelSelector = new DualChannelSelector(isFirstRow); + } + + @Override + public void draw() { + if (dualChannelSelector.getIsFirstRowChannelSelect()) { + drawChannelSelectExpander(); + } else { + //Draw extra grey space behind the second row of checklist buttons + if (isVisible) { + drawGrayBackground(x, y, w, h); + } + } + + if (isVisible) { + cp5_chanSelect.draw(); + drawExGChannelOnOffStatus(); + dualChannelSelector.drawRowLabel(x, y, offset); + } + } + + @Override + protected void updateChannelButtonPositions() { + final int ROW_LABEL_WIDTH = dualChannelSelector.getRowLabelWidth(); + final int ROW_LABEL_SPACER = dualChannelSelector.getRowLabelSpacer(); + for (int i = 0; i < globalChannelCount; i++) { + channelButtons.get(i).setPosition(x + ROW_LABEL_WIDTH + ROW_LABEL_SPACER + (button_spacer*(i+1)) + (buttonW*i), y + offset); + } + } +} diff --git a/OpenBCI_GUI/CircularFIFODataBuffer.pde b/OpenBCI_GUI/CircularFIFODataBuffer.pde new file mode 100644 index 000000000..dd6af4761 --- /dev/null +++ b/OpenBCI_GUI/CircularFIFODataBuffer.pde @@ -0,0 +1,72 @@ +public class CircularFIFODataBuffer { + + private float[][] buffer; + private int numChannels; + private int maxSamples; + private int[] front; + private int[] rear; + private int[] count; + + // Constructor to initialize the circular FIFO buffer with a specified maxSamples + public CircularFIFODataBuffer(int numChannels, int maxSamples) { + this.numChannels = numChannels; + this.maxSamples = maxSamples; + initArrays(); + } + + // Method to add a new float value to the buffer + public void add(int channel, float newValue) { + if (count[channel] < maxSamples) { + rear[channel] = (rear[channel] + 1) % maxSamples; + buffer[channel][rear[channel]] = newValue; + count[channel]++; + } else { + // Buffer is full, remove the oldest value + front[channel] = (front[channel] + 1) % maxSamples; + rear[channel] = (rear[channel] + 1) % maxSamples; + buffer[channel][rear[channel]] = newValue; + } + } + + // Method to get the 2D float array from the buffer + public float[][] getBuffer() { + float[][] result = new float[numChannels][maxSamples]; + for (int channel = 0; channel < numChannels; channel++) { + int index = front[channel]; + for (int sample = 0; sample < count[channel]; sample++) { + result[channel][sample] = buffer[channel][index]; + index = (index + 1) % maxSamples; + } + } + return result; + } + + // Method to get the last 'lastSamples' samples from the buffer + public float[][] getBuffer(int lastSamples) { + float[][] result = new float[numChannels][lastSamples]; + for (int channel = 0; channel < numChannels; channel++) { + int index = (rear[channel] - lastSamples + 1 + maxSamples) % maxSamples; + for (int sample = 0; sample < lastSamples; sample++) { + result[channel][sample] = buffer[channel][index]; + index = (index + 1) % maxSamples; + } + } + return result; + } + + public void initArrays() { + this.buffer = new float[numChannels][maxSamples]; + this.front = new int[numChannels]; + this.rear = new int[numChannels]; + this.count = new int[numChannels]; + Arrays.fill(front, 0); + Arrays.fill(rear, -1); + Arrays.fill(count, 0); + + for (int i = 0; i < numChannels; i++) { + for (int j = 0; j < maxSamples; j++) { + add(i, 0.0f); + } + } + } +} diff --git a/OpenBCI_GUI/ConsoleLog.pde b/OpenBCI_GUI/ConsoleLog.pde index dd623da0d..111333742 100644 --- a/OpenBCI_GUI/ConsoleLog.pde +++ b/OpenBCI_GUI/ConsoleLog.pde @@ -60,7 +60,7 @@ static class ConsoleWindow extends PApplet implements Runnable { logApplet = this; - surface.setAlwaysOnTop(false); + surface.setAlwaysOnTop(true); surface.setResizable(false); Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); @@ -69,6 +69,8 @@ static class ConsoleWindow extends PApplet implements Runnable { clipboardCopy = new ClipHelper(); cp5 = new ControlP5(this); + cp5.setGraphics(logApplet, 0, 0); + cp5.setAutoDraw(false); PFont textAreaFont = createFont("Arial", 12, true); consoleTextArea = cp5.addTextarea("ConsoleWindow") .setPosition(0, headerHeight) @@ -113,11 +115,12 @@ static class ConsoleWindow extends PApplet implements Runnable { } void draw() { - clear(); scene(); - cp5.draw(); - //checks if the screen is resized, similar to main GUI window - screenResized(); + try { + cp5.draw(); + } catch (ConcurrentModificationException e) { + println("ConsoleLog: Error drawing cp5: " + e.getMessage()); + } } void screenResized() { diff --git a/OpenBCI_GUI/Containers.pde b/OpenBCI_GUI/Containers.pde index 1dbd200e4..bd1bdbe71 100644 --- a/OpenBCI_GUI/Containers.pde +++ b/OpenBCI_GUI/Containers.pde @@ -80,8 +80,8 @@ void drawContainers() { if (widthOfLastScreen_C != width || heightOfLastScreen_C != height) { setupContainers(); //setupVizs(); //container extension example (more below) - settings.widthOfLastScreen = width; - settings.heightOfLastScreen = height; + sessionSettings.widthOfLastScreen = width; + sessionSettings.heightOfLastScreen = height; } } diff --git a/OpenBCI_GUI/ControlPanel.pde b/OpenBCI_GUI/ControlPanel.pde index 840934113..e50e9ea1e 100644 --- a/OpenBCI_GUI/ControlPanel.pde +++ b/OpenBCI_GUI/ControlPanel.pde @@ -35,8 +35,6 @@ class ControlPanel { public int x, y, w, h; public boolean isOpen; - PlotFontInfo fontInfo; - //various control panel elements that are unique to specific datasources DataSourceBox dataSourceBox; SerialBox serialBox; @@ -50,10 +48,7 @@ class ControlPanel { StreamingBoardBox streamingBoardBox; BLEBox bleBox; public SessionDataBox dataLogBoxGanglion; - WifiBox wifiBox; - InterfaceBoxCyton interfaceBoxCyton; InterfaceBoxGanglion interfaceBoxGanglion; - SampleRateCytonBox sampleRateCytonBox; SampleRateGanglionBox sampleRateGanglionBox; SDBox sdBox; BrainFlowStreamerBox bfStreamerBoxCyton; @@ -81,30 +76,22 @@ class ControlPanel { h = height - int(helpWidget.h); isOpen = false; - fontInfo = new PlotFontInfo(); globalPadding = 10; //controls the padding of all elements on the control panel - //boxes active when eegDataSource = Normal (OpenBCI) dataSourceBox = new DataSourceBox(x, y, w, h, globalPadding); - interfaceBoxCyton = new InterfaceBoxCyton(x + w, dataSourceBox.y, w, h, globalPadding); interfaceBoxGanglion = new InterfaceBoxGanglion(x + w, dataSourceBox.y, w, h, globalPadding); comPortBox = new ComPortBox(x+w*2, y, w, h, globalPadding); rcBox = new RadioConfigBox(x+w, y + comPortBox.h, w, h, globalPadding); - serialBox = new SerialBox(x + w, interfaceBoxCyton.y + interfaceBoxCyton.h, w, h, globalPadding); - wifiBox = new WifiBox(x + w + x + w - 3, interfaceBoxCyton.y, w, h, globalPadding); + serialBox = new SerialBox(x + w, dataSourceBox.y, w, h, globalPadding); channelCountBox = new ChannelCountBox(x + w, (serialBox.y + serialBox.h), w, h, globalPadding); dataLogBoxCyton = new SessionDataBox(x + w, (channelCountBox.y + channelCountBox.h), w, h, globalPadding, DATASOURCE_CYTON, dataLogger.getDataLoggerOutputFormat(), "sessionNameCyton"); bfStreamerBoxCyton = new BrainFlowStreamerBox(x + w, (dataLogBoxCyton.y + dataLogBoxCyton.h), w, h, globalPadding, "bfStreamerCyton"); sdBox = new SDBox(x + w, (bfStreamerBoxCyton.y + bfStreamerBoxCyton.h), w, h, globalPadding); - //Draw this to the right of the other cyton boxes - sampleRateCytonBox = new SampleRateCytonBox(wifiBox.x, wifiBox.y + wifiBox.h, w, h, globalPadding); - - //boxes active when eegDataSource = Playback int playbackWidth = int(w * 1.35); playbackFileBox = new PlaybackFileBox(x + w, dataSourceBox.y, playbackWidth, h, globalPadding); recentPlaybackBox = new RecentPlaybackBox(x + w, (playbackFileBox.y + playbackFileBox.h), playbackWidth, h, globalPadding); @@ -129,7 +116,6 @@ class ControlPanel { public void resetListItems(){ comPortBox.serialList.activeItem = -1; bleBox.bleList.activeItem = -1; - wifiBox.wifiList.activeItem = -1; } public void open(){ @@ -179,9 +165,6 @@ class ControlPanel { channelPopup.update(); dataLogBoxGanglion.update(); - - wifiBox.update(); - interfaceBoxCyton.update(); interfaceBoxGanglion.update(); } @@ -199,36 +182,23 @@ class ControlPanel { //Carefully draw certain boxes based on UI/UX flow... let each box handle what is drawn inside with localCp5 instances if (eegDataSource == DATASOURCE_CYTON) { //when data source is from OpenBCI - interfaceBoxCyton.draw(); - if (selectedProtocol != BoardProtocol.NONE) { - if (selectedProtocol == BoardProtocol.SERIAL) { - serialBox.y = interfaceBoxCyton.y + interfaceBoxCyton.h; - serialBox.draw(); - channelCountBox.y = serialBox.y + serialBox.h; - if (rcBox.isShowing) { - comPortBox.draw(); - rcBox.draw(); - comPortBox.serialList.setVisible(true); - if (channelPopup.wasClicked()) { - channelPopup.draw(); - } - } - } else if (selectedProtocol == BoardProtocol.WIFI) { - wifiBox.y = interfaceBoxCyton.y; - wifiBox.x = interfaceBoxCyton.x + interfaceBoxCyton.w; - sampleRateCytonBox.y = wifiBox.y + wifiBox.h; - channelCountBox.y = interfaceBoxCyton.y + interfaceBoxCyton.h; - wifiBox.draw(); - sampleRateCytonBox.draw(); + serialBox.draw(); + channelCountBox.y = serialBox.y + serialBox.h; + if (rcBox.isShowing) { + comPortBox.draw(); + rcBox.draw(); + comPortBox.serialList.setVisible(true); + if (channelPopup.wasClicked()) { + channelPopup.draw(); } - dataLogBoxCyton.y = channelCountBox.y + channelCountBox.h; - bfStreamerBoxCyton.y = dataLogBoxCyton.y + dataLogBoxCyton.h; - sdBox.y = bfStreamerBoxCyton.y + bfStreamerBoxCyton.h; - channelCountBox.draw(); - sdBox.draw(); - bfStreamerBoxCyton.draw(); - dataLogBoxCyton.draw(); //Drawing here allows max file size dropdown to be drawn on top } + dataLogBoxCyton.y = channelCountBox.y + channelCountBox.h; + bfStreamerBoxCyton.y = dataLogBoxCyton.y + dataLogBoxCyton.h; + sdBox.y = bfStreamerBoxCyton.y + bfStreamerBoxCyton.h; + channelCountBox.draw(); + sdBox.draw(); + bfStreamerBoxCyton.draw(); + dataLogBoxCyton.draw(); //Drawing here allows max file size dropdown to be drawn on top } else if (eegDataSource == DATASOURCE_PLAYBACKFILE) { //when data source is from playback file recentPlaybackBox.draw(); playbackFileBox.draw(); @@ -244,14 +214,6 @@ class ControlPanel { bleBox.y = interfaceBoxGanglion.y + interfaceBoxGanglion.h; dataLogBoxGanglion.y = bleBox.y + bleBox.h; bleBox.draw(); - } else if (selectedProtocol == BoardProtocol.WIFI) { - wifiBox.y = interfaceBoxGanglion.y; - wifiBox.x = interfaceBoxGanglion.x + interfaceBoxGanglion.w; - sampleRateGanglionBox.y = wifiBox.y + wifiBox.h; - sampleRateGanglionBox.x = wifiBox.x; - dataLogBoxGanglion.y = interfaceBoxGanglion.y + interfaceBoxGanglion.h; - wifiBox.draw(); - sampleRateGanglionBox.draw(); } bfStreamerBoxGanglion.y = dataLogBoxGanglion.y + dataLogBoxGanglion.h; bfStreamerBoxGanglion.draw(); @@ -281,7 +243,7 @@ class ControlPanel { public void hideRadioPopoutBox() { rcBox.isShowing = false; comPortBox.isShowing = false; - serialBox.popOutRadioConfigButton.getCaptionLabel().setText("Manual >"); + serialBox.popOutRadioConfigButton.setOff(); rcBox.closeSerialPort(); } @@ -304,18 +266,7 @@ class ControlPanel { sb.append("OpenBCISession_"); sb.append(dataLogger.getSessionName()); sb.append(File.separator); - settings.setSessionPath(sb.toString()); - } - - public void setDataLoggerOutputs() { - if (eegDataSource == DATASOURCE_CYTON) { - // Store the current text field value of "Session Name" to be passed along to dataFiles - dataLogger.setSessionName(controlPanel.dataLogBoxCyton.getSessionTextfieldString()); - } else if (eegDataSource == DATASOURCE_GANGLION) { - dataLogger.setSessionName(controlPanel.dataLogBoxGanglion.getSessionTextfieldString()); - } else if (eegDataSource == DATASOURCE_SYNTHETIC) { - dataLogger.setSessionName(directoryManager.getFileNameDateTime()); - } + dataLogger.setSessionPath(sb.toString()); } public void setBrainFlowStreamerOutput() { @@ -353,28 +304,34 @@ class ControlPanel { class DataSourceBox { public int x, y, w, h, padding; //size and position - private int numItems; + private final int NUM_ITEMS = 5; private int boxHeight = 24; private int spacing = 43; private ControlP5 datasource_cp5; private MenuList sourceList; + private boolean initialUpdate = false; DataSourceBox(int _x, int _y, int _w, int _h, int _padding) { - numItems = 5; x = _x; y = _y; w = _w; - h = spacing + (numItems * boxHeight); + h = spacing + (NUM_ITEMS * boxHeight); padding = _padding; //Instantiate local cp5 for this box datasource_cp5 = new ControlP5(ourApplet); datasource_cp5.setGraphics(ourApplet, 0,0); datasource_cp5.setAutoDraw(false); - createDatasourceList(datasource_cp5, "sourceList", x + padding, y + padding*2 + 13, w - padding*2, numItems * boxHeight, p3); + createDatasourceList(datasource_cp5, "DataSourceList", x + padding, y + padding*2 + 13, w - padding*2, NUM_ITEMS * boxHeight, p3); } public void update() { + if (!initialUpdate) { + initialUpdate = true; + if (eegDataSource >= 0) { + sourceList.selectItem(0); //Select the first item in the list on startup + } + } } public void draw() { @@ -395,8 +352,6 @@ class DataSourceBox { private void createDatasourceList(ControlP5 _cp5, String name, int _x, int _y, int _w, int _h, PFont font) { sourceList = new MenuList(_cp5, name, _w, _h, font); sourceList.setPosition(_x, _y); - // sourceList.itemHeight = 28; - // sourceList.padding = 9; sourceList.addItem("CYTON (live)", DATASOURCE_CYTON); sourceList.addItem("GANGLION (live)", DATASOURCE_GANGLION); sourceList.addItem("PLAYBACK (from file)", DATASOURCE_PLAYBACKFILE); @@ -409,7 +364,7 @@ class DataSourceBox { Map bob = sourceList.getItem(int(sourceList.getValue())); String str = (String)bob.get("headline"); // Get the text displayed in the MenuList int newDataSource = (int)bob.get("value"); - settings.controlEventDataSource = str; //Used for output message on system start + sessionSettings.controlEventDataSource = str; //Used for output message on system start eegDataSource = newDataSource; //Reset protocol @@ -418,12 +373,10 @@ class DataSourceBox { //Perform this check in a way that ignores order of items in the menulist if (eegDataSource == DATASOURCE_CYTON) { controlPanel.channelCountBox.set8ChanButtonActive(); - controlPanel.interfaceBoxCyton.resetCytonSelectedProtocol(); - controlPanel.wifiBox.setDefaultToDynamicIP(); + selectedProtocol = BoardProtocol.SERIAL; } else if (eegDataSource == DATASOURCE_GANGLION) { - updateToNChan(4); + updateGlobalChannelCount(4); controlPanel.interfaceBoxGanglion.resetGanglionSelectedProtocol(); - controlPanel.wifiBox.setDefaultToDynamicIP(); } else if (eegDataSource == DATASOURCE_PLAYBACKFILE) { //GUI auto detects number of channels for playback when file is selected } else if (eegDataSource == DATASOURCE_STREAMING) { @@ -456,7 +409,7 @@ class SerialBox { cytonsb_cp5.setAutoDraw(false); createAutoConnectButton("cytonAutoConnectButton", "AUTO-CONNECT", x + padding, y + padding*3 + 4, w - padding*3 - 70, 24); - createRadioConfigButton("cytonRadioConfigButton", "Manual >", x + w - 70 - padding, y + padding*3 + 4, 70, 24); + createRadioConfigButton("cytonRadioConfigButton", "Settings", x + w - 70 - padding, y + padding*3 + 4, 70, 24); } public void update() { @@ -474,9 +427,7 @@ class SerialBox { text("SERIAL CONNECT", x + padding, y + padding); popStyle(); - if (selectedProtocol == BoardProtocol.SERIAL) { - cytonsb_cp5.draw(); - } + cytonsb_cp5.draw(); } private Button createSBButton(String name, String text, int _x, int _y, int _w, int _h) { @@ -496,16 +447,15 @@ class SerialBox { private void createRadioConfigButton(String name, String text, int _x, int _y, int _w, int _h) { popOutRadioConfigButton = createSBButton(name, text, _x, _y, _w, _h); + popOutRadioConfigButton.setSwitch(true); popOutRadioConfigButton.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - if (selectedProtocol == BoardProtocol.SERIAL) { - if (controlPanel.rcBox.isShowing) { - controlPanel.hideRadioPopoutBox(); - } else { - controlPanel.rcBox.isShowing = true; - controlPanel.rcBox.print_onscreen(controlPanel.rcBox.initial_message); - popOutRadioConfigButton.getCaptionLabel().setText("Manual <"); - } + boolean showRadioBox = popOutRadioConfigButton.isOn(); + if (!showRadioBox) { + controlPanel.hideRadioPopoutBox(); + } else { + controlPanel.rcBox.isShowing = true; + controlPanel.rcBox.print_onscreen(controlPanel.rcBox.initial_message); } } }); @@ -586,8 +536,8 @@ class ComPortBox { public void controlEvent(CallbackEvent theEvent) { if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { Map bob = serialList.getItem(int(serialList.getValue())); - openBCI_portName = (String)bob.get("subline"); - output("ControlPanel: Selected OpenBCI Port " + openBCI_portName); + cytonDonglePortName = (String)bob.get("subline"); + output("ControlPanel: Selected OpenBCI Port " + cytonDonglePortName); } } }); @@ -598,7 +548,7 @@ class ComPortBox { println("\n-------------------------------------------------\nControlPanel: Attempting to Auto-Connect to Cyton\n-------------------------------------------------\n"); LinkedList comPorts = getCytonComPorts(); if (!comPorts.isEmpty()) { - openBCI_portName = comPorts.getFirst(); + cytonDonglePortName = comPorts.getFirst(); if (cytonRadioCfg.get_channel()) { controlPanel.initBox.initButtonPressed(); } else { @@ -614,7 +564,7 @@ class ComPortBox { //This is called after overlay has a chance to draw on top to inform users the GUI is working and not crashed private void cytonAutoConnect_AutoScan() { if (cytonRadioCfg.scan_channels()) { - println("Successfully connected to Cyton using " + openBCI_portName); + println("Successfully connected to Cyton using " + cytonDonglePortName); controlPanel.initBox.initButtonPressed(); } else { outputError("Unable to connect to Cyton. Please check hardware and power source."); @@ -724,7 +674,7 @@ class BLEBox { bleBox_cp5.draw(); } - private void refreshGanglionNativeList() { +private void refreshGanglionNativeList() { if (bleIsRefreshing) { output("Search for Ganglions using Native Bluetooth is in progress."); return; @@ -837,331 +787,18 @@ class BLEBox { } }; -class WifiBox { - public int x, y, w, h, padding; //size and position - private boolean wifiIsRefreshing = false; - private ControlP5 wifiBox_cp5; - private MenuList wifiList; - private Button refreshWifi; - private Button wifiIPAddressDynamic; - private Button wifiIPAddressStatic; - private Textfield staticIPAddressTF; - private int wifiDynamic_x; - private int wifiStatic_x; - private int wifiButtons_y; - private int refreshWifi_x; - private int refreshWifi_y; - - WifiBox(int _x, int _y, int _w, int _h, int _padding) { - x = _x; - y = _y; - w = _w; - h = 184 + _padding + 14; - padding = _padding; - - //Instantiate local cp5 for this box - wifiBox_cp5 = new ControlP5(ourApplet); - wifiBox_cp5.setGraphics(ourApplet, 0,0); - wifiBox_cp5.setAutoDraw(false); - - wifiDynamic_x = x + padding; - wifiStatic_x = x + padding*2 + (w-padding*3)/2; - wifiButtons_y = y + padding*2 + 16; - createDynamicIPAddressButton("wifiIPAddressDynamicButton", "DYNAMIC IP", wifiDynamic_x, wifiButtons_y, (w-padding*3)/2, 24); - createStaticIPAddressButton("wifiIPAddressStaticButton", "STATIC IP", wifiStatic_x, wifiButtons_y, (w-padding*3)/2, 24); - - refreshWifi_x = x + padding; - refreshWifi_y = y + padding*5 + 72 + 8 + 24; - createRefreshWifiButton("refreshWifiButton", "START SEARCH", refreshWifi_x, refreshWifi_y, w - padding*5, 24); - createWifiList(wifiBox_cp5, "wifiList", x + padding, y + padding*4 + 8 + 24, w - padding*2, 72 + 8, p3); - createStaticIPAddressTextfield(); - } - - public void update() { - wifiList.updateMenu(); - copyPaste.checkForCopyPaste(staticIPAddressTF); - } - - public void draw() { - pushStyle(); - fill(boxColor); - stroke(boxStrokeColor); - strokeWeight(1); - rect(x, y, w, h); - fill(OPENBCI_DARKBLUE); - textFont(h3, 16); - textAlign(LEFT, TOP); - text("WIFI SHIELDS", x + padding, y + padding); - popStyle(); - - wifiDynamic_x = x + padding; - wifiStatic_x = x + padding*2 + (w-padding*3)/2; - wifiButtons_y = y + padding*2 + 16; - wifiIPAddressDynamic.setPosition(wifiDynamic_x, wifiButtons_y); - wifiIPAddressStatic.setPosition(wifiStatic_x, wifiButtons_y); - - if (controlPanel.getWifiSearchStyle() == controlPanel.WIFI_STATIC) { - pushStyle(); - fill(OPENBCI_DARKBLUE); - textFont(h3, 16); - textAlign(LEFT, TOP); - text("ENTER IP ADDRESS", x + padding, y + h - 24 - 12 - padding*2); - popStyle(); - staticIPAddressTF.setPosition(x + padding, y + h - 24 - padding); - } else { - wifiList.setPosition(x + padding, wifiButtons_y + 24 + padding); - - refreshWifi_x = x + padding; - refreshWifi_y = y + padding*5 + 72 + 8 + 24; - refreshWifi.setPosition(refreshWifi_x, refreshWifi_y); - - String boardIpInfo = "BOARD IP: "; - if (wifi_portName != "N/A") { // If user has selected a board from the menulist... - boardIpInfo += wifi_ipAddress; - } - pushStyle(); - fill(OPENBCI_DARKBLUE); - textFont(h3, 16); - textAlign(LEFT, TOP); - text(boardIpInfo, x + w/2 - textWidth(boardIpInfo)/2, y + h - padding - 15); - popStyle(); - - if (wifiIsRefreshing){ - //Display spinning cog gif - image(loadingGIF_blue, x + 225, refreshWifi_y + 4, 20, 20); - } else { - //Draw small grey circle - pushStyle(); - fill(#999999); - ellipseMode(CENTER); - ellipse(x + 225 + 10, refreshWifi_y + 12, 12, 12); - popStyle(); - } - } - - wifiBox_cp5.draw(); - } - - public void refreshWifiList() { - output("Wifi Devices Refreshing"); - wifiList.items.clear(); - Thread thread = new Thread(){ - public void run() { - refreshWifi.getCaptionLabel().setText("SEARCHING..."); - wifiIsRefreshing = true; - try { - List devices = SSDPClient.discover (3000, "urn:schemas-upnp-org:device:Basic:1"); - if (devices.isEmpty ()) { - println("No WIFI Shields found"); - } - for (int i = 0; i < devices.size(); i++) { - wifiList.addItem(devices.get(i).getName(), devices.get(i).getIPAddress(), ""); - } - wifiList.updateMenu(); - } catch (Exception e) { - println("Exception in wifi shield scanning"); - e.printStackTrace (); - } - refreshWifi.getCaptionLabel().setText("START SEARCH"); - wifiIsRefreshing = false; - } - }; - thread.start(); - } - - private void createDynamicIPAddressButton(String name, String text, int _x, int _y, int _w, int _h) { - wifiIPAddressDynamic = createButton(wifiBox_cp5, name, text, _x, _y, _w, _h); - wifiIPAddressDynamic.setSwitch(true); - wifiIPAddressDynamic.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - h = 208; - controlPanel.setWiFiSearchStyle(controlPanel.WIFI_DYNAMIC); - println("ControlPanel: Using Dynamic IP address of the WiFi Shield!"); - wifiIPAddressDynamic.setOn(); - wifiIPAddressStatic.setOff(); - staticIPAddressTF.setVisible(false); - wifiList.setVisible(true); - } - }); - wifiIPAddressDynamic.setOn(); - } - - private void createStaticIPAddressButton(String name, String text, int _x, int _y, int _w, int _h) { - wifiIPAddressStatic = createButton(wifiBox_cp5, name, text, _x, _y, _w, _h); - wifiIPAddressStatic.setSwitch(true); - wifiIPAddressStatic.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - h = 120; - controlPanel.setWiFiSearchStyle(controlPanel.WIFI_STATIC); - println("ControlPanel: Using Static IP address of the WiFi Shield!"); - wifiIPAddressDynamic.setOff(); - wifiIPAddressStatic.setOn(); - staticIPAddressTF.setVisible(true); - wifiList.setVisible(false); - } - }); - } - - private void createRefreshWifiButton(String name, String text, int _x, int _y, int _w, int _h) { - refreshWifi = createButton(wifiBox_cp5, name, text, _x, _y, _w, _h); - refreshWifi.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - refreshWifiList(); - } - }); - } - - private void createWifiList(ControlP5 _cp5, String name, int _x, int _y, int _w, int _h, PFont font) { - wifiList = new MenuList(_cp5, name, _w, _h, font); - wifiList.setPosition(_x, _y); - wifiList.addCallback(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { - Map bob = wifiList.getItem(int(wifiList.getValue())); - wifi_portName = (String)bob.get("headline"); - wifi_ipAddress = (String)bob.get("subline"); - output("Selected WiFi Board: " + wifi_portName+ ", WiFi IP Address: " + wifi_ipAddress ); - } - } - }); - } - - private void createStaticIPAddressTextfield() { - staticIPAddressTF = wifiBox_cp5.addTextfield("staticIPAddress") - .setPosition(x + 90, y + 100) - .setCaptionLabel("") - .setSize(w - padding*2, 26) - .setFont(f2) - .setFocus(false) - .setColor(color(26, 26, 26)) - .setColorBackground(color(255, 255, 255)) // text field bg color - .setColorValueLabel(OPENBCI_DARKBLUE) // text color - .setColorForeground(OPENBCI_DARKBLUE) // border color when not selected - .setColorActive(isSelected_color) // border color when selected - .setColorCursor(color(26, 26, 26)) - .setText(wifi_ipAddress) - .align(5, 10, 20, 40) - .setAutoClear(true) - .setVisible(false); - //Clear textfield on double click - staticIPAddressTF.onDoublePress(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - output("WiFi Static IP: Enter your custom IP address for WiFi shield."); - staticIPAddressTF.clear(); - } - }); - } - - public void setDefaultToDynamicIP() { - h = 208; - controlPanel.setWiFiSearchStyle(controlPanel.WIFI_DYNAMIC); - wifiIPAddressDynamic.setOn(); - wifiIPAddressStatic.setOff(); - staticIPAddressTF.setVisible(false); - wifiList.setVisible(true); - } - - private void setStaticIPTextfield(String text) { - staticIPAddressTF.setText(text); - } -}; - -class InterfaceBoxCyton { - public int x, y, w, h, padding; //size and position - private ControlP5 ifbc_cp5; - private Button protocolSerialCyton; - private Button protocolWifiCyton; - - InterfaceBoxCyton(int _x, int _y, int _w, int _h, int _padding) { - x = _x; - y = _y; - w = _w; - h = (24 + _padding) * 3; - padding = _padding; - - //Instantiate local cp5 for this box - ifbc_cp5 = new ControlP5(ourApplet); - ifbc_cp5.setGraphics(ourApplet, 0,0); - ifbc_cp5.setAutoDraw(false); - - //Disabled both toggles by default for this box - createSerialCytonButton("protocolSerialCyton", "Serial (from Dongle)", false, x + padding, y + padding * 3 + 4, w - padding * 2, 24); - createWifiCytonButton("protocolWifiCyton", "Wifi (from Wifi Shield)", false, x + padding, y + padding * 4 + 24 + 4, w - padding * 2, 24); - } - - public void update() {} - - public void draw() { - pushStyle(); - fill(boxColor); - stroke(boxStrokeColor); - strokeWeight(1); - rect(x, y, w, h); - fill(OPENBCI_DARKBLUE); - textFont(h3, 16); - textAlign(LEFT, TOP); - text("PICK TRANSFER PROTOCOL", x + padding, y + padding); - popStyle(); - - ifbc_cp5.draw(); - } - - private Button createIFBCButton(String name, String text, boolean isToggled, int _x, int _y, int _w, int _h) { - final Button b = createButton(ifbc_cp5, name, text, _x, _y, _w, _h); - b.setSwitch(true); //This turns the button into a switch - if (isToggled) { - b.setOn(); - } - return b; - } - - private void createSerialCytonButton(String name, String text, boolean isToggled, int _x, int _y, int _w, int _h) { - protocolSerialCyton = createIFBCButton(name, text, isToggled, _x, _y, _w, _h); - protocolSerialCyton.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - controlPanel.wifiBox.wifiList.items.clear(); - controlPanel.bleBox.bleList.items.clear(); - selectedProtocol = BoardProtocol.SERIAL; - controlPanel.comPortBox.refreshPortListCyton(); - protocolSerialCyton.setOn(); - protocolWifiCyton.setOff(); - } - }); - } - - private void createWifiCytonButton(String name, String text, boolean isToggled, int _x, int _y, int _w, int _h) { - protocolWifiCyton = createIFBCButton(name, text, isToggled, _x, _y, _w, _h); - protocolWifiCyton.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - controlPanel.wifiBox.wifiList.items.clear(); - controlPanel.bleBox.bleList.items.clear(); - selectedProtocol = BoardProtocol.WIFI; - protocolSerialCyton.setOff(); - protocolWifiCyton.setOn(); - } - }); - } - - public void resetCytonSelectedProtocol() { - protocolSerialCyton.setOff(); - protocolWifiCyton.setOff(); - selectedProtocol = BoardProtocol.NONE; - } -}; - class InterfaceBoxGanglion { public int x, y, w, h, padding; //size and position private ControlP5 ifbg_cp5; private Button protocolGanglionNativeBLE; private Button protocolBLED112Ganglion; - private Button protocolWifiGanglion; InterfaceBoxGanglion(int _x, int _y, int _w, int _h, int _padding) { x = _x; y = _y; w = _w; padding = _padding; - h = (24 + _padding) * 4; + h = (24 + _padding) * 3; int buttonHeight = 24; //Instantiate local cp5 for this box @@ -1171,7 +808,6 @@ class InterfaceBoxGanglion { createGanglionNativeBLEButton("protocolNativeBLEGanglion", "Bluetooth (Native)", false, x + padding, y + padding * 3 + 4, w - padding * 2, 24); createBLED112Button("protocolBLED112Ganglion", "Bluetooth (BLED112 Dongle)", false, x + padding, y + (padding * 4) + 24 + 4, w - padding * 2, 24); - createGanglionWifiButton("protocolWifiGanglion", "Wifi (from Wifi Shield)", false, x + padding, y + (padding * 5) + (24 * 2) + 4, w - padding * 2, 24); } public void update() {} @@ -1205,13 +841,11 @@ class InterfaceBoxGanglion { protocolGanglionNativeBLE = createIFBGButton(name, text, isToggled, _x, _y, _w, _h); protocolGanglionNativeBLE.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - controlPanel.wifiBox.wifiList.items.clear(); controlPanel.bleBox.bleList.items.clear(); selectedProtocol = BoardProtocol.NATIVE_BLE; controlPanel.bleBox.refreshGanglionNativeList(); protocolGanglionNativeBLE.setOn(); protocolBLED112Ganglion.setOff(); - protocolWifiGanglion.setOff(); } }); } @@ -1220,27 +854,11 @@ class InterfaceBoxGanglion { protocolBLED112Ganglion = createIFBGButton(name, text, isToggled, _x, _y, _w, _h); protocolBLED112Ganglion.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - controlPanel.wifiBox.wifiList.items.clear(); controlPanel.bleBox.bleList.items.clear(); selectedProtocol = BoardProtocol.BLED112; controlPanel.bleBox.refreshGanglionBLEList(); protocolGanglionNativeBLE.setOff(); protocolBLED112Ganglion.setOn(); - protocolWifiGanglion.setOff(); - } - }); - } - - private void createGanglionWifiButton(String name, String text, boolean isToggled, int _x, int _y, int _w, int _h) { - protocolWifiGanglion = createIFBGButton(name, text, isToggled, _x, _y, _w, _h); - protocolWifiGanglion.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - controlPanel.wifiBox.wifiList.items.clear(); - controlPanel.bleBox.bleList.items.clear(); - selectedProtocol = BoardProtocol.WIFI; - protocolGanglionNativeBLE.setOff(); - protocolBLED112Ganglion.setOff(); - protocolWifiGanglion.setOn(); } }); } @@ -1248,7 +866,6 @@ class InterfaceBoxGanglion { public void resetGanglionSelectedProtocol() { protocolGanglionNativeBLE.setOff(); protocolBLED112Ganglion.setOff(); - protocolWifiGanglion.setOff(); selectedProtocol = BoardProtocol.NONE; } }; @@ -1293,12 +910,29 @@ class SessionDataBox { createODFButton("odfButton", "OpenBCI", dataLogger.getDataLoggerOutputFormat(), x + padding, y + padding*2 + 18 + 58, (w-padding*3)/2, 24); createBDFButton("bdfButton", "BDF+", dataLogger.getDataLoggerOutputFormat(), x + padding*2 + (w-padding*3)/2, y + padding*2 + 18 + 58, (w-padding*3)/2, 24); - createMaxDurationDropdown("maxFileDuration", Arrays.asList(settings.fileDurations)); - + List fileDurationList = EnumHelper.getEnumStrings(OdfFileDuration.class); + createMaxDurationDropdown("maxFileDuration", fileDurationList, odfFileDuration); } public void update() { - copyPaste.checkForCopyPaste(sessionNameTextfield); + textfieldUpdateHelper.checkTextfield(sessionNameTextfield); + + //Update the position of UI elements here, as this changes when user selects WiFi mode + sessionNameTextfield.setPosition(x + 60, y + 32); + autoSessionName.setPosition(x + padding, y + 66); + outputODF.setPosition(x + padding, y + padding*2 + 18 + 58); + outputBDF.setPosition(x + padding*2 + (w-padding*3)/2, y + padding*2 + 18 + 58); + + boolean odfIsSelected = dataLogger.getDataLoggerOutputFormat() == dataLogger.OUTPUT_SOURCE_ODF; + h = odfIsSelected ? odfModeHeight : bdfModeHeight; + maxDurationDropdown.setVisible(odfIsSelected); + if (odfIsSelected && outputBDF.isOn()) { + outputODF.setOn(); + outputBDF.setOff(); + } else if (!odfIsSelected && outputODF.isOn()) { + outputBDF.setOn(); + outputODF.setOff(); + } } public void draw() { @@ -1314,17 +948,9 @@ class SessionDataBox { textFont(p4, 14); text("Name", x + padding, y + padding*2 + 14); popStyle(); - - //Update the position of UI elements here, as this changes when user selects WiFi mode - sessionNameTextfield.setPosition(x + 60, y + 32); - autoSessionName.setPosition(x + padding, y + 66); - outputODF.setPosition(x + padding, y + padding*2 + 18 + 58); - outputBDF.setPosition(x + padding*2 + (w-padding*3)/2, y + padding*2 + 18 + 58); - maxDurationDropdown.setPosition(x + maxDurTextWidth, int(outputODF.getPosition()[1]) + 24 + padding); - + boolean odfIsSelected = dataLogger.getDataLoggerOutputFormat() == dataLogger.OUTPUT_SOURCE_ODF; maxDurationDropdown.setVisible(odfIsSelected); - if (odfIsSelected) { pushStyle(); //draw backgrounds to dropdown scrollableLists ... unfortunately ControlP5 doesn't have this by default, so we have to hack it to make it look nice... @@ -1338,6 +964,7 @@ class SessionDataBox { text("Max File Duration", maxDurText_x, y + h - 24 - padding + extraPadding); popStyle(); } + sessionData_cp5.draw(); } @@ -1383,10 +1010,10 @@ class SessionDataBox { }); } - private void createMaxDurationDropdown(String name, List _items){ + private void createMaxDurationDropdown(String name, List _items, OdfFileDuration defaultValue) { maxDurationDropdown = sessionData_cp5.addScrollableList(name) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(150) //.setColorBackground(OPENBCI_BLUE) // text field bg color .setColorValueLabel(OPENBCI_DARKBLUE) // text color @@ -1404,7 +1031,7 @@ class SessionDataBox { maxDurationDropdown .getCaptionLabel() //the caption label is the text object in the primary bar .toUpperCase(false) //DO NOT AUTOSET TO UPPERCASE!!! - .setText(settings.fileDurations[settings.defaultOBCIMaxFileSize]) + .setText(defaultValue.getString()) .setFont(p4) .setSize(14) .getStyle() //need to grab style before affecting the paddingTop @@ -1413,7 +1040,7 @@ class SessionDataBox { maxDurationDropdown .getValueLabel() //the value label is connected to the text objects in the dropdown item bars .toUpperCase(false) //DO NOT AUTOSET TO UPPERCASE!!! - .setText(settings.fileDurations[settings.defaultOBCIMaxFileSize]) + .setText(defaultValue.getString()) .setFont(h5) .setSize(12) //set the font size of the item bars to 14pt .getStyle() //need to grab style before affecting the paddingTop @@ -1423,7 +1050,7 @@ class SessionDataBox { public void controlEvent(CallbackEvent theEvent) { if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { int n = (int)(theEvent.getController()).getValue(); - settings.setLogFileDurationChoice(n); + dataLogger.setLogFileDurationChoice(n); println("ControlPanel: Chosen Recording Duration: " + n); } else if (theEvent.getAction() == ControlP5.ACTION_ENTER) { lockOutsideElements(true); @@ -1505,27 +1132,22 @@ class SessionDataBox { sessionNameTextfield.setText(s); } - // True locks elements, False unlocks elements private void lockOutsideElements (boolean _toggle) { if (eegDataSource == DATASOURCE_CYTON) { - //Cyton for Serial and WiFi (WiFi details are drawn to the right, so no need to lock) controlPanel.channelCountBox.lockCp5Objects(_toggle); - if (_toggle) { - controlPanel.sdBox.cp5_sdBox.get(ScrollableList.class, controlPanel.sdBox.sdBoxDropdownName).lock(); - } else { - controlPanel.sdBox.cp5_sdBox.get(ScrollableList.class, controlPanel.sdBox.sdBoxDropdownName).unlock(); - } + controlPanel.sdBox.lockCp5Objects(_toggle); + controlPanel.bfStreamerBoxCyton.lockCp5Objects(_toggle); controlPanel.sdBox.cp5_sdBox.get(ScrollableList.class, controlPanel.sdBox.sdBoxDropdownName).setUpdate(!_toggle); - } else { + } else if (eegDataSource == DATASOURCE_GANGLION) { controlPanel.sampleRateGanglionBox.lockCp5Objects(_toggle); } } - public void lockSessionDataBoxCp5Elements(boolean b) { - sessionNameTextfield.setLock(b); - autoSessionName.setLock(b); - outputODF.setLock(b); - outputBDF.setLock(b); + public void lockCp5Objects(boolean lock) { + sessionNameTextfield.setLock(lock); + autoSessionName.setLock(lock); + outputODF.setLock(lock); + outputBDF.setLock(lock); } }; @@ -1553,7 +1175,7 @@ class ChannelCountBox { cb8_butX = x + padding; cb16_butX = x + padding*2 + (w-padding*3)/2; cb_butY = y + padding*2 + 18; - boolean is8Channels = (nchan == 8) ? true : false; + boolean is8Channels = (globalChannelCount == 8) ? true : false; createChan8Button("cyton8ChanButton", "8 CHANNELS", is8Channels, cb8_butX, cb_butY, (w-padding*3)/2, 24); createChan16Button("cyton16ChanButton", "16 CHANNELS", is8Channels, cb16_butX, cb_butY, (w-padding*3)/2, 24); } @@ -1578,7 +1200,7 @@ class ChannelCountBox { fill(OPENBCI_DARKBLUE); //set color to green textFont(h3, 16); textAlign(LEFT, TOP); - text(" (" + str(nchan) + ")", x + padding + 142, y + padding); // print the channel count in green next to the box title + text(" (" + str(globalChannelCount) + ")", x + padding + 142, y + padding); // print the channel count in green next to the box title popStyle(); ccc_cp5.draw(); @@ -1597,7 +1219,7 @@ class ChannelCountBox { chanButton8 = createCCCButton(name, text, isToggled, _x, _y, _w, _h); chanButton8.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - updateToNChan(8); + updateGlobalChannelCount(8); chanButton8.setOn(); chanButton16.setOff(); } @@ -1608,7 +1230,7 @@ class ChannelCountBox { chanButton16 = createCCCButton(name, text, isToggled, _x, _y, _w, _h); chanButton16.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - updateToNChan(16); + updateGlobalChannelCount(16); chanButton8.setOff(); chanButton16.setOn(); } @@ -1621,7 +1243,7 @@ class ChannelCountBox { } public void set8ChanButtonActive() { - updateToNChan(8); + updateGlobalChannelCount(8); chanButton8.setOn(); chanButton16.setOff(); } @@ -1721,116 +1343,6 @@ class SampleRateGanglionBox { } }; -class SampleRateCytonBox { - public int x, y, w, h, padding; //size and position - private ControlP5 srcb_cp5; - private Button sampleRate250; - private Button sampleRate500; - private Button sampleRate1000; - private int sr250_butX; - private int sr500_butX; - private int sr1000_butX; - private int srButton_butY; - - SampleRateCytonBox(int _x, int _y, int _w, int _h, int _padding) { - x = _x; - y = _y; - w = _w; - h = 73; - padding = _padding; - - //Instantiate local cp5 for this box - srcb_cp5 = new ControlP5(ourApplet); - srcb_cp5.setGraphics(ourApplet, 0,0); - srcb_cp5.setAutoDraw(false); - - sr250_butX = x + padding; - sr500_butX = x + padding*2 + (w-padding*4)/3; - sr1000_butX = x + padding*3 + ((w-padding*4)/3)*2; - srButton_butY = y + padding*2 + 18; - createSR250Button("cytonSR250", "250Hz", false, sr250_butX, srButton_butY, (w-padding*4)/3, 24); - createSR500Button("cytonSR500", "500Hz", false, sr500_butX, srButton_butY, (w-padding*4)/3, 24); - //Make 1000Hz option selected by default - createSR1000Button("cytonSR1000", "1000Hz", true, sr1000_butX, srButton_butY, (w-padding*4)/3, 24); - } - - public void update() { - - } - - public void draw() { - - srButton_butY = y + padding*2 + 18; - sampleRate250.setPosition(sr250_butX, srButton_butY); - sampleRate500.setPosition(sr500_butX, srButton_butY); - sampleRate1000.setPosition(sr1000_butX, srButton_butY); - - pushStyle(); - fill(boxColor); - stroke(boxStrokeColor); - strokeWeight(1); - rect(x, y, w, h); - fill(OPENBCI_DARKBLUE); - textFont(h3, 16); - textAlign(LEFT, TOP); - text("SAMPLE RATE ", x + padding, y + padding); - fill(OPENBCI_DARKBLUE); //set color to green - textFont(h3, 16); - textAlign(LEFT, TOP); - popStyle(); - - srcb_cp5.draw(); - } - - private Button createSRCBButton(String name, String text, boolean isToggled, int _x, int _y, int _w, int _h) { - final Button b = createButton(srcb_cp5, name, text, _x, _y, _w, _h); - b.setSwitch(true); //This turns the button into a switch - if (isToggled) { - b.setOn(); - } - return b; - } - - private void createSR250Button(String name, String text, boolean isToggled, int _x, int _y, int _w, int _h) { - sampleRate250 = createSRCBButton(name, text, isToggled, _x, _y, _w, _h); - sampleRate250.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - selectedSamplingRate = 250; - println("ControlPanel: User selected Cyton+WiFi 250Hz"); - sampleRate250.setOn(); - sampleRate500.setOff(); - sampleRate1000.setOff(); - } - }); - } - - private void createSR500Button(String name, String text, boolean isToggled, int _x, int _y, int _w, int _h) { - sampleRate500 = createSRCBButton(name, text, isToggled, _x, _y, _w, _h); - sampleRate500.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - selectedSamplingRate = 500; - println("ControlPanel: User selected Cyton+WiFi 500Hz"); - sampleRate250.setOff(); - sampleRate500.setOn(); - sampleRate1000.setOff(); - } - }); - } - - private void createSR1000Button(String name, String text, boolean isToggled, int _x, int _y, int _w, int _h) { - sampleRate1000 = createSRCBButton(name, text, isToggled, _x, _y, _w, _h); - sampleRate1000.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - selectedSamplingRate = 1000; - println("ControlPanel: User selected Cyton+WiFi 1000Hz"); - sampleRate250.setOff(); - sampleRate500.setOff(); - sampleRate1000.setOn(); - } - }); - } -}; - class SyntheticChannelCountBox { public int x, y, w, h, padding; //size and position private ControlP5 sccb_cp5; @@ -1871,7 +1383,7 @@ class SyntheticChannelCountBox { fill(OPENBCI_DARKBLUE); //set color to green textFont(h3, 16); textAlign(LEFT, TOP); - text(" (" + str(nchan) + ")", x + padding + 142, y + padding); // print the channel count in green next to the box title + text(" (" + str(globalChannelCount) + ")", x + padding + 142, y + padding); // print the channel count in green next to the box title popStyle(); sccb_cp5.draw(); @@ -1890,7 +1402,7 @@ class SyntheticChannelCountBox { synthChanButton4 = createSCCBButton(name, text, false,_x, _y, _w, _h); synthChanButton4.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - updateToNChan(4); + updateGlobalChannelCount(4); synthChanButton4.setOn(); synthChanButton8.setOff(); synthChanButton16.setOff(); @@ -1903,7 +1415,7 @@ class SyntheticChannelCountBox { synthChanButton8 = createSCCBButton(name, text, true, _x, _y, _w, _h); synthChanButton8.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - updateToNChan(8); + updateGlobalChannelCount(8); synthChanButton4.setOff(); synthChanButton8.setOn(); synthChanButton16.setOff(); @@ -1915,7 +1427,7 @@ class SyntheticChannelCountBox { synthChanButton16 = createSCCBButton(name, text, false, _x, _y, _w, _h); synthChanButton16.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - updateToNChan(16); + updateGlobalChannelCount(16); synthChanButton4.setOff(); synthChanButton8.setOff(); synthChanButton16.setOn(); @@ -1924,7 +1436,7 @@ class SyntheticChannelCountBox { } public void set8ChanButtonActive() { - updateToNChan(8); + updateGlobalChannelCount(8); synthChanButton4.setOff(); synthChanButton8.setOn(); synthChanButton16.setOff(); @@ -1938,14 +1450,14 @@ class RecentPlaybackBox { private String filePickedShort = "Select Recent Playback File"; private ControlP5 rpb_cp5; private ScrollableList recentPlaybackSL; - private int titleH = 14; + private int TITLE_HEIGHT = 14; private int buttonH = 24; RecentPlaybackBox(int _x, int _y, int _w, int _h, int _padding) { x = _x; y = _y; w = _w; - h = titleH + buttonH + _padding*3; + h = TITLE_HEIGHT + buttonH + _padding*3; padding = _padding; rpb_cp5 = new ControlP5(ourApplet); @@ -2092,9 +1604,8 @@ class BrainFlowStreamerBox { private ControlP5 bfStreamerCp5; private int maxDurTextWidth = 82; private int maxDurText_x = 0; - private Textfield ipAddress; - private Textfield port; - private Button autoSessionName; + private Textfield ipAddressTextfield; + private Textfield portTextfield; private Button outputToNetwork; private Button outputToFile; private ScrollableList bfFileSaveOption; @@ -2125,16 +1636,16 @@ class BrainFlowStreamerBox { } public void update() { - copyPaste.checkForCopyPaste(ipAddress); - copyPaste.checkForCopyPaste(port); + textfieldUpdateHelper.checkTextfield(ipAddressTextfield); + textfieldUpdateHelper.checkTextfield(portTextfield); } - public void draw() { + public void draw() { int streamerTextfieldY = y + padding*3 + HEADER_H + OBJECT_H; bfFileSaveOption.setVisible(outputToFile.isOn()); - ipAddress.setVisible(outputToNetwork.isOn()); - port.setVisible(outputToNetwork.isOn()); + ipAddressTextfield.setVisible(outputToNetwork.isOn()); + portTextfield.setVisible(outputToNetwork.isOn()); pushStyle(); fill(boxColor); @@ -2150,7 +1661,7 @@ class BrainFlowStreamerBox { text("Location", x + padding, streamerTextfieldY + 2); } else if (outputToNetwork.isOn()) { text("IP", x + padding, streamerTextfieldY + 2); - text("Port", x + w - padding*2 - port.getWidth() - 14 - padding, streamerTextfieldY + 2); + text("Port", x + w - padding*2 - portTextfield.getWidth() - 14 - padding, streamerTextfieldY + 2); } popStyle(); @@ -2158,14 +1669,14 @@ class BrainFlowStreamerBox { outputToNetwork.setPosition(x + padding, y + HEADER_H + padding*2); outputToFile.setPosition(x + padding*2 + (w-padding*3)/2, y + HEADER_H + padding*2); bfFileSaveOption.setPosition(x + 80, streamerTextfieldY); - ipAddress.setPosition(x + padding * 3, streamerTextfieldY); - port.setPosition(x + w - padding - port.getWidth(), streamerTextfieldY); + ipAddressTextfield.setPosition(x + padding * 3, streamerTextfieldY); + portTextfield.setPosition(x + w - padding - portTextfield.getWidth(), streamerTextfieldY); bfStreamerCp5.draw(); } private void createNetworkTextfields() { - ipAddress = bfStreamerCp5.addTextfield("ipAddress") + ipAddressTextfield = bfStreamerCp5.addTextfield("ipAddressTextfield") .setPosition(x + padding * 3, y + HEADER_H + padding*2) .setCaptionLabel("") .setSize(120, OBJECT_H) @@ -2177,26 +1688,26 @@ class BrainFlowStreamerBox { .setColorForeground(OPENBCI_DARKBLUE) // border color when not selected .setColorActive(isSelected_color) // border color when selected .setColorCursor(color(26, 26, 26)) - .setText(DEFAULT_IP_ADDRESS) //default ipAddress == "" + .setText(DEFAULT_IP_ADDRESS) .align(5, 10, 20, 40) //.onDoublePress(cb) .addCallback(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - if (theEvent.getAction() == ControlP5.ACTION_BROADCAST && ipAddress.getText().equals("")) { - ipAddress.setText(DEFAULT_IP_ADDRESS); + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST && ipAddressTextfield.getText().equals("")) { + ipAddressTextfield.setText(DEFAULT_IP_ADDRESS); } } }) .onReleaseOutside(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - if (!ipAddress.isActive() && ipAddress.getText().equals("")) { - ipAddress.setText(DEFAULT_IP_ADDRESS); + if (!ipAddressTextfield.isActive() && ipAddressTextfield.getText().equals("")) { + ipAddressTextfield.setText(DEFAULT_IP_ADDRESS); } } }) .onDoublePress(cb); - port = bfStreamerCp5.addTextfield("port") + portTextfield = bfStreamerCp5.addTextfield("portTextfield") .setPosition(x + padding*5 + w/2, y + HEADER_H + padding*2) .setCaptionLabel("") .setSize(50, OBJECT_H) @@ -2208,20 +1719,20 @@ class BrainFlowStreamerBox { .setColorForeground(OPENBCI_DARKBLUE) // border color when not selected .setColorActive(isSelected_color) // border color when selected .setColorCursor(color(26, 26, 26)) - .setText(DEFAULT_PORT) //default port == 0 + .setText(DEFAULT_PORT) .align(5, 10, 20, 40) //.onDoublePress(cb) .addCallback(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - if (theEvent.getAction() == ControlP5.ACTION_BROADCAST && port.getText().equals("")) { - port.setText(DEFAULT_PORT); + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST && portTextfield.getText().equals("")) { + portTextfield.setText(DEFAULT_PORT); } } }) .onReleaseOutside(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - if (!port.isActive() && port.getText().equals("")) { - port.setText(DEFAULT_PORT); + if (!portTextfield.isActive() && portTextfield.getText().equals("")) { + portTextfield.setText(DEFAULT_PORT); } } }) @@ -2241,11 +1752,8 @@ class BrainFlowStreamerBox { outputToNetwork = createBrainFlowOutputToggle(name, text, false, _x, _y, _w, _h); outputToNetwork.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - //output(odfMessage); - //dataLogger.setDataLoggerOutputFormat(dataLogger.OUTPUT_SOURCE_ODF); outputToNetwork.setOn(); outputToFile.setOff(); - //setToODFHeight(); } }); outputToNetwork.setDescription("Use BrainFlow Streamer to output to network address. You can accept this data stream using a separate process which utilizes any BrainFlow binding. This is a helpful feature for developers."); @@ -2255,11 +1763,8 @@ class BrainFlowStreamerBox { outputToFile = createBrainFlowOutputToggle(name, text, true, _x, _y, _w, _h); outputToFile.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - //output(bdfMessage); - //dataLogger.setDataLoggerOutputFormat(dataLogger.OUTPUT_SOURCE_BDF); outputToNetwork.setOff(); outputToFile.setOn(); - //setToBDFHeight(); } }); outputToFile.setDescription("Set BrainFlow Streamer output to stream over network. A new file will be made in the session folder when the data stream is paused or max file duration is reached."); @@ -2268,7 +1773,7 @@ class BrainFlowStreamerBox { private void createDropdown(String name){ bfFileSaveOption = bfStreamerCp5.addScrollableList(name) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(150) .setSize(167, (dataWriterBfEnum.values().length + 1) * 24) .setBarHeight(24) //height of top/primary bar @@ -2306,10 +1811,11 @@ class BrainFlowStreamerBox { sb.append(" file location."); output(sb.toString()); if (dataWriterBfEnum.getIsCustomLocation()) { - selectOutput("Select a folder to save BrainFlow CSV files to:", - "bfSelectedFolder", - new File(directoryManager.getRecordingsPath()) - ); + FileChooser chooser = new FileChooser( + FileChooserMode.SAVE, + "bfSelectedFolder", + new File(directoryManager.getRecordingsPath()), + "Save BrainFlow CSV to file"); } } else if (theEvent.getAction() == ControlP5.ACTION_ENTER) { lockOutsideElements(true); @@ -2323,9 +1829,9 @@ class BrainFlowStreamerBox { private String getBFNetworkTextfieldsAsString() { StringBuilder sb = new StringBuilder("streaming_board://"); - sb.append(ipAddress.getText()); + sb.append(ipAddressTextfield.getText()); sb.append(":"); - sb.append(port.getText()); + sb.append(portTextfield.getText()); return sb.toString(); } @@ -2345,25 +1851,14 @@ class BrainFlowStreamerBox { return outputToFile.isOn() && dataWriterBfEnum.getIsDefaultLocation(); } - // True locks elements, False unlocks elements private void lockOutsideElements (boolean _toggle) { if (eegDataSource == DATASOURCE_CYTON) { - //Cyton for Serial and WiFi (WiFi details are drawn to the right, so no need to lock) controlPanel.channelCountBox.lockCp5Objects(_toggle); - if (_toggle) { - controlPanel.sdBox.cp5_sdBox.get(ScrollableList.class, controlPanel.sdBox.sdBoxDropdownName).lock(); - } else { - controlPanel.sdBox.cp5_sdBox.get(ScrollableList.class, controlPanel.sdBox.sdBoxDropdownName).unlock(); - } + controlPanel.sdBox.lockCp5Objects(_toggle); controlPanel.sdBox.cp5_sdBox.get(ScrollableList.class, controlPanel.sdBox.sdBoxDropdownName).setUpdate(!_toggle); } } - public void lockSessionDataBoxCp5Elements(boolean b) { - ipAddress.setLock(b); - port.setLock(b); - } - //Clear text field on double-click CallbackListener cb = new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { @@ -2371,6 +1866,14 @@ class BrainFlowStreamerBox { tf.clear(); } }; + + public void lockCp5Objects(boolean lock) { + ipAddressTextfield.setLock(lock); + portTextfield.setLock(lock); + outputToNetwork.setLock(lock); + outputToFile.setLock(lock); + bfFileSaveOption.setLock(lock); + } }; class StreamingBoardBox { @@ -2436,8 +1939,8 @@ class StreamingBoardBox { } public void update() { - copyPaste.checkForCopyPaste(ipAddress); - copyPaste.checkForCopyPaste(port); + textfieldUpdateHelper.checkTextfield(ipAddress); + textfieldUpdateHelper.checkTextfield(port); } public void draw() { @@ -2534,14 +2037,14 @@ class PlaybackFileBox { private Button selectPlaybackFile; private int sampleDataButton_w = 100; private int sampleDataButton_h = 20; - private int titleH = 14; + private int TITLE_HEIGHT = 14; private int buttonH = 24; PlaybackFileBox(int _x, int _y, int _w, int _h, int _padding) { x = _x; y = _y; w = _w; - h = buttonH + (_padding * 3) + titleH; + h = buttonH + (_padding * 3) + TITLE_HEIGHT; padding = _padding; //Instantiate local cp5 for this box @@ -2549,7 +2052,7 @@ class PlaybackFileBox { pbfb_cp5.setGraphics(ourApplet, 0,0); pbfb_cp5.setAutoDraw(false); - createSelectPlaybackFileButton("selectPlaybackFileControlPanel", "SELECT OPENBCI PLAYBACK FILE", x + padding, y + padding*2 + titleH, w - padding*2, buttonH); + createSelectPlaybackFileButton("selectPlaybackFileControlPanel", "SELECT OPENBCI PLAYBACK FILE", x + padding, y + padding*2 + TITLE_HEIGHT, w - padding*2, buttonH); createSampleDataButton("selectSampleDataControlPanel", "Sample Data", x + w - sampleDataButton_w - padding, y + padding - 2, sampleDataButton_w, sampleDataButton_h); } @@ -2576,10 +2079,11 @@ class PlaybackFileBox { selectPlaybackFile.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { output("Select a file for playback"); - selectInput("Select a pre-recorded file for playback:", - "playbackFileSelected", - new File(directoryManager.getGuiDataPath() + "Recordings") - ); + FileChooser chooser = new FileChooser( + FileChooserMode.LOAD, + "playbackFileSelected", + new File(directoryManager.getGuiDataPath() + "Recordings"), + "Select a file for playback"); } }); selectPlaybackFile.setDescription("Click to open a dialog box to select an OpenBCI playback file (.txt or .csv)."); @@ -2590,10 +2094,12 @@ class PlaybackFileBox { sampleDataButton.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { output("Select a file for playback"); - selectInput("Select a pre-recorded file for playback:", - "playbackFileSelected", - new File(directoryManager.getGuiDataPath() + "Sample_Data" + System.getProperty("file.separator") + "OpenBCI-sampleData-2-meditation.txt") - ); + File sampleDataFile = new File(directoryManager.getGuiDataPath() + "Sample_Data" + System.getProperty("file.separator") + "OpenBCI_GUI-v6-meditation.txt"); + FileChooser chooser = new FileChooser( + FileChooserMode.LOAD, + "playbackFileSelected", + sampleDataFile, + "Select a file for playback"); } }); //sampleDataButton.setCornerRoundness((int)(sampleDataButton_h)); @@ -2656,7 +2162,7 @@ class SDBox { sdList = cp5_sdBox.addScrollableList(name) .setOpen(false) - .setColor(settings.dropdownColors) + .setColor(dropdownColorsGlobal) .setOutlineColor(150) .setSize(w - padding*2, 2*24)//temporary size .setBarHeight(24) //height of top/primary bar @@ -2704,6 +2210,10 @@ class SDBox { public void updatePosition() { sdList.setPosition(x + padding, y + padding*2 + 14); } + + public void lockCp5Objects(boolean lock) { + sdList.setLock(lock); + } }; @@ -2961,22 +2471,16 @@ class InitBox { if ((eegDataSource == DATASOURCE_CYTON && selectedProtocol == BoardProtocol.NONE) || (eegDataSource == DATASOURCE_GANGLION && selectedProtocol == BoardProtocol.NONE)) { outputWarn("No Transfer Protocol selected. Please select your Transfer Protocol and retry system initiation."); return; - } else if (eegDataSource == DATASOURCE_CYTON && selectedProtocol == BoardProtocol.SERIAL && openBCI_portName == "N/A") { //if data source == normal && if no serial port selected OR no SD setting selected + } else if (eegDataSource == DATASOURCE_CYTON && selectedProtocol == BoardProtocol.SERIAL && cytonDonglePortName == "N/A") { //if data source == normal && if no serial port selected OR no SD setting selected outputWarn("No Serial/COM port selected. Attempting to AUTO-CONNECT to Cyton."); controlPanel.comPortBox.attemptAutoConnectCyton(); return; - } else if (eegDataSource == DATASOURCE_CYTON && selectedProtocol == BoardProtocol.WIFI && wifi_portName == "N/A" && controlPanel.getWifiSearchStyle() == controlPanel.WIFI_DYNAMIC) { - outputWarn("No Wifi Shield selected. Please select your Wifi Shield and retry system initiation."); - return; } else if (eegDataSource == DATASOURCE_PLAYBACKFILE && playbackData_fname == "N/A" && sdData_fname == "N/A") { //if data source == playback && playback file == 'N/A' outputWarn("No playback file selected. Please select a playback file and retry system initiation."); // tell user that they need to select a file before the system can be started return; } else if (eegDataSource == DATASOURCE_GANGLION && (selectedProtocol == BoardProtocol.NATIVE_BLE || selectedProtocol == BoardProtocol.BLED112) && ganglion_portName == "N/A") { outputWarn("No BLE device selected. Please select your Ganglion device and retry system initiation."); return; - } else if (eegDataSource == DATASOURCE_GANGLION && selectedProtocol == BoardProtocol.WIFI && wifi_portName == "N/A" && controlPanel.getWifiSearchStyle() == controlPanel.WIFI_DYNAMIC) { - outputWarn("No Wifi Shield selected. Please select your Wifi Shield and retry system initiation."); - return; } else if (eegDataSource == -1) {//if no data source selected outputWarn("No DATA SOURCE selected. Please select a DATA SOURCE and retry system initiation.");//tell user they must select a data source before initiating system return; @@ -2986,13 +2490,7 @@ class InitBox { // Global steps to START SESSION // Prepare the serial port - //Set data logger outputs to save data to BDF or CSV - controlPanel.setDataLoggerOutputs(); - - if (controlPanel.getWifiSearchStyle() == controlPanel.WIFI_STATIC && (selectedProtocol == BoardProtocol.WIFI || selectedProtocol == BoardProtocol.WIFI)) { - wifi_ipAddress = controlPanel.wifiBox.staticIPAddressTF.getText(); - println("Static IP address of " + wifi_ipAddress); - } + controlPanel.fetchSessionNameTextfieldAllBoards(); //Set this flag to true, and draw "Starting Session..." to screen after then next draw() loop midInit = true; @@ -3006,9 +2504,12 @@ class InitBox { //creates new data file name so that you don't accidentally overwrite the old one controlPanel.dataLogBoxCyton.setSessionTextfieldText(directoryManager.getFileNameDateTime()); controlPanel.dataLogBoxGanglion.setSessionTextfieldText(directoryManager.getFileNameDateTime()); - controlPanel.wifiBox.setStaticIPTextfield(wifi_ipAddress); - w_focus.killAuditoryFeedback(); + W_Focus focusWidget = (W_Focus) widgetManager.getWidget("W_Focus"); + W_Marker markerWidget = (W_Marker) widgetManager.getWidget("W_Marker"); + focusWidget.killAuditoryFeedback(); + markerWidget.disposeUdpMarkerReceiver(); haltSystem(); + widgetManager.setAllWidgetsNull(); } } diff --git a/OpenBCI_GUI/CustomCp5Classes.pde b/OpenBCI_GUI/CustomCp5Classes.pde index 0bcbc8c57..e9980adc3 100644 --- a/OpenBCI_GUI/CustomCp5Classes.pde +++ b/OpenBCI_GUI/CustomCp5Classes.pde @@ -245,13 +245,12 @@ public class MenuList extends controlP5.Controller { // When detecting a click, check if the click happend to the far right, if yes, scroll to that position, // Otherwise do whatever this item of the list is supposed to do. public void onClick() { - println(getName() + ": click! "); + if (items.size() > 0) { //Fixes #480 if (getPointer().x()>getWidth()-scrollerWidth) { if(getHeight() != 0){ npos= -map(getPointer().y(), 0, getHeight(), 0, items.size()*itemHeight); } - updateMenu = true; } else { int len = itemHeight * items.size(); int index = 0; @@ -260,6 +259,7 @@ public class MenuList extends controlP5.Controller { } setValue(index); activeItem = index; + println(getName() + ": Selected item " + getItem(index).get("headline").toString()); } updateMenu = true; } @@ -339,6 +339,13 @@ public class MenuList extends controlP5.Controller { public int getListSize() { return items.size(); } + + public void selectItem(int theIndex) { + setValue(theIndex); + activeItem = theIndex; + updateMenu = true; + println(getName() + ": Selected item " + getItem(theIndex).get("headline").toString()); + } }; //////////////////////////////////////////////////////////////// diff --git a/OpenBCI_GUI/CytonElectrodeStatus.pde b/OpenBCI_GUI/CytonElectrodeStatus.pde index 85bdd4f3a..e3b5588af 100644 --- a/OpenBCI_GUI/CytonElectrodeStatus.pde +++ b/OpenBCI_GUI/CytonElectrodeStatus.pde @@ -1,6 +1,6 @@ import java.text.NumberFormat; -public enum ElectrodeState { +public enum CytonElectrodeState { GREYED_OUT(0, #717577), RED(1, #ff0000), YELLOW(2, #e6c700), @@ -9,16 +9,16 @@ public enum ElectrodeState { NOT_TESTABLE(4, #717577); private final int value; - private final color _color; + private final color statusColor; - ElectrodeState(int newValue, color c) { - value = newValue; - _color = c; + CytonElectrodeState(int _value, color _color) { + value = _value; + statusColor = _color; } public int getValue() { return value; } - public int getColor() { return _color; } + public int getColor() { return statusColor; } } interface CytonElectrodeEnum { @@ -163,8 +163,8 @@ class CytonElectrodeStatus { protected double statusValue; protected String statusValueAsString; protected String anatomicalName; - protected ElectrodeState state_live; - protected ElectrodeState state_imp; + protected CytonElectrodeState state_live; + protected CytonElectrodeState state_imp; protected NumberFormat railedNF = NumberFormat.getInstance(); protected DecimalFormat impedanceNF; protected DecimalFormat impShortNF; @@ -185,7 +185,7 @@ class CytonElectrodeStatus { protected Gif checkingElectrodeGif; protected final int gifDiameterBorderOffset = 30; //From the weight of the pixels in the original gif - CytonElectrodeStatus(ControlP5 _cp5, CytonElectrodeEnum electrodeEnum, BoardCyton _impBoard, Gif statusGif) { + CytonElectrodeStatus(ControlP5 _cp5, CytonElectrodeEnum electrodeEnum, BoardCyton _impBoard) { local_cp5 = _cp5; cytonBoard = (BoardCyton)_impBoard; impedanceNF = new DecimalFormat("###,###.#"); @@ -199,10 +199,10 @@ class CytonElectrodeStatus { is_N_Pin = thisElectrode.isPin_N(); railedNF.setMaximumFractionDigits(2); dataTableColumnOffset = is_N_Pin ? 1 : 2; - checkingElectrodeGif = statusGif; + checkingElectrodeGif = checkingImpedanceStatusGif; - state_imp = ElectrodeState.GREYED_OUT; - state_live = ElectrodeState.GREYED_OUT; + state_imp = CytonElectrodeState.GREYED_OUT; + state_live = CytonElectrodeState.GREYED_OUT; //This will be resized and positioned during session starts when widget is assigned a container createCytonElectrodeTestingButton("electrode_"+electrodeLocation, "Test", 0, 0, 20, 10); @@ -213,7 +213,7 @@ class CytonElectrodeStatus { float x = w * thisElectrode.getCircleXY()[0]; float y = h * thisElectrode.getCircleXY()[1]; - ElectrodeState state = getElectrodeState(); + CytonElectrodeState state = getCytonElectrodeState(); pushStyle(); fill(state.getColor()); @@ -221,7 +221,7 @@ class CytonElectrodeStatus { ellipseMode(CENTER); ellipse(x, y, d, d); - if (state != ElectrodeState.NOT_TESTABLE && cytonBoard.isCheckingImpedanceNorP(channelNumber-1, is_N_Pin)) { + if (state != CytonElectrodeState.NOT_TESTABLE && cytonBoard.isCheckingImpedanceNorP(channelNumber-1, is_N_Pin)) { imageMode(CENTER); image(checkingElectrodeGif, x - 1, y - 1, d + gifDiameterBorderOffset, d + gifDiameterBorderOffset); } @@ -231,9 +231,9 @@ class CytonElectrodeStatus { public void update(Grid _dataTable, boolean _isImpedanceMode) { isInImpedanceMode = _isImpedanceMode; - ElectrodeState state = getElectrodeState(); + CytonElectrodeState state = getCytonElectrodeState(); - if (state == ElectrodeState.NOT_TESTABLE) { + if (state == CytonElectrodeState.NOT_TESTABLE) { return; } @@ -246,11 +246,11 @@ class CytonElectrodeStatus { boolean greaterThanZero = statusValue > Double.MIN_NORMAL; color railedTextColor = OPENBCI_DARKBLUE; if (statusValue > impedanceYellowCuttoff) { - state_imp = ElectrodeState.RED; + state_imp = CytonElectrodeState.RED; } else if (statusValue < impedanceYellowCuttoff && statusValue > impedanceGreenCutoff) { - state_imp = ElectrodeState.YELLOW; + state_imp = CytonElectrodeState.YELLOW; } else if (greaterThanZero && statusValue < impedanceGreenCutoff) { - state_imp = ElectrodeState.GREEN; + state_imp = CytonElectrodeState.GREEN; } //Impedance mode uses buttons carefully positioned in the table to display information testing_button.getCaptionLabel().setText(getImpValShortString()); @@ -263,13 +263,13 @@ class CytonElectrodeStatus { boolean greaterThanZero = statusValue > Double.MIN_NORMAL; color railedTextColor = OPENBCI_DARKBLUE; if (is_railed[i].is_railed) { - state_live = ElectrodeState.RED; + state_live = CytonElectrodeState.RED; railedTextColor = SIGNAL_CHECK_RED; } else if (is_railed[i].is_railed_warn) { - state_live = ElectrodeState.YELLOW; + state_live = CytonElectrodeState.YELLOW; railedTextColor = SIGNAL_CHECK_YELLOW; } else if (greaterThanZero) { - state_live = ElectrodeState.BLUE; + state_live = CytonElectrodeState.BLUE; } //Railed percentage mode (Live) uses text in the data table StringBuilder s = new StringBuilder(railedNF.format(statusValue)); @@ -298,11 +298,11 @@ class CytonElectrodeStatus { return channelNumber; } - public final ElectrodeState getElectrodeState() { + public final CytonElectrodeState getCytonElectrodeState() { return isInImpedanceMode ? state_imp : state_live; } - public void setElectrodeState(ElectrodeState s) { + public void setCytonElectrodeState(CytonElectrodeState s) { if (isInImpedanceMode) { state_imp = s; } else { @@ -332,8 +332,8 @@ class CytonElectrodeStatus { //Here is the method that creates a "Test" button for every electrode position protected void createCytonElectrodeTestingButton(String name, String text, int _x, int _y, int _w, int _h) { - ElectrodeState state = getElectrodeState(); - if (state == ElectrodeState.NOT_TESTABLE) { + CytonElectrodeState state = getCytonElectrodeState(); + if (state == CytonElectrodeState.NOT_TESTABLE) { return; //Some electrode positions cannot be tested } testing_button = createButton(local_cp5, name, text, _x, _y, _w, _h); @@ -346,15 +346,16 @@ class CytonElectrodeStatus { final int _chan = channelNumber - 1; final int curMillis = millis(); println("CytonElectrodeTestButton: Toggling Impedance on ~~ " + electrodeLocation); - w_cytonImpedance.toggleImpedanceOnElectrode(!cytonBoard.isCheckingImpedanceNorP(_chan, is_N_Pin), _chan, is_N_Pin, curMillis); + W_CytonImpedance cytonImpedanceWidget = (W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance"); + cytonImpedanceWidget.toggleImpedanceOnElectrode(!cytonBoard.isCheckingImpedanceNorP(_chan, is_N_Pin), _chan, is_N_Pin, curMillis); } }); testing_button.setDescription("Click to toggle impedance check for this ADS pin."); } public void resizeButton(Grid _dataTable) { - ElectrodeState state = getElectrodeState(); - if (state == ElectrodeState.NOT_TESTABLE) { + CytonElectrodeState state = getCytonElectrodeState(); + if (state == CytonElectrodeState.NOT_TESTABLE) { return; //Some electrode positions cannot be tested } cellDims = _dataTable.getCellDims(channelNumber, dataTableColumnOffset); @@ -364,20 +365,20 @@ class CytonElectrodeStatus { //Override the electrode state public void setElectrodeGreyedOut() { - ElectrodeState state = getElectrodeState(); - if (state == ElectrodeState.NOT_TESTABLE) { + CytonElectrodeState state = getCytonElectrodeState(); + if (state == CytonElectrodeState.NOT_TESTABLE) { return; } - state = ElectrodeState.GREYED_OUT; + state = CytonElectrodeState.GREYED_OUT; } //Override the electrode state public void setElectrodeGreenStatus() { - ElectrodeState state = getElectrodeState(); - if (state == ElectrodeState.NOT_TESTABLE) { + CytonElectrodeState state = getCytonElectrodeState(); + if (state == CytonElectrodeState.NOT_TESTABLE) { return; } - state = ElectrodeState.GREEN; + state = CytonElectrodeState.GREEN; } public void resetTestingButton() { diff --git a/OpenBCI_GUI/CytonImpedanceEnums.pde b/OpenBCI_GUI/CytonImpedanceEnums.pde index 21713bf9d..313b8d9de 100644 --- a/OpenBCI_GUI/CytonImpedanceEnums.pde +++ b/OpenBCI_GUI/CytonImpedanceEnums.pde @@ -6,7 +6,6 @@ public enum CytonSignalCheckMode implements IndexingInterface private int index; private String label; - private static CytonSignalCheckMode[] vals = values(); CytonSignalCheckMode(int _index, String _label) { this.index = _index; @@ -26,14 +25,6 @@ public enum CytonSignalCheckMode implements IndexingInterface public boolean getIsImpedanceMode() { return label.equals("Impedance"); } - - private static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum CytonImpedanceLabels implements IndexingInterface @@ -45,7 +36,6 @@ public enum CytonImpedanceLabels implements IndexingInterface private int index; private String label; private boolean boolean_value; - private static CytonImpedanceLabels[] vals = values(); CytonImpedanceLabels(int _index, String _label) { this.index = _index; @@ -65,14 +55,6 @@ public enum CytonImpedanceLabels implements IndexingInterface public boolean getIsAnatomicalName() { return label.equals("Anatomical"); } - - private static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum CytonImpedanceInterval implements IndexingInterface @@ -80,14 +62,12 @@ public enum CytonImpedanceInterval implements IndexingInterface FOUR (0, 4000, "4 sec"), FIVE (1, 5000, "5 sec"), SEVEN (2, 7000, "7 sec"), - TEN (3, 10000, "10 sec") - ; + TEN (3, 10000, "10 sec"); private int index; private int value; private String label; private boolean boolean_value; - private static CytonImpedanceInterval[] vals = values(); CytonImpedanceInterval(int _index, int _val, String _label) { this.index = _index; @@ -108,12 +88,4 @@ public enum CytonImpedanceInterval implements IndexingInterface public int getValue() { return value; } - - private static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } \ No newline at end of file diff --git a/OpenBCI_GUI/DataLogger.pde b/OpenBCI_GUI/DataLogger.pde index 0a1e7fb46..9aac64fc7 100644 --- a/OpenBCI_GUI/DataLogger.pde +++ b/OpenBCI_GUI/DataLogger.pde @@ -1,7 +1,6 @@ class DataLogger { //variables for writing EEG data out to a file private DataWriterODF fileWriterODF; - private DataWriterAuxODF fileWriterAuxODF; private DataWriterBDF fileWriterBDF; public DataWriterBF fileWriterBF; //Add the ability to simulataneously save to BrainFlow CSV, independent of BDF or ODF private String sessionName = "N/A"; @@ -9,6 +8,10 @@ class DataLogger { public final int OUTPUT_SOURCE_ODF = 1; // The OpenBCI CSV Data Format public final int OUTPUT_SOURCE_BDF = 2; // The BDF data format http://www.biosemi.com/faq/file_format.htm private int outputDataSource; + private String sessionPath = ""; + private boolean logFileIsOpen = false; + private long logFileStartTime; + private long logFileMaxDurationNano = -1; DataLogger() { //Default to OpenBCI CSV Data Format @@ -34,7 +37,7 @@ class DataLogger { private void saveNewData() { //If data is available, save to playback file... - if(!settings.isLogFileOpen()) { + if(!isLogFileOpen()) { return; } @@ -43,8 +46,6 @@ class DataLogger { switch (outputDataSource) { case OUTPUT_SOURCE_ODF: fileWriterODF.append(newData); - if (currentBoard instanceof AuxDataBoard) - fileWriterAuxODF.append(((AuxDataBoard)currentBoard).getAuxFrameData()); break; case OUTPUT_SOURCE_BDF: fileWriterBDF.writeRawData_dataPacket(newData); @@ -57,21 +58,21 @@ class DataLogger { } public void limitRecordingFileDuration() { - if (settings.isLogFileOpen() && outputDataSource == OUTPUT_SOURCE_ODF && settings.maxLogTimeReached()) { + if (isLogFileOpen() && outputDataSource == OUTPUT_SOURCE_ODF && maxLogTimeReached()) { println("DataLogging: Max recording duration reached for OpenBCI data format. Creating a new recording file in the session folder."); closeLogFile(); openNewLogFile(directoryManager.getFileNameDateTime()); - settings.setLogFileStartTime(System.nanoTime()); + setLogFileStartTime(System.nanoTime()); } } public void onStartStreaming() { if (outputDataSource > OUTPUT_SOURCE_NONE && eegDataSource != DATASOURCE_PLAYBACKFILE) { //open data file if it has not already been opened - if (!settings.isLogFileOpen()) { + if (!isLogFileOpen()) { openNewLogFile(directoryManager.getFileNameDateTime()); } - settings.setLogFileStartTime(System.nanoTime()); + setLogFileStartTime(System.nanoTime()); } //Print BrainFlow Streamer Info here after ODF and BDF println @@ -114,7 +115,7 @@ class DataLogger { // Do nothing... break; } - settings.setLogFileIsOpen(true); + setLogFileIsOpen(true); } /** @@ -130,8 +131,7 @@ class DataLogger { //open the new file fileWriterBDF = new DataWriterBDF(_fileName); - output_fname = fileWriterBDF.fname; - println("OpenBCI_GUI: openNewLogFile: opened BDF output file: " + output_fname); //Print filename of new BDF file to console + println("OpenBCI_GUI: openNewLogFile: opened BDF output file: " + fileWriterBDF.getFileName()); } /** @@ -146,14 +146,8 @@ class DataLogger { } //open the new file 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 + println("OpenBCI_GUI: openNewLogFile: opened ODF output file: " + fileWriterODF.getFileName()); } private void closeLogFile() { @@ -169,13 +163,9 @@ class DataLogger { // Do nothing... break; } - settings.setLogFileIsOpen(false); + setLogFileIsOpen(false); } - /** - * @description Close an open BDF file. This will also update the number of data - * records. - */ private void closeLogFileBDF() { if (fileWriterBDF != null) { fileWriterBDF.closeFile(); @@ -183,18 +173,11 @@ class DataLogger { fileWriterBDF = null; } - /** - * @description Close an open ODF file. - */ private void closeLogFileODF() { if (fileWriterODF != null) { fileWriterODF.closeFile(); } fileWriterODF = null; - if (fileWriterAuxODF != null) { - fileWriterAuxODF.closeFile(); - } - fileWriterAuxODF = null; } public int getDataLoggerOutputFormat() { @@ -209,22 +192,61 @@ class DataLogger { sessionName = s; } - public final String getSessionName() { + public String getSessionName() { return sessionName; } + + + public void setSessionPath (String _path) { + sessionPath = _path; + } + + public String getSessionPath() { + return sessionPath; + } + public void setBfWriterFolder(String _folderName, String _folderPath) { fileWriterBF.setBrainFlowStreamerFolderName(_folderName, _folderPath); } public void setBfWriterDefaultFolder() { - if (settings.getSessionPath() != "") { - settings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + sessionName); + if (getSessionPath() != "") { + setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + sessionName); } - fileWriterBF.setBrainFlowStreamerFolderName(sessionName, settings.getSessionPath()); + fileWriterBF.setBrainFlowStreamerFolderName(sessionName, getSessionPath()); } public String getBfWriterFilePath() { return fileWriterBF.getBrainFlowStreamerRecordingFileName(); } + + + private void setLogFileIsOpen(boolean _toggle) { + logFileIsOpen = _toggle; + } + + private boolean isLogFileOpen() { + return logFileIsOpen; + } + + private void setLogFileStartTime(long _time) { + logFileStartTime = _time; + verbosePrint("Settings: LogFileStartTime = " + _time); + } + + public void setLogFileDurationChoice(int n) { + int fileDurationMinutes = odfFileDuration.values()[n].getValue(); + logFileMaxDurationNano = fileDurationMinutes * 1000000000L * 60; + println("Settings: LogFileMaxDuration = " + fileDurationMinutes + " minutes"); + } + + //Only called during live mode && using OpenBCI Data Format + private boolean maxLogTimeReached() { + if (logFileMaxDurationNano < 0) { + return false; + } else { + return (System.nanoTime() - logFileStartTime) > (logFileMaxDurationNano); + } + } }; \ No newline at end of file diff --git a/OpenBCI_GUI/DataProcessing.pde b/OpenBCI_GUI/DataProcessing.pde index 16068a430..3dcd513d5 100644 --- a/OpenBCI_GUI/DataProcessing.pde +++ b/OpenBCI_GUI/DataProcessing.pde @@ -23,27 +23,29 @@ void processNewData() { int[] exgChannels = currentBoard.getEXGChannels(); int channelCount = currentBoard.getNumEXGChannels(); + if (currentData.size() == 0 || currentData.get(0).length == 0) { + return; + } + //update the data buffers - for (int Ichan=0; Ichan < channelCount; Ichan++) { + for (int channel=0; channel < channelCount; channel++) { for(int i = 0; i < getCurrentBoardBufferSize(); i++) { - dataProcessingRawBuffer[Ichan][i] = (float)currentData.get(i)[exgChannels[Ichan]]; + dataProcessingRawBuffer[channel][i] = (float)currentData.get(i)[exgChannels[channel]]; } - dataProcessingFilteredBuffer[Ichan] = dataProcessingRawBuffer[Ichan].clone(); + dataProcessingFilteredBuffer[channel] = dataProcessingRawBuffer[channel].clone(); } //apply additional processing for the time-domain montage plot (ie, filtering) dataProcessing.process(dataProcessingFilteredBuffer, fftBuff); - dataProcessing.newDataToSend = true; - //look to see if the latest data is railed so that we can notify the user on the GUI - for (int Ichan=0; Ichan < nchan; Ichan++) is_railed[Ichan].update(dataProcessingRawBuffer[Ichan], Ichan); + for (int channel=0; channel < globalChannelCount; channel++) is_railed[channel].update(dataProcessingRawBuffer[channel], channel); //compute the electrode impedance. Do it in a very simple way [rms to amplitude, then uVolt to Volt, then Volt/Amp to Ohm] - for (int Ichan=0; Ichan < nchan; Ichan++) { + for (int channel=0; channel < globalChannelCount; channel++) { // Calculate the impedance - float impedance = (sqrt(2.0)*dataProcessing.data_std_uV[Ichan]*1.0e-6) / BoardCytonConstants.leadOffDrive_amps; + float impedance = (sqrt(2.0)*dataProcessing.data_std_uV[channel]*1.0e-6) / BoardCytonConstants.leadOffDrive_amps; // Subtract the 2.2kOhm resistor impedance -= BoardCytonConstants.series_resistor_ohms; // Verify the impedance is not less than 0 @@ -52,25 +54,24 @@ void processNewData() { impedance = 0; } // Store to the global variable - data_elec_imp_ohm[Ichan] = impedance; + data_elec_imp_ohm[channel] = impedance; } } -void initializeFFTObjects(ddf.minim.analysis.FFT[] fftBuff, float[][] dataProcessingRawBuffer, int Nfft, float fs_Hz) { - +void initializeFFTObjects(ddf.minim.analysis.FFT[] fftBuff, float[][] dataProcessingRawBuffer, int fftPointCount, float fs_Hz) { float[] fooData; - for (int Ichan=0; Ichan < nchan; Ichan++) { + for (int channel=0; channel < globalChannelCount; channel++) { //make the FFT objects...Following "SoundSpectrum" example that came with the Minim library - fftBuff[Ichan].window(ddf.minim.analysis.FFT.HAMMING); + fftBuff[channel].window(ddf.minim.analysis.FFT.HAMMING); //do the FFT on the initial data - if (isFFTFiltered == true) { - fooData = dataProcessingFilteredBuffer[Ichan]; //use the filtered data for the FFT + if (globalFFTSettings.getDataIsFiltered()) { + fooData = dataProcessingFilteredBuffer[channel]; //use the filtered data for the FFT } else { - fooData = dataProcessingRawBuffer[Ichan]; //use the raw data for the FFT + fooData = dataProcessingRawBuffer[channel]; //use the raw data for the FFT } - fooData = Arrays.copyOfRange(fooData, fooData.length-Nfft, fooData.length); - fftBuff[Ichan].forward(fooData); //compute FFT on this channel of data + fooData = Arrays.copyOfRange(fooData, fooData.length-fftPointCount, fooData.length); + fftBuff[channel].forward(fooData); //compute FFT on this channel of data } } @@ -80,10 +81,8 @@ void initializeFFTObjects(ddf.minim.analysis.FFT[] fftBuff, float[][] dataProces class DataProcessing { private float fs_Hz; //sample rate - private int nchan; float data_std_uV[]; float polarity[]; - boolean newDataToSend; final int[] processing_band_low_Hz = { 1, 4, 8, 13, 30 }; //lower bound for each frequency band of interest (2D classifier only) @@ -94,142 +93,164 @@ class DataProcessing { float headWidePower[]; public EmgSettings emgSettings; + public NetworkingSettings networkingSettings; + public NetworkingDataAccumulator networkingDataAccumulator; - DataProcessing(int NCHAN, float sample_rate_Hz) { - nchan = NCHAN; + private final int DOWNSAMPLING_FACTOR = getDownsamplingFactor(); + private int downsamplingCounter = DOWNSAMPLING_FACTOR; // Start at DOWNSAMPLING_FACTOR to accept the first sample + + DataProcessing(float sample_rate_Hz) { fs_Hz = sample_rate_Hz; - data_std_uV = new float[nchan]; - polarity = new float[nchan]; - newDataToSend = false; - avgPowerInBins = new float[nchan][processing_band_low_Hz.length]; + data_std_uV = new float[globalChannelCount]; + polarity = new float[globalChannelCount]; + avgPowerInBins = new float[globalChannelCount][processing_band_low_Hz.length]; headWidePower = new float[processing_band_low_Hz.length]; - emgSettings = new EmgSettings(); + networkingSettings = new NetworkingSettings(); + networkingDataAccumulator = new NetworkingDataAccumulator(); } //Process data on a channel-by-channel basis - private synchronized void processChannel(int Ichan, float[][] data_forDisplay_uV, float[] prevFFTdata) { - int Nfft = getNfftSafe(); + private synchronized void processChannel(int channel, float[][] data_forDisplay_uV, float[] prevFFTdata) { + int fftPointCount = getNumFFTPoints(); double foo; // Filter the data in the time domain // TODO: Use double arrays here and convert to float only to plot data. // ^^^ This might not feasible or meaningful performance improvement. I looked into it a while ago and it seems we need floats for the FFT library also. -RW 2022) try { - double[] tempArray = floatToDoubleArray(data_forDisplay_uV[Ichan]); + double[] tempArray = floatToDoubleArray(data_forDisplay_uV[channel]); //Apply BandStop filter if the filter should be active on this channel - if (filterSettings.values.bandStopFilterActive[Ichan].isActive()) { + if (filterSettings.values.bandStopFilterActive[channel].isActive()) { DataFilter.perform_bandstop( tempArray, currentBoard.getSampleRate(), - filterSettings.values.bandStopStartFreq[Ichan], - filterSettings.values.bandStopStopFreq[Ichan], - filterSettings.values.bandStopFilterOrder[Ichan].getValue(), - filterSettings.values.bandStopFilterType[Ichan].getValue(), + filterSettings.values.bandStopStartFreq[channel], + filterSettings.values.bandStopStopFreq[channel], + filterSettings.values.bandStopFilterOrder[channel].getValue(), + filterSettings.values.bandStopFilterType[channel].getValue(), 1.0); } //Apply BandPass filter if the filter should be active on this channel - if (filterSettings.values.bandPassFilterActive[Ichan].isActive()) { + if (filterSettings.values.bandPassFilterActive[channel].isActive()) { DataFilter.perform_bandpass( tempArray, currentBoard.getSampleRate(), - filterSettings.values.bandPassStartFreq[Ichan], - filterSettings.values.bandPassStopFreq[Ichan], - filterSettings.values.bandPassFilterOrder[Ichan].getValue(), - filterSettings.values.bandPassFilterType[Ichan].getValue(), + filterSettings.values.bandPassStartFreq[channel], + filterSettings.values.bandPassStopFreq[channel], + filterSettings.values.bandPassFilterOrder[channel].getValue(), + filterSettings.values.bandPassFilterType[channel].getValue(), 1.0); } //Apply Environmental Noise filter on all channels. Do it like this since there are no codes for NONE or FIFTY_AND_SIXTY in BrainFlow switch (filterSettings.values.globalEnvFilter) { case FIFTY_AND_SIXTY: - DataFilter.remove_environmental_noise( + DataFilter.perform_bandstop( tempArray, currentBoard.getSampleRate(), - NoiseTypes.FIFTY.get_code()); - DataFilter.remove_environmental_noise( + 48d, + 52d, + 4, + BrainFlowFilterType.BUTTERWORTH.getValue(), + 1d); + DataFilter.perform_bandstop( tempArray, currentBoard.getSampleRate(), - NoiseTypes.SIXTY.get_code()); + 58d, + 62d, + 4, + BrainFlowFilterType.BUTTERWORTH.getValue(), + 1d); break; case FIFTY: - DataFilter.remove_environmental_noise( + DataFilter.perform_bandstop( tempArray, currentBoard.getSampleRate(), - NoiseTypes.FIFTY.get_code()); + 48d, + 52d, + 4, + BrainFlowFilterType.BUTTERWORTH.getValue(), + 1d); break; case SIXTY: - DataFilter.remove_environmental_noise( + DataFilter.perform_bandstop( tempArray, currentBoard.getSampleRate(), - NoiseTypes.SIXTY.get_code()); + 58d, + 62d, + 4, + BrainFlowFilterType.BUTTERWORTH.getValue(), + 1d); break; default: break; } - doubleToFloatArray(tempArray, data_forDisplay_uV[Ichan]); + doubleToFloatArray(tempArray, data_forDisplay_uV[channel]); } catch (BrainFlowError e) { e.printStackTrace(); } //compute the standard deviation of the filtered signal...this is for the head plot - float[] fooData_filt = dataProcessingFilteredBuffer[Ichan]; //use the filtered data + float[] fooData_filt = dataProcessingFilteredBuffer[channel]; //use the filtered data fooData_filt = Arrays.copyOfRange(fooData_filt, fooData_filt.length-((int)fs_Hz), fooData_filt.length); //just grab the most recent second of data - data_std_uV[Ichan]=std(fooData_filt); //compute the standard deviation for the whole array "fooData_filt" + data_std_uV[channel] = std(fooData_filt); //compute the standard deviation for the whole array "fooData_filt" //copy the previous FFT data...enables us to apply some smoothing to the FFT data - for (int I=0; I < fftBuff[Ichan].specSize(); I++) { - prevFFTdata[I] = fftBuff[Ichan].getBand(I); //copy the old spectrum values + for (int I=0; I < fftBuff[channel].specSize(); I++) { + prevFFTdata[I] = fftBuff[channel].getBand(I); //copy the old spectrum values } //prepare the data for the new FFT float[] fooData; - if (isFFTFiltered == true) { - fooData = dataProcessingFilteredBuffer[Ichan]; //use the filtered data for the FFT + if (globalFFTSettings.getDataIsFiltered()) { + fooData = dataProcessingFilteredBuffer[channel]; //use the filtered data for the FFT } else { - fooData = dataProcessingRawBuffer[Ichan]; //use the raw data for the FFT + fooData = dataProcessingRawBuffer[channel]; //use the raw data for the FFT } - fooData = Arrays.copyOfRange(fooData, fooData.length-Nfft, fooData.length); //trim to grab just the most recent block of data + fooData = Arrays.copyOfRange(fooData, fooData.length-fftPointCount, fooData.length); //trim to grab just the most recent block of data float meanData = mean(fooData); //compute the mean for (int I=0; I < fooData.length; I++) fooData[I] -= meanData; //remove the mean (for a better looking FFT //compute the FFT - fftBuff[Ichan].forward(fooData); //compute FFT on this channel of data + fftBuff[channel].forward(fooData); //compute FFT on this channel of data // FFT ref: https://www.mathworks.com/help/matlab/ref/fft.html // first calculate double-sided FFT amplitude spectrum - for (int I=0; I <= Nfft/2; I++) { - fftBuff[Ichan].setBand(I, (float)(fftBuff[Ichan].getBand(I) / Nfft)); + for (int I=0; I <= fftPointCount/2; I++) { + fftBuff[channel].setBand(I, (float)(fftBuff[channel].getBand(I) / fftPointCount)); } // then convert into single-sided FFT spectrum: DC & Nyquist (i=0 & i=N/2) remain the same, others multiply by two. - for (int I=1; I < Nfft/2; I++) { - fftBuff[Ichan].setBand(I, (float)(fftBuff[Ichan].getBand(I) * 2)); + for (int I=1; I < fftPointCount/2; I++) { + fftBuff[channel].setBand(I, (float)(fftBuff[channel].getBand(I) * 2)); } //average the FFT with previous FFT data so that it makes it smoother in time double min_val = 0.01d; - for (int I=0; I < fftBuff[Ichan].specSize(); I++) { //loop over each fft bin + float smoothingFactor = globalFFTSettings.getSmoothingFactor().getValue(); + for (int I=0; I < fftBuff[channel].specSize(); I++) { //loop over each fft bin if (prevFFTdata[I] < min_val) prevFFTdata[I] = (float)min_val; //make sure we're not too small for the log calls - foo = fftBuff[Ichan].getBand(I); + foo = fftBuff[channel].getBand(I); if (foo < min_val) foo = min_val; //make sure this value isn't too small if (true) { //smooth in dB power space - foo = (1.0d-smoothFac[smoothFac_ind]) * java.lang.Math.log(java.lang.Math.pow(foo, 2)); - foo += smoothFac[smoothFac_ind] * java.lang.Math.log(java.lang.Math.pow((double)prevFFTdata[I], 2)); + foo = (1.0d - smoothingFactor) * java.lang.Math.log(java.lang.Math.pow(foo, 2)); + foo += smoothingFactor * java.lang.Math.log(java.lang.Math.pow((double)prevFFTdata[I], 2)); foo = java.lang.Math.sqrt(java.lang.Math.exp(foo)); //average in dB space } else { + //LEGACY CODE -- NOT USED //smooth (average) in linear power space - foo = (1.0d-smoothFac[smoothFac_ind]) * java.lang.Math.pow(foo, 2); - foo+= smoothFac[smoothFac_ind] * java.lang.Math.pow((double)prevFFTdata[I], 2); + foo = (1.0d - smoothingFactor) * java.lang.Math.pow(foo, 2); + foo+= smoothingFactor * java.lang.Math.pow((double)prevFFTdata[I], 2); // take sqrt to be back into uV_rtHz foo = java.lang.Math.sqrt(foo); } - fftBuff[Ichan].setBand(I, (float)foo); //put the smoothed data back into the fftBuff data holder for use by everyone else - // fftBuff[Ichan].setBand(I, 1.0f); // test + fftBuff[channel].setBand(I, (float)foo); //put the smoothed data back into the fftBuff data holder for use by everyone else + // fftBuff[channel].setBand(I, 1.0f); // test } //end loop over FFT bins // calculate single-sided psd by single-sided FFT amplitude spectrum @@ -240,22 +261,22 @@ class DataProcessing { for (int i = 0; i < processing_band_low_Hz.length; i++) { float sum = 0; // int binNum = 0; - for (int Ibin = 0; Ibin <= Nfft/2; Ibin ++) { // loop over FFT bins - float FFT_freq_Hz = fftBuff[Ichan].indexToFreq(Ibin); // center frequency of this bin + for (int Ibin = 0; Ibin <= fftPointCount/2; Ibin ++) { // loop over FFT bins + float FFT_freq_Hz = fftBuff[channel].indexToFreq(Ibin); // center frequency of this bin float psdx = 0; // if the frequency matches a band if (FFT_freq_Hz >= processing_band_low_Hz[i] && FFT_freq_Hz < processing_band_high_Hz[i]) { - if (Ibin != 0 && Ibin != Nfft/2) { - psdx = fftBuff[Ichan].getBand(Ibin) * fftBuff[Ichan].getBand(Ibin) * Nfft/currentBoard.getSampleRate() / 4; + if (Ibin != 0 && Ibin != fftPointCount/2) { + psdx = fftBuff[channel].getBand(Ibin) * fftBuff[channel].getBand(Ibin) * fftPointCount/currentBoard.getSampleRate() / 4; } else { - psdx = fftBuff[Ichan].getBand(Ibin) * fftBuff[Ichan].getBand(Ibin) * Nfft/currentBoard.getSampleRate(); + psdx = fftBuff[channel].getBand(Ibin) * fftBuff[channel].getBand(Ibin) * fftPointCount/currentBoard.getSampleRate(); } sum += psdx; // binNum ++; } } - avgPowerInBins[Ichan][i] = sum; // total power in a band + avgPowerInBins[channel][i] = sum; // total power in a band // println(i, binNum, sum); } } @@ -264,35 +285,17 @@ class DataProcessing { float prevFFTdata[] = new float[fftBuff[0].specSize()]; - for (int Ichan=0; Ichan < nchan; Ichan++) { - processChannel(Ichan, data_forDisplay_uV, prevFFTdata); + for (int channel=0; channel < globalChannelCount; channel++) { + processChannel(channel, data_forDisplay_uV, prevFFTdata); } //end the loop over channels. for (int i = 0; i < processing_band_low_Hz.length; i++) { float sum = 0; - for (int j = 0; j < nchan; j++) { + for (int j = 0; j < globalChannelCount; j++) { sum += avgPowerInBins[j][i]; } - headWidePower[i] = sum/nchan; // averaging power over all channels - } - - // Calculate data used for Headplot - // Find strongest channel - int refChanInd = findMax(data_std_uV); - //println("EEG_Processing: strongest chan (one referenced) = " + (refChanInd+1)); - float[] refData_uV = dataProcessingFilteredBuffer[refChanInd]; //use the filtered data - refData_uV = Arrays.copyOfRange(refData_uV, refData_uV.length-((int)fs_Hz), refData_uV.length); //just grab the most recent second of data - // Compute polarity of each channel - for (int Ichan=0; Ichan < nchan; Ichan++) { - float[] fooData_filt = dataProcessingFilteredBuffer[Ichan]; //use the filtered data - fooData_filt = Arrays.copyOfRange(fooData_filt, fooData_filt.length-((int)fs_Hz), fooData_filt.length); //just grab the most recent second of data - float dotProd = calcDotProduct(fooData_filt, refData_uV); - if (dotProd >= 0.0f) { - polarity[Ichan]=1.0; - } else { - polarity[Ichan]=-1.0; - } + headWidePower[i] = sum/globalChannelCount; // averaging power over all channels } ///////////////////////////////////////////////////////////// @@ -300,13 +303,67 @@ class DataProcessing { // -RW #1094 // ///////////////////////////////////////////////////////////// emgSettings.values.process(dataProcessingFilteredBuffer); - w_focus.updateFocusWidgetData(); - w_bandPower.updateBandPowerWidgetData(); - w_emgJoystick.updateEmgJoystickWidgetData(); - if (w_pulsesensor != null) { - w_pulsesensor.updatePulseSensorWidgetData(); + ((W_Focus) widgetManager.getWidget("W_Focus")).updateFocusWidgetData(); + ((W_BandPower) widgetManager.getWidget("W_BandPower")).updateBandPowerWidgetData(); + ((W_EmgJoystick) widgetManager.getWidget("W_EmgJoystick")).updateEmgJoystickWidgetData(); + if (currentBoard instanceof BoardCyton) { + if (widgetManager.getWidgetExists("W_PulseSensor")) { + ((W_PulseSensor) widgetManager.getWidget("W_PulseSensor")).updatePulseSensorWidgetData(); + } + } + + networkingDataAccumulator.update(); + + addFilteredDataToDownsampledBuffer(); + } + + private void addFilteredDataToDownsampledBuffer() { + int[] exgChannels = currentBoard.getEXGChannels(); + float[][] filteredData = dataProcessingFilteredBuffer; + double[][] frameData = currentBoard.getFrameData(); + + if (frameData[exgChannels[0]].length == 0) { + return; + } + + if (!currentBoard.isStreaming()) { + return; + } + + int start = filteredData[0].length - frameData[exgChannels[0]].length; + + for (int iSample = start; iSample < filteredData[exgChannels[0]].length; iSample++) { + if (downsamplingCounter == DOWNSAMPLING_FACTOR) { + downsamplingCounter = 0; + for (int iChannel = 0; iChannel < exgChannels.length; iChannel++) { + downsampledFilteredBuffer.add(iChannel, filteredData[iChannel][iSample]); + } + } + downsamplingCounter++; } + } + + //Called when using the Playback Scrollbar to update the data while in Playback Mode + public void updateEntireDownsampledBuffer() { + int[] exgChannels = currentBoard.getEXGChannels(); + float[][] filteredData = dataProcessingFilteredBuffer; + downsampledFilteredBuffer.initArrays(); + + + for (int iSample = 0; iSample < filteredData[0].length; iSample++) { + if (downsamplingCounter == DOWNSAMPLING_FACTOR) { + downsamplingCounter = 0; + for (int iChannel = 0; iChannel < exgChannels.length; iChannel++) { + downsampledFilteredBuffer.add(iChannel, filteredData[iChannel][iSample]); + } + } + downsamplingCounter++; + } + } - w_networking.updateNetworkingWidgetData(); + private void clearCalculatedMetricWidgets() { + println("Clearing calculated metric widgets"); + ((W_Spectrogram) widgetManager.getWidget("W_Spectrogram")).clear(); + ((W_Focus) widgetManager.getWidget("W_Focus")).clear(); } } \ No newline at end of file diff --git a/OpenBCI_GUI/DataSourcePlayback.pde b/OpenBCI_GUI/DataSourcePlayback.pde index 8a5cbb177..680ff4ce7 100644 --- a/OpenBCI_GUI/DataSourcePlayback.pde +++ b/OpenBCI_GUI/DataSourcePlayback.pde @@ -1,15 +1,16 @@ abstract class DataSourcePlayback implements DataSource, FileBoard { - private String playbackFilePathExg; - private ArrayList rawDataExg; - private int currentSampleExg; - private int timeOfLastUpdateMSExg; - private String underlyingClassName; - private int numNewSamplesThisFrameExg; + protected String playbackFilePathExg; + protected ArrayList rawDataExg; + protected int currentSampleExg; + protected int timeOfLastUpdateMSExg; + protected int numNewSamplesThisFrameExg; private boolean initialized = false; - private boolean streaming = false; - + protected boolean streaming = false; + public Board underlyingBoard = null; + protected String underlyingClassName; + private int sampleRateExg = -1; private int numChannelsExg = 0; // use it instead getTotalChannelCount() method for old playback files @@ -49,9 +50,9 @@ abstract class DataSourcePlayback implements DataSource, FileBoard { //only needed for synthetic board. can delete if we get rid of synthetic board. if (line.startsWith("%Number of channels")) { int startIndex = line.indexOf('=') + 2; - String nchanStr = line.substring(startIndex); - int chanCount = Integer.parseInt(nchanStr); - updateToNChan(chanCount); // sythetic board depends on this being set before it's initialized + String channelCountString = line.substring(startIndex); + int channelCount = Integer.parseInt(channelCountString); + updateGlobalChannelCount(channelCount); // sythetic board depends on this being set before it's initialized } // some boards have configurable sample rate, so read it from header @@ -114,7 +115,7 @@ abstract class DataSourcePlayback implements DataSource, FileBoard { if (((valStrs.length - 1) != getTotalChannelCount()) && (numChannelsExg == 0)) { outputWarn("you are using old file for playback."); } - numChannelsExg = valStrs.length - 1; // -1 becaise of gui's timestamps + numChannelsExg = valStrs.length - 1; // -1 because of gui's timestamps double[] row = new double[numChannelsExg]; for (int iCol = 0; iCol < numChannelsExg; iCol++) { @@ -145,7 +146,7 @@ abstract class DataSourcePlayback implements DataSource, FileBoard { currentSampleExg += numNewSamplesThisFrameExg; if (endOfFileReached()) { - topNav.stopButtonWasPressed(); + topNav.dataStreamTogglePressed(); } // don't go beyond raw data array size @@ -309,12 +310,9 @@ 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); diff --git a/OpenBCI_GUI/DataSourcePlaybackSynthetic.pde b/OpenBCI_GUI/DataSourcePlaybackSynthetic.pde index 01ce69ef4..b990f4065 100644 --- a/OpenBCI_GUI/DataSourcePlaybackSynthetic.pde +++ b/OpenBCI_GUI/DataSourcePlaybackSynthetic.pde @@ -6,7 +6,7 @@ class DataSourcePlaybackSynthetic extends DataSourcePlayback implements Accelero protected boolean instantiateUnderlyingBoard() { try { - underlyingBoard = new BoardBrainFlowSynthetic(nchan); + underlyingBoard = new BoardBrainFlowSynthetic(globalChannelCount); } catch (Exception e) { println(e.getMessage()); e.printStackTrace(); diff --git a/OpenBCI_GUI/DataSourceSDCard.pde b/OpenBCI_GUI/DataSourceSDCard.pde index c494fe40d..9b4d8ef65 100644 --- a/OpenBCI_GUI/DataSourceSDCard.pde +++ b/OpenBCI_GUI/DataSourceSDCard.pde @@ -137,7 +137,7 @@ class DataSourceSDCard implements DataSource, FileBoard, AccelerometerCapableBoa currentSample += numNewSamplesThisFrame; if (endOfFileReached()) { - topNav.stopButtonWasPressed(); + topNav.dataStreamTogglePressed(); } // don't go beyond raw data array size diff --git a/OpenBCI_GUI/DataWriterAuxODF.pde b/OpenBCI_GUI/DataWriterAuxODF.pde deleted file mode 100644 index 86b552d94..000000000 --- a/OpenBCI_GUI/DataWriterAuxODF.pde +++ /dev/null @@ -1,30 +0,0 @@ -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) { - super(_sessionName, _fileName); - } - - protected int getNumberOfChannels() { - return ((AuxDataBoard)currentBoard).getNumAuxChannels(); - } - - protected int getSamplingRate() { - return ((AuxDataBoard)currentBoard).getAuxSampleRate(); - } - - protected String getUnderlyingBoardClass() { - return ((AuxDataBoard)currentBoard).getClass().getName(); - } - - protected String[] getChannelNames() { - return ((AuxDataBoard)currentBoard).getAuxChannelNames(); - } - - protected int getTimestampChannel() { - return ((AuxDataBoard)currentBoard).getAuxTimestampChannel(); - } - -}; diff --git a/OpenBCI_GUI/DataWriterBDF.pde b/OpenBCI_GUI/DataWriterBDF.pde index e29d07752..c596cb4a5 100644 --- a/OpenBCI_GUI/DataWriterBDF.pde +++ b/OpenBCI_GUI/DataWriterBDF.pde @@ -803,17 +803,17 @@ public class DataWriterBDF { // the following lines convert the new input type (double) // to the input type this function originally expected // (24 bit integer in a byte array of length 3) - int value = (int)allData[exgchannels[i]][sampleIndex]; + double valueDouble = allData[exgchannels[i]][sampleIndex]; + int valueInt = (int)Math.round(valueDouble); ByteBuffer bb = ByteBuffer.allocate(4); - bb.putInt(value); + bb.putInt(valueInt); byte[] bytes = bb.array(); - // skip the first byte which should be full of zeroes anyway (24 bit int) - byte[] values = {bytes[1], bytes[2], bytes[3]}; - // Make the values little endian - chanValBuf[i][samplesInDataRecord][0] = values[2]; - chanValBuf[i][samplesInDataRecord][1] = values[1]; - chanValBuf[i][samplesInDataRecord][2] = values[0]; + // skip the first byte, we only care about lower 24 bits + // and swap order to little-endian + chanValBuf[i][samplesInDataRecord][0] = bytes[3]; + chanValBuf[i][samplesInDataRecord][1] = bytes[2]; + chanValBuf[i][samplesInDataRecord][2] = bytes[1]; } } diff --git a/OpenBCI_GUI/DataWriterBF.pde b/OpenBCI_GUI/DataWriterBF.pde index 83f28f11c..6f7cbbed2 100644 --- a/OpenBCI_GUI/DataWriterBF.pde +++ b/OpenBCI_GUI/DataWriterBF.pde @@ -48,7 +48,8 @@ public class DataWriterBF { } public void setBrainFlowStreamerFolderName(String _folderName, String _folderPath) { - //settings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); + //FIX ME ? + //sessionSettings.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); folderName = _folderName; folderPath = _folderPath; diff --git a/OpenBCI_GUI/DataWriterODF.pde b/OpenBCI_GUI/DataWriterODF.pde index acab5e2b8..e4c313247 100644 --- a/OpenBCI_GUI/DataWriterODF.pde +++ b/OpenBCI_GUI/DataWriterODF.pde @@ -1,15 +1,28 @@ public class DataWriterODF { - private PrintWriter output; + protected PrintWriter output; private String fname; - private int rowsWritten; - private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + protected int rowsWritten; + protected 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(); + dataLogger.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); + fname = dataLogger.getSessionPath(); + fname += fileNamePrependString; + fname += _fileName; + fname += ".txt"; + output = createWriter(fname); //open the file + writeHeader(); //add the header + rowsWritten = 0; //init the counter + } + + // Overloaded constructor to allow for custom header and filename prepend string + DataWriterODF(String _sessionName, String _fileName, String _fileNamePrependString, String _headerFirstLineString) { + fileNamePrependString = _fileNamePrependString; + headerFirstLineString = _headerFirstLineString; + dataLogger.setSessionPath(directoryManager.getRecordingsPath() + "OpenBCISession_" + _sessionName + File.separator); + fname = dataLogger.getSessionPath(); fname += fileNamePrependString; fname += _fileName; fname += ".txt"; @@ -32,29 +45,31 @@ public class DataWriterODF { } output.print("Timestamp (Formatted)"); output.println(); - output.flush(); } public void append(double[][] data) { - //get current date time with Date() for (int iSample = 0; iSample < data[0].length; iSample++) { + + StringBuilder sb = new StringBuilder(); + for (int iChan = 0; iChan < data.length; iChan++) { - output.print(data[iChan][iSample]); - output.print(", "); + sb.append(data[iChan][iSample]); + sb.append(", "); } int timestampChan = getTimestampChannel(); // *1000 to convert from seconds to milliserconds long timestampMS = (long)(data[timestampChan][iSample] * 1000.0); - output.print(dateFormat.format(new Date(timestampMS))); - output.println(); + sb.append(dateFormat.format(new Date(timestampMS))); + output.println(sb.toString()); rowsWritten++; } } public void closeFile() { + output.flush(); output.close(); } @@ -63,7 +78,7 @@ public class DataWriterODF { } protected int getNumberOfChannels() { - return nchan; + return globalChannelCount; } protected int getSamplingRate() { @@ -85,5 +100,9 @@ public class DataWriterODF { protected int getMarkerChannel() { return ((Board)currentBoard).getMarkerChannel(); } + + public String getFileName() { + return fname; + } }; diff --git a/OpenBCI_GUI/Debugging.pde b/OpenBCI_GUI/Debugging.pde index caffb8fd9..ecf144322 100644 --- a/OpenBCI_GUI/Debugging.pde +++ b/OpenBCI_GUI/Debugging.pde @@ -70,11 +70,12 @@ class HelpWidget { public void update() { } + //This needs to be refactored public void draw() { pushStyle(); - if(colorScheme == COLOR_SCHEME_DEFAULT){ + if (colorScheme == COLOR_SCHEME_DEFAULT) { // draw background of widget stroke(OPENBCI_DARKBLUE); fill(255); diff --git a/OpenBCI_GUI/DeveloperCommandsPopup.pde b/OpenBCI_GUI/DeveloperCommandsPopup.pde new file mode 100644 index 000000000..dcce2c2f8 --- /dev/null +++ b/OpenBCI_GUI/DeveloperCommandsPopup.pde @@ -0,0 +1,353 @@ +public boolean developerCommandPopupIsOpen = false; + +// Instantiate this class to show a popup message +class DeveloperCommandPopup extends PApplet implements Runnable { + private final int DEFAULT_WIDTH = 500; + private final int DEFAULT_HEIGHT = 300; + + private final int HEADER_HEIGHT = 55; + protected final int PADDING = 20; + private final int PADDING_5 = 5; + + private final int BUTTON_WIDTH = 230; + protected final int BUTTON_HEIGHT = 30; + + private final int RESPONSE_TEXT_HEIGHT = 50; + + private String message = "Type a custom command and press Send Command.\nWarning: This is an expert mode feature. Use with caution."; + private String headerMessage = "Developer Commands"; + + private color headerColor = OPENBCI_BLUE; + protected color buttonColor = OPENBCI_BLUE; + private color backgroundColor = GREY_235; + + protected Textfield customCommandTF; + protected Button sendCustomCmdButton; + protected String responseText = ""; + + protected ControlP5 cp5; + + private LocalTextFieldUpdateHelper popupTextfieldUpdateHelper = new LocalTextFieldUpdateHelper(); + private LocalCopyPaste localCopyPaste = new LocalCopyPaste(); + + public DeveloperCommandPopup() { + super(); + developerCommandPopupIsOpen = true; + output("Developer Commands: Developer Command Popup Opened."); + Thread t = new Thread(this); + t.start(); + } + + @Override + public void run() { + PApplet.runSketch(new String[] {headerMessage}, this); + } + + @Override + public void settings() { + size(DEFAULT_WIDTH, DEFAULT_HEIGHT); + } + + @Override + public void setup() { + surface.setTitle(headerMessage); + surface.setAlwaysOnTop(true); + surface.setResizable(false); + + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.toFront(); + frame.requestFocus(); + + cp5 = new ControlP5(this); + cp5.setAutoDraw(false); + + createCustomCommandUI(); + } + + @Override + public void draw() { + + popupTextfieldUpdateHelper.checkTextfield(customCommandTF); + + final int w = width; + final int h = height; + + pushStyle(); + + // draw bg + background(OPENBCI_DARKBLUE); + stroke(204); + fill(backgroundColor); + rect(0, 0, w, h); + + // draw header + noStroke(); + fill(headerColor); + rect(0, 0, w, HEADER_HEIGHT); + + //draw header text + textFont(p0, 24); + fill(WHITE); + textAlign(LEFT, CENTER); + text(headerMessage, 0 + PADDING, HEADER_HEIGHT/2); + + //draw message + textFont(p3, 16); + fill(GREY_100); + textAlign(LEFT, TOP); + text(message, 0 + PADDING, 0 + PADDING + HEADER_HEIGHT, w - PADDING*2, h - PADDING*2 - HEADER_HEIGHT); + + //draw response + textFont(p4, 14); + fill(GREY_100); + textAlign(LEFT, TOP); + text("Response: " + responseText, 0 + PADDING, customCommandTF.getPosition()[1] + BUTTON_HEIGHT + PADDING_5, w - PADDING*2, RESPONSE_TEXT_HEIGHT); + + popStyle(); + + try { + cp5.draw(); + } catch (ConcurrentModificationException e) { + println("PopupMessage Base Class: Error drawing cp5" + e.getMessage()); + } catch (ArrayIndexOutOfBoundsException e) { + println("PopupMessage Base Class: Error drawing cp5" + e.getMessage()); + } + } + + @Override + void mousePressed() { + + } + + @Override + void mouseReleased() { + + } + + @Override + void keyPressed() { + if (localCopyPaste.checkIfPressedAllOS()) { + return; + } + } + + @Override + void keyReleased() { + localCopyPaste.checkIfReleasedAllOS(); + } + + @Override + void exit() { + dispose(); + developerCommandPopupIsOpen = false; + } + + // Dispose of the popup window externally + public void exitPopup() { + output("Developer Commands: Developer Command Popup closed"); + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.dispose(); + developerCommandPopupIsOpen = false; + } + + private void createCustomCommandUI() { + customCommandTF = cp5.addTextfield("customCommand") + .setPosition(0, 0) + .setCaptionLabel("") + .setSize(BUTTON_WIDTH, BUTTON_HEIGHT) + .setFont(f2) + .setFocus(false) + .setColor(color(26, 26, 26)) + .setColorBackground(color(255, 255, 255)) // text field bg color + .setColorValueLabel(OPENBCI_DARKBLUE) // text color + .setColorForeground(OBJECT_BORDER_GREY) // border color when not selected + .setColorActive(isSelected_color) // border color when selected + .setColorCursor(color(26, 26, 26)) + .setText("") + .align(5, 10, 20, 40) + .setAutoClear(false) //Don't clear textfield when pressing Enter key + ; + customCommandTF.setDescription("Type a custom command and Send to board."); + //Clear textfield on double click + customCommandTF.onDoublePress(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + output("[ExpertMode] Enter the custom command you would like to send to the board."); + customCommandTF.clear(); + } + }); + customCommandTF.addCallback(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + customCommandTF.setFocus(false); + } + } + }); + + sendCustomCmdButton = createButton(cp5, "sendCustomCommand", "Send Command", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT); + sendCustomCmdButton.setBorderColor(OBJECT_BORDER_GREY); + sendCustomCmdButton.getCaptionLabel().getStyle().setMarginLeft(1); + sendCustomCmdButton.onClick(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + String text = dropNonPrintableChars(customCommandTF.getText()); + Pair res = ((BoardBrainFlow)currentBoard).sendCommand(text); + if (res.getKey().booleanValue()) { + outputSuccess("[ExpertMode] Success sending command to board: " + text); + responseText = res.getValue(); + } else { + outputError("[ExpertMode] Failure sending command to board: " + text); + responseText = "NO RESPONSE FROM BOARD"; + } + println("ADSSettingsController: Response == " + res.getValue()); + + } + }); + + int objectX = width/2; + int messagePadding = HEADER_HEIGHT + PADDING; + int objectY = HEADER_HEIGHT + messagePadding + PADDING + PADDING_5; + customCommandTF.setPosition(objectX - BUTTON_WIDTH - PADDING_5, objectY - BUTTON_HEIGHT/2); + sendCustomCmdButton.setPosition(objectX + PADDING_5, objectY - BUTTON_HEIGHT/2); + } + + //We need a local copy of this class because this instance is unable to use the global instance from the main GUI. + public class LocalTextFieldUpdateHelper { + + // textFieldIsActive is used to ignore hotkeys when a textfield is active. Resets to false on every draw loop. + private boolean textFieldIsActive = false; + + LocalTextFieldUpdateHelper() { + } + + public void resetTextFieldIsActive() { + textFieldIsActive = false; + } + + public boolean getAnyTextfieldsActive() { + return textFieldIsActive; + } + + public void checkTextfield(Textfield tf) { + if (tf.isVisible()) { + tf.setUpdate(true); + if (tf.isFocus()) { + textFieldIsActive = true; + localCopyPaste.checkForCopyPaste(tf); + } + } else { + tf.setUpdate(false); + } + } + } + + class LocalCopyPaste { + + 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; + + LocalCopyPaste () { + + } + + 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); + } + } +}; \ No newline at end of file diff --git a/OpenBCI_GUI/DownsamplingRateEnum.pde b/OpenBCI_GUI/DownsamplingRateEnum.pde new file mode 100644 index 000000000..fac87805d --- /dev/null +++ b/OpenBCI_GUI/DownsamplingRateEnum.pde @@ -0,0 +1,12 @@ +public enum DownsamplingRateEnum { + NONE (1), + TWO (2), + FOUR (4), + EIGHT (8); + + public final int value; + + DownsamplingRateEnum(int _value) { + value = _value; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/EmgJoystickEnums.pde b/OpenBCI_GUI/EmgJoystickEnums.pde new file mode 100644 index 000000000..902739a2d --- /dev/null +++ b/OpenBCI_GUI/EmgJoystickEnums.pde @@ -0,0 +1,98 @@ + +public enum EmgJoystickSmoothing implements IndexingInterface +{ + OFF (0, "Off", 0f), + POINT_9 (1, "0.9", .9f), + POINT_95 (2, "0.95", .95f), + POINT_98 (3, "0.98", .98f), + POINT_99 (4, "0.99", .99f), + POINT_999 (5, "0.999", .999f), + POINT_9999 (6, "0.9999", .9999f); + + private int index; + private String name; + private float value; + + EmgJoystickSmoothing(int index, String name, float value) { + this.index = index; + this.name = name; + this.value = value; + } + + @Override + public int getIndex() { + return index; + } + + @Override + public String getString() { + return name; + } + + public float getValue() { + return value; + } +} + +public class EMGJoystickInput implements IndexingInterface{ + private int index; + private String name; + private int value; + + EMGJoystickInput(int index, String name, int value) { + this.index = index; + this.name = name; + this.value = value; + } + + @Override + public int getIndex() { + return index; + } + + @Override + public String getString() { + return name; + } + + public int getValue() { + return value; + } +} + +public class EMGJoystickInputs { + private final int NUM_EMG_INPUTS = 4; + private final EMGJoystickInput[] VALUES; + private final EMGJoystickInput[] INPUTS = new EMGJoystickInput[NUM_EMG_INPUTS]; + + EMGJoystickInputs(int numExGChannels) { + VALUES = new EMGJoystickInput[numExGChannels]; + for (int i = 0; i < numExGChannels; i++) { + VALUES[i] = new EMGJoystickInput(i, "Channel " + (i + 1), i); + } + } + + public EMGJoystickInput[] getValues() { + return VALUES; + } + + public EMGJoystickInput[] getInputs() { + return INPUTS; + } + + public EMGJoystickInput getInput(int index) { + return INPUTS[index]; + } + + public void setInputToChannel(int inputNumber, int channel) { + if (inputNumber < 0 || inputNumber >= NUM_EMG_INPUTS) { + println("Invalid input number: " + inputNumber); + return; + } + if (channel < 0 || channel >= VALUES.length) { + println("Invalid channel: " + channel); + return; + } + INPUTS[inputNumber] = VALUES[channel]; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/EmgSettings.pde b/OpenBCI_GUI/EmgSettings.pde index e4947932f..ffa778357 100644 --- a/OpenBCI_GUI/EmgSettings.pde +++ b/OpenBCI_GUI/EmgSettings.pde @@ -11,57 +11,35 @@ class EmgSettings { values = new EmgSettingsValues(); } - public boolean loadSettingsValues(String filename) { + public String getJson() { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + return gson.toJson(values); + } + + public boolean loadSettingsFromJson(String json) { try { - File file = new File(filename); - StringBuilder fileContents = new StringBuilder((int)file.length()); - Scanner scanner = new Scanner(file); - while(scanner.hasNextLine()) { - fileContents.append(scanner.nextLine() + System.lineSeparator()); - } Gson gson = new Gson(); - EmgSettingsValues tempValues = gson.fromJson(fileContents.toString(), EmgSettingsValues.class); + EmgSettingsValues tempValues = gson.fromJson(json, EmgSettingsValues.class); + + // Validate channel count matches if (tempValues.window.length != channelCount) { - outputError("Emg Settings: Loaded EMG Settings file has different number of channels than the current board."); + outputError("Emg Settings: Loaded EMG Settings JSON has different number of channels than the current board."); return false; } - //Explicitely copy values over to avoid reference issues - //(e.g. values = tempValues "nukes" the old values object) + + // Explicitly copy values to avoid reference issues values.window = tempValues.window; values.uvLimit = tempValues.uvLimit; values.creepIncreasing = tempValues.creepIncreasing; values.creepDecreasing = tempValues.creepDecreasing; values.minimumDeltaUV = tempValues.minimumDeltaUV; values.lowerThresholdMinimum = tempValues.lowerThresholdMinimum; + + settingsWereLoaded = true; return true; - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); - File f = new File(filename); - if (f.exists()) { - if (f.delete()) { - outputError("Emg Settings: Could not load EMG settings from disk. Deleting this file..."); - } else { - outputError("Emg Settings: Error deleting old/broken EMG settings file! Please make sure the GUI has proper read/write permissions."); - } - } - return false; - } - } - - public String getJson() { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - return gson.toJson(values); - } - - public boolean saveToFile(String filename) { - String json = getJson(); - try { - FileWriter writer = new FileWriter(filename); - writer.write(json); - writer.close(); - return true; - } catch (IOException e) { - e.printStackTrace(); + outputError("EmgSettings: Could not load EMG settings from JSON string."); return false; } } @@ -76,30 +54,6 @@ class EmgSettings { return channelCount; } - //Avoid error with popup being in another thread. - public void storeSettings() { - StringBuilder settingsFilename = new StringBuilder(directoryManager.getSettingsPath()); - settingsFilename.append("EmgSettings"); - settingsFilename.append("_"); - settingsFilename.append(getChannelCount()); - settingsFilename.append("Channels.json"); - String filename = settingsFilename.toString(); - File fileToSave = new File(filename); - selectOutput("Save EMG settings to file", "storeEmgSettings", fileToSave); - } - - //Avoid error with popup being in another thread. - public void loadSettings() { - StringBuilder settingsFilename = new StringBuilder(directoryManager.getSettingsPath()); - settingsFilename.append("EmgSettings"); - settingsFilename.append("_"); - settingsFilename.append(getChannelCount()); - settingsFilename.append("Channels.json"); - String filename = settingsFilename.toString(); - File fileToLoad = new File(filename); - selectInput("Select EMG settings file to load", "loadEmgSettings", fileToLoad); - } - public boolean getSettingsWereLoaded() { return settingsWereLoaded; } @@ -107,29 +61,4 @@ class EmgSettings { public void setSettingsWereLoaded(boolean settingsWereLoaded) { this.settingsWereLoaded = settingsWereLoaded; } -} - -//Used by button in the EMG UI. Must be global and public. Called in above loadSettings method. -public void loadEmgSettings(File selection) { - if (selection == null) { - output("EMG Settings file not selected."); - } else { - if (dataProcessing.emgSettings.loadSettingsValues(selection.getAbsolutePath())) { - outputSuccess("EMG Settings Loaded!"); - dataProcessing.emgSettings.setSettingsWereLoaded(true); - } - } -} - -//Used by button in the EMG UI. Must be global and public. Called in above storeSettings method. -public void storeEmgSettings(File selection) { - if (selection == null) { - output("EMG Settings file not selected."); - } else { - if (dataProcessing.emgSettings.saveToFile(selection.getAbsolutePath())) { - outputSuccess("EMG Settings Saved!"); - } else { - outputError("Failed to save EMG Settings."); - } - } } \ No newline at end of file diff --git a/OpenBCI_GUI/EmgSettingsEnums.pde b/OpenBCI_GUI/EmgSettingsEnums.pde index c7d195693..732bb12ac 100644 --- a/OpenBCI_GUI/EmgSettingsEnums.pde +++ b/OpenBCI_GUI/EmgSettingsEnums.pde @@ -140,6 +140,7 @@ public enum EmgMinimumDeltaUV implements EmgSettingsEnum SIX_UV (2, "6 uV", 6), EIGHT_UV (3, "8 uV", 8), TEN_UV (4, "10 uV", 10), + FIFTEEN_UV (4, "15 uV", 15), TWENTY_UV (5, "20 uV", 20), FORTY_UV (6, "40 uV", 40), EIGHTY_UV (7, "80 uV", 80); diff --git a/OpenBCI_GUI/EmgSettingsUI.pde b/OpenBCI_GUI/EmgSettingsUI.pde index 7573afc4d..a949ba773 100644 --- a/OpenBCI_GUI/EmgSettingsUI.pde +++ b/OpenBCI_GUI/EmgSettingsUI.pde @@ -27,17 +27,12 @@ class EmgSettingsUI extends PApplet implements Runnable { private boolean isFixedHeight; private int fixedHeight; private int[] dropdownYPositions; - private final int NUM_FOOTER_OBJECTS = 3; - private final int FOOTER_OBJECT_WIDTH = 45; - private final int FOOTER_OBJECT_HEIGHT = 26; - private int footerObjY; - private int[] footerObjX = new int[NUM_FOOTER_OBJECTS]; private final color HEADER_COLOR = OPENBCI_BLUE; private final color BACKGROUND_COLOR = GREY_235; private final color LABEL_COLOR = WHITE; - private final int defaultWidth = 600; + private final int defaultWidth = 680; private final int defaultHeight = 600; public EmgSettingsValues emgSettingsValues; @@ -52,8 +47,8 @@ class EmgSettingsUI extends PApplet implements Runnable { private ScrollableList[] windowLists; private ScrollableList[] uvLimitLists; - private ScrollableList[] creepIncLists; private ScrollableList[] creepDecLists; + private ScrollableList[] creepIncLists; private ScrollableList[] minDeltaUvLists; private ScrollableList[] lowLimitLists; @@ -61,10 +56,6 @@ class EmgSettingsUI extends PApplet implements Runnable { private String[] channelLabels; - private Button saveButton; - private Button loadButton; - private Button defaultButton; - @Override public void run() { PApplet.runSketch(new String[] {HEADER_MESSAGE}, this); @@ -98,7 +89,7 @@ class EmgSettingsUI extends PApplet implements Runnable { ourApplet = this; surface.setTitle(HEADER_MESSAGE); - surface.setAlwaysOnTop(false); + surface.setAlwaysOnTop(true); surface.setResizable(false); Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); @@ -145,8 +136,7 @@ class EmgSettingsUI extends PApplet implements Runnable { try { emgCp5.draw(); } catch (ConcurrentModificationException e) { - e.printStackTrace(); - outputError("EMG Settings UI: Unable to draw cp5 objects."); + println("EMG Settings UI: Error drawing cp5: " + e.getMessage()); } } @@ -217,14 +207,14 @@ class EmgSettingsUI extends PApplet implements Runnable { uvLimitLists[i].setPosition(dropdownX, dropdownYPositions[i]); uvLimitLists[i].setSize(dropdownWidth, (uvLimitLists[i].getItems().size()+1) * DROPDOWN_HEIGHT); - dropdownX += buttonXIncrement; - creepIncLists[i].setPosition(dropdownX, dropdownYPositions[i]); - creepIncLists[i].setSize(dropdownWidth, MAX_HEIGHT_ITEMS * DROPDOWN_HEIGHT); - dropdownX += buttonXIncrement; creepDecLists[i].setPosition(dropdownX, dropdownYPositions[i]); creepDecLists[i].setSize(dropdownWidth, MAX_HEIGHT_ITEMS * DROPDOWN_HEIGHT); + dropdownX += buttonXIncrement; + creepIncLists[i].setPosition(dropdownX, dropdownYPositions[i]); + creepIncLists[i].setSize(dropdownWidth, MAX_HEIGHT_ITEMS * DROPDOWN_HEIGHT); + dropdownX += buttonXIncrement; minDeltaUvLists[i].setPosition(dropdownX, dropdownYPositions[i]); minDeltaUvLists[i].setSize(dropdownWidth, MAX_HEIGHT_ITEMS * DROPDOWN_HEIGHT); @@ -236,17 +226,6 @@ class EmgSettingsUI extends PApplet implements Runnable { } private void createAllUIObjects() { - final int HALF_FOOTER_HEIGHT = (FOOTER_PADDING + (DROPDOWN_SPACER * 2)) / 2; - footerObjY = y + h - HALF_FOOTER_HEIGHT - (FOOTER_OBJECT_HEIGHT / 2); - int middle = x + w / 2; - int halfObjWidth = FOOTER_OBJECT_WIDTH / 2; - footerObjX[0] = middle - halfObjWidth - PADDING_12 - FOOTER_OBJECT_WIDTH; - footerObjX[1] = middle - halfObjWidth; - footerObjX[2] = middle + halfObjWidth + PADDING_12; - createEmgSettingsSaveButton("saveEmgSettingsButton", "Save", footerObjX[0], footerObjY, FOOTER_OBJECT_WIDTH, FOOTER_OBJECT_HEIGHT); - createEmgSettingsLoadButton("loadEmgSettingsButton", "Load", footerObjX[1], footerObjY, FOOTER_OBJECT_WIDTH, FOOTER_OBJECT_HEIGHT); - createEmgSettingsDefaultButton("defaultEmgSettingsButton", "Reset", footerObjX[2], footerObjY, FOOTER_OBJECT_WIDTH, FOOTER_OBJECT_HEIGHT); - channelLabels = new String[channelCount]; for (int i = 0; i < channelCount; i++) { channelLabels[i] = "Channel " + (i+1); @@ -261,8 +240,8 @@ class EmgSettingsUI extends PApplet implements Runnable { channelColumnLabel = new TextBox("Channel", x + colOffset, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); windowLabel = new TextBox("Window", x + colOffset + colWidth, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); uvLimitLabel = new TextBox("uV Limit", x + colOffset + colWidth*2, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); - creepIncLabel = new TextBox("Creep +", x + colOffset + colWidth*3, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); - creepDecLabel = new TextBox("Creep -", x + colOffset + colWidth*4, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); + creepDecLabel = new TextBox("Floor Creep", x + colOffset + colWidth*3, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); + creepIncLabel = new TextBox("Ceiling Creep", x + colOffset + colWidth*4, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); minDeltaUvLabel = new TextBox("Min \u0394uV", x + colOffset + colWidth*5, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); lowLimitLabel = new TextBox("Low Limit", x + colOffset + colWidth*6, labelY, labelTxt, labelBG, 14, h4, CENTER, CENTER); @@ -275,8 +254,8 @@ class EmgSettingsUI extends PApplet implements Runnable { windowLists = new ScrollableList[channelCount]; uvLimitLists = new ScrollableList[channelCount]; - creepIncLists = new ScrollableList[channelCount]; creepDecLists = new ScrollableList[channelCount]; + creepIncLists = new ScrollableList[channelCount]; minDeltaUvLists = new ScrollableList[channelCount]; lowLimitLists = new ScrollableList[channelCount]; @@ -287,8 +266,8 @@ class EmgSettingsUI extends PApplet implements Runnable { int exgChannel = i; windowLists[i] = createDropdown(exgChannel, "smooth_ch_"+(i+1), emgSettingsValues.window[exgChannel].values(), emgSettingsValues.window[exgChannel]); uvLimitLists[i] = createDropdown(exgChannel, "uvLimit_ch_"+(i+1), emgSettingsValues.uvLimit[exgChannel].values(), emgSettingsValues.uvLimit[exgChannel]); - creepIncLists[i] = createDropdown(exgChannel, "creep_inc_ch_"+(i+1), emgSettingsValues.creepIncreasing[exgChannel].values(), emgSettingsValues.creepIncreasing[exgChannel]); creepDecLists[i] = createDropdown(exgChannel, "creep_dec_ch_"+(i+1), emgSettingsValues.creepDecreasing[exgChannel].values(), emgSettingsValues.creepDecreasing[exgChannel]); + creepIncLists[i] = createDropdown(exgChannel, "creep_inc_ch_"+(i+1), emgSettingsValues.creepIncreasing[exgChannel].values(), emgSettingsValues.creepIncreasing[exgChannel]); minDeltaUvLists[i] = createDropdown(exgChannel, "minDeltaUv_ch_"+(i+1), emgSettingsValues.minimumDeltaUV[exgChannel].values(), emgSettingsValues.minimumDeltaUV[exgChannel]); lowLimitLists[i] = createDropdown(exgChannel, "lowLimit_ch_"+(i+1), emgSettingsValues.lowerThresholdMinimum[exgChannel].values(), emgSettingsValues.lowerThresholdMinimum[exgChannel]); } @@ -370,36 +349,6 @@ class EmgSettingsUI extends PApplet implements Runnable { } } - private void createEmgSettingsSaveButton(String name, String text, int _x, int _y, int _w, int _h) { - saveButton = createButton(emgCp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - saveButton.setBorderColor(OBJECT_BORDER_GREY); - saveButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - dataProcessing.emgSettings.storeSettings(); - } - }); - } - - private void createEmgSettingsLoadButton(String name, String text, int _x, int _y, int _w, int _h) { - loadButton = createButton(emgCp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - loadButton.setBorderColor(OBJECT_BORDER_GREY); - loadButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - dataProcessing.emgSettings.loadSettings(); - } - }); - } - - private void createEmgSettingsDefaultButton(String name, String text, int _x, int _y, int _w, int _h) { - defaultButton = createButton(emgCp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - defaultButton.setBorderColor(OBJECT_BORDER_GREY); - defaultButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - dataProcessing.emgSettings.revertAllChannelsToDefaultValues(); - } - }); - } - private void updateCp5Objects() { for (int i = 0; i < channelCount; i++) { //Fetch values from the EmgSettingsValues object @@ -413,8 +362,8 @@ class EmgSettingsUI extends PApplet implements Runnable { //Update the ScrollableLists windowLists[i].getCaptionLabel().setText(updateSmoothing.getString()); uvLimitLists[i].getCaptionLabel().setText(updateUVLimit.getString()); - creepIncLists[i].getCaptionLabel().setText(updateCreepIncreasing.getString()); creepDecLists[i].getCaptionLabel().setText(updateCreepDecreasing.getString()); + creepIncLists[i].getCaptionLabel().setText(updateCreepIncreasing.getString()); minDeltaUvLists[i].getCaptionLabel().setText(updateMinimumDeltaUV.getString()); lowLimitLists[i].getCaptionLabel().setText(updateLowerThresholdMinimum.getString()); } diff --git a/OpenBCI_GUI/EmgSettingsValues.pde b/OpenBCI_GUI/EmgSettingsValues.pde index c87d634f8..f4ff4a900 100644 --- a/OpenBCI_GUI/EmgSettingsValues.pde +++ b/OpenBCI_GUI/EmgSettingsValues.pde @@ -40,7 +40,7 @@ class EmgSettingsValues { averageuV = new float[channelCount]; Arrays.fill(window, EmgWindow.ONE_SECOND); - Arrays.fill(uvLimit, EmgUVLimit.TWO_HUNDRED_UV); + Arrays.fill(uvLimit, EmgUVLimit.ONE_HUNDRED_UV); Arrays.fill(creepIncreasing, EmgCreepIncreasing.POINT_9); Arrays.fill(creepDecreasing, EmgCreepDecreasing.POINT_99999); Arrays.fill(minimumDeltaUV, EmgMinimumDeltaUV.TEN_UV); @@ -99,10 +99,17 @@ class EmgSettingsValues { if(outputNormalized[i] < 0){ outputNormalized[i] = 0; //always make sure this value is >= 0 } + if (outputNormalized[i] > 1) { + outputNormalized[i] = 1; //always make sure this value is <= 1 + } } } - public float getOutputNormalized(int channel) { + public float[] getNormalizedValues() { + return outputNormalized; + } + + public float getNormalizedChannelValue(int channel) { return outputNormalized[channel]; } diff --git a/OpenBCI_GUI/EnumHelper.pde b/OpenBCI_GUI/EnumHelper.pde new file mode 100644 index 000000000..68c9e8ae5 --- /dev/null +++ b/OpenBCI_GUI/EnumHelper.pde @@ -0,0 +1,28 @@ +//Used for Widget Dropdown Enums +interface IndexingInterface { + public int getIndex(); + public String getString(); +} + +/** + * Helper class for working with IndexingInterface enums + */ +public static class EnumHelper { + /** + * Generic method to get enum strings as a list + */ + public static List getListAsStrings(T[] values) { + List enumStrings = new ArrayList<>(); + for (T enumValue : values) { + enumStrings.add(enumValue.getString()); + } + return enumStrings; + } + + /** + * Get list of strings for an enum class that implements IndexingInterface + */ + public static & IndexingInterface> List getEnumStrings(Class enumClass) { + return getListAsStrings(enumClass.getEnumConstants()); + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/Extras.pde b/OpenBCI_GUI/Extras.pde index b6e50c341..c6d664cc7 100644 --- a/OpenBCI_GUI/Extras.pde +++ b/OpenBCI_GUI/Extras.pde @@ -11,6 +11,12 @@ import static java.util.prefs.Preferences.systemRoot; import java.util.regex.Matcher; import java.util.regex.Pattern; +public void takeGUIScreenshot() { + String pictureFilename = "OpenBCI-" + directoryManager.getFileNameDateTime() + ".jpg"; + saveFrame(directoryManager.getGuiDataPath() + "Screenshots" + System.getProperty("file.separator") + pictureFilename); + output("Screenshot captured! Saved to /Documents/OpenBCI_GUI/Screenshots/" + pictureFilename); +} + /** * @description Helper function to determine if the system is linux or not. * @return {boolean} true if os is linux, false otherwise. @@ -410,6 +416,11 @@ void doubleToFloatArray(double[] array, float[] res) { } } +public double convertByteArrayToDouble(byte[] array) { + ByteBuffer buffer = ByteBuffer.wrap(array); + return buffer.getDouble(); +} + // shortens a string to a given width by adding [...] in the middle // make sure to pass the right font for accurate sizing String shortenString(String str, float maxWidth, PFont font) { @@ -570,14 +581,6 @@ class FilterConstants { } }; -class PlotFontInfo { - String fontName = "fonts/Raleway-Regular.otf"; - int axisLabel_size = 16; - int tickLabel_size = 14; - int buttonLabel_size = 12; -}; - - class TextBox { private int x, y; private int w, h; diff --git a/OpenBCI_GUI/FFTEnums.pde b/OpenBCI_GUI/FFTEnums.pde new file mode 100644 index 000000000..ce1b1e775 --- /dev/null +++ b/OpenBCI_GUI/FFTEnums.pde @@ -0,0 +1,185 @@ + +public class GlobalFFTSettings { + public FFTSmoothingFactor smoothingFactor = FFTSmoothingFactor.SMOOTH_90; + public FFTFilteredEnum dataIsFiltered = FFTFilteredEnum.FILTERED; + + GlobalFFTSettings() { + // Constructor + } + + public void setSmoothingFactor(FFTSmoothingFactor factor) { + this.smoothingFactor = factor; + } + + public FFTSmoothingFactor getSmoothingFactor() { + return smoothingFactor; + } + + public void setFilteredEnum(FFTFilteredEnum filteredEnum) { + this.dataIsFiltered = filteredEnum; + } + + public FFTFilteredEnum getFilteredEnum() { + return dataIsFiltered; + } + + public boolean getDataIsFiltered() { + return dataIsFiltered == FFTFilteredEnum.FILTERED; + } +} + + +// Used by FFT Widget, Band Power Widget, and Head Plot Widget +public enum FFTSmoothingFactor implements IndexingInterface { + NONE (0, 0.0f, "O.O"), + SMOOTH_50 (1, 0.5f, "0.5"), + SMOOTH_75 (2, 0.75f, "0.75"), + SMOOTH_90 (3, 0.9f, "0.9"), + SMOOTH_95 (4, 0.95f, "0.95"), + SMOOTH_98 (5, 0.98f, "0.98"), + SMOOTH_99 (6, 0.99f, "0.99"), + SMOOTH_999 (7, 0.999f, "0.999"); + + private int index; + private final float value; + private String label; + + FFTSmoothingFactor(int index, float value, String label) { + this.index = index; + this.value = value; + this.label = label; + } + + public float getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +// Used by FFT Widget and Band Power Widget +public enum FFTFilteredEnum implements IndexingInterface { + FILTERED (0, "Filtered"), + UNFILTERED (1, "Unfilt."); + + private int index; + private String label; + + FFTFilteredEnum(int index, String label) { + this.index = index; + this.label = label; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum FFTMaxFrequency implements IndexingInterface { + MAX_20 (0, 20, "20 Hz"), + MAX_40 (1, 40, "40 Hz"), + MAX_60 (2, 60, "60 Hz"), + MAX_100 (3, 100, "100 Hz"), + MAX_120 (4, 120, "120 Hz"), + MAX_250 (5, 250, "250 Hz"), + MAX_500 (6, 500, "500 Hz"), + MAX_800 (7, 800, "800 Hz"); + + private int index; + private final int value; + private String label; + + FFTMaxFrequency(int index, int value, String label) { + this.index = index; + this.value = value; + this.label = label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public int getHighestFrequency() { + return MAX_800.getValue(); + } +} + +public enum FFTVerticalScale implements IndexingInterface { + SCALE_10 (0, 10, "10 uV"), + SCALE_50 (1, 50, "50 uV"), + SCALE_100 (2, 100, "100 uV"), + SCALE_500 (3, 500, "500 uV"), + SCALE_1000 (3, 1000, "1000 uV"), + SCALE_1500 (4, 1500, "1500 uV"); + + private int index; + private final int value; + private String label; + + FFTVerticalScale(int index, int value, String label) { + this.index = index; + this.value = value; + this.label = label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum GraphLogLin implements IndexingInterface { + LOG (0, "Log"), + LIN (1, "Linear"); + + private int index; + private String label; + + GraphLogLin(int index, String label) { + this.index = index; + this.label = label; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/FifoChannelBar.pde b/OpenBCI_GUI/FifoChannelBar.pde new file mode 100644 index 000000000..f9debcc13 --- /dev/null +++ b/OpenBCI_GUI/FifoChannelBar.pde @@ -0,0 +1,302 @@ +//Reusable class to display data over time with built-in FIFO buffer +//Works with data that is calculated by the GUI each frame, then fed to this class. +class FifoChannelBar { + private int x, y, w, h; + private int xOffset; + + private GPlot plot; + private float barXAxisLabelPadding = 22; + private float barYAxisLabelPadding = 30; + private boolean showXAxis = true; + + private int layerCount = 1; + private CircularFIFODataBuffer fifoBuffer; + private CircularFIFODataBuffer fifoTimeBuffer; + private float lastTimerValue; + private int samplingRate; + private int totalBufferSeconds; + private int totalBufferPoints; + private int windowSeconds; + private int windowPointsCount; + private float timeBetweenPoints; + private boolean isOpenness = false; + + private TextBox valueTextBox; + + private GPlotAutoscaler gplotAutoscaler = new GPlotAutoscaler(); + + FifoChannelBar(PApplet _parentApplet, String yAxisLabel, int xLimit, float yLimit, int _x, int _y, int _w, int _h, color lineColor, int _layerCount, int _samplingRate, int _totalBufferSeconds) { + x = _x; + y = _y; + w = _w; + h = _h; + totalBufferSeconds = _totalBufferSeconds; + totalBufferPoints = totalBufferSeconds * _samplingRate; + windowSeconds = xLimit; + samplingRate = _samplingRate; + windowPointsCount = windowSeconds * samplingRate; + timeBetweenPoints = windowSeconds / windowPointsCount; + layerCount = _layerCount; + + if (yAxisLabel.equals("Openness")) { + isOpenness = true; + } + + plot = new GPlot(_parentApplet); + plot.setPos(x + 36 + 4 + xOffset, y); + plot.setDim(w - 36 - 4 - xOffset, h); + plot.setMar(0f, 0f, 0f, 0f); + plot.setLineColor((int)channelColors[(NUM_ACCEL_DIMS)%8]); + plot.setXLim(-windowSeconds,0); //horizontal scale + plot.setYLim(0, yLimit); //vertical scale + plot.setPointColor(0); + plot.getXAxis().setAxisLabelText("Time (s)"); + plot.getYAxis().setAxisLabelText(yAxisLabel); + plot.setAllFontProperties("Arial", 0, 14); + plot.getXAxis().getAxisLabel().setOffset(barXAxisLabelPadding); + plot.getYAxis().getAxisLabel().setOffset(barYAxisLabelPadding); + plot.getXAxis().setFontColor(OPENBCI_DARKBLUE); + plot.getXAxis().setLineColor(OPENBCI_DARKBLUE); + plot.getXAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); + plot.getYAxis().setFontColor(OPENBCI_DARKBLUE); + plot.getYAxis().setLineColor(OPENBCI_DARKBLUE); + plot.getYAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); + + adjustTimeAxis(windowSeconds); + + initArrays(); + + initLayers(); + + valueTextBox = new TextBox("t", x + 36 + 4 + (w - 36 - 4) - 2, y + h); + valueTextBox.textColor = OPENBCI_DARKBLUE; + valueTextBox.alignH = RIGHT; + valueTextBox.drawBackground = true; + valueTextBox.backgroundColor = color(255,255,255,125); + valueTextBox.string = "0"; + valueTextBox.setVisible(false); + } + + FifoChannelBar(PApplet _parentApplet, String yAxisLabel, int xLimit, int _x, int _y, int _w, int _h, color lineColor, int layerCount, int _totalBufferSeconds) { + this(_parentApplet, yAxisLabel, xLimit, 1, _x, _y, _w, _h, lineColor, layerCount, 200, _totalBufferSeconds); + } + + FifoChannelBar(PApplet _parentApplet, String yAxisLabel, int xLimit, float yLimit, int _x, int _y, int _w, int _h, color lineColor, int _totalBufferSeconds) { + this(_parentApplet, yAxisLabel, xLimit, yLimit, _x, _y, _w, _h, lineColor, 1, 200, _totalBufferSeconds); + } + + private void initArrays() { + fifoBuffer = new CircularFIFODataBuffer(layerCount, totalBufferPoints); + fifoTimeBuffer = new CircularFIFODataBuffer(layerCount, totalBufferPoints); + } + + public void initLayers() { + for (int i = 0; i < layerCount; i++) { + plot.addLayer("layer" + (i + 1), new GPointsArray(windowPointsCount - 1)); + plot.getLayer("layer" + (i + 1)).setLineColor((int)channelColors[(i + 4) % 8]); + } + } + + public void update(double value) { + update((float)value); + } + + public void update(int value) { + update((float)value); + } + + public void update(float value) { + resetTimer(); + addLastToFifo(0, value, lastTimerValue); + GPointsArray[] pointsArrays = updateGPlot(); + gplotAutoscaler.update(plot, pointsArrays); + } + + public void updateUsingPrecision(float value) { + resetTimer(); + addLastToFifo(0, value, lastTimerValue); + GPointsArray[] pointsArrays = updateGPlot(); + gplotAutoscaler.updatePrecise(plot, pointsArrays); + } + + public void update(float[] values) { + resetTimer(); + for (int i = 0; i < values.length; i++) { + addLastToFifo(i, values[i], lastTimerValue); + } + GPointsArray[] pointsArrays = updateGPlot(); + gplotAutoscaler.update(plot, pointsArrays); + } + + public void updateFifo(float[] values) { + resetTimer(); + for (int i = 0; i < values.length; i++) { + addLastToFifo(i, values[i], lastTimerValue); + } + } + + private void addLastToFifo(int layer, float value, float time) { + fifoBuffer.add(layer, value); + fifoTimeBuffer.add(layer, time); + } + + public void clear() { + initArrays(); + resetTimer(); + GPointsArray[] pointsArrays = updateGPlot(); + gplotAutoscaler.update(plot, pointsArrays); + } + + public void resetTimer() { + lastTimerValue = (float)millis() / 1000f; + } + + public void draw() { + plot.beginDraw(); + plot.drawBox(); + plot.drawGridLines(GPlot.BOTH); + plot.drawLines(); //Draw a Line graph! + plot.drawYAxis(); + if (showXAxis) { + plot.drawXAxis(); + plot.getXAxis().draw(); + } + plot.endDraw(); + + valueTextBox.draw(); + } + + public void screenResized(int _x, int _y, int _w, int _h) { + x = _x; + y = _y; + w = _w; + h = _h; + //reposition & resize the plot + plot.setPos(x + 36 + 4 + xOffset, y); + plot.setDim(w - 36 - 4 - xOffset, h); + valueTextBox.x = x + 36 + 4 + (w - 36 - 4) - 2; + valueTextBox.y = y + h; + } + + //Used to update the Points within the graph using the FIFO buffer in this class + private GPointsArray[] updateGPlot() { + float[][] timeData = fifoTimeBuffer.getBuffer(windowPointsCount); + float[][] data = fifoBuffer.getBuffer(windowPointsCount); + + int stopId = 0; + for (stopId = windowPointsCount - 1; stopId > 0; stopId--) { + if (lastTimerValue - timeData[0][stopId] > windowSeconds) { + break; + } + } + + GPointsArray[] pointsArrays = new GPointsArray[data.length]; + for (int layer = 0; layer < data.length; layer++) { + pointsArrays[layer] = updateGPlotPoints(layer, data[layer], timeData[layer]); + } + return pointsArrays; + } + + private GPointsArray updateGPlotPoints(int layer, float[] data, float[] timeData) { + int stopId = 0; + for (stopId = windowPointsCount - 1; stopId > 0; stopId--) { + if (lastTimerValue - timeData[stopId] > windowSeconds) { + break; + } + } + + int size = windowPointsCount - 1 - stopId; + + GPointsArray pointsArray = new GPointsArray(size); + for (int i = 0; i < size; i++) { + int dataIndex = i + stopId; + float _x = timeData[dataIndex] - lastTimerValue; + float _y = data[dataIndex]; + pointsArray.set(i, _x, _y, ""); + } + plot.setPoints(pointsArray, "layer" + (layer + 1)); + return pointsArray; + } + + //Update GPlot with external data and bypass the FIFO buffer in this class + public void updateGPlotPointsExternal(float[][] data) { + gplotAutoscaler.resetMinMax(); + GPointsArray[] pointsArrays = new GPointsArray[data.length]; + for (int layer = 0; layer < data.length; layer++) { + pointsArrays[layer] = new GPointsArray(data[layer].length); + for (int i = 0; i < data[layer].length; i++) { + float _x = -(float)windowSeconds + (float)(i * timeBetweenPoints); + float _y = data[layer][i]; + GPoint tempPoint = new GPoint(_x, _y); + pointsArrays[layer].set(i, tempPoint); + } + plot.setPoints(pointsArrays[layer], "layer" + (layer + 1)); + } + gplotAutoscaler.update(plot, pointsArrays); + } + + + public void adjustTimeAxis(int _newTimeSize) { + windowSeconds = _newTimeSize; + windowPointsCount = windowSeconds * samplingRate; + timeBetweenPoints = (float)windowSeconds / (float)windowPointsCount; + plot.setXLim(-_newTimeSize,0); + //Set the number of axis divisions based on the new time size + if (_newTimeSize > 1) { + plot.getXAxis().setNTicks(_newTimeSize); + }else{ + plot.getXAxis().setNTicks(10); + } + } + + public void adjustYAxis(float min, float max) { + plot.setYLim(min, max); + } + + public void setEnabled(boolean value) { + gplotAutoscaler.setEnabled(value); + } + + public void setAutoscaleSpacing(float spacing) { + gplotAutoscaler.setSpacing(spacing); + } + + public void setPlotPositionAndOuterDimensions(boolean channelSelectIsVisible) { + int _y = channelSelectIsVisible ? y + 22 : y; + int _h = channelSelectIsVisible ? h - 22 : h; + //reposition & resize the plot + plot.setPos(x + 36 + 4 + xOffset, _y); + plot.setDim(w - 36 - 4 - xOffset, _h); + } + + public void setYAxisLabel(String label) { + plot.getYAxis().setAxisLabelText(label); + } + + public void setXAxisLabel(String label) { + plot.getXAxis().setAxisLabelText(label); + } + + public void setNumYAxisTicks(int numTicks) { + plot.getYAxis().setNTicks(numTicks); + } + + public void setShowXAxis(boolean show) { + showXAxis = show; + } + + public void setShowValueTextBox(boolean show) { + valueTextBox.setVisible(show); + } + + public void setValueTextBoxString(String value) { + valueTextBox.string = value; + } + + public void setSamplingRate(int _samplingRate) { + samplingRate = _samplingRate; + totalBufferPoints = totalBufferSeconds * samplingRate; + windowPointsCount = windowSeconds * samplingRate; + timeBetweenPoints = (float)windowSeconds / (float)windowPointsCount; + } +}; \ No newline at end of file diff --git a/OpenBCI_GUI/FileChooser.pde b/OpenBCI_GUI/FileChooser.pde new file mode 100644 index 000000000..7569e053b --- /dev/null +++ b/OpenBCI_GUI/FileChooser.pde @@ -0,0 +1,116 @@ +import javax.swing.JFileChooser; +import java.awt.FileDialog; + +public enum FileChooserMode { + SAVE, LOAD; +} + +public enum FileChooserType { + NATIVE, JFILECHOOSER; +} + +class FileChooser implements Runnable { + FileChooserMode mode; + FileChooserType type; + String prompt; + File defaultSelection; + String callbackMethod; + File selectedFile; + + FileChooser(FileChooserMode _mode, String _callbackMethod) { + this(FileChooserType.NATIVE, _mode, _callbackMethod, null, null); + } + + FileChooser(FileChooserMode _mode, String _callbackMethod, File _defaultSelection) { + this(FileChooserType.NATIVE, _mode, _callbackMethod, _defaultSelection, null); + } + + FileChooser(FileChooserMode _mode, String _callbackMethod, File _defaultSelection, String _prompt) { + this(FileChooserType.NATIVE, _mode, _callbackMethod, _defaultSelection, _prompt); + } + + FileChooser(FileChooserType _type, FileChooserMode _mode, String _callbackMethod, File _defaultSelection, String _prompt) { + mode = _mode; + type = _type; + callbackMethod = _callbackMethod; + defaultSelection = _defaultSelection; + if (_prompt == null) { + prompt = mode == FileChooserMode.SAVE ? "Save file" : "Load file"; + } else { + prompt = _prompt; + } + Thread t = new Thread(this); + t.start(); + } + + @Override + public void run() { + switch (type) { + case JFILECHOOSER: + createJFileChooser(); + break; + case NATIVE: + createNativeFileChooser(); + break; + } + } + + private void createJFileChooser() { + JFileChooser fileChooser = new JFileChooser(); + if (mode == FileChooserMode.SAVE) { + fileChooser.showSaveDialog(null); + } else { + fileChooser.showOpenDialog(null); + } + PApplet.selectCallback(selectedFile, callbackMethod, ourApplet); + } + + private void createNativeFileChooser() { + int nativeDialogMode = mode == FileChooserMode.SAVE ? FileDialog.SAVE : FileDialog.LOAD; + + // Create a dummy frame to parent the file dialog. The main GUI window is JOGL's GLWindow, which is not a Frame. + Frame ourAppletDummyFrame = new Frame(); + ourAppletDummyFrame.setUndecorated(true); + ourAppletDummyFrame.setOpacity(0.0f); + ourAppletDummyFrame.setVisible(true); + ourAppletDummyFrame.toFront(); + + // Create the file dialog + FileDialog dialog = new FileDialog(ourAppletDummyFrame, prompt, nativeDialogMode); + if (defaultSelection != null) { + + println("FileChooser: Setting directory to " + defaultSelection.getAbsolutePath()); + if (defaultSelection.isDirectory()) { + dialog.setDirectory(defaultSelection.getAbsolutePath()); + dialog.setFile(""); + } else { + dialog.setDirectory(defaultSelection.getParent()); + dialog.setFile(defaultSelection.getName()); + } + } + + // Show the dialog. This method does not return until the dialog is closed by the user. + dialog.setVisible(true); + + // Dispose the dummy frame + ourAppletDummyFrame.setVisible(false); + ourAppletDummyFrame.dispose(); + + // Get the selected file + String directory = dialog.getDirectory(); + String filename = dialog.getFile(); + if (filename != null) { + selectedFile = new File(directory, filename); + } + + // If the user cancelled the dialog, the directory and filename will be null + if (directory == null && filename == null) { + println("FileChooser: No file selected"); + return; + } + + println("FileChooser: User selected " + selectedFile.getAbsolutePath()); + + PApplet.selectCallback(selectedFile, callbackMethod, ourApplet); + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/FileDurationEnum.pde b/OpenBCI_GUI/FileDurationEnum.pde new file mode 100644 index 000000000..acfd6dfcc --- /dev/null +++ b/OpenBCI_GUI/FileDurationEnum.pde @@ -0,0 +1,32 @@ +public enum OdfFileDuration implements IndexingInterface { + FIVE_MINUTES (0, 5, "5 Minutes"), + FIFTEEN_MINUTES (1, 15, "15 Minutes"), + THIRTY_MINUTES (2, 30, "30 Minutes"), + SIXTY_MINUTES (3, 60, "60 Minutes"), + ONE_HUNDRED_TWENTY_MINUTES (4, 120, "120 Minutes"), + NO_LIMIT (5, -1, "No Limit"); + + private int index; + private int duration; + private String label; + + OdfFileDuration(int _index, int _duration, String _label) { + this.index = _index; + this.duration = _duration; + this.label = _label; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } + + public int getValue() { + return duration; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/FilterSettings.pde b/OpenBCI_GUI/FilterSettings.pde index c3b5e413e..a627e325e 100644 --- a/OpenBCI_GUI/FilterSettings.pde +++ b/OpenBCI_GUI/FilterSettings.pde @@ -33,7 +33,7 @@ public class FilterSettingsValues { public FilterSettingsValues(int channelCount) { brainFlowFilter = BFFilter.BANDPASS; - filterChannelSelect = FilterChannelSelect.ALL_CHANNELS; + filterChannelSelect = FilterChannelSelect.CUSTOM_CHANNELS; globalEnvFilter = GlobalEnvironmentalFilter.FIFTY_AND_SIXTY; //Set Master Values for all channels for BandStop Filter @@ -92,46 +92,19 @@ class FilterSettings { defaultValues = new FilterSettingsValues(channelCount); } - public boolean loadSettingsValues(String filename) { - try { - File file = new File(filename); - StringBuilder fileContents = new StringBuilder((int)file.length()); - Scanner scanner = new Scanner(file); - while(scanner.hasNextLine()) { - fileContents.append(scanner.nextLine() + System.lineSeparator()); - } - Gson gson = new Gson(); - values = gson.fromJson(fileContents.toString(), FilterSettingsValues.class); - return true; - } catch (IOException e) { - e.printStackTrace(); - File f = new File(filename); - if (f.exists()) { - if (f.delete()) { - println("FilterSettings: Could not load filter settings from disk. Deleting this file..."); - } else { - println("FilterSettings: Error deleting old/broken filter settings file! Please make sure the GUI has proper read/write permissions."); - } - } - return false; - } - } - public String getJson() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); return gson.toJson(values); } - public boolean saveToFile(String filename) { - String json = getJson(); + public void loadSettingsFromJson(String json) { try { - FileWriter writer = new FileWriter(filename); - writer.write(json); - writer.close(); - return true; - } catch (IOException e) { - e.printStackTrace(); - return false; + Gson gson = new Gson(); + values = gson.fromJson(json, FilterSettingsValues.class); + filterSettingsWereLoadedFromFile = true; + } catch (Exception e) { + e.printStackTrace(); + println("FilterSettings: Could not load filter settings from JSON string."); } } @@ -143,54 +116,4 @@ class FilterSettings { public int getChannelCount() { return channelCount; } - - //Avoid error with popup being in another thread. - public void storeSettings() { - StringBuilder settingsFilename = new StringBuilder(directoryManager.getSettingsPath()); - settingsFilename.append("FilterSettings"); - settingsFilename.append("_"); - settingsFilename.append(getChannelCount()); - settingsFilename.append("Channels.json"); - String filename = settingsFilename.toString(); - File fileToSave = new File(filename); - selectOutput("Save filter settings to file", "storeFilterSettings", fileToSave); - } - //Avoid error with popup being in another thread. - public void loadSettings() { - StringBuilder settingsFilename = new StringBuilder(directoryManager.getSettingsPath()); - settingsFilename.append("FilterSettings"); - settingsFilename.append("_"); - settingsFilename.append(getChannelCount()); - settingsFilename.append("Channels.json"); - String filename = settingsFilename.toString(); - File fileToLoad = new File(filename); - selectInput("Select settings file to load", "loadFilterSettings", fileToLoad); - } -} - -//Used by button in the Filter UI. Must be global and public. -public void loadFilterSettings(File selection) { - if (selection == null) { - output("Filters Settings file not selected."); - } else { - if (filterSettings.loadSettingsValues(selection.getAbsolutePath())) { - outputSuccess("Filter Settings Loaded!"); - filterSettingsWereLoadedFromFile = true; - } else { - outputError("Failed to load Filter Settings. The old/broken file has been deleted."); - } - } -} - -//Used by button in the Filter UI. Must be global and public. -public void storeFilterSettings(File selection) { - if (selection == null) { - output("Filter Settings file not selected."); - } else { - if (filterSettings.saveToFile(selection.getAbsolutePath())) { - outputSuccess("Filter Settings Saved!"); - } else { - outputError("Failed to save Filter Settings."); - } - } } \ No newline at end of file diff --git a/OpenBCI_GUI/FilterUI.pde b/OpenBCI_GUI/FilterUI.pde index 018b1a8f5..2e1c86b26 100644 --- a/OpenBCI_GUI/FilterUI.pde +++ b/OpenBCI_GUI/FilterUI.pde @@ -25,12 +25,9 @@ class FilterUIPopup extends PApplet implements Runnable { private final int HALF_OBJ_WIDTH = HEADER_OBJ_WIDTH/2; private final int NUM_HEADER_OBJECTS = 4; private final int NUM_COLUMNS = 5; - private final int NUM_FOOTER_OBJECTS = 3; private int[] headerObjX = new int[NUM_HEADER_OBJECTS]; private final int HEADER_OBJ_Y = SM_SPACER; private int[] columnObjX = new int[NUM_COLUMNS]; - private int footerObjY = 0; - private int[] footerObjX = new int[NUM_FOOTER_OBJECTS]; private String message = "Sample text string"; private String headerMessage = "Filters"; @@ -50,9 +47,6 @@ class FilterUIPopup extends PApplet implements Runnable { private ScrollableList bfGlobalFilterDropdown; private ScrollableList bfEnvironmentalNoiseDropdown; - private Button saveButton; - private Button loadButton; - private Button defaultButton; private Button masterOnOffButton; private Textfield masterFirstColumnTextfield; @@ -99,6 +93,7 @@ class FilterUIPopup extends PApplet implements Runnable { public FilterUIPopup() { super(); filterUIPopupIsOpen = true; + output("Filter UI: Filter UI opened."); Thread t = new Thread(this); t.start(); @@ -115,9 +110,9 @@ class FilterUIPopup extends PApplet implements Runnable { filterSettingsWereModifiedFadeCounter = new int[numChans]; fixedWidth = (HEADER_OBJ_WIDTH * 6) + SM_SPACER*5; - maxHeight = HEADER_HEIGHT*3 + SM_SPACER*(numChans+5) + uiObjectHeight*(numChans+2) + EXPANDER_HEIGHT; + maxHeight = HEADER_HEIGHT*3 + SM_SPACER*(numChans+5) + uiObjectHeight*(numChans+1) + EXPANDER_HEIGHT; shortHeight = HEADER_HEIGHT*2 + SM_SPACER*(1+5) + uiObjectHeight*(1+2) + LG_SPACER + EXPANDER_HEIGHT; - variableHeight = shortHeight; + variableHeight = maxHeight; //Include spacer on the outside left and right of all columns. Used to draw visual feedback widthOfAllChannelColumns = HEADER_OBJ_WIDTH*NUM_COLUMNS + LG_SPACER*(NUM_COLUMNS-1) + LG_SPACER*2; } @@ -134,12 +129,8 @@ class FilterUIPopup extends PApplet implements Runnable { @Override void setup() { - if (!EXPANDER_IS_USED) { - filterSettings.values.filterChannelSelect = FilterChannelSelect.CUSTOM_CHANNELS; - } - surface.setTitle(headerMessage); - surface.setAlwaysOnTop(false); + surface.setAlwaysOnTop(true); surface.setResizable(false); Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); @@ -158,14 +149,15 @@ class FilterUIPopup extends PApplet implements Runnable { // Important: Reset the CP5 graphics reference points X,Y,W,H at the beginning of the next draw after screen has been resized. // Otherwise, the numbers are wrong. - if (needToResetCp5Graphics) { + if (needToResetCp5Graphics && EXPANDER_IS_USED) { + cp5.setGraphics(this, 0, 0); variableHeight = height; arrangeAllObjectsXY(); - cp5.setGraphics(this, 0, 0); } - if (needToResizePopup) { + if (needToResizePopup && EXPANDER_IS_USED) { // Resize the window. Reset the CP5 graphics at the beginning of the next draw(). + // Currently, we cannot resize a popup window with Processing using JAVA2D renderer. This issue would need to be fixed upstream. surface.setSize(fixedWidth, newVariableHeight); needToResizePopup = false; needToResetCp5Graphics = true; @@ -251,9 +243,8 @@ class FilterUIPopup extends PApplet implements Runnable { // No other Classes have access to the private Cp5 objects in this class. try { cp5.draw(); - } catch (Exception e) { - //println(e.getMessage()); - println("Caught ConcurrentModificationExcpetion in Filter UI..."); + } catch (ConcurrentModificationException e) { + println("Filter UI: Error drawing cp5" + e.getMessage()); } } @@ -276,6 +267,14 @@ class FilterUIPopup extends PApplet implements Runnable { filterUIPopupIsOpen = false; } + // Dispose of the popup window externally + public void exitPopup() { + output("Filter UI: Closing Filter UI."); + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.dispose(); + filterUIPopupIsOpen = false; + } + private void checkIfSessionWasClosed() { if (systemMode == SYSTEMMODE_PREINIT) { noLoop(); @@ -290,7 +289,9 @@ class FilterUIPopup extends PApplet implements Runnable { try { updateHeaderCp5Objects(); updateChannelCp5Objects(); - setUItoChannelMode(filterSettings.values.filterChannelSelect); + if (EXPANDER_IS_USED) { + setUItoChannelMode(filterSettings.values.filterChannelSelect); + } } catch (Exception e) { println(e.getMessage()); outputError("Filter Settings: Unable to apply settings. Please save Filter Settings to a new file."); @@ -300,11 +301,7 @@ class FilterUIPopup extends PApplet implements Runnable { } private void createAllCp5Objects() { - calculateXYForHeaderColumnsAndFooter(); - - createFilterSettingsSaveButton("saveFilterSettingsButton", "Save", footerObjX[0], footerObjY, HEADER_OBJ_WIDTH, uiObjectHeight); - createFilterSettingsLoadButton("loadFilterSettingsButton", "Load", footerObjX[1], footerObjY, HEADER_OBJ_WIDTH, uiObjectHeight); - createFilterSettingsDefaultButton("defaultFilterSettingsButton", "Reset", footerObjX[2], footerObjY, HEADER_OBJ_WIDTH, uiObjectHeight); + calculateXYForHeaderColumns(); createOnOffButtons(); createTextfields(); @@ -327,7 +324,7 @@ class FilterUIPopup extends PApplet implements Runnable { bfEnvironmentalNoiseDropdown.getCaptionLabel().setText(filterSettings.values.globalEnvFilter.getString()); } - private void calculateXYForHeaderColumnsAndFooter() { + private void calculateXYForHeaderColumns() { middle = width / 2; headerObjX[0] = middle - SM_SPACER*2 - HEADER_OBJ_WIDTH*2; @@ -341,17 +338,12 @@ class FilterUIPopup extends PApplet implements Runnable { columnObjX[3] = middle + HALF_OBJ_WIDTH + LG_SPACER; columnObjX[4] = middle + HALF_OBJ_WIDTH + LG_SPACER*2 + HEADER_OBJ_WIDTH; - footerObjX[0] = middle - HALF_OBJ_WIDTH - LG_SPACER - HEADER_OBJ_WIDTH; - footerObjX[1] = middle - HALF_OBJ_WIDTH; - footerObjX[2] = middle + HALF_OBJ_WIDTH + LG_SPACER; - setFooterObjYPosition(filterSettings.values.filterChannelSelect); - expanderLineOneEnd = middle - expanderBreakMiddle/2; expanderLineTwoStart = middle + expanderBreakMiddle/2; } public void arrangeAllObjectsXY() { - calculateXYForHeaderColumnsAndFooter(); + calculateXYForHeaderColumns(); bfGlobalFilterDropdown.setPosition(headerObjX[1], HEADER_OBJ_Y); bfEnvironmentalNoiseDropdown.setPosition(headerObjX[3], HEADER_OBJ_Y); @@ -393,10 +385,6 @@ class FilterUIPopup extends PApplet implements Runnable { filterTypeDropdowns[chan].setPosition(columnObjX[3], rowY); filterOrderDropdowns[chan].setPosition(filterOrderDropdownNewX, rowY); } - - saveButton.setPosition(footerObjX[0], footerObjY); - loadButton.setPosition(footerObjX[1], footerObjY); - defaultButton.setPosition(footerObjX[2], footerObjY); } // Master method to update objects from the FilterSettings Class @@ -913,37 +901,6 @@ class FilterUIPopup extends PApplet implements Runnable { cp5ElementsToCheck.add(masterFilterOrderDropdown); } - private void createFilterSettingsSaveButton(String name, String text, int _x, int _y, int _w, int _h) { - saveButton = createButton(cp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - saveButton.setBorderColor(OBJECT_BORDER_GREY); - saveButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - filterSettings.storeSettings(); - } - }); - } - - private void createFilterSettingsLoadButton(String name, String text, int _x, int _y, int _w, int _h) { - loadButton = createButton(cp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - loadButton.setBorderColor(OBJECT_BORDER_GREY); - loadButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - filterSettings.loadSettings(); - } - }); - } - - private void createFilterSettingsDefaultButton(String name, String text, int _x, int _y, int _w, int _h) { - defaultButton = createButton(cp5, name, text, _x, _y, _w, _h, h5, 12, colorNotPressed, OPENBCI_DARKBLUE); - defaultButton.setBorderColor(OBJECT_BORDER_GREY); - defaultButton.onClick(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - filterSettings.revertAllChannelsToDefaultValues(); - filterSettingsWereLoadedFromFile = true; - } - }); - } - private void createMasterOnOffButton(String name, final String text, int _x, int _y, int _w, int _h) { masterOnOffButton = createButton(cp5, name, text, _x, _y, _w, _h, 0, h2, 16, SUBNAV_LIGHTBLUE, WHITE, BUTTON_HOVER, BUTTON_PRESSED, (Integer) null, -2); masterOnOffButton.setCircularButton(true); @@ -985,7 +942,7 @@ class FilterUIPopup extends PApplet implements Runnable { private void setUItoChannelMode(FilterChannelSelect myEnum) { int numChans = filterSettings.getChannelCount(); boolean showAllChannels = myEnum == FilterChannelSelect.CUSTOM_CHANNELS; - newVariableHeight = showAllChannels ? maxHeight : shortHeight; + newVariableHeight = showAllChannels ? maxHeight : shortHeight; for (int chan = 0; chan < numChans; chan++) { onOffButtons[chan].setVisible(showAllChannels); @@ -995,25 +952,9 @@ class FilterUIPopup extends PApplet implements Runnable { filterOrderDropdowns[chan].setVisible(showAllChannels); } - setFooterObjYPosition(myEnum); - saveButton.setPosition(footerObjX[0], footerObjY); - loadButton.setPosition(footerObjX[1], footerObjY); - needToResizePopup = true; } - private void setFooterObjYPosition(FilterChannelSelect myEnum) { - boolean showAllChannels = myEnum == FilterChannelSelect.CUSTOM_CHANNELS; - int numChans = filterSettings.getChannelCount(); - int footerMaxHeightY = HEADER_HEIGHT*2 + SM_SPACER*(numChans+4) + uiObjectHeight*(numChans+1) + LG_SPACER*2 + EXPANDER_HEIGHT; - int footerMinHeightY = HEADER_HEIGHT*2 + SM_SPACER*4 + uiObjectHeight + LG_SPACER + EXPANDER_HEIGHT; - footerObjY = showAllChannels ? footerMaxHeightY : footerMinHeightY; - - if (!EXPANDER_IS_USED) { - footerObjY -= EXPANDER_HEIGHT + SM_SPACER; - } - } - private void filterSettingWasModifiedOnChannel(int chan) { filterSettingsWereModified[chan] = true; filterSettingsWereModifiedFadeCounter[chan] = millis(); diff --git a/OpenBCI_GUI/FocusEnums.pde b/OpenBCI_GUI/FocusEnums.pde index 7fa92c97f..59ce3191c 100644 --- a/OpenBCI_GUI/FocusEnums.pde +++ b/OpenBCI_GUI/FocusEnums.pde @@ -14,7 +14,6 @@ public enum FocusXLim implements IndexingInterface private int index; private int value; private String label; - private static FocusXLim[] vals = values(); FocusXLim(int _index, int _value, String _label) { this.index = _index; @@ -35,14 +34,6 @@ public enum FocusXLim implements IndexingInterface public int getIndex() { return index; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum FocusMetric implements IndexingInterface @@ -54,7 +45,6 @@ public enum FocusMetric implements IndexingInterface private String label; private BrainFlowMetrics metric; private String idealState; - private static FocusMetric[] vals = values(); FocusMetric(int _index, String _label, BrainFlowMetrics _metric, String _idealState) { this.index = _index; @@ -80,14 +70,6 @@ public enum FocusMetric implements IndexingInterface public String getIdealStateString() { return idealState; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum FocusClassifier implements IndexingInterface @@ -99,8 +81,6 @@ public enum FocusClassifier implements IndexingInterface private String label; private BrainFlowClassifiers classifier; - private static FocusClassifier[] vals = values(); - FocusClassifier(int _index, String _label, BrainFlowClassifiers _classifier) { this.index = _index; this.label = _label; @@ -120,14 +100,6 @@ public enum FocusClassifier implements IndexingInterface public BrainFlowClassifiers getClassifier() { return classifier; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } public enum FocusThreshold implements IndexingInterface @@ -142,8 +114,6 @@ public enum FocusThreshold implements IndexingInterface private float value; private String label; - private static FocusThreshold[] vals = values(); - FocusThreshold(int _index, float _value, String _label) { this.index = _index; this.value = _value; @@ -163,12 +133,4 @@ public enum FocusThreshold implements IndexingInterface public int getIndex() { return index; } - - public static List getEnumStringsAsList() { - List enumStrings = new ArrayList(); - for (IndexingInterface val : vals) { - enumStrings.add(val.getString()); - } - return enumStrings; - } } \ No newline at end of file diff --git a/OpenBCI_GUI/GPlotAutoscaler.pde b/OpenBCI_GUI/GPlotAutoscaler.pde new file mode 100644 index 000000000..081d91721 --- /dev/null +++ b/OpenBCI_GUI/GPlotAutoscaler.pde @@ -0,0 +1,177 @@ +class GPlotAutoscaler { + private boolean isEnabled = false; + private boolean useAverage = false; + private float spacing = 0f; //Provides a buffer between the data and the plot's edges + private float minimum = Float.MAX_VALUE; + private float maximum = Float.MIN_VALUE; + private int previousMillis = 0; + private int currentMillis = 0; + private int timerThresholdMillis = 1000; + + public GPlotAutoscaler() { + } + + public GPlotAutoscaler(boolean _isEnabled) { + isEnabled = _isEnabled; + } + + public GPlotAutoscaler(float _spacing) { + spacing = _spacing; + } + + public GPlotAutoscaler(boolean _isEnabled, float _spacing) { + isEnabled = _isEnabled; + spacing = _spacing; + } + + //Used for single layer plots e.g. EEG, EMG, Focus, etc. + public void update(GPlot plot, GPointsArray pointsArray) { + if (!isTimeToAutoscale()) { + return; + } + + resetMinMax(); + for (int i = 0; i < pointsArray.getNPoints(); i++) { + updateMinMax(pointsArray.getY(i)); + } + setYLimits(plot); + previousMillis = currentMillis; + } + + //Used for multilayer plots e.g. Cyton and Ganglion Accelerometer with X, Y, Z axes in the same plot + public void update(GPlot plot, GPointsArray[] pointsArrays) { + if (!isTimeToAutoscale()) { + return; + } + + resetMinMax(); + updateMinMaxMultilayer(pointsArrays); + setYLimits(plot); + previousMillis = currentMillis; + } + + //Used for single layer plots where minMax values are updated when the points are added to the plot + public void update(GPlot plot) { + if (!isTimeToAutoscale()) { + return; + } + + setYLimits(plot); + previousMillis = currentMillis; + } + + public void updatePrecise(GPlot plot) { + if (!isTimeToAutoscale()) { + return; + } + + setYLimitsPrecise(plot); + previousMillis = currentMillis; + } + + public void updatePrecise(GPlot plot, GPointsArray pointsArray) { + if (!isTimeToAutoscale()) { + return; + } + + resetMinMax(); + for (int i = 0; i < pointsArray.getNPoints(); i++) { + updateMinMax(pointsArray.getY(i)); + } + setYLimitsPrecise(plot); + previousMillis = currentMillis; + } + + public void updatePrecise(GPlot plot, GPointsArray[] pointsArrays) { + if (!isTimeToAutoscale()) { + return; + } + + resetMinMax(); + updateMinMaxMultilayer(pointsArrays); + setYLimitsPrecise(plot); + previousMillis = currentMillis; + } + + private void setYLimits(GPlot plot) { + if (minimum == Float.MAX_VALUE || maximum == -Float.MAX_VALUE) { + return; + } + double lowerLimit = Math.floor(minimum) - spacing; + double upperLimit = Math.ceil(maximum) + spacing; + //This is a very expensive method. Here is the bottleneck. + try { + plot.setYLim((float)lowerLimit, (float)upperLimit); + } catch (NumberFormatException e) { + System.out.println("Error in GPlotAutoscaler.update(GPlot plot): " + e); + println("Lower limit: " + lowerLimit + " Upper limit: " + upperLimit); + } + } + + private void setYLimitsPrecise(GPlot plot) { + if (minimum == Float.MAX_VALUE || maximum == -Float.MAX_VALUE) { + return; + } + double lowerLimit = minimum - spacing; + double upperLimit = maximum + spacing; + //This is a very expensive method. Here is the bottleneck. + try { + plot.setYLim((float)lowerLimit, (float)upperLimit); + } catch (NumberFormatException e) { + System.out.println("Error in GPlotAutoscaler.update(GPlot plot): " + e); + println("Lower limit: " + lowerLimit + " Upper limit: " + upperLimit); + } + } + + private void updateMinMaxMultilayer(GPointsArray[] pointsArrays) { + if (pointsArrays == null) { + println("Error in GPlotAutoscaler.minMaxMultilayer(GPointsArray[] pointsArrays): pointsArrays is null"); + return; + } + + float[] vals = new float[pointsArrays.length]; + for (int i = 0; i < pointsArrays[0].getNPoints(); i++) { + for (int j = 0; j < pointsArrays.length; j++) { + vals[j] = pointsArrays[j].getY(i); + } + minimum = min(minimum, min(vals)); + maximum = max(maximum, max(vals)); + } + } + + private boolean isTimeToAutoscale() { + return isEnabled && currentBoard.isStreaming() && timerHasElapsed(); + } + + private boolean timerHasElapsed() { + currentMillis = millis(); + return currentMillis > previousMillis + timerThresholdMillis; + } + + public void setEnabled(boolean value) { + isEnabled = value; + } + + public boolean getEnabled() { + return isEnabled; + } + + public float[] getMinMax() { + float[] minMax = {minimum, maximum}; + return minMax; + } + + public void setSpacing(float value) { + spacing = value; + } + + public void updateMinMax(float value) { + maximum = Math.max(value, maximum); + minimum = Math.min(value, minimum); + } + + public void resetMinMax() { + maximum = Float.MIN_VALUE; + minimum = Float.MAX_VALUE; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/GuiSettings.pde b/OpenBCI_GUI/GuiSettings.pde index db0ef837e..c492433c1 100644 --- a/OpenBCI_GUI/GuiSettings.pde +++ b/OpenBCI_GUI/GuiSettings.pde @@ -35,6 +35,11 @@ public class GuiSettingsValues { public ExpertModeEnum expertMode = ExpertModeEnum.OFF; public boolean showCytonSmoothingPopup = true; public boolean showGanglionUpgradePopup = true; + public boolean showStopStreamHardwareSettingsPopup = true; + public boolean showConfirmExitAppPopup = true; + public boolean autoStartDataStream = false; + public boolean autoStartNetworkStream = false; + public boolean autoLoadSessionSettings = false; public GuiSettingsValues() { } @@ -44,7 +49,6 @@ class GuiSettings { private GuiSettingsValues values; private String filename; - private List valueKeys = Arrays.asList("expertMode", "showCytonSmoothingPopup", "showGanglionUpgradePopup"); GuiSettings(String settingsDirectory) { @@ -130,10 +134,17 @@ class GuiSettings { } private boolean validateJsonKeys(String stringToSearch) { + List valueKeys = new ArrayList(); + Gson valueGson = new Gson(); + Map valueMap = valueGson.fromJson(getJson(), new TypeToken>() {}.getType()); + for (String mapKey : valueMap.keySet()) { + valueKeys.add(mapKey); + } + List foundKeys = new ArrayList(); - Gson gson = new Gson(); - Map map = gson.fromJson(stringToSearch, new TypeToken>() {}.getType()); - for (String mapKey : map.keySet()) { + Gson foundGson = new Gson(); + Map foundMap = foundGson.fromJson(stringToSearch, new TypeToken>() {}.getType()); + for (String mapKey : foundMap.keySet()) { foundKeys.add(mapKey); } @@ -145,10 +156,18 @@ class GuiSettings { return isEqual; } + public void resetAllSettings() { + values = new GuiSettingsValues(); + applySettings(); + } + //Call this method at the end of GUI main Setup in OpenBCI_GUI.pde to make sure everything exists //Has to be in this class to make sure other classes exist public void applySettings() { topNav.configSelector.toggleExpertModeFrontEnd(getExpertModeBoolean()); + topNav.configSelector.toggleAutoStartDataStreamFrontEnd(getAutoStartDataStream()); + topNav.configSelector.toggleAutoStartNetworkStreamFrontEnd(getAutoStartNetworkStream()); + topNav.configSelector.toggleAutoLoadSessionSettingsFrontEnd(getAutoLoadSessionSettings()); } public void setExpertMode(ExpertModeEnum val) { @@ -177,4 +196,49 @@ class GuiSettings { public boolean getShowGanglionUpgradePopup() { return values.showGanglionUpgradePopup; } + + public void setShowStopStreamHardwareSettingsPopup(boolean b) { + values.showStopStreamHardwareSettingsPopup = b; + saveToFile(); + } + + public boolean getShowStopStreamHardwareSettingsPopup() { + return values.showStopStreamHardwareSettingsPopup; + } + + public void setShowConfirmExitAppPopup(boolean b) { + values.showConfirmExitAppPopup = b; + saveToFile(); + } + + public boolean getShowConfirmExitAppPopup() { + return values.showConfirmExitAppPopup; + } + + public void setAutoStartDataStream(boolean b) { + values.autoStartDataStream = b; + saveToFile(); + } + + public boolean getAutoStartDataStream() { + return values.autoStartDataStream; + } + + public void setAutoStartNetworkStream(boolean b) { + values.autoStartNetworkStream = b; + saveToFile(); + } + + public boolean getAutoStartNetworkStream() { + return values.autoStartNetworkStream; + } + + public boolean getAutoLoadSessionSettings() { + return values.autoLoadSessionSettings; + } + + public void setAutoLoadSessionSettings(boolean b) { + values.autoLoadSessionSettings = b; + saveToFile(); + } } \ No newline at end of file diff --git a/OpenBCI_GUI/Interactivity.pde b/OpenBCI_GUI/Interactivity.pde index b30b75109..eba496f1c 100644 --- a/OpenBCI_GUI/Interactivity.pde +++ b/OpenBCI_GUI/Interactivity.pde @@ -52,17 +52,19 @@ void parseKey(char val) { switch (val) { case ' ': // space to start/stop the stream - topNav.stopButtonWasPressed(); + topNav.dataStreamTogglePressed(); return; case ',': drawContainers = !drawContainers; return; case '{': + /* if(colorScheme == COLOR_SCHEME_DEFAULT){ colorScheme = COLOR_SCHEME_ALTERNATIVE_A; } else if(colorScheme == COLOR_SCHEME_ALTERNATIVE_A) { colorScheme = COLOR_SCHEME_DEFAULT; } + */ //topNav.updateNavButtonsBasedOnColorScheme(); output("New Dark color scheme coming soon!"); return; @@ -99,14 +101,14 @@ void parseKey(char val) { ///////////////////// Save User settings lowercase n case 'n': println("Interactivity: Save key pressed!"); - settings.save(settings.getPath("User", eegDataSource, nchan)); - outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory settings."); + sessionSettings.save(sessionSettings.getPath("User", eegDataSource, globalChannelCount)); + outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory sessionSettings."); return; ///////////////////// Load User settings uppercase N case 'N': println("Interactivity: Load key pressed!"); - settings.loadKeyPressed(); + sessionSettings.loadKeyPressed(); return; case '?': @@ -116,16 +118,13 @@ void parseKey(char val) { return; case 'm': - String picfname = "OpenBCI-" + directoryManager.getFileNameDateTime() + ".jpg"; - //println("OpenBCI_GUI: 'm' was pressed...taking screenshot:" + picfname); - saveFrame(directoryManager.getGuiDataPath() + "Screenshots" + System.getProperty("file.separator") + picfname); // take a shot of that! - output("Screenshot captured! Saved to /Documents/OpenBCI_GUI/Screenshots/" + picfname); + takeGUIScreenshot(); return; default: break; } - if (nchan > 4) { + if (globalChannelCount > 4) { switch (val) { case '5': currentBoard.setEXGChannelActive(5-1, false); @@ -156,7 +155,7 @@ void parseKey(char val) { } } - if (nchan > 8) { + if (globalChannelCount > 8) { switch (val) { case 'q': currentBoard.setEXGChannelActive(9-1, false); @@ -214,19 +213,20 @@ void parseKey(char val) { // Fixes #976. These keyboard shortcuts enable synthetic square waves on Ganglion and Cyton if (currentBoard instanceof BoardGanglion || currentBoard instanceof BoardCyton) { if (val == '[' || val == ']') { - println("Expert Mode: '" + val + "' pressed. Sending to Ganglion..."); + println("Expert Mode: '" + val + "' pressed. Sending to board..."); Boolean success = ((Board)currentBoard).sendCommand(str(val)).getKey(); if (success) { - outputSuccess("Expert Mode: Success sending '" + val + "' to Ganglion!"); + outputSuccess("Expert Mode: Success sending '" + val + "' to board!"); } else { - outputWarn("Expert Mode: Error sending '" + val + "' to Ganglion. Try again with data stream stopped."); + outputWarn("Expert Mode: Error sending '" + val + "' to board. Try again with data stream stopped."); } return; } } // Check for software marker keyboard shortcuts - if (w_marker.checkForMarkerKeyPress(val)) { + W_Marker markerWidget = (W_Marker) widgetManager.getWidget("W_Marker"); + if (markerWidget.checkForMarkerKeyPress(val)) { return; } @@ -242,7 +242,7 @@ void mouseDragged() { //calling mouse dragged inly outside of Control Panel if (controlPanel.isOpen == false) { - wm.mouseDragged(); + widgetManager.mouseDragged(); } } } @@ -262,14 +262,11 @@ synchronized void mousePressed() { if (controlPanel.isOpen == false) { //was the stopButton pressed? - wm.mousePressed(); + widgetManager.mousePressed(); } } - //topNav is always clickable - topNav.mousePressed(); - //interacting with control panel if (controlPanel.isOpen) { //close control panel if you click outside... @@ -299,7 +296,7 @@ synchronized void mouseReleased() { if (systemMode >= SYSTEMMODE_POSTINIT) { // GUIWidgets_mouseReleased(); // to replace GUI_Manager version (above) soon... cdr 7/25/16 - wm.mouseReleased(); + widgetManager.mouseReleased(); } } diff --git a/OpenBCI_GUI/InterfaceSerial.pde b/OpenBCI_GUI/InterfaceSerial.pde index 0aaf1e305..0ab707095 100644 --- a/OpenBCI_GUI/InterfaceSerial.pde +++ b/OpenBCI_GUI/InterfaceSerial.pde @@ -25,6 +25,7 @@ // Global Variables & Instances //------------------------------------------------------------------------ +StringBuilder board_message; int _myCounter; int newPacketCounter = 0; boolean no_start_connection = false; @@ -220,9 +221,9 @@ class InterfaceSerial { // //manage the serial port public int openSerialPort(PApplet applet, String comPort, int baud) { - output("Attempting to open Serial/COM port: " + openBCI_portName); + output("Attempting to open Serial/COM port: " + cytonDonglePortName); try { - println("InterfaceSerial: openSerialPort: attempting to open serial port: " + openBCI_portName); + println("InterfaceSerial: openSerialPort: attempting to open serial port: " + cytonDonglePortName); serial_openBCI = new processing.serial.Serial(applet, comPort, baud); //open the com port serial_openBCI.clear(); // clear anything in the com port's buffer portIsOpen = true; diff --git a/OpenBCI_GUI/Layout.pde b/OpenBCI_GUI/Layout.pde new file mode 100644 index 000000000..8dcbc732f --- /dev/null +++ b/OpenBCI_GUI/Layout.pde @@ -0,0 +1,26 @@ + +//The Layout class is an organizational tool. A layout consists of a combination of containers (found in Container.pde). +class Layout { + + Container[] myContainers; + int[] containerInts; + + Layout(int[] _myContainers){ //when creating a new layout, you pass in the integer #s of the containers you want as part of the layout ... so if I pass in the array {5}, my layout is 1 container that takes up the whole GUI body + //constructor stuff + myContainers = new Container[_myContainers.length]; //make the myContainers array equal to the size of the incoming array of ints + containerInts = new int[_myContainers.length]; + for(int i = 0; i < _myContainers.length; i++){ + myContainers[i] = container[_myContainers[i]]; + containerInts[i] = _myContainers[i]; + } + } + + Container getContainer(int _numContainer){ + if(_numContainer < myContainers.length){ + return myContainers[_numContainer]; + } else{ + println("Widget Manager: Tried to return a non-existant container..."); + return myContainers[myContainers.length-1]; + } + } +}; \ No newline at end of file diff --git a/OpenBCI_GUI/MarkerEnums.pde b/OpenBCI_GUI/MarkerEnums.pde new file mode 100644 index 000000000..e80b592b3 --- /dev/null +++ b/OpenBCI_GUI/MarkerEnums.pde @@ -0,0 +1,64 @@ +public enum MarkerWindow implements IndexingInterface +{ + FIVE (0, 5, "5 sec"), + TEN (1, 10, "10 sec"), + TWENTY (2, 20, "20 sec"); + + private int index; + private int value; + private String label; + + MarkerWindow(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum MarkerVertScale implements IndexingInterface +{ + AUTO (0, 0, "Auto"), + TWO (1, 2, "2"), + FOUR (2, 4, "4"), + EIGHT (3, 8, "8"), + TEN (4, 10, "10"), + TWENTY (6, 20, "20"); + + private int index; + private int value; + private String label; + + MarkerVertScale(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} diff --git a/OpenBCI_GUI/NetworkStreamOut.pde b/OpenBCI_GUI/NetworkStreamOut.pde index e8ba7117f..71674c8dc 100644 --- a/OpenBCI_GUI/NetworkStreamOut.pde +++ b/OpenBCI_GUI/NetworkStreamOut.pde @@ -1,167 +1,44 @@ -class NetworkStreamOut extends Thread { - private String protocol; - private int streamNumber; - private String dataType; - private String ip; - private int port; - private String baseOscAddress; - private String streamType; - private String streamName; - private int numLslDataPoints; - private int numExgChannels; - private DecimalFormat threeDecimalPlaces; - private DecimalFormat fourLeadingPlaces; - - public Boolean isStreaming; - private int start; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; + +public abstract class NetworkStreamOut extends Thread { + protected NetworkProtocol protocol; + protected int streamNumber; + protected NetworkDataType dataType; + protected String ip; + protected int port; + protected int numExgChannels; + protected DecimalFormat threeDecimalPlaces; + protected DecimalFormat fourLeadingPlaces; + protected final int NUM_BAND_POWERS = 5; // DELTA, THETA, ALPHA, BETA, GAMMA + protected final int NUM_FFT_BINS_TO_SEND = 125; + + protected Boolean isStreaming; + private int startIndex; private double[][] previousFrameData; private int samplesSent = 0; private int sampleRateClock = 0; private int sampleRateClockInterval = 10000; private boolean debugSamplingRate = false; + private boolean debugAuxSamplingRate = false; + + protected NetworkingDataAccumulator dataAccumulator = dataProcessing.networkingDataAccumulator; - // OSC Objects - private OscP5 osc; - private NetAddress oscNetAddress; - private OscMessage msg; - // UDP Objects - private UDP udp; - // LSL objects - private LSL.StreamInfo info_data; - private LSL.StreamOutlet outlet_data; - - // Serial objects %%%%% - private processing.serial.Serial serial_networking; - private String portName; - private int baudRate; - private String serialMessage = ""; - - private PApplet pApplet; - - // OSC Stream - NetworkStreamOut(String dataType, String ip, int port, String baseAddress, int _streamNumber) { - this.protocol = "OSC"; - this.streamNumber = _streamNumber; - this.dataType = dataType; - this.ip = ip; - this.port = port; - this.baseOscAddress = baseAddress; - this.isStreaming = false; - updateNumChan(); - try { - closeNetwork(); - } catch (Exception e) { - outputError("Error closing network while creating OSC Stream: " + e); - } - } - - // UDP Stream - NetworkStreamOut(String dataType, String ip, int port, int _streamNumber) { - this.protocol = "UDP"; - this.streamNumber = _streamNumber; - this.dataType = dataType; - this.ip = ip; - this.port = port; - this.isStreaming = false; - updateNumChan(); - - // Force decimal formatting for all Locales - Locale currentLocale = Locale.getDefault(); - DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(currentLocale); - otherSymbols.setDecimalSeparator('.'); - otherSymbols.setGroupingSeparator(','); - threeDecimalPlaces = new DecimalFormat("0.000", otherSymbols); - fourLeadingPlaces = new DecimalFormat("####", otherSymbols); - - try { - closeNetwork(); - } catch (Exception e) { - outputError("Error closing network while creating UDP Stream: " + e); - } - } - - // LSL Stream - NetworkStreamOut(String dataType, String streamName, String streamType, int numLslDataPoints, int _streamNumber) { - this.protocol = "LSL"; - this.streamNumber = _streamNumber; + NetworkStreamOut(NetworkDataType dataType) { this.dataType = dataType; - this.streamName = streamName; - this.streamType = streamType; - this.numLslDataPoints = numLslDataPoints; this.isStreaming = false; - updateNumChan(); - try { - closeNetwork(); - } catch (Exception e) { - outputError("Error closing network while creating LSL Stream: " + e); - } - } - - // Serial Stream - NetworkStreamOut(String dataType, String portName, int baudRate, PApplet _this) { - this.protocol = "Serial"; - this.streamNumber = 0; - this.dataType = dataType; - this.portName = portName; - this.baudRate = baudRate; - this.isStreaming = false; - this.pApplet = _this; - updateNumChan(); - - // Force decimal formatting for all Locales - Locale currentLocale = Locale.getDefault(); - DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(currentLocale); - otherSymbols.setDecimalSeparator('.'); - otherSymbols.setGroupingSeparator(','); - threeDecimalPlaces = new DecimalFormat("0.000", otherSymbols); - fourLeadingPlaces = new DecimalFormat("####", otherSymbols); - - try { - closeNetwork(); - } catch (Exception e) { - outputError("Error closing network while creating Serial Stream: " + e); - } - } - - public void start() { - this.isStreaming = true; - if (!this.protocol.equals("LSL")) { - super.start(); - } else { - openNetwork(); - } + numExgChannels = currentBoard.getNumEXGChannels(); } public void run() { - if (this.protocol.equals("LSL")) { - runLSL(); - } else { - runUdpOscSerial(); - } - } - - private void runLSL() { - if (currentBoard.isStreaming()) { - // This method has been updated to reduce duplicate packets - RW 3/15/23 - if (checkForData()) { - sendData(); - } - } else { - try { - Thread.sleep(1); - } catch (InterruptedException e) { - println(e.getMessage()); - } - } - } - - private void runUdpOscSerial() { openNetwork(); - while (this.isStreaming) { + this.isStreaming = true; + while(this.isStreaming) { if (currentBoard.isStreaming()) { - if (checkForData()) { + if (checkDataIsReadyFlag()) { sendData(); + resetDataIsReadyFlag(); } else { try { Thread.sleep(1); @@ -179,95 +56,107 @@ class NetworkStreamOut extends Thread { } } - private void updateNumChan() { - numExgChannels = currentBoard.getNumEXGChannels(); - // Bug #638: ArrayOutOfBoundsException was thrown if - // nPointsPerUpdate was larger than 10, as start was - // set to dataProcessingFilteredBuffer[0].length - 10. - start = dataProcessingFilteredBuffer[0].length - nPointsPerUpdate; + public void quit() { + this.isStreaming = false; + closeNetwork(); + interrupt(); } - // This method has been updated to reduce duplicate packets - RW 3/15/23 - private synchronized Boolean checkForData() { - if (this.dataType.equals("TimeSeriesRaw")) { - return w_networking.newTimeSeriesDataToSend.compareAndSet(true, false); - } - - if (this.dataType.equals("TimeSeriesFilt")) { - return w_networking.newTimeSeriesDataToSendFiltered.compareAndSet(true, false); - } - - if (this.dataType.equals("Marker")) { - return w_networking.newMarkerDataToSend.compareAndSet(true, false); - } + protected void openNetwork() { + println("Networking: " + getAttributes()); + } - if (this.dataType.equals("Accel/Aux")) { - if (currentBoard instanceof AccelerometerCapableBoard) { - AccelerometerCapableBoard accelBoard = (AccelerometerCapableBoard) currentBoard; - if (accelBoard.isAccelerometerActive()) { - return w_networking.newAccelDataToSend.compareAndSet(true, false); + protected boolean checkDataIsReadyFlag() { + switch (dataType) { + case TIME_SERIES_RAW: + return dataAccumulator.newTimeSeriesDataToSend.get(); + case TIME_SERIES_FILTERED: + return dataAccumulator.newTimeSeriesDataToSendFiltered.get(); + case ACCEL_AUX: + if (currentBoard instanceof AccelerometerCapableBoard) { + AccelerometerCapableBoard accelBoard = (AccelerometerCapableBoard) currentBoard; + if (accelBoard.isAccelerometerActive()) { + return dataAccumulator.newAccelDataToSend.get(); + } } - } - if (currentBoard instanceof AnalogCapableBoard) { - AnalogCapableBoard analogBoard = (AnalogCapableBoard) currentBoard; - if (analogBoard.isAnalogActive()) { - return w_networking.newAnalogDataToSend.compareAndSet(true, false); + if (currentBoard instanceof AnalogCapableBoard) { + AnalogCapableBoard analogBoard = (AnalogCapableBoard) currentBoard; + if (analogBoard.isAnalogActive()) { + return dataAccumulator.newAnalogDataToSend.get(); + } } - } - if (currentBoard instanceof DigitalCapableBoard) { - DigitalCapableBoard digitalBoard = (DigitalCapableBoard) currentBoard; - if (digitalBoard.isDigitalActive()) { - return w_networking.newDigitalDataToSend.compareAndSet(true, false); + if (currentBoard instanceof DigitalCapableBoard) { + DigitalCapableBoard digitalBoard = (DigitalCapableBoard) currentBoard; + if (digitalBoard.isDigitalActive()) { + return dataAccumulator.newDigitalDataToSend.get(); + } } - } - } - - if (w_networking.networkingFrameLocks[this.streamNumber].compareAndSet(true, false)) { - return true; - } else { - return false; + case MARKER: + return dataAccumulator.newMarkerDataToSend.get(); + default: + return dataAccumulator.networkingFrameLocks[this.streamNumber].get(); } } - private void debugTimeSeriesDataSamplingRate() { - // This code is used to check the sample rate of the data stream - if (sampleRateClock == 0) sampleRateClock = millis(); - samplesSent = samplesSent + nPointsPerUpdate; - if (millis() > sampleRateClock + sampleRateClockInterval) { - float timeDelta = float(millis() - sampleRateClock) / 1000; - float sampleRateCheck = samplesSent / timeDelta; - println("\nNumber of samples collected = " + samplesSent); - println("Time Interval (Desired) = " + (sampleRateClockInterval / 1000)); - println("Time Interval (Actual) = " + timeDelta); - println("Sample Rate (Desired) = " + currentBoard.getSampleRate()); - println("Sample Rate (Actual) = " + sampleRateCheck); - sampleRateClock = 0; - samplesSent = 0; - } + protected void resetDataIsReadyFlag() { + switch (dataType) { + case TIME_SERIES_RAW: + dataAccumulator.newTimeSeriesDataToSend.set(false); + break; + case TIME_SERIES_FILTERED: + dataAccumulator.newTimeSeriesDataToSendFiltered.set(false); + break; + case ACCEL_AUX: + if (currentBoard instanceof AccelerometerCapableBoard) { + AccelerometerCapableBoard accelBoard = (AccelerometerCapableBoard) currentBoard; + if (accelBoard.isAccelerometerActive()) { + dataAccumulator.newAccelDataToSend.set(false); + } + } else if (currentBoard instanceof AnalogCapableBoard) { + AnalogCapableBoard analogBoard = (AnalogCapableBoard) currentBoard; + if (analogBoard.isAnalogActive()) { + dataAccumulator.newAnalogDataToSend.set(false); + } + } else if (currentBoard instanceof DigitalCapableBoard) { + DigitalCapableBoard digitalBoard = (DigitalCapableBoard) currentBoard; + if (digitalBoard.isDigitalActive()) { + dataAccumulator.newDigitalDataToSend.set(false); + } + } + break; + case MARKER: + dataAccumulator.newMarkerDataToSend.set(false); + break; + default: + dataAccumulator.networkingFrameLocks[streamNumber].set(false); + break; + } } - private void sendData() { - switch (this.dataType) { - case "TimeSeriesRaw": - case "TimeSeriesFilt": - sendTimeSeriesData(); + protected void sendData() { + switch (dataType) { + case TIME_SERIES_RAW: + sendTimeSeriesRawData(); break; - case "Focus": + case TIME_SERIES_FILTERED: + sendTimeSeriesFilteredData(); + break; + case FOCUS: sendFocusData(); break; - case "FFT": + case FFT: sendFFTData(); break; - case "EMG": + case EMG: sendEMGData(); break; - case "AvgBandPower": - sendNormalizedPowerBandData(); + case AVG_BAND_POWERS: + sendNormalizedBandPowerData(); break; - case "BandPower": - sendPowerBandData(); + case BAND_POWERS: + sendBandPowersAllChannels(); break; - case "Accel/Aux": + case ACCEL_AUX: if (currentBoard instanceof AccelerometerCapableBoard) { AccelerometerCapableBoard accelBoard = (AccelerometerCapableBoard) currentBoard; if (accelBoard.isAccelerometerActive()) { @@ -277,996 +166,65 @@ class NetworkStreamOut extends Thread { if (currentBoard instanceof AnalogCapableBoard) { AnalogCapableBoard analogBoard = (AnalogCapableBoard) currentBoard; if (analogBoard.isAnalogActive()) { - sendAnalogReadData(); + sendAnalogData(); } } if (currentBoard instanceof DigitalCapableBoard) { DigitalCapableBoard digitalBoard = (DigitalCapableBoard) currentBoard; if (digitalBoard.isDigitalActive()) { - sendDigitalReadData(); + sendDigitalData(); } } break; - case "Pulse": + case PULSE: sendPulseData(); break; - case "EMGJoystick": + case EMG_JOYSTICK: sendEMGJoystickData(); break; - case "Marker": + case MARKER: sendMarkerData(); break; } - } - - private void sendTimeSeriesData() { - - float[][] newDataFromBuffer = new float[currentBoard.getNumEXGChannels()][nPointsPerUpdate]; - String udpDataTypeName = "timeSeriesRaw"; - String oscDataTypeName = "time-series-raw"; - - if (this.dataType.equals("TimeSeriesRaw")) { - // Unfiltered - for (int i = 0; i < newDataFromBuffer.length; i++) { - newDataFromBuffer[i] = w_networking.dataBufferToSend[i]; - } - } else { - // Filtered - udpDataTypeName = "timeSeriesFilt"; - oscDataTypeName = "time-series-filtered"; - for (int i = 0; i < newDataFromBuffer.length; i++) { - newDataFromBuffer[i] = w_networking.dataBufferToSend_Filtered[i]; - } - } - - if (debugSamplingRate) { - debugTimeSeriesDataSamplingRate(); - } - - if (this.protocol.equals("UDP")) { - - StringBuilder output = new StringBuilder(); - output.append("{\"type\":\""); - output.append(udpDataTypeName); - output.append("\",\"data\":["); - - for (int i = 0; i < newDataFromBuffer.length; i++) { - output.append("["); - for (int j = 0; j < newDataFromBuffer[i].length; j++) { - output.append(str(newDataFromBuffer[i][j])); - if (j != newDataFromBuffer[i].length - 1) { - output.append(","); - } - } - String channelArrayEnding = i != newDataFromBuffer.length - 1 ? "]," : "]"; - output.append(channelArrayEnding); - } - - // End of entire packet - output.append("]}\r\n"); - - try { - this.udp.send(output.toString(), this.ip, this.port); - } catch (Exception e) { - println(e.getMessage()); - } - - } else if (this.protocol.equals("OSC")) { - - for (int i = 0; i < newDataFromBuffer.length; i++) { - msg.clearArguments(); - msg.setAddrPattern(baseOscAddress + "/" + oscDataTypeName + "/ch" + i); - for (int j = 0; j < newDataFromBuffer[i].length; j++) { - msg.add(newDataFromBuffer[i][j]); - } - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - } - - } else if (this.protocol.equals("LSL")) { - int numChannels = newDataFromBuffer.length; - int numSamples = newDataFromBuffer[0].length; - float[] dataToSend = new float[numChannels * numSamples]; - for (int sample = 0; sample < numSamples; sample++) { - for (int channel = 0; channel < numChannels; channel++) { - dataToSend[channel + sample * numChannels] = newDataFromBuffer[channel][sample]; - } - } - // From LSLLink Library: The time stamps of other samples are automatically - // derived based on the sampling rate of the stream. - outlet_data.push_chunk(dataToSend); - - } else if (this.protocol.equals("Serial")) { - - // Time Series over serial port should be disabled as there is no reasonable usage for this - StringBuilder serialMessage = new StringBuilder(); - for (int i = 0; i < newDataFromBuffer.length; i++) { - serialMessage.append("["); - for (int j = 0; j < newDataFromBuffer[i].length; j++) { - float chan_uV = newDataFromBuffer[i][j]; - - serialMessage.append(threeDecimalPlaces.format(chan_uV)); - if (i < numExgChannels - 1) { - // add a comma to serialMessage to separate chan values, as long as it isn't last value... - serialMessage.append(","); - } - } - serialMessage.append("]"); // close the message w/ "]" - try { - // Write message to serial - this.serial_networking.write(serialMessage.toString()); - // println(serialMesage.toString()); - } catch (Exception e) { - println(e.getMessage()); - } - } - - } - } - - // Send out 1 or 0 as an integer over all networking data types for "Focus" data - private void sendFocusData() { - final int metricValue = w_focus.getMetricExceedsThreshold(); - if (this.protocol.equals("OSC")) { - msg.clearArguments(); - msg.setAddrPattern(baseOscAddress + "/focus"); - msg.add(metricValue); - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - } else if (this.protocol.equals("UDP")) { - StringBuilder sb = new StringBuilder("{\"type\":\"focus\",\"data\":"); - sb.append(str(metricValue)); - sb.append("}\r\n"); - try { - this.udp.send(sb.toString(), this.ip, this.port); - } catch (Exception e) { - println(e.getMessage()); - } - } else if (this.protocol.equals("LSL")) { - float[] output = new float[] { (float) metricValue }; - outlet_data.push_sample(output); - // Serial - } else if (this.protocol.equals("Serial")) { - StringBuilder sb = new StringBuilder(); - sb.append(metricValue); - sb.append("\n"); - try { - // println("SerialMessage: " + serialMessage); - this.serial_networking.write(sb.toString()); - } catch (Exception e) { - println("Networking Serial: Focus Error"); - println(e.getMessage()); - } - } - } - - private void sendFFTData() { - // UNFILTERED & FILTERED ... influenced globally by the FFT filters dropdown - // EEG/FFT readings above 125Hz don't typically travel through the skull - // So for now, only send out 0-125Hz with 1 bin per Hz - // Bin 10 == 10Hz frequency range - if (this.protocol.equals("OSC")) { - for (int i = 0; i < numExgChannels; i++) { - for (int j = 0; j < 125; j++) { - msg.clearArguments(); - msg.setAddrPattern(baseOscAddress + "/fft/ch" + i + "/bin" + j); - msg.add(fftBuff[i].getBand(j)); - } - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - } - } else if (this.protocol.equals("UDP")) { - String outputter = "{\"type\":\"fft\",\"data\":[["; - for (int i = 0; i < numExgChannels; i++) { - for (int j = 0; j < 125; j++) { - outputter += str(fftBuff[i].getBand(j)); - if (j != 125 - 1) { - outputter += ","; - } - } - if (i != numExgChannels - 1) { - outputter += "],["; - } else { - outputter += "]]}\r\n"; - } - } - try { - this.udp.send(outputter, this.ip, this.port); - } catch (Exception e) { - println(e.getMessage()); - } - } else if (this.protocol.equals("LSL")) { - float[] dataToSend = new float[numExgChannels * 125]; - for (int i = 0; i < numExgChannels; i++) { - for (int j = 0; j < 125; j++) { - dataToSend[j + 125 * i] = fftBuff[i].getBand(j); - } - } - // From LSLLink Library: The time stamps of other samples are automatically - // derived based on the sampling rate of the stream. - outlet_data.push_chunk(dataToSend); - } else if (this.protocol.equals("Serial")) { - ///////////////////////////////// THIS OUTPUT IS DISABLED - // Send FFT Data over Serial ... - /* - * for (int i=0;i= 0) { - serialMessage.append("+"); - } - serialMessage.append(accelData_3dec); - if (j != w_networking.accelDataBufferToSend[i].length - 1) { - serialMessage.append(","); - } - } - serialMessage.append("]"); - } - try { - // println(serialMessage); - this.serial_networking.write(serialMessage.toString()); - } catch (Exception e) { - println(e.getMessage()); - } - } - } - - private void sendAnalogReadData() { - - final int NUM_ANALOG_READS = ((AnalogCapableBoard)currentBoard).getAnalogChannels().length; - - if (this.protocol.equals("OSC")) { - - for (int i = 0; i < NUM_ANALOG_READS; i++) { - msg.clearArguments(); - msg.setAddrPattern(baseOscAddress + "/analog/" + i); - for (int j = 0; j < w_networking.analogDataBufferToSend[i].length; j++) { - msg.add(w_networking.analogDataBufferToSend[i][j]); - } - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - } - - } else if (this.protocol.equals("UDP")) { - - StringBuilder output = new StringBuilder(); - output.append("{\"type\":\"analog\",\"data\":["); - - for (int i = 0; i < NUM_ANALOG_READS; i++) { - output.append("["); - for (int j = 0; j < w_networking.analogDataBufferToSend[i].length; j++) { - float analogData = w_networking.analogDataBufferToSend[i][j]; - // Formatting in this way is resilient to internationalization - String analogData_3dec = threeDecimalPlaces.format(analogData); - output.append(analogData_3dec); - if (j != w_networking.analogDataBufferToSend[i].length - 1) { - output.append(","); - } - } - String channelArrayEnding = i != NUM_ANALOG_READS - 1 ? "]," : "]"; - output.append(channelArrayEnding); - } - - output.append("]}\r\n"); - - try { - this.udp.send(output.toString(), this.ip, this.port); - } catch (Exception e) { - println(e.getMessage()); - } - - } else if (this.protocol.equals("LSL")) { - - int numChannels = NUM_ANALOG_READS; - int numSamples = w_networking.analogDataBufferToSend[0].length; - float[] dataToSend = new float[numChannels * numSamples]; - for (int sample = 0; sample < numSamples; sample++) { - for (int channel = 0; channel < numChannels; channel++) { - dataToSend[channel + sample * numChannels] = w_networking.analogDataBufferToSend[channel][sample]; - } - } - outlet_data.push_chunk(dataToSend); - - } else if (this.protocol.equals("Serial")) { - - StringBuilder serialMessage = new StringBuilder(); - - for (int i = 0; i < NUM_ANALOG_READS; i++) { - serialMessage.append("["); - for (int j = 0; j < w_networking.analogDataBufferToSend[i].length; j++) { - float analogData = w_networking.analogDataBufferToSend[i][j]; - // Formatting in this way is resilient to internationalization - String analogData_3dec = threeDecimalPlaces.format(analogData); - serialMessage.append(analogData_3dec); - if (j != w_networking.analogDataBufferToSend[i].length - 1) { - serialMessage.append(","); - } - } - String channelArrayEnding = i != NUM_ANALOG_READS - 1 ? "]," : "]"; - serialMessage.append(channelArrayEnding); - } - serialMessage.append("\n"); - try { - // println(serialMessage); - this.serial_networking.write(serialMessage.toString()); - } catch (Exception e) { - println(e.getMessage()); - } - } - } - - private void sendDigitalReadData() { - - final int NUM_DIGITAL_READS = w_digitalRead.getNumDigitalReads(); - - if (this.protocol.equals("OSC")) { - - for (int i = 0; i < NUM_DIGITAL_READS; i++) { - msg.clearArguments(); - msg.setAddrPattern(baseOscAddress + "/digital/" + i); - //msg.add(w_digitalRead.digitalReadDots[i].getDigitalReadVal()); - for (int j = 0; j < w_networking.digitalDataBufferToSend[i].length; j++) { - msg.add(w_networking.digitalDataBufferToSend[i][j]); - } - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - } - - } else if (this.protocol.equals("UDP")) { - - StringBuilder output = new StringBuilder(); - output.append("{\"type\":\"digital\",\"data\":["); - - for (int i = 0; i < NUM_DIGITAL_READS; i++) { - output.append("["); - for (int j = 0; j < w_networking.digitalDataBufferToSend[i].length; j++) { - int digitalData = w_networking.digitalDataBufferToSend[i][j]; - String digitalDataFormatted = String.format("%d", digitalData); - output.append(digitalDataFormatted); - if (j != w_networking.digitalDataBufferToSend[i].length - 1) { - output.append(","); - } - } - String channelArrayEnding = i != NUM_DIGITAL_READS - 1 ? "]," : "]"; - output.append(channelArrayEnding); - } - - output.append("]}\r\n"); - - try { - this.udp.send(output.toString(), this.ip, this.port); - } catch (Exception e) { - println(e.getMessage()); - } - - } else if (this.protocol.equals("LSL")) { - - int numChannels = NUM_DIGITAL_READS; - int numSamples = w_networking.digitalDataBufferToSend[0].length; - float[] dataToSend = new float[numChannels * numSamples]; - for (int sample = 0; sample < numSamples; sample++) { - for (int channel = 0; channel < numChannels; channel++) { - dataToSend[channel + sample * numChannels] = w_networking.digitalDataBufferToSend[channel][sample]; - } - } - outlet_data.push_chunk(dataToSend); - - } else if (this.protocol.equals("Serial")) { - - StringBuilder serialMessage = new StringBuilder(); - - for (int i = 0; i < NUM_DIGITAL_READS; i++) { - serialMessage.append("["); - for (int j = 0; j < w_networking.digitalDataBufferToSend[i].length; j++) { - int digitalData = w_networking.digitalDataBufferToSend[i][j]; - String digitalDataFormatted = String.format("%d", digitalData); - serialMessage.append(digitalDataFormatted); - if (j != w_networking.digitalDataBufferToSend[i].length - 1) { - serialMessage.append(","); - } - } - String channelArrayEnding = i != NUM_DIGITAL_READS - 1 ? "]," : "]"; - serialMessage.append(channelArrayEnding.toString()); - } - - serialMessage.append("\n"); - - try { - // println(serialMessage); - this.serial_networking.write(serialMessage.toString()); - } catch (Exception e) { - println(e.getMessage()); - } - } - } - - private void sendPulseData() { - // Get data from Board that - int numDataPoints = 2; - - if (this.protocol.equals("OSC")) { - - msg.clearArguments(); - msg.setAddrPattern(baseOscAddress + "/pulse/bpm"); - msg.add(w_pulsesensor.getBPM()); - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - - msg.clearArguments(); - msg.setAddrPattern(baseOscAddress + "/pulse/ibi"); - msg.add(w_pulsesensor.getIBI()); - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - - } else if (this.protocol.equals("UDP")) { - - StringBuilder output = new StringBuilder("{\"type\":\"pulse\",\"data\":["); - output.append(str(w_pulsesensor.getBPM())); - output.append(","); - output.append(str(w_pulsesensor.getIBI())); - output.append("]}\r\n"); - try { - this.udp.send(output.toString(), this.ip, this.port); - } catch (Exception e) { - println(e.getMessage()); - } - - } else if (this.protocol.equals("LSL")) { - - float[] dataToSend = new float[2]; - dataToSend[0] = w_pulsesensor.getBPM(); - dataToSend[1] = w_pulsesensor.getIBI(); - // From LSLLink Library: The time stamps of other samples are automatically - // derived based on the sampling rate of the stream. - outlet_data.push_sample(dataToSend); - - } else if (this.protocol.equals("Serial")) { - - serialMessage = ""; // clear message - serialMessage += w_pulsesensor.getBPM() + ","; - serialMessage += w_pulsesensor.getIBI(); - try { - this.serial_networking.write(serialMessage); - } catch (Exception e) { - println(e.getMessage()); - } - - } - }// End sendPulseData - - private void sendEMGJoystickData() { - - final float[] emgJoystickXY = w_emgJoystick.getJoystickXY(); - - if (this.protocol.equals("OSC")) { - for (int i = 0; i < emgJoystickXY.length; i++) { - msg.clearArguments(); - if (i == 0) { - msg.setAddrPattern(baseOscAddress + "/emg-joystick/x"); - } else if (i == 1) { - msg.setAddrPattern(baseOscAddress + "/emg-joystick/y"); - } - msg.add(emgJoystickXY[i]); - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - } - } else if (this.protocol.equals("UDP")) { - StringBuilder output = new StringBuilder("{\"type\":\"emgJoystick\",\"data\":["); - for (int i = 0; i < emgJoystickXY.length; i++) { - // Formatting in this way is resilient to internationalization - String dataFormatted = threeDecimalPlaces.format(emgJoystickXY[i]); - output.append(dataFormatted); - if (i != emgJoystickXY.length - 1) { - output.append(","); - } else { - output.append("]}\r\n"); - } - } - try { - this.udp.send(output.toString(), this.ip, this.port); - } catch (Exception e) { - println(e.getMessage()); - } - } else if (this.protocol.equals("LSL")) { - float[] dataToSend = new float[emgJoystickXY.length]; - for (int i = 0; i < emgJoystickXY.length; i++) { - dataToSend[i] = emgJoystickXY[i]; - } - outlet_data.push_sample(dataToSend); - } else if (this.protocol.equals("Serial")) { - // Data Format: +0.900,-0.042\n - // 7 chars per axis, including \n char - StringBuilder output = new StringBuilder(); - for (int i = 0; i < emgJoystickXY.length; i++) { - float data = emgJoystickXY[i]; - String dataFormatted = threeDecimalPlaces.format(data); - if (data >= 0) - output.append("+"); - output.append(dataFormatted); - if (i != emgJoystickXY.length - 1) { - output.append(","); - } else { - output.append("\n"); - } - } - try { - // println(serialMessage); - this.serial_networking.write(output.toString()); - } catch (Exception e) { - println(e.getMessage()); - } - } - } - - private void sendMarkerData() { - - float[] newDataFromBuffer = new float[nPointsPerUpdate]; - - for (int i = 0; i < newDataFromBuffer.length; i++) { - newDataFromBuffer[i] = w_networking.markerDataBufferToSend[i]; - } if (debugSamplingRate) { - debugTimeSeriesDataSamplingRate(); - } - - if (this.protocol.equals("UDP")) { - - StringBuilder output = new StringBuilder(); - output.append("{\"type\":\""); - output.append("marker"); - output.append("\",\"data\":["); - - for (int i = 0; i < newDataFromBuffer.length; i++) { - output.append(str(newDataFromBuffer[i])); - if (i != newDataFromBuffer.length - 1) { - output.append(","); - } - } - - // End of entire packet - output.append("]}\r\n"); - - try { - this.udp.send(output.toString(), this.ip, this.port); - } catch (Exception e) { - println(e.getMessage()); - } - - } else if (this.protocol.equals("OSC")) { - - for (int i = 0; i < newDataFromBuffer.length; i++) { - msg.clearArguments(); - msg.setAddrPattern(baseOscAddress + "/marker"); - msg.add(newDataFromBuffer[i]); - try { - this.osc.send(msg, this.oscNetAddress); - } catch (Exception e) { - println(e.getMessage()); - } - } - - } else if (this.protocol.equals("LSL")) { - // In this case, the newDataFromBuffer array is already formatted in an acceptable way. - // From LSLLink Library: The time stamps of other samples are automatically - // derived based on the sampling rate of the stream. - outlet_data.push_chunk(newDataFromBuffer); - - } else if (this.protocol.equals("Serial")) { - - // Time Series over serial port should be disabled as there is no reasonable usage for this - for (int i = 0; i < newDataFromBuffer.length; i++) { - StringBuilder serialMessage = new StringBuilder(); - float markerValue = newDataFromBuffer[i]; - serialMessage.append(threeDecimalPlaces.format(markerValue)); - serialMessage.append("\n"); - try { - // Write message to serial - this.serial_networking.write(serialMessage.toString()); - //println(serialMessage.toString()); - } catch (Exception e) { - println(e.getMessage()); - } + if (dataType == NetworkDataType.TIME_SERIES_RAW + || dataType == NetworkDataType.TIME_SERIES_FILTERED + || dataType == NetworkDataType.MARKER) { + debugTimeSeriesDataSamplingRate(); } } } - //// Add new stream function here (ex. sendWidgetData) in the same format as - //// above - - public void quit() { - this.isStreaming = false; - closeNetwork(); - interrupt(); - } - - private void closeNetwork() { - if (this.protocol.equals("OSC")) { - try { - this.osc.stop(); - } catch (Exception e) { - println(e.getMessage()); - } - } else if (this.protocol.equals("UDP")) { - this.udp.close(); - } else if (this.protocol.equals("LSL")) { - outlet_data.close(); - } else if (this.protocol.equals("Serial")) { - // Close Serial Port %%%%% - try { - serial_networking.clear(); - serial_networking.stop(); - println("Successfully closed SERIAL/COM port " + this.portName); - } catch (Exception e) { - println("Failed to close SERIAL/COM port " + this.portName); - } - } - } - - private void openNetwork() { - println("Networking: " + getAttributes()); - if (this.protocol.equals("OSC")) { - this.osc = new OscP5(this, this.port + 1000); - this.oscNetAddress = new NetAddress(this.ip, this.port); - this.msg = new OscMessage(this.baseOscAddress); - } else if (this.protocol.equals("UDP")) { - this.udp = new UDP(this); - this.udp.setBuffer(20000); - this.udp.listen(false); - this.udp.log(false); - output("UDP successfully connected"); - } else if (this.protocol.equals("LSL")) { - String stream_id = "openbcigui"; - info_data = new LSL.StreamInfo(this.streamName, this.streamType, this.numLslDataPoints, - currentBoard.getSampleRate(), LSL.ChannelFormat.float32, stream_id); - outlet_data = new LSL.StreamOutlet(info_data); - } else if (this.protocol.equals("Serial")) { - try { - serial_networking = new processing.serial.Serial(this.pApplet, this.portName, this.baudRate); - serial_networking.clear(); - verbosePrint("Successfully opened SERIAL/COM: " + this.portName); - output("Successfully opened SERIAL/COM (" + this.baudRate + "): " + this.portName); - } catch (Exception e) { - verbosePrint("W_Networking.pde: could not open SERIAL PORT: " + this.portName); - println("Error: " + e); - } - } - } - - // Used only to print attributes to the screen - private StringList getAttributes() { - StringList attributes = new StringList(); - if (this.protocol.equals("OSC")) { - attributes.append(this.dataType); - attributes.append(this.ip); - attributes.append(str(this.port)); - attributes.append(this.baseOscAddress); - } else if (this.protocol.equals("UDP")) { - attributes.append(this.dataType); - attributes.append(this.ip); - attributes.append(str(this.port)); - } else if (this.protocol.equals("LSL")) { - attributes.append(this.dataType); - attributes.append(this.streamName); - attributes.append(this.streamType); - attributes.append(str(this.numLslDataPoints)); - } else if (this.protocol.equals("Serial")) { - attributes.append(this.dataType); - attributes.append(this.portName); - attributes.append(str(this.baudRate)); + private void debugTimeSeriesDataSamplingRate() { + if (sampleRateClock == 0) sampleRateClock = millis(); + samplesSent = samplesSent + nPointsPerUpdate; + if (millis() > sampleRateClock + sampleRateClockInterval) { + float timeDelta = float(millis() - sampleRateClock) / 1000; + float sampleRateCheck = samplesSent / timeDelta; + println("\nNumber of samples collected = " + samplesSent); + println("Time Interval (Desired) = " + (sampleRateClockInterval / 1000)); + println("Time Interval (Actual) = " + timeDelta); + println("Sample Rate (Desired) = " + currentBoard.getSampleRate()); + println("Sample Rate (Actual) = " + sampleRateCheck); + sampleRateClock = 0; + samplesSent = 0; } - return attributes; } + protected abstract void closeNetwork(); + protected abstract StringList getAttributes(); + protected abstract void sendTimeSeriesFilteredData(); + protected abstract void sendTimeSeriesRawData(); + protected abstract void sendFocusData(); + protected abstract void sendFFTData(); + protected abstract void sendBandPowersAllChannels(); + protected abstract void sendNormalizedBandPowerData(); + protected abstract void sendEMGData(); + protected abstract void sendAccelerometerData(); + protected abstract void sendAnalogData(); + protected abstract void sendDigitalData(); + protected abstract void sendPulseData(); + protected abstract void sendEMGJoystickData(); + protected abstract void sendMarkerData(); } \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkStreamOutLSL.pde b/OpenBCI_GUI/NetworkStreamOutLSL.pde new file mode 100644 index 000000000..cf03511a0 --- /dev/null +++ b/OpenBCI_GUI/NetworkStreamOutLSL.pde @@ -0,0 +1,162 @@ +class NetworkStreamOutLSL extends NetworkStreamOut { + + private LSL.StreamOutlet outlet_data; + private String streamType; + private String streamName; + private int numLslDataPoints; + + NetworkStreamOutLSL(NetworkDataType dataType, String streamName, String streamType, int numLslDataPoints, int _streamNumber) { + super(dataType); + protocol = NetworkProtocol.LSL; + this.streamNumber = _streamNumber; + this.streamName = streamName; + this.streamType = streamType; + this.numLslDataPoints = numLslDataPoints; + //openNetwork(); + } + + @Override + protected void openNetwork() { + super.openNetwork(); + String streamId = "openbcigui"; + LSL.StreamInfo infoData = new LSL.StreamInfo(this.streamName, + this.streamType, + this.numLslDataPoints, + currentBoard.getSampleRate(), + LSL.ChannelFormat.float32, + streamId); + outlet_data = new LSL.StreamOutlet(infoData); + } + + @Override + protected void closeNetwork() { + outlet_data.close(); + } + + @Override + protected StringList getAttributes() { + StringList attributes = new StringList(); + attributes.append(dataType.getString()); + attributes.append(this.streamName); + attributes.append(this.streamType); + attributes.append(str(this.numLslDataPoints)); + return attributes; + } + + @Override + protected void sendTimeSeriesFilteredData() { + output2dArrayLSL(dataAccumulator.getTimeSeriesFilteredBuffer()); + } + + @Override + protected void sendTimeSeriesRawData() { + output2dArrayLSL(dataAccumulator.getTimeSeriesRawBuffer()); + } + + @Override + protected void sendFocusData() { + final int metricValue = dataAccumulator.getFocusValueExceedsThreshold(); + final float[] output = new float[] { (float) metricValue }; + outlet_data.push_sample(output); + } + + @Override + protected void sendFFTData() { + final ddf.minim.analysis.FFT[] fftBuff = dataAccumulator.getFFTBuffer(); + final float[] dataToSend = new float[numExgChannels * NUM_FFT_BINS_TO_SEND]; + for (int i = 0; i < numExgChannels; i++) { + for (int j = 0; j < NUM_FFT_BINS_TO_SEND; j++) { + dataToSend[j + NUM_FFT_BINS_TO_SEND * i] = fftBuff[i].getBand(j); + } + } + outlet_data.push_chunk(dataToSend); + } + + @Override + protected void sendBandPowersAllChannels() { + final float[][] bandPowerData = dataAccumulator.getAllBandPowerData(); + // Send out band powers for each channel sequentially via push_sample + // Prepend channel number to each array + // push_chunk will send out all channels at once...but doesn't seem to gaurantee all X channels of data will be pulled at once, despite extensive testing + // Example sample: [Channel Number, DELTA, THETA, ALPHA, BETA, GAMMA] + float[] dataToSend = new float[NUM_BAND_POWERS + 1]; + for (int channel = 0; channel < numExgChannels; channel++) { + for (int band = 0; band < NUM_BAND_POWERS + 1; band++) { + if (band == 0) { + dataToSend[band] = (float) channel; + } else { + dataToSend[band] = bandPowerData[channel][band - 1]; + } + } + outlet_data.push_sample(dataToSend); + } + } + + @Override + protected void sendNormalizedBandPowerData() { + outlet_data.push_sample(dataAccumulator.getNormalizedBandPowerData()); + } + + @Override + protected void sendEMGData() { + outlet_data.push_sample(dataAccumulator.getEmgNormalizedValues()); + } + + @Override + protected void sendAccelerometerData() { + output2dArrayLSL(dataAccumulator.getAccelBuffer()); + } + + @Override + protected void sendAnalogData() { + output2dArrayLSL(dataAccumulator.getAnalogBuffer()); + } + + @Override + protected void sendDigitalData() { + output2dArrayLSL(dataAccumulator.getDigitalBuffer()); + } + + @Override + protected void sendPulseData() { + float[] dataToSend = new float[2]; + dataToSend[0] = dataAccumulator.getPulseSensorBPM(); + dataToSend[1] = dataAccumulator.getPulseSensorIBI(); + outlet_data.push_sample(dataToSend); + } + + @Override + protected void sendEMGJoystickData() { + final float[] emgJoystickXY = dataAccumulator.getEMGJoystickXY(); + float[] dataToSend = new float[emgJoystickXY.length]; + for (int i = 0; i < emgJoystickXY.length; i++) { + dataToSend[i] = emgJoystickXY[i]; + } + outlet_data.push_sample(dataToSend); + } + + @Override + protected void sendMarkerData() { + outlet_data.push_chunk(dataAccumulator.getMarkerBuffer()); + } + + private void output2dArrayLSL(float[][] dataBuffer) { + float[] flattenedDataArray = new float[dataBuffer.length * dataBuffer[0].length]; + for (int sample = 0; sample < dataBuffer[0].length; sample++) { + for (int channel = 0; channel < dataBuffer.length; channel++) { + flattenedDataArray[channel + sample * dataBuffer.length] = dataBuffer[channel][sample]; + } + } + outlet_data.push_chunk(flattenedDataArray); + } + + private void output2dArrayLSL(int[][] dataBuffer) { + float[] flattenedDataArray = new float[dataBuffer.length * dataBuffer[0].length]; + for (int sample = 0; sample < dataBuffer[0].length; sample++) { + for (int channel = 0; channel < dataBuffer.length; channel++) { + flattenedDataArray[channel + sample * dataBuffer.length] = dataBuffer[channel][sample]; + } + } + outlet_data.push_chunk(flattenedDataArray); + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkStreamOutOSC.pde b/OpenBCI_GUI/NetworkStreamOutOSC.pde new file mode 100644 index 000000000..63b5769e3 --- /dev/null +++ b/OpenBCI_GUI/NetworkStreamOutOSC.pde @@ -0,0 +1,205 @@ +class NetworkStreamOutOSC extends NetworkStreamOut { + + private OscP5 osc; + private NetAddress oscNetAddress; + private OscMessage msg; + private String baseOscAddress; + private String dataTypeKey; + + NetworkStreamOutOSC(NetworkDataType dataType, String ip, int port, String baseAddress, int _streamNumber) { + super(dataType); + protocol = NetworkProtocol.OSC; + this.streamNumber = _streamNumber; + this.ip = ip; + this.port = port; + this.baseOscAddress = baseAddress; + dataTypeKey = dataType.getOSCKey(); + } + + @Override + protected void openNetwork() { + super.openNetwork(); + osc = new OscP5(this, port + 1000); + oscNetAddress = new NetAddress(ip, port); + msg = new OscMessage(baseOscAddress); + } + + @Override + protected void closeNetwork() { + try { + osc.stop(); + } catch (Exception e) { + println(e.getMessage()); + } + } + + @Override + protected StringList getAttributes() { + StringList attributes = new StringList(); + attributes.append(dataType.getString()); + attributes.append(this.ip); + attributes.append(str(this.port)); + attributes.append(this.baseOscAddress); + return attributes; + } + + @Override + protected void sendTimeSeriesFilteredData() { + output2dArrayOSC(dataAccumulator.getTimeSeriesFilteredBuffer()); + } + + @Override + protected void sendTimeSeriesRawData() { + output2dArrayOSC(dataAccumulator.getTimeSeriesRawBuffer()); + } + + @Override + protected void sendFocusData() { + final int metricValue = dataAccumulator.getFocusValueExceedsThreshold(); + msg.clearArguments(); + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey); + msg.add(metricValue); + outputUsingProtocol(); + } + + @Override + protected void sendFFTData() { + final ddf.minim.analysis.FFT[] fftBuff = dataAccumulator.getFFTBuffer(); + for (int i = 0; i < numExgChannels; i++) { + for (int j = 0; j < 125; j++) { + msg.clearArguments(); + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/ch" + i + "/bin" + j); + msg.add(fftBuff[i].getBand(j)); + } + outputUsingProtocol(); + } + } + + @Override + protected void sendBandPowersAllChannels() { + output2dArrayOSC(dataAccumulator.getAllBandPowerData()); + } + + @Override + protected void sendNormalizedBandPowerData() { + final float[] normalizedBandPowerData = dataAccumulator.getNormalizedBandPowerData(); + msg.clearArguments(); + for (int i = 0; i < NUM_BAND_POWERS; i++) { + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/" + i); + msg.add(normalizedBandPowerData[i]); + outputUsingProtocol(); + } + } + + @Override + protected void sendEMGData() { + final float[] emgValues = dataAccumulator.getEmgNormalizedValues(); + for (int i = 0; i < numExgChannels; i++) { + msg.clearArguments(); + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/" + i); + msg.add(emgValues[i]); + outputUsingProtocol(); + } + } + + @Override + protected void sendAccelerometerData() { + final float[][] accelBuffer = dataAccumulator.getAccelBuffer(); + for (int i = 0; i < NUM_ACCEL_DIMS; i++) { + for (int j = 0; j < accelBuffer[i].length; j++) { + msg.clearArguments(); + if (i == 0) { + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/x"); + } else if (i == 1) { + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/y"); + } else if (i == 2) { + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/z"); + } + msg.add(accelBuffer[i][j]); + outputUsingProtocol(); + } + } + } + + @Override + protected void sendAnalogData() { + output2dArrayOSC(dataAccumulator.getAnalogBuffer()); + } + + @Override + protected void sendDigitalData() { + output2dArrayOSC(dataAccumulator.getDigitalBuffer()); + } + + @Override + protected void sendPulseData() { + final int bpm = dataAccumulator.getPulseSensorBPM(); + final int ibi = dataAccumulator.getPulseSensorIBI(); + + msg.clearArguments(); + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/bpm"); + msg.add(bpm); + outputUsingProtocol(); + + msg.clearArguments(); + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/ibi"); + msg.add(ibi); + outputUsingProtocol(); + } + + @Override + protected void sendEMGJoystickData() { + final float[] emgJoystickXY = dataAccumulator.getEMGJoystickXY(); + for (int i = 0; i < emgJoystickXY.length; i++) { + msg.clearArguments(); + if (i == 0) { + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/x"); + } else if (i == 1) { + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/y"); + } + msg.add(emgJoystickXY[i]); + outputUsingProtocol(); + } + } + + @Override + protected void sendMarkerData() { + final float[] markerData = dataAccumulator.getMarkerBuffer(); + for (int i = 0; i < markerData.length; i++) { + msg.clearArguments(); + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey); + msg.add(markerData[i]); + outputUsingProtocol(); + } + } + + private void outputUsingProtocol() { + try { + osc.send(msg, oscNetAddress); + } catch (Exception e) { + println(e.getMessage()); + } + } + + private void output2dArrayOSC(float[][] dataBuffer) { + for (int i = 0; i < dataBuffer.length; i++) { + msg.clearArguments(); + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/ch" + i); + for (int j = 0; j < dataBuffer[i].length; j++) { + msg.add(dataBuffer[i][j]); + } + outputUsingProtocol(); + } + } + + private void output2dArrayOSC(int[][] dataBuffer) { + for (int i = 0; i < dataBuffer.length; i++) { + msg.clearArguments(); + msg.setAddrPattern(baseOscAddress + "/" + dataTypeKey + "/ch" + i); + for (int j = 0; j < dataBuffer[i].length; j++) { + msg.add(dataBuffer[i][j]); + } + outputUsingProtocol(); + } + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkStreamOutSerial.pde b/OpenBCI_GUI/NetworkStreamOutSerial.pde new file mode 100644 index 000000000..089f159b2 --- /dev/null +++ b/OpenBCI_GUI/NetworkStreamOutSerial.pde @@ -0,0 +1,253 @@ +public enum NetworkSerialShowPlusSigns { YES, NO }; + +class NetworkStreamOutSerial extends NetworkStreamOut { + + private processing.serial.Serial serialConnection; + private String portName; + private int baudRate; + private PApplet pApplet; + + private boolean debugSerialOutput = false; + + NetworkStreamOutSerial(NetworkDataType dataType, String portName, int baudRate, PApplet _this) { + super(dataType); + protocol = NetworkProtocol.SERIAL; + this.streamNumber = 0; + this.dataType = dataType; + this.portName = portName; + this.baudRate = baudRate; + this.pApplet = _this; + + // Force decimal formatting for all Locales + Locale currentLocale = Locale.getDefault(); + DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(currentLocale); + otherSymbols.setDecimalSeparator('.'); + otherSymbols.setGroupingSeparator(','); + threeDecimalPlaces = new DecimalFormat("0.000", otherSymbols); + fourLeadingPlaces = new DecimalFormat("####", otherSymbols); + } + + @Override + protected void openNetwork() { + super.openNetwork(); + try { + serialConnection = new processing.serial.Serial(pApplet, portName, baudRate); + serialConnection.clear(); + println("Networking: Successfully opened SERIAL/COM port " + portName + " at baud rate of " + baudRate); + } catch (Exception e) { + println("Networking: could not open SERIAL/COM port: " + portName); + println("Error: " + e); + } + } + + @Override + protected void closeNetwork() { + try { + serialConnection.clear(); + serialConnection.stop(); + println("Networking: Successfully closed SERIAL/COM port: " + portName); + } catch (Exception e) { + println("Networking: Failed to close SERIAL/COM port: " + portName); + } + } + + @Override + protected StringList getAttributes() { + StringList attributes = new StringList(); + attributes.append(dataType.getString()); + attributes.append(portName); + attributes.append(str(baudRate)); + return attributes; + } + + @Override + protected void sendTimeSeriesFilteredData() { + output2dArraySerial(dataAccumulator.getTimeSeriesFilteredBuffer(), NetworkSerialShowPlusSigns.YES); + } + + @Override + protected void sendTimeSeriesRawData() { + output2dArraySerial(dataAccumulator.getTimeSeriesRawBuffer(), NetworkSerialShowPlusSigns.YES); + } + + @Override + protected void sendFocusData() { + final int metricValue = dataAccumulator.getFocusValueExceedsThreshold(); + StringBuilder output = new StringBuilder(); + output.append(metricValue); + output.append("\n"); + outputUsingProtocol(output.toString()); + } + + @Override + protected void sendFFTData() { + //This output is disabled as there is no reasonable usage for FFT over serial + return; + } + + @Override + protected void sendBandPowersAllChannels() { + final float[][] bandPowerData = dataAccumulator.getAllBandPowerData(); + StringBuilder output = new StringBuilder(); + // Send out band powers for each channel sequentially + for (int channel = 0; channel < numExgChannels; channel++) { + output.append("[" + (channel + 1) + ","); + for (int band = 0; band < NUM_BAND_POWERS; band++) { + float value = bandPowerData[channel][band]; + String valueFormatted = threeDecimalPlaces.format(value); + output.append(valueFormatted); + if (band < NUM_BAND_POWERS - 1) { + output.append(","); + } + } + output.append("]"); + outputUsingProtocol(output.toString()); + } + } + + @Override + protected void sendNormalizedBandPowerData() { + final float[] normalizedBandPowerData = dataAccumulator.getNormalizedBandPowerData(); + StringBuilder output = new StringBuilder(); + for (int i = 0; i < NUM_BAND_POWERS; i++) { + float power_band = normalizedBandPowerData[i]; + String power_band_3dec = threeDecimalPlaces.format(power_band); + output.append(power_band_3dec); + if (i < NUM_BAND_POWERS - 1) { + output.append(","); + } + } + output.append("]"); + outputUsingProtocol(output.toString()); + } + + @Override + protected void sendEMGData() { + final float[] emgValues = dataAccumulator.getEmgNormalizedValues(); + StringBuilder output = new StringBuilder(); + for (int i = 0; i < numExgChannels; i++) { + float emg_normalized = emgValues[i]; + String emg_normalized_3dec = threeDecimalPlaces.format(emg_normalized); + output.append(emg_normalized_3dec); + if (i != numExgChannels - 1) { + output.append(","); + } else { + output.append("\n"); + } + } + outputUsingProtocol(output.toString()); + } + + @Override + protected void sendAccelerometerData() { + output2dArraySerial(dataAccumulator.getAccelBuffer(), NetworkSerialShowPlusSigns.YES); + } + + @Override + protected void sendAnalogData() { + output2dArraySerial(dataAccumulator.getAnalogBuffer(), NetworkSerialShowPlusSigns.NO); + } + + @Override + protected void sendDigitalData() { + output2dArraySerial(dataAccumulator.getDigitalBuffer(), NetworkSerialShowPlusSigns.NO); + } + + @Override + protected void sendPulseData() { + StringBuilder output = new StringBuilder(); + output.append(dataAccumulator.getPulseSensorBPM()); + output.append(","); + output.append(dataAccumulator.getPulseSensorIBI()); + outputUsingProtocol(output.toString()); + } + + @Override + protected void sendEMGJoystickData() { + final float[] emgJoystickXY = dataAccumulator.getEMGJoystickXY(); + // Data Format: +0.900,-0.042\n + // 7 chars per axis, including \n char + StringBuilder output = new StringBuilder(); + for (int i = 0; i < emgJoystickXY.length; i++) { + float data = emgJoystickXY[i]; + String dataFormatted = threeDecimalPlaces.format(data); + if (data >= 0) + output.append("+"); + output.append(dataFormatted); + if (i != emgJoystickXY.length - 1) { + output.append(","); + } else { + output.append("\n"); + } + } + outputUsingProtocol(output.toString()); + } + + @Override + protected void sendMarkerData() { + final float[] markerData = dataAccumulator.getMarkerBuffer(); + for (int i = 0; i < markerData.length; i++) { + StringBuilder output = new StringBuilder(); + float markerValue = markerData[i]; + output.append(threeDecimalPlaces.format(markerValue)); + output.append("\n"); + outputUsingProtocol(output.toString()); + } + } + + private void outputUsingProtocol(String data) { + try { + if (debugSerialOutput) { + println("SerialMessage: " + data); + } + serialConnection.write(data); + } catch (Exception e) { + println(e.getMessage()); + } + } + + private void output2dArraySerial(float[][] dataBuffer, NetworkSerialShowPlusSigns showPlusSign) { + StringBuilder output = new StringBuilder(); + for (int i = 0; i < dataBuffer.length; i++) { + output.append("["); + for (int j = 0; j < dataBuffer[i].length; j++) { + float data = dataBuffer[i][j]; + //Formatting in this way is resilient to internationalization + String dataFormatted = threeDecimalPlaces.format(data); + if (showPlusSign == NetworkSerialShowPlusSigns.YES && data >= 0) { + output.append("+"); + } + output.append(dataFormatted); + if (j != dataBuffer[i].length - 1) { + output.append(","); + } + } + String channelArrayEnding = i != dataBuffer.length - 1 ? "]," : "]"; + output.append(channelArrayEnding); + } + output.append("\n"); + outputUsingProtocol(output.toString()); + } + + private void output2dArraySerial(int[][] dataBuffer, NetworkSerialShowPlusSigns showPlusSign) { + StringBuilder output = new StringBuilder(); + for (int i = 0; i < dataBuffer.length; i++) { + output.append("["); + for (int j = 0; j < dataBuffer[i].length; j++) { + int data = dataBuffer[i][j]; + String dataFormatted = String.format("%d", data); + if (showPlusSign == NetworkSerialShowPlusSigns.YES && data >= 0) { + output.append("+"); + } + output.append(dataFormatted); + if (j != dataBuffer[i].length - 1) { + output.append(","); + } + } + String channelArrayEnding = i != dataBuffer.length - 1 ? "]," : "]"; + output.append(channelArrayEnding); + } + output.append("\n"); + outputUsingProtocol(output.toString()); + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkStreamOutUDP.pde b/OpenBCI_GUI/NetworkStreamOutUDP.pde new file mode 100644 index 000000000..78ab44e96 --- /dev/null +++ b/OpenBCI_GUI/NetworkStreamOutUDP.pde @@ -0,0 +1,221 @@ +class NetworkStreamOutUDP extends NetworkStreamOut { + + private UDP udp; + private String dataTypeKey; + + NetworkStreamOutUDP(NetworkDataType dataType, String ip, int port, int _streamNumber) { + super(dataType); + protocol = NetworkProtocol.UDP; + this.streamNumber = _streamNumber; + this.ip = ip; + this.port = port; + dataTypeKey = dataType.getUDPKey(); + + // Force decimal formatting for all Locales + Locale currentLocale = Locale.getDefault(); + DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(currentLocale); + otherSymbols.setDecimalSeparator('.'); + otherSymbols.setGroupingSeparator(','); + threeDecimalPlaces = new DecimalFormat("0.000", otherSymbols); + fourLeadingPlaces = new DecimalFormat("####", otherSymbols); + } + + @Override + protected void openNetwork() { + super.openNetwork(); + udp = new UDP(this); + udp.setBuffer(20000); + udp.listen(false); + udp.log(false); + } + + @Override + protected void closeNetwork() { + udp.close(); + } + + @Override + protected StringList getAttributes() { + StringList attributes = new StringList(); + attributes.append(dataType.getString()); + attributes.append(this.ip); + attributes.append(str(this.port)); + return attributes; + } + + @Override + protected void sendTimeSeriesFilteredData() { + output2dArrayUDP(dataAccumulator.getTimeSeriesFilteredBuffer()); + } + + @Override + protected void sendTimeSeriesRawData() { + output2dArrayUDP(dataAccumulator.getTimeSeriesRawBuffer()); + } + + @Override + protected void sendFocusData() { + final int metricValue = dataAccumulator.getFocusValueExceedsThreshold(); + StringBuilder output = new StringBuilder(); + output.append("{\"type\":\""); + output.append(dataTypeKey); + output.append("\",\"data\":"); + output.append(str(metricValue)); + output.append("}\r\n"); + outputUsingProtocol(output.toString()); + } + + @Override + protected void sendFFTData() { + final ddf.minim.analysis.FFT[] fftBuff = dataAccumulator.getFFTBuffer(); + StringBuilder output = new StringBuilder(); + output.append("{\"type\":\""); + output.append(dataTypeKey); + output.append("\",\"data\":[["); + for (int i = 0; i < numExgChannels; i++) { + for (int j = 0; j < NUM_FFT_BINS_TO_SEND; j++) { + output.append(str(fftBuff[i].getBand(j))); + if (j != NUM_FFT_BINS_TO_SEND - 1) { + output.append(","); + } + } + if (i != numExgChannels - 1) { + output.append("],["); + } else { + output.append("]]}\r\n"); + } + } + outputUsingProtocol(output.toString()); + } + + @Override + protected void sendBandPowersAllChannels() { + output2dArrayUDP(dataAccumulator.getAllBandPowerData()); + } + + @Override + protected void sendNormalizedBandPowerData() { + final float[] normalizedBandPowerData = dataAccumulator.getNormalizedBandPowerData(); + output1dArrayUDP(normalizedBandPowerData); + } + + @Override + protected void sendEMGData() { + final float[] emgValues = dataAccumulator.getEmgNormalizedValues(); + output1dArrayUDP(emgValues); + } + + @Override + protected void sendAccelerometerData() { + output2dArrayUDP(dataAccumulator.getAccelBuffer()); + } + + @Override + protected void sendAnalogData() { + output2dArrayUDP(dataAccumulator.getAnalogBuffer()); + } + + @Override + protected void sendDigitalData() { + output2dArrayUDP(dataAccumulator.getDigitalBuffer()); + } + + @Override + protected void sendPulseData() { + final int numDataPoints = 2; + final int bpm = dataAccumulator.getPulseSensorBPM(); + final int ibi = dataAccumulator.getPulseSensorIBI(); + StringBuilder output = new StringBuilder(); + output.append("{\"type\":\""); + output.append(dataTypeKey); + output.append("\",\"data\":["); + output.append(str(bpm)); + output.append(","); + output.append(str(ibi)); + output.append("]}\r\n"); + outputUsingProtocol(output.toString()); + } + + @Override + protected void sendEMGJoystickData() { + final float[] emgJoystickXY = dataAccumulator.getEMGJoystickXY(); + output1dArrayUDP(emgJoystickXY); + } + + @Override + protected void sendMarkerData() { + final float[] markerData = dataAccumulator.getMarkerBuffer(); + output1dArrayUDP(markerData); + } + + private void outputUsingProtocol(String data) { + try { + udp.send(data, ip, port); + } catch (Exception e) { + println(e.getMessage()); + } + } + + private void output2dArrayUDP(float[][] dataBuffer) { + StringBuilder output = new StringBuilder(); + output.append("{\"type\":\""); + output.append(dataTypeKey); + output.append("\",\"data\":["); + for (int i = 0; i < dataBuffer.length; i++) { + output.append("["); + for (int j = 0; j < dataBuffer[i].length; j++) { + float data = dataBuffer[i][j]; + //Formatting in this way is resilient to internationalization + String dataFormatted = threeDecimalPlaces.format(data); + output.append(dataFormatted); + if (j != dataBuffer[i].length - 1) { + output.append(","); + } + } + String channelArrayEnding = i != dataBuffer.length - 1 ? "]," : "]"; + output.append(channelArrayEnding); + } + output.append("]}\r\n"); + outputUsingProtocol(output.toString()); + } + + private void output2dArrayUDP(int[][] dataBuffer) { + StringBuilder output = new StringBuilder(); + output.append("{\"type\":\""); + output.append(dataTypeKey); + output.append("\",\"data\":["); + for (int i = 0; i < dataBuffer.length; i++) { + output.append("["); + for (int j = 0; j < dataBuffer[i].length; j++) { + int data = dataBuffer[i][j]; + String dataFormatted = String.format("%d", data); + output.append(dataFormatted); + if (j != dataBuffer[i].length - 1) { + output.append(","); + } + } + String channelArrayEnding = i != dataBuffer.length - 1 ? "]," : "]"; + output.append(channelArrayEnding); + } + output.append("]}\r\n"); + outputUsingProtocol(output.toString()); + } + + private void output1dArrayUDP(float[] dataBuffer) { + StringBuilder output = new StringBuilder(); + output.append("{\"type\":\""); + output.append(dataTypeKey); + output.append("\",\"data\":["); + for (int i = 0; i < dataBuffer.length; i++) { + float data = dataBuffer[i]; + //Formatting in this way is resilient to internationalization + String dataFormatted = threeDecimalPlaces.format(data); + output.append(dataFormatted); + if (i != dataBuffer.length - 1) { + output.append(","); + } + } + output.append("]}\r\n"); + outputUsingProtocol(output.toString()); + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkingDataAccumulator.pde b/OpenBCI_GUI/NetworkingDataAccumulator.pde new file mode 100644 index 000000000..ec4477a9d --- /dev/null +++ b/OpenBCI_GUI/NetworkingDataAccumulator.pde @@ -0,0 +1,319 @@ +public class NetworkingDataAccumulator { + + // These LinkedLists are used to accumulate data from the board in the main thread. + private LinkedList timeSeriesQueue; + private LinkedList filteredTimeSeriesQueue; + private LinkedList markerQueue; + private LinkedList accelerometerQueue; + private LinkedList digitalQueue; + private LinkedList analogQueue; + + // These buffers are used to store data that is consumed and sent by networking threads. + private float[][] timeSeriesBuffer; + private float[][] filteredTimeSeriesBuffer; + private float[] markerBuffer; + private float[][] accelerometerBuffer; + private int[][] digitalBuffer; + private float[][] analogBuffer; + + // These are used as flags to indicate when there is new data to send. Read by networking threads. + public AtomicBoolean[] networkingFrameLocks = new AtomicBoolean[NETWORKING_STREAMS_COUNT]; + public AtomicBoolean newTimeSeriesDataToSend = new AtomicBoolean(false); + public AtomicBoolean newTimeSeriesDataToSendFiltered = new AtomicBoolean(false); + public AtomicBoolean newMarkerDataToSend = new AtomicBoolean(false); + public AtomicBoolean newAccelDataToSend = new AtomicBoolean(false); + public AtomicBoolean newDigitalDataToSend = new AtomicBoolean(false); + public AtomicBoolean newAnalogDataToSend = new AtomicBoolean(false); + + private long startTime; + + public NetworkingDataAccumulator() { + Arrays.fill(networkingFrameLocks, new AtomicBoolean(false)); + + initNetworkingDataBuffers(); + } + + // Call this function in DataProcessing.pde to update the buffers + public void update() { + if (!currentBoard.isStreaming()) { + return; + } + + accumulateNewData(); + prepareDataToSend(); + } + + private void initNetworkingDataBuffers() { + + timeSeriesBuffer = new float[currentBoard.getNumEXGChannels()][nPointsPerUpdate]; + timeSeriesQueue = new LinkedList(); + + filteredTimeSeriesBuffer = new float[currentBoard.getNumEXGChannels()][nPointsPerUpdate]; + filteredTimeSeriesQueue = new LinkedList(); + + markerBuffer = new float[nPointsPerUpdate]; + markerQueue = new LinkedList(); + + if (currentBoard instanceof AccelerometerCapableBoard) { + AccelerometerCapableBoard accelBoard = (AccelerometerCapableBoard)currentBoard; + accelerometerBuffer = new float[accelBoard.getAccelerometerChannels().length][nPointsPerUpdate]; + accelerometerQueue = new LinkedList(); + } + + if (currentBoard instanceof DigitalCapableBoard) { + DigitalCapableBoard digitalBoard = (DigitalCapableBoard)currentBoard; + digitalBuffer = new int[digitalBoard.getDigitalChannels().length][nPointsPerUpdate]; + digitalQueue = new LinkedList(); + } + + if (currentBoard instanceof AnalogCapableBoard) { + AnalogCapableBoard analogBoard = (AnalogCapableBoard)currentBoard; + analogBuffer = new float[analogBoard.getAnalogChannels().length][nPointsPerUpdate]; + analogQueue = new LinkedList(); + } + } + + public void compareAndSetNetworkingFrameLocks() { + for (int i = 0; i < networkingFrameLocks.length; i++) { + networkingFrameLocks[i].compareAndSet(false, true); + } + } + + private void accumulateNewData() { + double[][] newData = currentBoard.getFrameData(); + int[] exgChannels = currentBoard.getEXGChannels(); + int markerChannel = currentBoard.getMarkerChannel(); + + if (newData[exgChannels[0]].length == 0) { + return; + } + + int start = dataProcessingFilteredBuffer[0].length - newData[exgChannels[0]].length; + + for (int iSample = 0; iSample < newData[exgChannels[0]].length; iSample++) { + + double[] sample = new double[exgChannels.length]; + float[] sample_filtered = new float[exgChannels.length]; + + for (int iChan = 0; iChan < exgChannels.length; iChan++) { + sample[iChan] = newData[exgChannels[iChan]][iSample]; + sample_filtered[iChan] = dataProcessingFilteredBuffer[iChan][start + iSample]; + } + timeSeriesQueue.add(sample); + filteredTimeSeriesQueue.add(sample_filtered); + markerQueue.add(newData[markerChannel][iSample]); + + if (currentBoard instanceof AccelerometerCapableBoard) { + AccelerometerCapableBoard accelBoard = (AccelerometerCapableBoard) currentBoard; + int[] accelChannels = accelBoard.getAccelerometerChannels(); + double[] accelSample = new double[accelChannels.length]; + for (int iChan = 0; iChan < accelChannels.length; iChan++) { + accelSample[iChan] = newData[accelChannels[iChan]][iSample]; + } + accelerometerQueue.add(accelSample); + } + + if (currentBoard instanceof DigitalCapableBoard) { + DigitalCapableBoard digitalBoard = (DigitalCapableBoard) currentBoard; + if (digitalBoard.isDigitalActive()) { + int[] digitalChannels = digitalBoard.getDigitalChannels(); + double[] digitalSample = new double[digitalChannels.length]; + for (int iChan = 0; iChan < digitalChannels.length; iChan++) { + digitalSample[iChan] = newData[digitalChannels[iChan]][iSample]; + } + digitalQueue.add(digitalSample); + } + } + + if (currentBoard instanceof AnalogCapableBoard) { + AnalogCapableBoard analogBoard = (AnalogCapableBoard) currentBoard; + if (analogBoard.isAnalogActive()) { + int[] analogChannels = analogBoard.getAnalogChannels(); + double[] analogSample = new double[analogChannels.length]; + for (int iChan = 0; iChan < analogChannels.length; iChan++) { + analogSample[iChan] = newData[analogChannels[iChan]][iSample]; + } + analogQueue.add(analogSample); + } + } + } + } + + + private void prepareDataToSend() { + + boolean timeSeriesDataIsReady = timeSeriesQueue.size() >= nPointsPerUpdate; + if (timeSeriesDataIsReady) { + if (!newTimeSeriesDataToSend.get()) { + popDoubleQueueTo2DFloatBuffer(timeSeriesQueue, timeSeriesBuffer, nPointsPerUpdate); + newTimeSeriesDataToSend.set(true); + } else { + popDoubleArrayQueueToMaintainSize(timeSeriesQueue, nPointsPerUpdate); + } + } + + boolean timeSeriesDataIsReadyFiltered = filteredTimeSeriesQueue.size() >= nPointsPerUpdate; + if (timeSeriesDataIsReadyFiltered) { + if (!newTimeSeriesDataToSendFiltered.get()) { + popFloatQueueTo2DFloatBuffer(filteredTimeSeriesQueue, filteredTimeSeriesBuffer, nPointsPerUpdate); + newTimeSeriesDataToSendFiltered.set(true); + } else { + popFloatArrayQueueToMaintainSize(filteredTimeSeriesQueue, nPointsPerUpdate); + } + } + + boolean markerDataIsReady = markerQueue.size() >= nPointsPerUpdate; + if (markerDataIsReady) { + if (!newMarkerDataToSend.get()) { + popDoubleQueueTo1DFloatBuffer(markerQueue, markerBuffer, nPointsPerUpdate); + newMarkerDataToSend.set(true); + } else { + popDoubleQueueToMaintainSize(markerQueue, nPointsPerUpdate); + } + } + + if (currentBoard instanceof AccelerometerCapableBoard) { + boolean accelDataIsReady = accelerometerQueue.size() >= nPointsPerUpdate; + if (accelDataIsReady) { + if (!newAccelDataToSend.get()) { + popDoubleQueueTo2DFloatBuffer(accelerometerQueue, accelerometerBuffer, nPointsPerUpdate); + newAccelDataToSend.set(true); + } else { + popDoubleArrayQueueToMaintainSize(accelerometerQueue, nPointsPerUpdate); + } + } + } + + if (currentBoard instanceof BoardCyton) { + boolean digitalDataIsReady = digitalQueue.size() >= nPointsPerUpdate; + if (digitalDataIsReady) { + if (!newDigitalDataToSend.get()) { + popDoubleQueueTo2DIntBuffer(digitalQueue, digitalBuffer, nPointsPerUpdate); + newDigitalDataToSend.set(true); + } else { + popDoubleArrayQueueToMaintainSize(digitalQueue, nPointsPerUpdate); + } + } + + boolean analogDataIsReady = analogQueue.size() >= nPointsPerUpdate; + if (analogDataIsReady) { + if (!newAnalogDataToSend.get()) { + popDoubleQueueTo2DFloatBuffer(analogQueue, analogBuffer, nPointsPerUpdate); + newAnalogDataToSend.set(true); + } else { + popDoubleArrayQueueToMaintainSize(analogQueue, nPointsPerUpdate); + } + } + } + } + + private void popDoubleQueueTo2DFloatBuffer(LinkedList queue, float[][] buffer, int pointsPerUpdate) { + for (int iSample = 0; iSample < pointsPerUpdate; iSample++) { + double[] sample = queue.pop(); + + for (int iChan = 0; iChan < sample.length; iChan++) { + buffer[iChan][iSample] = (float) sample[iChan]; + } + } + } + + private void popFloatQueueTo2DFloatBuffer(LinkedList queue, float[][] buffer, int pointsPerUpdate) { + for (int iSample = 0; iSample < pointsPerUpdate; iSample++) { + float[] sample = queue.pop(); + + for (int iChan = 0; iChan < sample.length; iChan++) { + buffer[iChan][iSample] = (float) sample[iChan]; + } + } + } + + private void popDoubleQueueTo2DIntBuffer(LinkedList queue, int[][] buffer, int pointsPerUpdate) { + for (int iSample = 0; iSample < pointsPerUpdate; iSample++) { + double[] sample = queue.pop(); + + for (int iChan = 0; iChan < sample.length; iChan++) { + buffer[iChan][iSample] = (int) sample[iChan]; + } + } + } + + private void popDoubleQueueTo1DFloatBuffer(LinkedList queue, float[] buffer, int pointsPerUpdate) { + for (int iSample = 0; iSample < pointsPerUpdate; iSample++) { + buffer[iSample] = queue.pop().floatValue(); + } + } + + private void popDoubleArrayQueueToMaintainSize(LinkedList queue, int pointsPerUpdate) { + for (int iSample = 0; iSample < pointsPerUpdate; iSample++) { + queue.pop(); + } + } + + private void popFloatArrayQueueToMaintainSize(LinkedList queue, int pointsPerUpdate) { + for (int iSample = 0; iSample < pointsPerUpdate; iSample++) { + queue.pop(); + } + } + + private void popDoubleQueueToMaintainSize(LinkedList queue, int pointsPerUpdate) { + for (int iSample = 0; iSample < pointsPerUpdate; iSample++) { + queue.pop(); + } + } + + public float[][] getTimeSeriesRawBuffer() { + return timeSeriesBuffer; + } + + public float[][] getTimeSeriesFilteredBuffer() { + return filteredTimeSeriesBuffer; + } + + public float[] getMarkerBuffer() { + return markerBuffer; + } + + public float[][] getAccelBuffer() { + return accelerometerBuffer; + } + + public int[][] getDigitalBuffer() { + return digitalBuffer; + } + + public float[][] getAnalogBuffer() { + return analogBuffer; + } + + public ddf.minim.analysis.FFT[] getFFTBuffer() { + return fftBuff; + } + + public float[][] getAllBandPowerData() { + return dataProcessing.avgPowerInBins; + } + + public float[] getNormalizedBandPowerData() { + return ((W_BandPower) widgetManager.getWidget("W_BandPower")).getNormalizedBPSelectedChannels(); + } + + public float[] getEmgNormalizedValues() { + return dataProcessing.emgSettings.values.getNormalizedValues(); + } + + public int getPulseSensorBPM() { + return ((W_PulseSensor) widgetManager.getWidget("W_PulseSensor")).getBPM(); + } + + public int getPulseSensorIBI() { + return ((W_PulseSensor) widgetManager.getWidget("W_PulseSensor")).getIBI(); + } + + public int getFocusValueExceedsThreshold() { + return ((W_Focus) widgetManager.getWidget("W_Focus")).getMetricExceedsThreshold(); + } + + public float[] getEMGJoystickXY() { + return ((W_EmgJoystick) widgetManager.getWidget("W_EmgJoystick")).getJoystickXY(); + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkingEnums.pde b/OpenBCI_GUI/NetworkingEnums.pde new file mode 100644 index 000000000..e03b0cccb --- /dev/null +++ b/OpenBCI_GUI/NetworkingEnums.pde @@ -0,0 +1,94 @@ +public enum NetworkProtocol implements IndexingInterface { + UDP (0, "UDP"), + OSC (1, "OSC"), + LSL (2, "LSL"), + SERIAL (3, "Serial"); + + private int index; + private String label; + private static final NetworkProtocol[] VALUES = values(); + + NetworkProtocol(int index, String label) { + this.index = index; + this.label = label; + } + + public int getIndex() { + return index; + } + + public String getString() { + return label; + } + + public static NetworkProtocol getByIndex(int _index) { + for (NetworkProtocol protocol : NetworkProtocol.values()) { + if (protocol.getIndex() == _index) { + return protocol; + } + } + return null; + } + + public static NetworkProtocol getByString(String _name) { + for (NetworkProtocol protocol : NetworkProtocol.values()) { + if (protocol.getString() == _name) { + return protocol; + } + } + return null; + } +} + +public enum NetworkDataType implements IndexingInterface { + NONE (-1, "None", null, null), + TIME_SERIES_FILTERED (0, "TimeSeriesFilt", "timeSeriesFiltered", "time-series-filtered"), + TIME_SERIES_RAW (1, "TimeSeriesRaw", "timeSeriesRaw", "time-series-raw"), + FOCUS (2, "Focus", "focus", "focus"), + FFT (3, "FFT", "fft", "fft"), + EMG (4, "EMG", "emg", "emg"), + AVG_BAND_POWERS (5, "AvgBandPowers", "avgBandPowers", "avg-band-powers"), + BAND_POWERS (6, "BandPowers", "bandPowers", "band-powers"), + ACCEL_AUX (7, "AccelAux", "accelAux", "accel-aux"), + PULSE (8, "Pulse", "pulse", "pulse"), + EMG_JOYSTICK (9, "EMGJoystick", "emgJoystick", "emg-joystick"), + MARKER (10, "Marker", "marker", "marker"); + + private int index; + private String label; + private String udpKey; + private String oscKey; + private static final NetworkDataType[] VALUES = values(); + + NetworkDataType(int index, String label, String udpKey, String oscKey) { + this.index = index; + this.label = label; + this.udpKey = udpKey; + this.oscKey = oscKey; + } + + public int getIndex() { + return index; + } + + public String getString() { + return label; + } + + public String getUDPKey() { + return udpKey; + } + + public String getOSCKey() { + return oscKey; + } + + public static NetworkDataType getByString(String _name) { + for (NetworkDataType dataType : NetworkDataType.values()) { + if (dataType.getString() == _name) { + return dataType; + } + } + return null; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkingSettings.pde b/OpenBCI_GUI/NetworkingSettings.pde new file mode 100644 index 000000000..b83ccf190 --- /dev/null +++ b/OpenBCI_GUI/NetworkingSettings.pde @@ -0,0 +1,341 @@ +public final int NETWORKING_STREAMS_COUNT = 12; +public boolean networkingSettingsChanged = false; + +public class NetworkingSettings { + + private NetworkStreamOut[] streams = new NetworkStreamOut[NETWORKING_STREAMS_COUNT]; + + private NetworkingSettingsValues values; + + private NetworkProtocol activeNetworkProtocol = null; + + public NetworkingSettings() { + values = new NetworkingSettingsValues(); + } + + public String getJson() { + Gson gson = new GsonBuilder().create(); + return gson.toJson(values); + } + + public void loadJson(String json) { + Gson gson = new Gson(); + NetworkingSettingsValues newValues = gson.fromJson(json, NetworkingSettingsValues.class); + if (newValues == null) { + outputError("Error loading Networking Settings from JSON"); + return; + } + values = newValues; + networkingSettingsChanged = true; + } + + public boolean getNetworkingIsStreaming() { + return activeNetworkProtocol != null; + } + + public NetworkProtocol getActiveNetworkProtocol() { + return activeNetworkProtocol; + } + + public void initializeStreams() { + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + NetworkDataType dataType = values.getDataType(i); + if (dataType == NetworkDataType.NONE) { + streams[i] = null; + continue; + } + + switch (values.protocol) { + case OSC: + String baseAddress = "/openbci"; + String oscIP = values.getOSCIp(i); + int oscPort = Integer.parseInt(values.getOSCPort(i)); + streams[i] = new NetworkStreamOutOSC(dataType, oscIP, oscPort, baseAddress, i); + break; + case UDP: + String udpIP = values.getUDPIp(i); + int udpPort = Integer.parseInt(values.getUDPPort(i)); + streams[i] = new NetworkStreamOutUDP(dataType, udpIP, udpPort, i); + break; + case LSL: + String lslName = values.getLSLName(i); + String lslType = values.getLSLType(i); + int numLslDataPoints = getDataTypeNumChanLSL(dataType); + streams[i] = new NetworkStreamOutLSL(dataType, lslName, lslType, numLslDataPoints, i); + break; + case SERIAL: + if (i > 0) { + streams[i] = null; + continue; + } + String serialComPort = values.getSerialPort(); + int serialBaudRate = Integer.parseInt(values.getSerialBaud()); + streams[i] = new NetworkStreamOutSerial(dataType, serialComPort, serialBaudRate, ourApplet); + break; + } + } + } + + public void startNetwork() { + activeNetworkProtocol = values.getProtocol(); + for (NetworkStreamOut stream : streams) { + if (stream != null) { + stream.start(); + } + } + } + + public void stopNetwork() { + activeNetworkProtocol = null; + for (NetworkStreamOut stream : streams) { + if (stream != null) { + stream.quit(); + stream = null; + } + } + } + + private int getDataTypeNumChanLSL(NetworkDataType dataType) { + switch (dataType) { + case TIME_SERIES_FILTERED: + case TIME_SERIES_RAW: + case EMG: + return currentBoard.getNumEXGChannels(); + case FOCUS: + case MARKER: + return 1; + case FFT: + return 125; + case AVG_BAND_POWERS: + return 5; + case BAND_POWERS: + //Send out band powers for each channel sequentially + //Prepend channel number to each array + return 5 + 1; + case PULSE: + case EMG_JOYSTICK: + return 2; + case ACCEL_AUX: + if (currentBoard instanceof AccelerometerCapableBoard) { + AccelerometerCapableBoard accelBoard = (AccelerometerCapableBoard) currentBoard; + if (accelBoard.isAccelerometerActive()) { + return accelBoard.getAccelerometerChannels().length; + } + } + if (currentBoard instanceof AnalogCapableBoard) { + AnalogCapableBoard analogBoard = (AnalogCapableBoard) currentBoard; + if (analogBoard.isAnalogActive()) { + return analogBoard.getAnalogChannels().length; + } + } + if (currentBoard instanceof DigitalCapableBoard) { + DigitalCapableBoard digitalBoard = (DigitalCapableBoard) currentBoard; + if (digitalBoard.isDigitalActive()) { + return digitalBoard.getDigitalChannels().length; + } + } + default: + throw new IllegalArgumentException("IllegalArgumentException: Error detecting number of channels for LSL stream data... please fix!"); + } + } + + public NetworkingSettingsValues getValues() { + return values; + } +} + +public class NetworkingSettingsValues { + + private NetworkProtocol protocol; + + private final NetworkDataType[] DATA_TYPES = new NetworkDataType[NETWORKING_STREAMS_COUNT]; + private LinkedList dataTypeNames; + + private final String[] OSC_IPS = new String[NETWORKING_STREAMS_COUNT]; + private final String[] OSC_PORTS = new String[NETWORKING_STREAMS_COUNT]; + private final String[] OSC_IP_DEFAULTS = new String[NETWORKING_STREAMS_COUNT]; + private final String[] OSC_PORT_DEFAULTS = new String[NETWORKING_STREAMS_COUNT]; + + private final String[] UDP_IPS = new String[NETWORKING_STREAMS_COUNT]; + private final String[] UDP_PORTS = new String[NETWORKING_STREAMS_COUNT]; + private final String[] UDP_IP_DEFAULTS = new String[NETWORKING_STREAMS_COUNT]; + private final String[] UDP_PORT_DEFAULTS = new String[NETWORKING_STREAMS_COUNT]; + + private final String[] LSL_NAMES = new String[NETWORKING_STREAMS_COUNT]; + private final String[] LSL_TYPES = new String[NETWORKING_STREAMS_COUNT]; + private final String[] LSL_NAME_DEFAULTS = new String[NETWORKING_STREAMS_COUNT]; + private final String[] LSL_TYPE_DEFAULTS = new String[NETWORKING_STREAMS_COUNT]; + + private LinkedList baudRates = new LinkedList(Arrays.asList("57600", "115200", "250000", "500000")); + private String baudRate = baudRates.get(0); + private String serialPort = "None"; + + public NetworkingSettingsValues() { + protocol = NetworkProtocol.UDP; + + initDataTypeNames(); + initTextfieldDefaults(); + initDataTypeDefaults(); + } + + private void initDataTypeDefaults() { + DATA_TYPES[0] = NetworkDataType.TIME_SERIES_FILTERED; + DATA_TYPES[1] = NetworkDataType.AVG_BAND_POWERS; + DATA_TYPES[2] = NetworkDataType.BAND_POWERS; + DATA_TYPES[3] = NetworkDataType.FFT; + DATA_TYPES[4] = NetworkDataType.EMG; + DATA_TYPES[5] = NetworkDataType.EMG_JOYSTICK; + DATA_TYPES[6] = NetworkDataType.FOCUS; + DATA_TYPES[7] = NetworkDataType.MARKER; + DATA_TYPES[8] = NetworkDataType.ACCEL_AUX; + DATA_TYPES[9] = NetworkDataType.NONE; + DATA_TYPES[10] = NetworkDataType.NONE; + DATA_TYPES[11] = NetworkDataType.NONE; + if (currentBoard instanceof BoardCyton) { + DATA_TYPES[9] = NetworkDataType.PULSE; + } + } + + private void initDataTypeNames() { + dataTypeNames = new LinkedList(Arrays.asList( + NetworkDataType.NONE.getString(), + NetworkDataType.TIME_SERIES_FILTERED.getString(), + NetworkDataType.AVG_BAND_POWERS.getString(), + NetworkDataType.BAND_POWERS.getString(), + NetworkDataType.FFT.getString(), + NetworkDataType.EMG.getString(), + NetworkDataType.EMG_JOYSTICK.getString(), + NetworkDataType.FOCUS.getString(), + NetworkDataType.MARKER.getString(), + NetworkDataType.ACCEL_AUX.getString(), + NetworkDataType.TIME_SERIES_RAW.getString(), + NetworkDataType.PULSE.getString() + )); + if (!(currentBoard instanceof BoardCyton)) { + dataTypeNames.remove(NetworkDataType.PULSE.getString()); + } + } + + private void initTextfieldDefaults() { + final int STARTING_PORT = 12345; + + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + LSL_TYPE_DEFAULTS[i] = "EXG"; + } + + LSL_TYPE_DEFAULTS[1] = "EEG"; + LSL_TYPE_DEFAULTS[2] = "EEG"; + LSL_TYPE_DEFAULTS[3] = "FFT"; + LSL_TYPE_DEFAULTS[4] = "EMG"; + LSL_TYPE_DEFAULTS[5] = "EMG"; + LSL_TYPE_DEFAULTS[6] = "FOCUS"; + LSL_TYPE_DEFAULTS[7] = "MARKER"; + LSL_TYPE_DEFAULTS[8] = "AUX"; + if (currentBoard instanceof BoardCyton) { + LSL_TYPE_DEFAULTS[9] = "PULSE"; + } + + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + OSC_IP_DEFAULTS[i] = "127.0.0.1"; + OSC_PORT_DEFAULTS[i] = Integer.toString(STARTING_PORT + i); + UDP_IP_DEFAULTS[i] = "127.0.0.1"; + UDP_PORT_DEFAULTS[i] = Integer.toString(STARTING_PORT + i); + LSL_NAME_DEFAULTS[i] = "obci_stream_" + i; + + OSC_IPS[i] = OSC_IP_DEFAULTS[i]; + OSC_PORTS[i] = OSC_PORT_DEFAULTS[i]; + UDP_IPS[i] = UDP_IP_DEFAULTS[i]; + UDP_PORTS[i] = UDP_PORT_DEFAULTS[i]; + LSL_NAMES[i] = LSL_NAME_DEFAULTS[i]; + LSL_TYPES[i] = LSL_TYPE_DEFAULTS[i]; + } + } + + public NetworkProtocol getProtocol() { + return protocol; + } + + public LinkedList getAllDataTypeNames() { + return dataTypeNames; + } + + public NetworkDataType getDataType(int i) { + return DATA_TYPES[i]; + } + + public String getOSCIp(int i) { + return OSC_IPS[i]; + } + + public String getOSCPort(int i) { + return OSC_PORTS[i]; + } + + public String getUDPIp(int i) { + return UDP_IPS[i]; + } + + public String getUDPPort(int i) { + return UDP_PORTS[i]; + } + + public String getLSLName(int i) { + return LSL_NAMES[i]; + } + + public String getLSLType(int i) { + return LSL_TYPES[i]; + } + + public LinkedList getBaudRateList() { + return baudRates; + } + + public String getSerialBaud() { + return baudRate; + } + + public String getSerialPort() { + return serialPort; + } + + public void setProtocol(int i) { + protocol = NetworkProtocol.getByIndex(i); + } + + public void setDataType(int i, int value) { + DATA_TYPES[i] = NetworkDataType.getByString(dataTypeNames.get(value)); + } + + public void setOSCIp(int i, String ip) { + OSC_IPS[i] = ip; + } + + public void setOSCPort(int i, String port) { + OSC_PORTS[i] = port; + } + + public void setUDPIp(int i, String ip) { + UDP_IPS[i] = ip; + } + + public void setUDPPort(int i, String port) { + UDP_PORTS[i] = port; + } + + public void setLSLName(int i, String name) { + LSL_NAMES[i] = name; + } + + public void setLSLType(int i, String type) { + LSL_TYPES[i] = type; + } + + public void setSerialPort(String port) { + serialPort = port; + } + + public void setSerialBaud(String _baudRate) { + baudRate = _baudRate; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/NetworkingUI.pde b/OpenBCI_GUI/NetworkingUI.pde new file mode 100644 index 000000000..c19b0f6f8 --- /dev/null +++ b/OpenBCI_GUI/NetworkingUI.pde @@ -0,0 +1,943 @@ + +/////////////////////////////////////////////////////////////////////////////// +// // +// Networking UI (formerly Networking Widget) // +// // +// This UI provides networking capabilities in the OpenBCI GUI. // +// The networking protocols can be used for outputting data // +// from the OpenBCI GUI to any program that can receive UDP, OSC, // +// or LSL input, such as Matlab, MaxMSP, Python, C/C++, etc. // +// // +// The protocols included are: UDP, OSC, LSL, and Serial // +// // +// // +// Created by: Gabriel Ibagon (github.com/gabrielibagon), January 2017 // +// Refactored: Richard Waltman, June-August 2023 // +// Converted to popup window: Richard Waltman, December 2023 // +// // +/////////////////////////////////////////////////////////////////////////////// + +public boolean networkingUIPopupIsOpen = false; + +class NetworkingUI extends PApplet implements Runnable { + + private final String HEADER_MESSAGE = "Networking UI"; + private color backgroundColor = GREY_235; + + private ControlP5 nwCp5; + + NetworkingSettingsValues nwValues; + + private NetworkingGrid grid; + private final int TOP_PADDING = 15; + private final int BOT_PADDING = 50; + private final int HORIZONTAL_PADDING = 15; + private final int UI_ELEMENT_PADDING = 5; + private final int NUM_GRID_ROWS = 12; + private final int NUM_GRID_COLUMNS = 7; + private final int ITEM_HEIGHT = 22; + private final int TEXTFIELD_WIDTH = 125; + private final int TEXTFIELD_HEIGHT = ITEM_HEIGHT; + private final int START_STOP_BUTTON_WIDTH = 200; + + private final int GRID_HEIGHT = (TEXTFIELD_HEIGHT + (UI_ELEMENT_PADDING * 2)) * NUM_GRID_ROWS; + private final int GRID_WIDTH = (TEXTFIELD_WIDTH * NUM_GRID_COLUMNS) + (UI_ELEMENT_PADDING * 2 * NUM_GRID_COLUMNS); + private final int WIDTH = GRID_WIDTH + (HORIZONTAL_PADDING * 2); + private final int HEIGHT = TOP_PADDING + GRID_HEIGHT + BOT_PADDING; + + private int x = 0; + private int y = 0; + private int w = WIDTH; + private int h = HEIGHT; + + private Button startButton; + private Button guideButton; + private Button dataOutputsButton; + private ScrollableList protocolDropdown; + + private final String NETWORKING_GUIDE_URL = "https://docs.openbci.com/Software/OpenBCISoftware/GUIWidgets/#networking"; + private final String NETWORKING_DATA_OUTPUTS_URL = "https://docs.google.com/document/d/e/2PACX-1vT-JXd4XyheeK_YKw_J22-nK1kDlsEGgDPnAd1FolEMV5TDBZjBZT-mWh6Jbfpxs1BfrTD6EUYhnC6t/pub"; + + private final ScrollableList[] DATATYPE_DROPDOWNS = new ScrollableList[NETWORKING_STREAMS_COUNT]; + private final Textfield[] FIRST_ROW_TEXTFIELDS = new Textfield[NETWORKING_STREAMS_COUNT]; + private final Textfield[] SECOND_ROW_TEXTFIELDS = new Textfield[NETWORKING_STREAMS_COUNT]; + private boolean[] firstRowTextfieldWasActive = new boolean[NETWORKING_STREAMS_COUNT]; + private boolean[] secondRowTextfieldWasActive = new boolean[NETWORKING_STREAMS_COUNT]; + + private List serialNetworkingComPorts; + private ScrollableList serialPortDropdown; + private ScrollableList serialBaudDropdown; + + NetworkingUI() { + super(); + networkingUIPopupIsOpen = true; + output("Networking UI: Networking UI opened."); + + Thread t = new Thread(this); + t.start(); + } + + @Override + public void run() { + PApplet.runSketch(new String[] {HEADER_MESSAGE}, this); + } + + @Override + void settings() { + size(w, h); + } + + @Override + void setup() { + + surface.setTitle(HEADER_MESSAGE); + surface.setAlwaysOnTop(true); + surface.setResizable(false); + + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.toFront(); + frame.requestFocus(); + + nwValues = dataProcessing.networkingSettings.getValues(); + + serialNetworkingComPorts = new ArrayList(getComPorts()); + + initializeUI(); + updateUIObjectPositions(); + } + + @Override + public synchronized void draw() { + update(); + + pushStyle(); + background(backgroundColor); + popStyle(); + + grid.draw(); + + pushStyle(); + textAlign(RIGHT, CENTER); + fill(OPENBCI_DARKBLUE); + RectDimensions protocolCellDims = grid.getCellDims(0, 5); + textFont(p5, 12); + text("Protocol", protocolCellDims.x + protocolCellDims.w - UI_ELEMENT_PADDING, protocolCellDims.y + protocolCellDims.h / 2 - 2); + popStyle(); + + //Draw cp5 objects on top of everything + try { + nwCp5.draw(); + } catch (ConcurrentModificationException e) { + outputError("Networking UI: Error drawing cp5: " + e.getMessage()); + } + + } + + @Override + void exit() { + dispose(); + networkingUIPopupIsOpen = false; + } + + // Dispose of the popup window externally + public void exitPopup() { + output("Networking UI: Closing Networking UI."); + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.dispose(); + networkingUIPopupIsOpen = false; + } + + private void update() { + showApplicablenwcp5Elements(); + + if (nwValues.getProtocol() == NetworkProtocol.SERIAL) { + // For serial mode, disable fft output by switching to bandpower instead + disableCertainSerialOutputs(); + } else { + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + textfieldUpdateHelper.checkTextfield(FIRST_ROW_TEXTFIELDS[i]); + textfieldUpdateHelper.checkTextfield(SECOND_ROW_TEXTFIELDS[i]); + } + } + + if (networkingSettingsChanged) { + println("Networking UI: Networking settings changed, updating UI..."); + updateAllUIElements(); + networkingSettingsChanged = false; + } + } + + private void initializeUI() { + nwCp5 = new ControlP5(this); + nwCp5.setGraphics(this, 0, 0); + nwCp5.setAutoDraw(false); + + grid = new NetworkingGrid(NUM_GRID_ROWS, NUM_GRID_COLUMNS, ITEM_HEIGHT); + setGridTextLabels(); + + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + String firstRowTextfieldString = ""; + String secondRowTextfieldString = ""; + + switch (nwValues.getProtocol()) { + case OSC: + firstRowTextfieldString = nwValues.getOSCIp(i); + secondRowTextfieldString = nwValues.getOSCPort(i); + break; + case UDP: + firstRowTextfieldString = nwValues.getUDPIp(i); + secondRowTextfieldString = nwValues.getUDPPort(i); + break; + case LSL: + firstRowTextfieldString = nwValues.getLSLName(i); + secondRowTextfieldString = nwValues.getLSLType(i); + break; + case SERIAL: + break; + } + + FIRST_ROW_TEXTFIELDS[i] = createTextField(i, "firstRowTextfield" + i, firstRowTextfieldString); + SECOND_ROW_TEXTFIELDS[i] = createTextField(i, "secondRowTextfield" + i, secondRowTextfieldString); + } + + createPortDropdown(); + createBaudDropdown(); + + createStartButton(); + + for (int i = NETWORKING_STREAMS_COUNT - 1; i >= 0; i--) { + DATATYPE_DROPDOWNS[i] = createDatatypeDropdown(i, "dataType_" + i, nwValues.getDataType(i).getString()); + } + + createGuideButton(); + createDataOutputsButton(); + createProtocolDropdown(); + + boolean showAllDataTypeDropdowns = nwValues.getProtocol() != NetworkProtocol.SERIAL; + showDataTypeDropdownsTwoThroughTen(showAllDataTypeDropdowns); + } + + private void updateUIObjectPositions() { + grid.setDim(x + HORIZONTAL_PADDING, y + TOP_PADDING, GRID_WIDTH); + grid.setTableHeight(GRID_HEIGHT); + grid.dynamicallySetTextVerticalPadding(3, 0); + grid.setHorizontalCenterTextInCells(true); + grid.setDrawTableInnerLines(false); + grid.setDrawTableBorder(false); + + final int uiPadding = UI_ELEMENT_PADDING; + + RectDimensions guideButtonDims = grid.getCellDims(0, 0); + guideButton.setPosition(guideButtonDims.x + uiPadding, guideButtonDims.y + uiPadding); + + RectDimensions dataOutputsButtonDims = grid.getCellDims(0, 1); + dataOutputsButton.setPosition(dataOutputsButtonDims.x + uiPadding, dataOutputsButtonDims.y + uiPadding); + + RectDimensions protocolDropdownDims = grid.getCellDims(0, 6); + protocolDropdown.setPosition(protocolDropdownDims.x + uiPadding, protocolDropdownDims.y + uiPadding); + + RectDimensions startButtonDims = grid.getCellDims(11, 2); + final int startButtonX = x + (w / 2) - (START_STOP_BUTTON_WIDTH / 2); + startButton.setPosition(startButtonX, startButtonDims.y + uiPadding); + + final int dropdownsItemsToShow = nwValues.getAllDataTypeNames().size() + 1; + final int dropdownHeight = dropdownsItemsToShow * ITEM_HEIGHT; + + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + + final int datatypeGridRow = i < NETWORKING_STREAMS_COUNT / 2 ? 3 : 7; + final int gridColumn = i % (NETWORKING_STREAMS_COUNT / 2) + 1; + + RectDimensions datatypeCellDims = grid.getCellDims(datatypeGridRow, gridColumn); + DATATYPE_DROPDOWNS[i].setPosition(datatypeCellDims.x + uiPadding, datatypeCellDims.y + uiPadding); + + RectDimensions firstTextfieldDims = grid.getCellDims(datatypeGridRow + 1, gridColumn); + RectDimensions secondTextfieldDims = grid.getCellDims(datatypeGridRow + 2, gridColumn); + + FIRST_ROW_TEXTFIELDS[i].setPosition(firstTextfieldDims.x + uiPadding, firstTextfieldDims.y + uiPadding); + SECOND_ROW_TEXTFIELDS[i].setPosition(secondTextfieldDims.x + uiPadding, secondTextfieldDims.y + uiPadding); + + if (i == 0) { + serialBaudDropdown.setPosition(firstTextfieldDims.x + uiPadding, firstTextfieldDims.y + uiPadding); + serialPortDropdown.setPosition(secondTextfieldDims.x + uiPadding, secondTextfieldDims.y + uiPadding); + } + } + } + + private LinkedList getComPorts() { + final SerialPort[] allCommPorts = SerialPort.getCommPorts(); + LinkedList cuCommPorts = new LinkedList(); + for (SerialPort port : allCommPorts) { + // Filter out .tty ports for Mac users, to only show .cu addresses + if (isMac() && port.getSystemPortName().startsWith("tty")) { + continue; + } + StringBuilder found = new StringBuilder(""); + if (isMac() || isLinux()) + found.append("/dev/"); + found.append(port.getSystemPortName()); + cuCommPorts.add(found.toString()); + } + return cuCommPorts; + } + + // Shows and Hides appropriate nwCp5 elements within widget + public void showApplicablenwcp5Elements() { + boolean isSerialProtocol = nwValues.getProtocol() == NetworkProtocol.SERIAL; + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + FIRST_ROW_TEXTFIELDS[i].setVisible(!isSerialProtocol); + SECOND_ROW_TEXTFIELDS[i].setVisible(!isSerialProtocol); + } + serialPortDropdown.setVisible(isSerialProtocol); + serialBaudDropdown.setVisible(isSerialProtocol); + } + + private Boolean textfieldsAreActive(Textfield[] textfields) { + boolean isActive = false; + for (Textfield tf : textfields) { + if (tf.isFocus()) { + isActive = true; + } + } + return isActive; + } + + /* Create textfields for network parameters */ + private Textfield createTextField( int _streamIndex, String name, String default_text) { + final int streamIndex = _streamIndex; + Textfield tf = nwCp5.addTextfield(name).align(10, 100, 10, 100) // Alignment + .setSize(TEXTFIELD_WIDTH, TEXTFIELD_HEIGHT) // Size of textfield + .setFont(f2) + .setFocus(false) // Deselects textfield + .setColor(OPENBCI_DARKBLUE) + .setColorBackground(color(255, 255, 255)) // text field bg color + .setColorValueLabel(OPENBCI_DARKBLUE) // text color + .setColorForeground(OPENBCI_DARKBLUE) // border color when not selected + .setColorActive(isSelected_color) // border color when selected + .setColorCursor(OPENBCI_DARKBLUE) + .setText(default_text) // Default text in the field + .setCaptionLabel("") // Remove caption label + .setVisible(false) // Initially hidden + .setAutoClear(false) // Autoclear + ; + tf.onDoublePress(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + output("Networking UI: Enter your custom streaming attribute."); + tf.clear(); + } + }); + tf.addCallback(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + String myTextfieldValue = tf.getText(); + boolean isFirstRow = name.startsWith("firstRowTextfield"); + //println("myTextfieldValue = " + myTextfieldValue + ", isFirstRow = " + isFirstRow + ", streamIndex = " + streamIndex); + //Set to default value if the textfield would be blank + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST && myTextfieldValue.equals("")) { + myTextfieldValue = getStoredTextfieldValue(isFirstRow, streamIndex); + setStreamAttributeFromTextfield(isFirstRow, streamIndex, myTextfieldValue); + } + //Pressing ENTER in the Textfield triggers a "Broadcast" + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + //Try to clean up typing accidents from user input in Textfield + String regexReplace = nwValues.getProtocol() == NetworkProtocol.LSL ? "[!@#$%^&()=/*]" : "[A-Za-z!@#$%^&()=/*_]"; + String cleanedTextfieldValue = myTextfieldValue.replaceAll(regexReplace,""); + tf.setText(cleanedTextfieldValue); + setStreamAttributeFromTextfield(isFirstRow, streamIndex, cleanedTextfieldValue); + } + if (tf.isActive()) { + if (isFirstRow) { + firstRowTextfieldWasActive[streamIndex] = true; + } else { + secondRowTextfieldWasActive[streamIndex] = true; + } + } + } + }); + //Autogenerate session name if user leaves textfield and value is null + tf.onReleaseOutside(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + String myTextfieldValue = tf.getText(); + boolean isFirstRow = name.startsWith("firstRowTextfield"); + if (!tf.isActive() && tf.getText().equals("")) { + myTextfieldValue = getStoredTextfieldValue(isFirstRow, streamIndex); + tf.setText(myTextfieldValue); + } else { + /// If released outside textfield and a state change has occured, submit, clean, and set the value + if (isFirstRow) { + if (firstRowTextfieldWasActive[streamIndex] != FIRST_ROW_TEXTFIELDS[streamIndex].isActive()) { + tf.submit(); + firstRowTextfieldWasActive[streamIndex] = false; + } + } else { + if (secondRowTextfieldWasActive[streamIndex] != SECOND_ROW_TEXTFIELDS[streamIndex].isActive()) { + tf.submit(); + secondRowTextfieldWasActive[streamIndex] = false; + } + } + } + } + }); + return tf; + } + + private void createStartButton() { + NetworkingSettings nwSettings = dataProcessing.networkingSettings; + startButton = createButton(nwCp5, "startStopNetworkStream", "", + x + w / 2 - 70, y + h - 40, START_STOP_BUTTON_WIDTH, TEXTFIELD_HEIGHT, + 0, p4, 14, TURN_ON_GREEN, OPENBCI_DARKBLUE, BUTTON_HOVER, BUTTON_PRESSED, OBJECT_BORDER_GREY, 0); + startButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + if (!nwSettings.getNetworkingIsStreaming()) { + try { + nwSettings.initializeStreams(); + nwSettings.startNetwork(); + output("Network Stream Started"); + } catch (Exception e) { + e.printStackTrace(); + String exception = e.toString(); + String[] nwError = split(exception, ':'); + outputError("Networking Error - Port: " + nwError[2]); + nwSettings.stopNetwork(); + } + } else { + nwSettings.stopNetwork(); + output("Network Stream Stopped"); + } + updateStartStopButton(); + } + }); + updateStartStopButton(); + startButton.setDescription("Click here to Start and Stop the network stream for the chosen protocol."); + } + + private void updateStartStopButton() { + NetworkingSettings nwSettings = dataProcessing.networkingSettings; + boolean isStreaming = nwSettings.getNetworkingIsStreaming(); + String protocolToDisplay = isStreaming ? + nwSettings.getActiveNetworkProtocol().getString() : + nwValues.getProtocol().getString(); + color buttonColor = isStreaming ? TURN_OFF_RED : TURN_ON_GREEN; + String buttonText = isStreaming ? + "Stop " + protocolToDisplay + " Stream" : + "Start " + protocolToDisplay + " Stream"; + startButton.setColorBackground(buttonColor); + startButton.getCaptionLabel().setText(buttonText); + } + + private void createGuideButton() { + guideButton = createButton(nwCp5, "networkingGuideButton", "Networking Guide", + x, y, TEXTFIELD_WIDTH, ITEM_HEIGHT, p5, 12, colorNotPressed, OPENBCI_DARKBLUE); + guideButton.setBorderColor(OBJECT_BORDER_GREY); + guideButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + openURLInBrowser(NETWORKING_GUIDE_URL); + output("Opening Networking Widget Guide using default browser."); + } + }); + guideButton.setDescription("Click to open the Networking Widget Guide in your default browser."); + } + + private void createDataOutputsButton() { + dataOutputsButton = createButton(nwCp5, "dataOutputsButton", "Data Outputs", + x, y, TEXTFIELD_WIDTH, ITEM_HEIGHT, p5, 12, colorNotPressed, + OPENBCI_DARKBLUE); + dataOutputsButton.setBorderColor(OBJECT_BORDER_GREY); + dataOutputsButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + openURLInBrowser(NETWORKING_DATA_OUTPUTS_URL); + output("Opening Networking Data Outputs Guide using default browser."); + } + }); + dataOutputsButton.setDescription("Click to open the Networking Data Outputs Guide in your default browser."); + } + + private ScrollableList createDatatypeDropdown(int _streamIndex, String name, String default_text) { + final int maxListItemsToShow = 8; + final int streamIndex = _streamIndex; + ScrollableList scrollList = nwCp5.addScrollableList(name) + .setOpen(false) + .setOutlineColor(OPENBCI_DARKBLUE) + .setColorBackground(OPENBCI_BLUE) // text field bg color + .setColorValueLabel(color(255)) // text color + .setColorCaptionLabel(color(255)) + .setColorForeground(color(125)) // border color when not selected + .setColorActive(BUTTON_PRESSED) // border color when selected + // .setColorCursor(color(26,26,26)) + .setSize(TEXTFIELD_WIDTH, maxListItemsToShow * ITEM_HEIGHT)// + maxFreqList.size()) + .setBarHeight(ITEM_HEIGHT) // height of top/primary bar + .setItemHeight(ITEM_HEIGHT) // height of all item/dropdown bars + .addItems(nwValues.getAllDataTypeNames()) // used to be .addItems(maxFreqList) + .setVisible(true); + scrollList.getCaptionLabel() // the caption label is the text object in the primary bar + .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! + .setText(default_text).setFont(h4).setSize(14) + .getStyle().setPaddingTop(4); // need to grab style before affecting the paddingTop + scrollList.getValueLabel() // the value label is connected to the text objects in the dropdown item bars + .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! + .setText(default_text).setFont(h5).setSize(12) // set the font size of the item bars to 14pt + .getStyle() // need to grab style before affecting the paddingTop + .setPaddingTop(3); // 4-pixel vertical offset to center text + scrollList.addCallback(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + int valueIndex = (int)(theEvent.getController()).getValue(); + println("name = " + name + ", value = " + valueIndex + ", streamIndex = " + streamIndex); + nwValues.setDataType(streamIndex, valueIndex); + } + } + }); + return scrollList; + } + + private void createProtocolDropdown() { + List protocolList = EnumHelper.getEnumStrings(NetworkProtocol.class); + protocolDropdown = nwCp5.addScrollableList("networkingProtocolDropdown") + .setOpen(false) + .setOutlineColor(OPENBCI_DARKBLUE) + .setColorBackground(OPENBCI_BLUE) // text field bg color + .setColorValueLabel(color(255)) // text color + .setColorCaptionLabel(color(255)) + .setColorForeground(color(125)) // border color when not selected + .setColorActive(BUTTON_PRESSED) // border color when selected + // .setColorCursor(color(26,26,26)) + .setSize(TEXTFIELD_WIDTH, (protocolList.size() + 1) * (ITEM_HEIGHT))// + maxFreqList.size()) + .setBarHeight(ITEM_HEIGHT) // height of top/primary bar + .setItemHeight(ITEM_HEIGHT) // height of all item/dropdown bars + .addItems(protocolList) // used to be .addItems(maxFreqList) + .setVisible(true); + protocolDropdown.getCaptionLabel() // the caption label is the text object in the primary bar + .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! + .setText(nwValues.getProtocol().getString()).setFont(h4).setSize(14) + .getStyle().setPaddingTop(4); // need to grab style before affecting the paddingTop + protocolDropdown.getValueLabel() // the value label is connected to the text objects in the dropdown item bars + .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! + .setText(nwValues.getProtocol().getString()).setFont(h5).setSize(12) // set the font size of the item bars to 14pt + .getStyle() // need to grab style before affecting the paddingTop + .setPaddingTop(3); // 4-pixel vertical offset to center text + protocolDropdown.addCallback(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + int valueIndex = (int)(theEvent.getController()).getValue(); + nwValues.setProtocol(valueIndex); + updateAllUIElements(); + } + } + }); + } + + private void createBaudDropdown() { + serialBaudDropdown = nwCp5.addScrollableList("baudRate").setOpen(false) + .setOutlineColor(OPENBCI_DARKBLUE).setColorBackground(OPENBCI_BLUE) // text field bg color + .setColorValueLabel(color(255)) // text color + .setColorCaptionLabel(color(255)) + .setColorForeground(color(125)) // border color when not selected + .setColorActive(BUTTON_PRESSED) // border color when selected + // .setColorCursor(color(26,26,26)) + .setSize(TEXTFIELD_WIDTH, (nwValues.getBaudRateList().size() + 1) * (ITEM_HEIGHT))// + maxFreqList.size()) + .setBarHeight(ITEM_HEIGHT) // height of top/primary bar + .setItemHeight(ITEM_HEIGHT) // height of all item/dropdown bars + .addItems(nwValues.getBaudRateList()) // used to be .addItems(maxFreqList) + .setVisible(true); + serialBaudDropdown.getCaptionLabel() // the caption label is the text object in the primary bar + .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! + .setText(nwValues.getSerialBaud()).setFont(h4).setSize(14) + .getStyle() // need to grab style before affecting the paddingTop + .setPaddingTop(4); + serialBaudDropdown.getValueLabel() // the value label is connected to the text objects in the dropdown item bars + .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! + .setText("None").setFont(h5).setSize(12) // set the font size of the item bars to 14pt + .getStyle() // need to grab style before affecting the paddingTop + .setPaddingTop(3); // 4-pixel vertical offset to center text + serialBaudDropdown.addCallback(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + int valueIndex = (int)(theEvent.getController()).getValue(); + String baudRate = nwValues.getBaudRateList().get(valueIndex); + nwValues.setSerialBaud(baudRate); + } + } + }); + } + + private void createPortDropdown() { + boolean noComPortsFound = serialNetworkingComPorts.size() == 0 ? true : false; + String currentPort = nwValues.getSerialPort(); + boolean listContainsCurrentPort = serialNetworkingComPorts.contains(currentPort); + if (noComPortsFound) { + serialNetworkingComPorts.add(currentPort); // Fix #642 and #637 + } else { + if (!listContainsCurrentPort) { + currentPort = "None"; + nwValues.setSerialPort(currentPort); + } + } + serialPortDropdown = nwCp5.addScrollableList("portName").setOpen(false) + .setOutlineColor(OPENBCI_DARKBLUE) + .setColorBackground(OPENBCI_BLUE) // text field bg color + .setColorValueLabel(color(255)) // text color + .setColorCaptionLabel(color(255)) + .setColorForeground(color(125)) // border color when not selected + .setColorActive(BUTTON_PRESSED) // border color when selected + // .setColorCursor(color(26,26,26)) + .setSize(TEXTFIELD_WIDTH, (serialNetworkingComPorts.size() + 1) * (ITEM_HEIGHT))// + maxFreqList.size()) + .setBarHeight(ITEM_HEIGHT) // height of top/primary bar + .setItemHeight(ITEM_HEIGHT) // height of all item/dropdown bars + .addItems(serialNetworkingComPorts) // used to be .addItems(maxFreqList) + .setVisible(true); + serialPortDropdown.getCaptionLabel() // the caption label is the text object in the primary bar + .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! + .setText(currentPort).setFont(h4).setSize(14) + .getStyle() // need to grab style before affecting the paddingTop + .setPaddingTop(4); + serialPortDropdown.getValueLabel() // the value label is connected to the text objects in the dropdown item bars + .toUpperCase(false) // DO NOT AUTOSET TO UPPERCASE!!! + .setText(currentPort).setFont(h5).setSize(12) // set the font size of the item bars to 14pt + .getStyle() // need to grab style before affecting the paddingTop + .setPaddingTop(3); // 4-pixel vertical offset to center text + serialPortDropdown.addCallback(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + if (theEvent.getAction() == ControlP5.ACTION_BROADCAST) { + int valueIndex = (int)(theEvent.getController()).getValue(); + String portName = serialNetworkingComPorts.get(valueIndex); + nwValues.setSerialPort(portName); + } + } + }); + } + + public synchronized void updateAllUIElements() { + nwValues = dataProcessing.networkingSettings.getValues(); + setProtocolDropdown(nwValues.getProtocol().getString()); + setGridTextLabels(); + if (!dataProcessing.networkingSettings.getNetworkingIsStreaming()) { + updateStartStopButton(); + } + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + setDataTypeDropdown(i, nwValues.getDataType(i).getString()); + setFirstRowTextfield(i, getStoredTextfieldValue(true, i)); + setSecondRowTextfield(i, getStoredTextfieldValue(false, i)); + } + boolean showAllDataTypeDropdowns = nwValues.getProtocol() != NetworkProtocol.SERIAL; + showDataTypeDropdownsTwoThroughTen(showAllDataTypeDropdowns); + setSerialPortDropdown(nwValues.getSerialPort()); + setSerialBaudDropdown(nwValues.getSerialBaud()); + } + + public void disableCertainSerialOutputs() { + // Disable serial fft ouput and display message, it's too much data for serial coms + if (nwValues.getProtocol() == NetworkProtocol.SERIAL) { + if (DATATYPE_DROPDOWNS[0].getCaptionLabel().getText().equals(NetworkDataType.FFT.getString())) { + outputError("Please use Band Power instead of FFT for Serial Output. Changing data type..."); + println("Networking: Changing data type from FFT to BandPower. FFT data is too large to send over Serial communication."); + DATATYPE_DROPDOWNS[0].getCaptionLabel().setText(NetworkDataType.BAND_POWERS.getString()); + DATATYPE_DROPDOWNS[0].setValue(nwValues.getAllDataTypeNames().indexOf(NetworkDataType.BAND_POWERS.getString())); + } + } + } + + private void setGridTextLabels() { + String firstRowTextfieldLabel = ""; + String secondRowTextfieldLabel = ""; + String firstRowTextfieldLabel2 = ""; + String secondRowTextfieldLabel2 = ""; + String secondRowDataTypeLabel = nwValues.getProtocol() != NetworkProtocol.SERIAL ? "Data Type" : ""; + switch (nwValues.getProtocol()) { + case OSC: + case UDP: + firstRowTextfieldLabel = "IP Address"; + secondRowTextfieldLabel = "Port"; + firstRowTextfieldLabel2 = firstRowTextfieldLabel; + secondRowTextfieldLabel2 = secondRowTextfieldLabel; + break; + case LSL: + firstRowTextfieldLabel = "Name"; + secondRowTextfieldLabel = "Type"; + firstRowTextfieldLabel2 = firstRowTextfieldLabel; + secondRowTextfieldLabel2 = secondRowTextfieldLabel; + break; + case SERIAL: + firstRowTextfieldLabel = "Baud Rate"; + secondRowTextfieldLabel = "Port"; + break; + } + grid.setString("Data Type", 3, 0); + grid.setString(firstRowTextfieldLabel, 4, 0); + grid.setString(secondRowTextfieldLabel, 5, 0); + grid.setString(secondRowDataTypeLabel, 7, 0); + grid.setString(firstRowTextfieldLabel2, 8, 0); + grid.setString(secondRowTextfieldLabel2, 9, 0); + for (int i = 0; i < NETWORKING_STREAMS_COUNT; i++) { + String streamNumberLabel = "Stream " + (i + 1); + if (nwValues.getProtocol() == NetworkProtocol.SERIAL && i > 0) { + streamNumberLabel = ""; + } + grid.setString(streamNumberLabel, i < NETWORKING_STREAMS_COUNT / 2 ? 2 : 6, i % (NETWORKING_STREAMS_COUNT / 2) + 1); + } + } + + private void showDataTypeDropdownsTwoThroughTen(boolean b) { + for (int i = 1; i < NETWORKING_STREAMS_COUNT; i++) { + DATATYPE_DROPDOWNS[i].setVisible(b); + } + } + + public ScrollableList getDataTypeDropdown(int i) { + return DATATYPE_DROPDOWNS[i]; + } + + public Textfield getFirstRowTextfield(int i) { + return FIRST_ROW_TEXTFIELDS[i]; + } + + public Textfield getSecondRowTextfield(int i) { + return SECOND_ROW_TEXTFIELDS[i]; + } + + public ScrollableList getSerialPortDropdown() { + return serialPortDropdown; + } + + public ScrollableList getSerialBaudDropdown() { + return serialBaudDropdown; + } + + public List getSerialNetworkingComPorts() { + return serialNetworkingComPorts; + } + + public void setProtocolDropdown(String s) { + protocolDropdown.getCaptionLabel().setText(s); + } + + public void setDataTypeDropdown(int i, String s) { + DATATYPE_DROPDOWNS[i].getCaptionLabel().setText(s); + } + + public void setFirstRowTextfield(int i, String s) { + FIRST_ROW_TEXTFIELDS[i].setText(s); + } + + public void setSecondRowTextfield(int i, String s) { + SECOND_ROW_TEXTFIELDS[i].setText(s); + } + + public void setSerialPortDropdown(String s) { + serialPortDropdown.getCaptionLabel().setText(s); + } + + public void setSerialBaudDropdown(String s) { + serialBaudDropdown.getCaptionLabel().setText(s); + } + + public NetworkingUI getInstance() { + return this; + } + + private void setStreamAttributeFromTextfield(boolean isFirstRow, int streamIndex, String myTextfieldValue) { + switch (nwValues.getProtocol()) { + case OSC: + if (isFirstRow) { + nwValues.setOSCIp(streamIndex, myTextfieldValue); + } else { + nwValues.setOSCPort(streamIndex, myTextfieldValue); + } + break; + case UDP: + if (isFirstRow) { + nwValues.setUDPIp(streamIndex, myTextfieldValue); + } else { + nwValues.setUDPPort(streamIndex, myTextfieldValue); + } + break; + case LSL: + if (isFirstRow) { + nwValues.setLSLName(streamIndex, myTextfieldValue); + } else { + nwValues.setLSLType(streamIndex, myTextfieldValue); + } + break; + case SERIAL: + break; + } + } + + private String getStoredTextfieldValue(boolean isFirstRow, int streamIndex) { + switch (nwValues.getProtocol()) { + case OSC: + if (isFirstRow) { + return nwValues.getOSCIp(streamIndex); + } else { + return nwValues.getOSCPort(streamIndex); + } + case UDP: + if (isFirstRow) { + return nwValues.getUDPIp(streamIndex); + } else { + return nwValues.getUDPPort(streamIndex); + } + case LSL: + if (isFirstRow) { + return nwValues.getLSLName(streamIndex); + } else { + return nwValues.getLSLType(streamIndex); + } + case SERIAL: + default: + return ""; + } + } + + + + class NetworkingGrid { + private int numRows; + private int numCols; + + private int[] colOffset; + private int[] rowOffset; + private int rowHeight; + private boolean horizontallyCenterTextInCells = false; + private boolean drawTableBorder = false; + private boolean drawTableInnerLines = true; + + private int x, y, w; + private int pad_horiz = 5; + private int pad_vert = 5; + + private PFont tableFont = p5; + private int tableFontSize = 12; + + private color[][] textColors; + + private String[][] strings; + + NetworkingGrid(int _numRows, int _numCols, int _rowHeight) { + numRows = _numRows; + numCols = _numCols; + rowHeight = _rowHeight; + + colOffset = new int[numCols]; + 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(OPENBCI_DARKBLUE); + textFont(tableFont, tableFontSize); + + if (drawTableInnerLines) { + // draw row lines + for (int i = 0; i < numRows - 1; i++) { + line(x, y + rowOffset[i], x + w, y + rowOffset[i]); + } + + // draw column lines + for (int i = 1; i < numCols; i++) { + line(x + colOffset[i], y, x + colOffset[i], y + rowOffset[numRows - 1]); + } + } + + // draw cell strings + for (int row = 0; row < numRows; row++) { + for (int col = 0; col < numCols; col++) { + if (strings[row][col] != null) { + 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(OPENBCI_DARKBLUE); + rect(x, y, w, rowOffset[numRows - 1]); + } + + popStyle(); + } + + public RectDimensions getCellDims(int row, int col) { + RectDimensions result = new RectDimensions(); + result.x = x + colOffset[col] + 1; // +1 accounts for line thickness + result.y = y + rowOffset[row] - rowHeight; + result.w = w / numCols - 1; // -1 account for line thickness + result.h = rowHeight; + + return result; + } + + public void setDim(int _x, int _y, int _w) { + x = _x; + y = _y; + w = _w; + + final float colFraction = 1.f / numCols; + + for (int i = 0; i < numCols; i++) { + colOffset[i] = round(w * colFraction * i); + } + + for (int i = 0; i < numRows; i++) { + rowOffset[i] = rowHeight * (i + 1); + } + } + + 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; + } + + public void setDrawTableInnerLines(boolean b) { + drawTableInnerLines = b; + } + + public int getHeight() { + return rowHeight * numRows; + } + } +}; diff --git a/OpenBCI_GUI/OpenBCI_GUI.pde b/OpenBCI_GUI/OpenBCI_GUI.pde index 0d2281425..b6cb0cbc1 100644 --- a/OpenBCI_GUI/OpenBCI_GUI.pde +++ b/OpenBCI_GUI/OpenBCI_GUI.pde @@ -43,9 +43,11 @@ import java.time.LocalDateTime; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; -// import java.net.InetAddress; // Used for ping, however not working right now. +import java.net.Socket; +import java.net.UnknownHostException; +import java.net.URL; +import java.net.HttpURLConnection; import java.util.Random; -import java.awt.Robot; //used for simulating mouse clicks import java.awt.AWTException; import netP5.*; // for OSC import oscP5.*; // for OSC @@ -62,8 +64,8 @@ import java.util.concurrent.atomic.AtomicBoolean; // Global Variables & Instances //------------------------------------------------------------------------ //Used to check GUI version in TopNav.pde and displayed on the splash screen on startup -String localGUIVersionString = "v6.0.0-beta.1"; -String localGUIVersionDate = "September 2023"; +String localGUIVersionString = "7.0.0"; +String localGUIVersionDisplayString = "v7.0.0"; String guiLatestVersionGithubAPI = "https://api.github.com/repos/OpenBCI/OpenBCI_GUI/releases/latest"; String guiLatestReleaseLocation = "https://github.com/OpenBCI/OpenBCI_GUI/releases/latest"; Boolean guiIsUpToDate; @@ -88,9 +90,9 @@ boolean abandonInit = false; boolean systemHasHalted = true; boolean reinitRequested = false; -final int NCHAN_CYTON = 8; +final int CYTON_CHANNEL_COUNT = 8; final int NCHAN_CYTON_DAISY = 16; -final int NCHAN_GANGLION = 4; +final int GANGLION_CHANNEL_COUNT = 4; //choose where to get the EEG data final int DATASOURCE_CYTON = 0; // new default, data from serial with Accel data CHIP 2014-11-03 @@ -98,14 +100,13 @@ final int DATASOURCE_GANGLION = 1; //looking for signal from OpenBCI board via final int DATASOURCE_PLAYBACKFILE = 2; //playback from a pre-recorded text file final int DATASOURCE_SYNTHETIC = 3; //Synthetically generated data final int DATASOURCE_STREAMING = 5; -public int eegDataSource = -1; //default to none of the options +public int eegDataSource = DATASOURCE_CYTON; final static int NUM_ACCEL_DIMS = 3; enum BoardProtocol { NONE, SERIAL, NATIVE_BLE, - WIFI, BLED112 } public BoardProtocol selectedProtocol = BoardProtocol.NONE; @@ -115,45 +116,43 @@ String startupErrorMessage = ""; //here are variables that are used if loading input data from a CSV text file...double slash ("\\") is necessary to make a single slash String playbackData_fname = "N/A"; //only used if loading input data from a file String sdData_fname = "N/A"; //only used if loading input data from a sd file -int nextPlayback_millis = -100; //any negative number // Initialize board DataSource currentBoard = new BoardNull(); DataLogger dataLogger = new DataLogger(); +OdfFileDuration odfFileDuration = OdfFileDuration.SIXTY_MINUTES; // Intialize interface protocols InterfaceSerial iSerial = new InterfaceSerial(); //This is messy, half-deprecated code. See comments in InterfaceSerial.pde - Nov. 2020 -String openBCI_portName = "N/A"; //starts as N/A but is selected from control panel to match your OpenBCI USB Dongle's serial/COM -int openBCI_baud = 115200; //baud rate from the Arduino +String cytonDonglePortName = "N/A"; //starts as N/A but is selected from control panel to match your OpenBCI USB Dongle's serial/COM +int cytonDongleBaudRate = 115200; //baud rate from the Arduino String ganglion_portName = "N/A"; -String wifi_portName = "N/A"; -String wifi_ipAddress = "192.168.4.1"; - String brainflowStreamer = ""; ////// ---- Define variables related to OpenBCI board operations //Define number of channels from cyton...first EEG channels, then aux channels -int nchan = NCHAN_CYTON; //Normally, 8 or 16. Choose a smaller number to show fewer on the GUI +int globalChannelCount = CYTON_CHANNEL_COUNT; //Normally, 8 or 16. Choose a smaller number to show fewer on the GUI //define variables related to warnings to the user about whether the EEG data is nearly railed (and, therefore, of dubious quality) -DataStatus is_railed[]; +DataStatus[] is_railed; //Cyton SD Card setting CytonSDMode cyton_sdSetting = CytonSDMode.NO_WRITE; // Calculate nPointsPerUpdate based on sampling rate and buffer update rate -// @UPDATE_MILLIS: update the buffer every 40 milliseconds +// @UPDATE_MILLIS: update the buffer every 80 milliseconds // @nPointsPerUpdate: update the GUI after this many data points have been received. // The sampling rate should be ideally a multiple of 25, so as to make actual buffer update rate exactly 40ms -final int UPDATE_MILLIS = 40; +final int UPDATE_MILLIS = 80; int nPointsPerUpdate; // no longer final, calculate every time in initSystem //define some data fields for handling data here in processing float dataProcessingRawBuffer[][]; //2D array to handle multiple data channels, each row is a new channel so that dataBuffY[3][] is channel 4 float dataProcessingFilteredBuffer[][]; +CircularFIFODataBuffer downsampledFilteredBuffer; float data_elec_imp_ohm[]; //define how much time is shown on the time-domain montage plot (and how much is used in the FFT plot?) @@ -162,8 +161,6 @@ int dataBuff_len_sec = 20 + 2; //Add two seconds to max buffer to account for fi StopWatch sessionTimeElapsed; StopWatch streamTimeElapsed; -String output_fname; - //Used mostly in W_playback.pde JSONObject savePlaybackHistoryJSON; JSONObject loadPlaybackHistoryJSON; @@ -175,26 +172,22 @@ boolean recentPlaybackFilesHaveUpdated = false; // Serial output processing.serial.Serial serial_output; -//Control Panel for (re)configuring system settings -PlotFontInfo fontInfo; - -//program variables -StringBuilder board_message; -boolean textFieldIsActive = false; - //set window size int win_w; //window width int win_h; //window height - -PImage cog; + +PImage openbciLogoCog; Gif loadingGIF; Gif loadingGIF_blue; +public Gif checkingImpedanceStatusGif; PImage logo_black; PImage logo_blue; PImage logo_white; PImage consoleImgBlue; PImage consoleImgWhite; +PImage screenshotImgWhite; +PImage checkMark_20x20; PFont f1; PFont f2; @@ -217,6 +210,9 @@ static PFont p4; //medium/small Open Sans PFont p13; static PFont p5; //small Open Sans PFont p6; //small Open Sans +PFont p_8; +PFont p_6; +PFont p_5; boolean setupComplete = false; @@ -261,17 +257,10 @@ final color SIGNAL_CHECK_YELLOW = color(221, 178, 13); //Same color as yellow ch final color SIGNAL_CHECK_YELLOW_LOWALPHA = color(221, 178, 13, 150); final color SIGNAL_CHECK_RED = BOLD_RED; final color SIGNAL_CHECK_RED_LOWALPHA = color(224, 56, 45, 150); +public CColor dropdownColorsGlobal = new CColor(); + - -final int COLOR_SCHEME_DEFAULT = 1; -final int COLOR_SCHEME_ALTERNATIVE_A = 2; -// int COLOR_SCHEME_ALTERNATIVE_B = 3; -int colorScheme = COLOR_SCHEME_ALTERNATIVE_A; - -WidgetManager wm; -boolean wmVisible = true; -CColor cp5_colors; - +//Channel Colors -- Defaulted to matching the OpenBCI electrode ribbon cable //Channel Colors -- Defaulted to matching the OpenBCI electrode ribbon cable final color[] channelColors = { color(129, 129, 129), @@ -284,8 +273,15 @@ final color[] channelColors = { color(162, 82, 49) }; +final int COLOR_SCHEME_DEFAULT = 1; +final int COLOR_SCHEME_ALTERNATIVE_A = 2; +// int COLOR_SCHEME_ALTERNATIVE_B = 3; +int colorScheme = COLOR_SCHEME_ALTERNATIVE_A; + +WidgetManager widgetManager; + //Global variable for general navigation bar height -final int navHeight = 22; +final int NAV_HEIGHT = 22; ButtonHelpText buttonHelpText; @@ -298,16 +294,19 @@ public final static String stopButton_pressToStop_txt = "Stop Data Stream"; public final static String stopButton_pressToStart_txt = "Start Data Stream"; DirectoryManager directoryManager; -SessionSettings settings; +SessionSettings sessionSettings; GuiSettings guiSettings; DataProcessing dataProcessing; FilterSettings filterSettings; +FilterUIPopup filterUI; +NetworkingUI networkUI; +DeveloperCommandPopup developerCommandPopup; final int navBarHeight = 32; TopNav topNav; -ddf.minim.analysis.FFT[] fftBuff = new ddf.minim.analysis.FFT[nchan]; //from the minim library -boolean isFFTFiltered = true; //yes by default ... this is used in dataProcessing.pde to determine which uV array feeds the FFT calculation +ddf.minim.analysis.FFT[] fftBuff = new ddf.minim.analysis.FFT[globalChannelCount]; //from the minim library +GlobalFFTSettings globalFFTSettings; StringBuilder globalScreenResolution; StringBuilder globalScreenDPI; @@ -344,6 +343,9 @@ void settings() { } void setup() { + + ourApplet = this; + frameRate(120); copyPaste = new CopyPaste(); @@ -374,8 +376,18 @@ void setup() { p13 = createFont("fonts/OpenSans-Regular.ttf", 13); p5 = createFont("fonts/OpenSans-Regular.ttf", 12); p6 = createFont("fonts/OpenSans-Regular.ttf", 10); + p_8 = createFont("fonts/OpenSans-Regular.ttf", 8); + p_6 = createFont("fonts/OpenSans-Regular.ttf", 6); + p_5 = createFont("fonts/OpenSans-Regular.ttf", 5); - cog = loadImage("obci-logo-blu-cog.png"); + openbciLogoCog = loadImage("obci-logo-blu-cog.png"); + + dropdownColorsGlobal.setActive((int)BUTTON_PRESSED); //bg color of box when pressed + dropdownColorsGlobal.setForeground((int)BUTTON_HOVER); //when hovering over any box (primary or dropdown) + dropdownColorsGlobal.setBackground((int)color(255)); //bg color of boxes (including primary) + dropdownColorsGlobal.setCaptionLabel((int)color(1, 18, 41)); //color of text in primary box + // dropdownColorsGlobal.setValueLabel((int)color(1, 18, 41)); //color of text in all dropdown boxes + dropdownColorsGlobal.setValueLabel((int)color(100)); //color of text in all dropdown boxes // check if the current directory is writable File dummy = new File(sketchPath()); @@ -421,18 +433,16 @@ void setup() { checkIsMacFullDetail(); } println("JVM Version: " + System.getProperty("java.version")); + println("GUI Version: OpenBCI GUI - " + localGUIVersionDisplayString); println("Welcome to the Processing-based OpenBCI GUI!"); //Welcome line. println("For more information, please visit: https://docs.openbci.com/Software/OpenBCISoftware/GUIDocs/"); // Copy sample data to the Users' Documents folder + create Recordings folder directoryManager.init(); - settings = new SessionSettings(); + sessionSettings = new SessionSettings(); guiSettings = new GuiSettings(directoryManager.getSettingsPath()); userPlaybackHistoryFile = directoryManager.getSettingsPath()+"UserPlaybackHistory.json"; - //open window - ourApplet = this; - // Bug #426: If setup takes too long, JOGL will time out waiting for the GUI to draw something. // moving the setup to a separate thread solves this. We just have to make sure not to // start drawing until delayed setup is done. @@ -443,19 +453,17 @@ void delayedSetup() { smooth(); //turn this off if it's too slow surface.setResizable(true); //updated from frame.setResizable in Processing 2 - settings.widthOfLastScreen = width; //for screen resizing (Thank's Tao) - settings.heightOfLastScreen = height; + sessionSettings.widthOfLastScreen = width; //for screen resizing (Thank's Tao) + sessionSettings.heightOfLastScreen = height; setupContainers(); - fontInfo = new PlotFontInfo(); helpWidget = new HelpWidget(0, win_h - 30, win_w, 30); //Instantiate buttonHelpText before any buttons have been made buttonHelpText = new ButtonHelpText(); textfieldUpdateHelper = new TextFieldUpdateHelper(); - //setup topNav - topNav = new TopNav(); + //Print BrainFlow version StringBuilder brainflowVersion = new StringBuilder("BrainFlow Version: "); @@ -472,10 +480,14 @@ void delayedSetup() { logo_white = loadImage("obci-logo-wht.png"); consoleImgBlue = loadImage("console-45x45-dots_blue.png"); consoleImgWhite = loadImage("console-45x45-dots_white.png"); + screenshotImgWhite = loadImage("camera-50x50-white.png"); + checkMark_20x20 = loadImage("Checkmark_20x20.png"); loadingGIF = new Gif(this, "ajax_loader_gray_512.gif"); loadingGIF.loop(); loadingGIF_blue = new Gif(this, "obci_cog_anim-normalblue.gif"); loadingGIF_blue.loop(); + checkingImpedanceStatusGif = new Gif(ourApplet, "Rolling-1s-200px.gif"); + checkingImpedanceStatusGif.loop(); prepareExitHandler(); @@ -485,6 +497,9 @@ void delayedSetup() { asyncLoadAudioFiles(); synchronized(this) { + //setup topNav + topNav = new TopNav(); + // Instantiate ControlPanel in the synchronized block. // It's important to avoid instantiating a ControlP5 during a draw() call // Otherwise we get a crash on launch 10% of the time @@ -523,11 +538,11 @@ synchronized void draw() { reinitRequested = false; } if (systemMode == SYSTEMMODE_POSTINIT) { - w_networking.compareAndSetNetworkingFrameLocks(); + dataProcessing.networkingDataAccumulator.compareAndSetNetworkingFrameLocks(); } } else if (systemMode == SYSTEMMODE_INTROANIMATION) { - if (settings.introAnimationInit == 0) { - settings.introAnimationInit = millis(); + if (sessionSettings.introAnimationInit == 0) { + sessionSettings.introAnimationInit = millis(); } else { introAnimation(); } @@ -548,6 +563,35 @@ private void prepareExitHandler () { )); } + +@Override +public void exit() { + //Only show the confirm exit popup if a session has been started + if (systemMode == SYSTEMMODE_POSTINIT) { + + if (guiSettings.getShowConfirmExitAppPopup() && !confirmCloseAppPopupIsVisible) { + PopupMessageConfirmCloseApp confirmCloseApp = new PopupMessageConfirmCloseApp(); + } else { + println("OpenBCI_GUI: exit() called"); + super.exit(); + } + + if (!allowGuiToClose) { + println("OpenBCI_GUI: exit() called, but GUI is not allowed to close."); + return; + } + } + + println("OpenBCI_GUI: exit() called"); + super.exit(); +} + +public void confirmExit() { + allowGuiToClose = true; + exit(); +} + + //Init system based on default settings. Called from the "START SESSION" button in the GUI's ControlPanel. void initSystem() { println(""); @@ -570,25 +614,15 @@ void initSystem() { //prepare the source of the input data switch (eegDataSource) { case DATASOURCE_CYTON: - if (selectedProtocol == BoardProtocol.SERIAL) { - if(nchan == 16) { - currentBoard = new BoardCytonSerialDaisy(openBCI_portName); - } - else { - currentBoard = new BoardCytonSerial(openBCI_portName); - } + if(globalChannelCount == 16) { + currentBoard = new BoardCytonSerialDaisy(cytonDonglePortName); } - else if (selectedProtocol == BoardProtocol.WIFI) { - if(nchan == 16) { - currentBoard = new BoardCytonWifiDaisy(wifi_ipAddress, selectedSamplingRate); - } - else { - currentBoard = new BoardCytonWifi(wifi_ipAddress, selectedSamplingRate); - } + else { + currentBoard = new BoardCytonSerial(cytonDonglePortName); } break; case DATASOURCE_SYNTHETIC: - currentBoard = new BoardBrainFlowSynthetic(nchan); + currentBoard = new BoardBrainFlowSynthetic(globalChannelCount); println("OpenBCI_GUI: Init session using Synthetic data source"); break; case DATASOURCE_PLAYBACKFILE: @@ -614,9 +648,7 @@ void initSystem() { guiSettings.setShowGanglionUpgradePopup(false); } - if (selectedProtocol == BoardProtocol.WIFI) { - currentBoard = new BoardGanglionWifi(wifi_ipAddress, selectedSamplingRate); - } else if (selectedProtocol == BoardProtocol.BLED112) { + if (selectedProtocol == BoardProtocol.BLED112) { String ganglionName = (String)(controlPanel.bleBox.bleList.getItem(controlPanel.bleBox.bleList.activeItem).get("headline")); String ganglionPort = (String)(controlPanel.bleBox.bleList.getItem(controlPanel.bleBox.bleList.activeItem).get("subline")); String ganglionMac = controlPanel.bleBox.bleMACAddrMap.get(ganglionName); @@ -632,10 +664,11 @@ void initSystem() { case DATASOURCE_STREAMING: currentBoard = new BoardBrainFlowStreaming( controlPanel.streamingBoardBox.getBoard().getBoardId(), - controlPanel.streamingBoardBox.getIP(), + getIpAddrFromStr(controlPanel.streamingBoardBox.getIP()), controlPanel.streamingBoardBox.getPort() ); println("OpenBCI_GUI: Init session using Streaming data source"); + break; default: break; } @@ -683,54 +716,70 @@ void initSystem() { } } - updateToNChan(currentBoard.getNumEXGChannels()); + if (abandonInit) { + haltSystem(); + outputError("Failed to initialize board. Please check that the board is on and has power. See Console Log for more details."); + controlPanel.open(); + return; + } - dataLogger.initialize(); + verbosePrint("OpenBCI_GUI: initSystem: -- Init 1 -- " + millis()); + verbosePrint("OpenBCI_GUI: initSystem: Initializing data logger and setting number of channels"); + if (eegDataSource != DATASOURCE_PLAYBACKFILE) { + dataLogger.initialize(); + updateGlobalChannelCount(currentBoard.getNumEXGChannels()); + } - verbosePrint("OpenBCI_GUI: initSystem: Initializing core data objects"); + verbosePrint("OpenBCI_GUI: initSystem: -- Init 2 -- " + millis()); + verbosePrint("OpenBCI_GUI: initSystem: Initializing core data and FFT objects"); initCoreDataObjects(); - - verbosePrint("OpenBCI_GUI: initSystem: -- Init 1 -- " + millis()); - verbosePrint("OpenBCI_GUI: initSystem: Initializing FFT data objects"); initFFTObjectsAndBuffer(); - verbosePrint("OpenBCI_GUI: initSystem: -- Init 2 -- " + millis()); - verbosePrint("OpenBCI_GUI: initSystem: Closing ControlPanel..."); + verbosePrint("OpenBCI_GUI: initSystem: -- Init 3 -- " + millis()); + verbosePrint("OpenBCI_GUI: initSystem: Initializing TopNav, GUI settings, and Filter settings"); + systemMode = SYSTEMMODE_POSTINIT; //tell system it's ok to leave control panel and start interfacing GUI + topNav.initSecondaryNav(); + + //Instantiate Global Filter Settings Class + filterSettings = new FilterSettings(((DataSource)currentBoard)); + + //Make sure topNav buttons draw in the correct spot + topNav.screenHasBeenResized(width, height); + //Close control panel controlPanel.close(); topNav.controlPanelCollapser.setOff(); - verbosePrint("OpenBCI_GUI: initSystem: -- Init 3 -- " + millis()); - - if (abandonInit) { - haltSystem(); - outputError("Failed to initialize board. Please check that the board is on and has power. See Console Log for more details."); - controlPanel.open(); - return; - } else { - //initilize the secondary topnav and all applicable widgets - topNav.initSecondaryNav(); - wm = new WidgetManager(this); - nextPlayback_millis = millis(); //used for synthesizeData and readFromFile. This restarts the clock that keeps the playback at the right pace. - systemMode = SYSTEMMODE_POSTINIT; //tell system it's ok to leave control panel and start interfacing GUI - } - verbosePrint("OpenBCI_GUI: initSystem: -- Init 4 -- " + millis()); + widgetManager = new WidgetManager(); + + verbosePrint("OpenBCI_GUI: initSystem: -- Init 5 -- " + millis()); - //don't save default session settings StreamingBoard + //don't save default session settings for StreamingBoard if (eegDataSource != DATASOURCE_STREAMING) { //Init software settings: create default settings file that is datasource unique - settings.init(); - settings.initCheckPointFive(); + sessionSettings.init(); + verbosePrint("OpenBCI_GUI: initSystem: Session settings initialized"); } - - //Make sure topNav buttons draw in the correct spot - topNav.screenHasBeenResized(width, height); - //Instantiate Global Filter Settings Class - filterSettings = new FilterSettings(((DataSource)currentBoard)); + if (guiSettings.getAutoLoadSessionSettings()) { + sessionSettings.autoLoadSessionSettings(); + verbosePrint("OpenBCI_GUI: initSystem: User default session settings automatically loaded"); + } - verbosePrint("OpenBCI_GUI: initSystem: -- Init 5 -- " + millis()); + verbosePrint("OpenBCI_GUI: initSystem: -- Init 6 -- " + millis()); + verbosePrint("OpenBCI_GUI: initSystem: Starting data stream and network if applicable"); + if (guiSettings.getAutoStartDataStream()) { + topNav.dataStreamTogglePressed(); + output("OpenBCI_GUI: initSystem: Data Stream Started Automatically"); + } + + if (guiSettings.getAutoStartNetworkStream()) { + NetworkingSettings nwSettings = dataProcessing.networkingSettings; + nwSettings.initializeStreams(); + nwSettings.startNetwork(); + output("OpenBCI_GUI: initSystem: Network Stream Started Automatically"); + } midInit = false; } //end initSystem @@ -739,11 +788,30 @@ public int getCurrentBoardBufferSize() { return dataBuff_len_sec * currentBoard.getSampleRate(); } +public int getDownsamplingFactor() { + switch(currentBoard.getSampleRate()) { + case 200: + return DownsamplingRateEnum.NONE.value; + case 250: + return DownsamplingRateEnum.TWO.value; + case 500: + return DownsamplingRateEnum.FOUR.value; + case 1000: + return DownsamplingRateEnum.EIGHT.value; + default: + return DownsamplingRateEnum.NONE.value; + } +} + +public int getDownsampledBufferSize() { + return getCurrentBoardBufferSize() / getDownsamplingFactor(); +} + /** * @description Get the correct points of FFT based on sampling rate * @returns `int` - Points of FFT. 125Hz, 200Hz, 250Hz -> 256points. 1000Hz -> 1024points. 1600Hz -> 2048 points. */ -int getNfftSafe() { +int getNumFFTPoints() { int sampleRate = currentBoard.getSampleRate(); switch (sampleRate) { case 500: @@ -762,31 +830,34 @@ int getNfftSafe() { void initCoreDataObjects() { nPointsPerUpdate = int(round(float(UPDATE_MILLIS) * currentBoard.getSampleRate()/ 1000.f)); - dataProcessingRawBuffer = new float[nchan][getCurrentBoardBufferSize()]; - dataProcessingFilteredBuffer = new float[nchan][getCurrentBoardBufferSize()]; + dataProcessingRawBuffer = new float[globalChannelCount][getCurrentBoardBufferSize()]; + dataProcessingFilteredBuffer = new float[globalChannelCount][getCurrentBoardBufferSize()]; + downsampledFilteredBuffer = new CircularFIFODataBuffer(globalChannelCount, getDownsampledBufferSize()); - data_elec_imp_ohm = new float[nchan]; - is_railed = new DataStatus[nchan]; - for (int i=0; i= settings.introAnimationInit) { - transparency = map(millis() - settings.introAnimationInit, t1, settings.introAnimationDuration, 0, 255); + if (millis() >= sessionSettings.introAnimationInit) { + transparency = map(millis() - sessionSettings.introAnimationInit, t1, sessionSettings.INTRO_ANIMATION_DURATION, 0, 255); verbosePrint(String.valueOf(transparency)); tint(255, transparency); //draw OpenBCI Logo Front & Center - image(cog, width/2, height/2, width/6, width/6); + image(openbciLogoCog, width/2, height/2, width/6, width/6); textFont(p3, 16); textLeading(24); fill(31, 69, 110, transparency); textAlign(CENTER, CENTER); - String displayVersion = "OpenBCI GUI " + localGUIVersionString; + String displayVersion = "OpenBCI GUI " + localGUIVersionDisplayString; text(displayVersion, width/2, height/2 + width/9); - text(localGUIVersionDate, width/2, height/2 + ((width/8) * 1.125)); } //Exit intro animation when the duration has expired AND the Control Panel is ready - if ((millis() >= settings.introAnimationInit + settings.introAnimationDuration) + if ((millis() >= sessionSettings.introAnimationInit + sessionSettings.INTRO_ANIMATION_DURATION) && controlPanel != null) { systemMode = SYSTEMMODE_PREINIT; controlPanel.open(); diff --git a/OpenBCI_GUI/PacketLossTracker.pde b/OpenBCI_GUI/PacketLossTracker.pde index 45037af52..f50069a73 100644 --- a/OpenBCI_GUI/PacketLossTracker.pde +++ b/OpenBCI_GUI/PacketLossTracker.pde @@ -151,8 +151,15 @@ class PacketLossTracker { if(!silent) { // print the packet loss event - println("WARNING: Lost " + numSamplesLost + " Samples Between " - + (int)lastSample[sampleIndexChannel] + "-" + (int)sample[sampleIndexChannel]); + StringBuilder sb = new StringBuilder("WARNING: Lost "); + sb.append(numSamplesLost); + sb.append(" Samples Between "); + sb.append((int)lastSample[sampleIndexChannel]); + sb.append("-"); + sb.append((int)sample[sampleIndexChannel]); + sb.append(" | Timestamp: "); + sb.append(fetchCurrentTimeString()); + println(sb.toString()); } } @@ -174,6 +181,12 @@ class PacketLossTracker { lastSample = null; } + private String fetchCurrentTimeString() { + LocalDateTime time = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); + return time.format(formatter); + } + protected void checkCurrentStreamStatus() { PacketRecord lastMillisPacketRecord = getCumulativePacketRecordForLast(windowSizeNotificationMs); if (lastMillisPacketRecord.getLostPercent() > thresholdNotification) { diff --git a/OpenBCI_GUI/PopupMessage.pde b/OpenBCI_GUI/PopupMessage.pde index ad62d8dbf..705aef5dd 100644 --- a/OpenBCI_GUI/PopupMessage.pde +++ b/OpenBCI_GUI/PopupMessage.pde @@ -8,21 +8,21 @@ class PopupMessage extends PApplet implements Runnable { private final int defaultHeight = 250; private final int headerHeight = 55; - private final int padding = 20; + protected final int padding = 20; private final int buttonWidth = 120; - private final int buttonHeight = 40; + protected final int buttonHeight = 40; private String message = "Empty Popup"; private String headerMessage = "Error"; - private String buttonMessage = "OK"; + protected String buttonMessage = "OK"; private String buttonLink = null; private color headerColor = OPENBCI_BLUE; - private color buttonColor = OPENBCI_BLUE; + protected color buttonColor = OPENBCI_BLUE; private color backgroundColor = GREY_235; - private ControlP5 cp5; + protected ControlP5 cp5; public PopupMessage(String header, String msg) { super(); @@ -59,7 +59,7 @@ class PopupMessage extends PApplet implements Runnable { @Override void setup() { surface.setTitle(headerMessage); - surface.setAlwaysOnTop(false); + surface.setAlwaysOnTop(true); surface.setResizable(false); Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); @@ -67,27 +67,16 @@ class PopupMessage extends PApplet implements Runnable { frame.requestFocus(); cp5 = new ControlP5(this); + cp5.setAutoDraw(false); - cp5.addButton("onButtonPressed") - .setPosition(width/2 - buttonWidth/2, height - buttonHeight - padding) - .setSize(buttonWidth, buttonHeight) - .setColorLabel(color(255)) - .setColorForeground(buttonColor) - .setColorBackground(buttonColor); - cp5.getController("onButtonPressed") - .getCaptionLabel() - .setFont(p1) - .toUpperCase(false) - .setSize(20) - .setText(buttonMessage) - .getStyle() - .setMarginTop(-2); + createPrimaryButton(); } @Override void draw() { - final int w = defaultWidth; - final int h = defaultHeight; + + final int w = width; + final int h = height; pushStyle(); @@ -95,28 +84,32 @@ class PopupMessage extends PApplet implements Runnable { background(OPENBCI_DARKBLUE); stroke(204); fill(backgroundColor); - rect((width - w)/2, (height - h)/2, w, h); + rect(0, 0, w, h); // draw header noStroke(); fill(headerColor); - rect((width - w)/2, (height - h)/2, w, headerHeight); + rect(0, 0, w, headerHeight); //draw header text textFont(p0, 24); fill(WHITE); textAlign(LEFT, CENTER); - text(headerMessage, (width - w)/2 + padding, headerHeight/2); + text(headerMessage, 0 + padding, headerHeight/2); //draw message textFont(p3, 16); - fill(102); + fill(GREY_100); textAlign(LEFT, TOP); - text(message, (width - w)/2 + padding, (height - h)/2 + padding + headerHeight, w-padding*2, h-padding*2-headerHeight); + text(message, 0 + padding, 0 + padding + headerHeight, w - padding*2, h - padding*2 - headerHeight); popStyle(); - cp5.draw(); + try { + cp5.draw(); + } catch (ConcurrentModificationException e) { + println("PopupMessage Base Class: Error drawing cp5" + e.getMessage()); + } } @Override @@ -134,7 +127,24 @@ class PopupMessage extends PApplet implements Runnable { dispose(); } - public void onButtonPressed() { + protected void createPrimaryButton() { + cp5.addButton("onPrimaryButtonPressed") + .setPosition(width/2 - buttonWidth/2, height - buttonHeight - padding) + .setSize(buttonWidth, buttonHeight) + .setColorLabel(color(255)) + .setColorForeground(BUTTON_HOVER) + .setColorBackground(buttonColor); + cp5.getController("onPrimaryButtonPressed") + .getCaptionLabel() + .setFont(p1) + .toUpperCase(false) + .setSize(20) + .setText(buttonMessage) + .getStyle() + .setMarginTop(-2); + } + + public void onPrimaryButtonPressed() { if (buttonLink != null) { link(buttonLink); } diff --git a/OpenBCI_GUI/PopupMessageConfirmCloseApp.pde b/OpenBCI_GUI/PopupMessageConfirmCloseApp.pde new file mode 100644 index 000000000..2a6810199 --- /dev/null +++ b/OpenBCI_GUI/PopupMessageConfirmCloseApp.pde @@ -0,0 +1,157 @@ +public boolean allowGuiToClose = false; +public boolean confirmCloseAppPopupIsVisible = false; + +public class PopupMessageConfirmCloseApp extends PopupMessage { + + private final int DEFAULT_WIDTH = 500; + private final int DEFAULT_HEIGHT = 300; + + private final String TOGGLE_CAPTION = "Don't show this popup again"; + private final int TOGGLE_CAPTION_WIDTH = 150; //Calculated from font size and caption length + private final int TOGGLE_CAPTION_LEFT_MARGIN_PADDING = 10; + + private final int PRIMARY_BUTTON_WIDTH = 180; + + private Toggle hideShowToggle; + private PImage checkmark; + + public PopupMessageConfirmCloseApp() { + super("Exit Application?", + "Are you sure you want to exit the OpenBCI GUI?", + "No", + null) + ; + confirmCloseAppPopupIsVisible = true; + } + + @Override + void settings() { + size(DEFAULT_WIDTH, DEFAULT_HEIGHT); + } + + @Override + void setup() { + super.setup(); + createSecondaryButton(); + createHideShowToggle(); + } + + @Override + public void draw() { + super.draw(); + drawCheckmark(); + } + + @Override + void exit() { + confirmCloseAppPopupIsVisible = false; + dispose(); + } + + @Override + protected void createPrimaryButton() { + cp5.addButton("onPrimaryButtonPressed") + .setPosition(width/2 - padding - PRIMARY_BUTTON_WIDTH, height - buttonHeight - padding) + .setSize(PRIMARY_BUTTON_WIDTH, buttonHeight) + .setColorLabel(color(255)) + .setColorForeground(BUTTON_HOVER) + .setColorBackground(buttonColor); + cp5.getController("onPrimaryButtonPressed") + .getCaptionLabel() + .setFont(p3) + .toUpperCase(false) + .setSize(16) + .setText(buttonMessage) + .getStyle() + .setMarginTop(-2); + } + + @Override + public void onPrimaryButtonPressed() { + noLoop(); + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.dispose(); + exit(); + } + + private void createSecondaryButton() { + Button myButton = cp5.addButton("onSecondaryButtonPressed") + .setPosition(width/2 + padding, height - buttonHeight - padding) + .setSize(PRIMARY_BUTTON_WIDTH, buttonHeight) + .setColorLabel(color(255)) + .setColorForeground(BUTTON_HOVER) + .setColorBackground(buttonColor); + myButton.getCaptionLabel() + .setFont(p3) + .toUpperCase(false) + .setSize(16) + .setText("Yes") + .getStyle() + .setMarginTop(-2); + myButton.onPress(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + confirmExit(); + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.dispose(); + exit(); + } + }); + } + + private void createHideShowToggle() { + int _w = 20; + int _h = 20; + int totalToggleWidth = _w*2 + TOGGLE_CAPTION_LEFT_MARGIN_PADDING + TOGGLE_CAPTION_WIDTH; + int _x = 0 + padding; + int _y = height - buttonHeight - padding*3 - _h; + boolean _value = !guiSettings.getShowConfirmExitAppPopup(); + + int _fontSize = 16; + hideShowToggle = cp5.addToggle("showThisPopupToggle") + .setPosition(_x, _y) + .setSize(_w, _h) + .setColorLabel(GREY_100) + .setColorForeground(color(120)) + .setColorBackground(color(150)) + .setColorActive(color(57, 128, 204)) + .setVisible(true) + .setValue(_value) + ; + hideShowToggle.getCaptionLabel() + .setFont(p3) + .toUpperCase(false) + .setSize(_fontSize) + .setText(TOGGLE_CAPTION) + .getStyle() //need to grab style before affecting margin and padding + .setMargin(-_h - 7, 0, 0, _w + TOGGLE_CAPTION_LEFT_MARGIN_PADDING) + .setPaddingLeft(10) + ; + hideShowToggle.onPress(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + boolean b = ((Toggle)theEvent.getController()).getBooleanValue(); + guiSettings.setShowConfirmExitAppPopup(!b); + if (b) { + println("Exit App Popup: Don't show this popup in the future"); + } else { + println("Exit App Popup: Show this popup in the future"); + } + } + }); + + if (checkMark_20x20 == null) { + checkMark_20x20 = loadImage("Checkmark_20x20.png"); + } + + if (checkMark_20x20 == null) { + println("Error: Could not load checkmark image"); + } + } + + private void drawCheckmark() { + if (hideShowToggle.getBooleanValue()) { + pushStyle(); + image(checkMark_20x20, padding, height - buttonHeight - padding*3 - 20); + popStyle(); + } + } +}; \ No newline at end of file diff --git a/OpenBCI_GUI/PopupMessageHardwareSettings.pde b/OpenBCI_GUI/PopupMessageHardwareSettings.pde new file mode 100644 index 000000000..bb2100d5c --- /dev/null +++ b/OpenBCI_GUI/PopupMessageHardwareSettings.pde @@ -0,0 +1,160 @@ +import java.awt.Frame; +import processing.awt.PSurfaceAWT; + +public boolean stopStreamHardwareSettingsPopupIsVisible = false; + +class PopupMessageHardwareSettings extends PopupMessage { + + private final int DEFAULT_WIDTH = 500; + private final int DEFAULT_HEIGHT = 300; + + private final String TOGGLE_CAPTION = "Don't show this popup again"; + private final int TOGGLE_CAPTION_WIDTH = 150; //Calculated from font size and caption length + private final int TOGGLE_CAPTION_LEFT_MARGIN_PADDING = 10; + + private final int PRIMARY_BUTTON_WIDTH = 180; + + private Toggle hideShowToggle; + + public PopupMessageHardwareSettings() { + super("Stop Streaming?", + "Streaming needs to be stopped before accessing Hardware Settings. Click the Stop Data Stream button to stop streaming and access Hardware Settings.", + "No", + null) + ; + stopStreamHardwareSettingsPopupIsVisible = true; + } + + @Override + void settings() { + size(DEFAULT_WIDTH, DEFAULT_HEIGHT); + } + + @Override + void setup() { + super.setup(); + createSecondaryButton(); + createHideShowToggle(); + } + + @Override + public void draw() { + super.draw(); + drawCheckmark(); + } + + @Override + void exit() { + stopStreamHardwareSettingsPopupIsVisible = false; + dispose(); + } + + @Override + protected void createPrimaryButton() { + cp5.addButton("onPrimaryButtonPressed") + .setPosition(width/2 - padding - PRIMARY_BUTTON_WIDTH, height - buttonHeight - padding) + .setSize(PRIMARY_BUTTON_WIDTH, buttonHeight) + .setColorLabel(color(255)) + .setColorForeground(BUTTON_HOVER) + .setColorBackground(buttonColor); + cp5.getController("onPrimaryButtonPressed") + .getCaptionLabel() + .setFont(p3) + .toUpperCase(false) + .setSize(16) + .setText(buttonMessage) + .getStyle() + .setMarginTop(-2); + } + + @Override + public void onPrimaryButtonPressed() { + noLoop(); + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.dispose(); + exit(); + } + + private void createSecondaryButton() { + Button myButton = cp5.addButton("onSecondaryButtonPressed") + .setPosition(width/2 + padding, height - buttonHeight - padding) + .setSize(PRIMARY_BUTTON_WIDTH, buttonHeight) + .setColorLabel(color(255)) + .setColorForeground(BUTTON_HOVER) + .setColorBackground(buttonColor); + myButton.getCaptionLabel() + .setFont(p3) + .toUpperCase(false) + .setSize(16) + .setText("Yes") + .getStyle() + .setMarginTop(-2); + myButton.onPress(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + topNav.dataStreamTogglePressed(); + widgetManager.getTimeSeriesWidget().setAdsSettingsVisible(true); + Frame frame = ( (PSurfaceAWT.SmoothCanvas) ((PSurfaceAWT)surface).getNative()).getFrame(); + frame.dispose(); + exit(); + } + }); + } + + private void createHideShowToggle() { + int _w = 20; + int _h = 20; + int _x = 0 + padding; + int _y = height - buttonHeight - padding*3 - _h; + boolean _value = !guiSettings.getShowStopStreamHardwareSettingsPopup(); + + int _fontSize = 16; + hideShowToggle = cp5.addToggle("showThisPopupToggle") + .setPosition(_x, _y) + .setSize(_w, _h) + .setColorLabel(GREY_100) + .setColorForeground(color(120)) + .setColorBackground(color(150)) + .setColorActive(color(57, 128, 204)) + .setVisible(true) + .setValue(_value) + ; + hideShowToggle.getCaptionLabel() + .setFont(p3) + .toUpperCase(false) + .setSize(_fontSize) + .setText(TOGGLE_CAPTION) + .getStyle() //need to grab style before affecting margin and padding + .setMargin(-_h - 7, 0, 0, _w + TOGGLE_CAPTION_LEFT_MARGIN_PADDING) + .setPaddingLeft(10) + ; + hideShowToggle.onPress(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + boolean b = ((Toggle)theEvent.getController()).getBooleanValue(); + guiSettings.setShowStopStreamHardwareSettingsPopup(!b); + if (b) { + println("Hardware Settings Popup: Don't show this popup in the future"); + } else { + println("Hardware Settings Popup: Show this popup in the future"); + } + } + }); + + if (checkMark_20x20 == null) { + checkMark_20x20 = loadImage("Checkmark_20x20.png"); + } + + if (checkMark_20x20 == null) { + println("Error: Could not load checkmark image"); + } + + //hideShowToggle.setImage(checkMark_20x20, controlP5.Controller.ACTIVE); + } + + private void drawCheckmark() { + if (hideShowToggle.getBooleanValue()) { + pushStyle(); + image(checkMark_20x20, padding, height - buttonHeight - padding*3 - 20); + popStyle(); + } + } +}; \ No newline at end of file diff --git a/OpenBCI_GUI/RadioConfig.pde b/OpenBCI_GUI/RadioConfig.pde index 60b96dd00..7710ebf21 100644 --- a/OpenBCI_GUI/RadioConfig.pde +++ b/OpenBCI_GUI/RadioConfig.pde @@ -85,7 +85,7 @@ class RadioConfig { if(!connect_to_portName(rcConfig)){ return; } - serial_direct_board = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //force open the com port + serial_direct_board = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //force open the com port if(serial_direct_board != null){ serial_direct_board.write(0xF0); serial_direct_board.write(0x07); @@ -93,9 +93,9 @@ class RadioConfig { if(print_bytes(rcConfig)){ String[] s = split(rcStringReceived, ':'); if (s[0].equals("Success")) { - outputSuccess("Successfully connected to Cyton using " + openBCI_portName); + outputSuccess("Successfully connected to Cyton using " + cytonDonglePortName); } else { - outputError("Failed to connect using " + openBCI_portName + ". Check hardware or try pressing 'Auto-Scan'."); + outputError("Failed to connect using " + cytonDonglePortName + ". Check hardware or try pressing 'Auto-Scan'."); } } } else { @@ -112,7 +112,7 @@ class RadioConfig { if(!connect_to_portName()){ return false; } - serial_direct_board = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //force open the com port + serial_direct_board = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //force open the com port if(serial_direct_board != null){ serial_direct_board.write(0xF0); serial_direct_board.write(0x07); @@ -124,10 +124,10 @@ class RadioConfig { String[] s = split(rcStringReceived, ':'); closeSerialPort(); if (s[0].equals("Success")) { - verbosePrint("Cyton Auto-Connect Button: Successfully connected to Cyton using " + openBCI_portName); + verbosePrint("Cyton Auto-Connect Button: Successfully connected to Cyton using " + cytonDonglePortName); return true; } else { - verbosePrint("Cyton Auto-Connect Button: Failed to connect using " + openBCI_portName + ". Check hardware or try pressing 'Auto-Scan'."); + verbosePrint("Cyton Auto-Connect Button: Failed to connect using " + cytonDonglePortName + ". Check hardware or try pressing 'Auto-Scan'."); return false; } } @@ -155,7 +155,7 @@ class RadioConfig { return; } } - serial_direct_board = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //force open the com port + serial_direct_board = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //force open the com port if(serial_direct_board != null){ serial_direct_board.write(0xF0); serial_direct_board.write(0x00); @@ -176,7 +176,7 @@ class RadioConfig { return false; } } - serial_direct_board = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //force open the com port + serial_direct_board = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //force open the com port if(serial_direct_board != null){ serial_direct_board.write(0xF0); serial_direct_board.write(0x00); @@ -188,10 +188,10 @@ class RadioConfig { String[] s = split(rcStringReceived, ':'); closeSerialPort(); if (s[0].equals("Success")) { - println(rcStringReceived + ". Using COM port: " + openBCI_portName); + println(rcStringReceived + ". Using COM port: " + cytonDonglePortName); return true; } else { - verbosePrint("Failed to connect using " + openBCI_portName + ". Check hardware or try pressing 'Auto-Scan'."); + verbosePrint("Failed to connect using " + cytonDonglePortName + ". Check hardware or try pressing 'Auto-Scan'."); return false; } } @@ -221,7 +221,7 @@ class RadioConfig { return; } } - serial_direct_board = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //force open the com port + serial_direct_board = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //force open the com port if(serial_direct_board != null){ if(channel_number > 0){ serial_direct_board.write(0xF0); @@ -260,7 +260,7 @@ class RadioConfig { return; } } - serial_direct_board = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //force open the com port + serial_direct_board = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //force open the com port if(serial_direct_board != null){ if(channel_number > 0){ serial_direct_board.write(0xF0); @@ -288,7 +288,7 @@ class RadioConfig { return; } } - serial_direct_board = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //force open the com port + serial_direct_board = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //force open the com port if(serial_direct_board != null){ if(channel_number > 0){ serial_direct_board.write(0xF0); @@ -308,11 +308,11 @@ class RadioConfig { /**** Function to connect to a selected port ****/ // JAM 1/2017 // Needs to be connected to something to perform the Radio_Config tasks private boolean connect_to_portName(RadioConfigBox rcConfig){ - if(openBCI_portName != "N/A"){ - output("Attempting to open Serial/COM port: " + openBCI_portName); + if(cytonDonglePortName != "N/A"){ + output("Attempting to open Serial/COM port: " + cytonDonglePortName); try { - println("Radios_Config: connect_to_portName: Attempting to open serial port: " + openBCI_portName); - serial_output = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //open the com port + println("Radios_Config: connect_to_portName: Attempting to open serial port: " + cytonDonglePortName); + serial_output = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //open the com port serial_output.clear(); // clear anything in the com port's buffer // portIsOpen = true; println("Radios_Config: connect_to_portName: Port is open!"); @@ -329,7 +329,7 @@ class RadioConfig { rcConfig.print_onscreen("Error connecting to Serial port.\n\nTry a different port?"); } closeSerialPort(); - println("Failed to connect using " + openBCI_portName); + println("Failed to connect using " + cytonDonglePortName); return false; } } else { @@ -340,11 +340,11 @@ class RadioConfig { } private boolean connect_to_portName(){ - if(openBCI_portName != "N/A"){ - verbosePrint("Attempting to open Serial/COM port: " + openBCI_portName); + if(cytonDonglePortName != "N/A"){ + verbosePrint("Attempting to open Serial/COM port: " + cytonDonglePortName); try { - verbosePrint("Radios_Config: connect_to_portName: Attempting to open serial port: " + openBCI_portName); - serial_output = new processing.serial.Serial(ourApplet, openBCI_portName, openBCI_baud); //open the com port + verbosePrint("Radios_Config: connect_to_portName: Attempting to open serial port: " + cytonDonglePortName); + serial_output = new processing.serial.Serial(ourApplet, cytonDonglePortName, cytonDongleBaudRate); //open the com port serial_output.clear(); // clear anything in the com port's buffer // portIsOpen = true; verbosePrint("Radios_Config: connect_to_portName: Port is open!"); @@ -360,7 +360,7 @@ class RadioConfig { println("Error connecting to selected Serial/COM port. Make sure your board is powered up and your dongle is plugged in."); } closeSerialPort(); - println("Failed to connect using " + openBCI_portName); + println("Failed to connect using " + cytonDonglePortName); return false; } } else { diff --git a/OpenBCI_GUI/SessionSettings.pde b/OpenBCI_GUI/SessionSettings.pde index b90bf1180..f8d1bf644 100644 --- a/OpenBCI_GUI/SessionSettings.pde +++ b/OpenBCI_GUI/SessionSettings.pde @@ -1,1170 +1,432 @@ -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -/* -// This sketch saves and loads User Settings that appear during Sessions. -// -- All Time Series widget settings in Live, Playback, and Synthetic modes -// -- All FFT widget settings -// -- Default Layout, Board Mode, and other Global Settings -// -- Networking Mode and All settings for active networking protocol -// -- Accelerometer, Analog Read, Head Plot, Band Power, and Spectrogram -// -- Widget/Container Pairs -// -- OpenBCI Data Format Settings (.txt and .csv) -// Created: Richard Waltman - May/June 2018 -// -// -- Start System first! -// -- Lowercase 'n' to Save -// -- Capital 'N' to Load -// -- Functions saveGUIsettings() and loadGUISettings() are called: -// - during system initialization between checkpoints 4 and 5 -// - in Interactivty.pde with the rest of the keyboard shortcuts -// - in TopNav.pde when "Config" --> "Save Settings" || "Load Settings" is clicked -// -- This allows User to store snapshots of most GUI settings in Users/.../Documents/OpenBCI_GUI/Settings/ -// -- After loading, only a few actions are required: start/stop the data stream and networking streams, open/close serial port -// -// Tips on adding a new setting: -// -- figure out if the setting is Global, in an existing widget, or in a new class or widget -// -- read the comments -// -- once you find the right place to add your setting, you can copy the surrounding style -// -- uses JSON keys -// -- Example2: GUI version and settings version -// -- Requires new JSON key 'version` and settingsVersion -// -*/ -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Refactored: Richard Waltman, April 2025 -///////////////////////////////// -// SessionSettings Class // -///////////////////////////////// class SessionSettings { - //Current version to save to JSON - String settingsVersion = "3.0.0"; - //for screen resizing - boolean screenHasBeenResized = false; - float timeOfLastScreenResize = 0; - int widthOfLastScreen = 0; - int heightOfLastScreen = 0; - //default layout variables - int currentLayout; - //Used to time the GUI intro animation - int introAnimationInit = 0; - final int introAnimationDuration = 2500; - //Max File Size #461, default option 4 -> 60 minutes - public final String[] fileDurations = {"5 Minutes", "15 minutes", "30 Minutes", "60 Minutes", "120 Minutes", "No Limit"}; - public final int[] fileDurationInts = {5, 15, 30, 60, 120, -1}; - public final int defaultOBCIMaxFileSize = 3; //4th option from the above list - private boolean logFileIsOpen = false; - private long logFileStartTime; - private long logFileMaxDurationNano = -1; - //this is a global CColor that determines the style of all widget dropdowns ... this should go in WidgetManager.pde - CColor dropdownColors = new CColor(); - ///These `Save` vars are set to default when each widget instantiates - ///and updated every time user selects from dropdown - //Accelerometer settings - int accVertScaleSave; - int accHorizScaleSave; - //FFT plot settings, - int fftMaxFrqSave; - int fftMaxuVSave; - int fftLogLinSave; - int fftSmoothingSave; - int fftFilterSave; - //Analog Read settings - int arVertScaleSave; - int arHorizScaleSave; - //Headplot settings - int hpIntensitySave; - int hpPolaritySave; - int hpContoursSave; - int hpSmoothingSave; - //default data types for streams 1-4 in Networking widget - int nwDataType1; - int nwDataType2; - int nwDataType3; - int nwDataType4; - String nwSerialPort; - int nwProtocolSave; - //Used to check if a playback file has data - int minNumRowsPlaybackFile = int(currentBoard.getSampleRate()); - //Spectrogram Widget settings - int spectMaxFrqSave; - int spectSampleRateSave; - int spectLogLinSave; - - //default configuration settings file location and file name variables - private String sessionPath = ""; - final String[] userSettingsFiles = { - "CytonUserSettings.json", - "DaisyUserSettings.json", - "GanglionUserSettings.json", - "PlaybackUserSettings.json", - "SynthFourUserSettings.json", - "SynthEightUserSettings.json", - "SynthSixteenUserSettings.json" - }; - final String[] defaultSettingsFiles = { - "CytonDefaultSettings.json", - "DaisyDefaultSettings.json", - "GanglionDefaultSettings.json", - "PlaybackDefaultSettings.json", - "SynthFourDefaultSettings.json", - "SynthEightDefaultSettings.json", - "SynthSixteenDefaultSettings.json" - }; - - //Used to print the status of each channel in the console when loading settings - String[] channelsActiveArray = {"Active", "Not Active"}; - String[] gainSettingsArray = { "x1", "x2", "x4", "x6", "x8", "x12", "x24"}; - String[] inputTypeArray = { "Normal", "Shorted", "BIAS_MEAS", "MVDD", "Temp.", "Test", "BIAS_DRP", "BIAS_DRN"}; - String[] biasIncludeArray = {"Don't Include", "Include"}; - String[] srb2SettingArray = {"Off", "On"}; - String[] srb1SettingArray = {"Off", "On"}; - - //Used to set text in dropdown menus when loading FFT settings - String[] fftMaxFrqArray = {"20 Hz", "40 Hz", "60 Hz", "100 Hz", "120 Hz", "250 Hz", "500 Hz", "800 Hz"}; - String[] fftVertScaleArray = {"10 uV", "50 uV", "100 uV", "1000 uV"}; - String[] fftLogLinArray = {"Log", "Linear"}; //share this with spectrogram also - String[] fftSmoothingArray = {"0.0", "0.5", "0.75", "0.9", "0.95", "0.98", "0.99", "0.999"}; - String[] fftFilterArray = {"Filtered", "Unfilt."}; - - //Used to set text in dropdown menus when loading Accelerometer settings - String[] accVertScaleArray = {"Auto","1 g", "2 g", "4 g"}; - String[] accHorizScaleArray = {"Sync", "1 sec", "3 sec", "5 sec", "10 sec", "20 sec"}; - - //Used to set text in dropdown menus when loading Analog Read settings - String[] arVertScaleArray = {"Auto", "50", "100", "200", "400", "1000", "10000"}; - String[] arHorizScaleArray = {"Sync", "1 sec", "3 sec", "5 sec", "10 sec", "20 sec"}; - - //Used to set text in dropdown menus when loading Head Plot settings - String[] hpIntensityArray = {"4x", "2x", "1x", "0.5x", "0.2x", "0.02x"}; - String[] hpPolarityArray = {"+/-", " + "}; - String[] hpContoursArray = {"ON", "OFF"}; - String[] hpSmoothingArray = {"0.0", "0.5", "0.75", "0.9", "0.95", "0.98"}; - - //Used to set text in dropdown menus when loading Spectrogram Setings - String[] spectMaxFrqArray = {"20 Hz", "40 Hz", "60 Hz", "100 Hz", "120 Hz", "250 Hz"}; - String[] spectSampleRateArray = {"30 Min.", "6 Min.", "3 Min.", "1.5 Min.", "1 Min."}; - - //Load Accel. dropdown variables - int loadAccelVertScale; - int loadAccelHorizScale; - - //Load Analog Read dropdown variables - int loadAnalogReadVertScale; - int loadAnalogReadHorizScale; - - //Load FFT dropdown variables - int fftMaxFrqLoad; - int fftMaxuVLoad; - int fftLogLinLoad; - int fftSmoothingLoad; - int fftFilterLoad; - - //Load Headplot dropdown variables - int hpIntensityLoad; - int hpPolarityLoad; - int hpContoursLoad; - int hpSmoothingLoad; - - //Band Power widget settings - //smoothing and filter dropdowns are linked to FFT, so no need to save again - List loadBPActiveChans = new ArrayList(); - - //Spectrogram widget settings - List loadSpectActiveChanTop = new ArrayList(); - List loadSpectActiveChanBot = new ArrayList(); - int spectMaxFrqLoad; - int spectSampleRateLoad; - int spectLogLinLoad; - - //Networking Settings save/load variables - int nwProtocolLoad; - //OSC load variables - String nwOscIp1Load; String nwOscIp2Load; String nwOscIp3Load; String nwOscIp4Load; - String nwOscPort1Load; String nwOscPort2Load; String nwOscPort3Load; String nwOscPort4Load; - //UDP load variables - String nwUdpIp1Load; String nwUdpIp2Load; String nwUdpIp3Load; - String nwUdpPort1Load; String nwUdpPort2Load; String nwUdpPort3Load; - //LSL load variables - String nwLSLName1Load; String nwLSLName2Load; String nwLSLName3Load; - String nwLSLType1Load; String nwLSLType2Load; String nwLSLType3Load; - //Serial load variables - int nwSerialBaudRateLoad; - - //EMG Widget - List loadEmgActiveChannels = new ArrayList(); - - //EMG Joystick Widget - int loadEmgJoystickSmoothing; - List loadEmgJoystickInputs = new ArrayList(); - - //Marker Widget - private int loadMarkerWindow; - private int loadMarkerVertScale; - - //Primary JSON objects for saving and loading data + // Current version and configuration + private String settingsVersion = "5.0.0"; + public int currentLayout; + + // Screen resizing variables + public boolean screenHasBeenResized = false; + public float timeOfLastScreenResize = 0; + public int widthOfLastScreen = 0, heightOfLastScreen = 0; + + // Animation timer + public int introAnimationInit = 0; + public final int INTRO_ANIMATION_DURATION = 2500; + + // JSON data for saving/loading private JSONObject saveSettingsJSONData; private JSONObject loadSettingsJSONData; - - private final String kJSONKeyDataInfo = "dataInfo"; - private final String kJSONKeyTimeSeries = "timeSeries"; - private final String kJSONKeySettings = "settings"; - private final String kJSONKeyFFT = "fft"; - private final String kJSONKeyAccel = "accelerometer"; - private final String kJSONKeyNetworking = "networking"; - private final String kJSONKeyHeadplot = "headplot"; - private final String kJSONKeyBandPower = "bandPower"; - private final String kJSONKeyWidget = "widget"; - private final String kJSONKeyVersion = "version"; - private final String kJSONKeySpectrogram = "spectrogram"; - private final String kJSONKeyEmg = "emg"; - private final String kJSONKeyEmgJoystick = "emgJoystick"; - private final String kJSONKeyMarker = "marker"; - - //used only in this class to count the number of channels being used while saving/loading, this gets updated in updateToNChan whenever the number of channels being used changes - int slnchan; - int numChanloaded; + + // Dialog control variables + String saveDialogName; + String loadDialogName; + String controlEventDataSource; + + // Error handling boolean chanNumError = false; - int numLoadedWidgets; - String [] loadedWidgetsArray; - int loadFramerate; - int loadDatasource; boolean dataSourceError = false; - - String saveDialogName; //Used when Save button is pressed - String loadDialogName; //Used when Load button is pressed - String controlEventDataSource; //Used for output message on system start - Boolean errorUserSettingsNotFound = false; //For error catching + boolean errorUserSettingsNotFound = false; + boolean loadErrorCytonEvent = false; int loadErrorTimerStart; - int loadErrorTimeWindow = 5000; //Time window in milliseconds to apply channel settings to Cyton board. This is to avoid a GUI crash at ~ 4500-5000 milliseconds. - Boolean loadErrorCytonEvent = false; - final int initTimeoutThreshold = 12000; //Timeout threshold in milliseconds - - SessionSettings() { - //Instantiated on app start in OpenBCI_GUI.pde - dropdownColors.setActive((int)BUTTON_PRESSED); //bg color of box when pressed - dropdownColors.setForeground((int)BUTTON_HOVER); //when hovering over any box (primary or dropdown) - dropdownColors.setBackground((int)color(255)); //bg color of boxes (including primary) - dropdownColors.setCaptionLabel((int)color(1, 18, 41)); //color of text in primary box - // dropdownColors.setValueLabel((int)color(1, 18, 41)); //color of text in all dropdown boxes - dropdownColors.setValueLabel((int)color(100)); //color of text in all dropdown boxes - - setLogFileDurationChoice(defaultOBCIMaxFileSize); - } - - /////////////////////////////////// - // OpenBCI Data Format Functions // - /////////////////////////////////// - - public void setLogFileIsOpen (boolean _toggle) { - logFileIsOpen = _toggle; - } - - public boolean isLogFileOpen() { - return logFileIsOpen; - } + int loadErrorTimeWindow = 5000; + final int initTimeoutThreshold = 12000; + + // Constants for JSON keys + private final String + KEY_GLOBAL = "globalSettings", + KEY_VERSION = "guiVersion", + KEY_SETTINGS_VERSION = "sessionSettingsVersion", + KEY_CHANNELS = "channelCount", + KEY_DATA_SOURCE = "dataSource", + KEY_SMOOTHING = "dataSmoothing", + KEY_LAYOUT = "widgetLayout", + KEY_NETWORKING = "networking", + KEY_CONTAINERS = "widgetContainerSettings", + KEY_WIDGET_SETTINGS = "widgetSettings", + KEY_FILTER_SETTINGS = "filterSettings", + KEY_EMG_SETTINGS = "emgSettings"; + + // File paths configuration + private final String[][] SETTING_FILES = { + {"CytonUserSettings.json", "CytonDefaultSettings.json"}, + {"DaisyUserSettings.json", "DaisyDefaultSettings.json"}, + {"GanglionUserSettings.json", "GanglionDefaultSettings.json"}, + {"PlaybackUserSettings.json", "PlaybackDefaultSettings.json"}, + {"SynthFourUserSettings.json", "SynthFourDefaultSettings.json"}, + {"SynthEightUserSettings.json", "SynthEightDefaultSettings.json"}, + {"SynthSixteenUserSettings.json", "SynthSixteenDefaultSettings.json"} + }; + private final int FILE_USER = 0, FILE_DEFAULT = 1; - public void setLogFileStartTime(long _time) { - logFileStartTime = _time; - verbosePrint("Settings: LogFileStartTime = " + _time); - } - - public void setLogFileDurationChoice(int choice) { - logFileMaxDurationNano = fileDurationInts[choice] * 1000000000L * 60; - println("Settings: LogFileMaxDuration = " + fileDurationInts[choice] + " minutes"); - } - - //Only called during live mode && using OpenBCI Data Format - public boolean maxLogTimeReached() { - if (logFileMaxDurationNano < 0) { - return false; - } else { - return (System.nanoTime() - logFileStartTime) > (logFileMaxDurationNano); - } - } - - public void setSessionPath (String _path) { - sessionPath = _path; - } - - public String getSessionPath() { - //println("SESSIONPATH==",sessionPath, millis()); - return sessionPath; - } - - //////////////////////////////////////////////////////////////// - // Init GUI Software Settings // - // // - // - Called during system initialization in OpenBCI_GUI.pde // - //////////////////////////////////////////////////////////////// + /** + * Initialize settings during system startup + */ void init() { - String defaultSettingsFileToSave = getPath("Default", eegDataSource, nchan); - int defaultNumChanLoaded = 0; - int defaultLoadedDataSource = 0; - String defaultSettingsVersion = ""; - String defaultGUIVersion = ""; - - //Take a snapshot of the default GUI settings on every system init + String defaultFile = getPath("Default", eegDataSource, globalChannelCount); println("InitSettings: Saving Default Settings to file!"); try { - this.save(defaultSettingsFileToSave); //to avoid confusion with save() image + save(defaultFile); } catch (Exception e) { outputError("Failed to save Default Settings during Init. Please submit an Issue on GitHub."); e.printStackTrace(); } } - /////////////////////////////// - // Save GUI Settings // - /////////////////////////////// - void save(String saveGUISettingsFileLocation) { - - //Set up a JSON array + /** + * Save current settings to a file + */ + void save(String saveFilePath) { + // Create main JSON object and global settings saveSettingsJSONData = new JSONObject(); - - //Save the number of channels being used and eegDataSource in the first object - JSONObject saveNumChannelsData = new JSONObject(); - saveNumChannelsData.setInt("Channels", slnchan); - saveNumChannelsData.setInt("Data Source", eegDataSource); - //println("Settings: NumChan: " + slnchan); - saveSettingsJSONData.setJSONObject(kJSONKeyDataInfo, saveNumChannelsData); - - //Make a new JSON Object for Time Series Settings - JSONObject saveTSSettings = new JSONObject(); - saveTSSettings.setInt("Time Series Vert Scale", w_timeSeries.getTSVertScale().getIndex()); - saveTSSettings.setInt("Time Series Horiz Scale", w_timeSeries.getTSHorizScale().getIndex()); - //Save data from the Active channel checkBoxes - JSONArray saveActiveChanTS = new JSONArray(); - int numActiveTSChan = w_timeSeries.tsChanSelect.activeChan.size(); - for (int i = 0; i < numActiveTSChan; i++) { - int activeChan = w_timeSeries.tsChanSelect.activeChan.get(i); - saveActiveChanTS.setInt(i, activeChan); - } - saveTSSettings.setJSONArray("activeChannels", saveActiveChanTS); - saveSettingsJSONData.setJSONObject(kJSONKeyTimeSeries, saveTSSettings); - - //Make a second JSON object within our JSONArray to store Global settings for the GUI - JSONObject saveGlobalSettings = new JSONObject(); - saveGlobalSettings.setInt("Current Layout", currentLayout); - saveGlobalSettings.setInt("Analog Read Vert Scale", arVertScaleSave); - saveGlobalSettings.setInt("Analog Read Horiz Scale", arHorizScaleSave); + JSONObject globalSettings = new JSONObject(); + + // Add global settings + globalSettings.setString(KEY_VERSION, localGUIVersionString); + globalSettings.setString(KEY_SETTINGS_VERSION, settingsVersion); + globalSettings.setInt(KEY_CHANNELS, globalChannelCount); + globalSettings.setInt(KEY_DATA_SOURCE, eegDataSource); + globalSettings.setInt(KEY_LAYOUT, currentLayout); + + // Add data smoothing setting if applicable if (currentBoard instanceof SmoothingCapableBoard) { - saveGlobalSettings.setBoolean("Data Smoothing", ((SmoothingCapableBoard)currentBoard).getSmoothingActive()); + globalSettings.setBoolean(KEY_SMOOTHING, + ((SmoothingCapableBoard)currentBoard).getSmoothingActive()); } - saveSettingsJSONData.setJSONObject(kJSONKeySettings, saveGlobalSettings); - - /////Setup JSON Object for gui version and settings Version - JSONObject saveVersionInfo = new JSONObject(); - saveVersionInfo.setString("gui", localGUIVersionString); - saveVersionInfo.setString("settings", settingsVersion); - saveSettingsJSONData.setJSONObject(kJSONKeyVersion, saveVersionInfo); - - ///////////////////////////////////////////////Setup new JSON object to save FFT settings - JSONObject saveFFTSettings = new JSONObject(); - - //Save FFT_Max Freq Setting. The max frq variable is updated every time the user selects a dropdown in the FFT widget - saveFFTSettings.setInt("FFT_Max Freq", fftMaxFrqSave); - //Save FFT_Max uV Setting. The max uV variable is updated also when user selects dropdown in the FFT widget - saveFFTSettings.setInt("FFT_Max uV", fftMaxuVSave); - //Save FFT_LogLin Setting. Same thing happens for LogLin - saveFFTSettings.setInt("FFT_LogLin", fftLogLinSave); - //Save FFT_Smoothing Setting - saveFFTSettings.setInt("FFT_Smoothing", fftSmoothingSave); - //Save FFT_Filter Setting - if (isFFTFiltered == true) fftFilterSave = 0; - if (isFFTFiltered == false) fftFilterSave = 1; - saveFFTSettings.setInt("FFT_Filter", fftFilterSave); - //Set the FFT JSON Object - saveSettingsJSONData.setJSONObject(kJSONKeyFFT, saveFFTSettings); //next object will be set to slnchan+3, etc. - - ///////////////////////////////////////////////Setup new JSON object to save Accelerometer settings - JSONObject saveAccSettings = new JSONObject(); - saveAccSettings.setInt("Accelerometer Vert Scale", accVertScaleSave); - saveAccSettings.setInt("Accelerometer Horiz Scale", accHorizScaleSave); - saveSettingsJSONData.setJSONObject(kJSONKeyAccel, saveAccSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Networking settings - JSONObject saveNetworkingSettings = new JSONObject(); - //Save Protocol - saveNetworkingSettings.setInt("Protocol", nwProtocolSave);//***Save User networking protocol mode - switch(nwProtocolSave) { - case 3: - for (int i = 1; i <= 4; i++) { - saveNetworkingSettings.setInt("OSC_DataType"+i, (Integer) w_networking.getCP5Map().get(w_networking.dataTypeNames.get(i-1))); - saveNetworkingSettings.setString("OSC_ip"+i, (String) w_networking.getCP5Map().get("OSC_ip"+i)); - saveNetworkingSettings.setString("OSC_port"+i, (String) w_networking.getCP5Map().get("OSC_port"+i)); - } - break; - case 2: - for (int i = 1; i <= 3; i++) { - saveNetworkingSettings.setInt("UDP_DataType"+i, (Integer) w_networking.getCP5Map().get(w_networking.dataTypeNames.get(i-1))); - saveNetworkingSettings.setString("UDP_ip"+i, (String) w_networking.getCP5Map().get("UDP_ip"+i)); - saveNetworkingSettings.setString("UDP_port"+i, (String) w_networking.getCP5Map().get("UDP_port"+i)); - } - break; - case 1: - for (int i = 1; i <= 3; i++) { - saveNetworkingSettings.setInt("LSL_DataType"+i, (Integer) w_networking.getCP5Map().get(w_networking.dataTypeNames.get(i-1))); - saveNetworkingSettings.setString("LSL_name"+i, (String) w_networking.getCP5Map().get("LSL_name"+i)); - saveNetworkingSettings.setString("LSL_type"+i, (String) w_networking.getCP5Map().get("LSL_type"+i)); - } - break; - case 0: - saveNetworkingSettings.setInt("Serial_DataType1", (Integer) w_networking.getCP5Map().get("dataType1")); - saveNetworkingSettings.setInt("Serial_baudrate", (Integer) w_networking.getCP5Map().get("baud_rate")); - saveNetworkingSettings.setString("Serial_portName", (String) w_networking.getCP5Map().get("port_name")); - break; - }//end of networking proctocol switch - //Set Networking Settings JSON Object - saveSettingsJSONData.setJSONObject(kJSONKeyNetworking, saveNetworkingSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Headplot settings - JSONObject saveHeadplotSettings = new JSONObject(); - - //Save Headplot Intesity - saveHeadplotSettings.setInt("HP_intensity", hpIntensitySave); - //Save Headplot Polarity - saveHeadplotSettings.setInt("HP_polarity", hpPolaritySave); - //Save Headplot contours - saveHeadplotSettings.setInt("HP_contours", hpContoursSave); - //Save Headplot Smoothing Setting - saveHeadplotSettings.setInt("HP_smoothing", hpSmoothingSave); - //Set the Headplot JSON Object - saveSettingsJSONData.setJSONObject(kJSONKeyHeadplot, saveHeadplotSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Band Power settings - JSONObject saveBPSettings = new JSONObject(); - - //Save data from the Active channel checkBoxes - JSONArray saveActiveChanBP = new JSONArray(); - int numActiveBPChan = w_bandPower.bpChanSelect.activeChan.size(); - for (int i = 0; i < numActiveBPChan; i++) { - int activeChan = w_bandPower.bpChanSelect.activeChan.get(i); - saveActiveChanBP.setInt(i, activeChan); - } - saveBPSettings.setJSONArray("activeChannels", saveActiveChanBP); - saveSettingsJSONData.setJSONObject(kJSONKeyBandPower, saveBPSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Spectrogram settings - JSONObject saveSpectrogramSettings = new JSONObject(); - //Save data from the Active channel checkBoxes - Top - JSONArray saveActiveChanSpectTop = new JSONArray(); - int numActiveSpectChanTop = w_spectrogram.spectChanSelectTop.activeChan.size(); - for (int i = 0; i < numActiveSpectChanTop; i++) { - int activeChan = w_spectrogram.spectChanSelectTop.activeChan.get(i); - saveActiveChanSpectTop.setInt(i, activeChan); - } - saveSpectrogramSettings.setJSONArray("activeChannelsTop", saveActiveChanSpectTop); - //Save data from the Active channel checkBoxes - Bottom - JSONArray saveActiveChanSpectBot = new JSONArray(); - int numActiveSpectChanBot = w_spectrogram.spectChanSelectBot.activeChan.size(); - for (int i = 0; i < numActiveSpectChanBot; i++) { - int activeChan = w_spectrogram.spectChanSelectBot.activeChan.get(i); - saveActiveChanSpectBot.setInt(i, activeChan); - } - saveSpectrogramSettings.setJSONArray("activeChannelsBot", saveActiveChanSpectBot); - //Save Spectrogram_Max Freq Setting. The max frq variable is updated every time the user selects a dropdown in the spectrogram widget - saveSpectrogramSettings.setInt("Spectrogram_Max Freq", spectMaxFrqSave); - saveSpectrogramSettings.setInt("Spectrogram_Sample Rate", spectSampleRateSave); - saveSpectrogramSettings.setInt("Spectrogram_LogLin", spectLogLinSave); - saveSettingsJSONData.setJSONObject(kJSONKeySpectrogram, saveSpectrogramSettings); - - ///////////////////////////////////////////////Setup new JSON object to save EMG Settings - JSONObject saveEMGSettings = new JSONObject(); - - //Save data from the Active channel checkBoxes - JSONArray saveActiveChanEMG = new JSONArray(); - int numActiveEMGChan = w_emg.emgChannelSelect.activeChan.size(); - for (int i = 0; i < numActiveEMGChan; i++) { - int activeChan = w_emg.emgChannelSelect.activeChan.get(i); - saveActiveChanEMG.setInt(i, activeChan); - } - saveEMGSettings.setJSONArray("activeChannels", saveActiveChanEMG); - saveSettingsJSONData.setJSONObject(kJSONKeyEmg, saveEMGSettings); - - ///////////////////////////////////////////////Setup new JSON object to save EMG Joystick Settings - JSONObject saveEmgJoystickSettings = new JSONObject(); - saveEmgJoystickSettings.setInt("smoothing", w_emgJoystick.joystickSmoothing.getIndex()); - JSONArray saveEmgJoystickInputs = new JSONArray(); - int numEmgJoystickInputs = w_emgJoystick.emgJoystickInputs.length; - for (int i = 0; i < numEmgJoystickInputs; i++) { - saveEmgJoystickInputs.setInt(i, w_emgJoystick.emgJoystickInputs[i].getIndex()); - } - saveEmgJoystickSettings.setJSONArray("joystickInputs", saveEmgJoystickInputs); - saveSettingsJSONData.setJSONObject(kJSONKeyEmgJoystick, saveEmgJoystickSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Marker Widget Settings - JSONObject saveMarkerSettings = new JSONObject(); - saveMarkerSettings.setInt("markerWindow", w_marker.getMarkerWindow().getIndex()); - saveMarkerSettings.setInt("markerVertScale", w_marker.getMarkerVertScale().getIndex()); - saveSettingsJSONData.setJSONObject(kJSONKeyMarker, saveMarkerSettings); - - ///////////////////////////////////////////////Setup new JSON object to save Widgets Active in respective Containers - JSONObject saveWidgetSettings = new JSONObject(); + + // Add all settings to the main JSON object + saveSettingsJSONData.setJSONObject(KEY_GLOBAL, globalSettings); + saveSettingsJSONData.setJSONObject(KEY_NETWORKING, + parseJSONObject(dataProcessing.networkingSettings.getJson())); + saveSettingsJSONData.setJSONObject(KEY_CONTAINERS, saveWidgetContainerPositions()); + saveSettingsJSONData.setJSONObject(KEY_WIDGET_SETTINGS, + parseJSONObject(widgetManager.getWidgetSettingsAsJson())); + saveSettingsJSONData.setJSONObject(KEY_FILTER_SETTINGS, + parseJSONObject(filterSettings.getJson())); + saveSettingsJSONData.setJSONObject(KEY_EMG_SETTINGS, + parseJSONObject(dataProcessing.emgSettings.getJson())); + + // Save to file + saveJSONObject(saveSettingsJSONData, saveFilePath); + } + /** + * Save widget container positions + */ + private JSONObject saveWidgetContainerPositions() { + JSONObject widgetLayout = new JSONObject(); int numActiveWidgets = 0; - //Save what Widgets are active and respective Container number (see Containers.pde) - for (int i = 0; i < wm.widgets.size(); i++) { //increment through all widgets - if (wm.widgets.get(i).getIsActive()) { //If a widget is active... - numActiveWidgets++; //increment numActiveWidgets - //println("Widget" + i + " is active"); - // activeWidgets.add(i); //keep track of the active widget - int containerCountsave = wm.widgets.get(i).currentContainer; - //println("Widget " + i + " is in Container " + containerCountsave); - saveWidgetSettings.setInt("Widget_"+i, containerCountsave); - } else if (!wm.widgets.get(i).getIsActive()) { //If a widget is not active... - saveWidgetSettings.remove("Widget_"+i); //remove non-active widget from JSON - //println("widget"+i+" is not active"); + + // Save active widgets and their container positions + for (int i = 0; i < widgetManager.widgets.size(); i++) { + Widget widget = widgetManager.widgets.get(i); + if (widget.getIsActive()) { + numActiveWidgets++; + widgetLayout.setInt("Widget_" + i, widget.currentContainer); } } + println("SessionSettings: " + numActiveWidgets + " active widgets saved!"); - //Print what widgets are in the containers used by current layout for only the number of active widgets - //for (int i = 0; i < numActiveWidgets; i++) { - //int containerCounter = wm.layouts.get(currentLayout).containerInts[i]; - //println("Container " + containerCounter + " is available"); //For debugging - //} - saveSettingsJSONData.setJSONObject(kJSONKeyWidget, saveWidgetSettings); - - ///////////////////////////////////////////////////////////////////////////////// - ///ADD more global settings above this line in the same formats as above///////// - - //Let's save the JSON array to a file! - saveJSONObject(saveSettingsJSONData, saveGUISettingsFileLocation); - - } //End of Save GUI Settings function - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Load GUI Settings // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - void load(String loadGUISettingsFileLocation) throws Exception { - //Load all saved User Settings from a JSON file if it exists - loadSettingsJSONData = loadJSONObject(loadGUISettingsFileLocation); - - verbosePrint(loadSettingsJSONData.toString()); + return widgetLayout; + } - //Check the number of channels saved to json first! - JSONObject loadDataSettings = loadSettingsJSONData.getJSONObject(kJSONKeyDataInfo); - numChanloaded = loadDataSettings.getInt("Channels"); - //Print error if trying to load a different number of channels - if (numChanloaded != slnchan) { - println("Channels being loaded from " + loadGUISettingsFileLocation + " don't match channels being used!"); + /** + * Load settings from a file + */ + void load(String loadFilePath) throws Exception { + // Load and parse JSON data + loadSettingsJSONData = loadJSONObject(loadFilePath); + JSONObject globalSettings = loadSettingsJSONData.getJSONObject(KEY_GLOBAL); + + // Validate settings match current configuration + validateSettings(globalSettings); + + // Apply settings in order + currentLayout = globalSettings.getInt(KEY_LAYOUT); + applyDataSmoothingSettings(globalSettings); + applyNetworkingSettings(); + applyWidgetLayout(); + applyWidgetSettings(); + applyFilterSettings(); + applyEmgSettings(); + } + + /** + * Validate that loaded settings are compatible with current configuration + */ + private void validateSettings(JSONObject globalSettings) throws Exception { + // Check channel count match + int loadedChannels = globalSettings.getInt(KEY_CHANNELS); + if (loadedChannels != globalChannelCount) { chanNumError = true; - throw new Exception(); - } else { - chanNumError = false; + throw new Exception("Channel count mismatch"); } - //Check the Data Source integer next: Cyton = 0, Ganglion = 1, Playback = 2, Synthetic = 3 - loadDatasource = loadDataSettings.getInt("Data Source"); - verbosePrint("loadGUISettings: Data source loaded: " + loadDatasource + ". Current data source: " + eegDataSource); - //Print error if trying to load a different data source (ex. Live != Synthetic) - if (loadDatasource != eegDataSource) { - println("Data source being loaded from " + loadGUISettingsFileLocation + " doesn't match current data source."); + chanNumError = false; + + // Check data source match + int loadedDataSource = globalSettings.getInt(KEY_DATA_SOURCE); + if (loadedDataSource != eegDataSource) { dataSourceError = true; - throw new Exception(); - } else { - dataSourceError = false; - } - - //get the global settings JSON object - JSONObject loadGlobalSettings = loadSettingsJSONData.getJSONObject(kJSONKeySettings); - //Store loaded layout to current layout variable - currentLayout = loadGlobalSettings.getInt("Current Layout"); - loadAnalogReadVertScale = loadGlobalSettings.getInt("Analog Read Vert Scale"); - loadAnalogReadHorizScale = loadGlobalSettings.getInt("Analog Read Horiz Scale"); - //Load more global settings after this line, if needed - Boolean loadDataSmoothingSetting = (currentBoard instanceof SmoothingCapableBoard) ? loadGlobalSettings.getBoolean("Data Smoothing") : null; - - //get the FFT settings - JSONObject loadFFTSettings = loadSettingsJSONData.getJSONObject(kJSONKeyFFT); - fftMaxFrqLoad = loadFFTSettings.getInt("FFT_Max Freq"); - fftMaxuVLoad = loadFFTSettings.getInt("FFT_Max uV"); - fftLogLinLoad = loadFFTSettings.getInt("FFT_LogLin"); - fftSmoothingLoad = loadFFTSettings.getInt("FFT_Smoothing"); - fftFilterLoad = loadFFTSettings.getInt("FFT_Filter"); - - //get the Accelerometer settings - JSONObject loadAccSettings = loadSettingsJSONData.getJSONObject(kJSONKeyAccel); - loadAccelVertScale = loadAccSettings.getInt("Accelerometer Vert Scale"); - loadAccelHorizScale = loadAccSettings.getInt("Accelerometer Horiz Scale"); - - //get the Networking Settings - JSONObject loadNetworkingSettings = loadSettingsJSONData.getJSONObject(kJSONKeyNetworking); - nwProtocolLoad = loadNetworkingSettings.getInt("Protocol"); - switch (nwProtocolLoad) { - case 3: - nwDataType1 = loadNetworkingSettings.getInt("OSC_DataType1"); - nwDataType2 = loadNetworkingSettings.getInt("OSC_DataType2"); - nwDataType3 = loadNetworkingSettings.getInt("OSC_DataType3"); - nwDataType4 = loadNetworkingSettings.getInt("OSC_DataType4"); - nwOscIp1Load = loadNetworkingSettings.getString("OSC_ip1"); - nwOscIp2Load = loadNetworkingSettings.getString("OSC_ip2"); - nwOscIp3Load = loadNetworkingSettings.getString("OSC_ip3"); - nwOscIp4Load = loadNetworkingSettings.getString("OSC_ip4"); - nwOscPort1Load = loadNetworkingSettings.getString("OSC_port1"); - nwOscPort2Load = loadNetworkingSettings.getString("OSC_port2"); - nwOscPort3Load = loadNetworkingSettings.getString("OSC_port3"); - nwOscPort4Load = loadNetworkingSettings.getString("OSC_port4"); - break; - case 2: - nwDataType1 = loadNetworkingSettings.getInt("UDP_DataType1"); - nwDataType2 = loadNetworkingSettings.getInt("UDP_DataType2"); - nwDataType3 = loadNetworkingSettings.getInt("UDP_DataType3"); - nwUdpIp1Load = loadNetworkingSettings.getString("UDP_ip1"); - nwUdpIp2Load = loadNetworkingSettings.getString("UDP_ip2"); - nwUdpIp3Load = loadNetworkingSettings.getString("UDP_ip3"); - nwUdpPort1Load = loadNetworkingSettings.getString("UDP_port1"); - nwUdpPort2Load = loadNetworkingSettings.getString("UDP_port2"); - nwUdpPort3Load = loadNetworkingSettings.getString("UDP_port3"); - break; - case 1: - nwDataType1 = loadNetworkingSettings.getInt("LSL_DataType1"); - nwDataType2 = loadNetworkingSettings.getInt("LSL_DataType2"); - nwDataType3 = loadNetworkingSettings.getInt("LSL_DataType3"); - nwLSLName1Load = loadNetworkingSettings.getString("LSL_name1"); - nwLSLName2Load = loadNetworkingSettings.getString("LSL_name2"); - nwLSLName3Load = loadNetworkingSettings.getString("LSL_name3"); - nwLSLType1Load = loadNetworkingSettings.getString("LSL_type1"); - nwLSLType2Load = loadNetworkingSettings.getString("LSL_type2"); - nwLSLType3Load = loadNetworkingSettings.getString("LSL_type3"); - break; - case 0: - nwDataType1 = loadNetworkingSettings.getInt("Serial_DataType1"); - nwSerialBaudRateLoad = loadNetworkingSettings.getInt("Serial_baudrate"); - nwSerialPort = loadNetworkingSettings.getString("Serial_portName"); - break; - } //end switch case for all networking types - - //get the Headplot settings - JSONObject loadHeadplotSettings = loadSettingsJSONData.getJSONObject(kJSONKeyHeadplot); - hpIntensityLoad = loadHeadplotSettings.getInt("HP_intensity"); - hpPolarityLoad = loadHeadplotSettings.getInt("HP_polarity"); - hpContoursLoad = loadHeadplotSettings.getInt("HP_contours"); - hpSmoothingLoad = loadHeadplotSettings.getInt("HP_smoothing"); - - //Get Band Power widget settings - loadBPActiveChans.clear(); - JSONObject loadBPSettings = loadSettingsJSONData.getJSONObject(kJSONKeyBandPower); - JSONArray loadBPChan = loadBPSettings.getJSONArray("activeChannels"); - for (int i = 0; i < loadBPChan.size(); i++) { - loadBPActiveChans.add(loadBPChan.getInt(i)); - } - //println("Settings: band power active chans loaded = " + loadBPActiveChans ); - - try { - //Get Spectrogram widget settings - loadSpectActiveChanTop.clear(); - loadSpectActiveChanBot.clear(); - JSONObject loadSpectSettings = loadSettingsJSONData.getJSONObject(kJSONKeySpectrogram); - JSONArray loadSpectChanTop = loadSpectSettings.getJSONArray("activeChannelsTop"); - for (int i = 0; i < loadSpectChanTop.size(); i++) { - loadSpectActiveChanTop.add(loadSpectChanTop.getInt(i)); - } - JSONArray loadSpectChanBot = loadSpectSettings.getJSONArray("activeChannelsBot"); - for (int i = 0; i < loadSpectChanBot.size(); i++) { - loadSpectActiveChanBot.add(loadSpectChanBot.getInt(i)); - } - spectMaxFrqLoad = loadSpectSettings.getInt("Spectrogram_Max Freq"); - spectSampleRateLoad = loadSpectSettings.getInt("Spectrogram_Sample Rate"); - spectLogLinLoad = loadSpectSettings.getInt("Spectrogram_LogLin"); - //println(loadSpectActiveChanTop, loadSpectActiveChanBot); - } catch (Exception e) { - e.printStackTrace(); - } - - //Get EMG widget settings - loadEmgActiveChannels.clear(); - JSONObject loadEmgSettings = loadSettingsJSONData.getJSONObject(kJSONKeyEmg); - JSONArray loadEmgChan = loadEmgSettings.getJSONArray("activeChannels"); - for (int i = 0; i < loadEmgChan.size(); i++) { - loadEmgActiveChannels.add(loadEmgChan.getInt(i)); - } - - //Get EMG Joystick widget settings - JSONObject loadEmgJoystickSettings = loadSettingsJSONData.getJSONObject(kJSONKeyEmgJoystick); - loadEmgJoystickSmoothing = loadEmgJoystickSettings.getInt("smoothing"); - loadEmgJoystickInputs.clear(); - JSONArray loadJoystickInputsJson = loadEmgJoystickSettings.getJSONArray("joystickInputs"); - for (int i = 0; i < loadJoystickInputsJson.size(); i++) { - loadEmgJoystickInputs.add(loadJoystickInputsJson.getInt(i)); - } - - //Get Marker widget settings - JSONObject loadMarkerSettings = loadSettingsJSONData.getJSONObject(kJSONKeyMarker); - loadMarkerWindow = loadMarkerSettings.getInt("markerWindow"); - loadMarkerVertScale = loadMarkerSettings.getInt("markerVertScale"); - - //get the Widget/Container settings - JSONObject loadWidgetSettings = loadSettingsJSONData.getJSONObject(kJSONKeyWidget); - //Apply Layout directly before loading and applying widgets to containers - wm.setNewContainerLayout(currentLayout); - verbosePrint("LoadGUISettings: Layout " + currentLayout + " Loaded!"); - numLoadedWidgets = loadWidgetSettings.size(); - - - //int numActiveWidgets = 0; //reset the counter - for (int w = 0; w < wm.widgets.size(); w++) { //increment through all widgets - if (wm.widgets.get(w).getIsActive()) { //If a widget is active... - verbosePrint("Deactivating widget [" + w + "]"); - wm.widgets.get(w).setIsActive(false); - //numActiveWidgets++; //counter the number of de-activated widgets - } + throw new Exception("Data source mismatch"); } - - //Store the Widget number keys from JSON to a string array - loadedWidgetsArray = (String[]) loadWidgetSettings.keys().toArray(new String[loadWidgetSettings.size()]); - //printArray(loadedWidgetsArray); - int widgetToActivate = 0; - for (int w = 0; w < numLoadedWidgets; w++) { - String [] loadWidgetNameNumber = split(loadedWidgetsArray[w], '_'); - //Store the value of the widget to be activated - widgetToActivate = Integer.valueOf(loadWidgetNameNumber[1]); - //Load the container for the current widget[w] - int containerToApply = loadWidgetSettings.getInt(loadedWidgetsArray[w]); - - wm.widgets.get(widgetToActivate).setIsActive(true);//activate the new widget - wm.widgets.get(widgetToActivate).setContainer(containerToApply);//map it to the container that was loaded! - println("LoadGUISettings: Applied Widget " + widgetToActivate + " to Container " + containerToApply); - }//end case for all widget/container settings - - ///////////////////////////////////////////////////////////// - // Load more widget settings above this line as above // - ///////////////////////////////////////////////////////////// - - ///////////////////////////////////////////////////////////// - // Apply Settings below this line // - ///////////////////////////////////////////////////////////// - - //Apply Data Smoothing for capable boards - if (currentBoard instanceof SmoothingCapableBoard) { - ((SmoothingCapableBoard)currentBoard).setSmoothingActive(loadDataSmoothingSetting); + dataSourceError = false; + } + + /** + * Apply data smoothing settings if available + */ + private void applyDataSmoothingSettings(JSONObject globalSettings) { + if (currentBoard instanceof SmoothingCapableBoard && + globalSettings.hasKey(KEY_SMOOTHING)) { + + ((SmoothingCapableBoard)currentBoard).setSmoothingActive( + globalSettings.getBoolean(KEY_SMOOTHING)); topNav.updateSmoothingButtonText(); } - - //Load and apply all of the settings that are in dropdown menus. It's a bit much, so it has it's own function below. - loadApplyWidgetDropdownText(); - - //Apply Time Series Settings Last!!! - loadApplyTimeSeriesSettings(); - - //Force headplot to redraw if it is active - int hpWidgetNumber; - if (eegDataSource == DATASOURCE_GANGLION) { - hpWidgetNumber = 6; - } else { - hpWidgetNumber = 5; - } - if (wm.widgets.get(hpWidgetNumber).getIsActive()) { - w_headPlot.headPlot.setPositionSize(w_headPlot.headPlot.hp_x, w_headPlot.headPlot.hp_y, w_headPlot.headPlot.hp_w, w_headPlot.headPlot.hp_h, w_headPlot.headPlot.hp_win_x, w_headPlot.headPlot.hp_win_y); - println("Headplot is active: Redrawing"); - } - } //end of loadGUISettings - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - private void loadApplyWidgetDropdownText() { - - ////////Apply Time Series dropdown settings in loadApplyTimeSeriesSettings() instead of here - - ////////Apply FFT settings - MaxFreq(fftMaxFrqLoad); //This changes the back-end - w_fft.cp5_widget.getController("MaxFreq").getCaptionLabel().setText(fftMaxFrqArray[fftMaxFrqLoad]); //This changes front-end... etc. - - VertScale(fftMaxuVLoad); - w_fft.cp5_widget.getController("VertScale").getCaptionLabel().setText(fftVertScaleArray[fftMaxuVLoad]); - - LogLin(fftLogLinLoad); - w_fft.cp5_widget.getController("LogLin").getCaptionLabel().setText(fftLogLinArray[fftLogLinLoad]); - - Smoothing(fftSmoothingLoad); - w_fft.cp5_widget.getController("Smoothing").getCaptionLabel().setText(fftSmoothingArray[fftSmoothingLoad]); - - UnfiltFilt(fftFilterLoad); - w_fft.cp5_widget.getController("UnfiltFilt").getCaptionLabel().setText(fftFilterArray[fftFilterLoad]); - - ////////Apply Accelerometer settings; - accelVertScale(loadAccelVertScale); - w_accelerometer.cp5_widget.getController("accelVertScale").getCaptionLabel().setText(accVertScaleArray[loadAccelVertScale]); - - accelDuration(loadAccelHorizScale); - w_accelerometer.cp5_widget.getController("accelDuration").getCaptionLabel().setText(accHorizScaleArray[loadAccelHorizScale]); - - ////////Apply Anolog Read dropdowns to Live Cyton Only - if (eegDataSource == DATASOURCE_CYTON) { - ////////Apply Analog Read settings - VertScale_AR(loadAnalogReadVertScale); - w_analogRead.cp5_widget.getController("VertScale_AR").getCaptionLabel().setText(arVertScaleArray[loadAnalogReadVertScale]); - - Duration_AR(loadAnalogReadHorizScale); - w_analogRead.cp5_widget.getController("Duration_AR").getCaptionLabel().setText(arHorizScaleArray[loadAnalogReadHorizScale]); - } - - ////////////////////////////Apply Headplot settings - Intensity(hpIntensityLoad); - w_headPlot.cp5_widget.getController("Intensity").getCaptionLabel().setText(hpIntensityArray[hpIntensityLoad]); - - Polarity(hpPolarityLoad); - w_headPlot.cp5_widget.getController("Polarity").getCaptionLabel().setText(hpPolarityArray[hpPolarityLoad]); - - ShowContours(hpContoursLoad); - w_headPlot.cp5_widget.getController("ShowContours").getCaptionLabel().setText(hpContoursArray[hpContoursLoad]); - - SmoothingHeadPlot(hpSmoothingLoad); - w_headPlot.cp5_widget.getController("SmoothingHeadPlot").getCaptionLabel().setText(hpSmoothingArray[hpSmoothingLoad]); - - //Force redraw headplot on load. Fixes issue where heaplot draws outside of the widget. - w_headPlot.headPlot.setPositionSize(w_headPlot.headPlot.hp_x, w_headPlot.headPlot.hp_y, w_headPlot.headPlot.hp_w, w_headPlot.headPlot.hp_h, w_headPlot.headPlot.hp_win_x, w_headPlot.headPlot.hp_win_y); - - ////////////////////////////Apply Band Power settings - try { - //apply channel checkbox settings - w_bandPower.bpChanSelect.deactivateAllButtons();; - for (int i = 0; i < loadBPActiveChans.size(); i++) { - w_bandPower.bpChanSelect.setToggleState(loadBPActiveChans.get(i), true); - } - } catch (Exception e) { - println("Settings: Exception caught applying band power settings " + e); - } - verbosePrint("Settings: Band Power Active Channels: " + loadBPActiveChans); - - ////////////////////////////Apply Spectrogram settings - //Apply Max Freq dropdown - SpectrogramMaxFreq(spectMaxFrqLoad); - w_spectrogram.cp5_widget.getController("SpectrogramMaxFreq").getCaptionLabel().setText(spectMaxFrqArray[spectMaxFrqLoad]); - SpectrogramSampleRate(spectSampleRateLoad); - w_spectrogram.cp5_widget.getController("SpectrogramSampleRate").getCaptionLabel().setText(spectSampleRateArray[spectSampleRateLoad]); - SpectrogramLogLin(spectLogLinLoad); - w_spectrogram.cp5_widget.getController("SpectrogramLogLin").getCaptionLabel().setText(fftLogLinArray[spectLogLinLoad]); - try { - //apply channel checkbox settings - w_spectrogram.spectChanSelectTop.deactivateAllButtons(); - w_spectrogram.spectChanSelectBot.deactivateAllButtons(); - for (int i = 0; i < loadSpectActiveChanTop.size(); i++) { - w_spectrogram.spectChanSelectTop.setToggleState(loadSpectActiveChanTop.get(i), true); - } - for (int i = 0; i < loadSpectActiveChanBot.size(); i++) { - w_spectrogram.spectChanSelectBot.setToggleState(loadSpectActiveChanBot.get(i), true); - } - } catch (Exception e) { - println("Settings: Exception caught applying spectrogram settings channel bar " + e); - } - println("Settings: Spectrogram Active Channels: TOP - " + loadSpectActiveChanTop + " || BOT - " + loadSpectActiveChanBot); - - ///////////Apply Networking Settings - //Update protocol with loaded value - Protocol(nwProtocolLoad); - //Update dropdowns and textfields in the Networking widget with loaded values - w_networking.cp5_widget.getController("Protocol").getCaptionLabel().setText(w_networking.protocols.get(nwProtocolLoad)); //Reference the dropdown from the appropriate widget - switch (nwProtocolLoad) { - case 3: //Apply OSC if loaded - println("Apply OSC Networking Mode"); - w_networking.cp5_networking_dropdowns.getController("dataType1").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType1)); //Set text on frontend - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType1").setValue(nwDataType1); //Set value in backend - w_networking.cp5_networking_dropdowns.getController("dataType2").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType2)); //etc... - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType2").setValue(nwDataType2); - w_networking.cp5_networking_dropdowns.getController("dataType3").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType3)); - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType3").setValue(nwDataType3); - w_networking.cp5_networking_dropdowns.getController("dataType4").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType4)); - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType4").setValue(nwDataType4); - w_networking.cp5_networking.get(Textfield.class, "OSC_ip1").setText(nwOscIp1Load); //Simply set the text for text boxes - w_networking.cp5_networking.get(Textfield.class, "OSC_ip2").setText(nwOscIp2Load); //The strings are referenced on command - w_networking.cp5_networking.get(Textfield.class, "OSC_ip3").setText(nwOscIp3Load); - w_networking.cp5_networking.get(Textfield.class, "OSC_ip4").setText(nwOscIp4Load); - w_networking.cp5_networking.get(Textfield.class, "OSC_port1").setText(nwOscPort1Load); - w_networking.cp5_networking.get(Textfield.class, "OSC_port2").setText(nwOscPort2Load); - w_networking.cp5_networking.get(Textfield.class, "OSC_port3").setText(nwOscPort3Load); - w_networking.cp5_networking.get(Textfield.class, "OSC_port4").setText(nwOscPort4Load); - break; - case 2: //Apply UDP if loaded - println("Apply UDP Networking Mode"); - w_networking.cp5_networking_dropdowns.getController("dataType1").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType1)); //Set text on frontend - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType1").setValue(nwDataType1); //Set value in backend - w_networking.cp5_networking_dropdowns.getController("dataType2").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType2)); //etc... - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType2").setValue(nwDataType2); - w_networking.cp5_networking_dropdowns.getController("dataType3").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType3)); - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType3").setValue(nwDataType3); - w_networking.cp5_networking.get(Textfield.class, "UDP_ip1").setText(nwUdpIp1Load); - w_networking.cp5_networking.get(Textfield.class, "UDP_ip2").setText(nwUdpIp2Load); - w_networking.cp5_networking.get(Textfield.class, "UDP_ip3").setText(nwUdpIp3Load); - w_networking.cp5_networking.get(Textfield.class, "UDP_port1").setText(nwUdpPort1Load); - w_networking.cp5_networking.get(Textfield.class, "UDP_port2").setText(nwUdpPort2Load); - w_networking.cp5_networking.get(Textfield.class, "UDP_port3").setText(nwUdpPort3Load); - break; - case 1: //Apply LSL if loaded - println("Apply LSL Networking Mode"); - w_networking.cp5_networking_dropdowns.getController("dataType1").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType1)); //Set text on frontend - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType1").setValue(nwDataType1); //Set value in backend - w_networking.cp5_networking_dropdowns.getController("dataType2").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType2)); //etc... - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType2").setValue(nwDataType2); - w_networking.cp5_networking_dropdowns.getController("dataType3").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType3)); - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType3").setValue(nwDataType3); - w_networking.cp5_networking.get(Textfield.class, "LSL_name1").setText(nwLSLName1Load); - w_networking.cp5_networking.get(Textfield.class, "LSL_name2").setText(nwLSLName2Load); - w_networking.cp5_networking.get(Textfield.class, "LSL_name3").setText(nwLSLName3Load); - w_networking.cp5_networking.get(Textfield.class, "LSL_type1").setText(nwLSLType1Load); - w_networking.cp5_networking.get(Textfield.class, "LSL_type2").setText(nwLSLType2Load); - w_networking.cp5_networking.get(Textfield.class, "LSL_type3").setText(nwLSLType3Load); - break; - case 0: //Apply Serial if loaded - println("Apply Serial Networking Mode"); - w_networking.cp5_networking_dropdowns.getController("dataType1").getCaptionLabel().setText(w_networking.dataTypes.get(nwDataType1)); //Set text on frontend - w_networking.cp5_networking_dropdowns.get(ScrollableList.class, "dataType1").setValue(nwDataType1); //Set value in backend - w_networking.cp5_networking_baudRate.getController("baud_rate").getCaptionLabel().setText(w_networking.baudRates.get(nwSerialBaudRateLoad)); //Set text - w_networking.cp5_networking_baudRate.get(ScrollableList.class, "baud_rate").setValue(nwSerialBaudRateLoad); //Set value in backend - - //Look for the portName in the dropdown list - int listSize = w_networking.cp5_networking_portName.get(ScrollableList.class, "port_name").getItems().size(); - for (int i = 0; i < listSize; i++) { - String s = w_networking.cp5_networking_portName.get(ScrollableList.class, "port_name").getItem(i).get("name").toString(); - if (s.equals(nwSerialPort)) { - verbosePrint("Settings: NWSerial: Found com port " + s + " !"); - w_networking.cp5_networking_portName.getController("port_name").getCaptionLabel().setText(s); - w_networking.cp5_networking_portName.get(ScrollableList.class, "port_name").setValue(i); - break; - } else { - if (i == listSize - 1) verbosePrint("Settings: NWSerial: Port not found..."); - } - } - break; - }//end switch-case for networking settings for all networking protocols - - ////////////////////////////Apply EMG widget settings - try { - //apply channel checkbox settings - w_emg.emgChannelSelect.deactivateAllButtons();; - for (int i = 0; i < loadEmgActiveChannels.size(); i++) { - w_emg.emgChannelSelect.setToggleState(loadEmgActiveChannels.get(i), true); - } - } catch (Exception e) { - println("Settings: Exception caught applying EMG widget settings " + e); + } + + /** + * Apply networking settings + */ + private void applyNetworkingSettings() { + dataProcessing.networkingSettings.loadJson( + loadSettingsJSONData.getJSONObject(KEY_NETWORKING).toString()); + } + + /** + * Apply widget layout and container positions + */ + private void applyWidgetLayout() { + // Set layout first + widgetManager.setNewContainerLayout(currentLayout); + + // Deactivate all widgets initially + for (Widget widget : widgetManager.widgets) { + widget.setIsActive(false); } - verbosePrint("Settings: EMG Widget Active Channels: " + loadEmgActiveChannels); - - ////////////////////////////Apply EMG Joystick settings - w_emgJoystick.setJoystickSmoothing(loadEmgJoystickSmoothing); - w_emgJoystick.cp5_widget.getController("emgJoystickSmoothingDropdown").getCaptionLabel() - .setText(EmgJoystickSmoothing.getEnumStringsAsList().get(loadEmgJoystickSmoothing)); - try { - for (int i = 0; i < loadEmgJoystickInputs.size(); i++) { - w_emgJoystick.updateJoystickInput(i, loadEmgJoystickInputs.get(i)); - } - } catch (Exception e) { - println("Settings: Exception caught applying EMG Joystick settings " + e); + + // Get widget container settings + JSONObject containerSettings = loadSettingsJSONData.getJSONObject(KEY_CONTAINERS); + + // Activate widgets and set containers + // Fix: Properly handle keys as a Set from containerSettings.keys() + for (Object keyObj : containerSettings.keys()) { + String key = keyObj.toString(); + String[] keyParts = split(key, '_'); + int widgetIndex = Integer.valueOf(keyParts[1]); + int containerIndex = containerSettings.getInt(key); + + Widget widget = widgetManager.widgets.get(widgetIndex); + widget.setIsActive(true); + widget.setContainer(containerIndex); } + } + + /** + * Apply individual widget settings + */ + private void applyWidgetSettings() { + widgetManager.loadWidgetSettingsFromJson( + loadSettingsJSONData.getJSONObject(KEY_WIDGET_SETTINGS).toString()); + } - ////////////////////////////Apply Marker Widget settings - w_marker.setMarkerWindow(loadMarkerWindow); - w_marker.cp5_widget.getController("markerWindowDropdown").getCaptionLabel().setText(w_marker.getMarkerWindow().getString()); - w_marker.setMarkerVertScale(loadMarkerVertScale); - w_marker.cp5_widget.getController("markerVertScaleDropdown").getCaptionLabel().setText(w_marker.getMarkerVertScale().getString()); - - //////////////////////////////////////////////////////////// - // Apply more loaded widget settings above this line // - - } //end of loadApplyWidgetDropdownText() + private void applyFilterSettings() { + JSONObject filterSettingsJSON = loadSettingsJSONData.getJSONObject(KEY_FILTER_SETTINGS); + String filterSettingsString = filterSettingsJSON.toString(); + filterSettings.loadSettingsFromJson(filterSettingsString); + } - private void loadApplyTimeSeriesSettings() { + private void applyEmgSettings() { + JSONObject emgSettingsJSON = loadSettingsJSONData.getJSONObject(KEY_EMG_SETTINGS); + String emgSettingsString = emgSettingsJSON.toString(); + dataProcessing.emgSettings.loadSettingsFromJson(emgSettingsString); + } - JSONObject loadTimeSeriesSettings = loadSettingsJSONData.getJSONObject(kJSONKeyTimeSeries); - ////////Apply Time Series widget settings - w_timeSeries.setTSVertScale(loadTimeSeriesSettings.getInt("Time Series Vert Scale")); - w_timeSeries.cp5_widget.getController("VertScale_TS").getCaptionLabel().setText(w_timeSeries.getTSVertScale().getString()); //changes front-end + /** + * Get the appropriate settings file path based on mode and configuration + */ + String getPath(String mode, int dataSource, int channelCount) { + // Determine which settings file to use + int modeIndex = mode.equals("Default") ? FILE_DEFAULT : FILE_USER; + int fileIndex; - w_timeSeries.setTSHorizScale(loadTimeSeriesSettings.getInt("Time Series Horiz Scale")); - w_timeSeries.cp5_widget.getController("Duration").getCaptionLabel().setText(w_timeSeries.getTSHorizScale().getString()); - - JSONArray loadTSChan = loadTimeSeriesSettings.getJSONArray("activeChannels"); - w_timeSeries.tsChanSelect.deactivateAllButtons(); - try { - for (int i = 0; i < loadTSChan.size(); i++) { - w_timeSeries.tsChanSelect.setToggleState(loadTSChan.getInt(i), true); + if (dataSource == DATASOURCE_CYTON) { + fileIndex = (channelCount == CYTON_CHANNEL_COUNT) ? 0 : 1; + } else if (dataSource == DATASOURCE_GANGLION) { + fileIndex = 2; + } else if (dataSource == DATASOURCE_PLAYBACKFILE) { + fileIndex = 3; + } else if (dataSource == DATASOURCE_SYNTHETIC) { + if (channelCount == GANGLION_CHANNEL_COUNT) { + fileIndex = 4; + } else if (channelCount == CYTON_CHANNEL_COUNT) { + fileIndex = 5; + } else { + fileIndex = 6; } - } catch (Exception e) { - println("Settings: Exception caught applying time series settings " + e); + } else { + return "Error"; } - verbosePrint("Settings: Time Series Active Channels: " + loadBPActiveChans); - - } //end loadApplyTimeSeriesSettings + + return directoryManager.getSettingsPath() + SETTING_FILES[fileIndex][modeIndex]; + } /** - * @description Used in TopNav when user clicks ClearSettings->AreYouSure->Yes - * @params none - * Output Success message to bottom of GUI when done - */ + * Clear all settings files + */ void clearAll() { - for (File file: new File(directoryManager.getSettingsPath()).listFiles()) - if (!file.isDirectory()) + // Delete all settings files + for (File file : new File(directoryManager.getSettingsPath()).listFiles()) { + if (!file.isDirectory()) { file.delete(); + } + } + + // Clear playback history controlPanel.recentPlaybackBox.rpb_cp5.get(ScrollableList.class, "recentPlaybackFilesCP").clear(); controlPanel.recentPlaybackBox.shortFileNames.clear(); controlPanel.recentPlaybackBox.longFilePaths.clear(); + outputSuccess("All settings have been cleared!"); } /** - * @description Used in System Init, TopNav, and Interactivity - * @params mode="User"or"Default", dataSource, and number of channels - * @returns {String} - filePath or Error if mode not specified correctly - */ - String getPath(String _mode, int dataSource, int _nchan) { - String filePath = directoryManager.getSettingsPath(); - String[] fileNames = new String[7]; - if (_mode.equals("Default")) { - fileNames = defaultSettingsFiles; - } else if (_mode.equals("User")) { - fileNames = userSettingsFiles; - } else { - filePath = "Error"; - } - if (!filePath.equals("Error")) { - if (dataSource == DATASOURCE_CYTON) { - filePath += (_nchan == NCHAN_CYTON) ? - fileNames[0] : - fileNames[1]; - } else if (dataSource == DATASOURCE_GANGLION) { - filePath += fileNames[2]; - } else if (dataSource == DATASOURCE_PLAYBACKFILE) { - filePath += fileNames[3]; - } else if (dataSource == DATASOURCE_SYNTHETIC) { - if (_nchan == NCHAN_GANGLION) { - filePath += fileNames[4]; - } else if (_nchan == NCHAN_CYTON) { - filePath += fileNames[5]; - } else { - filePath += fileNames[6]; - } - } - } - return filePath; - } - - void initCheckPointFive() { - outputSuccess("Session started!"); - } - + * Handle key press to load settings + */ void loadKeyPressed() { loadErrorTimerStart = millis(); - String settingsFileToLoad = getPath("User", eegDataSource, nchan); + String settingsFile = getPath("User", eegDataSource, globalChannelCount); + try { - load(settingsFileToLoad); + load(settingsFile); errorUserSettingsNotFound = false; + outputSuccess("Settings Loaded!"); } catch (Exception e) { - //println(e.getMessage()); - e.printStackTrace(); - println(settingsFileToLoad + " not found or other error. Save settings with keyboard 'n' or using dropdown menu."); errorUserSettingsNotFound = true; + handleLoadError(settingsFile); } - //Output message when Loading settings is complete - String err = null; - if (chanNumError == false && dataSourceError == false && errorUserSettingsNotFound == false && loadErrorCytonEvent == false) { - outputSuccess("Settings Loaded!"); - } else if (chanNumError) { - err = "Invalid number of channels"; + } + + /** + * Handle errors when loading settings + */ + private void handleLoadError(String settingsFile) { + if (chanNumError) { + outputError("Settings Error: Channel Number Mismatch"); } else if (dataSourceError) { - err = "Invalid data source"; - } else if (errorUserSettingsNotFound) { - err = settingsFileToLoad + " not found."; - } - - //Only try to delete file for SettingsNotFound/Broken settings - if (err != null && (!chanNumError && !dataSourceError)) { - println("Load Settings Error: " + err); - File f = new File(settingsFileToLoad); - if (f.exists()) { - if (f.delete()) { - outputError("Found old/broken GUI settings. Please reconfigure the GUI and save new settings."); - } else { - outputError("SessionSettings: Error deleting old/broken settings file..."); - } + outputError("Settings Error: Data Source Mismatch"); + } else { + File f = new File(settingsFile); + if (f.exists() && f.delete()) { + outputError("Found old/broken GUI settings. Please reconfigure the GUI and save new settings."); + } else if (f.exists()) { + outputError("Error deleting old/broken settings file."); } } } + /** + * Handle save button press + */ void saveButtonPressed() { if (saveDialogName == null) { - selectOutput("Save a custom settings file as JSON:", - "saveConfigFile", - dataFile(settings.getPath("User", eegDataSource, nchan))); + // Open file chooser dialog + File fileToSave = dataFile(getPath("User", eegDataSource, globalChannelCount)); + new FileChooser(FileChooserMode.SAVE, "saveConfigFile", fileToSave, + "Save settings to file"); } else { - println("saveSettingsFileName = " + saveDialogName); saveDialogName = null; } } + /** + * Handle load button press + */ void loadButtonPressed() { - //Select file to load from dialog box if (loadDialogName == null) { - selectInput("Load a custom settings file from JSON:", "loadConfigFile"); + // Open file chooser dialog + new FileChooser(FileChooserMode.LOAD, "loadConfigFile", + new File(directoryManager.getGuiDataPath() + "Settings"), + "Select a settings file to load"); saveDialogName = null; } else { - println("loadSettingsFileName = " + loadDialogName); loadDialogName = null; } } + /** + * Reset to default settings + */ void defaultButtonPressed() { - //Revert GUI to default settings that were flashed on system start! - String defaultSettingsFileToLoad = getPath("Default", eegDataSource, nchan); + String defaultFile = getPath("Default", eegDataSource, globalChannelCount); try { - //Load all saved User Settings from a JSON file to see if it exists - JSONObject loadDefaultSettingsJSONData = loadJSONObject(defaultSettingsFileToLoad); - this.load(defaultSettingsFileToLoad); + // Check if default settings exist and load them + loadJSONObject(defaultFile); + load(defaultFile); outputSuccess("Default Settings Loaded!"); } catch (Exception e) { outputError("Default Settings Error: Valid Default Settings will be saved next system start."); - File f = new File(defaultSettingsFileToLoad); - if (f.exists()) { - if (f.delete()) { - println("SessionSettings: Old/Broken Default Settings file succesfully deleted."); - } else { - println("SessionSettings: Error deleting Default Settings file..."); - } + File f = new File(defaultFile); + if (f.exists() && !f.delete()) { + println("SessionSettings: Error deleting Default Settings file..."); } } } -} //end of Software Settings class + /** + * Auto-load settings at startup + */ + public void autoLoadSessionSettings() { + loadKeyPressed(); + } +} -////////////////////////////////////////// -// Global Functions // -// Called by Buttons with the same name // -////////////////////////////////////////// -// Select file to save custom settings using dropdown in TopNav.pde +/** + * Process file selection for saving settings + */ void saveConfigFile(File selection) { if (selection == null) { - println("SessionSettings: saveConfigFile: Window was closed or the user hit cancel."); - } else { - println("SessionSettings: saveConfigFile: User selected " + selection.getAbsolutePath()); - settings.saveDialogName = selection.getAbsolutePath(); - settings.save(settings.saveDialogName); //save current settings to JSON file in SavedData - outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key. Click \"Default\" to revert to factory settings."); //print success message to screen - settings.saveDialogName = null; //reset this variable for future use + return; } + + sessionSettings.save(selection.getAbsolutePath()); + outputSuccess("Settings Saved! Using Expert Mode, you can load these settings using 'N' key."); + sessionSettings.saveDialogName = null; } -// Select file to load custom settings using dropdown in TopNav.pde + +/** + * Process file selection for loading settings + */ void loadConfigFile(File selection) { if (selection == null) { - println("SessionSettings: loadConfigFile: Window was closed or the user hit cancel."); + return; + } + + try { + sessionSettings.load(selection.getAbsolutePath()); + if (!sessionSettings.chanNumError && !sessionSettings.dataSourceError && + !sessionSettings.loadErrorCytonEvent) { + outputSuccess("Settings Loaded!"); + } + } catch (Exception e) { + handleLoadConfigError(selection); + } + + sessionSettings.loadDialogName = null; +} + +/** + * Handle errors in loadConfigFile + */ +void handleLoadConfigError(File selection) { + if (sessionSettings.chanNumError) { + outputError("Settings Error: Channel Number Mismatch Detected"); + } else if (sessionSettings.dataSourceError) { + outputError("Settings Error: Data Source Mismatch Detected"); } else { - println("SessionSettings: loadConfigFile: User selected " + selection.getAbsolutePath()); - //output("You have selected \"" + selection.getAbsolutePath() + "\" to Load custom settings."); - settings.loadDialogName = selection.getAbsolutePath(); - try { - settings.load(settings.loadDialogName); //load settings from JSON file in /data/ - //Output success message when Loading settings is complete without errors - if (settings.chanNumError == false - && settings.dataSourceError == false - && settings.loadErrorCytonEvent == false) { - outputSuccess("Settings Loaded!"); - } - } catch (Exception e) { - println("SessionSettings: Incompatible settings file or other error"); - if (settings.chanNumError == true) { - outputError("Settings Error: Channel Number Mismatch Detected"); - } else if (settings.dataSourceError == true) { - outputError("Settings Error: Data Source Mismatch Detected"); - } else { - outputError("Error trying to load settings file, possibly from previous GUI. Removing old settings."); - if (selection.exists()) selection.delete(); - } + outputError("Error trying to load settings file, possibly from previous GUI."); + if (selection.exists()) { + selection.delete(); } - settings.loadDialogName = null; //reset this variable for future use } } \ No newline at end of file diff --git a/OpenBCI_GUI/SignalCheckThresholds.pde b/OpenBCI_GUI/SignalCheckThresholds.pde index 94d698814..f24a9bea0 100644 --- a/OpenBCI_GUI/SignalCheckThresholds.pde +++ b/OpenBCI_GUI/SignalCheckThresholds.pde @@ -7,51 +7,45 @@ class SignalCheckThresholdUI { private int defaultValue_kOhms; private int valuePercentage; private int valuekOhms; - private CytonSignalCheckMode signalCheckMode; + private CytonSignalCheckMode signalCheckModeCyton; private color textColor = OPENBCI_DARKBLUE; - private boolean hasUpdatedTextColor = false; + private color isActiveBorderColor; - SignalCheckThresholdUI(ControlP5 _cp5, String _name, int _x, int _y, int _w, int _h, color _textColor, CytonSignalCheckMode _mode) { - signalCheckMode = _mode; + SignalCheckThresholdUI(ControlP5 _cp5, String _name, int _x, int _y, int _w, int _h, color _isActiveBorderColor, CytonSignalCheckMode _mode) { + signalCheckModeCyton = _mode; name = _name; - textColor = _textColor; + isActiveBorderColor = _isActiveBorderColor; defaultValue_Percentage = name.equals("errorThreshold") ? 90 : 75; valuePercentage = defaultValue_Percentage; defaultValue_kOhms = name == "errorThreshold" ? 2500 : 750; valuekOhms = defaultValue_kOhms; - thresholdTF = createTextfield(_cp5, _name, 0, _x, _y, _w, _h, _textColor); + thresholdTF = createTextfield(_cp5, _name, 0, _x, _y, _w, _h, _isActiveBorderColor); updateTextfieldModeChanged(_mode); //textfieldHeight = _h; } public void update() { - if (!hasUpdatedTextColor) { - thresholdTF.setColorValueLabel(textColor); - thresholdTF.setColorActive(textColor); - hasUpdatedTextColor = true; - } - textfieldUpdateHelper.checkTextfield(thresholdTF); } - + public void updateTextfieldModeChanged(CytonSignalCheckMode _mode) { - signalCheckMode = _mode; + signalCheckModeCyton = _mode; customThreshold(thresholdTF, getTextfieldIntVal()); } - private Textfield createTextfield(ControlP5 _cp5, String name, int intValue, int _x, int _y, int _w, int _h, color _textColor) { + private Textfield createTextfield(ControlP5 _cp5, String name, int intValue, int _x, int _y, int _w, int _h, color _isActiveBorderColor) { //Create these textfields under cp5_widget base instance so because they are always visible final Textfield myTextfield = _cp5.addTextfield(name) .setPosition(_x, _y) .setCaptionLabel("") .setSize(_w, _h) - .setFont(f5) + .setFont(p5) .setFocus(false) .setColor(color(26, 26, 26)) .setColorBackground(color(255, 255, 255)) // text field bg color - .setColorValueLabel(_textColor) // text color - .setColorForeground(color(210)) // border color when not selected - grey - .setColorActive(isSelected_color) // border color when selected - green + .setColorValueLabel(textColor) // text color + .setColorForeground(isActiveBorderColor) // border color when not selected - grey + .setColorActive(isSelected_color) // border color when selected .setColorCursor(color(26, 26, 26)) .setText("%") //set the text .align(5, 10, 20, 40) @@ -107,6 +101,10 @@ class SignalCheckThresholdUI { thresholdTF.setPosition(_x, _y); } + public float[] getPosition() { + return thresholdTF.getPosition(); + } + private int getDefaultTextfieldIntVal() { return isSignalCheckRailedMode() ? defaultValue_Percentage : defaultValue_kOhms; } @@ -118,26 +116,33 @@ class SignalCheckThresholdUI { private void setTextfieldVal(int val) { if (isSignalCheckRailedMode()) { if (name == "errorThreshold") { - for (int i = 0; i < nchan; i++) { + for (int i = 0; i < globalChannelCount; i++) { is_railed[i].setRailedThreshold((double) val); } } else { - for (int i = 0; i < nchan; i++) { + for (int i = 0; i < globalChannelCount; i++) { is_railed[i].setRailedWarnThreshold((double) val); } } valuePercentage = val; } else { - if (name == "errorThreshold") { - w_cytonImpedance.updateElectrodeStatusYellowThreshold((double)val); - } else { - w_cytonImpedance.updateElectrodeStatusGreenThreshold((double)val); + if (currentBoard instanceof BoardCyton) { + W_CytonImpedance cytonImpedanceWidget = (W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance"); + if (name == "errorThreshold") { + cytonImpedanceWidget.updateElectrodeStatusYellowThreshold((double)val); + } else { + cytonImpedanceWidget.updateElectrodeStatusGreenThreshold((double)val); + } } valuekOhms = val; } } private boolean isSignalCheckRailedMode() { - return signalCheckMode == CytonSignalCheckMode.LIVE; + if (currentBoard instanceof BoardCyton) { + return signalCheckModeCyton == CytonSignalCheckMode.LIVE; + } else { + return false; + } } }; \ No newline at end of file diff --git a/OpenBCI_GUI/SpectrogramEnums.pde b/OpenBCI_GUI/SpectrogramEnums.pde new file mode 100644 index 000000000..7b1a6d86e --- /dev/null +++ b/OpenBCI_GUI/SpectrogramEnums.pde @@ -0,0 +1,82 @@ +public enum SpectrogramMaxFrequency implements IndexingInterface { + MAX_20 (0, 20, "20 Hz", new int[]{20, 15, 10, 5, 0, 5, 10, 15, 20}), + MAX_40 (1, 40, "40 Hz", new int[]{40, 30, 20, 10, 0, 10, 20, 30, 40}), + MAX_60 (2, 60, "60 Hz", new int[]{60, 45, 30, 15, 0, 15, 30, 45, 60}), + MAX_100 (3, 100, "100 Hz", new int[]{100, 75, 50, 25, 0, 25, 50, 75, 100}), + MAX_120 (4, 120, "120 Hz", new int[]{120, 90, 60, 30, 0, 30, 60, 90, 120}), + MAX_250 (5, 250, "250 Hz", new int[]{250, 188, 125, 63, 0, 63, 125, 188, 250}); + + private int index; + private final int value; + private String label; + private final int[] axisLabels; + + SpectrogramMaxFrequency(int index, int value, String label, int[] axisLabels) { + this.index = index; + this.value = value; + this.label = label; + this.axisLabels = axisLabels; + } + + public int getValue() { + return value; + } + + public int[] getAxisLabels() { + return axisLabels; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum SpectrogramWindowSize implements IndexingInterface { + ONE_MINUTE (0, 1f, "1 Min.", new float[]{1, .5, 0}, 25), + ONE_MINUTE_THIRTY (1, 1.5f, "1.5 Min.", new float[]{1.5, 1, .5, 0}, 50), + THREE_MINUTES (2, 3f, "3 Min.", new float[]{3, 2, 1, 0}, 100), + SIX_MINUTES (3, 6f, "6 Min.", new float[]{6, 5, 4, 3, 2, 1, 0}, 200), + THIRTY_MINUTES (4, 30f, "30 Min.", new float[]{30, 25, 20, 15, 10, 5, 0}, 1000); + + private int index; + private final float value; + private String label; + private final float[] axisLabels; + private final int scrollSpeed; + + SpectrogramWindowSize(int index, float value, String label, float[] axisLabels, int scrollSpeed) { + this.index = index; + this.value = value; + this.label = label; + this.axisLabels = axisLabels; + this.scrollSpeed = scrollSpeed; + } + + public float getValue() { + return value; + } + + public float[] getAxisLabels() { + return axisLabels; + } + + public int getScrollSpeed() { + return scrollSpeed; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} diff --git a/OpenBCI_GUI/TimeSeriesEnums.pde b/OpenBCI_GUI/TimeSeriesEnums.pde new file mode 100644 index 000000000..a45a63323 --- /dev/null +++ b/OpenBCI_GUI/TimeSeriesEnums.pde @@ -0,0 +1,100 @@ +public enum TimeSeriesXLim implements IndexingInterface +{ + ONE (0, 1, "1 sec"), + THREE (1, 3, "3 sec"), + FIVE (2, 5, "5 sec"), + TEN (3, 10, "10 sec"), + TWENTY (4, 20, "20 sec"); + + private int index; + private int value; + private String label; + + TimeSeriesXLim(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum TimeSeriesYLim implements IndexingInterface +{ + AUTO (0, 0, "Auto"), + UV_10(1, 10, "10 uV"), + UV_25(2, 25, "25 uV"), + UV_50 (3, 50, "50 uV"), + UV_100 (4, 100, "100 uV"), + UV_200 (5, 200, "200 uV"), + UV_400 (6, 400, "400 uV"), + UV_1000 (7, 1000, "1000 uV"), + UV_10000 (8, 10000, "10000 uV"); + + private int index; + private int value; + private String label; + + TimeSeriesYLim(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} + +public enum TimeSeriesLabelMode implements IndexingInterface +{ + OFF (0, 0, "Off"), + MINIMAL (1, 1, "Minimal"), + ON (2, 2, "On"); + + private int index; + private int value; + private String label; + + TimeSeriesLabelMode(int _index, int _value, String _label) { + this.index = _index; + this.value = _value; + this.label = _label; + } + + public int getValue() { + return value; + } + + @Override + public String getString() { + return label; + } + + @Override + public int getIndex() { + return index; + } +} \ No newline at end of file diff --git a/OpenBCI_GUI/TimeSeriesWidgetHelperClasses.pde b/OpenBCI_GUI/TimeSeriesWidgetHelperClasses.pde new file mode 100644 index 000000000..994cba5c4 --- /dev/null +++ b/OpenBCI_GUI/TimeSeriesWidgetHelperClasses.pde @@ -0,0 +1,718 @@ + +//======================================================================================================================== +// CHANNEL BAR CLASS -- Implemented by Time Series 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) +class ChannelBar { + + int channelIndex; //duh + String channelString; + int x, y, w, h; + int defaultH; + ControlP5 cbCp5; + Button onOffButton; + int onOff_diameter; + int yScaleButton_h; + int yScaleButton_w; + Button yScaleButton_pos; + Button yScaleButton_neg; + int yAxisLabel_h; + private TextBox yAxisMax; + private TextBox yAxisMin; + + int yAxisUpperLim; + int yAxisLowerLim; + int uiSpaceWidth; + int padding_4 = 4; + int minimumChannelHeight; + int plotBottomWellH = 35; + + GPlot plot; //the actual grafica-based GPlot that will be rendering the Time Se ries trace + GPointsArray channelPoints; + int nPoints; + int numSeconds; + float timeBetweenPoints; + private GPlotAutoscaler gplotAutoscaler; + + color channelColor; //color of plot trace + + TextBox voltageValue; + TextBox impValue; + + boolean drawVoltageValue; + + ChannelBar(PApplet _parentApplet, int _channelIndex, int _x, int _y, int _w, int _h, PImage expand_default, PImage expand_hover, PImage expand_active, PImage contract_default, PImage contract_hover, PImage contract_active) { + + cbCp5 = new ControlP5(ourApplet); + cbCp5.setGraphics(ourApplet, x, y); + cbCp5.setAutoDraw(false); //Setting this saves code as cp5 elements will only be drawn/visible when [cp5].draw() is called + + channelIndex = _channelIndex; + channelString = str(channelIndex + 1); + + x = _x; + y = _y; + w = _w; + h = _h; + defaultH = h; + + 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); + + //Create GPlot for this Channel + uiSpaceWidth = 36 + padding_4; + yAxisUpperLim = 200; + yAxisLowerLim = -200; + numSeconds = 5; + plot = new GPlot(_parentApplet); + plot.setPos(x + uiSpaceWidth, y); + plot.setDim(w - uiSpaceWidth, h); + plot.setMar(0f, 0f, 0f, 0f); + plot.setLineColor((int)channelColors[channelIndex%8]); + plot.setXLim(-5,0); + plot.setYLim(yAxisLowerLim, yAxisUpperLim); + plot.setPointSize(2); + plot.setPointColor(0); + plot.setAllFontProperties("Arial", 0, 14); + plot.getXAxis().setFontColor(OPENBCI_DARKBLUE); + plot.getXAxis().setLineColor(OPENBCI_DARKBLUE); + plot.getXAxis().getAxisLabel().setFontColor(OPENBCI_DARKBLUE); + if (channelIndex == globalChannelCount-1) { + plot.getXAxis().setAxisLabelText("Time (s)"); + plot.getXAxis().getAxisLabel().setOffset(plotBottomWellH/2 + 5f); + } + gplotAutoscaler = new GPlotAutoscaler(); + + //Fill the GPlot with initial data + nPoints = nPointsBasedOnDataSource(); + channelPoints = new GPointsArray(nPoints); + timeBetweenPoints = (float)numSeconds / (float)nPoints; + for (int i = 0; i < nPoints; i++) { + float time = -(float)numSeconds + (float)i*timeBetweenPoints; + float filt_uV_value = 0.0; //0.0 for all points to start + GPoint tempPoint = new GPoint(time, filt_uV_value); + channelPoints.set(i, tempPoint); + } + plot.setPoints(channelPoints); //set the plot with 0.0 for all channelPoints to start + + //Create a UI to custom scale the Y axis for this channel + yScaleButton_w = 18; + yScaleButton_h = 18; + yAxisLabel_h = 12; + int padding = 2; + yAxisMax = new TextBox("+"+yAxisUpperLim+"uV", x + uiSpaceWidth + padding, y + int(padding*1.5), OPENBCI_DARKBLUE, color(255,255,255,175), LEFT, TOP); + yAxisMin = new TextBox(yAxisLowerLim+"uV", x + uiSpaceWidth + padding, y + h - yAxisLabel_h - padding_4, OPENBCI_DARKBLUE, color(255,255,255,175), LEFT, TOP); + customYLim(yAxisMax, yAxisUpperLim); + customYLim(yAxisMin, yAxisLowerLim); + yScaleButton_neg = createYScaleButton(channelIndex, false, "decreaseYscale", "-T", x + uiSpaceWidth + padding, y + w/2 - yScaleButton_h/2, yScaleButton_w, yScaleButton_h, contract_default, contract_hover, contract_active); + yScaleButton_pos = createYScaleButton(channelIndex, true, "increaseYscale", "+T", x + uiSpaceWidth + padding*2 + yScaleButton_w, y + w/2 - yScaleButton_h/2, yScaleButton_w, yScaleButton_h, expand_default, expand_hover, expand_active); + + //Create textBoxes to display the current values + impValue = new TextBox("", x + uiSpaceWidth + (int)plot.getDim()[0], y + padding, OPENBCI_DARKBLUE, color(255,255,255,175), RIGHT, TOP); + voltageValue = new TextBox("", x + uiSpaceWidth + (int)plot.getDim()[0] - padding, y + h, OPENBCI_DARKBLUE, color(255,255,255,175), RIGHT, BOTTOM); + drawVoltageValue = true; + + //Establish a minimumChannelHeight + minimumChannelHeight = padding_4 + yAxisLabel_h*2; + } + + void update(boolean hardwareSettingsAreOpen, TimeSeriesLabelMode _labelMode) { + + //Reusable variables + String fmt; float val; + + //Update the voltage value TextBox + val = dataProcessing.data_std_uV[channelIndex]; + voltageValue.string = String.format(getFmt(val),val) + " uVrms"; + if (is_railed != null) { + voltageValue.setText(is_railed[channelIndex].notificationString + voltageValue.string); + voltageValue.setTextColor(OPENBCI_DARKBLUE); + color bgColor = color(255,255,255,175); // Default white background for voltage TextBox + if (is_railed[channelIndex].is_railed) { + bgColor = SIGNAL_CHECK_RED_LOWALPHA; + } else if (is_railed[channelIndex].is_railed_warn) { + bgColor = SIGNAL_CHECK_YELLOW_LOWALPHA; + } + voltageValue.setBackgroundColor(bgColor); + } + + //update the impedance values + val = data_elec_imp_ohm[channelIndex]/1000; + fmt = String.format(getFmt(val),val) + " kOhm"; + if (is_railed != null && is_railed[channelIndex].is_railed == true) { + fmt = "RAILED - " + fmt; + } + impValue.setText(fmt); + + // update data in plot + updatePlotPoints(); + + if (currentBoard.isEXGChannelActive(channelIndex)) { + onOffButton.setColorBackground(channelColors[channelIndex%8]); // power down == false, set color to vibrant + } + else { + onOffButton.setColorBackground(50); // power down == true, set to grey + } + + //Hide yAxisButtons when hardware settings are open, using autoscale, and labels are turn on + boolean b = !hardwareSettingsAreOpen + && h > minimumChannelHeight + && !gplotAutoscaler.getEnabled() + && _labelMode == TimeSeriesLabelMode.ON; + yScaleButton_pos.setVisible(b); + yScaleButton_neg.setVisible(b); + yScaleButton_pos.setUpdate(b); + yScaleButton_neg.setUpdate(b); + b = !hardwareSettingsAreOpen + && h > minimumChannelHeight + && _labelMode == TimeSeriesLabelMode.ON; + yAxisMin.setVisible(b); + yAxisMax.setVisible(b); + voltageValue.setVisible(_labelMode != TimeSeriesLabelMode.OFF); + } + + 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 void updatePlotPoints() { + float[][] buffer = downsampledFilteredBuffer.getBuffer(); + final int bufferSize = buffer[channelIndex].length; + final int startIndex = bufferSize - nPoints; + for (int i = startIndex; i < bufferSize; i++) { + int adjustedIndex = i - startIndex; + float time = -(float)numSeconds + (float)(adjustedIndex)*timeBetweenPoints; + float filt_uV_value = buffer[channelIndex][i]; + channelPoints.set(adjustedIndex, time, filt_uV_value, ""); + } + plot.setPoints(channelPoints); + + gplotAutoscaler.update(plot, channelPoints); + + if (gplotAutoscaler.getEnabled()) { + float[] minMax = gplotAutoscaler.getMinMax(); + customYLim(yAxisMin, (int)minMax[0]); + customYLim(yAxisMax, (int)minMax[1]); + } + } + + public void draw(boolean hardwareSettingsAreOpen) { + + plot.beginDraw(); + plot.drawBox(); + plot.drawGridLines(GPlot.VERTICAL); + try { + plot.drawLines(); + } catch (NullPointerException e) { + e.printStackTrace(); + println("PLOT ERROR ON CHANNEL " + channelIndex); + + } + //Draw the x axis label on the bottom channel bar, hide if hardware settings are open + if (isBottomChannel() && !hardwareSettingsAreOpen) { + plot.drawXAxis(); + plot.getXAxis().draw(); + } + plot.endDraw(); + + //draw channel holder background + pushStyle(); + stroke(OPENBCI_BLUE_ALPHA50); + noFill(); + rect(x,y,w,h); + popStyle(); + + //draw channelBar separator line in the middle of INTER_CHANNEL_BAR_SPACE + if (!isBottomChannel()) { + pushStyle(); + stroke(OPENBCI_DARKBLUE); + strokeWeight(1); + int separator_y = y + h + int(widgetManager.getTimeSeriesWidget().INTER_CHANNEL_BAR_SPACE / 2); + line(x, separator_y, x + w, separator_y); + popStyle(); + } + + //draw impedance values in time series also for each channel + drawVoltageValue = true; + if (currentBoard instanceof ImpedanceSettingsBoard) { + if (((ImpedanceSettingsBoard)currentBoard).isCheckingImpedance(channelIndex)) { + impValue.draw(); + drawVoltageValue = false; + } + } + + if (drawVoltageValue) { + voltageValue.draw(); + } + + try { + cbCp5.draw(); + } catch (NullPointerException e) { + e.printStackTrace(); + println("CP5 ERROR ON CHANNEL " + channelIndex); + } + + yAxisMin.draw(); + yAxisMax.draw(); + } + + private int nPointsBasedOnDataSource() { + return (numSeconds * currentBoard.getSampleRate()) / getDownsamplingFactor(); + } + + public void adjustTimeAxis(int _newTimeSize) { + numSeconds = _newTimeSize; + plot.setXLim(-_newTimeSize,0); + + nPoints = nPointsBasedOnDataSource(); + channelPoints = new GPointsArray(nPoints); + timeBetweenPoints = (float)numSeconds / (float)nPoints; + if (_newTimeSize > 1) { + plot.getXAxis().setNTicks(_newTimeSize); //sets the number of axis divisions... + }else{ + plot.getXAxis().setNTicks(10); + } + + updatePlotPoints(); + } + + public void adjustVertScale(int _vertScaleValue) { + boolean enableAutoscale = _vertScaleValue == 0; + gplotAutoscaler.setEnabled(enableAutoscale); + if (enableAutoscale) { + return; + } + yAxisLowerLim = -_vertScaleValue; + yAxisUpperLim = _vertScaleValue; + plot.setYLim(yAxisLowerLim, yAxisUpperLim); + //Update button text + customYLim(yAxisMin, yAxisLowerLim); + customYLim(yAxisMax, yAxisUpperLim); + } + + //Update yAxis text and responsively size Textfield + private void customYLim(TextBox tb, int limit) { + StringBuilder s = new StringBuilder(limit > 0 ? "+" : ""); + s.append(limit); + s.append("uV"); + tb.setText(s.toString()); + } + + public void resize(int _x, int _y, int _w, int _h) { + x = _x; + y = _y; + w = _w; + h = _h; + + //reposition & resize the plot + int plotW = w - uiSpaceWidth; + plot.setPos(x + uiSpaceWidth, y); + plot.setDim(plotW, h); + + int padding = 2; + voltageValue.setPosition(x + uiSpaceWidth + (w - uiSpaceWidth) - padding, y + h); + impValue.setPosition(x + uiSpaceWidth + (int)plot.getDim()[0], y + padding); + + yAxisMax.setPosition(x + uiSpaceWidth + padding, y + int(padding*1.5) - 2); + yAxisMin.setPosition(x + uiSpaceWidth + padding, y + h - yAxisLabel_h - padding - 1); + + final int yAxisLabelWidth = yAxisMax.getWidth(); + int yScaleButtonX = x + uiSpaceWidth + padding_4; + int yScaleButtonY = y + h/2 - yScaleButton_h/2; + boolean enoughSpaceBetweenAxisLabels = h > yScaleButton_h + yAxisLabel_h*2 + 2; + yScaleButtonX += enoughSpaceBetweenAxisLabels ? 0 : yAxisLabelWidth; + yScaleButton_neg.setPosition(yScaleButtonX, yScaleButtonY); + yScaleButtonX += yScaleButton_w + padding; + yScaleButton_pos.setPosition(yScaleButtonX, yScaleButtonY); + + onOff_diameter = h > 26 ? 26 : h - 2; + onOffButton.setSize(onOff_diameter, onOff_diameter); + onOffButton.setPosition(x + 6, y + int(h/2) - int(onOff_diameter/2)); + } + + public void updateCP5(PApplet _parentApplet) { + cbCp5.setGraphics(_parentApplet, 0, 0); + } + + private boolean isBottomChannel() { + int numActiveChannels = widgetManager.getTimeSeriesWidget().tsChanSelect.getActiveChannels().size(); + boolean isLastChannel = channelIndex == widgetManager.getTimeSeriesWidget().tsChanSelect.getActiveChannels().get(numActiveChannels - 1); + return isLastChannel; + } + + public void mousePressed() { + } + + public void mouseReleased() { + } + + private void createOnOffButton(String name, String text, int _x, int _y, int _w, int _h) { + onOffButton = createButton(cbCp5, name, text, _x, _y, _w, _h, 0, h2, 16, channelColors[channelIndex%8], WHITE, BUTTON_HOVER, BUTTON_PRESSED, (Integer) null, -2); + onOffButton.setCircularButton(true); + onOffButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + boolean newState = !currentBoard.isEXGChannelActive(channelIndex); + println("[" + channelString + "] onOff released - " + (newState ? "On" : "Off")); + currentBoard.setEXGChannelActive(channelIndex, newState); + if (currentBoard instanceof ADS1299SettingsBoard) { + W_TimeSeries timeSeriesWidget = widgetManager.getTimeSeriesWidget(); + timeSeriesWidget.adsSettingsController.updateChanSettingsDropdowns(channelIndex, currentBoard.isEXGChannelActive(channelIndex)); + boolean hasUnappliedChanges = currentBoard.isEXGChannelActive(channelIndex) != newState; + timeSeriesWidget.adsSettingsController.setHasUnappliedSettings(channelIndex, hasUnappliedChanges); + } + } + }); + onOffButton.setDescription("Click to toggle channel " + channelString + "."); + } + + private Button createYScaleButton(int chan, boolean shouldIncrease, String bName, String bText, int _x, int _y, int _w, int _h, PImage _default, PImage _hover, PImage _active) { + _default.resize(_w, _h); + _hover.resize(_w, _h); + _active.resize(_w, _h); + final Button myButton = cbCp5.addButton(bName) + .setPosition(_x, _y) + .setSize(_w, _h) + .setColorLabel(color(255)) + .setColorForeground(OPENBCI_BLUE) + .setColorBackground(color(144, 100)) + .setImages(_default, _hover, _active) + ; + myButton.onClick(new yScaleButtonCallbackListener(chan, shouldIncrease)); + return myButton; + } + + private class yScaleButtonCallbackListener implements CallbackListener { + private int channel; + private boolean increase; + private final int hardLimit = 10; + private int yLimOption = TimeSeriesYLim.UV_200.getValue(); + //private int delta = 0; //value to change limits by + + yScaleButtonCallbackListener(int theChannel, boolean isIncrease) { + channel = theChannel; + increase = isIncrease; + } + public void controlEvent(CallbackEvent theEvent) { + verbosePrint("A button was pressed for channel " + (channel+1) + ". Should we increase (or decrease?): " + increase); + + int inc = increase ? 1 : -1; + int factor = yAxisUpperLim > 25 || (yAxisUpperLim == 25 && increase) ? 25 : 5; + int n = (int)(log10(abs(yAxisLowerLim))) * factor * inc; + yAxisLowerLim -= n; + n = (int)(log10(yAxisUpperLim)) * factor * inc; + yAxisUpperLim += n; + + yAxisLowerLim = yAxisLowerLim <= -hardLimit ? yAxisLowerLim : -hardLimit; + yAxisUpperLim = yAxisUpperLim >= hardLimit ? yAxisUpperLim : hardLimit; + plot.setYLim(yAxisLowerLim, yAxisUpperLim); + //Update button text + customYLim(yAxisMin, yAxisLowerLim); + customYLim(yAxisMax, yAxisUpperLim); + } + } +}; + +//======================================================================================================================== +// END OF -- CHANNEL BAR CLASS +//======================================================================================================================== + + +//========================== PLAYBACKSLIDER ========================== +class PlaybackScrollbar { + private final float ps_Padding = 40.0; //used to make room for skip to start button + private int x, y, w, h; + private int swidth, sheight; // width and height of bar + private float xpos, ypos; // x and y position of bar + private float spos; // x position of slider + private float sposMin, sposMax; // max and min values of slider + private boolean over; // is the mouse over the slider? + private boolean locked; + private ControlP5 pbsb_cp5; + private Button skipToStartButton; + private int skipToStart_diameter; + private String currentAbsoluteTimeToDisplay = ""; + private String currentTimeInSecondsToDisplay = ""; + private FileBoard fileBoard; + + private final DateFormat currentTimeFormatShort = new SimpleDateFormat("mm:ss"); + private final DateFormat currentTimeFormatLong = new SimpleDateFormat("HH:mm:ss"); + private final DateFormat timeStampFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + PlaybackScrollbar (int _x, int _y, int _w, int _h, float xp, float yp, int sw, int sh) { + x = _x; + y = _y; + w = _w; + h = _h; + swidth = sw; + sheight = sh; + xpos = xp + ps_Padding; //lots of padding to make room for button + ypos = yp-sheight/2; + spos = xpos; + sposMin = xpos; + sposMax = xpos + swidth - sheight/2; + + pbsb_cp5 = new ControlP5(ourApplet); + pbsb_cp5.setGraphics(ourApplet, 0,0); + pbsb_cp5.setAutoDraw(false); + + //Let's make a button to return to the start of playback!! + skipToStart_diameter = 25; + createSkipToStartButton("skipToStartButton", "", int(xp) + int(skipToStart_diameter*.5), int(yp) + int(sh/2) - skipToStart_diameter, skipToStart_diameter, skipToStart_diameter); + + fileBoard = (FileBoard)currentBoard; + } + + private void createSkipToStartButton(String name, String text, int _x, int _y, int _w, int _h) { + skipToStartButton = createButton(pbsb_cp5, name, text, _x, _y, _w, _h, 0, p5, 12, GREY_235, OPENBCI_DARKBLUE, BUTTON_HOVER, BUTTON_PRESSED, (Integer)null, 0); + PImage defaultImage = loadImage("skipToStart_default-30x26.png"); + skipToStartButton.setImage(defaultImage); + skipToStartButton.setForceDrawBackground(true); + skipToStartButton.onRelease(new CallbackListener() { + public void controlEvent(CallbackEvent theEvent) { + skipToStartButtonAction(); + } + }); + skipToStartButton.setDescription("Click to go back to the beginning of the file."); + } + + /////////////// Update loop for PlaybackScrollbar + void update() { + checkMouseOver(); // check if mouse is over + + if (mousePressed && over) { + locked = true; + } + if (!mousePressed) { + locked = false; + } + //if the slider is being used, update new position based on user mouseX + if (locked) { + spos = constrain(mouseX-sheight/2, sposMin, sposMax); + scrubToPosition(); + } + else { + updateCursor(); + } + + // update timestamp + currentAbsoluteTimeToDisplay = getAbsoluteTimeToDisplay(); + + //update elapsed time to display + currentTimeInSecondsToDisplay = getCurrentTimeToDisplaySeconds(); + + } //end update loop for PlaybackScrollbar + + void updateCursor() { + float currentSample = float(fileBoard.getCurrentSample()); + float totalSamples = float(fileBoard.getTotalSamples()); + float currentPlaybackPos = currentSample / totalSamples; + + spos = lerp(sposMin, sposMax, currentPlaybackPos); + } + + void scrubToPosition() { + int totalSamples = fileBoard.getTotalSamples(); + int newSamplePos = floor(totalSamples * getCursorPercentage()); + + fileBoard.goToIndex(newSamplePos); + dataProcessing.updateEntireDownsampledBuffer(); + dataProcessing.clearCalculatedMetricWidgets(); + } + + float getCursorPercentage() { + return (spos - sposMin) / (sposMax - sposMin); + } + + String getAbsoluteTimeToDisplay() { + List currentData = currentBoard.getData(1); + if (currentData.get(0).length == 0) { + return ""; + } + int timeStampChan = currentBoard.getTimestampChannel(); + long timestampMS = (long)(currentData.get(0)[timeStampChan] * 1000.0); + if (timestampMS == 0) { + return ""; + } + + return timeStampFormat.format(new Date(timestampMS)); + } + + String getCurrentTimeToDisplaySeconds() { + double totalMillis = fileBoard.getTotalTimeSeconds() * 1000.0; + double currentMillis = fileBoard.getCurrentTimeSeconds() * 1000.0; + + String totalTimeStr = formatCurrentTime(totalMillis); + String currentTimeStr = formatCurrentTime(currentMillis); + + return currentTimeStr + " / " + totalTimeStr; + } + + String formatCurrentTime(double millis) { + DateFormat formatter = currentTimeFormatShort; + if (millis >= 3600000.0) { // bigger than 60 minutes + formatter = currentTimeFormatLong; + } + + return formatter.format(new Date((long)millis)); + } + + //checks if mouse is over the playback scrollbar + private void checkMouseOver() { + if (mouseX > xpos && mouseX < xpos+swidth && + mouseY > ypos && mouseY < ypos+sheight) { + if (!over) { + onMouseEnter(); + } + } + else { + if (over) { + onMouseExit(); + } + } + } + + // called when the mouse enters the playback scrollbar + private void onMouseEnter() { + over = true; + cursor(HAND); //changes cursor icon to a hand + } + + private void onMouseExit() { + over = false; + cursor(ARROW); + } + + void draw() { + pushStyle(); + + fill(GREY_235); + stroke(OPENBCI_BLUE); + rect(x, y, w, h); + + //draw the playback slider inside the playback sub-widget + noStroke(); + fill(GREY_200); + rect(xpos, ypos, swidth, sheight); + + //select color for playback indicator + if (over || locked) { + fill(OPENBCI_DARKBLUE); + } else { + fill(102, 102, 102); + } + //draws playback position indicator + rect(spos, ypos, sheight/2, sheight); + + //draw current timestamp and X of Y Seconds above scrollbar + textFont(p2, 18); + fill(OPENBCI_DARKBLUE); + textAlign(LEFT, TOP); + float textHeight = textAscent() - textDescent(); + float textY = y - textHeight - 10; + float tw = textWidth(currentAbsoluteTimeToDisplay); + text(currentAbsoluteTimeToDisplay, xpos + swidth - tw, textY); + text(currentTimeInSecondsToDisplay, xpos, textY); + + popStyle(); + + pbsb_cp5.draw(); + } + + void screenResized(int _x, int _y, int _w, int _h, float _pbx, float _pby, float _pbw, float _pbh) { + x = _x; + y = _y; + w = _w; + h = _h; + swidth = int(_pbw); + sheight = int(_pbh); + xpos = _pbx + ps_Padding; //add lots of padding for use + ypos = _pby - sheight/2; + sposMin = xpos; + sposMax = xpos + swidth - sheight/2; + //update the position of the playback indicator us + //newspos = updatePos(); + + pbsb_cp5.setGraphics(ourApplet, 0, 0); + + skipToStartButton.setPosition( + int(_pbx) + int(skipToStart_diameter*.5), + int(_pby) - int(skipToStart_diameter*.5) + ); + } + + //This function scrubs to the beginning of the playback file + //Useful to 'reset' the scrollbar before loading a new playback file + void skipToStartButtonAction() { + fileBoard.goToIndex(0); + dataProcessing.updateEntireDownsampledBuffer(); + dataProcessing.clearCalculatedMetricWidgets(); + } + +};//end PlaybackScrollbar class + +//========================== TimeDisplay ========================== +class TimeDisplay { + int swidth, sheight; // width and height of bar + float xpos, ypos; // x and y position of bar + String currentAbsoluteTimeToDisplay = ""; + Boolean updatePosition = false; + LocalDateTime time; + + TimeDisplay (float xp, float yp, int sw, int sh) { + swidth = sw; + sheight = sh; + xpos = xp; //lots of padding to make room for button + ypos = yp; + currentAbsoluteTimeToDisplay = fetchCurrentTimeString(); + } + + /////////////// Update loop for TimeDisplay when data stream is running + void update() { + if (currentBoard.isStreaming()) { + //Fetch Local time + try { + currentAbsoluteTimeToDisplay = fetchCurrentTimeString(); + } catch (NullPointerException e) { + println("TimeDisplay: Timestamp error..."); + e.printStackTrace(); + } + + } + } //end update loop for TimeDisplay + + void draw() { + pushStyle(); + //draw current timestamp at the bottom of the Widget container + if (!currentAbsoluteTimeToDisplay.equals(null)) { + int fontSize = 17; + textFont(p2, fontSize); + fill(OPENBCI_DARKBLUE); + float tw = textWidth(currentAbsoluteTimeToDisplay); + text(currentAbsoluteTimeToDisplay, xpos + swidth - tw, ypos); + text(streamTimeElapsed.toString(), xpos + 10, ypos); + } + popStyle(); + } + + void screenResized(float _x, float _y, float _w, float _h) { + swidth = int(_w); + sheight = int(_h); + xpos = _x; + ypos = _y; + } + + String fetchCurrentTimeString() { + time = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss"); + return time.format(formatter); + } +};//end TimeDisplay class diff --git a/OpenBCI_GUI/TopNav.pde b/OpenBCI_GUI/TopNav.pde index b8d4a5cea..36be7ca7d 100644 --- a/OpenBCI_GUI/TopNav.pde +++ b/OpenBCI_GUI/TopNav.pde @@ -9,13 +9,9 @@ /////////////////////////////////////////////////////////////////////////////////////// import java.awt.Desktop; -import java.net.*; import java.nio.file.*; class TopNav { - - private final color TOPNAV_DARKBLUE = OPENBCI_BLUE; - private final color SUBNAV_LIGHTBLUE = buttonsLightBlue; private color strokeColor = OPENBCI_DARKBLUE; private ControlP5 topNav_cp5; @@ -28,13 +24,13 @@ class TopNav { public Button smoothingButton; public Button debugButton; + public Button screenshotButton; public Button tutorialsButton; - public Button shopButton; - public Button issuesButton; public Button updateGuiVersionButton; public Button layoutButton; public Button settingsButton; + public Button networkButton; public LayoutSelector layoutSelector; public TutorialSelector tutorialSelector; @@ -51,8 +47,10 @@ class TopNav { private final int SUBNAV_BUT_W = 70; private final int SUBNAV_BUT_H = 26; private final int TOPNAV_BUT_H = SUBNAV_BUT_H; + private final int NETWORK_BUT_W = 100; + private final int CONFIG_SELECTOR_W = 150; - private boolean topNavDropdownMenuIsOpen = false; + private boolean topNavDropdownMenuIsOpen = true; TopNav() { int controlPanel_W = 256; @@ -67,23 +65,25 @@ class TopNav { //TOP RIGHT OF GUI, FROM LEFT<---Right createDebugButton(" ", width - DEBUG_BUT_W - PAD_3, PAD_3, DEBUG_BUT_W, TOPNAV_BUT_H, h3, 16, TOPNAV_DARKBLUE, WHITE); - createTutorialsButton("Help", (int)debugButton.getPosition()[0] - TOPRIGHT_BUT_W - PAD_3, PAD_3, TOPRIGHT_BUT_W, TOPNAV_BUT_H, h3, 16, TOPNAV_DARKBLUE, WHITE); - createIssuesButton("Issues", (int)tutorialsButton.getPosition()[0] - TOPRIGHT_BUT_W - PAD_3, PAD_3, TOPRIGHT_BUT_W, TOPNAV_BUT_H, h3, 16, TOPNAV_DARKBLUE, WHITE); - createShopButton("Shop", (int)issuesButton.getPosition()[0] - TOPRIGHT_BUT_W - PAD_3, PAD_3, TOPRIGHT_BUT_W, TOPNAV_BUT_H, h3, 16, TOPNAV_DARKBLUE, WHITE); - createUpdateGuiButton("Update", (int)shopButton.getPosition()[0] - TOPRIGHT_BUT_W - PAD_3, PAD_3, TOPRIGHT_BUT_W, TOPNAV_BUT_H, h3, 16, TOPNAV_DARKBLUE, WHITE); + createScreenshotButton(" ", (int)debugButton.getPosition()[0] - DEBUG_BUT_W - PAD_3, PAD_3, DEBUG_BUT_W, TOPNAV_BUT_H, h3, 16, TOPNAV_DARKBLUE, WHITE); + createTutorialsButton("Docs", (int)screenshotButton.getPosition()[0] - TOPRIGHT_BUT_W - PAD_3, PAD_3, TOPRIGHT_BUT_W, TOPNAV_BUT_H, h3, 16, TOPNAV_DARKBLUE, WHITE); + createUpdateGuiButton("Update", (int)tutorialsButton.getPosition()[0] - TOPRIGHT_BUT_W - PAD_3, PAD_3, TOPRIGHT_BUT_W, TOPNAV_BUT_H, h3, 16, TOPNAV_DARKBLUE, WHITE); //SUBNAV TOP RIGHT createTopNavSettingsButton("Settings", width - SUBNAV_BUT_W - PAD_3, SUBNAV_BUT_Y, SUBNAV_BUT_W, SUBNAV_BUT_H, h4, 14, SUBNAV_LIGHTBLUE, WHITE); - - layoutSelector = new LayoutSelector(); + tutorialSelector = new TutorialSelector(); - configSelector = new ConfigSelector(); + configSelector = new ConfigSelector(width - (SUBNAV_BUT_W * 2) - PAD_3, (navBarHeight * 2) - PAD_3, CONFIG_SELECTOR_W); - //updateNavButtonsBasedOnColorScheme(); + try { + updateNavButtonsBasedOnColorScheme(); + updateSecondaryNavButtonsColor(); + } catch (Exception e) { + outputError("TopNav: Error initializing buttons. " + e); + } } void initSecondaryNav() { - boolean needToMakeSmoothingButton = (currentBoard instanceof SmoothingCapableBoard) && smoothingButton == null; if (!secondaryNavInit) { @@ -92,18 +92,81 @@ class TopNav { createFiltersButton("Filters", PAD_3*2 + toggleDataStreamingButton.getWidth(), SUBNAV_BUT_Y, SUBNAV_BUT_W, SUBNAV_BUT_H, h4, 14, SUBNAV_LIGHTBLUE, WHITE); //Appears at Top Right SubNav while in a Session - createLayoutButton("Layout", width - 3 - 60, SUBNAV_BUT_Y, 60, SUBNAV_BUT_H, h4, 14, SUBNAV_LIGHTBLUE, WHITE); + createLayoutButton("Layout", width - SUBNAV_BUT_W - PAD_3, SUBNAV_BUT_Y, SUBNAV_BUT_W, SUBNAV_BUT_H, h4, 14, SUBNAV_LIGHTBLUE, WHITE); + createNetworkButton("Network", width - (SUBNAV_BUT_W*2) - PAD_3*2, SUBNAV_BUT_Y, NETWORK_BUT_W, SUBNAV_BUT_H, h4, 14, SUBNAV_LIGHTBLUE, WHITE); + layoutSelector = new LayoutSelector(); secondaryNavInit = true; } if (needToMakeSmoothingButton) { int pos_x = (int)filtersButton.getPosition()[0] + filtersButton.getWidth() + PAD_3; //Make smoothing button wider than most other topnav buttons to fit text comfortably - createSmoothingButton(getSmoothingString(), pos_x, SUBNAV_BUT_Y, SUBNAV_BUT_W + 48, SUBNAV_BUT_H, h4, 14, SUBNAV_LIGHTBLUE, WHITE); + createSmoothingButton(getSmoothingString(), pos_x, SUBNAV_BUT_Y, SUBNAV_BUT_W + 40, SUBNAV_BUT_H, h4, 14, SUBNAV_LIGHTBLUE, WHITE); + } + } + + void updateNavButtonsBasedOnColorScheme() { + color _colorNotPressed = WHITE; + color _textColorNotActive = OPENBCI_DARKBLUE; + color borderColor = OPENBCI_DARKBLUE; + + if (colorScheme == COLOR_SCHEME_ALTERNATIVE_A) { + _colorNotPressed = OPENBCI_BLUE; + _textColorNotActive = WHITE; } + + controlPanelCollapser.setColorBackground(_colorNotPressed); + debugButton.setColorBackground(_colorNotPressed); + screenshotButton.setColorBackground(_colorNotPressed); + tutorialsButton.setColorBackground(_colorNotPressed); + //updateGuiVersionButton.setColorBackground(_colorNotPressed); + + controlPanelCollapser.getCaptionLabel().setColor(_textColorNotActive); + debugButton.getCaptionLabel().setColor(_textColorNotActive); + screenshotButton.getCaptionLabel().setColor(_textColorNotActive); + tutorialsButton.getCaptionLabel().setColor(_textColorNotActive); + //updateGuiVersionButton.getCaptionLabel().setColor(_textColorNotActive); - - //updateSecondaryNavButtonsColor(); + controlPanelCollapser.setBorderColor(borderColor); + debugButton.setBorderColor(borderColor); + screenshotButton.setBorderColor(borderColor); + tutorialsButton.setBorderColor(borderColor); + //updateGuiVersionButton.setBorderColor(borderColor); + } + + void updateSecondaryNavButtonsColor() { + color _colorNotPressed = WHITE; + color _textColorNotActive = OPENBCI_DARKBLUE; + color borderColor = OPENBCI_DARKBLUE; + + if (colorScheme == COLOR_SCHEME_ALTERNATIVE_A) { + _colorNotPressed = SUBNAV_LIGHTBLUE; + _textColorNotActive = WHITE; + } + + settingsButton.setColorBackground(_colorNotPressed); + settingsButton.getCaptionLabel().setColor(_textColorNotActive); + settingsButton.setBorderColor(borderColor); + + if (systemMode >= SYSTEMMODE_POSTINIT) { + filtersButton.setColorBackground(_colorNotPressed); + layoutButton.setColorBackground(_colorNotPressed); + networkButton.setColorBackground(_colorNotPressed); + + filtersButton.getCaptionLabel().setColor(_textColorNotActive); + layoutButton.getCaptionLabel().setColor(_textColorNotActive); + networkButton.getCaptionLabel().setColor(_textColorNotActive); + + filtersButton.setBorderColor(borderColor); + layoutButton.setBorderColor(borderColor); + networkButton.setBorderColor(borderColor); + } + + if (currentBoard instanceof SmoothingCapableBoard) { + smoothingButton.getCaptionLabel().setColor(_textColorNotActive); + smoothingButton.setColorBackground(_colorNotPressed); + smoothingButton.setBorderColor(borderColor); + } } void update() { @@ -113,44 +176,44 @@ class TopNav { //Make sure these buttons don't get accidentally locked if (systemMode >= SYSTEMMODE_POSTINIT) { setLockTopLeftSubNavCp5Objects(controlPanel.isOpen); + networkButton.setLock(tutorialSelector.isVisible); + layoutButton.setLock(tutorialSelector.isVisible); } if (previousSystemMode != systemMode) { if (systemMode >= SYSTEMMODE_POSTINIT) { - layoutSelector.update(); tutorialSelector.update(); - if (int(settingsButton.getPosition()[0]) != width - (SUBNAV_BUT_W*2) + 3) { - settingsButton.setPosition(width - (SUBNAV_BUT_W*2) + 3, SUBNAV_BUT_Y); - verbosePrint("TopNav: Updated Settings Button Position"); - } - } else { - if (int(settingsButton.getPosition()[0]) != width - 70 - 3) { - settingsButton.setPosition(width - 70 - 3, SUBNAV_BUT_Y); - verbosePrint("TopNav: Updated Settings Button Position"); - } } - configSelector.update(); + + updateNavButtonsBasedOnColorScheme(); + updateSecondaryNavButtonsColor(); + previousSystemMode = systemMode; } - - boolean topNavSubClassIsOpen = layoutSelector.isVisible || configSelector.isVisible || tutorialSelector.isVisible; + + boolean layoutSelectorIsOpen = systemMode >= SYSTEMMODE_POSTINIT ? layoutSelector.isVisible : false; + boolean topNavSubClassIsOpen = layoutSelectorIsOpen || configSelector.isVisible || tutorialSelector.isVisible; setDropdownMenuIsOpen(topNavSubClassIsOpen); } void draw() { PImage logo; + int logo_w = 128; + int logo_h = 22; color topNavBg; color subNavBg; + if (colorScheme == COLOR_SCHEME_ALTERNATIVE_A) { topNavBg = OPENBCI_BLUE; subNavBg = SUBNAV_LIGHTBLUE; logo = logo_white; } else { - topNavBg = color(255); + topNavBg = WHITE; subNavBg = color(229); logo = logo_black; } + //Draw background rectangles for TopNav and SubNav pushStyle(); //stroke(OPENBCI_DARKBLUE); fill(topNavBg); @@ -173,7 +236,7 @@ class TopNav { toggleDataStreamingButton.setVisible(isSession); filtersButton.setVisible(isSession); layoutButton.setVisible(isSession); - + networkButton.setVisible(isSession); } if (smoothingButton != null) { smoothingButton.setVisible(isSession); @@ -182,33 +245,39 @@ class TopNav { //Draw CP5 Objects topNav_cp5.draw(); + //Draw Network Button Status Circle on top of cp5 object + if (isSession) { + drawNetworkButtonStatusCircle(); + } + //Draw everything in these selector boxes above all topnav cp5 objects - layoutSelector.draw(); - tutorialSelector.draw(); + if (isSession) { + layoutSelector.draw(); + } configSelector.draw(); + tutorialSelector.draw(); //Draw Console Log Image on top of cp5 object PImage _logo = (colorScheme == COLOR_SCHEME_DEFAULT) ? consoleImgBlue : consoleImgWhite; - image(_logo, debugButton.getPosition()[0] + 6, debugButton.getPosition()[1] + 2, 22, 22); - - + image(_logo, debugButton.getPosition()[0] + 6, debugButton.getPosition()[1] + 2, 22, 22); + //Draw camera image on top of cp5 object + image(screenshotImgWhite, screenshotButton.getPosition()[0] + 6, screenshotButton.getPosition()[1] + 2, 22, 22); } void screenHasBeenResized(int _x, int _y) { topNav_cp5.setGraphics(ourApplet, 0, 0); //Important! debugButton.setPosition(width - debugButton.getWidth() - PAD_3, PAD_3); - tutorialsButton.setPosition((int)debugButton.getPosition()[0] - TOPRIGHT_BUT_W - PAD_3, PAD_3); - issuesButton.setPosition(tutorialsButton.getPosition()[0] - tutorialsButton.getWidth() - PAD_3, PAD_3); - shopButton.setPosition(issuesButton.getPosition()[0] - issuesButton.getWidth() - PAD_3, PAD_3); - updateGuiVersionButton.setPosition(shopButton.getPosition()[0] - shopButton.getWidth() - PAD_3, PAD_3); - settingsButton.setPosition(width - settingsButton.getWidth() - PAD_3, SUBNAV_BUT_Y); + screenshotButton.setPosition((int)debugButton.getPosition()[0] - screenshotButton.getWidth() - PAD_3, PAD_3); + tutorialsButton.setPosition((int)screenshotButton.getPosition()[0] - tutorialsButton.getWidth() - PAD_3, PAD_3); + //updateGuiVersionButton.setPosition(debugButton.getPosition()[0] - debugButton.getWidth() - PAD_3, PAD_3); + settingsButton.setPosition(width - SUBNAV_BUT_W - PAD_3, SUBNAV_BUT_Y); if (systemMode == SYSTEMMODE_POSTINIT) { toggleDataStreamingButton.setPosition(PAD_3, SUBNAV_BUT_Y); filtersButton.setPosition(PAD_3*2 + toggleDataStreamingButton.getWidth(), SUBNAV_BUT_Y); - layoutButton.setPosition(width - 3 - layoutButton.getWidth(), SUBNAV_BUT_Y); - settingsButton.setPosition(width - (settingsButton.getWidth()*2) + PAD_3, SUBNAV_BUT_Y); + layoutButton.setPosition(width - (SUBNAV_BUT_W*2) - PAD_3*2, SUBNAV_BUT_Y); + networkButton.setPosition(width - (SUBNAV_BUT_W*2) - NETWORK_BUT_W - (PAD_3*3), SUBNAV_BUT_Y); //Make sure to re-position UI in selector boxes layoutSelector.screenResized(); } @@ -217,17 +286,13 @@ class TopNav { configSelector.screenResized(); } - void mousePressed() { - layoutSelector.mousePressed(); //pass mousePressed along to layoutSelector - tutorialSelector.mousePressed(); - configSelector.mousePressed(); - } - void mouseReleased() { - layoutSelector.mouseReleased(); //pass mouseReleased along to layoutSelector + if (systemMode == SYSTEMMODE_POSTINIT) { + layoutSelector.mouseReleased(); + } tutorialSelector.mouseReleased(); configSelector.mouseReleased(); - } //end mouseReleased + } //Load data from the latest release page using Github API and compare to local version public Boolean guiVersionIsUpToDate() { @@ -337,7 +402,7 @@ class TopNav { toggleDataStreamingButton = createTNButton("toggleDataStreamingButton", text, _x, _y, _w, _h, font, _fontSize, _bg, _textColor); toggleDataStreamingButton.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - stopButtonWasPressed(); + dataStreamTogglePressed(); } }); toggleDataStreamingButton.setDescription("Press this button to Stop/Start the data stream. Or press "); @@ -348,13 +413,48 @@ class TopNav { filtersButton.onRelease(new CallbackListener() { public synchronized void controlEvent(CallbackEvent theEvent) { if (!filterUIPopupIsOpen) { - FilterUIPopup filtersUI = new FilterUIPopup(); + filterUI = new FilterUIPopup(); + } else { + filterUI.exitPopup(); + filterUI = null; } } }); filtersButton.setDescription("Here you can adjust the Filters that are applied to \"Filtered\" data."); } + private void createNetworkButton(String text, int _x, int _y, int _w, int _h, PFont font, int _fontSize, color _bg, color _textColor) { + networkButton = createTNButton("networkButton", text, _x, _y, _w, _h, font, _fontSize, _bg, _textColor); + networkButton.onRelease(new CallbackListener() { + public synchronized void controlEvent(CallbackEvent theEvent) { + if (!networkingUIPopupIsOpen) { + networkUI = new NetworkingUI(); + } else { + networkUI.exitPopup(); + networkUI = null; + } + } + }); + networkButton.getCaptionLabel().align(ControlP5.LEFT, ControlP5.CENTER); + networkButton.getCaptionLabel().getStyle().setPaddingLeft(5); + networkButton.setDescription("Configure network outputs from the OpenBCI GUI. Click \"Help\" -> \"Networking\" for more info."); + } + + private void drawNetworkButtonStatusCircle() { + float[] xy = networkButton.getPosition(); + float circleX = xy[0] + networkButton.getWidth() - networkButton.getWidth()/5 + 4; + float circleY = xy[1] + networkButton.getHeight()/2 + 1; + pushStyle(); + textFont(h4, 14); + float circleH = textAscent(); + stroke(OPENBCI_DARKBLUE); + strokeWeight(1); + fill(dataProcessing.networkingSettings.getNetworkingIsStreaming() ? TURN_ON_GREEN : GREY_125); + ellipseMode(CENTER); + ellipse(circleX, circleY, circleH, circleH); + popStyle(); + } + private void createSmoothingButton(String text, int _x, int _y, int _w, int _h, PFont font, int _fontSize, final color _bg, color _textColor) { SmoothingCapableBoard smoothBoard = (SmoothingCapableBoard)currentBoard; color bgColor = smoothBoard.getSmoothingActive() ? _bg : BUTTON_LOCKED_GREY; @@ -375,11 +475,7 @@ class TopNav { layoutButton = createTNButton("layoutButton", text, _x, _y, _w, _h, font, _fontSize, _bg, _textColor); layoutButton.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - //make sure that you can't open the layout selector accidentally - if (!tutorialSelector.isVisible) { - //println("TopNav: Layout Dropdown Toggled"); - layoutSelector.toggleVisibility(); - } + layoutSelector.toggleVisibility(); } }); layoutButton.setDescription("Here you can alter the overall layout of the GUI, allowing for different container configurations with more or less widgets."); @@ -405,25 +501,14 @@ class TopNav { tutorialsButton.setDescription("Click to find links to helpful online tutorials and getting started guides. Also, check out how to create custom widgets for the GUI!"); } - private void createIssuesButton(String text, int _x, int _y, int _w, int _h, PFont font, int _fontSize, color _bg, color _textColor) { - final String helpText = "If you have suggestions or want to share a bug you've found, please create an issue on the GUI's Github repo!"; - issuesButton = createTNButton("issuesButton", text, _x, _y, _w, _h, font, _fontSize, _bg, _textColor); - issuesButton.onRelease(new CallbackListener() { + private void createScreenshotButton(String text, int _x, int _y, int _w, int _h, PFont font, int _fontSize, color _bg, color _textColor) { + screenshotButton = createTNButton("screenshotButton", text, _x, _y, _w, _h, font, _fontSize, _bg, _textColor); + screenshotButton.onRelease(new CallbackListener() { public void controlEvent(CallbackEvent theEvent) { - openURLInBrowser("https://github.com/OpenBCI/OpenBCI_GUI/issues"); + takeGUIScreenshot(); } }); - issuesButton.setDescription("If you have suggestions or want to share a bug you've found, please create an issue on the GUI's Github repo!"); - } - - private void createShopButton(String text, int _x, int _y, int _w, int _h, PFont font, int _fontSize, color _bg, color _textColor) { - shopButton = createTNButton("shopButton", text, _x, _y, _w, _h, font, _fontSize, _bg, _textColor); - shopButton.onRelease(new CallbackListener() { - public void controlEvent(CallbackEvent theEvent) { - openURLInBrowser("https://shop.openbci.com/"); - } - }); - shopButton.setDescription("Head to our online store to purchase the latest OpenBCI hardware and accessories."); + screenshotButton.setDescription("Click to take a screenshot of the GUI! Screenshots are saved to Documents/OpenBCI_GUI/Screenshots/."); } private void createUpdateGuiButton(String text, int _x, int _y, int _w, int _h, PFont font, int _fontSize, color _bg, color _textColor) { @@ -475,34 +560,33 @@ class TopNav { } //Execute this function whenver the stop button is pressed - public void stopButtonWasPressed() { + public void dataStreamTogglePressed() { //Exit method if doing Cyton impedance check. Avoids a BrainFlow error. - if (currentBoard instanceof BoardCyton && w_cytonImpedance != null) { + if (currentBoard instanceof BoardCyton && widgetManager.getWidgetExists("W_CytonImpedance")) { Integer checkingImpOnChan = ((ImpedanceSettingsBoard)currentBoard).isCheckingImpedanceOnChannel(); - //println("isCheckingImpedanceOnAnythingEZCHECK==",w_cytonImpedance.isCheckingImpedanceOnAnything); - if (checkingImpOnChan != null || w_cytonImpedance.cytonMasterImpedanceCheckIsActive() || w_cytonImpedance.isCheckingImpedanceOnAnything) { + W_CytonImpedance cytonImpedanceWidget = (W_CytonImpedance) widgetManager.getWidget("W_CytonImpedance"); + if (checkingImpOnChan != null || cytonImpedanceWidget.cytonMasterImpedanceCheckIsActive() || cytonImpedanceWidget.getIsCheckingImpedanceOnAnything()) { PopupMessage msg = new PopupMessage("Busy Checking Impedance", "Please turn off impedance check to begin recording the data stream."); println("OpenBCI_GUI::Cyton: Please turn off impedance check to begin recording the data stream."); return; } } - //toggle the data transfer state of the ADS1299...stop it or start it... if (currentBoard.isStreaming()) { - output("openBCI_GUI: stopButton was pressed. Stopping data transfer, wait a few seconds."); + output("OpenBCI_GUI: stopButton was pressed. Stopping data transfer, wait a few seconds."); stopRunning(); if (!currentBoard.isStreaming()) { toggleDataStreamingButton.getCaptionLabel().setText(stopButton_pressToStart_txt); toggleDataStreamingButton.setColorBackground(TURN_ON_GREEN); } - } else { //not running - output("openBCI_GUI: startButton was pressed. Starting data transfer, wait a few seconds."); + } else { + output("OpenBCI_GUI: startButton was pressed. Starting data transfer, wait a few seconds."); + dataProcessing.clearCalculatedMetricWidgets(); startRunning(); if (currentBoard.isStreaming()) { toggleDataStreamingButton.getCaptionLabel().setText(stopButton_pressToStop_txt); toggleDataStreamingButton.setColorBackground(TURN_OFF_RED); - nextPlayback_millis = millis(); //used for synthesizeData and readFromFile. This restarts the clock that keeps the playback at the right pace. } } } @@ -562,30 +646,20 @@ class LayoutSelector { layoutOptions = new ArrayList