Comparar commits
122 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| d819ef61ea | |||
| 4ed59b96ee | |||
| 0a0ec6eaab | |||
| a0ca1deefb | |||
| 8f72399b15 | |||
| 1c00ac0805 | |||
| b57d116a76 | |||
| 6d4812c529 | |||
| 2cb1e29ce1 | |||
| de500e378a | |||
| bd702ea138 | |||
| 713ed133c9 | |||
| 96782b1e53 | |||
| 500d7f8492 | |||
| 80ba445186 | |||
| c3e9925b2d | |||
| 5ae8742861 | |||
| e5caa175f9 | |||
| 8bddd396d2 | |||
| 190f41ee88 | |||
| 659d0583b6 | |||
| a81f544b2b | |||
| 53fd92f6e6 | |||
| c78cd910b4 | |||
| b03a23495c | |||
| 91de383110 | |||
| 0dc1348425 | |||
| d97ac44899 | |||
| 2d40d6c5fd | |||
| bce31eb179 | |||
| 1aa81a9109 | |||
| 5a9321c31e | |||
| 48f7565fa6 | |||
| eb620af17f | |||
| c27826cead | |||
| d4731e69fd | |||
| 61f7f5c6dd | |||
| ed9697719f | |||
| 4261717119 | |||
| 0045fe4a7f | |||
| 727b5c64cb | |||
| a421a381e3 | |||
| 4314eff36e | |||
| 93c5cdfeca | |||
| b727ed3016 | |||
| f3ad3d60c0 | |||
| 05c06dfa7e | |||
| 3a73303b7b | |||
| d5ddd66d66 | |||
| ec91ee60a5 | |||
| 8f8a13f519 | |||
| 14c8fb079d | |||
| d300f46b4c | |||
| 918c4a8c8e | |||
| a0f1a2e988 | |||
| be98cbb41b | |||
| c7b7c64cb6 | |||
| 9a241225f5 | |||
| 932b4aead2 | |||
| 2aec155533 | |||
| 13a1844c49 | |||
| 8e626f3e47 | |||
| 3c8435798a | |||
| a2b3aa4775 | |||
| 946efe16a4 | |||
| 2b0f399868 | |||
| faede23036 | |||
| 3aecc447d2 | |||
| 2450200c7e | |||
| e69b4ad5d1 | |||
| 981530ab82 | |||
| 5f280af800 | |||
| 6ba5a78219 | |||
| 916f91eafa | |||
| c801e4c8dc | |||
| 92c5d5c8af | |||
| b8216e7ae6 | |||
| ca24f1d3e8 | |||
| fd86a5f3c2 | |||
| 66ef387db2 | |||
| c918ab20c3 | |||
| d266fd7bc4 | |||
| f04e11df3d | |||
| 1c38cdb53a | |||
| 06b9a90334 | |||
| 72a8c7aa2b | |||
| 43bb529cc8 | |||
| f3823587a1 | |||
| 0f7101eab5 | |||
| d57ef8583d | |||
| aac35a5818 | |||
| 10c2cc34d3 | |||
| 3afc2e5a1f | |||
| ed9f4cca32 | |||
| 8fcb2d9890 | |||
| aff1112fae | |||
| 67c6ccf444 | |||
| b6896490e2 | |||
| 6846839196 | |||
| b1f252c193 | |||
| ae36b1c176 | |||
| fd5954f9bb | |||
| c5505bde4b | |||
| 9b19cc83c7 | |||
| 0a8f4a364a | |||
| 0e47abdeaa | |||
| 319c9877c3 | |||
| df25bc958b | |||
| 39f365a5a0 | |||
| 842fd11f09 | |||
| 5ff92098c8 | |||
| a6a4dc60bd | |||
| 84df350dfe | |||
| 6b9b55d6d3 | |||
| 1c9c1f48b2 | |||
| f706decc79 | |||
| f0febed2a6 | |||
| 79035ed61b | |||
| 93ce70ac9f | |||
| 94637e614f | |||
| 5dfa04ddfc | |||
| daa0787c20 |
+1
@@ -0,0 +1 @@
|
||||
GENERATE_SOURCEMAP=false
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at kylemath@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -11,6 +11,24 @@ To start, you can find all the open issues [here](https://github.com/kylemath/EE
|
||||
|
||||
At any point in time you can create a pull request, so others can see your changes and give you feedback.
|
||||
|
||||
# Setup
|
||||
|
||||
## Fork the Repository
|
||||
|
||||
Start with a fork of the repository so you can work worry-free on your own fork.
|
||||
|
||||
You will need to fork once. Then, you can call `git fetch upstream` and `git pull 'branch-name'` before you do your local changes to fetch the latest remote changes make sure your changes are being made on up-to-date source code.
|
||||
|
||||
## Make Development Environment and Change Away
|
||||
|
||||
Configure your Dev Environment and make your changes on your fork.
|
||||
|
||||
# Contributing
|
||||
|
||||
## Creating a Pull Request
|
||||
|
||||
Once you have made your changes, you can create a pull request, so others can see your changes and give you feedback. Create all pull requests on the master branch. If your PR is a work-in-progress, add a [WIP] at the start of the PR title. Example:[WIP] Heart Rate Monitor Module.
|
||||
|
||||
## Wait for Reviews
|
||||
|
||||
After each PR others will look into your feature, discuss the changes in the PR, and then merge into the master branch when changes are ready.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Kyle Mathewson
|
||||
Copyright (c) 2020 Kyle Mathewson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+146
-43
@@ -1,100 +1,203 @@
|
||||
# EEGEdu
|
||||
|
||||
EEGEdu is an educational website to learn about working with electroencephalogram (EEG) data. It is a teaching tool that allows for students to quickly interact with their own brain waves.
|
||||
<p align="center">
|
||||
<a href="https://github.com/kylemath/EEGEdu/blob/master/LICENSE" target="_blank">
|
||||
<img src="https://img.shields.io/badge/License-MIT-brightgreen.svg" alt="MIT license" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/v/release/kylemath/EEGEdu?include_prereleases" />
|
||||
<a href="https://github.com/kylemath/EEGEdu/blob/master/CONTRIBUTING.md" target="_blank">
|
||||
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome!" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/issues-raw/kylemath/EEGEdu" alt="issues" />
|
||||
<img src="https://img.shields.io/github/issues-closed-raw/kylemath/EEGEdu" />
|
||||
<a href="https://neurotechx.herokuapp.com/" target="_blank">
|
||||
<img src="https://neurotechx.herokuapp.com/badge.svg">
|
||||
</a>
|
||||
<a href="https://neurotechx.slack.com/messages/eegedu/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/NeurotechXChannel-%23eegedu-lightgrey" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
It is an interactive web page that you interact with multiple demonstrations of working with EEG data. It is partially inspired by [EEG101](https://github.com/NeuroTechX/eeg-101) but it is strictly web-based. This allows students to interact with EEG brain data without having to install any software.
|
||||
|
||||
This is a useful set of tools that has been inspired by multiple works that came before. Previously, others in the field have been using [Neurotech EEG-notebooks in python](https://github.com/NeuroTechX/eeg-notebooks) for data collection and analysis with [muse-lsl](https://github.com/alexandrebarachant/muse-lsl). These software support the Interaxon MUSE headset but required a bluetooth low-energey (BLE) dongle to work with Windows of Mac systems. It also required the editing of some pyglatt code to connect properly. These software are cumbersome, and serve as a barrier to entry for many students learning about EEG.
|
||||
|
||||
Visit [https://eegedu.com/](https://eegedu.com/]) for the live website.
|
||||
|
||||
<p align="center">
|
||||
<img src="logo.png" alt="Interactive Brain Playground Logo" width=500 />
|
||||
</p>
|
||||
|
||||
`EEGEdu` is an Interactive Brain Playground.
|
||||
|
||||
`EEGEdu` is served live at [https://eegedu.com/](https://eegedu.com). This website is served from the software in this repository. So, all you need to do to try the system out is head to [EEGEdu](https://eegedu.com/).
|
||||
|
||||
`EEGEdu` is designed as an interactive educational website to learn/teach about working with electroencephalogram (EEG) data. It is a teaching tool that allows for students to interact with their own brain waves.
|
||||
|
||||
`EEGEdu` has been inspired by multiple works that came before. It is inspired by [EEG101](https://github.com/NeuroTechX/eeg-101), but `EEGEdu` is web-based. Being web-based allows students to interact with EEG brain data without having to install any software. Others have used [Neurotech EEG-notebooks in python](https://github.com/NeuroTechX/eeg-notebooks) for data collection and analysis with [muse-lsl](https://github.com/alexandrebarachant/muse-lsl). These software support the Interaxon MUSE headset but require a bluetooth low-energey (BLE) dongle to work with common operating systems (e.g. Windows or Mac OSX). These tools also required the editing `pyglatt` code to connect to Muse headsets. Thus, previous software are cumbersome and serve as a barrier to entry for many students learning about EEG. `EEGEdu` aims to provide students with an accesible introduction to working with their own brain waves.
|
||||
|
||||
|
||||
# EEGEdu Curriculum
|
||||
|
||||
EEGEdu provides an step-by-step incremental tutorial for students to interact with EEG-based brain signals. So, we break down the curriculum into 10 lessons as follows:
|
||||
|
||||
1. [Connect + hardware, Biophysics + signal viewing](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduIntro/EEGEduIntro.js)
|
||||
2. [Heart rate time domain data](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduHeartRaw/EEGEduHeartRaw.js)
|
||||
3. [Heart rate frequency domain -beats per minute](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduHeartSpectra/EEGEduHeartSpectra.js)
|
||||
4. [Raw Data + artifacts + blinks + record](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduRaw/EEGEduRaw.js)
|
||||
5. [Frequency domain explanation + Raw spectra + record](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduSpectra/EEGEduSpectra.js)
|
||||
6. [Frequency bands + record](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduBands/EEGEduBands.js)
|
||||
7. [Spectrogram](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduSpectro/EEGEduSpectro.js)
|
||||
8. [Neurofeedback p5js demos](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduAnimate/EEGEduAnimate.js)
|
||||
9. [Eyes closed eyes open experiment](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduAlpha/EEGEduAlpha.js)
|
||||
10. [SSVEP experiment](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduSsvep/EEGEduSsvep.js)
|
||||
11. [BCI trainer](https://github.com/kylemath/EEGEdu/blob/master/src/components/PageSwitcher/components/EEGEduPredict/EEGEduPredict.js)
|
||||
|
||||
# Installation for Development
|
||||
|
||||
If you are interested in developing EEGEdu, here are some instructions to get you started.
|
||||
If you are interested in developing EEGEdu, here are some instructions to get the software running on your system. *Note*: Currently EEGEdu development requires a Mac OSX operating system.
|
||||
|
||||
Note: Currently EEGEdu development requires a Mac OSX operating system.
|
||||
|
||||
To start, you will need to install [Homebrew](https://brew.sh) and [yarn](https://yarnpkg.com/lang/en/docs/install/#mac-stable). These are easy one-line installations for Mac users:
|
||||
To start, you will need to install [Homebrew](https://brew.sh) and [yarn](https://yarnpkg.com/lang/en/docs/install/#mac-stable). These are easy to install with the following Terminal / `bash` commands:
|
||||
|
||||
```sh
|
||||
# Install homebrew
|
||||
## Install homebrew
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
|
||||
# Install yarn
|
||||
## Install yarn
|
||||
# NOTE: this will also install Node.js if it is not already installed.
|
||||
brew install yarn
|
||||
# Node.js must be version 10.x for Muse interaction
|
||||
# Thus, if you are getting version issues, install n and switch versions
|
||||
|
||||
# NOTE: Node.js must be version 10.x for Muse interaction
|
||||
|
||||
# Thus, if you are getting version issues, install n, with the following command:
|
||||
# sudo npm install -g n
|
||||
|
||||
# Then, you can switch to version 10.x with the following command:
|
||||
# sudo n 10.16.0
|
||||
```
|
||||
|
||||
Then, in terminal, clone the git repo and enter the folder:
|
||||
Then, in Terminal/`bash`, clone this Git repository and change directory into the newly cloned folder:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/kylemath/EEGEdu
|
||||
cd EEGEdu
|
||||
```
|
||||
|
||||
You then need to install the required packages for EEGEdu
|
||||
Then, you can install the required `yarn` packages for EEGEdu:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Local Development Environment
|
||||
Then, you can run the *Development Environment* of EEGEdu:
|
||||
Then, you can run the *Local Development Environment* of EEGEdu:
|
||||
|
||||
```sh
|
||||
yarn start dev
|
||||
```
|
||||
|
||||
If it is working correctly, the EEGEdu application will open in a browser window at http://localhost:3000.
|
||||
If it is working correctly, the EEGEdu application will automatically open in a browser window at http://localhost:3000.
|
||||
|
||||
## Local Production Environment
|
||||
|
||||
To start the *Local Production Environment*, you can use the following commands:
|
||||
|
||||
```sh
|
||||
yarn cache clean
|
||||
yarn run build
|
||||
serve -s build
|
||||
```
|
||||
|
||||
## Local Testing of Changes
|
||||
|
||||
1. Install any new packages `yarn install`
|
||||
1. Start the *Local Development Environment* `yarn start dev`
|
||||
1. Look for errors in terminal log
|
||||
1. Open's browser to http://localhost:3000
|
||||
1. Open Javascript console
|
||||
1. Look for errors in console
|
||||
1. Connect Mock data stream by clicking Connect button
|
||||
1. Run through the `checkFunction` below with Mock data.
|
||||
1. Disconnect Mock data stream
|
||||
1. Turn on Interaxon Muse headband
|
||||
1. Connect Muse data stream
|
||||
1. Repeat the `checkFunction` below with Muse data.
|
||||
|
||||
```sh
|
||||
# Pseudocode for checking EEGEdu functionality
|
||||
checkFunction = {
|
||||
view raw data
|
||||
change sliders
|
||||
make sure data changes and no errors
|
||||
click spectra
|
||||
move sliders
|
||||
make sure changes
|
||||
click bands
|
||||
move sliders
|
||||
make sure changes
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
[EEGEdu](https://eegedu.com) is running on [Firebase](https://firebase.google.com/) and deployment happens automagically using GitHub post-commit hooks, or [Actions](https://github.com/kylemath/EEGEdu/actions), as they are commonly called. You can see how the application is build and deployed by [inspecting the workflow](https://github.com/kylemath/EEGEdu/blob/master/.github/workflows/workflow.yml).
|
||||
[EEGEdu](https://eegedu.com) is running on [Firebase](https://firebase.google.com/) and deployment happens automagically using GitHub post-commit hooks, or [Actions](https://github.com/kylemath/EEGEdu/actions), as they are commonly called. You can see how the application is build and deployed by [inspecting the workflow](https://github.com/kylemath/EEGEdu/blob/master/.github/workflows/workflow.yml).
|
||||
|
||||
Currently this automagic deployment is not working, so we can deploy to firebase manually:
|
||||
|
||||
First, install the Firebase deployment tools:
|
||||
|
||||
```sh
|
||||
sudo brew install firebase
|
||||
sudo yarn global add firebase-tools
|
||||
sudo yarn global add firebase
|
||||
```
|
||||
|
||||
The first deployment requires login and initialization once:
|
||||
|
||||
```sh
|
||||
firebase login
|
||||
```
|
||||
|
||||
Browser opens, and login to Google account authorized for Firebase deployment:
|
||||
|
||||
```sh
|
||||
firebase init
|
||||
```
|
||||
|
||||
* options: Hosting Sites only
|
||||
* public directory: build
|
||||
* single-page app: No
|
||||
* Overwrite - No
|
||||
* Overwrite - No
|
||||
|
||||
Then, deployment to Firebase happens with the following commands:
|
||||
|
||||
```sh
|
||||
# clean the local cache to ensure most recent version is served
|
||||
yarn cache clean
|
||||
|
||||
# build the latest version of the site
|
||||
yarn run build
|
||||
|
||||
# deploy the latest version to firebase
|
||||
firebase deploy
|
||||
```
|
||||
|
||||
# References and Related Tools
|
||||
|
||||
* [Muse 2016 EEG Headset JavaScript Library (using Web Bluetooth)](https://github.com/urish/muse-js)
|
||||
* [Muse 2016 + Web Bluetooth demo app in Angular](https://github.com/NeuroJS/angular-muse)
|
||||
* [Explore Muse headband data in frequency domain](https://github.com/tanvach/muse-fft)
|
||||
* [Pipeable RxJS operators for working with EEG data in Node and the Browser](https://github.com/neurosity/eeg-pipes)
|
||||
* [React, A JavaScript library for building user interfaces](https://reactjs.org/)
|
||||
* [Simple yet flexible JavaScript charting for designers & developers](https://www.chartjs.org/docs/latest/)
|
||||
* [Muse 2016 EEG Headset LSL (NodeJS)](https://github.com/urish/muse-lsl)
|
||||
* [Electroencephalogram (EEG) Recording Protocol for Cognitive and Affective
|
||||
Human Neuroscience Research](https://static1.squarespace.com/static/5abefa62d274cb16de90e935/t/5df7db5956ec9170a9b402e1/1576524637645/Electrode_Application_Protocol_Final_Compressed.pdf)
|
||||
|
||||
# Contributing
|
||||
The guide for contributors can be found [here](https://github.com/kylemath/EEGEdu/blob/master/CONTRIBUTING.md). It covers everything you need to know to start contributing to EEGEdu.
|
||||
|
||||
# Development Roadmap
|
||||
|
||||
We are aiming to include chances for students to interact with EEG-based brain signals. So we might break down a curriculum into 10 lessons as follows:
|
||||
|
||||
1. Connect + hardware
|
||||
2. Biophysics + signal viewing
|
||||
3. Simple evoked example
|
||||
4. Frequency domain explanation
|
||||
5. Raw spectra
|
||||
6. Frequency bands
|
||||
7. Spectrogram
|
||||
8. Record data
|
||||
9. BCI trainer
|
||||
10. p5js demos: https://p5js.org/
|
||||
|
||||
# References
|
||||
|
||||
* https://github.com/urish/muse-js - based toolbox for interacting with muse
|
||||
* https://github.com/NeuroJS/angular-muse - demo with streaming data in Angular, record button,
|
||||
* https://github.com/tanvach/muse-fft - starting point react demo
|
||||
* https://github.com/neurosity/eeg-pipes - easy pipable operations on eeg data from muse-js
|
||||
* https://reactjs.org/ - React for web development
|
||||
* https://www.chartjs.org/docs/latest/ - interactive charts
|
||||
* https://github.com/urish/muse-lsl - maybe useful to stream to LSL
|
||||
|
||||
# Credits
|
||||
|
||||
`EEGEdu` - An Interactive Electrophysiology Tutorial with the Interaxon Muse brought to you by Mathewson Sons
|
||||
`EEGEdu` - An Interactive Electrophysiology Tutorial with the Interaxon Muse brought to you by Mathewson Sons. A [Ky](http://kylemathewson.com)[Kor](http://korymathewson.com)[Key](http://keyfer.ca) Production.
|
||||
|
||||
# License
|
||||
|
||||
|
||||
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"hosting": {
|
||||
"headers": [
|
||||
{ "source":"/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}] }
|
||||
],
|
||||
"public": "build",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
|
||||
BIN
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 123 KiB |
+4
-2
@@ -13,6 +13,8 @@
|
||||
"file-saver": "^2.0.2",
|
||||
"firebase": "^7.5.0",
|
||||
"firebase-tools": "^7.9.0",
|
||||
"handlebars": "^4.3.0",
|
||||
"ml5": "^0.4.3",
|
||||
"muse-js": "^3.0.1",
|
||||
"p5": "^0.10.2",
|
||||
"p5.js-widget": "https://github.com/toolness/p5.js-widget.git",
|
||||
@@ -25,8 +27,8 @@
|
||||
"rxjs": "^6.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"start": "react-scripts --max_old_space_size=4096 start",
|
||||
"build": "react-scripts --max_old_space_size=4096 build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
<!-- The core Firebase JS SDK is always required and must be listed first -->
|
||||
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
|
||||
|
||||
<!-- TODO: Add SDKs for Firebase products that you want to use
|
||||
https://firebase.google.com/docs/web/setup#available-libraries -->
|
||||
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-analytics.js"></script>
|
||||
<script>
|
||||
const firebaseConfig = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { PageSwitcher } from "../PageSwitcher/PageSwitcher";
|
||||
import { AppProvider, Card, Page } from "@shopify/polaris";
|
||||
import { AppProvider, Card, Page, Link } from "@shopify/polaris";
|
||||
import enTranslations from "@shopify/polaris/locales/en.json";
|
||||
import * as translations from "./translations/en.json";
|
||||
|
||||
@@ -10,7 +10,14 @@ export function App() {
|
||||
<Page title={translations.title} subtitle={translations.subtitle}>
|
||||
<PageSwitcher />
|
||||
<Card sectioned>
|
||||
<p>{translations.footer}</p>
|
||||
<p>{translations.footer}
|
||||
A
|
||||
<Link url="http://kylemathewson.com"> Ky</Link>
|
||||
<Link url="http://korymathewson.com">Kor</Link>
|
||||
<Link url="http://keyfer.ca">Key </Link>
|
||||
Production.
|
||||
<Link url="https://github.com/kylemath/EEGEdu/"> Github source code </Link>
|
||||
</p>
|
||||
</Card>
|
||||
</Page>
|
||||
</AppProvider>
|
||||
|
||||
@@ -2,16 +2,20 @@
|
||||
"title": "EEGEdu",
|
||||
"subtitle": [
|
||||
"Welcome to the EEGEdu live EEG tutorial. ",
|
||||
"This tutorial is designed to be used with the Muse and the Muse 2 headbands from Interaxon. ",
|
||||
"Muse with two auxillary ports made in 2014 will not work. ",
|
||||
"This tutorial has been tested on Android Pixels (Mobile) and Mac OSX (laptop) with the latest chrome browser. ",
|
||||
"This tutorial will help you learn about how neurons produce electrical activity we can measure. ",
|
||||
"By sticking electrodes on the head we can pick up these changes in electricity. ",
|
||||
"The tutorial will walk you through the basics of EEG signal generation, data collection, and analysis with a focus on live control based on physiological signals. ",
|
||||
"All demos are done in this browser. ",
|
||||
"This tutorial is designed to be used with the Muse and the Muse 2 headbands from Interaxon. ",
|
||||
"If you do not have one handy, there is an option to stream mock data as well. ",
|
||||
"Muse with two auxillary ports made in 2014 will not work. ",
|
||||
"This tutorial has been tested on Android Pixels (Mobile) and Mac OSX (laptop) with the latest chrome browser. ",
|
||||
"The first step will be to turn on your Muse headband and click the connect button. ",
|
||||
"This will open a screen and will list available Muse devices. ",
|
||||
"Select the serial number written on your Muse. ",
|
||||
"If you do not have a Muse headband you can click the Mock Data button to use simluated data. ",
|
||||
"Then scroll down to see you live brain activity!"
|
||||
],
|
||||
"footer": "EEGEdu - An Interactive Electrophysiology Tutorial with the Muse brought to you by Mathewson Sons"
|
||||
"footer": "EEGEdu - An Interactive Electrophysiology Tutorial with the Muse brought to you by Mathewson Sons. "
|
||||
|
||||
}
|
||||
|
||||
@@ -1,43 +1,67 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { MuseClient } from "muse-js";
|
||||
import { Select, Card, Stack, Button, ButtonGroup, Modal, TextContainer } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
import { Select, Card, Stack, Button, ButtonGroup } from "@shopify/polaris";
|
||||
|
||||
import { mockMuseEEG } from "./utils/mockMuseEEG";
|
||||
import { generateXTics } from "./utils/chartUtils";
|
||||
|
||||
import * as translations from "./translations/en.json";
|
||||
import * as generalTranslations from "./components/translations/en";
|
||||
import { emptyChannelData } from "./components/chartOptions";
|
||||
import { emptyChannelData, emptySingleChannelData } from "./components/chartOptions";
|
||||
|
||||
import * as funIntro from "./components/EEGEduIntro/EEGEduIntro"
|
||||
import * as funHeartRaw from "./components/EEGEduHeartRaw/EEGEduHeartRaw"
|
||||
import * as funHeartSpectra from "./components/EEGEduHeartSpectra/EEGEduHeartSpectra"
|
||||
import * as funRaw from "./components/EEGEduRaw/EEGEduRaw";
|
||||
import * as funSpectra from "./components/EEGEduSpectra/EEGEduSpectra";
|
||||
import * as funBands from "./components/EEGEduBands/EEGEduBands";
|
||||
import * as funAnimate from "./components/EEGEduAnimate/EEGEduAnimate"
|
||||
import * as funAnimate from "./components/EEGEduAnimate/EEGEduAnimate";
|
||||
import * as funSpectro from "./components/EEGEduSpectro/EEGEduSpectro";
|
||||
import * as funAlpha from "./components/EEGEduAlpha/EEGEduAlpha";
|
||||
import * as funSsvep from "./components/EEGEduSsvep/EEGEduSsvep";
|
||||
import * as funEvoked from "./components/EEGEduEvoked/EEGEduEvoked";
|
||||
import * as funPredict from "./components/EEGEduPredict/EEGEduPredict";
|
||||
|
||||
const intro = translations.types.intro;
|
||||
const heartRaw = translations.types.heartRaw;
|
||||
const heartSpectra = translations.types.heartSpectra;
|
||||
const raw = translations.types.raw;
|
||||
const spectra = translations.types.spectra;
|
||||
const bands = translations.types.bands;
|
||||
const animate = translations.types.animate;
|
||||
const spectro = translations.types.spectro;
|
||||
const alpha = translations.types.alpha;
|
||||
const ssvep = translations.types.ssvep;
|
||||
const evoked = translations.types.evoked;
|
||||
const predict = translations.types.predict;
|
||||
|
||||
export function PageSwitcher() {
|
||||
|
||||
// data pulled out of multicast$
|
||||
const [introData, setIntroData] = useState(emptyChannelData)
|
||||
const [heartRawData, setHeartRawData] = useState(emptyChannelData);
|
||||
const [heartSpectraData, setHeartSpectraData] = useState(emptySingleChannelData);
|
||||
const [rawData, setRawData] = useState(emptyChannelData);
|
||||
const [spectraData, setSpectraData] = useState(emptyChannelData);
|
||||
const [bandsData, setBandsData] = useState(emptyChannelData);
|
||||
const [animateData, setAnimateData] = useState(emptyChannelData);
|
||||
const [spectroData, setSpectroData] = useState(emptyChannelData);
|
||||
const [alphaData, setAlphaData] = useState(emptyChannelData);
|
||||
const [ssvepData, setSsvepData] = useState(emptyChannelData);
|
||||
const [evokedData, setEvokedData] = useState(emptyChannelData);
|
||||
const [predictData, setPredictData] = useState(emptyChannelData);
|
||||
|
||||
// pipe settings
|
||||
const [introSettings] = useState(funIntro.getSettings);
|
||||
const [heartRawSettings] = useState(funHeartRaw.getSettings);
|
||||
const [heartSpectraSettings] = useState(funHeartSpectra.getSettings);
|
||||
const [rawSettings, setRawSettings] = useState(funRaw.getSettings);
|
||||
const [spectraSettings, setSpectraSettings] = useState(funSpectra.getSettings);
|
||||
const [bandsSettings, setBandsSettings] = useState(funBands.getSettings);
|
||||
const [animateSettings, setAnimateSettings] = useState(funAnimate.getSettings);
|
||||
const [spectroSettings, setSpectroSettings] = useState(funSpectro.getSettings);
|
||||
const [alphaSettings, setAlphaSettings] = useState(funAlpha.getSettings);
|
||||
const [ssvepSettings, setSsvepSettings] = useState(funSsvep.getSettings);
|
||||
const [evokedSettings] = useState(funEvoked.getSettings);
|
||||
const [predictSettings, setPredictSettings] = useState(funPredict.getSettings);
|
||||
|
||||
// connection status
|
||||
const [status, setStatus] = useState(generalTranslations.connect);
|
||||
@@ -50,10 +74,17 @@ export function PageSwitcher() {
|
||||
console.log("Switching to: " + value);
|
||||
|
||||
if (window.subscriptionIntro) window.subscriptionIntro.unsubscribe();
|
||||
if (window.subscriptionHeartRaw) window.subscriptionHeartRaw.unsubscribe();
|
||||
if (window.subscriptionHeartSpectra) window.subscriptionHeartSpectra.unsubscribe();
|
||||
if (window.subscriptionRaw) window.subscriptionRaw.unsubscribe();
|
||||
if (window.subscriptionSpectra) window.subscriptionSpectra.unsubscribe();
|
||||
if (window.subscriptionBands) window.subscriptionBands.unsubscribe();
|
||||
if (window.subscriptionAnimate) window.subscriptionAnimate.unsubscribe();
|
||||
if (window.subscriptionSpectro) window.subscriptionSpectro.unsubscribe();
|
||||
if (window.subscriptionAlpha) window.subscriptionAlpha.unsubscribe();
|
||||
if (window.subscriptionSsvep) window.subscriptionSsvep.unsubscribe();
|
||||
if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe();
|
||||
if (window.subscriptionPredict) window.subscriptionPredict.unsubscribe();
|
||||
|
||||
subscriptionSetup(value);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -63,20 +94,39 @@ export function PageSwitcher() {
|
||||
const [recordPop, setRecordPop] = useState(false);
|
||||
const recordPopChange = useCallback(() => setRecordPop(!recordPop), [recordPop]);
|
||||
|
||||
// for popup flag when recording 2nd condition
|
||||
const [recordTwoPop, setRecordTwoPop] = useState(false);
|
||||
const recordTwoPopChange = useCallback(() => setRecordTwoPop(!recordTwoPop), [recordTwoPop]);
|
||||
|
||||
const chartTypes = [
|
||||
{ label: intro, value: intro },
|
||||
{ label: heartRaw, value: heartRaw },
|
||||
{ label: heartSpectra, value: heartSpectra },
|
||||
{ label: raw, value: raw },
|
||||
{ label: spectra, value: spectra },
|
||||
{ label: bands, value: bands },
|
||||
{ label: animate, value: animate }
|
||||
{ label: animate, value: animate },
|
||||
{ label: spectro, value: spectro },
|
||||
{ label: alpha, value: alpha },
|
||||
{ label: ssvep, value: ssvep },
|
||||
{ label: evoked, value: evoked },
|
||||
{ label: predict, value: predict }
|
||||
|
||||
];
|
||||
|
||||
function buildPipes(value) {
|
||||
funIntro.buildPipe(introSettings);
|
||||
funHeartRaw.buildPipe(heartRawSettings);
|
||||
funHeartSpectra.buildPipe(heartSpectraSettings);
|
||||
funRaw.buildPipe(rawSettings);
|
||||
funSpectra.buildPipe(spectraSettings);
|
||||
funBands.buildPipe(bandsSettings);
|
||||
funAnimate.buildPipe(animateSettings);
|
||||
funSpectro.buildPipe(spectroSettings);
|
||||
funAlpha.buildPipe(alphaSettings);
|
||||
funSsvep.buildPipe(ssvepSettings);
|
||||
funEvoked.buildPipe(evokedSettings);
|
||||
funPredict.buildPipe(predictSettings);
|
||||
}
|
||||
|
||||
function subscriptionSetup(value) {
|
||||
@@ -84,6 +134,12 @@ export function PageSwitcher() {
|
||||
case intro:
|
||||
funIntro.setup(setIntroData, introSettings);
|
||||
break;
|
||||
case heartRaw:
|
||||
funHeartRaw.setup(setHeartRawData, heartRawSettings);
|
||||
break;
|
||||
case heartSpectra:
|
||||
funHeartSpectra.setup(setHeartSpectraData, heartSpectraSettings);
|
||||
break;
|
||||
case raw:
|
||||
funRaw.setup(setRawData, rawSettings);
|
||||
break;
|
||||
@@ -91,10 +147,25 @@ export function PageSwitcher() {
|
||||
funSpectra.setup(setSpectraData, spectraSettings);
|
||||
break;
|
||||
case bands:
|
||||
funBands.setup(setBandsData, bandsSettings)
|
||||
funBands.setup(setBandsData, bandsSettings);
|
||||
break;
|
||||
case animate:
|
||||
funAnimate.setup(setAnimateData, animateSettings)
|
||||
funAnimate.setup(setAnimateData, animateSettings);
|
||||
break;
|
||||
case spectro:
|
||||
funSpectro.setup(setSpectroData, spectroSettings);
|
||||
break;
|
||||
case alpha:
|
||||
funAlpha.setup(setAlphaData, alphaSettings);
|
||||
break;
|
||||
case ssvep:
|
||||
funSsvep.setup(setSsvepData, ssvepSettings);
|
||||
break;
|
||||
case evoked:
|
||||
funEvoked.setup(setEvokedData, evokedSettings);
|
||||
break;
|
||||
case predict:
|
||||
funPredict.setup(setPredictData, predictSettings);
|
||||
break;
|
||||
default:
|
||||
console.log(
|
||||
@@ -103,116 +174,11 @@ export function PageSwitcher() {
|
||||
}
|
||||
}
|
||||
|
||||
function saveToCSV(value) {
|
||||
const numSamplesToSave = 50;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
|
||||
console.log('making ' + value + ' headers')
|
||||
|
||||
// for each module subscribe to multicast and make header
|
||||
switch (value) {
|
||||
case raw:
|
||||
// take one sample from selected observable object for headers
|
||||
localObservable$ = window.multicastRaw$.pipe(
|
||||
take(1)
|
||||
);
|
||||
//take one sample to get header info
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",",
|
||||
"info",
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
});
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastRaw$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
break;
|
||||
case spectra:
|
||||
// take one sample from selected observable object for headers
|
||||
localObservable$ = window.multicastSpectra$.pipe(
|
||||
take(1)
|
||||
);
|
||||
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
let freqs = Object.values(x.freqs);
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
|
||||
"info",
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
});
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastSpectra$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
break;
|
||||
case bands:
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
"delta0,delta1,delta2,delta3,deltaAux,",
|
||||
"theta0,theta1,theta2,theta3,thetaAux,",
|
||||
"alpha0,alpha1,alpha2,alpha3,alphaAux,",
|
||||
"beta0,beta1,beta2,beta3,betaAux,",
|
||||
"delta0,delta1,delta2,delta3,deltaAux\n"
|
||||
);
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastBands$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.log(
|
||||
"Error on creating header in module: " + value
|
||||
);
|
||||
}
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
|
||||
// logging is useful for debugging -yup
|
||||
// console.log(x);
|
||||
},
|
||||
error(err) { console.log(err); },
|
||||
complete() {
|
||||
console.log('Trying to save')
|
||||
var blob = new Blob(
|
||||
dataToSave,
|
||||
{type: "text/plain;charset=utf-8"}
|
||||
);
|
||||
saveAs(blob, value + "_Recording.csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
try {
|
||||
if (window.debugWithMock) {
|
||||
// Debug with Mock EEG Data
|
||||
// Initialize the mockMuseEEG data stream with sampleRate
|
||||
console.log("Connecting to mock data source...");
|
||||
setStatus(generalTranslations.connectingMock);
|
||||
|
||||
window.source = {};
|
||||
window.source.connectionStatus = {};
|
||||
window.source.connectionStatus.value = true;
|
||||
@@ -220,28 +186,19 @@ export function PageSwitcher() {
|
||||
setStatus(generalTranslations.connectedMock);
|
||||
} else {
|
||||
// Connect with the Muse EEG Client
|
||||
console.log("Connecting to data source observable...");
|
||||
setStatus(generalTranslations.connecting);
|
||||
|
||||
window.source = new MuseClient();
|
||||
await window.source.connect();
|
||||
await window.source.start();
|
||||
window.source.eegReadings$ = window.source.eegReadings;
|
||||
setStatus(generalTranslations.connected);
|
||||
}
|
||||
|
||||
if (
|
||||
window.source.connectionStatus.value === true &&
|
||||
window.source.eegReadings$
|
||||
) {
|
||||
console.log("Starting to build the data pipes from the data source...");
|
||||
|
||||
buildPipes(selected);
|
||||
console.log("Finished building the data pipes from the data source");
|
||||
|
||||
subscriptionSetup(selected);
|
||||
console.log("Finished subscribing to the data source");
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus(generalTranslations.connect);
|
||||
@@ -257,39 +214,56 @@ export function PageSwitcher() {
|
||||
switch(selected) {
|
||||
case intro:
|
||||
return null
|
||||
case heartRaw:
|
||||
return null
|
||||
case heartSpectra:
|
||||
return null
|
||||
case raw:
|
||||
return (
|
||||
<Card title={selected + ' Settings'} sectioned>
|
||||
{funRaw.renderSliders(setRawData, setRawSettings, status, rawSettings)}
|
||||
</Card>
|
||||
funRaw.renderSliders(setRawData, setRawSettings, status, rawSettings)
|
||||
);
|
||||
case spectra:
|
||||
return (
|
||||
<Card title={selected + ' Settings'} sectioned>
|
||||
{funSpectra.renderSliders(setSpectraData, setSpectraSettings, status, spectraSettings)}
|
||||
</Card>
|
||||
funSpectra.renderSliders(setSpectraData, setSpectraSettings, status, spectraSettings)
|
||||
);
|
||||
case bands:
|
||||
return (
|
||||
<Card title={selected + ' Settings'} sectioned>
|
||||
{funBands.renderSliders(setBandsData, setBandsSettings, status, bandsSettings)}
|
||||
</Card>
|
||||
funBands.renderSliders(setBandsData, setBandsSettings, status, bandsSettings)
|
||||
);
|
||||
case animate:
|
||||
return (
|
||||
<Card title={selected + ' Settings'} sectioned>
|
||||
{funAnimate.renderSliders(setAnimateData, setAnimateSettings, status, animateSettings)}
|
||||
</Card>
|
||||
funAnimate.renderSliders(setAnimateData, setAnimateSettings, status, animateSettings)
|
||||
);
|
||||
case spectro:
|
||||
return (
|
||||
funSpectro.renderSliders(setSpectroData, setSpectroSettings, status, spectroSettings)
|
||||
);
|
||||
case alpha:
|
||||
return (
|
||||
funAlpha.renderSliders(setAlphaData, setAlphaSettings, status, alphaSettings)
|
||||
);
|
||||
case ssvep:
|
||||
return (
|
||||
funSsvep.renderSliders(setSsvepData, setSsvepSettings, status, ssvepSettings)
|
||||
);
|
||||
case evoked:
|
||||
return null
|
||||
case predict:
|
||||
return (
|
||||
funPredict.renderSliders(setPredictData, setPredictSettings, status, predictSettings)
|
||||
);
|
||||
default: console.log('Error rendering settings display');
|
||||
}
|
||||
}
|
||||
|
||||
function renderCharts() {
|
||||
console.log("Rendering " + selected + " Component");
|
||||
switch (selected) {
|
||||
case intro:
|
||||
return <funIntro.renderModule data={introData} />;
|
||||
case heartRaw:
|
||||
return <funHeartRaw.renderModule data={heartRawData} />;
|
||||
case heartSpectra:
|
||||
return <funHeartSpectra.renderModule data={heartSpectraData} />;
|
||||
case raw:
|
||||
return <funRaw.renderModule data={rawData} />;
|
||||
case spectra:
|
||||
@@ -297,147 +271,70 @@ export function PageSwitcher() {
|
||||
case bands:
|
||||
return <funBands.renderModule data={bandsData} />;
|
||||
case animate:
|
||||
return <funAnimate.renderModule
|
||||
data={animateData}
|
||||
/>;
|
||||
return <funAnimate.renderModule data={animateData} />;
|
||||
case spectro:
|
||||
return <funSpectro.renderModule data={spectroData} />;
|
||||
case alpha:
|
||||
return <funAlpha.renderModule data={alphaData} />;
|
||||
case ssvep:
|
||||
return <funSsvep.renderModule data={ssvepData} />;
|
||||
case evoked:
|
||||
return <funEvoked.renderModule data={evokedData} />;
|
||||
case predict:
|
||||
return <funPredict.renderModule data={predictData} />;
|
||||
default:
|
||||
console.log("Error on renderCharts switch.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderRecord() {
|
||||
switch (selected) {
|
||||
case intro:
|
||||
return null
|
||||
case heartRaw:
|
||||
return null
|
||||
case heartSpectra:
|
||||
return (
|
||||
funHeartSpectra.renderRecord(recordPopChange, recordPop, status, heartSpectraSettings)
|
||||
)
|
||||
case raw:
|
||||
return (
|
||||
<Card title={'Record Raw Data'} sectioned>
|
||||
<Card.Section>
|
||||
<p>
|
||||
{"When you are recording raw data it is recommended you set the "}
|
||||
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
|
||||
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
|
||||
{"It will make the live plots appear more choppy."}
|
||||
</p>
|
||||
</Card.Section>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(selected);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Save to CSV'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title="Recording Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<TextContainer>
|
||||
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
funRaw.renderRecord(recordPopChange, recordPop, status, rawSettings)
|
||||
)
|
||||
case spectra:
|
||||
return (
|
||||
<Card title={'Record Raw Data'} sectioned>
|
||||
<Card.Section>
|
||||
<p>
|
||||
{"When you are recording raw data it is recommended you set the "}
|
||||
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
|
||||
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
|
||||
{"It will make the live plots appear more choppy."}
|
||||
</p>
|
||||
</Card.Section>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(selected);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Save to CSV'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title="Recording Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Card>
|
||||
funSpectra.renderRecord(recordPopChange, recordPop, status, spectraSettings)
|
||||
)
|
||||
case bands:
|
||||
return (
|
||||
<Card title={'Record Data'} sectioned>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(selected);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Save to CSV'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title="Recording Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Card>
|
||||
funBands.renderRecord(recordPopChange, recordPop, status, bandsSettings)
|
||||
)
|
||||
case animate:
|
||||
return null
|
||||
case spectro:
|
||||
return null
|
||||
case alpha:
|
||||
return (
|
||||
funAlpha.renderRecord(recordPopChange, recordPop, status, alphaSettings, recordTwoPopChange, recordTwoPop)
|
||||
)
|
||||
case ssvep:
|
||||
return (
|
||||
funSsvep.renderRecord(recordPopChange, recordPop, status, ssvepSettings, recordTwoPopChange, recordTwoPop)
|
||||
)
|
||||
case evoked:
|
||||
return (
|
||||
funEvoked.renderRecord(recordPopChange, recordPop, status, evokedSettings)
|
||||
)
|
||||
case predict:
|
||||
return (
|
||||
funPredict.renderRecord(status)
|
||||
)
|
||||
default:
|
||||
console.log("Error on renderRecord.");
|
||||
}
|
||||
}
|
||||
|
||||
// Render the entire page using above functions
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card sectioned>
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
|
||||
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { channelNames } from "muse-js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
import { zipSamples } from "muse-js";
|
||||
|
||||
import {
|
||||
bandpassFilter,
|
||||
epoch,
|
||||
fft,
|
||||
sliceFFT
|
||||
} from "@neurosity/pipes";
|
||||
|
||||
import { chartStyles, generalOptions } from "../chartOptions";
|
||||
|
||||
import * as generalTranslations from "../translations/en";
|
||||
import * as specificTranslations from "./translations/en";
|
||||
|
||||
import P5Wrapper from 'react-p5-wrapper';
|
||||
import sketchFixation from "./sketchFixation"
|
||||
|
||||
export function getSettings() {
|
||||
return {
|
||||
cutOffLow: 2,
|
||||
cutOffHigh: 20,
|
||||
nbChannels: 4,
|
||||
interval: 100,
|
||||
bins: 256,
|
||||
sliceFFTLow: 1,
|
||||
sliceFFTHigh: 30,
|
||||
duration: 1024,
|
||||
srate: 256,
|
||||
name: 'Alpha'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionAlpha) window.subscriptionAlpha.unsubscribe();
|
||||
|
||||
window.pipeAlpha$ = null;
|
||||
window.multicastAlpha$ = null;
|
||||
window.subscriptionAlpha = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipeAlpha$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
epoch({
|
||||
duration: Settings.duration,
|
||||
interval: Settings.interval,
|
||||
samplingRate: Settings.srate
|
||||
}),
|
||||
fft({ bins: Settings.bins }),
|
||||
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
|
||||
catchError(err => {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
|
||||
window.multicastAlpha$ = window.pipeAlpha$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastAlpha$) {
|
||||
window.subscriptionAlpha = window.multicastAlpha$.subscribe(data => {
|
||||
setData(alphaData => {
|
||||
Object.values(alphaData).forEach((channel, index) => {
|
||||
if (index < 4) {
|
||||
channel.datasets[0].data = data.psd[index];
|
||||
channel.xLabels = data.freqs;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ch0: alphaData.ch0,
|
||||
ch1: alphaData.ch1,
|
||||
ch2: alphaData.ch2,
|
||||
ch3: alphaData.ch3
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastAlpha$.connect();
|
||||
console.log("Subscribed to " + Settings.name);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModule(channels) {
|
||||
function renderCharts() {
|
||||
return Object.values(channels.data).map((channel, index) => {
|
||||
const options = {
|
||||
...generalOptions,
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
scaleLabel: {
|
||||
...generalOptions.scales.xAxes[0].scaleLabel,
|
||||
labelString: specificTranslations.xlabel
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
scaleLabel: {
|
||||
...generalOptions.scales.yAxes[0].scaleLabel,
|
||||
labelString: specificTranslations.ylabel
|
||||
},
|
||||
ticks: {
|
||||
max: 25,
|
||||
min: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 3
|
||||
}
|
||||
},
|
||||
title: {
|
||||
...generalOptions.title,
|
||||
text: generalTranslations.channel + channelNames[index]
|
||||
}
|
||||
};
|
||||
|
||||
if (index === 0) {
|
||||
return (
|
||||
<Card.Section key={"Card_" + index}>
|
||||
<Line key={"Line_" + index} data={channel} options={options} />
|
||||
</Card.Section>
|
||||
);
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={specificTranslations.title}>
|
||||
<Card.Section>
|
||||
<Stack>
|
||||
<TextContainer>
|
||||
<p>{specificTranslations.description}</p>
|
||||
</TextContainer>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderSliders(setData, setSettings, status, Settings) {
|
||||
|
||||
function resetPipeSetup(value) {
|
||||
buildPipe(Settings);
|
||||
setup(setData, Settings)
|
||||
}
|
||||
|
||||
function handleIntervalRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, interval: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleSliceFFTLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, sliceFFTLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleSliceFFTHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, sliceFFTHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleDurationRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, duration: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
label={'Epoch duration (Sampling Points): ' + Settings.duration}
|
||||
value={Settings.duration}
|
||||
onChange={handleDurationRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={10} step={5} max={Settings.duration}
|
||||
label={'Sampling points between epochs onsets: ' + Settings.interval}
|
||||
value={Settings.interval}
|
||||
onChange={handleIntervalRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={.01} step={.5} max={Settings.cutOffHigh - .5}
|
||||
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
|
||||
value={Settings.cutOffLow}
|
||||
onChange={handleCutoffLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
|
||||
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={1} max={Settings.sliceFFTHigh - 1}
|
||||
label={'Slice FFT Lower limit: ' + Settings.sliceFFTLow + ' Hz'}
|
||||
value={Settings.sliceFFTLow}
|
||||
onChange={handleSliceFFTLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.sliceFFTLow + 1}
|
||||
label={'Slice FFT Upper limit: ' + Settings.sliceFFTHigh + ' Hz'}
|
||||
value={Settings.sliceFFTHigh}
|
||||
onChange={handleSliceFFTHighRangeSliderChange}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderRecord(recordPopChange, recordPop, status, Settings, recordTwoPopChange, recordTwoPop) {
|
||||
return(
|
||||
<Card title={'Record ' + Settings.name +' Data'} sectioned>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings, "Closed");
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Record Eyes Closed Data'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings, "Open");
|
||||
recordTwoPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Record Eyes Open Data'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title="Recording Eye Closed Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<Card.Section>
|
||||
<P5Wrapper sketch={sketchFixation} />
|
||||
</Card.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={recordTwoPop}
|
||||
onClose={recordTwoPopChange}
|
||||
title="Recording Eyes Open Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<Card.Section>
|
||||
<P5Wrapper sketch={sketchFixation} />
|
||||
</Card.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function saveToCSV(Settings, condition) {
|
||||
const numSamplesToSave = 50;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
|
||||
console.log('making ' + Settings.name + ' headers')
|
||||
|
||||
// take one sample from selected observable object for headers
|
||||
localObservable$ = window.multicastAlpha$.pipe(
|
||||
take(1)
|
||||
);
|
||||
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
let freqs = Object.values(x.freqs);
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
|
||||
"info",
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
});
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastAlpha$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
|
||||
// logging is useful for debugging -yup
|
||||
// console.log(x);
|
||||
},
|
||||
error(err) { console.log(err); },
|
||||
complete() {
|
||||
console.log('Trying to save')
|
||||
var blob = new Blob(
|
||||
dataToSave,
|
||||
{type: "text/plain;charset=utf-8"}
|
||||
);
|
||||
saveAs(blob, Settings.name + "_" + condition + "_Recording_" + Date.now() + ".csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export default function sketchFixation (p) {
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(300, 300);
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.createCanvas(300, 300);
|
||||
}
|
||||
|
||||
|
||||
p.mousePressed = function () {
|
||||
p.background(256);
|
||||
}
|
||||
|
||||
p.draw = function () {
|
||||
p.background(255);
|
||||
p.fill(255,0,0);
|
||||
p.text("+", p.width/2, p.height/2);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Eyes open vs. Eyes Closed Alpha Experiment",
|
||||
"description": [
|
||||
"In the next demo we run our first experiment, comparing the spectra when eyes are open vs when they are closed. ",
|
||||
"In this module you will first adjust the muse so the signal is good, and then record two sessions of data. ",
|
||||
"In one, you will keep you eyes open and stare at a single point on the screen that pops up. ",
|
||||
"In the other, you will close your eyes and the recording will begin. ",
|
||||
"Alpha oscillations are found to be larger when the eyes are closed than when they are open."
|
||||
],
|
||||
"xlabel": "Frequency (Hz)",
|
||||
"ylabel": "Power (\u03BCV\u00B2)"
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import sketchTone from './sketchTone'
|
||||
import sketchCube from './sketchCube'
|
||||
import sketchFlock from './sketchFlock'
|
||||
import sketchDraw from './sketchDraw'
|
||||
import sketchFlock3D from './sketchFlock3D'
|
||||
|
||||
import P5Wrapper from 'react-p5-wrapper';
|
||||
|
||||
@@ -35,19 +36,20 @@ export function getSettings () {
|
||||
interval: 16,
|
||||
bins: 256,
|
||||
duration: 128,
|
||||
srate: 256
|
||||
srate: 256,
|
||||
name: 'Animate'
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionBands) window.subscriptionBands.unsubscribe();
|
||||
if (window.subscriptionAnimate) window.subscriptionAnimate.unsubscribe();
|
||||
|
||||
window.pipeBands$ = null;
|
||||
window.multicastBands$ = null;
|
||||
window.subscriptionBands = null;
|
||||
window.pipeAnimate$ = null;
|
||||
window.multicastAnimate$ = null;
|
||||
window.subscriptionAnimate = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipeBands$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
window.pipeAnimate$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
@@ -62,7 +64,7 @@ export function buildPipe(Settings) {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
window.multicastBands$ = window.pipeBands$.pipe(
|
||||
window.multicastAnimate$ = window.pipeAnimate$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
@@ -70,10 +72,10 @@ export function buildPipe(Settings) {
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastBands$) {
|
||||
window.subscriptionBands = window.multicastBands$.subscribe(data => {
|
||||
setData(bandsData => {
|
||||
Object.values(bandsData).forEach((channel, index) => {
|
||||
if (window.multicastAnimate$) {
|
||||
window.subscriptionAnimate = window.multicastAnimate$.subscribe(data => {
|
||||
setData(animateData => {
|
||||
Object.values(animateData).forEach((channel, index) => {
|
||||
if (index < 4) {
|
||||
channel.datasets[0].data = [
|
||||
data.delta[index],
|
||||
@@ -87,15 +89,15 @@ export function setup(setData, Settings) {
|
||||
});
|
||||
|
||||
return {
|
||||
ch0: bandsData.ch0,
|
||||
ch1: bandsData.ch1,
|
||||
ch2: bandsData.ch2,
|
||||
ch3: bandsData.ch3
|
||||
ch0: animateData.ch0,
|
||||
ch1: animateData.ch1,
|
||||
ch2: animateData.ch2,
|
||||
ch3: animateData.ch3
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastBands$.connect();
|
||||
window.multicastAnimate$.connect();
|
||||
console.log("Subscribed to " + Settings.name);
|
||||
}
|
||||
}
|
||||
@@ -108,13 +110,15 @@ export function renderModule(channels) {
|
||||
const cube = 'cube';
|
||||
const flock = 'flock';
|
||||
const draw = 'draw';
|
||||
const flock3d = 'flock3d';
|
||||
|
||||
const chartTypes = [
|
||||
{ label: bands, value: bands },
|
||||
{ label: tone, value: tone },
|
||||
{ label: cube, value: cube },
|
||||
{ label: flock, value: flock },
|
||||
{ label: draw, value: draw}
|
||||
{ label: draw, value: draw },
|
||||
{ label: flock3d, value: flock3d }
|
||||
];
|
||||
|
||||
// for picking a new animation
|
||||
@@ -153,6 +157,9 @@ export function renderModule(channels) {
|
||||
case draw:
|
||||
thisSketch = sketchDraw;
|
||||
break
|
||||
case flock3d:
|
||||
thisSketch = sketchFlock3D;
|
||||
break
|
||||
default: console.log("Error on switch to " + selectedAnimation)
|
||||
}
|
||||
|
||||
@@ -232,7 +239,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
@@ -261,7 +268,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ export default function sketchBands (p) {
|
||||
let gamma = 0;
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(p.windowWidth*.6, 800, p.WEBGL);
|
||||
p.createCanvas(p.windowWidth*.5, p.windowWidth*.5, p.WEBGL);
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.resizeCanvas(p.windowWidth*.6, 800);
|
||||
p.resizeCanvas(p.windowWidth*.5, p.windowWidth*.5);
|
||||
}
|
||||
|
||||
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
|
||||
@@ -22,6 +22,9 @@ export default function sketchBands (p) {
|
||||
};
|
||||
|
||||
p.draw = function () {
|
||||
|
||||
let unit = p.width/5;
|
||||
|
||||
p.background(256);
|
||||
p.ambientMaterial(250);
|
||||
p.noStroke();
|
||||
@@ -34,38 +37,43 @@ export default function sketchBands (p) {
|
||||
|
||||
|
||||
p.push();
|
||||
p.translate(-100,0);
|
||||
p.fill(255,0,0);
|
||||
p.translate(-unit,0);
|
||||
p.rotateY(50);
|
||||
p.rotateX(50);
|
||||
p.box(50, delta* 10, 100);
|
||||
p.box(unit/2, delta* 10, unit);
|
||||
p.pop();
|
||||
|
||||
p.push();
|
||||
p.translate(-50,0);
|
||||
p.fill(0,255,0);
|
||||
p.translate(-unit/2,0);
|
||||
p.rotateY(50);
|
||||
p.rotateX(50);
|
||||
p.box(50, theta* 10, 100);
|
||||
p.box(unit/2, theta* 10, unit);
|
||||
p.pop();
|
||||
|
||||
p.push();
|
||||
p.fill(0,0,255);
|
||||
p.translate(0,0);
|
||||
p.rotateY(50);
|
||||
p.rotateX(50);
|
||||
p.box(50, alpha* 10, 100);
|
||||
p.box(unit/2, alpha* 10, unit);
|
||||
p.pop();
|
||||
|
||||
p.push();
|
||||
p.translate(50,0);
|
||||
p.fill(0,128, 128);
|
||||
p.translate(unit/2,0);
|
||||
p.rotateY(50);
|
||||
p.rotateX(50);
|
||||
p.box(50, beta* 10, 100);
|
||||
p.box(unit/2, beta* 10, unit);
|
||||
p.pop();
|
||||
|
||||
p.push();
|
||||
p.translate(100,0);
|
||||
p.fill(128,0,128);
|
||||
p.translate(unit,0);
|
||||
p.rotateY(50);
|
||||
p.rotateX(50);
|
||||
p.box(50, gamma* 10, 100);
|
||||
p.box(unit/2, gamma* 10, unit);
|
||||
p.pop();
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export default function sketchDraw (p) {
|
||||
let xVar = 0;
|
||||
let yVar = 0;
|
||||
|
||||
let brushWidth = 50;
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(p.windowWidth*.6, 500);
|
||||
@@ -18,24 +19,24 @@ export default function sketchDraw (p) {
|
||||
}
|
||||
|
||||
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
|
||||
delta = Math.floor(50*props.delta);
|
||||
theta = Math.floor(100*props.theta);
|
||||
alpha = Math.floor(100*props.alpha);
|
||||
beta = Math.floor(300*props.beta);
|
||||
gamma = Math.floor(1000*props.gamma);
|
||||
delta = Math.floor((props.delta/20) * 255);
|
||||
theta = Math.floor((props.theta/10) * 255);
|
||||
alpha = Math.floor((props.alpha/5) * p.width);
|
||||
beta = Math.floor((props.beta/2) * p.height);
|
||||
gamma = Math.floor((props.gamma/2) * 255);
|
||||
|
||||
xVar = beta;
|
||||
yVar = alpha;
|
||||
xVar = alpha;
|
||||
yVar = beta;
|
||||
|
||||
if (xVar > p.width) {
|
||||
xVar = p.width;
|
||||
xVar = p.width-brushWidth/2;
|
||||
}
|
||||
if (yVar > p.height) {
|
||||
yVar = p.height;
|
||||
yVar = p.height-brushWidth/2;
|
||||
}
|
||||
|
||||
console.log(xVar)
|
||||
console.log(yVar)
|
||||
// console.log(xVar)
|
||||
// console.log(yVar)
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +47,7 @@ export default function sketchDraw (p) {
|
||||
p.draw = function () {
|
||||
p.fill(theta, delta, gamma, 20);
|
||||
p.noStroke();
|
||||
p.ellipse(xVar, yVar, 50);
|
||||
p.ellipse(xVar, yVar, brushWidth);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -2,11 +2,10 @@ import p5 from "p5";
|
||||
import "p5/lib/addons/p5.sound";
|
||||
|
||||
export default function sketchFlock (p) {
|
||||
let delta = 0;
|
||||
let theta = 0;
|
||||
|
||||
let alpha = 0;
|
||||
let beta = 0;
|
||||
let gamma = 0;
|
||||
|
||||
|
||||
let xVar = 0;
|
||||
let yVar = 0;
|
||||
@@ -17,8 +16,8 @@ export default function sketchFlock (p) {
|
||||
p.createCanvas(p.windowWidth*.6, 500);
|
||||
flock = new p.Flock();
|
||||
|
||||
for (var i = 0; i < 100; i++) {
|
||||
var b = new p.Boid(p.width / 2, p.height / 2, 2000);
|
||||
for (var i = 0; i < 150; i++) {
|
||||
var b = new p.Boid(p.width / 2, p.height / 2);
|
||||
flock.addBoid(b)
|
||||
}
|
||||
};
|
||||
@@ -27,32 +26,31 @@ export default function sketchFlock (p) {
|
||||
p.createCanvas(p.windowWidth*.6, 500);
|
||||
}
|
||||
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
|
||||
delta = Math.floor(props.delta);
|
||||
theta = Math.floor(100*props.theta);
|
||||
alpha = Math.floor(100*props.alpha);
|
||||
beta = Math.floor(300*props.beta);
|
||||
gamma = Math.floor(props.gamma);
|
||||
|
||||
xVar = beta;
|
||||
yVar = alpha;
|
||||
alpha = Math.floor((props.alpha/5) * p.width);
|
||||
beta = Math.floor((props.beta/2) * p.height);
|
||||
|
||||
|
||||
xVar = alpha;
|
||||
yVar = beta;
|
||||
|
||||
if (xVar > p.width) {
|
||||
xVar = p.width;
|
||||
xVar = p.width-5;
|
||||
}
|
||||
if (yVar > p.height) {
|
||||
yVar = p.height;
|
||||
yVar = p.height-5;
|
||||
}
|
||||
|
||||
console.log(xVar)
|
||||
console.log(yVar)
|
||||
// console.log(xVar)
|
||||
// console.log(yVar)
|
||||
};
|
||||
|
||||
p.draw = function () {
|
||||
p.background(0);
|
||||
p.background(255);
|
||||
p.fill(255,0,0)
|
||||
p.push();
|
||||
p.translate(xVar, yVar);
|
||||
p.ellipse(0,0,5,5);
|
||||
p.ellipse(0,0,10,10);
|
||||
p.pop();
|
||||
flock.run();
|
||||
}
|
||||
@@ -80,8 +78,8 @@ export default function sketchFlock (p) {
|
||||
this.velocity = p.createVector(p.random(-1, 1), p.random(-1, 1));
|
||||
this.position = p.createVector(x, y);
|
||||
this.r = 2.0; //Size of object Boid
|
||||
this.maxspeed = 3; // Maximum speed
|
||||
this.maxforce = 0.05; // Maximum steer ing force
|
||||
this.maxspeed = 5; // Maximum speed
|
||||
this.maxforce = 0.1; // Maximum steer ing force
|
||||
}
|
||||
|
||||
p.Boid.prototype.run = function(boids) {
|
||||
@@ -134,7 +132,7 @@ export default function sketchFlock (p) {
|
||||
p.Boid.prototype.render = function() {
|
||||
// Draw a triangle rotated in the direction of velocity
|
||||
var theta = this.velocity.heading() + p.radians(90);
|
||||
p.fill(255);
|
||||
p.fill(0);
|
||||
p.noStroke();
|
||||
p.push();
|
||||
p.translate(this.position.x, this.position.y);
|
||||
@@ -231,7 +229,7 @@ export default function sketchFlock (p) {
|
||||
};
|
||||
|
||||
p.Boid.prototype.mouuse = function(boids) {
|
||||
var neighbordist = 100;
|
||||
var neighbordist = 500;
|
||||
var m = p.createVector(xVar, yVar);
|
||||
var d = p5.Vector.dist(this.position, m);
|
||||
if ((d > 0) && (d < neighbordist)) {
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
import p5 from 'p5';
|
||||
|
||||
export default function sketchFlock3D (p) {
|
||||
|
||||
const flock = []; // Array of boids
|
||||
let depth = 800; // The Z location of the boid tend to stay between +depth/2 and -depth/2
|
||||
let gap = 300; // Boids can go further than the edges, this further distance is the gap
|
||||
let quadTree; // A quad tree to minimize the cost of distance calculation
|
||||
|
||||
let useQuadTree = true; // Toogle the use of a quad tree
|
||||
let showPerceptionRadius = false; // Toogle vizualization of perception radius
|
||||
|
||||
let boidsSlider, perceptionSlider, alignmentSlider, cohesionSlider, separationSlider; // Sliders
|
||||
let boidsP, perceptionP, alignmentP, cohesionP, separationP; // Paragraphs
|
||||
let startingBoids = 50; // Amount of boid at the start of the sketch
|
||||
let startingPerception = 90; // Perception radius at the start of the sketch
|
||||
let t = 0; // Counts the frame from the time boids go out of the middle of space
|
||||
|
||||
let theta = 0;
|
||||
let alpha = 0;
|
||||
let beta = 0;
|
||||
let xVar = 0;
|
||||
let yVar = 0;
|
||||
let zVar = 0;
|
||||
|
||||
// SETUP FUNCTION ---------------------------------------------------
|
||||
// Make the canvas, declare some variables, create the DOM elements and the initial boid population
|
||||
p.setup = function () {
|
||||
// Declaration of a canvas to allow canvas download
|
||||
p.createCanvas(p.windowWidth*.5, p.windowWidth*.5, p.WEBGL); // You can change the resolution here
|
||||
|
||||
// Declaration of depth (z axis), unit vectors, and the camera
|
||||
p.depth = p.height;
|
||||
let cameraX = 1000 / 600 * p.width;
|
||||
let cameraY = -800 / 600 * p.height;
|
||||
let cameraZ = -200 / 500 * p.depth;
|
||||
p.camera(cameraX, cameraY, cameraZ, 0, 0, 0, 0, 0, 1);
|
||||
|
||||
// Create the DOM elements: sliders and paragraphs
|
||||
createDOMs();
|
||||
|
||||
// Create an initial population of 100 boids
|
||||
for (let i = 0; i < boidsSlider.value(); i++) {
|
||||
pushRandomBoid();
|
||||
}
|
||||
}
|
||||
|
||||
p.windowResized = function() {
|
||||
p.createCanvas(p.windowWidth*.5, p.windowWidth*.5);
|
||||
p.depth = p.height;
|
||||
let cameraX = 1000 / 600 * p.width;
|
||||
let cameraY = -800 / 600 * p.height;
|
||||
let cameraZ = -200 / 500 * p.depth;
|
||||
p.camera(cameraX, cameraY, cameraZ, 0, 0, 0, 0, 0, 1);
|
||||
|
||||
}
|
||||
|
||||
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
|
||||
theta = Math.floor((props.theta/10) * p.width);
|
||||
alpha = Math.floor((props.alpha/10) * p.height);
|
||||
beta = Math.floor((props.beta/10) * p.depth);
|
||||
|
||||
xVar = theta-(p.width/2)+200;
|
||||
yVar = alpha-(p.height/2)+200;
|
||||
zVar = beta-(p.depth/2)+200;
|
||||
|
||||
if (Math.abs(xVar) > p.width/2) {
|
||||
xVar = Math.sign(xVar) * (p.width/2);
|
||||
}
|
||||
if (Math.abs(yVar) > p.height/2) {
|
||||
yVar = Math.sign(yVar) * (p.height/2);
|
||||
}
|
||||
if (Math.abs(zVar) > p.depth/2) {
|
||||
zVar = Math.sign(zVar) * (p.depth/2);
|
||||
}
|
||||
|
||||
// console.log(xVar + ' ' + yVar + ' ' + zVar)
|
||||
};
|
||||
|
||||
// DRAW FUNCTION ---------------------------------------------------
|
||||
p.draw = function () {
|
||||
// Background and lightning
|
||||
p.background(200);
|
||||
//drag to move the world.
|
||||
p.orbitControl();
|
||||
|
||||
p.directionalLight(150, 150, 150, 1, 1, 0);
|
||||
p.ambientLight(150);
|
||||
|
||||
// Draw the corners of a box showing the space where boids can fly
|
||||
p.stroke(80);
|
||||
p.strokeWeight(8);
|
||||
p.noFill();
|
||||
p.box(p.width + gap/2, p.height + gap/2, p.depth + gap/2);
|
||||
|
||||
p.noStroke();
|
||||
p.fill(255);
|
||||
p.ambientMaterial(0, 0, 255);
|
||||
p.push()
|
||||
p.translate(xVar, yVar, zVar);
|
||||
p.sphere(5); // A sphere where the boid is
|
||||
p.pop();
|
||||
|
||||
|
||||
// Make the quad tree
|
||||
let boundary = new Cube(0, 0, 0, p.width + 2 * gap, p.height + 2 * gap, p.depth + 2 * gap);
|
||||
quadTree = new QuadTree(boundary, 4);
|
||||
for (let boid of flock) {
|
||||
quadTree.insert(boid);
|
||||
}
|
||||
|
||||
// Each boid determines its acceleration for the next frame
|
||||
for (let boid of flock) {
|
||||
boid.flock(flock, quadTree);
|
||||
}
|
||||
// Each boid updates its position and velocity, and is displayed on screen
|
||||
for (let boid of flock) {
|
||||
boid.update(gap);
|
||||
boid.show();
|
||||
}
|
||||
|
||||
// Adjust the amount of boids on screen according to the slider value
|
||||
let maxBoids = boidsSlider.value();
|
||||
let difference = flock.length - maxBoids;
|
||||
if (difference < 0) {
|
||||
for (let i = 0; i < -difference; i++) {
|
||||
pushRandomBoid(); // Add boids if there are less boids than the slider value
|
||||
}
|
||||
} else if (difference > 0) {
|
||||
for (let i = 0; i < difference; i++) {
|
||||
flock.pop(); // Remove boids if there are more boids than the slider value
|
||||
}
|
||||
}
|
||||
|
||||
// Update the DOM elements
|
||||
boidsP.html(`Boids: ${boidsSlider.value()}`);
|
||||
perceptionP.html(`Perception: ${perceptionSlider.value()}`);
|
||||
alignmentP.html(`Alignment: ${alignmentSlider.value()}`);
|
||||
cohesionP.html(`Cohesion: ${cohesionSlider.value()}`);
|
||||
separationP.html(`Separation: ${separationSlider.value()}`);
|
||||
|
||||
t++; // t counts the number of frames, it is used to not have cohesion in the first 40 frames
|
||||
}
|
||||
|
||||
|
||||
// Create the DOM elements
|
||||
function createDOMs() {
|
||||
// Create the paragraphs and sliders
|
||||
boidsP = p.createP('Boids');
|
||||
perceptionP = p.createP('Perception');
|
||||
alignmentP = p.createP('Alignment');
|
||||
cohesionP = p.createP('Cohesion');
|
||||
separationP = p.createP('Separation');
|
||||
|
||||
if (p.windowWidth * p.windowHeight > 1200 * 1200) startingPerception = 150; // Larger perception on a larger screen
|
||||
boidsSlider = p.createSlider(1, 500, startingBoids, 1);
|
||||
perceptionSlider = p.createSlider(0, 1000, startingPerception, 1);
|
||||
alignmentSlider = p.createSlider(0, 5, 0.2, 0.1);
|
||||
cohesionSlider = p.createSlider(0, 5, 0.3, 0.1);
|
||||
separationSlider = p.createSlider(0, 5, 0.7, 0.1);
|
||||
|
||||
// Position the DOM elements on the top left corner
|
||||
let DOMoffset = 1050; // Place the DOM elements underneath the canvas when we want to download the canvas
|
||||
let DOMgap = 5; // Gap between the DOM elements
|
||||
let leftGap = 200;
|
||||
boidsSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 0 + 1 * DOMgap);
|
||||
perceptionSlider.position(leftGap + DOMgap, DOMoffset + boidsSlider.height * 1 + 2 * DOMgap);
|
||||
alignmentSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 2 + 3 * DOMgap);
|
||||
cohesionSlider.position( leftGap + DOMgap, DOMoffset + boidsSlider.height * 3 + 4 * DOMgap);
|
||||
separationSlider.position(leftGap + DOMgap, DOMoffset + boidsSlider.height * 4 + 5 * DOMgap);
|
||||
boidsP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 0 + 0 * DOMgap + 2);
|
||||
perceptionP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 1 + 1 * DOMgap + 2);
|
||||
alignmentP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 2 + 2 * DOMgap + 2);
|
||||
cohesionP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 3 + 3 * DOMgap + 2);
|
||||
separationP.position( leftGap + boidsSlider.width + DOMgap * 2, DOMoffset + boidsSlider.height * 4 + 4 * DOMgap + 2);
|
||||
}
|
||||
|
||||
// Make a new boid
|
||||
function pushRandomBoid() {
|
||||
//let pos = createVector(random(width), random(height), random(-depth/2, depth/2)); // Uncomment and comment next line to create boids at random position
|
||||
let pos = p.createVector(0, 0, 0); // Create a boid at the center of space
|
||||
let vel = p5.Vector.random3D().mult(p.random(0.5, 3)); // Give a random velocity
|
||||
let boid = new Boid(pos, vel); // Create a new boid
|
||||
flock.push(boid); // Add the new boid to the flock
|
||||
}
|
||||
|
||||
///---
|
||||
///---
|
||||
///---
|
||||
|
||||
|
||||
|
||||
// Boid class with flocking behavior
|
||||
class Boid {
|
||||
constructor(pos, vel) {
|
||||
this.pos = pos; // Position
|
||||
this.vel = vel; // Velocity
|
||||
this.acc = p.createVector(0, 0, 0); // Acceleration
|
||||
this.maxForce = 1; // Maximum steering force for alignment, cohesion, separation
|
||||
this.maxSpeed = 10; // Desired velocity for the steering behaviors
|
||||
this.r = 255; // red color of the boid
|
||||
this.g = p.floor(p.random(50, 120)); // green color of the boid
|
||||
this.b = p.floor(p.random(50, 120)); // blue color of the boid
|
||||
}
|
||||
|
||||
// Alignment rule
|
||||
// Steering to average neighbors velocity
|
||||
alignment(neighbors) {
|
||||
let steering = p.createVector();
|
||||
for (let other of neighbors) steering.add(other.vel); // Sum of neighbor velocities
|
||||
if (neighbors.length > 0) {
|
||||
steering.div(neighbors.length); // Average neighbors velocity
|
||||
steering.setMag(this.maxSpeed); // Desired velocity
|
||||
steering.sub(this.vel); // Actual steering
|
||||
steering.limit(this.maxForce); // Steering limited to maxForce
|
||||
}
|
||||
return steering;
|
||||
}
|
||||
|
||||
// Cohesion rule
|
||||
// Steering to the average neighbors position
|
||||
cohesion(neighbors) {
|
||||
let steering = p.createVector();
|
||||
for (let other of neighbors) steering.add(other.pos); // Sum of neighbor positions
|
||||
if (neighbors.length > 0) {
|
||||
steering.div(neighbors.length); // Average neighbors position
|
||||
steering.sub(this.pos); // Orientation of the desired velocity
|
||||
steering.setMag(this.maxSpeed); // Desired velocity
|
||||
steering.sub(this.vel); // Actual steering
|
||||
steering.limit(this.maxForce); // Steering limited to maxForce
|
||||
}
|
||||
return steering;
|
||||
}
|
||||
|
||||
// Separation rule
|
||||
// Steering to avoid proximity of the neighbors
|
||||
separation(neighbors) {
|
||||
let steering = p.createVector();
|
||||
for (let other of neighbors) {
|
||||
let diff = p5.Vector.sub(this.pos, other.pos); // Vector from other boid to this boid
|
||||
let d = p.max(other.distance, 0.01); // Distance between other boid and this boid
|
||||
steering.add(diff.div(d)); // Magnitude inversely proportional to the distance
|
||||
}
|
||||
if (neighbors.length > 0) {
|
||||
steering.div(neighbors.length); // Orientation of the desired velocity
|
||||
steering.setMag(this.maxSpeed); // Desired velocity
|
||||
steering.sub(this.vel); // Actual steering
|
||||
steering.limit(this.maxForce); // Steering limited to maxForce
|
||||
}
|
||||
return steering;
|
||||
}
|
||||
|
||||
// Application of the rules
|
||||
flock(boids, quadTree) {
|
||||
// Go to the middle if goMiddle is true
|
||||
// Create a large force towards the middle, apply it to the boid, and "return" to not apply other forces
|
||||
let force = p.createVector(xVar-this.pos.x, yVar-this.pos.y, zVar-this.pos.z);
|
||||
force.setMag(this.maxForce);
|
||||
this.acc.add(force);
|
||||
|
||||
let radius = perceptionSlider.value(); // Max distance of a neighbor
|
||||
let neighbors = [];
|
||||
|
||||
if (useQuadTree === true) {
|
||||
// VERSION WITH QUADTREE
|
||||
// Make an array of neighbors, i.e. all boids closer than the perception radius
|
||||
// The array will be passed to the different flocking behaviors
|
||||
let range = new Cube(this.pos.x, this.pos.y, this.pos.z, radius, radius, radius);
|
||||
let maybeNeighbors = quadTree.query(range);
|
||||
for (let other of maybeNeighbors) {
|
||||
let distance = this.pos.dist(other.pos);
|
||||
if (other !== this && distance < radius) {
|
||||
other.distance = distance; // Record the distance so it can be used later
|
||||
neighbors.push(other); // Put this neighbor in the "neighbors" array
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// VERSION WITHOUT QUADTREE
|
||||
// Make an array of neighbors, i.e. all boids closer than the perception radius
|
||||
// The array will be passed to the different flocking behaviors
|
||||
for (let other of boids) {
|
||||
let distance = this.pos.dist(other.pos);
|
||||
if (other !== this && distance < radius) {
|
||||
other.distance = distance; // Record the distance so it can be used later
|
||||
neighbors.push(other); // Put this neighbor in the "neighbors" array
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Calculate the force of alignments and apply it to the boid
|
||||
let alignment = this.alignment(neighbors);
|
||||
alignment.mult(alignmentSlider.value());
|
||||
this.acc.add(alignment);
|
||||
|
||||
// Calculate the force of cohesion and apply it to the boid
|
||||
if (t > 2) { // No cohesion in the first 40 frames
|
||||
let cohesion = this.cohesion(neighbors);
|
||||
cohesion.mult(cohesionSlider.value());
|
||||
this.acc.add(cohesion);
|
||||
}
|
||||
|
||||
// Calculate the force of separation and apply it to the boid
|
||||
let separation = this.separation(neighbors);
|
||||
separation.mult(separationSlider.value());
|
||||
this.acc.add(separation);
|
||||
|
||||
// If the boid is flies too high or too low, apply another force to make it fly around the middle of space's depth
|
||||
if (this.pos.z < -depth/8 || this.pos.z > depth/8) {
|
||||
let force = p.createVector(0, 0, -this.pos.z / depth * this.maxForce * 2);
|
||||
this.acc.add(force);
|
||||
}
|
||||
|
||||
// If the boid has no neighbor, apply random forces so it can go find other boids
|
||||
if (neighbors.length === 0) {
|
||||
let force = p5.Vector.random3D().mult(this.maxForce/4);
|
||||
force.z = 0; // Only go find other in an XY plane
|
||||
this.acc.add(force);
|
||||
}
|
||||
}
|
||||
|
||||
// Update position, velocity, and acceleration
|
||||
update(gap) {
|
||||
// Apply physics
|
||||
this.pos.add(this.vel);
|
||||
this.vel.add(this.acc);
|
||||
this.vel.mult(0.999); // Some friction
|
||||
this.vel.limit(this.maxSpeed);
|
||||
this.acc.mult(0);
|
||||
|
||||
// Teleport to opposite side if the boid goes further than a side of space (X and Y axis)
|
||||
// Except for the Z axis, as there is already a force keeping the boid from getting too far
|
||||
if (this.pos.x > p.width/2 + gap) this.pos.x -= p.width + 1.7 * gap;
|
||||
if (this.pos.x < -(p.width/2 + gap)) this.pos.x += p.width + 1.7 * gap;
|
||||
if (this.pos.y > p.height/2 + gap) this.pos.y -= p.height + 1.7 * gap;
|
||||
if (this.pos.y < -(p.height/2 + gap)) this.pos.y += p.eight + 1.7 * gap;
|
||||
}
|
||||
|
||||
// Show the boid on screen
|
||||
show() {
|
||||
p.noStroke();
|
||||
p.fill(255);
|
||||
p.ambientMaterial(this.r, this.g, this.b);
|
||||
|
||||
p.push()
|
||||
p.translate(this.pos.x, this.pos.y, this.pos.z);
|
||||
p.sphere(10); // A sphere where the boid is
|
||||
let arrow = p.createVector(this.vel.x, this.vel.y, this.vel.z).setMag(10);
|
||||
p.translate(arrow.x, arrow.y, arrow.z);
|
||||
p.sphere(5); // Another sphere, smaller, in the direction of the boid's velocity
|
||||
p.pop();
|
||||
|
||||
// Show perception radius, all circles are drawn at z = 0
|
||||
if (showPerceptionRadius) {
|
||||
p.stroke(255, 255, 255, 100);
|
||||
p.noFill();
|
||||
p.strokeWeight(1);
|
||||
let perception = perceptionSlider.value() * 2;
|
||||
p.push();
|
||||
p.translate(0,0,this.pos.z)
|
||||
p.ellipse(this.pos.x, this.pos.y, perception, perception);
|
||||
p.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
|
||||
// This file contains the QuadTree class
|
||||
// as well as the Cube classe used by the QuadTree
|
||||
|
||||
// Cube --------------------------------------------------
|
||||
// A cube delimiting the volume of a quad tree
|
||||
// or the volume used for asking boids from a quad tree
|
||||
class Cube {
|
||||
constructor(x, y, z, w, h, d) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
this.d = d;
|
||||
|
||||
this.xMin = x - w;
|
||||
this.xMax = x + w;
|
||||
this.yMin = y - h;
|
||||
this.yMax = y + h;
|
||||
this.zMin = z - d;
|
||||
this.zMax = z + d;
|
||||
}
|
||||
|
||||
// Checks if a boid is inside the cube
|
||||
contains(boid) {
|
||||
let pos = boid.pos;
|
||||
return (pos.x >= this.xMin && pos.x <= this.xMax &&
|
||||
pos.y >= this.yMin && pos.y <= this.yMax &&
|
||||
pos.z >= this.zMin && pos.z <= this.zMax);
|
||||
}
|
||||
|
||||
// Check if two cubes intersect
|
||||
intersects(range) {
|
||||
return !(this.xMax < range.xMin || this.xMin > range.xMax ||
|
||||
this.yMax < range.yMin || this.yMin > range.yMax ||
|
||||
this.zMax < range.zMin || this.zMin > range.zMax);
|
||||
}
|
||||
}
|
||||
|
||||
// QUAD TREE --------------------------------------------------
|
||||
// The quad tree stores points in a tree structure
|
||||
// to minimize the cost of distance calculation
|
||||
class QuadTree {
|
||||
constructor(boundary, capacity) {
|
||||
this.boundary = boundary; // cube giving the borders of the quad tree
|
||||
this.capacity = capacity; // Maximum amount of points that can be stored in the quad tree
|
||||
this.boids = []; // Array storing the boids in the quad tree
|
||||
this.divided = false; // True when the quad tree subdivides
|
||||
}
|
||||
|
||||
// Insert a boid in the quad tree
|
||||
insert(boid) {
|
||||
// Return if the boid is not in the area of this layer of quad tree
|
||||
if (!this.boundary.contains(boid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the boid at this layer or a deeper layer depending on capacity
|
||||
if (this.boids.length < this.capacity) {
|
||||
// Add the point to this layer if there is still room for it
|
||||
this.boids.push(boid);
|
||||
return true;
|
||||
} else {
|
||||
// Otherwise, subdivide to make room for the new boid
|
||||
// Subdivision divides the quad tree area into 8 new children quad trees
|
||||
if (!this.divided) {
|
||||
this.subdivide();
|
||||
}
|
||||
|
||||
// Add the boid to the relevant subdivision
|
||||
// N = North, S = South, E = East, W = West, B = Bottom, T = Top
|
||||
if (this.NWT.insert(boid)) {
|
||||
return true;
|
||||
} else if (this.NET.insert(boid)) {
|
||||
return true;
|
||||
} else if (this.SET.insert(boid)) {
|
||||
return true;
|
||||
} else if (this.SWT.insert(boid)) {
|
||||
return true;
|
||||
} else if (this.NWB.insert(boid)) {
|
||||
return true;
|
||||
} else if (this.NEB.insert(boid)) {
|
||||
return true;
|
||||
} else if (this.SEB.insert(boid)) {
|
||||
return true;
|
||||
} else if (this.SWB.insert(boid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subdivides the quad tree if it is at full capacity, creating 8 new children quad trees
|
||||
subdivide() {
|
||||
this.divided = true; // Informs of the subdivision to only subdivide once
|
||||
|
||||
let x = this.boundary.x;
|
||||
let y = this.boundary.y;
|
||||
let z = this.boundary.z;
|
||||
let w = this.boundary.w / 2;
|
||||
let h = this.boundary.h / 2;
|
||||
let d = this.boundary.d / 2;
|
||||
|
||||
// Creates the 8 children quad trees with the relevant positions and area
|
||||
// North West Top quad tree
|
||||
let NWTBoundary = new Cube(x - w, y - h, z - d, w, h, d);
|
||||
this.NWT = new QuadTree(NWTBoundary, this.capacity);
|
||||
|
||||
// North East Top quad tree
|
||||
let NETBoundary = new Cube(x + w, y - h, z - d, w, h, d);
|
||||
this.NET = new QuadTree(NETBoundary, this.capacity);
|
||||
|
||||
// South East Top quad tree
|
||||
let SETBoundary = new Cube(x + w, y + h, z - d, w, h, d);
|
||||
this.SET = new QuadTree(SETBoundary, this.capacity);
|
||||
|
||||
// South West Top quad tree
|
||||
let SWTBoundary = new Cube(x - w, y + h, z - d, w, h, d);
|
||||
this.SWT = new QuadTree(SWTBoundary, this.capacity);
|
||||
|
||||
// North West Bot quad tree
|
||||
let NWBBoundary = new Cube(x - w, y - h, z + d, w, h, d);
|
||||
this.NWB = new QuadTree(NWBBoundary, this.capacity);
|
||||
|
||||
// North East Bot quad tree
|
||||
let NEBBoundary = new Cube(x + w, y - h, z + d, w, h, d);
|
||||
this.NEB = new QuadTree(NEBBoundary, this.capacity);
|
||||
|
||||
// South East Bot quad tree
|
||||
let SEBBoundary = new Cube(x + w, y + h, z + d, w, h, d);
|
||||
this.SEB = new QuadTree(SEBBoundary, this.capacity);
|
||||
|
||||
// South West Bot quad tree
|
||||
let SWBBoundary = new Cube(x - w, y + h, z + d, w, h, d);
|
||||
this.SWB = new QuadTree(SWBBoundary, this.capacity);
|
||||
}
|
||||
|
||||
// Returns all the points in a given range (Cube) and put them in the "found" array
|
||||
query(range, found) {
|
||||
// The array "found" will check all quad trees intersecting with the range,
|
||||
// looking for points intersecting with the range
|
||||
if (!found) found = []; // Creates the array at the beginning of the recursion
|
||||
|
||||
if (!this.boundary.intersects(range)) {
|
||||
return found; // No intersection between the quad tree and the range, no need to check for points
|
||||
} else {
|
||||
// If the range intersects this quad tree, check for the intersection of its points with the range
|
||||
for (let boid of this.boids) {
|
||||
if (range.contains(boid)) {
|
||||
found.push(boid); // Add the points intersecting with the range to "found"
|
||||
}
|
||||
}
|
||||
|
||||
// This quad tree intersects with the range, now do the same for its children quad trees
|
||||
if (this.divided) {
|
||||
this.NWT.query(range, found);
|
||||
this.NET.query(range, found);
|
||||
this.SET.query(range, found);
|
||||
this.SWT.query(range, found);
|
||||
this.NWB.query(range, found);
|
||||
this.NEB.query(range, found);
|
||||
this.SEB.query(range, found);
|
||||
this.SWB.query(range, found);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export default function sketchTone (p) {
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.createCanvas(p.windowWidth*.6, 800, p.WEBGL);
|
||||
p.resizeCanvas(p.windowWidth*.6, 200);
|
||||
}
|
||||
|
||||
p.draw = function () {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
|
||||
import { Card, Stack, TextContainer, RangeSlider } from "@shopify/polaris";
|
||||
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { channelNames } from "muse-js";
|
||||
@@ -30,7 +32,8 @@ export function getSettings () {
|
||||
interval: 100,
|
||||
bins: 256,
|
||||
duration: 1024,
|
||||
srate: 256
|
||||
srate: 256,
|
||||
name: 'Bands'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -180,7 +183,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
@@ -209,7 +212,85 @@ export function renderSliders(setData, setSettings, status, Settings) {
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderRecord(recordPopChange, recordPop, status, Settings) {
|
||||
return (
|
||||
<Card title={'Record Data'} sectioned>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Save to CSV'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title="Recording Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function saveToCSV(Settings) {
|
||||
const numSamplesToSave = 50;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
|
||||
console.log('making ' + Settings.name + ' headers')
|
||||
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
"delta0,delta1,delta2,delta3,deltaAux,",
|
||||
"theta0,theta1,theta2,theta3,thetaAux,",
|
||||
"alpha0,alpha1,alpha2,alpha3,alphaAux,",
|
||||
"beta0,beta1,beta2,beta3,betaAux,",
|
||||
"delta0,delta1,delta2,delta3,deltaAux\n"
|
||||
);
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastBands$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
|
||||
// logging is useful for debugging -yup
|
||||
// console.log(x);
|
||||
},
|
||||
error(err) { console.log(err); },
|
||||
complete() {
|
||||
console.log('Trying to save')
|
||||
var blob = new Blob(
|
||||
dataToSave,
|
||||
{type: "text/plain;charset=utf-8"}
|
||||
);
|
||||
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
|
||||
import { zipSamples } from "muse-js";
|
||||
|
||||
import {
|
||||
bandpassFilter,
|
||||
epoch
|
||||
} from "@neurosity/pipes";
|
||||
|
||||
import { chartStyles } from "../chartOptions";
|
||||
|
||||
import * as generalTranslations from "../translations/en";
|
||||
import * as specificTranslations from "./translations/en";
|
||||
|
||||
import { generateXTics, standardDeviation } from "../../utils/chartUtils";
|
||||
|
||||
import P5Wrapper from 'react-p5-wrapper';
|
||||
import sketchEvoked from './sketchEvoked'
|
||||
|
||||
export function getSettings () {
|
||||
return {
|
||||
cutOffLow: 2,
|
||||
cutOffHigh: 20,
|
||||
nbChannels: 4,
|
||||
interval: 1,
|
||||
srate: 256,
|
||||
duration: 1,
|
||||
name: 'Evoked'
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionEvoked) window.subscriptionEvoked.unsubscribe();
|
||||
|
||||
window.pipeEvoked$ = null;
|
||||
window.multicastEvoked$ = null;
|
||||
window.subscriptionEvoked = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipeEvoked$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
epoch({
|
||||
duration: Settings.duration,
|
||||
interval: Settings.interval,
|
||||
samplingRate: Settings.srate
|
||||
}),
|
||||
catchError(err => {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
window.multicastEvoked$ = window.pipeEvoked$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastEvoked$) {
|
||||
window.subscriptionEvoked = window.multicastEvoked$.subscribe(data => {
|
||||
setData(evokedData => {
|
||||
Object.values(evokedData).forEach((channel, index) => {
|
||||
if (index < 4) {
|
||||
channel.datasets[0].data = data.data[index];
|
||||
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
|
||||
channel.datasets[0].qual = standardDeviation(data.data[index])
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ch0: evokedData.ch0,
|
||||
ch1: evokedData.ch1,
|
||||
ch2: evokedData.ch2,
|
||||
ch3: evokedData.ch3
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastEvoked$.connect();
|
||||
console.log("Subscribed to Evoked");
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModule(channels) {
|
||||
function renderCharts() {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={specificTranslations.title}>
|
||||
<Card.Section>
|
||||
<Stack>
|
||||
<TextContainer>
|
||||
<p>{specificTranslations.description}</p>
|
||||
</TextContainer>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderSliders(setData, setSettings, status, Settings) {
|
||||
|
||||
function resetPipeSetup(value) {
|
||||
buildPipe(Settings);
|
||||
setup(setData, Settings)
|
||||
}
|
||||
|
||||
function handleIntervalRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, interval: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleDurationRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, duration: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
label={'Epoch duration (Sampling Points): ' + Settings.duration}
|
||||
value={Settings.duration}
|
||||
onChange={handleDurationRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={10} step={1} max={Settings.duration}
|
||||
label={'Sampling points between epochs onsets: ' + Settings.interval}
|
||||
value={Settings.interval}
|
||||
onChange={handleIntervalRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={.01} step={.5} max={Settings.cutOffHigh - .5}
|
||||
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
|
||||
value={Settings.cutOffLow}
|
||||
onChange={handleCutoffLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
|
||||
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderRecord(recordPopChange, recordPop, status, Settings) {
|
||||
return (
|
||||
<Card title={'Run ERP experiment'} sectioned>
|
||||
<Card.Section>
|
||||
<p>
|
||||
{"Clicking this button will begin the experiment so check your data quality on the raw module first. "}
|
||||
{"A window will pop up when you click the button and a series of circles will appear. Stare at the cross in the center. "}
|
||||
{"There will be red and blue circles, ignore the blue ones."}
|
||||
{"Whenever you see a red circle, as fast as you can either press spacebar on a keyboard, or tap the touchscreen on a tablet or phone. "}
|
||||
{"This entire time the eeg data will be saved along with a column indicating which target was on the screen, and another for the responses. "}
|
||||
{"The task will continue for a few minutes and once it is finished a .csv file will automatically download. "}
|
||||
{"This .csv file has a row for each time point, a column for each electrode, and the columns indicating when targets appeared, and when responses were made. "}
|
||||
{"It saves a marker of 20 when the target is on, a marker of 10 when the blue standards are on, and a peak when the spacebar or touchscreen are pressed, here you can see those synced with an eeg channel: "}
|
||||
|
||||
</p>
|
||||
<p>
|
||||
<img
|
||||
src={ require("./dataExample.png")}
|
||||
alt="dataExample"
|
||||
width="100%"
|
||||
height="auto"
|
||||
></img>
|
||||
</p>
|
||||
</Card.Section>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Run oddball experiment'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title={"Press Spacebar or tap screen when you see RED circle"}
|
||||
>
|
||||
<Modal.Section>
|
||||
<Card.Section>
|
||||
<P5Wrapper sketch={sketchEvoked} />
|
||||
</Card.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function saveToCSV(Settings) {
|
||||
const numSamplesToSave = 10000;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
window.marker = 0;
|
||||
window.responseMarker = 0;
|
||||
window.touchMarker = 0;
|
||||
|
||||
console.log('making ' + Settings.name + ' headers')
|
||||
|
||||
// for each module subscribe to multicast and make header
|
||||
// take one sample from selected observable object for headers
|
||||
localObservable$ = window.multicastEvoked$.pipe(
|
||||
take(1)
|
||||
);
|
||||
//take one sample to get header info
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
"Marker,",
|
||||
"SpaceBar,",
|
||||
"TouchMarker,",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",",
|
||||
"info",
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
});
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastEvoked$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(
|
||||
Date.now() + "," +
|
||||
window.marker + "," +
|
||||
window.responseMarker + "," +
|
||||
window.touchMarker + "," +
|
||||
Object.values(x).join(",") + "\n");
|
||||
// logging is useful for debugging -yup
|
||||
// console.log(x);
|
||||
},
|
||||
error(err) { console.log(err); },
|
||||
complete() {
|
||||
console.log('Trying to save')
|
||||
var blob = new Blob(
|
||||
dataToSave,
|
||||
{type: "text/plain;charset=utf-8"}
|
||||
);
|
||||
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 224 KiB |
@@ -0,0 +1,64 @@
|
||||
export default function sketchEvoked (p) {
|
||||
|
||||
let x = 0;
|
||||
let thisRand = 0.5; //for random choice of target type
|
||||
let targProp = 0.25;
|
||||
let isTarget = false;
|
||||
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(300, 300);
|
||||
p.frameRate(30);
|
||||
p.noStroke();
|
||||
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.createCanvas(300, 300);
|
||||
}
|
||||
|
||||
p.touchStarted = function() {
|
||||
if (window.touchMarker === 0) {
|
||||
window.touchMarker = 255;
|
||||
} else {
|
||||
window.touchMarker = 0;
|
||||
}
|
||||
}
|
||||
|
||||
p.draw = function () {
|
||||
|
||||
if (p.keyIsPressed === true) {
|
||||
window.responseMarker = p.keyCode;
|
||||
} else {
|
||||
window.responseMarker = 0;
|
||||
}
|
||||
p.background(255);
|
||||
x = x+1;
|
||||
// var num = int(random(0, 11));
|
||||
if (x % 20 === 0) { // When a target shown (every ith frame for now)
|
||||
|
||||
thisRand = p.random();
|
||||
if (thisRand < targProp) { // targets 20% of the time
|
||||
isTarget = true;
|
||||
} else {
|
||||
isTarget = false;
|
||||
}
|
||||
|
||||
if (isTarget) {
|
||||
p.fill(250, 150, 150);
|
||||
window.marker = 20;
|
||||
} else {
|
||||
p.fill(150,150,250);
|
||||
window.marker = 10;
|
||||
}
|
||||
|
||||
} else { // during time between targets
|
||||
p.fill(255, 255, 255);
|
||||
window.marker = 0;
|
||||
}
|
||||
p.ellipse(p.width/2, p.height/2, 300);
|
||||
p.fill(255,0,0);
|
||||
p.text("+", p.width/2, p.height/2);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Stimulus Evoked Event-related potential (ERP)",
|
||||
"description": [
|
||||
"The electrical activity evoked by individual presentations of stimuli does create small changes in electrical voltage. ",
|
||||
"These changes are, however, very small, about 1-10 microVolts (\u03BCV), and the noise from other things like eye movements and muscles is sometimes 10x as large. ",
|
||||
"This noise can be mitigated by averaging this evoked activity over repeated presentations of the same stimulus. ",
|
||||
"This average voltage in response to a stimulus is called an Event-related potential (ERP) or evoked potential (EP). ",
|
||||
"Here..."
|
||||
],
|
||||
"xlabel": "Time (ms)",
|
||||
"ylabel": "Votage (\u03BCV)"
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { TextContainer, Card, Stack } from "@shopify/polaris";
|
||||
import { channelNames } from "muse-js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
import { zipSamples } from "muse-js";
|
||||
|
||||
import {
|
||||
bandpassFilter,
|
||||
epoch
|
||||
} from "@neurosity/pipes";
|
||||
|
||||
import { chartStyles, generalOptions } from "../chartOptions";
|
||||
|
||||
import * as generalTranslations from "../translations/en";
|
||||
import * as specificTranslations from "./translations/en";
|
||||
|
||||
import { generateXTics, standardDeviation } from "../../utils/chartUtils";
|
||||
|
||||
export function getSettings () {
|
||||
return {
|
||||
cutOffLow: 2,
|
||||
cutOffHigh: 20,
|
||||
nbChannels: 4,
|
||||
interval: 50,
|
||||
srate: 256,
|
||||
duration: 1024,
|
||||
name: 'HeartRaw'
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionHeartRaw) window.subscriptionHeartRaw.unsubscribe();
|
||||
|
||||
window.pipeHeartRaw$ = null;
|
||||
window.multicastHeartRaw$ = null;
|
||||
window.subscriptionHeartRaw = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipeHeartRaw$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
epoch({
|
||||
duration: Settings.duration,
|
||||
interval: Settings.interval,
|
||||
samplingRate: Settings.srate
|
||||
}),
|
||||
catchError(err => {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
window.multicastHeartRaw$ = window.pipeHeartRaw$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastHeartRaw$) {
|
||||
window.subscriptionHeartRaw = window.multicastHeartRaw$.subscribe(data => {
|
||||
setData(heartRawData => {
|
||||
Object.values(heartRawData).forEach((channel, index) => {
|
||||
if (index < 4) {
|
||||
channel.datasets[0].data = data.data[index];
|
||||
channel.xLabels = generateXTics(Settings.srate, Settings.duration);
|
||||
channel.datasets[0].qual = standardDeviation(data.data[index])
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ch0: heartRawData.ch0,
|
||||
ch1: heartRawData.ch1,
|
||||
ch2: heartRawData.ch2,
|
||||
ch3: heartRawData.ch3
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastHeartRaw$.connect();
|
||||
console.log("Subscribed to HeartRaw");
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModule(channels) {
|
||||
function renderCharts() {
|
||||
return Object.values(channels.data).map((channel, index) => {
|
||||
const options = {
|
||||
...generalOptions,
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
scaleLabel: {
|
||||
...generalOptions.scales.xAxes[0].scaleLabel,
|
||||
labelString: specificTranslations.xlabel
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
scaleLabel: {
|
||||
...generalOptions.scales.yAxes[0].scaleLabel,
|
||||
labelString: specificTranslations.ylabel
|
||||
},
|
||||
ticks: {
|
||||
max: 300,
|
||||
min: -300
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
borderColor: 'rgba(' + channel.datasets[0].qual + ', 128, 128)',
|
||||
fill: false
|
||||
},
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
title: {
|
||||
...generalOptions.title,
|
||||
text: generalTranslations.channel + channelNames[index] + ' --- SD: ' + channel.datasets[0].qual
|
||||
}
|
||||
};
|
||||
|
||||
if (index === 1) {
|
||||
return (
|
||||
<Card.Section key={"Card_" + index}>
|
||||
<Line key={"Line_" + index} data={channel} options={options} />
|
||||
</Card.Section>
|
||||
);
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={specificTranslations.title}>
|
||||
<Card.Section>
|
||||
<Stack>
|
||||
<TextContainer>
|
||||
<p>{specificTranslations.description}</p>
|
||||
</TextContainer>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": "Electrocardiogram (Heart Beats)",
|
||||
"description": [
|
||||
"As a first introduction to measurement of electrical potentials from the body we will look at something accessible. ",
|
||||
"As the heart beats and pumps blood throuhgouts our body, a series of electrical potentials are created, which can be measured using electrodes placed around the heart. ",
|
||||
"This is referred to as the Electrocardiogram (ECG), and is best measured comparing the potential accross the left vs. right side of the body. ",
|
||||
"Therefore, we can take off the muse and place a finger on one hand on the muse's reference electrode (in the center of the forehead). ",
|
||||
"We can then place a finger of our opposite hand on one of the eeg electrodes. For this example pick the left forehead electrode. ",
|
||||
"So place your left fingers pinching the left forehead electrode, and your right fingers pinching the center electrode. ",
|
||||
"Rest the muse on the table as you do this, and relax your body. Soon you should see spikes in voltage measured between thsoe two electrodes each time your heart beats. "
|
||||
],
|
||||
"xlabel": "Time (msec)",
|
||||
"ylabel": "Voltage (\u03BCV)"
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
|
||||
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { channelNames } from "muse-js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
import { zipSamples } from "muse-js";
|
||||
|
||||
import {
|
||||
bandpassFilter,
|
||||
epoch,
|
||||
fft,
|
||||
sliceFFT
|
||||
} from "@neurosity/pipes";
|
||||
|
||||
import { chartStyles, generalOptions } from "../chartOptions";
|
||||
|
||||
import * as generalTranslations from "../translations/en";
|
||||
import * as specificTranslations from "./translations/en";
|
||||
|
||||
export function getSettings() {
|
||||
return {
|
||||
cutOffLow: .01,
|
||||
cutOffHigh: 20,
|
||||
nbChannels: 4,
|
||||
interval: 100,
|
||||
bins: 8192,
|
||||
sliceFFTLow: 0.6,
|
||||
sliceFFTHigh: 2,
|
||||
duration: 4096,
|
||||
srate: 256,
|
||||
name: 'HeartSpectra'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionHeartHeartSpectra) window.subscriptionHeartSpectra.unsubscribe();
|
||||
|
||||
window.pipeHeartSpectra$ = null;
|
||||
window.multicastHeartSpectra$ = null;
|
||||
window.subscriptionHeartSpectra = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipeHeartSpectra$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
epoch({
|
||||
duration: Settings.duration,
|
||||
interval: Settings.interval,
|
||||
samplingRate: Settings.srate
|
||||
}),
|
||||
fft({ bins: Settings.bins }),
|
||||
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
|
||||
catchError(err => {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
|
||||
window.multicastHeartSpectra$ = window.pipeHeartSpectra$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastHeartSpectra$) {
|
||||
window.subscriptionHeartSpectra = window.multicastHeartSpectra$.subscribe(data => {
|
||||
setData(heartSpectraData => {
|
||||
Object.values(heartSpectraData).forEach((channel, index) => {
|
||||
channel.datasets[0].data = data.psd[1];
|
||||
channel.xLabels = data.freqs.map(function(x) {return x * 60});
|
||||
channel.peakF = channel.xLabels[indexOfMax(data.psd[1])];
|
||||
});
|
||||
|
||||
return {
|
||||
ch1: heartSpectraData.ch1,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastHeartSpectra$.connect();
|
||||
console.log("Subscribed to " + Settings.name);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModule(channels) {
|
||||
function renderCharts() {
|
||||
return Object.values(channels.data).map((channel, index) => {
|
||||
const options = {
|
||||
...generalOptions,
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
scaleLabel: {
|
||||
...generalOptions.scales.xAxes[0].scaleLabel,
|
||||
labelString: specificTranslations.xlabel
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
scaleLabel: {
|
||||
...generalOptions.scales.yAxes[0].scaleLabel,
|
||||
labelString: specificTranslations.ylabel
|
||||
},
|
||||
ticks: {
|
||||
min: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 3
|
||||
}
|
||||
},
|
||||
title: {
|
||||
...generalOptions.title,
|
||||
text: generalTranslations.channel +
|
||||
channelNames[index] +
|
||||
" - Estimated HR: " +
|
||||
channel.peakF + " BPM"
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card.Section key={"Card_" + index}>
|
||||
<Line key={"Line_" + index} data={channel} options={options} />
|
||||
</Card.Section>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={specificTranslations.title}>
|
||||
<Card.Section>
|
||||
<Stack>
|
||||
<TextContainer>
|
||||
<p>{specificTranslations.description}</p>
|
||||
</TextContainer>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderSliders(setData, setSettings, status, Settings) {
|
||||
|
||||
function resetPipeSetup(value) {
|
||||
buildPipe(Settings);
|
||||
setup(setData, Settings)
|
||||
}
|
||||
|
||||
function handleIntervalRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, interval: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleSliceFFTLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, sliceFFTLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleSliceFFTHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, sliceFFTHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleDurationRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, duration: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
label={'Epoch duration (Sampling Points): ' + Settings.duration}
|
||||
value={Settings.duration}
|
||||
onChange={handleDurationRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={10} step={5} max={Settings.duration}
|
||||
label={'Sampling points between epochs onsets: ' + Settings.interval}
|
||||
value={Settings.interval}
|
||||
onChange={handleIntervalRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={.01} step={.5} max={Settings.cutOffHigh - .5}
|
||||
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
|
||||
value={Settings.cutOffLow}
|
||||
onChange={handleCutoffLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
|
||||
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={1} max={Settings.sliceFFTHigh - 1}
|
||||
label={'Slice FFT Lower limit: ' + Settings.sliceFFTLow + ' Hz'}
|
||||
value={Settings.sliceFFTLow}
|
||||
onChange={handleSliceFFTLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.sliceFFTLow + 1}
|
||||
label={'Slice FFT Upper limit: ' + Settings.sliceFFTHigh + ' Hz'}
|
||||
value={Settings.sliceFFTHigh}
|
||||
onChange={handleSliceFFTHighRangeSliderChange}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderRecord(recordPopChange, recordPop, status, Settings) {
|
||||
return(
|
||||
<Card title={'Record ' + Settings.name +' Data'} sectioned>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Save to CSV'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title="Recording Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function saveToCSV(Settings) {
|
||||
const numSamplesToSave = 100;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
|
||||
console.log('making ' + Settings.name + ' headers')
|
||||
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
"Estimated BPM",
|
||||
"\n"
|
||||
);
|
||||
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastHeartSpectra$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(Date.now() + "," + x.freqs[indexOfMax(x.psd[1])]*60 + "\n");
|
||||
// logging is useful for debugging -yup
|
||||
console.log();
|
||||
},
|
||||
error(err) { console.log(err); },
|
||||
complete() {
|
||||
console.log('Trying to save')
|
||||
var blob = new Blob(
|
||||
dataToSave,
|
||||
{type: "text/plain;charset=utf-8"}
|
||||
);
|
||||
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Find the index of the max value in an array
|
||||
function indexOfMax(arr) {
|
||||
if (arr.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
var max = arr[0];
|
||||
var maxIndex = 0;
|
||||
for (var i = 1; i < arr.length; i++) {
|
||||
if (arr[i] > max) {
|
||||
maxIndex = i;
|
||||
max = arr[i];
|
||||
}
|
||||
}
|
||||
return maxIndex;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Heart Rate (Beats per minute)",
|
||||
"description": [
|
||||
"Now we look at the same data, but in a different way. Instead of looking at the voltage over time, ",
|
||||
"We now transform the data to show us what frequencies are present in the continuous signal. ",
|
||||
"In this frequency domain, we now ignore time and consider how much power of each frequency there is in a segment of data. ",
|
||||
"If you place your fingers the same way you did when looking at your ECG, you should start to see a peak ",
|
||||
"Along the horizontal axis is the beats per minute, or the frequency of the peaks in your ECG. ",
|
||||
"The vertical y-axis shows the power of the rhythms in the data at each frequency, or how large the changes are between peak and through of the oscillations. "
|
||||
],
|
||||
"xlabel": "Heart Frequency (BPM)",
|
||||
"ylabel": "Power (\u03BCV\u00B2)"
|
||||
}
|
||||
@@ -170,6 +170,9 @@ export function renderModule(channels) {
|
||||
width="100%"
|
||||
height="auto"
|
||||
></img>
|
||||
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/neuronarrow.png"> Image Source - EEG101 </Link>
|
||||
<br />
|
||||
<br />
|
||||
<p>
|
||||
{specificTranslations.neurons2}
|
||||
</p>
|
||||
@@ -179,6 +182,9 @@ export function renderModule(channels) {
|
||||
width="100%"
|
||||
height="auto"
|
||||
></img>
|
||||
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/neuronmultiarrow.png"> Image Source - EEG101 </Link>
|
||||
<br />
|
||||
<br />
|
||||
<p>
|
||||
{specificTranslations.neurons3}
|
||||
</p>
|
||||
@@ -196,6 +202,9 @@ export function renderModule(channels) {
|
||||
width="100%"
|
||||
height="auto"
|
||||
></img>
|
||||
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/awakeasleep.gif"> Image Source - EEG101 </Link>
|
||||
<br />
|
||||
<br />
|
||||
<p>
|
||||
{specificTranslations.oscillations2}
|
||||
</p>
|
||||
@@ -219,18 +228,23 @@ export function renderModule(channels) {
|
||||
></img>
|
||||
<br />
|
||||
<br />
|
||||
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/electrodelocations.png"> Image Source - EEG101 </Link>
|
||||
<br />
|
||||
<br />
|
||||
<p>
|
||||
{specificTranslations.hardware3}
|
||||
<br />
|
||||
<br />
|
||||
{specificTranslations.hardware4}
|
||||
<br />
|
||||
<img
|
||||
src={ require("./assets/DigitalDAQv2.png")} //https://upload.wikimedia.org/wikipedia/commons/9/97/DigitalDAQv2.pdf
|
||||
alt="DAQ diagram"
|
||||
width="100%"
|
||||
height="auto"
|
||||
></img>
|
||||
<img
|
||||
src={ require("./assets/DigitalDAQv2.png")} //https://upload.wikimedia.org/wikipedia/commons/9/97/DigitalDAQv2.pdf
|
||||
alt="DAQ diagram"
|
||||
width="100%"
|
||||
height="auto"
|
||||
></img>
|
||||
<Link url="https://upload.wikimedia.org/wikipedia/commons/9/97/DigitalDAQv2.pdf"> Image Source - Wikipedia </Link>
|
||||
<br />
|
||||
<br />
|
||||
{specificTranslations.hardware5}
|
||||
</p>
|
||||
@@ -250,6 +264,9 @@ export function renderModule(channels) {
|
||||
height="auto"
|
||||
></img>
|
||||
<br />
|
||||
<br />
|
||||
<Link url="https://miro.medium.com/max/2854/1*pK_tLFd8c7_xlOTm1lHdAw.png"> Image Source - @urish </Link>
|
||||
<br />
|
||||
<br />
|
||||
{specificTranslations.muse2}
|
||||
<br />
|
||||
@@ -268,6 +285,9 @@ export function renderModule(channels) {
|
||||
></img>
|
||||
<br />
|
||||
<br />
|
||||
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/electrodediagram"> Image Source - EEG101 </Link>
|
||||
<br />
|
||||
<br />
|
||||
{specificTranslations.muse3}
|
||||
</p>
|
||||
</Card.Section>
|
||||
@@ -284,6 +304,10 @@ export function renderModule(channels) {
|
||||
width="50%"
|
||||
height="auto"
|
||||
></img>
|
||||
<br />
|
||||
<Link url="https://github.com/NeuroTechX/eeg-101/blob/master/EEG101/src/assets/electrodediagram"> Image Source - EEG101 </Link>
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
<div style={chartStyles.wrapperStyle.style}>
|
||||
{renderCharts()}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
|
||||
import { TextContainer, Card, Stack, Button, ButtonGroup } from "@shopify/polaris";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { zipSamples } from "muse-js";
|
||||
|
||||
import {
|
||||
bandpassFilter,
|
||||
epoch,
|
||||
fft,
|
||||
sliceFFT
|
||||
} from "@neurosity/pipes";
|
||||
|
||||
import { chartStyles } from "../chartOptions";
|
||||
|
||||
import * as generalTranslations from "../translations/en";
|
||||
import * as specificTranslations from "./translations/en";
|
||||
|
||||
import P5Wrapper from 'react-p5-wrapper';
|
||||
import sketchPredict from './sketchPredict';
|
||||
|
||||
import ml5 from 'ml5'
|
||||
|
||||
let knnClassifier = ml5.KNNClassifier();
|
||||
|
||||
|
||||
export function getSettings() {
|
||||
return {
|
||||
cutOffLow: 2,
|
||||
cutOffHigh: 20,
|
||||
nbChannels: 4,
|
||||
interval: 256,
|
||||
bins: 256,
|
||||
sliceFFTLow: 1,
|
||||
sliceFFTHigh: 30,
|
||||
duration: 512,
|
||||
srate: 256,
|
||||
name: 'Predict'
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionPredict) window.subscriptionPredict.unsubscribe();
|
||||
|
||||
window.pipePredict$ = null;
|
||||
window.multicastPredict$ = null;
|
||||
window.subscriptionPredict = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipePredict$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
epoch({
|
||||
duration: Settings.duration,
|
||||
interval: Settings.interval,
|
||||
samplingRate: Settings.srate
|
||||
}),
|
||||
fft({ bins: Settings.bins }),
|
||||
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
|
||||
catchError(err => {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
|
||||
window.multicastPredict$ = window.pipePredict$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastPredict$) {
|
||||
window.subscriptionPredict = window.multicastPredict$.subscribe(data => {
|
||||
setData(predictData => {
|
||||
Object.values(predictData).forEach((channel, index) => {
|
||||
if (index < 4) {
|
||||
channel.datasets[0].data = data.psd[index];
|
||||
channel.xLabels = data.freqs;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ch0: predictData.ch0,
|
||||
ch1: predictData.ch1,
|
||||
ch2: predictData.ch2,
|
||||
ch3: predictData.ch3
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastPredict$.connect();
|
||||
console.log("Subscribed to " + Settings.name);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModule(channels) {
|
||||
function renderCharts() {
|
||||
return Object.values(channels.data).map((channel, index) => {
|
||||
if (index === 0) {
|
||||
|
||||
if (channel.datasets[0].data) {
|
||||
window.psd = channel.datasets[0].data;
|
||||
window.freqs = channel.xLabels;
|
||||
if (channel.xLabels) {
|
||||
window.bins = channel.xLabels.length;
|
||||
}
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={specificTranslations.title}>
|
||||
<Card.Section>
|
||||
<Stack>
|
||||
<TextContainer>
|
||||
<p>{specificTranslations.description}</p>
|
||||
</TextContainer>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderSliders(setData, setSettings, status, Settings) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Classification algorithm (using renderRecord function)
|
||||
window.exampleCounts = {A: 0, B: 0};
|
||||
window.thisLabel = 'A';
|
||||
window.confidences = {A: 1, B: 0};
|
||||
|
||||
window.isPredicting = false;
|
||||
window.enoughLabels = false;
|
||||
|
||||
export function renderRecord(status) {
|
||||
const condA = "A";
|
||||
const condB = "B";
|
||||
|
||||
// Adds example from current incoming psd
|
||||
function addExample (label) {
|
||||
if (window.psd) {
|
||||
knnClassifier.addExample(window.psd, label);
|
||||
window.exampleCounts[label]++;
|
||||
|
||||
const numLabels = knnClassifier.getNumLabels();
|
||||
if (numLabels === 2) {
|
||||
window.enoughLabels = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Classifies current incoming psd and outputs results
|
||||
function classify () {
|
||||
window.isPredicting = true;
|
||||
knnClassifier.classify(window.psd, gotResults)
|
||||
}
|
||||
|
||||
// callback from classify to assign results to window and recurse
|
||||
function gotResults(err, result) {
|
||||
if (result.confidencesByLabel) {
|
||||
window.confidences = result.confidencesByLabel;
|
||||
if (result.label) {
|
||||
window.thisLabel = result.label;
|
||||
}
|
||||
}
|
||||
classify(); //recursive so it continues to run
|
||||
}
|
||||
|
||||
//buttons for training at prediction
|
||||
return(
|
||||
<React.Fragment>
|
||||
<Card title={'Record Training Data'} sectioned>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
addExample('A');
|
||||
}}
|
||||
disabled={window.isPredicting || status === generalTranslations.connect}
|
||||
>
|
||||
{'Record ' + condA +' Data - Count: ' + window.exampleCounts['A']}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
addExample('B');
|
||||
}}
|
||||
disabled={window.isPredicting || status === generalTranslations.connect}
|
||||
>
|
||||
{'Record ' + condB + ' Data - Count: ' + window.exampleCounts['B']}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card title={'Predict current brain state after Training'} sectioned>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log('Attempting to classify state')
|
||||
classify();
|
||||
}}
|
||||
disabled={window.isPredicting || !window.enoughLabels || status === generalTranslations.connect}
|
||||
primary={true}
|
||||
>
|
||||
{'Predict State: ' + window.thisLabel + ', Confidence: ' + window.confidences[window.thisLabel].toFixed(2)}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Card.Section>
|
||||
<P5Wrapper sketch={sketchPredict}
|
||||
label={window.thisLabel}
|
||||
confidences={window.confidences}
|
||||
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export default function sketchPredict (p) {
|
||||
|
||||
let label;
|
||||
let confidence;
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(p.windowWidth*.6, 300);
|
||||
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.resizeCanvas(p.windowWidth*.6, 300);
|
||||
}
|
||||
|
||||
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
|
||||
label = props.label;
|
||||
confidence = props.confidences[label];
|
||||
};
|
||||
|
||||
p.draw = function () {
|
||||
p.background(250, 250, 150);
|
||||
p.fill(0);
|
||||
p.strokeWeight(5);
|
||||
p.line(p.width/2, 0, p.width/2, p.height);
|
||||
p.textSize(30);
|
||||
p.text('A', p.width/4, 30);
|
||||
p.text('B', p.width-p.width/4, 30)
|
||||
if (label === 'A') {
|
||||
p.fill(120, 120, 250);
|
||||
if (confidence > .8) {
|
||||
p.ellipse(p.width/6, p.height/2, 60);
|
||||
} else {
|
||||
p.ellipse(p.width/3, p.height/2, 20);
|
||||
}
|
||||
} else {
|
||||
p.fill(120, 250, 120);
|
||||
if (confidence > .8) {
|
||||
p.ellipse(p.width-p.width/6, p.height/2, 60);
|
||||
} else {
|
||||
p.ellipse(p.width-p.width/3, p.height/2, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Predict brain states with a trained classifier",
|
||||
"description": [
|
||||
"In the next module we will train and test classifiers of brain data like we have been looking at so far. ",
|
||||
"We will collect data in two different conditions, with the goal of inducing two different brain states. ",
|
||||
"We will then train a classifier based on the pattern of activity over time, frequency, and space on the head. ",
|
||||
"We will then use the classifier to predict on real time which of the two brain states are currently happening. ",
|
||||
"That is, we will attempt to predict if peoples brain activity more closely resembles condition A or conditoin B ",
|
||||
"Of the training data. "
|
||||
],
|
||||
"xlabel": "Frequency (Hz)",
|
||||
"ylabel": "Power (\u03BCV\u00B2)"
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { TextContainer, Card, Stack, RangeSlider } from "@shopify/polaris";
|
||||
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
import { channelNames } from "muse-js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
@@ -28,7 +30,8 @@ export function getSettings () {
|
||||
nbChannels: 4,
|
||||
interval: 50,
|
||||
srate: 256,
|
||||
duration: 1024
|
||||
duration: 1024,
|
||||
name: 'Raw'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,7 +157,6 @@ export function renderModule(channels) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function renderSliders(setData, setSettings, status, Settings) {
|
||||
|
||||
@@ -184,7 +186,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
@@ -213,6 +215,107 @@ export function renderSliders(setData, setSettings, status, Settings) {
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderRecord(recordPopChange, recordPop, status, Settings) {
|
||||
return (
|
||||
<Card title={'Record ' + Settings.name + ' Data'} sectioned>
|
||||
<Card.Section>
|
||||
<p>
|
||||
{"When you are recording raw data it is recommended you set the "}
|
||||
{"number of sampling points between epochs onsets to be equal to the epoch duration. "}
|
||||
{"This will ensure that consecutive rows of your output file are not overlapping in time."}
|
||||
{"It will make the live plots appear more choppy."}
|
||||
</p>
|
||||
</Card.Section>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Save to CSV'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title="Recording Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<TextContainer>
|
||||
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function saveToCSV(Settings) {
|
||||
const numSamplesToSave = 50;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
|
||||
console.log('making ' + Settings.name + ' headers')
|
||||
|
||||
// for each module subscribe to multicast and make header
|
||||
// take one sample from selected observable object for headers
|
||||
localObservable$ = window.multicastRaw$.pipe(
|
||||
take(1)
|
||||
);
|
||||
//take one sample to get header info
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch0_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch1_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch2_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "ch3_" + f + "ms"}) + ",",
|
||||
generateXTics(x.info.samplingRate,x.data[0].length,false).map(function(f) {return "chAux_" + f + "ms"}) + ",",
|
||||
"info",
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
});
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastRaw$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
|
||||
// logging is useful for debugging -yup
|
||||
// console.log(x);
|
||||
},
|
||||
error(err) { console.log(err); },
|
||||
complete() {
|
||||
console.log('Trying to save')
|
||||
var blob = new Blob(
|
||||
dataToSave,
|
||||
{type: "text/plain;charset=utf-8"}
|
||||
);
|
||||
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
|
||||
import { Card, Stack, TextContainer, RangeSlider } from "@shopify/polaris";
|
||||
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { channelNames } from "muse-js";
|
||||
@@ -31,7 +33,8 @@ export function getSettings() {
|
||||
sliceFFTLow: 1,
|
||||
sliceFFTHigh: 30,
|
||||
duration: 1024,
|
||||
srate: 256
|
||||
srate: 256,
|
||||
name: 'Spectra'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,7 +195,7 @@ export function renderSliders(setData, setSettings, status, Settings) {
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
@@ -235,9 +238,99 @@ export function renderSliders(setData, setSettings, status, Settings) {
|
||||
value={Settings.sliceFFTHigh}
|
||||
onChange={handleSliceFFTHighRangeSliderChange}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderRecord(recordPopChange, recordPop, status, Settings) {
|
||||
return(
|
||||
<Card title={'Record ' + Settings.name +' Data'} sectioned>
|
||||
<Stack>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Save to CSV'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title="Recording Data"
|
||||
>
|
||||
<Modal.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function saveToCSV(Settings) {
|
||||
const numSamplesToSave = 50;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
|
||||
console.log('making ' + Settings.name + ' headers')
|
||||
|
||||
// take one sample from selected observable object for headers
|
||||
localObservable$ = window.multicastSpectra$.pipe(
|
||||
take(1)
|
||||
);
|
||||
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
let freqs = Object.values(x.freqs);
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
|
||||
"info",
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
});
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastSpectra$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
|
||||
// logging is useful for debugging -yup
|
||||
// console.log(x);
|
||||
},
|
||||
error(err) { console.log(err); },
|
||||
complete() {
|
||||
console.log('Trying to save')
|
||||
var blob = new Blob(
|
||||
dataToSave,
|
||||
{type: "text/plain;charset=utf-8"}
|
||||
);
|
||||
saveAs(blob, Settings.name + "_Recording_" + Date.now() + ".csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
|
||||
import { Card, Stack, TextContainer, RangeSlider} from "@shopify/polaris";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { zipSamples } from "muse-js";
|
||||
|
||||
import {
|
||||
bandpassFilter,
|
||||
epoch,
|
||||
fft,
|
||||
sliceFFT
|
||||
} from "@neurosity/pipes";
|
||||
|
||||
import { chartStyles } from "../chartOptions";
|
||||
|
||||
import * as generalTranslations from "../translations/en";
|
||||
import * as specificTranslations from "./translations/en";
|
||||
|
||||
import sketchSpectro from './sketchSpectro'
|
||||
|
||||
import P5Wrapper from 'react-p5-wrapper';
|
||||
|
||||
export function getSettings () {
|
||||
return {
|
||||
cutOffLow: 1,
|
||||
cutOffHigh: 100,
|
||||
nbChannels: 4,
|
||||
interval: 16,
|
||||
bins: 128,
|
||||
duration: 128,
|
||||
srate: 256,
|
||||
name: 'Spectro',
|
||||
sliceFFTLow: 1,
|
||||
sliceFFTHigh: 100,
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionSpectro) window.subscriptionSpectro.unsubscribe();
|
||||
|
||||
window.pipeSpectro$ = null;
|
||||
window.multicastSpectro$ = null;
|
||||
window.subscriptionSpectro = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipeSpectro$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
epoch({
|
||||
duration: Settings.duration,
|
||||
interval: Settings.interval,
|
||||
samplingRate: Settings.srate
|
||||
}),
|
||||
fft({ bins: Settings.bins }),
|
||||
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
|
||||
catchError(err => {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
window.multicastSpectro$ = window.pipeSpectro$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastSpectro$) {
|
||||
window.subscriptionSpectro = window.multicastSpectro$.subscribe(data => {
|
||||
setData(spectroData => {
|
||||
Object.values(spectroData).forEach((channel, index) => {
|
||||
if (index < 4) {
|
||||
channel.datasets[0].data = data.psd[index];
|
||||
channel.xLabels = data.freqs
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ch0: spectroData.ch0,
|
||||
ch1: spectroData.ch1,
|
||||
ch2: spectroData.ch2,
|
||||
ch3: spectroData.ch3
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastSpectro$.connect();
|
||||
console.log("Subscribed to " + Settings.name);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModule(channels) {
|
||||
function RenderCharts() {
|
||||
return Object.values(channels.data).map((channel, index) => {
|
||||
if (channel.datasets[0].data) {
|
||||
window.psd = channel.datasets[0].data;
|
||||
window.freqs = channel.xLabels;
|
||||
if (channel.xLabels) {
|
||||
window.bins = channel.xLabels.length;
|
||||
}
|
||||
}
|
||||
|
||||
//only left frontal channel
|
||||
if (index === 1) {
|
||||
return (
|
||||
<React.Fragment key={'dum'}>
|
||||
<Card.Section>
|
||||
<P5Wrapper sketch={sketchSpectro}
|
||||
psd={window.psd}
|
||||
bins={window.bins}
|
||||
/>
|
||||
</Card.Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={specificTranslations.title}>
|
||||
|
||||
<Card.Section>
|
||||
<Stack>
|
||||
<TextContainer>
|
||||
<p>{specificTranslations.description}</p>
|
||||
</TextContainer>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<div style={chartStyles.wrapperStyle.style}>{RenderCharts()}</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderSliders(setData, setSettings, status, Settings) {
|
||||
|
||||
function resetPipeSetup(value) {
|
||||
buildPipe(Settings);
|
||||
setup(setData, Settings);
|
||||
}
|
||||
|
||||
function handleIntervalRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, interval: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleDurationRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, duration: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleSliceFFTHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, sliceFFTHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleSliceFFTLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, sliceFFTLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
label={'Epoch duration (Sampling Points): ' + Settings.duration}
|
||||
value={Settings.duration}
|
||||
onChange={handleDurationRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={10} step={5} max={Settings.duration}
|
||||
label={'Sampling points between epochs onsets: ' + Settings.interval}
|
||||
value={Settings.interval}
|
||||
onChange={handleIntervalRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={.01} step={.5} max={Settings.cutOffHigh - .5}
|
||||
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
|
||||
value={Settings.cutOffLow}
|
||||
onChange={handleCutoffLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
|
||||
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={1} max={Settings.sliceFFTHigh - 1}
|
||||
label={'Slice FFT Lower limit: ' + Settings.sliceFFTLow + ' Hz'}
|
||||
value={Settings.sliceFFTLow}
|
||||
onChange={handleSliceFFTLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.sliceFFTLow + 1}
|
||||
label={'Slice FFT Upper limit: ' + Settings.sliceFFTHigh + ' Hz'}
|
||||
value={Settings.sliceFFTHigh}
|
||||
onChange={handleSliceFFTHighRangeSliderChange}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import "p5/lib/addons/p5.sound";
|
||||
|
||||
export default function sketchSpectro (p) {
|
||||
|
||||
let spectrum;
|
||||
let binCount;
|
||||
let speed = 4;
|
||||
|
||||
// canvas is global so we can copy it
|
||||
let cnv;
|
||||
|
||||
p.setup = function () {
|
||||
cnv = p.createCanvas(p.windowWidth*.6, 400);
|
||||
p.noStroke();
|
||||
p.colorMode(p.RGB);
|
||||
};
|
||||
|
||||
p.myCustomRedrawAccordingToNewPropsHandler = function (props) {
|
||||
spectrum = props.psd;
|
||||
binCount = props.bins;
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.resizeCanvas(p.windowWidth*.6, 400);
|
||||
}
|
||||
|
||||
p.draw = function () {
|
||||
if (spectrum) {
|
||||
// copy the sketch and move it over based on the speed
|
||||
p.copy(cnv, 0, 0, p.width.toFixed(0), p.height.toFixed(0), -speed, 0, p.width.toFixed(0), p.height.toFixed(0));
|
||||
|
||||
// iterate thru current freq spectrum
|
||||
for (let i = 0; i < binCount; i++) {
|
||||
let value;
|
||||
value = spectrum[i];
|
||||
|
||||
let c = (value/5)*255;
|
||||
p.fill(255-c, 255-c, 255-c);
|
||||
let percent = i / binCount;
|
||||
let y = percent * p.height;
|
||||
p.rect(p.width - speed, p.height - y, speed, p.height / binCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Spectrogram (Spectra over time)",
|
||||
"description": [
|
||||
"Back to the full spectra, we can also look at how this changes over time in a single image. ",
|
||||
"This image, with time on the horizontal x-axis and freqency on the vertical y-axis is like a heat map or topographic map. ",
|
||||
"Darker regions represent frequencies and times with larger power. ",
|
||||
"Remember that on the spectra power is the vertical dimension, here is the is the colour."
|
||||
],
|
||||
"xlabel": "Time (ms)",
|
||||
"ylabel": "Frequency (Hz)",
|
||||
"zlabel": "Power (\u03BCV\u00B2)"
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
import React from "react";
|
||||
import { catchError, multicast } from "rxjs/operators";
|
||||
|
||||
import { TextContainer, Card, Stack, RangeSlider, Button, ButtonGroup, Modal } from "@shopify/polaris";
|
||||
import { saveAs } from 'file-saver';
|
||||
import { take } from "rxjs/operators";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { channelNames } from "muse-js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
import { zipSamples } from "muse-js";
|
||||
|
||||
import {
|
||||
bandpassFilter,
|
||||
epoch,
|
||||
fft,
|
||||
sliceFFT
|
||||
} from "@neurosity/pipes";
|
||||
|
||||
import { chartStyles, generalOptions } from "../chartOptions";
|
||||
|
||||
import * as generalTranslations from "../translations/en";
|
||||
import * as specificTranslations from "./translations/en";
|
||||
|
||||
import P5Wrapper from 'react-p5-wrapper';
|
||||
import sketchFlashSlow from './sketchFlashSlow';
|
||||
import sketchFlashFast from './sketchFlashFast';
|
||||
|
||||
export function getSettings() {
|
||||
return {
|
||||
cutOffLow: 2,
|
||||
cutOffHigh: 20,
|
||||
nbChannels: 4,
|
||||
interval: 100,
|
||||
bins: 256,
|
||||
sliceFFTLow: 1,
|
||||
sliceFFTHigh: 30,
|
||||
duration: 1024,
|
||||
srate: 256,
|
||||
name: 'Ssvep'
|
||||
}
|
||||
};
|
||||
|
||||
export function buildPipe(Settings) {
|
||||
if (window.subscriptionSsvep) window.subscriptionSsvep.unsubscribe();
|
||||
|
||||
window.pipeSsvep$ = null;
|
||||
window.multicastSsvep$ = null;
|
||||
window.subscriptionSsvep = null;
|
||||
|
||||
// Build Pipe
|
||||
window.pipeSsvep$ = zipSamples(window.source.eegReadings$).pipe(
|
||||
bandpassFilter({
|
||||
cutoffFrequencies: [Settings.cutOffLow, Settings.cutOffHigh],
|
||||
nbChannels: Settings.nbChannels }),
|
||||
epoch({
|
||||
duration: Settings.duration,
|
||||
interval: Settings.interval,
|
||||
samplingRate: Settings.srate
|
||||
}),
|
||||
fft({ bins: Settings.bins }),
|
||||
sliceFFT([Settings.sliceFFTLow, Settings.sliceFFTHigh]),
|
||||
catchError(err => {
|
||||
console.log(err);
|
||||
})
|
||||
);
|
||||
|
||||
window.multicastSsvep$ = window.pipeSsvep$.pipe(
|
||||
multicast(() => new Subject())
|
||||
);
|
||||
}
|
||||
|
||||
export function setup(setData, Settings) {
|
||||
console.log("Subscribing to " + Settings.name);
|
||||
|
||||
if (window.multicastSsvep$) {
|
||||
window.subscriptionSsvep = window.multicastSsvep$.subscribe(data => {
|
||||
setData(ssvepData => {
|
||||
Object.values(ssvepData).forEach((channel, index) => {
|
||||
if (index < 4) {
|
||||
channel.datasets[0].data = data.psd[index];
|
||||
channel.xLabels = data.freqs;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ch0: ssvepData.ch0,
|
||||
ch1: ssvepData.ch1,
|
||||
ch2: ssvepData.ch2,
|
||||
ch3: ssvepData.ch3
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
window.multicastSsvep$.connect();
|
||||
console.log("Subscribed to " + Settings.name);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModule(channels) {
|
||||
function renderCharts() {
|
||||
return Object.values(channels.data).map((channel, index) => {
|
||||
const options = {
|
||||
...generalOptions,
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
scaleLabel: {
|
||||
...generalOptions.scales.xAxes[0].scaleLabel,
|
||||
labelString: specificTranslations.xlabel
|
||||
}
|
||||
}
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
scaleLabel: {
|
||||
...generalOptions.scales.yAxes[0].scaleLabel,
|
||||
labelString: specificTranslations.ylabel
|
||||
},
|
||||
ticks: {
|
||||
max: 25,
|
||||
min: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 3
|
||||
}
|
||||
},
|
||||
title: {
|
||||
...generalOptions.title,
|
||||
text: generalTranslations.channel + channelNames[index]
|
||||
}
|
||||
};
|
||||
|
||||
if (index === 0) {
|
||||
return (
|
||||
<Card.Section key={"Card_" + index}>
|
||||
<Line key={"Line_" + index} data={channel} options={options} />
|
||||
</Card.Section>
|
||||
);
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={specificTranslations.title}>
|
||||
<Card.Section>
|
||||
<Stack>
|
||||
<TextContainer>
|
||||
<p>{specificTranslations.description}</p>
|
||||
</TextContainer>
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<div style={chartStyles.wrapperStyle.style}>{renderCharts()}</div>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderSliders(setData, setSettings, status, Settings) {
|
||||
|
||||
function resetPipeSetup(value) {
|
||||
buildPipe(Settings);
|
||||
setup(setData, Settings)
|
||||
}
|
||||
|
||||
function handleIntervalRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, interval: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleCutoffHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, cutOffHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleSliceFFTLowRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, sliceFFTLow: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleSliceFFTHighRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, sliceFFTHigh: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
function handleDurationRangeSliderChange(value) {
|
||||
setSettings(prevState => ({...prevState, duration: value}));
|
||||
resetPipeSetup();
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title={Settings.name + ' Settings'} sectioned>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={128} step={128} max={4096}
|
||||
label={'Epoch duration (Sampling Points): ' + Settings.duration}
|
||||
value={Settings.duration}
|
||||
onChange={handleDurationRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={10} step={5} max={Settings.duration}
|
||||
label={'Sampling points between epochs onsets: ' + Settings.interval}
|
||||
value={Settings.interval}
|
||||
onChange={handleIntervalRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={.01} step={.5} max={Settings.cutOffHigh - .5}
|
||||
label={'Cutoff Frequency Low: ' + Settings.cutOffLow + ' Hz'}
|
||||
value={Settings.cutOffLow}
|
||||
onChange={handleCutoffLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.cutOffLow + .5} step={.5} max={Settings.srate/2}
|
||||
label={'Cutoff Frequency High: ' + Settings.cutOffHigh + ' Hz'}
|
||||
value={Settings.cutOffHigh}
|
||||
onChange={handleCutoffHighRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={1} max={Settings.sliceFFTHigh - 1}
|
||||
label={'Slice FFT Lower limit: ' + Settings.sliceFFTLow + ' Hz'}
|
||||
value={Settings.sliceFFTLow}
|
||||
onChange={handleSliceFFTLowRangeSliderChange}
|
||||
/>
|
||||
<RangeSlider
|
||||
disabled={status === generalTranslations.connect}
|
||||
min={Settings.sliceFFTLow + 1}
|
||||
label={'Slice FFT Upper limit: ' + Settings.sliceFFTHigh + ' Hz'}
|
||||
value={Settings.sliceFFTHigh}
|
||||
onChange={handleSliceFFTHighRangeSliderChange}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderRecord(recordPopChange, recordPop, status, Settings, recordTwoPopChange, recordTwoPop) {
|
||||
const cond1 = "11Hz";
|
||||
const cond2 = "14Hz";
|
||||
|
||||
return(
|
||||
<Card title={'Record ' + Settings.name +' Data'} sectioned>
|
||||
<Stack>
|
||||
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings, cond1);
|
||||
recordPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Record ' + cond1 +' Data'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
saveToCSV(Settings, cond2);
|
||||
recordTwoPopChange();
|
||||
}}
|
||||
primary={status !== generalTranslations.connect}
|
||||
disabled={status === generalTranslations.connect}
|
||||
>
|
||||
{'Record ' + cond2 + ' Data'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
|
||||
<Modal
|
||||
open={recordPop}
|
||||
onClose={recordPopChange}
|
||||
title={"Recording " + cond1 + " Data"}
|
||||
>
|
||||
<Modal.Section>
|
||||
<Card.Section>
|
||||
<P5Wrapper sketch={sketchFlashSlow} />
|
||||
</Card.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={recordTwoPop}
|
||||
onClose={recordTwoPopChange}
|
||||
title={"Recording " + cond2 + " Data"}
|
||||
>
|
||||
<Modal.Section>
|
||||
<Card.Section>
|
||||
<P5Wrapper sketch={sketchFlashFast} />
|
||||
</Card.Section>
|
||||
<TextContainer>
|
||||
<p>
|
||||
Your data is currently recording,
|
||||
once complete it will be downloaded as a .csv file
|
||||
and can be opened with your favorite spreadsheet program.
|
||||
Close this window once the download completes.
|
||||
</p>
|
||||
</TextContainer>
|
||||
</Modal.Section>
|
||||
</Modal>
|
||||
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function saveToCSV(Settings, condition) {
|
||||
const numSamplesToSave = 50;
|
||||
console.log('Saving ' + numSamplesToSave + ' samples...');
|
||||
var localObservable$ = null;
|
||||
const dataToSave = [];
|
||||
|
||||
console.log('making ' + Settings.name + ' headers')
|
||||
|
||||
// take one sample from selected observable object for headers
|
||||
localObservable$ = window.multicastSsvep$.pipe(
|
||||
take(1)
|
||||
);
|
||||
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
let freqs = Object.values(x.freqs);
|
||||
dataToSave.push(
|
||||
"Timestamp (ms),",
|
||||
freqs.map(function(f) {return "ch0_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch1_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch2_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "ch3_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "chAux_" + f + "Hz"}) + ",",
|
||||
freqs.map(function(f) {return "f_" + f + "Hz"}) + "," ,
|
||||
"info",
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
});
|
||||
// put selected observable object into local and start taking samples
|
||||
localObservable$ = window.multicastSsvep$.pipe(
|
||||
take(numSamplesToSave)
|
||||
);
|
||||
|
||||
|
||||
// now with header in place subscribe to each epoch and log it
|
||||
localObservable$.subscribe({
|
||||
next(x) {
|
||||
dataToSave.push(Date.now() + "," + Object.values(x).join(",") + "\n");
|
||||
// logging is useful for debugging -yup
|
||||
// console.log(x);
|
||||
},
|
||||
error(err) { console.log(err); },
|
||||
complete() {
|
||||
console.log('Trying to save')
|
||||
var blob = new Blob(
|
||||
dataToSave,
|
||||
{type: "text/plain;charset=utf-8"}
|
||||
);
|
||||
saveAs(blob, Settings.name + "_" + condition + "_Recording_" + Date.now() + ".csv");
|
||||
console.log('Completed');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export default function sketchFlash (p) {
|
||||
|
||||
let x = 0;
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(300, 300);
|
||||
p.frameRate(30);
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.createCanvas(300, 300);
|
||||
}
|
||||
|
||||
|
||||
p.mousePressed = function () {
|
||||
p.background(256);
|
||||
}
|
||||
|
||||
p.draw = function () {
|
||||
p.background(255);
|
||||
x = x+1;
|
||||
if (x % 5 === 0) {
|
||||
p.fill(0, 0, 0);
|
||||
} else {
|
||||
p.fill(255, 255, 255);
|
||||
}
|
||||
p.noStroke();
|
||||
p.ellipse(p.width/2, p.height/2, 300);
|
||||
p.fill(255,0,0);
|
||||
p.text("+", p.width/2, p.height/2);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
export default function sketchFlash (p) {
|
||||
|
||||
let x = 0;
|
||||
|
||||
p.setup = function () {
|
||||
p.createCanvas(300, 300);
|
||||
p.frameRate(30);
|
||||
};
|
||||
|
||||
p.windowResized = function() {
|
||||
p.createCanvas(300, 300);
|
||||
}
|
||||
|
||||
|
||||
p.mousePressed = function () {
|
||||
p.background(256);
|
||||
}
|
||||
|
||||
p.draw = function () {
|
||||
p.background(255);
|
||||
x = x+1;
|
||||
if (x % 11 === 0) {
|
||||
p.fill(0, 0, 0);
|
||||
} else {
|
||||
p.fill(255, 255, 255);
|
||||
}
|
||||
p.noStroke();
|
||||
p.ellipse(p.width/2, p.height/2, 300);
|
||||
p.fill(255,0,0);
|
||||
p.text("+", p.width/2, p.height/2);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "Steady-State Visual Evoked Potential (SSVEP) Experiment",
|
||||
"description": [
|
||||
"In the next demo we run our second experiment, comparing the spectra in another two conditions. ",
|
||||
"When we see or hear rhythms in our environemnts (music, flashing lights, etc.), our brain picks up on those rhythms. ",
|
||||
"The brain starts to pick up on the rhythms and begins to oscillate at the same frequency. ",
|
||||
"This is called a steady-state response, or sometimes entrainement, and depends on cognitive factors like attention as well. ",
|
||||
"Here we will compare the spectra in two conditions of different rhythmic visual stimulation.",
|
||||
"WARNING: The following experiment utilizes flashing stimuli"
|
||||
],
|
||||
"xlabel": "Frequency (Hz)",
|
||||
"ylabel": "Power (\u03BCV\u00B2)"
|
||||
}
|
||||
@@ -23,6 +23,13 @@ export const emptyChannelData = {
|
||||
}
|
||||
};
|
||||
|
||||
export const emptySingleChannelData = {
|
||||
ch1: {
|
||||
datasets: [{}]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const generalOptions = {
|
||||
scales: {
|
||||
xAxes: [
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
{
|
||||
"title": "Choose your Module",
|
||||
"types": {
|
||||
"intro": "Introduction",
|
||||
"raw": "Raw and Filtered Data",
|
||||
"spectra": "Frequency Spectra",
|
||||
"bands": "Frequency Bands",
|
||||
"animate": "Brain Controlled Animation"
|
||||
"intro": "1. Introduction",
|
||||
"heartRaw": "2. Electrocardiogram (Heart beats)",
|
||||
"heartSpectra": "3. Heart Rate (Beats per minute)",
|
||||
"raw": "4. Raw and Filtered Data",
|
||||
"spectra": "5. Frequency Spectra",
|
||||
"bands": "6. Frequency Bands",
|
||||
"animate": "7. Brain Controlled Animation",
|
||||
"spectro": "8. Spectrogram (spectra over time)",
|
||||
"alpha": "9. Eyes open vs. Eyes closed Experiment",
|
||||
"ssvep": "10. Steady-State Visual Evoked Potential (SSVEP) Experiment",
|
||||
"evoked": "11. Stimulus Evoked Event-related potential (ERP)",
|
||||
"predict": "12. Predict brain states with a trained classifier"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { App } from "./components/App/App";
|
||||
import registerServiceWorker from "./workers/registerServiceWorker";
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById("root"));
|
||||
registerServiceWorker();
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
// In production, we register a service worker to serve assets from local cache.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on the "N+1" visit to a page, since previously
|
||||
// cached resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
||||
// This link also includes instructions on opting out of this behavior.
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export default function register() {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
console.log('New content is available; please refresh.');
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
+952
-18
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
Referência em uma Nova Issue
Bloquear um usuário