diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..3e74d10 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,8 @@ +languages: + Python: true +exclude_paths: +- "externals/*" +- "images/*" +- "plugins/*" +- "scripts/*" +- "tests/*" \ No newline at end of file diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a86e032 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,15 @@ +[report] +include = */openbci/* +omit = + */python?.?/* + */site-packages/nose/* + */plugin_interface.py + */test_log.py + */setup.py + */users.py + */bluepy/* + */externals/* + */images/* + */plugins/* + */scripts/* + */tests/* diff --git a/.gitignore b/.gitignore index e361c22..4d784a5 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,5 @@ target/ .idea/* .idea/codeStyleSettings.xml .idea/vcs.xml + +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ab9f1f7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,42 @@ +language: python +env: + - PYTHON=2.7 + - PYTHON=3.4 +# command to install dependencies\ +cache: pip +sudo: false +virtualenv: + system_site_packages: true +addons: + apt: + packages: + - libatlas-dev + - libatlas3gf-base + - libblas-dev + - liblapack-dev + - python-matplotlib + - gfortran + - python-tk +install: + - conda create -n testenv --yes pip python=$PYTHON + - source activate testenv + - conda install --yes --quiet numpy pyserial mock nose coverage + - pip install codecov xmltodict bluepy + - python setup.py build install + +# Setup anaconda +before_install: + - wget -q http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh + - chmod +x miniconda.sh + - ./miniconda.sh -b -p /home/travis/miniconda + - export PATH=/home/travis/miniconda/bin:$PATH + - conda update --yes --quiet conda + # We need to create a (fake) display on Travis (allows Mayavi tests to run) + - export DISPLAY=:99.0 + - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render -noreset + + +# command to run tests +script: nosetests --with-coverage --cover-package=openbci +after_success: + - codecov diff --git a/CHANGELOG.md b/CHANGELOG.md index 32746bb..c6a5842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +# v1.0.0 + +### New Features + +* High speed mode for WiFi shield sends raw data - #51 +* Unit testing with Nosetests +* Continuous Integration with Travis.ci + +### Breaking Changes + +* Refactored library for pip +* Moved plugins folder into openbci dir so plugins can be imported when installed with pip + +# v0.1 ## dev diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100755 index 0000000..766ec44 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,69 @@ +# OpenBCI Python Code of Conduct + +## Purpose + +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). + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [info@pushtheworld.us](mailto:info@pushtheworld.us). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 0000000..9bc0265 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing + +:tada::clinking_glasses: First off, thanks for taking the time to contribute! :tada::clinking_glasses: + +Contributions are always welcome, no matter how small. + +The following is a small set of guidelines for how to contribute to the project + +## Where to start + +### Code of Conduct +This project adheres to the Contributor Covenant [Code of Conduct](CODE_OF_CONDUCT.md). +By participating you are expected to adhere to these expectations. Please report unacceptable behaviour to [info@pushtheworld.us](mailto:info@pushtheworld.us) + +### Contributing on Github + +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/). + +### Community + +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. + +## How can I contribute? + +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. + +If you find a bug, or have a suggestion on how to improve the project, just fill out a [Github issue](../../issues) diff --git a/README.md b/README.md index 6ccc7be..33fe859 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,110 @@ -OpenBCI_Python -============== +# OpenBCI Python -The Python software library designed to work with OpenBCI hardware. +
+ +
++ Provide a stable Python driver for all OpenBCI Biosensors +
-Please direct any questions, suggestions and bug reports to the github repo at: https://github.com/OpenBCI/OpenBCI_Python +[![Build Status](https://travis-ci.org/OpenBCI/OpenBCI_Python.svg?branch=master)](https://travis-ci.org/OpenBCI/OpenBCI_Python) -## Dependancies: +## Welcome! + +First and foremost, Welcome! :tada: Willkommen! :confetti_ball: Bienvenue! :balloon::balloon::balloon: + +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. + +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. + +* [What are we doing? (And why?)](#what-are-we-doing) +* [Who are we?](#who-are-we) +* [What do we need?](#what-do-we-need) +* [How can you get involved?](#get-involved) +* [Get in touch](#contact-us) +* [Find out more](#find-out-more) +* [Glossary](#glossary) +* [Dependencies](#dependencies) +* [Install](#install) +* [Functionality](#functionality) + +## What are we doing? + +### The problem + +* OpenBCI is an incredible biosensor that can be challenging to work with +* Data comes into the computer very quickly +* Complex byte streams +* Lot's of things can go wrong when dealing with a raw serial byte stream +* The boards all use different physical technologies to move data to computers such as bluetooth or wifi +* Developers want to integrate OpenBCI with other platforms and interfaces + +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. + +### The solution + +The OpenBCI Python will: + +* Allow Python users to install one module and use any board they choose +* Provide examples of using Python to port data to other apps like lab streaming layer +* Perform the heavy lifting when extracting and transforming raw binary byte streams +* Use unit tests to ensure perfect quality of core code + +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*** + +## Who are we? + +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! + +The contributors to these repos are people using Python mainly for their data acquisition and analytics. + +## What do we need? + +**You**! In whatever way you can help. + +We need expertise in programming, user experience, software sustainability, documentation and technical writing and project management. + +We'd love your feedback along the way. + +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. + +## Get involved + +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). + +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. + +## Contact us + +If you want to report a problem or suggest an enhancement we'd love for you to [open an issue](../../issues) at this github repository because then we can get right on it. But you can also contact [AJ][link_aj_keller] by email (pushtheworldllc AT gmail DOT com) or on [twitter](https://twitter.com/aj-ptw). + +## Find out more + +You might be interested in: + +* Purchase a [Cyton][link_shop_cyton] | [Ganglion][link_shop_ganglion] | [WiFi Shield][link_shop_wifi_shield] from [OpenBCI][link_openbci] +* Get taught how to use OpenBCI devices by [Push The World][link_ptw] BCI Consulting + +And of course, you'll want to know our: + +* [Contributors' guidelines](CONTRIBUTING.md) +* [Roadmap](ROADMAP.md) + +## Glossary + +OpenBCI boards are commonly referred to as _biosensors_. A biosensor converts biological data into digital data. + +The [Ganglion][link_shop_ganglion] has 4 channels, meaning the Ganglion can take four simultaneous voltage readings. + +The [Cyton][link_shop_cyton] has 8 channels and [Cyton with Daisy][link_shop_cyton_daisy] has 16 channels. + +Generally speaking, the Cyton records at a high quality with less noise. Noise is anything that is not signal. + +## Thank you + +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. + +## Dependencies * Python 2.7 or later (https://www.python.org/download/releases/2.7/) * Numpy 1.7 or later (http://www.numpy.org/) @@ -31,17 +130,35 @@ On linux, assuming `hci0` is the name of your bluetooth adapter: `sudo bash -c 'echo 10 > /sys/kernel/debug/bluetooth/hci0/conn_max_interval'` -# Audience: +## Install + +### Using PyPI + +``` +pip install openbci +``` -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. +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. -If this is not what you are looking for, you can visit http://openbci.com/downloads and browse other OpenBCI software that will fit your needs. +### From sources + +For the latest version, you can install the package from the sources using the setup.py script + +``` +python setup.py install +``` + +or in developer mode to be able to modify the sources. + +``` +python setup.py develop +``` ## Functionality ### Basic usage -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. +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. OpenBCISample members: -id: @@ -55,13 +172,13 @@ OpenBCISample members: ### user.py -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). +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). Then simply run the code given as an argument the port your board is connected to: Ex Linux: -> $python user.py -p /dev/ttyUSB0 +> $python user.py -p /dev/ttyUSB0 -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. +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. A good first test is to try is to type '?': >--> ? @@ -88,15 +205,15 @@ Alternatively, there are 6 test signals pre configured: 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. -For example, writing -> -->x3020000X +For example, writing +> -->x3020000X will do the following: ‘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. Pre-configured commands that use the / prefix are: -test (As explained above) +test (As explained above) > --> /test4 @@ -123,7 +240,7 @@ Serial established... View command map at http://docs.openbci.com. Type start to run. Type /exit to exit. ---> +--> OpenBCI V3 8bit Board Setting ADS1299 Channel Values ADS1299 Device ID: 0x3E @@ -176,17 +293,17 @@ Add new functionalities to user.py by creating new scripts inside the `plugins` ```python import plugin_interface as plugintypes - + class PluginPrint(plugintypes.IPluginExtended): def activate(self): print "Print activated" - + def deactivate(self): print "Goodbye" - + def show_help(self): print "I do not need any parameter, just printing stuff." - + # called with each new sample def __call__(self, sample): print "----------------" @@ -219,7 +336,7 @@ You're done, your plugin should be automatically detected by `user.py`. * `sample_rate`: Print effective sampling rate averaged over XX seconds (default: 10). -* `streamer_tcp`: Acts as a TCP server, using a "raw" protocol to send value. +* `streamer_tcp`: Acts as a TCP server, using a "raw" protocol to send value. * 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). * Default IP: localhost, default port: 12345 @@ -245,3 +362,19 @@ Note: copy `open_bci_v3.py` there if you want to run the code -- no proper packa * `test.py`: minimal example, printing values. * `stream_data.py` a version of a TCP streaming server that somehow oversamples OpenBCI from 250 to 256Hz. * `upd_server.py` *DEPRECATED* (Use Plugin): see https://github.com/OpenBCI/OpenBCI_Node for implementation example. + +## License: + +MIT + +[link_aj_keller]: https://github.com/aj-ptw +[link_shop_wifi_shield]: https://shop.openbci.com/collections/frontpage/products/wifi-shield?variant=44534009550 +[link_shop_ganglion]: https://shop.openbci.com/collections/frontpage/products/pre-order-ganglion-board +[link_shop_cyton]: https://shop.openbci.com/collections/frontpage/products/cyton-biosensing-board-8-channel +[link_shop_cyton_daisy]: https://shop.openbci.com/collections/frontpage/products/cyton-daisy-biosensing-boards-16-channel +[link_nodejs_cyton]: https://github.com/openbci/openbci_nodejs_cyton +[link_nodejs_ganglion]: https://github.com/openbci/openbci_nodejs_ganglion +[link_nodejs_wifi]: https://github.com/openbci/openbci_nodejs_wifi +[link_javascript_utilities]: https://github.com/OpenBCI/OpenBCI_JavaScript_Utilities +[link_ptw]: https://www.pushtheworldllc.com +[link_openbci]: http://www.openbci.com diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100755 index 0000000..aa1339e --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,15 @@ +# Roadmap + +## OpenBCI Python + +Provide a stable Python driver for all OpenBCI Biosensors + +## Short term - what we're working on now + +- WiFi + +## Medium term + +- Emotion detection +- Default set of instructions to send to the board on startup via command line +- Time sync with Cyton diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a24b2c5..0000000 --- a/TODO.md +++ /dev/null @@ -1,6 +0,0 @@ - -* default set of instructions to send to the board on startup via command line -* make a proper package -* requirements.txt for pip -* fix long recording crash (might be hardware or software fix) - diff --git a/images/openbci_large.png b/images/openbci_large.png new file mode 100644 index 0000000..fd3e7ee Binary files /dev/null and b/images/openbci_large.png differ diff --git a/openbci/__init__.py b/openbci/__init__.py new file mode 100644 index 0000000..3717f85 --- /dev/null +++ b/openbci/__init__.py @@ -0,0 +1,9 @@ + +from .cyton import OpenBCICyton +from .ganglion import OpenBCIGanglion +from .plugins import * +from .utils import * +from .wifi import OpenBCIWiFi + + +__version__ = "1.0.0" diff --git a/open_bci_v3.py b/openbci/cyton.py similarity index 98% rename from open_bci_v3.py rename to openbci/cyton.py index 013cb58..80d0109 100644 --- a/open_bci_v3.py +++ b/openbci/cyton.py @@ -55,7 +55,7 @@ def handle_sample(sample): command_biasFixed = "~"; ''' -class OpenBCIBoard(object): +class OpenBCICyton(object): """ Handle a connection to an OpenBCI board. @@ -237,22 +237,22 @@ def read(n): channel_data = [] for c in range(self.eeg_channels_per_sample): - #3 byte ints + # 3 byte ints literal_read = read(3) unpacked = struct.unpack('3B', literal_read) - log_bytes_in = log_bytes_in + '|' + str(literal_read); + log_bytes_in = log_bytes_in + '|' + str(literal_read) - #3byte int in 2s compliment + # 3byte int in 2s compliment if (unpacked[0] > 127): pre_fix = bytes(bytearray.fromhex('FF')) else: pre_fix = bytes(bytearray.fromhex('00')) - literal_read = pre_fix + literal_read; + literal_read = pre_fix + literal_read - #unpack little endian(>) signed integer(i) (makes unpacking platform independent) + # unpack little endian(>) signed integer(i) (makes unpacking platform independent) myInt = struct.unpack('>i', literal_read)[0] if self.scaling_output: @@ -260,7 +260,7 @@ def read(n): else: channel_data.append(myInt) - self.read_state = 2; + self.read_state = 2 #---------Accelerometer Data--------- elif self.read_state == 2: diff --git a/open_bci_ganglion.py b/openbci/ganglion.py similarity index 99% rename from open_bci_ganglion.py rename to openbci/ganglion.py index 423fdb4..aa6a011 100644 --- a/open_bci_ganglion.py +++ b/openbci/ganglion.py @@ -26,7 +26,14 @@ def handle_sample(sample): # local bluepy should take precedence import sys sys.path.insert(0,"bluepy/bluepy") -from btle import Scanner, DefaultDelegate, Peripheral + +STUB_BTLE = False + +try: + from btle import Scanner, DefaultDelegate, Peripheral +except: + DefaultDelegate = object + STUB_BTLE = True SAMPLE_RATE = 200.0 # Hz scale_fac_uVolts_per_count = 1200 / (8388607.0 * 1.5 * 51.0) @@ -46,7 +53,8 @@ def handle_sample(sample): command_startBinary = "b"; ''' -class OpenBCIBoard(object): + +class OpenBCIGanglion(object): """ Handle a connection to an OpenBCI board. diff --git a/plugins/README.md b/openbci/plugins/README.md similarity index 100% rename from plugins/README.md rename to openbci/plugins/README.md diff --git a/openbci/plugins/__init__.py b/openbci/plugins/__init__.py new file mode 100644 index 0000000..60e9339 --- /dev/null +++ b/openbci/plugins/__init__.py @@ -0,0 +1,9 @@ + +from .csv_collect import * +from .noise_test import * +from .streamer_lsl import * +from .streamer_osc import * +from .streamer_tcp_server import * +from .udp_server import * + +__version__ = "1.0.0" diff --git a/plugins/csv_collect.py b/openbci/plugins/csv_collect.py similarity index 100% rename from plugins/csv_collect.py rename to openbci/plugins/csv_collect.py diff --git a/plugins/csv_collect.yapsy-plugin b/openbci/plugins/csv_collect.yapsy-plugin similarity index 100% rename from plugins/csv_collect.yapsy-plugin rename to openbci/plugins/csv_collect.yapsy-plugin diff --git a/plugins/noise_test.py b/openbci/plugins/noise_test.py similarity index 100% rename from plugins/noise_test.py rename to openbci/plugins/noise_test.py diff --git a/plugins/noise_test.yapsy-plugin b/openbci/plugins/noise_test.yapsy-plugin similarity index 100% rename from plugins/noise_test.yapsy-plugin rename to openbci/plugins/noise_test.yapsy-plugin diff --git a/plugins/print.py b/openbci/plugins/print.py similarity index 100% rename from plugins/print.py rename to openbci/plugins/print.py diff --git a/plugins/print.yapsy-plugin b/openbci/plugins/print.yapsy-plugin similarity index 100% rename from plugins/print.yapsy-plugin rename to openbci/plugins/print.yapsy-plugin diff --git a/plugins/sample_rate.py b/openbci/plugins/sample_rate.py similarity index 100% rename from plugins/sample_rate.py rename to openbci/plugins/sample_rate.py diff --git a/plugins/sample_rate.yapsy-plugin b/openbci/plugins/sample_rate.yapsy-plugin similarity index 100% rename from plugins/sample_rate.yapsy-plugin rename to openbci/plugins/sample_rate.yapsy-plugin diff --git a/plugins/streamer_lsl.py b/openbci/plugins/streamer_lsl.py similarity index 100% rename from plugins/streamer_lsl.py rename to openbci/plugins/streamer_lsl.py diff --git a/plugins/streamer_lsl.yapsy-plugin b/openbci/plugins/streamer_lsl.yapsy-plugin similarity index 100% rename from plugins/streamer_lsl.yapsy-plugin rename to openbci/plugins/streamer_lsl.yapsy-plugin diff --git a/plugins/streamer_osc.py b/openbci/plugins/streamer_osc.py similarity index 100% rename from plugins/streamer_osc.py rename to openbci/plugins/streamer_osc.py diff --git a/plugins/streamer_osc.yapsy-plugin b/openbci/plugins/streamer_osc.yapsy-plugin similarity index 100% rename from plugins/streamer_osc.yapsy-plugin rename to openbci/plugins/streamer_osc.yapsy-plugin diff --git a/plugins/streamer_tcp.yapsy-plugin b/openbci/plugins/streamer_tcp.yapsy-plugin similarity index 100% rename from plugins/streamer_tcp.yapsy-plugin rename to openbci/plugins/streamer_tcp.yapsy-plugin diff --git a/openbci/plugins/streamer_tcp_server.py b/openbci/plugins/streamer_tcp_server.py new file mode 100755 index 0000000..0945f97 --- /dev/null +++ b/openbci/plugins/streamer_tcp_server.py @@ -0,0 +1,132 @@ +from threading import Thread +import socket, select, struct, time +import plugin_interface as plugintypes + +# Simple TCP server to "broadcast" data to clients, handling deconnections. Binary format use network endianness (i.e., big-endian), float32 + +# TODO: does not listen for anything at the moment, could use it to set options + +# Handling new client in separate thread +class MonitorStreamer(Thread): + """Launch and monitor a "Streamer" entity (incoming connections if implemented, current sampling rate).""" + # tcp_server: the TCPServer instance that will be used + def __init__(self, streamer): + Thread.__init__(self) + # bind to Streamer entity + self.server = streamer + + def run(self): + # run until we DIE + while True: + # check FPS + listen for new connections + # FIXME: not so great with threads -- use a lock? + # TODO: configure interval + self.server.check_connections() + time.sleep(1) + + +class StreamerTCPServer(plugintypes.IPluginExtended): + """ + + Relay OpenBCI values to TCP clients + + Args: + port: Port of the server + ip: IP address of the server + + """ + + def __init__(self, ip='localhost', port=12345): + # list of socket clients + self.CONNECTION_LIST = [] + # connection infos + self.ip = ip + self.port = port + + # From IPlugin + def activate(self): + if len(self.args) > 0: + self.ip = self.args[0] + if len(self.args) > 1: + self.port = int(self.args[1]) + + # init network + print("Selecting raw TCP streaming. IP: " + self.ip + ", port: " + str(self.port)) + self.initialize() + + # init the daemon that monitors connections + self.monit = MonitorStreamer(self) + self.monit.daemon = True + # launch monitor + self.monit.start() + + # the initialize method reads settings and outputs the first header + def initialize(self): + # init server + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # this has no effect, why ? + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # create connection + self.server_socket.bind((self.ip, self.port)) + self.server_socket.listen(1) + print("Server started on port " + str(self.port)) + + # From Streamer, to be called each time we're willing to accept new connections + def check_connections(self): + # First listen for new connections, and new connections only -- this is why we pass only server_socket + read_sockets,write_sockets,error_sockets = select.select([self.server_socket],[],[], 0) + for sock in read_sockets: + # New connection + sockfd, addr = self.server_socket.accept() + self.CONNECTION_LIST.append(sockfd) + print("Client (%s, %s) connected" % addr) + # and... don't bother with incoming messages + + # From IPlugin: close sockets, send message to client + def deactivate(self): + # close all remote connections + for sock in self.CONNECTION_LIST: + if sock != self.server_socket: + try: + sock.send("closing!\n") + # at this point don't bother if message not sent + except: + continue + sock.close(); + # close server socket + self.server_socket.close(); + + # broadcast channels values to all clients + # as_string: many for debug, send values with a nice "[34.45, 30.4, -38.0]"-like format + def __call__(self, sample, as_string=False): + values=sample.channel_data + # save sockets that are closed to remove them later on + outdated_list = [] + for sock in self.CONNECTION_LIST: + # If one error should happen, we remove socket from the list + try: + if as_string: + sock.send(str(values) + "\n") + else: + nb_channels=len(values) + # format for binary data, network endian (big) and float (float32) + packer = struct.Struct('!%sf' % nb_channels) + # convert values to bytes + packed_data = packer.pack(*values) + sock.send(packed_data) + # TODO: should check if the correct number of bytes passed through + except: + # sometimes (always?) it's only during the second write to a close socket that an error is raised? + print("Something bad happened, will close socket") + outdated_list.append(sock) + # now we are outside of the main list, it's time to remove outdated sockets, if any + for bad_sock in outdated_list: + print("Removing socket...") + self.CONNECTION_LIST.remove(bad_sock) + # not very costly to be polite + bad_sock.close() + + def show_help(self): + print("""Optional arguments: [ip [port]] + \t ip: target IP address (default: 'localhost') + \t port: target port (default: 12345)""") diff --git a/plugins/udp_server.py b/openbci/plugins/udp_server.py similarity index 100% rename from plugins/udp_server.py rename to openbci/plugins/udp_server.py diff --git a/plugins/udp_server.yapsy-plugin b/openbci/plugins/udp_server.yapsy-plugin similarity index 100% rename from plugins/udp_server.yapsy-plugin rename to openbci/plugins/udp_server.yapsy-plugin diff --git a/openbci/utils/__init__.py b/openbci/utils/__init__.py new file mode 100644 index 0000000..16f1acf --- /dev/null +++ b/openbci/utils/__init__.py @@ -0,0 +1,6 @@ +from .constants import Constants as k +from .parse import * +from .ssdp import SSDPResponse +from .utilities import * + +__version__ = "1.0.0" diff --git a/openbci/utils/constants.py b/openbci/utils/constants.py new file mode 100644 index 0000000..dc0f238 --- /dev/null +++ b/openbci/utils/constants.py @@ -0,0 +1,94 @@ +class Constants: + """The constants!""" + + ADS1299_GAIN_1 = 1.0 + ADS1299_GAIN_2 = 2.0 + ADS1299_GAIN_4 = 4.0 + ADS1299_GAIN_6 = 6.0 + ADS1299_GAIN_8 = 8.0 + ADS1299_GAIN_12 = 12.0 + ADS1299_GAIN_24 = 24.0 + + ADS1299_VREF = 4.5 # reference voltage for ADC in ADS1299. set by its hardware + + BOARD_CYTON = 'cyton' + BOARD_DAISY = 'daisy' + BOARD_GANGLION = 'ganglion' + BOARD_NONE = 'none' + + CYTON_ACCEL_SCALE_FACTOR_GAIN = 0.002 / (pow(2, 4)) # assume set to +/4G, so 2 mG + + """ Errors """ + ERROR_INVALID_BYTE_LENGTH = 'Invalid Packet Byte Length' + ERROR_INVALID_BYTE_START = 'Invalid Start Byte' + ERROR_INVALID_BYTE_STOP = 'Invalid Stop Byte' + ERROR_INVALID_DATA = 'Invalid data - try again' + ERROR_INVALID_TYPW = 'Invalid type - check comments for input type' + ERROR_MISSING_REGISTER_SETTING = 'Missing register setting' + ERROR_MISSING_REQUIRED_PROPERTY = 'Missing property in JSON' + ERROR_TIME_SYNC_IS_NULL = "'this.sync.curSyncObj' must not be null" + ERROR_TIME_SYNC_NO_COMMA = 'Missed the time sync sent confirmation. Try sync again' + ERROR_UNDEFINED_OR_NULL_INPUT = 'Undefined or Null Input' + + """ Possible number of channels """ + + NUMBER_OF_CHANNELS_CYTON = 8 + NUMBER_OF_CHANNELS_DAISY = 16 + NUMBER_OF_CHANNELS_GANGLION = 4 + + """ Protocols """ + PROTOCOL_BLE = 'ble' + PROTOCOL_SERIAL = 'serial' + PROTOCOL_WIFI = 'wifi' + + RAW_BYTE_START = 0xA0 + RAW_BYTE_STOP = 0xC0 + RAW_PACKET_ACCEL_NUMBER_AXIS = 3 + RAW_PACKET_SIZE = 33 + """ + OpenBCI Raw Packet Positions + 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 + """ + RAW_PACKET_POSITION_CHANNEL_DATA_START = 2 + RAW_PACKET_POSITION_CHANNEL_DATA_STOP = 25 + RAW_PACKET_POSITION_SAMPLE_NUMBER = 1 + RAW_PACKET_POSITION_START_BYTE = 0 + RAW_PACKET_POSITION_STOP_BYTE = 32 + RAW_PACKET_POSITION_START_AUX = 26 + RAW_PACKET_POSITION_STOP_AUX = 31 + RAW_PACKET_POSITION_TIME_SYNC_AUX_START = 26 + RAW_PACKET_POSITION_TIME_SYNC_AUX_STOP = 28 + RAW_PACKET_POSITION_TIME_SYNC_TIME_START = 28 + RAW_PACKET_POSITION_TIME_SYNC_TIME_STOP = 32 + + """ Stream packet types """ + RAW_PACKET_TYPE_STANDARD_ACCEL = 0 # 0000 + RAW_PACKET_TYPE_STANDARD_RAW_AUX = 1 # 0001 + RAW_PACKET_TYPE_USER_DEFINED_TYPE = 2 # 0010 + RAW_PACKET_TYPE_ACCEL_TIME_SYNC_SET = 3 # 0011 + RAW_PACKET_TYPE_ACCEL_TIME_SYNCED = 4 # 0100 + RAW_PACKET_TYPE_RAW_AUX_TIME_SYNC_SET = 5 # 0101 + RAW_PACKET_TYPE_RAW_AUX_TIME_SYNCED = 6 # 0110 + RAW_PACKET_TYPE_IMPEDANCE = 7 # 0111 + + """ Max sample number """ + SAMPLE_NUMBER_MAX_CYTON = 255 + SAMPLE_NUMBER_MAX_GANGLION = 200 + + """ Possible Sample Rates """ + SAMPLE_RATE_1000 = 1000 + SAMPLE_RATE_125 = 125 + SAMPLE_RATE_12800 = 12800 + SAMPLE_RATE_1600 = 1600 + SAMPLE_RATE_16000 = 16000 + SAMPLE_RATE_200 = 200 + SAMPLE_RATE_2000 = 2000 + SAMPLE_RATE_250 = 250 + SAMPLE_RATE_25600 = 25600 + SAMPLE_RATE_3200 = 3200 + SAMPLE_RATE_400 = 400 + SAMPLE_RATE_4000 = 4000 + SAMPLE_RATE_500 = 500 + SAMPLE_RATE_6400 = 6400 + SAMPLE_RATE_800 = 800 + SAMPLE_RATE_8000 = 8000 diff --git a/openbci/utils/parse.py b/openbci/utils/parse.py new file mode 100644 index 0000000..fe629f6 --- /dev/null +++ b/openbci/utils/parse.py @@ -0,0 +1,282 @@ +import time +import struct + +from constants import Constants as k + + +class ParseRaw(object): + def __init__(self, + board_type=k.BOARD_CYTON, + gains=None, + log=False, + micro_volts=False, + scaled_output=True): + self.board_type = board_type + self.gains = gains + self.log = log + self.micro_volts = micro_volts + self.scale_factors = [] + self.scaled_output = scaled_output + + if gains is not None: + self.scale_factors = self.get_ads1299_scale_factors(self.gains, self.micro_volts) + + self.raw_data_to_sample = RawDataToSample(gains=gains, + scale=scaled_output, + scale_factors=self.scale_factors, + verbose=log) + + def is_stop_byte(self, byte): + """ + Used to check and see if a byte adheres to the stop byte structure of 0xCx where x is the set of numbers + from 0-F in hex of 0-15 in decimal. + :param byte: {int} - The number to test + :return: {boolean} - True if `byte` follows the correct form + """ + return (byte & 0xF0) == k.RAW_BYTE_STOP + + def get_ads1299_scale_factors(self, gains, micro_volts=None): + out = [] + for gain in gains: + scale_factor = k.ADS1299_VREF / float((pow(2, 23) - 1)) / float(gain) + if micro_volts is None: + if self.micro_volts: + scale_factor *= 1000000. + else: + if micro_volts: + scale_factor *= 1000000. + + out.append(scale_factor) + return out + + def get_channel_data_array(self, raw_data_to_sample): + """ + + :param raw_data_to_sample: RawDataToSample + :return: + """ + channel_data = [] + number_of_channels = len(raw_data_to_sample.scale_factors) + daisy = number_of_channels == k.NUMBER_OF_CHANNELS_DAISY + channels_in_packet = k.NUMBER_OF_CHANNELS_CYTON + if not daisy: + channels_in_packet = number_of_channels + # Channel data arrays are always 8 long + + for i in range(channels_in_packet): + counts = self.interpret_24_bit_as_int_32(raw_data_to_sample.raw_data_packet[(i * 3) + k.RAW_PACKET_POSITION_CHANNEL_DATA_START:(i * 3) + k.RAW_PACKET_POSITION_CHANNEL_DATA_START + 3]) + channel_data.append(raw_data_to_sample.scale_factors[i] * counts if raw_data_to_sample.scale else counts) + + return channel_data + + def get_data_array_accel(self, raw_data_to_sample): + accel_data = [] + for i in range(k.RAW_PACKET_ACCEL_NUMBER_AXIS): + counts = self.interpret_16_bit_as_int_32(raw_data_to_sample.raw_data_packet[k.RAW_PACKET_POSITION_START_AUX + (i * 2): k.RAW_PACKET_POSITION_START_AUX + (i * 2) + 2]) + accel_data.append(k.CYTON_ACCEL_SCALE_FACTOR_GAIN * counts if raw_data_to_sample.scale else counts) + return accel_data + + def get_raw_packet_type(self, stop_byte): + return stop_byte & 0xF + + def interpret_16_bit_as_int_32(self, two_byte_buffer): + return struct.unpack('>h', two_byte_buffer)[0] + + def interpret_24_bit_as_int_32(self, three_byte_buffer): + # 3 byte ints + unpacked = struct.unpack('3B', three_byte_buffer) + + # 3byte int in 2s compliment + if unpacked[0] > 127: + pre_fix = bytes(bytearray.fromhex('FF')) + else: + pre_fix = bytes(bytearray.fromhex('00')) + + three_byte_buffer = pre_fix + three_byte_buffer + + # unpack little endian(>) signed integer(i) (makes unpacking platform independent) + return struct.unpack('>i', three_byte_buffer)[0] + + def parse_packet_standard_accel(self, raw_data_to_sample): + + """ + + :param raw_data_to_sample: RawDataToSample + :return: + """ + # Check to make sure data is not null. + if raw_data_to_sample is None: + raise RuntimeError(k.ERROR_UNDEFINED_OR_NULL_INPUT) + if raw_data_to_sample.raw_data_packet is None: + raise RuntimeError(k.ERROR_UNDEFINED_OR_NULL_INPUT) + + # Check to make sure the buffer is the right size. + if len(raw_data_to_sample.raw_data_packet) != k.RAW_PACKET_SIZE: + raise RuntimeError(k.ERROR_INVALID_BYTE_LENGTH) + + # Verify the correct stop byte. + if raw_data_to_sample.raw_data_packet[0] != k.RAW_BYTE_START: + raise RuntimeError(k.ERROR_INVALID_BYTE_START) + + sample_object = OpenBCISample() + + sample_object.accel_data = self.get_data_array_accel(raw_data_to_sample) + + sample_object.channel_data = self.get_channel_data_array(raw_data_to_sample) + + sample_object.sample_number = raw_data_to_sample.raw_data_packet[k.RAW_PACKET_POSITION_SAMPLE_NUMBER] + sample_object.start_byte = raw_data_to_sample.raw_data_packet[k.RAW_PACKET_POSITION_START_BYTE] + sample_object.stop_byte = raw_data_to_sample.raw_data_packet[k.RAW_PACKET_POSITION_STOP_BYTE] + + sample_object.valid = True + + now_ms = int(round(time.time() * 1000)) + + sample_object.timestamp = now_ms + sample_object.boardTime = 0 + + return sample_object + + def parse_packet_standard_raw_aux(self, raw_data_to_sample): + pass + + def parse_packet_time_synced_accel(self, raw_data_to_sample): + pass + + def parse_packet_time_synced_raw_aux(self, raw_data_to_sample): + pass + + def set_ads1299_scale_factors(self, gains, micro_volts=None): + self.scale_factors = self.get_ads1299_scale_factors(gains, micro_volts=micro_volts) + + def transform_raw_data_packet_to_sample(self, raw_data): + """ + Used transform raw data packets into fully qualified packets + :param raw_data: + :return: + """ + try: + self.raw_data_to_sample.raw_data_packet = raw_data + packet_type = self.get_raw_packet_type(raw_data[k.RAW_PACKET_POSITION_STOP_BYTE]) + if packet_type == k.RAW_PACKET_TYPE_STANDARD_ACCEL: + sample = self.parse_packet_standard_accel(self.raw_data_to_sample) + elif packet_type == k.RAW_PACKET_TYPE_STANDARD_RAW_AUX: + sample = self.parse_packet_standard_raw_aux(self.raw_data_to_sample) + elif packet_type == k.RAW_PACKET_TYPE_ACCEL_TIME_SYNC_SET or packet_type == k.RAW_PACKET_TYPE_ACCEL_TIME_SYNCED: + sample = self.parse_packet_time_synced_accel(self.raw_data_to_sample) + elif packet_type == k.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNC_SET or packet_type == k.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNCED: + sample = self.parse_packet_time_synced_raw_aux(self.raw_data_to_sample) + else: + sample = OpenBCISample() + sample.error = 'This module does not support packet type %d' % packet_type + sample.valid = False + + sample.packet_type = packet_type + except BaseException as e: + sample = OpenBCISample() + sample.error = e.message + sample.valid = False + + return sample + + """ + /** + * @description Used transform raw data packets into fully qualified packets + * @param o {RawDataToSample} - Used to hold data and configuration settings + * @return {Array} samples An array of {Sample} + * @author AJ Keller (@aj-ptw) + */ +function transformRawDataPacketsToSample (o) { + let samples = []; + for (let i = 0; i < o.rawDataPackets.length; i++) { + o.rawDataPacket = o.rawDataPackets[i]; + const sample = transformRawDataPacketToSample(o); + samples.push(sample); + if (sample.hasOwnProperty('sampleNumber')) { + o['lastSampleNumber'] = sample.sampleNumber; + } else if (!sample.hasOwnProperty('impedanceValue')) { + o['lastSampleNumber'] = o.rawDataPacket[k.OBCIPacketPositionSampleNumber]; + } + } + return samples; +} + """ + def transform_raw_data_packets_to_sample(self, raw_data_packets): + samples = [] + + for raw_data_packet in raw_data_packets: + sample = self.transform_raw_data_packet_to_sample(raw_data_packet) + samples.append(sample) + self.raw_data_to_sample.last_sample_number = sample.sample_number + + return samples + + +class RawDataToSample(object): + """Object encapulsating a parsing object.""" + def __init__(self, + accel_data=None, + gains=None, + last_sample_number=0, + raw_data_packets=None, + raw_data_packet=None, + scale=True, + scale_factors=None, + time_offset=0, + verbose=False): + """ + RawDataToSample + :param accel_data: list + The channel settings array + :param gains: list + The gains of each channel, this is used to derive number of channels + :param last_sample_number: int + :param raw_data_packets: list + list of raw_data_packets + :param raw_data_packet: bytearray + A single raw data packet + :param scale: boolean + Default `true`. A gain of 24 for Cyton will be used and 51 for ganglion by default. + :param scale_factors: list + Calculated scale factors + :param time_offset: int + For non time stamp use cases i.e. 0xC0 or 0xC1 (default and raw aux) + :param verbose: + """ + self.accel_data = accel_data if accel_data is not None else [] + self.gains = gains if gains is not None else [] + self.time_offset = time_offset + self.last_sample_number = last_sample_number + self.raw_data_packets = raw_data_packets if raw_data_packets is not None else [] + self.raw_data_packet = raw_data_packet + self.scale = scale + self.scale_factors = scale_factors if scale_factors is not None else [] + self.verbose = verbose + + +class OpenBCISample(object): + """Object encapulsating a single sample from the OpenBCI board.""" + def __init__(self, + aux_data=None, + board_time=0, + channel_data=None, + error=None, + imp_data=None, + packet_type=k.RAW_PACKET_TYPE_STANDARD_ACCEL, + protocol=k.PROTOCOL_WIFI, + sample_number=0, + start_byte=0, + stop_byte=0, + valid=True): + self.aux_data = aux_data if aux_data is not None else [] + self.board_time = board_time + self.channel_data = channel_data if aux_data is not None else [] + self.error = error + self.id = sample_number + self.imp_data = imp_data if aux_data is not None else [] + self.packet_type = packet_type + self.protocol = protocol + self.sample_number = sample_number + self.start_byte = start_byte + self.stop_byte = stop_byte + self.valid = valid diff --git a/ssdp.py b/openbci/utils/ssdp.py similarity index 100% rename from ssdp.py rename to openbci/utils/ssdp.py diff --git a/openbci/utils/utilities.py b/openbci/utils/utilities.py new file mode 100644 index 0000000..71ecd63 --- /dev/null +++ b/openbci/utils/utilities.py @@ -0,0 +1,64 @@ +from constants import Constants as k + + +def make_tail_byte_from_packet_type(packet_type): + """ + Converts a packet type {Number} into a OpenBCI stop byte + :param packet_type: {int} The number to smash on to the stop byte. Must be 0-15, + out of bounds input will result in a 0 + :return: A properly formatted OpenBCI stop byte + """ + if packet_type < 0 or packet_type > 15: + packet_type = 0 + + return k.RAW_BYTE_STOP | packet_type + + +def sample_number_normalize(sample_number=None): + if sample_number is not None: + if sample_number > k.SAMPLE_NUMBER_MAX_CYTON: + sample_number = k.SAMPLE_NUMBER_MAX_CYTON + else: + sample_number = 0x45 + + return sample_number + + +def sample_packet(sample_number=0x45): + return bytearray([0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 0, 1, 0, 2, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_STANDARD_ACCEL)]) + + +def sample_packet_zero(sample_number): + return bytearray([0xA0, sample_number_normalize(sample_number), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_STANDARD_ACCEL)]) + + +def sample_packet_real(sample_number): + return bytearray([0xA0, sample_number_normalize(sample_number), 0x8F, 0xF2, 0x40, 0x8F, 0xDF, 0xF4, 0x90, 0x2B, 0xB6, 0x8F, 0xBF, 0xBF, 0x7F, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0x94, 0x25, 0x34, 0x20, 0xB6, 0x7D, 0, 0xE0, 0, 0xE0, 0x0F, 0x70, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_STANDARD_ACCEL)]) + + +def sample_packet_standard_raw_aux(sample_number): + return bytearray([0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 1, 2, 3, 4, 5, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_STANDARD_RAW_AUX)]) + + +def sample_packet_accel_time_sync_set(sample_number): + return bytearray([0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 1, 0, 0, 0, 1, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_ACCEL_TIME_SYNC_SET)]) + + +def sample_packet_accel_time_synced(sample_number): + return bytearray([0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 1, 0, 0, 0, 1, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_ACCEL_TIME_SYNCED)]) + + +def sample_packet_raw_aux_time_sync_set(sample_number): + return bytearray([0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0x00, 0x01, 0, 0, 0, 1, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNC_SET)]) + + +def sample_packet_raw_aux_time_synced(sample_number): + return bytearray([0xA0, sample_number_normalize(sample_number), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0x00, 0x01, 0, 0, 0, 1, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNCED)]) + + +def sample_packet_impedance(channel_number): + return bytearray([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, 0, 0, 0, 0, 0, 0, 0, 0, make_tail_byte_from_packet_type(k.RAW_PACKET_TYPE_IMPEDANCE)]) + + +def sample_packet_user_defined(): + return bytearray([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, 1, 1, 1, 1, make_tail_byte_from_packet_type(k.OBCIStreamPacketUserDefinedType)]); diff --git a/open_bci_wifi.py b/openbci/wifi.py similarity index 84% rename from open_bci_wifi.py rename to openbci/wifi.py index 9ccbad4..97f5e29 100644 --- a/open_bci_wifi.py +++ b/openbci/wifi.py @@ -16,21 +16,19 @@ def handle_sample(sample): TODO: Cyton Raw """ -import struct -import time -import timeit +import asyncore import atexit +import json import logging -import numpy as np -import sys -import ssdp -import urllib2 -import xmltodict import re -import asyncore import socket +import timeit +import urllib2 + import requests -import json +import xmltodict + +from openbci.utils import k, ParseRaw, ssdp SAMPLE_RATE = 0 # Hz @@ -42,7 +40,7 @@ def handle_sample(sample): ''' -class OpenBCIWifi(object): +class OpenBCIWiFi(object): """ Handle a connection to an OpenBCI wifi shield. @@ -57,18 +55,20 @@ class OpenBCIWifi(object): max_packets_to_skip: will try to disconnect / reconnect after too many packets are skipped """ - def __init__(self, ip_address=None, shield_name=None, sample_rate=None, log=True, timeout=2, - max_packets_to_skip=20, latency=10000): + def __init__(self, ip_address=None, shield_name=None, sample_rate=None, log=True, timeout=3, + max_packets_to_skip=20, latency=10000, high_speed=True, ssdp_attempts=5): # these one are used - self.log = log # print_incoming_text needs log - self.streaming = False - self.timeout = timeout - self.max_packets_to_skip = max_packets_to_skip + self.high_speed = high_speed self.impedance = False self.ip_address = ip_address - self.shield_name = shield_name - self.sample_rate = sample_rate self.latency = latency + self.log = log # print_incoming_text needs log + self.max_packets_to_skip = max_packets_to_skip + self.sample_rate = sample_rate + self.shield_name = shield_name + self.ssdp_attempts = ssdp_attempts + self.streaming = False + self.timeout = timeout # might be handy to know API self.board_type = "none" @@ -91,7 +91,14 @@ def __init__(self, ip_address=None, shield_name=None, sample_rate=None, log=True print("Opened socket on %s:%d" % (self.local_ip_address, self.local_wifi_server_port)) if ip_address is None: - self.find_wifi_shield(wifi_shield_cb=self.on_shield_found) + for i in range(ssdp_attempts): + try: + self.find_wifi_shield(wifi_shield_cb=self.on_shield_found) + break + except OSError: + # Try again + if self.log: + print("Did not find any WiFi Shields") else: self.on_shield_found(ip_address) @@ -147,11 +154,15 @@ def connect(self): if self.log: print("Connected to %s with %s channels" % (self.board_type, self.eeg_channels_per_sample)) + if self.high_speed: + output_style = 'raw' + else: + output_style = 'json' res_tcp_post = requests.post("http://%s/tcp" % self.ip_address, json={ 'ip': self.local_ip_address, 'port': self.local_wifi_server_port, - 'output': 'json', + 'output': output_style, 'delimiter': True, 'latency': self.latency }) @@ -178,7 +189,7 @@ def find_wifi_shield(self, shield_name=None, wifi_shield_cb=None): if self.log: print("Try to find WiFi shields on your local wireless network") - print("Scanning for 5 seconds nearby devices...") + print("Scanning for %d seconds nearby devices..." % self.timeout) list_ip = [] list_id = [] @@ -202,7 +213,7 @@ def wifi_shield_found(response): if wifi_shield_cb is not None: wifi_shield_cb(cur_ip_address) - ssdp_hits = ssdp.discover("urn:schemas-upnp-org:device:Basic:1", timeout=3, wifi_found_cb=wifi_shield_found) + ssdp_hits = ssdp.discover("urn:schemas-upnp-org:device:Basic:1", timeout=self.timeout, wifi_found_cb=wifi_shield_found) nb_wifi_shields = len(list_id) @@ -236,7 +247,7 @@ def wifi_write(self, output): raise RuntimeError("Error code: %d %s" % (res_command_post.status_code, res_command_post.text)) def getSampleRate(self): - return SAMPLE_RATE + return self.sample_rate def getNbEEGChannels(self): """Will not get new data on impedance check.""" @@ -394,48 +405,52 @@ def reconnect(self): self.init_streaming() -class OpenBCISample(object): - """Object encapulsating a single sample from the OpenBCI board.""" - - def __init__(self, packet_id, channel_data, aux_data, imp_data): - self.id = packet_id - self.channel_data = channel_data - self.aux_data = aux_data - self.imp_data = imp_data - - class WiFiShieldHandler(asyncore.dispatcher_with_send): - def __init__(self, sock, callback=None): + def __init__(self, sock, callback=None, high_speed=True, parser=None): asyncore.dispatcher_with_send.__init__(self, sock) + self.high_speed = high_speed self.callback = callback + self.parser = parser if parser is not None else ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24]) def handle_read(self): data = self.recv(3000) # 3000 is the max data the WiFi shield is allowed to send over TCP if len(data) > 2: - try: - possible_chunks = data.split('\r\n') - if len(possible_chunks) > 1: - possible_chunks = possible_chunks[:-1] - for possible_chunk in possible_chunks: - if len(possible_chunk) > 2: - chunk_dict = json.loads(possible_chunk) - if 'chunk' in chunk_dict: - for sample in chunk_dict['chunk']: - if self.callback is not None: - self.callback(sample) - else: - print("not a sample packet") - except ValueError as e: - print("failed to parse: %s" % data) - print e - except BaseException as e: - print e + if self.high_speed: + packets = len(data)/33 + raw_data_packets = [] + for i in range(packets): + raw_data_packets.append(bytearray(data[i * k.RAW_PACKET_SIZE: i * k.RAW_PACKET_SIZE + k.RAW_PACKET_SIZE])) + samples = self.parser.transform_raw_data_packets_to_sample(raw_data_packets=raw_data_packets) + + for sample in samples: + if self.callback is not None: + self.callback(sample) + + else: + try: + possible_chunks = data.split('\r\n') + if len(possible_chunks) > 1: + possible_chunks = possible_chunks[:-1] + for possible_chunk in possible_chunks: + if len(possible_chunk) > 2: + chunk_dict = json.loads(possible_chunk) + if 'chunk' in chunk_dict: + for sample in chunk_dict['chunk']: + if self.callback is not None: + self.callback(sample) + else: + print("not a sample packet") + except ValueError as e: + print("failed to parse: %s" % data) + print e + except BaseException as e: + print e class WiFiShieldServer(asyncore.dispatcher): - def __init__(self, host, port, callback=None): + def __init__(self, host, port, callback=None, gains=None, high_speed=True): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() @@ -443,15 +458,20 @@ def __init__(self, host, port, callback=None): self.listen(5) self.callback = None self.handler = None + self.parser = ParseRaw(gains=gains) + self.high_speed = high_speed def handle_accept(self): pair = self.accept() if pair is not None: sock, addr = pair print 'Incoming connection from %s' % repr(addr) - self.handler = WiFiShieldHandler(sock, self.callback) + self.handler = WiFiShieldHandler(sock, self.callback, high_speed=self.high_speed, parser=self.parser) def set_callback(self, callback): self.callback = callback if self.handler is not None: self.handler.callback = callback + + def set_gains(self, gains): + self.parser.set_ads1299_scale_factors(gains) \ No newline at end of file diff --git a/plugins/streamer_tcp_server.py b/plugins/streamer_tcp_server.py deleted file mode 100755 index 1571163..0000000 --- a/plugins/streamer_tcp_server.py +++ /dev/null @@ -1,132 +0,0 @@ -from threading import Thread -import socket, select, struct, time -import plugin_interface as plugintypes - -# Simple TCP server to "broadcast" data to clients, handling deconnections. Binary format use network endianness (i.e., big-endian), float32 - -# TODO: does not listen for anything at the moment, could use it to set options - -# Handling new client in separate thread -class MonitorStreamer(Thread): - """Launch and monitor a "Streamer" entity (incoming connections if implemented, current sampling rate).""" - # tcp_server: the TCPServer instance that will be used - def __init__(self, streamer): - Thread.__init__(self) - # bind to Streamer entity - self.server = streamer - - def run(self): - # run until we DIE - while True: - # check FPS + listen for new connections - # FIXME: not so great with threads -- use a lock? - # TODO: configure interval - self.server.check_connections() - time.sleep(1) - - -class StreamerTCPServer(plugintypes.IPluginExtended): - """ - - Relay OpenBCI values to TCP clients - - Args: - port: Port of the server - ip: IP address of the server - - """ - - def __init__(self, ip='localhost', port=12345): - # list of socket clients - self.CONNECTION_LIST = [] - # connection infos - self.ip = ip - self.port = port - - # From IPlugin - def activate(self): - if len(self.args) > 0: - self.ip = self.args[0] - if len(self.args) > 1: - self.port = int(self.args[1]) - - # init network - print("Selecting raw TCP streaming. IP: " + self.ip + ", port: " + str(self.port)) - self.initialize() - - # init the daemon that monitors connections - self.monit = MonitorStreamer(self) - self.monit.daemon = True - # launch monitor - self.monit.start() - - # the initialize method reads settings and outputs the first header - def initialize(self): - # init server - self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # this has no effect, why ? - self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # create connection - self.server_socket.bind((self.ip, self.port)) - self.server_socket.listen(1) - print("Server started on port " + str(self.port)) - - # From Streamer, to be called each time we're willing to accept new connections - def check_connections(self): - # First listen for new connections, and new connections only -- this is why we pass only server_socket - read_sockets,write_sockets,error_sockets = select.select([self.server_socket],[],[], 0) - for sock in read_sockets: - # New connection - sockfd, addr = self.server_socket.accept() - self.CONNECTION_LIST.append(sockfd) - print("Client (%s, %s) connected" % addr) - # and... don't bother with incoming messages - - # From IPlugin: close sockets, send message to client - def deactivate(self): - # close all remote connections - for sock in self.CONNECTION_LIST: - if sock != self.server_socket: - try: - sock.send("closing!\n") - # at this point don't bother if message not sent - except: - continue - sock.close(); - # close server socket - self.server_socket.close(); - - # broadcast channels values to all clients - # as_string: many for debug, send values with a nice "[34.45, 30.4, -38.0]"-like format - def __call__(self, sample, as_string=False): - values=sample.channel_data - # save sockets that are closed to remove them later on - outdated_list = [] - for sock in self.CONNECTION_LIST: - # If one error should happen, we remove socket from the list - try: - if as_string: - sock.send(str(values) + "\n") - else: - nb_channels=len(values) - # format for binary data, network endian (big) and float (float32) - packer = struct.Struct('!%sf' % nb_channels) - # convert values to bytes - packed_data = packer.pack(*values) - sock.send(packed_data) - # TODO: should check if the correct number of bytes passed through - except: - # sometimes (always?) it's only during the second write to a close socket that an error is raised? - print("Something bad happened, will close socket") - outdated_list.append(sock) - # now we are outside of the main list, it's time to remove outdated sockets, if any - for bad_sock in outdated_list: - print("Removing socket...") - self.CONNECTION_LIST.remove(bad_sock) - # not very costly to be polite - bad_sock.close() - - def show_help(self): - print("""Optional arguments: [ip [port]] - \t ip: target IP address (default: 'localhost') - \t port: target port (default: 12345)""") diff --git a/requirements.txt b/requirements.txt index b8dcee3..9df1b12 100755 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ websocket-client==0.32.0 wheel==0.24.0 Yapsy==1.11.23 bluepy==1.0.5 +xmltodict \ No newline at end of file diff --git a/scripts/stream_data.py b/scripts/stream_data.py index 6a3126e..f900c12 100644 --- a/scripts/stream_data.py +++ b/scripts/stream_data.py @@ -1,6 +1,6 @@ -import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder -import open_bci_v3 as bci -import streamer_tcp_server +import sys; sys.path.append('..') # help python find cyton.py relative to scripts folder +from openbci import cyton as bci +from openbci.plugins import StreamerTCPServer import time, timeit from threading import Thread @@ -47,93 +47,93 @@ def __init__(self): self.start_tick = self.tick def run(self): - while True: - # check FPS + listen for new connections - new_tick = timeit.default_timer() - elapsed_time = new_tick - self.tick - current_samples_in = nb_samples_in - current_samples_out = nb_samples_out - print "--- at t: ", (new_tick - self.start_tick), " ---" - print "elapsed_time: ", elapsed_time - print "nb_samples_in: ", current_samples_in - self.nb_samples_in - print "nb_samples_out: ", current_samples_out - self.nb_samples_out - self.tick = new_tick - self.nb_samples_in = nb_samples_in - self.nb_samples_out = nb_samples_out - # time to watch for connection - # FIXME: not so great with threads - server.check_connections() - time.sleep(1) + while True: + # check FPS + listen for new connections + new_tick = timeit.default_timer() + elapsed_time = new_tick - self.tick + current_samples_in = nb_samples_in + current_samples_out = nb_samples_out + print "--- at t: ", (new_tick - self.start_tick), " ---" + print "elapsed_time: ", elapsed_time + print "nb_samples_in: ", current_samples_in - self.nb_samples_in + print "nb_samples_out: ", current_samples_out - self.nb_samples_out + self.tick = new_tick + self.nb_samples_in = nb_samples_in + self.nb_samples_out = nb_samples_out + # time to watch for connection + # FIXME: not so great with threads + server.check_connections() + time.sleep(1) def streamData(sample): - - global last_values - - global tick - - # check packet skipped - global last_id - # TODO: duplicate packet if skipped to stay sync - if sample.id != last_id + 1: - print "time", tick, ": paquet skipped!" - if sample.id == 255: - last_id = -1 - else: - last_id = sample.id - - # update counters - global nb_samples_in, nb_samples_out - nb_samples_in = nb_samples_in + 1 - - # check for duplication, by default 1 (...which is *no* duplication of the one current sample) - global leftover_duplications - - # first method with sampling rate and elapsed time (depends on system clock accuracy) - if (SAMPLING_RATE > 0): - # elapsed time since last call, update tick - now = timeit.default_timer() - elapsed_time = now - tick; - # now we have to compute how many times we should send data to keep up with sample rate (oversampling) - leftover_duplications = SAMPLING_RATE * elapsed_time + leftover_duplications - 1 - tick = now - # second method with a samplin factor (depends on openbci accuracy) - elif SAMPLING_FACTOR > 0: - leftover_duplications = SAMPLING_FACTOR + leftover_duplications - 1 - #print "needed_duplications: ", needed_duplications, "leftover_duplications: ", leftover_duplications - # If we need to insert values, will interpolate between current packet and last one - # 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 - if (leftover_duplications > 1): - leftover_duplications = leftover_duplications - 1 - interpol_values = list(last_values) - for i in range(0,len(interpol_values)): - # OK, it's a very rough interpolation - interpol_values[i] = (last_values[i] + sample.channel_data[i]) / 2 - if DEBUG: - print " --" - print " last values: ", last_values - print " interpolation: ", interpol_values - print " current sample: ", sample.channel_data - # send to clients interpolated sample - #leftover_duplications = 0 - server.broadcast_values(interpol_values) + + global last_values + + global tick + + # check packet skipped + global last_id + # TODO: duplicate packet if skipped to stay sync + if sample.id != last_id + 1: + print "time", tick, ": paquet skipped!" + if sample.id == 255: + last_id = -1 + else: + last_id = sample.id + + # update counters + global nb_samples_in, nb_samples_out + nb_samples_in = nb_samples_in + 1 + + # check for duplication, by default 1 (...which is *no* duplication of the one current sample) + global leftover_duplications + + # first method with sampling rate and elapsed time (depends on system clock accuracy) + if (SAMPLING_RATE > 0): + # elapsed time since last call, update tick + now = timeit.default_timer() + elapsed_time = now - tick; + # now we have to compute how many times we should send data to keep up with sample rate (oversampling) + leftover_duplications = SAMPLING_RATE * elapsed_time + leftover_duplications - 1 + tick = now + # second method with a samplin factor (depends on openbci accuracy) + elif SAMPLING_FACTOR > 0: + leftover_duplications = SAMPLING_FACTOR + leftover_duplications - 1 + #print "needed_duplications: ", needed_duplications, "leftover_duplications: ", leftover_duplications + # If we need to insert values, will interpolate between current packet and last one + # 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 + if (leftover_duplications > 1): + leftover_duplications = leftover_duplications - 1 + interpol_values = list(last_values) + for i in range(0,len(interpol_values)): + # OK, it's a very rough interpolation + interpol_values[i] = (last_values[i] + sample.channel_data[i]) / 2 + if DEBUG: + print " --" + print " last values: ", last_values + print " interpolation: ", interpol_values + print " current sample: ", sample.channel_data + # send to clients interpolated sample + # leftover_duplications = 0 + server.broadcast_values(interpol_values) + nb_samples_out = nb_samples_out + 1 + + # send to clients current sample + server.broadcast_values(sample.channel_data) nb_samples_out = nb_samples_out + 1 - - # send to clients current sample - server.broadcast_values(sample.channel_data) - nb_samples_out = nb_samples_out + 1 - - # save current values for possible interpolation - last_values = list(sample.channel_data) + + # save current values for possible interpolation + last_values = list(sample.channel_data) if __name__ == '__main__': - # init server - server = streamer_tcp_server.StreamerTCPServer(ip=SERVER_IP, port=SERVER_PORT, nb_channels=NB_CHANNELS) - # init board - port = '/dev/ttyUSB1' - baud = 115200 - monit = Monitor() - # daemonize theard to terminate it altogether with the main when time will come - monit.daemon = True - monit.start() - board = bci.OpenBCIBoard(port=port, baud=baud, filter_data=False) - board.startStreaming(streamData) + # init server + server = StreamerTCPServer(ip=SERVER_IP, port=SERVER_PORT) + # init board + port = '/dev/tty.usbserial-DB00JAM0' + baud = 115200 + monit = Monitor() + # daemonize theard to terminate it altogether with the main when time will come + monit.daemon = True + monit.start() + board = bci.OpenBCICyton(port=port, baud=baud, filter_data=False) + board.start_streaming(streamData) diff --git a/scripts/stream_data_wifi.py b/scripts/stream_data_wifi.py index 9c6fae5..17bc0ac 100644 --- a/scripts/stream_data_wifi.py +++ b/scripts/stream_data_wifi.py @@ -1,5 +1,5 @@ -import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder -import open_bci_wifi as bci +import sys; sys.path.append('..') # help python find cyton.py relative to scripts folder +from openbci import wifi as bci import logging @@ -11,7 +11,7 @@ def printData(sample): shield_name = 'OpenBCI-E2B6' logging.basicConfig(filename="test.log",format='%(asctime)s - %(levelname)s : %(message)s',level=logging.DEBUG) logging.info('---------LOG START-------------') - shield = bci.OpenBCIWifi(shield_name=shield_name, log=True) + shield = bci.OpenBCIWiFi(shield_name=shield_name, log=True) print("WiFi Shield Instantiated") shield.start_streaming(printData) diff --git a/scripts/stream_data_wifi_high_speed.py b/scripts/stream_data_wifi_high_speed.py new file mode 100644 index 0000000..1694738 --- /dev/null +++ b/scripts/stream_data_wifi_high_speed.py @@ -0,0 +1,20 @@ +import sys; sys.path.append('..') # help python find cyton.py relative to scripts folder +from openbci import wifi as bci +import logging + + +def printData(sample): + print sample.sample_number + + +if __name__ == '__main__': + logging.basicConfig(filename="test.log",format='%(asctime)s - %(levelname)s : %(message)s',level=logging.DEBUG) + logging.info('---------LOG START-------------') + # If you don't know your IP Address, you can use shield name option + # If you know IP, such as with wifi direct 192.168.4.1, then use ip_address='192.168.4.1' + shield_name = 'OpenBCI-E218' + shield = bci.OpenBCIWiFi(shield_name=shield_name, log=True, high_speed=True) + print("WiFi Shield Instantiated") + shield.start_streaming(printData) + + shield.loop() diff --git a/scripts/test.py b/scripts/test.py index 230a2ff..4f49f0b 100644 --- a/scripts/test.py +++ b/scripts/test.py @@ -1,28 +1,28 @@ -import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder -import open_bci_v3 as bci -import os +import sys; sys.path.append('..') # help python find cyton.py relative to scripts folder +from openbci import cyton as bci import logging import time def printData(sample): - #os.system('clear') - print "----------------" - print("%f" %(sample.id)) - print sample.channel_data - print sample.aux_data - print "----------------" + #os.system('clear') + print "----------------" + print("%f" %(sample.id)) + print sample.channel_data + print sample.aux_data + print "----------------" if __name__ == '__main__': - port = '/dev/tty.OpenBCI-DN008VTF' - #port = '/dev/tty.OpenBCI-DN0096XA' - baud = 115200 - logging.basicConfig(filename="test.log",format='%(asctime)s - %(levelname)s : %(message)s',level=logging.DEBUG) - logging.info('---------LOG START-------------') - board = bci.OpenBCIBoard(port=port, scaled_output=False, log=True) - print("Board Instantiated") - board.ser.write('v') - time.sleep(10) - #board.start_streaming(printData) - board.print_bytes_in() + # port = '/dev/tty.OpenBCI-DN008VTF' + port = '/dev/tty.usbserial-DB00JAM0' + # port = '/dev/tty.OpenBCI-DN0096XA' + baud = 115200 + logging.basicConfig(filename="test.log",format='%(asctime)s - %(levelname)s : %(message)s',level=logging.DEBUG) + logging.info('---------LOG START-------------') + board = bci.OpenBCICyton(port=port, scaled_output=False, log=True) + print("Board Instantiated") + board.ser.write('v') + time.sleep(10) + board.start_streaming(printData) + board.print_bytes_in() diff --git a/scripts/udp_client.py b/scripts/udp_client.py index 7179c11..f6c4e28 100644 --- a/scripts/udp_client.py +++ b/scripts/udp_client.py @@ -3,8 +3,7 @@ import argparse import cPickle as pickle import json -import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder -import open_bci_v3 as open_bci +import sys; sys.path.append('..') # help python find cyton.py relative to scripts folder import socket diff --git a/scripts/udp_server.py b/scripts/udp_server.py index b9fd931..f548190 100644 --- a/scripts/udp_server.py +++ b/scripts/udp_server.py @@ -10,8 +10,8 @@ import argparse import cPickle as pickle import json -import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder -import open_bci_v3 as open_bci +import sys; sys.path.append('..') # help python find cyton.py relative to scripts folder +from openbci import cyton as open_bci import socket @@ -67,7 +67,7 @@ def handle_sample(self, sample): args = parser.parse_args() -obci = open_bci.OpenBCIBoard(args.serial, int(args.baud)) +obci = open_bci.OpenBCICyton(args.serial, int(args.baud)) if args.filter_data: obci.filter_data = True sock_server = UDPServer(args.host, int(args.port), args.json) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c637411 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup, find_packages + +setup(name = 'OpenBCI_Python', + version = '1.0.0', + description = 'A lib for controlling OpenBCI Devices', + author='AJ Keller', + author_email='pushtheworldllc@gmail.com', + license='MIT', + packages=find_packages(), + install_requires=['numpy'], + url='https://github.com/openbci/openbci_python', # use the URL to the github repo + download_url='https://github.com/openbci/openbci_python/archive/v0.1.0.tar.gz', + keywords=['device', 'control', 'eeg', 'emg', 'ekg', 'ads1299', 'openbci', 'ganglion', 'cyton', 'wifi'], # arbitrary keywords + zip_safe=False) diff --git a/test_log.py b/test_log.py index e45a1e9..8ec7da0 100644 --- a/test_log.py +++ b/test_log.py @@ -1,6 +1,5 @@ -import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder -import open_bci_v3 as bci -import os +import sys; sys.path.append('..') # help python find cyton.py relative to scripts folder +from openbci import cyton as bci import logging import time @@ -19,7 +18,7 @@ def printData(sample): baud = 115200 logging.basicConfig(filename="test.log",format='%(message)s',level=logging.DEBUG) logging.info('---------LOG START-------------') - board = bci.OpenBCIBoard(port=port, scaled_output=False, log=True) + board = bci.OpenBCICyton(port=port, scaled_output=False, log=True) #32 bit reset board.ser.write('v') diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 0000000..f67219d --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,99 @@ +from unittest import TestCase, main, skip +import mock + +from openbci.utils import k + + +class TestConstants(TestCase): + + def test_ads1299(self): + self.assertEqual(k.ADS1299_GAIN_1, 1.0) + self.assertEqual(k.ADS1299_GAIN_2, 2.0) + self.assertEqual(k.ADS1299_GAIN_4, 4.0) + self.assertEqual(k.ADS1299_GAIN_6, 6.0) + self.assertEqual(k.ADS1299_GAIN_8, 8.0) + self.assertEqual(k.ADS1299_GAIN_12, 12.0) + self.assertEqual(k.ADS1299_GAIN_24, 24.0) + self.assertEqual(k.ADS1299_VREF, 4.5) + + def test_board_types(self): + self.assertEqual(k.BOARD_CYTON, 'cyton') + self.assertEqual(k.BOARD_DAISY, 'daisy') + self.assertEqual(k.BOARD_GANGLION, 'ganglion') + self.assertEqual(k.BOARD_NONE, 'none') + + def test_cyton_variables(self): + self.assertEqual(k.CYTON_ACCEL_SCALE_FACTOR_GAIN, 0.002 / (pow(2, 4))) + + def test_errors(self): + self.assertEqual(k.ERROR_INVALID_BYTE_LENGTH, 'Invalid Packet Byte Length') + self.assertEqual(k.ERROR_INVALID_BYTE_START, 'Invalid Start Byte') + self.assertEqual(k.ERROR_INVALID_BYTE_STOP, 'Invalid Stop Byte') + self.assertEqual(k.ERROR_INVALID_DATA, 'Invalid data - try again') + self.assertEqual(k.ERROR_INVALID_TYPW, 'Invalid type - check comments for input type') + self.assertEqual(k.ERROR_MISSING_REGISTER_SETTING, 'Missing register setting') + self.assertEqual(k.ERROR_MISSING_REQUIRED_PROPERTY, 'Missing property in JSON') + self.assertEqual(k.ERROR_TIME_SYNC_IS_NULL, "'this.sync.curSyncObj' must not be null") + self.assertEqual(k.ERROR_TIME_SYNC_NO_COMMA, 'Missed the time sync sent confirmation. Try sync again') + self.assertEqual(k.ERROR_UNDEFINED_OR_NULL_INPUT, 'Undefined or Null Input') + + def test_number_of_channels(self): + self.assertEqual(k.NUMBER_OF_CHANNELS_CYTON, 8) + self.assertEqual(k.NUMBER_OF_CHANNELS_DAISY, 16) + self.assertEqual(k.NUMBER_OF_CHANNELS_GANGLION, 4) + + def test_protocols(self): + """ Protocols """ + self.assertEqual(k.PROTOCOL_BLE, 'ble') + self.assertEqual(k.PROTOCOL_SERIAL, 'serial') + self.assertEqual(k.PROTOCOL_WIFI, 'wifi') + + def test_raw(self): + self.assertEqual(k.RAW_BYTE_START, 0xA0) + self.assertEqual(k.RAW_BYTE_STOP, 0xC0) + self.assertEqual(k.RAW_PACKET_ACCEL_NUMBER_AXIS, 3) + self.assertEqual(k.RAW_PACKET_SIZE, 33) + self.assertEqual(k.RAW_PACKET_POSITION_CHANNEL_DATA_START, 2) + self.assertEqual(k.RAW_PACKET_POSITION_CHANNEL_DATA_STOP, 25) + self.assertEqual(k.RAW_PACKET_POSITION_SAMPLE_NUMBER, 1) + self.assertEqual(k.RAW_PACKET_POSITION_START_BYTE, 0) + self.assertEqual(k.RAW_PACKET_POSITION_STOP_BYTE, 32) + self.assertEqual(k.RAW_PACKET_POSITION_START_AUX, 26) + self.assertEqual(k.RAW_PACKET_POSITION_STOP_AUX, 31) + self.assertEqual(k.RAW_PACKET_POSITION_TIME_SYNC_AUX_START, 26) + self.assertEqual(k.RAW_PACKET_POSITION_TIME_SYNC_AUX_STOP, 28) + self.assertEqual(k.RAW_PACKET_POSITION_TIME_SYNC_TIME_START, 28) + self.assertEqual(k.RAW_PACKET_POSITION_TIME_SYNC_TIME_STOP, 32) + self.assertEqual(k.RAW_PACKET_TYPE_STANDARD_ACCEL, 0) + self.assertEqual(k.RAW_PACKET_TYPE_STANDARD_RAW_AUX, 1) + self.assertEqual(k.RAW_PACKET_TYPE_USER_DEFINED_TYPE, 2) + self.assertEqual(k.RAW_PACKET_TYPE_ACCEL_TIME_SYNC_SET, 3) + self.assertEqual(k.RAW_PACKET_TYPE_ACCEL_TIME_SYNCED, 4) + self.assertEqual(k.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNC_SET, 5) + self.assertEqual(k.RAW_PACKET_TYPE_RAW_AUX_TIME_SYNCED, 6) + self.assertEqual(k.RAW_PACKET_TYPE_IMPEDANCE, 7) + + def test_sample_number_max(self): + self.assertEqual(k.SAMPLE_NUMBER_MAX_CYTON, 255) + self.assertEqual(k.SAMPLE_NUMBER_MAX_GANGLION, 200) + + def test_sample_rates(self): + self.assertEqual(k.SAMPLE_RATE_1000, 1000) + self.assertEqual(k.SAMPLE_RATE_125, 125) + self.assertEqual(k.SAMPLE_RATE_12800, 12800) + self.assertEqual(k.SAMPLE_RATE_1600, 1600) + self.assertEqual(k.SAMPLE_RATE_16000, 16000) + self.assertEqual(k.SAMPLE_RATE_200, 200) + self.assertEqual(k.SAMPLE_RATE_2000, 2000) + self.assertEqual(k.SAMPLE_RATE_250, 250) + self.assertEqual(k.SAMPLE_RATE_25600, 25600) + self.assertEqual(k.SAMPLE_RATE_3200, 3200) + self.assertEqual(k.SAMPLE_RATE_400, 400) + self.assertEqual(k.SAMPLE_RATE_4000, 4000) + self.assertEqual(k.SAMPLE_RATE_500, 500) + self.assertEqual(k.SAMPLE_RATE_6400, 6400) + self.assertEqual(k.SAMPLE_RATE_800, 800) + self.assertEqual(k.SAMPLE_RATE_8000, 8000) + +if __name__ == '__main__': + main() diff --git a/tests/test_parse.py b/tests/test_parse.py new file mode 100644 index 0000000..69e383e --- /dev/null +++ b/tests/test_parse.py @@ -0,0 +1,250 @@ +from unittest import TestCase, main, skip +import mock + +from openbci.utils import (k, + ParseRaw, + sample_packet, + sample_packet_standard_raw_aux, + sample_packet_accel_time_sync_set, + sample_packet_accel_time_synced, + sample_packet_raw_aux_time_sync_set, + sample_packet_raw_aux_time_synced, + RawDataToSample) + + +class TestParseRaw(TestCase): + + def test_get_channel_data_array(self): + expected_gains = [24, 24, 24, 24, 24, 24, 24, 24] + expected_sample_number = 0 + + data = sample_packet(expected_sample_number) + + parser = ParseRaw(gains=expected_gains, scaled_output=True) + + scale_factors = parser.get_ads1299_scale_factors(expected_gains) + + expected_channel_data = [] + for i in range(k.NUMBER_OF_CHANNELS_CYTON): + expected_channel_data.append(scale_factors[i] * (i + 1)) + + parser.raw_data_to_sample.raw_data_packet = data + + actual_channel_data = parser.get_channel_data_array(parser.raw_data_to_sample) + + self.assertListEqual(actual_channel_data, expected_channel_data) + + def test_get_data_array_accel(self): + expected_sample_number = 0 + + data = sample_packet(expected_sample_number) + + parser = ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24], scaled_output=True) + + expected_accel_data = [] + for i in range(k.RAW_PACKET_ACCEL_NUMBER_AXIS): + expected_accel_data.append(k.CYTON_ACCEL_SCALE_FACTOR_GAIN * i) + + parser.raw_data_to_sample.raw_data_packet = data + + actual_accel_data = parser.get_data_array_accel(parser.raw_data_to_sample) + + self.assertListEqual(actual_accel_data, expected_accel_data) + + def test_interpret_16_bit_as_int_32(self): + + parser = ParseRaw() + + # 0x0690 === 1680 + self.assertEqual(parser.interpret_16_bit_as_int_32(bytearray([0x06, 0x90])), + 1680, + 'converts a small positive number') + + # 0x02C0 === 704 + self.assertEqual(parser.interpret_16_bit_as_int_32(bytearray([0x02, 0xC0])), + 704, + 'converts a large positive number') + + # 0xFFFF === -1 + self.assertEqual(parser.interpret_16_bit_as_int_32(bytearray([0xFF, 0xFF])), + -1, + 'converts a small negative number') + + # 0x81A1 === -32351 + self.assertEqual(parser.interpret_16_bit_as_int_32(bytearray([0x81, 0xA1])), + -32351, + 'converts a large negative number') + + def test_interpret_24_bit_as_int_32(self): + + parser = ParseRaw() + + # 0x000690 === 1680 + expected_value = 1680 + actual_value = parser.interpret_24_bit_as_int_32(bytearray([0x00, 0x06, 0x90])) + self.assertEqual(actual_value, + expected_value, + 'converts a small positive number') + + # 0x02C001 === 180225 + expected_value = 180225 + actual_value = parser.interpret_24_bit_as_int_32(bytearray([0x02, 0xC0, 0x01])) + self.assertEqual(actual_value, + expected_value, + 'converts a large positive number') + + # 0xFFFFFF === -1 + expected_value = -1 + actual_value = parser.interpret_24_bit_as_int_32(bytearray([0xFF, 0xFF, 0xFF])) + self.assertEqual(actual_value, + expected_value, + 'converts a small negative number') + + # 0x81A101 === -8281855 + expected_value = -8281855 + actual_value = parser.interpret_24_bit_as_int_32(bytearray([0x81, 0xA1, 0x01])) + self.assertEqual(actual_value, + expected_value, + 'converts a large negative number') + + def test_parse_raw_init(self): + expected_board_type = k.BOARD_DAISY + expected_gains = [24, 24, 24, 24, 24, 24, 24, 24] + expected_log = True + expected_micro_volts = True + expected_scaled_output = False + + parser = ParseRaw(board_type=expected_board_type, + gains=expected_gains, + log=expected_log, + micro_volts=expected_micro_volts, + scaled_output=expected_scaled_output) + + self.assertEqual(parser.board_type, expected_board_type) + self.assertEqual(parser.scaled_output, expected_scaled_output) + self.assertEqual(parser.log, expected_log) + + def test_get_ads1299_scale_factors_volts(self): + gains = [24, 24, 24, 24, 24, 24, 24, 24] + expected_scale_factors = [] + for gain in gains: + scale_factor = 4.5 / float((pow(2, 23) - 1)) / float(gain) + expected_scale_factors.append(scale_factor) + + parser = ParseRaw() + + actual_scale_factors = parser.get_ads1299_scale_factors(gains) + + self.assertEqual(actual_scale_factors, + expected_scale_factors, + "should be able to get scale factors for gains in volts") + + def test_get_ads1299_scale_factors_micro_volts(self): + gains = [24, 24, 24, 24, 24, 24, 24, 24] + micro_volts = True + expected_scale_factors = [] + for gain in gains: + scale_factor = 4.5 / float((pow(2, 23) - 1)) / float(gain) * 1000000. + expected_scale_factors.append(scale_factor) + + parser = ParseRaw() + + actual_scale_factors = parser.get_ads1299_scale_factors(gains, micro_volts) + + self.assertEqual(actual_scale_factors, + expected_scale_factors, + "should be able to get scale factors for gains in volts") + + def test_parse_packet_standard_accel(self): + data = sample_packet() + + expected_scale_factor = 4.5 / 24 / (pow(2, 23) - 1) + + parser = ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24], scaled_output=True) + + parser.raw_data_to_sample.raw_data_packet = data + + sample = parser.parse_packet_standard_accel(parser.raw_data_to_sample) + + self.assertIsNotNone(sample) + for i in range(len(sample.channel_data)): + self.assertEqual(sample.channel_data[i], expected_scale_factor * (i + 1)) + for i in range(len(sample.accel_data)): + self.assertEqual(sample.accel_data[i], k.CYTON_ACCEL_SCALE_FACTOR_GAIN * i) + self.assertEqual(sample.packet_type, k.RAW_PACKET_TYPE_STANDARD_ACCEL) + self.assertEqual(sample.sample_number, 0x45) + self.assertEqual(sample.start_byte, 0xA0) + self.assertEqual(sample.stop_byte, 0xC0) + self.assertTrue(sample.valid) + + @mock.patch.object(ParseRaw, 'parse_packet_standard_accel') + def test_transform_raw_data_packet_to_sample_accel(self, mock_parse_packet_standard_accel): + data = sample_packet(0) + + parser = ParseRaw() + + parser.transform_raw_data_packet_to_sample(data) + + mock_parse_packet_standard_accel.assert_called_once() + + @mock.patch.object(ParseRaw, 'parse_packet_standard_raw_aux') + def test_transform_raw_data_packet_to_sample_raw_aux(self, mock_parse_packet_standard_raw_aux): + data = sample_packet_standard_raw_aux(0) + + parser = ParseRaw() + + parser.transform_raw_data_packet_to_sample(data) + + mock_parse_packet_standard_raw_aux.assert_called_once() + + @mock.patch.object(ParseRaw, 'parse_packet_time_synced_accel') + def test_transform_raw_data_packet_to_sample_time_sync_accel(self, mock_parse_packet_time_synced_accel): + data = sample_packet_accel_time_sync_set(0) + + parser = ParseRaw() + + parser.transform_raw_data_packet_to_sample(data) + + mock_parse_packet_time_synced_accel.assert_called_once() + + mock_parse_packet_time_synced_accel.reset_mock() + + data = sample_packet_accel_time_synced(0) + + parser.transform_raw_data_packet_to_sample(data) + + mock_parse_packet_time_synced_accel.assert_called_once() + + @mock.patch.object(ParseRaw, 'parse_packet_time_synced_raw_aux') + def test_transform_raw_data_packet_to_sample_time_sync_raw(self, mock_parse_packet_time_synced_raw_aux): + data = sample_packet_raw_aux_time_sync_set(0) + + parser = ParseRaw() + + parser.transform_raw_data_packet_to_sample(data) + + mock_parse_packet_time_synced_raw_aux.assert_called_once() + + mock_parse_packet_time_synced_raw_aux.reset_mock() + + data = sample_packet_raw_aux_time_synced(0) + + parser.transform_raw_data_packet_to_sample(data) + + mock_parse_packet_time_synced_raw_aux.assert_called_once() + + def test_transform_raw_data_packets_to_sample(self): + datas = [sample_packet(0), sample_packet(1), sample_packet(2)] + + parser = ParseRaw(gains=[24, 24, 24, 24, 24, 24, 24, 24]) + + samples = parser.transform_raw_data_packets_to_sample(datas) + + self.assertEqual(len(samples), len(datas)) + + for i in range(len(samples)): + self.assertEqual(samples[i].sample_number, i) + + +if __name__ == '__main__': + main() diff --git a/tests/test_wifi.py b/tests/test_wifi.py new file mode 100644 index 0000000..1797d24 --- /dev/null +++ b/tests/test_wifi.py @@ -0,0 +1,45 @@ +from unittest import TestCase, main, skip +import mock + +from openbci import OpenBCIWiFi + + +class TestOpenBCIWiFi(TestCase): + + @mock.patch.object(OpenBCIWiFi, 'on_shield_found') + def test_wifi_init(self, mock_on_shield_found): + expected_ip_address = '192.168.0.1' + expected_shield_name = 'OpenBCI-E218' + expected_sample_rate = 500 + expected_log = False + expected_timeout = 5 + expected_max_packets_to_skip = 10 + expected_latency = 5000 + expected_high_speed = False + expected_ssdp_attempts = 2 + + wifi = OpenBCIWiFi(ip_address=expected_ip_address, + shield_name=expected_shield_name, + sample_rate=expected_sample_rate, + log=expected_log, + timeout=expected_timeout, + max_packets_to_skip=expected_max_packets_to_skip, + latency=expected_latency, + high_speed=expected_high_speed, + ssdp_attempts=expected_ssdp_attempts) + + self.assertEqual(wifi.ip_address, expected_ip_address) + self.assertEqual(wifi.shield_name, expected_shield_name) + self.assertEqual(wifi.sample_rate, expected_sample_rate) + self.assertEqual(wifi.log, expected_log) + self.assertEqual(wifi.timeout, expected_timeout) + self.assertEqual(wifi.max_packets_to_skip, expected_max_packets_to_skip) + self.assertEqual(wifi.latency, expected_latency) + self.assertEqual(wifi.high_speed, expected_high_speed) + self.assertEqual(wifi.ssdp_attempts, expected_ssdp_attempts) + + mock_on_shield_found.assert_called_with(expected_ip_address) + + +if __name__ == '__main__': + main() diff --git a/user.py b/user.py index 438ee70..b1fa63c 100644 --- a/user.py +++ b/user.py @@ -1,12 +1,11 @@ #!/usr/bin/env python2.7 import argparse # new in Python2.7 -import os -import time -import string import atexit -import threading import logging +import string import sys +import threading +import time logging.basicConfig(level=logging.ERROR) @@ -60,10 +59,10 @@ if args.board == "cyton": print ("Board type: OpenBCI Cyton (v3 API)") - import open_bci_v3 as bci + from openbci import cyton as bci elif args.board == "ganglion": print ("Board type: OpenBCI Ganglion") - import open_bci_ganglion as bci + import openbci.ganglion as bci else: raise ValueError('Board type %r was not recognized. Known are 3 and 4' % args.board) @@ -112,12 +111,12 @@ print ("user.py: Logging Disabled.") print ("\n-------INSTANTIATING BOARD-------") - board = bci.OpenBCIBoard(port=args.port, - daisy=args.daisy, - filter_data=args.filtering, - scaled_output=True, - log=args.log, - aux=args.aux) + board = bci.OpenBCIGanglion(port=args.port, + daisy=args.daisy, + filter_data=args.filtering, + scaled_output=True, + log=args.log, + aux=args.aux) # Info about effective number of channels and sampling rate if board.daisy: