com.xrlab.labframe_brainbit 1.0.8 → 1.1.0

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
  }
@@ -1,9 +1,11 @@
1
1
  using UnityEngine;
2
2
  using System.Collections;
3
3
  using System.Collections.Generic;
4
+ using System.Collections.Concurrent;
4
5
  using LabFrame2023;
5
6
  using UnityEngine.Events;
6
7
  using NeuroSDK;
8
+ using SignalMath;
7
9
  using System;
8
10
 
9
11
  /// <summary>
@@ -42,6 +44,21 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
42
44
  /// 是否正在掃描設備
43
45
  /// </summary>
44
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;
45
62
  #endregion
46
63
 
47
64
  #region Events
@@ -69,6 +86,31 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
69
86
  /// 錯誤事件
70
87
  /// </summary>
71
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;
72
114
  #endregion
73
115
 
74
116
  #region Private Fields
@@ -91,6 +133,19 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
91
133
  private string _currentEEGTag = "eeg";
92
134
  // 用於記錄目前阻抗寫入資料的標籤
93
135
  private string _currentImpedanceTag = "impedance";
136
+
137
+ // 主執行緒派發佇列,用於將背景執行緒的回呼安全地轉移到主執行緒執行
138
+ private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();
139
+
140
+ // === Emotions ===
141
+ private EmotionsController _emotionsController;
142
+ private bool _autoWriteEmotionData = false;
143
+ private string _currentMindTag = "mind";
144
+ private string _currentSpectralTag = "spectral";
145
+ private BrainBit_MindData _lastMindData;
146
+ private BrainBit_SpectralData _lastSpectralData;
147
+ // 若為 true,代表 EEG 串流是被情緒處理自動啟動的 — StopEmotionsProcessing 時要一起停
148
+ private bool _emotionsStartedEEG = false;
94
149
  #endregion
95
150
 
96
151
  #region IManager Implementation
@@ -119,11 +174,30 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
119
174
  LabTools.Log($"[BrainBit] Config - AutoSelectBestSignal: {_config.AutoSelectBestSignal}, ConnectDelay: {_config.ConnectDelaySeconds}s");
120
175
  }
121
176
 
177
+ /// <summary>
178
+ /// 每幀執行主執行緒佇列中的 Action(處理從背景執行緒派發過來的回呼)
179
+ /// </summary>
180
+ private void Update()
181
+ {
182
+ while (_mainThreadActions.TryDequeue(out var action))
183
+ {
184
+ try
185
+ {
186
+ action?.Invoke();
187
+ }
188
+ catch (Exception e)
189
+ {
190
+ LabTools.LogError($"[BrainBit] Error executing main thread action: {e.Message}");
191
+ }
192
+ }
193
+ }
194
+
122
195
  public IEnumerator ManagerDispose()
