├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── examples ├── getStreaming │ └── getStreaming.js ├── getStreamingWiFiDirect │ └── getStreamingWiFiDirect.js └── xstreamCSV │ └── xstreamCSV.js ├── images └── WiFi_front_product.jpg ├── openBCIWifi.js ├── package-lock.json ├── package.json └── test ├── bluebirdChecks.js ├── openBCIWifi-test.js └── timingEventsAsPromises.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | 51 | # Webstorm 52 | .idea 53 | .idea/*.xml 54 | .DS_Store 55 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | test_mocks/ 3 | 4 | # MIRRORED FROM .gitignore PLEASE MAINTAIN 5 | logs 6 | *.log 7 | pids 8 | *.pid 9 | *.seed 10 | lib-cov 11 | coverage 12 | .grunt 13 | .lock-wscript 14 | build/Release 15 | node_modules 16 | .idea 17 | .DS_Store 18 | public 19 | myOutput.txt 20 | *.tgz 21 | openBCISerialFormat 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.11" 4 | - "8.5" 5 | - "10" 6 | install: 7 | - npm install --all 8 | script: 9 | - npm run test-cov 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.0.2 2 | 3 | ### Bug Fixes 4 | 5 | * Get host IP address on Windows machines using valid network interface 6 | 7 | # v1.0.1 8 | 9 | ### Bug Fixes 10 | 11 | * Updated references to .Debug and .Utilities to .debug and .utilities 12 | 13 | # v1.0.0 14 | 15 | ### Breaking Changes 16 | 17 | * Name of package is now @openbci/wifi 18 | 19 | ### Enhancements 20 | 21 | * Include node 10 in testing 22 | 23 | # v0.4.2 24 | 25 | ### Examples 26 | 27 | 1. Refactored examples `getStreaming.js` and `getStreamingWiFiDirect.js` 28 | - Removed dead code 29 | - Renamed variables 30 | - Variables properly classed into `const` and `let` 31 | - Improved readability 32 | - Eliminated functional redundancies 33 | 2. Added example `xstreamCSV.js` 34 | - Direct WiFi 35 | - Pipes data from OpenBCI hardware into an [XStream](https://github.com/staltz/xstream) producer 36 | - Outputs to console, CSV (via WriteStream) using listeners 37 | 38 | # v0.4.1 39 | 40 | ### Bug Fixes 41 | 42 | * DELETE, GET, and POST would resolve even if code was not equal to 200 43 | 44 | # v0.4.0 45 | 46 | ### New Features 47 | 48 | * Add UDP support! New option on connect and Constructor called `protocol` that can be either `'udp'` or `'tcp'` (Default is `tcp`) 49 | * Add burst mode UDP support! New option on connect and Constructor called `burst` that can be either `true` or `false` (default `false`). When `true` and `protocol` option is UDP, will tell WiFi Shield to send every packet three times. The module will automatically only process new data. 50 | * Went through all the docs again and cleaned up! 51 | 52 | ### New Example 53 | 54 | * WiFi direct example! 55 | 56 | ### Bug Fixes 57 | 58 | * Now starting both UDP and TCP systems incase so user can select at `connect` function. 59 | 60 | # v0.3.1 61 | 62 | ### New Features 63 | 64 | * Add UDP support. 65 | 66 | # v0.3.0 67 | 68 | Docs for all!! 69 | 70 | ### New Files 71 | 72 | * `CODE_OF_CONDUCT.md` added to govern community 73 | * `CONTRIBUTING.md` added to describe how people should contribute 74 | * `ROADMAP.md` added to outline a roadmap for project 75 | 76 | ### Bug Fixes 77 | 78 | * Bump utilities to 0.2.7 to get patch for wifi accel data and stop byte 79 | 80 | ### Breaking Changes 81 | 82 | * Removed index.js to conform to other cyton and ganglion modules 83 | 84 | # v0.2.1 85 | 86 | ### Bug Fixes 87 | 88 | * Bumping utility version to 0.2.4 fixes bug in this repo too for ganglion over wifi with no scale. 89 | 90 | # 0.2.0 91 | 92 | ### Breaking changes 93 | 94 | * Update `autoFindAndConnectToWifiShield()` to be called `searchToStream` and upgraded it's power! Checkout the source code for how to use the function. 95 | 96 | ### New Features 97 | 98 | * Added function for telling the WiFi Shield to forget it's credentials and turn back into an access point, aka broad casting it's unique name ready for someone to connect and have it join another network. 99 | 100 | # 0.1.4 101 | 102 | ### Enhancements 103 | 104 | * Bump `openbci-utilities` to v0.2.0 for accelDataCounts support on cyton. 105 | 106 | # 0.1.3 107 | 108 | ### New Features 109 | 110 | * Add `channelSet` function 111 | 112 | # 0.1.2 113 | 114 | ### New Features 115 | 116 | * Add `sdStart`, `sdStop`, and `syncRegisterSettings` 117 | 118 | # 0.1.1 119 | 120 | ### New Features 121 | 122 | * Added function called `autoFindAndConnectToWifiShield()` which will automatically search the local network for wifi shields and connect to the first one it finds. 123 | * Add `latency` setting to default options 124 | 125 | # 0.1.0 126 | 127 | ### Breaking Change 128 | 129 | * Updated `localName` building for wifi v0.2.1+ where name changed to `PTW-0001-XXXX` where the last four are the last two bytes of the mac address. 130 | 131 | # 0.0.5 132 | 133 | ### Enhancements 134 | 135 | * Bump utilities to 0.1.2 136 | 137 | # 0.0.4 138 | 139 | ### New Features 140 | 141 | * Switch `getSampleRate` and `setSampleRate` to regex parseing which adds support for ganglion now. 142 | * Started saving the wifi shields found so the user can pass a `localName` and the module will use the ip address to route to it. mDNS is too router dependent. 143 | 144 | # 0.0.3 145 | 146 | This actually seems to be generally working for cyton and ganglion. Daisy needs more testing. 147 | 148 | ### Enhancements 149 | 150 | * Now working with daisy, cyton and ganglion. 151 | * Bumped `openbci-utilities` to v0.0.8 152 | * Now setting the sample rate it possible checkout the example 153 | * Module will now sync itself with the shield on `connect` which will align board type and channel gains. 154 | 155 | # 0.0.2 156 | 157 | The readme and package.json were horribly wrong in `v0.0.1` 158 | 159 | # 0.0.1 160 | 161 | Initial release 162 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # OpenBCI WiFi NodeJS Code of Conduct 2 | 3 | ## Purpose 4 | 5 | It is our hope that any one is able to contribute to OpenBCI WiFi NodeJS regardless of their background. Thus, we hope to provide a safe, welcoming, and warmly geeky environment for everybody, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment 10 | include: 11 | 12 | * Using welcoming and inclusive language 13 | * Being respectful of differing viewpoints and experiences 14 | * Gracefully accepting constructive criticism 15 | * Focusing on what is best for the community 16 | * Showing empathy towards other community members 17 | 18 | Examples of unacceptable behavior by participants include: 19 | 20 | * The use of sexualized language or imagery and unwelcome sexual attention or 21 | advances 22 | * Trolling, insulting/derogatory comments, and personal or political attacks 23 | * Public or private harassment 24 | * Publishing others' private information, such as a physical or electronic 25 | address, without explicit permission 26 | * Other conduct which could reasonably be considered inappropriate in a 27 | professional setting 28 | 29 | ## Our Responsibilities 30 | 31 | Project maintainers are responsible for clarifying the standards of acceptable 32 | behavior and are expected to take appropriate and fair corrective action in 33 | response to any instances of unacceptable behavior. 34 | 35 | Project maintainers have the right and responsibility to remove, edit, or 36 | reject comments, commits, code, wiki edits, issues, and other contributions 37 | that are not aligned to this Code of Conduct, or to ban temporarily or 38 | permanently any contributor for other behaviors that they deem inappropriate, 39 | threatening, offensive, or harmful. 40 | 41 | ## Scope 42 | 43 | This Code of Conduct applies both within project spaces and in public spaces 44 | when an individual is representing the project or its community. Examples of 45 | representing a project or community include using an official project e-mail 46 | address, posting via an official social media account, or acting as an appointed 47 | representative at an online or offline event. Representation of a project may be 48 | further defined and clarified by project maintainers. 49 | 50 | ## Enforcement 51 | 52 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 53 | reported by contacting the project team at [info@pushtheworld.us](mailto:info@pushtheworld.us). All 54 | complaints will be reviewed and investigated and will result in a response that 55 | is deemed necessary and appropriate to the circumstances. The project team is 56 | obligated to maintain confidentiality with regard to the reporter of an incident. 57 | Further details of specific enforcement policies may be posted separately. 58 | 59 | Project maintainers who do not follow or enforce the Code of Conduct in good 60 | faith may face temporary or permanent repercussions as determined by other 61 | members of the project's leadership. 62 | 63 | ## Attribution 64 | 65 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 66 | available at [http://contributor-covenant.org/version/1/4][version] 67 | 68 | [homepage]: http://contributor-covenant.org 69 | [version]: http://contributor-covenant.org/version/1/4/ 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | :tada::clinking_glasses: First off, thanks for taking the time to contribute! :tada::clinking_glasses: 4 | 5 | Contributions are always welcome, no matter how small. 6 | 7 | The following is a small set of guidelines for how to contribute to the project 8 | 9 | ## Where to start 10 | 11 | ### Code of Conduct 12 | This project adheres to the Contributor Covenant [Code of Conduct](CODE_OF_CONDUCT.md). 13 | By participating you are expected to adhere to these expectations. Please report unacceptable behaviour to [info@pushtheworld.us](mailto:info@pushtheworld.us) 14 | 15 | ### Contributing on Github 16 | 17 | If you're new to Git and want to learn how to fork this repo, make your own additions, and include those additions in the master version of this project, check out this [great tutorial](http://blog.davidecoppola.com/2016/11/howto-contribute-to-open-source-project-on-github/). 18 | 19 | ### Community 20 | 21 | This project is maintained by the [OpenBCI](www.openbci.com) and [NeuroTechX](www.neurotechx.com) community. Join the NeuroTechX Slack to check out our #devices channel, where discussions about OpenBCI takes place. 22 | 23 | ## How can I contribute? 24 | 25 | This is currently a small, humble project so our contribution process is rather casual. If there's a feature you'd be interested in building, go ahead! Let us know by [opening an issue](../../issues/new) and we'll support you as much as we can. When you're finished submit a pull request to the master branch referencing the specific issue you addressed. 26 | 27 | If you find a bug, or have a suggestion on how to improve the project, just fill out a [Github issue](../../issues) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 OpenBCI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **THIS REPOSITORY HAS BEEN DEPRECATED AND IS NO LONGER IN ACTIVE DEVELOPMENT.** 2 | 3 | 4 | # OpenBCI WiFi Shield NodeJS SDK 5 | 6 |

7 | banner 8 |

9 |

10 | Make programming with OpenBCI reliable, easy, research grade and fun! 11 |

