TeslasuitDocumentation
Frameworks

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:

  1. Add the muscle to fes_framework/config/muscle_map_4R.json with its bone index and channel slice.
  2. Add a matching field to EmsData in fes_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_contact

Can 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:

  1. IsMuted=True. Defaults are muted; check your assignment.
  2. UtilityMessage.FesIsActive=False. Global kill switch.
  3. Amplitude / pulse width is 0. Defaults are 0; you have to set them.
  4. Strategy condition never fires. Print self.contacts.right_foot_contact etc. to confirm the input is what you expect.
  5. 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    # stricter

Default 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?#

  1. lsl_enabled=True? Default is False.
  2. Firewall. LSL uses UDP multicast.
  3. Network interface. LabRecorder and the application must be on the same network. Try same-machine first.
  4. 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#