> 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/devices/xbud/analog-input.md).

# Acquire analog input

> **Applies to:** [xBud](/bxi-studio/developer/overview/devices/xbud.md) only. Anthriq headsets do not declare the `analog_input` feature; their acquisition path is [Stream EXG](/bxi-studio/developer/overview/features/exg.md).

Configure an acquisition task, then read from it — a one-shot sample, a fixed block, or a continuous stream.

The whole sequence as one runnable example is on the [xBud](/bxi-studio/developer/overview/devices/xbud.md) page. This page covers each step in depth.

## Connect

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

```typescript
import { BxiClient } from "@anthriq_dev/bxi-interface";

const client = new BxiClient<"ni-usb-daq">({ deviceType: "ni-usb-daq" });

await client.initialize("localhost");
await client.connect();
```

{% endtab %}

{% tab title="Python" %}

```python
from anthriq_bxi_interface import BxiClient

client = BxiClient(device_type="ni-usb-daq")

await client.initialize("localhost")
await client.connect()
```

{% endtab %}
{% endtabs %}

## Find attached hardware

`scan_devices` enumerates attached hardware. It is declared by xBud and not by `anthriq-instinct`.

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

```typescript
const response = await client.features.executeOperation("system", "scan_devices", {});
```

{% endtab %}

{% tab title="Python" %}

```python
response = await client.execute_operation("system", "scan_devices", {})
```

{% endtab %}
{% endtabs %}

`query` returns detail for one device, or for one channel when `channel` is supplied:

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

```typescript
import { parseQueryInfo } from "@anthriq_dev/bxi-interface";

const response = await client.features.executeOperation("analog_input", "query", {
  device_name: "Dev1",
});

const info = parseQueryInfo(response.data);
console.log(info.product_type, info.ai_max_multi_chan_rate_hz, info.ai_physical_chans);
```

{% endtab %}

{% tab title="Python" %}

```python
from anthriq_bxi_interface import parse_query_info

response = await client.execute_operation("analog_input", "query", {
    "device_name": "Dev1",
})

info = parse_query_info(response.data)
print(info["product_type"], info["ai_max_multi_chan_rate_hz"], info["ai_physical_chans"])
```

{% endtab %}
{% endtabs %}

Check `ai_max_multi_chan_rate_hz` against the rate you intend to configure: it is the aggregate across channels, so four channels at 20 kHz aggregate is 5 kHz each.

Query before configuring to learn the device's maximum rate and supported terminal configurations, rather than assuming them.

## Configure a task

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

```typescript
await client.features.executeOperation("analog_input", "configure", {
  device_name: "Dev1",
  ai_channels: "Dev1/ai0:3",
  sample_clock_rate_hz: 1000,
  terminal_config: "RSE",
  voltage_range_min: -10,
  voltage_range_max: 10,
  sample_mode: "CONTINUOUS",
});
```

{% endtab %}

{% tab title="Python" %}

```python
await client.execute_operation("analog_input", "configure", {
    "device_name": "Dev1",
    "ai_channels": "Dev1/ai0:3",
    "sample_clock_rate_hz": 1000,
    "terminal_config": "RSE",
    "voltage_range_min": -10,
    "voltage_range_max": 10,
    "sample_mode": "CONTINUOUS",
})
```

{% endtab %}
{% endtabs %}

| Field                                    | Type                                                        | Required | Description                    |
| ---------------------------------------- | ----------------------------------------------------------- | -------- | ------------------------------ |
| `device_name`                            | `string`                                                    | Yes      | DAQmx device name              |
| `ai_channels`                            | `string`                                                    | Yes      | Physical channel list or range |
| `sample_clock_rate_hz`                   | `number`                                                    | Yes      | Sample rate                    |
| `voltage_range_min`, `voltage_range_max` | `number`                                                    | No       | Input range in volts           |
| `terminal_config`                        | `"RSE"`, `"NRSE"`, `"Differential"`, `"PseudoDifferential"` | No       | Terminal wiring                |
| `sample_mode`                            | `"FINITE"`, `"CONTINUOUS"`, `"ON_DEMAND"`                   | No       | Acquisition mode               |
| `samples_per_channel`                    | `number`                                                    | No       | Buffer depth per channel       |
| `sample_clock_source`                    | `string`                                                    | No       | External clock terminal        |
| `sample_clock_active_edge`               | `"RISING"`, `"FALLING"`                                     | No       | Clock edge                     |
| `read_timeout_s`                         | `number`                                                    | No       | Per-read timeout               |
| `start_trigger_type`                     | `"NONE"`, `"DIGITAL_EDGE"`, `"ANALOG_EDGE"`                 | No       | Trigger source type            |
| `start_trigger_source`                   | `string`                                                    | No       | Trigger terminal               |
| `start_trigger_edge`                     | `"RISING"`, `"FALLING"`                                     | No       | Trigger edge                   |
| `analog_trigger_level_v`                 | `number`                                                    | No       | Analog trigger level           |
| `delivery_model`                         | `"BLOCKING_READ"`, `"EVERY_N_CALLBACK"`                     | No       | Read strategy                  |

`configure` caches the task. `start_stream`, `stop_stream`, and `read_finite` operate on that cached configuration, so configure before calling them.

## Read samples

Three ways, depending on what you need.

**One shot, independent of any configuration:**

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

