com.xrlab.labframe_brainbit 1.0.9 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,11 +2,16 @@
2
2
 
3
3
  此套件為 LabFrame 2023 專用的 BrainBit 設備插件,用於連接與管理 BrainBit 腦波儀。
4
4
 
5
+ > [!NOTE]
6
+ > 請再記得多安裝此套件 https://github.com/BrainbitLLC/unity_em_st_artifacts.git#a04238a934b3da0494dd9120a489005277063a1f
7
+ > 開發當下此套件最新版(1.0.3)在android平台會有問題
8
+
5
9
  ## 支援功能
6
10
  1. **設備連線管理:** 自動搜尋並手動觸發連接藍牙 BrainBit 設備。
7
11
  2. **EEG 腦波數據收集:** 自動收集四個通道 (T3, T4, O1, O2) 的腦波數據。
8
12
  3. **即時阻抗檢查:** 確認電極與頭皮的接觸阻抗值是否過高 (> 200,000Ω)。
9
13
  4. **多階段資料分流儲存:** 收集期間可動態切換儲存 Tag(依照遊戲階段無縫寫入不同檔案)。
14
+ 5. **情緒與光譜分析:** 透過 NeuroSDK `EegEmotionalMath` 即時運算專注 / 放鬆(MindData)與 δ/θ/α/β/γ 五頻段光譜百分比。
10
15
 
11
16
  ---
12
17
 
@@ -103,7 +108,95 @@ void CheckImpedanceStatus()
103
108
 
104
109
  ---
105
110
 
106
- ### 4. 設備搜尋與多設備選擇機制
111
+ ### 4. 情緒與光譜分析 (MindData / SpectralData)
112
+
113
+ 整合 NeuroSDK `EegEmotionalMath`,即時取得受測者的**專注度 / 放鬆度**以及**五頻段光譜百分比**。
114
+
115
+ > 情緒處理需要先完成約 6 秒的**校正** (請受測者安靜配戴),校正完成後才會開始輸出有效的 MindData / SpectralData。
116
+
117
+ #### 👉 開始 / 停止情緒處理
118
+
119
+ ```csharp
120
+ // 啟動情緒處理(若 EEG 尚未啟動,會自動開啟;停止時會一併停止該次自動啟動的 EEG)
121
+ BrainBitManager.Instance.StartEmotionsProcessing(
122
+ autoWriteToLabData: true,
123
+ mindTag: "Gameplay_Mind",
124
+ spectralTag: "Gameplay_Spectral");
125
+
126
+ // 停止情緒處理
127
+ BrainBitManager.Instance.StopEmotionsProcessing();
128
+ ```
129
+
130
+ #### 👉 等待校正完成
131
+
132
+ ```csharp
133
+ BrainBitManager.Instance.OnCalibrationProgress += pct => Debug.Log($"校正中 {pct}%");
134
+ BrainBitManager.Instance.OnCalibrationFinished += () => Debug.Log("校正完成!");
135
+
136
+ // 或在輪詢中判斷:
137
+ if (BrainBitManager.Instance.IsEmotionsCalibrated)
138
+ {
139
+ // 此時才能讀到有效的 MindData / SpectralData
140
+ }
141
+ ```
142
+
143
+ 若遊戲中需要重新校正(換受測者、中途摘下又戴回):
144
+ ```csharp
145
+ BrainBitManager.Instance.RestartCalibration();
146
+ ```
147
+
148
+ #### 👉 取得最新的專注 / 放鬆值
149
+
150
+ ```csharp
151
+ void Update()
152
+ {
153
+ if (!BrainBitManager.Instance.IsEmotionsCalibrated) return;
154
+
155
+ var mind = BrainBitManager.Instance.GetLatestMindData();
156
+ if (mind == null) return;
157
+
158
+ Debug.Log($"專注: {mind.Attention:F1} / 放鬆: {mind.Relaxation:F1}");
159
+ // mind.InstAttention / mind.InstRelaxation 為瞬時值(抖動大,僅進階使用)
160
+ }
161
+ ```
162
+
163
+ #### 👉 取得最新的五頻段光譜百分比
164
+
165
+ ```csharp
166
+ var spec = BrainBitManager.Instance.GetLatestSpectralData();
167
+ if (spec != null)
168
+ {
169
+ Debug.Log($"δ:{spec.Delta:F1} θ:{spec.Theta:F1} α:{spec.Alpha:F1} β:{spec.Beta:F1} γ:{spec.Gamma:F1}");
170
+ }
171
+ ```
172
+
173
+ #### 👉 事件訂閱(event-driven 寫法)
174
+
175
+ ```csharp
176
+ BrainBitManager.Instance.OnMindDataReceived += mind => { /* 更新 UI */ };
177
+ BrainBitManager.Instance.OnSpectralDataReceived += spec => { /* 更新 UI */ };
178
+ BrainBitManager.Instance.OnEmotionsArtifact += hasArtifact => { /* 顯示雜訊警告 */ };
179
+ ```
180
+
181
+ #### 👉 動態無縫切換儲存階段 (Tag)
182
+
183
+ ```csharp
184
+ // 不用停情緒處理,下一筆資料就會被寫進新 tag
185
+ BrainBitManager.Instance.SetMindTag("Boss_Phase_Mind");
186
+ BrainBitManager.Instance.SetSpectralTag("Boss_Phase_Spectral");
187
+ ```
188
+
189
+ #### 👉 可調設定(`BrainBitConfig`)
190
+
191
+ | 欄位 | 預設 | 說明 |
192
+ |---|---|---|
193
+ | `EmotionsCalibrationLength` | `6` | 校正時間(秒)。越長越穩,但受測者需要等更久 |
194
+ | `EmotionsMentalEstimation` | `false` | 啟用 Mental Estimation(依實驗類型決定) |
195
+ | `EmotionsPrioritySide` | `SideType.NONE` | 優先分析腦側:`NONE` / `LEFT` / `RIGHT` |
196
+
197
+ ---
198
+
199
+ ### 5. 設備搜尋與多設備選擇機制
107
200
 
