Skip to main content
EEGBase← Full API docs
Integration Brief · Mendi × EEGBase

Mendi BLE Integration — What We Need

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.

3 values, one afternoon
BLE Service UUID · GATT Characteristic UUID · Byte layout — that is the entire integration surface. Everything else (connect flow, error handling, data storage, visualisation) is already built.

What We Need From Mendi

ItemExample / Current ValueNotesStatus
BLE Service UUID
0000xxxx-0000-1000-8000-00805f9b34fb
Currently: Placeholder: 00001234-0000-1000-8000-00805f9b34fb
Primary GATT service the headband advertisesPending
GATT Characteristic UUID
0000xxxx-0000-1000-8000-00805f9b34fb
Currently: Placeholder: 00001235-0000-1000-8000-00805f9b34fb
Notify characteristic that streams fNIRS dataPending
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 confirmedAssumed

Exact Lines to Update in src/lib/device/mendi.ts

The 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 Has a JS SDK

If Mendi provides a JavaScript/TypeScript SDK (npm package or CDN) instead of raw BLE UUIDs, the swap takes roughly 15 minutes:

REST API Alternative

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.

API Docs · Live Demo · Contact