├── .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 |
9 |
10 |
11 | Provide a stable Python driver for all OpenBCI Biosensors
12 |
13 |
14 | [](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 |
--------------------------------------------------------------------------------