108
201
  預設情況下,`BrainBitManager` 會自動過濾周遭的藍牙設備,只尋找型號為 `SensorLEBrainBit` 的腦波儀。
109
202
 
@@ -115,7 +208,7 @@ void CheckImpedanceStatus()
115
208
 
116
209
  ---
117
210
 
118
- ### 5. 實用除錯工具:獲取設備詳細參數
211
+ ### 6. 實用除錯工具:獲取設備詳細參數
119
212
 
120
213
  如果需要查看設備底層的詳細資訊(例如:電量、硬體版本、韌體版本、取樣頻率等),可使用內建的解析器 `SensorInfoProvider.cs` 將複雜的底層參數結構化。
121
214
 
@@ -1,6 +1,7 @@
1
1
  using UnityEngine;
2
2
  using System.Collections;
3
3
  using System.Collections.Generic;
4
+ using SignalMath;
4
5
 
5
6
  /// <summary>
6
7
  /// BrainBit 設備配置
@@ -48,4 +49,19 @@ public class BrainBitConfig
48
49
  /// 停止掃描後等待一段時間再建立連接
49
50
  /// </summary>
50
51
  public float ConnectDelaySeconds = 1.0f;
52
+
53
+ /// <summary>
54
+ /// 情緒處理校正時間(秒)。較長 → 較穩定,但受測者要等更久。
55
+ /// </summary>
56
+ public int EmotionsCalibrationLength = 6;
57
+
58
+ /// <summary>
59
+ /// 啟用 Mental Estimation(依實驗類型決定是否開啟)。
60
+ /// </summary>
61
+ public bool EmotionsMentalEstimation = false;
62
+
63
+ /// <summary>
64
+ /// 情緒分析優先腦側:NONE / LEFT / RIGHT,預設 NONE(雙側平均)。
65
+ /// </summary>
66
+ public SideType EmotionsPrioritySide = SideType.NONE;
51
67
  }
@@ -3,6 +3,7 @@ using System.Collections;
3
3
  using System.Collections.Generic;
4
4
  using LabFrame2023;
5
5
  using System;
6
+ using SignalMath;
6
7
 
7
8
  /// <summary>
8
9
  /// BrainBit EEG 數據
@@ -184,4 +185,72 @@ public class BrainBit_ConnectionData : LabDataBase
184
185
  {
185
186
  return $"{EventType}: {DeviceName} ({DeviceAddress}) - Connected: {IsConnected}";
186
187
  }
188
+ }
189
+
190
+ /// <summary>
191
+ /// BrainBit 情緒/心智狀態數據(專注、放鬆)
192
+ /// </summary>
193
+ [Serializable]
194
+ public class BrainBit_MindData : LabDataBase
195
+ {
196
+ /// <summary>相對專注度(平滑值,0-100,建議使用)</summary>
197
+ public double Attention;
198
+
199
+ /// <summary>相對放鬆度(平滑值,0-100,建議使用)</summary>
200
+ public double Relaxation;
201
+
202
+ /// <summary>瞬時專注度(抖動大,進階用)</summary>
203
+ public double InstAttention;
204
+
205
+ /// <summary>瞬時放鬆度(抖動大,進階用)</summary>
206
+ public double InstRelaxation;
207
+
208
+ public BrainBit_MindData() : base() { }
209
+
210
+ public BrainBit_MindData(MindData raw) : base()
211
+ {
212
+ Attention = raw.RelAttention;
213
+ Relaxation = raw.RelRelaxation;
214
+ InstAttention = raw.InstAttention;
215
+ InstRelaxation = raw.InstRelaxation;
216
+ }
217
+
218
+ public override string ToString()
219
+ => $"Attention: {Attention:F1} | Relaxation: {Relaxation:F1}";
220
+ }
221
+
222
+ /// <summary>
223
+ /// BrainBit 五頻段光譜百分比
224
+ /// </summary>
225
+ [Serializable]
226
+ public class BrainBit_SpectralData : LabDataBase
227
+ {
228
+ /// <summary>δ 波(深度放鬆 / 睡眠)</summary>
229
+ public double Delta;
230
+
231
+ /// <summary>θ 波(冥想 / 創意)</summary>
232
+ public double Theta;
233
+
234
+ /// <summary>α 波(放鬆清醒)</summary>
235
+ public double Alpha;
236
+
237
+ /// <summary>β 波(專注思考)</summary>
238
+ public double Beta;
239
+
240
+ /// <summary>γ 波(高階認知)</summary>
241
+ public double Gamma;
242
+
243
+ public BrainBit_SpectralData() : base() { }
244
+
245
+ public BrainBit_SpectralData(SpectralDataPercents raw) : base()
246
+ {
247
+ Delta = raw.Delta;
248
+ Theta = raw.Theta;
249
+ Alpha = raw.Alpha;
250
+ Beta = raw.Beta;
251
+ Gamma = raw.Gamma;
252
+ }
253
+
254
+ public override string ToString()
255
+ => $"δ:{Delta:F1} θ:{Theta:F1} α:{Alpha:F1} β:{Beta:F1} γ:{Gamma:F1}";
187
256
  }
@@ -5,6 +5,7 @@ using System.Collections.Concurrent;
5
5
  using LabFrame2023;
6
6
  using UnityEngine.Events;
7
7
  using NeuroSDK;
8
+ using SignalMath;
8
9
  using System;
9
10
 
10
11
  /// <summary>
