EEGBase already has a complete WebBluetooth adapter for Mendi. To wire it up to real hardware, we need 3 values — and it can be done in an afternoon.
| Item | Example / Current Value | Notes | Status |
|---|---|---|---|
| BLE Service UUID | 0000xxxx-0000-1000-8000-00805f9b34fb Currently: Placeholder: 00001234-0000-1000-8000-00805f9b34fb | Primary GATT service the headband advertises | Pending |
| GATT Characteristic UUID | 0000xxxx-0000-1000-8000-00805f9b34fb Currently: Placeholder: 00001235-0000-1000-8000-00805f9b34fb | Notify characteristic that streams fNIRS data | Pending |
| Byte layout | 20 bytes · 5 × float32 LE: [oxyHbL, oxyHbR, deoxyHbL, deoxyHbR, reward] Currently: Assumed from Mendi app behaviour — not confirmed | DataView parsing is ready; just needs field positions confirmed | Assumed |
src/lib/device/mendi.tsThe adapter is complete. The only two constants that need real values:
// src/lib/device/mendi.ts — lines 30-31 // ── Current (placeholder) ───────────────────────────────────────────────────── const MENDI_SERVICE_UUID = "00001234-0000-1000-8000-00805f9b34fb"; // ← replace const MENDI_FNIRS_CHAR_UUID = "00001235-0000-1000-8000-00805f9b34fb"; // ← replace // ── After update (example) ──────────────────────────────────────────────────── const MENDI_SERVICE_UUID = "<actual-service-uuid-from-mendi>"; const MENDI_FNIRS_CHAR_UUID = "<actual-characteristic-uuid-from-mendi>";
If the byte layout differs from our assumption, update the _parse() method below. The output shape (DeviceSample) must not change — everything downstream depends on it.
// Current _parse() — assumes 5 × float32 LE (20 bytes)
// Bytes 0-3 : oxyHbLeft (μM)
// Bytes 4-7 : oxyHbRight (μM)
// Bytes 8-11 : deoxyHbLeft (μM)
// Bytes 12-15: deoxyHbRight(μM)
// Bytes 16-19: rewardScore (0-100) — optional, computed if absent
private _parse(view: DataView): DeviceSample {
const oxyHbLeft = view.byteLength >= 4 ? view.getFloat32(0, true) : undefined;
const oxyHbRight = view.byteLength >= 8 ? view.getFloat32(4, true) : undefined;
const deoxyHbLeft = view.byteLength >= 12 ? view.getFloat32(8, true) : undefined;
const deoxyHbRight= view.byteLength >= 16 ? view.getFloat32(12, true) : undefined;
const rewardScore = view.byteLength >= 20 ? view.getFloat32(16, true) : undefined;
// ...
}If Mendi provides a JavaScript/TypeScript SDK (npm package or CDN) instead of raw BLE UUIDs, the swap takes roughly 15 minutes:
If hardware integration is not available yet, Mendi can push completed sessions directly to EEGBase via REST — no BLE required. The same endpoint is used by the EEGBase desktop session flow.
POST /api/v1/sessions
Authorization: Bearer <clinic-id>
Content-Type: application/json
{
"clientId": "<client-uuid>",
"deviceType": "mendi",
"startedAt": "2026-05-11T09:30:00Z",
"durationSeconds": 600,
"samples": [
{
"timestampMs": 0,
"oxyHbLeft": 0.05, // μM — oxygenated Hb, left PFC
"oxyHbRight": 0.04, // μM — oxygenated Hb, right PFC
"deoxyHbLeft": -0.02, // μM — de-oxygenated Hb, left
"deoxyHbRight": -0.03, // μM — de-oxygenated Hb, right
"rewardScore": 62.1 // 0–100, optional (computed if omitted)
}
// ... up to 50,000 samples
],
"preSession": { "focus": 6, "mood": 7, "anxiety": 4, "energy": 6 },
"postSession": { "focus": 8, "mood": 8, "anxiety": 3, "energy": 7 }
}
// Response: 201 Created
{ "sessionId": "3f4a9b2e-..." }A working demo push script is in the repo: scripts/demo-api-push.sh (bash) and scripts/demo-api-push.ts (TypeScript). Run npm run demo:api to test.