Skip to content

Commit 5d8e9b0

Browse files
authored
Cor 5226: update python example (#226)
* COR-5226: add python version and module importing checking * COR-5221: dont disconnect headset to export record * COR-5226: update readme for python example
1 parent f97a561 commit 5d8e9b0

6 files changed

Lines changed: 152 additions & 109 deletions

File tree

python/README.md

Lines changed: 84 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,86 @@
1-
# Python Example
2-
3-
## Requirement
4-
- This example works with Python >= 3.7
5-
- Install websocket client via `pip install websocket-client`
6-
- Install python-dispatch via `pip install python-dispatch`
7-
8-
## Before you start
9-
10-
To run the existing example you will need to do a few things.
11-
12-
1. You will need an EMOTIV headset. You can purchase a headset in our [online
13-
store](https://www.emotiv.com/)
14-
2. Next, [download and install](https://www.emotiv.com/developer/) the Cortex
15-
service. Please note that currently, the Cortex service is only available
16-
for Windows and macOS.
17-
3. We have updated our Terms of Use, Privacy Policy and EULA to comply with
18-
GDPR. Please login via the EMOTIV Launcher to read and accept our latest policies
19-
in order to proceed using the following examples.
20-
4. Next, to get a client id and a client secret, you must connect to your
21-
Emotiv account on
22-
[emotiv.com](https://www.emotiv.com/my-account/cortex-apps/) and create a
23-
Cortex app. If you don't have a EmotivID, you can [register
24-
here](https://id.emotivcloud.com/eoidc/account/registration/).
25-
5. Then, if you have not already, you will need to login with your Emotiv id in
26-
the EMOTIV Launcher.
27-
6. Finally, the first time you run these examples, you also need to authorize
28-
them in the EMOTIV Launcher.
29-
30-
This code is purely an example of how to work with Cortex. We strongly
31-
recommend adjusting the code to your purposes.
32-
33-
## Cortex Library
34-
- [`cortex.py`](./cortex.py) - the wrapper lib around EMOTIV Cortex API.
35-
36-
## Susbcribe Data
37-
- [`sub_data.py`](./sub_data.py) shows data streaming from Cortex: EEG, motion, band power and Performance Metrics.
38-
- For more details https://emotiv.gitbook.io/cortex-api/data-subscription
39-
40-
## BCI
41-
- [`mental_command_train.py`](./mental_command_train.py) shows Mental Command training.
42-
- [`facial_expression_train.py`](./facial_expression_train.py) shows facial expression training.
43-
- For more details https://emotiv.gitbook.io/cortex-api/bci
44-
45-
## Advanced BCI
46-
- [`live_advance.py`](./live_advance.py) shows the ability to get and set sensitivity of mental command action in live mode.
47-
- For more details https://emotiv.gitbook.io/cortex-api/advanced-bci
48-
49-
## Create record and export to file
50-
- [`record.py`](./record.py) shows how to create record and export data to CSV or EDF format.
51-
- For more details https://emotiv.gitbook.io/cortex-api/records
52-
53-
## Inject marker while recording
54-
- [`marker.py`](./marker.py) shows how to inject marker during a recording.
55-
- For more details https://emotiv.gitbook.io/cortex-api/markers
1+
2+
# Emotiv Cortex API Python Examples
3+
4+
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.
5+
6+
7+
## Requirements
8+
9+
- Python 2.7+ or Python 3.4+
10+
- Install dependencies:
11+
- `pip install websocket-client`
12+
- `pip install python-dispatch`
13+
14+
15+
## Getting Started
16+
17+
Before running the examples, please ensure you have completed the following steps:
18+
19+
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.
20+
2. **Accept Policies**: If prompted, accept any additional policies in the EMOTIV Launcher.
21+
3. **Obtain an EMOTIV Headset or Create a Virtual Device**:
22+
- Purchase a headset from the [EMOTIV online store](https://www.emotiv.com/), **or**
23+
- 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).
24+
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.
25+
5. **Authorize Examples**: The first time you run these examples, you may need to grant permission for your application to work with Emotiv Cortex.
26+
27+
---
28+
29+
## Example Scripts Overview
30+
31+
### 1. `cortex.py` — Cortex API Wrapper
32+
Central wrapper class for the Cortex API. Handles:
33+
- Opening and managing the websocket connection
34+
- Buidling JSON-RPC requests
35+
- Handling responses, errors, and emitting events to corresponding classes
36+
- Parsing and dispatching data to workflow scripts
37+
38+
### 2. `sub_data.py` — Subscribe to Data Streams
39+
Demonstrates how to:
40+
- Subscribe to data streams (EEG, motion, performance metrics, etc.)
41+
- Print or process incoming data
42+
See: [Data Subscription](https://emotiv.gitbook.io/cortex-api/data-subscription)
43+
44+
### 3. `record.py` — Record and Export Data
45+
Demonstrates how to:
46+
- Create a new record
47+
- Stop a record
48+
- Export recorded data to CSV or EDF
49+
See: [Records](https://emotiv.gitbook.io/cortex-api/records)
50+
51+
### 4. `marker.py` — Inject Markers
52+
Demonstrates how to:
53+
- Inject markers into a record during data collection
54+
- Export records with marker information
55+
See: [Markers](https://emotiv.gitbook.io/cortex-api/markers)
56+
57+
58+
### 5. `mental_command_train.py` — Mental Command Training
59+
Demonstrates how to:
60+
- Load or create a training profile
61+
- Train mental command actions (e.g., neutral, push, pull)
62+
See: [BCI](https://emotiv.gitbook.io/cortex-api/bci)
63+
64+
65+
### 6. `facial_expression_train.py` — Facial Expression Training
66+
Demonstrates how to:
67+
- Load or create a training profile
68+
- Train facial expression actions (e.g., neutral, surprise, smile)
69+
See: [BCI](https://emotiv.gitbook.io/cortex-api/bci)
70+
71+
### 7. `live_advance.py` — Advanced Live Data & Sensitivity
72+
Demonstrates how to:
73+
- Load a trained profile
74+
- Subscribe to the 'com' stream for live mental command data
75+
- (Optionally) Subscribe to the 'fac' stream for live facial expression data
76+
- Get and set sensitivity for mental command actions in live mode
77+
See: [Advanced BCI](https://emotiv.gitbook.io/cortex-api/advanced-bci)
78+
79+
---
80+
81+
## Tips
82+
- Each script is self-contained and demonstrates a specific workflow.
83+
- Adjust the code as needed for your own applications.
84+
- For more details, refer to the [official Cortex API documentation](https://emotiv.gitbook.io/cortex-api/).
5685

5786

python/cortex.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
1-
import websocket #'pip install websocket-client' for install
2-
from datetime import datetime
3-
import json
4-
import ssl
5-
import time
61
import sys
7-
from pydispatch import Dispatcher
82
import warnings
9-
import threading
3+
# --- BEGIN: Simplified environment checks ---
4+
# 1. Check Python version
5+
if sys.version_info < (2, 7) or (3, 0) <= sys.version_info < (3, 4):
6+
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)
7+
sys.exit(1)
8+
9+
# 2. Check websocket-client
10+
try:
11+
import websocket
12+
except ImportError:
13+
print(f"[ERROR] Required library 'websocket-client' is not installed. Please run: {sys.executable} -m pip install websocket-client", file=sys.stderr)
14+
sys.exit(1)
15+
16+
# 3. Check python-dispatch
17+
try:
18+
from pydispatch import Dispatcher # needed for class inheritance
19+
except ImportError:
20+
print(f"[ERROR] Required library 'python-dispatch' is not installed. Please run: {sys.executable} -m pip install python-dispatch", file=sys.stderr)
21+
sys.exit(1)
22+
# --- END: Simplified environment checks ---
1023

24+
import threading
25+
import ssl
26+
import time
27+
import json
28+
from datetime import datetime
1129

1230
# define request id
1331
QUERY_HEADSET_ID = 1
@@ -92,7 +110,7 @@ def __init__(self, client_id, client_secret, debug_mode=False, **kwargs):
92110
if key == 'license':
93111
self.license = value
94112
elif key == 'debit':
95-
self.debit == value
113+
self.debit = value
96114
elif key == 'headset_id':
97115
self.headset_id = value
98116

@@ -104,24 +122,24 @@ def open(self):
104122
on_open = self.on_open,
105123
on_error=self.on_error,
106124
on_close=self.on_close)
107-
threadName = "WebsockThread:-{:%Y%m%d%H%M%S}".format(datetime.utcnow())
125+
thread_name = "WebsockThread:-{:%Y%m%d%H%M%S}".format(datetime.now())
108126

109127
# As default, a Emotiv self-signed certificate is required.
110128
# If you don't want to use the certificate, please replace by the below line by sslopt={"cert_reqs": ssl.CERT_NONE}
111129
sslopt = {'ca_certs': "../certificates/rootCA.pem", "cert_reqs": ssl.CERT_REQUIRED}
112130

113-
self.websock_thread = threading.Thread(target=self.ws.run_forever, args=(None, sslopt), name=threadName)
131+
self.websock_thread = threading.Thread(target=self.ws.run_forever, args=(None, sslopt), name=thread_name)
114132
self.websock_thread .start()
115133
self.websock_thread.join()
116134

117135
def close(self):
118136
self.ws.close()
119137

120-
def set_wanted_headset(self, headsetId):
121-
self.headset_id = headsetId
138+
def set_wanted_headset(self, headset_id):
139+
self.headset_id = headset_id
122140

123-
def set_wanted_profile(self, profileName):
124-
self.profile_name = profileName
141+
def set_wanted_profile(self, profile_name):
142+
self.profile_name = profile_name
125143

126144
def on_open(self, *args, **kwargs):
127145
print("websocket opened")
@@ -305,7 +323,7 @@ def handle_result(self, recv_dic):
305323
self.emit('export_record_done', data=success_export)
306324
elif req_id == INJECT_MARKER_REQUEST_ID:
307325
self.emit('inject_marker_done', data=result_dic['marker'])
308-
elif req_id == INJECT_MARKER_REQUEST_ID:
326+
elif req_id == UPDATE_MARKER_REQUEST_ID:
309327
self.emit('update_marker_done', data=result_dic['marker'])
310328
else:
311329
print('No handling for response of request ' + str(req_id))
@@ -784,11 +802,11 @@ def inject_marker_request(self, time, value, label, **kwargs):
784802
print('inject marker request \n', json.dumps(inject_marker_request, indent=4))
785803
self.ws.send(json.dumps(inject_marker_request))
786804

787-
def update_marker_request(self, markerId, time, **kwargs):
805+
def update_marker_request(self, marker_id, time, **kwargs):
788806
print('update marker --------------------------------')
789807
params_val = {"cortexToken": self.auth,
790808
"session": self.session_id,
791-
"markerId": markerId,
809+
"markerId": marker_id,
792810
"time": time}
793811

794812
for key, value in kwargs.items():

python/live_advance.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def __init__(self, app_client_id, app_client_secret, **kwargs):
3737
self.c.bind(mc_action_sensitivity_done=self.on_mc_action_sensitivity_done)
3838
self.c.bind(inform_error=self.on_inform_error)
3939

40-
def start(self, profile_name, headsetId=''):
40+
def start(self, profile_name, headset_id=''):
4141
"""
4242
To start live process as below workflow
4343
(1) check access right -> authorize -> connect headset->create session
@@ -48,9 +48,9 @@ def start(self, profile_name, headsetId=''):
4848
----------
4949
profile_name : string, required
5050
name of profile
51-
headsetId: string , optional
51+
headset_id: string , optional
5252
id of wanted headet which you want to work with it.
53-
If the headsetId is empty, the first headset in list will be set as wanted headset
53+
If the headset_id is empty, the first headset in list will be set as wanted headset
5454
Returns
5555
-------
5656
None
@@ -61,8 +61,8 @@ def start(self, profile_name, headsetId=''):
6161
self.profile_name = profile_name
6262
self.c.set_wanted_profile(profile_name)
6363

64-
if headsetId != '':
65-
self.c.set_wanted_headset(headsetId)
64+
if headset_id != '':
65+
self.c.set_wanted_headset(headset_id)
6666

6767
self.c.open()
6868

python/marker.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ def __init__(self, app_client_id, app_client_secret, **kwargs):
88
self.c.bind(create_session_done=self.on_create_session_done)
99
self.c.bind(create_record_done=self.on_create_record_done)
1010
self.c.bind(stop_record_done=self.on_stop_record_done)
11-
self.c.bind(warn_cortex_stop_all_sub=self.on_warn_cortex_stop_all_sub)
1211
self.c.bind(inject_marker_done=self.on_inject_marker_done)
1312
self.c.bind(export_record_done=self.on_export_record_done)
1413
self.c.bind(inform_error=self.on_inform_error)
14+
self.c.bind(warn_record_post_processing_done=self.on_warn_record_post_processing_done)
1515

16-
def start(self, number_markers=10, headsetId=''):
16+
def start(self, number_markers=10, headset_id=''):
1717
"""
1818
To start data recording and inject marker process as below workflow
1919
(1) check access right -> authorize -> connect headset->create session
@@ -23,18 +23,18 @@ def start(self, number_markers=10, headsetId=''):
2323
number_markers: int, required
2424
number of markers
2525
26-
headsetId: string , optional
26+
headset_id: string , optional
2727
id of wanted headet which you want to work with it.
28-
If the headsetId is empty, the first headset in list will be set as wanted headset
28+
If the headset_id is empty, the first headset in list will be set as wanted headset
2929
Returns
3030
-------
3131
None
3232
"""
3333
self.number_markers = number_markers
3434
self.marker_idx = 0
3535

36-
if headsetId != '':
37-
self.c.set_wanted_headset(headsetId)
36+
if headset_id != '':
37+
self.c.set_wanted_headset(headset_id)
3838

3939
self.c.open()
4040

@@ -96,7 +96,7 @@ def inject_marker(self, time, value, label, **kwargs):
9696
"""
9797
self.c.inject_marker_request(time, value, label, **kwargs)
9898

99-
def update_marker(self, markerId, time, **kwargs):
99+
def update_marker(self, marker_id, time, **kwargs):
100100
"""
101101
To update a marker that was previously created by inject_marker
102102
Parameters
@@ -106,7 +106,7 @@ def update_marker(self, markerId, time, **kwargs):
106106
-------
107107
None
108108
"""
109-
self.c.update_marker_request(markerId, time, **kwargs)
109+
self.c.update_marker_request(marker_id, time, **kwargs)
110110

111111
# callbacks functions
112112
def on_create_session_done(self, *args, **kwargs):
@@ -136,12 +136,7 @@ def on_stop_record_done(self, *args, **kwargs):
136136
title = data['title']
137137
print('on_stop_record_done: recordId: {0}, title: {1}, startTime: {2}, endTime: {3}'.format(record_id, title, start_time, end_time))
138138

139-
# disconnect headset to export record
140-
print('on_stop_record_done: Disconnect the headset to export record')
141-
self.c.disconnect_headset()
142-
143139
def on_inject_marker_done(self, *args, **kwargs):
144-
145140
data = kwargs.get('data')
146141
marker_id = data['uuid']
147142
start_time = data['startDatetime']
@@ -153,14 +148,6 @@ def on_inject_marker_done(self, *args, **kwargs):
153148
# stop record
154149
self.stop_record()
155150

156-
def on_warn_cortex_stop_all_sub(self, *args, **kwargs):
157-
print('on_warn_cortex_stop_all_sub')
158-
# cortex has closed session. Wait some seconds before exporting record
159-
time.sleep(3)
160-
161-
self.export_record(self.record_export_folder, self.record_export_data_types,
162-
self.record_export_format, [self.record_id], self.record_export_version)
163-
164151
def on_export_record_done(self, *args, **kwargs):
165152
print('on_export_record_done')
166153
data = kwargs.get('data')
@@ -170,6 +157,15 @@ def on_export_record_done(self, *args, **kwargs):
170157
def on_inform_error(self, *args, **kwargs):
171158
error_data = kwargs.get('error_data')
172159
print(error_data)
160+
161+
def on_warn_record_post_processing_done(self, *args, **kwargs):
162+
record_id = kwargs.get('data')
163+
print('on_warn_record_post_processing_done: The record ', record_id, 'has been post-processed. Now, you can export the record')
164+
165+
# you must stop the record before you can export it.
166+
# 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.
167+
self.export_record(self.record_export_folder, self.record_export_data_types,
168+
self.record_export_format, [record_id], self.record_export_version)
173169

174170

175171
# -----------------------------------------------------------

0 commit comments

Comments
 (0)