@@ -43,6 +44,21 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
43
44
  /// 是否正在掃描設備
44
45
  /// </summary>
45
46
  public bool IsScanning { get; private set; } = false;
47
+
48
+ /// <summary>
49
+ /// 正在跑情緒處理?
50
+ /// </summary>
51
+ public bool IsProcessingEmotions { get; private set; } = false;
52
+
53
+ /// <summary>
54
+ /// 情緒校正是否已完成?校正完成前 Mind / Spectral 回傳 null
55
+ /// </summary>
56
+ public bool IsEmotionsCalibrated { get; private set; } = false;
57
+
58
+ /// <summary>
59
+ /// 情緒校正進度 0-100
60
+ /// </summary>
61
+ public int CalibrationProgress { get; private set; } = 0;
46
62
  #endregion
47
63
 
48
64
  #region Events
@@ -70,6 +86,31 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
70
86
  /// 錯誤事件
71
87
  /// </summary>
72
88
  public event UnityAction<string> OnError;
89
+
90
+ /// <summary>
91
+ /// 情緒/心智資料更新事件
92
+ /// </summary>
93
+ public event UnityAction<BrainBit_MindData> OnMindDataReceived;
94
+
95
+ /// <summary>
96
+ /// 五頻段光譜資料更新事件
97
+ /// </summary>
98
+ public event UnityAction<BrainBit_SpectralData> OnSpectralDataReceived;
99
+
100
+ /// <summary>
101
+ /// 情緒校正進度 0-100
102
+ /// </summary>
103
+ public event UnityAction<int> OnCalibrationProgress;
104
+
105
+ /// <summary>
106
+ /// 情緒校正完成
107
+ /// </summary>
108
+ public event UnityAction OnCalibrationFinished;
109
+
110
+ /// <summary>
111
+ /// 偵測到情緒處理期間的雜訊(sequence 或雙側)
112
+ /// </summary>
113
+ public event UnityAction<bool> OnEmotionsArtifact;
73
114
  #endregion
74
115
 
75
116
  #region Private Fields
@@ -83,10 +124,12 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
83
124
  private bool _autoWriteEEGData = false;
84
125
  private bool _autoWriteImpedanceData = false;
85
126
 
86
- private Coroutine _connectionMonitorCoroutine;
87
- private Coroutine _scanTimeoutCoroutine;
88
-
89
- private int _reconnectAttempts = 0;
127
+ private Coroutine _connectionMonitorCoroutine;
128
+ private Coroutine _scanTimeoutCoroutine;
129
+
130
+ private int _reconnectAttempts = 0;
131
+ private bool _isInitialized = false;
132
+ private bool _autoConnectScheduled = false;
90
133
 
91
134
  // 用於記錄目前 EEG 寫入資料的標籤 (對應不同的儲存檔案)
92
135
  private string _currentEEGTag = "eeg";
@@ -95,6 +138,16 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
95
138
 
96
139
  // 主執行緒派發佇列,用於將背景執行緒的回呼安全地轉移到主執行緒執行
97
140
  private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();
141
+
142
+ // === Emotions ===
143
+ private EmotionsController _emotionsController;
144
+ private bool _autoWriteEmotionData = false;
145
+ private string _currentMindTag = "mind";
146
+ private string _currentSpectralTag = "spectral";
147
+ private BrainBit_MindData _lastMindData;
148
+ private BrainBit_SpectralData _lastSpectralData;
149
+ // 若為 true,代表 EEG 串流是被情緒處理自動啟動的 — StopEmotionsProcessing 時要一起停
150
+ private bool _emotionsStartedEEG = false;
98
151
  #endregion
99
152
 
100
153
  #region IManager Implementation
@@ -103,20 +156,21 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
103
156
  LabTools.Log("[BrainBit] Initializing BrainBit Manager...");
104
157
 
105
158
  // 載入配置
106
- _config = LabTools.GetConfig<BrainBitConfig>(true);
159
+ if (!EnsureInitialized(nameof(ManagerInit)))
160
+ {
161
+ return;
162
+ }
107
163
 
108
164
  // 初始化數據對象
109
- _lastEEGData = new BrainBit_EEGData();
110
- _lastImpedanceData = new BrainBit_ImpedanceData();
111
165
 
112
166
  // 開始連接監控
113
- _connectionMonitorCoroutine = StartCoroutine(MonitorConnection());
114
167
 
115
168
  // 自動連接
116
- if (_config.AutoConnectOnInit)
117
- {
118
- StartCoroutine(DelayedAutoConnect());
119
- }
169
+ if (_config.AutoConnectOnInit && !_autoConnectScheduled && !IsConnected && !IsScanning)
170
+ {
171
+ _autoConnectScheduled = true;
172
+ StartCoroutine(DelayedAutoConnect());
173
+ }
120
174
 
121
175
  LabTools.Log("[BrainBit] Manager initialized successfully");
122
176
  LabTools.Log($"[BrainBit] Config - AutoConnect: {_config.AutoConnectOnInit}, ScanTimeout: {_config.ScanTimeoutSeconds}s");
@@ -146,6 +200,7 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
146
200
  LabTools.Log("[BrainBit] Disposing BrainBit Manager...");
147
201
 
148
202
  // 停止所有數據流
203
+ StopEmotionsProcessing();
149
204
  StopEEGStream();
150
205
  StopImpedanceStream();
151
206
 
@@ -180,17 +235,59 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
180
235
  }
181
236
  #endregion
182
237
 
