TeslasuitDocumentation
Frameworks

MuscleMap

Anatomical name → SDK channel ID. The indirection that lets you write self.ems_output.quadriceps_left = ... instead of computing bone indices and channel slices.

Source: fes_framework/muscle_map.py Config: fes_framework/config/muscle_map_4R.json API reference: api_reference.md → MuscleMap

MuscleMap · EMSsuit_4_5_4 · 20 muscle groups
frontback
unwrapped suit · click a muscle group

Quadriceps

rightanterior

Write self.ems_output.quadriceps_right — the Stimulator fires 2 of 5 channels on this zone via the MuscleMap.

Resolved channels (2)
45
Amplitude40%
Pulse width120µs
# Quadriceps (right) — write the slot; the Stimulator resolves
# channels via MuscleMap. You never touch SDK channel IDs.
self.ems_output.quadriceps_right = EMSParamData(
    IsMuted=False,
    Amplitude=40,    # % of calibrated max
    PulseWidth=120, # microseconds
    Period=20.0,    # ms  (= 50 Hz)
)

What it is#

MuscleMap is a runtime lookup table that resolves anatomical muscle names ("quadriceps_left", "biceps_right", …) to the integer channel IDs the Teslasuit SDK uses to deliver stimulation. It loads a JSON config at construction time, asks the SDK's mapper subsystem for the current bone layout, and turns each muscle's bone_index + channel_slice spec into a concrete list of channel IDs.

A MuscleMap instance is held by SuitHandler.muscle_map and exposed to your strategy as self.muscles.

Why it exists#

The Teslasuit SDK exposes the suit as a geometric structure — bones, nodes, channels. To stimulate the left quadriceps you have to know that it lives on bone index 1 (the left upper leg), and that the relevant channels on that bone are slice [0:3] for hardware version 4.x. That mapping changes between hardware versions (Teslasuit 4.x and XR5).

This is the wrong API for clinical and research developers. They think in muscle groups, not bone-index-channel-slice tuples. The MuscleMap formalises the translation as one-time configuration, with the following consequences:

  • Application code is portable across hardware versions — swap a JSON file, no code changes.
  • The translation is consulted at connection time, then cached. There is no per-cycle lookup overhead.
  • Adding a new muscle is a config change, not a code change.

See: Design principle 3.

When to use it#

Implicitly: every EmsData write goes through the MuscleMap. The Stimulator looks up the channel IDs for quadriceps_left and routes the stimulation pulse there. You don't write this code.

Explicitly: when you want to stimulate by region or side without naming each muscle:

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

Or when you need the underlying channel IDs (rare — usually only when implementing a custom haptic library):

channels = self.muscles.get_channels("quadriceps_left")  # → [14, 15, 16]

Default muscle inventory (4.x config)#

The shipping muscle_map_4R.json (covering the Teslasuit 4.x family) defines 20 muscles:

Lower body (10)#

Field nameAnatomy
quadriceps_left/_rightAnterior thigh
hamstring_left/_rightPosterior thigh
gastrocnemius_left/_rightCalf
tibialis_anterior_left/_rightShin
gluteus_left/_rightButtock

Upper body (10)#

Field nameAnatomy
deltoid_left/_rightShoulder
biceps_left/_rightAnterior upper arm
triceps_left/_rightPosterior upper arm
wrist_flexors_left/_rightAnterior forearm
wrist_extensors_left/_rightPosterior forearm

These are the only muscle field names that exist on EmsData. To add a new muscle, you'd edit muscle_map_4R.json and extend the EmsData dataclass. The two must stay in sync.


Query API#

The MuscleMap supports a minimal set of queries:

self.muscles["quadriceps_left"]            # → MuscleInfo
"quadriceps_left" in self.muscles          # → True
list(self.muscles)                         # → [MuscleInfo, MuscleInfo, ...]
len(self.muscles)                          # → 20

self.muscles.get_channels("quadriceps_left")  # → [14, 15, 16]
self.muscles.list_muscles()                   # → ["quadriceps_left", "quadriceps_right", ...]
self.muscles.by_side("left")                  # → [MuscleInfo, MuscleInfo, ...]
self.muscles.by_region("upper_leg")           # → [MuscleInfo, MuscleInfo, ...]

Each MuscleInfo carries the metadata you might need:

@dataclass
class MuscleInfo:
    name: str                        # "quadriceps_left"
    channel_ids: list                # [14, 15, 16]
    side: str                        # "left" or "right"
    body_region: str                 # "upper_leg", "lower_leg", "hip", ...
    default_period_ms: int           # from config
    default_amplitude_pct: int       # from config
    default_pulse_width_us: int      # from config

The default_* fields are loaded from the config's stimulation_defaults section per muscle. You can use them as sensible starting parameters when initialising a strategy.


Minimal example — region-based control#

from fes_framework.control.strategy_base import ControlStrategyBase
from fes_framework.data.types import EMSParamData

class WarmupStrategy(ControlStrategyBase):
    """Stimulate every muscle in the upper leg at low amplitude."""

    def process(self) -> None:
        for muscle in self.muscles.by_region("upper_leg"):
            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,
            ))

Notice: no hardcoded muscle names, no hardcoded amplitude. The strategy will continue to work if you later add a new upper-leg muscle to the config.


Hardware versions#

The framework supports Teslasuit 4.x and XR5. It ships with muscle_map_4R.json (covering the 4.x family). To run on XR5 (or any other supported variant):

  1. Author or obtain a JSON config for that version (e.g. muscle_map_XR5.json).
  2. Pass it explicitly:
    suit_handler = SuitHandler(muscle_map_config="path/to/muscle_map_XR5.json")
    engine = ClosedLoopEngine(control_strategy=MyStrategy(),
                              suit_handler=suit_handler)

The framework detects the hardware version from the config's hardware_version field; you can read it as self.muscles.hardware_version.


Config file shape (abbreviated)#

{
  "hardware_version": "4R",
  "muscles": {
    "quadriceps_left": {
      "side": "left",
      "body_region": "upper_leg",
      "channels": [
        { "bone_index": 1, "channel_slice": [0, 3] }
      ],
      "stimulation_defaults": {
        "period_ms": 20,
        "amplitude_pct": 0,
        "pulse_width_us": 120
      }
    },
    // ...
  }
}

channel_slice accepts:

  • [i] — single channel ID
  • [start, end] — Python-style slice
  • [start, null] — slice from start to end of bone
  • [i, j, k, ...] (length > 2) — explicit list of channel IDs

See fes_framework/config/muscle_map_4R.json for the full file.


See also#