Skip to content

Vhi.VhiControlService

VHI_godot

Vhi

VhiControlService Class

The one gRPC service VHI serves: a manifest of every control it exports, per-frame commands for the predicted and control hands, plus the recording-session coordination a capture pipeline drives them through.

public class VhiControlService : Myogestic.Remote.RemoteControl.RemoteControlBase

Inheritance System.Object → RemoteControlBase → VhiControlService

Remarks

A client calls GetControlManifest(GetControlManifestRequest, ServerCallContext) once, unconditionally, and gets back every address VHI exports ("vhi.prediction.index") with what it can drive — no per-client negotiation, no declared subset. Vhi.VhiControlService.Renderable is the only table in VHI that knows both the manifest's addresses and the rig they resolve to, and it exists so that nothing outside this file has to.

Which hand renders what. Continuous DOFs drive the predicted hand; discrete DOFs drive the control hand's movements. The two never contend, so an application can hold a continuous grip and a discrete grasp state at once. A discrete DOF is refused by name when a control-pose stream is driving the control hand instead — see SetControl(SetControlRequest, ServerCallContext).

Recording-session coordination.SetRecordingSession(SetRecordingSessionRequest, ServerCallContext), StartRecordingTrajectory(StartRecordingTrajectoryRequest, ServerCallContext), StopRecordingTrajectory(StopRecordingTrajectoryRequest, ServerCallContext) and GetRecordingSessionState(GetRecordingSessionStateRequest, ServerCallContext) are a second, unrelated concern that lives here because both drive the same control hand through the same state machine — splitting them into a second service implied an independence the target does not have. Nothing here is a standard DOF and none of it may change what one means: a standard discrete DOF is a held state, while a recording trajectory keeps the control hand cycling so the recorded pose stream sweeps a continuous range for EMG windows to be aligned against. A trajectory names a VHI movement, which is fine precisely because this is not standard — a recording aid is allowed to be application-specific.

Threading: every RPC body is a closure handed to InvokeOnMainThread<T>(Func<T>), so scene mutation happens on Godot's main thread. SweepControl(SweepControlRequest, ServerCallContext) is the one exception and the one async method in src/ — it has to span frames, because a pose set inside a single main-thread closure is never rendered before the closure returns.

Constructors

VhiControlService(ControlHandSkeleton, PredictedHandSkeleton, GrpcControlServer) Constructor

public VhiControlService(Vhi.ControlHandSkeleton controlHand, Vhi.PredictedHandSkeleton predictedHand, Vhi.GrpcControlServer server);

Methods

VhiControlService.GetControlManifest(GetControlManifestRequest, ServerCallContext) Method

Report every control this build exports, and how to send each one.

public override System.Threading.Tasks.Task<Myogestic.Remote.ControlManifest> GetControlManifest(Myogestic.Remote.GetControlManifestRequest request, Grpc.Core.ServerCallContext context);

Returns

System.Threading.Tasks.Task<ControlManifest>

VhiControlService.GetRecordingSessionState(GetRecordingSessionStateRequest, ServerCallContext) Method

Report the recording session's state and the movements a trajectory may name.

public override System.Threading.Tasks.Task<Myogestic.Remote.RecordingSessionState> GetRecordingSessionState(Myogestic.Remote.GetRecordingSessionStateRequest request, Grpc.Core.ServerCallContext context);

Returns

System.Threading.Tasks.Task<RecordingSessionState>

VhiControlService.PoseStreams() Method

Every pose stream this build subscribes to: the stream's name, which hand it drives, and the pose channel its one value lands on.

public static System.Collections.Generic.IEnumerable<(string Name,bool Control,int Channel)> PoseStreams();

Returns

System.Collections.Generic.IEnumerable<<System.String,System.Boolean,System.Int32>>

Remarks

The name \<i>is\</i> the address — one stream per DOF, one channel wide — which is why the two tables above are the only place either fact is written down. LSLCommunicationController reads this to know what to resolve, so a control added to those tables gets an inlet and a manifest entry together and cannot get one without the other.