123
196
  {
124
197
  LabTools.Log("[BrainBit] Disposing BrainBit Manager...");
125
198
 
126
199
  // 停止所有數據流
200
+ StopEmotionsProcessing();
127
201
  StopEEGStream();
128
202
  StopImpedanceStream();
129
203
 
@@ -174,8 +248,11 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
174
248
  {
175
249
  LabTools.Log("[BrainBit] Starting device scan...");
176
250
 
177
- // 根據 BrainBit SDK2 文檔,確保使用正確的 SensorFamily
178
- _scanner = new Scanner(SensorFamily.SensorLEBrainBit);
251
+ // 重用 Scanner,避免重複建立造成 native 資源洩漏
252
+ if (_scanner == null)
253
+ {
254
+ _scanner = new Scanner(SensorFamily.SensorLEBrainBit);
255
+ }
179
256
  _scanner.EventSensorsChanged += OnSensorsFound;
180
257
 
181
258
  // 開始掃描
@@ -520,37 +597,46 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
520
597
 
521
598
  private void OnSensorsFound(IScanner scanner, IReadOnlyList<SensorInfo> sensors)
522
599
  {
600
+ // 此回呼由 NeuroSDK 在背景執行緒觸發!
601
+ // 必須將所有 Unity API 呼叫(StartCoroutine、StopCoroutine 等)派發到主執行緒
523
602
  try
524
603
  {
525
- LabTools.Log($"[BrainBit] Found {sensors.Count} device(s)");
526
-
527
- // 觸發設備發現事件
528
- OnDeviceFound?.Invoke(new List<SensorInfo>(sensors));
604
+ LabTools.Log($"[BrainBit] Found {sensors.Count} device(s) (background thread)");
529
605
 
530
606
  if (sensors.Count > 0)
531
607
  {
532
- // 重要:根據 BrainBit SDK 要求,必須先停止掃描才能建立連接
533
- StopScan();
534
-
535
- // 選擇要連接的設備
608
+ // 選擇要連接的設備(純邏輯,不涉及 Unity API,可以在背景執行緒執行)
536
609
  SensorInfo targetSensor = SelectBestDevice(sensors);
537
-
538
610
  LabTools.Log($"[BrainBit] Selected device: {targetSensor.Name} (RSSI: {targetSensor.RSSI})");
539
- if (_scanTimeoutCoroutine != null)
611
+
612
+ // 先在背景緒停止 Scanner(SDK 層級,非 Unity API)
613
+ _scanner?.Stop();
614
+ _scanner.EventSensorsChanged -= OnSensorsFound;
615
+
616
+ // 將剩下的操作排入主執行緒執行
617
+ _mainThreadActions.Enqueue(() =>
540
618
  {
541
- StopCoroutine(_scanTimeoutCoroutine);
542
- _scanTimeoutCoroutine = null;
543
- LabTools.Log("[BrainBit] Scan timeout coroutine stopped");
544
- }
619
+ IsScanning = false;
545
620
 
546
- // 等待一段時間後建立連接(確保掃描完全停止)
547
- StartCoroutine(DelayedConnect(targetSensor));
621
+ // 觸發設備發現事件
622
+ OnDeviceFound?.Invoke(new List<SensorInfo>(sensors));
623
+
624
+ if (_scanTimeoutCoroutine != null)
625
+ {
626
+ StopCoroutine(_scanTimeoutCoroutine);
627
+ _scanTimeoutCoroutine = null;
628
+ LabTools.Log("[BrainBit] Scan timeout coroutine stopped");
629
+ }
630
+
631
+ // 等待一段時間後建立連接(確保掃描完全停止)
632
+ StartCoroutine(DelayedConnect(targetSensor));
633
+ });
548
634
  }
549
635
  }
550
636
  catch (Exception e)
551
637
  {
552
638
  LabTools.LogError($"[BrainBit] Error in OnSensorsFound: {e.Message}");
553
- OnError?.Invoke($"Device discovery error: {e.Message}");
639
+ _mainThreadActions.Enqueue(() => OnError?.Invoke($"Device discovery error: {e.Message}"));
554
640
  }
555
641
  }
556
642
 
@@ -612,6 +698,13 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
612
698
  {
613
699
  LabTools.Log($"[BrainBit] Connecting to device: {sensorInfo.Name} ({sensorInfo.Address})");
614
700
 
701
+ // 清理舊的 Sensor(如果有的話),避免 native 資源洩漏
702
+ if (_currentSensor != null)
703
+ {
704
+ try { _currentSensor.Dispose(); } catch (Exception) { }
705
+ _currentSensor = null;
706
+ }
707
+
615
708
  // 創建 Sensor(此時掃描已停止)
616
709
  _currentSensor = _scanner.CreateSensor(sensorInfo) as BrainBitSensor;
617
710
 
@@ -657,6 +750,7 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
657
750
  if (IsConnected)
658
751
  {
659
752
  // 停止所有數據流
753
+ StopEmotionsProcessing();
660
754
  StopEEGStream();
661
755
  StopImpedanceStream();
662
756
 
@@ -716,20 +810,32 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
716
810
 
717
811
  private void OnSignalDataReceived(ISensor sensor, BrainBitSignalData[] data)
718
812
  {
813
+ // 此回呼由 NeuroSDK 在背景執行緒觸發!
719
814
  try
720
815
  {
721
816
  foreach (var packet in data)
722
817
  {
723
- _lastEEGData = new BrainBit_EEGData(packet.T3, packet.T4, packet.O1, packet.O2);
724
-
725
- // 觸發事件
726
- OnEEGDataReceived?.Invoke(_lastEEGData);
818
+ var eegData = new BrainBit_EEGData(packet.T3, packet.T4, packet.O1, packet.O2);
819
+ _lastEEGData = eegData;
727
820
 
728
- // 自動保存到 LabDataManager
729
- if (_autoWriteEEGData && LabDataManager.Instance.IsInited)
821
+ // 將事件觸發和資料寫入排入主執行緒
822
+ _mainThreadActions.Enqueue(() =>
730
823
  {
731
- LabDataManager.Instance.WriteData(_lastEEGData, _currentEEGTag);
732
- }
824
+ // 觸發事件(訂閱者可能更新 UI)
825
+ OnEEGDataReceived?.Invoke(eegData);
826
+
827
+ // 自動保存到 LabDataManager
828
+ if (_autoWriteEEGData && LabDataManager.Instance.IsInited)
829
+ {
830
+ LabDataManager.Instance.WriteData(eegData, _currentEEGTag);
831
+ }
832
+ });
833
+ }
834
+
835
+ // 情緒處理(寄生在同一條 NeuroSDK 背景緒,不開額外 thread)
836
+ if (IsProcessingEmotions)
837
+ {
838
+ _emotionsController?.ProcessData(data);
733
839
  }
734
840
  }
735
841
  catch (Exception e)
@@ -740,24 +846,30 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
740
846
 
741
847
  private void OnResistanceDataReceived(ISensor sensor, BrainBitResistData data)
742
848
  {
849
+ // 此回呼由 NeuroSDK 在背景執行緒觸發!
743
850
  try
744
851
  {
745
- _lastImpedanceData = new BrainBit_ImpedanceData(data.T3, data.T4, data.O1, data.O2);
746
-
747
- // 觸發事件
748
- OnImpedanceDataReceived?.Invoke(_lastImpedanceData);
852
+ var impedanceData = new BrainBit_ImpedanceData(data.T3, data.T4, data.O1, data.O2);
853
+ _lastImpedanceData = impedanceData;
749
854
 
750
- // 檢查阻抗警告: 當有任一通道阻抗超過 200,000 時
751
- if (!_lastImpedanceData.IsImpedanceGood)
855
+ // 將事件觸發和資料寫入排入主執行緒
856
+ _mainThreadActions.Enqueue(() =>
752
857
  {
753
- LabTools.LogError($"[BrainBit] High impedance detected: {_lastImpedanceData.GetImpedanceStatus()}");
754
- }
858
+ // 觸發事件(訂閱者可能更新 UI)
859
+ OnImpedanceDataReceived?.Invoke(impedanceData);
755
860
 
756
- // 自動保存到 LabDataManager
757
- if (_autoWriteImpedanceData && LabDataManager.Instance.IsInited)
758
- {
759
- LabDataManager.Instance.WriteData(_lastImpedanceData, _currentImpedanceTag);
760
- }
861
+ // 檢查阻抗警告: 當有任一通道阻抗超過 200,000 時
862
+ if (!impedanceData.IsImpedanceGood)
863
+ {
864
+ LabTools.LogError($"[BrainBit] High impedance detected: {impedanceData.GetImpedanceStatus()}");
865
+ }
866
+
867
+ // 自動保存到 LabDataManager
868
+ if (_autoWriteImpedanceData && LabDataManager.Instance.IsInited)
869
+ {
870
+ LabDataManager.Instance.WriteData(impedanceData, _currentImpedanceTag);
871
+ }
872
+ });
761
873
  }
762
874
  catch (Exception e)
763
875
  {
@@ -785,6 +897,16 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
785
897
  _currentSensor = null;
786
898
  }
787
899
 
900
+ // 清理情緒控制器
901
+ if (_emotionsController != null)
902
+ {
903
+ UnwireEmotionsCallbacks();
904
+ _emotionsController.Dispose();
905
+ _emotionsController = null;
906
+ }
907
+ IsProcessingEmotions = false;
908
+ IsEmotionsCalibrated = false;
909
+
788
910
  // 重置狀態
789
911
  IsConnected = false;
790
912
  IsStreamingEEG = false;
@@ -799,4 +921,233 @@ public class BrainBitManager : LabSingleton<BrainBitManager>, IManager
799
921
  }
800
922
  }
801
923
  #endregion
924
+
925
+ #region Emotions Processing
926
+
927
+ /// <summary>
928
+ /// 啟動情緒處理(MindData / SpectralData / 校正進度)。
929
+ /// 若 EEG 串流未啟動,會自動啟動;呼叫 StopEmotionsProcessing 時會同步停止該 EEG 串流。
930
+ /// </summary>
931
+ public void StartEmotionsProcessing(bool autoWriteToLabData = true,
932
+ string mindTag = "mind",
933
+ string spectralTag = "spectral")
934
+ {
935
+ if (!IsConnected)
936
+ {
937
+ LabTools.LogError("[BrainBit] Device not connected");
938
+ OnError?.Invoke("Device not connected");
939
+ return;
940
+ }
941
+
942
+ if (IsProcessingEmotions)
943
+ {
944
+ LabTools.LogError("[BrainBit] Emotions processing already active");
945
+ return;
946
+ }
947
+
948
+ try
949
+ {
950
+ _autoWriteEmotionData = autoWriteToLabData;
951
+ _currentMindTag = string.IsNullOrEmpty(mindTag) ? "mind" : mindTag;
952
+ _currentSpectralTag = string.IsNullOrEmpty(spectralTag) ? "spectral" : spectralTag;
953
+
954
+ // 若 EEG 未啟動,自動啟動並記錄
955
+ if (!IsStreamingEEG)
956
+ {
957
+ StartEEGStream(autoWriteToLabData: false);
958
+ _emotionsStartedEEG = true;
959
+ }
960
+ else
961
+ {
962
+ _emotionsStartedEEG = false;
963
+ }
964
+
965
+ // 建立 / 重建 controller(套用目前 BrainBitConfig)
966
+ _emotionsController?.Dispose();
967
+ _emotionsController = new EmotionsController(_config);
968
+ WireEmotionsCallbacks();
969
+
970
+ // 校正狀態重置
971
+ IsEmotionsCalibrated = false;
972
+ CalibrationProgress = 0;
973
+ _lastMindData = null;
974
+ _lastSpectralData = null;
975
+
976
+ _emotionsController.StartCalibration();
977
+ IsProcessingEmotions = true;
978
+
979
+ LabTools.Log($"[BrainBit] Emotions processing started (mindTag: {_currentMindTag}, spectralTag: {_currentSpectralTag})");
980
+ }
981
+ catch (Exception e)
982
+ {
983
+ if (_emotionsStartedEEG)
984
+ {
985
+ StopEEGStream();
986
+ _emotionsStartedEEG = false;
987
+ }
988
+ LabTools.LogError($"[BrainBit] Failed to start emotions processing: {e.Message}");
989
+ OnError?.Invoke($"Emotions processing failed: {e.Message}");
990
+ }
991
+ }
992
+
993
+ /// <summary>
994
+ /// 停止情緒處理。若本次 EEG 串流是由情緒處理自動啟動的,同步停止該 EEG 串流;
995
+ /// 否則保留 EEG 串流(避免誤停使用者正在錄的 EEG)。
996
+ /// </summary>
997
+ public void StopEmotionsProcessing()
998
+ {
999
+ if (!IsProcessingEmotions) return;
1000
+
1001
+ try
1002
+ {
1003
+ IsProcessingEmotions = false;
1004
+
1005
+ if (_emotionsController != null)
1006
+ {
1007
+ UnwireEmotionsCallbacks();
1008
+ _emotionsController.Dispose();
1009
+ _emotionsController = null;
1010
+ }
1011
+
1012
+ _autoWriteEmotionData = false;
1013
+ IsEmotionsCalibrated = false;
1014
+ CalibrationProgress = 0;
1015
+
1016
+ if (_emotionsStartedEEG && IsStreamingEEG)
1017
+ {
1018
+ StopEEGStream();
1019
+ }
1020
+ _emotionsStartedEEG = false;
1021
+
1022
+ LabTools.Log("[BrainBit] Emotions processing stopped");
1023
+ }
1024
+ catch (Exception e)
1025
+ {
1026
+ LabTools.LogError($"[BrainBit] Error stopping emotions processing: {e.Message}");
1027
+ }
1028
+ }
1029
+
1030
+ /// <summary>
1031
+ /// 重新校正(例如換受測者、中途摘下又戴回)。
1032
+ /// </summary>
1033
+ public void RestartCalibration()
1034
+ {
1035
+ if (!IsProcessingEmotions || _emotionsController == null)
1036
+ {
1037
+ LabTools.LogError("[BrainBit] Cannot restart calibration - emotions processing not active");
1038
+ return;
1039
+ }
1040
+
1041
+ IsEmotionsCalibrated = false;
1042
+ CalibrationProgress = 0;
1043
+ _emotionsController.StartCalibration();
1044
+
1045
+ LabTools.Log("[BrainBit] Emotion calibration restarted");
1046
+ }
1047
+
1048
+ /// <summary>
1049
+ /// 動態切換 MindData 寫入 Tag
1050
+ /// </summary>
1051
+ public void SetMindTag(string tag)
1052
+ {
1053
+ _currentMindTag = string.IsNullOrEmpty(tag) ? "mind" : tag;
1054
+ LabTools.Log($"[BrainBit] Mind data tag dynamically changed to: {_currentMindTag}");
1055
+ }
1056
+
1057
+ /// <summary>
1058
+ /// 動態切換 SpectralData 寫入 Tag
1059
+ /// </summary>
1060
+ public void SetSpectralTag(string tag)
1061
+ {
1062
+ _currentSpectralTag = string.IsNullOrEmpty(tag) ? "spectral" : tag;
1063
+ LabTools.Log($"[BrainBit] Spectral data tag dynamically changed to: {_currentSpectralTag}");
1064
+ }
1065
+
1066
+ /// <summary>
1067
+ /// 取得最新的情緒/心智資料。校正完成前回傳 null。
1068
+ /// </summary>
1069
+ public BrainBit_MindData GetLatestMindData() => _lastMindData;
1070
+
1071
+ /// <summary>
1072
+ /// 取得最新的五頻段光譜資料。校正完成前回傳 null。
1073
+ /// </summary>
1074
+ public BrainBit_SpectralData GetLatestSpectralData() => _lastSpectralData;
1075
+
1076
+ private void WireEmotionsCallbacks()
1077
+ {
1078
+ if (_emotionsController == null) return;
1079
+
1080
+ _emotionsController.progressCalibrationCallback = OnEmotionsCalibrationProgress;
1081
+ _emotionsController.lastMindDataCallback = OnRawMindDataReceived;
1082
+ _emotionsController.lastSpectralDataCallback = OnRawSpectralDataReceived;
1083
+ _emotionsController.isArtefactedSequenceCallback = OnEmotionsArtifactDetected;
1084
+ _emotionsController.isBothSidesArtifactedCallback = OnEmotionsArtifactDetected;
1085
+ }
1086
+
1087
+ private void UnwireEmotionsCallbacks()
1088
+ {
1089
+ if (_emotionsController == null) return;
1090
+
1091
+ _emotionsController.progressCalibrationCallback = null;
1092
+ _emotionsController.lastMindDataCallback = null;
1093
+ _emotionsController.lastSpectralDataCallback = null;
1094
+ _emotionsController.isArtefactedSequenceCallback = null;
1095
+ _emotionsController.isBothSidesArtifactedCallback = null;
1096
+ }
1097
+
1098
+ private void OnEmotionsCalibrationProgress(int progress)
1099
+ {
1100
+ // 這些 callback 由 NeuroSDK/EmotionsController 在背景緒觸發
1101
+ _mainThreadActions.Enqueue(() =>
1102
+ {
1103
+ CalibrationProgress = progress;
1104
+ OnCalibrationProgress?.Invoke(progress);
1105
+
1106
+ if (progress >= 100 && !IsEmotionsCalibrated)
1107
+ {
1108
+ IsEmotionsCalibrated = true;
1109
+ OnCalibrationFinished?.Invoke();
1110
+ LabTools.Log("[BrainBit] Emotion calibration finished");
1111
+ }
1112
+ });
1113
+ }
1114
+
1115
+ private void OnRawMindDataReceived(MindData raw)
1116
+ {
1117
+ var wrapped = new BrainBit_MindData(raw);
1118
+
1119
+ _mainThreadActions.Enqueue(() =>
1120
+ {
1121
+ _lastMindData = wrapped;
1122
+ OnMindDataReceived?.Invoke(wrapped);
1123
+
1124
+ if (_autoWriteEmotionData && LabDataManager.Instance.IsInited)
1125
+ {
1126
+ LabDataManager.Instance.WriteData(wrapped, _currentMindTag);
1127
+ }
1128
+ });
1129
+ }
1130
+
1131
+ private void OnRawSpectralDataReceived(SpectralDataPercents raw)
1132
+ {
1133
+ var wrapped = new BrainBit_SpectralData(raw);
1134
+
1135
+ _mainThreadActions.Enqueue(() =>
1136
+ {
1137
+ _lastSpectralData = wrapped;
1138
+ OnSpectralDataReceived?.Invoke(wrapped);
1139
+
1140
+ if (_autoWriteEmotionData && LabDataManager.Instance.IsInited)
1141
+ {
1142
+ LabDataManager.Instance.WriteData(wrapped, _currentSpectralTag);
1143
+ }
1144
+ });
1145
+ }
1146
+
1147
+ private void OnEmotionsArtifactDetected(bool hasArtifact)
1148
+ {
1149
+ _mainThreadActions.Enqueue(() => OnEmotionsArtifact?.Invoke(hasArtifact));
1150
+ }
1151
+
1152
+ #endregion
802
1153
  }
@@ -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.8",
3
+ "version": "1.1.0",
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",