12 | 13 | [![codecov](https://codecov.io/gh/OpenBCI/OpenBCI_NodeJS_Wifi/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenBCI/OpenBCI_NodeJS_Wifi) 14 | [![Dependency Status](https://david-dm.org/OpenBCI/OpenBCI_NodeJS_Wifi.svg)](https://david-dm.org/OpenBCI/OpenBCI_NodeJS_Wifi) 15 | [![npm](https://img.shields.io/npm/dm/@openbci/wifi.svg?maxAge=2592000)](http://npmjs.com/package/@openbci/wifi) 16 | [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard) 17 | 18 | ## Welcome! 19 | 20 | First and foremost, Welcome! :tada: Willkommen! :confetti_ball: Bienvenue! :balloon::balloon::balloon: 21 | 22 | Thank you for visiting the OpenBCI WiFi Shield NodeJS SDK repository. 23 | 24 | This document (the README file) is a hub to give you some information about the project. Jump straight to one of the sections below, or just scroll down to find out more. 25 | 26 | * [What are we doing? (And why?)](#what-are-we-doing) 27 | * [Who are we?](#who-are-we) 28 | * [What do we need?](#what-do-we-need) 29 | * [How can you get involved?](#get-involved) 30 | * [Get in touch](#contact-us) 31 | * [Find out more](#find-out-more) 32 | * [Understand the jargon](#glossary) 33 | 34 | ## What are we doing? 35 | 36 | ### The problem 37 | 38 | * Users continuously struggle to get prerequisites properly installed to get current OpenBCI Cyton and Ganglion, hours/days/weeks are wasted just _trying to get the data_. 39 | * Bluetooth requires you to stay close to your computer, if you go to far, data is lost and the experiment is over. 40 | * Bluetooth is too slow for transmitting research grade EEG, researchers want 1000Hz (samples per second), bluetooth with 8 channels is limted to 250Hz and with 16 channels limited to 125Hz. 41 | * Bluetooth is unreliable when many other Bluetooth devices are around, demo device or use in busy real life experiment is not reliable. (think grand central station at rush hour) 42 | * Bluetooth requires data to be sent to desktops in raw or compressed form, must use other node modules to parse complex byte streams, prevents from running in browser. 43 | * OpenBCI Cyton (8 and 16 channel) with Bluetooth cannot go to any mobile device because of required Bluetooth-to-USB "Dongle". Must use USB port on Desktop/Laptop computers. 44 | * Bluetooth on Ganglion requires low level drivers to use computers bluetooth hardware. 45 | * The OpenBCI Cyton and Ganglion must transmit data to another computer over Bluetooth before going to the cloud for storage or analytics 46 | * OpenBCI Cyton Dongle FTDI virtual comm port drivers have high latency by default which limits the rate at which new data is made available to your application to twice a second when it should get data as close to 250 times a second. 47 | * Using Cyton or Ganglion NodeJS drivers requires the use of [_native C++ modules_](https://nodejs.org/api/addons.html) which continuously confuse developers of all levels. 48 | 49 | So, if even the very best developers integrate the current easy to use Cyton and Ganglion NodeJS drivers, they are still burdened by the limitations of the physical hardware on the OpenBCI system. 50 | 51 | ### The solution 52 | 53 | The OpenBCI WiFi Shield NodeJS SDK will: 54 | 55 | * Find, connect, sync, and configure (e.g. set sample rate) with OpenBCI WiFi Shield and Carrier board (Ganglion or Cyton or Cyton with Daisy) in a single function call 56 | * Use TCP over WiFi to prevent packet loss 57 | * Relies on **zero** [_native C++ modules_](https://nodejs.org/api/addons.html) 58 | * Enable streaming of high speed (samples rates over 100Hz), low latency (by default, send data every 10ms), research grade EEG (no lost data) directly to any internet connected device (i.e. iPhone, Android, macOS, Windows, Linux, Raspberry Pi 3) 59 | * With WiFi Shield you can now use OpenBCI Ganglion and Cyton anywhere you have a good enough WiFi signal. 60 | * Hotspots create a stable wireless transmission system even in crowded areas 61 | 62 | Using WiFi physically solves limitations with the current state-of-the-art open source bio sensor. The goal for the WiFi Shield firmware was to create a [_one up_](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=0ahUKEwjF7pax67PWAhUH6oMKHdfJAcgQFggoMAA&url=https%3A%2F%2Fwww.electroimpact.com%2FWhitePapers%2F2008-01-2297.pdf&usg=AFQjCNHSyVXxRNtkFrmPiRqM5WqHWdO9-g) data pipeline, where scientific data in JSON is sent instead of raw/compressed ADC counts (yuk!) to ***make programming with OpenBCI reliable, easy, research grade and fun!*** 63 | 64 | ## Who are we? 65 | 66 | The founder of the OpenBCI WiFi Shield NodeJS SDK is [AJ Keller][link_aj_keller]. 67 | 68 | 69 | 74 | 75 | 76 | [AJ][link_aj_keller] is an invited member of the 4th cohort [Open Leaders Cohort][link_openleaderscohort] of the [Mozilla Science Lab][link_mozsci] who brought together open science advocates from around the world to participate in the first [Working Open Workshop][link_mozwow] in Berlin in February 2016. The [training exercises][link_mozwow] (which are free and easy to reuse) focused on how to build and effectively engage communities so they can work together to develop tools and resources for the greater good. 77 | 78 | ## What do we need? 79 | 80 | **You**! In whatever way you can help. 81 | 82 | We need expertise in programming, user experience, software sustainability, documentation and technical writing and project management. 83 | 84 | We'd love your feedback along the way. 85 | 86 | Our primary goal is to make programming with OpenBCI reliable, easy, research grade and fun, and we're excited to support the professional development of any and all of our contributors. If you're looking to learn to code, try out working collaboratively, or translate you skills to the digital domain, we're here to help. 87 | 88 | ## Get involved 89 | 90 | If you think you can help in any of the areas listed above (and we bet you can) or in any of the many areas that we haven't yet thought of (and here we're *sure* you can) then please check out our [contributors' guidelines](CONTRIBUTING.md) and our [roadmap](ROADMAP.md). 91 | 92 | Please note that it's very important to us that we maintain a positive and supportive environment for everyone who wants to participate. When you join us we ask that you follow our [code of conduct](CODE_OF_CONDUCT.md) in all interactions both on and offline. 93 | 94 | 95 | ## Contact us 96 | 97 | If you want to report a problem or suggest an enhancement we'd love for you to [open an issue](../../issues) at this github repository because then we can get right on it. But you can also contact [AJ][link_aj_keller] by email (pushtheworldllc AT gmail DOT com) or on [twitter](https://twitter.com/aj-ptw). 98 | 99 | You can also hang out, ask questions and share stories in the [OpenBCI NodeJS room](https://gitter.im/OpenBCI/OpenBCI_NodeJS?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) on Gitter. 100 | 101 | ## Find out more 102 | 103 | You might be interested in: 104 | 105 | * Purchase a [WiFi Shield from OpenBCI](https://shop.openbci.com/collections/frontpage/products/wifi-shield?variant=44534009550) 106 | * A NodeJS example for WiFi Shield: [getStreaming.js][link_wifi_get_streaming] 107 | 108 | And of course, you'll want to know our: 109 | 110 | * [Contributors' guidelines](CONTRIBUTING.md) 111 | * [Roadmap](ROADMAP.md) 112 | 113 | ## Thank you 114 | 115 | Thank you so much (Danke schön! Merci beaucoup!) for visiting the project and we do hope that you'll join us on this amazing journey to make programming with OpenBCI fun and easy. 116 | 117 | # Documentation 118 | 119 | ## Table of Contents: 120 | --- 121 | 122 | 1. [Installation](#install) 123 | 2. [TL;DR](#tldr) 124 | 3. [Developing](#developing) 125 | 4. [Contribute](#contribute) 126 | 5. [License](#license) 127 | 6. [General Overview](#general-overview) 128 | 7. [Classes](#classes) 129 | 8. [Typedefs](#typedefs) 130 | 9. [Wifi](#wifi) 131 | 132 | ## Installation: 133 | 134 | We assume you have NodeJS installed already, if you don't please [download the latest stable version from NodeJS](https://nodejs.org/en/download/). 135 | 136 | Then simply using the node package manager command line tool, enter: 137 | 138 | ``` 139 | npm install @openbci/wifi 140 | ``` 141 | 142 | ## TL;DR: 143 | Get connected and [start streaming right now with the example code](examples/getStreaming/getStreaming.js). 144 | 145 | ```ecmascript 6 146 | const Wifi = require('@openbci/wifi'); 147 | let wifi = new Wifi({ 148 | debug: false, 149 | verbose: true, 150 | latency: 10000 151 | }); 152 | 153 | wifi.on(k.OBCIEmitterSample, (sample) => { 154 | for (let i = 0; i < wifi.getNumberOfChannels(); i++) { 155 | console.log("Channel " + (i + 1) + ": " + sample.channelData[i].toFixed(8) + " Volts."); 156 | // prints to the console 157 | // "Channel 1: 0.00001987 Volts." 158 | // "Channel 2: 0.00002255 Volts." 159 | // ... 160 | // "Channel 8: -0.00001875 Volts." 161 | } 162 | }); 163 | 164 | wifi.searchToStream({ 165 | sampleRate: 1000 // Custom sample rate 166 | shieldName: 'OpenBCI-2C34', // Enter the unique name for your wifi shield 167 | streamStart: true // Call to start streaming in this function 168 | }).catch(console.log); 169 | ``` 170 | ## Developing: 171 | ### Running: 172 | 173 | ``` 174 | npm install 175 | ``` 176 | 177 | ### Testing: 178 | 179 | ``` 180 | npm test 181 | ``` 182 | 183 | ## Contribute: 184 | 185 | 1. Checkout [contributors' guidelines](CONTRIBUTING.md) 186 | 2. Fork it! 187 | 3. Branch off of `development`: `git checkout development` 188 | 4. Create your feature branch: `git checkout -b my-new-feature` 189 | 5. Make changes 190 | 6. If adding a feature, please add test coverage. 191 | 7. Ensure tests all pass. (`npm test`) 192 | 8. Commit your changes: `git commit -m 'Add some feature'` 193 | 9. Push to the branch: `git push origin my-new-feature` 194 | 10. Submit a pull request. Make sure it is based off of the `development` branch when submitting! :D 195 | 196 | ## License: 197 | 198 | MIT 199 | 200 | ## General Overview 201 | 202 | Initialization 203 | -------------- 204 | 205 | Initializing the board: 206 | 207 | ```js 208 | const Wifi = require('../../openBCIWifi'); 209 | const ourBoard = new Wifi(); 210 | ``` 211 | Go [checkout out the get streaming example](examples/getStreaming/getStreaming.js)! 212 | 213 | For initializing with options, such as verbose print outs: 214 | 215 | ```js 216 | const Wifi = require('../../openBCIWifi'); 217 | const wifi = new Wifi({ 218 | verbose: true 219 | }); 220 | ``` 221 | 222 | or if you are using ES6: 223 | ```js 224 | import Wifi from '../../openBCIWifi'; 225 | import { constants } from '@openbci/utilities'; 226 | const wifi = new Wifi(); 227 | wifi.connect("OpenBCI-2114"); 228 | ``` 229 | 230 | To debug, it's amazing, do: 231 | ```js 232 | const Wifi = require('../../openBCIWifi'); 233 | const wifi = new Wifi({ 234 | debug: true 235 | }); 236 | ``` 237 | 238 | Sample properties: 239 | ------------------ 240 | * `startByte` (`Number` should be `0xA0`) 241 | * `sampleNumber` (a `Number` between 0-255) 242 | * `channelData` (channel data indexed at 0 filled with floating point `Numbers` in Volts) if `sendCounts` is false 243 | * `channelDataCounts` (channel data indexed at 0 filled with floating point `Numbers` in Volts) if `sendCounts` is true 244 | * `accelData` (`Array` with X, Y, Z accelerometer values when new data available) if `sendCounts` is false 245 | * `accelDataCounts` (`Array` with X, Y, Z accelerometer values when new data available) Only present if `sendCounts` is true 246 | * `auxData` (`Buffer` filled with either 2 bytes (if time synced) or 6 bytes (not time synced)) 247 | * `stopByte` (`Number` should be `0xCx` where x is 0-15 in hex) 248 | * `boardTime` (`Number` the raw board time) 249 | * `timeStamp` (`Number` the `boardTime` plus the NTP calculated offset) 250 | 251 | The power of this module is in using the sample emitter, to be provided with samples to do with as you wish. 252 | 253 | To get a 'sample' event, you need to: 254 | ------------------------------------- 255 | 1. Install the 'sample' event emitter 256 | 2. Call [`.searchToStream(_options_)`](#Wifi-connect) 257 | ```js 258 | const Wifi = require('../../openBCIWifi'); 259 | let wifi = new Wifi({ 260 | debug: false, 261 | verbose: true, 262 | latency: 10000 263 | }); 264 | 265 | wifi.on(k.OBCIEmitterSample, (sample) => { 266 | for (let i = 0; i < wifi.getNumberOfChannels(); i++) { 267 | console.log("Channel " + (i + 1) + ": " + sample.channelData[i].toFixed(8) + " Volts."); 268 | // prints to the console 269 | // "Channel 1: 0.00001987 Volts." 270 | // "Channel 2: 0.00002255 Volts." 271 | // ... 272 | // "Channel 8: -0.00001875 Volts." 273 | } 274 | }); 275 | 276 | wifi.searchToStream({ 277 | sampleRate: 1000 // Custom sample rate 278 | shieldName: 'OpenBCI-2C34', // Enter the unique name for your wifi shield 279 | streamStart: true // Call to start streaming in this function 280 | }).catch(console.log); 281 | ``` 282 | Close the connection with [`.streamStop()`](#Wifi+streamStop) and disconnect with [`.disconnect()`](#Wifi+disconnect) 283 | ```js 284 | const Wifi = require('../../openBCIWifi'); 285 | const wifi = new Wifi(); 286 | wifi.streamStop().then(wifi.disconnect()); 287 | ``` 288 | 289 | ## Classes 290 | 291 |
292 |
Wifi
293 |
294 |
295 | 296 | ## Typedefs 297 | 298 |
299 |
InitializationObject : Object
300 |
301 |
302 | 303 | 304 | 305 | ## Wifi 306 | **Kind**: global class 307 | **Author**: AJ Keller (@aj-ptw) 308 | 309 | * [Wifi](#Wifi) 310 | * [new Wifi(options)](#new_Wifi_new) 311 | * _instance_ 312 | * [.options](#Wifi+options) : [InitializationObject](#InitializationObject) 313 | * [._accelArray](#Wifi+_accelArray) 314 | * [.curOutputMode](#Wifi+curOutputMode) 315 | * [.bufferRawDataPackets(rawDataPackets)](#Wifi+bufferRawDataPackets) ⇒ Array 316 | * [.channelOff(channelNumber)](#Wifi+channelOff) ⇒ Promise.<T> 317 | * [.channelOn(channelNumber)](#Wifi+channelOn) ⇒ Promise.<T> \| \* 318 | * [.channelSet(channelNumber, powerDown, gain, inputType, bias, srb2, srb1)](#Wifi+channelSet) ⇒ Promise 319 | * [.impedanceSet(channelNumber, pInputApplied, nInputApplied)](#Wifi+impedanceSet) ⇒ Promise 320 | * [.connect(o)](#Wifi+connect) ⇒ Promise 321 | * [.disconnect()](#Wifi+disconnect) ⇒ Promise 322 | * [.isConnected()](#Wifi+isConnected) ⇒ boolean 323 | * [.isSearching()](#Wifi+isSearching) ⇒ boolean 324 | * [.isStreaming()](#Wifi+isStreaming) ⇒ boolean 325 | * [.getBoardType()](#Wifi+getBoardType) ⇒ \* 326 | * [.getFirmwareVersion()](#Wifi+getFirmwareVersion) ⇒ String 327 | * [.getIpAddress()](#Wifi+getIpAddress) ⇒ null \| String 328 | * [.getLatency()](#Wifi+getLatency) ⇒ Number 329 | * [.getMacAddress()](#Wifi+getMacAddress) ⇒ null \| String 330 | * [.getNumberOfChannels()](#Wifi+getNumberOfChannels) ⇒ Number 331 | * [.getSampleRate()](#Wifi+getSampleRate) ⇒ Number 332 | * [.getShieldName()](#Wifi+getShieldName) ⇒ null \| String 333 | * [.impedanceStart()](#Wifi+impedanceStart) ⇒ global.Promise \| Promise 334 | * [.impedanceStop()](#Wifi+impedanceStop) ⇒ global.Promise \| Promise 335 | * [.searchToStream(o)](#Wifi+searchToStream) ⇒ Promise 336 | * [.setSampleRate(sampleRate)](#Wifi+setSampleRate) ⇒ Promise 337 | * [.syncSampleRate()](#Wifi+syncSampleRate) ⇒ Promise 338 | * [.searchStart()](#Wifi+searchStart) ⇒ Promise 339 | * [.searchStop()](#Wifi+searchStop) ⇒ global.Promise \| Promise 340 | * [.sdStop()](#Wifi+sdStop) ⇒ Promise 341 | * [.syncRegisterSettings()](#Wifi+syncRegisterSettings) ⇒ Promise.<T> \| \* 342 | * [.softReset()](#Wifi+softReset) ⇒ Promise 343 | * [.eraseWifiCredentials()](#Wifi+eraseWifiCredentials) ⇒ Promise 344 | * [.streamStart()](#Wifi+streamStart) ⇒ Promise 345 | * [.streamStop()](#Wifi+streamStop) ⇒ Promise 346 | * [.syncInfo(o)](#Wifi+syncInfo) ⇒ Promise.<TResult> 347 | * [.write(data)](#Wifi+write) ⇒ Promise 348 | * [.destroy()](#Wifi+destroy) 349 | * [.wifiGetLocalPort()](#Wifi+wifiGetLocalPort) ⇒ number 350 | * [.wifiGetLocalPortUDP()](#Wifi+wifiGetLocalPortUDP) ⇒ number 351 | * [.wifiGetLocalPortTCP()](#Wifi+wifiGetLocalPortTCP) ⇒ number 352 | * [.wifiInitServer()](#Wifi+wifiInitServer) 353 | * [.delete(path)](#Wifi+delete) ⇒ Promise 354 | * [.get(path)](#Wifi+get) ⇒ Promise 355 | * [.post(path, payload)](#Wifi+post) ⇒ Promise 356 | * _inner_ 357 | * [~o](#Wifi..o) 358 | 359 | 360 | 361 | ### new Wifi(options) 362 | The initialization method to call first, before any other method. 363 | 364 | 365 | | Param | Type | Description | 366 | | --- | --- | --- | 367 | | options | [InitializationObject](#InitializationObject) | (optional) - Board optional configurations. | 368 | 369 | 370 | 371 | ### wifi.options : [InitializationObject](#InitializationObject) 372 | **Kind**: instance property of [Wifi](#Wifi) 373 | 374 | 375 | ### wifi._accelArray 376 | Private Properties (keep alphabetical) 377 | 378 | **Kind**: instance property of [Wifi](#Wifi) 379 | 380 | 381 | ### wifi.curOutputMode 382 | Public Properties (keep alphabetical) 383 | 384 | **Kind**: instance property of [Wifi](#Wifi) 385 | 386 | 387 | ### wifi.bufferRawDataPackets(rawDataPackets) ⇒ Array 388 | This function is for redundancy after a long packet send, the wifi firmware can resend the same 389 | packet again, using this till add redundancy on poor networks. 390 | 391 | **Kind**: instance method of [Wifi](#Wifi) 392 | **Author**: AJ Keller (@aj-ptw) 393 | 394 | | Param | Description | 395 | | --- | --- | 396 | | rawDataPackets | - | 397 | 398 | 399 | 400 | ### wifi.channelOff(channelNumber) ⇒ Promise.<T> 401 | Send a command to the board to turn a specified channel off 402 | 403 | **Kind**: instance method of [Wifi](#Wifi) 404 | **Author**: AJ Keller (@aj-ptw) 405 | 406 | | Param | 407 | | --- | 408 | | channelNumber | 409 | 410 | 411 | 412 | ### wifi.channelOn(channelNumber) ⇒ Promise.<T> \| \* 413 | Send a command to the board to turn a specified channel on 414 | 415 | **Kind**: instance method of [Wifi](#Wifi) 416 | **Author**: AJ Keller (@aj-ptw) 417 | 418 | | Param | 419 | | --- | 420 | | channelNumber | 421 | 422 | 423 | 424 | ### wifi.channelSet(channelNumber, powerDown, gain, inputType, bias, srb2, srb1) ⇒ Promise 425 | To send a channel setting command to the board 426 | 427 | **Kind**: instance method of [Wifi](#Wifi) 428 | **Returns**: Promise - resolves if sent, rejects on bad input or no board 429 | **Author**: AJ Keller (@aj-ptw) 430 | 431 | | Param | Description | 432 | | --- | --- | 433 | | channelNumber | Number (1-16) | 434 | | powerDown | Bool (true -> OFF, false -> ON (default)) turns the channel on or off | 435 | | gain | Number (1,2,4,6,8,12,24(default)) sets the gain for the channel | 436 | | inputType | String (normal,shorted,biasMethod,mvdd,temp,testsig,biasDrp,biasDrn) selects the ADC channel input source | 437 | | bias | Bool (true -> Include in bias (default), false -> remove from bias) selects to include the channel input in bias generation | 438 | | srb2 | Bool (true -> Connect this input to SRB2 (default), false -> Disconnect this input from SRB2) Select to connect (true) this channel's P input to the SRB2 pin. This closes a switch between P input and SRB2 for the given channel, and allows the P input to also remain connected to the ADC. | 439 | | srb1 | Bool (true -> connect all N inputs to SRB1, false -> Disconnect all N inputs from SRB1 (default)) Select to connect (true) all channels' N inputs to SRB1. This effects all pins, and disconnects all N inputs from the ADC. | 440 | 441 | 442 | 443 | ### wifi.impedanceSet(channelNumber, pInputApplied, nInputApplied) ⇒ Promise 444 | To send an impedance setting command to the board 445 | 446 | **Kind**: instance method of [Wifi](#Wifi) 447 | **Returns**: Promise - resolves if sent, rejects on bad input or no board 448 | **Author**: AJ Keller (@aj-ptw) 449 | 450 | | Param | Type | Description | 451 | | --- | --- | --- | 452 | | channelNumber | Number | (1-16) | 453 | | pInputApplied | Boolean | (true -> ON, false -> OFF (default)) | 454 | | nInputApplied | Boolean | (true -> ON, false -> OFF (default)) | 455 | 456 | 457 | 458 | ### wifi.connect(o) ⇒ Promise 459 | The essential precursor method to be called initially to establish a 460 | ble connection to the OpenBCI ganglion board. 461 | 462 | **Kind**: instance method of [Wifi](#Wifi) 463 | **Returns**: Promise - If the board was able to connect. 464 | **Author**: AJ Keller (@aj-ptw) 465 | 466 | | Param | Type | Description | 467 | | --- | --- | --- | 468 | | o | Object | | 469 | | o.burst | Boolean | Set this option true to have UDP do burst mode 3x | 470 | | o.examineMode | Boolean | Set this option true to connect to the WiFi Shield even if there is no board attached. | 471 | | o.ipAddress | String | The ip address of the shield if you know it | 472 | | o.latency | Number | If you want to set the latency of the system you can here too. | 473 | | o.protocol | String | Either send the data over TCP or UDP. UDP seems better for either a bad router or slow router. Default is TCP | 474 | | o.sampleRate | | The sample rate to set the board connected to the wifi shield | 475 | | o.shieldName | String | If supplied, will search for a shield by this name, if not supplied, will connect to the first shield found. | 476 | | o.streamStart | Boolean | Set `true` if you want the board to start streaming. | 477 | 478 | 479 | 480 | ### wifi.disconnect() ⇒ Promise 481 | Closes the connection to the board. Waits for stop streaming command to 482 | be sent if currently streaming. 483 | 484 | **Kind**: instance method of [Wifi](#Wifi) 485 | **Returns**: Promise - - fulfilled by a successful close, rejected otherwise. 486 | **Author**: AJ Keller (@aj-ptw) 487 | 488 | 489 | ### wifi.isConnected() ⇒ boolean 490 | Checks if the driver is connected to a board. 491 | 492 | **Kind**: instance method of [Wifi](#Wifi) 493 | **Returns**: boolean - - True if connected. 494 | **Author**: AJ Keller (@aj-ptw) 495 | 496 | 497 | ### wifi.isSearching() ⇒ boolean 498 | Checks if noble is currently scanning. 499 | 500 | **Kind**: instance method of [Wifi](#Wifi) 501 | **Returns**: boolean - - True if streaming. 502 | **Author**: AJ Keller (@aj-ptw) 503 | 504 | 505 | ### wifi.isStreaming() ⇒ boolean 506 | Checks if the board is currently sending samples. 507 | 508 | **Kind**: instance method of [Wifi](#Wifi) 509 | **Returns**: boolean - - True if streaming. 510 | **Author**: AJ Keller (@aj-ptw) 511 | 512 | 513 | ### wifi.getBoardType() ⇒ \* 514 | Get the current board type 515 | 516 | **Kind**: instance method of [Wifi](#Wifi) 517 | **Author**: AJ Keller (@aj-ptw) 518 | 519 | 520 | ### wifi.getFirmwareVersion() ⇒ String 521 | Get the firmware version of connected and synced wifi shield. 522 | 523 | **Kind**: instance method of [Wifi](#Wifi) 524 | **Returns**: String - The version number 525 | Note: This is dependent on if you called connect 526 | **Author**: AJ Keller (@aj-ptw) 527 | 528 | 529 | ### wifi.getIpAddress() ⇒ null \| String 530 | Return the ip address of the attached WiFi Shield device. 531 | 532 | **Kind**: instance method of [Wifi](#Wifi) 533 | **Author**: AJ Keller (@aj-ptw) 534 | 535 | 536 | ### wifi.getLatency() ⇒ Number 537 | Return the latency to be set on the WiFi Shield. 538 | 539 | **Kind**: instance method of [Wifi](#Wifi) 540 | **Author**: AJ Keller (@aj-ptw) 541 | 542 | 543 | ### wifi.getMacAddress() ⇒ null \| String 544 | Return the MAC address of the attached WiFi Shield device. 545 | 546 | **Kind**: instance method of [Wifi](#Wifi) 547 | **Author**: AJ Keller (@aj-ptw) 548 | 549 | 550 | ### wifi.getNumberOfChannels() ⇒ Number 551 | This function is used as a convenience method to determine how many 552 | channels the current board is using. 553 | Note: This is dependent on if your wifi shield is attached to another board and how many channels are there. 554 | 555 | **Kind**: instance method of [Wifi](#Wifi) 556 | **Returns**: Number - A number 557 | **Author**: AJ Keller (@aj-ptw) 558 | 559 | 560 | ### wifi.getSampleRate() ⇒ Number 561 | Get the the current sample rate is. 562 | Note: This is dependent on if you configured the board correctly on setup options 563 | 564 | **Kind**: instance method of [Wifi](#Wifi) 565 | **Returns**: Number - The sample rate 566 | **Author**: AJ Keller (@aj-ptw) 567 | 568 | 569 | ### wifi.getShieldName() ⇒ null \| String 570 | Return the shield name of the attached WiFi Shield device. 571 | 572 | **Kind**: instance method of [Wifi](#Wifi) 573 | **Author**: AJ Keller (@aj-ptw) 574 | 575 | 576 | ### wifi.impedanceStart() ⇒ global.Promise \| Promise 577 | Call to start testing impedance. 578 | 579 | **Kind**: instance method of [Wifi](#Wifi) 580 | **Author**: AJ Keller (@aj-ptw) 581 | 582 | 583 | ### wifi.impedanceStop() ⇒ global.Promise \| Promise 584 | Call to stop testing impedance. 585 | 586 | **Kind**: instance method of [Wifi](#Wifi) 587 | **Author**: AJ Keller (@aj-ptw) 588 | 589 | 590 | ### wifi.searchToStream(o) ⇒ Promise 591 | Used to search for an OpenBCI WiFi Shield. Will connect to the first one if no `shieldName` is supplied. 592 | 593 | **Kind**: instance method of [Wifi](#Wifi) 594 | **Returns**: Promise - - Resolves after successful connection, rejects otherwise with Error. 595 | **Author**: AJ Keller (@aj-ptw) 596 | 597 | | Param | Type | Description | 598 | | --- | --- | --- | 599 | | o | Object | (optional) | 600 | | o.sampleRate | | The sample rate to set the board connected to the wifi shield | 601 | | o.shieldName | String | If supplied, will search for a shield by this name, if not supplied, will connect to the first shield found. | 602 | | o.streamStart | Boolean | Set `true` if you want the board to start streaming. | 603 | | o.timeout | Number | The time in milli seconds to wait for the system to try and auto find and connect to the shield. | 604 | 605 | 606 | 607 | ### wifi.setSampleRate(sampleRate) ⇒ Promise 608 | Set the sample rate of the remote OpenBCI shield 609 | 610 | **Kind**: instance method of [Wifi](#Wifi) 611 | **Author**: AJ Keller (@aj-ptw) 612 | 613 | | Param | Type | Description | 614 | | --- | --- | --- | 615 | | sampleRate | Number | the sample rate you want to set to. | 616 | 617 | 618 | 619 | ### wifi.syncSampleRate() ⇒ Promise 620 | Returns the sample rate from the board 621 | 622 | **Kind**: instance method of [Wifi](#Wifi) 623 | **Author**: AJ Keller (@aj-ptw) 624 | 625 | 626 | ### wifi.searchStart() ⇒ Promise 627 | List available peripherals so the user can choose a device when not 628 | automatically found. 629 | 630 | **Kind**: instance method of [Wifi](#Wifi) 631 | **Returns**: Promise - - If scan was started 632 | **Author**: AJ Keller (@aj-ptw) 633 | 634 | 635 | ### wifi.searchStop() ⇒ global.Promise \| Promise 636 | Called to end a search. 637 | 638 | **Kind**: instance method of [Wifi](#Wifi) 639 | **Author**: AJ Keller (@aj-ptw) 640 | 641 | 642 | ### wifi.sdStop() ⇒ Promise 643 | Sends the stop SD logging command to the board. If not streaming then `eot` event will be emitted 644 | with request response from the board. 645 | 646 | **Kind**: instance method of [Wifi](#Wifi) 647 | **Returns**: Promise - - Resolves when written 648 | **Author**: AJ Keller (@aj-ptw) 649 | 650 | 651 | ### wifi.syncRegisterSettings() ⇒ Promise.<T> \| \* 652 | Syncs the internal channel settings object with a cyton, this will take about 653 | over a second because there are delays between the register reads in the firmware. 654 | 655 | **Kind**: instance method of [Wifi](#Wifi) 656 | **Returns**: Promise.<T> \| \* - Resolved once synced, rejects on error or 2 second timeout 657 | **Author**: AJ Keller (@aj-ptw) 658 | 659 | 660 | ### wifi.softReset() ⇒ Promise 661 | Sends a soft reset command to the board 662 | 663 | **Kind**: instance method of [Wifi](#Wifi) 664 | **Returns**: Promise - - Fulfilled if the command was sent to board. 665 | **Author**: AJ Keller (@aj-ptw) 666 | 667 | 668 | ### wifi.eraseWifiCredentials() ⇒ Promise 669 | Tells the WiFi Shield to forget it's network credentials. This will cause the board to drop all 670 | connections. 671 | 672 | **Kind**: instance method of [Wifi](#Wifi) 673 | **Returns**: Promise - Resolves when WiFi Shield has been reset and the module disconnects. 674 | **Author**: AJ Keller (@aj-ptw) 675 | 676 | 677 | ### wifi.streamStart() ⇒ Promise 678 | Sends a start streaming command to the board. 679 | 680 | **Kind**: instance method of [Wifi](#Wifi) 681 | **Returns**: Promise - indicating if the signal was able to be sent. 682 | Note: You must have successfully connected to an OpenBCI board using the connect 683 | method. Just because the signal was able to be sent to the board, does not 684 | mean the board will start streaming. 685 | **Author**: AJ Keller (@aj-ptw) 686 | 687 | 688 | ### wifi.streamStop() ⇒ Promise 689 | Sends a stop streaming command to the board. 690 | 691 | **Kind**: instance method of [Wifi](#Wifi) 692 | **Returns**: Promise - indicating if the signal was able to be sent. 693 | Note: You must have successfully connected to an OpenBCI board using the connect 694 | method. Just because the signal was able to be sent to the board, does not 695 | mean the board stopped streaming. 696 | **Author**: AJ Keller (@aj-ptw) 697 | 698 | 699 | ### wifi.syncInfo(o) ⇒ Promise.<TResult> 700 | Sync the info of this wifi module 701 | 702 | **Kind**: instance method of [Wifi](#Wifi) 703 | **Author**: AJ Keller (@aj-ptw) 704 | 705 | | Param | Type | Description | 706 | | --- | --- | --- | 707 | | o | Object | | 708 | | o.examineMode | Boolean | Set this option true to connect to the WiFi Shield even if there is no board attached. | 709 | 710 | 711 | 712 | ### wifi.write(data) ⇒ Promise 713 | Used to send data to the board. 714 | 715 | **Kind**: instance method of [Wifi](#Wifi) 716 | **Returns**: Promise - - fulfilled if command was able to be sent 717 | **Author**: AJ Keller (@aj-ptw) 718 | 719 | | Param | Type | Description | 720 | | --- | --- | --- | 721 | | data | Array \| Buffer \| Number \| String | The data to write out | 722 | 723 | 724 | 725 | ### wifi.destroy() 726 | Call this to shut down the servers. 727 | 728 | **Kind**: instance method of [Wifi](#Wifi) 729 | 730 | 731 | ### wifi.wifiGetLocalPort() ⇒ number 732 | Get the local port number of either the TCP or UDP server. Based on `options.protocol` being set to 733 | either `udp` or `tcp`. 734 | 735 | **Kind**: instance method of [Wifi](#Wifi) 736 | **Returns**: number - The port number that was dynamically assigned to this module on startup. 737 | 738 | 739 | ### wifi.wifiGetLocalPortUDP() ⇒ number 740 | Get the local port number of the UDP server. 741 | 742 | **Kind**: instance method of [Wifi](#Wifi) 743 | **Returns**: number - The port number that was dynamically assigned to this module on startup. 744 | 745 | 746 | ### wifi.wifiGetLocalPortTCP() ⇒ number 747 | Get the local port number of the UDP server. 748 | 749 | **Kind**: instance method of [Wifi](#Wifi) 750 | **Returns**: number - The port number that was dynamically assigned to this module on startup. 751 | 752 | 753 | ### wifi.wifiInitServer() 754 | Initialization function that will start the TCP server and bind the UDP port. 755 | 756 | **Kind**: instance method of [Wifi](#Wifi) 757 | 758 | 759 | ### wifi.delete(path) ⇒ Promise 760 | Send a delete message to the connected wifi shield. 761 | 762 | **Kind**: instance method of [Wifi](#Wifi) 763 | **Returns**: Promise - - Resolves if gets a response from the client/server, rejects with error 764 | 765 | | Param | Type | Description | 766 | | --- | --- | --- | 767 | | path | String | the path/route to send the delete message to | 768 | 769 | 770 | 771 | ### wifi.get(path) ⇒ Promise 772 | Send a GET message to the connected wifi shield. 773 | 774 | **Kind**: instance method of [Wifi](#Wifi) 775 | **Returns**: Promise - - Resolves if gets/with a response from the client/server, rejects with error 776 | 777 | | Param | Type | Description | 778 | | --- | --- | --- | 779 | | path | String | the path/route to send the GET message to | 780 | 781 | 782 | 783 | ### wifi.post(path, payload) ⇒ Promise 784 | Send a POST message to the connected wifi shield. 785 | 786 | **Kind**: instance method of [Wifi](#Wifi) 787 | **Returns**: Promise - - Resolves if gets a response from the client/server, rejects with error 788 | 789 | | Param | Type | Description | 790 | | --- | --- | --- | 791 | | path | String | the path/route to send the POST message to | 792 | | payload | \* | can really be anything but should be a JSON object. | 793 | 794 | 795 | 796 | ### Wifi~o 797 | Configuring Options 798 | 799 | **Kind**: inner property of [Wifi](#Wifi) 800 | 801 | 802 | ## InitializationObject : Object 803 | **Kind**: global typedef 804 | **Properties** 805 | 806 | | Name | Type | Description | 807 | | --- | --- | --- | 808 | | attempts | Number | The number of times to try and perform an SSDP search before quitting. (Default 10) | 809 | | burst | Boolean | Only applies for UDP, but the wifi shield will send 3 of the same packets on UDP to increase the chance packets arrive to this module (Default false) | 810 | | debug | Boolean | Print out a raw dump of bytes sent and received. (Default `false`) | 811 | | latency | Number | The latency, or amount of time between packet sends, of the WiFi shield. The time is in micro seconds! | 812 | | protocol | String | Either send the data over TCP or UDP. UDP seems better for either a bad router or slow router. Default is TCP | 813 | | sampleRate | Number | The sample rate to set the board to. (Default is zero) | 814 | | sendCounts | Boolean | Send integer raw counts instead of scaled floats. (Default `false`) | 815 | | verbose | Boolean | Print out useful debugging events. (Default `false`) | 816 | 817 | 818 | [link_aj_keller]: https://github.com/aj-ptw 819 | [link_shop_wifi_shield]: https://shop.openbci.com/collections/frontpage/products/wifi-shield?variant=44534009550 820 | [link_shop_ganglion]: https://shop.openbci.com/collections/frontpage/products/pre-order-ganglion-board 821 | [link_shop_cyton]: https://shop.openbci.com/collections/frontpage/products/cyton-biosensing-board-8-channel 822 | [link_shop_cyton_daisy]: https://shop.openbci.com/collections/frontpage/products/cyton-daisy-biosensing-boards-16-channel 823 | [link_ptw]: https://www.pushtheworldllc.com 824 | [link_openbci]: http://www.openbci.com 825 | [link_mozwow]: http://mozillascience.github.io/working-open-workshop/index.html 826 | [link_wifi_get_streaming]: examples/getStreaming/getStreaming.js 827 | [link_openleaderscohort]: https://medium.com/@MozOpenLeaders 828 | [link_mozsci]: https://science.mozilla.org 829 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | ## OpenBCI WiFi NodeJS 4 | 5 | Make programming with OpenBCI reliable, easy, research grade and fun! 6 | 7 | ## Short term - what we're working on now 8 | 9 | - Unit tests! 10 | 11 | ## Medium term 12 | 13 | - Make this module work in the browser 14 | - Get the simulator added for the wifi shield 15 | -------------------------------------------------------------------------------- /examples/getStreaming/getStreaming.js: -------------------------------------------------------------------------------- 1 | /* jslint es6 */ 2 | /** 3 | * This is an example from the readme.md 4 | * On windows you should run with PowerShell not git bash. 5 | * Install 6 | * [nodejs](https://nodejs.org/en/) 7 | * 8 | * To run: 9 | * change directory to this file `cd examples/getStreaming` 10 | * do `npm install` 11 | * then `npm start` 12 | */ 13 | 'use strict'; 14 | const OBCIConsts = require('@openbci/utilities').constants; 15 | const OBCIWifi = require('../../openBCIWifi'); 16 | 17 | const wifi = new OBCIWifi({ 18 | debug: false, // Pretty print bytes 19 | verbose: true, // Verbose output 20 | sendCounts: false, 21 | latency: 16667, 22 | protocol: 'tcp' // or "udp" 23 | }); 24 | 25 | let counter = 0; 26 | let sampleRateCounterInterval = null; 27 | let lastSampleNumber = 0; 28 | let MAX_SAMPLE_NUMBER = 255; 29 | let droppedPacketArray = []; 30 | let sampleRateArray = []; 31 | let droppedPackets = 0; 32 | 33 | const sum = (acc, cur) => acc + cur; 34 | 35 | const sampleFunc = (sample) => { 36 | try { 37 | // console.log(JSON.stringify(sample)); 38 | if (sample.valid) { 39 | counter++; 40 | if (sampleRateCounterInterval === null) { 41 | sampleRateCounterInterval = setInterval(() => { 42 | droppedPacketArray.push(droppedPackets); 43 | sampleRateArray.push(counter); 44 | 45 | const dpSum = droppedPacketArray.reduce(sum, 0); 46 | const srSum = sampleRateArray.reduce(sum, 0); 47 | 48 | console.log(`SR: ${counter}`); 49 | console.log(`Dropped ${droppedPackets} packets`); 50 | console.log(`Dropped packet average: ${dpSum / droppedPacketArray.length}`); 51 | console.log(`Sample rate average: ${srSum / sampleRateArray.length}`); 52 | 53 | droppedPackets = 0; 54 | counter = 0; 55 | }, 1000); 56 | } 57 | 58 | let packetDiff = sample.sampleNumber - lastSampleNumber; 59 | 60 | if (packetDiff < 0) packetDiff += MAX_SAMPLE_NUMBER; 61 | 62 | if (packetDiff > 1) { 63 | console.log(`dropped ${packetDiff} packets | cur sn: ${sample.sampleNumber} | last sn: ${lastSampleNumber}`); 64 | droppedPackets += packetDiff; 65 | } 66 | 67 | lastSampleNumber = sample.sampleNumber; 68 | // console.log(JSON.stringify(sample)); 69 | } 70 | } catch (err) { 71 | console.error(err); 72 | } 73 | }; 74 | 75 | wifi.on(OBCIConsts.OBCIEmitterImpedance, (impedance) => { 76 | console.log(JSON.stringify(impedance)); 77 | }); 78 | 79 | wifi.on(OBCIConsts.OBCIEmitterSample, sampleFunc); 80 | // wifi.on(OBCIConsts.OBCIEmitterRawDataPacket, console.log); 81 | 82 | wifi.searchToStream({ 83 | streamStart: true, 84 | sampleRate: 200 85 | }) 86 | .then(() => { 87 | MAX_SAMPLE_NUMBER = wifi.getNumberOfChannels() === 4 ? 200 : 255; 88 | }) 89 | .catch((err) => { 90 | console.error(err); 91 | process.exit(0); 92 | }); 93 | 94 | function exitHandler (options, err) { 95 | if (options.cleanup) { 96 | if (options.verbose) console.log('clean'); 97 | /** Do additional clean up here */ 98 | if (wifi.isConnected()) wifi.disconnect().catch(console.log); 99 | 100 | wifi.removeAllListeners('rawDataPacket'); 101 | wifi.removeAllListeners('sample'); 102 | wifi.destroy(); 103 | 104 | if (sampleRateCounterInterval) clearInterval(sampleRateCounterInterval); 105 | } 106 | 107 | if (err) console.error(err.stack); 108 | 109 | if (options.exit) { 110 | if (options.verbose) console.log('exit'); 111 | 112 | if (wifi.isStreaming()) { 113 | const _t = setTimeout(() => { 114 | console.log('timeout'); 115 | process.exit(0); 116 | }, 1000); 117 | 118 | wifi.streamStop() 119 | .then(() => { 120 | console.log('stream stopped'); 121 | if (_t) clearTimeout(_t); 122 | process.exit(0); 123 | }).catch((err) => { 124 | console.error(err); 125 | process.exit(0); 126 | }); 127 | } else { 128 | process.exit(0); 129 | } 130 | } 131 | } 132 | 133 | if (process.platform === 'win32') { 134 | const rl = require('readline').createInterface({ 135 | input: process.stdin, 136 | output: process.stdout 137 | }); 138 | 139 | rl.on('SIGINT', function () { 140 | process.emit('SIGINT'); 141 | }); 142 | } 143 | 144 | // Perform actions on exit 145 | process.on('exit', exitHandler.bind(null, { 146 | cleanup: true 147 | })); 148 | 149 | // Perform actions on SIGINT 150 | process.on('SIGINT', exitHandler.bind(null, { 151 | exit: true 152 | })); 153 | 154 | // Perform actions on uncaught exceptions 155 | process.on('uncaughtException', exitHandler.bind(null, { 156 | exit: true 157 | })); 158 | -------------------------------------------------------------------------------- /examples/getStreamingWiFiDirect/getStreamingWiFiDirect.js: -------------------------------------------------------------------------------- 1 | /* jslint es6 */ 2 | /** 3 | * This is an example from the readme.md 4 | * On windows you should run with PowerShell not git bash. 5 | * Install 6 | * [nodejs](https://nodejs.org/en/) 7 | * 8 | * To run: 9 | * change directory to this file `cd examples/debug` 10 | * do `npm install` 11 | * then `npm start` 12 | */ 13 | 'use strict'; 14 | const OpenBCIConsts = require('@openbci/utilities').constants; 15 | const OpenBCIWifi = require('../../openBCIWifi'); 16 | 17 | const deviceAddr = '10.0.1.3'; 18 | 19 | const wifi = new OpenBCIWifi({ 20 | debug: false, // Pretty print bytes 21 | verbose: false, // Verbose output 22 | sendCounts: false, 23 | latency: 16667, 24 | protocol: 'tcp', // or "udp" 25 | burst: true 26 | }); 27 | 28 | let counter = 0; 29 | let sampleRateCounterInterval = null; 30 | let lastSampleNumber = 0; 31 | let MAX_SAMPLE_NUMBER = 255; 32 | let droppedPacketArray = []; 33 | let sampleRateArray = []; 34 | let droppedPackets = 0; 35 | 36 | const sum = (acc, cur) => acc + cur; 37 | 38 | const sampleFunc = (sample) => { 39 | try { 40 | // console.log(JSON.stringify(sample)); 41 | if (sample.valid) { 42 | counter++; 43 | if (sampleRateCounterInterval === null) { 44 | sampleRateCounterInterval = setInterval(() => { 45 | droppedPacketArray.push(droppedPackets); 46 | sampleRateArray.push(counter); 47 | 48 | const dpSum = droppedPacketArray.reduce(sum, 0); 49 | const srSum = sampleRateArray.reduce(sum, 0); 50 | 51 | console.log(`SR: ${counter}`); 52 | console.log(`Dropped ${droppedPackets} packets`); 53 | console.log(`Dropped packet average: ${dpSum / droppedPacketArray.length}`); 54 | console.log(`Sample rate average: ${srSum / sampleRateArray.length}`); 55 | 56 | droppedPackets = 0; 57 | counter = 0; 58 | }, 1000); 59 | } 60 | 61 | let packetDiff = sample.sampleNumber - lastSampleNumber; 62 | 63 | if (packetDiff < 0) packetDiff += MAX_SAMPLE_NUMBER; 64 | 65 | if (packetDiff > 1) { 66 | console.log(`dropped ${packetDiff} packets | cur sn: ${sample.sampleNumber} | last sn: ${lastSampleNumber}`); 67 | droppedPackets += packetDiff; 68 | } 69 | 70 | lastSampleNumber = sample.sampleNumber; 71 | // console.log(JSON.stringify(sample)); 72 | } 73 | } catch (err) { 74 | console.log(err); 75 | } 76 | }; 77 | 78 | wifi.on(OpenBCIConsts.OBCIEmitterImpedance, (impedance) => { 79 | console.log(JSON.stringify(impedance)); 80 | }); 81 | 82 | wifi.on(OpenBCIConsts.OBCIEmitterSample, sampleFunc); 83 | // wifi.on(OpenBCIConsts.OBCIEmitterRawDataPacket, console.log); 84 | 85 | wifi.connect({ 86 | sampleRate: 200, 87 | streamStart: true, 88 | ipAddress: deviceAddr 89 | }) 90 | .then(() => { 91 | MAX_SAMPLE_NUMBER = wifi.getNumberOfChannels() === 4 ? 200 : 255; 92 | }) 93 | .catch((err) => { 94 | console.log(err); 95 | process.exit(0); 96 | }); 97 | 98 | function exitHandler (options, err) { 99 | if (options.cleanup) { 100 | if (options.verbose) console.log('clean'); 101 | /** Do additional clean up here */ 102 | if (wifi.isConnected()) wifi.disconnect().catch(console.log); 103 | 104 | wifi.removeAllListeners('rawDataPacket'); 105 | wifi.removeAllListeners('sample'); 106 | wifi.destroy(); 107 | 108 | if (sampleRateCounterInterval) clearInterval(sampleRateCounterInterval); 109 | } 110 | 111 | if (err) console.log(err.stack); 112 | 113 | if (options.exit) { 114 | if (options.verbose) console.log('exit'); 115 | 116 | if (wifi.isStreaming()) { 117 | const _t = setTimeout(() => { 118 | console.log('timeout'); 119 | process.exit(0); 120 | }, 1000); 121 | 122 | wifi.streamStop() 123 | .then(() => { 124 | console.log('stream stopped'); 125 | if (_t) clearTimeout(_t); 126 | process.exit(0); 127 | }).catch((err) => { 128 | console.log(err); 129 | process.exit(0); 130 | }); 131 | } else { 132 | process.exit(0); 133 | } 134 | } 135 | } 136 | 137 | if (process.platform === 'win32') { 138 | const rl = require('readline').createInterface({ 139 | input: process.stdin, 140 | output: process.stdout 141 | }); 142 | 143 | rl.on('SIGINT', function () { 144 | process.emit('SIGINT'); 145 | }); 146 | } 147 | 148 | // do something when app is closing 149 | process.on('exit', exitHandler.bind(null, { 150 | cleanup: true 151 | })); 152 | 153 | // catches ctrl+c event 154 | process.on('SIGINT', exitHandler.bind(null, { 155 | exit: true 156 | })); 157 | 158 | // catches uncaught exceptions 159 | process.on('uncaughtException', exitHandler.bind(null, { 160 | exit: true 161 | })); 162 | -------------------------------------------------------------------------------- /examples/xstreamCSV/xstreamCSV.js: -------------------------------------------------------------------------------- 1 | /* jslint es6 */ 2 | 'use strict'; 3 | const fs = require('fs'); 4 | const OBCIConst = require('@openbci/utilities').constants; 5 | const Wifi = require('../../openBCIWifi'); 6 | const xs = require('xstream').Stream; 7 | 8 | // GLOBALS 9 | const deviceAddr = '10.0.1.3'; 10 | let f_ = null; // WriteStream, assigned by wifi.connect({...}) 11 | 12 | const wifi = new Wifi({ 13 | debug: false, 14 | verbose: false, 15 | sendCounts: false, 16 | latency: 20000, 17 | protocol: 'tcp' 18 | }); 19 | 20 | // PRODUCERS 21 | const OBCIDataStream = { 22 | start: (listener) => { 23 | wifi.on(OBCIConst.OBCIEmitterSample, (s) => { 24 | listener.next(s); 25 | }); 26 | }, 27 | stop: () => { 28 | console.log('OBCIDataStream stop'); 29 | } 30 | }; 31 | const data_ = xs.create(OBCIDataStream); 32 | 33 | // LISTENERS 34 | const CSVWriter = { 35 | next: (v) => { 36 | const row = [v.sampleNumber, v.timestamp, ...v.channelData, ...v.accelData, v.valid].join(','); 37 | f_.write(row + '\n'); 38 | }, 39 | error: (err) => console.error(err), 40 | complete: () => console.log('CSVWriter complete') 41 | }; 42 | 43 | const StreamPrinter = { 44 | next: (v) => console.log(v), 45 | error: (e) => console.error(e), 46 | complete: () => console.log('StreamPrinter complete') 47 | }; 48 | 49 | // APPLICATION CODE 50 | wifi.connect({ 51 | ipAddress: deviceAddr, 52 | sampleRate: 200, 53 | streamStart: false 54 | }).then(() => { 55 | f_ = fs.createWriteStream(`${Date.now()}-${wifi.getShieldName()}-${wifi.getSampleRate()}hz.csv`); 56 | f_.write('sample,t,ch1,ch2,ch3,ch4,x,y,z,valid\n'); 57 | wifi.streamStart().then(() => { 58 | data_.addListener(CSVWriter); 59 | data_.addListener(StreamPrinter); 60 | }); 61 | }); 62 | 63 | // HANDLERS 64 | const exitHandler = (opt, err) => { 65 | if (opt.cleanup) { 66 | console.log('Disconnect'); 67 | if (wifi.isConnected()) wifi.disconnect().catch(console.log); 68 | wifi.removeAllListeners('rawDataPacket'); 69 | wifi.removeAllListeners('sample'); 70 | wifi.destroy(); 71 | } 72 | 73 | if (err) console.error(err); 74 | 75 | if (wifi.isStreaming()) { 76 | const tOut = setTimeout(() => { 77 | console.log('Stream timed out'); 78 | process.exit(0); 79 | }, 1000); 80 | 81 | wifi.streamStop() 82 | .then(() => { 83 | console.log('WiFi stream stop'); 84 | if (tOut) clearTimeout(tOut); 85 | OBCIDataStream.stop(); 86 | f_.end(); 87 | process.exit(0); 88 | }) 89 | .catch((err) => { 90 | console.error(err); 91 | process.exit(0); 92 | }); 93 | } else { 94 | process.exit(0); 95 | } 96 | }; 97 | 98 | process.on('exit', exitHandler.bind(null, { cleanup: true })); 99 | process.on('SIGINT', exitHandler.bind(null, { exit: true })); 100 | process.on('uncaughtException', exitHandler.bind(null, { exit: true })); 101 | -------------------------------------------------------------------------------- /images/WiFi_front_product.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openbci-archive/OpenBCI_NodeJS_Wifi/d6cc78d2223e1e9a3578ab5e126f556ba1e7b455/images/WiFi_front_product.jpg -------------------------------------------------------------------------------- /openBCIWifi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EventEmitter = require('events').EventEmitter; 3 | const _ = require('lodash'); 4 | const util = require('util'); 5 | // Local imports 6 | const OpenBCIUtilities = require('@openbci/utilities'); 7 | const obciUtils = OpenBCIUtilities.utilities; 8 | const k = OpenBCIUtilities.constants; 9 | const obciDebug = OpenBCIUtilities.debug; 10 | const clone = require('clone'); 11 | const os = require('os'); 12 | const ip = require('ip'); 13 | const Client = require('node-ssdp').Client; 14 | const net = require('net'); 15 | const http = require('http'); 16 | const bufferEqual = require('buffer-equal'); 17 | const Buffer = require('safe-buffer').Buffer; 18 | const dgram = require('dgram'); 19 | const isWin = process.platform === 'win32'; 20 | 21 | const wifiOutputModeRaw = 'raw'; 22 | const wifiOutputProtocolUDP = 'udp'; 23 | const wifiOutputProtocolTCP = 'tcp'; 24 | /** 25 | * Options object 26 | * @type {InitializationObject} 27 | * @private 28 | */ 29 | const _options = { 30 | attempts: 10, 31 | burst: false, 32 | debug: false, 33 | latency: 10000, 34 | protocol: [wifiOutputProtocolTCP, wifiOutputProtocolUDP], 35 | sampleRate: 0, 36 | sendCounts: false, 37 | verbose: false 38 | }; 39 | 40 | /** 41 | * @typedef {Object} InitializationObject 42 | * @property {Number} attempts - The number of times to try and perform an SSDP search before quitting. (Default 10) 43 | * 44 | * @property {Boolean} burst - Only applies for UDP, but the wifi shield will send 3 of the same packets on UDP to 45 | * increase the chance packets arrive to this module (Default false) 46 | * 47 | * @property {Boolean} debug - Print out a raw dump of bytes sent and received. (Default `false`) 48 | * 49 | * @property {Number} latency - The latency, or amount of time between packet sends, of the WiFi shield. The time is in 50 | * micro seconds! 51 | * 52 | * @property {String} protocol - Either send the data over TCP or UDP. UDP seems better for either a bad router or slow 53 | * router. Default is TCP 54 | * 55 | * @property {Number} sampleRate - The sample rate to set the board to. (Default is zero) 56 | * 57 | * @property {Boolean} sendCounts - Send integer raw counts instead of scaled floats. 58 | * (Default `false`) 59 | * 60 | * @property {Boolean} verbose - Print out useful debugging events. (Default `false`) 61 | * 62 | */ 63 | 64 | /** 65 | * @description The initialization method to call first, before any other method. 66 | * @param options {InitializationObject} (optional) - Board optional configurations. 67 | * @constructor 68 | * @author AJ Keller (@aj-ptw) 69 | */ 70 | function Wifi (options) { 71 | if (!(this instanceof Wifi)) { 72 | return new Wifi(options); 73 | } 74 | 75 | options = ((typeof options !== 'function') && options) || {}; 76 | let opts = {}; 77 | 78 | /** Configuring Options */ 79 | let o; 80 | for (o in _options) { 81 | var userOption = (o in options) ? o : o.toLowerCase(); 82 | var userValue = options[userOption]; 83 | delete options[userOption]; 84 | 85 | if (typeof _options[o] === 'object') { 86 | // an array specifying a list of choices 87 | // if the choice is not in the list, the first one is defaulted to 88 | 89 | if (_options[o].indexOf(userValue) !== -1) { 90 | opts[o] = userValue; 91 | } else { 92 | opts[o] = _options[o][0]; 93 | } 94 | } else { 95 | // anything else takes the user value if provided, otherwise is a default 96 | 97 | if (userValue !== undefined) { 98 | opts[o] = userValue; 99 | } else { 100 | opts[o] = _options[o]; 101 | } 102 | } 103 | } 104 | 105 | for (o in options) throw new Error('"' + o + '" is not a valid option'); 106 | 107 | /** 108 | * @type {InitializationObject} 109 | */ 110 | this.options = clone(opts); 111 | 112 | /** 113 | * @type {RawDataToSample} 114 | * @private 115 | */ 116 | this._rawDataPacketToSample = k.rawDataToSampleObjectDefault(8); 117 | this._rawDataPacketToSample.scale = !this.options.sendCounts; 118 | this._rawDataPacketToSample.protocol = k.OBCIProtocolWifi; 119 | this._rawDataPacketToSample.verbose = this.options.verbose; 120 | 121 | /** Private Properties (keep alphabetical) */ 122 | 123 | this._accelArray = [0, 0, 0]; 124 | this._allInfo = null; 125 | this._boardConnected = false; 126 | this._boardInfo = null; 127 | this._boardType = k.OBCIBoardNone; 128 | this._connected = false; 129 | this._droppedPacketCounter = 0; 130 | this._firstPacket = true; 131 | this._ipAddress = null; 132 | this._info = null; 133 | this._latency = this.options.latency; 134 | this._lowerChannelsSampleObject = null; 135 | this._macAddress = null; 136 | this._multiPacketBuffer = null; 137 | this._numberOfChannels = 0; 138 | this._packetCounter = 0; 139 | this._peripheral = null; 140 | this._sampleRate = this.options.sampleRate; 141 | this._scanning = false; 142 | this._shieldName = null; 143 | this._shieldName = null; 144 | this._streaming = false; 145 | this._version = null; 146 | this._lastPacketArrival = 0; 147 | 148 | /** Public Properties (keep alphabetical) */ 149 | 150 | this.curOutputMode = wifiOutputModeRaw; 151 | this.internalRawDataPackets = []; 152 | this.wifiShieldArray = []; 153 | this.wifiServerUDPPort = 0; 154 | 155 | /** Initializations */ 156 | 157 | this.wifiInitServer(); 158 | } 159 | 160 | // This allows us to use the emitter class freely outside of the module 161 | util.inherits(Wifi, EventEmitter); 162 | 163 | /** 164 | * This function is for redundancy after a long packet send, the wifi firmware can resend the same 165 | * packet again, using this till add redundancy on poor networks. 166 | * @param rawDataPackets - 167 | * @returns {Array} 168 | * @author AJ Keller (@aj-ptw) 169 | */ 170 | Wifi.prototype.bufferRawDataPackets = function (rawDataPackets) { 171 | if (this.internalRawDataPackets.length === 0) { 172 | this.internalRawDataPackets = rawDataPackets; 173 | return []; 174 | } else { 175 | let out = []; 176 | let coldStorage = []; 177 | _.forEach(rawDataPackets, (newRDP) => { 178 | let found = false; 179 | _.forEach(this.internalRawDataPackets, (oldRDP) => { 180 | if (!found && bufferEqual(new Buffer(newRDP), new Buffer(oldRDP))) { 181 | out.push(oldRDP); 182 | found = true; 183 | } 184 | }); 185 | if (!found) { 186 | coldStorage.push(newRDP); 187 | } 188 | }); 189 | if (out.length === 0) { 190 | out = this.internalRawDataPackets; 191 | this.internalRawDataPackets = rawDataPackets; 192 | } else { 193 | this.internalRawDataPackets = coldStorage; 194 | } 195 | return out; 196 | } 197 | }; 198 | 199 | /** 200 | * @description Send a command to the board to turn a specified channel off 201 | * @param channelNumber 202 | * @returns {Promise.} 203 | * @author AJ Keller (@aj-ptw) 204 | */ 205 | Wifi.prototype.channelOff = function (channelNumber) { 206 | return k.commandChannelOff(channelNumber).then((charCommand) => { 207 | // console.log('sent command to turn channel ' + channelNumber + ' by sending command ' + charCommand) 208 | return this.write(charCommand); 209 | }); 210 | }; 211 | 212 | /** 213 | * @description Send a command to the board to turn a specified channel on 214 | * @param channelNumber 215 | * @returns {Promise.|*} 216 | * @author AJ Keller (@aj-ptw) 217 | */ 218 | Wifi.prototype.channelOn = function (channelNumber) { 219 | return k.commandChannelOn(channelNumber).then((charCommand) => { 220 | // console.log('sent command to turn channel ' + channelNumber + ' by sending command ' + charCommand) 221 | return this.write(charCommand); 222 | }); 223 | }; 224 | 225 | /** 226 | * @description To send a channel setting command to the board 227 | * @param channelNumber - Number (1-16) 228 | * @param powerDown - Bool (true -> OFF, false -> ON (default)) 229 | * turns the channel on or off 230 | * @param gain - Number (1,2,4,6,8,12,24(default)) 231 | * sets the gain for the channel 232 | * @param inputType - String (normal,shorted,biasMethod,mvdd,temp,testsig,biasDrp,biasDrn) 233 | * selects the ADC channel input source 234 | * @param bias - Bool (true -> Include in bias (default), false -> remove from bias) 235 | * selects to include the channel input in bias generation 236 | * @param srb2 - Bool (true -> Connect this input to SRB2 (default), 237 | * false -> Disconnect this input from SRB2) 238 | * Select to connect (true) this channel's P input to the SRB2 pin. This closes 239 | * a switch between P input and SRB2 for the given channel, and allows the 240 | * P input to also remain connected to the ADC. 241 | * @param srb1 - Bool (true -> connect all N inputs to SRB1, 242 | * false -> Disconnect all N inputs from SRB1 (default)) 243 | * Select to connect (true) all channels' N inputs to SRB1. This effects all pins, 244 | * and disconnects all N inputs from the ADC. 245 | * @returns {Promise} resolves if sent, rejects on bad input or no board 246 | * @author AJ Keller (@aj-ptw) 247 | */ 248 | Wifi.prototype.channelSet = function (channelNumber, powerDown, gain, inputType, bias, srb2, srb1) { 249 | let arrayOfCommands = []; 250 | return new Promise((resolve, reject) => { 251 | k.getChannelSetter(channelNumber, powerDown, gain, inputType, bias, srb2, srb1) 252 | .then((val) => { 253 | arrayOfCommands = val.commandArray; 254 | this._rawDataPacketToSample.channelSettings[channelNumber - 1] = val.newChannelSettingsObject; 255 | return this.write(arrayOfCommands.join('')); 256 | }).then(resolve, reject); 257 | }); 258 | }; 259 | 260 | /** 261 | * @description To send an impedance setting command to the board 262 | * @param channelNumber {Number} (1-16) 263 | * @param pInputApplied {Boolean} (true -> ON, false -> OFF (default)) 264 | * @param nInputApplied {Boolean} (true -> ON, false -> OFF (default)) 265 | * @returns {Promise} resolves if sent, rejects on bad input or no board 266 | * @author AJ Keller (@aj-ptw) 267 | */ 268 | Wifi.prototype.impedanceSet = function (channelNumber, pInputApplied, nInputApplied) { 269 | return new Promise((resolve, reject) => { 270 | k.getImpedanceSetter(channelNumber, pInputApplied, nInputApplied) 271 | .then((val) => { 272 | return this.write(val.join('')); 273 | }).then(resolve, reject); 274 | }); 275 | }; 276 | 277 | /** 278 | * @description The essential precursor method to be called initially to establish a 279 | * ble connection to the OpenBCI ganglion board. 280 | * @param o {Object} 281 | * @param o.burst {Boolean} - Set this option true to have UDP do burst mode 3x 282 | * @param o.examineMode {Boolean} - Set this option true to connect to the WiFi Shield even if there is no board attached. 283 | * @param o.ipAddress {String} - The ip address of the shield if you know it 284 | * @param o.latency {Number} - If you want to set the latency of the system you can here too. 285 | * @param o.protocol {String} - Either send the data over TCP or UDP. UDP seems better for either a bad router or slow 286 | * router. Default is TCP 287 | * @param o.sampleRate - The sample rate to set the board connected to the wifi shield 288 | * @param o.shieldName {String} - If supplied, will search for a shield by this name, if not supplied, will connect to 289 | * the first shield found. 290 | * @param o.streamStart {Boolean} - Set `true` if you want the board to start streaming. 291 | * @returns {Promise} If the board was able to connect. 292 | * @author AJ Keller (@aj-ptw) 293 | */ 294 | Wifi.prototype.connect = function (o) { 295 | return new Promise((resolve, reject) => { 296 | let ipAddress = ''; 297 | if (o.hasOwnProperty('ipAddress')) { 298 | ipAddress = o.ipAddress; 299 | } else if (o.hasOwnProperty('shieldName')) { 300 | _.forEach(this.wifiShieldArray, (shield) => { 301 | if (shield.localName === o.shieldName) { 302 | ipAddress = shield.ipAddress; 303 | } 304 | }); 305 | } 306 | if (o.hasOwnProperty('latency')) { 307 | this._latency = o.latency; 308 | } 309 | if (o.hasOwnProperty('burst')) { 310 | this.options.burst = o.burst; 311 | } 312 | if (o.hasOwnProperty('protocol')) { 313 | this.options.protocol = o.protocol; 314 | } 315 | this._ipAddress = ipAddress; 316 | if (this.options.verbose) console.log(`Attempting to connect to ${this._ipAddress}`); 317 | this._connectSocket() 318 | .then(() => { 319 | if (this.options.verbose) console.log(`Connected to ${this._ipAddress}`); 320 | this._connected = true; 321 | return this.syncInfo(o); 322 | }) 323 | .then(() => { 324 | if (this.options.verbose) console.log(`Synced info with ${this._shieldName}`); 325 | if (o.hasOwnProperty('sampleRate')) { 326 | if (this.options.verbose) console.log(`Attempting to set sample rate to ${o.sampleRate}`); 327 | return this.setSampleRate(o.sampleRate); 328 | } 329 | if (o.hasOwnProperty('examineMode')) { 330 | if (o.examineMode) return Promise.resolve(0); 331 | } 332 | return this.syncSampleRate(); 333 | }) 334 | .then((sampleRate) => { 335 | if (this.options.verbose) console.log(`Sample rate is ${sampleRate}`); 336 | this._sampleRate = sampleRate; 337 | if (o.hasOwnProperty('streamStart')) { 338 | if (o.streamStart) { 339 | if (this.options.verbose) console.log('Attempting to start stream'); 340 | return this.streamStart(); 341 | } 342 | } 343 | return Promise.resolve(); 344 | }) 345 | .then(() => { 346 | resolve(); 347 | }) 348 | .catch((err) => { 349 | this._ipAddress = null; 350 | this._shieldName = null; 351 | this._connected = false; 352 | reject(err); 353 | }); 354 | }); 355 | }; 356 | 357 | /** 358 | * @description Closes the connection to the board. Waits for stop streaming command to 359 | * be sent if currently streaming. 360 | * @returns {Promise} - fulfilled by a successful close, rejected otherwise. 361 | * @author AJ Keller (@aj-ptw) 362 | */ 363 | Wifi.prototype.disconnect = function () { 364 | this._disconnected(); 365 | return Promise.resolve(); 366 | }; 367 | 368 | /** 369 | * @description Checks if the driver is connected to a board. 370 | * @returns {boolean} - True if connected. 371 | * @author AJ Keller (@aj-ptw) 372 | */ 373 | Wifi.prototype.isConnected = function () { 374 | return this._connected; 375 | }; 376 | 377 | /** 378 | * @description Checks if noble is currently scanning. 379 | * @returns {boolean} - True if streaming. 380 | * @author AJ Keller (@aj-ptw) 381 | */ 382 | Wifi.prototype.isSearching = function () { 383 | return this._scanning; 384 | }; 385 | 386 | /** 387 | * @description Checks if the board is currently sending samples. 388 | * @returns {boolean} - True if streaming. 389 | * @author AJ Keller (@aj-ptw) 390 | */ 391 | Wifi.prototype.isStreaming = function () { 392 | return this._streaming; 393 | }; 394 | 395 | /** 396 | * @description Get the current board type 397 | * @returns {*} 398 | * @author AJ Keller (@aj-ptw) 399 | */ 400 | Wifi.prototype.getBoardType = function () { 401 | return this._boardType; 402 | }; 403 | 404 | /** 405 | * @description Get the firmware version of connected and synced wifi shield. 406 | * @returns {String} The version number 407 | * Note: This is dependent on if you called connect 408 | * @author AJ Keller (@aj-ptw) 409 | */ 410 | Wifi.prototype.getFirmwareVersion = function () { 411 | return this._version; 412 | }; 413 | 414 | /** 415 | * Return the ip address of the host machine. 416 | * @return {null|String} 417 | * @author Richard Waltman (@retiutut) 418 | */ 419 | Wifi.prototype.getLocalIPAddress = function () { 420 | if (isWin) { 421 | if (this.options.verbose) console.log(os.networkInterfaces()); 422 | if (os.networkInterfaces().hasOwnProperty('Ethernet')) { 423 | return ip.address('Ethernet', 'ipv4'); 424 | } else if (os.networkInterfaces().hasOwnProperty('Wi-Fi')) { 425 | return ip.address('Wi-Fi', 'ipv4'); 426 | } else { 427 | return ip.address(); 428 | } 429 | } else { 430 | return ip.address(); 431 | } 432 | }; 433 | 434 | /** 435 | * Return the ip address of the attached WiFi Shield device. 436 | * @return {null|String} 437 | * @author AJ Keller (@aj-ptw) 438 | */ 439 | Wifi.prototype.getIpAddress = function () { 440 | return this._ipAddress; 441 | }; 442 | 443 | /** 444 | * Return the latency to be set on the WiFi Shield. 445 | * @return {Number} 446 | * @author AJ Keller (@aj-ptw) 447 | */ 448 | Wifi.prototype.getLatency = function () { 449 | return this._latency; 450 | }; 451 | 452 | /** 453 | * Return the MAC address of the attached WiFi Shield device. 454 | * @return {null|String} 455 | * @author AJ Keller (@aj-ptw) 456 | */ 457 | Wifi.prototype.getMacAddress = function () { 458 | return this._macAddress; 459 | }; 460 | 461 | /** 462 | * @description This function is used as a convenience method to determine how many 463 | * channels the current board is using. 464 | * Note: This is dependent on if your wifi shield is attached to another board and how many channels are there. 465 | * @returns {Number} A number 466 | * @author AJ Keller (@aj-ptw) 467 | */ 468 | Wifi.prototype.getNumberOfChannels = function () { 469 | return this._numberOfChannels; 470 | }; 471 | 472 | /** 473 | * @description Get the the current sample rate is. 474 | * Note: This is dependent on if you configured the board correctly on setup options 475 | * @returns {Number} The sample rate 476 | * @author AJ Keller (@aj-ptw) 477 | */ 478 | Wifi.prototype.getSampleRate = function () { 479 | return this._sampleRate; 480 | }; 481 | 482 | /** 483 | * Return the shield name of the attached WiFi Shield device. 484 | * @return {null|String} 485 | * @author AJ Keller (@aj-ptw) 486 | */ 487 | Wifi.prototype.getShieldName = function () { 488 | return this._shieldName; 489 | }; 490 | 491 | /** 492 | * Call to start testing impedance. 493 | * @return {global.Promise|Promise} 494 | * @author AJ Keller (@aj-ptw) 495 | */ 496 | Wifi.prototype.impedanceStart = function () { 497 | if (this.getBoardType() !== k.OBCIBoardGanglion) return Promise.reject(Error('Expected board type to be Ganglion')); 498 | return this.write(k.OBCIGanglionImpedanceStart); 499 | }; 500 | 501 | /** 502 | * Call to stop testing impedance. 503 | * @return {global.Promise|Promise} 504 | * @author AJ Keller (@aj-ptw) 505 | */ 506 | Wifi.prototype.impedanceStop = function () { 507 | if (this.getBoardType() !== k.OBCIBoardGanglion) return Promise.reject(Error('Expected board type to be Ganglion')); 508 | return this.write(k.OBCIGanglionImpedanceStop); 509 | }; 510 | 511 | /** 512 | * Used to search for an OpenBCI WiFi Shield. Will connect to the first one if no `shieldName` is supplied. 513 | * @param o {Object} (optional) 514 | * @param o.sampleRate - The sample rate to set the board connected to the wifi shield 515 | * @param o.shieldName {String} - If supplied, will search for a shield by this name, if not supplied, will connect to 516 | * the first shield found. 517 | * @param o.streamStart {Boolean} - Set `true` if you want the board to start streaming. 518 | * @param o.timeout {Number} - The time in milli seconds to wait for the system to try and auto find and connect to the 519 | * shield. 520 | * @return {Promise} - Resolves after successful connection, rejects otherwise with Error. 521 | * @author AJ Keller (@aj-ptw) 522 | */ 523 | Wifi.prototype.searchToStream = function (o) { 524 | return new Promise((resolve, reject) => { 525 | let autoFindTimeOut = null; 526 | let timeout = 10000; 527 | if (o.hasOwnProperty('timeout')) timeout = o.timeout; 528 | this.once(k.OBCIEmitterWifiShield, (shield) => { 529 | if (o.hasOwnProperty('shieldName')) { 530 | if (!_.eq(o.shieldName, shield.localName)) return; 531 | } 532 | if (autoFindTimeOut) clearTimeout(autoFindTimeOut); 533 | o['ipAddress'] = shield.ipAddress; 534 | this.searchStop() 535 | .then(() => { 536 | return this.connect(o); 537 | }) 538 | .then(() => { 539 | if (this.options.verbose) console.log('Done with search connect and sync'); 540 | resolve(); 541 | }) 542 | .catch((err) => { 543 | reject(err); 544 | }); 545 | }); 546 | this.searchStart().catch(console.log); 547 | 548 | autoFindTimeOut = setTimeout(() => { 549 | this.searchStop().catch(console.log); 550 | reject(Error(`Failed to autoFindAndConnectToWifiShield within ${timeout} ms`)); 551 | }, timeout); 552 | }); 553 | }; 554 | 555 | /** 556 | * Set the sample rate of the remote OpenBCI shield 557 | * @param sampleRate {Number} the sample rate you want to set to. 558 | * @returns {Promise} 559 | * @author AJ Keller (@aj-ptw) 560 | */ 561 | Wifi.prototype.setSampleRate = function (sampleRate) { 562 | const numPattern = /\d+/g; 563 | return new Promise((resolve, reject) => { 564 | k.getSampleRateSetter(this._boardType, sampleRate) 565 | .then((cmds) => { 566 | return this.write(cmds); 567 | }) 568 | .then((res) => { 569 | if (_.includes(res, k.OBCIParseSuccess)) { 570 | this._sampleRate = Number(res.match(numPattern)[0]); 571 | resolve(this._sampleRate); 572 | } else { 573 | reject(res); 574 | } 575 | }) 576 | .catch((err) => { 577 | reject(err); 578 | }); 579 | }); 580 | }; 581 | 582 | /** 583 | * Returns the sample rate from the board 584 | * @returns {Promise} 585 | * @author AJ Keller (@aj-ptw) 586 | */ 587 | Wifi.prototype.syncSampleRate = function () { 588 | const numPattern = /\d+/g; 589 | return new Promise((resolve, reject) => { 590 | this.write(`${k.OBCISampleRateSet}${k.OBCISampleRateCmdGetCur}`) 591 | .then((res) => { 592 | if (_.includes(res, k.OBCIParseSuccess)) { 593 | this._sampleRate = Number(res.match(numPattern)[0]); 594 | resolve(this._sampleRate); 595 | } else { 596 | reject(Error(`syncSampleRate: MESSAGE:${res.message}`)); 597 | } 598 | }) 599 | .catch((err) => { 600 | reject(err); 601 | }); 602 | }); 603 | }; 604 | 605 | /** 606 | * @description List available peripherals so the user can choose a device when not 607 | * automatically found. 608 | * @returns {Promise} - If scan was started 609 | * @author AJ Keller (@aj-ptw) 610 | */ 611 | Wifi.prototype.searchStart = function () { 612 | return new Promise((resolve) => { 613 | this._scanning = true; 614 | this.wifiClient = new Client({}); 615 | let attemptCounter = 0; 616 | let _timeout = 3 * 1000; // Retry every 3 seconds 617 | let timeoutFunc = () => { 618 | if (attemptCounter < this.options.attempts) { 619 | if (this.wifiClient) { 620 | this.wifiClient.stop(); 621 | this.wifiClient.search('urn:schemas-upnp-org:device:Basic:1'); 622 | attemptCounter++; 623 | if (this.options.verbose) console.log(`SSDP: still trying to find a board - attempt ${attemptCounter} of ${this.options.attempts}`); 624 | this.ssdpTimeout = setTimeout(timeoutFunc, _timeout); 625 | } 626 | } else { 627 | if (this.options.verbose) console.log('SSDP: stopping because out of attempts'); 628 | this.searchStop(); 629 | } 630 | }; 631 | this.wifiClient.on('response', (headers, code, rinfo) => { 632 | if (this.options.verbose) console.log('SSDP:Got a response to an m-search:\n%d\n%s\n%s', code, JSON.stringify(headers, null, ' '), JSON.stringify(rinfo, null, ' ')); 633 | try { 634 | const shieldName = `OpenBCI-${headers.SERVER.split('/')[2].split('-')[2]}`; 635 | const shieldIpAddress = rinfo.address; 636 | const wifiShieldObject = { 637 | ipAddress: shieldIpAddress, 638 | localName: shieldName 639 | }; 640 | let addShield = true; 641 | _.forEach(this.wifiShieldArray, (shield) => { 642 | if (shield.ipAddress === shieldIpAddress) { 643 | addShield = false; 644 | } 645 | }); 646 | if (addShield) this.wifiShieldArray.push(wifiShieldObject); 647 | this.emit(k.OBCIEmitterWifiShield, wifiShieldObject); 648 | } catch (err) { 649 | console.log('not an openbci shield'); 650 | } 651 | }); 652 | // Search for just the wifi shield 653 | this.wifiClient.search('urn:schemas-upnp-org:device:Basic:1'); 654 | this.ssdpTimeout = setTimeout(timeoutFunc, _timeout); 655 | resolve(); 656 | }); 657 | }; 658 | 659 | /** 660 | * Called to end a search. 661 | * @return {global.Promise|Promise} 662 | * @author AJ Keller (@aj-ptw) 663 | */ 664 | Wifi.prototype.searchStop = function () { 665 | if (this.wifiClient) this.wifiClient.stop(); 666 | if (this.ssdpTimeout) clearTimeout(this.ssdpTimeout); 667 | this._scanning = false; 668 | this.emit('scanStopped'); // TODO: When 0.2.4 util is merged change to const 669 | return Promise.resolve(); 670 | }; 671 | 672 | /** 673 | * @description Start logging to the SD card. If not streaming then `eot` event will be emitted with request 674 | * response from the board. 675 | * @param recordingDuration {String} - The duration you want to log SD information for. Limited to: 676 | * '14sec', '5min', '15min', '30min', '1hour', '2hour', '4hour', '12hour', '24hour' 677 | * @returns {Promise} - Resolves when the command has been written. 678 | * @private 679 | * @author AJ Keller (@aj-ptw) 680 | */ 681 | Wifi.prototype.sdStart = function (recordingDuration) { 682 | return new Promise((resolve, reject) => { 683 | if (!this.isConnected()) return reject(Error('Must be connected to the device')); 684 | k.sdSettingForString(recordingDuration) 685 | .then(command => { 686 | return this.write(command); 687 | }) 688 | .then((res) => { 689 | resolve(res); 690 | }) 691 | .catch(err => reject(err)); 692 | }); 693 | }; 694 | 695 | /** 696 | * @description Sends the stop SD logging command to the board. If not streaming then `eot` event will be emitted 697 | * with request response from the board. 698 | * @returns {Promise} - Resolves when written 699 | * @author AJ Keller (@aj-ptw) 700 | */ 701 | Wifi.prototype.sdStop = function () { 702 | return new Promise((resolve, reject) => { 703 | if (!this.isConnected()) return reject(Error('Must be connected to the device')); 704 | // If we are not streaming, then expect a confirmation message back from the board 705 | this.write(k.OBCISDLogStop) 706 | .then((res) => { 707 | if (this.options.verbose) console.log('Sent sd stop to board.'); 708 | resolve(res); 709 | }) 710 | .catch(reject); 711 | }); 712 | }; 713 | 714 | /** 715 | * @description Syncs the internal channel settings object with a cyton, this will take about 716 | * over a second because there are delays between the register reads in the firmware. 717 | * @returns {Promise.|*} Resolved once synced, rejects on error or 2 second timeout 718 | * @author AJ Keller (@aj-ptw) 719 | */ 720 | Wifi.prototype.syncRegisterSettings = function () { 721 | return new Promise((resolve, reject) => { 722 | this.write(k.OBCIMiscQueryRegisterSettings) 723 | .then((res) => { 724 | this._rawDataPacketToSample.data = Buffer.from(res.message); 725 | try { 726 | obciUtils.syncChannelSettingsWithRawData(this._rawDataPacketToSample); 727 | resolve(this._rawDataPacketToSample.channelSettings); 728 | } catch (e) { 729 | reject(e); 730 | } 731 | }) 732 | .catch((err) => { 733 | reject(err); 734 | }); 735 | }); 736 | }; 737 | 738 | /** 739 | * @description Sends a soft reset command to the board 740 | * @returns {Promise} - Fulfilled if the command was sent to board. 741 | * @author AJ Keller (@aj-ptw) 742 | */ 743 | Wifi.prototype.softReset = function () { 744 | return this.write(k.OBCIMiscSoftReset); 745 | }; 746 | 747 | /** 748 | * @description Tells the WiFi Shield to forget it's network credentials. This will cause the board to drop all 749 | * connections. 750 | * @returns {Promise} Resolves when WiFi Shield has been reset and the module disconnects. 751 | * @author AJ Keller (@aj-ptw) 752 | */ 753 | Wifi.prototype.eraseWifiCredentials = function () { 754 | return new Promise((resolve, reject) => { 755 | this.delete('/wifi') 756 | .then((res) => { 757 | if (this.options.verbose) console.log(res); 758 | return this.disconnect(); 759 | }) 760 | .then(() => { 761 | resolve(); 762 | }) 763 | .catch((err) => { 764 | if (this.options.verbose) console.log(err); 765 | reject(err); 766 | }); 767 | }); 768 | }; 769 | 770 | /** 771 | * @description Sends a start streaming command to the board. 772 | * @returns {Promise} indicating if the signal was able to be sent. 773 | * Note: You must have successfully connected to an OpenBCI board using the connect 774 | * method. Just because the signal was able to be sent to the board, does not 775 | * mean the board will start streaming. 776 | * @author AJ Keller (@aj-ptw) 777 | */ 778 | Wifi.prototype.streamStart = function () { 779 | return new Promise((resolve, reject) => { 780 | if (this.isStreaming()) return reject(Error('Error [.streamStart()]: Already streaming')); 781 | this._streaming = true; 782 | this._lastPacketArrival = 0; 783 | this.write(k.OBCIStreamStart) 784 | .then(() => { 785 | if (this.options.verbose) console.log('Sent stream start to board.'); 786 | resolve(); 787 | }) 788 | .catch(reject); 789 | }); 790 | }; 791 | 792 | /** 793 | * @description Sends a stop streaming command to the board. 794 | * @returns {Promise} indicating if the signal was able to be sent. 795 | * Note: You must have successfully connected to an OpenBCI board using the connect 796 | * method. Just because the signal was able to be sent to the board, does not 797 | * mean the board stopped streaming. 798 | * @author AJ Keller (@aj-ptw) 799 | */ 800 | Wifi.prototype.streamStop = function () { 801 | return new Promise((resolve, reject) => { 802 | if (!this.isStreaming()) return reject(Error('Error [.streamStop()]: No stream to stop')); 803 | this._streaming = false; 804 | this.write(k.OBCIStreamStop) 805 | .then(() => { 806 | resolve(); 807 | }) 808 | .catch(reject); 809 | }); 810 | }; 811 | 812 | /** 813 | * Sync the info of this wifi module 814 | * @param o {Object} 815 | * @param o.examineMode {Boolean} - Set this option true to connect to the WiFi Shield even if there is no board attached. 816 | * @returns {Promise.} 817 | * @author AJ Keller (@aj-ptw) 818 | */ 819 | Wifi.prototype.syncInfo = function (o) { 820 | return this.get('/board') 821 | .then((res) => { 822 | try { 823 | const info = JSON.parse(res.message); 824 | this._boardConnected = info['board_connected']; 825 | this._numberOfChannels = info['num_channels']; 826 | this._boardType = info['board_type']; 827 | if (o.hasOwnProperty('examineMode')) { 828 | if (!o.examineMode && !this._boardConnected) { 829 | return Promise.reject(Error('No OpenBCI Board (Ganglion or Cyton) connected, please check power of the boards')); 830 | } 831 | } else { 832 | if (!this._boardConnected) return Promise.reject(Error('No OpenBCI Board (Ganglion or Cyton) connected, please check power of the boards')); 833 | } 834 | 835 | const channelSettings = k.channelSettingsArrayInit(this.getNumberOfChannels()); 836 | _.forEach(channelSettings, (settings, index) => { 837 | settings['gain'] = info['gains'][index]; 838 | }); 839 | this._rawDataPacketToSample.channelSettings = channelSettings; 840 | if (this.options.verbose) console.log(`Got all info from GET /board`); 841 | this._boardInfo = info; 842 | return this.get('/all'); 843 | } catch (err) { 844 | return Promise.reject(err); 845 | } 846 | }) 847 | .then((res) => { 848 | try { 849 | const allInfo = JSON.parse(res.message); 850 | this._shieldName = allInfo.name; 851 | this._macAddress = allInfo.mac; 852 | this._version = allInfo.version; 853 | this._latency = allInfo.latency; 854 | this._allInfo = allInfo; 855 | return Promise.resolve(this._boardInfo); 856 | } catch (err) { 857 | return Promise.reject(err); 858 | } 859 | }) 860 | .catch((err) => { 861 | console.log(err); 862 | return Promise.reject(err); 863 | }); 864 | }; 865 | 866 | /** 867 | * @description Used to send data to the board. 868 | * @param data {Array | Buffer | Number | String} - The data to write out 869 | * @returns {Promise} - fulfilled if command was able to be sent 870 | * @author AJ Keller (@aj-ptw) 871 | */ 872 | Wifi.prototype.write = function (data) { 873 | return new Promise((resolve, reject) => { 874 | if (this._ipAddress) { 875 | if (!Buffer.isBuffer(data)) { 876 | if (_.isArray(data)) { 877 | data = Buffer.alloc(data.length, data.join('')); 878 | } else { 879 | data = new Buffer(data); 880 | } 881 | } 882 | if (this.options.debug) obciDebug.debugBytes('>>>', data); 883 | this.post('/command', { 'command': data.toString() }) 884 | .then((res) => { 885 | resolve(res.message); 886 | }) 887 | .catch((err) => { 888 | reject(err); 889 | }); 890 | } else { 891 | reject(Error('ipAddress is not set. Please call connect with ip address of wifi shield')); 892 | } 893 | }); 894 | }; 895 | 896 | // //////// // 897 | // PRIVATES // 898 | // //////// // 899 | /** 900 | * @description Called once when for any reason the ble connection is no longer open. 901 | * @private 902 | */ 903 | Wifi.prototype._disconnected = function () { 904 | this._streaming = false; 905 | this._connected = false; 906 | 907 | if (this.options.verbose) console.log(`Private disconnect clean up`); 908 | 909 | this.emit('close'); 910 | }; 911 | 912 | /** 913 | * Route incoming data to proper functions 914 | * @param data {Buffer} - Data buffer from noble Wifi. 915 | * @private 916 | * @author AJ Keller (@aj-ptw) 917 | */ 918 | Wifi.prototype._processBytes = function (data) { 919 | if (this.options.debug) obciDebug.debugBytes('<<', data); 920 | if (this.curOutputMode === wifiOutputModeRaw) { 921 | if (this.buffer) { 922 | this.prevBuffer = this.buffer; 923 | this.buffer = new Buffer([this.buffer, data]); 924 | } else { 925 | this.buffer = data; 926 | } 927 | const output = obciUtils.extractRawDataPackets(this.buffer); 928 | 929 | this.buffer = output.buffer; 930 | 931 | // if (this.options.redundancy) { 932 | // this._rawDataPacketToSample.rawDataPackets = this.bufferRawDataPackets(output.rawDataPackets); 933 | // } else { 934 | // this._rawDataPacketToSample.rawDataPackets = output.rawDataPackets; 935 | // } 936 | this._rawDataPacketToSample.rawDataPackets = output.rawDataPackets; 937 | 938 | _.forEach(this._rawDataPacketToSample.rawDataPackets, (rawDataPacket) => { 939 | this.emit(k.OBCIEmitterRawDataPacket, rawDataPacket); 940 | }); 941 | 942 | const samples = obciUtils.transformRawDataPacketsToSample(this._rawDataPacketToSample); 943 | 944 | if (samples.length > 0) { 945 | if (samples[0].hasOwnProperty('impedanceValue')) { 946 | _.forEach(samples, (impedance) => { 947 | this.emit(k.OBCIEmitterImpedance, impedance); 948 | }); 949 | } else { 950 | _.forEach(samples, (sample) => { 951 | if (this.getBoardType() === k.OBCIBoardDaisy) { 952 | // Send the sample for downstream sample compaction 953 | this._finalizeNewSampleForDaisy(sample); 954 | } else { 955 | // console.log(JSON.stringify(sample)); 956 | this.emit(k.OBCIEmitterSample, sample); 957 | } 958 | }); 959 | } 960 | } 961 | 962 | // Prevent bad data from being carried through continuously 963 | if (this.buffer) { 964 | if (bufferEqual(this.buffer, this.prevBuffer)) { 965 | this.buffer = null; 966 | } 967 | } 968 | } 969 | }; 970 | 971 | /** 972 | * @description This function is called every sample if the boardType is Daisy. The function stores odd sampleNumber 973 | * sample objects to a private global variable called `._lowerChannelsSampleObject`. The method will emit a 974 | * sample object only when the upper channels arrive in an even sampleNumber sample object. No sample will be 975 | * emitted on an even sampleNumber if _lowerChannelsSampleObject is null and one will be added to the 976 | * missedPacket counter. Further missedPacket will increase if two odd sampleNumber packets arrive in a row. 977 | * @param sampleObject {Object} - The sample object to finalize 978 | * @private 979 | * @author AJ Keller (@aj-ptw) 980 | */ 981 | Wifi.prototype._finalizeNewSampleForDaisy = function (sampleObject) { 982 | if (k.isNull(this._lowerChannelsSampleObject)) { 983 | this._lowerChannelsSampleObject = sampleObject; 984 | } else { 985 | // Make sure there is an odd packet waiting to get merged with this packet 986 | if (this._lowerChannelsSampleObject.sampleNumber === sampleObject.sampleNumber) { 987 | // Merge these two samples 988 | var mergedSample = obciUtils.makeDaisySampleObjectWifi(this._lowerChannelsSampleObject, sampleObject); 989 | // Set the _lowerChannelsSampleObject object to null 990 | this._lowerChannelsSampleObject = null; 991 | // Emit the new merged sample 992 | this.emit('sample', mergedSample); 993 | } else { 994 | // Missed the odd packet, i.e. two evens in a row 995 | this._lowerChannelsSampleObject = sampleObject; 996 | } 997 | } 998 | }; 999 | 1000 | /** 1001 | * Used for client connecting to 1002 | * @private 1003 | */ 1004 | Wifi.prototype._connectSocket = function () { 1005 | if (this.options.protocol === 'udp') { 1006 | return this.post(`/${this.options.protocol}`, { 1007 | ip: this.getLocalIPAddress(), 1008 | output: this.curOutputMode, 1009 | port: this.wifiGetLocalPortUDP(), 1010 | delimiter: false, 1011 | latency: this._latency, 1012 | redundancy: this.options.burst 1013 | }); 1014 | } else { 1015 | return this.post(`/${this.options.protocol}`, { 1016 | ip: this.getLocalIPAddress(), 1017 | output: this.curOutputMode, 1018 | port: this.wifiGetLocalPortTCP(), 1019 | delimiter: false, 1020 | latency: this._latency 1021 | }); 1022 | } 1023 | }; 1024 | 1025 | /** 1026 | * Call this to shut down the servers. 1027 | */ 1028 | Wifi.prototype.destroy = function () { 1029 | this.wifiServer = null; 1030 | if (this.wifiClient) { 1031 | this.wifiClient.stop(); 1032 | } 1033 | if (this.wifiServerUDP) { 1034 | this.wifiServerUDP.removeAllListeners('error'); 1035 | this.wifiServerUDP.removeAllListeners('message'); 1036 | this.wifiServerUDP.removeAllListeners('listening'); 1037 | this.wifiServerUDP.close(); 1038 | } 1039 | this.wifiClient = null; 1040 | }; 1041 | 1042 | /** 1043 | * Get the local port number of either the TCP or UDP server. Based on `options.protocol` being set to 1044 | * either `udp` or `tcp`. 1045 | * @returns {number} The port number that was dynamically assigned to this module on startup. 1046 | */ 1047 | Wifi.prototype.wifiGetLocalPort = function () { 1048 | if (this.options.protocol === wifiOutputProtocolUDP) { 1049 | return this.wifiServerUDPPort; 1050 | } else { 1051 | return this.wifiServer.address().port; 1052 | } 1053 | }; 1054 | 1055 | /** 1056 | * Get the local port number of the UDP server. 1057 | * @returns {number} The port number that was dynamically assigned to this module on startup. 1058 | */ 1059 | Wifi.prototype.wifiGetLocalPortUDP = function () { 1060 | return this.wifiServerUDPPort; 1061 | }; 1062 | 1063 | /** 1064 | * Get the local port number of the UDP server. 1065 | * @returns {number} The port number that was dynamically assigned to this module on startup. 1066 | */ 1067 | Wifi.prototype.wifiGetLocalPortTCP = function () { 1068 | return this.wifiServer.address().port; 1069 | }; 1070 | 1071 | /** 1072 | * Initialization function that will start the TCP server and bind the UDP port. 1073 | */ 1074 | Wifi.prototype.wifiInitServer = function () { 1075 | this.wifiServer = net.createServer((socket) => { 1076 | socket.on('data', (data) => { 1077 | this._processBytes(data); 1078 | }); 1079 | socket.on('error', (err) => { 1080 | if (this.options.verbose) console.log('SSDP:', err); 1081 | }); 1082 | }).listen(); 1083 | if (this.options.verbose) console.log('TCP: on port: ', this.wifiGetLocalPortTCP()); 1084 | 1085 | this.wifiServerUDP = dgram.createSocket('udp4'); 1086 | 1087 | this.wifiServerUDP.on('error', (err) => { 1088 | if (this.options.verbose) console.log(`server error:\n${err.stack}`); 1089 | this.wifiServerUDP.close(); 1090 | }); 1091 | 1092 | let lastMsg = null; 1093 | 1094 | this.wifiServerUDP.on('message', (msg) => { 1095 | if (!bufferEqual(lastMsg, msg)) { 1096 | this._processBytes(msg); 1097 | } 1098 | lastMsg = msg; 1099 | }); 1100 | 1101 | this.wifiServerUDP.on('listening', () => { 1102 | const address = this.wifiServerUDP.address(); 1103 | this.wifiServerUDPPort = address.port; 1104 | if (this.options.verbose) console.log(`server listening ${address.address}:${address.port}`); 1105 | }); 1106 | 1107 | this.wifiServerUDP.bind({ 1108 | address: this.getLocalIPAddress() 1109 | }); 1110 | }; 1111 | 1112 | /** 1113 | * Used to process a response from either a GET, POST, or DELETE. 1114 | * @param res {Object} The response from the server or client 1115 | * @param cb {callback} Callback to know the response and everything is done. Can contain the message. 1116 | * @private 1117 | */ 1118 | Wifi.prototype._processResponse = function (res) { 1119 | return new Promise((resolve, reject) => { 1120 | if (this.options.verbose) { 1121 | console.log(`STATUS: ${res.statusCode}`); 1122 | // console.log(`HEADERS: ${JSON.stringify(res.headers)}`); 1123 | } 1124 | let processResponseTimeout = setTimeout(() => { 1125 | reject(Error('Timeout waiting for response')); 1126 | }, 2000); 1127 | res.setEncoding('utf8'); 1128 | let msg = ''; 1129 | res.on('data', (chunk) => { 1130 | msg += chunk.toString(); 1131 | }); 1132 | res.once('end', () => { 1133 | clearTimeout(processResponseTimeout); 1134 | if (this.options.verbose) console.log('No more data in response.'); 1135 | res['msg'] = msg; 1136 | if (this.options.verbose) console.log(`BODY: ${msg}`); 1137 | if (res.statusCode !== 200) { 1138 | reject(Error(`ERROR: CODE: ${res.statusCode} MESSAGE: ${res.msg}`)); 1139 | } else { 1140 | resolve({ 1141 | message: res.msg, 1142 | code: res.statusCode 1143 | }); 1144 | } 1145 | }); 1146 | }); 1147 | }; 1148 | 1149 | Wifi.prototype._delete = function (host, path) { 1150 | return new Promise((resolve, reject) => { 1151 | const options = { 1152 | host: host, 1153 | port: 80, 1154 | path: path, 1155 | method: 'DELETE' 1156 | }; 1157 | 1158 | const req = http.request(options, (res) => { 1159 | this._processResponse(res) 1160 | .then(resolve) 1161 | .catch(reject); 1162 | }); 1163 | 1164 | req.once('error', (e) => { 1165 | if (this.options.verbose) console.log(`DELETE problem with request: ${e.message}`); 1166 | reject(e); 1167 | }); 1168 | 1169 | req.end(); 1170 | }); 1171 | }; 1172 | 1173 | /** 1174 | * Send a delete message to the connected wifi shield. 1175 | * @param path {String} - the path/route to send the delete message to 1176 | * @returns {Promise} - Resolves if gets a response from the client/server, rejects with error 1177 | */ 1178 | Wifi.prototype.delete = function (path) { 1179 | if (this.options.verbose) console.log(`-> DELETE: ${this._ipAddress}${path}`); 1180 | return this._delete(this._ipAddress, path); 1181 | }; 1182 | 1183 | Wifi.prototype._get = function (host, path) { 1184 | return new Promise((resolve, reject) => { 1185 | const options = { 1186 | host: host, 1187 | port: 80, 1188 | path: path, 1189 | method: 'GET' 1190 | }; 1191 | 1192 | const req = http.request(options, (res) => { 1193 | this._processResponse(res) 1194 | .then(resolve) 1195 | .catch(reject); 1196 | }); 1197 | 1198 | req.once('error', (e) => { 1199 | if (this.options.verbose) console.log(`GET problem with request: ${e.message}`); 1200 | reject(e); 1201 | }); 1202 | 1203 | req.end(); 1204 | }); 1205 | }; 1206 | 1207 | /** 1208 | * Send a GET message to the connected wifi shield. 1209 | * @param path {String} - the path/route to send the GET message to 1210 | * @returns {Promise} - Resolves if gets/with a response from the client/server, rejects with error 1211 | */ 1212 | Wifi.prototype.get = function (path) { 1213 | if (this.options.verbose) console.log(`-> GET: ${this._ipAddress}${path}`); 1214 | return this._get(this._ipAddress, path); 1215 | }; 1216 | 1217 | Wifi.prototype._post = function (host, path, payload) { 1218 | return new Promise((resolve, reject) => { 1219 | const output = JSON.stringify(payload); 1220 | const options = { 1221 | host: host, 1222 | port: 80, 1223 | path: path, 1224 | method: 'POST', 1225 | headers: { 1226 | 'Content-Type': 'application/json', 1227 | 'Content-Length': output.length 1228 | } 1229 | }; 1230 | 1231 | const req = http.request(options, (res) => { 1232 | this._processResponse(res) 1233 | .then(resolve) 1234 | .catch(reject); 1235 | }); 1236 | 1237 | req.once('error', (e) => { 1238 | if (this.options.verbose) console.log(`problem with request: ${e.message}`); 1239 | reject(e); 1240 | }); 1241 | 1242 | // write data to request body 1243 | req.write(output); 1244 | req.end(); 1245 | }); 1246 | }; 1247 | 1248 | /** 1249 | * Send a POST message to the connected wifi shield. 1250 | * @param path {String} - the path/route to send the POST message to 1251 | * @param payload {*} - can really be anything but should be a JSON object. 1252 | * @returns {Promise} - Resolves if gets a response from the client/server, rejects with error 1253 | */ 1254 | Wifi.prototype.post = function (path, payload) { 1255 | if (this.options.verbose) console.log(`-> POST: ${this._ipAddress}${path} ${JSON.stringify(payload)}`); 1256 | return this._post(this._ipAddress, path, payload); 1257 | }; 1258 | 1259 | module.exports = Wifi; 1260 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openbci/wifi", 3 | "version": "1.0.2", 4 | "description": "The official SDK for the OpenBCI/Push The World Wifi Shield connected to either OpenBCI Cytons or Ganglions.", 5 | "main": "openBCIWifi.js", 6 | "scripts": { 7 | "test": "semistandard | snazzy && mocha test", 8 | "test-cov": "semistandard | snazzy && istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec && codecov" 9 | }, 10 | "keywords": [ 11 | "openbci", 12 | "openbci-node", 13 | "ganglion", 14 | "cyton", 15 | "wifi" 16 | ], 17 | "author": "AJ Keller (www.openbci.com)", 18 | "license": "MIT", 19 | "dependencies": { 20 | "buffer-equal": "^1.0.0", 21 | "clone": "^2.1.2", 22 | "fs": "0.0.1-security", 23 | "ip": "^1.1.5", 24 | "lodash": "^4.17.11", 25 | "node-ssdp": "^3.2.1", 26 | "@openbci/utilities": "^1.0.0", 27 | "os": "^0.1.1", 28 | "safe-buffer": "^5.1.1", 29 | "xstream": "^11.10.0" 30 | }, 31 | "directories": { 32 | "test": "test" 33 | }, 34 | "devDependencies": { 35 | "bluebird": "3.4.6", 36 | "chai": "^4.2.0", 37 | "chai-as-promised": "^7.1.1", 38 | "codecov": "^2.2.0", 39 | "dirty-chai": "^2.0.1", 40 | "istanbul": "^0.4.4", 41 | "mocha": "^3.0.2", 42 | "sandboxed-module": "^2.0.3", 43 | "semistandard": "^11.0.0", 44 | "sinon": "^2.3.7", 45 | "sinon-chai": "^2.11.0", 46 | "snazzy": "^7.0.0" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/openbci/openbci_nodejs_wifi.git" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/openbci/openbci_nodejs_wifi/issues" 54 | }, 55 | "homepage": "https://github.com/openbci/openbci_nodejs_wifi#readme", 56 | "engines": { 57 | "node": ">=4" 58 | }, 59 | "semistandard": { 60 | "globals": [ 61 | "describe", 62 | "xdescribe", 63 | "context", 64 | "before", 65 | "beforeEach", 66 | "after", 67 | "afterEach", 68 | "it", 69 | "expect", 70 | "should" 71 | ] 72 | }, 73 | "publishConfig": { 74 | "access": "public" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/bluebirdChecks.js: -------------------------------------------------------------------------------- 1 | var timingEventsAsPromises = require('./timingEventsAsPromises'); 2 | exports.BluebirdPromise = require('bluebird'); 3 | exports.PromiseIgnored = global.Promise; 4 | 5 | // Enable bluebird for all promise usage during tests only 6 | // Fails tests for issues bluebird finds 7 | // Exports a function to list all promises (getPendingPromises) 8 | // Exports a function to verify no promises pending within a timeout (noPendingPromises) 9 | 10 | exports.BluebirdPromise.config({ 11 | // TODO: wForgottenReturn is disabled because timingEventsAsPromises triggers it; find a workaround 12 | warnings: { wForgottenReturn: false }, 13 | longStackTraces: true, 14 | monitoring: true, 15 | cancellation: true 16 | }); 17 | 18 | // nextTick conveniently not instrumented by timingEventsAsPromises 19 | exports.BluebirdPromise.setScheduler(process.nextTick); 20 | 21 | // unhandled rejections become test failures 22 | process.on('unhandledRejection', (reason, promise) => { 23 | if (!(reason instanceof Error)) { 24 | reason = new Error('unhandled promise rejection: ' + reason); 25 | } else { 26 | reason.message = 'unhandled promise rejection: ' + reason.message; 27 | } 28 | process.nextTick(() => { throw reason; }); 29 | }); 30 | 31 | // // warnings become test failures 32 | // process.on('warning', (warning) => { 33 | // var error = new Error(warning); 34 | // process.nextTick(() => { throw error; }); 35 | // }); 36 | 37 | // provide access to all currently pending promises 38 | var pendingPromises = {}; 39 | var promiseId = 0; 40 | var nested = 0; 41 | 42 | function promiseCreationHandler (promise) { 43 | // promise created already resolved; ignore 44 | if (!promise.isPending()) return; 45 | 46 | // need to create another promise to get access to the extended stack trace 47 | // nested detects if we are inside our own dummy promise 48 | ++nested; 49 | if (nested === 1) { 50 | // not the dummy promise 51 | promise.___id = ++promiseId; 52 | // store promise details 53 | var error = new Error('Promise ' + promise.___id + ' is still pending'); 54 | var entry = { 55 | promise: promise, 56 | id: promise.___id, 57 | error: error 58 | }; 59 | pendingPromises[promise.___id] = entry; 60 | // extract stack trace by rejecting an error; bluebird fills in expanded stack 61 | exports.BluebirdPromise.reject(error).catch(error => { 62 | entry.error = error; 63 | entry.stack = error.stack; 64 | }); 65 | } else { 66 | promise.___nested = nested; 67 | } 68 | --nested; 69 | } 70 | process.on('promiseCreated', promiseCreationHandler); 71 | 72 | function promiseDoneHandler (promise) { 73 | if (promise.___nested) return; 74 | delete pendingPromises[promise.___id]; 75 | } 76 | process.on('promiseFulfilled', promiseDoneHandler); 77 | process.on('promiseRejected', promiseDoneHandler); 78 | process.on('promiseResolved', promiseDoneHandler); 79 | process.on('promiseCancelled', promiseDoneHandler); 80 | 81 | exports.getPendingPromises = function () { 82 | var ret = []; 83 | for (var promise in pendingPromises) { 84 | ret.push(pendingPromises[promise]); 85 | } 86 | return ret; 87 | }; 88 | 89 | exports.noPendingPromises = function (milliseconds) { 90 | if (!milliseconds) milliseconds = 0; 91 | 92 | return new exports.PromiseIgnored((resolve, reject) => { 93 | function waited100 () { 94 | var promises = exports.getPendingPromises(); 95 | 96 | if (promises.length === 0) { 97 | return resolve(); 98 | } 99 | 100 | if (milliseconds > 0) { 101 | milliseconds -= 100; 102 | return timingEventsAsPromises.setTimeoutIgnored(waited100, 100); 103 | } 104 | 105 | // timed out, but promises remaining: cancel all 106 | 107 | console.log(promises.length + ' promises still pending'); 108 | 109 | promises.forEach(promise => { 110 | promise.promise.cancel(); 111 | }); 112 | 113 | // report one 114 | reject(promises[0].error); 115 | } 116 | 117 | timingEventsAsPromises.setTimeoutIgnored(waited100, 0); 118 | }); 119 | }; 120 | 121 | // now instrument the Promise object itself to always use a simplified version of bluebird 122 | // bluebird is composed inside a bare-bones Promise object providing only the official calls 123 | 124 | global.Promise = function (handler) { 125 | this._promise = new exports.BluebirdPromise(handler); 126 | }; 127 | 128 | // compose class methods 129 | ['all', 'race', 'reject', 'resolve'].forEach(classMethod => { 130 | global.Promise[classMethod] = function () { 131 | return exports.BluebirdPromise[classMethod].apply(exports.BluebirdPromise, [].slice.call(arguments)); 132 | }; 133 | Object.defineProperty(global.Promise[classMethod], 'name', { value: 'Promise.' + classMethod }); 134 | }); 135 | 136 | // compose object methods 137 | ['then', 'catch'].forEach(objectMethod => { 138 | global.Promise.prototype[objectMethod] = function () { 139 | return this._promise[objectMethod].apply(this._promise, [].slice.call(arguments)); 140 | }; 141 | Object.defineProperty(global.Promise.prototype[objectMethod], 'name', { value: 'Promise.' + objectMethod }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/openBCIWifi-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const chai = require('chai'); 3 | const expect = chai.expect; 4 | const Wifi = require('../openBCIWifi'); 5 | const OpenBCIUtilities = require('@openbci/utilities'); 6 | const openBCIUtilities = OpenBCIUtilities.utilities; 7 | const chaiAsPromised = require('chai-as-promised'); 8 | const sinonChai = require('sinon-chai'); 9 | const dirtyChai = require('dirty-chai'); 10 | 11 | chai.use(chaiAsPromised); 12 | chai.use(sinonChai); 13 | chai.use(dirtyChai); 14 | 15 | describe('@openbci/wifi', function () { 16 | /** 17 | * Test the function that parses an incoming data buffer for packets 18 | */ 19 | describe('#bufferRawDataPackets', function () { 20 | let wifi = new Wifi(); 21 | 22 | beforeEach(() => { 23 | wifi = new Wifi(); 24 | }); 25 | 26 | it('should buffer raw data packets and return no data packets', () => { 27 | // declare the array of raw packets 28 | let rawDataPackets = [ 29 | openBCIUtilities.samplePacketReal(0), 30 | openBCIUtilities.samplePacketReal(1), 31 | openBCIUtilities.samplePacketReal(2) 32 | ]; 33 | 34 | // Call the function under test 35 | let rawDataPacketsOutput = wifi.bufferRawDataPackets(rawDataPackets); 36 | 37 | // Ensure that no buffers were output 38 | expect(rawDataPacketsOutput).to.deep.equal([]); 39 | expect(wifi.internalRawDataPackets).to.deep.equal(rawDataPackets); 40 | }); 41 | it('should output old data in internal raw packet buffer even if duplicate in current packets', () => { 42 | // declare the array of raw packets 43 | let rawDataPackets = [ 44 | openBCIUtilities.samplePacketReal(0), 45 | openBCIUtilities.samplePacketReal(1), 46 | openBCIUtilities.samplePacketReal(2) 47 | ]; 48 | 49 | // Call the function under test one time to preload buffer 50 | wifi.bufferRawDataPackets(rawDataPackets); 51 | 52 | // Then push in the same raw data packets but more as if the first one took a while to send 53 | // and then the first three got sent again for good measure 54 | rawDataPackets = [ 55 | openBCIUtilities.samplePacketReal(0), 56 | openBCIUtilities.samplePacketReal(1), 57 | openBCIUtilities.samplePacketReal(2), 58 | openBCIUtilities.samplePacketReal(3), 59 | openBCIUtilities.samplePacketReal(4), 60 | openBCIUtilities.samplePacketReal(5) 61 | ]; 62 | 63 | // Call the function under test 64 | let rawDataPacketsOutput = wifi.bufferRawDataPackets(rawDataPackets); 65 | 66 | // Ensure that the first three buffers were output 67 | expect(rawDataPacketsOutput).to.deep.equal([ 68 | openBCIUtilities.samplePacketReal(0), 69 | openBCIUtilities.samplePacketReal(1), 70 | openBCIUtilities.samplePacketReal(2) 71 | ]); 72 | expect(wifi.internalRawDataPackets).to.deep.equal([ 73 | openBCIUtilities.samplePacketReal(3), 74 | openBCIUtilities.samplePacketReal(4), 75 | openBCIUtilities.samplePacketReal(5) 76 | ]); 77 | }); 78 | it('should output old data in internal buffer if no duplicate', () => { 79 | // declare the array of raw packets 80 | let rawDataPackets = [ 81 | openBCIUtilities.samplePacketReal(0), 82 | openBCIUtilities.samplePacketReal(1), 83 | openBCIUtilities.samplePacketReal(2) 84 | ]; 85 | 86 | // Call the function under test 87 | wifi.bufferRawDataPackets(rawDataPackets); 88 | 89 | // Then push in good packets 90 | rawDataPackets = [ 91 | openBCIUtilities.samplePacketReal(3), 92 | openBCIUtilities.samplePacketReal(4), 93 | openBCIUtilities.samplePacketReal(5) 94 | ]; 95 | 96 | // Call the function under test 97 | const rawDataPacketsOutput = wifi.bufferRawDataPackets(rawDataPackets); 98 | 99 | // Ensure that the first three buffers were output 100 | expect(rawDataPacketsOutput).to.deep.equal([ 101 | openBCIUtilities.samplePacketReal(0), 102 | openBCIUtilities.samplePacketReal(1), 103 | openBCIUtilities.samplePacketReal(2) 104 | ]); 105 | expect(wifi.internalRawDataPackets).to.deep.equal([ 106 | openBCIUtilities.samplePacketReal(3), 107 | openBCIUtilities.samplePacketReal(4), 108 | openBCIUtilities.samplePacketReal(5) 109 | ]); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /test/timingEventsAsPromises.js: -------------------------------------------------------------------------------- 1 | // Converts timing events to use promises, to gain bluebird's checks 2 | 3 | function instrumentTimingEvents (module, type) { 4 | // replaces module[type] with a function that does the same thing but uses a promise 5 | // assumes the first argument will be to a callback function 6 | 7 | // if this a setSomething function with a clearSomething partner, the partner will also be instrumented 8 | var setName = type; 9 | var clearName = null; 10 | if (type.substring(0, 3) === 'set') { 11 | clearName = 'clear' + type.substring(3); 12 | } 13 | 14 | if (!module[setName]) return; 15 | 16 | // store original functions 17 | var originalSet = module[setName]; 18 | var originalClear = module[clearName]; 19 | exports[setName + 'Ignored'] = originalSet; 20 | 21 | // dictionary to store promise details for each call 22 | var events = {}; 23 | 24 | // setAsPromise() is the brunt of the function. It replaces the previous global function, 25 | // and sets up a promise to resolve when the callback is called 26 | var eventCount = 0; 27 | var setAsPromise = function (callback) { 28 | var args = [].slice.call(arguments); 29 | 30 | var eventNum = ++eventCount; 31 | var eventHandle; 32 | 33 | if (setAsPromise._ignoreCount > 0) { 34 | --setAsPromise._ignoreCount; 35 | 36 | return originalSet.apply(this, args); 37 | } 38 | 39 | // actual callback is replaced by promise resolve 40 | args[0] = function () { 41 | if (!events[eventNum]) throw new Error(setName + ' ' + eventNum + ' disappeared'); 42 | events[eventNum].resolve([].slice.call(arguments)); 43 | }; 44 | 45 | eventHandle = originalSet.apply(this, args) || eventNum; 46 | 47 | // this portion is a function so that setInterval may be handled via recursion 48 | function dispatch () { 49 | var handlerDetails = { handle: eventHandle }; 50 | 51 | var promise = new Promise((resolve, reject) => { 52 | handlerDetails.resolve = resolve; 53 | handlerDetails.reject = reject; 54 | }); 55 | 56 | handlerDetails.promise = promise; 57 | events[eventNum] = handlerDetails; 58 | 59 | promise.then(function (argumentArray) { 60 | if (type !== 'setInterval') { 61 | delete events[eventNum]; 62 | } else { 63 | dispatch(); 64 | } 65 | 66 | // call original handler 67 | callback.apply(this, argumentArray); 68 | }, () => { 69 | originalClear(eventHandle); 70 | delete events[eventNum]; 71 | }); 72 | 73 | return promise; 74 | } 75 | 76 | dispatch(); 77 | 78 | return eventNum; 79 | }; 80 | 81 | // actually replace functions with instrumented ones 82 | setAsPromise._ignoreCount = 0; 83 | module[setName] = setAsPromise; 84 | Object.defineProperty(setAsPromise, 'name', { value: setName + 'AsPromise' }); 85 | 86 | if (clearName) { 87 | module[clearName] = (eventNum) => { 88 | if (!events[eventNum]) { 89 | originalClear(eventNum); 90 | } else { 91 | events[eventNum].reject(new Error('cleared')); 92 | } 93 | }; 94 | Object.defineProperty(module[clearName], 'name', { value: clearName + 'AsPromise' }); 95 | } 96 | } 97 | 98 | instrumentTimingEvents(global, 'setTimeout'); 99 | instrumentTimingEvents(global, 'setInterval'); 100 | instrumentTimingEvents(global, 'setImmediate'); 101 | // // Possible TODO: nextTick needs some exceptions included to prevent infinite recursion 102 | // instrumentTimingEvents(process, 'nextTick'); 103 | 104 | // the next call to the passed function should not be promisified 105 | // may be queued multiple times 106 | exports.ignoreOnce = (ignored) => { 107 | ignored._ignoreCount++; 108 | }; 109 | --------------------------------------------------------------------------------