183
- #region Public Methods
184
- /// <summary>
238
+ #region Public Methods
239
+ private bool EnsureInitialized(string callerName)
240
+ {
241
+ if (_isInitialized && _config != null)
242
+ {
243
+ return true;
244
+ }
245
+
246
+ try
247
+ {
248
+ _config ??= LabTools.GetConfig<BrainBitConfig>(true);
249
+ if (_config == null)
250
+ {
251
+ LabTools.LogError($"[BrainBit] {callerName} failed - BrainBitConfig is missing");
252
+ OnError?.Invoke("BrainBit config is missing");
253
+ return false;
254
+ }
255
+
256
+ _lastEEGData ??= new BrainBit_EEGData();
257
+ _lastImpedanceData ??= new BrainBit_ImpedanceData();
258
+
259
+ if (_connectionMonitorCoroutine == null)
260
+ {
261
+ _connectionMonitorCoroutine = StartCoroutine(MonitorConnection());
262
+ }
263
+
264
+ _isInitialized = true;
265
+ LabTools.Log($"[BrainBit] Initialization ready for {callerName}");
266
+ return true;
267
+ }
268
+ catch (Exception e)
269
+ {
270
+ LabTools.LogError($"[BrainBit] {callerName} failed during initialization: {e.Message}");
271
+ OnError?.Invoke($"Initialization failed: {e.Message}");
272
+ return false;
273
+ }
274
+ }
275
+
276
+ /// <summary>
185
277
  /// 開始掃描 BrainBit 設備
186
278
  /// </summary>
