PhysioStream governs live physiological data streams: Lab Streaming Layer inlets and outlets, XDF interchange, MQTT/TTL stimulation triggers, and a live biofeedback scope. Those transports need a device or a running network and are not exercised here.
Everything else runs in process, with no device.
This vignette walks the device-free path end to end on a tiny synthetic
signal: stream metadata, the deterministic loopback source, a snapshot
to a PhysioExperiment, a synchronous processing pipeline,
online DSP, clock correction, and a governed stimulation trigger. Every
code block below is executed when the vignette is built.
library(PhysioStream)
#> Loading required package: PhysioExperimentStream metadata
A StreamInfo is validated, immutable, and serializable.
It is the single source of truth for channel identity, rate, and storage
type.
info <- streamInfo(
"demo-eeg",
type = "EEG",
channel_names = c("C3", "C4"),
nominal_srate = 100,
channel_units = c("uV", "uV")
)
info
#> StreamInfo<1.0.0>: demo-eeg
#> type: EEG; channels: 2; rate: 100 Hz; dtype: float64
streamChannels(info)
#> [1] "C3" "C4"A deterministic loopback stream
loopbackSource() is an in-process
StreamSource backed by a bounded native ring buffer. It
exercises the same lifecycle (created ->
open -> closed) and the same
sample/timestamp governance as a live transport, but you feed it
yourself with loopbackFeed().
source <- streamOpen(loopbackSource(info, capacity = 256L))
streamState(source)
#> [1] "open"
n <- 50
t <- seq_len(n) / 100
samples <- cbind(
C3 = sin(2 * pi * 10 * t),
C4 = sin(2 * pi * 10 * t + 0.5)
)
loopbackFeed(source, samples, t)Snapshot to a PhysioExperiment
streamSnapshot() copies buffered samples into a
PhysioExperiment, preserving the original timestamps and
the monotonic stream sequence. From here the rest of the ecosystem
applies.
pe <- streamSnapshot(source)
dim(pe)
#> [1] 50 2
SummarizedExperiment::assayNames(pe)
#> [1] "stream"A synchronous processing pipeline
A StreamPipeline owns a bounded chunk queue and an
ordered, causal operation graph. Here we register the compiled
second-order-section + rolling-RMS operation. A pass-through SOS keeps
the example self-contained; use
PhysioPreprocess::sosDesign() for a real band
definition.
sos <- matrix(
c(1, 0, 0, 1, 0, 0), 1L, 6L,
dimnames = list(NULL, c("b0", "b1", "b2", "a0", "a1", "a2"))
)
pipeline <- streamPipeline(chunk_size = 64L)
onChunk(pipeline, bandpassRmsOp(sos, window_samples = 4L))
pipelineEnqueue(pipeline, samples, t, ingest_time_ns = 0)
result <- pipelineStep(pipeline)
head(result$results[[1]]$output$samples)
#> C3_rms C4_rms
#> [1,] 0.5877853 0.9036935
#> [2,] 0.7905694 0.9440659
#> [3,] 0.8474488 0.8667517
#> [4,] 0.7905694 0.7533510
#> [5,] 0.7339122 0.6487176
#> [6,] 0.6315638 0.6193022Online DSP
The DSP processors are stateful and chunk-invariant. An adaptive filter, for example, estimates the part of a channel that is linearly predictable from a paired reference.
Clock correction
Explicit clock models correct offset and drift and regularize jitter. They change timestamps only – samples are never interpolated or reordered.
device <- seq(0, 10, by = 0.5)
offset <- -0.4 + 0.005 * (device - median(device))
model <- clockOffset(device, offset, method = "ols")
model$segments$drift_ppm
#> [1] 5000
jittered <- c(10.001, 10.010, 10.021, 10.029)
as.numeric(dejitter(jittered, nominal_srate = 100))
#> [1] 10.00025 10.01025 10.02025 10.03025A governed stimulation trigger
The loopback trigger records commands and timing with no device, while exercising the same arm, dead-man, maximum, duplicate-ID, and refractory interlocks as the MQTT and TTL transports. Software interlocks are safeguards only; independently fail-safe stimulation hardware is still required.
trigger <- loopbackTrigger(
allowed_channels = "left",
max_intensity = 20,
intensity_unit = "mA",
max_duration_ms = 500,
refractory_ms = 0,
deadman_ms = 1000
)
triggerOpen(trigger)
armTrigger(trigger, "demo-session", now_ns = 0)
receipt <- sendStim(
trigger, intensity = 2.5, channel = "left",
duration_ms = 125, command_id = "cmd-1", now_ns = 10
)
receipt$status
#> [1] "acknowledged"
triggerClose(trigger)Where to go next
-
Live acquisition.
lslResolveStreams(),lslInlet(), andlslOutlet()bind Lab Streaming Layer streams;lslAvailable()reports whether the backend is installed. -
File interchange.
readXDF()/writeXDF()require the pyxdf backend (xdfAvailable()). -
Closed-loop control.
closedLoop()binds a detector (emgOnsetOp(),erdIntentOp(),phaseTargetOp()) to a trigger with commit-before-stimulation semantics. -
Live visualization.
biofeedbackScope()provides a bounded display-only scope (Shiny is required only to launch it).