Skip to content

Looking Glass Bridge

quiltwright.bridge

Looking Glass Bridge HTTP client.

Casts a saved quilt onto a connected panel, and the transport controls (pause / resume / stop) that go with it. Stdlib only -- importing this module must not load VTK.

Bridge's HTTP API requires PUT. It answers POST with 200 OK and an empty body, so the wrong verb fails silently.

Re-exported from :mod:quiltwright.lfd for one release, so from quiltwright.lfd import cast_quilt keeps working.

Part of Quiltwright -- https://github.com/Flux-Frontiers/quiltwright Author: Eric G. Suchanek, PhD

available_output_devices(bridge_url, timeout, *, token=None)

Ask Bridge which output devices it can see.

Every entry Bridge reports, panel or not -- an ordinary monitor shows up with hardware_version: "thirdparty". Filter on that field yourself to count panels only; :func:cast_quilt's own check does not distinguish a panel from a monitor, since head_index=-1 deliberately accepts either -- it only rejects an empty list, the state that means nothing is registered at all.

Parameters:

Name Type Description Default
bridge_url str

Base URL of the Bridge HTTP API.

required
timeout float

HTTP timeout in seconds.

required
token str | None

An existing orchestration token, to skip a redundant enter_orchestration round trip when the caller already holds one (:func:cast_quilt does). Entered fresh when omitted.

None

Returns:

Type Description
list[dict[str, str]]

One dict per head: {"index", "hardware_version", "hwid"}.

Source code in src/quiltwright/bridge.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def available_output_devices(
    bridge_url: str, timeout: float, *, token: str | None = None
) -> list[dict[str, str]]:
    """Ask Bridge which output devices it can see.

    Every entry Bridge reports, panel or not -- an ordinary monitor shows up
    with ``hardware_version: "thirdparty"``. Filter on that field yourself to
    count panels only; :func:`cast_quilt`'s own check does not distinguish
    a panel from a monitor, since ``head_index=-1`` deliberately accepts
    either -- it only rejects an *empty* list, the state that means nothing
    is registered at all.

    :param bridge_url: Base URL of the Bridge HTTP API.
    :param timeout: HTTP timeout in seconds.
    :param token: An existing orchestration token, to skip a redundant
        ``enter_orchestration`` round trip when the caller already holds one
        (:func:`cast_quilt` does). Entered fresh when omitted.
    :return: One dict per head: ``{"index", "hardware_version", "hwid"}``.
    """
    if token is None:
        token = enter_orchestration(bridge_url, timeout)
    payload = bridge_post(bridge_url, "available_output_devices", {"orchestration": token}, timeout)
    heads = []
    for index, entry in (payload.get("payload", {}).get("value", {}) or {}).items():
        value = entry.get("value", {})
        heads.append(
            {
                "index": str(index),
                "hardware_version": _field(value, "hardwareVersion") or "?",
                "hwid": _field(value, "hwid"),
            }
        )
    return heads

bridge_post(bridge_url, endpoint, payload, timeout)

Send a JSON payload to a Bridge endpoint and decode the response.

Bridge's HTTP API expects PUT. It answers POST with 200 OK and an empty body, so using the wrong verb fails silently: the caller reads back no orchestration token and every later call is a no-op.

