Skip to main content
This is the contract your integration must satisfy. Apps should see a stable Elata transport surface, not vendor-specific packet formats or Bluetooth details.

The Core Rule

Your device adapter should emit HeadbandFrameV1 frames through a HeadbandTransport. That is the compatibility boundary for browser apps and downstream analysis.
If the frame contract is correct, app code can stay simple. If the frame contract is unstable, everything above it becomes harder to trust.

HeadbandFrameV1

Every transport produces the same top-level frame shape:
interface HeadbandFrameV1 {
  schemaVersion: "v1";
  source: string;
  sequenceId: number;
  emittedAtMs: number;
  eeg: HeadbandSignalBlock;
  ppgRaw?: HeadbandSignalBlock;
  optics?: HeadbandSignalBlock;
  accgyro?: HeadbandSignalBlock;
  battery?: HeadbandBatteryBlock;
}
In practice, most integrations should treat eeg as the required block and add other blocks only when the device exposes them clearly.

HeadbandSignalBlock

The EEG payload should be normalized into this shape:
interface HeadbandSignalBlock {
  sampleRateHz: number;
  channelNames: string[];
  channelCount: number;
  samples: number[][];
  timestampsMs?: number[];
  clockSource?: "device" | "local";
}

What Must Stay Stable

FieldWhat good looks like
sourceA stable, readable identifier such as vendor-ble
sequenceIdMonotonic within the running session
sampleRateHzMatches the actual device behavior
channelNamesStable and documented
channelCountAlways matches the real row width
samplesEach row is one time step and uses the same channel order
timestampsMsOptional, but aligned with the emitted rows when present
clockSourceExplicitly documented as device or local

HeadbandTransport

All transports implement the same lifecycle surface:
interface HeadbandTransport {
  onFrame?: (frame: HeadbandFrameV1) => void;
  onStatus?: (status: HeadbandTransportStatus) => void;
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  start(): Promise<void>;
  stop(): Promise<void>;
}

Lifecycle Expectations

MethodExpected behavior
connect()Pair or attach to the device and prepare the session
start()Begin streaming valid frames
stop()Stop streaming without corrupting the session
disconnect()Release the session and underlying resources
The transition rules matter as much as the methods themselves. A clean integration should make it obvious whether the transport is idle, connected, streaming, degraded, reconnecting, disconnected, or in error.

HeadbandTransportStatus

Status updates should be explicit enough for apps to react correctly:
interface HeadbandTransportStatus {
  state: HeadbandTransportState;
  atMs: number;
  reason?: string;
  errorCode?: string;
  recoverable?: boolean;
  details?: Record<string, unknown>;
}

Practical Rules for Integrators

1

Keep channel order fixed

If the device streams TP9, AF7, AF8, TP10, every emitted row should use that order consistently.
2

Make row width match channel count

A row should never contain more or fewer EEG values than channelCount.
3

Use the right sample rate

Do not hardcode a nominal value if the actual output rate differs.
4

Document timestamp behavior

State whether timestamps come from the device clock or a local browser clock.
5

Surface failures through status updates

Apps need clear signals for disconnects, retries, and unrecoverable errors.

Minimal Usage Pattern

const transport: HeadbandTransport = /* BleTransport or another transport */;

transport.onFrame = (frame) => {
  const eegRows = frame.eeg.samples;
  console.log(eegRows.length);
};

transport.onStatus = (status) => {
  console.log(status.state, status.reason ?? "");
};

await transport.connect();
await transport.start();

// ... later
await transport.stop();
await transport.disconnect();

Next

Protocol Requirements

Gather the packet and metadata details needed to satisfy the contract.

Adapter Implementation

Wire the contract into a real device adapter.