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— aBiomechanicalDatainstance holding all 29 joint angles (in degrees, PascalCase fields likeKneeFlexExtR,HipFlexExtL).self.contacts— aStepDetectorDatainstance 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 default —
self.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_contact — True 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()#
| Attribute | Type | Sample fields |
|---|---|---|
self.joints | BiomechanicalData | KneeFlexExtL/R, HipFlexExtL/R, AnkleFlexExtL/R, ShoulderFlexExtL/R, ElbowFlexExtL/R, … (29 total) |
self.contacts | StepDetectorData | left_foot_contact, right_foot_contact |
self.params | ControlMessage | (your subclass — Step 5) |
self.external_data | dict | (LSL inlets — Step 6) |
self.muscles | MuscleMap | (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
selfbefore everyprocess()call. - Biomechanical-angle collection is on by default —
self.jointsis 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.
