TeslasuitDocumentation
Frameworks

Step 2 — Reading sensor data

Goal: read joint angles and foot contacts in process() and react to them. You'll use: BiomechanicalData, StepDetectorData Builds on: Step 1 Anchored example: examples/atomic/reading_sensor_data.py


What you're adding#

You'll start using the sensor data the framework already populates on self. Two things become available in your process():

  • self.joints — a BiomechanicalData instance holding all 29 joint angles (in degrees, PascalCase fields like KneeFlexExtR, HipFlexExtL).
  • self.contacts — a StepDetectorData instance with two booleans: left_foot_contact, right_foot_contact.

That's all you need to start writing reactive logic. There are no new imports — these slots have always been there; we just didn't read them in Step 1.

Note: biomechanical-angle collection runs by defaultself.joints is refreshed every cycle without any extra wiring. The underlying SDK call (inverse-kinematics solver) is expensive, so if your app never reads joint angles you can opt out via data_streamer.set_biomech_collection(False) to claw back per-cycle CPU. See DataStreamer concept page.


Code#

# app/strategy.py
from fes_framework.control.strategy_base import ControlStrategyBase


class MyStrategy(ControlStrategyBase):

    def __init__(self) -> None:
        super().__init__()
        self._cycles = 0

    def process(self) -> None:
        self._cycles += 1
        if self._cycles % 100 != 0:        # print roughly once per second
            return

        print(
            f"cycle={self._cycles} "
            f"KneeFlexExtR={self.joints.KneeFlexExtR:+6.1f}° "
            f"HipFlexExtR={self.joints.HipFlexExtR:+6.1f}° "
            f"contact_R={self.contacts.right_foot_contact} "
            f"contact_L={self.contacts.left_foot_contact}"
        )
# app/main.py
from fes_framework.orchestrator import launch
from app.strategy import MyStrategy

if __name__ == "__main__":
    launch(MyStrategy)

That's it — no engine subclass needed. Biomechanical-angle collection is on by default in DataStreamer, so self.joints.KneeFlexExtR etc. are refreshed every cycle automatically.

If you want the opposite — an app that explicitly disables biomech collection for performance — subclass ClosedLoopEngine and flip the flag once in __init__ (or on_start()):

# Only needed if your app DOES NOT read self.joints.* and you want
# to claw back the SDK IK call's per-cycle CPU cost.
class HapticOnlyEngine(ClosedLoopEngine):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.data_streamer.set_biomech_collection(False)

Walkthrough#

self.joints.KneeFlexExtR — the right knee's flexion/extension angle in degrees. Positive = flexed (bent), negative = extended (hyperextended). All 29 angle fields use this convention; see the Data types reference for the full list.

self.contacts.right_foot_contactTrue when the right foot is in stance phase (on the ground), False in swing. Source: the SDK's built-in step detector.

Biomechanical-angle collection runs by default. DataStreamer calls get_biomechanical_angles_on_ready() (the SDK IK solver) every cycle and writes the result into self.biomechanical_data, which the framework exposes to the strategy as self.joints. You only need to think about the flag if you want to disable it for performance — call data_streamer.set_biomech_collection(False) (the SDK IK call is the single most expensive one in the per-cycle pipeline; skipping it is a meaningful optimisation for apps that never read joint angles).

if self._cycles % 100 != 0: return — printing 100× per second swamps the console and slows the loop. Print at most once per second.


What you can read in process()#

AttributeTypeSample fields
self.jointsBiomechanicalDataKneeFlexExtL/R, HipFlexExtL/R, AnkleFlexExtL/R, ShoulderFlexExtL/R, ElbowFlexExtL/R, … (29 total)
self.contactsStepDetectorDataleft_foot_contact, right_foot_contact
self.paramsControlMessage(your subclass — Step 5)
self.external_datadict(LSL inlets — Step 6)
self.musclesMuscleMap(Step 3)

Everything is read-only. Mutating self.joints.KneeFlexExtR = ... won't change what the SDK reports next cycle. (Custom signal processing is DataStreamer.process() — out of scope for this step.)


Verify#

Run the app while wearing the suit and walking around. Expected output:

cycle=100  KneeFlexExtR=  +5.2° HipFlexExtR= +12.3° contact_R=True  contact_L=False
cycle=200  KneeFlexExtR= +18.6° HipFlexExtR= +24.1° contact_R=False contact_L=True
cycle=300  KneeFlexExtR=  +1.4° HipFlexExtR=  +8.9° contact_R=True  contact_L=False

Joint angles should respond to motion (knee flexes when you bend the knee, hip flexes when you raise the leg). Foot contacts should flip as you step.

If joint angles are static at 0:

  • Confirm nothing in your app called self.data_streamer.set_biomech_collection(False) (default is on, so the typical failure mode is "someone disabled it for perf and forgot").
  • Confirm calibration has been run (next step). Without calibration, joint angles can be unreliable. (For Step 2, just verify they change with motion.)

What you've learned#

  • The framework populates sensor data on self before every process() call.
  • Biomechanical-angle collection is on by default — self.joints is ready out of the box. Disable it (set_biomech_collection(False)) only if your app never reads joint angles and you want to skip the expensive SDK IK call.
  • Print sparingly. The loop runs at 100 Hz.

Next#

Step 3 — Stimulating muscles by name. You'll start writing to self.ems_output so the suit actually does something.