Skip to content

Bridges

A Bridge is a subprocess MyoGestic spawns alongside the app for heavy-data acquisition that doesn't fit the LSL pull model - typically a webcam decoder that writes frames straight to Zarr and publishes an LSL clock outlet so the rest of the app can align timestamps.

Bridges are registered via app.bridges(...). Registering does not spawn anything — you call start() when you want the subprocess up — and App.run() calls stop() on every registered bridge during cleanup.

Base class

Bridge

Bridge(name: str, command: list[str])

A subprocess MyoGestic spawns alongside the app and tears down on exit.

The escape hatch for heavy-data acquisition that doesn't fit the LSL pull model - a webcam decoder that writes frames straight to Zarr, an ultrasound capture daemon, a custom script that owns its own buffer. The bridge subprocess runs whatever it wants; MyoGestic only cares that it stays alive and exits cleanly.

There is no IPC contract beyond "the subprocess exists, is alive, and stops on terminate". Data flows back into the app the way it already travels: the subprocess publishes an LSL outlet, or writes a Zarr file the app reads.

Registered via app.bridges(...). Registering does not spawn anything - you call start when you want it up - and App.run() calls stop on every registered bridge on cleanup.

Parameters:

Name Type Description Default
name str

Human label, used in log messages about this bridge.

required
command list[str]

argv passed to subprocess.Popen. Stdout and stderr go to DEVNULL: a child that outgrows the pipe buffer blocks in write() forever while alive still reports True. To watch its output, run command in a terminal.

required

Examples:

>>> import sys
>>> from myogestic.bridges import Bridge
>>> bridge = Bridge("capture", [sys.executable, "capture.py"])
>>> bridge.start()
>>> bridge.stop()

status property

status: str

"running" or "stopped", read from the subprocess on every read.

Derived rather than assigned at each transition: a stored copy goes stale the moment the subprocess exits on its own.

Two values only. A signalled child carries a non-zero return code, so any "code != 0 means crashed" rule would report every successful stop as a failure. Whether an exit was clean is on process.returncode, which stop leaves in place.

alive property

alive: bool

True while the subprocess is running.

start

start() -> None

Spawn the subprocess, unless one is already alive.

Idempotent: overwriting a live handle drops the only reference to a running process, and the next call then stacks a second one on top of it.

stop

stop() -> None

Terminate the subprocess - SIGTERM, then SIGKILL if it ignores that.

Returns while still holding the handle if neither signal lands: a process parked in an uninterruptible kernel wait ignores SIGKILL too. alive and status then keep saying it is running and start refuses. Call this again to retry.

SIGTERM first, so a bridge script's own handler and cleanup get to run.

Built-in bridges

WebCamBridge

WebCamBridge(name: str, device: int = 0, zarr_path: str = 'session/cam.zarr')

Bases: Bridge

Bridge that runs the built-in webcam decoder subprocess.

Wraps python -m myogestic.bridges.webcam (the main below): captures frames from an OpenCV device, writes them to a Zarr array, and publishes the per-frame LSL clock so the rest of the app can align webcam time with EMG time.

Parameters:

Name Type Description Default
name str

Bridge label. The published LSL clock outlet is named "{name}_clock" (e.g. WebCamBridge("cam") publishes "cam_clock").

required
device int

OpenCV device index. 0 is the system default camera; secondary cameras get 1, 2, ... in the order the OS enumerates them.

0
zarr_path str

Where to write the frame array. Created if missing.

'session/cam.zarr'

Examples:

>>> from myogestic.bridges import WebCamBridge
>>> camera = WebCamBridge("cam", device=0, zarr_path="session/cam.zarr")
>>> camera.start()
>>> camera.stop()

CustomBridge

CustomBridge(name: str, script: str)

Bases: Bridge

Bridge that runs an arbitrary user Python script as a subprocess.

The script runs with the same Python interpreter as the app (sys.executable); the rest is up to you (publish LSL, write Zarr, talk to a custom message bus, ...). For the structured alternative see WebCamBridge.

Parameters:

Name Type Description Default
name str

Bridge label.

required
script str

Path to the Python script to spawn (e.g. "capture/ultrasound.py").

required

Examples:

>>> from myogestic.bridges import CustomBridge
>>> bridge = CustomBridge("ultrasound", "capture/ultrasound.py")
>>> bridge.start()
>>> bridge.stop()

The webcam runner

WebCamBridge invokes python -m myogestic.bridges.webcam as a subprocess. The same runner can be launched directly for testing:

uv run python -m myogestic.bridges.webcam --device 0 --zarr session/cam.zarr --lsl-name webcam_clock

Flags:

  • --device N - OpenCV device index (default 0).
  • --zarr PATH - where to write the Zarr array. Frames are appended one chunk per capture.
  • --lsl-name NAME - LSL outlet name for the per-frame timestamp clock; the app subscribes to this to align webcam frames with EMG.