> 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/features/impedance.md).

# Measure contact impedance

> **Applies to:** [Instinct](/bxi-studio/developer/overview/devices/instinct.md) and [xSys](/bxi-studio/developer/overview/devices/xsys.md). Not available on [xBud](/bxi-studio/developer/overview/devices/xbud.md).

Contact impedance tells you whether an electrode is seated. The stream carries a per-channel magnitude, not an impedance in ohms — see [Impedance measurement](/bxi-studio/developer/overview/concepts/impedance.md) for what separates the two.

The stream follows the same six-step lifecycle as EXG, described in [Stream lifecycle](/bxi-studio/developer/overview/concepts/streams.md). Only the feature name changes.

## Add the stream

{% tabs %}
{% tab title="Node.js" %}

```typescript
const streamId = 21;

await client.features.executeOperation("impedance", "add_stream", {
  stream: {
    stream_id: streamId,
    protocol: "udp",
    host: "127.0.0.1",
    port: 9021,
    elements_before_flush: 1,
  },
});
```

{% endtab %}

{% tab title="Python" %}

```python
stream_id = 21

await client.execute_operation("impedance", "add_stream", {
    "stream": {
        "stream_id": stream_id,
        "protocol": "udp",
        "host": "127.0.0.1",
        "port": 9021,
        "elements_before_flush": 1,
    },
})
```

{% endtab %}
{% endtabs %}

The impedance stream runs at 4 Hz, so `elements_before_flush: 1` delivers one packet every 250 ms. Raising it batches several measurements per packet.

## Subscribe and start

Subscribe before starting, so no packet arrives without a handler.

{% tabs %}
{% tab title="Node.js" %}

```typescript
const subscription = await client.features.subscribeToOperation(
  "impedance",
  "stream",
  (update) => {
    if (!update.success) return;

    const payload = update.data.details.payload;
    for (const frame of payload.frames ?? []) {
      for (const channel of frame.channels ?? []) {
        console.log(channel.channel_id, channel.adc_data);
      }
    }
  },
  { stream_id: streamId }
);

const subscriptionId = subscription.data.subscriptionId;

await client.features.executeOperation("impedance", "start_stream", {
  stream_id: streamId,
});
```

{% endtab %}

{% tab title="Python" %}

```python
subscription = await client.subscribe_to_operation(
    "impedance",
    "stream",
    lambda update: [
        print(channel["channel_id"], channel["adc_data"])
        for frame in update["data"]["details"]["payload"].get("frames", [])
        for channel in frame.get("channels", [])
    ],
    {"stream_id": stream_id},
)

subscription_id = subscription["data"]["subscriptionId"]

await client.execute_operation("impedance", "start_stream", {"stream_id": stream_id})
```

{% endtab %}
{% endtabs %}

## Read a frame

Each channel entry carries the same four fields the EXG stream uses. For the impedance feature, `adc_data` is the magnitude the device computed for that channel, not a voltage and not an impedance.

| Field        | Type      | Meaning                                                             |
| ------------ | --------- | ------------------------------------------------------------------- |
| `channel_id` | `number`  | Absolute channel, 0 to 15                                           |
| `adc_data`   | `number`  | Signed 24-bit magnitude for this channel                            |
| `has_error`  | `boolean` | Device flagged this sample                                          |
| `error_info` | `number`  | Device-specific error code, meaningful only when `has_error` is set |

Channel ids are decoded from each sample word rather than from its position in the frame, so a frame that omits a channel stays correctly labelled.

> **Warning:** Skip samples with `has_error` set. The device flags readings it could not produce, and their `adc_data` is not a measurement.

## Convert to ohms

The SDK does not convert magnitudes to ohms or to a contact verdict. Both conversions need inputs the SDK has no source for — a per-channel excitation current from a calibration run, and the device's quality thresholds. [Impedance measurement](/bxi-studio/developer/overview/concepts/impedance.md) sets out the arithmetic and what each step needs, so you can apply it to the magnitudes this stream delivers.

## Clean up

Stop the stream, let the buffer drain, then unsubscribe and remove.

{% tabs %}
{% tab title="Node.js" %}

```typescript
await client.features.executeOperation("impedance", "stop_stream", { stream_id: streamId });
await new Promise((resolve) => setTimeout(resolve, 2000));

await client.features.unsubscribeFromOperation("impedance", "stream", { subscriptionId });
await client.features.executeOperation("impedance", "remove_stream", { stream_id: streamId });
```

{% endtab %}

{% tab title="Python" %}

```python
await client.execute_operation("impedance", "stop_stream", {"stream_id": stream_id})
await asyncio.sleep(2)

await client.unsubscribe_from_operation(
    "impedance", "stream", {"subscriptionId": subscription_id}
)
await client.execute_operation("impedance", "remove_stream", {"stream_id": stream_id})
```

{% endtab %}
{% endtabs %}

Packets already in flight arrive during the drain window, so keep the handler able to run until the unsubscribe returns.

## Next steps

* [Position the headset](/bxi-studio/developer/overview/devices/instinct/motors.md)
* [Stream EXG](/bxi-studio/developer/overview/features/exg.md)
* [Run parallel streams](/bxi-studio/developer/overview/features/parallel.md)