Source code in src/quiltwright/bridge.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def bridge_post(bridge_url: str, endpoint: str, payload: dict, timeout: float) -> dict:
    """Send a JSON payload to a Bridge endpoint and decode the response.

    Bridge's HTTP API expects ``PUT``.  It answers ``POST`` with ``200 OK``
    and an *empty* body, so using the wrong verb fails silently: the caller
    reads back no orchestration token and every later call is a no-op.
    """
    req = urllib.request.Request(
        f"{bridge_url}/{endpoint}",
        data=json.dumps(payload).encode(),
        headers={"Content-Type": "application/json"},
        method="PUT",
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        body = resp.read().decode()
    return json.loads(body) if body else {}

cast_playlist(entries, *, bridge_url=BRIDGE_URL, playlist=None, timeout=10.0, head_index=-1, duration_ms=20000, loop=True)

Play several saved quilts on the connected Looking Glass as one playlist.

Follows Bridge's orchestration sequence: enter orchestration, show the display window, create a playlist, insert every quilt into it, and only then play it. The order is the contract. A playlist built completely before play_playlist advances through its entries and loops -- verified on a Looking Glass Go under Bridge 2.6.3, with 11x6 and 8x6 quilts mixed in one list. Entries inserted into a playlist that is already playing do not take effect.

Every call plays a playlist of its own. instance_playlist on a name Bridge already holds does not start that playlist over: it hands back the existing one, insert_playlist_entry adds to it, and play_playlist carries on showing what was already playing. Reusing one name therefore made every cast after the first a silent no-op -- each returned Completion while the panel kept the first quilt, confirmed on a real panel. Clearing the old playlist first is not available: delete_playlist hangs Bridge (see :func:stop_quilt). A fresh name per call is what reliably replaces the picture.

Parameters:

Name Type Description Default
entries Sequence[tuple[str | Path, QuiltSpec]]

(path, spec) pairs, in playing order. Each path is on the Bridge host's filesystem; each spec carries that quilt's own tiling and aspect, so grids may differ between entries.

required
bridge_url str

Base URL of the Bridge HTTP API.

BRIDGE_URL
playlist str | None

Bridge playlist name. None (the default) generates a unique quiltwright-<id> name. Passing a name Bridge already holds adds to that playlist rather than replacing it, for the reason above.

None
timeout float

HTTP timeout in seconds per request.

10.0
head_index int

Which Bridge output device to play on. -1 lets Bridge choose, which is right on a single-panel machine. Bridge enumerates ordinary monitors alongside Looking Glass panels -- a laptop screen appears as hardwareVersion: thirdparty with no calibration -- so on a multi-display box the default can land the window somewhere that is not the glass. available_output_devices lists the indices; quiltwright cast --check prints them.

-1
duration_ms int

How long each entry shows before the next, in milliseconds.

20000
loop bool

Start over after the last entry.

True

Returns:

Type Description
dict

Decoded JSON response of the final play_playlist call.

Raises:

Type Description
ValueError

If entries is empty -- checked before Bridge is contacted, so nothing on the panel changes.

RuntimeError

If Bridge answers but reports zero output devices -- a state its own orchestration calls do not otherwise fail on. Casting to an ordinary monitor (no panel, but at least one device) is unaffected; this only catches the case with nothing to show on at all.

Source code in src/quiltwright/bridge.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def cast_playlist(
    entries: Sequence[tuple[str | Path, QuiltSpec]],
    *,
    bridge_url: str = BRIDGE_URL,
    playlist: str | None = None,
    timeout: float = 10.0,
    head_index: int = -1,
    duration_ms: int = 20000,
    loop: bool = True,
) -> dict:
    """Play several saved quilts on the connected Looking Glass as one playlist.

    Follows Bridge's orchestration sequence: enter orchestration, show the
    display window, create a playlist, insert every quilt into it, and only
    then play it.  The order is the contract.  A playlist built completely
    before ``play_playlist`` advances through its entries and loops -- verified
    on a Looking Glass Go under Bridge 2.6.3, with 11x6 and 8x6 quilts mixed in
    one list.  Entries inserted into a playlist that is *already* playing do
    not take effect.

    Every call plays a playlist of its own.  ``instance_playlist`` on a name
    Bridge already holds does not start that playlist over: it hands back the
    existing one, ``insert_playlist_entry`` adds to it, and ``play_playlist``
    carries on showing what was already playing.  Reusing one name therefore
    made every cast after the first a silent no-op -- each returned
    ``Completion`` while the panel kept the first quilt, confirmed on a real
    panel.  Clearing the old playlist first is not available:
    ``delete_playlist`` hangs Bridge (see :func:`stop_quilt`).  A fresh name
    per call is what reliably replaces the picture.

    :param entries: ``(path, spec)`` pairs, in playing order.  Each path is on
        the *Bridge host's* filesystem; each spec carries that quilt's own
        tiling and aspect, so grids may differ between entries.
    :param bridge_url: Base URL of the Bridge HTTP API.
    :param playlist: Bridge playlist name.  ``None`` (the default) generates a
        unique ``quiltwright-<id>`` name.  Passing a name Bridge already holds
        adds to that playlist rather than replacing it, for the reason above.
    :param timeout: HTTP timeout in seconds per request.
    :param head_index: Which Bridge output device to play on.  ``-1`` lets
        Bridge choose, which is right on a single-panel machine.  Bridge
        enumerates ordinary monitors alongside Looking Glass panels -- a
        laptop screen appears as ``hardwareVersion: thirdparty`` with no
        calibration -- so on a multi-display box the default can land the
        window somewhere that is not the glass.  ``available_output_devices``
        lists the indices; ``quiltwright cast --check`` prints them.
    :param duration_ms: How long each entry shows before the next, in
        milliseconds.
    :param loop: Start over after the last entry.
    :raises ValueError: If *entries* is empty -- checked before Bridge is
        contacted, so nothing on the panel changes.
    :raises RuntimeError: If Bridge answers but reports zero output devices
        -- a state its own orchestration calls do not otherwise fail on.
        Casting to an ordinary monitor (no panel, but at least one device)
        is unaffected; this only catches the case with nothing to show on
        at all.
    :return: Decoded JSON response of the final ``play_playlist`` call.
    """
    if not entries:
        raise ValueError("a playlist needs at least one quilt")
    if playlist is None:
        playlist = f"quiltwright-{uuid.uuid4().hex[:8]}"
    token = enter_orchestration(bridge_url, timeout)

    # Every orchestration call below returns 200 whether or not anything is
    # actually listening -- confirmed live: Bridge answered `Completion` on
    # every step while `available_output_devices` reported zero entries, so
    # a `cast_quilt` return was never distinguishable from "showing on
    # nothing". `--check` diagnosed this from the CLI already; casting
    # itself did not.
    heads = available_output_devices(bridge_url, timeout, token=token)
    if not heads:
        raise RuntimeError(
            f"Looking Glass Bridge at {bridge_url} reports no output devices. "
            "It is most likely wedged -- quit and relaunch it."
        )

    bridge_post(
        bridge_url,
        "show_window",
        {"orchestration": token, "show_window": True, "head_index": head_index},
        timeout,
    )
    bridge_post(
        bridge_url,
        "instance_playlist",
        {"orchestration": token, "name": playlist, "loop": loop},
        timeout,
    )
    for index, (quilt_path, spec) in enumerate(entries):
        bridge_post(
            bridge_url,
            "insert_playlist_entry",
            {
                "orchestration": token,
                "name": playlist,
                "index": index,
                "uri": str(Path(quilt_path).resolve()),
                "rows": spec.rows,
                "cols": spec.columns,
                "aspect": spec.aspect,
                "view_count": spec.n_views,
                "durationMS": duration_ms,
                "isRGBD": 0,
            },
            timeout,
        )
    return bridge_post(
        bridge_url,
        "play_playlist",
        {"orchestration": token, "name": playlist, "head_index": head_index},
        timeout,
    )

cast_quilt(quilt_path, spec, *, bridge_url=BRIDGE_URL, playlist=None, timeout=10.0, head_index=-1)

Show a saved quilt on the connected Looking Glass via Bridge.

Requires Looking Glass Bridge <https://lookingglassfactory.com/software/looking-glass-bridge>_ (>= 2.2) running on the machine the display is plugged into. A one-quilt :func:cast_playlist: see it for the orchestration sequence, for why every call plays a playlist of its own, and for the full parameters.

Parameters:

Name Type Description Default
quilt_path str | Path

Path to a quilt PNG on the Bridge host's filesystem.

required
spec QuiltSpec

Quilt specification (tiling + aspect sent to Bridge).

required
bridge_url str

Base URL of the Bridge HTTP API.

BRIDGE_URL
playlist str | None

Bridge playlist name; None generates a fresh one, so the cast replaces whatever is showing.

None
timeout float

HTTP timeout in seconds per request.

10.0
head_index int

Which Bridge output device to play on; -1 lets Bridge choose.

-1

Returns:

Type Description
dict

Decoded JSON response of the final play_playlist call.

Raises:

Type Description
RuntimeError

If Bridge answers but reports zero output devices.

Source code in src/quiltwright/bridge.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def cast_quilt(
    quilt_path: str | Path,
    spec: QuiltSpec,
    *,
    bridge_url: str = BRIDGE_URL,
    playlist: str | None = None,
    timeout: float = 10.0,
    head_index: int = -1,
) -> dict:
    """Show a saved quilt on the connected Looking Glass via Bridge.

    Requires `Looking Glass Bridge <https://lookingglassfactory.com/software/looking-glass-bridge>`_
    (>= 2.2) running on the machine the display is plugged into.  A one-quilt
    :func:`cast_playlist`: see it for the orchestration sequence, for why
    every call plays a playlist of its own, and for the full parameters.

    :param quilt_path: Path to a quilt PNG on the *Bridge host's* filesystem.
    :param spec: Quilt specification (tiling + aspect sent to Bridge).
    :param bridge_url: Base URL of the Bridge HTTP API.
    :param playlist: Bridge playlist name; ``None`` generates a fresh one, so
        the cast replaces whatever is showing.
    :param timeout: HTTP timeout in seconds per request.
    :param head_index: Which Bridge output device to play on; ``-1`` lets
        Bridge choose.
    :raises RuntimeError: If Bridge answers but reports zero output devices.
    :return: Decoded JSON response of the final ``play_playlist`` call.
    """
    return cast_playlist(
        [(quilt_path, spec)],
        bridge_url=bridge_url,
        playlist=playlist,
        timeout=timeout,
        head_index=head_index,
    )

enter_orchestration(bridge_url, timeout)

Enter (or rejoin) the default Bridge orchestration session.

Bridge scopes all playback control to an orchestration session; calling enter_orchestration again while one is already active returns the same token rather than starting a new session, so every helper in this module can call this independently without stepping on the others.

Returns:

Type Description
str

The orchestration token required by every other Bridge call.

Source code in src/quiltwright/bridge.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def enter_orchestration(bridge_url: str, timeout: float) -> str:
    """Enter (or rejoin) the default Bridge orchestration session.

    Bridge scopes all playback control to an orchestration session; calling
    ``enter_orchestration`` again while one is already active returns the
    *same* token rather than starting a new session, so every helper in this
    module can call this independently without stepping on the others.

    :return: The orchestration token required by every other Bridge call.
    """
    resp = bridge_post(bridge_url, "enter_orchestration", {"name": "default"}, timeout)
    token = resp.get("payload", {}).get("value", "")
    if not token:
        raise RuntimeError(
            f"Looking Glass Bridge at {bridge_url} returned no orchestration token "
            f"(response: {resp!r}).  Is Bridge running and >= 2.2?"
        )
    return token

pause_quilt(*, bridge_url=BRIDGE_URL, timeout=10.0)

Pause playback on the connected Looking Glass.

Freezes the current frame; the playlist and its position are retained, so :func:resume_quilt continues from where it left off. This is Bridge's transport control group -- there is no stop_playlist or pause_playlist endpoint (a guessed endpoint name doesn't 404: Bridge answers with 200 OK and an empty body, indistinguishable from a slow success unless you check that the response has no status field). Confirmed against the endpoint list in the official bridge.js <https://github.com/Looking-Glass/bridge.js>_ SDK source.