187
- public void StartScan()
188
- {
189
- if (IsScanning)
190
- {
191
- LabTools.LogError("[BrainBit] Already scanning for devices");
192
- return;
193
- }
279
+ public void StartScan()
280
+ {
281
+ if (!EnsureInitialized(nameof(StartScan)))
282
+ {
283
+ return;
284
+ }
285
+
286
+ if (IsScanning)
287
+ {
288
+ LabTools.LogError("[BrainBit] Already scanning for devices");
289
+ return;
290
+ }
194
291
 
195
292
  try
196
293
  {
@@ -525,10 +622,18 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
525
622
  }
526
623
  }
527
624
 
528
- private IEnumerator ScanTimeout()
529
- {
530
- LabTools.Log($"[BrainBit] Scan timeout set for {_config.ScanTimeoutSeconds} seconds");
531
- yield return new WaitForSeconds(_config.ScanTimeoutSeconds);
625
+ private IEnumerator ScanTimeout()
626
+ {
627
+ if (!EnsureInitialized(nameof(ScanTimeout)))
628
+ {
629
+ IsScanning = false;
630
+ _scanTimeoutCoroutine = null;
631
+ yield break;
632
+ }
633
+
634
+ float timeoutSeconds = _config.ScanTimeoutSeconds;
635
+ LabTools.Log($"[BrainBit] Scan timeout set for {timeoutSeconds} seconds");
636
+ yield return new WaitForSeconds(timeoutSeconds);
532
637
 
533
638
  if (_scanTimeoutCoroutine != null && _currentSensor == null)
534
639
  {
@@ -629,12 +734,18 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
629
734
  /// 延遲連接設備
630
735
  /// </summary>
631
736
  /// <param name="sensorInfo">設備信息</param>
632
- private IEnumerator DelayedConnect(SensorInfo sensorInfo)
633
- {
634
- LabTools.Log($"[BrainBit] Waiting {_config.ConnectDelaySeconds}s before connecting...");
737
+ private IEnumerator DelayedConnect(SensorInfo sensorInfo)
738
+ {
739
+ if (!EnsureInitialized(nameof(DelayedConnect)))
740
+ {
741
+ yield break;
742
+ }
743
+
744
+ float connectDelaySeconds = _config.ConnectDelaySeconds;
745
+ LabTools.Log($"[BrainBit] Waiting {connectDelaySeconds}s before connecting...");
635
746
 
636
747
  // 等待指定時間,確保掃描完全停止
637
- yield return new WaitForSeconds(_config.ConnectDelaySeconds);
748
+ yield return new WaitForSeconds(connectDelaySeconds);
638
749
 
639
750
  // 建立連接
640
751
  ConnectToDevice(sensorInfo);
@@ -698,6 +809,7 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
698
809
  if (IsConnected)
699
810
  {
700
811
  // 停止所有數據流
812
+ StopEmotionsProcessing();
701
813
  StopEEGStream();
702
814
  StopImpedanceStream();
703
815
 
@@ -724,12 +836,18 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
724
836
  }
725
837
  }
726
838
 
727
- private void HandleDisconnection()
728
- {
729
- LabTools.LogError($"[BrainBit] Device disconnected: {ConnectedDeviceName}");
730
-
731
- if (_config.DisconnectNotification)
732
- {
839
+ private void HandleDisconnection()
840
+ {
841
+ LabTools.LogError($"[BrainBit] Device disconnected: {ConnectedDeviceName}");
842
+
843
+ if (!EnsureInitialized(nameof(HandleDisconnection)))
844
+ {
845
+ Disconnect();
846
+ return;
847
+ }
848
+
849
+ if (_config.DisconnectNotification)
850
+ {
733
851
  LabPromptBox.Show($"BrainBit 設備已斷線!\nDevice {ConnectedDeviceName} disconnected!");
734
852
  }
735
853
 
@@ -742,12 +860,18 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
742
860
  Disconnect();
743
861
  }
744
862
 
745
- private IEnumerator AttemptReconnect()
746
- {
747
- _reconnectAttempts++;
748
- LabTools.Log($"[BrainBit] Attempting reconnection ({_reconnectAttempts}/{_config.AutoReconnectAttempts})...");
749
-
750
- yield return new WaitForSeconds(_config.ReconnectIntervalSeconds);
863
+ private IEnumerator AttemptReconnect()
864
+ {
865
+ if (!EnsureInitialized(nameof(AttemptReconnect)))
866
+ {
867
+ yield break;
868
+ }
869
+
870
+ _reconnectAttempts++;
871
+ LabTools.Log($"[BrainBit] Attempting reconnection ({_reconnectAttempts}/{_config.AutoReconnectAttempts})...");
872
+
873
+ float reconnectIntervalSeconds = _config.ReconnectIntervalSeconds;
874
+ yield return new WaitForSeconds(reconnectIntervalSeconds);
751
875
 
752
876
  if (!IsConnected)
753
877
  {
@@ -778,6 +902,12 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
778
902
  }
779
903
  });
780
904
  }
905
+
906
+ // 情緒處理(寄生在同一條 NeuroSDK 背景緒,不開額外 thread)
907
+ if (IsProcessingEmotions)
908
+ {
909
+ _emotionsController?.ProcessData(data);
910
+ }
781
911
  }
782
912
  catch (Exception e)
783
913
  {
@@ -838,13 +968,25 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
838
968
  _currentSensor = null;
839
969
  }
840
970
 
841
- // 重置狀態
842
- IsConnected = false;
843
- IsStreamingEEG = false;
844
- IsStreamingImpedance = false;
845
- IsScanning = false;
971
+ // 清理情緒控制器
972
+ if (_emotionsController != null)
973
+ {
974
+ UnwireEmotionsCallbacks();
975
+ _emotionsController.Dispose();
976
+ _emotionsController = null;
977
+ }
978
+ IsProcessingEmotions = false;
979
+ IsEmotionsCalibrated = false;
846
980
 
847
- LabTools.Log("[BrainBit] Resources cleaned up");
981
+ // 重置狀態
982
+ IsConnected = false;
983
+ IsStreamingEEG = false;
984
+ IsStreamingImpedance = false;
985
+ IsScanning = false;
986
+ _isInitialized = false;
987
+ _autoConnectScheduled = false;
988
+
989
+ LabTools.Log("[BrainBit] Resources cleaned up");
848
990
  }
849
991
  catch (Exception e)
850
992
  {
@@ -852,4 +994,238 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
852
994
  }
853
995
  }
854
996
  #endregion
855
- }
997
+
998
+ #region Emotions Processing
999
+
1000
+ /// <summary>
1001
+ /// 啟動情緒處理(MindData / SpectralData / 校正進度)。
1002
+ /// 若 EEG 串流未啟動,會自動啟動;呼叫 StopEmotionsProcessing 時會同步停止該 EEG 串流。
1003
+ /// </summary>
1004
+ public void StartEmotionsProcessing(bool autoWriteToLabData = true,
1005
+ string mindTag = "mind",
1006
+ string spectralTag = "spectral")
1007
+ {
1008
+ if (!EnsureInitialized(nameof(StartEmotionsProcessing)))
1009
+ {
1010
+ return;
1011
+ }
1012
+
1013
+ if (!IsConnected)
1014
+ {
1015
+ LabTools.LogError("[BrainBit] Device not connected");
1016
+ OnError?.Invoke("Device not connected");
1017
+ return;
1018
+ }
1019
+
1020
+ if (IsProcessingEmotions)
1021
+ {
1022
+ LabTools.LogError("[BrainBit] Emotions processing already active");
1023
+ return;
1024
+ }
1025
+
1026
+ try
1027
+ {
1028
+ _autoWriteEmotionData = autoWriteToLabData;
1029
+ _currentMindTag = string.IsNullOrEmpty(mindTag) ? "mind" : mindTag;
1030
+ _currentSpectralTag = string.IsNullOrEmpty(spectralTag) ? "spectral" : spectralTag;
1031
+
1032
+ // 若 EEG 未啟動,自動啟動並記錄
1033
+ if (!IsStreamingEEG)
1034
+ {
1035
+ StartEEGStream(autoWriteToLabData: false);
1036
+ _emotionsStartedEEG = true;
1037
+ }
1038
+ else
1039
+ {
1040
+ _emotionsStartedEEG = false;
1041
+ }
1042
+
1043
+ // 建立 / 重建 controller(套用目前 BrainBitConfig)
1044
+ _emotionsController?.Dispose();
1045
+ _emotionsController = new EmotionsController(_config);
1046
+ WireEmotionsCallbacks();
1047
+
1048
+ // 校正狀態重置
1049
+ IsEmotionsCalibrated = false;
1050
+ CalibrationProgress = 0;
1051
+ _lastMindData = null;
1052
+ _lastSpectralData = null;
1053
+
1054
+ _emotionsController.StartCalibration();
1055
+ IsProcessingEmotions = true;
1056
+
1057
+ LabTools.Log($"[BrainBit] Emotions processing started (mindTag: {_currentMindTag}, spectralTag: {_currentSpectralTag})");
1058
+ }
1059
+ catch (Exception e)
1060
+ {
1061
+ if (_emotionsStartedEEG)
1062
+ {
1063
+ StopEEGStream();
1064
+ _emotionsStartedEEG = false;
1065
+ }
1066
+ LabTools.LogError($"[BrainBit] Failed to start emotions processing: {e.Message}");
1067
+ OnError?.Invoke($"Emotions processing failed: {e.Message}");
1068
+ }
1069
+ }
1070
+
1071
+ /// <summary>
1072
+ /// 停止情緒處理。若本次 EEG 串流是由情緒處理自動啟動的,同步停止該 EEG 串流;
1073
+ /// 否則保留 EEG 串流(避免誤停使用者正在錄的 EEG)。
1074
+ /// </summary>
1075
+ public void StopEmotionsProcessing()
1076
+ {
1077
+ if (!IsProcessingEmotions) return;
1078
+
1079
+ try
1080
+ {
1081
+ IsProcessingEmotions = false;
1082
+
1083
+ if (_emotionsController != null)
1084
+ {
1085
+ UnwireEmotionsCallbacks();
1086
+ _emotionsController.Dispose();
1087
+ _emotionsController = null;
1088
+ }
1089
+
1090
+ _autoWriteEmotionData = false;
1091
+ IsEmotionsCalibrated = false;
1092
+ CalibrationProgress = 0;
1093
+
1094
+ if (_emotionsStartedEEG && IsStreamingEEG)
1095
+ {
1096
+ StopEEGStream();
1097
+ }
1098
+ _emotionsStartedEEG = false;
1099
+
1100
+ LabTools.Log("[BrainBit] Emotions processing stopped");
1101
+ }
1102
+ catch (Exception e)
1103
+ {
1104
+ LabTools.LogError($"[BrainBit] Error stopping emotions processing: {e.Message}");
1105
+ }
1106
+ }
1107
+
1108
+ /// <summary>
1109
+ /// 重新校正(例如換受測者、中途摘下又戴回)。
1110
+ /// </summary>
1111
+ public void RestartCalibration()
1112
+ {
1113
+ if (!IsProcessingEmotions || _emotionsController == null)
1114
+ {
1115
+ LabTools.LogError("[BrainBit] Cannot restart calibration - emotions processing not active");
1116
+ return;
1117
+ }
1118
+
1119
+ IsEmotionsCalibrated = false;
1120
+ CalibrationProgress = 0;
1121
+ _emotionsController.StartCalibration();
1122
+
1123
+ LabTools.Log("[BrainBit] Emotion calibration restarted");
1124
+ }
1125
+
1126
+ /// <summary>
1127
+ /// 動態切換 MindData 寫入 Tag
1128
+ /// </summary>
1129
+ public void SetMindTag(string tag)
1130
+ {
1131
+ _currentMindTag = string.IsNullOrEmpty(tag) ? "mind" : tag;
1132
+ LabTools.Log($"[BrainBit] Mind data tag dynamically changed to: {_currentMindTag}");
1133
+ }
1134
+
1135
+ /// <summary>
1136
+ /// 動態切換 SpectralData 寫入 Tag
1137
+ /// </summary>
1138
+ public void SetSpectralTag(string tag)
1139
+ {
1140
+ _currentSpectralTag = string.IsNullOrEmpty(tag) ? "spectral" : tag;
1141
+ LabTools.Log($"[BrainBit] Spectral data tag dynamically changed to: {_currentSpectralTag}");
1142
+ }
1143
+
1144
+ /// <summary>
1145
+ /// 取得最新的情緒/心智資料。校正完成前回傳 null。
1146
+ /// </summary>
1147
+ public BrainBit_MindData GetLatestMindData() => _lastMindData;
1148
+
1149
+ /// <summary>
1150
+ /// 取得最新的五頻段光譜資料。校正完成前回傳 null。
1151
+ /// </summary>
1152
+ public BrainBit_SpectralData GetLatestSpectralData() => _lastSpectralData;
1153
+
1154
+ private void WireEmotionsCallbacks()
1155
+ {
1156
+ if (_emotionsController == null) return;
1157
+
1158
+ _emotionsController.progressCalibrationCallback = OnEmotionsCalibrationProgress;
1159
+ _emotionsController.lastMindDataCallback = OnRawMindDataReceived;
1160
+ _emotionsController.lastSpectralDataCallback = OnRawSpectralDataReceived;
1161
+ _emotionsController.isArtefactedSequenceCallback = OnEmotionsArtifactDetected;
1162
+ _emotionsController.isBothSidesArtifactedCallback = OnEmotionsArtifactDetected;
1163
+ }
1164
+
1165
+ private void UnwireEmotionsCallbacks()
1166
+ {
1167
+ if (_emotionsController == null) return;
1168
+
1169
+ _emotionsController.progressCalibrationCallback = null;
1170
+ _emotionsController.lastMindDataCallback = null;
1171
+ _emotionsController.lastSpectralDataCallback = null;
1172
+ _emotionsController.isArtefactedSequenceCallback = null;
1173
+ _emotionsController.isBothSidesArtifactedCallback = null;
1174
+ }
1175
+
1176
+ private void OnEmotionsCalibrationProgress(int progress)
1177
+ {
1178
+ // 這些 callback 由 NeuroSDK/EmotionsController 在背景緒觸發
1179
+ _mainThreadActions.Enqueue(() =>
1180
+ {
1181
+ CalibrationProgress = progress;
1182
+ OnCalibrationProgress?.Invoke(progress);
1183
+
1184
+ if (progress >= 100 && !IsEmotionsCalibrated)
1185
+ {
1186
+ IsEmotionsCalibrated = true;
1187
+ OnCalibrationFinished?.Invoke();
1188
+ LabTools.Log("[BrainBit] Emotion calibration finished");
1189
+ }
1190
+ });
1191
+ }
1192
+
1193
+ private void OnRawMindDataReceived(MindData raw)
1194
+ {
1195
+ var wrapped = new BrainBit_MindData(raw);
1196
+
1197
+ _mainThreadActions.Enqueue(() =>
1198
+ {
1199
+ _lastMindData = wrapped;
1200
+ OnMindDataReceived?.Invoke(wrapped);
1201
+
1202
+ if (_autoWriteEmotionData && LabDataManager.Instance.IsInited)
1203
+ {
1204
+ LabDataManager.Instance.WriteData(wrapped, _currentMindTag);
1205
+ }
1206
+ });
1207
+ }
1208
+
1209
+ private void OnRawSpectralDataReceived(SpectralDataPercents raw)
1210
+ {
1211
+ var wrapped = new BrainBit_SpectralData(raw);
1212
+
1213
+ _mainThreadActions.Enqueue(() =>
1214
+ {
1215
+ _lastSpectralData = wrapped;
1216
+ OnSpectralDataReceived?.Invoke(wrapped);
1217
+
1218
+ if (_autoWriteEmotionData && LabDataManager.Instance.IsInited)
1219
+ {
1220
+ LabDataManager.Instance.WriteData(wrapped, _currentSpectralTag);
1221
+ }
1222
+ });
1223
+ }
1224
+
1225
+ private void OnEmotionsArtifactDetected(bool hasArtifact)
1226
+ {
1227
+ _mainThreadActions.Enqueue(() => OnEmotionsArtifact?.Invoke(hasArtifact));
1228
+ }
1229
+
1230
+ #endregion
1231
+ }
@@ -0,0 +1,115 @@
1
+ using SignalMath;
2
+
3
+
4
+ public class EmotionalMathConfig
5
+ {
6
+ public static EmotionalMathConfig GetDefault(bool isBipolar, BrainBitConfig labConfig = null)
7
+ {
8
+ int samplingFrequencyHz = 250;
9
+
10
+ var mathLib = new MathLibSetting
11
+ {
12
+ sampling_rate = (uint)samplingFrequencyHz,
13
+ process_win_freq = 25,
14
+ fft_window = (uint)samplingFrequencyHz * 2,
15
+ n_first_sec_skipped = 4,
16
+ bipolar_mode = isBipolar,
17
+ squared_spectrum = true,
18
+ channels_number = (uint)1,
19
+ channel_for_analysis = 0
20
+ };
21
+
22
+ var artsDetect = new ArtifactDetectSetting
23
+ {
24
+ art_bord = 110,
25
+ allowed_percent_artpoints = 70,
26
+ raw_betap_limit = 800_000,
27
+ total_pow_border = (uint)(8 * 1e7),
28
+ global_artwin_sec = 4,
29
+ spect_art_by_totalp = false,
30
+ hanning_win_spectrum = false,
31
+ hamming_win_spectrum = true,
32
+ num_wins_for_quality_avg = 100
33
+ };
34
+
35
+ var shortArtsDetect = new ShortArtifactDetectSetting
36
+ {
37
+ ampl_art_detect_win_size = 200,
38
+ ampl_art_zerod_area = 200,
39
+ ampl_art_extremum_border = 25
40
+ };
41
+
42
+ var mentalAndSpectralSettings = new MentalAndSpectralSetting
43
+ {
44
+ n_sec_for_instant_estimation = 2,
45
+ n_sec_for_averaging = 4
46
+ };
47
+
48
+ var emConfigs = new EmotionalMathConfig(samplingFrequencyHz, mathLib, artsDetect, shortArtsDetect, mentalAndSpectralSettings);
49
+
50
+ if (labConfig != null)
51
+ {
52
+ emConfigs.CallibrationLength = labConfig.EmotionsCalibrationLength;
53
+ emConfigs.MentalEstimation = labConfig.EmotionsMentalEstimation;
54
+ emConfigs.PrioritySide = labConfig.EmotionsPrioritySide;
55
+ }
56
+
57
+ return emConfigs;
58
+ }
59
+
60
+ public int SamplingFrequencyHz
61
+ {
62
+ get; set;
63
+ }
64
+
65
+ public MathLibSetting MathLib;
66
+
67
+ public ArtifactDetectSetting ArtifactDetect;
68
+
69
+ public ShortArtifactDetectSetting ShortArtifactDetect;
70
+
71
+ public MentalAndSpectralSetting MentalAndSpectral;
72
+
73
+ public bool MentalEstimation { get; set; } = false;
74
+
75
+ public SideType PrioritySide { get; set; } = SideType.NONE;
76
+
77
+ public int CallibrationLength { get; set; } = 6;
78
+
79
+ public int SkipWinsAfterArtifact { get; set; } = 5;
80
+
81
+ // ZeroSpectWaves
82
+ public bool Active { get; set; } = true;
83
+ public int alpha { get; set; } = 1;
84
+ public int beta { get; set; } = 1;
85
+ public int theta { get; set; } = 1;
86
+ public int delta { get; set; } = 0;
87
+ public int gamma { get; set; } = 0;
88
+
89
+ // WeightsForSpectra
90
+ public double delta_c { get; set; } = 1;
91
+ public double theta_c { get; set; } = 1;
92
+ public double alpha_c { get; set; } = 1;
93
+ public double beta_c { get; set; } = 1;
94
+ public double gamma_c { get; set; } = 1;
95
+
96
+ public bool SpectNormalizationByBandsWidth { get; set; }
97
+
98
+ public bool SpectNormalizationByCoeffs { get; set; }
99
+
100
+ public EmotionalMathConfig(
101
+ int samplingFrequencyHz,
102
+ MathLibSetting mathLib,
103
+ ArtifactDetectSetting artifactDetect,
104
+ ShortArtifactDetectSetting shortArtifactDetect,
105
+ MentalAndSpectralSetting mentalAndSpectral
106
+ )
107
+ {
108
+ SamplingFrequencyHz = samplingFrequencyHz;
109
+ MathLib = mathLib;
110
+ ArtifactDetect = artifactDetect;
111
+ ShortArtifactDetect = shortArtifactDetect;
112
+ MentalAndSpectral = mentalAndSpectral;
113
+ }
114
+
115
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 7c68ef6736878fc4d9a35c9653976b09
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -0,0 +1,133 @@
1
+ using NeuroSDK;
2
+ using SignalMath;
3
+ using System;
4
+ using System.Linq;
5
+
6
+ public class EmotionsController
7
+ {
8
+ private readonly EegEmotionalMath _math;
9
+
10
+ public Action<int> progressCalibrationCallback = null;
11
+ public Action<bool> isArtefactedSequenceCallback = null;
12
+ public Action<bool> isBothSidesArtifactedCallback = null;
13
+ public Action<SpectralDataPercents> lastSpectralDataCallback = null;
14
+ public Action<RawSpectVals> rawSpectralDataCallback = null;
15
+ public Action<MindData> lastMindDataCallback = null;
16
+
17
+ private bool isCalibrated = false;
18
+
19
+ public EmotionsController(BrainBitConfig labConfig = null)
20
+ {
21
+ var config = EmotionalMathConfig.GetDefault(true, labConfig);
22
+
23
+ _math = new EegEmotionalMath(
24
+ config.MathLib,
25
+ config.ArtifactDetect,
26
+ config.ShortArtifactDetect,
27
+ config.MentalAndSpectral);
28
+
29
+ _math?.SetZeroSpectWaves(config.Active, config.delta, config.theta, config.alpha, config.beta, config.gamma);
30
+ _math?.SetWeightsForSpectra(config.delta_c, config.theta_c, config.alpha_c, config.beta_c, config.gamma_c);
31
+ _math?.SetCallibrationLength(config.CallibrationLength);
32
+ _math?.SetMentalEstimationMode(config.MentalEstimation);
33
+ _math?.SetPrioritySide(config.PrioritySide);
34
+ _math?.SetSkipWinsAfterArtifact(config.SkipWinsAfterArtifact);
35
+ _math?.SetSpectNormalizationByBandsWidth(config.SpectNormalizationByBandsWidth);
36
+ _math?.SetSpectNormalizationByCoeffs(config.SpectNormalizationByCoeffs);
37
+ }
38
+
39
+ public void Dispose() { _math.Dispose(); }
40
+
41
+ public void StartCalibration()
42
+ {
43
+ isCalibrated = false;
44
+ _math.StartCalibration();
45
+ }
46
+
47
+ public void ProcessData(BrainBitSignalData[] samples)
48
+ {
49
+ var bipolarSamples = new RawChannels[samples.Length];
50
+
51
+ for (var i = 0; i < samples.Length; i++)
52
+ {
53
+ bipolarSamples[i].LeftBipolar = samples[i].T3 - samples[i].O1;
54
+ bipolarSamples[i].RightBipolar = samples[i].T4 - samples[i].O2;
55
+ }
56
+
57
+ try
58
+ {
59
+ _math.PushData(bipolarSamples);
60
+ _math.ProcessDataArr();
61
+
62
+ resolveArtefacted();
63
+
64
+ if (!isCalibrated)
65
+ {
66
+ processCalibration();
67
+ }
68
+ else
69
+ {
70
+ resolveSpectralData();
71
+ resolveRawSpectralData();
72
+ resolveMindData();
73
+ }
74
+ }
75
+ catch (Exception ex)
76
+ {
77
+ Console.WriteLine(ex.ToString());
78
+ }
79
+ }
80
+
81
+ private void resolveArtefacted()
82
+ {
83
+ // sequence artifacts
84
+ bool isArtifactedSequence = _math.IsArtifactedSequence();
85
+ isArtefactedSequenceCallback?.Invoke(isArtifactedSequence);
86
+
87
+ // both sides artifacts
88
+ bool isBothSideArtifacted = _math.IsBothSidesArtifacted();
89
+ isBothSidesArtifactedCallback?.Invoke(isBothSideArtifacted);
90
+ }
91
+
92
+ private void processCalibration()
93
+ {
94
+ bool wasCalibrated = isCalibrated;
95
+ isCalibrated = _math.CalibrationFinished();
96
+
97
+ if (!isCalibrated)
98
+ {
99
+ int progress = _math.GetCallibrationPercents();
100
+ progressCalibrationCallback?.Invoke(progress);
101
+ }
102
+ else if (!wasCalibrated)
103
+ {
104
+ // Transition: just finished calibrating — emit 100% once
105
+ progressCalibrationCallback?.Invoke(100);
106
+ }
107
+ }
108
+
109
+ private void resolveSpectralData()
110
+ {
111
+ var spectralValues = _math?.ReadSpectralDataPercentsArr();
112
+ if (spectralValues.Length > 0)
113
+ {
114
+ var spectralVal = spectralValues.Last();
115
+ //if(spectralVal.Delta > 0)
116
+ lastSpectralDataCallback?.Invoke(spectralValues.Last());
117
+ }
118
+ }
119
+ private void resolveRawSpectralData()
120
+ {
121
+ var rawSpectralValues = _math.ReadRawSpectralVals();
122
+ rawSpectralDataCallback?.Invoke(rawSpectralValues);
123
+ }
124
+ private void resolveMindData()
125
+ {
126
+ var mentalValues = _math.ReadMentalDataArr();
127
+ if (mentalValues.Length > 0)
128
+ {
129
+ lastMindDataCallback?.Invoke(mentalValues.Last());
130
+ }
131
+ }
132
+
133
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 0877a16ab18a1734bb4c2847f2aa94ba
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -4,7 +4,8 @@
4
4
  "references": [
5
5
  "GUID:446fbb70571d4224d98108c0517d6b29",
6
6
  "GUID:a5a806c53ddbe413484b9d0109675c29",
7
- "GUID:6055be8ebefd69e48b49212b09b47b2f"
7
+ "GUID:6055be8ebefd69e48b49212b09b47b2f",
8
+ "GUID:920348d3c44c9b44493d16cb002928b8"
8
9
  ],
9
10
  "includePlatforms": [],
10
11
  "excludePlatforms": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.xrlab.labframe_brainbit",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "displayName": "Lab Frame 2023 - BrainBit Plugin",
5
5
  "description": "BrainBit Support for LabFrame2023.\nNote: Currently only supports BrainBit in lab!!",
6
6
  "unity": "2022.3",