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:
| Family | Examples | Lifetime |
|---|---|---|
| Sensor data (input) | BiomechanicalData, ProcessedData, StepDetectorData, RawData, HeartRateData, HRVData, RawPPGData | Updated in place every cycle by DataStreamer |
| Stimulation (output) | EmsData, EMSParamData, EMSCalibrationData, CustomPlayable | Written by the strategy, read by the stimulator |
| Messaging (IPC) | ControlMessage, UtilityMessage, HapticLibrary | Sent over multiprocessing.Queue between backend and GUI |
Why they exist#
Three reasons:
- Discoverability. A dataclass instance is self-describing —
dir(joints)lists every joint angle field, IDE autocomplete works,@dataclassplays well with type checkers. New users don't have to read SDK headers to know what data is available. - 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.
- 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#
| Task | Type |
|---|---|
| Read joint angle | BiomechanicalData (via self.joints) |
| Read foot contact | StepDetectorData (via self.contacts) |
| Write stim command | EmsData (via self.ems_output) and EMSParamData |
| Define your params | Subclass ControlMessage |
| Send a system flag | Modify UtilityMessage |
| Define a haptic library | Subclass 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) —Trueto silence this muscle this cycleAmplitude(int, 0–100) — stimulation intensity, percentPulseWidth(int, 10–140 μs) — pulse durationPeriod(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 = 40Sent 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 forDataStreamer'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.jointsinstance 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.jointsfollowed byprint(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#
- Data Types Reference — every dataclass field-by-field
- DataStreamer — the producer of sensor data
- ControlStrategy — the consumer of sensor data + producer of EMS
- Stimulator — the consumer of EMS
- Messaging —
ControlMessage,UtilityMessage,HapticLibrary - Source:
fes_framework/data/types.py
