LSL streams¶
VHI uses Lab Streaming Layer for all continuous time-series - hand poses in and out. Discrete commands go over gRPC instead.
All LSL I/O lives in LSLCommunicationController. Streams are resolved by
name; an inlet that isn't found yet is retried every few seconds, so VHI and
its peer can start in any order.
Inlets - what VHI consumes¶
One stream per DOF. Every control VHI can be driven with is its own LSL stream, named for its own address and one channel wide:
| Stream family | Drives | Shape | Rate |
|---|---|---|---|
vhi.prediction.* - nine streams |
the predicted hand | 1 × float32 each |
the producer's pace (~32 Hz from MyoGestic) |
vhi.control.pose.* - nine streams |
the control hand (while any is delivering) | 1 × float32 each |
the producer's pace |
The two families are independent: vhi.prediction.* only ever drives the
predicted hand, vhi.control.pose.* only ever drives the control hand. Every
individual stream is optional too - producing the control-pose family at all is
unusual, and most setups let the control hand play its named movements instead.
The stream name is the address GetControlManifest publishes, so a client
reads the name it must publish under rather than agreeing on a layout — and the
manifest carries nothing else about the wire, because there is nothing else to
carry. See the LSL reference
for all eighteen names.
One channel is not a convention here, it is enforced: a stream that resolves under one of those names and turns out to be any other width is logged and never opened. Tolerating a wider one would mean reading element zero of somebody's whole-pose frame, which is the thumb — and rendering the thumb on every DOF is exactly the sort of wrong that never reports itself.
The inlet rate is whatever the producer pushes, per stream. MyoGestic's
default prediction loop runs at ~32 Hz, but VHI itself doesn't impose or assume
a rate - it applies whatever arrives, optionally blends toward it (see
SetPresentation), and renders at its own physics tick.
Nothing waits for a whole pose¶
A sample is applied to the hand the moment it arrives, and a DOF nobody is driving holds what it was last commanded to. There is no frame to fill in and nothing that blocks on one.
That is the reason for the shape rather than a concession to missing data. The DOFs are independently actuated, may come from different producers, and may update at different rates, so a hand whose index has moved and whose thumb has not is a real pose, not a corrupt one. Two producers can each own some DOFs of the same hand without contending — a model driving the fingers and a data glove driving the wrist, say — which is the capability this shape exists for.
The corollary: pushing 0 is how you put a DOF back at rest. Going silent is a
different statement, and on the control hand the last stream going silent
releases the whole hand back to its own movements.
Outlets - what VHI publishes¶
| Stream name | Carries | Rate |
|---|---|---|
VHI_Control |
the control hand's current pose | 60 Hz |
VHI_Predict |
the predicted hand's current pose | 60 Hz |
Each outlet carries its hand's whole nine-channel pose, and that has not changed with the per-DOF inlets: a read-back is a recording, and a recording wants one row per instant rather than nine independently timed ones.
These let the rest of the experiment record what VHI is actually showing - for
example, MyoGestic consumes VHI_Control as a regression target (the
control-hand kinematics the model should learn to reproduce). Both outlets are
always published.
Input rate ≠ display rate
The inlets carry the commanded timeline, one per DOF, at whatever rate
each producer pushes (~32 Hz for a MyoGestic model). The outlets carry the
displayed-pose timeline (whatever VHI is rendering, 60 Hz). They are not the same signal: the
outlets are an interpolated, smoothed, mode-aware view of what reached
the screen. For frame-accurate experiment recording, treat VHI_Predict
/ VHI_Control as the authoritative record of what the participant saw,
and the inlets as the cause. Record both - LSL's XDF format is
designed for exactly this heterogeneous-rate, multi-stream case
(pyxdf.load_xdf lines them up on a common clock).
gRPC commands don't appear on LSL
Discrete commands go over gRPC, not LSL, so they have
no LSL timestamps and don't show up in an XDF recording. The effect
of a command (a movement starts, a freeze is engaged) becomes visible
on VHI_Control at the next physics tick - but rejected commands
(applied=false) and exact a discrete DOF-issued instants are not in
the LSL record. If experiment integrity requires that timeline, log
enqueue and ack timestamps client-side (see the
gRPC API reference).
Dropped streams
Earlier versions also published VHI_MovementState and VHI_MenuState.
These were removed: the commanded movement and the settings are now known
to MyoGestic directly (it issues the commands), and the actual kinematics
are already in VHI_Control.
The nine DOFs¶
A hand is nine degrees of freedom: thumb flexion and abduction, one flexion DOF per finger, then wrist flexion, abduction and rotation. All nine render. The wrist is bone 0, the common ancestor of all five digit chains, so it turns the whole hand.
Which nine they are is the only thing both directions share. Inbound each is a stream of
its own; outbound all nine are channels of one pose. Values are standard — [-1, 1],
+1 the direction the DOF's name denotes — and VHI expands them across the 16 animated
joints internally by per-joint gains (see Architecture).
One authoritative map, and it is not on this page
The exact DOF-to-bone mapping, the outlet channel numbers, and — critically — which sign convention applies live in the LSL reference. This page deliberately does not restate them.
That is not tidiness. This map was previously written out in several places and they
disagreed: one described channel 0 as thumb rotation, another had channel 1 as 0
in a fist where recordings show -1.0. Duplicating it is how it drifts, so there is
now one copy.
Everything is standard as of 2.0: +1 is the direction the DOF's name denotes, on the
inbound streams and both outlets alike. VHI_Control was the exception and it was a
bug — it published VHI's own rig units, opposite on five channels, so a fist read
-1 on the stream you train from and +1 on the one you drive. Recordings from before
the fix are converted by myogestic.tools.migrate_vhi_sessions; the outlets advertise
pose_convention so a reader never has to infer which it holds.
See the LSL reference for stream types, source IDs and exact metadata.