Parameters:

Name Type Description Default
bridge_url str

Base URL of the Bridge HTTP API.

BRIDGE_URL
timeout float

HTTP timeout in seconds.

10.0

Returns:

Type Description
dict

Decoded JSON response of the transport_control_pause call.

Source code in src/quiltwright/bridge.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
def pause_quilt(*, bridge_url: str = BRIDGE_URL, timeout: float = 10.0) -> dict:
    """Pause playback on the connected Looking Glass.

    Freezes the current frame; the playlist and its position are retained,
    so :func:`resume_quilt` continues from where it left off. This is
    Bridge's *transport control* group -- there is no ``stop_playlist`` or
    ``pause_playlist`` endpoint (a guessed endpoint name doesn't 404: Bridge
    answers with ``200 OK`` and an empty body, indistinguishable from a slow
    success unless you check that the response has no ``status`` field).
    Confirmed against the endpoint list in the official
    `bridge.js <https://github.com/Looking-Glass/bridge.js>`_ SDK source.

    :param bridge_url: Base URL of the Bridge HTTP API.
    :param timeout: HTTP timeout in seconds.
    :return: Decoded JSON response of the ``transport_control_pause`` call.
    """
    token = enter_orchestration(bridge_url, timeout)
    return bridge_post(bridge_url, "transport_control_pause", {"orchestration": token}, timeout)

