TeslasuitDocumentation
Frameworks

Data Types

Concept-level tour of the typed dataclasses that flow through the framework. For field-by-field reference, see Data Types Reference.

Source: fes_framework/data/types.py


What they are#

Every piece of data the framework reads from the SDK or sends to it is wrapped in a Python dataclass. There are no raw ctypes structs in the user-facing API, no unparsed numpy arrays, no untyped dicts.

The dataclasses fall into three families:

FamilyExamplesLifetime
Sensor data (input)BiomechanicalData, ProcessedData, StepDetectorData, RawData, HeartRateData, HRVData, RawPPGDataUpdated in place every cycle by DataStreamer
Stimulation (output)EmsData, EMSParamData, EMSCalibrationData, CustomPlayableWritten by the strategy, read by the stimulator
Messaging (IPC)ControlMessage, UtilityMessage, HapticLibrarySent over multiprocessing.Queue between backend and GUI

Why they exist#

Three reasons:

  1. Discoverability. A dataclass instance is self-describing — dir(joints) lists every joint angle field, IDE autocomplete works, @dataclass plays well with type checkers. New users don't have to read SDK headers to know what data is available.
  2. Single ownership. Each container is allocated once at engine startup and updated in place. There is no per-cycle allocation churn at 100 Hz, and no question about who owns the memory.
  3. Stable surface. Adding a new field is additive — old strategies that never reference it keep working. Field renames or removals are intentional, visible, and mechanically refactorable.

When you touch them#

TaskType
Read joint angleBiomechanicalData (via self.joints)
Read foot contactStepDetectorData (via self.contacts)
Write stim commandEmsData (via self.ems_output) and EMSParamData
Define your paramsSubclass ControlMessage
Send a system flagModify UtilityMessage
Define a haptic librarySubclass HapticLibrary, populate slots

Concept summaries#

BiomechanicalData#

29 joint angles in degrees, named in PascalCase with L/R suffix: HipFlexExtL, KneeFlexExtR, AnkleFlexExtL, ShoulderAddAbdR, ElbowFlexExtL, etc. Plus pelvis: PelvisTilt, PelvisList, PelvisRotation. Plus a timestamp field.

Read via self.joints in your strategy. Read-only — modifying it won't change what the SDK reports next cycle.

Collection is gated by a flag (off by default). Enable via engine.data_streamer.set_biomech_collection(True) or UtilityMessage.BiomechanicalDataCollectionIsActive=True.

StepDetectorData#

Two booleans: left_foot_contact, right_foot_contact. Plus timestamp. Read via self.contacts.

Source: Teslasuit SDK's built-in step detector (foot-IMU acceleration threshold). To override the source, subclass DataStreamer and write your own values in process().

EmsData / EMSParamData#

EmsData is a flat container holding 20 EMSParamData instances — one per muscle, named after the MuscleMap config: quadriceps_left, quadriceps_right, biceps_left, …

EMSParamData has four fields:

  • IsMuted (bool) — True to silence this muscle this cycle
  • Amplitude (int, 0–100) — stimulation intensity, percent
  • PulseWidth (int, 10–140 μs) — pulse duration
  • Period (float, ms) — time between pulses

Write via self.ems_output.<muscle> = EMSParamData(...) in your strategy. The stimulator reads it after process() returns and the FES kill switch has been applied.

ProcessedData#

20 bones, each with a 3D position and a quaternion rotation. Bone names: Hips, LeftUpperLeg, RightUpperLeg, LeftLowerLeg, RightLowerLeg, LeftFoot, RightFoot, Spine, Chest, UpperChest, Neck, Head, LeftShoulder, RightShoulder, LeftUpperArm, RightUpperArm, LeftLowerArm, RightLowerArm, LeftHand, RightHand. Plus timestamp.

Available in your strategy via self.suit.streamer access (rare) or the LSL TS_BonePosition outlet. Most applications don't read this directly — it's there for visualisation and lab integration.

RawData#

20 sensor segments, each with: a bone ID, a 6DOF orientation quaternion, accelerometer, gyroscope, and linear acceleration (gravity-removed). Plus timestamp.

Same naming as ProcessedData. Used for ML step detection, custom filters, and CalibrationAPI's quality checks.

HeartRateData, HRVData, RawPPGData#

Only populated when DataStreamer.ppg_available is True.

  • HeartRateData — current heart rate, validity flag, timestamp.
  • HRVData — heart-rate variability metrics: mean_rr, sdnn, sdsd, rmssd, sd1, sd2, hlf.
  • RawPPGData — last 2 raw photodiode samples for IR, red, blue, green channels (PPG runs at 200 Hz, engine at 100 Hz).

ControlMessage (subclass to add fields)#

The base class is empty. Your application defines what runtime parameters look like by subclassing it:

@dataclass
class MyControlMessage(ControlMessage):
    threshold_deg: float = 15.0
    quad_amplitude: int = 40

Sent via the GUI's control queue; received as self.params in your strategy.

UtilityMessage#

Framework-defined system flags. Fields:

  • FesIsActive (bool) — global FES kill switch.
  • RecordingIsActive (bool) — recording state.
  • CalibrationLoopIsActive (bool) — calibration in progress.
  • TSAPIStepDetectionIsActive, VUStepDetectionIsActive, ModelBasedStepDetectionIsActive (bool) — step-detector mode flags.
  • BiomechanicalDataCollectionIsActive (bool) — gate for DataStreamer's biomech-angle collection.
  • FolderPath (str) — recording folder.

Bidirectional: GUI sends them to the backend, backend can send them back. The strategy doesn't usually subscribe — the framework reacts to FesIsActive and BiomechanicalDataCollectionIsActive directly.

HapticLibrary (subclass to add slots)#

Empty base class — applications subclass it to declare named CustomPlayable slots:

@dataclass
class NavCues(HapticLibrary):
    cue_left: CustomPlayable = field(default_factory=CustomPlayable)
    cue_right: CustomPlayable = field(default_factory=CustomPlayable)

Populate slots in strategy.setup() via self.suit.create_haptic_touch() or self.suit.load_haptic_asset(). Toggle slot.IsMuted in process() to fire/silence. See Messaging — HapticLibrary.

CustomPlayable#

Slot type used by HapticLibrary. Fields: playable_id (int, set by the SDK at creation time), IsMuted (bool), is_looped (bool).


Update semantics#

All sensor data containers are updated in place every cycle. That has two practical consequences:

  • You see live data. The same self.joints instance you read this cycle is the one that will hold next cycle's values. There's no copying.
  • Don't store references for later. prev = self.joints followed by print(prev.HipFlexExtR) next cycle reads the new value, not last cycle's. If you want history, copy the field values (prev_hip = self.joints.HipFlexExtR).

For EmsData, the strategy writes new EMSParamData instances per muscle each cycle (self.ems_output.quadriceps_right = EMSParamData(...)). The framework copies them into a backend-owned ems_data after applying the FES kill switch, so concurrent stimulator reads see a stable snapshot.


See also#