Walking FES — full application walkthrough
This page walks through examples/walking_fes/ — the
full reference walking-FES application. It demonstrates every framework
concept covered in the Implementation Guide
in a real research scenario: gait-phase-adaptive Functional Electrical
Stimulation for 5 muscle groups per leg.
Prerequisites: Steps 1–8 of the Implementation Guide. Use the API Reference for class signatures.
1. What the Walking FES Example Demonstrates#
The Walking FES application is the reference implementation of the framework. It shows:
- Gait-phase-adaptive control: stimulation timing adapts to the user's actual walking speed by continuously measuring phase durations.
- 5 lower-body muscle groups per leg: quadriceps, hamstring, gastrocnemius, tibialis anterior, gluteus — independently for left and right (the framework supports 20 muscles total including upper body).
- Operator GUI: PyQt5 interface with per-muscle sliders and real-time plots.
- Multiple inheritance pattern: how to compose utility classes with
ControlStrategyBase. - Production-quality calibration flow: how to guard the control loop on calibration.
2. Project Structure#
examples/walking_fes/
├── main.py ← Entry point (dual-process launcher)
├── backend_mainloop.py ← Backend coordinator (wraps ClosedLoopEngine)
├── walking_control_strategy.py ← SwingStanceAdaptiveWalkingStrategy
├── waking_utils.py ← Gait phase utilities (4 utility classes)
└── gui/
├── main_window.py ← MainWindow (3 tabs)
├── data_handler.py ← Data routing to GUI components
├── tabs/
│ ├── overview_tab.py ← FES controls + step indicators
│ ├── biomechanics_tab.py ← Real-time joint angle plots
│ └── gait_statistics_tab.py ← Phase duration statistics
└── widgets/ ← Custom widgets (sliders, bar charts)3. Entry Point: main.py#
The entry point manually orchestrates the dual-process launch (predating the current
orchestrator.launch() helper — both approaches are valid):
# Simplified version of examples/walking_fes/main.py
if __name__ == "__main__":
freeze_support()
utility_queue = Queue()
control_queue = Queue()
# Start backend in subprocess
backend_process = Process(
target=run_backend_process,
args=(control_queue, utility_queue)
)
backend_process.start()
time.sleep(5) # Wait for Teslasuit hardware initialisation
# Run GUI in main process
run_gui(control_queue=control_queue, utility_queue=utility_queue)The time.sleep(5) delay allows the backend time to connect to the Teslasuit hardware
before the GUI appears. This matches orchestrator.launch(hardware_init_delay=5.0).
4. Backend: backend_mainloop.py#
The Walking FES backend uses its own BackendMainloop class (written before ClosedLoopEngine
was extracted). Functionally it is equivalent — it runs the same seven-step cycle at
~100 Hz.
For new applications, prefer ClosedLoopEngine directly:
# Modern equivalent of BackendMainloop:
from fes_framework.engine import ClosedLoopEngine
from examples.walking_fes.walking_control_strategy import SwingStanceAdaptiveWalkingStrategy
engine = ClosedLoopEngine(
control_strategy=SwingStanceAdaptiveWalkingStrategy(),
control_queue=control_queue,
utility_queue=utility_queue,
)
engine.run()5. Control Strategy: SwingStanceAdaptiveWalkingStrategy#
The strategy uses multiple inheritance to compose four utility classes with
ControlStrategyBase:
class SwingStanceAdaptiveWalkingStrategy(
SwingStanceAdaptiveScheduler, # timing window management
LegsEMSParameterUpdater, # activate/deactivate methods
ControlStrategyBase # must be last in MRO
):
def process(self) -> None:
self.internal_logic_loop()process() delegates entirely to internal_logic_loop(), which contains the full
gait phase detection and stimulation logic. This keeps process() minimal and the
logic well-encapsulated.
What internal_logic_loop() does each cycle:#
Step 1: _check_swing_to_stance_transition_()
Detect stance↔swing transitions for both legs by comparing
current vs. previous foot contacts.
Step 2: update_gait_timings()
Update measured phase durations and running averages.
Stance duration = time since last swing→stance transition.
Swing duration = time since last stance→swing transition.
Step 3: update_stimulation_timeline_if_transition_occurs()
When a transition occurs, recalculate absolute stimulation
start/end times for all muscles based on measured durations.
Step 4: _update_previous_foot_contact_()
Cache current foot contact states for next cycle comparison.
Step 5: check_is_standing() → if True, deactivate_all_stimulation()
If both feet are on ground for > 1 second, enter standing mode.
Step 6: LEFT LEG — stance or swing phase stimulation logic
For each of the 5 muscles:
if elapsed_time > start_time AND < end_time: activate
else: deactivate
Step 7: RIGHT LEG — same logic, mirroredTiming Windows#
The strategy reads stimulation timing from self.params (the WalkingControlMessage
from the GUI). Each muscle has stance and swing windows defined as fractions (0.0–1.0)
of the measured phase duration:
Stance phase duration = (measured from foot contact to lift-off)
quadriceps window = [stance_start × duration, stance_end × duration]
Swing phase duration = (measured from lift-off to foot contact)
quadriceps window = [swing_start × duration, swing_end × duration]This makes stimulation automatically adapt to walking speed — faster walking = shorter phases = shorter stimulation windows in absolute time.
6. Gait Utility Classes: waking_utils.py#
Four utility classes provide the supporting machinery, designed for mixin use:
WalkingPhaseChangeChecker#
Detects stance↔swing transitions by comparing current vs. previous foot contacts. Sets four boolean flags each cycle:
left_stance_to_swing_transition # True for one cycle when left foot lifts
left_swing_to_stance_transition # True for one cycle when left foot contacts
right_stance_to_swing_transition # True for one cycle when right foot lifts
right_swing_to_stance_transition # True for one cycle when right foot contactsAlso detects double-support (standing) when both feet are in contact for > 1 second.
GaitPhasesDurationEstimator#
Tracks measured gait phase durations and computes running averages. Inherits from
WalkingPhaseChangeChecker.
- On each swing→stance transition: records stance phase duration
- On each stance→swing transition: records swing phase duration
- Maintains a rolling average for use in timing window calculations
SwingStanceAdaptiveScheduler#
Manages absolute stimulation start/end times for all muscles. Inherits from
GaitPhasesDurationEstimator.
- On phase transitions: recalculates
LEFT_LEG_STANCE_RELATIVE_START_TIMEetc. dicts using the fraction × measured duration formula LOCAL_TIMER_START_TIMEdict records when each phase started (used for elapsed time)
LegsEMSParameterUpdater#
Provides activate/deactivate convenience methods for each muscle group. These methods
write to self.ems_output using the parameters from self.params:
# Activate left quadriceps using params from ControlMessage:
self.activate_stimulation_left_quadriceps()
# Deactivate:
self.deactivate_stimulation_left_quadriceps()
# Available for all 10 lower-body muscles used in the walking strategy:
# activate/deactivate_stimulation_{side}_{muscle}
# where side: left/right, muscle: quadriceps/hamstring/gastrocnemius/tibialis/gluteus7. GUI: Three-Tab Interface#
The Walking FES GUI (examples/walking_fes/gui/) provides an operator interface with three tabs:
Overview Tab#
The primary control interface:
- FES Active checkbox — global kill switch (maps to
UtilityMessage.FesIsActive) - Step Detection Mode — dropdown to switch between Teslasuit API / ML step detector
- Calibration button — triggers mocap calibration
- Left / Right Leg Panels: per-muscle controls:
- Enable checkbox
- Frequency spinbox (0–100 Hz)
- Pulse Width slider (1–100%)
- Stance phase timing range slider
- Swing phase timing range slider
- Muscle Activity Graph — real-time stimulation activity bars
Biomechanics Tab#
Real-time scrolling plots of all 29 joint angles, organised by body region (Pelvis,
Hips, Knees, Ankles, Shoulders, Elbows, Forearms, Wrists). Updated at 30 Hz from
SharedRingBuffer. Only updates when tab is active (performance optimisation).
Gait Statistics Tab#
Displays running gait phase statistics:
- Average stance / swing duration per leg (seconds)
- Total step count
- Current phase per leg (Stance / Swing / Standing)
8. Running the Walking FES Example#
Prerequisites#
- Teslasuit Control Center is running
- Teslasuit is powered and on the same WiFi network
- Dependencies installed:
pip install PyQt5 pyqtgraph
Run#
cd fes_framework
python examples/walking_fes/main.pyExpected Startup Sequence#
Project root added to Python path: C:\path\to\fes_framework
Starting backend process...
[Backend] Connecting to Teslasuit hardware...
[Backend] SuitHandler initialised
[Backend] DataStreamer ready
[Backend] CalibrationAPI ready
(5 second wait for hardware init)
Starting production GUI...
QApplication created successfully
MainWindow created successfully
GUI window shown, starting event loopFirst Session Workflow#
- Connect: Verify the Overview tab shows live step detection indicators.
- Calibrate: Click the Calibration button and stand in I-pose while it runs.
- Set Parameters: Start with a low pulse width (20–30%) for each muscle.
- Enable Muscles: Check the enable boxes for muscles you want active.
- Enable FES: Check FES Active to start stimulation.
- Walk: Observe the muscle activity graphs and gait statistics.
- Tune: Adjust timing windows and pulse width based on observed response.
- Stop: Uncheck FES Active or close the window.
9. Adapting the Walking FES Pattern for Your Own Application#
The Walking FES example is designed to be studied and adapted. Here is the recommended pattern for a new walking FES application:
# 1. Start from the control strategy pattern
from fes_framework.control.strategy_base import ControlStrategyBase
from examples.walking_fes.waking_utils import (
SwingStanceAdaptiveScheduler,
LegsEMSParameterUpdater,
)
class MyWalkingStrategy(SwingStanceAdaptiveScheduler,
LegsEMSParameterUpdater,
ControlStrategyBase):
"""Custom walking strategy building on Walking FES utilities."""
def process(self) -> None:
# Use Walking FES utilities for phase detection:
self._check_swing_to_stance_transition_()
self.update_gait_timings()
self.update_stimulation_timeline_if_transition_occurs()
self._update_previous_foot_contact_()
# Add your own logic:
if self.contacts.right_foot_contact:
elapsed = time.time() - self.LOCAL_TIMER_START_TIME["RightStance"]
if elapsed < 0.4: # first 400ms of stance
self.activate_stimulation_right_quadriceps()
else:
self.deactivate_stimulation_right_quadriceps()# 2. Create a ControlMessage with your parameters
from dataclasses import dataclass, field
from fes_framework.data.types import ControlMessage, _default_stim_params
@dataclass
class MyControlMessage(ControlMessage):
quadriceps_right: dict = field(default_factory=_default_stim_params)
# ... add more muscles# 3. Launch the application
from fes_framework.orchestrator import launch
from my_gui import my_gui_runner
if __name__ == "__main__":
launch(
MyWalkingStrategy,
gui_runner=my_gui_runner,
hardware_init_delay=5.0,
)Summary: Framework Concepts in the Walking FES Example#
| Framework concept | Walking FES implementation |
|---|---|
ControlStrategyBase.process() | SwingStanceAdaptiveWalkingStrategy.process() |
| Semantic muscle names | LegsEMSParameterUpdater.activate_stimulation_*() |
self.contacts (foot detection) | WalkingPhaseChangeChecker._check_swing_to_stance_transition_() |
self.params (runtime params) | Per-muscle dicts in WalkingControlMessage |
ClosedLoopEngine | BackendMainloop (equivalent, written before engine extraction) |
orchestrator.launch() | Manual Process + Queue setup in main.py |
SharedRingBuffer | Data handler writes frames; GUI plots reads them |
QueueHandler | Bidirectional control/utility message handling |
CalibrationAPI | Calibration button triggers suit_handler.mocap_calibrate_skeleton() |
| LSL streaming | LSLStreamer in backend (enabled via GUI flag) |
See also#
- API Reference — complete class / method signatures
- Concepts overview — framework concept reference
- Quick Start — 10-minute minimal application
- Implementation Guide → Step 9 — what a full application looks like
