TeslasuitDocumentation
Frameworks

Step 3 — Stimulating muscles by name

Goal: stimulate a muscle when a sensor condition is met. You'll use: MuscleMap, EMSParamData, EmsData Builds on: Step 2 Anchored example: examples/atomic/semantic_muscle_control.py


What you're adding#

This is where the suit starts to do something. You'll write EMSParamData to self.ems_output whenever a sensor condition is met. The framework's stimulator will pick it up, look up the SDK channels via MuscleMap, and deliver pulses to the muscle.

⚠️ Safety: stimulation is real and immediate. The suit must be calibrated to the wearer in Control Center before any stimulation — amplitude is a percentage of that calibrated range. Test on yourself first at low amplitude (20–30%), keep your hand on Ctrl-C throughout, and never run on someone else's body without their explicit consent and the Safety checklist understood.


Code#

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


class StanceQuadStrategy(ControlStrategyBase):
    """Stimulate right quadriceps during right-foot stance."""

    AMPLITUDE = 30      # %  — start low
    PULSE_WIDTH = 120   # μs
    PERIOD = 20.0       # ms = 50 Hz

    def process(self) -> None:
        if self.contacts.right_foot_contact:
            self.ems_output.quadriceps_right = EMSParamData(
                IsMuted=False,
                Amplitude=self.AMPLITUDE,
                PulseWidth=self.PULSE_WIDTH,
                Period=self.PERIOD,
            )
        else:
            self.ems_output.quadriceps_right = EMSParamData(IsMuted=True)
# app/main.py
from fes_framework.orchestrator import launch
from app.strategy import StanceQuadStrategy

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

(No engine subclass needed this time. Foot-contact data is collected automatically — only the biomechanical-angle collection from Step 2 is opt-in. If you need both, add a JointReadingEngine-style subclass.)


Walkthrough#

self.ems_output.quadriceps_right = EMSParamData(...) — the only output the strategy is allowed to write. The slot name comes from the MuscleMap config (muscle_map_4R.json); see Data types reference → EmsData.

EMSParamData(...) — four fields:

FieldUnitRangeMeaning
IsMutedboolTrue = no pulses delivered this cycle
Amplitude%0–100Stimulation intensity (% of device max)
PulseWidthμs10–140Pulse duration (device range: 10–140)
Periodms> 0Time between pulses; 1000 / Period = Hz

Period=20.0 → 50 Hz, the typical FES pulse rate.

Why the else branch. When the foot is in swing (no contact), you want the quadriceps to stop. Setting EMSParamData(IsMuted=True) mutes the looped playable. Without the else, the muscle would stay stimulated indefinitely from the last stance pulse — the framework only reads what process() writes.

Why no SDK calls. You never call haptic.play_touch() or look up channel IDs. The framework's Stimulator does that, using the MuscleMap lookup, after every process() returns.


Available muscles#

The default muscle_map_4R.json defines 20:

# Lower body
self.ems_output.quadriceps_left
self.ems_output.quadriceps_right
self.ems_output.hamstring_left
self.ems_output.hamstring_right
self.ems_output.gastrocnemius_left
self.ems_output.gastrocnemius_right
self.ems_output.tibialis_anterior_left
self.ems_output.tibialis_anterior_right
self.ems_output.gluteus_left
self.ems_output.gluteus_right

# Upper body
self.ems_output.deltoid_left
self.ems_output.deltoid_right
self.ems_output.biceps_left
self.ems_output.biceps_right
self.ems_output.triceps_left
self.ems_output.triceps_right
self.ems_output.wrist_flexors_left
self.ems_output.wrist_flexors_right
self.ems_output.wrist_extensors_left
self.ems_output.wrist_extensors_right

Iterate by side or region using self.muscles:

def process(self) -> None:
    for muscle in self.muscles.by_side("left"):
        setattr(self.ems_output, muscle.name, EMSParamData(
            IsMuted=False,
            Amplitude=muscle.default_amplitude_pct,
            PulseWidth=muscle.default_pulse_width_us,
            Period=muscle.default_period_ms,
        ))

The MuscleInfo.default_* fields come from the JSON config and give you sensible starting parameters per muscle.


Verify#

Wear the suit. Stand still. Pick up your right foot — stimulation should stop. Plant it again — stimulation resumes. The transition should be near-instantaneous (within ~20 ms).

If nothing happens:

  • Check Control Center says "stimulation enabled" or equivalent. Some SDK versions require an explicit enable.
  • Confirm IsMuted=False in your code (typo-easy mistake).
  • Print self.contacts.right_foot_contact to confirm the sensor is flipping.
  • See Troubleshooting → Stimulation not activating.

If stimulation feels too weak: increase AMPLITUDE by 5 each run until you feel a clear contraction. Stop increasing immediately if the user (yourself or others) reports discomfort.


Things you should not do#

  • Don't write to self.ems_output outside process(). The framework reads it once per cycle, right after process() returns.
  • Don't call time.sleep() to "ramp" amplitude. Ramp by writing small per-cycle deltas instead.
  • Don't allocate huge arrays per cycle. The 100 Hz loop is tight.
  • Don't forget the FES kill switch — your strategy doesn't need to check it (the framework enforces UtilityMessage.FesIsActive), but you should still build an emergency-stop path into your GUI later.

What you've learned#

  • The strategy writes one EMSParamData per muscle to self.ems_output per cycle.
  • The framework's Stimulator translates that to SDK haptic calls via MuscleMap. You never see channel IDs.
  • The FES kill switch is enforced by the framework after every process().

Next#

Step 4 — Calibration gate. You'll refuse to start the loop until calibration quality is acceptable.