Skip to main content
Once your protocol inventory is complete, build the adapter. The goal is to isolate device-specific BLE and packet logic while preserving the shared Elata transport contract for apps. For browser BLE integrations, the adapter should satisfy the BleDeviceLike shape expected by @elata-biosciences/eeg-web-ble. That means the device layer handles discovery, session setup, subscription, and packet decoding, while BleTransport handles the app-facing transport surface.

Implementation Order

1

Implement discovery

Define the requestDevice filters and any optional services required to find the device cleanly in the browser picker.
2

Prepare the session

Connect to GATT, resolve characteristics, and do any startup writes that are required before streaming can begin.
3

Decode packets into EEG rows

Turn vendor packets into number[][] where each row is one time step and the row width matches numEegChannels.
4

Emit stable metadata

Expose samplingRate, eegNames, numEegChannels, and related device metadata accurately.
5

Handle stop and release cleanly

Unsubscribe, stop the stream safely, and release the session without leaking resources.

Minimal Adapter Skeleton

import type { BleDeviceLike } from "@elata-biosciences/eeg-web-ble";

export class VendorBleDevice implements BleDeviceLike {
  isAthena = false;
  samplingRate = 250;
  eegNames = ["CH1", "CH2"];
  numEegChannels = 2;
  opticsChannelCount = 0;

  getBoardInfo() {
    return { device_name: "VENDOR_DEVICE" };
  }

  getCharacteristicInfo() {
    return { characteristics: [] };
  }

  async prepareSession() {
    // Connect to GATT, resolve characteristics, and initialize streaming state
  }

  async releaseSession() {
    // Tear down subscriptions and release the browser session
  }

  async startStream(eegCb, _ppgCb) {
    // Subscribe to notifications and emit decoded EEG rows
    eegCb([[0, 0]]);
  }

  async stopStream() {
    // Stop device streaming and unsubscribe
  }
}

What the Adapter Must Own

ResponsibilityAdapter owns it?
Device discoveryyes
GATT connection and characteristic setupyes
Packet decodingyes
Accurate metadatayes
App-facing transport lifecycleusually through BleTransport
Normalized frame delivery to appsthrough the shared transport contract

Packet Decoding Rules

Keep the decoder boring and deterministic.
RuleWhy it matters
Reject malformed packets earlyPrevents bad rows from reaching apps
Preserve channel orderKeeps downstream features and models consistent
Batch rows only when neededAvoids hidden latency and timestamp confusion
Track packet counters when availableHelps detect drops or corruption
Separate EEG from aux logic cleanlyMakes debugging and testing easier

Session Lifecycle Rules

Your adapter should make these transitions unsurprising:
StageGood behavior
Discoveronly matching devices are shown
Connectrequired services and characteristics resolve clearly
Startstream begins once, without duplicate subscriptions
Stopnotifications stop cleanly
Releasethe session can be started again later

Reliability Notes

startStreaming() is the safest default for app consumers because it combines connect and start. Your adapter should still make the underlying connect() and start() steps reliable on their own.
If the device exposes auxiliary signals such as PPG, optics, IMU, or battery, map them only when the semantics are clear. Do not force Muse-specific assumptions onto non-Muse hardware.

Next

Validate the integration

Turn the adapter into a reliable transport under real failure conditions.

Prepare the handoff

Package the integration with the docs and caveats other teams need.