├── .codeclimate.yml ├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ROADMAP.md ├── externals └── mne_openbci.py ├── images └── openbci_large.png ├── openbci ├── __init__.py ├── cyton.py ├── ganglion.py ├── plugins │ ├── README.md │ ├── __init__.py │ ├── csv_collect.py │ ├── csv_collect.yapsy-plugin │ ├── noise_test.py │ ├── noise_test.yapsy-plugin │ ├── print.py │ ├── print.yapsy-plugin │ ├── sample_rate.py │ ├── sample_rate.yapsy-plugin │ ├── streamer_lsl.py │ ├── streamer_lsl.yapsy-plugin │ ├── streamer_osc.py │ ├── streamer_osc.yapsy-plugin │ ├── streamer_tcp.yapsy-plugin │ ├── streamer_tcp_server.py │ ├── udp_server.py │ └── udp_server.yapsy-plugin ├── utils │ ├── __init__.py │ ├── constants.py │ ├── parse.py │ ├── ssdp.py │ └── utilities.py └── wifi.py ├── plugin_interface.py ├── requirements.txt ├── scripts ├── README.md ├── simple_serial.py ├── socket_client.py ├── stream_data.py ├── stream_data_wifi.py ├── stream_data_wifi_high_speed.py ├── test.py ├── udp_client.py └── udp_server.py ├── setup.py ├── test_log.py ├── tests ├── test_constants.py ├── test_cyton.py ├── test_parse.py └── test_wifi.py └── user.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Python: true 3 | exclude_paths: 4 | - "externals/*" 5 | - "images/*" 6 | - "plugins/*" 7 | - "scripts/*" 8 | - "tests/*" -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | include = */openbci/* 3 | omit = 4 | */python?.?/* 5 | */site-packages/nose/* 6 | */plugin_interface.py 7 | */test_log.py 8 | */setup.py 9 | */users.py 10 | */bluepy/* 11 | */externals/* 12 | */images/* 13 | */plugins/* 14 | */scripts/* 15 | */tests/* 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | venv/ 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Django stuff: 49 | *.log 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | # PyBuilder 55 | target/ 56 | 57 | # Vim temp files 58 | .*.swp 59 | 60 | # CSV 61 | *.csv 62 | 63 | # Matlab 64 | *.m 65 | 66 | # PyCharm 67 | .idea/* 68 | .idea/codeStyleSettings.xml 69 | .idea/vcs.xml 70 | 71 | .DS_Store 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | # command to install dependencies 6 | cache: pip 7 | sudo: false 8 | virtualenv: 9 | system_site_packages: true 10 | addons: 11 | apt: 12 | packages: 13 | - libatlas-dev 14 | - libatlas3gf-base 15 | - libblas-dev 16 | - libglib2.0-dev 17 | - liblapack-dev 18 | - python-matplotlib 19 | - gfortran 20 | - python-tk 21 | install: 22 | # Install project requirements 23 | - pip install -r requirements.txt 24 | # Install BluePy for Ganglion because Travis runs Linux 25 | - pip install bluepy 26 | # Install test and coverage requirements 27 | - pip install codecov mock nose coverage pylint 28 | 29 | # Run tests 30 | script: 31 | - python setup.py install 32 | - nosetests --with-coverage --cover-package=openbci 33 | after_success: 34 | - codecov 35 | - pylint openbci 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.0.2 2 | 3 | ### Bug Fixes 4 | 5 | * Fixed plugin directory in user.py. Calling `python ./user.py --list` lists all the plugins now. 6 | * Cleaned up BluePy import for ganglion. 7 | * Test coverage improvements for Cyton. 8 | 9 | # v1.0.1 10 | 11 | ### Bug Fixes 12 | 13 | * user.py always loaded the ganglion and didnt work with the cyton any more. 14 | * Multiple other fixes 15 | 16 | # v1.0.0 17 | 18 | ### New Features 19 | 20 | * High speed mode for WiFi shield sends raw data - #51 21 | * Unit testing with Nosetests 22 | * Continuous Integration with Travis.ci 23 | 24 | ### Breaking Changes 25 | 26 | * Refactored library for pip 27 | * Moved plugins folder into openbci dir so plugins can be imported when installed with pip 28 | 29 | 30 | ## Beta 0 31 | 32 | * Adds high speed for Daisy over WiFi - now all boards are supported! 33 | 34 | ## Alpha 1 35 | 36 | * Adds high speed for Ganglion over WiFi 37 | 38 | ## Alpha 0 39 | 40 | * Adds high speed for Cyton over WiFi 41 | 42 | # v0.1 43 | 44 | ## dev 45 | 46 | Features: 47 | - Stream data over TCP (OpenViBE telnet reader format), OSC, UDP, LSL 48 | - 16 channels support (daisy module) 49 | - test sampling rate 50 | - plugin system 51 | - several different callback functions 52 | - start streaming in a separate thread so new commands can be issued 53 | 54 | Bugfixes: 55 | - scale factor 56 | - timing for Windows OS 57 | - aux data endianness 58 | - reset board on startup 59 | 60 | ## 0.1 (2015-02-11) 61 | 62 | First stable version. (?) 63 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # OpenBCI Python Code of Conduct 2 | 3 | ## Purpose 4 | 5 | It is our hope that any one is able to contribute to OpenBCI Python 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 | If there's a feature you'd be interested in building, go ahead 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 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 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 IS NOW DEPRECATED AND NO LONGER IN ACTIVE DEVELOPMENT. PLEASE REFER TO ["FOR DEVELOPERS" IN THE DOCS](11ForDevelopers/01-SoftwareDevelopment.md) SECTION FOR INFORMATION ON [BRAINFLOW-PYTHON](https://brainflow.readthedocs.io/en/stable/BuildBrainFlow.html#python).** 2 | 3 | 4 | 5 | # OpenBCI Python 6 | 7 |

8 | banner 9 |

10 |

11 | Provide a stable Python driver for all OpenBCI Biosensors 12 |

13 | 14 | [![Build Status](https://travis-ci.org/OpenBCI/OpenBCI_Python.svg?branch=master)](https://travis-ci.org/OpenBCI/OpenBCI_Python) 15 | 16 | ## Welcome! 17 | 18 | First and foremost, Welcome! :tada: Willkommen! :confetti_ball: Bienvenue! :balloon::balloon::balloon: 19 | 20 | Thank you for visiting the OpenBCI Python repository. This python code is meant to be used by people familiar with python and programming in general. It's purpose is to allow for programmers to interface with OpenBCI technology directly, both to acquire data and to write programs that can use that data on a live setting, using python. 21 | 22 | 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. 23 | 24 | * [What are we doing? (And why?)](#what-are-we-doing) 25 | * [Who are we?](#who-are-we) 26 | * [What do we need?](#what-do-we-need) 27 | * [How can you get involved?](#get-involved) 28 | * [Get in touch](#contact-us) 29 | * [Find out more](#find-out-more) 30 | * [Glossary](#glossary) 31 | * [Dependencies](#dependencies) 32 | * [Install](#install) 33 | * [Functionality](#functionality) 34 | 35 | ## What are we doing? 36 | 37 | ### The problem 38 | 39 | * OpenBCI is an incredible biosensor that can be challenging to work with 40 | * Data comes into the computer very quickly 41 | * Complex byte streams 42 | * Lot's of things can go wrong when dealing with a raw serial byte stream 43 | * The boards all use different physical technologies to move data to computers such as bluetooth or wifi 44 | * Developers want to integrate OpenBCI with other platforms and interfaces 45 | 46 | So, if even the very best developers want to use Python with their OpenBCI boards, they are left scratching their heads with where to begin. 47 | 48 | ### The solution 49 | 50 | The OpenBCI Python will: 51 | 52 | * Allow Python users to install one module and use any board they choose 53 | * Provide examples of using Python to port data to other apps like lab streaming layer 54 | * Perform the heavy lifting when extracting and transforming raw binary byte streams 55 | * Use unit tests to ensure perfect quality of core code 56 | 57 | Using this repo provides a building block for developing with Python. The goal for the Python library is to ***provide a stable Python driver for all OpenBCI Biosensors*** 58 | 59 | ## Who are we? 60 | 61 | The founder of the OpenBCI Python repository is Jermey Frey. The Python driver is one of the most popular repositories and has the most contributors! 62 | 63 | The contributors to these repos are people using Python mainly for their data acquisition and analytics. 64 | 65 | ## What do we need? 66 | 67 | **You**! In whatever way you can help. 68 | 69 | We need expertise in programming, user experience, software sustainability, documentation and technical writing and project management. 70 | 71 | We'd love your feedback along the way. 72 | 73 | Our primary goal is to provide a stable Python driver for all OpenBCI Biosensors, 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. 74 | 75 | ## Get involved 76 | 77 | 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). 78 | 79 | 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. 80 | 81 | ## Contact us 82 | 83 | 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. 84 | 85 | ## Find out more 86 | 87 | You might be interested in: 88 | 89 | * Purchase a [Cyton][link_shop_cyton] | [Ganglion][link_shop_ganglion] | [WiFi Shield][link_shop_wifi_shield] from [OpenBCI][link_openbci] 90 | * contact us at *contact@openbci.com* 91 | 92 | And of course, you'll want to know our: 93 | 94 | * [Contributors' guidelines](CONTRIBUTING.md) 95 | * [Roadmap](ROADMAP.md) 96 | 97 | ## Glossary 98 | 99 | OpenBCI boards are commonly referred to as _biosensors_. A biosensor converts biological data into digital data. 100 | 101 | The [Ganglion][link_shop_ganglion] has 4 channels, meaning the Ganglion can take four simultaneous voltage readings. 102 | 103 | The [Cyton][link_shop_cyton] has 8 channels and [Cyton with Daisy][link_shop_cyton_daisy] has 16 channels. 104 | 105 | Generally speaking, the Cyton records at a high quality with less noise. Noise is anything that is not signal. 106 | 107 | ## Thank you 108 | 109 | 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. 110 | 111 | ## Dependencies 112 | 113 | * Python 2.7 or later (https://www.python.org/download/releases/2.7/) 114 | * Numpy 1.7 or later (http://www.numpy.org/) 115 | * Yapsy -- if using pluging via `user.py` (http://yapsy.sourceforge.net/) 116 | 117 | NOTE: For comprehensive list see requirments.txt: (https://github.com/OpenBCI/OpenBCI_Python/blob/master/requirements.txt) 118 | 119 | OpenBCI 8 and 32 bit board with 8 or 16 channels. 120 | 121 | This library includes the OpenBCICyton and OpenBCIGanglion classes which are drivers for their respective devices. The OpenBCICyton class is designed to work on all systems, while the OpenBCIGanglion class relies on a Bluetooth driver that is only available on Linux, discussed in the next section. 122 | 123 | For additional details on connecting your Cyton board visit: http://docs.openbci.com/Hardware/02-Cyton 124 | 125 | ### Ganglion Board 126 | 127 | The Ganglion board relies on Bluetooth Low Energy connectivity (BLE), and our code relies on the BluePy library to communicate with it. The BluePy library currently only works on Linux-based operating systems. To use Ganglion you will need to install it: 128 | 129 | `pip install bluepy` 130 | 131 | You may be able to use the Ganglion board from a virtual machine (VM) running Linux on other operating systems, such as MacOS or Windows. See [this thread](https://github.com/OpenBCI/OpenBCI_Python/issues/68) for advice. 132 | 133 | You may need to alter the settings of your Bluetooth adapter in order to reduce latency and avoid packet drops -- e.g. if the terminal spams "Warning: Dropped 1 packets" several times a seconds, DO THAT. 134 | 135 | On Linux, assuming `hci0` is the name of your bluetooth adapter: 136 | 137 | `sudo bash -c 'echo 9 > /sys/kernel/debug/bluetooth/hci0/conn_min_interval'` 138 | 139 | `sudo bash -c 'echo 10 > /sys/kernel/debug/bluetooth/hci0/conn_max_interval'` 140 | 141 | ## Install 142 | 143 | ### Using PyPI 144 | 145 | ``` 146 | pip install openbci-python 147 | ``` 148 | 149 | Anaconda is not currently supported, if you want to use anaconda, you need to create a virtual environment in anaconda, activate it and use the above command to install it. 150 | 151 | ### From sources 152 | 153 | For the latest version, you can install the package from the sources using the setup.py script 154 | 155 | ``` 156 | python setup.py install 157 | ``` 158 | 159 | or in developer mode to be able to modify the sources. 160 | 161 | ``` 162 | python setup.py develop 163 | ``` 164 | 165 | ## Functionality 166 | 167 | ### Basic usage 168 | 169 | The startStreaming function of the Board object takes a callback function and begins streaming data from the board. Each packet it receives is then parsed as an OpenBCISample which is passed to the callback function as an argument. 170 | 171 | OpenBCISample members: 172 | -id: 173 | int from 0-255. Used to tell if packets were skipped. 174 | 175 | -channel_data: 176 | 8 int array with current voltage value of each channel (1-8) 177 | 178 | -aux_data: 179 | 3 int array with current auxiliary data. (0s by default) 180 | 181 | ### user.py 182 | 183 | This code provides a simple user interface (called user.py) to handle various plugins and communicate with the board. To use it, connect the board to your computer using the dongle (see http://docs.openbci.com/tutorials/01-GettingStarted for details). 184 | 185 | Then simply run the code given as an argument the port your board is connected to: 186 | Ex Linux: 187 | > $python user.py -p /dev/ttyUSB0 188 | 189 | The program should establish a serial connection and reset the board to default settings. When a '-->' appears, you can type a character (character map http://docs.openbci.com/software/01-OpenBCI_SDK) that will be sent to the board using ser.write. This allows you to change the settings on the board. 190 | 191 | A good first test is to try is to type '?': 192 | >--> ? 193 | 194 | This should output the current configuration settings on the board. 195 | 196 | Another test would be to change the board settings so that all the pins in the board are internally connected to a test (square) wave. To do this, type: 197 | 198 | >--> [ 199 | 200 | Alternatively, there are 6 test signals pre configured: 201 | 202 | > --> /test1 (connect all pins to ground) 203 | 204 | > --> /test2 (connect all pins to vcc) 205 | 206 | > --> /test3 (Connecting pins to low frequency 1x amp signal) 207 | 208 | > --> /test4 (Connecting pins to high frequency 1x amp signal) 209 | 210 | > --> /test5 (Connecting pins to low frequency 2x amp signal) 211 | 212 | > --> /test6 (Connecting pins to high frequency 2x amp signal) 213 | 214 | The / is used in the interface to execute a pre-configured command. Writing anything without a preceding '/' will automatically write those characters, one by one, to the board. 215 | 216 | For example, writing 217 | > -->x3020000X 218 | will do the following: 219 | 220 | ‘x’ enters Channel Settings mode. Channel 3 is set up to be powered up, with gain of 2, normal input, removed from BIAS generation, removed from SRB2, removed from SRB1. The final ‘X’ latches the settings to the ADS1299 channel settings register. 221 | 222 | Pre-configured commands that use the / prefix are: 223 | 224 | test (As explained above) 225 | 226 | > --> /test4 227 | 228 | start selected plugins (see below) 229 | 230 | > --> /start 231 | 232 | Adding the argument "T:number" will set a timeout on the start command. 233 | 234 | > --> /start T:5 235 | 236 | Stop the steam to issue new commands 237 | 238 | > --> /stop 239 | 240 | #### Useful commands: 241 | 242 | Writting to SD card a high frequency square wave (test5) for 3 seconds: 243 | ``` 244 | $ python user.py -p /dev/ttyUSB0 245 | User serial interface enabled... 246 | Connecting to /dev/ttyUSB0 247 | Serial established... 248 | View command map at http://docs.openbci.com. 249 | Type start to run. Type /exit to exit. 250 | 251 | --> 252 | OpenBCI V3 8bit Board 253 | Setting ADS1299 Channel Values 254 | ADS1299 Device ID: 0x3E 255 | LIS3DH Device ID: 0x33 256 | Free RAM: 447 257 | $$$ 258 | --> /test5 259 | Warning: Connecting pins to high frequency 2x amp signal 260 | 261 | --> a 262 | Corresponding SD file OBCI_18.TXT$$$ 263 | --> /start T:3 264 | ``` 265 | 266 | NOTES: 267 | 268 | When writing to the board and expecting a response, give the board a second. It sometimes lags and requires 269 | the user to hit enter on the user.py script until you get a response. 270 | 271 | #### Ganglion 272 | 273 | The Ganglion board is currently supported only on Linux. The communication is made directly through bluetooth (BLE), instead of using a dongle through a serial port. To launch the script, auto-detect the bluetooth MAC address of the nearby board and print values upon `/start`: 274 | 275 | > $sudo python user.py --board ganglion --add print 276 | 277 | Note that if you want to configure manually the board, the API differs from the Cyton, refer to the proper documentation, i.e. http://docs.openbci.com/OpenBCI%20Software/06-OpenBCI_Ganglion_SDK 278 | 279 | ### Plugins 280 | 281 | #### Use plugins 282 | 283 | Select the print plugin: 284 | 285 | > $python user.py -p /dev/ttyUSB0 --add print 286 | 287 | Plugin with optional parameter: 288 | 289 | > $python user.py -p /dev/ttyUSB0 --add csv_collect record.csv 290 | 291 | Select several plugins, e.g. streaming to OSC and displaying effective sample rate: 292 | 293 | > $python user.py -p /dev/ttyUSB0 --add streamer_osc --add sample_rate 294 | 295 | Change the plugin path: 296 | 297 | > $python user.py -p /dev/ttyUSB0 --add print --plugins-path /home/user/my_plugins 298 | 299 | Note: type `/start` to launch the selected plugins. 300 | 301 | #### Create new plugins 302 | 303 | Add new functionalities to user.py by creating new scripts inside the `plugins` folder. You class must inherit from yapsy.IPlugin, see below a minimal example with `print` plugin: 304 | 305 | ```python 306 | import plugin_interface as plugintypes 307 | 308 | class PluginPrint(plugintypes.IPluginExtended): 309 | def activate(self): 310 | print("Print activated") 311 | 312 | def deactivate(self): 313 | print("Goodbye") 314 | 315 | def show_help(self): 316 | print("I do not need any parameter, just printing stuff.") 317 | 318 | # called with each new sample 319 | def __call__(self, sample): 320 | print("----------------") 321 | print("%f" % sample.id) 322 | print(sample.channel_data) 323 | print(sample.aux_data) 324 | ``` 325 | 326 | Describe your plugin with a corresponding `print.yapsy-plugin`: 327 | 328 | ``` 329 | [Core] 330 | Name = print 331 | Module = print 332 | 333 | [Documentation] 334 | Author = Various 335 | Version = 0.1 336 | Description = Print board values on stdout 337 | ``` 338 | 339 | 340 | You're done, your plugin should be automatically detected by `user.py`. 341 | 342 | #### Existing plugins 343 | 344 | * `print`: Display sample values -- *verbose* output! 345 | 346 | * `csv_collect`: Export data to a csv file. 347 | 348 | * `sample_rate`: Print effective sampling rate averaged over XX seconds (default: 10). 349 | 350 | * `streamer_tcp`: Acts as a TCP server, using a "raw" protocol to send value. 351 | * The stream can be acquired with [OpenViBE](http://openvibe.inria.fr/) acquisition server, selecting telnet, big endian, float 32 bits, forcing 250 sampling rate (125 if daisy mode is used). 352 | * Default IP: localhost, default port: 12345 353 | 354 | * `streamer_osc`: Data is sent through OSC (UDP layer). 355 | * Default IP: localhost, default port: 12345, default stream name: `/openbci` 356 | * Requires pyosc. On linux type either `pip install --pre pyosc` as root, or `pip install --pre --user`. 357 | 358 | * `udp_server`: Very simple UDP server that sends data as json. Made to work with: https://github.com/OpenBCI/OpenBCI_Node 359 | * Default IP: 127.0.0.1, default port: 8888 360 | 361 | * `streamer_lsl`: Data is sent through [LSL](https://github.com/sccn/labstreaminglayer/). 362 | * Default EEG stream name "OpenBCI_EEG", ID "openbci_eeg_id1"; default AUX stream name "OpenBCI_AUX", ID "openbci_aux_id1". 363 | * Requires LSL library. Download last version from offcial site, e.g., ftp://sccn.ucsd.edu/pub/software/LSL/SDK/liblsl-Python-1.10.2.zip and unzip files in a "lib" folder at the same level as `user.py`. 364 | 365 | Tip: Type `python user.py --list` to list available plugins and `python user.py --help [plugin_name]` to get more information. 366 | 367 | ### Scripts 368 | 369 | In the `scripts` folder you will find code snippets that use directly the `OpenBCIBoard` class from `open_bci_v3.py`. 370 | 371 | Note: copy `open_bci_v3.py` there if you want to run the code -- no proper package yet. 372 | 373 | * `test.py`: minimal example, printing values. 374 | * `stream_data.py` a version of a TCP streaming server that somehow oversamples OpenBCI from 250 to 256Hz. 375 | * `upd_server.py` *DEPRECATED* (Use Plugin): see https://github.com/OpenBCI/OpenBCI_Node for implementation example. 376 | 377 | ## License: 378 | 379 | MIT 380 | 381 | [link_aj_keller]: https://github.com/aj-ptw 382 | [link_shop_wifi_shield]: https://shop.openbci.com/collections/frontpage/products/wifi-shield?variant=44534009550 383 | [link_shop_ganglion]: https://shop.openbci.com/collections/frontpage/products/pre-order-ganglion-board 384 | [link_shop_cyton]: https://shop.openbci.com/collections/frontpage/products/cyton-biosensing-board-8-channel 385 | [link_shop_cyton_daisy]: https://shop.openbci.com/collections/frontpage/products/cyton-daisy-biosensing-boards-16-channel 386 | [link_nodejs_cyton]: https://github.com/openbci/openbci_nodejs_cyton 387 | [link_nodejs_ganglion]: https://github.com/openbci/openbci_nodejs_ganglion 388 | [link_nodejs_wifi]: https://github.com/openbci/openbci_nodejs_wifi 389 | [link_javascript_utilities]: https://github.com/OpenBCI/OpenBCI_JavaScript_Utilities 390 | [link_openbci]: http://www.openbci.com 391 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | ## OpenBCI Python 4 | 5 | Provide a stable Python driver for all OpenBCI Biosensors 6 | 7 | ## Short term - what we're working on now 8 | 9 | - WiFi 10 | 11 | ## Medium term 12 | 13 | - Emotion detection 14 | - Default set of instructions to send to the board on startup via command line 15 | - Time sync with Cyton 16 | -------------------------------------------------------------------------------- /externals/mne_openbci.py: -------------------------------------------------------------------------------- 1 | """Conversion tool from OpenBCI to MNE Raw Class""" 2 | 3 | # Authors: Teon Brooks 4 | # 5 | # License: BSD (3-clause) 6 | 7 | import warnings 8 | 9 | np = None 10 | try: 11 | import numpy as np 12 | except ImportError: 13 | raise ImportError('Numpy is needed to use function.') 14 | mne = None 15 | try: 16 | from mne.utils import verbose, logger 17 | from mne.io.meas_info import create_info 18 | from mne.io.base import _BaseRaw 19 | except ImportError: 20 | raise ImportError('MNE is needed to use function.') 21 | 22 | 23 | class RawOpenBCI(_BaseRaw): 24 | """Raw object from OpenBCI file 25 | 26 | Parameters 27 | ---------- 28 | input_fname : str 29 | Path to the OpenBCI file. 30 | montage : str | None | instance of Montage 31 | Path or instance of montage containing electrode positions. 32 | If None, sensor locations are (0,0,0). See the documentation of 33 | :func:`mne.channels.read_montage` for more information. 34 | eog : list or tuple 35 | Names of channels or list of indices that should be designated 36 | EOG channels. Default is None. 37 | misc : list or tuple 38 | List of indices that should be designated MISC channels. 39 | Default is (-3, -2, -1), which are the accelerator sensors. 40 | stim_channel : int | None 41 | The channel index (starting at 0). 42 | If None (default), there will be no stim channel added. 43 | scale : float 44 | The scaling factor for EEG data. Units for MNE are in volts. 45 | OpenBCI data are typically stored in microvolts. Default scale 46 | factor is 1e-6. 47 | sfreq : int 48 | The sampling frequency of the data. OpenBCI defaults are 250 Hz. 49 | missing_tol : int 50 | The tolerance for interpolating missing samples. Default is 1. If the 51 | number of contiguous missing samples is greater than tolerance, then 52 | values are marked as NaN. 53 | preload : bool 54 | If True, all data are loaded at initialization. 55 | If False, data are not read until save. 56 | verbose : bool, str, int, or None 57 | If not None, override default verbose level (see mne.verbose). 58 | 59 | 60 | See Also 61 | -------- 62 | mne.io.Raw : Documentation of attribute and methods. 63 | """ 64 | 65 | @verbose 66 | def __init__(self, input_fname, montage=None, eog=None, 67 | misc=(-3, -2, -1), stim_channel=None, scale=1e-6, sfreq=250, 68 | missing_tol=1, preload=True, verbose=None): 69 | 70 | bci_info = {'missing_tol': missing_tol, 'stim_channel': stim_channel} 71 | if not eog: 72 | eog = list() 73 | if not misc: 74 | misc = list() 75 | nsamps, nchan = self._get_data_dims(input_fname) 76 | 77 | last_samps = [nsamps - 1] 78 | ch_names = ['EEG %03d' % num for num in range(1, nchan + 1)] 79 | ch_types = ['eeg'] * nchan 80 | if misc: 81 | misc_names = ['MISC %03d' % ii for ii in range(1, len(misc) + 1)] 82 | misc_types = ['misc'] * len(misc) 83 | for ii, mi in enumerate(misc): 84 | ch_names[mi] = misc_names[ii] 85 | ch_types[mi] = misc_types[ii] 86 | if eog: 87 | eog_names = ['EOG %03d' % ii for ii in range(len(eog))] 88 | eog_types = ['eog'] * len(eog) 89 | for ii, ei in enumerate(eog): 90 | ch_names[ei] = eog_names[ii] 91 | ch_types[ei] = eog_types[ii] 92 | if stim_channel: 93 | ch_names[stim_channel] = 'STI 014' 94 | ch_types[stim_channel] = 'stim' 95 | 96 | # fix it for eog and misc marking 97 | info = create_info(ch_names, sfreq, ch_types, montage) 98 | super(RawOpenBCI, self).__init__(info, last_samps=last_samps, 99 | raw_extras=[bci_info], 100 | filenames=[input_fname], 101 | preload=False, verbose=verbose) 102 | # load data 103 | if preload: 104 | self.preload = preload 105 | logger.info('Reading raw data from %s...' % input_fname) 106 | self._data, _ = self._read_segment() 107 | 108 | def _read_segment_file(self, data, idx, offset, fi, start, stop, 109 | cals, mult): 110 | """Read a chunk of raw data""" 111 | input_fname = self._filenames[fi] 112 | data_ = np.genfromtxt(input_fname, delimiter=',', comments='%', 113 | skip_footer=1) 114 | """ 115 | Dealing with the missing data 116 | ----------------------------- 117 | When recording with OpenBCI over Bluetooth, it is possible for some of 118 | the data packets, samples, to not be recorded. This does not happen 119 | often but it poses a problem for maintaining proper sampling periods. 120 | OpenBCI data format combats this by providing a counter on the sample 121 | to know which ones are missing. 122 | 123 | Solution 124 | -------- 125 | Interpolate the missing samples by resampling the surrounding samples. 126 | 1. Find where the missing samples are. 127 | 2. Deal with the counter reset (resets after cycling a byte). 128 | 3. Resample given the diffs. 129 | 4. Insert resampled data in the array using the diff indices 130 | (index + 1). 131 | 5. If number of missing samples is greater than the missing_tol, Values 132 | are replaced with np.nan. 133 | """ 134 | # counter goes from 0 to 255, maxdiff is 255. 135 | # make diff one like others. 136 | missing_tol = self._raw_extras[fi]['missing_tol'] 137 | diff = np.abs(np.diff(data_[:, 0])) 138 | diff = np.mod(diff, 254) - 1 139 | missing_idx = np.where(diff != 0)[0] 140 | missing_samps = diff[missing_idx].astype(int) 141 | 142 | if missing_samps.size: 143 | missing_nsamps = np.sum(missing_samps, dtype=int) 144 | missing_cumsum = np.insert(np.cumsum(missing_samps), 0, 0)[:-1] 145 | missing_data = np.empty((missing_nsamps, data_.shape[-1]), 146 | dtype=float) 147 | insert_idx = list() 148 | for idx_, nn, ii in zip(missing_idx, missing_samps, 149 | missing_cumsum): 150 | missing_data[ii:ii + nn] = np.mean(data_[(idx_, idx_ + 1), :]) 151 | if nn > missing_tol: 152 | missing_data[ii:ii + nn] *= np.nan 153 | warnings.warn('The number of missing samples exceeded the ' 154 | 'missing_tol threshold.') 155 | insert_idx.append([idx_] * nn) 156 | insert_idx = np.hstack(insert_idx) 157 | data_ = np.insert(data_, insert_idx, missing_data, axis=0) 158 | # data_ dimensions are samples by channels. transpose for MNE. 159 | data_ = data_[start:stop, 1:].T 160 | data[:, offset:offset + stop - start] = \ 161 | np.dot(mult, data_[idx]) if mult is not None else data_[idx] 162 | 163 | def _get_data_dims(self, input_fname): 164 | """Briefly scan the data file for info""" 165 | # raw data formatting is nsamps by nchans + counter 166 | data = np.genfromtxt(input_fname, delimiter=',', comments='%', 167 | skip_footer=1) 168 | diff = np.abs(np.diff(data[:, 0])) 169 | diff = np.mod(diff, 254) - 1 170 | missing_idx = np.where(diff != 0)[0] 171 | missing_samps = diff[missing_idx].astype(int) 172 | nsamps, nchan = data.shape 173 | # add the missing samples 174 | nsamps += sum(missing_samps) 175 | # remove the tracker column 176 | nchan -= 1 177 | del data 178 | 179 | return nsamps, nchan 180 | 181 | 182 | def read_raw_openbci(input_fname, montage=None, eog=None, misc=(-3, -2, -1), 183 | stim_channel=None, scale=1e-6, sfreq=250, missing_tol=1, 184 | preload=True, verbose=None): 185 | """Raw object from OpenBCI file 186 | 187 | Parameters 188 | ---------- 189 | input_fname : str 190 | Path to the OpenBCI file. 191 | montage : str | None | instance of Montage 192 | Path or instance of montage containing electrode positions. 193 | If None, sensor locations are (0,0,0). See the documentation of 194 | :func:`mne.channels.read_montage` for more information. 195 | eog : list or tuple 196 | Names of channels or list of indices that should be designated 197 | EOG channels. Default is None. 198 | misc : list or tuple 199 | List of indices that should be designated MISC channels. 200 | Default is (-3, -2, -1), which are the accelerator sensors. 201 | stim_channel : str | int | None 202 | The channel name or channel index (starting at 0). 203 | -1 corresponds to the last channel (default). 204 | If None, there will be no stim channel added. 205 | scale : float 206 | The scaling factor for EEG data. Units for MNE are in volts. 207 | OpenBCI data are typically stored in microvolts. Default scale 208 | factor is 1e-6. 209 | sfreq : int 210 | The sampling frequency of the data. OpenBCI defaults are 250 Hz. 211 | missing_tol : int 212 | The tolerance for interpolating missing samples. Default is 1. If the 213 | number of contiguous missing samples is greater than tolerance, then 214 | values are marked as NaN. 215 | preload : bool 216 | If True, all data are loaded at initialization. 217 | If False, data are not read until save. 218 | verbose : bool, str, int, or None 219 | If not None, override default verbose level (see mne.verbose). 220 | 221 | Returns 222 | ------- 223 | raw : Instance of RawOpenBCI 224 | A Raw object containing OpenBCI data. 225 | 226 | 227 | See Also 228 | -------- 229 | mne.io.Raw : Documentation of attribute and methods. 230 | """ 231 | raw = RawOpenBCI(input_fname=input_fname, montage=montage, eog=eog, 232 | misc=misc, stim_channel=stim_channel, scale=scale, 233 | sfreq=sfreq, missing_tol=missing_tol, preload=preload, 234 | verbose=verbose) 235 | return raw 236 | -------------------------------------------------------------------------------- /images/openbci_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openbci-archive/OpenBCI_Python/1a8a0a1f8c9158b6c6a4714d605ba781c2630f64/images/openbci_large.png -------------------------------------------------------------------------------- /openbci/__init__.py: -------------------------------------------------------------------------------- 1 | from .cyton import OpenBCICyton 2 | from .plugins import * 3 | from .utils import * 4 | from .wifi import OpenBCIWiFi 5 | if sys.platform.startswith("linux"): 6 | from .ganglion import OpenBCIGanglion 7 | -------------------------------------------------------------------------------- /openbci/cyton.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core OpenBCI object for handling connections and samples from the board. 3 | 4 | EXAMPLE USE: 5 | 6 | def handle_sample(sample): 7 | print(sample.channel_data) 8 | 9 | board = OpenBCIBoard() 10 | board.print_register_settings() 11 | board.start_streaming(handle_sample) 12 | 13 | NOTE: If daisy modules is enabled, the callback will occur every two samples, hence "packet_id" 14 | will only contain even numbers. As a side effect, the sampling rate will be divided by 2. 15 | 16 | FIXME: at the moment we can just force daisy mode, do not check that the module is detected. 17 | TODO: enable impedance 18 | 19 | """ 20 | from __future__ import print_function 21 | import serial 22 | import struct 23 | import numpy as np 24 | import time 25 | import timeit 26 | import atexit 27 | import logging 28 | import threading 29 | import sys 30 | import pdb 31 | import glob 32 | 33 | SAMPLE_RATE = 250.0 # Hz 34 | START_BYTE = 0xA0 # start of data packet 35 | END_BYTE = 0xC0 # end of data packet 36 | ADS1299_Vref = 4.5 # reference voltage for ADC in ADS1299. set by its hardware 37 | ADS1299_gain = 24.0 # assumed gain setting for ADS1299. set by its Arduino code 38 | scale_fac_uVolts_per_count = ADS1299_Vref / \ 39 | float((pow(2, 23) - 1)) / ADS1299_gain * 1000000. 40 | scale_fac_accel_G_per_count = 0.002 / \ 41 | (pow(2, 4)) # assume set to +/4G, so 2 mG 42 | ''' 43 | #Commands for in SDK http://docs.openbci.com/software/01-Open BCI_SDK: 44 | 45 | command_stop = "s"; 46 | command_startText = "x"; 47 | command_startBinary = "b"; 48 | command_startBinary_wAux = "n"; 49 | command_startBinary_4chan = "v"; 50 | command_activateFilters = "F"; 51 | command_deactivateFilters = "g"; 52 | command_deactivate_channel = {"1", "2", "3", "4", "5", "6", "7", "8"}; 53 | command_activate_channel = {"q", "w", "e", "r", "t", "y", "u", "i"}; 54 | command_activate_leadoffP_channel = {"!", "@", "#", "$", "%", "^", "&", "*"}; //shift + 1-8 55 | command_deactivate_leadoffP_channel = {"Q", "W", "E", "R", "T", "Y", "U", "I"}; //letters (plus shift) right below 1-8 56 | command_activate_leadoffN_channel = {"A", "S", "D", "F", "G", "H", "J", "K"}; //letters (plus shift) below the letters below 1-8 57 | command_deactivate_leadoffN_channel = {"Z", "X", "C", "V", "B", "N", "M", "<"}; //letters (plus shift) below the letters below the letters below 1-8 58 | command_biasAuto = "`"; 59 | command_biasFixed = "~"; 60 | ''' 61 | 62 | 63 | class OpenBCICyton(object): 64 | """ 65 | Handle a connection to an OpenBCI board. 66 | 67 | Args: 68 | port: The port to connect to. 69 | baud: The baud of the serial connection. 70 | daisy: Enable or disable daisy module and 16 chans readings 71 | aux, impedance: unused, for compatibility with ganglion API 72 | """ 73 | 74 | def __init__(self, port=None, baud=115200, filter_data=True, scaled_output=True, 75 | daisy=False, aux=False, impedance=False, log=True, timeout=None): 76 | self.log = log # print_incoming_text needs log 77 | self.streaming = False 78 | self.baudrate = baud 79 | self.timeout = timeout 80 | if not port: 81 | port = self.find_port() 82 | self.port = port 83 | # might be handy to know API 84 | self.board_type = "cyton" 85 | print("Connecting to V3 at port %s" % (port)) 86 | if port == "loop://": 87 | # For testing purposes 88 | self.ser = serial.serial_for_url(port, baudrate=baud, timeout=timeout) 89 | else: 90 | self.ser = serial.Serial(port=port, baudrate=baud, timeout=timeout) 91 | 92 | print("Serial established...") 93 | 94 | time.sleep(2) 95 | # Initialize 32-bit board, doesn't affect 8bit board 96 | self.ser.write(b'v') 97 | 98 | # wait for device to be ready 99 | time.sleep(1) 100 | if port != "loop://": 101 | self.print_incoming_text() 102 | 103 | self.streaming = False 104 | self.filtering_data = filter_data 105 | self.scaling_output = scaled_output 106 | # number of EEG channels per sample *from the board* 107 | self.eeg_channels_per_sample = 8 108 | # number of AUX channels per sample *from the board* 109 | self.aux_channels_per_sample = 3 110 | self.imp_channels_per_sample = 0 # impedance check not supported at the moment 111 | self.read_state = 0 112 | self.daisy = daisy 113 | self.last_odd_sample = OpenBCISample(-1, [], []) # used for daisy 114 | self.log_packet_count = 0 115 | self.attempt_reconnect = False 116 | self.last_reconnect = 0 117 | self.reconnect_freq = 5 118 | self.packets_dropped = 0 119 | 120 | # Disconnects from board when terminated 121 | atexit.register(self.disconnect) 122 | 123 | def getBoardType(self): 124 | """ Returns the version of the board """ 125 | return self.board_type 126 | 127 | def setImpedance(self, flag): 128 | """ Enable/disable impedance measure. Not implemented at the moment on Cyton. """ 129 | return 130 | 131 | def ser_write(self, b): 132 | """Access serial port object for write""" 133 | self.ser.write(b) 134 | 135 | def ser_read(self): 136 | """Access serial port object for read""" 137 | return self.ser.read() 138 | 139 | def ser_inWaiting(self): 140 | """Access serial port object for inWaiting""" 141 | return self.ser.inWaiting() 142 | 143 | def getSampleRate(self): 144 | if self.daisy: 145 | return SAMPLE_RATE / 2 146 | else: 147 | return SAMPLE_RATE 148 | 149 | def getNbEEGChannels(self): 150 | if self.daisy: 151 | return self.eeg_channels_per_sample * 2 152 | else: 153 | return self.eeg_channels_per_sample 154 | 155 | def getNbAUXChannels(self): 156 | return self.aux_channels_per_sample 157 | 158 | def getNbImpChannels(self): 159 | return self.imp_channels_per_sample 160 | 161 | def start_streaming(self, callback, lapse=-1): 162 | """ 163 | Start handling streaming data from the board. Call a provided callback 164 | for every single sample that is processed (every two samples with daisy module). 165 | 166 | Args: 167 | callback: A callback function, or a list of functions, that will receive a single 168 | argument of the OpenBCISample object captured. 169 | """ 170 | if not self.streaming: 171 | self.ser.write(b'b') 172 | self.streaming = True 173 | 174 | start_time = timeit.default_timer() 175 | 176 | # Enclose callback funtion in a list if it comes alone 177 | if not isinstance(callback, list): 178 | callback = [callback] 179 | 180 | # Initialize check connection 181 | self.check_connection() 182 | 183 | while self.streaming: 184 | 185 | # read current sample 186 | sample = self._read_serial_binary() 187 | # if a daisy module is attached, wait to concatenate two samples 188 | # (main board + daisy) before passing it to callback 189 | if self.daisy: 190 | # odd sample: daisy sample, save for later 191 | if ~sample.id % 2: 192 | self.last_odd_sample = sample 193 | # even sample: concatenate and send if last sample was the fist part, 194 | # otherwise drop the packet 195 | elif sample.id - 1 == self.last_odd_sample.id: 196 | # the aux data will be the average between the two samples, as the channel 197 | # samples themselves have been averaged by the board 198 | avg_aux_data = list( 199 | (np.array(sample.aux_data) + np.array(self.last_odd_sample.aux_data)) / 2) 200 | whole_sample = OpenBCISample(sample.id, 201 | sample.channel_data + 202 | self.last_odd_sample.channel_data, 203 | avg_aux_data) 204 | for call in callback: 205 | call(whole_sample) 206 | else: 207 | for call in callback: 208 | call(sample) 209 | 210 | if lapse > 0 and (timeit.default_timer() - start_time) > lapse: 211 | self.stop() 212 | if self.log: 213 | self.log_packet_count = self.log_packet_count + 1 214 | 215 | """ 216 | PARSER: 217 | Parses incoming data packet into OpenBCISample. 218 | Incoming Packet Structure: 219 | Start Byte(1)|Sample ID(1)|Channel Data(24)|Aux Data(6)|End Byte(1) 220 | 0xA0|0-255|8, 3-byte signed ints|3 2-byte signed ints|0xC0 221 | 222 | """ 223 | 224 | def _read_serial_binary(self, max_bytes_to_skip=3000): 225 | def read(n): 226 | bb = self.ser.read(n) 227 | if not bb: 228 | self.warn('Device appears to be stalled. Quitting...') 229 | sys.exit() 230 | raise Exception('Device Stalled') 231 | sys.exit() 232 | return '\xFF' 233 | else: 234 | return bb 235 | 236 | for rep in range(max_bytes_to_skip): 237 | 238 | # ---------Start Byte & ID--------- 239 | if self.read_state == 0: 240 | 241 | b = read(1) 242 | 243 | if struct.unpack('B', b)[0] == START_BYTE: 244 | if (rep != 0): 245 | self.warn( 246 | 'Skipped %d bytes before start found' % (rep)) 247 | rep = 0 248 | # packet id goes from 0-255 249 | packet_id = struct.unpack('B', read(1))[0] 250 | log_bytes_in = str(packet_id) 251 | 252 | self.read_state = 1 253 | 254 | # ---------Channel Data--------- 255 | elif self.read_state == 1: 256 | channel_data = [] 257 | for c in range(self.eeg_channels_per_sample): 258 | 259 | # 3 byte ints 260 | literal_read = read(3) 261 | 262 | unpacked = struct.unpack('3B', literal_read) 263 | log_bytes_in = log_bytes_in + '|' + str(literal_read) 264 | 265 | # 3byte int in 2s compliment 266 | if (unpacked[0] > 127): 267 | pre_fix = bytes(bytearray.fromhex('FF')) 268 | else: 269 | pre_fix = bytes(bytearray.fromhex('00')) 270 | 271 | literal_read = pre_fix + literal_read 272 | 273 | # unpack little endian(>) signed integer(i) 274 | # (makes unpacking platform independent) 275 | myInt = struct.unpack('>i', literal_read)[0] 276 | 277 | if self.scaling_output: 278 | channel_data.append(myInt * scale_fac_uVolts_per_count) 279 | else: 280 | channel_data.append(myInt) 281 | 282 | self.read_state = 2 283 | 284 | # ---------Accelerometer Data--------- 285 | elif self.read_state == 2: 286 | aux_data = [] 287 | for a in range(self.aux_channels_per_sample): 288 | 289 | # short = h 290 | acc = struct.unpack('>h', read(2))[0] 291 | log_bytes_in = log_bytes_in + '|' + str(acc) 292 | 293 | if self.scaling_output: 294 | aux_data.append(acc * scale_fac_accel_G_per_count) 295 | else: 296 | aux_data.append(acc) 297 | 298 | self.read_state = 3 299 | # ---------End Byte--------- 300 | elif self.read_state == 3: 301 | val = struct.unpack('B', read(1))[0] 302 | log_bytes_in = log_bytes_in + '|' + str(val) 303 | self.read_state = 0 # read next packet 304 | if (val == END_BYTE): 305 | sample = OpenBCISample(packet_id, channel_data, aux_data) 306 | self.packets_dropped = 0 307 | return sample 308 | else: 309 | self.warn("ID:<%d> instead of <%s>" 310 | % (packet_id, val, END_BYTE)) 311 | logging.debug(log_bytes_in) 312 | self.packets_dropped = self.packets_dropped + 1 313 | 314 | """ 315 | 316 | Clean Up (atexit) 317 | 318 | """ 319 | 320 | def stop(self): 321 | print("Stopping streaming...\nWait for buffer to flush...") 322 | self.streaming = False 323 | self.ser.write(b's') 324 | if self.log: 325 | logging.warning('sent : stopped streaming') 326 | 327 | def disconnect(self): 328 | if (self.streaming == True): 329 | self.stop() 330 | if (self.ser.isOpen()): 331 | print("Closing Serial...") 332 | self.ser.close() 333 | logging.warning('serial closed') 334 | 335 | """ 336 | 337 | SETTINGS AND HELPERS 338 | 339 | """ 340 | 341 | def warn(self, text): 342 | if self.log: 343 | # log how many packets where sent succesfully in between warnings 344 | if self.log_packet_count: 345 | logging.info('Data packets received:' + 346 | str(self.log_packet_count)) 347 | self.log_packet_count = 0 348 | logging.warning(text) 349 | print("Warning: %s" % text) 350 | 351 | def print_incoming_text(self): 352 | """ 353 | 354 | When starting the connection, print all the debug data until 355 | we get to a line with the end sequence '$$$'. 356 | 357 | """ 358 | line = '' 359 | # Wait for device to send data 360 | time.sleep(1) 361 | 362 | if self.ser.inWaiting(): 363 | line = '' 364 | c = '' 365 | # Look for end sequence $$$ 366 | while '$$$' not in line: 367 | # we're supposed to get UTF8 text, but the board might behave otherwise 368 | c = self.ser.read().decode('utf-8', 369 | errors='replace') 370 | line += c 371 | print(line) 372 | else: 373 | self.warn("No Message") 374 | 375 | def openbci_id(self, serial): 376 | """ 377 | 378 | When automatically detecting port, parse the serial return for the "OpenBCI" ID. 379 | 380 | """ 381 | line = '' 382 | # Wait for device to send data 383 | time.sleep(2) 384 | 385 | if serial.inWaiting(): 386 | line = '' 387 | c = '' 388 | # Look for end sequence $$$ 389 | while '$$$' not in line: 390 | # we're supposed to get UTF8 text, but the board might behave otherwise 391 | c = serial.read().decode('utf-8', 392 | errors='replace') 393 | line += c 394 | if "OpenBCI" in line: 395 | return True 396 | return False 397 | 398 | def print_register_settings(self): 399 | self.ser.write(b'?') 400 | time.sleep(0.5) 401 | self.print_incoming_text() 402 | 403 | # DEBBUGING: Prints individual incoming bytes 404 | def print_bytes_in(self): 405 | if not self.streaming: 406 | self.ser.write(b'b') 407 | self.streaming = True 408 | while self.streaming: 409 | print(struct.unpack('B', self.ser.read())[0]) 410 | 411 | '''Incoming Packet Structure: 412 | Start Byte(1)|Sample ID(1)|Channel Data(24)|Aux Data(6)|End Byte(1) 413 | 0xA0|0-255|8, 3-byte signed ints|3 2-byte signed ints|0xC0''' 414 | 415 | def print_packets_in(self): 416 | while self.streaming: 417 | b = struct.unpack('B', self.ser.read())[0] 418 | 419 | if b == START_BYTE: 420 | self.attempt_reconnect = False 421 | if skipped_str: 422 | logging.debug('SKIPPED\n' + skipped_str + '\nSKIPPED') 423 | skipped_str = '' 424 | 425 | packet_str = "%03d" % (b) + '|' 426 | b = struct.unpack('B', self.ser.read())[0] 427 | packet_str = packet_str + "%03d" % (b) + '|' 428 | 429 | # data channels 430 | for i in range(24 - 1): 431 | b = struct.unpack('B', self.ser.read())[0] 432 | packet_str = packet_str + '.' + "%03d" % (b) 433 | 434 | b = struct.unpack('B', self.ser.read())[0] 435 | packet_str = packet_str + '.' + "%03d" % (b) + '|' 436 | 437 | # aux channels 438 | for i in range(6 - 1): 439 | b = struct.unpack('B', self.ser.read())[0] 440 | packet_str = packet_str + '.' + "%03d" % (b) 441 | 442 | b = struct.unpack('B', self.ser.read())[0] 443 | packet_str = packet_str + '.' + "%03d" % (b) + '|' 444 | 445 | # end byte 446 | b = struct.unpack('B', self.ser.read())[0] 447 | 448 | # Valid Packet 449 | if b == END_BYTE: 450 | packet_str = packet_str + '.' + "%03d" % (b) + '|VAL' 451 | print(packet_str) 452 | # logging.debug(packet_str) 453 | 454 | # Invalid Packet 455 | else: 456 | packet_str = packet_str + '.' + "%03d" % (b) + '|INV' 457 | # Reset 458 | self.attempt_reconnect = True 459 | 460 | else: 461 | print(b) 462 | if b == END_BYTE: 463 | skipped_str = skipped_str + '|END|' 464 | else: 465 | skipped_str = skipped_str + "%03d" % (b) + '.' 466 | 467 | if self.attempt_reconnect and \ 468 | (timeit.default_timer() - self.last_reconnect) > self.reconnect_freq: 469 | self.last_reconnect = timeit.default_timer() 470 | self.warn('Reconnecting') 471 | self.reconnect() 472 | 473 | def check_connection(self, interval=2, max_packets_to_skip=10): 474 | # stop checking when we're no longer streaming 475 | if not self.streaming: 476 | return 477 | # check number of dropped packages and establish connection problem if too large 478 | if self.packets_dropped > max_packets_to_skip: 479 | # if error, attempt to reconnect 480 | self.reconnect() 481 | # check again again in 2 seconds 482 | threading.Timer(interval, self.check_connection).start() 483 | 484 | def reconnect(self): 485 | self.packets_dropped = 0 486 | self.warn('Reconnecting') 487 | self.stop() 488 | time.sleep(0.5) 489 | self.ser.write(b'v') 490 | time.sleep(0.5) 491 | self.ser.write(b'b') 492 | time.sleep(0.5) 493 | self.streaming = True 494 | # self.attempt_reconnect = False 495 | 496 | # Adds a filter at 60hz to cancel out ambient electrical noise 497 | def enable_filters(self): 498 | self.ser.write(b'f') 499 | self.filtering_data = True 500 | 501 | def disable_filters(self): 502 | self.ser.write(b'g') 503 | self.filtering_data = False 504 | 505 | def test_signal(self, signal): 506 | """ Enable / disable test signal """ 507 | if signal == 0: 508 | self.ser.write(b'0') 509 | self.warn("Connecting all pins to ground") 510 | elif signal == 1: 511 | self.ser.write(b'p') 512 | self.warn("Connecting all pins to Vcc") 513 | elif signal == 2: 514 | self.ser.write(b'-') 515 | self.warn("Connecting pins to low frequency 1x amp signal") 516 | elif signal == 3: 517 | self.ser.write(b'=') 518 | self.warn("Connecting pins to high frequency 1x amp signal") 519 | elif signal == 4: 520 | self.ser.write(b'[') 521 | self.warn("Connecting pins to low frequency 2x amp signal") 522 | elif signal == 5: 523 | self.ser.write(b']') 524 | self.warn("Connecting pins to high frequency 2x amp signal") 525 | else: 526 | self.warn("%s is not a known test signal. Valid signals go from 0-5" % signal) 527 | 528 | def set_channel(self, channel, toggle_position): 529 | """ Enable / disable channels """ 530 | # Commands to set toggle to on position 531 | if toggle_position == 1: 532 | if channel is 1: 533 | self.ser.write(b'!') 534 | if channel is 2: 535 | self.ser.write(b'@') 536 | if channel is 3: 537 | self.ser.write(b'#') 538 | if channel is 4: 539 | self.ser.write(b'$') 540 | if channel is 5: 541 | self.ser.write(b'%') 542 | if channel is 6: 543 | self.ser.write(b'^') 544 | if channel is 7: 545 | self.ser.write(b'&') 546 | if channel is 8: 547 | self.ser.write(b'*') 548 | if channel is 9 and self.daisy: 549 | self.ser.write(b'Q') 550 | if channel is 10 and self.daisy: 551 | self.ser.write(b'W') 552 | if channel is 11 and self.daisy: 553 | self.ser.write(b'E') 554 | if channel is 12 and self.daisy: 555 | self.ser.write(b'R') 556 | if channel is 13 and self.daisy: 557 | self.ser.write(b'T') 558 | if channel is 14 and self.daisy: 559 | self.ser.write(b'Y') 560 | if channel is 15 and self.daisy: 561 | self.ser.write(b'U') 562 | if channel is 16 and self.daisy: 563 | self.ser.write(b'I') 564 | # Commands to set toggle to off position 565 | elif toggle_position == 0: 566 | if channel is 1: 567 | self.ser.write(b'1') 568 | if channel is 2: 569 | self.ser.write(b'2') 570 | if channel is 3: 571 | self.ser.write(b'3') 572 | if channel is 4: 573 | self.ser.write(b'4') 574 | if channel is 5: 575 | self.ser.write(b'5') 576 | if channel is 6: 577 | self.ser.write(b'6') 578 | if channel is 7: 579 | self.ser.write(b'7') 580 | if channel is 8: 581 | self.ser.write(b'8') 582 | if channel is 9 and self.daisy: 583 | self.ser.write(b'q') 584 | if channel is 10 and self.daisy: 585 | self.ser.write(b'w') 586 | if channel is 11 and self.daisy: 587 | self.ser.write(b'e') 588 | if channel is 12 and self.daisy: 589 | self.ser.write(b'r') 590 | if channel is 13 and self.daisy: 591 | self.ser.write(b't') 592 | if channel is 14 and self.daisy: 593 | self.ser.write(b'y') 594 | if channel is 15 and self.daisy: 595 | self.ser.write(b'u') 596 | if channel is 16 and self.daisy: 597 | self.ser.write(b'i') 598 | 599 | def find_port(self): 600 | # Finds the serial port names 601 | if sys.platform.startswith('win'): 602 | ports = ['COM%s' % (i + 1) for i in range(256)] 603 | elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): 604 | ports = glob.glob('/dev/ttyUSB*') 605 | elif sys.platform.startswith('darwin'): 606 | ports = glob.glob('/dev/tty.usbserial*') 607 | else: 608 | raise EnvironmentError('Error finding ports on your operating system') 609 | openbci_port = '' 610 | for port in ports: 611 | try: 612 | s = serial.Serial(port=port, baudrate=self.baudrate, timeout=self.timeout) 613 | s.write(b'v') 614 | openbci_serial = self.openbci_id(s) 615 | s.close() 616 | if openbci_serial: 617 | openbci_port = port 618 | except (OSError, serial.SerialException): 619 | pass 620 | if openbci_port == '': 621 | raise OSError('Cannot find OpenBCI port') 622 | else: 623 | return openbci_port 624 | 625 | 626 | class OpenBCISample(object): 627 | """Object encapulsating a single sample from the OpenBCI board. 628 | NB: dummy imp for plugin compatiblity 629 | """ 630 | 631 | def __init__(self, packet_id, channel_data, aux_data): 632 | self.id = packet_id 633 | self.channel_data = channel_data 634 | self.aux_data = aux_data 635 | self.imp_data = [] 636 | -------------------------------------------------------------------------------- /openbci/plugins/README.md: -------------------------------------------------------------------------------- 1 | 2 | To create a new plugin, see print.py and print.yapsy-plugin for a minimal example and plugin_interface.py for documentation about more advanced features. 3 | 4 | Note: "__init__" will be automatically called when the main program loads, even if the plugin is not used, put computationally intensive instructions in activate() instead. 5 | -------------------------------------------------------------------------------- /openbci/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from .csv_collect import * 2 | from .noise_test import * 3 | from .streamer_lsl import * 4 | from .streamer_osc import * 5 | from .streamer_tcp_server import * 6 | from .udp_server import * 7 | 8 | __version__ = "1.0.0" 9 | -------------------------------------------------------------------------------- /openbci/plugins/csv_collect.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import csv 3 | import timeit 4 | import datetime 5 | 6 | import plugin_interface as plugintypes 7 | 8 | 9 | class PluginCSVCollect(plugintypes.IPluginExtended): 10 | def __init__(self, file_name="collect.csv", delim=",", verbose=False): 11 | now = datetime.datetime.now() 12 | self.time_stamp = '%d-%d-%d_%d-%d-%d' \ 13 | % (now.year, now.month, now.day, now.hour, now.minute, now.second) 14 | self.file_name = self.time_stamp 15 | self.start_time = timeit.default_timer() 16 | self.delim = delim 17 | self.verbose = verbose 18 | 19 | def activate(self): 20 | if len(self.args) > 0: 21 | if 'no_time' in self.args: 22 | self.file_name = self.args[0] 23 | else: 24 | self.file_name = self.args[0] + '_' + self.file_name 25 | if 'verbose' in self.args: 26 | self.verbose = True 27 | 28 | self.file_name = self.file_name + '.csv' 29 | print("Will export CSV to:" + self.file_name) 30 | # Open in append mode 31 | with open(self.file_name, 'a') as f: 32 | f.write('%' + self.time_stamp + '\n') 33 | 34 | def deactivate(self): 35 | print("Closing, CSV saved to:" + self.file_name) 36 | return 37 | 38 | def show_help(self): 39 | print("Optional argument: [filename] (default: collect.csv)") 40 | 41 | def __call__(self, sample): 42 | t = timeit.default_timer() - self.start_time 43 | 44 | # print(timeSinceStart|Sample Id) 45 | if self.verbose: 46 | print("CSV: %f | %d" % (t, sample.id)) 47 | 48 | row = '' 49 | row += str(t) 50 | row += self.delim 51 | row += str(sample.id) 52 | row += self.delim 53 | for i in sample.channel_data: 54 | row += str(i) 55 | row += self.delim 56 | for i in sample.aux_data: 57 | row += str(i) 58 | row += self.delim 59 | # remove last comma 60 | row += '\n' 61 | with open(self.file_name, 'a') as f: 62 | f.write(row) 63 | -------------------------------------------------------------------------------- /openbci/plugins/csv_collect.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = csv_collect 3 | Module = csv_collect 4 | 5 | [Documentation] 6 | Author = Various 7 | Version = 0.1 8 | Description = Write cvs data 9 | -------------------------------------------------------------------------------- /openbci/plugins/noise_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import timeit 3 | import numpy as np 4 | import plugin_interface as plugintypes 5 | 6 | 7 | class PluginNoiseTest(plugintypes.IPluginExtended): 8 | # update counters value 9 | def __call__(self, sample): 10 | # keep tract of absolute value of 11 | self.diff = np.add(self.diff, np.absolute(np.asarray(sample.channel_data))) 12 | self.sample_count = self.sample_count + 1 13 | 14 | elapsed_time = timeit.default_timer() - self.last_report 15 | if elapsed_time > self.polling_interval: 16 | channel_noise_power = np.divide(self.diff, self.sample_count) 17 | 18 | print(channel_noise_power) 19 | self.diff = np.zeros(self.eeg_channels) 20 | self.last_report = timeit.default_timer() 21 | 22 | # # Instanciate "monitor" thread 23 | def activate(self): 24 | # The difference between the ref and incoming signal. 25 | # IMPORTANT: For noise tests, the reference and channel should have the same input signal. 26 | self.diff = np.zeros(self.eeg_channels) 27 | self.last_report = timeit.default_timer() 28 | self.sample_count = 0 29 | self.polling_interval = 1.0 30 | 31 | if len(self.args) > 0: 32 | self.polling_interval = float(self.args[0]) 33 | 34 | def show_help(self): 35 | print("Optional argument: polling_interval -- in seconds, default: 10. \n \ 36 | Returns the power of the system noise.\n \ 37 | NOTE: The reference and channel should have the same input signal.") 38 | -------------------------------------------------------------------------------- /openbci/plugins/noise_test.yapsy-plugin: -------------------------------------------------------------------------------- 1 | [Core] 2 | Name = noise_test 3 | Module = noise_test 4 | 5 | [Documentation] 6 | Author = Various 7 | Version = 0.1 8 | Description = Power of system noise on each channel. -------------------------------------------------------------------------------- /openbci/plugins/print.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import plugin_interface as plugintypes 3 | 4 | 5 | class PluginPrint(plugintypes.IPluginExtended): 6 | def activate(self): 7 | print("Print activated") 8 | 9 | # called with each new sample 10 | def __call__(self, sample): 11 | if sample: 12 | # print impedance if supported 13 | if self.imp_channels > 0: 14 | sample_string = "ID: %f\n%s\n%s\n%s" %\ 15 | (sample.id, str(sample.channel_data)[1:-1], 16 | str(sample.aux_data)[1:-1], str(sample.imp_data)[1:-1]) 17 | else: 18 | sample_string = "ID: %f\n%s\n%s" % ( 19 | sample.id, str(sample.channel_data)[1:-1], str(sample.aux_data)[1:-1]) 20 | print("---------------------------------") 21 | print(sample_string) 22 | print("---------------------------------") 23 | 24 | # DEBBUGING 25 | # try: 26 | # sample_string.decode('ascii') 27 | # except UnicodeDecodeError: 28 | # print("Not a ascii-encoded unicode string") 29 | # else: 30 | # print(sample_string) 31 | -------------------------------------------------------------------------------- /openbci/plugins/print.yapsy-plugin: -------------------------------------------------------------------------------- 1 | 2 | [Core] 3 | Name = print 4 | Module = print 5 | 6 | [Documentation] 7 | Author = Various 8 | Version = 0.1 9 | Description = Print board values on stdout 10 | -------------------------------------------------------------------------------- /openbci/plugins/sample_rate.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import time 3 | import timeit 4 | from threading import Thread 5 | 6 | import plugin_interface as plugintypes 7 | 8 | # counter for sampling rate 9 | nb_samples_out = -1 10 | 11 | 12 | # try to ease work for main loop 13 | class Monitor(Thread): 14 | def __init__(self): 15 | Thread.__init__(self) 16 | self.nb_samples_out = -1 17 | 18 | # Init time to compute sampling rate 19 | self.tick = timeit.default_timer() 20 | self.start_tick = self.tick 21 | self.polling_interval = 10 22 | 23 | def run(self): 24 | while True: 25 | # check FPS + listen for new connections 26 | new_tick = timeit.default_timer() 27 | elapsed_time = new_tick - self.tick 28 | current_samples_out = nb_samples_out 29 | print("--- at t: " + str(new_tick - self.start_tick) + " ---") 30 | print("elapsed_time: " + str(elapsed_time)) 31 | print("nb_samples_out: " + str(current_samples_out - self.nb_samples_out)) 32 | sampling_rate = (current_samples_out - self.nb_samples_out) / elapsed_time 33 | print("sampling rate: " + str(sampling_rate)) 34 | self.tick = new_tick 35 | self.nb_samples_out = nb_samples_out 36 | time.sleep(self.polling_interval) 37 | 38 | 39 | class PluginSampleRate(plugintypes.IPluginExtended): 40 | # update counters value 41 | def __call__(self, sample): 42 | global nb_samples_out 43 | nb_samples_out = nb_samples_out + 1 44 | 45 | # Instanciate "monitor" thread 46 | def activate(self): 47 | monit = Monitor() 48 | if len(self.args) > 0: 49 | monit.polling_interval = float(self.args[0]) 50 | # daemonize thread to terminate it altogether with the main when time will come 51 | monit.daemon = True 52 | monit.start() 53 | 54 | def show_help(self): 55 | print("Optional argument: polling_interval -- in seconds, default: 10.") 56 | -------------------------------------------------------------------------------- /openbci/plugins/sample_rate.yapsy-plugin: -------------------------------------------------------------------------------- 1 | 2 | [Core] 3 | Name = sample_rate 4 | Module = sample_rate 5 | 6 | [Documentation] 7 | Author = Various 8 | Version = 0.1 9 | Description = Print average sample rate. 10 | -------------------------------------------------------------------------------- /openbci/plugins/streamer_lsl.py: -------------------------------------------------------------------------------- 1 | # download LSL and pylsl from https://code.google.com/p/labstreaminglayer/ 2 | # Eg: ftp://sccn.ucsd.edu/pub/software/LSL/SDK/liblsl-Python-1.10.2.zip 3 | # put in "lib" folder (same level as user.py) 4 | from __future__ import print_function 5 | import plugin_interface as plugintypes 6 | from pylsl import StreamInfo, StreamOutlet 7 | import sys 8 | 9 | # help python find pylsl relative to this example program 10 | sys.path.append('lib') 11 | 12 | 13 | # Use LSL protocol to broadcast data using one stream for EEG, 14 | # one stream for AUX, one last for impedance testing 15 | # (on supported board, if enabled) 16 | class StreamerLSL(plugintypes.IPluginExtended): 17 | # From IPlugin 18 | def activate(self): 19 | eeg_stream = "OpenBCI_EEG" 20 | eeg_id = "openbci_eeg_id1" 21 | aux_stream = "OpenBCI_AUX" 22 | aux_id = "openbci_aux_id1" 23 | imp_stream = "OpenBCI_Impedance" 24 | imp_id = "openbci_imp_id1" 25 | 26 | if len(self.args) > 0: 27 | eeg_stream = self.args[0] 28 | if len(self.args) > 1: 29 | eeg_id = self.args[1] 30 | if len(self.args) > 2: 31 | aux_stream = self.args[2] 32 | if len(self.args) > 3: 33 | aux_id = self.args[3] 34 | if len(self.args) > 4: 35 | imp_stream = self.args[4] 36 | if len(self.args) > 5: 37 | imp_id = self.args[5] 38 | 39 | # Create a new streams info, one for EEG values, one for AUX (eg, accelerometer) values 40 | print("Creating LSL stream for EEG. Name:" + eeg_stream + "- ID:" + eeg_id + 41 | "- data type: float32." + str(self.eeg_channels) + 42 | "channels at" + str(self.sample_rate) + "Hz.") 43 | info_eeg = StreamInfo(eeg_stream, 'EEG', self.eeg_channels, 44 | self.sample_rate, 'float32', eeg_id) 45 | # NB: set float32 instead of int16 so as OpenViBE takes it into account 46 | print("Creating LSL stream for AUX. Name:" + aux_stream + "- ID:" + aux_id + 47 | "- data type: float32." + str(self.aux_channels) + 48 | "channels at" + str(self.sample_rate) + "Hz.") 49 | info_aux = StreamInfo(aux_stream, 'AUX', self.aux_channels, 50 | self.sample_rate, 'float32', aux_id) 51 | 52 | # make outlets 53 | self.outlet_eeg = StreamOutlet(info_eeg) 54 | self.outlet_aux = StreamOutlet(info_aux) 55 | 56 | if self.imp_channels > 0: 57 | print("Creating LSL stream for Impedance. Name:" + imp_stream + "- ID:" + 58 | imp_id + "- data type: float32." + str(self.imp_channels) + 59 | "channels at" + str(self.sample_rate) + "Hz.") 60 | info_imp = StreamInfo(imp_stream, 'Impedance', self.imp_channels, 61 | self.sample_rate, 'float32', imp_id) 62 | self.outlet_imp = StreamOutlet(info_imp) 63 | 64 | # send channels values 65 | def __call__(self, sample): 66 | self.outlet_eeg.push_sample(sample.channel_data) 67 | self.outlet_aux.push_sample(sample.aux_data) 68 | if self.imp_channels > 0: 69 | self.outlet_imp.push_sample(sample.imp_data) 70 | 71 | def show_help(self): 72 | print("""Optional arguments: 73 | [EEG_stream_name [EEG_stream_ID [AUX_stream_name [AUX_stream_ID [Impedance_steam_name [Impedance_stream_ID]]]]]] 74 | \t Defaults: "OpenBCI_EEG" / "openbci_eeg_id1" and "OpenBCI_AUX" / "openbci_aux_id1" 75 | / "OpenBCI_Impedance" / "openbci_imp_id1".""") 76 | -------------------------------------------------------------------------------- /openbci/plugins/streamer_lsl.yapsy-plugin: -------------------------------------------------------------------------------- 1 | 2 | [Core] 3 | Name = streamer_lsl 4 | Module = streamer_lsl 5 | 6 | [Documentation] 7 | Author = Various 8 | Version = 0.1 9 | Description = Use LSL protocol to broadcast data. Requires LSL and pylsl. 10 | -------------------------------------------------------------------------------- /openbci/plugins/streamer_osc.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | # requires python-osc 3 | from pythonosc import osc_message_builder 4 | from pythonosc import udp_client 5 | import plugin_interface as plugintypes 6 | 7 | 8 | # Use OSC protocol to broadcast data (UDP layer), using "/openbci" stream. 9 | # (NB. does not check numbers of channel as TCP server) 10 | 11 | class StreamerOSC(plugintypes.IPluginExtended): 12 | """ 13 | Relay OpenBCI values to OSC clients 14 | 15 | Args: 16 | port: Port of the server 17 | ip: IP address of the server 18 | address: name of the stream 19 | """ 20 | 21 | def __init__(self, ip='localhost', port=12345, address="/openbci"): 22 | # connection infos 23 | self.ip = ip 24 | self.port = port 25 | self.address = address 26 | 27 | # From IPlugin 28 | def activate(self): 29 | if len(self.args) > 0: 30 | self.ip = self.args[0] 31 | if len(self.args) > 1: 32 | self.port = int(self.args[1]) 33 | if len(self.args) > 2: 34 | self.address = self.args[2] 35 | # init network 36 | print("Selecting OSC streaming. IP: " + self.ip + ", port: " + 37 | str(self.port) + ", address: " + self.address) 38 | self.client = udp_client.SimpleUDPClient(self.ip, self.port) 39 | 40 | # From IPlugin: close connections, send message to client 41 | def deactivate(self): 42 | self.client.send_message("/quit") 43 | 44 | # send channels values 45 | def __call__(self, sample): 46 | # silently pass if connection drops 47 | try: 48 | self.client.send_message(self.address, sample.channel_data) 49 | except: 50 | return 51 | 52 | def show_help(self): 53 | print("""Optional arguments: [ip [port [address]]] 54 | \t ip: target IP address (default: 'localhost') 55 | \t port: target port (default: 12345) 56 | \t address: select target address (default: '/openbci')""") 57 | -------------------------------------------------------------------------------- /openbci/plugins/streamer_osc.yapsy-plugin: -------------------------------------------------------------------------------- 1 | 2 | [Core] 3 | Name = streamer_osc 4 | Module = streamer_osc 5 | 6 | [Documentation] 7 | Author = Various 8 | Version = 0.1 9 | Description = Use OSC protocol to broadcast data (UDP layer). Requires pyosc. 10 | -------------------------------------------------------------------------------- /openbci/plugins/streamer_tcp.yapsy-plugin: -------------------------------------------------------------------------------- 1 | 2 | [Core] 3 | Name = streamer_tcp 4 | Module = streamer_tcp_server 5 | 6 | [Documentation] 7 | Author = Various 8 | Version = 0.1 9 | Description = Simple TCP server to "broadcast" data to clients, handling deconnections. Binary format use network endianness (i.e., big-endian), float32. Could be used with OpenViBE acquisition server by selecting "Telnet reader". 10 | -------------------------------------------------------------------------------- /openbci/plugins/streamer_tcp_server.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from threading import Thread 3 | import socket 4 | import select 5 | import struct 6 | import time 7 | import plugin_interface as plugintypes 8 | 9 | 10 | # Simple TCP server to "broadcast" data to clients, handling deconnections. 11 | # Binary format use network endianness (i.e., big-endian), float32 12 | 13 | # TODO: does not listen for anything at the moment, could use it to set options 14 | 15 | # Handling new client in separate thread 16 | class MonitorStreamer(Thread): 17 | """Launch and monitor a "Streamer" entity 18 | (incoming connections if implemented, current sampling rate). 19 | """ 20 | 21 | # tcp_server: the TCPServer instance that will be used 22 | def __init__(self, streamer): 23 | Thread.__init__(self) 24 | # bind to Streamer entity 25 | self.server = streamer 26 | 27 | def run(self): 28 | # run until we DIE 29 | while True: 30 | # check FPS + listen for new connections 31 | # FIXME: not so great with threads -- use a lock? 32 | # TODO: configure interval 33 | self.server.check_connections() 34 | time.sleep(1) 35 | 36 | 37 | class StreamerTCPServer(plugintypes.IPluginExtended): 38 | """ 39 | 40 | Relay OpenBCI values to TCP clients 41 | 42 | Args: 43 | port: Port of the server 44 | ip: IP address of the server 45 | 46 | """ 47 | 48 | def __init__(self, ip='localhost', port=12345): 49 | # list of socket clients 50 | self.CONNECTION_LIST = [] 51 | # connection infos 52 | self.ip = ip 53 | self.port = port 54 | 55 | # From IPlugin 56 | def activate(self): 57 | if len(self.args) > 0: 58 | self.ip = self.args[0] 59 | if len(self.args) > 1: 60 | self.port = int(self.args[1]) 61 | 62 | # init network 63 | print("Selecting raw TCP streaming. IP: " + self.ip + ", port: " + str(self.port)) 64 | self.initialize() 65 | 66 | # init the daemon that monitors connections 67 | self.monit = MonitorStreamer(self) 68 | self.monit.daemon = True 69 | # launch monitor 70 | self.monit.start() 71 | 72 | # the initialize method reads settings and outputs the first header 73 | def initialize(self): 74 | # init server 75 | self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 76 | # this has no effect, why ? 77 | self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 78 | # create connection 79 | self.server_socket.bind((self.ip, self.port)) 80 | self.server_socket.listen(1) 81 | print("Server started on port " + str(self.port)) 82 | 83 | # From Streamer, to be called each time we're willing to accept new connections 84 | def check_connections(self): 85 | # First listen for new connections, and new connections only, 86 | # this is why we pass only server_socket 87 | read_sockets, write_sockets, error_sockets = select.select([self.server_socket], [], [], 0) 88 | for sock in read_sockets: 89 | # New connection 90 | sockfd, addr = self.server_socket.accept() 91 | self.CONNECTION_LIST.append(sockfd) 92 | print("Client (%s, %s) connected" % addr) 93 | # and... don't bother with incoming messages 94 | 95 | # From IPlugin: close sockets, send message to client 96 | def deactivate(self): 97 | # close all remote connections 98 | for sock in self.CONNECTION_LIST: 99 | if sock != self.server_socket: 100 | try: 101 | sock.send("closing!\n") 102 | # at this point don't bother if message not sent 103 | except: 104 | continue 105 | sock.close() 106 | # close server socket 107 | self.server_socket.close() 108 | 109 | # broadcast channels values to all clients 110 | # as_string: many for debug, send values with a nice "[34.45, 30.4, -38.0]"-like format 111 | def __call__(self, sample, as_string=False): 112 | values = sample.channel_data 113 | # save sockets that are closed to remove them later on 114 | outdated_list = [] 115 | for sock in self.CONNECTION_LIST: 116 | # If one error should happen, we remove socket from the list 117 | try: 118 | if as_string: 119 | sock.send(str(values) + "\n") 120 | else: 121 | nb_channels = len(values) 122 | # format for binary data, network endian (big) and float (float32) 123 | packer = struct.Struct('!%sf' % nb_channels) 124 | # convert values to bytes 125 | packed_data = packer.pack(*values) 126 | sock.send(packed_data) 127 | # TODO: should check if the correct number of bytes passed through 128 | except: 129 | # sometimes (always?) it's only during the second write to a close socket 130 | # that an error is raised? 131 | print("Something bad happened, will close socket") 132 | outdated_list.append(sock) 133 | # now we are outside of the main list, it's time to remove outdated sockets, if any 134 | for bad_sock in outdated_list: 135 | print("Removing socket...") 136 | self.CONNECTION_LIST.remove(bad_sock) 137 | # not very costly to be polite 138 | bad_sock.close() 139 | 140 | def show_help(self): 141 | print("""Optional arguments: [ip [port]] 142 | \t ip: target IP address (default: 'localhost') 143 | \t port: target port (default: 12345)""") 144 | -------------------------------------------------------------------------------- /openbci/plugins/udp_server.py: -------------------------------------------------------------------------------- 1 | """A server that handles a connection with an OpenBCI board and serves that 2 | data over both a UDP socket server and a WebSocket server. 3 | 4 | Requires: 5 | - pyserial 6 | - asyncio 7 | - websockets 8 | """ 9 | 10 | from __future__ import print_function 11 | try: 12 | import cPickle as pickle 13 | except ImportError: 14 | import _pickle as pickle 15 | import json 16 | import socket 17 | 18 | import plugin_interface as plugintypes 19 | 20 | 21 | # class PluginPrint(IPlugin): 22 | # # args: passed by command line 23 | # def activate(self, args): 24 | # print("Print activated") 25 | # # tell outside world that init went good 26 | # return True 27 | 28 | # def deactivate(self): 29 | # print("Print Deactivated") 30 | 31 | # def show_help(self): 32 | # print("I do not need any parameter, just printing stuff.") 33 | 34 | # # called with each new sample 35 | # def __call__(self, sample): 36 | # sample_string = 37 | # "ID: %f\n%s\n%s" %(sample.id, str(sample.channel_data)[1:-1], str(sample.aux_data)[1:-1]) 38 | # print("---------------------------------") 39 | # print(sample_string) 40 | # print("---------------------------------") 41 | 42 | # # DEBBUGING 43 | # # try: 44 | # # sample_string.decode('ascii') 45 | # # except UnicodeDecodeError: 46 | # # print("Not a ascii-encoded unicode string") 47 | # # else: 48 | # # print(sample_string) 49 | 50 | 51 | class UDPServer(plugintypes.IPluginExtended): 52 | def __init__(self, ip='localhost', port=8888): 53 | self.ip = ip 54 | self.port = port 55 | self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 56 | 57 | def activate(self): 58 | print("udp_server plugin") 59 | print(self.args) 60 | 61 | if len(self.args) > 0: 62 | self.ip = self.args[0] 63 | if len(self.args) > 1: 64 | self.port = int(self.args[1]) 65 | 66 | # init network 67 | print("Selecting raw UDP streaming. IP: " + self.ip + ", port: " + str(self.port)) 68 | 69 | self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 70 | 71 | print("Server started on port " + str(self.port)) 72 | 73 | def __call__(self, sample): 74 | self.send_data(json.dumps(sample.channel_data)) 75 | 76 | def send_data(self, data): 77 | self.server.sendto(data, (self.ip, self.port)) 78 | 79 | # From IPlugin: close sockets, send message to client 80 | def deactivate(self): 81 | self.server.close() 82 | 83 | def show_help(self): 84 | print("""Optional arguments: [ip [port]] 85 | \t ip: target IP address (default: 'localhost') 86 | \t port: target port (default: 12345)""") 87 | -------------------------------------------------------------------------------- /openbci/plugins/udp_server.yapsy-plugin: -------------------------------------------------------------------------------- 1 | 2 | [Core] 3 | Name = udp_server 4 | Module = udp_server 5 | 6 | [Documentation] 7 | Author = Various 8 | Version = 0.1 9 | Description = Print board values on stdout 10 | -------------------------------------------------------------------------------- /openbci/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .constants import Constants as k 2 | from .parse import * 3 | from .ssdp import SSDPResponse 4 | from .utilities import * 5 | 6 | __version__ = "1.0.0" 7 | -------------------------------------------------------------------------------- /openbci/utils/constants.py: -------------------------------------------------------------------------------- 1 | class Constants: 2 | """The constants!""" 3 | 4 | ADS1299_GAIN_1 = 1.0 5 | ADS1299_GAIN_2 = 2.0 6 | ADS1299_GAIN_4 = 4.0 7 | ADS1299_GAIN_6 = 6.0 8 | ADS1299_GAIN_8 = 8.0 9 | ADS1299_GAIN_12 = 12.0 10 | ADS1299_GAIN_24 = 24.0 11 | 12 | ADS1299_VREF = 4.5 # reference voltage for ADC in ADS1299. set by its hardware 13 | 14 | BOARD_CYTON = 'cyton' 15 | BOARD_DAISY = 'daisy' 16 | BOARD_GANGLION = 'ganglion' 17 | BOARD_NONE = 'none' 18 | 19 | CYTON_ACCEL_SCALE_FACTOR_GAIN = 0.002 / (pow(2, 4)) # assume set to +/4G, so 2 mG 20 | 21 | """ Errors """ 22 | ERROR_INVALID_BYTE_LENGTH = 'Invalid Packet Byte Length' 23 | ERROR_INVALID_BYTE_START = 'Invalid Start Byte' 24 | ERROR_INVALID_BYTE_STOP = 'Invalid Stop Byte' 25 | ERROR_INVALID_DATA = 'Invalid data - try again' 26 | ERROR_INVALID_TYPW = 'Invalid type - check comments for input type' 27 | ERROR_MISSING_REGISTER_SETTING = 'Missing register setting' 28 | ERROR_MISSING_REQUIRED_PROPERTY = 'Missing property in JSON' 29 | ERROR_TIME_SYNC_IS_NULL = "'this.sync.curSyncObj' must not be null" 30 | ERROR_TIME_SYNC_NO_COMMA = 'Missed the time sync sent confirmation. Try sync again' 31 | ERROR_UNDEFINED_OR_NULL_INPUT = 'Undefined or Null Input' 32 | 33 | """ Possible number of channels """ 34 | 35 | NUMBER_OF_CHANNELS_CYTON = 8 36 | NUMBER_OF_CHANNELS_DAISY = 16 37 | NUMBER_OF_CHANNELS_GANGLION = 4 38 | 39 | """ Protocols """ 40 | PROTOCOL_BLE = 'ble' 41 | PROTOCOL_SERIAL = 'serial' 42 | PROTOCOL_WIFI = 'wifi' 43 | 44 | RAW_BYTE_START = 0xA0 45 | RAW_BYTE_STOP = 0xC0 46 | RAW_PACKET_ACCEL_NUMBER_AXIS = 3 47 | RAW_PACKET_SIZE = 33 48 | """ 49 | OpenBCI Raw Packet Positions 50 | 0:[startByte] | 1:[sampleNumber] | 2:[Channel-1.1] | 3:[Channel-1.2] | 4:[Channel-1.3] | 5:[Channel-2.1] | 6:[Channel-2.2] | 7:[Channel-2.3] | 8:[Channel-3.1] | 9:[Channel-3.2] | 10:[Channel-3.3] | 11:[Channel-4.1] | 12:[Channel-4.2] | 13:[Channel-4.3] | 14:[Channel-5.1] | 15:[Channel-5.2] | 16:[Channel-5.3] | 17:[Channel-6.1] | 18:[Channel-6.2] | 19:[Channel-6.3] | 20:[Channel-7.1] | 21:[Channel-7.2] | 22:[Channel-7.3] | 23:[Channel-8.1] | 24:[Channel-8.2] | 25:[Channel-8.3] | 26:[Aux-1.1] | 27:[Aux-1.2] | 28:[Aux-2.1] | 29:[Aux-2.2] | 30:[Aux-3.1] | 31:[Aux-3.2] | 32:StopByte 51 | """ 52 | RAW_PACKET_POSITION_CHANNEL_DATA_START = 2 53 | RAW_PACKET_POSITION_CHANNEL_DATA_STOP = 25 54 | RAW_PACKET_POSITION_SAMPLE_NUMBER = 1 55 | RAW_PACKET_POSITION_START_BYTE = 0 56 | RAW_PACKET_POSITION_STOP_BYTE = 32 57 | RAW_PACKET_POSITION_START_AUX = 26 58 | RAW_PACKET_POSITION_STOP_AUX = 31 59 | RAW_PACKET_POSITION_TIME_SYNC_AUX_START = 26 60 | RAW_PACKET_POSITION_TIME_SYNC_AUX_STOP = 28 61 | RAW_PACKET_POSITION_TIME_SYNC_TIME_START = 28 62 | RAW_PACKET_POSITION_TIME_SYNC_TIME_STOP = 32 63 | 64 | """ Stream packet types """ 65 | RAW_PACKET_TYPE_STANDARD_ACCEL = 0 # 0000 66 | RAW_PACKET_TYPE_STANDARD_RAW_AUX = 1 # 0001 67 | RAW_PACKET_TYPE_USER_DEFINED_TYPE = 2 # 0010 68 | RAW_PACKET_TYPE_ACCEL_TIME_SYNC_SET = 3 # 0011 69 | RAW_PACKET_TYPE_ACCEL_TIME_SYNCED = 4 # 0100 70 | RAW_PACKET_TYPE_RAW_AUX_TIME_SYNC_SET = 5 # 0101 71 | RAW_PACKET_TYPE_RAW_AUX_TIME_SYNCED = 6 # 0110 72 | RAW_PACKET_TYPE_IMPEDANCE = 7 # 0111 73 | 74 | """ Max sample number """ 75 | SAMPLE_NUMBER_MAX_CYTON = 255 76 | SAMPLE_NUMBER_MAX_GANGLION = 200 77 | 78 | """ Possible Sample Rates """ 79 | SAMPLE_RATE_1000 = 1000 80 | SAMPLE_RATE_125 = 125 81 | SAMPLE_RATE_12800 = 12800 82 | SAMPLE_RATE_1600 = 1600 83 | SAMPLE_RATE_16000 = 16000 84 | SAMPLE_RATE_200 = 200 85 | SAMPLE_RATE_2000 = 2000 86 | SAMPLE_RATE_250 = 250 87 | SAMPLE_RATE_25600 = 25600 88 | SAMPLE_RATE_3200 = 3200 89 | SAMPLE_RATE_400 = 400 90 | SAMPLE_RATE_4000 = 4000 91 | SAMPLE_RATE_500 = 500 92 | SAMPLE_RATE_6400 = 6400 93 | SAMPLE_RATE_800 = 800 94 | SAMPLE_RATE_8000 = 8000 95 | -------------------------------------------------------------------------------- /openbci/utils/parse.py: -------------------------------------------------------------------------------- 1 | import time 2 | import struct 3 | 4 | from openbci.utils.constants import Constants as k 5 | 6 | 7 | class ParseRaw(object): 8 | def __init__(self, 9 | board_type=k.BOARD_CYTON, 10 | gains=None, 11 | log=False, 12 | micro_volts=False, 13 | scaled_output=True): 14 | self.board_type = board_type 15 | self.gains = gains 16 | self.log = log 17 | self.micro_volts = micro_volts 18 | self.scale_factors = [] 19 | self.scaled_output = scaled_output 20 | 21 | if gains is not None: 22 | self.scale_factors = self.get_ads1299_scale_factors(self.gains, self.micro_volts) 23 | 24 | self.raw_data_to_sample = RawDataToSample(gains=gains, 25 | scale=scaled_output, 26 | scale_factors=self.scale_factors, 27 | verbose=log) 28 | 29 | def is_stop_byte(self, byte): 30 | """ 31 | Used to check and see if a byte adheres to the stop byte structure 32 | of 0xCx where x is the set of numbers from 0-F in hex of 0-15 in decimal. 33 | :param byte: {int} - The number to test 34 | :return: {boolean} - True if `byte` follows the correct form 35 | """ 36 | return (byte & 0xF0) == k.RAW_BYTE_STOP 37 | 38 | def get_ads1299_scale_factors(self, gains, micro_volts=None): 39 | out = [] 40 | for gain in gains: 41 | scale_factor = k.ADS1299_VREF / float((pow(2, 23) - 1)) / float(gain) 42 | if micro_volts is None: 43 | if self.micro_volts: 44 | scale_factor *= 1000000. 45 | else: 46 | if micro_volts: 47 | scale_factor *= 1000000. 48 | 49 | out.append(scale_factor) 50 | return out 51 | 52 | def get_channel_data_array(self, raw_data_to_sample): 53 | """ 54 | 55 | :param raw_data_to_sample: RawDataToSample 56 | :return: 57 | """ 58 | channel_data = [] 59 | number_of_channels = len(raw_data_to_sample.scale_factors) 60 | daisy = number_of_channels == k.NUMBER_OF_CHANNELS_DAISY 61 | channels_in_packet = k.NUMBER_OF_CHANNELS_CYTON 62 | if not daisy: 63 | channels_in_packet = number_of_channels 64 | # Channel data arrays are always 8 long 65 | 66 | for i in range(channels_in_packet): 67 | counts = self.interpret_24_bit_as_int_32( 68 | raw_data_to_sample.raw_data_packet[ 69 | (i * 3) + 70 | k.RAW_PACKET_POSITION_CHANNEL_DATA_START:(i * 3) + 71 | k.RAW_PACKET_POSITION_CHANNEL_DATA_START + 3 72 | ] 73 | ) 74 | channel_data.append( 75 | raw_data_to_sample.scale_factors[i] * 76 | counts if raw_data_to_sample.scale else counts 77 | ) 78 | 79 | return channel_data 80 | 81 | def get_data_array_accel(self, raw_data_to_sample): 82 | accel_data = [] 83 | for i in range(k.RAW_PACKET_ACCEL_NUMBER_AXIS): 84 | counts = self.interpret_16_bit_as_int_32( 85 | raw_data_to_sample.raw_data_packet[ 86 | k.RAW_PACKET_POSITION_START_AUX + 87 | (i * 2): k.RAW_PACKET_POSITION_START_AUX + (i * 2) + 2]) 88 | accel_data.append(k.CYTON_ACCEL_SCALE_FACTOR_GAIN * 89 | counts if raw_data_to_sample.scale else counts) 90 | return accel_data 91 | 92 | def get_raw_packet_type(self, stop_byte): 93 | return stop_byte & 0xF 94 | 95 | def interpret_16_bit_as_int_32(self, two_byte_buffer): 96 | return struct.unpack('>h', two_byte_buffer)[0] 97 | 98 | def interpret_24_bit_as_int_32(self, three_byte_buffer): 99 | # 3 byte ints 100 | unpacked = struct.unpack('3B', three_byte_buffer) 101 | 102 | # 3byte int in 2s compliment 103 | if unpacked[0] > 127: 104 | pre_fix = bytes(bytearray.fromhex('FF')) 105 | else: 106 | pre_fix = bytes(bytearray.fromhex('00')) 107 | 108 | three_byte_buffer = pre_fix + three_byte_buffer 109 | 110 | # unpack little endian(>) signed integer(i) (makes unpacking platform independent) 111 | return struct.unpack('>i', three_byte_buffer)[0] 112 | 113 | def parse_packet_standard_accel(self, raw_data_to_sample): 114 | """ 115 | 116 | :param raw_data_to_sample: RawDataToSample 117 | :return: 118 | """ 119 | # Check to make sure data is not null. 120 | if raw_data_to_sample is None: 121 | raise RuntimeError(k.ERROR_UNDEFINED_OR_NULL_INPUT) 122 | if raw_data_to_sample.raw_data_packet is None: 123 | raise RuntimeError(k.ERROR_UNDEFINED_OR_NULL_INPUT) 124 | 125 | # Check to make sure the buffer is the right size. 126 | if len(raw_data_to_sample.raw_data_packet) != k.RAW_PACKET_SIZE: 127 | raise RuntimeError(k.ERROR_INVALID_BYTE_LENGTH) 128 | 129 | # Verify the correct stop byte. 130 | if raw_data_to_sample.raw_data_packet[0] != k.RAW_BYTE_START: 131 | raise RuntimeError(k.ERROR_INVALID_BYTE_START) 132 | 133 | sample_object = OpenBCISample() 134 | 135 | sample_object.accel_data = self.get_data_array_accel(raw_data_to_sample) 136 | 137 | sample_object.channel_data = self.get_channel_data_array(raw_data_to_sample) 138 | 139 | sample_object.sample_number = raw_data_to_sample.raw_data_packet[ 140 | k.RAW_PACKET_POSITION_SAMPLE_NUMBER 141 | ] 142 | sample_object.start_byte = raw_data_to_sample.raw_data_packet[ 143 | k.RAW_PACKET_POSITION_START_BYTE 144 | ] 145 | sample_object.stop_byte = raw_data_to_sample.raw_data_packet[ 146 | k.RAW_PACKET_POSITION_STOP_BYTE 147 | ] 148 | 149 | sample_object.valid = True 150 | 151 | now_ms = int(round(time.time() * 1000)) 152 | 153 | sample_object.timestamp = now_ms 154 | sample_object.boardTime = 0 155 | 156 | return sample_object 157 | 158 | def parse_packet_standard_raw_aux(self, raw_data_to_sample): 159 | pass 160 | 161 | def parse_packet_time_synced_accel(self, raw_data_to_sample): 162 | pass 163 | 164 | def parse_packet_time_synced_raw_aux(self, raw_data_to_sample): 165 | pass 166 | 167 | def set_ads1299_scale_factors(self, gains, micro_volts=None): 168 | self.scale_factors = self.get_ads1299_scale_factors(gains, micro_volts=micro_volts) 169 | 170 | def transform_raw_data_packet_to_sample(self, raw_data): 171 | """ 172 | Used transform raw data packets into fully qualified packets 173 | :param raw_data: 174 | :return: 175 | """ 176 | try: 177 | self.raw_data_to_sample.raw_data_packet = raw_data 178 | packet_type = self.get_raw_packet_type(raw_data[k.RAW_PACKET_POSITION_STOP_BYTE]) 179 | if packet_type == k.RAW_PACKET_TYPE_STANDARD_ACCEL: 180 | sample = self.parse_packet_standard_accel(self.raw_data_to_sample) 181 | elif packet_type == k.RAW_PACKET_TYPE_STANDARD_RAW_AUX: 182 | sample = self.parse_packet_standard_raw_aux(self.raw_data_to_sample) 183 | elif packet_type == k.RAW_PACKET_TYPE_ACCEL_TIME_SYNC_SET or \ 184 | packet_type == k.RAW_PACKET_TYPE_ACCEL_TIME_SYNCED: 185 | sample = self.parse_packet_time_synced_accel(self.raw_data_to_sample) 186 | elif packet_type == k.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNC_SET or \ 187 | packet_type == k.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNCED: 188 | sample = self.parse_packet_time_synced_raw_aux(self.raw_data_to_sample) 189 | else: 190 | sample = OpenBCISample() 191 | sample.error = 'This module does not support packet type %d' % packet_type 192 | sample.valid = False 193 | 194 | sample.packet_type = packet_type 195 | except BaseException as e: 196 | sample = OpenBCISample() 197 | if hasattr(e, 'message'): 198 | sample.error = e.message 199 | else: 200 | sample.error = e 201 | sample.valid = False 202 | 203 | return sample 204 | 205 | def make_daisy_sample_object_wifi(self, lower_sample_object, upper_sample_object): 206 | """ 207 | /** 208 | * @description Used to make one sample object from two sample 209 | * objects. The sample number of the new daisy sample will be the 210 | * upperSampleObject's sample number divded by 2. This allows us 211 | * to preserve consecutive sample numbers that flip over at 127 212 | * instead of 255 for an 8 channel. The daisySampleObject will 213 | * also have one `channelData` array with 16 elements inside it, 214 | * with the lowerSampleObject in the lower indices and the 215 | * upperSampleObject in the upper set of indices. The auxData from 216 | * both channels shall be captured in an object called `auxData` 217 | * which contains two arrays referenced by keys `lower` and 218 | * `upper` for the `lowerSampleObject` and `upperSampleObject`, 219 | * respectively. The timestamps shall be averaged and moved into 220 | * an object called `timestamp`. Further, the un-averaged 221 | * timestamps from the `lowerSampleObject` and `upperSampleObject` 222 | * shall be placed into an object called `_timestamps` which shall 223 | * contain two keys `lower` and `upper` which contain the original 224 | * timestamps for their respective sampleObjects. 225 | * @param lowerSampleObject {Object} - Lower 8 channels with odd sample number 226 | * @param upperSampleObject {Object} - Upper 8 channels with even sample number 227 | * @returns {Object} - The new merged daisy sample object 228 | */ 229 | """ 230 | daisy_sample_object = OpenBCISample() 231 | 232 | if lower_sample_object.channel_data is not None: 233 | daisy_sample_object.channel_data = lower_sample_object.channel_data + \ 234 | upper_sample_object.channel_data 235 | 236 | daisy_sample_object.sample_number = upper_sample_object.sample_number 237 | daisy_sample_object.id = daisy_sample_object.sample_number 238 | 239 | daisy_sample_object.aux_data = { 240 | 'lower': lower_sample_object.aux_data, 241 | 'upper': upper_sample_object.aux_data 242 | } 243 | 244 | if lower_sample_object.timestamp: 245 | daisy_sample_object.timestamp = lower_sample_object.timestamp 246 | 247 | daisy_sample_object.stop_byte = lower_sample_object.stop_byte 248 | 249 | daisy_sample_object._timestamps = { 250 | 'lower': lower_sample_object.timestamp, 251 | 'upper': upper_sample_object.timestamp 252 | } 253 | 254 | if lower_sample_object.accel_data: 255 | if lower_sample_object.accel_data[0] > 0 or lower_sample_object.accel_data[1] > 0 or \ 256 | lower_sample_object.accel_data[2] > 0: 257 | daisy_sample_object.accel_data = lower_sample_object.accel_data 258 | else: 259 | daisy_sample_object.accel_data = upper_sample_object.accel_data 260 | 261 | daisy_sample_object.valid = True 262 | 263 | return daisy_sample_object 264 | 265 | """ 266 | /** 267 | * @description Used transform raw data packets into fully qualified packets 268 | * @param o {RawDataToSample} - Used to hold data and configuration settings 269 | * @return {Array} samples An array of {Sample} 270 | * @author AJ Keller (@aj-ptw) 271 | */ 272 | function transformRawDataPacketsToSample (o) { 273 | let samples = []; 274 | for (let i = 0; i < o.rawDataPackets.length; i++) { 275 | o.rawDataPacket = o.rawDataPackets[i]; 276 | const sample = transformRawDataPacketToSample(o); 277 | samples.push(sample); 278 | if (sample.hasOwnProperty('sampleNumber')) { 279 | o['lastSampleNumber'] = sample.sampleNumber; 280 | } else if (!sample.hasOwnProperty('impedanceValue')) { 281 | o['lastSampleNumber'] = o.rawDataPacket[k.OBCIPacketPositionSampleNumber]; 282 | } 283 | } 284 | return samples; 285 | } 286 | """ 287 | 288 | def transform_raw_data_packets_to_sample(self, raw_data_packets): 289 | samples = [] 290 | 291 | for raw_data_packet in raw_data_packets: 292 | sample = self.transform_raw_data_packet_to_sample(raw_data_packet) 293 | samples.append(sample) 294 | self.raw_data_to_sample.last_sample_number = sample.sample_number 295 | 296 | return samples 297 | 298 | 299 | class RawDataToSample(object): 300 | """Object encapulsating a parsing object.""" 301 | 302 | def __init__(self, 303 | accel_data=None, 304 | gains=None, 305 | last_sample_number=0, 306 | raw_data_packets=None, 307 | raw_data_packet=None, 308 | scale=True, 309 | scale_factors=None, 310 | time_offset=0, 311 | verbose=False): 312 | """ 313 | RawDataToSample 314 | :param accel_data: list 315 | The channel settings array 316 | :param gains: list 317 | The gains of each channel, this is used to derive number of channels 318 | :param last_sample_number: int 319 | :param raw_data_packets: list 320 | list of raw_data_packets 321 | :param raw_data_packet: bytearray 322 | A single raw data packet 323 | :param scale: boolean 324 | Default `true`. A gain of 24 for Cyton will be used and 51 for ganglion by default. 325 | :param scale_factors: list 326 | Calculated scale factors 327 | :param time_offset: int 328 | For non time stamp use cases i.e. 0xC0 or 0xC1 (default and raw aux) 329 | :param verbose: 330 | """ 331 | self.accel_data = accel_data if accel_data is not None else [] 332 | self.gains = gains if gains is not None else [] 333 | self.time_offset = time_offset 334 | self.last_sample_number = last_sample_number 335 | self.raw_data_packets = raw_data_packets if raw_data_packets is not None else [] 336 | self.raw_data_packet = raw_data_packet 337 | self.scale = scale 338 | self.scale_factors = scale_factors if scale_factors is not None else [] 339 | self.verbose = verbose 340 | 341 | 342 | class OpenBCISample(object): 343 | """Object encapulsating a single sample from the OpenBCI board.""" 344 | 345 | def __init__(self, 346 | aux_data=None, 347 | board_time=0, 348 | channel_data=None, 349 | error=None, 350 | imp_data=None, 351 | packet_type=k.RAW_PACKET_TYPE_STANDARD_ACCEL, 352 | protocol=k.PROTOCOL_WIFI, 353 | sample_number=0, 354 | start_byte=0, 355 | stop_byte=0, 356 | valid=True, 357 | accel_data=None): 358 | self.aux_data = aux_data if aux_data is not None else [] 359 | self.board_time = board_time 360 | self.channel_data = channel_data if aux_data is not None else [] 361 | self.error = error 362 | self.id = sample_number 363 | self.imp_data = imp_data if aux_data is not None else [] 364 | self.packet_type = packet_type 365 | self.protocol = protocol 366 | self.sample_number = sample_number 367 | self.start_byte = start_byte 368 | self.stop_byte = stop_byte 369 | self.timestamp = 0 370 | self._timestamps = {} 371 | self.valid = valid 372 | self.accel_data = accel_data if accel_data is not None else [] 373 | -------------------------------------------------------------------------------- /openbci/utils/ssdp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Dan Krause 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import socket 16 | import sys 17 | 18 | pyVersion = sys.version_info[0] 19 | if pyVersion == 2: 20 | # Imports for Python 2 21 | import httplib 22 | from StringIO import StringIO as SocketIO 23 | else: 24 | # Imports for Python 3+ 25 | import http.client 26 | from io import BytesIO as SocketIO 27 | 28 | 29 | class SSDPResponse(object): 30 | class _FakeSocket(SocketIO): 31 | def makefile(self, *args, **kw): 32 | return self 33 | 34 | def __init__(self, response): 35 | 36 | if pyVersion == 2: 37 | r = httplib.HTTPResponse(self._FakeSocket(response)) 38 | else: 39 | r = http.client.HTTPResponse(self._FakeSocket(response)) 40 | 41 | r.begin() 42 | self.location = r.getheader("location") 43 | self.usn = r.getheader("usn") 44 | self.st = r.getheader("st") 45 | self.cache = r.getheader("cache-control").split("=")[1] 46 | 47 | def __repr__(self): 48 | return "".format(**self.__dict__) 49 | 50 | 51 | def discover(service, timeout=5, retries=1, mx=3, wifi_found_cb=None): 52 | group = ("239.255.255.250", 1900) 53 | message = "\r\n".join([ 54 | 'M-SEARCH * HTTP/1.1', 55 | 'HOST: {0}:{1}', 56 | 'MAN: "ssdp:discover"', 57 | 'ST: {st}', 'MX: {mx}', '', '']) 58 | 59 | socket.setdefaulttimeout(timeout) 60 | responses = {} 61 | for _ in range(retries): 62 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 63 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 64 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) 65 | sockMessage = message.format(*group, st=service, mx=mx) 66 | if pyVersion == 3: 67 | sockMessage = sockMessage.encode("utf-8") 68 | sock.sendto(sockMessage, group) 69 | while True: 70 | try: 71 | response = SSDPResponse(sock.recv(1024)) 72 | if wifi_found_cb is not None: 73 | wifi_found_cb(response) 74 | responses[response.location] = response 75 | except socket.timeout: 76 | break 77 | return list(responses.values()) 78 | -------------------------------------------------------------------------------- /openbci/utils/utilities.py: -------------------------------------------------------------------------------- 1 | from openbci.utils.constants import Constants 2 | 3 | 4 | def make_tail_byte_from_packet_type(packet_type): 5 | """ 6 | Converts a packet type {Number} into a OpenBCI stop byte 7 | :param packet_type: {int} The number to smash on to the stop byte. Must be 0-15, 8 | out of bounds input will result in a 0 9 | :return: A properly formatted OpenBCI stop byte 10 | """ 11 | if packet_type < 0 or packet_type > 15: 12 | packet_type = 0 13 | 14 | return Constants.RAW_BYTE_STOP | packet_type 15 | 16 | 17 | def sample_number_normalize(sample_number=None): 18 | if sample_number is not None: 19 | if sample_number > Constants.SAMPLE_NUMBER_MAX_CYTON: 20 | sample_number = Constants.SAMPLE_NUMBER_MAX_CYTON 21 | else: 22 | sample_number = 0x45 23 | 24 | return sample_number 25 | 26 | 27 | def sample_packet(sample_number=0x45): 28 | return bytearray( 29 | [0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 30 | 0, 0, 6, 0, 0, 7, 0, 31 | 0, 8, 0, 0, 0, 1, 0, 2, 32 | make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_STANDARD_ACCEL)]) 33 | 34 | 35 | def sample_packet_zero(sample_number): 36 | return bytearray( 37 | [0xA0, sample_number_normalize(sample_number), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38 | 0, 0, 0, 0, 0, 0, 0, 39 | 0, 0, 0, 0, 0, 0, 0, 0, 40 | make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_STANDARD_ACCEL)]) 41 | 42 | 43 | def sample_packet_real(sample_number): 44 | return bytearray( 45 | [0xA0, sample_number_normalize(sample_number), 0x8F, 0xF2, 0x40, 0x8F, 0xDF, 0xF4, 0x90, 46 | 0x2B, 0xB6, 0x8F, 0xBF, 47 | 0xBF, 0x7F, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0x94, 0x25, 0x34, 0x20, 0xB6, 0x7D, 0, 0xE0, 0, 48 | 0xE0, 0x0F, 0x70, 49 | make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_STANDARD_ACCEL)]) 50 | 51 | 52 | def sample_packet_standard_raw_aux(sample_number): 53 | return bytearray( 54 | [0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 55 | 0, 0, 6, 0, 0, 7, 0, 56 | 0, 8, 0, 1, 2, 3, 4, 5, 57 | make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_STANDARD_RAW_AUX)]) 58 | 59 | 60 | def sample_packet_accel_time_sync_set(sample_number): 61 | return bytearray( 62 | [0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 63 | 0, 0, 6, 0, 0, 7, 0, 64 | 0, 8, 0, 1, 0, 0, 0, 1, 65 | make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_ACCEL_TIME_SYNC_SET)]) 66 | 67 | 68 | def sample_packet_accel_time_synced(sample_number): 69 | return bytearray( 70 | [0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 71 | 0, 0, 6, 0, 0, 7, 0, 72 | 0, 8, 0, 1, 0, 0, 0, 1, 73 | make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_ACCEL_TIME_SYNCED)]) 74 | 75 | 76 | def sample_packet_raw_aux_time_sync_set(sample_number): 77 | return bytearray( 78 | [0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 79 | 0, 0, 6, 0, 0, 7, 0, 80 | 0, 8, 0x00, 0x01, 0, 0, 0, 1, 81 | make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNC_SET)]) 82 | 83 | 84 | def sample_packet_raw_aux_time_synced(sample_number): 85 | return bytearray( 86 | [0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 87 | 0, 0, 6, 0, 0, 7, 0, 88 | 0, 8, 0x00, 0x01, 0, 0, 0, 1, 89 | make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNCED)]) 90 | 91 | 92 | def sample_packet_impedance(channel_number): 93 | return bytearray( 94 | [0xA0, channel_number, 54, 52, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 95 | 0, 0, 0, 0, 0, 0, 0, 96 | 0, make_tail_byte_from_packet_type(Constants.RAW_PACKET_TYPE_IMPEDANCE)]) 97 | 98 | 99 | def sample_packet_user_defined(): 100 | return bytearray( 101 | [0xA0, 0x00, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 102 | 1, 1, 1, 1, 103 | make_tail_byte_from_packet_type(Constants.OBCIStreamPacketUserDefinedType)]) 104 | -------------------------------------------------------------------------------- /openbci/wifi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core OpenBCI object for handling connections and samples from the WiFi Shield 3 | 4 | Note that the LIB will take care on its own to print incoming ASCII messages if any (FIXME, BTW). 5 | 6 | EXAMPLE USE: 7 | 8 | def handle_sample(sample): 9 | print(sample.channels_data) 10 | 11 | wifi = OpenBCIWifi() 12 | wifi.start(handle_sample) 13 | 14 | TODO: Cyton/Ganglion JSON 15 | TODO: Ganglion Raw 16 | TODO: Cyton Raw 17 | 18 | """ 19 | from __future__ import print_function 20 | import asyncore 21 | import atexit 22 | import json 23 | import logging 24 | import re 25 | import socket 26 | import timeit 27 | 28 | try: 29 | import urllib2 30 | except ImportError: 31 | import urllib 32 | 33 | import requests 34 | import xmltodict 35 | 36 | from openbci.utils import Constants, ParseRaw, OpenBCISample, ssdp 37 | 38 | SAMPLE_RATE = 0 # Hz 39 | 40 | ''' 41 | #Commands for in SDK 42 | 43 | command_stop = "s"; 44 | command_startBinary = "b"; 45 | ''' 46 | 47 | 48 | class OpenBCIWiFi(object): 49 | """ 50 | Handle a connection to an OpenBCI wifi shield. 51 | 52 | Args: 53 | ip_address: The IP address of the WiFi Shield, "None" to attempt auto-detect. 54 | shield_name: The unique name of the WiFi Shield, such as `OpenBCI-2AD4`, will use SSDP to 55 | get IP address still, if `shield_name` is "None" and `ip_address` is "None", 56 | will connect to the first WiFi Shield found using SSDP 57 | sample_rate: The sample rate to set the attached board to. If the sample rate picked 58 | is not a sample rate the attached board can support, i.e. you send 300 to Cyton, 59 | then error will be thrown. 60 | log: 61 | timeout: in seconds, disconnect / reconnect after a period without new data 62 | should be high if impedance check 63 | max_packets_to_skip: will try to disconnect / reconnect after too many packets are skipped 64 | """ 65 | 66 | def __init__(self, ip_address=None, shield_name=None, sample_rate=None, log=True, timeout=3, 67 | max_packets_to_skip=20, latency=10000, high_speed=True, ssdp_attempts=5, 68 | num_channels=8, local_ip_address=None): 69 | # these one are used 70 | self.daisy = False 71 | self.gains = None 72 | self.high_speed = high_speed 73 | self.impedance = False 74 | self.ip_address = ip_address 75 | self.latency = latency 76 | self.log = log # print_incoming_text needs log 77 | self.max_packets_to_skip = max_packets_to_skip 78 | self.num_channels = num_channels 79 | self.sample_rate = sample_rate 80 | self.shield_name = shield_name 81 | self.ssdp_attempts = ssdp_attempts 82 | self.streaming = False 83 | self.timeout = timeout 84 | 85 | # might be handy to know API 86 | self.board_type = "none" 87 | # number of EEG channels 88 | self.eeg_channels_per_sample = 0 89 | self.read_state = 0 90 | self.log_packet_count = 0 91 | self.packets_dropped = 0 92 | self.time_last_packet = 0 93 | 94 | if self.log: 95 | print("Welcome to OpenBCI Native WiFi Shield Driver - Please contribute code!") 96 | 97 | self.local_ip_address = local_ip_address 98 | if not self.local_ip_address: 99 | self.local_ip_address = self._get_local_ip_address() 100 | 101 | # Intentionally bind to port 0 102 | self.local_wifi_server = WiFiShieldServer(self.local_ip_address, 0) 103 | self.local_wifi_server_port = self.local_wifi_server.socket.getsockname()[1] 104 | if self.log: 105 | print("Opened socket on %s:%d" % 106 | (self.local_ip_address, self.local_wifi_server_port)) 107 | 108 | if ip_address is None: 109 | for i in range(ssdp_attempts): 110 | try: 111 | self.find_wifi_shield(wifi_shield_cb=self.on_shield_found) 112 | break 113 | except OSError: 114 | # Try again 115 | if self.log: 116 | print("Did not find any WiFi Shields") 117 | else: 118 | self.on_shield_found(ip_address) 119 | 120 | def on_shield_found(self, ip_address): 121 | self.ip_address = ip_address 122 | self.connect() 123 | # Disconnects from board when terminated 124 | atexit.register(self.disconnect) 125 | 126 | def loop(self): 127 | asyncore.loop() 128 | 129 | def _get_local_ip_address(self): 130 | """ 131 | Gets the local ip address of this computer 132 | @returns str Local IP address 133 | """ 134 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 135 | s.connect(("8.8.8.8", 80)) 136 | local_ip_address = s.getsockname()[0] 137 | s.close() 138 | return local_ip_address 139 | 140 | def getBoardType(self): 141 | """ Returns the version of the board """ 142 | return self.board_type 143 | 144 | def setImpedance(self, flag): 145 | """ Enable/disable impedance measure """ 146 | self.impedance = bool(flag) 147 | 148 | def connect(self): 149 | """ Connect to the board and configure it. Note: recreates various objects upon call. """ 150 | if self.ip_address is None: 151 | raise ValueError('self.ip_address cannot be None') 152 | 153 | if self.log: 154 | print("Init WiFi connection with IP: " + self.ip_address) 155 | 156 | """ 157 | Docs on these HTTP requests and more are found: 158 | https://app.swaggerhub.com/apis/pushtheworld/openbci-wifi-server/1.3.0 159 | """ 160 | 161 | res_board = requests.get("http://%s/board" % self.ip_address) 162 | 163 | if res_board.status_code == 200: 164 | board_info = res_board.json() 165 | if not board_info['board_connected']: 166 | raise RuntimeError("No board connected to WiFi Shield. " 167 | "To learn how to connect to a Cyton or Ganglion visit " 168 | "http://docs.openbci.com/Tutorials/03-Wifi_Getting_Started_Guide") 169 | self.board_type = board_info['board_type'] 170 | self.eeg_channels_per_sample = board_info['num_channels'] 171 | if self.log: 172 | print("Connected to %s with %s channels" % 173 | (self.board_type, self.eeg_channels_per_sample)) 174 | 175 | self.gains = None 176 | if self.board_type == Constants.BOARD_CYTON: 177 | self.gains = [24, 24, 24, 24, 24, 24, 24, 24] 178 | self.daisy = False 179 | elif self.board_type == Constants.BOARD_DAISY: 180 | self.gains = [24, 24, 24, 24, 24, 24, 24, 181 | 24, 24, 24, 24, 24, 24, 24, 24, 24] 182 | self.daisy = True 183 | elif self.board_type == Constants.BOARD_GANGLION: 184 | self.gains = [51, 51, 51, 51] 185 | self.daisy = False 186 | self.local_wifi_server.set_daisy(daisy=self.daisy) 187 | self.local_wifi_server.set_parser( 188 | ParseRaw(gains=self.gains, board_type=self.board_type)) 189 | 190 | if self.high_speed: 191 | output_style = 'raw' 192 | else: 193 | output_style = 'json' 194 | res_tcp_post = requests.post("http://%s/tcp" % self.ip_address, 195 | json={ 196 | 'ip': self.local_ip_address, 197 | 'port': self.local_wifi_server_port, 198 | 'output': output_style, 199 | 'delimiter': True, 200 | 'latency': self.latency 201 | }) 202 | if res_tcp_post.status_code == 200: 203 | tcp_status = res_tcp_post.json() 204 | if tcp_status['connected']: 205 | if self.log: 206 | print("WiFi Shield to Python TCP Socket Established") 207 | else: 208 | raise RuntimeWarning("WiFi Shield is not able to connect to local server." 209 | "Please open an issue.") 210 | 211 | def init_streaming(self): 212 | """ Tell the board to record like crazy. """ 213 | res_stream_start = requests.get( 214 | "http://%s/stream/start" % self.ip_address) 215 | if res_stream_start.status_code == 200: 216 | self.streaming = True 217 | self.packets_dropped = 0 218 | self.time_last_packet = timeit.default_timer() 219 | else: 220 | raise EnvironmentError("Unable to start streaming." 221 | "Check API for status code %d on /stream/start" 222 | % res_stream_start.status_code) 223 | 224 | def find_wifi_shield(self, shield_name=None, wifi_shield_cb=None): 225 | """Detects Ganglion board MAC address if more than 1, will select first. Needs root.""" 226 | 227 | if self.log: 228 | print("Try to find WiFi shields on your local wireless network") 229 | print("Scanning for %d seconds nearby devices..." % self.timeout) 230 | 231 | list_ip = [] 232 | list_id = [] 233 | found_shield = False 234 | 235 | def wifi_shield_found(response): 236 | res = requests.get(response.location, verify=False).text 237 | device_description = xmltodict.parse(res) 238 | cur_shield_name = str( 239 | device_description['root']['device']['serialNumber']) 240 | cur_base_url = str(device_description['root']['URLBase']) 241 | cur_ip_address = re.findall(r'[0-9]+(?:\.[0-9]+){3}', cur_base_url)[0] 242 | list_id.append(cur_shield_name) 243 | list_ip.append(cur_ip_address) 244 | found_shield = True 245 | if shield_name is None: 246 | print("Found WiFi Shield %s with IP Address %s" % 247 | (cur_shield_name, cur_ip_address)) 248 | if wifi_shield_cb is not None: 249 | wifi_shield_cb(cur_ip_address) 250 | else: 251 | if shield_name == cur_shield_name: 252 | if wifi_shield_cb is not None: 253 | wifi_shield_cb(cur_ip_address) 254 | 255 | ssdp_hits = ssdp.discover("urn:schemas-upnp-org:device:Basic:1", timeout=self.timeout, 256 | wifi_found_cb=wifi_shield_found) 257 | 258 | nb_wifi_shields = len(list_id) 259 | 260 | if nb_wifi_shields < 1: 261 | print("No WiFi Shields found ;(") 262 | raise OSError('Cannot find OpenBCI WiFi Shield with local name') 263 | 264 | if nb_wifi_shields > 1: 265 | print( 266 | "Found " + str(nb_wifi_shields) + 267 | ", selecting first named: " + list_id[0] + 268 | " with IPV4: " + list_ip[0]) 269 | return list_ip[0] 270 | 271 | def wifi_write(self, output): 272 | """ 273 | Pass through commands from the WiFi Shield to the Carrier board 274 | :param output: 275 | :return: 276 | """ 277 | res_command_post = requests.post("http://%s/command" % self.ip_address, 278 | json={'command': output}) 279 | if res_command_post.status_code == 200: 280 | ret_val = res_command_post.text 281 | if self.log: 282 | print(ret_val) 283 | return ret_val 284 | else: 285 | if self.log: 286 | print("Error code: %d %s" % 287 | (res_command_post.status_code, res_command_post.text)) 288 | raise RuntimeError("Error code: %d %s" % ( 289 | res_command_post.status_code, res_command_post.text)) 290 | 291 | def getSampleRate(self): 292 | return self.sample_rate 293 | 294 | def getNbEEGChannels(self): 295 | """Will not get new data on impedance check.""" 296 | return self.eeg_channels_per_sample 297 | 298 | def start_streaming(self, callback, lapse=-1): 299 | """ 300 | Start handling streaming data from the board. Call a provided callback 301 | for every single sample that is processed 302 | 303 | Args: 304 | callback: A callback function, or a list of functions, that will receive a single 305 | argument of the OpenBCISample object captured. 306 | """ 307 | start_time = timeit.default_timer() 308 | 309 | # Enclose callback function in a list if it comes alone 310 | if not isinstance(callback, list): 311 | self.local_wifi_server.set_callback(callback) 312 | else: 313 | self.local_wifi_server.set_callback(callback[0]) 314 | 315 | if not self.streaming: 316 | self.init_streaming() 317 | 318 | # while self.streaming: 319 | # # should the board get disconnected and we could not wait for notification anymore 320 | # # a reco should be attempted through timeout mechanism 321 | # try: 322 | # # at most we will get one sample per packet 323 | # self.waitForNotifications(1. / self.getSampleRate()) 324 | # except Exception as e: 325 | # print("Something went wrong while waiting for a new sample: " + str(e)) 326 | # # retrieve current samples on the stack 327 | # samples = self.delegate.getSamples() 328 | # self.packets_dropped = self.delegate.getMaxPacketsDropped() 329 | # if samples: 330 | # self.time_last_packet = timeit.default_timer() 331 | # for call in callback: 332 | # for sample in samples: 333 | # call(sample) 334 | # 335 | # if (lapse > 0 and timeit.default_timer() - start_time > lapse): 336 | # self.stop(); 337 | # if self.log: 338 | # self.log_packet_count = self.log_packet_count + 1; 339 | # 340 | # # Checking connection -- timeout and packets dropped 341 | # self.check_connection() 342 | 343 | def test_signal(self, signal): 344 | """ Enable / disable test signal """ 345 | if signal == 0: 346 | self.warn("Disabling synthetic square wave") 347 | try: 348 | self.wifi_write(']') 349 | except Exception as e: 350 | print("Something went wrong while setting signal: " + str(e)) 351 | elif signal == 1: 352 | self.warn("Enabling synthetic square wave") 353 | try: 354 | self.wifi_write('[') 355 | except Exception as e: 356 | print("Something went wrong while setting signal: " + str(e)) 357 | else: 358 | self.warn("%s is not a known test signal. Valid signal is 0-1" % signal) 359 | 360 | def set_channel(self, channel, toggle_position): 361 | """ Enable / disable channels """ 362 | try: 363 | if channel > self.num_channels: 364 | raise ValueError('Cannot set non-existant channel') 365 | # Commands to set toggle to on position 366 | if toggle_position == 1: 367 | if channel is 1: 368 | self.wifi_write('!') 369 | if channel is 2: 370 | self.wifi_write('@') 371 | if channel is 3: 372 | self.wifi_write('#') 373 | if channel is 4: 374 | self.wifi_write('$') 375 | if channel is 5: 376 | self.wifi_write('%') 377 | if channel is 6: 378 | self.wifi_write('^') 379 | if channel is 7: 380 | self.wifi_write('&') 381 | if channel is 8: 382 | self.wifi_write('*') 383 | if channel is 9: 384 | self.wifi_write('Q') 385 | if channel is 10: 386 | self.wifi_write('W') 387 | if channel is 11: 388 | self.wifi_write('E') 389 | if channel is 12: 390 | self.wifi_write('R') 391 | if channel is 13: 392 | self.wifi_write('T') 393 | if channel is 14: 394 | self.wifi_write('Y') 395 | if channel is 15: 396 | self.wifi_write('U') 397 | if channel is 16: 398 | self.wifi_write('I') 399 | # Commands to set toggle to off position 400 | elif toggle_position == 0: 401 | if channel is 1: 402 | self.wifi_write('1') 403 | if channel is 2: 404 | self.wifi_write('2') 405 | if channel is 3: 406 | self.wifi_write('3') 407 | if channel is 4: 408 | self.wifi_write('4') 409 | if channel is 5: 410 | self.wifi_write('5') 411 | if channel is 6: 412 | self.wifi_write('6') 413 | if channel is 7: 414 | self.wifi_write('7') 415 | if channel is 8: 416 | self.wifi_write('8') 417 | if channel is 9: 418 | self.wifi_write('q') 419 | if channel is 10: 420 | self.wifi_write('w') 421 | if channel is 11: 422 | self.wifi_write('e') 423 | if channel is 12: 424 | self.wifi_write('r') 425 | if channel is 13: 426 | self.wifi_write('t') 427 | if channel is 14: 428 | self.wifi_write('y') 429 | if channel is 15: 430 | self.wifi_write('u') 431 | if channel is 16: 432 | self.wifi_write('i') 433 | except Exception as e: 434 | print("Something went wrong while setting channels: " + str(e)) 435 | 436 | # See Cyton SDK for options 437 | def set_channel_settings(self, channel, enabled=True, gain=24, input_type=0, 438 | include_bias=True, use_srb2=True, use_srb1=True): 439 | try: 440 | if channel > self.num_channels: 441 | raise ValueError('Cannot set non-existant channel') 442 | if self.board_type == Constants.BOARD_GANGLION: 443 | raise ValueError('Cannot use with Ganglion') 444 | ch_array = list("12345678QWERTYUI") 445 | # defaults 446 | command = list("x1060110X") 447 | # Set channel 448 | command[1] = ch_array[channel - 1] 449 | # Set power down if needed (default channel enabled) 450 | if not enabled: 451 | command[2] = '1' 452 | # Set gain (default 24) 453 | if gain == 1: 454 | command[3] = '0' 455 | if gain == 2: 456 | command[3] = '1' 457 | if gain == 4: 458 | command[3] = '2' 459 | if gain == 6: 460 | command[3] = '3' 461 | if gain == 8: 462 | command[3] = '4' 463 | if gain == 12: 464 | command[3] = '5' 465 | 466 | # TODO: Implement input type (default normal) 467 | 468 | # Set bias inclusion (default include) 469 | if not include_bias: 470 | command[5] = '0' 471 | # Set srb2 use (default use) 472 | if not use_srb2: 473 | command[6] = '0' 474 | # Set srb1 use (default don't use) 475 | if use_srb1: 476 | command[6] = '1' 477 | command_send = ''.join(command) 478 | self.wifi_write(command_send) 479 | 480 | # Make sure to update gain in wifi 481 | self.gains[channel - 1] = gain 482 | self.local_wifi_server.set_gains(gains=self.gains) 483 | self.local_wifi_server.set_parser( 484 | ParseRaw(gains=self.gains, board_type=self.board_type)) 485 | 486 | except ValueError as e: 487 | print("Something went wrong while setting channel settings: " + str(e)) 488 | 489 | def set_sample_rate(self, sample_rate): 490 | """ Change sample rate """ 491 | try: 492 | if self.board_type == Constants.BOARD_CYTON or self.board_type == Constants.BOARD_DAISY: 493 | if sample_rate == 250: 494 | self.wifi_write('~6') 495 | elif sample_rate == 500: 496 | self.wifi_write('~5') 497 | elif sample_rate == 1000: 498 | self.wifi_write('~4') 499 | elif sample_rate == 2000: 500 | self.wifi_write('~3') 501 | elif sample_rate == 4000: 502 | self.wifi_write('~2') 503 | elif sample_rate == 8000: 504 | self.wifi_write('~1') 505 | elif sample_rate == 16000: 506 | self.wifi_write('~0') 507 | else: 508 | print("Sample rate not supported: " + str(sample_rate)) 509 | elif self.board_type == Constants.BOARD_GANGLION: 510 | if sample_rate == 200: 511 | self.wifi_write('~7') 512 | elif sample_rate == 400: 513 | self.wifi_write('~6') 514 | elif sample_rate == 800: 515 | self.wifi_write('~5') 516 | elif sample_rate == 1600: 517 | self.wifi_write('~4') 518 | elif sample_rate == 3200: 519 | self.wifi_write('~3') 520 | elif sample_rate == 6400: 521 | self.wifi_write('~2') 522 | elif sample_rate == 12800: 523 | self.wifi_write('~1') 524 | elif sample_rate == 25600: 525 | self.wifi_write('~0') 526 | else: 527 | print("Sample rate not supported: " + str(sample_rate)) 528 | else: 529 | print("Board type not supported for setting sample rate") 530 | except Exception as e: 531 | print("Something went wrong while setting sample rate: " + str(e)) 532 | 533 | def set_accelerometer(self, toggle_position): 534 | """ Enable / disable accelerometer """ 535 | try: 536 | if self.board_type == Constants.BOARD_GANGLION: 537 | # Commands to set toggle to on position 538 | if toggle_position == 1: 539 | self.wifi_write('n') 540 | # Commands to set toggle to off position 541 | elif toggle_position == 0: 542 | self.wifi_write('N') 543 | else: 544 | print("Board type not supported for setting accelerometer") 545 | except Exception as e: 546 | print("Something went wrong while setting accelerometer: " + str(e)) 547 | 548 | """ 549 | 550 | Clean Up (atexit) 551 | 552 | """ 553 | 554 | def stop(self): 555 | print("Stopping streaming...") 556 | self.streaming = False 557 | # connection might be already down here 558 | try: 559 | if self.impedance: 560 | print("Stopping with impedance testing") 561 | self.wifi_write('Z') 562 | else: 563 | self.wifi_write('s') 564 | except Exception as e: 565 | print("Something went wrong while asking the board to stop streaming: " + str(e)) 566 | if self.log: 567 | logging.warning('sent : stopped streaming') 568 | 569 | def disconnect(self): 570 | if self.streaming: 571 | self.stop() 572 | 573 | # should not try to read/write anything after that, will crash 574 | 575 | """ 576 | 577 | SETTINGS AND HELPERS 578 | 579 | """ 580 | 581 | def warn(self, text): 582 | if self.log: 583 | # log how many packets where sent succesfully in between warnings 584 | if self.log_packet_count: 585 | logging.info('Data packets received:' + 586 | str(self.log_packet_count)) 587 | self.log_packet_count = 0 588 | logging.warning(text) 589 | print("Warning: %s" % text) 590 | 591 | def check_connection(self): 592 | """ Check connection quality in term of lag and number of packets drop. 593 | Reinit connection if necessary. 594 | FIXME: parameters given to the board will be lost. 595 | """ 596 | # stop checking when we're no longer streaming 597 | if not self.streaming: 598 | return 599 | # check number of dropped packets and duration without new packets, deco/reco if too large 600 | if self.packets_dropped > self.max_packets_to_skip: 601 | self.warn("Too many packets dropped, attempt to reconnect") 602 | self.reconnect() 603 | elif self.timeout > 0 and timeit.default_timer() - self.time_last_packet > self.timeout: 604 | self.warn("Too long since got new data, attempt to reconnect") 605 | # if error, attempt to reconect 606 | self.reconnect() 607 | 608 | def reconnect(self): 609 | """ In case of poor connection, will shut down and relaunch everything. 610 | FIXME: parameters given to the board will be lost.""" 611 | self.warn('Reconnecting') 612 | self.stop() 613 | self.disconnect() 614 | self.connect() 615 | self.init_streaming() 616 | 617 | 618 | class WiFiShieldHandler(asyncore.dispatcher_with_send): 619 | def __init__(self, sock, callback=None, high_speed=True, 620 | parser=None, daisy=False): 621 | asyncore.dispatcher_with_send.__init__(self, sock) 622 | 623 | self.callback = callback 624 | self.daisy = daisy 625 | self.high_speed = high_speed 626 | self.last_odd_sample = OpenBCISample() 627 | self.parser = parser if parser is not None else ParseRaw( 628 | gains=[24, 24, 24, 24, 24, 24, 24, 24]) 629 | 630 | def handle_read(self): 631 | # 3000 is the max data the WiFi shield is allowed to send over TCP 632 | data = self.recv(3000) 633 | if len(data) > 2: 634 | if self.high_speed: 635 | packets = int(len(data) / 33) 636 | raw_data_packets = [] 637 | for i in range(packets): 638 | raw_data_packets.append( 639 | bytearray(data[i * Constants.RAW_PACKET_SIZE: i * Constants.RAW_PACKET_SIZE + 640 | Constants.RAW_PACKET_SIZE])) 641 | samples = self.parser.transform_raw_data_packets_to_sample( 642 | raw_data_packets=raw_data_packets) 643 | 644 | for sample in samples: 645 | # if a daisy module is attached, wait to concatenate two samples 646 | # (main board + daisy) before passing it to callback 647 | if self.daisy: 648 | # odd sample: daisy sample, save for later 649 | if ~sample.sample_number % 2: 650 | self.last_odd_sample = sample 651 | # even sample: concatenate and send if last sample was the first part, 652 | # otherwise drop the packet 653 | elif sample.sample_number - 1 == self.last_odd_sample.sample_number: 654 | # the aux data will be the average between the two samples, as the 655 | # channel samples themselves have been averaged by the board 656 | daisy_sample = self.parser.make_daisy_sample_object_wifi( 657 | self.last_odd_sample, sample) 658 | if self.callback is not None: 659 | self.callback(daisy_sample) 660 | else: 661 | if self.callback is not None: 662 | self.callback(sample) 663 | 664 | else: 665 | try: 666 | possible_chunks = data.split('\r\n') 667 | if len(possible_chunks) > 1: 668 | possible_chunks = possible_chunks[:-1] 669 | for possible_chunk in possible_chunks: 670 | if len(possible_chunk) > 2: 671 | chunk_dict = json.loads(possible_chunk) 672 | if 'chunk' in chunk_dict: 673 | for sample in chunk_dict['chunk']: 674 | if self.callback is not None: 675 | self.callback(sample) 676 | else: 677 | print("not a sample packet") 678 | except ValueError as e: 679 | print("failed to parse: %s" % data) 680 | print(e) 681 | except BaseException as e: 682 | print(e) 683 | 684 | 685 | class WiFiShieldServer(asyncore.dispatcher): 686 | 687 | def __init__(self, host, port, callback=None, gains=None, high_speed=True, daisy=False): 688 | asyncore.dispatcher.__init__(self) 689 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 690 | self.set_reuse_addr() 691 | self.bind((host, port)) 692 | self.daisy = daisy 693 | self.listen(5) 694 | self.callback = None 695 | self.handler = None 696 | self.parser = ParseRaw(gains=gains) 697 | self.high_speed = high_speed 698 | 699 | def handle_accept(self): 700 | pair = self.accept() 701 | if pair is not None: 702 | sock, addr = pair 703 | print('Incoming connection from %s' % repr(addr)) 704 | self.handler = WiFiShieldHandler(sock, self.callback, high_speed=self.high_speed, 705 | parser=self.parser, daisy=self.daisy) 706 | 707 | def set_callback(self, callback): 708 | self.callback = callback 709 | if self.handler is not None: 710 | self.handler.callback = callback 711 | 712 | def set_daisy(self, daisy): 713 | self.daisy = daisy 714 | if self.handler is not None: 715 | self.handler.daisy = daisy 716 | 717 | def set_gains(self, gains): 718 | self.parser.set_ads1299_scale_factors(gains) 719 | 720 | def set_parser(self, parser): 721 | self.parser = parser 722 | if self.handler is not None: 723 | self.handler.parser = parser 724 | -------------------------------------------------------------------------------- /plugin_interface.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extends Yapsy IPlugin interface to pass information about the board to plugins. 3 | 4 | Fields of interest for plugins: 5 | args: list of arguments passed to the plugins 6 | sample_rate: actual sample rate of the board 7 | eeg_channels: number of EEG 8 | aux_channels: number of AUX channels 9 | 10 | If needed, plugins that need to report an error can set self.is_activated to False during activate() call. 11 | 12 | NB: because of how yapsy discovery system works, plugins must use the following syntax to inherit to use polymorphism (see http://yapsy.sourceforge.net/Advices.html): 13 | 14 | import plugin_interface as plugintypes 15 | 16 | class PluginExample(plugintypes.IPluginExtended): 17 | ... 18 | """ 19 | 20 | from __future__ import print_function 21 | from yapsy.IPlugin import IPlugin 22 | 23 | 24 | class IPluginExtended(IPlugin): 25 | # args: passed by command line 26 | def pre_activate(self, args, sample_rate=250, eeg_channels=8, aux_channels=3, imp_channels=0): 27 | self.args = args 28 | self.sample_rate = sample_rate 29 | self.eeg_channels = eeg_channels 30 | self.aux_channels = aux_channels 31 | self.imp_channels = imp_channels 32 | # by default we say that activation was okay -- inherited from IPlugin 33 | self.is_activated = True 34 | self.activate() 35 | # tell outside world if init went good or bad 36 | return self.is_activated 37 | 38 | # inherited from IPlugin 39 | def activate(self): 40 | print("Plugin %s activated." % (self.__class__.__name__)) 41 | 42 | # inherited from IPlugin 43 | def deactivate(self): 44 | print("Plugin %s deactivated." % (self.__class__.__name__)) 45 | 46 | # plugins that require arguments should implement this method 47 | def show_help(self): 48 | print("I, %s, do not need any parameter." % (self.__class__.__name__)) 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.9.2 2 | pylsl==1.10.4 3 | python-osc==1.6.3 4 | pyserial==2.7 5 | requests==2.7.0 6 | six==1.9.0 7 | socketIO-client==0.6.5 8 | websocket-client==0.32.0 9 | wheel==0.24.0 10 | Yapsy==1.11.23 11 | xmltodict -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | Various use-case of `open_bci_v3.py` for those who don't want to go through `user.py` and plugins -- but beware of code fragmentation! 2 | -------------------------------------------------------------------------------- /scripts/simple_serial.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import struct 3 | import numpy as np 4 | import time 5 | import timeit 6 | import atexit 7 | import logging 8 | import threading 9 | import sys 10 | import pdb 11 | 12 | port = '/dev/tty.OpenBCI-DN008VTF' 13 | # port = '/dev/tty.OpenBCI-DN0096XA' 14 | baud = 115200 15 | ser = serial.Serial(port=port, baudrate=baud, timeout=None) 16 | pdb.set_trace() 17 | -------------------------------------------------------------------------------- /scripts/socket_client.py: -------------------------------------------------------------------------------- 1 | from socketIO_client import SocketIO 2 | 3 | 4 | def on_sample(*args): 5 | print(args) 6 | 7 | 8 | socketIO = SocketIO('10.0.1.194', 8880) 9 | socketIO.on('openbci', on_sample) 10 | socketIO.wait(seconds=10) 11 | -------------------------------------------------------------------------------- /scripts/stream_data.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | 4 | sys.path.append('..') # help python find cyton.py relative to scripts folder 5 | from openbci import cyton as bci 6 | from openbci.plugins import StreamerTCPServer 7 | import time, timeit 8 | from threading import Thread 9 | 10 | # Transmit data to openvibe acquisition server, intelpolating data (well, sort of) from 250Hz to 256Hz 11 | # Listen to new connections every second using a separate thread. 12 | 13 | # NB: Left here for resampling algorithm, prefer the use of user.py. 14 | 15 | NB_CHANNELS = 8 16 | 17 | # If > 0 will interpolate based on samples count, typically 1.024 to go from 250Hz to 256Hz 18 | SAMPLING_FACTOR = -1.024 19 | # If > 0 will interbolate based on elapsed time 20 | SAMPLING_RATE = 256 21 | 22 | SERVER_PORT = 12345 23 | SERVER_IP = "localhost" 24 | 25 | DEBUG = False 26 | 27 | # check packet drop 28 | last_id = -1 29 | 30 | # counter for sampling rate 31 | nb_samples_in = -1 32 | nb_samples_out = -1 33 | 34 | # last seen values for interpolation 35 | last_values = [0] * NB_CHANNELS 36 | 37 | # counter to trigger duplications... 38 | leftover_duplications = 0 39 | 40 | tick = timeit.default_timer() 41 | 42 | 43 | # try to ease work for main loop 44 | class Monitor(Thread): 45 | def __init__(self): 46 | Thread.__init__(self) 47 | self.nb_samples_in = -1 48 | self.nb_samples_out = -1 49 | # Init time to compute sampling rate 50 | self.tick = timeit.default_timer() 51 | self.start_tick = self.tick 52 | 53 | def run(self): 54 | while True: 55 | # check FPS + listen for new connections 56 | new_tick = timeit.default_timer() 57 | elapsed_time = new_tick - self.tick 58 | current_samples_in = nb_samples_in 59 | current_samples_out = nb_samples_out 60 | print("--- at t: ", (new_tick - self.start_tick), " ---") 61 | print("elapsed_time: ", elapsed_time) 62 | print("nb_samples_in: ", current_samples_in - self.nb_samples_in) 63 | print("nb_samples_out: ", current_samples_out - self.nb_samples_out) 64 | self.tick = new_tick 65 | self.nb_samples_in = nb_samples_in 66 | self.nb_samples_out = nb_samples_out 67 | # time to watch for connection 68 | # FIXME: not so great with threads 69 | server.check_connections() 70 | time.sleep(1) 71 | 72 | 73 | def streamData(sample): 74 | global last_values 75 | 76 | global tick 77 | 78 | # check packet skipped 79 | global last_id 80 | # TODO: duplicate packet if skipped to stay sync 81 | if sample.id != last_id + 1: 82 | print("time", tick, ": paquet skipped!") 83 | if sample.id == 255: 84 | last_id = -1 85 | else: 86 | last_id = sample.id 87 | 88 | # update counters 89 | global nb_samples_in, nb_samples_out 90 | nb_samples_in = nb_samples_in + 1 91 | 92 | # check for duplication, by default 1 (...which is *no* duplication of the one current sample) 93 | global leftover_duplications 94 | 95 | # first method with sampling rate and elapsed time (depends on system clock accuracy) 96 | if (SAMPLING_RATE > 0): 97 | # elapsed time since last call, update tick 98 | now = timeit.default_timer() 99 | elapsed_time = now - tick 100 | # now we have to compute how many times we should send data to keep up with sample rate (oversampling) 101 | leftover_duplications = SAMPLING_RATE * elapsed_time + leftover_duplications - 1 102 | tick = now 103 | # second method with a samplin factor (depends on openbci accuracy) 104 | elif SAMPLING_FACTOR > 0: 105 | leftover_duplications = SAMPLING_FACTOR + leftover_duplications - 1 106 | # print "needed_duplications: ", needed_duplications, "leftover_duplications: ", leftover_duplications 107 | # If we need to insert values, will interpolate between current packet and last one 108 | # FIXME: ok, at the moment because we do packet per packet treatment, only handles nb_duplications == 1 for more interpolation is bad and sends nothing 109 | if (leftover_duplications > 1): 110 | leftover_duplications = leftover_duplications - 1 111 | interpol_values = list(last_values) 112 | for i in range(0, len(interpol_values)): 113 | # OK, it's a very rough interpolation 114 | interpol_values[i] = (last_values[i] + sample.channel_data[i]) / 2 115 | if DEBUG: 116 | print(" --") 117 | print(" last values: ", last_values) 118 | print(" interpolation: ", interpol_values) 119 | print(" current sample: ", sample.channel_data) 120 | # send to clients interpolated sample 121 | # leftover_duplications = 0 122 | server.broadcast_values(interpol_values) 123 | nb_samples_out = nb_samples_out + 1 124 | 125 | # send to clients current sample 126 | server.broadcast_values(sample.channel_data) 127 | nb_samples_out = nb_samples_out + 1 128 | 129 | # save current values for possible interpolation 130 | last_values = list(sample.channel_data) 131 | 132 | 133 | if __name__ == '__main__': 134 | # init server 135 | server = StreamerTCPServer(ip=SERVER_IP, port=SERVER_PORT) 136 | # init board 137 | port = '/dev/tty.usbserial-DB00JAM0' 138 | baud = 115200 139 | monit = Monitor() 140 | # daemonize theard to terminate it altogether with the main when time will come 141 | monit.daemon = True 142 | monit.start() 143 | board = bci.OpenBCICyton(port=port, baud=baud, filter_data=False) 144 | board.start_streaming(streamData) 145 | -------------------------------------------------------------------------------- /scripts/stream_data_wifi.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | 4 | sys.path.append('..') # help python find cyton.py relative to scripts folder 5 | from openbci import wifi as bci 6 | import logging 7 | 8 | 9 | def printData(sample): 10 | print(sample.sample_number) 11 | print(sample.channel_data) 12 | 13 | 14 | if __name__ == '__main__': 15 | shield_name = 'OpenBCI-E2B6' 16 | logging.basicConfig(filename="test.log", format='%(asctime)s - %(levelname)s : %(message)s', level=logging.DEBUG) 17 | logging.info('---------LOG START-------------') 18 | shield = bci.OpenBCIWiFi(shield_name=shield_name, log=True, high_speed=False) 19 | print("WiFi Shield Instantiated") 20 | shield.start_streaming(printData) 21 | 22 | shield.loop() 23 | -------------------------------------------------------------------------------- /scripts/stream_data_wifi_high_speed.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | 4 | sys.path.append('..') # help python find cyton.py relative to scripts folder 5 | from openbci import wifi as bci 6 | import logging 7 | 8 | 9 | def printData(sample): 10 | print(sample.sample_number) 11 | print(sample.channel_data) 12 | 13 | 14 | if __name__ == '__main__': 15 | logging.basicConfig(filename="test.log", format='%(asctime)s - %(levelname)s : %(message)s', level=logging.DEBUG) 16 | logging.info('---------LOG START-------------') 17 | # If you don't know your IP Address, you can use shield name option 18 | # If you know IP, such as with wifi direct 192.168.4.1, then use ip_address='192.168.4.1' 19 | shield_name = 'OpenBCI-E218' 20 | shield = bci.OpenBCIWiFi(shield_name=shield_name, log=True, high_speed=True) 21 | print("WiFi Shield Instantiated") 22 | shield.start_streaming(printData) 23 | 24 | shield.loop() 25 | -------------------------------------------------------------------------------- /scripts/test.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | 4 | sys.path.append('..') # help python find cyton.py relative to scripts folder 5 | from openbci import cyton as bci 6 | import logging 7 | import time 8 | 9 | 10 | def printData(sample): 11 | # os.system('clear') 12 | print("----------------") 13 | print("%f" % (sample.id)) 14 | print(sample.channel_data) 15 | print(sample.aux_data) 16 | print("----------------") 17 | 18 | 19 | if __name__ == '__main__': 20 | # port = '/dev/tty.OpenBCI-DN008VTF' 21 | port = '/dev/tty.usbserial-DB00JAM0' 22 | # port = '/dev/tty.OpenBCI-DN0096XA' 23 | baud = 115200 24 | logging.basicConfig(filename="test.log", format='%(asctime)s - %(levelname)s : %(message)s', level=logging.DEBUG) 25 | logging.info('---------LOG START-------------') 26 | board = bci.OpenBCICyton(port=port, scaled_output=False, log=True) 27 | print("Board Instantiated") 28 | board.ser.write('v') 29 | time.sleep(10) 30 | board.start_streaming(printData) 31 | board.print_bytes_in() 32 | -------------------------------------------------------------------------------- /scripts/udp_client.py: -------------------------------------------------------------------------------- 1 | """A sample client for the OpenBCI UDP server.""" 2 | 3 | from __future__ import print_function 4 | import argparse 5 | 6 | try: 7 | import cPickle as pickle 8 | except ImportError: 9 | import _pickle as pickle 10 | import json 11 | import sys 12 | 13 | sys.path.append('..') # help python find cyton.py relative to scripts folder 14 | import socket 15 | 16 | parser = argparse.ArgumentParser( 17 | description='Run a UDP client listening for streaming OpenBCI data.') 18 | parser.add_argument( 19 | '--json', 20 | action='store_true', 21 | help='Handle JSON data rather than pickled Python objects.') 22 | parser.add_argument( 23 | '--host', 24 | help='The host to listen on.', 25 | default='127.0.0.1') 26 | parser.add_argument( 27 | '--port', 28 | help='The port to listen on.', 29 | default='8888') 30 | 31 | 32 | class UDPClient(object): 33 | 34 | def __init__(self, ip, port, json): 35 | self.ip = ip 36 | self.port = port 37 | self.json = json 38 | self.client = socket.socket( 39 | socket.AF_INET, # Internet 40 | socket.SOCK_DGRAM) 41 | self.client.bind((ip, port)) 42 | 43 | def start_listening(self, callback=None): 44 | while True: 45 | data, addr = self.client.recvfrom(1024) 46 | print("data") 47 | if self.json: 48 | sample = json.loads(data) 49 | # In JSON mode we only recieve channel data. 50 | print(data) 51 | else: 52 | sample = pickle.loads(data) 53 | # Note that sample is an OpenBCISample object. 54 | print(sample.id) 55 | print(sample.channel_data) 56 | 57 | 58 | args = parser.parse_args() 59 | client = UDPClient(args.host, int(args.port), args.json) 60 | client.start_listening() 61 | -------------------------------------------------------------------------------- /scripts/udp_server.py: -------------------------------------------------------------------------------- 1 | """A server that handles a connection with an OpenBCI board and serves that 2 | data over both a UDP socket server and a WebSocket server. 3 | 4 | Requires: 5 | - pyserial 6 | - asyncio 7 | - websockets 8 | """ 9 | 10 | from __future__ import print_function 11 | import argparse 12 | 13 | try: 14 | import cPickle as pickle 15 | except ImportError: 16 | import _pickle as pickle 17 | import json 18 | import sys 19 | 20 | sys.path.append('..') # help python find cyton.py relative to scripts folder 21 | from openbci import cyton as open_bci 22 | import socket 23 | 24 | parser = argparse.ArgumentParser( 25 | description='Run a UDP server streaming OpenBCI data.') 26 | parser.add_argument( 27 | '--json', 28 | action='store_true', 29 | help='Send JSON data rather than pickled Python objects.') 30 | parser.add_argument( 31 | '--filter_data', 32 | action='store_true', 33 | help='Enable onboard filtering.') 34 | parser.add_argument( 35 | '--host', 36 | help='The host to listen on.', 37 | default='127.0.0.1') 38 | parser.add_argument( 39 | '--port', 40 | help='The port to listen on.', 41 | default='8888') 42 | parser.add_argument( 43 | '--serial', 44 | help='The serial port to communicate with the OpenBCI board.', 45 | default='/dev/tty.usbmodem1421') 46 | parser.add_argument( 47 | '--baud', 48 | help='The baud of the serial connection with the OpenBCI board.', 49 | default='115200') 50 | 51 | 52 | class UDPServer(object): 53 | 54 | def __init__(self, ip, port, json): 55 | self.ip = ip 56 | self.port = port 57 | self.json = json 58 | print("Selecting raw UDP streaming. IP: ", self.ip, ", port: ", str(self.port)) 59 | self.server = socket.socket( 60 | socket.AF_INET, # Internet 61 | socket.SOCK_DGRAM) 62 | 63 | def send_data(self, data): 64 | self.server.sendto(data, (self.ip, self.port)) 65 | 66 | def handle_sample(self, sample): 67 | if self.json: 68 | # Just send channel data. 69 | self.send_data(json.dumps(sample.channel_data)) 70 | else: 71 | # Pack up and send the whole OpenBCISample object. 72 | self.send_data(pickle.dumps(sample)) 73 | 74 | 75 | args = parser.parse_args() 76 | obci = open_bci.OpenBCICyton(args.serial, int(args.baud)) 77 | if args.filter_data: 78 | obci.filter_data = True 79 | sock_server = UDPServer(args.host, int(args.port), args.json) 80 | obci.start_streaming(sock_server.handle_sample) 81 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='OpenBCI_Python', 4 | version='1.0.2', 5 | description='A lib for controlling OpenBCI Devices', 6 | author='AJ Keller', 7 | author_email='pushtheworldllc@gmail.com', 8 | license='MIT', 9 | packages=find_packages(), 10 | install_requires=['numpy'], 11 | url='https://github.com/openbci/openbci_python', # use the URL to the github repo 12 | download_url='https://github.com/openbci/openbci_python/archive/v1.0.2.tar.gz', 13 | keywords=['device', 'control', 'eeg', 'emg', 'ekg', 'ads1299', 'openbci', 'ganglion', 'cyton', 'wifi'], 14 | zip_safe=False) 15 | -------------------------------------------------------------------------------- /test_log.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import time 3 | import logging 4 | from openbci import cyton as bci 5 | import sys 6 | 7 | sys.path.append('..') # help python find cyton.py relative to scripts folder 8 | 9 | 10 | def printData(sample): 11 | # os.system('clear') 12 | print("----------------") 13 | print("%f" % (sample.id)) 14 | print(sample.channel_data) 15 | print(sample.aux_data) 16 | print("----------------") 17 | 18 | 19 | if __name__ == '__main__': 20 | port = '/dev/tty.usbserial-DN0096XA' 21 | baud = 115200 22 | logging.basicConfig(filename="test.log", format='%(message)s', level=logging.DEBUG) 23 | logging.info('---------LOG START-------------') 24 | board = bci.OpenBCICyton(port=port, scaled_output=False, log=True) 25 | 26 | # 32 bit reset 27 | board.ser.write('v') 28 | time.sleep(0.100) 29 | 30 | # connect pins to vcc 31 | board.ser.write('p') 32 | time.sleep(0.100) 33 | 34 | # board.start_streaming(printData) 35 | board.print_packets_in() 36 | -------------------------------------------------------------------------------- /tests/test_constants.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, main, skip 2 | import mock 3 | 4 | from openbci.utils import Constants 5 | 6 | 7 | class TestConstants(TestCase): 8 | 9 | def test_ads1299(self): 10 | self.assertEqual(Constants.ADS1299_GAIN_1, 1.0) 11 | self.assertEqual(Constants.ADS1299_GAIN_2, 2.0) 12 | self.assertEqual(Constants.ADS1299_GAIN_4, 4.0) 13 | self.assertEqual(Constants.ADS1299_GAIN_6, 6.0) 14 | self.assertEqual(Constants.ADS1299_GAIN_8, 8.0) 15 | self.assertEqual(Constants.ADS1299_GAIN_12, 12.0) 16 | self.assertEqual(Constants.ADS1299_GAIN_24, 24.0) 17 | self.assertEqual(Constants.ADS1299_VREF, 4.5) 18 | 19 | def test_board_types(self): 20 | self.assertEqual(Constants.BOARD_CYTON, 'cyton') 21 | self.assertEqual(Constants.BOARD_DAISY, 'daisy') 22 | self.assertEqual(Constants.BOARD_GANGLION, 'ganglion') 23 | self.assertEqual(Constants.BOARD_NONE, 'none') 24 | 25 | def test_cyton_variables(self): 26 | self.assertEqual(Constants.CYTON_ACCEL_SCALE_FACTOR_GAIN, 0.002 / (pow(2, 4))) 27 | 28 | def test_errors(self): 29 | self.assertEqual(Constants.ERROR_INVALID_BYTE_LENGTH, 'Invalid Packet Byte Length') 30 | self.assertEqual(Constants.ERROR_INVALID_BYTE_START, 'Invalid Start Byte') 31 | self.assertEqual(Constants.ERROR_INVALID_BYTE_STOP, 'Invalid Stop Byte') 32 | self.assertEqual(Constants.ERROR_INVALID_DATA, 'Invalid data - try again') 33 | self.assertEqual(Constants.ERROR_INVALID_TYPW, 'Invalid type - check comments for input type') 34 | self.assertEqual(Constants.ERROR_MISSING_REGISTER_SETTING, 'Missing register setting') 35 | self.assertEqual(Constants.ERROR_MISSING_REQUIRED_PROPERTY, 'Missing property in JSON') 36 | self.assertEqual(Constants.ERROR_TIME_SYNC_IS_NULL, "'this.sync.curSyncObj' must not be null") 37 | self.assertEqual(Constants.ERROR_TIME_SYNC_NO_COMMA, 'Missed the time sync sent confirmation. Try sync again') 38 | self.assertEqual(Constants.ERROR_UNDEFINED_OR_NULL_INPUT, 'Undefined or Null Input') 39 | 40 | def test_number_of_channels(self): 41 | self.assertEqual(Constants.NUMBER_OF_CHANNELS_CYTON, 8) 42 | self.assertEqual(Constants.NUMBER_OF_CHANNELS_DAISY, 16) 43 | self.assertEqual(Constants.NUMBER_OF_CHANNELS_GANGLION, 4) 44 | 45 | def test_protocols(self): 46 | """ Protocols """ 47 | self.assertEqual(Constants.PROTOCOL_BLE, 'ble') 48 | self.assertEqual(Constants.PROTOCOL_SERIAL, 'serial') 49 | self.assertEqual(Constants.PROTOCOL_WIFI, 'wifi') 50 | 51 | def test_raw(self): 52 | self.assertEqual(Constants.RAW_BYTE_START, 0xA0) 53 | self.assertEqual(Constants.RAW_BYTE_STOP, 0xC0) 54 | self.assertEqual(Constants.RAW_PACKET_ACCEL_NUMBER_AXIS, 3) 55 | self.assertEqual(Constants.RAW_PACKET_SIZE, 33) 56 | self.assertEqual(Constants.RAW_PACKET_POSITION_CHANNEL_DATA_START, 2) 57 | self.assertEqual(Constants.RAW_PACKET_POSITION_CHANNEL_DATA_STOP, 25) 58 | self.assertEqual(Constants.RAW_PACKET_POSITION_SAMPLE_NUMBER, 1) 59 | self.assertEqual(Constants.RAW_PACKET_POSITION_START_BYTE, 0) 60 | self.assertEqual(Constants.RAW_PACKET_POSITION_STOP_BYTE, 32) 61 | self.assertEqual(Constants.RAW_PACKET_POSITION_START_AUX, 26) 62 | self.assertEqual(Constants.RAW_PACKET_POSITION_STOP_AUX, 31) 63 | self.assertEqual(Constants.RAW_PACKET_POSITION_TIME_SYNC_AUX_START, 26) 64 | self.assertEqual(Constants.RAW_PACKET_POSITION_TIME_SYNC_AUX_STOP, 28) 65 | self.assertEqual(Constants.RAW_PACKET_POSITION_TIME_SYNC_TIME_START, 28) 66 | self.assertEqual(Constants.RAW_PACKET_POSITION_TIME_SYNC_TIME_STOP, 32) 67 | self.assertEqual(Constants.RAW_PACKET_TYPE_STANDARD_ACCEL, 0) 68 | self.assertEqual(Constants.RAW_PACKET_TYPE_STANDARD_RAW_AUX, 1) 69 | self.assertEqual(Constants.RAW_PACKET_TYPE_USER_DEFINED_TYPE, 2) 70 | self.assertEqual(Constants.RAW_PACKET_TYPE_ACCEL_TIME_SYNC_SET, 3) 71 | self.assertEqual(Constants.RAW_PACKET_TYPE_ACCEL_TIME_SYNCED, 4) 72 | self.assertEqual(Constants.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNC_SET, 5) 73 | self.assertEqual(Constants.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNCED, 6) 74 | self.assertEqual(Constants.RAW_PACKET_TYPE_IMPEDANCE, 7) 75 | 76 | def test_sample_number_max(self): 77 | self.assertEqual(Constants.SAMPLE_NUMBER_MAX_CYTON, 255) 78 | self.assertEqual(Constants.SAMPLE_NUMBER_MAX_GANGLION, 200) 79 | 80 | def test_sample_rates(self): 81 | self.assertEqual(Constants.SAMPLE_RATE_1000, 1000) 82 | self.assertEqual(Constants.SAMPLE_RATE_125, 125) 83 | self.assertEqual(Constants.SAMPLE_RATE_12800, 12800) 84 | self.assertEqual(Constants.SAMPLE_RATE_1600, 1600) 85 | self.assertEqual(Constants.SAMPLE_RATE_16000, 16000) 86 | self.assertEqual(Constants.SAMPLE_RATE_200, 200) 87 | self.assertEqual(Constants.SAMPLE_RATE_2000, 2000) 88 | self.assertEqual(Constants.SAMPLE_RATE_250, 250) 89 | self.assertEqual(Constants.SAMPLE_RATE_25600, 25600) 90 | self.assertEqual(Constants.SAMPLE_RATE_3200, 3200) 91 | self.assertEqual(Constants.SAMPLE_RATE_400, 400) 92 | self.assertEqual(Constants.SAMPLE_RATE_4000, 4000) 93 | self.assertEqual(Constants.SAMPLE_RATE_500, 500) 94 | self.assertEqual(Constants.SAMPLE_RATE_6400, 6400) 95 | self.assertEqual(Constants.SAMPLE_RATE_800, 800) 96 | self.assertEqual(Constants.SAMPLE_RATE_8000, 8000) 97 | 98 | 99 | if __name__ == '__main__': 100 | main() 101 | -------------------------------------------------------------------------------- /tests/test_cyton.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from openbci.cyton import OpenBCICyton 3 | 4 | PORT = 'loop://' 5 | 6 | 7 | class TestOpenBCICyton(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.cyton = OpenBCICyton(port=PORT) 11 | 12 | def tearDown(self): 13 | self.cyton.disconnect() 14 | 15 | def test_init(self): 16 | """After initialization, we send b'v' to initialize 32-bit board.""" 17 | self.assertEqual(self.cyton.ser_read(), b'v', 18 | "Expected initialization character") 19 | 20 | def test_filter_toggles(self): 21 | self.test_init() 22 | 23 | self.cyton.enable_filters() 24 | self.assertEqual(self.cyton.ser_read(), b'f', 25 | "Expected enable filter character") 26 | self.assertTrue(self.cyton.filtering_data) 27 | 28 | self.cyton.disable_filters() 29 | self.assertEqual(self.cyton.ser_read(), b'g', 30 | "Expected disable filter character") 31 | self.assertFalse(self.cyton.filtering_data) 32 | 33 | def test_test_signal(self): 34 | self.test_init() 35 | 36 | self.cyton.test_signal(0) 37 | self.assertEqual(self.cyton.ser_read(), b'0') 38 | self.cyton.test_signal(1) 39 | self.assertEqual(self.cyton.ser_read(), b'p') 40 | self.cyton.test_signal(2) 41 | self.assertEqual(self.cyton.ser_read(), b'-') 42 | self.cyton.test_signal(3) 43 | self.assertEqual(self.cyton.ser_read(), b'=') 44 | self.cyton.test_signal(4) 45 | self.assertEqual(self.cyton.ser_read(), b'[') 46 | self.cyton.test_signal(5) 47 | self.assertEqual(self.cyton.ser_read(), b']') 48 | 49 | def test_set_channel(self): 50 | self.test_init() 51 | self.cyton.daisy = True 52 | 53 | self.cyton.set_channel(channel=1, toggle_position=0) 54 | self.assertEqual(self.cyton.ser_read(), b'1') 55 | self.cyton.set_channel(channel=2, toggle_position=0) 56 | self.assertEqual(self.cyton.ser_read(), b'2') 57 | self.cyton.set_channel(channel=3, toggle_position=0) 58 | self.assertEqual(self.cyton.ser_read(), b'3') 59 | self.cyton.set_channel(channel=4, toggle_position=0) 60 | self.assertEqual(self.cyton.ser_read(), b'4') 61 | self.cyton.set_channel(channel=5, toggle_position=0) 62 | self.assertEqual(self.cyton.ser_read(), b'5') 63 | self.cyton.set_channel(channel=6, toggle_position=0) 64 | self.assertEqual(self.cyton.ser_read(), b'6') 65 | self.cyton.set_channel(channel=7, toggle_position=0) 66 | self.assertEqual(self.cyton.ser_read(), b'7') 67 | self.cyton.set_channel(channel=8, toggle_position=0) 68 | self.assertEqual(self.cyton.ser_read(), b'8') 69 | self.cyton.set_channel(channel=9, toggle_position=0) 70 | self.assertEqual(self.cyton.ser_read(), b'q') 71 | self.cyton.set_channel(channel=10, toggle_position=0) 72 | self.assertEqual(self.cyton.ser_read(), b'w') 73 | self.cyton.set_channel(channel=11, toggle_position=0) 74 | self.assertEqual(self.cyton.ser_read(), b'e') 75 | self.cyton.set_channel(channel=12, toggle_position=0) 76 | self.assertEqual(self.cyton.ser_read(), b'r') 77 | self.cyton.set_channel(channel=13, toggle_position=0) 78 | self.assertEqual(self.cyton.ser_read(), b't') 79 | self.cyton.set_channel(channel=14, toggle_position=0) 80 | self.assertEqual(self.cyton.ser_read(), b'y') 81 | self.cyton.set_channel(channel=15, toggle_position=0) 82 | self.assertEqual(self.cyton.ser_read(), b'u') 83 | self.cyton.set_channel(channel=16, toggle_position=0) 84 | self.assertEqual(self.cyton.ser_read(), b'i') 85 | 86 | self.cyton.set_channel(channel=1, toggle_position=1) 87 | self.assertEqual(self.cyton.ser_read(), b'!') 88 | self.cyton.set_channel(channel=2, toggle_position=1) 89 | self.assertEqual(self.cyton.ser_read(), b'@') 90 | self.cyton.set_channel(channel=3, toggle_position=1) 91 | self.assertEqual(self.cyton.ser_read(), b'#') 92 | self.cyton.set_channel(channel=4, toggle_position=1) 93 | self.assertEqual(self.cyton.ser_read(), b'$') 94 | self.cyton.set_channel(channel=5, toggle_position=1) 95 | self.assertEqual(self.cyton.ser_read(), b'%') 96 | self.cyton.set_channel(channel=6, toggle_position=1) 97 | self.assertEqual(self.cyton.ser_read(), b'^') 98 | self.cyton.set_channel(channel=7, toggle_position=1) 99 | self.assertEqual(self.cyton.ser_read(), b'&') 100 | self.cyton.set_channel(channel=8, toggle_position=1) 101 | self.assertEqual(self.cyton.ser_read(), b'*') 102 | self.cyton.set_channel(channel=9, toggle_position=1) 103 | self.assertEqual(self.cyton.ser_read(), b'Q') 104 | self.cyton.set_channel(channel=10, toggle_position=1) 105 | self.assertEqual(self.cyton.ser_read(), b'W') 106 | self.cyton.set_channel(channel=11, toggle_position=1) 107 | self.assertEqual(self.cyton.ser_read(), b'E') 108 | self.cyton.set_channel(channel=12, toggle_position=1) 109 | self.assertEqual(self.cyton.ser_read(), b'R') 110 | self.cyton.set_channel(channel=13, toggle_position=1) 111 | self.assertEqual(self.cyton.ser_read(), b'T') 112 | self.cyton.set_channel(channel=14, toggle_position=1) 113 | self.assertEqual(self.cyton.ser_read(), b'Y') 114 | self.cyton.set_channel(channel=15, toggle_position=1) 115 | self.assertEqual(self.cyton.ser_read(), b'U') 116 | self.cyton.set_channel(channel=16, toggle_position=1) 117 | self.assertEqual(self.cyton.ser_read(), b'I') 118 | 119 | 120 | if __name__ == "__main__": 121 | unittest.main() 122 | -------------------------------------------------------------------------------- /tests/test_parse.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, main, skip 2 | import mock 3 | 4 | from openbci.utils import (Constants, 5 | OpenBCISample, 6 | ParseRaw, 7 | sample_packet, 8 | sample_packet_standard_raw_aux, 9 | sample_packet_accel_time_sync_set, 10 | sample_packet_accel_time_synced, 11 | sample_packet_raw_aux_time_sync_set, 12 | sample_packet_raw_aux_time_synced, 13 | RawDataToSample) 14 | 15 | 16 | class TestParseRaw(TestCase): 17 | 18 | def test_get_channel_data_array(self): 19 | expected_gains = [24, 24, 24, 24, 24, 24, 24, 24] 20 | expected_sample_number = 0 21 | 22 | data = sample_packet(expected_sample_number) 23 | 24 | parser = ParseRaw(gains=expected_gains, scaled_output=True) 25 | 26 | scale_factors = parser.get_ads1299_scale_factors(expected_gains) 27 | 28 | expected_channel_data = [] 29 | for i in range(Constants.NUMBER_OF_CHANNELS_CYTON): 30 | expected_channel_data.append(scale_factors[i] * (i + 1)) 31 | 32 | parser.raw_data_to_sample.raw_data_packet = data 33 | 34 | actual_channel_data = parser.get_channel_data_array(parser.raw_data_to_sample) 35 | 36 | self.assertListEqual(actual_channel_data, expected_channel_data) 37 | 38 | def test_get_data_array_accel(self): 39 | expected_sample_number = 0 40 | 41 | data = sample_packet(expected_sample_number) 42 | 43 | parser = ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24], scaled_output=True) 44 | 45 | expected_accel_data = [] 46 | for i in range(Constants.RAW_PACKET_ACCEL_NUMBER_AXIS): 47 | expected_accel_data.append(Constants.CYTON_ACCEL_SCALE_FACTOR_GAIN * i) 48 | 49 | parser.raw_data_to_sample.raw_data_packet = data 50 | 51 | actual_accel_data = parser.get_data_array_accel(parser.raw_data_to_sample) 52 | 53 | self.assertListEqual(actual_accel_data, expected_accel_data) 54 | 55 | def test_interpret_16_bit_as_int_32(self): 56 | 57 | parser = ParseRaw() 58 | 59 | # 0x0690 === 1680 60 | self.assertEqual(parser.interpret_16_bit_as_int_32(bytearray([0x06, 0x90])), 61 | 1680, 62 | 'converts a small positive number') 63 | 64 | # 0x02C0 === 704 65 | self.assertEqual(parser.interpret_16_bit_as_int_32(bytearray([0x02, 0xC0])), 66 | 704, 67 | 'converts a large positive number') 68 | 69 | # 0xFFFF === -1 70 | self.assertEqual(parser.interpret_16_bit_as_int_32(bytearray([0xFF, 0xFF])), 71 | -1, 72 | 'converts a small negative number') 73 | 74 | # 0x81A1 === -32351 75 | self.assertEqual(parser.interpret_16_bit_as_int_32(bytearray([0x81, 0xA1])), 76 | -32351, 77 | 'converts a large negative number') 78 | 79 | def test_interpret_24_bit_as_int_32(self): 80 | 81 | parser = ParseRaw() 82 | 83 | # 0x000690 === 1680 84 | expected_value = 1680 85 | actual_value = parser.interpret_24_bit_as_int_32(bytearray([0x00, 0x06, 0x90])) 86 | self.assertEqual(actual_value, 87 | expected_value, 88 | 'converts a small positive number') 89 | 90 | # 0x02C001 === 180225 91 | expected_value = 180225 92 | actual_value = parser.interpret_24_bit_as_int_32(bytearray([0x02, 0xC0, 0x01])) 93 | self.assertEqual(actual_value, 94 | expected_value, 95 | 'converts a large positive number') 96 | 97 | # 0xFFFFFF === -1 98 | expected_value = -1 99 | actual_value = parser.interpret_24_bit_as_int_32(bytearray([0xFF, 0xFF, 0xFF])) 100 | self.assertEqual(actual_value, 101 | expected_value, 102 | 'converts a small negative number') 103 | 104 | # 0x81A101 === -8281855 105 | expected_value = -8281855 106 | actual_value = parser.interpret_24_bit_as_int_32(bytearray([0x81, 0xA1, 0x01])) 107 | self.assertEqual(actual_value, 108 | expected_value, 109 | 'converts a large negative number') 110 | 111 | def test_parse_raw_init(self): 112 | expected_board_type = Constants.BOARD_DAISY 113 | expected_gains = [24, 24, 24, 24, 24, 24, 24, 24] 114 | expected_log = True 115 | expected_micro_volts = True 116 | expected_scaled_output = False 117 | 118 | parser = ParseRaw(board_type=expected_board_type, 119 | gains=expected_gains, 120 | log=expected_log, 121 | micro_volts=expected_micro_volts, 122 | scaled_output=expected_scaled_output) 123 | 124 | self.assertEqual(parser.board_type, expected_board_type) 125 | self.assertEqual(parser.scaled_output, expected_scaled_output) 126 | self.assertEqual(parser.log, expected_log) 127 | 128 | def test_get_ads1299_scale_factors_volts(self): 129 | gains = [24, 24, 24, 24, 24, 24, 24, 24] 130 | expected_scale_factors = [] 131 | for gain in gains: 132 | scale_factor = 4.5 / float((pow(2, 23) - 1)) / float(gain) 133 | expected_scale_factors.append(scale_factor) 134 | 135 | parser = ParseRaw() 136 | 137 | actual_scale_factors = parser.get_ads1299_scale_factors(gains) 138 | 139 | self.assertEqual(actual_scale_factors, 140 | expected_scale_factors, 141 | "should be able to get scale factors for gains in volts") 142 | 143 | def test_get_ads1299_scale_factors_micro_volts(self): 144 | gains = [24, 24, 24, 24, 24, 24, 24, 24] 145 | micro_volts = True 146 | expected_scale_factors = [] 147 | for gain in gains: 148 | scale_factor = 4.5 / float((pow(2, 23) - 1)) / float(gain) * 1000000. 149 | expected_scale_factors.append(scale_factor) 150 | 151 | parser = ParseRaw() 152 | 153 | actual_scale_factors = parser.get_ads1299_scale_factors(gains, micro_volts) 154 | 155 | self.assertEqual(actual_scale_factors, 156 | expected_scale_factors, 157 | "should be able to get scale factors for gains in volts") 158 | 159 | def test_parse_packet_standard_accel(self): 160 | data = sample_packet() 161 | 162 | expected_scale_factor = 4.5 / 24 / (pow(2, 23) - 1) 163 | 164 | parser = ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24], scaled_output=True) 165 | 166 | parser.raw_data_to_sample.raw_data_packet = data 167 | 168 | sample = parser.parse_packet_standard_accel(parser.raw_data_to_sample) 169 | 170 | self.assertIsNotNone(sample) 171 | for i in range(len(sample.channel_data)): 172 | self.assertEqual(sample.channel_data[i], expected_scale_factor * (i + 1)) 173 | for i in range(len(sample.accel_data)): 174 | self.assertEqual(sample.accel_data[i], Constants.CYTON_ACCEL_SCALE_FACTOR_GAIN * i) 175 | self.assertEqual(sample.packet_type, Constants.RAW_PACKET_TYPE_STANDARD_ACCEL) 176 | self.assertEqual(sample.sample_number, 0x45) 177 | self.assertEqual(sample.start_byte, 0xA0) 178 | self.assertEqual(sample.stop_byte, 0xC0) 179 | self.assertTrue(sample.valid) 180 | 181 | @mock.patch.object(ParseRaw, 'parse_packet_standard_accel') 182 | def test_transform_raw_data_packet_to_sample_accel(self, mock_parse_packet_standard_accel): 183 | data = sample_packet(0) 184 | 185 | parser = ParseRaw() 186 | 187 | parser.transform_raw_data_packet_to_sample(data) 188 | 189 | mock_parse_packet_standard_accel.assert_called_once() 190 | 191 | @mock.patch.object(ParseRaw, 'parse_packet_standard_raw_aux') 192 | def test_transform_raw_data_packet_to_sample_raw_aux(self, mock_parse_packet_standard_raw_aux): 193 | data = sample_packet_standard_raw_aux(0) 194 | 195 | parser = ParseRaw() 196 | 197 | parser.transform_raw_data_packet_to_sample(data) 198 | 199 | mock_parse_packet_standard_raw_aux.assert_called_once() 200 | 201 | @mock.patch.object(ParseRaw, 'parse_packet_time_synced_accel') 202 | def test_transform_raw_data_packet_to_sample_time_sync_accel(self, mock_parse_packet_time_synced_accel): 203 | data = sample_packet_accel_time_sync_set(0) 204 | 205 | parser = ParseRaw() 206 | 207 | parser.transform_raw_data_packet_to_sample(data) 208 | 209 | mock_parse_packet_time_synced_accel.assert_called_once() 210 | 211 | mock_parse_packet_time_synced_accel.reset_mock() 212 | 213 | data = sample_packet_accel_time_synced(0) 214 | 215 | parser.transform_raw_data_packet_to_sample(data) 216 | 217 | mock_parse_packet_time_synced_accel.assert_called_once() 218 | 219 | @mock.patch.object(ParseRaw, 'parse_packet_time_synced_raw_aux') 220 | def test_transform_raw_data_packet_to_sample_time_sync_raw(self, mock_parse_packet_time_synced_raw_aux): 221 | data = sample_packet_raw_aux_time_sync_set(0) 222 | 223 | parser = ParseRaw() 224 | 225 | parser.transform_raw_data_packet_to_sample(data) 226 | 227 | mock_parse_packet_time_synced_raw_aux.assert_called_once() 228 | 229 | mock_parse_packet_time_synced_raw_aux.reset_mock() 230 | 231 | data = sample_packet_raw_aux_time_synced(0) 232 | 233 | parser.transform_raw_data_packet_to_sample(data) 234 | 235 | mock_parse_packet_time_synced_raw_aux.assert_called_once() 236 | 237 | def test_transform_raw_data_packets_to_sample(self): 238 | datas = [sample_packet(0), sample_packet(1), sample_packet(2)] 239 | 240 | parser = ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24]) 241 | 242 | samples = parser.transform_raw_data_packets_to_sample(datas) 243 | 244 | self.assertEqual(len(samples), len(datas)) 245 | 246 | for i in range(len(samples)): 247 | self.assertEqual(samples[i].sample_number, i) 248 | 249 | def test_make_daisy_sample_object_wifi(self): 250 | parser = ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24]) 251 | # Make the lower sample(channels 1 - 8) 252 | lower_sample_object = OpenBCISample(sample_number=1) 253 | lower_sample_object.channel_data = [1, 2, 3, 4, 5, 6, 7, 8] 254 | lower_sample_object.aux_data = [0, 1, 2] 255 | lower_sample_object.timestamp = 4 256 | lower_sample_object.accel_data = [0, 0, 0] 257 | # Make the upper sample(channels 9 - 16) 258 | upper_sample_object = OpenBCISample(sample_number=2) 259 | upper_sample_object.channel_data = [9, 10, 11, 12, 13, 14, 15, 16] 260 | upper_sample_object.accel_data = [0, 1, 2] 261 | upper_sample_object.aux_data = [3, 4, 5] 262 | upper_sample_object.timestamp = 8 263 | 264 | daisy_sample_object = parser.make_daisy_sample_object_wifi(lower_sample_object, upper_sample_object) 265 | 266 | # should have valid object true 267 | self.assertTrue(daisy_sample_object.valid) 268 | 269 | # should make a channelData array 16 elements long 270 | self.assertEqual(len(daisy_sample_object.channel_data), Constants.NUMBER_OF_CHANNELS_DAISY) 271 | 272 | # should make a channelData array with lower array in front of upper array 273 | for i in range(16): 274 | self.assertEqual(daisy_sample_object.channel_data[i], i + 1) 275 | 276 | self.assertEqual(daisy_sample_object.id, daisy_sample_object.sample_number) 277 | self.assertEqual(daisy_sample_object.sample_number, daisy_sample_object.sample_number) 278 | 279 | # should put the aux packets in an object 280 | self.assertIsNotNone(daisy_sample_object.aux_data['lower']) 281 | self.assertIsNotNone(daisy_sample_object.aux_data['upper']) 282 | 283 | # should put the aux packets in an object in the right order 284 | for i in range(3): 285 | self.assertEqual(daisy_sample_object.aux_data['lower'][i], i) 286 | self.assertEqual(daisy_sample_object.aux_data['upper'][i], i + 3) 287 | 288 | # should take the lower timestamp 289 | self.assertEqual(daisy_sample_object.timestamp, lower_sample_object.timestamp) 290 | 291 | # should take the lower stopByte 292 | self.assertEqual(daisy_sample_object.stop_byte, lower_sample_object.stop_byte) 293 | 294 | # should place the old timestamps in an object 295 | self.assertEqual(daisy_sample_object._timestamps['lower'], lower_sample_object.timestamp) 296 | self.assertEqual(daisy_sample_object._timestamps['upper'], upper_sample_object.timestamp) 297 | 298 | # should store an accelerometer value if present 299 | self.assertIsNotNone(daisy_sample_object.accel_data) 300 | self.assertListEqual(daisy_sample_object.accel_data, [0, 1, 2]) 301 | 302 | lower_sample = OpenBCISample(sample_number=1) 303 | lower_sample.accel_data = [0, 1, 2] 304 | upper_sample = OpenBCISample(sample_number=2) 305 | upper_sample.accel_data = [0, 0, 0] 306 | 307 | # Call the function under test 308 | daisy_sample = parser.make_daisy_sample_object_wifi(lower_sample, upper_sample) 309 | 310 | self.assertIsNotNone(daisy_sample.accel_data) 311 | self.assertListEqual(daisy_sample.accel_data, [0, 1, 2]) 312 | 313 | 314 | if __name__ == '__main__': 315 | main() 316 | -------------------------------------------------------------------------------- /tests/test_wifi.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, main, skip 2 | import mock 3 | 4 | from openbci import OpenBCIWiFi 5 | 6 | 7 | class TestOpenBCIWiFi(TestCase): 8 | 9 | @mock.patch.object(OpenBCIWiFi, 'on_shield_found') 10 | def test_wifi_init(self, mock_on_shield_found): 11 | expected_ip_address = '192.168.0.1' 12 | expected_shield_name = 'OpenBCI-E218' 13 | expected_sample_rate = 500 14 | expected_log = False 15 | expected_timeout = 5 16 | expected_max_packets_to_skip = 10 17 | expected_latency = 5000 18 | expected_high_speed = False 19 | expected_ssdp_attempts = 2 20 | 21 | wifi = OpenBCIWiFi(ip_address=expected_ip_address, 22 | shield_name=expected_shield_name, 23 | sample_rate=expected_sample_rate, 24 | log=expected_log, 25 | timeout=expected_timeout, 26 | max_packets_to_skip=expected_max_packets_to_skip, 27 | latency=expected_latency, 28 | high_speed=expected_high_speed, 29 | ssdp_attempts=expected_ssdp_attempts) 30 | 31 | self.assertEqual(wifi.ip_address, expected_ip_address) 32 | self.assertEqual(wifi.shield_name, expected_shield_name) 33 | self.assertEqual(wifi.sample_rate, expected_sample_rate) 34 | self.assertEqual(wifi.log, expected_log) 35 | self.assertEqual(wifi.timeout, expected_timeout) 36 | self.assertEqual(wifi.max_packets_to_skip, expected_max_packets_to_skip) 37 | self.assertEqual(wifi.latency, expected_latency) 38 | self.assertEqual(wifi.high_speed, expected_high_speed) 39 | self.assertEqual(wifi.ssdp_attempts, expected_ssdp_attempts) 40 | 41 | mock_on_shield_found.assert_called_with(expected_ip_address) 42 | 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | from __future__ import print_function 3 | from yapsy.PluginManager import PluginManager 4 | import argparse # new in Python2.7 5 | import atexit 6 | import logging 7 | import string 8 | import sys 9 | import threading 10 | import time 11 | 12 | logging.basicConfig(level=logging.ERROR) 13 | 14 | 15 | # Load the plugins from the plugin directory. 16 | manager = PluginManager() 17 | 18 | if __name__ == '__main__': 19 | 20 | print("------------user.py-------------") 21 | parser = argparse.ArgumentParser(description="OpenBCI 'user'") 22 | parser.add_argument('--board', default="cyton", 23 | help="Choose between [cyton] and [ganglion] boards.") 24 | parser.add_argument('-l', '--list', action='store_true', 25 | help="List available plugins.") 26 | parser.add_argument('-i', '--info', metavar='PLUGIN', 27 | help="Show more information about a plugin.") 28 | parser.add_argument('-p', '--port', 29 | help="For Cyton, port to connect to OpenBCI Dongle " + 30 | "( ex /dev/ttyUSB0 or /dev/tty.usbserial-* ). " + 31 | "For Ganglion, MAC address of the board. For both, AUTO to attempt auto-detection.") 32 | parser.set_defaults(port="AUTO") 33 | # baud rate is not currently used 34 | parser.add_argument('-b', '--baud', default=115200, type=int, 35 | help="Baud rate (not currently used)") 36 | parser.add_argument('--no-filtering', dest='filtering', 37 | action='store_false', 38 | help="Disable notch filtering") 39 | parser.set_defaults(filtering=True) 40 | parser.add_argument('-d', '--daisy', dest='daisy', 41 | action='store_true', 42 | help="Force daisy mode (cyton board)") 43 | parser.add_argument('-x', '--aux', dest='aux', 44 | action='store_true', 45 | help="Enable accelerometer/AUX data (ganglion board)") 46 | # first argument: plugin name, then parameters for plugin 47 | parser.add_argument('-a', '--add', metavar=('PLUGIN', 'PARAM'), 48 | action='append', nargs='+', 49 | help="Select which plugins to activate and set parameters.") 50 | parser.add_argument('--log', dest='log', action='store_true', 51 | help="Log program") 52 | parser.add_argument('--plugins-path', dest='plugins_path', nargs='+', 53 | help="Additional path(s) to look for plugins") 54 | 55 | parser.set_defaults(daisy=False, log=False) 56 | 57 | args = parser.parse_args() 58 | 59 | if not args.add: 60 | print("WARNING: no plugin selected, you will only be able to communicate with the board. " 61 | "You should select at least one plugin with '--add [plugin_name]'. " 62 | "Use '--list' to show available plugins or '--info [plugin_name]' to get more information.") 63 | 64 | if args.board == "cyton": 65 | print("Board type: OpenBCI Cyton (v3 API)") 66 | import openbci.cyton as bci 67 | elif args.board == "ganglion": 68 | print("Board type: OpenBCI Ganglion") 69 | import openbci.ganglion as bci 70 | else: 71 | raise ValueError('Board type %r was not recognized. Known are 3 and 4' % args.board) 72 | 73 | # Check AUTO port selection, a "None" parameter for the board API 74 | if "AUTO" == args.port.upper(): 75 | print("Will try do auto-detect board's port. Set it manually with '--port' if it goes wrong.") 76 | args.port = None 77 | else: 78 | print("Port: ", args.port) 79 | 80 | plugins_paths = ["openbci/plugins"] 81 | if args.plugins_path: 82 | plugins_paths += args.plugins_path 83 | manager.setPluginPlaces(plugins_paths) 84 | manager.collectPlugins() 85 | 86 | # Print list of available plugins and exit 87 | if args.list: 88 | print("Available plugins:") 89 | for plugin in manager.getAllPlugins(): 90 | print("\t- " + plugin.name) 91 | exit() 92 | 93 | # User wants more info about a plugin... 94 | if args.info: 95 | plugin = manager.getPluginByName(args.info) 96 | if plugin == None: 97 | # eg: if an import fail inside a plugin, yapsy skip it 98 | print("Error: [ " + args.info + 99 | " ] not found or could not be loaded. Check name and requirements.") 100 | else: 101 | print(plugin.description) 102 | plugin.plugin_object.show_help() 103 | exit() 104 | 105 | print("\n------------SETTINGS-------------") 106 | print("Notch filtering:" + str(args.filtering)) 107 | 108 | # Logging 109 | if args.log: 110 | print("Logging Enabled: " + str(args.log)) 111 | logging.basicConfig(filename="OBCI.log", format='%(asctime)s - %(levelname)s : %(message)s', 112 | level=logging.DEBUG) 113 | logging.getLogger('yapsy').setLevel(logging.DEBUG) 114 | logging.info('---------LOG START-------------') 115 | logging.info(args) 116 | else: 117 | print("user.py: Logging Disabled.") 118 | 119 | print("\n-------INSTANTIATING BOARD-------") 120 | if args.board == "cyton": 121 | board = bci.OpenBCICyton(port=args.port, 122 | baud=args.baud, 123 | daisy=args.daisy, 124 | filter_data=args.filtering, 125 | scaled_output=True, 126 | log=args.log) 127 | elif args.board == "ganglion": 128 | board = bci.OpenBCIGanglion(port=args.port, 129 | filter_data=args.filtering, 130 | scaled_output=True, 131 | log=args.log, 132 | aux=args.aux) 133 | 134 | # Info about effective number of channels and sampling rate 135 | if board.daisy: 136 | print("Force daisy mode:") 137 | else: 138 | print("No daisy:") 139 | print(board.getNbEEGChannels(), "EEG channels and", board.getNbAUXChannels(), "AUX channels at", 140 | board.getSampleRate(), "Hz.") 141 | 142 | print("\n------------PLUGINS--------------") 143 | # Loop round the plugins and print their names. 144 | print("Found plugins:") 145 | for plugin in manager.getAllPlugins(): 146 | print("[ " + plugin.name + " ]") 147 | print("\n") 148 | 149 | # Fetch plugins, try to activate them, add to the list if OK 150 | plug_list = [] 151 | callback_list = [] 152 | if args.add: 153 | for plug_candidate in args.add: 154 | # first value: plugin name, then optional arguments 155 | plug_name = plug_candidate[0] 156 | plug_args = plug_candidate[1:] 157 | # Try to find name 158 | plug = manager.getPluginByName(plug_name) 159 | if plug == None: 160 | # eg: if an import fail inside a plugin, yapsy skip it 161 | print("Error: [ " + plug_name + " ] not found or could not be loaded. Check name and requirements.") 162 | else: 163 | print("\nActivating [ " + plug_name + " ] plugin...") 164 | if not plug.plugin_object.pre_activate(plug_args, sample_rate=board.getSampleRate(), 165 | eeg_channels=board.getNbEEGChannels(), 166 | aux_channels=board.getNbAUXChannels(), 167 | imp_channels=board.getNbImpChannels()): 168 | print("Error while activating [ " + plug_name + " ], check output for more info.") 169 | else: 170 | print("Plugin [ " + plug_name + "] added to the list") 171 | plug_list.append(plug.plugin_object) 172 | callback_list.append(plug.plugin_object) 173 | 174 | if len(plug_list) == 0: 175 | fun = None 176 | else: 177 | fun = callback_list 178 | 179 | def cleanUp(): 180 | board.disconnect() 181 | print("Deactivating Plugins...") 182 | for plug in plug_list: 183 | plug.deactivate() 184 | print("User.py exiting...") 185 | 186 | atexit.register(cleanUp) 187 | 188 | print("--------------INFO---------------") 189 | print("User serial interface enabled...\n\ 190 | View command map at http://docs.openbci.com.\n\ 191 | Type /start to run (/startimp for impedance \n\ 192 | checking, if supported) -- and /stop\n\ 193 | before issuing new commands afterwards.\n\ 194 | Type /exit to exit. \n\ 195 | Board outputs are automatically printed as: \n\ 196 | % message\n\ 197 | $$$ signals end of message") 198 | 199 | print("\n-------------BEGIN---------------") 200 | # Init board state 201 | # s: stop board streaming; v: soft reset of the 32-bit board (no effect with 8bit board) 202 | s = 'sv' 203 | # Tell the board to enable or not daisy module 204 | if board.daisy: 205 | s = s + 'C' 206 | else: 207 | s = s + 'c' 208 | # d: Channels settings back to default 209 | s = s + 'd' 210 | 211 | while s != "/exit": 212 | # Send char and wait for registers to set 213 | if not s: 214 | pass 215 | elif "help" in s: 216 | print("View command map at: \ 217 | http://docs.openbci.com/software/01-OpenBCI_SDK.\n\ 218 | For user interface: read README or view \ 219 | https://github.com/OpenBCI/OpenBCI_Python") 220 | 221 | elif board.streaming and s != "/stop": 222 | print("Error: the board is currently streaming data, please type '/stop' before issuing new commands.") 223 | else: 224 | # read silently incoming packet if set (used when stream is stopped) 225 | flush = False 226 | 227 | if '/' == s[0]: 228 | s = s[1:] 229 | rec = False # current command is recognized or fot 230 | 231 | if "T:" in s: 232 | lapse = int(s[string.find(s, "T:") + 2:]) 233 | rec = True 234 | elif "t:" in s: 235 | lapse = int(s[string.find(s, "t:") + 2:]) 236 | rec = True 237 | else: 238 | lapse = -1 239 | 240 | if 'startimp' in s: 241 | if board.getBoardType() == "cyton": 242 | print("Impedance checking not supported on cyton.") 243 | else: 244 | board.setImpedance(True) 245 | if (fun != None): 246 | # start streaming in a separate thread so we could always send commands in here 247 | boardThread = threading.Thread(target=board.start_streaming, args=(fun, lapse)) 248 | boardThread.daemon = True # will stop on exit 249 | try: 250 | boardThread.start() 251 | except: 252 | raise 253 | else: 254 | print("No function loaded") 255 | rec = True 256 | 257 | elif "start" in s: 258 | board.setImpedance(False) 259 | if fun != None: 260 | # start streaming in a separate thread so we could always send commands in here 261 | boardThread = threading.Thread(target=board.start_streaming, args=(fun, lapse)) 262 | boardThread.daemon = True # will stop on exit 263 | try: 264 | boardThread.start() 265 | except: 266 | raise 267 | else: 268 | print("No function loaded") 269 | rec = True 270 | 271 | elif 'test' in s: 272 | test = int(s[s.find("test") + 4:]) 273 | board.test_signal(test) 274 | rec = True 275 | elif 'stop' in s: 276 | board.stop() 277 | rec = True 278 | flush = True 279 | if rec == False: 280 | print("Command not recognized...") 281 | 282 | elif s: 283 | for c in s: 284 | if sys.hexversion > 0x03000000: 285 | board.ser_write(bytes(c, 'utf-8')) 286 | else: 287 | board.ser_write(bytes(c)) 288 | time.sleep(0.100) 289 | 290 | line = '' 291 | time.sleep(0.1) # Wait to see if the board has anything to report 292 | # The Cyton nicely return incoming packets -- here supposedly messages 293 | # whereas the Ganglion prints incoming ASCII message by itself 294 | if board.getBoardType() == "cyton": 295 | while board.ser_inWaiting(): 296 | # we're supposed to get UTF8 text, but the board might behave otherwise 297 | c = board.ser_read().decode('utf-8', errors='replace') 298 | line += c 299 | time.sleep(0.001) 300 | if (c == '\n') and not flush: 301 | print('%\t' + line[:-1]) 302 | line = '' 303 | elif board.getBoardType() == "ganglion": 304 | while board.ser_inWaiting(): 305 | board.waitForNotifications(0.001) 306 | 307 | if not flush: 308 | print(line) 309 | 310 | # Take user input 311 | # s = input('--> ') 312 | if sys.hexversion > 0x03000000: 313 | s = input('--> ') 314 | else: 315 | s = raw_input('--> ') 316 | --------------------------------------------------------------------------------