LslIn

Receive an LSL stream: [channels, samples] per tick, with its labels and rate.

LslIn py
out
Type name
signal:LslIn
Plane
signal
Tags
input
Language
python
Tier
in-process
Bundle
signal
Source
node-bundles/signal/lsl_in.py
Availability
available

Slots

SlotDirectionType
outoutputARRAY

Parameters

lsl

NameTypeDefaultRangeDoc
namestring""The stream's name; empty takes any stream of `type`. refreshable
typestringEEGThe stream's type; empty takes any type.

common

NameTypeDefaultRangeDoc
autotriggerbooltrueRun on the node's own schedule, instead of waiting for an input frame. Turn this on for sources; leave it off for transforms driven by their input.
max_frequencyfloat00 … 100Rate cap for this node, read through `frequency_mode`. 0 means uncapped — the node runs as often as the scheduler and its inputs allow.
frequency_modestringupdates_per_secondupdates_per_second | seconds_per_updateHow to read `max_frequency`: as a rate in Hz (updates per second), or as a period in seconds between updates — convenient for very slow nodes.

Source

The current source of this node, as it stands in the goofi repository at node-bundles/signal/lsl_in.py.

node-bundles/signal/lsl_in.py
"""LslIn — receives a Lab Streaming Layer stream, as `[channels, samples]` per tick.

The stream is resolved in the background and connected the moment it appears, so the node may
be added before the device is on: it stays silent until then, and it reconnects on its own if
the stream drops. Channel labels and the sample rate come from the stream's own description.
"""

import numpy as np
import pylsl
import goofi


class LslIn(goofi.Node):
    """Receive an LSL stream: [channels, samples] per tick, with its labels and rate."""

    TAGS = ["input"]
    OUTPUTS = {"out": goofi.DataType.ARRAY}
    PRODUCER = True
    PARAMS = {
        "lsl": {
            "name": goofi.StringParam(
                "", options=[""], refresh=True, doc="The stream's name; empty takes any stream of `type`."
            ),
            "type": goofi.StringParam("EEG", doc="The stream's type; empty takes any type."),
        }
    }

    def setup(self):
        self.wanted = None
        self.resolver = None
        self.inlet = None

    def process(self):
        p = self.params.lsl
        if (p.name, p.type) != self.wanted:
            self.wanted = (p.name, p.type)
            self.inlet = None
            pred = " and ".join(f"{k}='{v}'" for k, v in (("name", p.name), ("type", p.type)) if v)
            self.resolver = pylsl.ContinuousResolver(pred=pred or None)
        if self.inlet is None:
            found = self.resolver.results()
            if not found:
                return None
            self.inlet = pylsl.StreamInlet(found[0], recover=True)
            info = self.inlet.info()
            self.sfreq = info.nominal_srate() or None
            self.channels = labels(info)
        samples, _ = self.inlet.pull_chunk(timeout=0.0, max_samples=32768)
        if not samples:
            return None
        return np.asarray(samples, dtype=np.float32).T, {"sfreq": self.sfreq, "channels": {"dim0": self.channels}}

    def refresh_lsl_name(self):
        return [""] + sorted({s.name() for s in pylsl.resolve_streams(wait_time=1.0)})


def labels(info):
    ch = info.desc().child("channels").child("channel")
    out = []
    for k in range(info.channel_count()):
        out.append(ch.child_value("label") or f"{info.type()} {k + 1}")
        ch = ch.next_sibling()
    return out

← All nodes

This reference describes goofi 3.1.0(537cd394), generated from a running instance on 2026-09-06.