> For the complete documentation index, see [llms.txt](https://docs.anthriq.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.anthriq.com/bxi-studio/developer/overview/concepts/streams.md).

# Stream lifecycle

EXG and impedance both deliver continuous frames, and both follow the same six-step lifecycle. The steps and the frame formats are described once here; the task pages cover what to do with the data.

> **Note:** The signal is EXG, covering EEG, ECG, and EMG. The API feature is spelled `eeg`, and the register field that sets its oversampling is `osr_for_exg`. Use `"eeg"` wherever a feature name is passed; EXG is the term for the signal itself.

## Follow the six steps

1. `add_stream` registers the stream and opens its server socket.
2. Subscribe, attaching the callback that receives frames.
3. `start_stream` begins transmission.
4. `stop_stream` ends transmission.
5. Unsubscribe, detaching the callback.
6. `remove_stream` releases the stream on the device.

> **Warning:** A stream that is started but never removed keeps the device transmitting after the process exits. Run steps 4 through 6 in a `finally` block so they happen on the error path too.

Two waits are load-bearing, and skipping either costs data:

| After        | Wait      | Because                                                        |
| ------------ | --------- | -------------------------------------------------------------- |
| `add_stream` | About 1 s | The stream server needs to bind before a subscriber can attach |
| Subscribe    | About 2 s | The WebSocket client needs to connect before `start_stream`    |

## Size the batch

`elements_before_flush` sets how many samples accumulate before a batch is sent, which decides the callback cadence:

| Feature     | Typical rate | `elements_before_flush` | Batch interval |
| ----------- | ------------ | ----------------------- | -------------- |
| `eeg`       | 4 kHz        | `80`                    | 20 ms          |
| `impedance` | 4 Hz         | `1`                     | 250 ms         |

A smaller value lowers latency and raises callback frequency. At 4 kHz with 80-sample batches the callback runs about 50 times a second, which leaves little room for work inside it.

## Let the buffer drain

Frames already in flight arrive after `stop_stream` returns. Allow the buffer to empty before concluding the callback has gone quiet:

| Feature     | Drain window | Expected trailing batches |
| ----------- | ------------ | ------------------------- |
| `eeg`       | 1500 ms      | Up to about 60            |
| `impedance` | 2000 ms      | Up to about 15            |

## Read the frame format

The device emits a fixed 32-byte header followed by rows of channel elements:

```
[0..3]   "CRBX" identifier
[4..7]   stream_id
[8..11]  sequence / wrapper id
[12..15] reserved
[16..19] frameCount       number of timesteps
[20..23] samplesPerFrame  channels per timestep
[24..27] bytesPerSample   8
[28..31] dataSize         frameCount * samplesPerFrame * bytesPerSample
[32..]   data             frameCount rows of (samplesPerFrame x bytesPerSample)
```

> **Note:** Both features use an 8-byte element, so width alone cannot tell them apart. The **feature name** on the notification selects the layout, and the two pack that word differently — get it wrong and you read plausible-looking but incorrect channel ids. A packet whose `bytesPerSample` is not 8 is dropped rather than guessed at.

The SDK performs this decode. The layout matters when reading raw captures or writing a consumer in another language.

### EXG frames

An EXG element packs the sample across the 32-bit word boundary. The SDK reconstructs it and sign-extends, so a negative sample reads as negative.

| Field        | Type      | Description                                            |
| ------------ | --------- | ------------------------------------------------------ |
| `channel_id` | `number`  | Absolute channel, `(adc_id << 3) \| channel_local`     |
| `adc_data`   | `number`  | Signed 24-bit ADC sample                               |
| `has_error`  | `boolean` | Error bit for this sample                              |
| `error_info` | `number`  | 8-bit error detail, meaningful when `has_error` is set |

### Impedance frames

An impedance element embeds its routing in the word rather than taking it from the element's position, and puts the sample in the low bits. The global channel is `dma_id * 8 + channel_local`, giving 0 to 15.

| Field        | Type      | Description                                            |
| ------------ | --------- | ------------------------------------------------------ |
| `channel_id` | `number`  | Absolute channel, 0 to 15                              |
| `adc_data`   | `number`  | Signed 24-bit magnitude for this channel               |
| `has_error`  | `boolean` | Error bit for this sample                              |
| `error_info` | `number`  | 8-bit error detail, meaningful when `has_error` is set |

Because routing is embedded per word, a frame that omits a channel stays correctly labelled rather than shifting every channel after the gap.

> **Warning:** For the impedance feature `adc_data` is a magnitude, not a voltage and not an impedance. Converting it to ohms requires the channel's excitation current, which is measured per device and is not part of the SDK. Treating a magnitude as an impedance produces a confident wrong number. See [Impedance measurement](/bxi-studio/developer/overview/concepts/impedance.md).

Exclude samples with `has_error` set before averaging. The device flags readings it could not produce, and their `adc_data` is not a measurement.

## Share one stream between consumers

Several callbacks can read one stream. The SDK opens a single bridge subscription and fans updates out locally, so a second subscriber adds no device traffic.

Each subscribe call returns its own subscription id. Unsubscribing one leaves the others running, and the bridge subscription is released only when the last local callback goes.

Concurrent subscribes to the same stream are safe: the SDK holds a per-stream lock, so simultaneous calls still produce one bridge subscription.

## Handle a malformed frame

A frame whose header is inconsistent with its body yields an empty frame list rather than an error. A bad packet drops samples instead of tearing down a live subscription.

Every header field is range-checked before use, because the header is data from a device and an implausible frame count would otherwise drive an unbounded allocation.

## Next steps

* [Stream EXG](/bxi-studio/developer/overview/features/exg.md)
* [Measure contact impedance](/bxi-studio/developer/overview/features/impedance.md)
* [Run parallel streams](/bxi-studio/developer/overview/features/parallel.md)
