FAQ
Frequently asked questions, organised by topic. For specific errors, see Troubleshooting. For concept deep-dives, see the Concepts pages.
Getting started#
What's the smallest possible application?#
About 10 lines:
from fes_framework.control.strategy_base import ControlStrategyBase
from fes_framework.orchestrator import launch
class NoOp(ControlStrategyBase):
def process(self) -> None:
pass
if __name__ == "__main__":
launch(NoOp)This connects to the suit, runs the closed loop, and shuts down cleanly on Ctrl-C. No stimulation. See Step 1.
Do I need a GUI to run an application?#
No. Headless mode is fully supported. Just don't pass a gui_runner
to launch(). The main process waits on the backend until Ctrl-C.
Can I run without Teslasuit hardware?#
No. SuitHandler.__init__() calls
device_manager.get_or_wait_last_device_attached(), which blocks
until a device appears. There is no mock / simulator path in the
framework.
Why is the loop running at 100 Hz?#
Because the Teslasuit SDK's get_*_on_ready calls return at that
rate. The framework's sample_rate parameter is metadata only — it
labels LSL streams and informs diagnostics, but it does not
regulate execution. See Design principle 1.
Strategy#
How do I add a new muscle?#
Two edits, in lockstep:
- Add the muscle to
fes_framework/config/muscle_map_4R.jsonwith its bone index and channel slice. - Add a matching field to
EmsDatainfes_framework/data/types.py.
The framework will pick up the new muscle automatically; your
strategy can write self.ems_output.<new_muscle> and the
stimulator will route it.
What if my strategy needs to track state between cycles?#
Initialise the state in setup() (or __init__() after
super().__init__()), update it in process():
class MyStrategy(ControlStrategyBase):
def setup(self, muscles=None, suit=None, config=None):
super().setup(muscles, suit, config)
self._step_count = 0
self._last_phase = None
def process(self):
if self.contacts.right_foot_contact != self._last_phase:
self._step_count += 1
self._last_phase = self.contacts.right_foot_contactCan I have multiple strategies and switch between them?#
Not within one engine. The engine holds a single control_strategy
reference. Patterns to consider:
- Phase-based dispatch inside one strategy: branch on a state variable to call different methods.
- Wrapper strategy: write a strategy that owns multiple sub-strategies and delegates to one of them based on state.
- Multiple engine instances: usually overkill, but possible if the use cases are completely separate.
How do I implement a custom signal filter?#
Subclass DataStreamer and override process():
class FilteredStreamer(DataStreamer):
def process(self):
# Low-pass filter on right knee
self.biomechanical_data.KneeFlexExtR = my_filter(
self.biomechanical_data.KneeFlexExtR
)Pass it to the engine: data_streamer=FilteredStreamer(suit_handler).
See DataStreamer.
Stimulation#
Why is my stimulation not firing?#
In order of likelihood:
IsMuted=True. Defaults are muted; check your assignment.UtilityMessage.FesIsActive=False. Global kill switch.- Amplitude / pulse width is 0. Defaults are 0; you have to set them.
- Strategy condition never fires. Print
self.contacts.right_foot_contactetc. to confirm the input is what you expect. - Hardware not enabled. Some Teslasuit Control Center versions need an explicit "stimulation enable" toggle.
See Troubleshooting → Stimulation not activating.
What's a safe starting amplitude?#
20–30%. Start there, increase by 5 per session, stop immediately if the user reports discomfort. Never run on someone you can't observe.
How do I ramp amplitude smoothly?#
Don't use time.sleep. Compute the next amplitude per cycle and
write it:
def process(self):
target = 50
self._amp = min(self._amp + 1, target) # +1 per cycle = ~5s ramp
self.ems_output.quadriceps_right = EMSParamData(
IsMuted=False, Amplitude=self._amp,
PulseWidth=120, Period=20.0,
)Note: changing Amplitude between cycles destroys and re-creates
the looped playable; for very smooth ramps consider keeping
parameters constant and modulating IsMuted (PWM-like).
Calibration#
Should every application calibrate?#
If your strategy reads self.joints (any joint angle), yes.
Calibration sets the I-pose reference; without it, joint angles
drift and any threshold-based decision becomes unreliable.
If your strategy only reads self.contacts (foot contacts) or
nothing at all, you can skip calibration.
How do I tune the quality threshold?#
self.calibration.QUALITY_THRESHOLD = 0.85 # stricterDefault is 0.7. For development iteration, lower it. For
clinical sessions, push it higher.
Why does calibration sometimes fail?#
Most often the user wasn't actually in I-pose, or a suit segment
is loose. quality.issues and quality.missing_sensors will tell
you which.
LSL#
Why don't my LSL streams appear?#
lsl_enabled=True? Default isFalse.- Firewall. LSL uses UDP multicast.
- Network interface. LabRecorder and the application must be on the same network. Try same-machine first.
- Click Update in LabRecorder after starting the app — streams aren't auto-discovered while LabRecorder is idle.
Can I disable individual outlets?#
Not currently. The 7 framework outlets are all-or-nothing via
lsl_enabled. Filter on the LabRecorder side by ticking only the
streams you want.
How do I run two applications without their LSL streams clashing?#
Pass distinct source_id strings to the engine:
engine = ClosedLoopEngine(control_strategy=..., source_id="app_session_1")Each outlet uses {source_id}_<suffix>, so different source_ids
keep recordings separate.
Multiprocessing and GUI#
Why two processes?#
Python's GIL prevents true parallel execution. PyQt5 blocks regularly. Running the engine in the same process as the GUI risks GUI redraws delaying stimulation pulses. See Design principle 5.
My GUI shows no data — why?#
Most often the SharedRingBuffer name differs between processes.
Check that both sides use the same name:
# backend
buffer = SharedRingBuffer(name="my_app_buffer", ...)
# GUI
adapter = DataAdapter(buffer_name="my_app_buffer", ...)Also check that the backend creates the buffer in on_start()
before the GUI tries to attach.
Can I use Tkinter / Electron / a web GUI instead of PyQt5?#
Yes. The framework's GUI integration is just multiprocessing.Queue +
SharedRingBuffer. Use any GUI tech that can:
- run in a separate process (or thread, if you accept the trade-offs),
- read from
multiprocessing.Queue, - read numpy structured arrays from shared memory.
Most non-Python GUIs (Electron, web frontend) connect via WebSocket or LSL — write a small Python adapter that bridges the two.
Production#
Can I package this as a standalone .exe?#
Yes — the examples/elbow_flexion/main.spec shows a PyInstaller
spec that bundles the framework + Teslasuit SDK + GUI into a
single Windows binary. The Walking FES application is distributed this way.
Is this ready for clinical use?#
No. The framework is for research and supervised demonstration. There is no medical-device certification, no electrical-isolation guarantee beyond Teslasuit's hardware, no regulatory pathway. See Safety.
How do I report a bug?#
For partner deployments, contact support@teslasuit.io. The framework's internal issue tracker isn't public.
Architecture and design#
Why is process() argument-less?#
So that adding new inputs (LSL inlets, PPG data, custom signals) doesn't break existing strategy signatures. Inputs are slot-based. See Design principle 2.
Why does ControlMessage start empty?#
Because the framework doesn't know what your application's parameters are. Subclassing it is intentional — it lets the LSL streamer auto-discover your fields and lets the IPC system pickle them generically.
Why is UtilityMessage not user-extensible?#
Because its fields are framework concerns (FES kill switch,
calibration loop status). Adding application-specific flags to it
would require the framework to know about them. Application flags
go on your ControlMessage subclass.
What's the difference between Stimulator and LibraryStimulator?#
Stimulator handles the 20 standard muscles defined by EmsData
(via MuscleMap). LibraryStimulator handles user-defined
custom playables stored in a HapticLibrary subclass. Both run
after every process(), in series. See Messaging — HapticLibrary.
See also#
- Troubleshooting — error messages and fixes
- Concepts — building-block deep dives
- API Reference — every signature
- Design principles — the why behind everything