VhiControlService.SetControl(SetControlRequest, ServerCallContext) Method

Apply one standard frame. Both maps are keyed by address.

public override System.Threading.Tasks.Task<Myogestic.Remote.ControlAck> SetControl(Myogestic.Remote.SetControlRequest request, Grpc.Core.ServerCallContext context);

Returns

System.Threading.Tasks.Task<ControlAck>

VhiControlService.SetPresentation(SetPresentationRequest, ServerCallContext) Method

Configure how the target blends between commanded values.

public override System.Threading.Tasks.Task<Myogestic.Remote.ControlAck> SetPresentation(Myogestic.Remote.SetPresentationRequest request, Grpc.Core.ServerCallContext context);

Returns

System.Threading.Tasks.Task<ControlAck>

Remarks

Appearance only. This is the third of three separate layers and the one most easily misused: 1. Continuous smoothing, on the MyoGestic side inside its ControlBus, before any target sees a frame — that layer decides what value is actually commanded. 2. Discrete debounce and hysteresis, also MyoGestic-side and declared on the DOF, which gates a noisy classifier before its state becomes a transition. A discrete control is never numerically filtered like an axis: that would interpolate through states nobody selected. 3. This — purely visual interpolation, so the hand does not snap jarringly between poses.

It changes only how a commanded value looks on the way to being reached. It cannot make an unstable prediction stable, and a build with blending on but no debounce still jumps between states — just smoothly.

VhiControlService.SetRecordingSession(SetRecordingSessionRequest, ServerCallContext) Method

Mark a recording session active or finished.

public override System.Threading.Tasks.Task<Myogestic.Remote.RecordingAck> SetRecordingSession(Myogestic.Remote.SetRecordingSessionRequest request, Grpc.Core.ServerCallContext context);

Returns

System.Threading.Tasks.Task<RecordingAck>

VhiControlService.StartRecordingTrajectory(StartRecordingTrajectoryRequest, ServerCallContext) Method

Start cycling the control hand to generate a recording trajectory.

public override System.Threading.Tasks.Task<Myogestic.Remote.RecordingAck> StartRecordingTrajectory(Myogestic.Remote.StartRecordingTrajectoryRequest request, Grpc.Core.ServerCallContext context);

Returns

System.Threading.Tasks.Task<RecordingAck>

VhiControlService.StopRecordingTrajectory(StopRecordingTrajectoryRequest, ServerCallContext) Method

Stop the trajectory. Rests the hand only if one was running.

public override System.Threading.Tasks.Task<Myogestic.Remote.RecordingAck> StopRecordingTrajectory(Myogestic.Remote.StopRecordingTrajectoryRequest request, Grpc.Core.ServerCallContext context);

Returns

System.Threading.Tasks.Task<RecordingAck>

VhiControlService.SweepControl(SweepControlRequest, ServerCallContext) Method

Drive one DOF across its range and report which bone moved, and how far, in signed degrees read back off the skeleton.

public override System.Threading.Tasks.Task<Myogestic.Remote.SweepControlReply> SweepControl(Myogestic.Remote.SweepControlRequest request, Grpc.Core.ServerCallContext context);

Returns

System.Threading.Tasks.Task<SweepControlReply>

Remarks

async on purpose, and the only such method in src/. A pose set inside a single InvokeOnMainThread<T>(Func<T>) closure is never rendered before that closure returns, so a sweep that looped inside one would block the main thread and read every value back from the same frozen frame. Each excursion therefore gets its own main-thread hop, with the wait happening on the gRPC thread in between.

What this does and does not prove: the read-back comes from Skeleton3D.GetBonePoseRotation, so it round-trips through Godot's quaternion and will catch a wrong axis, a gimbal problem, or a gain of the wrong sign. It does not independently verify the rig — which is why Element reports the model's own bone name rather than a hand-written label. If the FBX is ever re-rigged or its bones renamed, that name changes and the caller can see it.

A client must not be streaming poses over LSL during a sweep: the stream and the sweep would fight for the same channels, and the sweep would report whichever arrived last.