├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── externals └── mne_openbci.py ├── open_bci_ganglion.py ├── open_bci_v3.py ├── plugin_interface.py ├── plugins ├── README.md ├── 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 ├── requirements.txt ├── scripts ├── README.md ├── simple_serial.py ├── socket_client.py ├── stream_data.py ├── test.py ├── udp_client.py └── udp_server.py ├── test_log.py └── user.py /.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 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "bluepy"] 2 | path = bluepy 3 | url = https://github.com/IanHarvey/bluepy.git 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## dev 3 | 4 | Features: 5 | - Stream data over TCP (OpenViBE telnet reader format), OSC, UDP, LSL 6 | - 16 channels support (daisy module) 7 | - test sampling rate 8 | - plugin system 9 | - several different callback functions 10 | - start streaming in a separate thread so new commands can be issued 11 | 12 | Bugfixes: 13 | - scale factor 14 | - timing for Windows OS 15 | - aux data endianness 16 | - reset board on startup 17 | 18 | ## 0.1 (2015-02-11) 19 | 20 | First stable version. (?) 21 | -------------------------------------------------------------------------------- /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 | OpenBCI_Python 2 | ============== 3 | 4 | The Python software library designed to work with OpenBCI hardware. 5 | 6 | Please direct any questions, suggestions and bug reports to the github repo at: https://github.com/OpenBCI/OpenBCI_Python 7 | 8 | ## Dependancies: 9 | 10 | * Python 2.7 or later (https://www.python.org/download/releases/2.7/) 11 | * Numpy 1.7 or later (http://www.numpy.org/) 12 | * Yapsy -- if using pluging via `user.py` (http://yapsy.sourceforge.net/) 13 | 14 | NOTE: For comprehensive list see requirments.txt: (https://github.com/OpenBCI/OpenBCI_Python/blob/master/requirements.txt) 15 | 16 | OpenBCI 8 and 32 bit board with 8 or 16 channels. 17 | 18 | This library includes the main open_bci_v3 class definition that instantiates an OpenBCI Board object. This object will initialize communication with the board and get the environment ready for data streaming. This library is designed to work with iOS and Linux distributions. To use a Windows OS, change the __init__ function in open_bci_v3.py to establish a serial connection in Windows. 19 | 20 | For additional details on connecting your board visit: http://docs.openbci.com/tutorials/01-GettingStarted 21 | 22 | ### Ganglion Board 23 | 24 | The Ganglion board relies on Bluetooth Low Energy connectivity (BLE). You should also retrieve the bluepy submodule for a more up-to-date version than the version `1.0.5` available at that time through `pip`. To do so, clone this repo with the `--recursive` flag then type `make` inside `bluepy/bluepy`. Note that you may need to run the script with root privileges to for some functionality, e.g. auto-detect MAC address. 25 | 26 | You may also 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. 27 | 28 | On linux, assuming `hci0` is the name of your bluetooth adapter: 29 | 30 | `sudo echo 9 > /sys/kernel/debug/bluetooth/hci0/conn_min_interval` 31 | `sudo echo 10 > /sys/kernel/debug/bluetooth/hci0/conn_max_interval` 32 | 33 | # Audience: 34 | 35 | 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. 36 | 37 | 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. 38 | 39 | ## Functionality 40 | 41 | ### Basic usage 42 | 43 | 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. 44 | 45 | OpenBCISample members: 46 | -id: 47 | int from 0-255. Used to tell if packets were skipped. 48 | 49 | -channel_data: 50 | 8 int array with current voltage value of each channel (1-8) 51 | 52 | -aux_data: 53 | 3 int array with current auxiliary data. (0s by default) 54 | 55 | ### user.py 56 | 57 | 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). 58 | 59 | Then simply run the code given as an argument the port your board is connected to: 60 | Ex Linux: 61 | > $python user.py -p /dev/ttyUSB0 62 | 63 | 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. 64 | 65 | A good first test is to try is to type '?': 66 | >--> ? 67 | 68 | This should output the current configuration settings on the board. 69 | 70 | 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: 71 | 72 | >--> [ 73 | 74 | Alternatively, there are 6 test signals pre configured: 75 | 76 | > --> /test1 (connect all pins to ground) 77 | 78 | > --> /test2 (connect all pins to vcc) 79 | 80 | > --> /test3 (Connecting pins to low frequency 1x amp signal) 81 | 82 | > --> /test4 (Connecting pins to high frequency 1x amp signal) 83 | 84 | > --> /test5 (Connecting pins to low frequency 2x amp signal) 85 | 86 | > --> /test6 (Connecting pins to high frequency 2x amp signal) 87 | 88 | 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. 89 | 90 | For example, writing 91 | > -->x3020000X 92 | will do the following: 93 | 94 | ‘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. 95 | 96 | Pre-configured commands that use the / prefix are: 97 | 98 | test (As explained above) 99 | 100 | > --> /test4 101 | 102 | start selected plugins (see below) 103 | 104 | > --> /start 105 | 106 | Adding the argument "T:number" will set a timeout on the start command. 107 | 108 | > --> /start T:5 109 | 110 | Stop the steam to issue new commands 111 | 112 | > --> /stop 113 | 114 | #### Useful commands: 115 | 116 | Writting to SD card a high frequency square wave (test5) for 3 seconds: 117 | ``` 118 | $ python user.py -p /dev/ttyUSB0 119 | User serial interface enabled... 120 | Connecting to /dev/ttyUSB0 121 | Serial established... 122 | View command map at http://docs.openbci.com. 123 | Type start to run. Type /exit to exit. 124 | 125 | --> 126 | OpenBCI V3 8bit Board 127 | Setting ADS1299 Channel Values 128 | ADS1299 Device ID: 0x3E 129 | LIS3DH Device ID: 0x33 130 | Free RAM: 447 131 | $$$ 132 | --> /test5 133 | Warning: Connecting pins to high frequency 2x amp signal 134 | 135 | --> a 136 | Corresponding SD file OBCI_18.TXT$$$ 137 | --> /start T:3 138 | 139 | ``` 140 | 141 | NOTES: 142 | 143 | When writing to the board and expecting a response, give the board a second. It sometimes lags and requires 144 | the user to hit enter on the user.py script until you get a response. 145 | 146 | ### Plugins 147 | 148 | #### Use plugins 149 | 150 | Select the print plugin: 151 | 152 | > $python user.py -p /dev/ttyUSB0 --add print 153 | 154 | Plugin with optional parameter: 155 | 156 | > $python user.py -p /dev/ttyUSB0 --add csv_collect record.csv 157 | 158 | Select several plugins, e.g. streaming to OSC and displaying effective sample rate: 159 | 160 | > $python user.py -p /dev/ttyUSB0 --add streamer_osc --add sample_rate 161 | 162 | Note: type `/start` to launch the selected plugins. 163 | 164 | #### Create new plugins 165 | 166 | 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: 167 | 168 | ```python 169 | import plugin_interface as plugintypes 170 | 171 | class PluginPrint(plugintypes.IPluginExtended): 172 | def activate(self): 173 | print "Print activated" 174 | 175 | def deactivate(self): 176 | print "Goodbye" 177 | 178 | def show_help(self): 179 | print "I do not need any parameter, just printing stuff." 180 | 181 | # called with each new sample 182 | def __call__(self, sample): 183 | print "----------------" 184 | print("%f" %(sample.id)) 185 | print sample.channel_data 186 | print sample.aux_data 187 | ``` 188 | 189 | Describe your plugin with a corresponding `print.yapsy-plugin`: 190 | 191 | ``` 192 | [Core] 193 | Name = print 194 | Module = print 195 | 196 | [Documentation] 197 | Author = Various 198 | Version = 0.1 199 | Description = Print board values on stdout 200 | ``` 201 | 202 | 203 | You're done, your plugin should be automatically detected by `user.py`. 204 | 205 | #### Existing plugins 206 | 207 | * `print`: Display sample values -- *verbose* output! 208 | 209 | * `csv_collect`: Export data to a csv file. 210 | 211 | * `sample_rate`: Print effective sampling rate averaged over XX seconds (default: 10). 212 | 213 | * `streamer_tcp`: Acts as a TCP server, using a "raw" protocol to send value. 214 | * 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). 215 | * Default IP: localhost, default port: 12345 216 | 217 | * `streamer_osc`: Data is sent through OSC (UDP layer). 218 | * Default IP: localhost, default port: 12345, default stream name: `/openbci` 219 | * Requires pyosc. On linux type either `pip install --pre pyosc` as root, or `pip install --pre --user`. 220 | 221 | * `udp_server`: Very simple UDP server that sends data as json. Made to work with: https://github.com/OpenBCI/OpenBCI_Node 222 | * Default IP: 127.0.0.1, default port: 8888 223 | 224 | * `streamer_lsl`: Data is sent through [LSL](https://github.com/sccn/labstreaminglayer/). 225 | * Default EEG stream name "OpenBCI_EEG", ID "openbci_eeg_id1"; default AUX stream name "OpenBCI_AUX", ID "openbci_aux_id1". 226 | * 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`. 227 | 228 | Tip: Type `python user.py --list` to list available plugins and `python user.py --help [plugin_name]` to get more information. 229 | 230 | ### Scripts 231 | 232 | In the `scripts` folder you will find code snippets that use directly the `OpenBCIBoard` class from `open_bci_v3.py`. 233 | 234 | Note: copy `open_bci_v3.py` there if you want to run the code -- no proper package yet. 235 | 236 | * `test.py`: minimal example, printing values. 237 | * `stream_data.py` a version of a TCP streaming server that somehow oversamples OpenBCI from 250 to 256Hz. 238 | * `upd_server.py` *DEPRECATED* (Use Plugin): see https://github.com/OpenBCI/OpenBCI_Node for implementation example. 239 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | * default set of instructions to send to the board on startup via command line 3 | * make a proper package 4 | * requirements.txt for pip 5 | * fix long recording crash (might be hardware or software fix) 6 | 7 | -------------------------------------------------------------------------------- /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 | np = None 9 | try: 10 | import numpy as np 11 | except ImportError: 12 | raise ImportError('Numpy is needed to use function.') 13 | mne = None 14 | try: 15 | from mne.utils import verbose, logger 16 | from mne.io.meas_info import create_info 17 | from mne.io.base import _BaseRaw 18 | except ImportError: 19 | raise ImportError('MNE is needed to use function.') 20 | 21 | class RawOpenBCI(_BaseRaw): 22 | """Raw object from OpenBCI file 23 | 24 | Parameters 25 | ---------- 26 | input_fname : str 27 | Path to the OpenBCI file. 28 | montage : str | None | instance of Montage 29 | Path or instance of montage containing electrode positions. 30 | If None, sensor locations are (0,0,0). See the documentation of 31 | :func:`mne.channels.read_montage` for more information. 32 | eog : list or tuple 33 | Names of channels or list of indices that should be designated 34 | EOG channels. Default is None. 35 | misc : list or tuple 36 | List of indices that should be designated MISC channels. 37 | Default is (-3, -2, -1), which are the accelerator sensors. 38 | stim_channel : int | None 39 | The channel index (starting at 0). 40 | If None (default), there will be no stim channel added. 41 | scale : float 42 | The scaling factor for EEG data. Units for MNE are in volts. 43 | OpenBCI data are typically stored in microvolts. Default scale 44 | factor is 1e-6. 45 | sfreq : int 46 | The sampling frequency of the data. OpenBCI defaults are 250 Hz. 47 | missing_tol : int 48 | The tolerance for interpolating missing samples. Default is 1. If the 49 | number of contiguous missing samples is greater than tolerance, then 50 | values are marked as NaN. 51 | preload : bool 52 | If True, all data are loaded at initialization. 53 | If False, data are not read until save. 54 | verbose : bool, str, int, or None 55 | If not None, override default verbose level (see mne.verbose). 56 | 57 | 58 | See Also 59 | -------- 60 | mne.io.Raw : Documentation of attribute and methods. 61 | """ 62 | @verbose 63 | def __init__(self, input_fname, montage=None, eog=None, 64 | misc=(-3, -2, -1), stim_channel=None, scale=1e-6, sfreq=250, 65 | missing_tol=1, preload=True, verbose=None): 66 | 67 | bci_info = {'missing_tol': missing_tol, 'stim_channel': stim_channel} 68 | if not eog: 69 | eog = list() 70 | if not misc: 71 | misc = list() 72 | nsamps, nchan = self._get_data_dims(input_fname) 73 | 74 | last_samps = [nsamps - 1] 75 | ch_names = ['EEG %03d' % num for num in range(1, nchan + 1)] 76 | ch_types = ['eeg'] * nchan 77 | if misc: 78 | misc_names = ['MISC %03d' % ii for ii in range(1, len(misc) + 1)] 79 | misc_types = ['misc'] * len(misc) 80 | for ii, mi in enumerate(misc): 81 | ch_names[mi] = misc_names[ii] 82 | ch_types[mi] = misc_types[ii] 83 | if eog: 84 | eog_names = ['EOG %03d' % ii for ii in range(len(eog))] 85 | eog_types = ['eog'] * len(eog) 86 | for ii, ei in enumerate(eog): 87 | ch_names[ei] = eog_names[ii] 88 | ch_types[ei] = eog_types[ii] 89 | if stim_channel: 90 | ch_names[stim_channel] = 'STI 014' 91 | ch_types[stim_channel] = 'stim' 92 | 93 | # fix it for eog and misc marking 94 | info = create_info(ch_names, sfreq, ch_types, montage) 95 | super(RawOpenBCI, self).__init__(info, last_samps=last_samps, 96 | raw_extras=[bci_info], 97 | filenames=[input_fname], 98 | preload=False, verbose=verbose) 99 | # load data 100 | if preload: 101 | self.preload = preload 102 | logger.info('Reading raw data from %s...' % input_fname) 103 | self._data, _ = self._read_segment() 104 | 105 | def _read_segment_file(self, data, idx, offset, fi, start, stop, 106 | cals, mult): 107 | """Read a chunk of raw data""" 108 | input_fname = self._filenames[fi] 109 | data_ = np.genfromtxt(input_fname, delimiter=',', comments='%', 110 | skip_footer=1) 111 | """ 112 | Dealing with the missing data 113 | ----------------------------- 114 | When recording with OpenBCI over Bluetooth, it is possible for some of 115 | the data packets, samples, to not be recorded. This does not happen 116 | often but it poses a problem for maintaining proper sampling periods. 117 | OpenBCI data format combats this by providing a counter on the sample 118 | to know which ones are missing. 119 | 120 | Solution 121 | -------- 122 | Interpolate the missing samples by resampling the surrounding samples. 123 | 1. Find where the missing samples are. 124 | 2. Deal with the counter reset (resets after cycling a byte). 125 | 3. Resample given the diffs. 126 | 4. Insert resampled data in the array using the diff indices 127 | (index + 1). 128 | 5. If number of missing samples is greater than the missing_tol, Values 129 | are replaced with np.nan. 130 | """ 131 | # counter goes from 0 to 255, maxdiff is 255. 132 | # make diff one like others. 133 | missing_tol = self._raw_extras[fi]['missing_tol'] 134 | diff = np.abs(np.diff(data_[:, 0])) 135 | diff = np.mod(diff, 254) - 1 136 | missing_idx = np.where(diff != 0)[0] 137 | missing_samps = diff[missing_idx].astype(int) 138 | 139 | if missing_samps.size: 140 | missing_nsamps = np.sum(missing_samps, dtype=int) 141 | missing_cumsum = np.insert(np.cumsum(missing_samps), 0, 0)[:-1] 142 | missing_data = np.empty((missing_nsamps, data_.shape[-1]), 143 | dtype=float) 144 | insert_idx = list() 145 | for idx_, nn, ii in zip(missing_idx, missing_samps, 146 | missing_cumsum): 147 | missing_data[ii:ii + nn] = np.mean(data_[(idx_, idx_ + 1), :]) 148 | if nn > missing_tol: 149 | missing_data[ii:ii + nn] *= np.nan 150 | warnings.warn('The number of missing samples exceeded the ' 151 | 'missing_tol threshold.') 152 | insert_idx.append([idx_] * nn) 153 | insert_idx = np.hstack(insert_idx) 154 | data_ = np.insert(data_, insert_idx, missing_data, axis=0) 155 | # data_ dimensions are samples by channels. transpose for MNE. 156 | data_ = data_[start:stop, 1:].T 157 | data[:, offset:offset + stop - start] = \ 158 | np.dot(mult, data_[idx]) if mult is not None else data_[idx] 159 | 160 | def _get_data_dims(self, input_fname): 161 | """Briefly scan the data file for info""" 162 | # raw data formatting is nsamps by nchans + counter 163 | data = np.genfromtxt(input_fname, delimiter=',', comments='%', 164 | skip_footer=1) 165 | diff = np.abs(np.diff(data[:, 0])) 166 | diff = np.mod(diff, 254) - 1 167 | missing_idx = np.where(diff != 0)[0] 168 | missing_samps = diff[missing_idx].astype(int) 169 | nsamps, nchan = data.shape 170 | # add the missing samples 171 | nsamps += sum(missing_samps) 172 | # remove the tracker column 173 | nchan -= 1 174 | del data 175 | 176 | return nsamps, nchan 177 | 178 | 179 | def read_raw_openbci(input_fname, montage=None, eog=None, misc=(-3, -2, -1), 180 | stim_channel=None, scale=1e-6, sfreq=250, missing_tol=1, 181 | preload=True, verbose=None): 182 | """Raw object from OpenBCI file 183 | 184 | Parameters 185 | ---------- 186 | input_fname : str 187 | Path to the OpenBCI file. 188 | montage : str | None | instance of Montage 189 | Path or instance of montage containing electrode positions. 190 | If None, sensor locations are (0,0,0). See the documentation of 191 | :func:`mne.channels.read_montage` for more information. 192 | eog : list or tuple 193 | Names of channels or list of indices that should be designated 194 | EOG channels. Default is None. 195 | misc : list or tuple 196 | List of indices that should be designated MISC channels. 197 | Default is (-3, -2, -1), which are the accelerator sensors. 198 | stim_channel : str | int | None 199 | The channel name or channel index (starting at 0). 200 | -1 corresponds to the last channel (default). 201 | If None, there will be no stim channel added. 202 | scale : float 203 | The scaling factor for EEG data. Units for MNE are in volts. 204 | OpenBCI data are typically stored in microvolts. Default scale 205 | factor is 1e-6. 206 | sfreq : int 207 | The sampling frequency of the data. OpenBCI defaults are 250 Hz. 208 | missing_tol : int 209 | The tolerance for interpolating missing samples. Default is 1. If the 210 | number of contiguous missing samples is greater than tolerance, then 211 | values are marked as NaN. 212 | preload : bool 213 | If True, all data are loaded at initialization. 214 | If False, data are not read until save. 215 | verbose : bool, str, int, or None 216 | If not None, override default verbose level (see mne.verbose). 217 | 218 | Returns 219 | ------- 220 | raw : Instance of RawOpenBCI 221 | A Raw object containing OpenBCI data. 222 | 223 | 224 | See Also 225 | -------- 226 | mne.io.Raw : Documentation of attribute and methods. 227 | """ 228 | raw = RawOpenBCI(input_fname=input_fname, montage=montage, eog=eog, 229 | misc=misc, stim_channel=stim_channel, scale=scale, 230 | sfreq=sfreq, missing_tol=missing_tol, preload=preload, 231 | verbose=verbose) 232 | return raw 233 | -------------------------------------------------------------------------------- /open_bci_ganglion.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core OpenBCI object for handling connections and samples from the gnaglion board. 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 | board = OpenBCIBoard() 12 | board.start(handle_sample) 13 | 14 | TODO: support impedance 15 | TODO: reset board with 'v'? 16 | """ 17 | import struct 18 | import time 19 | import timeit 20 | import atexit 21 | import logging 22 | import numpy as np 23 | import sys 24 | import pdb 25 | import glob 26 | # local bluepy should take precedence 27 | import sys 28 | sys.path.insert(0,"bluepy/bluepy") 29 | from btle import Scanner, DefaultDelegate, Peripheral 30 | 31 | SAMPLE_RATE = 200.0 # Hz 32 | scale_fac_uVolts_per_count = 1200 / (8388607.0 * 1.5 * 51.0) 33 | scale_fac_accel_G_per_count = 0.000032 34 | 35 | # service for communication, as per docs 36 | BLE_SERVICE = "fe84" 37 | # characteristics of interest 38 | BLE_CHAR_RECEIVE = "2d30c082f39f4ce6923f3484ea480596" 39 | BLE_CHAR_SEND = "2d30c083f39f4ce6923f3484ea480596" 40 | BLE_CHAR_DISCONNECT = "2d30c084f39f4ce6923f3484ea480596" 41 | 42 | ''' 43 | #Commands for in SDK http://docs.openbci.com/Hardware/08-Ganglion_Data_Forma 44 | 45 | command_stop = "s"; 46 | command_startBinary = "b"; 47 | ''' 48 | 49 | class OpenBCIBoard(object): 50 | """ 51 | Handle a connection to an OpenBCI board. 52 | 53 | Args: 54 | port: MAC address of the Ganglion Board. "None" to attempt auto-detect. 55 | aux: enable on not aux channels (i.e. switch to 18bit mode if set) 56 | impedance: measures impedance when start streaming 57 | timeout: in seconds, if set will try to disconnect / reconnect after a period without new data -- should be high if impedance check 58 | max_packets_to_skip: will try to disconnect / reconnect after too many packets are skipped 59 | baud, filter_data, daisy: Not used, for compatibility with v3 60 | """ 61 | 62 | def __init__(self, port=None, baud=0, filter_data=False, 63 | scaled_output=True, daisy=False, log=True, aux=False, impedance=False, timeout=2, max_packets_to_skip=20): 64 | # unused, for compatibility with Cyton v3 API 65 | self.daisy = False 66 | # these one are used 67 | self.log = log # print_incoming_text needs log 68 | self.aux = aux 69 | self.streaming = False 70 | self.timeout = timeout 71 | self.max_packets_to_skip = max_packets_to_skip 72 | self.scaling_output = scaled_output 73 | self.impedance = False 74 | 75 | # might be handy to know API 76 | self.board_type = "ganglion" 77 | 78 | print("Looking for Ganglion board") 79 | if port == None: 80 | port = self.find_port() 81 | self.port = port # find_port might not return string 82 | 83 | self.connect() 84 | 85 | self.streaming = False 86 | # number of EEG channels and (optionally) accelerometer channel 87 | self.eeg_channels_per_sample = 4 88 | self.aux_channels_per_sample = 3 89 | self.imp_channels_per_sample = 5 90 | self.read_state = 0 91 | self.log_packet_count = 0 92 | self.packets_dropped = 0 93 | self.time_last_packet = 0 94 | 95 | # Disconnects from board when terminated 96 | atexit.register(self.disconnect) 97 | 98 | def getBoardType(self): 99 | """ Returns the version of the board """ 100 | return self.board_type 101 | 102 | def setImpedance(self, flag): 103 | """ Enable/disable impedance measure """ 104 | self.impedance = bool(flag) 105 | 106 | def connect(self): 107 | """ Connect to the board and configure it. Note: recreates various objects upon call. """ 108 | print ("Init BLE connection with MAC: " + self.port) 109 | print ("NB: if it fails, try with root privileges.") 110 | self.gang = Peripheral(self.port, 'random') # ADDR_TYPE_RANDOM 111 | 112 | print ("Get mainservice...") 113 | self.service = self.gang.getServiceByUUID(BLE_SERVICE) 114 | print ("Got:" + str(self.service)) 115 | 116 | print ("Get characteristics...") 117 | self.char_read = self.service.getCharacteristics(BLE_CHAR_RECEIVE)[0] 118 | print ("receive, properties: " + str(self.char_read.propertiesToString()) + ", supports read: " + str(self.char_read.supportsRead())) 119 | 120 | self.char_write = self.service.getCharacteristics(BLE_CHAR_SEND)[0] 121 | print ("write, properties: " + str(self.char_write.propertiesToString()) + ", supports read: " + str(self.char_write.supportsRead())) 122 | 123 | self.char_discon = self.service.getCharacteristics(BLE_CHAR_DISCONNECT)[0] 124 | print ("disconnect, properties: " + str(self.char_discon.propertiesToString()) + ", supports read: " + str(self.char_discon.supportsRead())) 125 | 126 | # set delegate to handle incoming data 127 | self.delegate = GanglionDelegate(self.scaling_output) 128 | self.gang.setDelegate(self.delegate) 129 | 130 | # enable AUX channel 131 | if self.aux: 132 | print("Enabling AUX data...") 133 | try: 134 | self.ser_write(b'n') 135 | except Exception as e: 136 | print("Something went wrong while enabling aux channels: " + str(e)) 137 | 138 | print("Turn on notifications") 139 | # nead up-to-date bluepy, cf https://github.com/IanHarvey/bluepy/issues/53 140 | self.desc_notify = self.char_read.getDescriptors(forUUID=0x2902)[0] 141 | try: 142 | self.desc_notify.write(b"\x01") 143 | except Exception as e: 144 | print("Something went wrong while trying to enable notification: " + str(e)) 145 | 146 | print("Connection established") 147 | 148 | def init_streaming(self): 149 | """ Tell the board to record like crazy. """ 150 | try: 151 | if self.impedance: 152 | print("Starting with impedance testing") 153 | self.ser_write(b'z') 154 | else: 155 | self.ser_write(b'b') 156 | except Exception as e: 157 | print("Something went wrong while asking the board to start streaming: " + str(e)) 158 | self.streaming = True 159 | self.packets_dropped = 0 160 | self.time_last_packet = timeit.default_timer() 161 | 162 | def find_port(self): 163 | """Detects Ganglion board MAC address -- if more than 1 around, will select first. Needs root privilege.""" 164 | 165 | print("Try to detect Ganglion MAC address. NB: Turn on bluetooth and run as root for this to work! Might not work with every BLE dongles.") 166 | scan_time = 5 167 | print("Scanning for 5 seconds nearby devices...") 168 | 169 | # From bluepy example 170 | class ScanDelegate(DefaultDelegate): 171 | def __init__(self): 172 | DefaultDelegate.__init__(self) 173 | 174 | def handleDiscovery(self, dev, isNewDev, isNewData): 175 | if isNewDev: 176 | print ("Discovered device: " + dev.addr) 177 | elif isNewData: 178 | print ("Received new data from: " + dev.addr) 179 | 180 | scanner = Scanner().withDelegate(ScanDelegate()) 181 | devices = scanner.scan(scan_time) 182 | 183 | nb_devices = len(devices) 184 | if nb_devices < 1: 185 | print("No BLE devices found. Check connectivity.") 186 | return "" 187 | else: 188 | print("Found " + str(nb_devices) + ", detecting Ganglion") 189 | list_mac = [] 190 | list_id = [] 191 | 192 | for dev in devices: 193 | # "Ganglion" should appear inside the "value" associated to "Complete Local Name", e.g. "Ganglion-b2a6" 194 | for (adtype, desc, value) in dev.getScanData(): 195 | if desc == "Complete Local Name" and value.startswith("Ganglion"): 196 | list_mac.append(dev.addr) 197 | list_id.append(value) 198 | print("Got Ganglion: " + value + ", with MAC: " + dev.addr) 199 | break 200 | nb_ganglions = len(list_mac) 201 | 202 | if nb_ganglions < 1: 203 | print("No Ganglion found ;(") 204 | raise OSError('Cannot find OpenBCI Ganglion MAC address') 205 | 206 | if nb_ganglions > 1: 207 | print("Found " + str(nb_ganglions) + ", selecting first") 208 | 209 | print("Selecting MAC address " + list_mac[0] + " for " + list_id[0]) 210 | return list_mac[0] 211 | 212 | def ser_write(self, b): 213 | """Access serial port object for write""" 214 | self.char_write.write(b) 215 | 216 | def ser_read(self): 217 | """Access serial port object for read""" 218 | return self.char_read.read() 219 | 220 | def ser_inWaiting(self): 221 | """ Slightly different from Cyton API, return True if ASCII messages are incoming.""" 222 | # FIXME: might have a slight problem with thread because of notifications... 223 | if self.delegate.receiving_ASCII: 224 | # in case the packet indicating the end of the message drops, we use a 1s timeout 225 | if timeit.default_timer() - self.delegate.time_last_ASCII > 2: 226 | self.delegate.receiving_ASCII = False 227 | return self.delegate.receiving_ASCII 228 | 229 | def getSampleRate(self): 230 | return SAMPLE_RATE 231 | 232 | def getNbEEGChannels(self): 233 | """Will not get new data on impedance check.""" 234 | return self.eeg_channels_per_sample 235 | 236 | def getNbAUXChannels(self): 237 | """Might not be used depending on the mode.""" 238 | return self.aux_channels_per_sample 239 | 240 | def getNbImpChannels(self): 241 | """Might not be used depending on the mode.""" 242 | return self.imp_channels_per_sample 243 | 244 | def start_streaming(self, callback, lapse=-1): 245 | """ 246 | Start handling streaming data from the board. Call a provided callback 247 | for every single sample that is processed 248 | 249 | Args: 250 | callback: A callback function -- or a list of functions -- that will receive a single argument of the 251 | OpenBCISample object captured. 252 | """ 253 | if not self.streaming: 254 | self.init_streaming() 255 | 256 | start_time = timeit.default_timer() 257 | 258 | # Enclose callback funtion in a list if it comes alone 259 | if not isinstance(callback, list): 260 | callback = [callback] 261 | 262 | while self.streaming: 263 | # should the board get disconnected and we could not wait for notification anymore, a reco should be attempted through timeout mechanism 264 | try: 265 | # at most we will get one sample per packet 266 | self.waitForNotifications(1./self.getSampleRate()) 267 | except Exception as e: 268 | print("Something went wrong while waiting for a new sample: " + str(e)) 269 | # retrieve current samples on the stack 270 | samples = self.delegate.getSamples() 271 | self.packets_dropped = self.delegate.getMaxPacketsDropped() 272 | if samples: 273 | self.time_last_packet = timeit.default_timer() 274 | for call in callback: 275 | for sample in samples: 276 | call(sample) 277 | 278 | if(lapse > 0 and timeit.default_timer() - start_time > lapse): 279 | self.stop(); 280 | if self.log: 281 | self.log_packet_count = self.log_packet_count + 1; 282 | 283 | # Checking connection -- timeout and packets dropped 284 | self.check_connection() 285 | 286 | def waitForNotifications(self, delay): 287 | """ Allow some time for the board to receive new data. """ 288 | self.gang.waitForNotifications(delay) 289 | 290 | 291 | def test_signal(self, signal): 292 | """ Enable / disable test signal """ 293 | if signal == 0: 294 | self.warn("Disabling synthetic square wave") 295 | try: 296 | self.char_write.write(b']') 297 | except Exception as e: 298 | print("Something went wrong while setting signal: " + str(e)) 299 | elif signal == 1: 300 | self.warn("Eisabling synthetic square wave") 301 | try: 302 | self.char_write.write(b'[') 303 | except Exception as e: 304 | print("Something went wrong while setting signal: " + str(e)) 305 | else: 306 | self.warn("%s is not a known test signal. Valid signal is 0-1" %(signal)) 307 | 308 | def set_channel(self, channel, toggle_position): 309 | """ Enable / disable channels """ 310 | try: 311 | #Commands to set toggle to on position 312 | if toggle_position == 1: 313 | if channel is 1: 314 | self.ser.write(b'!') 315 | if channel is 2: 316 | self.ser.write(b'@') 317 | if channel is 3: 318 | self.ser.write(b'#') 319 | if channel is 4: 320 | self.ser.write(b'$') 321 | #Commands to set toggle to off position 322 | elif toggle_position == 0: 323 | if channel is 1: 324 | self.ser.write(b'1') 325 | if channel is 2: 326 | self.ser.write(b'2') 327 | if channel is 3: 328 | self.ser.write(b'3') 329 | if channel is 4: 330 | self.ser.write(b'4') 331 | except Exception as e: 332 | print("Something went wrong while setting channels: " + str(e)) 333 | 334 | """ 335 | 336 | Clean Up (atexit) 337 | 338 | """ 339 | def stop(self): 340 | print("Stopping streaming...") 341 | self.streaming = False 342 | # connection might be already down here 343 | try: 344 | if self.impedance: 345 | print("Stopping with impedance testing") 346 | self.ser_write(b'Z') 347 | else: 348 | self.ser_write(b's') 349 | except Exception as e: 350 | print("Something went wrong while asking the board to stop streaming: " + str(e)) 351 | if self.log: 352 | logging.warning('sent : stopped streaming') 353 | 354 | def disconnect(self): 355 | if(self.streaming == True): 356 | self.stop() 357 | print("Closing BLE..") 358 | try: 359 | self.char_discon.write(b' ') 360 | except Exception as e: 361 | print("Something went wrong while asking the board to disconnect: " + str(e)) 362 | # should not try to read/write anything after that, will crash 363 | try: 364 | self.gang.disconnect() 365 | except Exception as e: 366 | print("Something went wrong while shutting down BLE link: " + str(e)) 367 | logging.warning('BLE closed') 368 | 369 | 370 | """ 371 | 372 | SETTINGS AND HELPERS 373 | 374 | """ 375 | def warn(self, text): 376 | if self.log: 377 | #log how many packets where sent succesfully in between warnings 378 | if self.log_packet_count: 379 | logging.info('Data packets received:'+str(self.log_packet_count)) 380 | self.log_packet_count = 0; 381 | logging.warning(text) 382 | print("Warning: %s" % text) 383 | 384 | def check_connection(self): 385 | """ Check connection quality in term of lag and number of packets drop. Reinit connection if necessary. FIXME: parameters given to the board will be lost.""" 386 | # stop checking when we're no longer streaming 387 | if not self.streaming: 388 | return 389 | #check number of dropped packets and duration without new packets, deco/reco if too large 390 | if self.packets_dropped > self.max_packets_to_skip: 391 | self.warn("Too many packets dropped, attempt to reconnect") 392 | self.reconnect() 393 | elif self.timeout > 0 and timeit.default_timer() - self.time_last_packet > self.timeout: 394 | self.warn("Too long since got new data, attempt to reconnect") 395 | #if error, attempt to reconect 396 | self.reconnect() 397 | 398 | def reconnect(self): 399 | """ In case of poor connection, will shut down and relaunch everything. FIXME: parameters given to the board will be lost.""" 400 | self.warn('Reconnecting') 401 | self.stop() 402 | self.disconnect() 403 | self.connect() 404 | self.init_streaming() 405 | 406 | class OpenBCISample(object): 407 | """Object encapulsating a single sample from the OpenBCI board.""" 408 | def __init__(self, packet_id, channel_data, aux_data, imp_data): 409 | self.id = packet_id 410 | self.channel_data = channel_data 411 | self.aux_data = aux_data 412 | self.imp_data = imp_data 413 | 414 | class GanglionDelegate(DefaultDelegate): 415 | """ Called by bluepy (handling BLE connection) when new data arrive, parses samples. """ 416 | def __init__(self, scaling_output = True): 417 | DefaultDelegate.__init__(self) 418 | # holds samples until OpenBCIBoard claims them 419 | self.samples = [] 420 | # detect gaps between packets 421 | self.last_id = -1 422 | self.packets_dropped = 0 423 | # save uncompressed data to compute deltas 424 | self.lastChannelData = [0, 0, 0, 0] 425 | # 18bit data got here and then accelerometer with it 426 | self.lastAcceleromoter = [0, 0, 0] 427 | # when the board is manually set in the right mode (z to start, Z to stop), impedance will be measured. 4 channels + ref 428 | self.lastImpedance = [0, 0, 0, 0, 0] 429 | self.scaling_output = scaling_output 430 | # handling incoming ASCII messages 431 | self.receiving_ASCII = False 432 | self.time_last_ASCII = timeit.default_timer() 433 | 434 | def handleNotification(self, cHandle, data): 435 | if len(data) < 1: 436 | print('Warning: a packet should at least hold one byte...') 437 | return 438 | self.parse(data) 439 | 440 | """ 441 | PARSER: 442 | Parses incoming data packet into OpenBCISample -- see docs. Will call the corresponding parse* function depending on the format of the packet. 443 | """ 444 | def parse(self, packet): 445 | # bluepy returnds INT with python3 and STR with python2 446 | if type(packet) is str: 447 | # convert a list of strings in bytes 448 | unpac = struct.unpack(str(len(packet)) + 'B', "".join(packet)) 449 | else: 450 | unpac = packet 451 | 452 | start_byte = unpac[0] 453 | 454 | # Give the informative part of the packet to proper handler -- split between ID and data bytes 455 | # Raw uncompressed 456 | if start_byte == 0: 457 | self.receiving_ASCII = False 458 | self.parseRaw(start_byte, unpac[1:]) 459 | # 18-bit compression with Accelerometer 460 | elif start_byte >= 1 and start_byte <= 100: 461 | self.receiving_ASCII = False 462 | self.parse18bit(start_byte, unpac[1:]) 463 | # 19-bit compression without Accelerometer 464 | elif start_byte >=101 and start_byte <= 200: 465 | self.receiving_ASCII = False 466 | self.parse19bit(start_byte-100, unpac[1:]) 467 | # Impedance Channel 468 | elif start_byte >= 201 and start_byte <= 205: 469 | self.receiving_ASCII = False 470 | self.parseImpedance(start_byte, packet[1:]) 471 | # Part of ASCII -- TODO: better formatting of incoming ASCII 472 | elif start_byte == 206: 473 | print("%\t" + str(packet[1:])) 474 | self.receiving_ASCII = True 475 | self.time_last_ASCII = timeit.default_timer() 476 | 477 | # End of ASCII message 478 | elif start_byte == 207: 479 | print("%\t" + str(packet[1:])) 480 | print ("$$$") 481 | self.receiving_ASCII = False 482 | else: 483 | print("Warning: unknown type of packet: " + str(start_byte)) 484 | 485 | def parseRaw(self, packet_id, packet): 486 | """ Dealing with "Raw uncompressed" """ 487 | if len(packet) != 19: 488 | print('Wrong size, for raw data' + str(len(data)) + ' instead of 19 bytes') 489 | return 490 | 491 | chan_data = [] 492 | # 4 channels of 24bits, take values one by one 493 | for i in range(0,12,3): 494 | chan_data.append(conv24bitsToInt(packet[i:i+3])) 495 | # save uncompressed raw channel for future use and append whole sample 496 | self.pushSample(packet_id, chan_data, self.lastAcceleromoter, self.lastImpedance) 497 | self.lastChannelData = chan_data 498 | self.updatePacketsCount(packet_id) 499 | 500 | def parse19bit(self, packet_id, packet): 501 | """ Dealing with "19-bit compression without Accelerometer" """ 502 | if len(packet) != 19: 503 | print('Wrong size, for 19-bit compression data' + str(len(data)) + ' instead of 19 bytes') 504 | return 505 | 506 | # should get 2 by 4 arrays of uncompressed data 507 | deltas = decompressDeltas19Bit(packet) 508 | # the sample_id will be shifted 509 | delta_id = 1 510 | for delta in deltas: 511 | # convert from packet to sample id 512 | sample_id = (packet_id - 1) * 2 + delta_id 513 | # 19bit packets hold deltas between two samples 514 | # TODO: use more broadly numpy 515 | full_data = list(np.array(self.lastChannelData) - np.array(delta)) 516 | # NB: aux data updated only in 18bit mode, send values here only to be consistent 517 | self.pushSample(sample_id, full_data, self.lastAcceleromoter, self.lastImpedance) 518 | self.lastChannelData = full_data 519 | delta_id += 1 520 | self.updatePacketsCount(packet_id) 521 | 522 | 523 | def parse18bit(self, packet_id, packet): 524 | """ Dealing with "18-bit compression without Accelerometer" """ 525 | if len(packet) != 19: 526 | print('Wrong size, for 18-bit compression data' + str(len(data)) + ' instead of 19 bytes') 527 | return 528 | 529 | # accelerometer X 530 | if packet_id % 10 == 1: 531 | self.lastAcceleromoter[0] = conv8bitToInt8(packet[18]) 532 | # accelerometer Y 533 | elif packet_id % 10 == 2: 534 | self.lastAcceleromoter[1] = conv8bitToInt8(packet[18]) 535 | # accelerometer Z 536 | elif packet_id % 10 == 3: 537 | self.lastAcceleromoter[2] = conv8bitToInt8(packet[18]) 538 | 539 | # deltas: should get 2 by 4 arrays of uncompressed data 540 | deltas = decompressDeltas18Bit(packet[:-1]) 541 | # the sample_id will be shifted 542 | delta_id = 1 543 | for delta in deltas: 544 | # convert from packet to sample id 545 | sample_id = (packet_id - 1) * 2 + delta_id 546 | # 19bit packets hold deltas between two samples 547 | # TODO: use more broadly numpy 548 | full_data = list(np.array(self.lastChannelData) - np.array(delta)) 549 | self.pushSample(sample_id, full_data, self.lastAcceleromoter, self.lastImpedance) 550 | self.lastChannelData = full_data 551 | delta_id += 1 552 | self.updatePacketsCount(packet_id) 553 | 554 | 555 | def parseImpedance(self, packet_id, packet): 556 | """ Dealing with impedance data. packet: ASCII data. NB: will take few packet (seconds) to fill""" 557 | if packet[-2:] != "Z\n": 558 | print('Wrong format for impedance check, should be ASCII ending with "Z\\n"') 559 | 560 | # convert from ASCII to actual value 561 | imp_value = int(packet[:-2]) 562 | # from 201 to 205 codes to the right array size 563 | self.lastImpedance[packet_id- 201] = imp_value 564 | self.pushSample(packet_id - 200, self.lastChannelData, self.lastAcceleromoter, self.lastImpedance) 565 | 566 | 567 | def pushSample(self, sample_id, chan_data, aux_data, imp_data): 568 | """ Add a sample to inner stack, setting ID and dealing with scaling if necessary. """ 569 | if self.scaling_output: 570 | chan_data = list(np.array(chan_data) * scale_fac_uVolts_per_count) 571 | aux_data = list(np.array(aux_data) * scale_fac_accel_G_per_count) 572 | sample = OpenBCISample(sample_id, chan_data, aux_data, imp_data) 573 | self.samples.append(sample) 574 | 575 | def updatePacketsCount(self, packet_id): 576 | """Update last packet ID and dropped packets""" 577 | if self.last_id == -1: 578 | self.last_id = packet_id 579 | self.packets_dropped = 0 580 | return 581 | # ID loops every 101 packets 582 | if packet_id > self.last_id: 583 | self.packets_dropped = packet_id - self.last_id - 1 584 | else: 585 | self.packets_dropped = packet_id + 101 - self.last_id - 1 586 | self.last_id = packet_id 587 | if self.packets_dropped > 0: 588 | print("Warning: dropped " + str(self.packets_dropped) + " packets.") 589 | 590 | def getSamples(self): 591 | """ Retrieve and remove from buffer last samples. """ 592 | unstack_samples = self.samples 593 | self.samples = [] 594 | return unstack_samples 595 | 596 | def getMaxPacketsDropped(self): 597 | """ While processing last samples, how many packets were dropped?""" 598 | # TODO: return max value of the last samples array? 599 | return self.packets_dropped 600 | 601 | 602 | 603 | """ 604 | DATA conversion, for the most part courtesy of OpenBCI_NodeJS_Ganglion 605 | 606 | """ 607 | 608 | def conv24bitsToInt(unpacked): 609 | """ Convert 24bit data coded on 3 bytes to a proper integer """ 610 | if len(unpacked) != 3: 611 | raise ValueError("Input should be 3 bytes long.") 612 | 613 | # FIXME: quick'n dirty, unpack wants strings later on 614 | literal_read = struct.pack('3B', unpacked[0], unpacked[1], unpacked[2]) 615 | 616 | #3byte int in 2s compliment 617 | if (unpacked[0] >= 127): 618 | pre_fix = bytes(bytearray.fromhex('FF')) 619 | else: 620 | pre_fix = bytes(bytearray.fromhex('00')) 621 | 622 | literal_read = pre_fix + literal_read; 623 | 624 | #unpack little endian(>) signed integer(i) (makes unpacking platform independent) 625 | myInt = struct.unpack('>i', literal_read)[0] 626 | 627 | return myInt 628 | 629 | def conv19bitToInt32(threeByteBuffer): 630 | """ Convert 19bit data coded on 3 bytes to a proper integer (LSB bit 1 used as sign). """ 631 | if len(threeByteBuffer) != 3: 632 | raise ValueError("Input should be 3 bytes long.") 633 | 634 | prefix = 0; 635 | 636 | # if LSB is 1, negative number, some hasty unsigned to signed conversion to do 637 | if threeByteBuffer[2] & 0x01 > 0: 638 | prefix = 0b1111111111111; 639 | return ((prefix << 19) | (threeByteBuffer[0] << 16) | (threeByteBuffer[1] << 8) | threeByteBuffer[2]) | ~0xFFFFFFFF 640 | else: 641 | return (prefix << 19) | (threeByteBuffer[0] << 16) | (threeByteBuffer[1] << 8) | threeByteBuffer[2] 642 | 643 | def conv18bitToInt32(threeByteBuffer): 644 | """ Convert 18bit data coded on 3 bytes to a proper integer (LSB bit 1 used as sign) """ 645 | if len(threeByteBuffer) != 3: 646 | raise Valuerror("Input should be 3 bytes long.") 647 | 648 | prefix = 0; 649 | 650 | # if LSB is 1, negative number, some hasty unsigned to signed conversion to do 651 | if threeByteBuffer[2] & 0x01 > 0: 652 | prefix = 0b11111111111111; 653 | return ((prefix << 18) | (threeByteBuffer[0] << 16) | (threeByteBuffer[1] << 8) | threeByteBuffer[2]) | ~0xFFFFFFFF 654 | else: 655 | return (prefix << 18) | (threeByteBuffer[0] << 16) | (threeByteBuffer[1] << 8) | threeByteBuffer[2] 656 | 657 | def conv8bitToInt8(byte): 658 | """ Convert one byte to signed value """ 659 | 660 | if byte > 127: 661 | return (256-byte) * (-1) 662 | else: 663 | return byte 664 | 665 | def decompressDeltas19Bit(buffer): 666 | """ 667 | Called to when a compressed packet is received. 668 | buffer: Just the data portion of the sample. So 19 bytes. 669 | return {Array} - An array of deltas of shape 2x4 (2 samples per packet and 4 channels per sample.) 670 | """ 671 | if len(buffer) != 19: 672 | raise ValueError("Input should be 19 bytes long.") 673 | 674 | receivedDeltas = [[0, 0, 0, 0],[0, 0, 0, 0]] 675 | 676 | # Sample 1 - Channel 1 677 | miniBuf = [ 678 | (buffer[0] >> 5), 679 | ((buffer[0] & 0x1F) << 3 & 0xFF) | (buffer[1] >> 5), 680 | ((buffer[1] & 0x1F) << 3 & 0xFF) | (buffer[2] >> 5) 681 | ] 682 | 683 | receivedDeltas[0][0] = conv19bitToInt32(miniBuf) 684 | 685 | # Sample 1 - Channel 2 686 | miniBuf = [ 687 | (buffer[2] & 0x1F) >> 2, 688 | (buffer[2] << 6 & 0xFF) | (buffer[3] >> 2), 689 | (buffer[3] << 6 & 0xFF) | (buffer[4] >> 2) 690 | ] 691 | receivedDeltas[0][1] = conv19bitToInt32(miniBuf) 692 | 693 | # Sample 1 - Channel 3 694 | miniBuf = [ 695 | ((buffer[4] & 0x03) << 1 & 0xFF) | (buffer[5] >> 7), 696 | ((buffer[5] & 0x7F) << 1 & 0xFF) | (buffer[6] >> 7), 697 | ((buffer[6] & 0x7F) << 1 & 0xFF) | (buffer[7] >> 7) 698 | ] 699 | receivedDeltas[0][2] = conv19bitToInt32(miniBuf) 700 | 701 | # Sample 1 - Channel 4 702 | miniBuf = [ 703 | ((buffer[7] & 0x7F) >> 4), 704 | ((buffer[7] & 0x0F) << 4 & 0xFF) | (buffer[8] >> 4), 705 | ((buffer[8] & 0x0F) << 4 & 0xFF) | (buffer[9] >> 4) 706 | ] 707 | receivedDeltas[0][3] = conv19bitToInt32(miniBuf) 708 | 709 | # Sample 2 - Channel 1 710 | miniBuf = [ 711 | ((buffer[9] & 0x0F) >> 1), 712 | (buffer[9] << 7 & 0xFF) | (buffer[10] >> 1), 713 | (buffer[10] << 7 & 0xFF) | (buffer[11] >> 1) 714 | ] 715 | receivedDeltas[1][0] = conv19bitToInt32(miniBuf) 716 | 717 | # Sample 2 - Channel 2 718 | miniBuf = [ 719 | ((buffer[11] & 0x01) << 2 & 0xFF) | (buffer[12] >> 6), 720 | (buffer[12] << 2 & 0xFF) | (buffer[13] >> 6), 721 | (buffer[13] << 2 & 0xFF) | (buffer[14] >> 6) 722 | ] 723 | receivedDeltas[1][1] = conv19bitToInt32(miniBuf) 724 | 725 | # Sample 2 - Channel 3 726 | miniBuf = [ 727 | ((buffer[14] & 0x38) >> 3), 728 | ((buffer[14] & 0x07) << 5 & 0xFF) | ((buffer[15] & 0xF8) >> 3), 729 | ((buffer[15] & 0x07) << 5 & 0xFF) | ((buffer[16] & 0xF8) >> 3) 730 | ] 731 | receivedDeltas[1][2] = conv19bitToInt32(miniBuf) 732 | 733 | # Sample 2 - Channel 4 734 | miniBuf = [(buffer[16] & 0x07), buffer[17], buffer[18]] 735 | receivedDeltas[1][3] = conv19bitToInt32(miniBuf) 736 | 737 | return receivedDeltas; 738 | 739 | def decompressDeltas18Bit(buffer): 740 | """ 741 | Called to when a compressed packet is received. 742 | buffer: Just the data portion of the sample. So 19 bytes. 743 | return {Array} - An array of deltas of shape 2x4 (2 samples per packet and 4 channels per sample.) 744 | """ 745 | if len(buffer) != 18: 746 | raise ValueError("Input should be 18 bytes long.") 747 | 748 | receivedDeltas = [[0, 0, 0, 0],[0, 0, 0, 0]] 749 | 750 | # Sample 1 - Channel 1 751 | miniBuf = [ 752 | (buffer[0] >> 6), 753 | ((buffer[0] & 0x3F) << 2 & 0xFF) | (buffer[1] >> 6), 754 | ((buffer[1] & 0x3F) << 2 & 0xFF) | (buffer[2] >> 6) 755 | ] 756 | receivedDeltas[0][0] = conv18bitToInt32(miniBuf); 757 | 758 | # Sample 1 - Channel 2 759 | miniBuf = [ 760 | (buffer[2] & 0x3F) >> 4, 761 | (buffer[2] << 4 & 0xFF) | (buffer[3] >> 4), 762 | (buffer[3] << 4 & 0xFF) | (buffer[4] >> 4) 763 | ] 764 | receivedDeltas[0][1] = conv18bitToInt32(miniBuf); 765 | 766 | # Sample 1 - Channel 3 767 | miniBuf = [ 768 | (buffer[4] & 0x0F) >> 2, 769 | (buffer[4] << 6 & 0xFF) | (buffer[5] >> 2), 770 | (buffer[5] << 6 & 0xFF) | (buffer[6] >> 2) 771 | ] 772 | receivedDeltas[0][2] = conv18bitToInt32(miniBuf); 773 | 774 | # Sample 1 - Channel 4 775 | miniBuf = [ 776 | (buffer[6] & 0x03), 777 | buffer[7], 778 | buffer[8] 779 | ] 780 | receivedDeltas[0][3] = conv18bitToInt32(miniBuf); 781 | 782 | # Sample 2 - Channel 1 783 | miniBuf = [ 784 | (buffer[9] >> 6), 785 | ((buffer[9] & 0x3F) << 2 & 0xFF) | (buffer[10] >> 6), 786 | ((buffer[10] & 0x3F) << 2 & 0xFF) | (buffer[11] >> 6) 787 | ] 788 | receivedDeltas[1][0] = conv18bitToInt32(miniBuf); 789 | 790 | # Sample 2 - Channel 2 791 | miniBuf = [ 792 | (buffer[11] & 0x3F) >> 4, 793 | (buffer[11] << 4 & 0xFF) | (buffer[12] >> 4), 794 | (buffer[12] << 4 & 0xFF) | (buffer[13] >> 4) 795 | ] 796 | receivedDeltas[1][1] = conv18bitToInt32(miniBuf); 797 | 798 | # Sample 2 - Channel 3 799 | miniBuf = [ 800 | (buffer[13] & 0x0F) >> 2, 801 | (buffer[13] << 6 & 0xFF) | (buffer[14] >> 2), 802 | (buffer[14] << 6 & 0xFF) | (buffer[15] >> 2) 803 | ] 804 | receivedDeltas[1][2] = conv18bitToInt32(miniBuf); 805 | 806 | # Sample 2 - Channel 4 807 | miniBuf = [ 808 | (buffer[15] & 0x03), 809 | buffer[16], 810 | buffer[17] 811 | ] 812 | receivedDeltas[1][3] = conv18bitToInt32(miniBuf); 813 | 814 | return receivedDeltas; 815 | -------------------------------------------------------------------------------- /open_bci_v3.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" will only contain even numbers. As a side effect, the sampling rate will be divided by 2. 14 | 15 | FIXME: at the moment we can just force daisy mode, do not check that the module is detected. 16 | TODO: enable impedance 17 | 18 | """ 19 | import serial 20 | import struct 21 | import numpy as np 22 | import time 23 | import timeit 24 | import atexit 25 | import logging 26 | import threading 27 | import sys 28 | import pdb 29 | import glob 30 | 31 | SAMPLE_RATE = 250.0 # Hz 32 | START_BYTE = 0xA0 # start of data packet 33 | END_BYTE = 0xC0 # end of data packet 34 | ADS1299_Vref = 4.5 #reference voltage for ADC in ADS1299. set by its hardware 35 | ADS1299_gain = 24.0 #assumed gain setting for ADS1299. set by its Arduino code 36 | scale_fac_uVolts_per_count = ADS1299_Vref/float((pow(2,23)-1))/ADS1299_gain*1000000. 37 | scale_fac_accel_G_per_count = 0.002 /(pow(2,4)) #assume set to +/4G, so 2 mG 38 | ''' 39 | #Commands for in SDK http://docs.openbci.com/software/01-Open BCI_SDK: 40 | 41 | command_stop = "s"; 42 | command_startText = "x"; 43 | command_startBinary = "b"; 44 | command_startBinary_wAux = "n"; 45 | command_startBinary_4chan = "v"; 46 | command_activateFilters = "F"; 47 | command_deactivateFilters = "g"; 48 | command_deactivate_channel = {"1", "2", "3", "4", "5", "6", "7", "8"}; 49 | command_activate_channel = {"q", "w", "e", "r", "t", "y", "u", "i"}; 50 | command_activate_leadoffP_channel = {"!", "@", "#", "$", "%", "^", "&", "*"}; //shift + 1-8 51 | command_deactivate_leadoffP_channel = {"Q", "W", "E", "R", "T", "Y", "U", "I"}; //letters (plus shift) right below 1-8 52 | command_activate_leadoffN_channel = {"A", "S", "D", "F", "G", "H", "J", "K"}; //letters (plus shift) below the letters below 1-8 53 | command_deactivate_leadoffN_channel = {"Z", "X", "C", "V", "B", "N", "M", "<"}; //letters (plus shift) below the letters below the letters below 1-8 54 | command_biasAuto = "`"; 55 | command_biasFixed = "~"; 56 | ''' 57 | 58 | class OpenBCIBoard(object): 59 | """ 60 | 61 | Handle a connection to an OpenBCI board. 62 | 63 | Args: 64 | port: The port to connect to. 65 | baud: The baud of the serial connection. 66 | daisy: Enable or disable daisy module and 16 chans readings 67 | aux, impedance: unused, for compatibility with ganglion API 68 | """ 69 | 70 | def __init__(self, port=None, baud=115200, filter_data=True, 71 | scaled_output=True, daisy=False, aux=False, impedance=False, log=True, timeout=None): 72 | self.log = log # print_incoming_text needs log 73 | self.streaming = False 74 | self.baudrate = baud 75 | self.timeout = timeout 76 | if not port: 77 | port = self.find_port() 78 | self.port = port 79 | # might be handy to know API 80 | self.board_type = "cyton" 81 | print("Connecting to V3 at port %s" %(port)) 82 | self.ser = serial.Serial(port= port, baudrate = baud, timeout=timeout) 83 | 84 | print("Serial established...") 85 | 86 | time.sleep(2) 87 | #Initialize 32-bit board, doesn't affect 8bit board 88 | self.ser.write(b'v'); 89 | 90 | #wait for device to be ready 91 | time.sleep(1) 92 | self.print_incoming_text() 93 | 94 | self.streaming = False 95 | self.filtering_data = filter_data 96 | self.scaling_output = scaled_output 97 | self.eeg_channels_per_sample = 8 # number of EEG channels per sample *from the board* 98 | self.aux_channels_per_sample = 3 # number of AUX channels per sample *from the board* 99 | self.imp_channels_per_sample = 0 # impedance check not supported at the moment 100 | self.read_state = 0 101 | self.daisy = daisy 102 | self.last_odd_sample = OpenBCISample(-1, [], []) # used for daisy 103 | self.log_packet_count = 0 104 | self.attempt_reconnect = False 105 | self.last_reconnect = 0 106 | self.reconnect_freq = 5 107 | self.packets_dropped = 0 108 | 109 | #Disconnects from board when terminated 110 | atexit.register(self.disconnect) 111 | 112 | def getBoardType(self): 113 | """ Returns the version of the board """ 114 | return self.board_type 115 | 116 | def setImpedance(self, flag): 117 | """ Enable/disable impedance measure. Not implemented at the moment on Cyton. """ 118 | return 119 | 120 | def ser_write(self, b): 121 | """Access serial port object for write""" 122 | self.ser.write(b) 123 | 124 | def ser_read(self): 125 | """Access serial port object for read""" 126 | return self.ser.read() 127 | 128 | def ser_inWaiting(self): 129 | """Access serial port object for inWaiting""" 130 | return self.ser.inWaiting(); 131 | 132 | def getSampleRate(self): 133 | if self.daisy: 134 | return SAMPLE_RATE/2 135 | else: 136 | return SAMPLE_RATE 137 | 138 | def getNbEEGChannels(self): 139 | if self.daisy: 140 | return self.eeg_channels_per_sample*2 141 | else: 142 | return self.eeg_channels_per_sample 143 | 144 | def getNbAUXChannels(self): 145 | return self.aux_channels_per_sample 146 | 147 | def getNbImpChannels(self): 148 | return self.imp_channels_per_sample 149 | 150 | def start_streaming(self, callback, lapse=-1): 151 | """ 152 | Start handling streaming data from the board. Call a provided callback 153 | for every single sample that is processed (every two samples with daisy module). 154 | 155 | Args: 156 | callback: A callback function -- or a list of functions -- that will receive a single argument of the 157 | OpenBCISample object captured. 158 | """ 159 | if not self.streaming: 160 | self.ser.write(b'b') 161 | self.streaming = True 162 | 163 | start_time = timeit.default_timer() 164 | 165 | # Enclose callback funtion in a list if it comes alone 166 | if not isinstance(callback, list): 167 | callback = [callback] 168 | 169 | 170 | #Initialize check connection 171 | self.check_connection() 172 | 173 | while self.streaming: 174 | 175 | # read current sample 176 | sample = self._read_serial_binary() 177 | # if a daisy module is attached, wait to concatenate two samples (main board + daisy) before passing it to callback 178 | if self.daisy: 179 | # odd sample: daisy sample, save for later 180 | if ~sample.id % 2: 181 | self.last_odd_sample = sample 182 | # even sample: concatenate and send if last sample was the fist part, otherwise drop the packet 183 | elif sample.id - 1 == self.last_odd_sample.id: 184 | # the aux data will be the average between the two samples, as the channel samples themselves have been averaged by the board 185 | avg_aux_data = list((np.array(sample.aux_data) + np.array(self.last_odd_sample.aux_data))/2) 186 | whole_sample = OpenBCISample(sample.id, sample.channel_data + self.last_odd_sample.channel_data, avg_aux_data) 187 | for call in callback: 188 | call(whole_sample) 189 | else: 190 | for call in callback: 191 | call(sample) 192 | 193 | if(lapse > 0 and timeit.default_timer() - start_time > lapse): 194 | self.stop(); 195 | if self.log: 196 | self.log_packet_count = self.log_packet_count + 1; 197 | 198 | 199 | """ 200 | PARSER: 201 | Parses incoming data packet into OpenBCISample. 202 | Incoming Packet Structure: 203 | Start Byte(1)|Sample ID(1)|Channel Data(24)|Aux Data(6)|End Byte(1) 204 | 0xA0|0-255|8, 3-byte signed ints|3 2-byte signed ints|0xC0 205 | 206 | """ 207 | def _read_serial_binary(self, max_bytes_to_skip=3000): 208 | def read(n): 209 | bb = self.ser.read(n) 210 | if not bb: 211 | self.warn('Device appears to be stalled. Quitting...') 212 | sys.exit() 213 | raise Exception('Device Stalled') 214 | sys.exit() 215 | return '\xFF' 216 | else: 217 | return bb 218 | 219 | for rep in range(max_bytes_to_skip): 220 | 221 | #---------Start Byte & ID--------- 222 | if self.read_state == 0: 223 | 224 | b = read(1) 225 | 226 | if struct.unpack('B', b)[0] == START_BYTE: 227 | if(rep != 0): 228 | self.warn('Skipped %d bytes before start found' %(rep)) 229 | rep = 0; 230 | packet_id = struct.unpack('B', read(1))[0] #packet id goes from 0-255 231 | log_bytes_in = str(packet_id); 232 | 233 | self.read_state = 1 234 | 235 | #---------Channel Data--------- 236 | elif self.read_state == 1: 237 | channel_data = [] 238 | for c in range(self.eeg_channels_per_sample): 239 | 240 | #3 byte ints 241 | literal_read = read(3) 242 | 243 | unpacked = struct.unpack('3B', literal_read) 244 | log_bytes_in = log_bytes_in + '|' + str(literal_read); 245 | 246 | #3byte int in 2s compliment 247 | if (unpacked[0] >= 127): 248 | pre_fix = bytes(bytearray.fromhex('FF')) 249 | else: 250 | pre_fix = bytes(bytearray.fromhex('00')) 251 | 252 | 253 | literal_read = pre_fix + literal_read; 254 | 255 | #unpack little endian(>) signed integer(i) (makes unpacking platform independent) 256 | myInt = struct.unpack('>i', literal_read)[0] 257 | 258 | if self.scaling_output: 259 | channel_data.append(myInt*scale_fac_uVolts_per_count) 260 | else: 261 | channel_data.append(myInt) 262 | 263 | self.read_state = 2; 264 | 265 | #---------Accelerometer Data--------- 266 | elif self.read_state == 2: 267 | aux_data = [] 268 | for a in range(self.aux_channels_per_sample): 269 | 270 | #short = h 271 | acc = struct.unpack('>h', read(2))[0] 272 | log_bytes_in = log_bytes_in + '|' + str(acc); 273 | 274 | if self.scaling_output: 275 | aux_data.append(acc*scale_fac_accel_G_per_count) 276 | else: 277 | aux_data.append(acc) 278 | 279 | self.read_state = 3; 280 | #---------End Byte--------- 281 | elif self.read_state == 3: 282 | val = struct.unpack('B', read(1))[0] 283 | log_bytes_in = log_bytes_in + '|' + str(val); 284 | self.read_state = 0 #read next packet 285 | if (val == END_BYTE): 286 | sample = OpenBCISample(packet_id, channel_data, aux_data) 287 | self.packets_dropped = 0 288 | return sample 289 | else: 290 | self.warn("ID:<%d> instead of <%s>" 291 | %(packet_id, val, END_BYTE)) 292 | logging.debug(log_bytes_in); 293 | self.packets_dropped = self.packets_dropped + 1 294 | 295 | """ 296 | 297 | Clean Up (atexit) 298 | 299 | """ 300 | def stop(self): 301 | print("Stopping streaming...\nWait for buffer to flush...") 302 | self.streaming = False 303 | self.ser.write(b's') 304 | if self.log: 305 | logging.warning('sent : stopped streaming') 306 | 307 | def disconnect(self): 308 | if(self.streaming == True): 309 | self.stop() 310 | if (self.ser.isOpen()): 311 | print("Closing Serial...") 312 | self.ser.close() 313 | logging.warning('serial closed') 314 | 315 | 316 | """ 317 | 318 | SETTINGS AND HELPERS 319 | 320 | """ 321 | def warn(self, text): 322 | if self.log: 323 | #log how many packets where sent succesfully in between warnings 324 | if self.log_packet_count: 325 | logging.info('Data packets received:'+str(self.log_packet_count)) 326 | self.log_packet_count = 0; 327 | logging.warning(text) 328 | print("Warning: %s" % text) 329 | 330 | 331 | def print_incoming_text(self): 332 | """ 333 | 334 | When starting the connection, print all the debug data until 335 | we get to a line with the end sequence '$$$'. 336 | 337 | """ 338 | line = '' 339 | #Wait for device to send data 340 | time.sleep(1) 341 | 342 | if self.ser.inWaiting(): 343 | line = '' 344 | c = '' 345 | #Look for end sequence $$$ 346 | while '$$$' not in line: 347 | c = self.ser.read().decode('utf-8', errors='replace') # we're supposed to get UTF8 text, but the board might behave otherwise 348 | line += c 349 | print(line); 350 | else: 351 | self.warn("No Message") 352 | 353 | def openbci_id(self, serial): 354 | """ 355 | 356 | When automatically detecting port, parse the serial return for the "OpenBCI" ID. 357 | 358 | """ 359 | line = '' 360 | #Wait for device to send data 361 | time.sleep(2) 362 | 363 | if serial.inWaiting(): 364 | line = '' 365 | c = '' 366 | #Look for end sequence $$$ 367 | while '$$$' not in line: 368 | c = serial.read().decode('utf-8', errors='replace') # we're supposed to get UTF8 text, but the board might behave otherwise 369 | line += c 370 | if "OpenBCI" in line: 371 | return True 372 | return False 373 | 374 | def print_register_settings(self): 375 | self.ser.write(b'?') 376 | time.sleep(0.5) 377 | self.print_incoming_text(); 378 | #DEBBUGING: Prints individual incoming bytes 379 | def print_bytes_in(self): 380 | if not self.streaming: 381 | self.ser.write(b'b') 382 | self.streaming = True 383 | while self.streaming: 384 | print(struct.unpack('B',self.ser.read())[0]); 385 | 386 | '''Incoming Packet Structure: 387 | Start Byte(1)|Sample ID(1)|Channel Data(24)|Aux Data(6)|End Byte(1) 388 | 0xA0|0-255|8, 3-byte signed ints|3 2-byte signed ints|0xC0''' 389 | 390 | def print_packets_in(self): 391 | while self.streaming: 392 | b = struct.unpack('B', self.ser.read())[0]; 393 | 394 | if b == START_BYTE: 395 | self.attempt_reconnect = False 396 | if skipped_str: 397 | logging.debug('SKIPPED\n' + skipped_str + '\nSKIPPED') 398 | skipped_str = '' 399 | 400 | packet_str = "%03d"%(b) + '|'; 401 | b = struct.unpack('B', self.ser.read())[0]; 402 | packet_str = packet_str + "%03d"%(b) + '|'; 403 | 404 | #data channels 405 | for i in range(24-1): 406 | b = struct.unpack('B', self.ser.read())[0]; 407 | packet_str = packet_str + '.' + "%03d"%(b); 408 | 409 | b = struct.unpack('B', self.ser.read())[0]; 410 | packet_str = packet_str + '.' + "%03d"%(b) + '|'; 411 | 412 | #aux channels 413 | for i in range(6-1): 414 | b = struct.unpack('B', self.ser.read())[0]; 415 | packet_str = packet_str + '.' + "%03d"%(b); 416 | 417 | b = struct.unpack('B', self.ser.read())[0]; 418 | packet_str = packet_str + '.' + "%03d"%(b) + '|'; 419 | 420 | #end byte 421 | b = struct.unpack('B', self.ser.read())[0]; 422 | 423 | #Valid Packet 424 | if b == END_BYTE: 425 | packet_str = packet_str + '.' + "%03d"%(b) + '|VAL'; 426 | print(packet_str) 427 | #logging.debug(packet_str) 428 | 429 | #Invalid Packet 430 | else: 431 | packet_str = packet_str + '.' + "%03d"%(b) + '|INV'; 432 | #Reset 433 | self.attempt_reconnect = True 434 | 435 | 436 | else: 437 | print(b) 438 | if b == END_BYTE: 439 | skipped_str = skipped_str + '|END|' 440 | else: 441 | skipped_str = skipped_str + "%03d"%(b) + '.' 442 | 443 | if self.attempt_reconnect and (timeit.default_timer()-self.last_reconnect) > self.reconnect_freq: 444 | self.last_reconnect = timeit.default_timer() 445 | self.warn('Reconnecting') 446 | self.reconnect() 447 | 448 | 449 | 450 | def check_connection(self, interval = 2, max_packets_to_skip=10): 451 | # stop checking when we're no longer streaming 452 | if not self.streaming: 453 | return 454 | #check number of dropped packages and establish connection problem if too large 455 | if self.packets_dropped > max_packets_to_skip: 456 | #if error, attempt to reconect 457 | self.reconnect() 458 | # check again again in 2 seconds 459 | threading.Timer(interval, self.check_connection).start() 460 | 461 | def reconnect(self): 462 | self.packets_dropped = 0 463 | self.warn('Reconnecting') 464 | self.stop() 465 | time.sleep(0.5) 466 | self.ser.write(b'v') 467 | time.sleep(0.5) 468 | self.ser.write(b'b') 469 | time.sleep(0.5) 470 | self.streaming = True 471 | #self.attempt_reconnect = False 472 | 473 | 474 | #Adds a filter at 60hz to cancel out ambient electrical noise 475 | def enable_filters(self): 476 | self.ser.write(b'f') 477 | self.filtering_data = True; 478 | 479 | def disable_filters(self): 480 | self.ser.write(b'g') 481 | self.filtering_data = False; 482 | 483 | def test_signal(self, signal): 484 | """ Enable / disable test signal """ 485 | if signal == 0: 486 | self.ser.write(b'0') 487 | self.warn("Connecting all pins to ground") 488 | elif signal == 1: 489 | self.ser.write(b'p') 490 | self.warn("Connecting all pins to Vcc") 491 | elif signal == 2: 492 | self.ser.write(b'-') 493 | self.warn("Connecting pins to low frequency 1x amp signal") 494 | elif signal == 3: 495 | self.ser.write(b'=') 496 | self.warn("Connecting pins to high frequency 1x amp signal") 497 | elif signal == 4: 498 | self.ser.write(b'[') 499 | self.warn("Connecting pins to low frequency 2x amp signal") 500 | elif signal == 5: 501 | self.ser.write(b']') 502 | self.warn("Connecting pins to high frequency 2x amp signal") 503 | else: 504 | self.warn("%s is not a known test signal. Valid signals go from 0-5" %(signal)) 505 | 506 | def set_channel(self, channel, toggle_position): 507 | """ Enable / disable channels """ 508 | #Commands to set toggle to on position 509 | if toggle_position == 1: 510 | if channel is 1: 511 | self.ser.write(b'!') 512 | if channel is 2: 513 | self.ser.write(b'@') 514 | if channel is 3: 515 | self.ser.write(b'#') 516 | if channel is 4: 517 | self.ser.write(b'$') 518 | if channel is 5: 519 | self.ser.write(b'%') 520 | if channel is 6: 521 | self.ser.write(b'^') 522 | if channel is 7: 523 | self.ser.write(b'&') 524 | if channel is 8: 525 | self.ser.write(b'*') 526 | if channel is 9 and self.daisy: 527 | self.ser.write(b'Q') 528 | if channel is 10 and self.daisy: 529 | self.ser.write(b'W') 530 | if channel is 11 and self.daisy: 531 | self.ser.write(b'E') 532 | if channel is 12 and self.daisy: 533 | self.ser.write(b'R') 534 | if channel is 13 and self.daisy: 535 | self.ser.write(b'T') 536 | if channel is 14 and self.daisy: 537 | self.ser.write(b'Y') 538 | if channel is 15 and self.daisy: 539 | self.ser.write(b'U') 540 | if channel is 16 and self.daisy: 541 | self.ser.write(b'I') 542 | #Commands to set toggle to off position 543 | elif toggle_position == 0: 544 | if channel is 1: 545 | self.ser.write(b'1') 546 | if channel is 2: 547 | self.ser.write(b'2') 548 | if channel is 3: 549 | self.ser.write(b'3') 550 | if channel is 4: 551 | self.ser.write(b'4') 552 | if channel is 5: 553 | self.ser.write(b'5') 554 | if channel is 6: 555 | self.ser.write(b'6') 556 | if channel is 7: 557 | self.ser.write(b'7') 558 | if channel is 8: 559 | self.ser.write(b'8') 560 | if channel is 9 and self.daisy: 561 | self.ser.write(b'q') 562 | if channel is 10 and self.daisy: 563 | self.ser.write(b'w') 564 | if channel is 11 and self.daisy: 565 | self.ser.write(b'e') 566 | if channel is 12 and self.daisy: 567 | self.ser.write(b'r') 568 | if channel is 13 and self.daisy: 569 | self.ser.write(b't') 570 | if channel is 14 and self.daisy: 571 | self.ser.write(b'y') 572 | if channel is 15 and self.daisy: 573 | self.ser.write(b'u') 574 | if channel is 16 and self.daisy: 575 | self.ser.write(b'i') 576 | 577 | def find_port(self): 578 | # Finds the serial port names 579 | if sys.platform.startswith('win'): 580 | ports = ['COM%s' % (i+1) for i in range(256)] 581 | elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): 582 | ports = glob.glob('/dev/ttyUSB*') 583 | elif sys.platform.startswith('darwin'): 584 | ports = glob.glob('/dev/tty.usbserial*') 585 | else: 586 | raise EnvironmentError('Error finding ports on your operating system') 587 | openbci_port = '' 588 | for port in ports: 589 | try: 590 | s = serial.Serial(port= port, baudrate = self.baudrate, timeout=self.timeout) 591 | s.write(b'v') 592 | openbci_serial = self.openbci_id(s) 593 | s.close() 594 | if openbci_serial: 595 | openbci_port = port; 596 | except (OSError, serial.SerialException): 597 | pass 598 | if openbci_port == '': 599 | raise OSError('Cannot find OpenBCI port') 600 | else: 601 | return openbci_port 602 | 603 | class OpenBCISample(object): 604 | """Object encapulsating a single sample from the OpenBCI board. NB: dummy imp for plugin compatiblity""" 605 | def __init__(self, packet_id, channel_data, aux_data): 606 | self.id = packet_id 607 | self.channel_data = channel_data 608 | self.aux_data = aux_data 609 | self.imp_data = [] 610 | 611 | 612 | -------------------------------------------------------------------------------- /plugin_interface.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Extends Yapsy IPlugin interface to pass information about the board to plugins. 4 | 5 | Fields of interest for plugins: 6 | args: list of arguments passed to the plugins 7 | sample_rate: actual sample rate of the board 8 | eeg_channels: number of EEG 9 | aux_channels: number of AUX channels 10 | 11 | If needed, plugins that need to report an error can set self.is_activated to False during activate() call. 12 | 13 | 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): 14 | 15 | import plugin_interface as plugintypes 16 | 17 | class PluginExample(plugintypes.IPluginExtended): 18 | ... 19 | """ 20 | 21 | from yapsy.IPlugin import IPlugin 22 | 23 | class IPluginExtended(IPlugin): 24 | # args: passed by command line 25 | def pre_activate(self, args, sample_rate=250, eeg_channels=8, aux_channels=3, imp_channels=0): 26 | self.args = args 27 | self.sample_rate = sample_rate 28 | self.eeg_channels = eeg_channels 29 | self.aux_channels = aux_channels 30 | self.imp_channels = imp_channels 31 | # by default we say that activation was okay -- inherited from IPlugin 32 | self.is_activated = True 33 | self.activate() 34 | # tell outside world if init went good or bad 35 | return self.is_activated 36 | 37 | # inherited from IPlugin 38 | def activate(self): 39 | print "Plugin %s activated." % (self.__class__.__name__) 40 | 41 | # inherited from IPlugin 42 | def deactivate(self): 43 | print "Plugin %s deactivated." % (self.__class__.__name__) 44 | 45 | # plugins that require arguments should implement this method 46 | def show_help(self): 47 | print "I, %s, do not need any parameter." % (self.__class__.__name__) 48 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /plugins/csv_collect.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import timeit 3 | import datetime 4 | 5 | import plugin_interface as plugintypes 6 | 7 | class PluginCSVCollect(plugintypes.IPluginExtended): 8 | def __init__(self, file_name="collect.csv", delim = ",", verbose=False): 9 | now = datetime.datetime.now() 10 | self.time_stamp = '%d-%d-%d_%d-%d-%d'%(now.year,now.month,now.day,now.hour,now.minute,now.second) 11 | self.file_name = self.time_stamp 12 | self.start_time = timeit.default_timer() 13 | self.delim = delim 14 | self.verbose = verbose 15 | 16 | def activate(self): 17 | if len(self.args) > 0: 18 | if 'no_time' in self.args: 19 | self.file_name = self.args[0] 20 | else: 21 | self.file_name = self.args[0] + '_' + self.file_name; 22 | if 'verbose' in self.args: 23 | self.verbose = True 24 | 25 | self.file_name = self.file_name + '.csv' 26 | print "Will export CSV to:", self.file_name 27 | #Open in append mode 28 | with open(self.file_name, 'a') as f: 29 | f.write('%'+self.time_stamp + '\n') 30 | 31 | def deactivate(self): 32 | print "Closing, CSV saved to:", self.file_name 33 | return 34 | 35 | def show_help(self): 36 | print "Optional argument: [filename] (default: collect.csv)" 37 | 38 | def __call__(self, sample): 39 | t = timeit.default_timer() - self.start_time 40 | 41 | #print timeSinceStart|Sample Id 42 | if self.verbose: 43 | print("CSV: %f | %d" %(t,sample.id)) 44 | 45 | row = '' 46 | row += str(t) 47 | row += self.delim 48 | row += str(sample.id) 49 | row += self.delim 50 | for i in sample.channel_data: 51 | row += str(i) 52 | row += self.delim 53 | for i in sample.aux_data: 54 | row += str(i) 55 | row += self.delim 56 | #remove last comma 57 | row += '\n' 58 | with open(self.file_name, 'a') as f: 59 | f.write(row) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /plugins/noise_test.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | import numpy as np 3 | import plugin_interface as plugintypes 4 | 5 | class PluginNoiseTest(plugintypes.IPluginExtended): 6 | # update counters value 7 | def __call__(self, sample): 8 | # keep tract of absolute value of 9 | self.diff = np.add(self.diff,np.absolute(np.asarray(sample.channel_data))) 10 | self.sample_count = self.sample_count + 1 11 | 12 | 13 | elapsed_time = timeit.default_timer() - self.last_report 14 | if elapsed_time > self.polling_interval: 15 | channel_noise_power = np.divide(self.diff,self.sample_count) 16 | 17 | print (channel_noise_power) 18 | self.diff = np.zeros(self.eeg_channels) 19 | self.last_report = timeit.default_timer() 20 | 21 | 22 | 23 | # # Instanciate "monitor" thread 24 | def activate(self): 25 | # The difference between the ref and incoming signal. 26 | # IMPORTANT: For noise tests, the reference and channel should have the same input signal. 27 | self.diff = np.zeros(self.eeg_channels) 28 | self.last_report = timeit.default_timer() 29 | self.sample_count = 0 30 | self.polling_interval = 1.0 31 | 32 | if len(self.args) > 0: 33 | self.polling_interval = float(self.args[0]) 34 | 35 | 36 | 37 | def show_help(self): 38 | print "Optional argument: polling_interval -- in seconds, default: 10. \n \ 39 | Returns the power of the system noise.\n \ 40 | NOTE: The reference and channel should have the same input signal." 41 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /plugins/print.py: -------------------------------------------------------------------------------- 1 | import plugin_interface as plugintypes 2 | 3 | class PluginPrint(plugintypes.IPluginExtended): 4 | def activate(self): 5 | print "Print activated" 6 | 7 | # called with each new sample 8 | def __call__(self, sample): 9 | if sample: 10 | # print impedance if supported 11 | if self.imp_channels > 0: 12 | sample_string = "ID: %f\n%s\n%s\n%s" %(sample.id, str(sample.channel_data)[1:-1], str(sample.aux_data)[1:-1], str(sample.imp_data)[1:-1]) 13 | else: 14 | sample_string = "ID: %f\n%s\n%s" %(sample.id, str(sample.channel_data)[1:-1], str(sample.aux_data)[1:-1]) 15 | print "---------------------------------" 16 | print sample_string 17 | print "---------------------------------" 18 | 19 | # DEBBUGING 20 | # try: 21 | # sample_string.decode('ascii') 22 | # except UnicodeDecodeError: 23 | # print "Not a ascii-encoded unicode string" 24 | # else: 25 | # print sample_string 26 | 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /plugins/sample_rate.py: -------------------------------------------------------------------------------- 1 | import time 2 | import timeit 3 | from threading import Thread 4 | 5 | import plugin_interface as plugintypes 6 | 7 | # counter for sampling rate 8 | nb_samples_out = -1 9 | 10 | # try to ease work for main loop 11 | class Monitor(Thread): 12 | def __init__(self): 13 | Thread.__init__(self) 14 | self.nb_samples_out = -1 15 | 16 | # Init time to compute sampling rate 17 | self.tick = timeit.default_timer() 18 | self.start_tick = self.tick 19 | self.polling_interval = 10 20 | 21 | def run(self): 22 | while True: 23 | # check FPS + listen for new connections 24 | new_tick = timeit.default_timer() 25 | elapsed_time = new_tick - self.tick 26 | current_samples_out = nb_samples_out 27 | print "--- at t: ", (new_tick - self.start_tick), " ---" 28 | print "elapsed_time: ", elapsed_time 29 | print "nb_samples_out: ", current_samples_out - self.nb_samples_out 30 | sampling_rate = (current_samples_out - self.nb_samples_out) / elapsed_time 31 | print "sampling rate: ", sampling_rate 32 | self.tick = new_tick 33 | self.nb_samples_out = nb_samples_out 34 | time.sleep(self.polling_interval) 35 | 36 | class PluginSampleRate(plugintypes.IPluginExtended): 37 | # update counters value 38 | def __call__(self, sample): 39 | global nb_samples_out 40 | nb_samples_out = nb_samples_out + 1 41 | 42 | # Instanciate "monitor" thread 43 | def activate(self): 44 | monit = Monitor() 45 | if len(self.args) > 0: 46 | monit.polling_interval = float(self.args[0]) 47 | # daemonize thread to terminate it altogether with the main when time will come 48 | monit.daemon = True 49 | monit.start() 50 | 51 | def show_help(self): 52 | print "Optional argument: polling_interval -- in seconds, default: 10." 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | import sys; sys.path.append('lib') # help python find pylsl relative to this example program 5 | 6 | from pylsl import StreamInfo, StreamOutlet 7 | import plugin_interface as plugintypes 8 | 9 | # Use LSL protocol to broadcast data using one stream for EEG, one stream for AUX, one last for impedance testing (on supported board, if enabled) 10 | class StreamerLSL(plugintypes.IPluginExtended): 11 | # From IPlugin 12 | def activate(self): 13 | eeg_stream = "OpenBCI_EEG" 14 | eeg_id = "openbci_eeg_id1" 15 | aux_stream = "OpenBCI_AUX" 16 | aux_id = "openbci_aux_id1" 17 | imp_stream = "OpenBCI_Impedance" 18 | imp_id = "openbci_imp_id1" 19 | 20 | if len(self.args) > 0: 21 | eeg_stream = self.args[0] 22 | if len(self.args) > 1: 23 | eeg_id = self.args[1] 24 | if len(self.args) > 2: 25 | aux_stream = self.args[2] 26 | if len(self.args) > 3: 27 | aux_id = self.args[3] 28 | if len(self.args) > 4: 29 | imp_stream = self.args[4] 30 | if len(self.args) > 5: 31 | imp_id = self.args[5] 32 | 33 | # Create a new streams info, one for EEG values, one for AUX (eg, accelerometer) values 34 | print "Creating LSL stream for EEG. Name:", eeg_stream, "- ID:", eeg_id, "- data type: float32.", self.eeg_channels, "channels at", self.sample_rate, "Hz." 35 | info_eeg = StreamInfo(eeg_stream, 'EEG', self.eeg_channels,self.sample_rate,'float32',eeg_id); 36 | # NB: set float32 instead of int16 so as OpenViBE takes it into account 37 | print "Creating LSL stream for AUX. Name:", aux_stream, "- ID:", aux_id, "- data type: float32.", self.aux_channels, "channels at", self.sample_rate, "Hz." 38 | info_aux = StreamInfo(aux_stream, 'AUX', self.aux_channels,self.sample_rate,'float32',aux_id); 39 | 40 | # make outlets 41 | self.outlet_eeg = StreamOutlet(info_eeg) 42 | self.outlet_aux = StreamOutlet(info_aux) 43 | 44 | if self.imp_channels > 0: 45 | print "Creating LSL stream for Impedance. Name:", imp_stream, "- ID:", imp_id, "- data type: float32.", self.imp_channels, "channels at", self.sample_rate, "Hz." 46 | info_imp = StreamInfo(imp_stream, 'Impedance', self.imp_channels,self.sample_rate,'float32',imp_id); 47 | self.outlet_imp = StreamOutlet(info_imp) 48 | 49 | # send channels values 50 | def __call__(self, sample): 51 | self.outlet_eeg.push_sample(sample.channel_data) 52 | self.outlet_aux.push_sample(sample.aux_data) 53 | if self.imp_channels > 0: 54 | self.outlet_imp.push_sample(sample.imp_data) 55 | 56 | def show_help(self): 57 | print """Optional arguments: [EEG_stream_name [EEG_stream_ID [AUX_stream_name [AUX_stream_ID [Impedance_steam_name [Impedance_stream_ID]]]]]] 58 | \t Defaults: "OpenBCI_EEG" / "openbci_eeg_id1" and "OpenBCI_AUX" / "openbci_aux_id1" / "OpenBCI_Impedance" / "openbci_imp_id1".""" 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /plugins/streamer_osc.py: -------------------------------------------------------------------------------- 1 | 2 | # requires pyosc 3 | from OSC import OSCClient, OSCMessage 4 | import plugin_interface as plugintypes 5 | 6 | # Use OSC protocol to broadcast data (UDP layer), using "/openbci" stream. (NB. does not check numbers of channel as TCP server) 7 | 8 | class StreamerOSC(plugintypes.IPluginExtended): 9 | """ 10 | 11 | Relay OpenBCI values to OSC clients 12 | 13 | Args: 14 | port: Port of the server 15 | ip: IP address of the server 16 | address: name of the stream 17 | """ 18 | 19 | def __init__(self, ip='localhost', port=12345, address="/openbci"): 20 | # connection infos 21 | self.ip = ip 22 | self.port = port 23 | self.address = address 24 | 25 | # From IPlugin 26 | def activate(self): 27 | if len(self.args) > 0: 28 | self.ip = self.args[0] 29 | if len(self.args) > 1: 30 | self.port = int(self.args[1]) 31 | if len(self.args) > 2: 32 | self.address = self.args[2] 33 | # init network 34 | print "Selecting OSC streaming. IP: ", self.ip, ", port: ", self.port, ", address: ", self.address 35 | self.client = OSCClient() 36 | self.client.connect( (self.ip, self.port) ) 37 | 38 | # From IPlugin: close connections, send message to client 39 | def deactivate(self): 40 | self.client.send(OSCMessage("/quit") ) 41 | 42 | # send channels values 43 | def __call__(self, sample): 44 | mes = OSCMessage(self.address) 45 | mes.append(sample.channel_data) 46 | # silently pass if connection drops 47 | try: 48 | self.client.send(mes) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /plugins/streamer_tcp_server.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | import socket, select, struct, time 3 | import plugin_interface as plugintypes 4 | 5 | # Simple TCP server to "broadcast" data to clients, handling deconnections. Binary format use network endianness (i.e., big-endian), float32 6 | 7 | # TODO: does not listen for anything at the moment, could use it to set options 8 | 9 | # Handling new client in separate thread 10 | class MonitorStreamer(Thread): 11 | """Launch and monitor a "Streamer" entity (incoming connections if implemented, current sampling rate).""" 12 | # tcp_server: the TCPServer instance that will be used 13 | def __init__(self, streamer): 14 | Thread.__init__(self) 15 | # bind to Streamer entity 16 | self.server = streamer 17 | 18 | def run(self): 19 | # run until we DIE 20 | while True: 21 | # check FPS + listen for new connections 22 | # FIXME: not so great with threads -- use a lock? 23 | # TODO: configure interval 24 | self.server.check_connections() 25 | time.sleep(1) 26 | 27 | 28 | class StreamerTCPServer(plugintypes.IPluginExtended): 29 | """ 30 | 31 | Relay OpenBCI values to TCP clients 32 | 33 | Args: 34 | port: Port of the server 35 | ip: IP address of the server 36 | 37 | """ 38 | 39 | def __init__(self, ip='localhost', port=12345): 40 | # list of socket clients 41 | self.CONNECTION_LIST = [] 42 | # connection infos 43 | self.ip = ip 44 | self.port = port 45 | 46 | # From IPlugin 47 | def activate(self): 48 | if len(self.args) > 0: 49 | self.ip = self.args[0] 50 | if len(self.args) > 1: 51 | self.port = int(self.args[1]) 52 | 53 | # init network 54 | print "Selecting raw TCP streaming. IP: ", self.ip, ", port: ", self.port 55 | self.initialize() 56 | 57 | # init the daemon that monitors connections 58 | self.monit = MonitorStreamer(self) 59 | self.monit.daemon = True 60 | # launch monitor 61 | self.monit.start() 62 | 63 | # the initialize method reads settings and outputs the first header 64 | def initialize(self): 65 | # init server 66 | self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 67 | # this has no effect, why ? 68 | self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 69 | # create connection 70 | self.server_socket.bind((self.ip, self.port)) 71 | self.server_socket.listen(1) 72 | print "Server started on port " + str(self.port) 73 | 74 | # From Streamer, to be called each time we're willing to accept new connections 75 | def check_connections(self): 76 | # First listen for new connections, and new connections only -- this is why we pass only server_socket 77 | read_sockets,write_sockets,error_sockets = select.select([self.server_socket],[],[], 0) 78 | for sock in read_sockets: 79 | # New connection 80 | sockfd, addr = self.server_socket.accept() 81 | self.CONNECTION_LIST.append(sockfd) 82 | print "Client (%s, %s) connected" % addr 83 | # and... don't bother with incoming messages 84 | 85 | # From IPlugin: close sockets, send message to client 86 | def deactivate(self): 87 | # close all remote connections 88 | for sock in self.CONNECTION_LIST: 89 | if sock != self.server_socket: 90 | try: 91 | sock.send("closing!\n") 92 | # at this point don't bother if message not sent 93 | except: 94 | continue 95 | sock.close(); 96 | # close server socket 97 | self.server_socket.close(); 98 | 99 | # broadcast channels values to all clients 100 | # as_string: many for debug, send values with a nice "[34.45, 30.4, -38.0]"-like format 101 | def __call__(self, sample, as_string=False): 102 | values=sample.channel_data 103 | # save sockets that are closed to remove them later on 104 | outdated_list = [] 105 | for sock in self.CONNECTION_LIST: 106 | # If one error should happen, we remove socket from the list 107 | try: 108 | if as_string: 109 | sock.send(str(values) + "\n") 110 | else: 111 | nb_channels=len(values) 112 | # format for binary data, network endian (big) and float (float32) 113 | packer = struct.Struct('!%sf' % nb_channels) 114 | # convert values to bytes 115 | packed_data = packer.pack(*values) 116 | sock.send(packed_data) 117 | # TODO: should check if the correct number of bytes passed through 118 | except: 119 | # sometimes (always?) it's only during the second write to a close socket that an error is raised? 120 | print "Something bad happened, will close socket" 121 | outdated_list.append(sock) 122 | # now we are outside of the main list, it's time to remove outdated sockets, if any 123 | for bad_sock in outdated_list: 124 | print "Removing socket..." 125 | self.CONNECTION_LIST.remove(bad_sock) 126 | # not very costly to be polite 127 | bad_sock.close() 128 | 129 | def show_help(self): 130 | print """Optional arguments: [ip [port]] 131 | \t ip: target IP address (default: 'localhost') 132 | \t port: target port (default: 12345)""" 133 | -------------------------------------------------------------------------------- /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 | import cPickle as pickle 11 | import json 12 | import socket 13 | 14 | import plugin_interface as plugintypes 15 | 16 | # class PluginPrint(IPlugin): 17 | # # args: passed by command line 18 | # def activate(self, args): 19 | # print "Print activated" 20 | # # tell outside world that init went good 21 | # return True 22 | 23 | # def deactivate(self): 24 | # print "Print Deactivated" 25 | 26 | # def show_help(self): 27 | # print "I do not need any parameter, just printing stuff." 28 | 29 | # # called with each new sample 30 | # def __call__(self, sample): 31 | # sample_string = "ID: %f\n%s\n%s" %(sample.id, str(sample.channel_data)[1:-1], str(sample.aux_data)[1:-1]) 32 | # print "---------------------------------" 33 | # print sample_string 34 | # print "---------------------------------" 35 | 36 | # # DEBBUGING 37 | # # try: 38 | # # sample_string.decode('ascii') 39 | # # except UnicodeDecodeError: 40 | # # print "Not a ascii-encoded unicode string" 41 | # # else: 42 | # # print sample_string 43 | 44 | 45 | class UDPServer(plugintypes.IPluginExtended): 46 | def __init__(self, ip='localhost', port=8888): 47 | self.ip = ip 48 | self.port = port 49 | self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 50 | 51 | def activate(self): 52 | print "udp_server plugin" 53 | print self.args 54 | 55 | if len(self.args) > 0: 56 | self.ip = self.args[0] 57 | if len(self.args) > 1: 58 | self.port = int(self.args[1]) 59 | 60 | # init network 61 | print "Selecting raw UDP streaming. IP: ", self.ip, ", port: ", str(self.port) 62 | 63 | self.server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 64 | 65 | print "Server started on port " + str(self.port) 66 | 67 | def __call__(self, sample): 68 | self.send_data(json.dumps(sample.channel_data)) 69 | 70 | def send_data(self, data): 71 | self.server.sendto(data, (self.ip, self.port)) 72 | 73 | # From IPlugin: close sockets, send message to client 74 | def deactivate(self): 75 | self.server.close(); 76 | 77 | def show_help(self): 78 | print """Optional arguments: [ip [port]] 79 | \t ip: target IP address (default: 'localhost') 80 | \t port: target port (default: 12345)""" 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.9.2 2 | pylsl==1.10.4 3 | pyOSC==0.3.5b5294 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 | bluepy==1.0.5 12 | -------------------------------------------------------------------------------- /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() -------------------------------------------------------------------------------- /scripts/socket_client.py: -------------------------------------------------------------------------------- 1 | from socketIO_client import SocketIO 2 | 3 | def on_sample(*args): 4 | print args 5 | 6 | socketIO = SocketIO('10.0.1.194', 8880) 7 | socketIO.on('openbci', on_sample) 8 | socketIO.wait(seconds=10) 9 | -------------------------------------------------------------------------------- /scripts/stream_data.py: -------------------------------------------------------------------------------- 1 | import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder 2 | import open_bci_v3 as bci 3 | import streamer_tcp_server 4 | import time, timeit 5 | from threading import Thread 6 | 7 | # Transmit data to openvibe acquisition server, intelpolating data (well, sort of) from 250Hz to 256Hz 8 | # Listen to new connections every second using a separate thread. 9 | 10 | # NB: Left here for resampling algorithm, prefer the use of user.py. 11 | 12 | NB_CHANNELS = 8 13 | 14 | # If > 0 will interpolate based on samples count, typically 1.024 to go from 250Hz to 256Hz 15 | SAMPLING_FACTOR = -1.024 16 | # If > 0 will interbolate based on elapsed time 17 | SAMPLING_RATE = 256 18 | 19 | SERVER_PORT=12345 20 | SERVER_IP="localhost" 21 | 22 | DEBUG=False 23 | 24 | # check packet drop 25 | last_id = -1 26 | 27 | # counter for sampling rate 28 | nb_samples_in = -1 29 | nb_samples_out = -1 30 | 31 | # last seen values for interpolation 32 | last_values = [0] * NB_CHANNELS 33 | 34 | # counter to trigger duplications... 35 | leftover_duplications = 0 36 | 37 | tick=timeit.default_timer() 38 | 39 | # try to ease work for main loop 40 | class Monitor(Thread): 41 | def __init__(self): 42 | Thread.__init__(self) 43 | self.nb_samples_in = -1 44 | self.nb_samples_out = -1 45 | # Init time to compute sampling rate 46 | self.tick = timeit.default_timer() 47 | self.start_tick = self.tick 48 | 49 | def run(self): 50 | while True: 51 | # check FPS + listen for new connections 52 | new_tick = timeit.default_timer() 53 | elapsed_time = new_tick - self.tick 54 | current_samples_in = nb_samples_in 55 | current_samples_out = nb_samples_out 56 | print "--- at t: ", (new_tick - self.start_tick), " ---" 57 | print "elapsed_time: ", elapsed_time 58 | print "nb_samples_in: ", current_samples_in - self.nb_samples_in 59 | print "nb_samples_out: ", current_samples_out - self.nb_samples_out 60 | self.tick = new_tick 61 | self.nb_samples_in = nb_samples_in 62 | self.nb_samples_out = nb_samples_out 63 | # time to watch for connection 64 | # FIXME: not so great with threads 65 | server.check_connections() 66 | time.sleep(1) 67 | 68 | def streamData(sample): 69 | 70 | global last_values 71 | 72 | global tick 73 | 74 | # check packet skipped 75 | global last_id 76 | # TODO: duplicate packet if skipped to stay sync 77 | if sample.id != last_id + 1: 78 | print "time", tick, ": paquet skipped!" 79 | if sample.id == 255: 80 | last_id = -1 81 | else: 82 | last_id = sample.id 83 | 84 | # update counters 85 | global nb_samples_in, nb_samples_out 86 | nb_samples_in = nb_samples_in + 1 87 | 88 | # check for duplication, by default 1 (...which is *no* duplication of the one current sample) 89 | global leftover_duplications 90 | 91 | # first method with sampling rate and elapsed time (depends on system clock accuracy) 92 | if (SAMPLING_RATE > 0): 93 | # elapsed time since last call, update tick 94 | now = timeit.default_timer() 95 | elapsed_time = now - tick; 96 | # now we have to compute how many times we should send data to keep up with sample rate (oversampling) 97 | leftover_duplications = SAMPLING_RATE * elapsed_time + leftover_duplications - 1 98 | tick = now 99 | # second method with a samplin factor (depends on openbci accuracy) 100 | elif SAMPLING_FACTOR > 0: 101 | leftover_duplications = SAMPLING_FACTOR + leftover_duplications - 1 102 | #print "needed_duplications: ", needed_duplications, "leftover_duplications: ", leftover_duplications 103 | # If we need to insert values, will interpolate between current packet and last one 104 | # 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 105 | if (leftover_duplications > 1): 106 | leftover_duplications = leftover_duplications - 1 107 | interpol_values = list(last_values) 108 | for i in range(0,len(interpol_values)): 109 | # OK, it's a very rough interpolation 110 | interpol_values[i] = (last_values[i] + sample.channel_data[i]) / 2 111 | if DEBUG: 112 | print " --" 113 | print " last values: ", last_values 114 | print " interpolation: ", interpol_values 115 | print " current sample: ", sample.channel_data 116 | # send to clients interpolated sample 117 | #leftover_duplications = 0 118 | server.broadcast_values(interpol_values) 119 | nb_samples_out = nb_samples_out + 1 120 | 121 | # send to clients current sample 122 | server.broadcast_values(sample.channel_data) 123 | nb_samples_out = nb_samples_out + 1 124 | 125 | # save current values for possible interpolation 126 | last_values = list(sample.channel_data) 127 | 128 | if __name__ == '__main__': 129 | # init server 130 | server = streamer_tcp_server.StreamerTCPServer(ip=SERVER_IP, port=SERVER_PORT, nb_channels=NB_CHANNELS) 131 | # init board 132 | port = '/dev/ttyUSB1' 133 | baud = 115200 134 | monit = Monitor() 135 | # daemonize theard to terminate it altogether with the main when time will come 136 | monit.daemon = True 137 | monit.start() 138 | board = bci.OpenBCIBoard(port=port, baud=baud, filter_data=False) 139 | board.startStreaming(streamData) 140 | -------------------------------------------------------------------------------- /scripts/test.py: -------------------------------------------------------------------------------- 1 | import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder 2 | import open_bci_v3 as bci 3 | import os 4 | import logging 5 | import time 6 | 7 | def printData(sample): 8 | #os.system('clear') 9 | print "----------------" 10 | print("%f" %(sample.id)) 11 | print sample.channel_data 12 | print sample.aux_data 13 | print "----------------" 14 | 15 | 16 | 17 | if __name__ == '__main__': 18 | port = '/dev/tty.OpenBCI-DN008VTF' 19 | #port = '/dev/tty.OpenBCI-DN0096XA' 20 | baud = 115200 21 | logging.basicConfig(filename="test.log",format='%(asctime)s - %(levelname)s : %(message)s',level=logging.DEBUG) 22 | logging.info('---------LOG START-------------') 23 | board = bci.OpenBCIBoard(port=port, scaled_output=False, log=True) 24 | print("Board Instantiated") 25 | board.ser.write('v') 26 | time.sleep(10) 27 | #board.start_streaming(printData) 28 | board.print_bytes_in() 29 | -------------------------------------------------------------------------------- /scripts/udp_client.py: -------------------------------------------------------------------------------- 1 | """A sample client for the OpenBCI UDP server.""" 2 | 3 | import argparse 4 | import cPickle as pickle 5 | import json 6 | import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder 7 | import open_bci_v3 as open_bci 8 | import socket 9 | 10 | 11 | parser = argparse.ArgumentParser( 12 | description='Run a UDP client listening for streaming OpenBCI data.') 13 | parser.add_argument( 14 | '--json', 15 | action='store_true', 16 | help='Handle JSON data rather than pickled Python objects.') 17 | parser.add_argument( 18 | '--host', 19 | help='The host to listen on.', 20 | default='127.0.0.1') 21 | parser.add_argument( 22 | '--port', 23 | help='The port to listen on.', 24 | default='8888') 25 | 26 | 27 | class UDPClient(object): 28 | 29 | def __init__(self, ip, port, json): 30 | self.ip = ip 31 | self.port = port 32 | self.json = json 33 | self.client = socket.socket( 34 | socket.AF_INET, # Internet 35 | socket.SOCK_DGRAM) 36 | self.client.bind((ip, port)) 37 | 38 | def start_listening(self, callback=None): 39 | while True: 40 | data, addr = self.client.recvfrom(1024) 41 | print("data") 42 | if self.json: 43 | sample = json.loads(data) 44 | # In JSON mode we only recieve channel data. 45 | print data 46 | else: 47 | sample = pickle.loads(data) 48 | # Note that sample is an OpenBCISample object. 49 | print sample.id 50 | print sample.channel_data 51 | 52 | 53 | args = parser.parse_args() 54 | client = UDPClient(args.host, int(args.port), args.json) 55 | client.start_listening() 56 | -------------------------------------------------------------------------------- /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 | import argparse 11 | import cPickle as pickle 12 | import json 13 | import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder 14 | import open_bci_v3 as open_bci 15 | import socket 16 | 17 | 18 | parser = argparse.ArgumentParser( 19 | description='Run a UDP server streaming OpenBCI data.') 20 | parser.add_argument( 21 | '--json', 22 | action='store_true', 23 | help='Send JSON data rather than pickled Python objects.') 24 | parser.add_argument( 25 | '--filter_data', 26 | action='store_true', 27 | help='Enable onboard filtering.') 28 | parser.add_argument( 29 | '--host', 30 | help='The host to listen on.', 31 | default='127.0.0.1') 32 | parser.add_argument( 33 | '--port', 34 | help='The port to listen on.', 35 | default='8888') 36 | parser.add_argument( 37 | '--serial', 38 | help='The serial port to communicate with the OpenBCI board.', 39 | default='/dev/tty.usbmodem1421') 40 | parser.add_argument( 41 | '--baud', 42 | help='The baud of the serial connection with the OpenBCI board.', 43 | default='115200') 44 | 45 | 46 | class UDPServer(object): 47 | 48 | def __init__(self, ip, port, json): 49 | self.ip = ip 50 | self.port = port 51 | self.json = json 52 | print "Selecting raw UDP streaming. IP: ", self.ip, ", port: ", str(self.port) 53 | self.server = socket.socket( 54 | socket.AF_INET, # Internet 55 | socket.SOCK_DGRAM) 56 | 57 | def send_data(self, data): 58 | self.server.sendto(data, (self.ip, self.port)) 59 | 60 | def handle_sample(self, sample): 61 | if self.json: 62 | # Just send channel data. 63 | self.send_data(json.dumps(sample.channel_data)) 64 | else: 65 | # Pack up and send the whole OpenBCISample object. 66 | self.send_data(pickle.dumps(sample)) 67 | 68 | 69 | args = parser.parse_args() 70 | obci = open_bci.OpenBCIBoard(args.serial, int(args.baud)) 71 | if args.filter_data: 72 | obci.filter_data = True 73 | sock_server = UDPServer(args.host, int(args.port), args.json) 74 | obci.start_streaming(sock_server.handle_sample) 75 | -------------------------------------------------------------------------------- /test_log.py: -------------------------------------------------------------------------------- 1 | import sys; sys.path.append('..') # help python find open_bci_v3.py relative to scripts folder 2 | import open_bci_v3 as bci 3 | import os 4 | import logging 5 | import time 6 | 7 | def printData(sample): 8 | #os.system('clear') 9 | print "----------------" 10 | print("%f" %(sample.id)) 11 | print sample.channel_data 12 | print sample.aux_data 13 | print "----------------" 14 | 15 | 16 | 17 | if __name__ == '__main__': 18 | port = '/dev/tty.usbserial-DN0096XA' 19 | baud = 115200 20 | logging.basicConfig(filename="test.log",format='%(message)s',level=logging.DEBUG) 21 | logging.info('---------LOG START-------------') 22 | board = bci.OpenBCIBoard(port=port, scaled_output=False, log=True) 23 | 24 | #32 bit reset 25 | board.ser.write('v') 26 | time.sleep(0.100) 27 | 28 | #connect pins to vcc 29 | board.ser.write('p') 30 | time.sleep(0.100) 31 | 32 | #board.start_streaming(printData) 33 | board.print_packets_in() -------------------------------------------------------------------------------- /user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | import argparse # new in Python2.7 3 | import os 4 | import time 5 | import string 6 | import atexit 7 | import threading 8 | import logging 9 | import sys 10 | 11 | logging.basicConfig(level=logging.ERROR) 12 | 13 | from yapsy.PluginManager import PluginManager 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="Port to connect to OpenBCI Dongle " + 30 | "( ex /dev/ttyUSB0 or /dev/tty.usbserial-* ) or AUTO to attempt auto-dection.") 31 | parser.set_defaults(port="AUTO") 32 | # baud rate is not currently used 33 | parser.add_argument('-b', '--baud', default=115200, type=int, 34 | help="Baud rate (not currently used)") 35 | parser.add_argument('--no-filtering', dest='filtering', 36 | action='store_false', 37 | help="Disable notch filtering") 38 | parser.set_defaults(filtering=True) 39 | parser.add_argument('-d', '--daisy', dest='daisy', 40 | action='store_true', 41 | help="Force daisy mode (cyton board)") 42 | parser.add_argument('-x', '--aux', dest='aux', 43 | action='store_true', 44 | help="Enable accelerometer/AUX data (ganglion board)") 45 | # first argument: plugin name, then parameters for plugin 46 | parser.add_argument('-a', '--add', metavar=('PLUGIN', 'PARAM'), 47 | action='append', nargs='+', 48 | help="Select which plugins to activate and set parameters.") 49 | parser.add_argument('--log', dest='log', action='store_true', 50 | help="Log program") 51 | parser.add_argument('--plugins-path', dest='plugins_path', nargs='+', 52 | help="Additional path(s) to look for plugins") 53 | 54 | parser.set_defaults(daisy=False, log=False) 55 | 56 | args = parser.parse_args() 57 | 58 | if not(args.add): 59 | print ("WARNING: no plugin selected, you will only be able to communicate with the board. You should select at least one plugin with '--add [plugin_name]'. Use '--list' to show available plugins or '--info [plugin_name]' to get more information.") 60 | 61 | if args.board == "cyton": 62 | print ("Board type: OpenBCI Cyton (v3 API)") 63 | import open_bci_v3 as bci 64 | elif args.board == "ganglion": 65 | print ("Board type: OpenBCI Ganglion") 66 | import open_bci_ganglion as bci 67 | else: 68 | raise ValueError('Board type %r was not recognized. Known are 3 and 4' % args.board) 69 | 70 | # Check AUTO port selection, a "None" parameter for the board API 71 | if "AUTO" == args.port.upper(): 72 | print("Will try do auto-detect board's port. Set it manually with '--port' if it goes wrong.") 73 | args.port = None 74 | else: 75 | print("Port: ", args.port) 76 | 77 | plugins_paths = ["plugins"] 78 | if args.plugins_path: 79 | plugins_paths += args.plugins_path 80 | manager.setPluginPlaces(plugins_paths) 81 | manager.collectPlugins() 82 | 83 | # Print list of available plugins and exit 84 | if args.list: 85 | print ("Available plugins:") 86 | for plugin in manager.getAllPlugins(): 87 | print ("\t- " + plugin.name) 88 | exit() 89 | 90 | # User wants more info about a plugin... 91 | if args.info: 92 | plugin = manager.getPluginByName(args.info) 93 | if plugin == None: 94 | # eg: if an import fail inside a plugin, yapsy skip it 95 | print ("Error: [ " + args.info + " ] not found or could not be loaded. Check name and requirements.") 96 | else: 97 | print (plugin.description) 98 | plugin.plugin_object.show_help() 99 | exit() 100 | 101 | print ("\n------------SETTINGS-------------") 102 | print ("Notch filtering:" + str(args.filtering)) 103 | 104 | # Logging 105 | if args.log: 106 | print ("Logging Enabled: " + str(args.log)) 107 | logging.basicConfig(filename="OBCI.log", format='%(asctime)s - %(levelname)s : %(message)s', level=logging.DEBUG) 108 | logging.getLogger('yapsy').setLevel(logging.DEBUG) 109 | logging.info('---------LOG START-------------') 110 | logging.info(args) 111 | else: 112 | print ("user.py: Logging Disabled.") 113 | 114 | print ("\n-------INSTANTIATING BOARD-------") 115 | board = bci.OpenBCIBoard(port=args.port, 116 | daisy=args.daisy, 117 | filter_data=args.filtering, 118 | scaled_output=True, 119 | log=args.log, 120 | aux=args.aux) 121 | 122 | # Info about effective number of channels and sampling rate 123 | if board.daisy: 124 | print ("Force daisy mode:") 125 | else: 126 | print ("No daisy:") 127 | print (board.getNbEEGChannels(), "EEG channels and", board.getNbAUXChannels(), "AUX channels at", board.getSampleRate(), "Hz.") 128 | 129 | print ("\n------------PLUGINS--------------") 130 | # Loop round the plugins and print their names. 131 | print ("Found plugins:") 132 | for plugin in manager.getAllPlugins(): 133 | print ("[ " + plugin.name + " ]") 134 | print("\n") 135 | 136 | 137 | # Fetch plugins, try to activate them, add to the list if OK 138 | plug_list = [] 139 | callback_list = [] 140 | if args.add: 141 | for plug_candidate in args.add: 142 | # first value: plugin name, then optional arguments 143 | plug_name = plug_candidate[0] 144 | plug_args = plug_candidate[1:] 145 | # Try to find name 146 | plug = manager.getPluginByName(plug_name) 147 | if plug == None: 148 | # eg: if an import fail inside a plugin, yapsy skip it 149 | print ("Error: [ " + plug_name + " ] not found or could not be loaded. Check name and requirements.") 150 | else: 151 | print ("\nActivating [ " + plug_name + " ] plugin...") 152 | if not plug.plugin_object.pre_activate(plug_args, sample_rate=board.getSampleRate(), eeg_channels=board.getNbEEGChannels(), aux_channels=board.getNbAUXChannels(), imp_channels=board.getNbImpChannels()): 153 | print ("Error while activating [ " + plug_name + " ], check output for more info.") 154 | else: 155 | print ("Plugin [ " + plug_name + "] added to the list") 156 | plug_list.append(plug.plugin_object) 157 | callback_list.append(plug.plugin_object) 158 | 159 | if len(plug_list) == 0: 160 | fun = None 161 | else: 162 | fun = callback_list 163 | 164 | def cleanUp(): 165 | board.disconnect() 166 | print ("Deactivating Plugins...") 167 | for plug in plug_list: 168 | plug.deactivate() 169 | print ("User.py exiting...") 170 | 171 | atexit.register(cleanUp) 172 | 173 | print ("--------------INFO---------------") 174 | print ("User serial interface enabled...\n\ 175 | View command map at http://docs.openbci.com.\n\ 176 | Type /start to run (/startimp for impedance \n\ 177 | checking, if supported) -- and /stop\n\ 178 | before issuing new commands afterwards.\n\ 179 | Type /exit to exit. \n\ 180 | Board outputs are automatically printed as: \n\ 181 | % message\n\ 182 | $$$ signals end of message") 183 | 184 | print("\n-------------BEGIN---------------") 185 | # Init board state 186 | # s: stop board streaming; v: soft reset of the 32-bit board (no effect with 8bit board) 187 | s = 'sv' 188 | # Tell the board to enable or not daisy module 189 | if board.daisy: 190 | s = s + 'C' 191 | else: 192 | s = s + 'c' 193 | # d: Channels settings back to default 194 | s = s + 'd' 195 | 196 | while(s != "/exit"): 197 | # Send char and wait for registers to set 198 | if (not s): 199 | pass 200 | elif("help" in s): 201 | print ("View command map at: \ 202 | http://docs.openbci.com/software/01-OpenBCI_SDK.\n\ 203 | For user interface: read README or view \ 204 | https://github.com/OpenBCI/OpenBCI_Python") 205 | 206 | elif board.streaming and s != "/stop": 207 | print ("Error: the board is currently streaming data, please type '/stop' before issuing new commands.") 208 | else: 209 | # read silently incoming packet if set (used when stream is stopped) 210 | flush = False 211 | 212 | if('/' == s[0]): 213 | s = s[1:] 214 | rec = False # current command is recognized or fot 215 | 216 | if("T:" in s): 217 | lapse = int(s[string.find(s, "T:")+2:]) 218 | rec = True 219 | elif("t:" in s): 220 | lapse = int(s[string.find(s, "t:")+2:]) 221 | rec = True 222 | else: 223 | lapse = -1 224 | 225 | if('startimp' in s): 226 | if board.getBoardType() == "cyton": 227 | print ("Impedance checking not supported on cyton.") 228 | else: 229 | board.setImpedance(True) 230 | if(fun != None): 231 | # start streaming in a separate thread so we could always send commands in here 232 | boardThread = threading.Thread(target=board.start_streaming, args=(fun, lapse)) 233 | boardThread.daemon = True # will stop on exit 234 | try: 235 | boardThread.start() 236 | except: 237 | raise 238 | else: 239 | print ("No function loaded") 240 | rec = True 241 | 242 | elif("start" in s): 243 | board.setImpedance(False) 244 | if(fun != None): 245 | # start streaming in a separate thread so we could always send commands in here 246 | boardThread = threading.Thread(target=board.start_streaming, args=(fun, lapse)) 247 | boardThread.daemon = True # will stop on exit 248 | try: 249 | boardThread.start() 250 | except: 251 | raise 252 | else: 253 | print ("No function loaded") 254 | rec = True 255 | 256 | elif('test' in s): 257 | test = int(s[s.find("test")+4:]) 258 | board.test_signal(test) 259 | rec = True 260 | elif('stop' in s): 261 | board.stop() 262 | rec = True 263 | flush = True 264 | if rec == False: 265 | print("Command not recognized...") 266 | 267 | elif s: 268 | for c in s: 269 | if sys.hexversion > 0x03000000: 270 | board.ser_write(bytes(c, 'utf-8')) 271 | else: 272 | board.ser_write(bytes(c)) 273 | time.sleep(0.100) 274 | 275 | line = '' 276 | time.sleep(0.1) #Wait to see if the board has anything to report 277 | # The Cyton nicely return incoming packets -- here supposedly messages -- whereas the Ganglion prints incoming ASCII message by itself 278 | if board.getBoardType() == "cyton": 279 | while board.ser_inWaiting(): 280 | c = board.ser_read().decode('utf-8', errors='replace') # we're supposed to get UTF8 text, but the board might behave otherwise 281 | line += c 282 | time.sleep(0.001) 283 | if (c == '\n') and not flush: 284 | print('%\t'+line[:-1]) 285 | line = '' 286 | elif board.getBoardType() == "ganglion": 287 | while board.ser_inWaiting(): 288 | board.waitForNotifications(0.001) 289 | 290 | if not flush: 291 | print(line) 292 | 293 | # Take user input 294 | #s = input('--> ') 295 | if sys.hexversion > 0x03000000: 296 | s = input('--> ') 297 | else: 298 | s = raw_input('--> ') 299 | --------------------------------------------------------------------------------