Haptic navigation (custom haptic library)
Directional haptic cues. Demonstrates the HapticLibrary extension
point — declaring named slots for CustomPlayable and toggling
their IsMuted field to fire/silence them.
Source: examples/haptic_navigation/
What it shows#
This is the canonical example of using EMS for non-functional output — i.e. tactile feedback rather than producing movement. The suit is used as a body-mounted haptic display: arrow keys in the GUI fire haptic cues on different parts of the wearer's body.
Capabilities demonstrated:
HapticLibrarysubclass —NavCuesdeclares namedCustomPlayableslots:belly_cue,back_cue,left_shoulder_cue,right_shoulder_cue.SuitHandler.create_haptic_touch()— building looped EMS playables programmatically (no haptic asset files needed).- Slot mute toggle pattern —
slot.IsMuted = Falseto fire,slot.IsMuted = Trueto silence. The framework'sLibraryStimulatortranslates these to SDK calls. ControlMessagefor direction state — the GUI signals which arrow keys are held; the strategy maps that to slot mutes.
Project layout#
examples/haptic_navigation/
├── main.py ── orchestrator.launch()
├── haptic_navigation_strategy.py ── HapticNavigationStrategy
├── haptic_navigation_types.py ── NavControlMessage, NavCues (HapticLibrary)
└── gui/
└── main.py ── PyQt5 GUI with arrow-key handlingHow the strategy works#
In setup(), the strategy populates four playable slots — one per
direction:
def setup(self, muscles=None, suit=None, config=None) -> None:
super().setup(muscles, suit, config)
self.haptic_library = NavCues()
self.haptic_library.belly_cue = self.suit.create_haptic_touch(
bone_id=BELLY_BONE,
channel_list=BELLY_CHANNELS,
period=20.0, amplitude=30, pulse_width=120,
)
# ... back, left_shoulder, right_shoulder similarly ...In process(), the strategy reads which directions are held in
self.params (a NavControlMessage subclass) and toggles slot
mutes:
def process(self) -> None:
if self.params is None:
return
self.haptic_library.belly_cue.IsMuted = not self.params.up_held
self.haptic_library.back_cue.IsMuted = not self.params.down_held
self.haptic_library.left_shoulder_cue.IsMuted = not self.params.left_held
self.haptic_library.right_shoulder_cue.IsMuted = not self.params.right_heldThat's it. The framework's LibraryStimulator runs after process()
and converts each mute change to the corresponding SDK call. Custom
haptic patterns become a one-line toggle.
Running it#
python -m examples.haptic_navigation.mainA small GUI window appears. Arrow keys fire haptic cues:
- ↑ — belly
- ↓ — back
- ← — left shoulder
- → — right shoulder
Multiple arrow keys can be held simultaneously.
When to use HapticLibrary vs. EmsData#
| If you want to … | Use |
|---|---|
| Stimulate one of the 20 standard muscles | EmsData (self.ems_output.<muscle>) |
| Fire a custom looped haptic pattern by mute toggle | HapticLibrary slot |
Load a pre-recorded .hpt haptic asset | HapticLibrary + suit.load_haptic_asset() |
| Target arbitrary bones / channel groups | HapticLibrary + suit.create_haptic_touch() |
| Use the SDK's haptic API directly | (escape hatch — self.suit.haptic.*) |
HapticLibrary is the supported way to extend beyond the 20 muscle
slots. See Messaging — HapticLibrary.
Things you can copy from this example#
| For your application | Copy |
|---|---|
| Pattern for naming and populating playable slots | haptic_navigation_strategy.py setup() |
| Direction-flag → slot-mute translation | process() |
| Arrow-key event handling for GUI cues | gui/main.py |
Setting up a NavControlMessage for "which keys are held" | haptic_navigation_types.py |
Safety notes#
The strategy uses a 30% amplitude default — comfortable as tactile feedback without producing involuntary movement. Adjust upward only if the wearer reports the cue as too subtle.
Unlike the elbow-flexion example, there's no closed-loop feedback — the user responds to the cues by changing what keys they hold. Treat this as a UX device, not a control loop.
See also#
- Concepts → Messaging — HapticLibrary
fes_framework/io/stimulator.py—LibraryStimulator- Step 9 — full application — the framework's full surface area
