# Notebook display for instruments and publishers

Watch a bench power supply from a notebook: its settings, the latest value of every channel, and where its data goes. Instruments and publishers display as cards, described in the [notebook display guide](https://github.com/nominal-io/instro/blob/main/docs/library/notebook-display.md).

The example uses a `VirtualPSU`, so it needs no hardware. With a `default` Nominal [profile](https://docs.nominal.io/core/sdk/python-client/authentication), it streams the supply’s channels to a Nominal dataset. Without one, it uses an offline stand-in for the dataset and uploads nothing.

From a clone of the repository, `just notebook` opens this file in JupyterLab: right-click `notebook_display.md` and choose **Open With → Jupytext Notebook**.

```ipython3
import tempfile
import time

from instro.lib import set_display_options
from instro.lib.publishers import FilePublisher, QueuedPublisher
from instro.lib.types import BackgroundDaemonConfig
from instro.psu import InstroPSU
from instro.psu.drivers import VirtualPSU

# A dataset RID to stream to with your "default" Nominal profile. With None, the example finds or
# creates its own dataset in that profile's workspace.
DATASET_RID = None

# The file publisher writes JSON Lines here.
capture_dir = tempfile.mkdtemp(prefix="instro-notebook-")
```

## An instrument card

An instrument at the end of a cell displays as a card. Before `start()`, the card shows the driver, the channel count, the background daemon’s settings, the publishers and the default tags. Building it never talks to the hardware.

```ipython3
psu = InstroPSU(
    name="bench_psu",
    driver=VirtualPSU(num_channels=2),
    num_channels=2,
    publishers=[QueuedPublisher(FilePublisher(capture_dir, format="jsonl"))],
    background_config=BackgroundDaemonConfig(interval=0.1),
    # Default tags, added to every published value.
    test_stand="rig-3",
    dut_serial="SN-0042",
)
psu.open()
psu
```

```
InstroPSU(name='bench_psu', driver=VirtualPSU, publishers=[QueuedPublisher], background_running=False)
```

InstroPSU **bench_psu**

| Driver | `VirtualPSU` |
| --- | --- |
| Channel count | 2 |
| Background | stopped (every 100 ms when started) |

Publishers (1)

| queued (0/1000) → jsonl file `/tmp/instro-notebook-_mx2zm76/measurements-2026-11-13-17-29-36.jsonl` |
| --- |

Default tags (3)

| dut_serial | SN-0042 |
| --- | --- |
| instro | 1.23.0 |
| test_stand | rig-3 |

Outside a notebook, `repr()` gives a one-line summary.

```ipython3
print(repr(psu))
```

```myst-ansi
InstroPSU(name='bench_psu', driver=VirtualPSU, publishers=[QueuedPublisher], background_running=False)
```

## While it runs

Once the background daemon runs, the card has a **running** badge and the last loop time. The **Channels** table lists every channel published so far, with its latest value and time. Settings you send, such as the voltage, appear as `.cmd` channels.

```ipython3
# Each setter publishes a .cmd channel.
psu.set_current_limit(0.5, channel=1)
psu.set_voltage(3.3, channel=1)
psu.output_enable(True, channel=1)
# Poll the supply every 0.1 s, then let a few readings arrive.
psu.start()
time.sleep(1.5)
psu
```

```
InstroPSU(name='bench_psu', driver=VirtualPSU, publishers=[QueuedPublisher, DequeInMemoryPublisher], background_running=True)
```

InstroPSU **bench_psu** running

| Driver | `VirtualPSU` |
| --- | --- |
| Channel count | 2 |
| Background | running, every 100 ms |
| Loop time | 100 ms |

Publishers (2)

| queued (0/1000) → jsonl file `/tmp/instro-notebook-_mx2zm76/measurements-2026-11-13-17-29-36.jsonl` |
| --- |
| in-memory buffer (8 channels, last 10 samples each) |

Channels (8)

| Channel | Latest | Time |
| --- | --- | --- |
| `ch1.voltage` | 3.30375 | 2026-11-13T17:29:37Z |
| `ch1.current` | 0.00329927 | 2026-11-13T17:29:37Z |
| `ch1.enabled` | 1 | 2026-11-13T17:29:37Z |
| `ch2.voltage` | 0 | 2026-11-13T17:29:37Z |
| `ch2.current` | 0 | 2026-11-13T17:29:37Z |
| `ch2.enabled` | 0 | 2026-11-13T17:29:37Z |
| `loop_time` | 0.10013 | 2026-11-13T17:29:37Z |
| `daemon_work_time` | 0.000134603 | 2026-11-13T17:29:37Z |

Default tags (3)

| dut_serial | SN-0042 |
| --- | --- |
| instro | 1.23.0 |
| test_stand | rig-3 |

## Publisher cards

Each publisher has its own card. This one is the file publisher inside the `QueuedPublisher`.

```ipython3
psu.publishers[0].publisher
```

```
FilePublisher(file_path='/tmp/instro-notebook-_mx2zm76/measurements-2026-11-13-17-29-36.jsonl', format='jsonl')
```

FilePublisher **measurements-2026-11-13-17-29-36.jsonl**

| Path | `/tmp/instro-notebook-_mx2zm76/measurements-2026-11-13-17-29-36.jsonl` |
| --- | --- |
| Format | jsonl |

## Stream to Nominal

A `NominalCorePublisher` streams every value the instrument publishes to a Nominal dataset. Add it to the running instrument with `add_publisher`, and the instrument’s card lists it with a link to the dataset. A publisher only gets values published after you add it, so the current limit set above isn’t in the dataset.

Show code cell source

Hide code cell source

```ipython3
# Find the dataset to stream to: DATASET_RID, or this example's dataset in your "default"
# profile's workspace, created on the first run. Without a profile, use an offline stand-in
# so the notebook still runs. The docs page collapses this cell.
from unittest.mock import MagicMock, create_autospec

from nominal.config import NominalConfig
from nominal.core import Dataset, NominalClient
from nominal.exceptions import NominalConfigError
from nominal_streaming import NominalDatasetStream

EXAMPLE_PROPERTIES = {"example": "instro-notebook-display"}

try:
    NominalConfig.from_yaml().get_profile("default")
    client = NominalClient.from_profile("default")
except (FileNotFoundError, NominalConfigError):
    client = None

if client is None:
    dataset = create_autospec(Dataset, instance=True)
    dataset.rid = "ri.catalog.main.dataset.00000000-0000-0000-0000-000000000000"
    dataset.name = "bench_psu run (offline stand-in)"
    stream = create_autospec(NominalDatasetStream, instance=True)
    stream._impl = MagicMock()
    dataset.get_write_stream.return_value = stream
    type(dataset).nominal_url = property(lambda _: "https://app.gov.nominal.io/")
    print("No 'default' Nominal profile: using an offline stand-in, so nothing is uploaded.")
elif DATASET_RID:
    dataset = client.get_dataset(DATASET_RID)
else:
    found = client.search_datasets(properties=EXAMPLE_PROPERTIES)
    dataset = found[0] if found else client.create_dataset(
        "instro notebook display example",
        description="Channels streamed by instro's notebook display example.",
        properties=EXAMPLE_PROPERTIES,
    )
```

```myst-ansi
No 'default' Nominal profile: using an offline stand-in, so nothing is uploaded.
```

```ipython3
from instro.lib.publishers import NominalCorePublisher

# Credentials come from the dataset; pass the RID and profile= instead to look it up here.
nominal = NominalCorePublisher(dataset=dataset)
psu.add_publisher(nominal)
time.sleep(2.0)
psu
```

```
InstroPSU(name='bench_psu', driver=VirtualPSU, publishers=[QueuedPublisher, DequeInMemoryPublisher, NominalCorePublisher], background_running=True)
```

InstroPSU **bench_psu** running

| Driver | `VirtualPSU` |
| --- | --- |
| Channel count | 2 |
| Background | running, every 100 ms |
| Loop time | 101 ms |

Publishers (3)

| queued (0/1000) → jsonl file `/tmp/instro-notebook-_mx2zm76/measurements-2026-11-13-17-29-36.jsonl` |
| --- |
| in-memory buffer (8 channels, last 10 samples each) |
| Nominal dataset [bench_psu run (offline stand-in)](https://app.gov.nominal.io/)⧉ |

Channels (8)

| Channel | Latest | Time |
| --- | --- | --- |
| `ch1.voltage` | 3.2954 | 2026-11-13T17:29:39Z |
| `ch1.current` | 0.00330273 | 2026-11-13T17:29:39Z |
| `ch1.enabled` | 1 | 2026-11-13T17:29:39Z |
| `ch2.voltage` | 0 | 2026-11-13T17:29:39Z |
| `ch2.current` | 0 | 2026-11-13T17:29:39Z |
| `ch2.enabled` | 0 | 2026-11-13T17:29:39Z |
| `loop_time` | 0.10105 | 2026-11-13T17:29:39Z |
| `daemon_work_time` | 0.000403338 | 2026-11-13T17:29:39Z |

Default tags (3)

| dut_serial | SN-0042 |
| --- | --- |
| instro | 1.23.0 |
| test_stand | rig-3 |

## Display options

`set_display_options` changes every card, as a call or for the length of a `with` block.

```ipython3
with set_display_options(time_precision="ms", time_format="readable", timezone="local"):
    display(psu)
```

```
InstroPSU(name='bench_psu', driver=VirtualPSU, publishers=[QueuedPublisher, DequeInMemoryPublisher, NominalCorePublisher], background_running=True)
```

InstroPSU **bench_psu** running

| Driver | `VirtualPSU` |
| --- | --- |
| Channel count | 2 |
| Background | running, every 100 ms |
| Loop time | 101 ms |

Publishers (3)

| queued (0/1000) → jsonl file `/tmp/instro-notebook-_mx2zm76/measurements-2026-11-13-17-29-36.jsonl` |
| --- |
| in-memory buffer (8 channels, last 10 samples each) |
| Nominal dataset [bench_psu run (offline stand-in)](https://app.gov.nominal.io/)⧉ |

Channels (8)

| Channel | Latest | Time |
| --- | --- | --- |
| `ch1.voltage` | 3.2954 | 2026-11-13 17:29:39.962 UTC |
| `ch1.current` | 0.00330273 | 2026-11-13 17:29:39.962 UTC |
| `ch1.enabled` | 1 | 2026-11-13 17:29:39.962 UTC |
| `ch2.voltage` | 0 | 2026-11-13 17:29:39.962 UTC |
| `ch2.current` | 0 | 2026-11-13 17:29:39.962 UTC |
| `ch2.enabled` | 0 | 2026-11-13 17:29:39.962 UTC |
| `loop_time` | 0.10105 | 2026-11-13 17:29:39.962 UTC |
| `daemon_work_time` | 0.000403338 | 2026-11-13 17:29:39.962 UTC |

Default tags (3)

| dut_serial | SN-0042 |
| --- | --- |
| instro | 1.23.0 |
| test_stand | rig-3 |

```ipython3
with set_display_options(style="text"):
    display(psu)  # the plain repr
```

```myst-ansi
InstroPSU(name='bench_psu', driver=VirtualPSU, publishers=[QueuedPublisher, DequeInMemoryPublisher, NominalCorePublisher], background_running=True)
```

## A live view

A card is a snapshot taken when the cell runs. To watch it change, refresh a display handle.

```ipython3
# Step the voltage for five seconds and redraw the card after each step.
handle = display(psu, display_id=True)
for step in range(10):
    psu.set_voltage(1.0 + 0.5 * step, channel=1)
    time.sleep(0.5)
    handle.update(psu)
```

```
InstroPSU(name='bench_psu', driver=VirtualPSU, publishers=[QueuedPublisher, DequeInMemoryPublisher, NominalCorePublisher], background_running=True)
```

InstroPSU **bench_psu** running

| Driver | `VirtualPSU` |
| --- | --- |
| Channel count | 2 |
| Background | running, every 100 ms |
| Loop time | 100 ms |

Publishers (3)

| queued (0/1000) → jsonl file `/tmp/instro-notebook-_mx2zm76/measurements-2026-11-13-17-29-36.jsonl` |
| --- |
| in-memory buffer (8 channels, last 10 samples each) |
| Nominal dataset [bench_psu run (offline stand-in)](https://app.gov.nominal.io/)⧉ |

Channels (8)

| Channel | Latest | Time |
| --- | --- | --- |
| `ch1.voltage` | 5.50814 | 2026-11-13T17:29:44Z |
| `ch1.current` | 0.00550054 | 2026-11-13T17:29:44Z |
| `ch1.enabled` | 1 | 2026-11-13T17:29:44Z |
| `ch2.voltage` | 0 | 2026-11-13T17:29:44Z |
| `ch2.current` | 0 | 2026-11-13T17:29:44Z |
| `ch2.enabled` | 0 | 2026-11-13T17:29:44Z |
| `loop_time` | 0.100128 | 2026-11-13T17:29:44Z |
| `daemon_work_time` | 0.000379604 | 2026-11-13T17:29:44Z |

Default tags (3)

| dut_serial | SN-0042 |
| --- | --- |
| instro | 1.23.0 |
| test_stand | rig-3 |

## Clean up

Closing the instrument stops the daemon, flushes the stream to Nominal, and closes every publisher. The Nominal publisher’s card then links to the dataset with everything this notebook sent.

```ipython3
psu.close()
nominal
```

```myst-ansi
Closing QueuedPublisher with 1 queued item(s) that may be dropped (publisher=FilePublisher)
```

```
NominalCorePublisher(dataset='bench_psu run (offline stand-in)', dataset_rid='ri.catalog.main.dataset.12b9377c-be7e-5c94-e8a7-0d9d23929523', credentials='from the dataset passed in', closed=True)
```

NominalCorePublisher **bench_psu run (offline stand-in)** closed [Open in Nominal ↗](https://app.gov.nominal.io/)

| Dataset RID | `ri.catalog.main.dataset.12b9377c-be7e-5c94-e8a7-0d9d23929523`⧉ |
| --- | --- |
| Credentials | from the dataset passed in |
| Batch size | 250000 |
| Max wait | 250 ms |