resume_quilt(*, bridge_url=BRIDGE_URL, timeout=10.0)

Resume playback after :func:pause_quilt.

Parameters:

Name Type Description Default
bridge_url str

Base URL of the Bridge HTTP API.

BRIDGE_URL
timeout float

HTTP timeout in seconds.

10.0

Returns:

Type Description
dict

Decoded JSON response of the transport_control_play call.

Source code in src/quiltwright/bridge.py
332
333
334
335
336
337
338
339
340
def resume_quilt(*, bridge_url: str = BRIDGE_URL, timeout: float = 10.0) -> dict:
    """Resume playback after :func:`pause_quilt`.

    :param bridge_url: Base URL of the Bridge HTTP API.
    :param timeout: HTTP timeout in seconds.
    :return: Decoded JSON response of the ``transport_control_play`` call.
    """
    token = enter_orchestration(bridge_url, timeout)
    return bridge_post(bridge_url, "transport_control_play", {"orchestration": token}, timeout)

save_and_cast_quilt(quilt, stem, spec, *, cast=True, bridge_url=BRIDGE_URL, timeout=10.0)

Write a quilt to disk, then hand Bridge the path to it.

The two calls this composes take different argument types, and the mistake is invisible until a panel is connected: :func:save_quilt takes the array, :func:cast_quilt takes a path. Passing the array to the caster raises argument should be a str or an os.PathLike object ... not 'ndarray' -- after the render, which for a ray-traced quilt is minutes later and the worst possible moment to find out.

