From 428a86709beef9bab73cd59fce5a9e68f74e39e8 Mon Sep 17 00:00:00 2001 From: Nguyen Tung Date: Tue, 8 Jul 2025 06:33:14 +0700 Subject: [PATCH 1/3] COR-5318: update export feature and readme for csharp --- csharp/CortexAccess/Config.cs | 5 +- csharp/CortexAccess/CtxClient.cs | 140 +++++++++++++++-------- csharp/CortexAccess/DataStreamExample.cs | 20 +++- csharp/CortexAccess/HeadsetFinder.cs | 4 + csharp/CortexAccess/RecordManager.cs | 87 +++++++++++--- csharp/CortexAccess/Training.cs | 36 +++++- csharp/EEGLogger/Program.cs | 3 +- csharp/InjectMarker/Program.cs | 2 +- csharp/MotionLogger/Program.cs | 3 +- csharp/PMLogger/Program.cs | 5 +- csharp/README.md | 112 ++++++------------ csharp/RecordData/Program.cs | 62 +++++++++- 12 files changed, 325 insertions(+), 154 deletions(-) diff --git a/csharp/CortexAccess/Config.cs b/csharp/CortexAccess/Config.cs index 9c54dcd..de71066 100644 --- a/csharp/CortexAccess/Config.cs +++ b/csharp/CortexAccess/Config.cs @@ -7,8 +7,8 @@ static class Config * account on emotiv.com and create a Cortex app. * https://www.emotiv.com/my-account/cortex-apps/ */ - public static string AppClientId = "The client id of your Cortex app goes here"; - public static string AppClientSecret = "The client secret of your Cortex app goes here"; + public static string AppClientId = "put_your_application_client_id_here"; + public static string AppClientSecret = "put_your_application_client_secret_here"; // If you use an Epoc Flex headset, then you must put your configuration here public static string FlexMapping = @"{ @@ -38,6 +38,7 @@ public static class WarningCode public const int UserLoginOnAnotherOsUser = 16; public const int EULAAccepted = 17; public const int StreamWritingClosed = 18; + public const int DataPostProcessingFinished = 30; public const int HeadsetWrongInformation = 100; public const int HeadsetCannotConnected = 101; public const int HeadsetConnectingTimeout = 102; diff --git a/csharp/CortexAccess/CtxClient.cs b/csharp/CortexAccess/CtxClient.cs index b7df86d..13f5856 100644 --- a/csharp/CortexAccess/CtxClient.cs +++ b/csharp/CortexAccess/CtxClient.cs @@ -117,6 +117,7 @@ public sealed class CortexClient public event EventHandler OnUpdateRecord; public event EventHandler> OnQueryRecords; public event EventHandler OnDeleteRecords; + public event EventHandler ExportRecordsFinished; public event EventHandler OnInjectMarker; public event EventHandler OnUpdateMarker; public event EventHandler OnGetDetectionInfo; @@ -133,6 +134,7 @@ public sealed class CortexClient public event EventHandler SessionClosedNotify; public event EventHandler HeadsetConnectNotify; public event EventHandler HeadsetScanFinished; + public event EventHandler DataPostProcessingFinished; // Constructor static CortexClient() @@ -425,6 +427,13 @@ private void HandleResponse(string method, JToken data) { OnGetTrainingTime(this, (double)data["time"]); } + // export record + else if (method == "exportRecord") + { + JArray successList = (JArray)data["success"]; + JArray failList = (JArray)data["failure"]; + ExportRecordsFinished(this, new MultipleResultEventArgs(successList, failList)); + } } @@ -432,65 +441,58 @@ private void HandleResponse(string method, JToken data) private void HandleWarning(int code, JToken messageData) { Console.WriteLine("handleWarning: " + code); - if (code == WarningCode.AccessRightGranted) + switch (code) { - // granted access right + case WarningCode.AccessRightGranted: OnAccessRightGranted(this, true); - } - else if (code == WarningCode.AccessRightRejected) - { + break; + case WarningCode.AccessRightRejected: OnAccessRightGranted(this, false); - } - else if (code == WarningCode.EULAAccepted) - { + break; + case WarningCode.EULAAccepted: OnEULAAccepted(this, true); - } - else if (code == WarningCode.UserLogin) - { - string message = messageData.ToString(); - OnUserLogin(this, message); - } - else if (code == WarningCode.UserLogout) - { - string message = messageData.ToString(); - OnUserLogout(this, message); - } - else if (code == WarningCode.HeadsetScanFinished) - { - string message = messageData["behavior"].ToString(); - HeadsetScanFinished(this, message); - } - else if (code == WarningCode.StreamStop) - { - string sessionId = messageData["sessionId"].ToString(); - SessionClosedNotify(this, sessionId); - } - else if (code == WarningCode.SessionAutoClosed) - { - string sessionId = messageData["sessionId"].ToString(); - SessionClosedNotify(this, sessionId); - } - else if (code == WarningCode.HeadsetConnected) - { + break; + case WarningCode.UserLogin: + OnUserLogin(this, messageData.ToString()); + break; + case WarningCode.UserLogout: + OnUserLogout(this, messageData.ToString()); + break; + case WarningCode.HeadsetScanFinished: + HeadsetScanFinished(this, messageData["behavior"].ToString()); + break; + case WarningCode.StreamStop: + case WarningCode.SessionAutoClosed: + SessionClosedNotify(this, messageData["sessionId"].ToString()); + break; + case WarningCode.HeadsetConnected: + { string headsetId = messageData["headsetId"].ToString(); string message = messageData["behavior"].ToString(); Console.WriteLine("handleWarning:" + message); HeadsetConnectNotify(this, new HeadsetConnectEventArgs(true, message, headsetId)); - } - else if (code == WarningCode.HeadsetWrongInformation || - code == WarningCode.HeadsetCannotConnected || - code == WarningCode.HeadsetConnectingTimeout) - { + break; + } + case WarningCode.HeadsetWrongInformation: + case WarningCode.HeadsetCannotConnected: + case WarningCode.HeadsetConnectingTimeout: + { string headsetId = messageData["headsetId"].ToString(); string message = messageData["behavior"].ToString(); HeadsetConnectNotify(this, new HeadsetConnectEventArgs(false, message, headsetId)); - } - else if (code == WarningCode.CortexAutoUnloadProfile) - { - // the current profile is unloaded automatically + break; + } + case WarningCode.CortexAutoUnloadProfile: OnUnloadProfile(this, true); + break; + case WarningCode.DataPostProcessingFinished: + Console.WriteLine("Data post processing finished"); + DataPostProcessingFinished(this, messageData["recordId"].ToString()); + break; + default: + Console.WriteLine("Warning code: " + code + ", message: " + messageData.ToString()); + break; } - } private void WebSocketClient_Closed(object sender, EventArgs e) { @@ -726,6 +728,52 @@ public void DeleteRecord(string cortexToken, List records) SendTextMessage(param, "deleteRecord", true); } + // Export Records + // Required params: cortexToken, records, folderPath, streamTypes, format + public void ExportRecord(string cortexToken, List records, string folderPath, + List streamTypes, string format, string version = null, + List licenseIds = null, bool includeDemographics = false, + bool includeMarkerExtraInfos = false, bool includeSurvey = false, + bool includeDeprecatedPM = false) + { + JObject param = new JObject(); + param.Add("recordIds", JArray.FromObject(records)); + param.Add("cortexToken", cortexToken); + param.Add("folder", folderPath); + param.Add("streamTypes", JArray.FromObject(streamTypes)); + param.Add("format", format); // EDF, CSV, EDFPLUS, BDFPLUS + + // If the format is "EDF", then you must omit version parameter. + // If the format is "CSV", then version parameter must be "V1" or "V2". + if (version != null) + { + param.Add("version", version); + } + if (licenseIds != null) + { + param.Add("licenseIds", JArray.FromObject(licenseIds)); + } + + if (includeDemographics) + { + param.Add("includeDemographics", includeDemographics); + } + if (includeMarkerExtraInfos) + { + param.Add("includeMarkerExtraInfos", includeMarkerExtraInfos); + } + if (includeSurvey) + { + param.Add("includeSurvey", includeSurvey); + } + if (includeDeprecatedPM) + { + param.Add("includeDeprecatedPM", includeDeprecatedPM); + } + + SendTextMessage(param, "exportRecord", true); + } + // InjectMarker // Required params: session, cortexToken, label, value, time public void InjectMarker(string cortexToken, string sessionId, string label, JToken value, double time, string port = null) diff --git a/csharp/CortexAccess/DataStreamExample.cs b/csharp/CortexAccess/DataStreamExample.cs index 40c77e6..9c69034 100644 --- a/csharp/CortexAccess/DataStreamExample.cs +++ b/csharp/CortexAccess/DataStreamExample.cs @@ -194,7 +194,12 @@ private void MessageErrorRecieved(object sender, ErrorMsgEventArgs e) Console.WriteLine("MessageErrorRecieved :code " + e.Code + " message " + e.MessageError); } - // set Streams + /// + /// Add a data stream to the subscription list. + /// Call this before Start(). + /// Example: AddStreams("eeg"), AddStreams("mot"), etc. + /// + /// Stream name (e.g., "eeg", "mot", "pow", "met") public void AddStreams(string stream) { if (!_streams.Contains(stream)) @@ -202,7 +207,13 @@ public void AddStreams(string stream) _streams.Add(stream); } } - // start + /// + /// Start the workflow: authorization, headset finding, session creation, and data stream subscription. + /// You must call AddStreams() for each stream you want to subscribe before calling Start(). + /// + /// (Obsolete) This parameter is kept for backward compatibility. Always set to empty string "". + /// True to use a license-required session (needed for EEG, high performance metrics, etc.) + /// Specify headset ID to connect (default: first headset found) public void Start(string licenseID="", bool activeSession = false, string wantedHeadsetId = "") { _wantedHeadsetId = wantedHeadsetId; @@ -210,7 +221,10 @@ public void Start(string licenseID="", bool activeSession = false, string wanted _authorizer.Start(licenseID); } - // Unsubscribe + /// + /// Unsubscribe from data streams. If no streams are specified, unsubscribes from all currently subscribed streams. + /// + /// List of stream names to unsubscribe (optional) public void UnSubscribe(List streams = null) { if (streams == null) diff --git a/csharp/CortexAccess/HeadsetFinder.cs b/csharp/CortexAccess/HeadsetFinder.cs index 3c3b90c..3c7da2e 100644 --- a/csharp/CortexAccess/HeadsetFinder.cs +++ b/csharp/CortexAccess/HeadsetFinder.cs @@ -6,6 +6,10 @@ namespace CortexAccess { + /// + /// HeadsetFinder is a utility class for scanning, querying, and connecting to Emotiv headsets. + /// Use FindHeadset(headsetId) to start a timer to query headset and set desired headset. The class starts a timer to repeatedly query headsets until a connection is established. + /// public class HeadsetFinder { private CortexClient _ctxClient; diff --git a/csharp/CortexAccess/RecordManager.cs b/csharp/CortexAccess/RecordManager.cs index 5ed8f16..17f76dd 100644 --- a/csharp/CortexAccess/RecordManager.cs +++ b/csharp/CortexAccess/RecordManager.cs @@ -7,7 +7,9 @@ namespace CortexAccess { /// - /// Reponsible for managing and handling records and markers. + /// RecordManager is a utility class for managing Cortex records and markers. + /// It handles authorization, headset selection, session management, record creation, stopping, updating, querying, deleting, exporting, and marker injection/updating. + /// Use Start() to begin authorization and session setup. Then use the other methods to manage records and markers. /// public class RecordManager { @@ -30,9 +32,20 @@ public class RecordManager public event EventHandler informMarkerResult; public event EventHandler sessionCreateOk; + public event EventHandler DataPostProcessingFinished + { + add { _ctxClient.DataPostProcessingFinished += value; } + remove { _ctxClient.DataPostProcessingFinished -= value; } + } + + public event EventHandler ExportRecordsFinished + { + add { _ctxClient.ExportRecordsFinished += value; } + remove { _ctxClient.ExportRecordsFinished -= value; } + } // Constructor - public RecordManager () + public RecordManager() { _authorizer = new Authorizer(); _headsetFinder = new HeadsetFinder(); @@ -164,16 +177,19 @@ private void MessageErrorRecieved(object sender, ErrorMsgEventArgs errorInfo) /// - /// start a record manager with a license ID and a wanted headset ID. + /// Start the record manager workflow: authorization, headset selection, and session creation. + /// Call this before performing any record or marker operations. /// - public void Start( string wantedHeadsetId = "", string licenseID = "") + /// Optional headset ID to use (default: first headset found) + /// (Obsolete) This parameter is kept for backward compatibility. Always set to empty string "" + public void Start(string wantedHeadsetId = "", string licenseID = "") { _wantedHeadsetId = wantedHeadsetId; _authorizer.Start(licenseID); } /// - /// stop a record manager + /// Stop the record manager and close the current session. Clears record state. /// public void Stop() { @@ -185,62 +201,103 @@ public void Stop() /// /// Create a new record. /// + /// Title for the record + /// Optional description + /// Optional subject name + /// Optional list of tags public void StartRecord(string title, JToken description = null, - JToken subjectName = null, List tags= null) + JToken subjectName = null, List tags = null) { // start record _sessionCreator.StartRecord(_authorizer.CortexToken, title, description, subjectName, tags); } /// - /// Stop a record that was previously started by StartRecord + /// Stop the current record that was previously started by StartRecord(). /// public void StopRecord() { _sessionCreator.StopRecord(_authorizer.CortexToken); } /// - /// Update for record has uuid is recordId + /// Update an existing record with a new description and/or tags. /// - public void UpdateRecord(string recordId , string description = null, List tags = null) + /// Record UUID to update + /// Optional new description + /// Optional new tags + public void UpdateRecord(string recordId, string description = null, List tags = null) { _sessionCreator.UpdateRecord(_authorizer.CortexToken, recordId, description, tags); } /// - /// Query records + /// Query records with custom filters, ordering, and pagination. /// + /// Query filters as a JObject + /// Order by fields as a JArray + /// Maximum number of records to return + /// Offset for pagination public void QueryRecords(JObject queryObj, JArray orderBy, int limit, int offset) { queryObj.Add("applicationId", _sessionCreator.ApplicationId); - _ctxClient.QueryRecord(_authorizer.CortexToken, queryObj, orderBy, offset, limit); } + /// + /// Delete one or more records by their UUIDs. + /// + /// List of record UUIDs to delete public void DeleteRecords(List records) { _ctxClient.DeleteRecord(_authorizer.CortexToken, records); } + + /// + /// Export one or more records to a specified folder with customizable options. + /// + /// List of record UUIDs to export + /// Absolute path to the folder for exported files + /// List of stream types to include (e.g., "EEG", "MOTION") + /// Export file format ("EDF", "EDFPLUS", "BDFPLUS", "CSV") + /// Optional. For "CSV" format, use "V1" or "V2" + /// Optional. License IDs for exporting records from other apps + /// Include demographic info + /// Include extra marker info + /// Include survey data + /// Include deprecated performance metrics + /// See https://emotiv.gitbook.io/cortex-api/records/exportrecord for details + public void ExportRecord(List records, string folderPath, + List streamTypes, string format, string version = null, + List licenseIds = null, bool includeDemographics = false, + bool includeMarkerExtraInfos = false, bool includeSurvey = false, + bool includeDeprecatedPM = false) + { + _ctxClient.ExportRecord(_authorizer.CortexToken, records, folderPath, + streamTypes, format, version, licenseIds, + includeDemographics, includeMarkerExtraInfos, + includeSurvey, includeDeprecatedPM); + } + /// - /// inject marker + /// Inject a marker into the current record. /// + /// Label for the marker + /// Value for the marker public void InjectMarker(string markerLabel, string markerValue) { string cortexToken = _authorizer.CortexToken; string sessionId = _sessionCreator.SessionId; - // inject marker _ctxClient.InjectMarker(cortexToken, sessionId, markerLabel, markerValue, Utils.GetEpochTimeNow()); } /// - /// update marker to set the end date time of a marker, turning an "instance" marker into an "interval" marker + /// Update the current marker to set its end time, converting it from an "instance" to an "interval" marker. /// public void UpdateMarker() { string cortexToken = _authorizer.CortexToken; string sessionId = _sessionCreator.SessionId; - // update marker _ctxClient.UpdateMarker(cortexToken, sessionId, _currMarkerId, Utils.GetEpochTimeNow()); } diff --git a/csharp/CortexAccess/Training.cs b/csharp/CortexAccess/Training.cs index b0d6cee..8cda1d2 100644 --- a/csharp/CortexAccess/Training.cs +++ b/csharp/CortexAccess/Training.cs @@ -4,9 +4,13 @@ namespace CortexAccess { + /// + /// Training is a helper class for handling profile management and training workflows with Emotiv Cortex. + /// It manages authorization, headset selection, detection type, profile creation/loading/unloading, and training actions. + /// Call Start() to begin authorization and set detection/headset. After authorization, it will get detection info and query available profiles. + /// public class Training { - private CortexClient _ctxClient; private string _profileName; // must not existed private string _cortexToken; @@ -101,7 +105,7 @@ private void ProfileSavedOK(object sender, string profileName) private void StreamDataReceived(object sender, StreamDataEventArgs e) { - + if (e.StreamName == "sys") { List data = e.Data.ToObject>(); @@ -207,7 +211,7 @@ private void SessionCreatedOk(object sender, string sessionId) // subscribe _sessionId = sessionId; // Subscribe sys - List stream = new List() { "sys"}; + List stream = new List() { "sys" }; _ctxClient.Subscribe(_cortexToken, _sessionId, stream); } @@ -235,6 +239,12 @@ private void MessageErrorRecieved(object sender, ErrorMsgEventArgs e) Console.WriteLine("MessageErrorRecieved :code " + e.Code + " message " + e.MessageError); } + /// + /// Start authorization and set detection type and desired headset. + /// After authorization, will get detection info and query available profiles. + /// + /// Detection type: "mentalCommand" or "facialExpression" + /// Optional headset ID to use public void Start(string detection, string wantedHeadsetId = "") { if (detection == "mentalCommand" || @@ -247,9 +257,15 @@ public void Start(string detection, string wantedHeadsetId = "") else { Console.WriteLine("Unsupported detection. Only mentalCommand or facialExpression supported."); - } + } } + /// + /// Start training for a specific action and status (e.g., "start", "accept", "reject"). + /// The profile must be loaded and the action must be available in detection info. + /// + /// Action to train (e.g., "neutral", "push", "pull") + /// Training status ("start", "accept", "reject") public void DoTraining(string action, string status) { Console.WriteLine(status + " " + action + " training."); @@ -269,6 +285,10 @@ public void DoTraining(string action, string status) } } + /// + /// Create a new training profile for the current headset. + /// + /// Profile name (must not already exist) public void CreateProfile(string profileName) { if (_profileLists.Contains(profileName)) @@ -279,6 +299,10 @@ public void CreateProfile(string profileName) _ctxClient.SetupProfile(_cortexToken, profileName, "create", _headsetId); } + /// + /// Load an existing training profile for the current headset. + /// + /// Profile name to load public void LoadProfile(string profileName) { if (_profileLists.Contains(profileName)) @@ -287,6 +311,10 @@ public void LoadProfile(string profileName) Console.WriteLine("The profile can not be loaded. The name " + profileName + " has not existed."); } + /// + /// Unload a training profile for the current headset. + /// + /// Profile name to unload public void UnLoadProfile(string profileName) { if (_profileLists.Contains(profileName)) diff --git a/csharp/EEGLogger/Program.cs b/csharp/EEGLogger/Program.cs index 0db4c8a..009abb4 100644 --- a/csharp/EEGLogger/Program.cs +++ b/csharp/EEGLogger/Program.cs @@ -13,7 +13,6 @@ class Program { // init constants before running const string OutFilePath = @"EEGLogger.csv"; - const string LicenseID = ""; // Should be put empty string. Emotiv Cloud will choose the license automatically const string WantedHeadsetId = ""; // if you want to connect to specific headset, put headset id here. For example: "EPOCX-71D833AC" private static FileStream OutFileStream; @@ -36,7 +35,7 @@ static void Main(string[] args) dse.OnSubscribed += SubscribedOK; dse.OnEEGDataReceived += OnEEGDataReceived; - dse.Start(LicenseID, true, WantedHeadsetId); + dse.Start("", true, WantedHeadsetId); Console.WriteLine("Press Esc to flush data to file and exit"); while (Console.ReadKey().Key != ConsoleKey.Escape) { } diff --git a/csharp/InjectMarker/Program.cs b/csharp/InjectMarker/Program.cs index eb1bdfe..f9763b5 100644 --- a/csharp/InjectMarker/Program.cs +++ b/csharp/InjectMarker/Program.cs @@ -81,7 +81,7 @@ static void Main(string[] args) } else { - Console.WriteLine("The preparation for injecting marker is unsuccessful. Please try again"); + Console.WriteLine("Failed to prepare for marker injection. Please ensure a headset is available for connection and try again."); } } private static void OnSessionCreatedOk(object sender, bool isOk) diff --git a/csharp/MotionLogger/Program.cs b/csharp/MotionLogger/Program.cs index 07200b8..0639b4d 100644 --- a/csharp/MotionLogger/Program.cs +++ b/csharp/MotionLogger/Program.cs @@ -14,7 +14,6 @@ class Program { // init constants before running const string OutFilePath = @"MotionLogger.csv"; - const string LicenseID = ""; // Should be put empty string. Emotiv Cloud will choose the license automatically const string WantedHeadsetId = ""; // if you want to connect to specific headset, put headset id here. For example: "EPOCX-71D833AC" private static FileStream OutFileStream; @@ -37,7 +36,7 @@ static void Main(string[] args) dse.OnSubscribed += SubscribedOK; dse.OnMotionDataReceived += OnMotionDataReceived; - dse.Start(LicenseID, false, WantedHeadsetId); + dse.Start("", false, WantedHeadsetId); Console.WriteLine("Press Esc to flush data to file and exit"); while (Console.ReadKey().Key != ConsoleKey.Escape) { } diff --git a/csharp/PMLogger/Program.cs b/csharp/PMLogger/Program.cs index c6166b3..8dacbf6 100644 --- a/csharp/PMLogger/Program.cs +++ b/csharp/PMLogger/Program.cs @@ -13,9 +13,8 @@ class Program { // init constants before running const string OutFilePath = @"PMLogger.csv"; - const string LicenseID = ""; // Should be put empty string. Emotiv Cloud will choose the license automatically const string WantedHeadsetId = ""; // if you want to connect to specific headset, put headset id here. For example: "EPOCX-71D833AC" - const bool ActiveSession = false; // set true if you want to uses the license of the user. Depending on the license scope of the license, you can subscribe high resolution performance metrics + const bool ActiveSession = false; // Set to true to use an active session with a license which is required for high performance metrics. private static FileStream OutFileStream; @@ -37,7 +36,7 @@ static void Main(string[] args) dse.OnSubscribed += SubscribedOK; dse.OnPerfDataReceived += OnPMDataReceived; - dse.Start(LicenseID, ActiveSession, WantedHeadsetId); + dse.Start("", ActiveSession, WantedHeadsetId); Console.WriteLine("Press Esc to flush data to file and exit"); while (Console.ReadKey().Key != ConsoleKey.Escape) { } diff --git a/csharp/README.md b/csharp/README.md index 9203410..572445e 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -1,91 +1,55 @@ -# Cortex C# Examples -These examples show how to call the Cortex APIs from C# which describe at [Cortex Document](https://app.gitbook.com/@emotiv/s/cortex-api/) -## Getting Started -These instructions will get you a copy of the project up and running on your local machine for development. -### Prerequisites -* You might have [Visual Studio](https://www.visualstudio.com/) with C# supported (msvc14 or higher recommended). -* The Cortex have to be running on your machine such as service. You can get the Cortex from (https://www.emotiv.com/developer/). -* Register a Application at https://www.emotiv.com/my-account/cortex-apps/ and get a pair of client id and client secret. You must connect to your Emotiv account on emotiv.com and create a Cortex app. If you don't have a EmotivID, you can [register here](https://id.emotivcloud.com/eoidc/account/registration/). -* We have updated our Terms of Use, Privacy Policy and EULA to comply with GDPR. Please login via EMOTIV Launcher to read and accept our latest policies in order to proceed using the following examples. +# Emotiv Cortex C# Example Suite -### How to compile - -1. Open CortexExamples.sln by Visual Studio IDE -2. Use Nuget Package Manager to install _Newtonsoft.Json, SuperSocket.ClientEngine.Core, WebSocket4Net_ for CortexAccess project -3. Login via EMOTIV Launcher and put your client id and client secret to Config.cs. More detail please see below. -4. You can compile and run the examples directly from the IDE. +This repository provides C# console applications that demonstrate how to use the [Emotiv Cortex API](https://emotiv.gitbook.io/cortex-api) to connect to Emotiv headsets, stream data, manage records, and perform training tasks. -### Code structure - -This section describe structure overview, core classes and examples. The C# Cortex examples contain 2 parts. Firstly, CortexAcess project is core and responsible for processing all requests and responses such as a middle layer between user and cortex. Secondly, Example projects, each example as a project which reference to CortexAccess project. - -#### Structure overview -* CortexClient: Responsible for sending requests to Cortex and handle responses, warning, data from Cortex. -* Config: Contain configurations. User must fill clientId, client Secret of App. To get EEG and Performance metric data, an appropriate license is required. -* Authorizer: Responsible for getUserLogin, requestAccess, authorize for App. -* HeadsetFinder: Reponsible for finding headsets, connect headset. If you don't want to connect automatically to a headset (after first time), you should set IsAutoConnect = false -* SessionCreator: Responsible for createSession, updateSession, start/stop/update record for work-flow. -* RecordManager: To handle records and markers -* Training: Responsible for create/load/unload/ query Profiles and training. -* Examples: We have examples to demo subscribe data, record, inject marker, training. +## 1. Setup & Prerequisites -#### Examples -**1. EEGLogger** -* This example opens a session with the first Emotiv headset. Then subscribe and save eeg data to EEGLogger.csv file until Esc key pressed. -* The basic work-flow: Login via EMOTIV Launcher -> requestAccess-> Authorize (an appropriate license is required) -> find and connect headset -> Create Session -> Subscribe EEG data. -* Notes: - - 1) Press Esc to flush data to output file and exit. +### Requirements +- **Visual Studio 2019 or newer** (Community/Professional/Enterprise) +- **.NET Framework 4.7.2 or newer** +- **C# compiler**: Use the default provided by Visual Studio +- **NuGet Packages** (install via NuGet Package Manager): + - `Newtonsoft.Json` + - `SuperSocket.ClientEngine.Core` + - `WebSocket4Net` +- **EMOTIV Launcher** (required for login and granting application access). Installing EMOTIV Launcher will also run the Emotiv Cortex Service locally. [Download here](https://www.emotiv.com/products/emotiv-launcher) -**2. MotionLogger** -* This example opens a session with the first Emotiv headset. Then subscribe and save motion data to MotionLogger.csv file until Esc key pressed. -* The basic work-flow: Login via EMOTIV Launcher -> requestAccess-> Authorize() -> find and connect headset -> Create Session -> Subscribe Motion data. -* Notes: - - 1) Press Esc to flush data to output file and exit. -**3. BandPowerLogger** -* This example opens a session with the first Emotiv headset. Then subscribe and save motion data to BandPowerLogger.csv file until Esc key pressed. -* The basic work-flow: Login via EMOTIV Launcher -> requestAccess-> Authorize() -> find and connect headset -> Create Session -> Subscribe Band Power data. -* Notes: - - 1) Press Esc to flush data to output file and exit. +### Setup Steps +1. Open `CortexExamples.sln` in Visual Studio +2. Use NuGet Package Manager to install required packages for the `CortexAccess` project +3. Enter your client ID and client secret in `Config.cs` +4. Build and run any example project from Visual Studio -**4. MentalCommandTraining** -* This example opens a session with the first Emotiv headset. Then User can create/load/unload profile then train actions following console guide. -* The example demo for train: neutral, push, pull actions but you can add more actions as getDetectionInfo Output. -* The basic work-flow: Login -> Login via EMOTIV Launcher -> requestAccess-> Authorize() -> find and connect headset -> Create Session -> Subscribe _sys_ data -> Load/Create profile -> Start training actions -> Accept/Reject training. +## 2. Core Classes in CortexAccess -**5. FacialExpressionTraining** -* This example opens a session with the first Emotiv headset. Then User can create/load/unload profile then train actions following guideline on console. -* The example demo for train: neutral, smile, frown, clench actions but you can add more actions as getDetectionInfo Output. -* The basic work-flow: Login -> Login via EMOTIV Launcher -> requestAccess-> Authorize() -> find and connect headset -> Create Session -> Subscribe _sys_ data -> Load/Create profile -> Start training actions -> Accept/Reject training. +### Main Classes +- **CtxClient.cs**: Handles connection to the Emotiv Cortex WebSocket service, builds requests, sends them, and processes responses/events. +- **DataStreamExample.cs**: The easiest entry point for subscribing to data streams. This class manages the entire workflow: opening the WebSocket, authorization, session creation, and data subscription. Simply call `Start()` and it will sequence all required steps. You can customize this class for advanced workflows (e.g., create a record before subscribing, load a training profile before subscribing to 'com' data, or connect to a specific headset by setting `_wantedHeadsetId`). +- **RecordManager.cs**: Manages record-related tasks: create, inject marker, export, and delete records. +- **Training.cs**: Handles profile creation/loading and training for a specific headset (`_wantedHeadsetId`). But does not subscribe to 'com' or 'fac' data streams for mental command power. +- **Other utility classes**: Helpers for configuration, session management, headset finding, authorization, etc. -**6. InjectMarkers** -* This example opens a session with the first Emotiv headset. Then createRecord and inject markers to data stream. -* Following guideline shown on console. Press a certain key to set a label of marker into injectmarker function. The program ignores Tab, Enter, Spacebar and Backspace Key. -* The basic work-flow: Login via EMOTIV Launcher -> requestAccess-> Authorize() -> find and connect headset -> Create Session -> CreateRecord -> InjectMarker. +## 3. How to Use the Examples -**7. RecordData** -* This example opens a session with the first Emotiv headset. Then create/stop/update/delete record. -* The basic work-flow: Login via EMOTIV Launcher -> requestAccess-> Authorize() -> find and connect headset -> Create Session -> CreateRecord -> Stop/Update/Query/Delete Record. +All examples are console applications. Each example demonstrates a specific Cortex API workflow: -**8. PMLogger** -* This example opens a session with the first Emotiv headset. Then subscribe and save pm data to PMLogger.csv file until Esc key pressed. -* The basic work-flow: Login via EMOTIV Launcher -> requestAccess-> Authorize (an appropriate license is required) -> find and connect headset -> Create Session -> Subscribe PM data. -* The performance metric data frequency depend on scope of license. For low performance metric : 1 sample/ 10 seconds ; for high performance metric 2 samples/ seconds. -* Notes: - - 1) Press Esc to flush data to output file and exit. - - 2) Each performance metric is a decimal number between 0 and 1. Zero means "low power", 1 means "high power". If the detection cannot run because of a bad contact quality then the value can also be **null** -### Notes -* You must login and logout via EMOTIV Launcher. -* You must use EMOTIV Launcher to grant AccessRight for the App one time for one emotiv user. -* You need a valid license to subscribe EEG data, Performance metrics data. -* From Emotiv Cortex 3.7, you need to call ScanHeadsets() at HeadsetFinder.cs to start headset scanning. Otherwise your headsets might not appeared in the headset list return from queryHeadsets(). If IsHeadsetScanning = false, you need re-call the ScanHeadsets() if want to re-scan headsets again. -* The Examples are only demo for some Apis not all apis and to be continue updating. +- **XYLogger (EEGLogger, BandPowerLogger, MotionLogger, PMLogger)**: Demonstrate how to subscribe to EEG, Band Power, Motion, or Performance Metrics data streams. Data is saved to a `.csv` file. Press `Esc` to stop and save. +- **RecordData**: Interactive demo for creating, stopping, querying, updating, deleting, and exporting records. Commands are listed in the console. +- **InjectMarker**: Shows how to create a record and inject markers. Follow the console instructions. +- **FacialExpressionTraining & MentalCommandTraining**: Demonstrate profile creation/loading and training. Follow console prompts for actions and keys. +## 4. Notes for Beginners -* TODO: Basic UI for examples. +- You must log in via EMOTIV Launcher and grant AccessRight for your app (one time per user). +- A valid license is required for EEG and Performance Metrics data. +- By default, the examples use the first headset found. To use a different headset, set `_wantedHeadsetId` in the relevant class. +- From Cortex 3.7+, call `ScanHeadsets()` in `HeadsetFinder.cs` to start EMOTIV headset scanning. +- These examples are intended as starting points. You can extend or modify them for your own research or product needs. + +## 5. References -### References 1. https://www.newtonsoft.com/json 2. http://www.supersocket.net/ 3. http://websocket4net.codeplex.com/ diff --git a/csharp/RecordData/Program.cs b/csharp/RecordData/Program.cs index 4a0d669..ced3843 100644 --- a/csharp/RecordData/Program.cs +++ b/csharp/RecordData/Program.cs @@ -11,6 +11,7 @@ class Program const string WantedHeadsetId = ""; // if you want to connect to specific headset, put headset id here. For example: "EPOCX-71D833AC" private static int _recordNo = 1; + private static string _recentRecordId = ""; // keep track of the most recent record ID created private static RecordManager _recordManager; private static AutoResetEvent _readyForRecordDataEvent = new AutoResetEvent(false); @@ -22,6 +23,8 @@ static void Main(string[] args) _recordManager = new RecordManager(); _recordManager.sessionCreateOk += OnSessionCreatedOk; + _recordManager.DataPostProcessingFinished += OnDataPostProcessingFinished; + _recordManager.ExportRecordsFinished += onExportRecordsFinished; Console.WriteLine("Prepare to record Data"); // Start @@ -33,6 +36,7 @@ static void Main(string[] args) Console.WriteLine("Press S to stop record"); Console.WriteLine("Press Q to query record"); Console.WriteLine("Press D to delete first record Id from recording list"); + Console.WriteLine("Press E to export the most recently stopped record."); Console.WriteLine("Press U to update record"); Console.WriteLine("Press H to show all commands"); Console.WriteLine("Press Esc to quit"); @@ -103,7 +107,24 @@ static void Main(string[] args) else { Console.WriteLine("Please queryRecords first before call deleteRecord which delete first record in Lists"); - } + } + } + else if (keyInfo.Key == ConsoleKey.E) + { + // Export Record + if (string.IsNullOrEmpty(_recentRecordId)) + { + Console.WriteLine("No record available to export. Please create a record first."); + } + else + { + string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + List recordsToExport = new List { _recentRecordId }; + List streamTypes = new List { "EEG", "MOTION" }; // Specify the stream types you want to export + string format = "CSV"; // or "CSV", "EDFPLUS", "BDFPLUS" + string version = "V2"; // Optional, specify if needed + _recordManager.ExportRecord(recordsToExport, folderPath, streamTypes, format, version); + } } else if (keyInfo.Key == ConsoleKey.H) { @@ -113,6 +134,7 @@ static void Main(string[] args) Console.WriteLine("Press Q to query record"); Console.WriteLine("Press D to delete record"); Console.WriteLine("Press U to update record"); + Console.WriteLine("Press E to export the most recently stopped record"); Console.WriteLine("Press H to show all commands"); Console.WriteLine("Press Esc to quit"); } @@ -135,10 +157,40 @@ static void Main(string[] args) } else { - Console.WriteLine("The preparation for injecting marker is unsuccessful. Please try again"); + Console.WriteLine("Unable to prepare for recording data. Please ensure your headset is available and ready to connect, then try again."); } } + private static void onExportRecordsFinished(object sender, MultipleResultEventArgs e) + { + // extract the result from e.Result + // get successful list + JArray successfulList = e.SuccessList; + // check _recentRecordId is in the successful list + bool isExportedSuccess = false; + if (successfulList != null && successfulList.Count > 0) + { + foreach (var record in successfulList) + { + if (record is JObject recordObj && recordObj["recordId"]?.ToString() == _recentRecordId) + { + isExportedSuccess = true; + break; + } + } + } + + if (!isExportedSuccess) + { + Console.WriteLine("Export failed for record with ID: " + _recentRecordId); + } + else + { + Console.WriteLine("Export finished for record with ID: " + _recentRecordId); + } + + } + private static void OnSessionCreatedOk(object sender, bool isOK) { if (isOK) @@ -147,5 +199,11 @@ private static void OnSessionCreatedOk(object sender, bool isOK) _readyForRecordDataEvent.Set(); } } + private static void OnDataPostProcessingFinished(object sender, string recordId) + { + Console.WriteLine("Data post processing finished for record: " + recordId + + ". You can now export the record."); + _recentRecordId = recordId; // Update the most recent record ID + } } } From 9620bc36e7d1d7df4dcd99da3e66b4abcb1357f0 Mon Sep 17 00:00:00 2001 From: tung_tung Date: Fri, 18 Jul 2025 14:03:20 +0700 Subject: [PATCH 2/3] COR-5687: update readme for unity example --- unity/Assets/Plugins/Emotiv-Unity-Plugin | 2 +- unity/README.md | 100 +++++++++++++++-------- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/unity/Assets/Plugins/Emotiv-Unity-Plugin b/unity/Assets/Plugins/Emotiv-Unity-Plugin index eb4af2f..1711801 160000 --- a/unity/Assets/Plugins/Emotiv-Unity-Plugin +++ b/unity/Assets/Plugins/Emotiv-Unity-Plugin @@ -1 +1 @@ -Subproject commit eb4af2f8736c6346d9a3b3d40ba41f0732f867f2 +Subproject commit 17118013a133954bde9d464eb47f78a6b5d0797a diff --git a/unity/README.md b/unity/README.md index babdc93..7b176a5 100644 --- a/unity/README.md +++ b/unity/README.md @@ -1,48 +1,78 @@ -# Emotiv Unity Example - -This example demonstrates how to work with the Emotiv Cortex Service (Cortex) in Unity. -## Prerequisites +# Emotiv Unity Example -* **Install Unity:** You can download it for free at [www.unity3d.com](https://unity3d.com/get-unity/download). -* **** We tested with Unity 6 (also recommended by Unity: https://docs.unity.com/ugs/manual/devops/manual/build-automation/reference/available-xcode-versions) -* **Get the Emotiv Unity Plugin:** Obtain the latest version of the [Emotiv Unity Plugin](https://github.com/Emotiv/unity-plugin) as a submodule: +This example demonstrates how to work with the Emotiv Cortex Service (Cortex) and Emotiv Embedded Library in Unity, supporting Desktop (Windows/macOS), Android, and iOS platforms. + +## Platform Support, Requirements & Prerequisites + +- **Desktop (Windows/macOS):** + - Works with Emotiv Cortex Service via EMOTIV Launcher. + - Ensure `USE_EMBEDDED_LIB` is **not** defined in your Unity project. + - **Embedded Library for Desktop is under development and not ready for production.** +- **Android:** + - Supported via Emotiv Embedded Library. + - You must request Bluetooth and Location permissions at runtime. + - See [Unity Android Permissions](https://docs.unity3d.com/Manual/android-RequestingPermissions.html). + - **Gradle Plugin:** The default `baseProjectTemplate.gradle` uses version 7.4.2 (best for Unity 2021). If your Unity version is higher, update the Gradle plugin version via Android Studio after first build. + - **mainTemplate.gradle:** After setting your clientId in `AppConfig.cs`, also add it to `mainTemplate.gradle` as required by your build process. +- **iOS:** + - Supported via Emotiv Embedded Library. + - After building, export to Xcode and ensure Bluetooth permissions are set in `Info.plist`. + - The Emotiv Embedded `.xcframework` must be included in your Xcode project. + - **Xcode Version:** Use an Xcode version compatible with your Unity version. For example, Unity 6.0 (6000.0) generally requires Xcode 15+ or Xcode 16+ for iOS builds. + +**Note:** +- For **desktop**, you must install and run the EMOTIV Launcher (Cortex Service). This is not required for mobile platforms. And do not need UniWebview submodule so only pull unity-plugin submodule as below ``` - git submodule update --init + git submodule update --init ``` -* **Install and Run EMOTIV Launcher with Cortex:** Download and run the EMOTIV Launcher with Cortex from [https://www.emotiv.com/developer/](https://www.emotiv.com/developer/). -* **Emotiv Account and Application:** - * Log in to the Emotiv website with your valid EmotivID. If you don't have one, you can [register here](https://id.emotivcloud.com/eoidc/account/registration/). - * Register an application at [https://www.emotiv.com/my-account/cortex-apps/](https://www.emotiv.com/my-account/cortex-apps/) to obtain your `clientId` and `clientSecret`. -* **Accept Updated Policies:** Please log in via the EMOTIV Launcher to read and accept our latest Terms of Use, Privacy Policy, and EULA to proceed with the following examples. This is required due to GDPR compliance. +- For **mobile (Android/iOS)**, you do **not** need the EMOTIV Launcher, but you must pull the UniWebView submodule. UniWebView is a private repository used to open a webview for login on mobile. Please contact Emotiv to get access. + - To pull all submodules (including UniWebView), use: + ``` + git submodule update --init --recursive + ``` + - UniWebView is a submodule inside the unity-plugin directory. + ## How to Use -1. **Open the Example Scene:** Open the **SimpleExample.unity** scene. This scene contains a comprehensive demonstration. -2. **Set Credentials:** In the **AppConfig.cs** script, locate and set the `clientId` and `clientSecret` with the values from your registered application before running the scene. -3. **Query Headsets:** Run the example. Once authorization is complete, click the **"Query Headset"** button to list available headsets. -4. **Create a Session:** - * Enter the ID of the desired headset (e.g., "INSIGHT-A12345") in the text field next to the **"Create Session"** button. - * If the text field is left empty, the first headset in the queried list will be used by default. - * Click the **"Create Session"** button to connect to the headset and establish a session. -5. **Interact with Data and Training:** After successfully creating a session, you can perform the following actions: - * **Start and Stop Recording:** Enter a title for your recording in the designated field before clicking **"Start Record"**. An optional description can also be added. Use the **"Stop Record"** button to end the recording. - * **Inject Marker:** While a recording is active, you can inject instance markers. Enter a **"marker value"** and a **"marker label"** and then click the **"Inject Marker"** button. - * **Subscribe and Unsubscribe Data Streams:** Select the desired data streams from the available options before clicking the **"Subscribe Data"** button. The received data will be displayed in the log box. You can unsubscribe by clicking the **"Unsubscribe Data"** button. - * **Load Profile and Training:** - * Enter a **"profile name"** before clicking **"Load Profile"**. If a profile with that name does not exist, it will be created and then loaded. - * **Important:** Subscribe to the **"System Event"** data stream to observe training events. - * Select a mental command for training from the dropdown menu and click **"Start Training"**. - * You might observe a **"MC\_Succeeded"** event after approximately 8 seconds, indicating a successful training attempt. You can then choose to accept or reject the training. - * After training, click **"Save Profile"** to persist the trained data. - * **Important:** Before closing the application, click **"Unload Profile"** to release the trained data. +### 1. Open the Example Scene +Open the **SimpleExample.unity** scene. This scene demonstrates all major features and works on Desktop, Android, and iOS. + +### 2. Set Credentials +In **AppConfig.cs**, set your `clientId` and `clientSecret` from your Emotiv Cortex App registration. + +### 3. Platform-Specific Setup +- **Android:** Ensure your app requests Bluetooth and Location permissions at runtime. Unity 2021+ supports this via the `Android.Permission` API. +- **iOS:** After exporting to Xcode, add Bluetooth permissions to `Info.plist` and ensure the Emotiv `.xcframework` is included. +- **Desktop:** Make sure EMOTIV Launcher is running and `USE_EMBEDDED_LIB` is **not** defined. + +### 4. Run and Interact +1. Run the scene in the Unity Editor or on your target device. +2. Click **"Query Headset"** to list available headsets after authorization. +3. Enter a headset ID (or leave blank to use the first found) and click **"Create Session"**. +4. You can now: + - **Subscribe/Unsubscribe Data Streams:** Select streams and click **"Subscribe Data"** or **"Unsubscribe Data"**. + - **Start/Stop Recording:** Enter a title and click **"Start Record"**/**"Stop Record"**. + - **Inject Marker:** Enter a label/value and click **"Inject Marker"** while recording. +> The UI and workflow are almost the same for Desktop, Android, and iOS. +> +> **For Emotiv Embedded Library:** +> - If you are not logged in with your EmotivID, the **Sign In** button will be active. Clicking **Sign In** opens a webview for authentication. +> - After successful login, authorization is handled automatically. +> - Once you see "Authorize done" in the message box on the UI, you can proceed to query headsets, create sessions, and use other features as described above. +> - On mobile, ensure all required permissions are granted. > **Please Note:** > -> * **Headset Scanning (Cortex 3.7+):** Starting from Emotiv Cortex 3.7, you need to call RefreshHeadset() to scan bluetooth devices. But the process is proceed automatically after authorization. -> * **Interface:** This example utilizes **EmotivUnityItf.cs** as an interface to interact with the Emotiv Cortex Service. -> * **Data Buffering:** By default, subscribed data is not saved to a data buffer and is only displayed in the message log box. You can enable saving data to a buffer by setting `IsDataBufferUsing = true` in the `AppConfig.cs` file. -> * **Mental Command and Facial Expression:** Ensure you load a trained profile before subscribing to **"Mental Command"** or **"Facial Expression"** data streams; otherwise, you will only observe neutral actions. +> * **Headset Scanning (Cortex 3.7+):** Headset scanning is automatic after authorization. Manual `RefreshHeadset()` is not required in most cases. +> * **Interface:** The example uses **EmotivUnityItf.cs** for all Cortex API operations. +> * **Data Buffering:** To save data to a buffer for later access, set `IsDataBufferUsing = true` in `AppConfig.cs`. +> * **Mental Command/Facial Expression:** Always load a trained profile before subscribing to these streams, or only neutral actions will be received. +> * **Permissions:** +> - **Android:** Request Bluetooth and Location permissions at runtime. +> - **iOS:** Add Bluetooth permissions to `Info.plist` and ensure `.xcframework` is present in Xcode. +> - **Desktop:** No special permissions, but EMOTIV Launcher must be running. ## Change Log From 3dbb568bcbc9923744a3350e0f5f78f47cbf00c4 Mon Sep 17 00:00:00 2001 From: tung_tung Date: Fri, 18 Jul 2025 16:02:23 +0700 Subject: [PATCH 3/3] COR-5687: update rework for unity readme --- unity/Assets/Plugins/Emotiv-Unity-Plugin | 2 +- unity/README.md | 41 +++++++++++++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/unity/Assets/Plugins/Emotiv-Unity-Plugin b/unity/Assets/Plugins/Emotiv-Unity-Plugin index 1711801..43092bf 160000 --- a/unity/Assets/Plugins/Emotiv-Unity-Plugin +++ b/unity/Assets/Plugins/Emotiv-Unity-Plugin @@ -1 +1 @@ -Subproject commit 17118013a133954bde9d464eb47f78a6b5d0797a +Subproject commit 43092bfe8ba8c806efe522def017e1db23d30f51 diff --git a/unity/README.md b/unity/README.md index 7b176a5..9967fa2 100644 --- a/unity/README.md +++ b/unity/README.md @@ -13,20 +13,18 @@ This example demonstrates how to work with the Emotiv Cortex Service (Cortex) an - Supported via Emotiv Embedded Library. - You must request Bluetooth and Location permissions at runtime. - See [Unity Android Permissions](https://docs.unity3d.com/Manual/android-RequestingPermissions.html). - - **Gradle Plugin:** The default `baseProjectTemplate.gradle` uses version 7.4.2 (best for Unity 2021). If your Unity version is higher, update the Gradle plugin version via Android Studio after first build. - - **mainTemplate.gradle:** After setting your clientId in `AppConfig.cs`, also add it to `mainTemplate.gradle` as required by your build process. + - **iOS:** - Supported via Emotiv Embedded Library. - After building, export to Xcode and ensure Bluetooth permissions are set in `Info.plist`. - - The Emotiv Embedded `.xcframework` must be included in your Xcode project. + - The Emotiv Embedded Library `EmotivCortexLib.xcframework` must be included in your Xcode project. - **Xcode Version:** Use an Xcode version compatible with your Unity version. For example, Unity 6.0 (6000.0) generally requires Xcode 15+ or Xcode 16+ for iOS builds. -**Note:** -- For **desktop**, you must install and run the EMOTIV Launcher (Cortex Service). This is not required for mobile platforms. And do not need UniWebview submodule so only pull unity-plugin submodule as below +- For **desktop**, ensure the EMOTIV Launcher (Cortex Service) is installed and running. But the UniWebView submodule is not required for desktop, so you only need to pull the `unity-plugin` submodule as shown below: ``` git submodule update --init ``` -- For **mobile (Android/iOS)**, you do **not** need the EMOTIV Launcher, but you must pull the UniWebView submodule. UniWebView is a private repository used to open a webview for login on mobile. Please contact Emotiv to get access. +- For **mobile (Android/iOS)**, you do **not** need the EMOTIV Launcher, but you must pull the UniWebView submodule. The UniWebView submodule is a private repository used to open a webview for login on mobile. Please contact Emotiv to get access. - To pull all submodules (including UniWebView), use: ``` git submodule update --init --recursive @@ -43,8 +41,35 @@ Open the **SimpleExample.unity** scene. This scene demonstrates all major featur In **AppConfig.cs**, set your `clientId` and `clientSecret` from your Emotiv Cortex App registration. ### 3. Platform-Specific Setup -- **Android:** Ensure your app requests Bluetooth and Location permissions at runtime. Unity 2021+ supports this via the `Android.Permission` API. -- **iOS:** After exporting to Xcode, add Bluetooth permissions to `Info.plist` and ensure the Emotiv `.xcframework` is included. + +#### Android Setup +- **Get the Embedded Library:** Contact Emotiv to obtain `EmotivCortexLib.aar` and place it in `./Assets/Plugins/Emotiv-Unity-Plugin/Src/AndroidPlugin/EmotivCortexLib/`. +- **Permissions:** Ensure your app requests Bluetooth and Location permissions at runtime. Unity 2021+ supports this via the `Android.Permission` API. +- **Update clientId:** Edit `mainTemplate.gradle` to set your `client_Id` for webview redirect URI. +- **Gradle Plugin:** The default `baseProjectTemplate.gradle` uses Gradle version 7.4.2, which is optimal for Unity 2021. If you are using a newer Unity version (such as Unity 2022 or later), you may need to update the Gradle plugin version to ensure compatibility with the Android build tools and SDKs required by your Unity version. On your first build in Android Studio, you might see a prompt to update the Gradle version—click **Yes** to proceed. Updating ensures your project builds successfully and takes advantage of the latest Android features and security updates. + +#### iOS Setup +- **Get the Embedded Library:** Contact Emotiv to obtain `EmotivCortexLib.xcframework` and place it in `./Assets/Plugins/Emotiv-Unity-Plugin/Src/IosPlugin/EmotivCortexLib/`. +- **Build Settings:** In Unity, select iOS platform and click Build. Unity will generate an Xcode project (`.xcodeproj`). +- **Xcode Configuration:** + - **Embed Framework:** + - Open `.xcodeproj` in Xcode. + - Go to Project > General > Frameworks, Libraries, and Embedded Content. + - Add `EmotivCortexLib.xcframework` and set to Embed & Sign. + - **Set Signing Team:** + - Go to Signing & Capabilities. + - Choose your Apple Developer Team before building. + - **Bluetooth Permission (Info.plist):** + - Add: + ```xml + NSBluetoothAlwaysUsageDescription + This app uses Bluetooth to discover, connect, and transfer data between devices. + ``` + - **Link Framework:** + - Go to Build Phases > Link Binary with Libraries. + - Add `EmotivCortexLib.xcframework` if not already listed. +- **Build and Run:** Build the app and run it on a physical iOS device. + - **Desktop:** Make sure EMOTIV Launcher is running and `USE_EMBEDDED_LIB` is **not** defined. ### 4. Run and Interact