diff --git a/python/README.md b/python/README.md index 483d4ad..e3bdab3 100644 --- a/python/README.md +++ b/python/README.md @@ -1,57 +1,86 @@ -# Python Example - -## Requirement -- This example works with Python >= 3.7 -- Install websocket client via `pip install websocket-client` -- Install python-dispatch via `pip install python-dispatch` - -## Before you start - -To run the existing example you will need to do a few things. - -1. You will need an EMOTIV headset. You can purchase a headset in our [online - store](https://www.emotiv.com/) -2. Next, [download and install](https://www.emotiv.com/developer/) the Cortex - service. Please note that currently, the Cortex service is only available - for Windows and macOS. -3. We have updated our Terms of Use, Privacy Policy and EULA to comply with - GDPR. Please login via the EMOTIV Launcher to read and accept our latest policies - in order to proceed using the following examples. -4. Next, to get a client id and a client secret, you must connect to your - Emotiv account on - [emotiv.com](https://www.emotiv.com/my-account/cortex-apps/) and create a - Cortex app. If you don't have a EmotivID, you can [register - here](https://id.emotivcloud.com/eoidc/account/registration/). -5. Then, if you have not already, you will need to login with your Emotiv id in - the EMOTIV Launcher. -6. Finally, the first time you run these examples, you also need to authorize - them in the EMOTIV Launcher. - -This code is purely an example of how to work with Cortex. We strongly -recommend adjusting the code to your purposes. - -## Cortex Library -- [`cortex.py`](./cortex.py) - the wrapper lib around EMOTIV Cortex API. - -## Susbcribe Data -- [`sub_data.py`](./sub_data.py) shows data streaming from Cortex: EEG, motion, band power and Performance Metrics. -- For more details https://emotiv.gitbook.io/cortex-api/data-subscription - -## BCI -- [`mental_command_train.py`](./mental_command_train.py) shows Mental Command training. -- [`facial_expression_train.py`](./facial_expression_train.py) shows facial expression training. -- For more details https://emotiv.gitbook.io/cortex-api/bci - -## Advanced BCI -- [`live_advance.py`](./live_advance.py) shows the ability to get and set sensitivity of mental command action in live mode. -- For more details https://emotiv.gitbook.io/cortex-api/advanced-bci - -## Create record and export to file -- [`record.py`](./record.py) shows how to create record and export data to CSV or EDF format. -- For more details https://emotiv.gitbook.io/cortex-api/records - -## Inject marker while recording -- [`marker.py`](./marker.py) shows how to inject marker during a recording. -- For more details https://emotiv.gitbook.io/cortex-api/markers + +# Emotiv Cortex API Python Examples + +This repository provides a set of Python examples to help you get started with the [Emotiv Cortex API](https://emotiv.gitbook.io/cortex-api). Each script demonstrates a specific workflow, making it easier to understand and integrate Cortex API features into your own projects. + + +## Requirements + +- Python 2.7+ or Python 3.4+ +- Install dependencies: + - `pip install websocket-client` + - `pip install python-dispatch` + + +## Getting Started + +Before running the examples, please ensure you have completed the following steps: + +1. **Download and Install EMOTIV Launcher**: Download from [here](https://www.emotiv.com/products/emotiv-launcher). Log in with your Emotiv ID and accept the latest Terms of Use, Privacy Policy, and EULA in the Launcher. +2. **Accept Policies**: If prompted, accept any additional policies in the EMOTIV Launcher. +3. **Obtain an EMOTIV Headset or Create a Virtual Device**: + - Purchase a headset from the [EMOTIV online store](https://www.emotiv.com/), **or** + - Use a virtual headset in the EMOTIV Launcher by following [these instructions](https://emotiv.gitbook.io/emotiv-launcher/devices-setting-up-virtual-brainwear-r/creating-a-virtual-brainwear-device). +4. **Get Client ID & Secret**: Log in to your Emotiv account at [emotiv.com](https://www.emotiv.com/my-account/cortex-apps/) and create a Cortex app. [Register here](https://id.emotivcloud.com/eoidc/account/registration/) if you don't have an account. +5. **Authorize Examples**: The first time you run these examples, you may need to grant permission for your application to work with Emotiv Cortex. + +--- + +## Example Scripts Overview + +### 1. `cortex.py` — Cortex API Wrapper +Central wrapper class for the Cortex API. Handles: +- Opening and managing the websocket connection +- Buidling JSON-RPC requests +- Handling responses, errors, and emitting events to corresponding classes +- Parsing and dispatching data to workflow scripts + +### 2. `sub_data.py` — Subscribe to Data Streams +Demonstrates how to: +- Subscribe to data streams (EEG, motion, performance metrics, etc.) +- Print or process incoming data +See: [Data Subscription](https://emotiv.gitbook.io/cortex-api/data-subscription) + +### 3. `record.py` — Record and Export Data +Demonstrates how to: +- Create a new record +- Stop a record +- Export recorded data to CSV or EDF +See: [Records](https://emotiv.gitbook.io/cortex-api/records) + +### 4. `marker.py` — Inject Markers +Demonstrates how to: +- Inject markers into a record during data collection +- Export records with marker information +See: [Markers](https://emotiv.gitbook.io/cortex-api/markers) + + +### 5. `mental_command_train.py` — Mental Command Training +Demonstrates how to: +- Load or create a training profile +- Train mental command actions (e.g., neutral, push, pull) +See: [BCI](https://emotiv.gitbook.io/cortex-api/bci) + + +### 6. `facial_expression_train.py` — Facial Expression Training +Demonstrates how to: +- Load or create a training profile +- Train facial expression actions (e.g., neutral, surprise, smile) +See: [BCI](https://emotiv.gitbook.io/cortex-api/bci) + +### 7. `live_advance.py` — Advanced Live Data & Sensitivity +Demonstrates how to: +- Load a trained profile +- Subscribe to the 'com' stream for live mental command data +- (Optionally) Subscribe to the 'fac' stream for live facial expression data +- Get and set sensitivity for mental command actions in live mode +See: [Advanced BCI](https://emotiv.gitbook.io/cortex-api/advanced-bci) + +--- + +## Tips +- Each script is self-contained and demonstrates a specific workflow. +- Adjust the code as needed for your own applications. +- For more details, refer to the [official Cortex API documentation](https://emotiv.gitbook.io/cortex-api/). diff --git a/python/cortex.py b/python/cortex.py index 4dedaf8..27685bb 100644 --- a/python/cortex.py +++ b/python/cortex.py @@ -1,13 +1,31 @@ -import websocket #'pip install websocket-client' for install -from datetime import datetime -import json -import ssl -import time import sys -from pydispatch import Dispatcher import warnings -import threading +# --- BEGIN: Simplified environment checks --- +# 1. Check Python version +if sys.version_info < (2, 7) or (3, 0) <= sys.version_info < (3, 4): + print(f"[ERROR] Python 2.7+ or 3.4+ is required. You are using Python {sys.version_info.major}.{sys.version_info.minor}.", file=sys.stderr) + sys.exit(1) + +# 2. Check websocket-client +try: + import websocket +except ImportError: + print(f"[ERROR] Required library 'websocket-client' is not installed. Please run: {sys.executable} -m pip install websocket-client", file=sys.stderr) + sys.exit(1) + +# 3. Check python-dispatch +try: + from pydispatch import Dispatcher # needed for class inheritance +except ImportError: + print(f"[ERROR] Required library 'python-dispatch' is not installed. Please run: {sys.executable} -m pip install python-dispatch", file=sys.stderr) + sys.exit(1) +# --- END: Simplified environment checks --- +import threading +import ssl +import time +import json +from datetime import datetime # define request id QUERY_HEADSET_ID = 1 @@ -92,7 +110,7 @@ def __init__(self, client_id, client_secret, debug_mode=False, **kwargs): if key == 'license': self.license = value elif key == 'debit': - self.debit == value + self.debit = value elif key == 'headset_id': self.headset_id = value @@ -104,24 +122,24 @@ def open(self): on_open = self.on_open, on_error=self.on_error, on_close=self.on_close) - threadName = "WebsockThread:-{:%Y%m%d%H%M%S}".format(datetime.utcnow()) + thread_name = "WebsockThread:-{:%Y%m%d%H%M%S}".format(datetime.now()) # As default, a Emotiv self-signed certificate is required. # If you don't want to use the certificate, please replace by the below line by sslopt={"cert_reqs": ssl.CERT_NONE} sslopt = {'ca_certs': "../certificates/rootCA.pem", "cert_reqs": ssl.CERT_REQUIRED} - self.websock_thread = threading.Thread(target=self.ws.run_forever, args=(None, sslopt), name=threadName) + self.websock_thread = threading.Thread(target=self.ws.run_forever, args=(None, sslopt), name=thread_name) self.websock_thread .start() self.websock_thread.join() def close(self): self.ws.close() - def set_wanted_headset(self, headsetId): - self.headset_id = headsetId + def set_wanted_headset(self, headset_id): + self.headset_id = headset_id - def set_wanted_profile(self, profileName): - self.profile_name = profileName + def set_wanted_profile(self, profile_name): + self.profile_name = profile_name def on_open(self, *args, **kwargs): print("websocket opened") @@ -305,7 +323,7 @@ def handle_result(self, recv_dic): self.emit('export_record_done', data=success_export) elif req_id == INJECT_MARKER_REQUEST_ID: self.emit('inject_marker_done', data=result_dic['marker']) - elif req_id == INJECT_MARKER_REQUEST_ID: + elif req_id == UPDATE_MARKER_REQUEST_ID: self.emit('update_marker_done', data=result_dic['marker']) else: print('No handling for response of request ' + str(req_id)) @@ -784,11 +802,11 @@ def inject_marker_request(self, time, value, label, **kwargs): print('inject marker request \n', json.dumps(inject_marker_request, indent=4)) self.ws.send(json.dumps(inject_marker_request)) - def update_marker_request(self, markerId, time, **kwargs): + def update_marker_request(self, marker_id, time, **kwargs): print('update marker --------------------------------') params_val = {"cortexToken": self.auth, "session": self.session_id, - "markerId": markerId, + "markerId": marker_id, "time": time} for key, value in kwargs.items(): diff --git a/python/live_advance.py b/python/live_advance.py index cfaf052..69eab76 100644 --- a/python/live_advance.py +++ b/python/live_advance.py @@ -37,7 +37,7 @@ def __init__(self, app_client_id, app_client_secret, **kwargs): self.c.bind(mc_action_sensitivity_done=self.on_mc_action_sensitivity_done) self.c.bind(inform_error=self.on_inform_error) - def start(self, profile_name, headsetId=''): + def start(self, profile_name, headset_id=''): """ To start live process as below workflow (1) check access right -> authorize -> connect headset->create session @@ -48,9 +48,9 @@ def start(self, profile_name, headsetId=''): ---------- profile_name : string, required name of profile - headsetId: string , optional + headset_id: string , optional id of wanted headet which you want to work with it. - If the headsetId is empty, the first headset in list will be set as wanted headset + If the headset_id is empty, the first headset in list will be set as wanted headset Returns ------- None @@ -61,8 +61,8 @@ def start(self, profile_name, headsetId=''): self.profile_name = profile_name self.c.set_wanted_profile(profile_name) - if headsetId != '': - self.c.set_wanted_headset(headsetId) + if headset_id != '': + self.c.set_wanted_headset(headset_id) self.c.open() diff --git a/python/marker.py b/python/marker.py index b8bd07e..a6b535c 100644 --- a/python/marker.py +++ b/python/marker.py @@ -8,12 +8,12 @@ def __init__(self, app_client_id, app_client_secret, **kwargs): self.c.bind(create_session_done=self.on_create_session_done) self.c.bind(create_record_done=self.on_create_record_done) self.c.bind(stop_record_done=self.on_stop_record_done) - self.c.bind(warn_cortex_stop_all_sub=self.on_warn_cortex_stop_all_sub) self.c.bind(inject_marker_done=self.on_inject_marker_done) self.c.bind(export_record_done=self.on_export_record_done) self.c.bind(inform_error=self.on_inform_error) + self.c.bind(warn_record_post_processing_done=self.on_warn_record_post_processing_done) - def start(self, number_markers=10, headsetId=''): + def start(self, number_markers=10, headset_id=''): """ To start data recording and inject marker process as below workflow (1) check access right -> authorize -> connect headset->create session @@ -23,9 +23,9 @@ def start(self, number_markers=10, headsetId=''): number_markers: int, required number of markers - headsetId: string , optional + headset_id: string , optional id of wanted headet which you want to work with it. - If the headsetId is empty, the first headset in list will be set as wanted headset + If the headset_id is empty, the first headset in list will be set as wanted headset Returns ------- None @@ -33,8 +33,8 @@ def start(self, number_markers=10, headsetId=''): self.number_markers = number_markers self.marker_idx = 0 - if headsetId != '': - self.c.set_wanted_headset(headsetId) + if headset_id != '': + self.c.set_wanted_headset(headset_id) self.c.open() @@ -96,7 +96,7 @@ def inject_marker(self, time, value, label, **kwargs): """ self.c.inject_marker_request(time, value, label, **kwargs) - def update_marker(self, markerId, time, **kwargs): + def update_marker(self, marker_id, time, **kwargs): """ To update a marker that was previously created by inject_marker Parameters @@ -106,7 +106,7 @@ def update_marker(self, markerId, time, **kwargs): ------- None """ - self.c.update_marker_request(markerId, time, **kwargs) + self.c.update_marker_request(marker_id, time, **kwargs) # callbacks functions def on_create_session_done(self, *args, **kwargs): @@ -136,12 +136,7 @@ def on_stop_record_done(self, *args, **kwargs): title = data['title'] print('on_stop_record_done: recordId: {0}, title: {1}, startTime: {2}, endTime: {3}'.format(record_id, title, start_time, end_time)) - # disconnect headset to export record - print('on_stop_record_done: Disconnect the headset to export record') - self.c.disconnect_headset() - def on_inject_marker_done(self, *args, **kwargs): - data = kwargs.get('data') marker_id = data['uuid'] start_time = data['startDatetime'] @@ -153,14 +148,6 @@ def on_inject_marker_done(self, *args, **kwargs): # stop record self.stop_record() - def on_warn_cortex_stop_all_sub(self, *args, **kwargs): - print('on_warn_cortex_stop_all_sub') - # cortex has closed session. Wait some seconds before exporting record - time.sleep(3) - - self.export_record(self.record_export_folder, self.record_export_data_types, - self.record_export_format, [self.record_id], self.record_export_version) - def on_export_record_done(self, *args, **kwargs): print('on_export_record_done') data = kwargs.get('data') @@ -170,6 +157,15 @@ def on_export_record_done(self, *args, **kwargs): def on_inform_error(self, *args, **kwargs): error_data = kwargs.get('error_data') print(error_data) + + def on_warn_record_post_processing_done(self, *args, **kwargs): + record_id = kwargs.get('data') + print('on_warn_record_post_processing_done: The record ', record_id, 'has been post-processed. Now, you can export the record') + + # you must stop the record before you can export it. + # if you want to export a record immediately after you stop it then you must wait for the warning 30 before you try to export. + self.export_record(self.record_export_folder, self.record_export_data_types, + self.record_export_format, [record_id], self.record_export_version) # ----------------------------------------------------------- diff --git a/python/mental_command_train.py b/python/mental_command_train.py index eab6b8b..728445e 100644 --- a/python/mental_command_train.py +++ b/python/mental_command_train.py @@ -50,7 +50,7 @@ def __init__(self, app_client_id, app_client_secret, **kwargs): self.c.bind(new_sys_data=self.on_new_sys_data) self.c.bind(inform_error=self.on_inform_error) - def start(self, profile_name, actions, headsetId=''): + def start(self, profile_name, actions, headset_id=''): """ To start training process as below workflow (1) check access right -> authorize -> connect headset->create session @@ -62,9 +62,9 @@ def start(self, profile_name, actions, headsetId=''): name of profile actions : list, required list of actions which will be trained - headsetId: string , optional + headset_id: string , optional id of wanted headet which you want to work with it. - If the headsetId is empty, the first headset in list will be set as wanted headset + If the headset_id is empty, the first headset in list will be set as wanted headset Returns ------- None @@ -78,8 +78,8 @@ def start(self, profile_name, actions, headsetId=''): self.c.set_wanted_profile(profile_name) - if headsetId != '': - self.c.set_wanted_headset(headsetId) + if headset_id != '': + self.c.set_wanted_headset(headset_id) self.c.open() @@ -154,7 +154,7 @@ def get_active_action(self, profile_name): def get_command_brain_map(self, profile_name): self.c.get_mental_command_brain_map(profile_name) - def get_training_threshold(self): + def get_training_threshold(self, profile_name): self.c.get_mental_command_training_threshold(profile_name) def train_mc_action(self, status): diff --git a/python/sub_data.py b/python/sub_data.py index b7d56cf..fba3347 100644 --- a/python/sub_data.py +++ b/python/sub_data.py @@ -44,7 +44,7 @@ def __init__(self, app_client_id, app_client_secret, **kwargs): self.c.bind(new_pow_data=self.on_new_pow_data) self.c.bind(inform_error=self.on_inform_error) - def start(self, streams, headsetId=''): + def start(self, streams, headset_id=''): """ To start data subscribing process as below workflow (1)check access right -> authorize -> connect headset->create session @@ -60,17 +60,17 @@ def start(self, streams, headsetId=''): ---------- streams : list, required list of streams. For example, ['eeg', 'mot'] - headsetId: string , optional + headset_id: string , optional id of wanted headet which you want to work with it. - If the headsetId is empty, the first headset in list will be set as wanted headset + If the headset_id is empty, the first headset in list will be set as wanted headset Returns ------- None """ self.streams = streams - if headsetId != '': - self.c.set_wanted_headset(headsetId) + if headset_id != '': + self.c.set_wanted_headset(headset_id) self.c.open()