The file is confirmed on disk before Bridge is contacted, and a failed cast is returned rather than raised, so losing the display never costs the render.

Parameters:

Name Type Description Default
quilt ndarray

RGB array from :func:render_quilt or :func:assemble_quilt.

required
stem str | Path

Output path without the quilt suffix or extension.

required
spec QuiltSpec

Quilt specification, used for both the filename and Bridge.

required
cast bool

Whether to push the written file to the Looking Glass. False writes and returns without contacting Bridge.

True
bridge_url str

Base URL of the Bridge HTTP API.

BRIDGE_URL
timeout float

HTTP timeout in seconds per Bridge request.

10.0

Returns:

Type Description
tuple[Path, str | None]

(written path, error message or None). The path is always real; a non-None error means the file is on disk but is not showing.

Source code in src/quiltwright/bridge.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def save_and_cast_quilt(
    quilt: np.ndarray,
    stem: str | Path,
    spec: QuiltSpec,
    *,
    cast: bool = True,
    bridge_url: str = BRIDGE_URL,
    timeout: float = 10.0,
) -> tuple[Path, str | None]:
    """Write a quilt to disk, then hand Bridge the **path** to it.

    The two calls this composes take different argument types, and the mistake
    is invisible until a panel is connected: :func:`save_quilt` takes the
    array, :func:`cast_quilt` takes a path.  Passing the array to the caster
    raises ``argument should be a str or an os.PathLike object ... not
    'ndarray'`` -- after the render, which for a ray-traced quilt is minutes
    later and the worst possible moment to find out.

    The file is confirmed on disk before Bridge is contacted, and a failed
    cast is *returned* rather than raised, so losing the display never costs
    the render.

    :param quilt: RGB array from :func:`render_quilt` or :func:`assemble_quilt`.
    :param stem: Output path *without* the quilt suffix or extension.
    :param spec: Quilt specification, used for both the filename and Bridge.
    :param cast: Whether to push the written file to the Looking Glass.
        ``False`` writes and returns without contacting Bridge.
    :param bridge_url: Base URL of the Bridge HTTP API.
    :param timeout: HTTP timeout in seconds per Bridge request.
    :return: ``(written path, error message or None)``.  The path is always
        real; a non-None error means the file is on disk but is not showing.
    """
    out = save_quilt(quilt, stem, spec)
    if not cast:
        return out, None
    if not out.exists():
        return out, f"{out} was not written"
    try:
        cast_quilt(out, spec, bridge_url=bridge_url, timeout=timeout)
    except Exception as exc:  # noqa: BLE001 - the quilt file is kept regardless
        return out, str(exc)
    return out, None