```typescript
import { parseSamples } from "@anthriq_dev/bxi-interface";

const response = await client.features.executeOperation("analog_input", "read_single", {
  ai_channels: "Dev1/ai0",
});

const values = parseSamples(response.data);
```

{% endtab %}

{% tab title="Python" %}

```python
from anthriq_bxi_interface import parse_samples

response = await client.execute_operation("analog_input", "read_single", {
    "ai_channels": "Dev1/ai0",
})

values = parse_samples(response.data)
```

{% endtab %}
{% endtabs %}

**A fixed block from the configured task:**

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

```typescript
import { parseStreamFrame } from "@anthriq_dev/bxi-interface";

const response = await client.features.executeOperation("analog_input", "read_finite", {
  samples_per_channel: 500,
});

const frame = parseStreamFrame(response.data);
console.log(frame.n_channels, frame.sample_count, frame.samples.length);
```

{% endtab %}

{% tab title="Python" %}

```python
from anthriq_bxi_interface import parse_stream_frame

response = await client.execute_operation("analog_input", "read_finite", {
    "samples_per_channel": 500,
})

frame = parse_stream_frame(response.data)
print(frame["n_channels"], frame["sample_count"], len(frame["samples"]))
```

{% endtab %}
{% endtabs %}

**Continuously:**

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

```typescript
import { parseSubscriptionFrame } from "@anthriq_dev/bxi-interface";

const subscription = await client.features.subscribeToOperation(
  "analog_input",
  "stream",
  (update) => {
    if (!update.success) return;
    const frame = parseSubscriptionFrame(update.data);
    process(frame.samples);
  },
  {}
);

await client.features.executeOperation("analog_input", "start_stream", {});
```

{% endtab %}

{% tab title="Python" %}

```python
from anthriq_bxi_interface import parse_subscription_frame

def on_frame(update):
    if not update.success:
        return
    frame = parse_subscription_frame(update.data)
    process(frame["samples"])

subscription = await client.subscribe_to_operation(
    "analog_input", "stream", on_frame, {}
)

await client.execute_operation("analog_input", "start_stream", {})
```

{% endtab %}
{% endtabs %}

## Parse the wire format

The stub returns values in a flat string map, so arrays and objects arrive JSON-encoded. Use the exported helpers rather than reading fields directly.

| Helper                   | Reads                                        |
| ------------------------ | -------------------------------------------- |
| `parseSamples`           | `read_single` response into a number array   |
| `parseStreamFrame`       | `read_finite` response into a frame          |
| `parseSubscriptionFrame` | A stream subscription update into a frame    |
| `parseQueryInfo`         | `query` response into device or channel info |

Each accepts either the JSON string or an already-parsed value, so it works whichever form the transport delivered.

A frame carries samples channels-first:

| Field          | Type               | Description                        |
| -------------- | ------------------ | ---------------------------------- |
| `sequence_no`  | `number`           | Frame counter                      |
| `timestamp_ns` | `number`           | Frame timestamp                    |
| `sample_count` | `number`           | Samples per channel                |
| `n_channels`   | `number`           | Channel count                      |
| `layout`       | `"channels_first"` | Array ordering                     |
| `samples`      | `number[][]`       | Shape `(n_channels, sample_count)` |

`samples[channel][index]`, not `samples[index][channel]`. Reading it the other way transposes the data silently and produces plausible-looking nonsense.

## Stop and clean up

Stop the stream before unsubscribing, then shut the client down. The acquisition task lives in the bridge, so a stream left running keeps the device sampling after your process exits.

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

```typescript
await client.features.executeOperation("analog_input", "stop_stream", {});
await client.features.unsubscribeFromOperation("analog_input", "stream", { subscriptionId });
await client.shutdown();
```

{% endtab %}

{% tab title="Python" %}

```python
await client.execute_operation("analog_input", "stop_stream", {})
await client.unsubscribe_from_operation(
    "analog_input", "stream", {"subscriptionId": subscription_id}
)
await client.shutdown()
```

{% endtab %}
{% endtabs %}

## Drive several devices

Each device needs its own bridge process, because the stub holds one acquisition task per bridge. Give each client an `instanceId` and select the device through `bridgeEnv`:

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

```typescript
const dev1 = new BxiClient<"ni-usb-daq">({
  deviceType: "ni-usb-daq",
  instanceId: "dev1",
  bridgeEnv: { NI_DEVICE_NAME: "Dev1" },
});

const dev2 = new BxiClient<"ni-usb-daq">({
  deviceType: "ni-usb-daq",
  instanceId: "dev2",
  bridgeEnv: { NI_DEVICE_NAME: "Dev2" },
});
```

{% endtab %}

{% tab title="Python" %}

```python
dev1 = BxiClient(
    device_type="ni-usb-daq",
    instance_id="dev1",
    bridge_env={"NI_DEVICE_NAME": "Dev1"},
)

dev2 = BxiClient(
    device_type="ni-usb-daq",
    instance_id="dev2",
    bridge_env={"NI_DEVICE_NAME": "Dev2"},
)
```

{% endtab %}
{% endtabs %}

> **Warning:** Use `bridgeEnv` rather than process environment variables. The environment is shared, so on concurrent connects the second write reaches the bridge spawned for the first client and both end up on one device.

See [Install and connect](/bxi-studio/developer/overview/get-started/connect.md).

## Next steps

* [Read device information](/bxi-studio/developer/overview/get-started/device-info.md)
* [Run pipelines](/bxi-studio/developer/overview/backend-services/pipelines.md)