stop_quilt(*, bridge_url=BRIDGE_URL, timeout=10.0)

Stop playback: pause the current frame and hide the display window.

Bridge's own bridge.js <https://github.com/Looking-Glass/bridge.js>_ SDK documents delete_playlist as the way to stop a playlist, and an earlier version of this function called it. In testing it reliably left Bridge unresponsive to every further HTTP call -- reproduced twice, once mid-video and once on a single still image, so it isn't a large-file decode race. This function deliberately avoids delete_playlist and reaches the same end state (nothing visible, playback halted) through calls already proven safe: the playlist from :func:cast_quilt is left instantiated but paused and hidden, rather than deleted. The next :func:cast_quilt plays a playlist of its own rather than reusing this one, so what is left behind does not block it.

Parameters:

Name Type Description Default
bridge_url str

Base URL of the Bridge HTTP API.

BRIDGE_URL
timeout float

HTTP timeout in seconds.

10.0

Returns:

Type Description
dict

Decoded JSON response of the final show_window call.

Source code in src/quiltwright/bridge.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def stop_quilt(*, bridge_url: str = BRIDGE_URL, timeout: float = 10.0) -> dict:
    """Stop playback: pause the current frame and hide the display window.

    Bridge's own `bridge.js <https://github.com/Looking-Glass/bridge.js>`_
    SDK documents ``delete_playlist`` as *the* way to stop a playlist, and
    an earlier version of this function called it. In testing it reliably
    left Bridge unresponsive to every further HTTP call -- reproduced twice,
    once mid-video and once on a single still image, so it isn't a
    large-file decode race. This function deliberately avoids
    ``delete_playlist`` and reaches the same end state (nothing visible,
    playback halted) through calls already proven safe: the playlist from
    :func:`cast_quilt` is left instantiated but paused and hidden, rather
    than deleted. The next :func:`cast_quilt` plays a playlist of its own
    rather than reusing this one, so what is left behind does not block it.

    :param bridge_url: Base URL of the Bridge HTTP API.
    :param timeout: HTTP timeout in seconds.
    :return: Decoded JSON response of the final ``show_window`` call.
    """
    token = enter_orchestration(bridge_url, timeout)
    bridge_post(bridge_url, "transport_control_pause", {"orchestration": token}, timeout)
    return bridge_post(
        bridge_url,
        "show_window",
        {"orchestration": token, "show_window": False, "head_index": -1},
        timeout,
    )