TeslasuitDocumentation
Frameworks

Project anatomy

The shape of an RapidKit application — what files you typically write, where each lives, and how they fit together.


A typical headless application#

my_fes_app/
├── app/
│   ├── __init__.py
│   ├── strategy.py         # ControlStrategyBase subclass — your algorithm
│   └── messages.py         # MyControlMessage subclass — your parameters
└── main.py                 # orchestrator.launch(MyStrategy)

That's all you need for a working closed-loop application. No GUI, no IPC code, no hardware boilerplate.

A typical GUI application#

my_fes_app/
├── app/
│   ├── __init__.py
│   ├── strategy.py
│   ├── messages.py
│   ├── engine.py           # ClosedLoopEngine subclass — calibration gate, on_cycle hook
│   └── gui/
│       ├── __init__.py
│       ├── main_window.py  # PyQt5 MainWindow
│       └── widgets/
│           ├── controls.py # operator sliders / toggles
│           └── plots.py    # real-time data plots
└── main.py                 # orchestrator.launch(...)

Both examples/elbow_flexion/ and examples/walking_fes/ follow this layout. Use either as a template.


What each file does#

app/strategy.py#

Your ControlStrategyBase subclass. The closed-loop algorithm itself — read self.joints, write self.ems_output. No multiprocessing, no Qt, no SDK calls.

This is the file you'll edit the most.

app/messages.py#

Your ControlMessage subclass — the runtime parameters your operator can adjust. Plain dataclass fields with sensible defaults.

@dataclass
class MyControlMessage(ControlMessage):
    threshold_deg: float = 15.0
    quad_amplitude: int = 30

The fields here become channels in the AppData_ControlMessage LSL outlet (when LSL is enabled) and become rows in the GUI.

app/engine.py#

Optional. A ClosedLoopEngine subclass when you need lifecycle hooks: calibration gate (on_start), shared-memory writes (on_cycle_complete), reactions to messages (on_control_message, on_utility_message), or custom shutdown (on_stop).

If your application doesn't need any hooks, skip this file and pass the strategy directly to launch().

app/gui/#

PyQt5 widgets when present. A typical layout:

  • main_window.py — the top-level QMainWindow subclass; owns tabs, menus, the Qt event loop entry point.
  • widgets/ — leaf widgets (sliders, plots, status indicators). Many will subclass fes_framework.gui.base_widget.FesWidget for the standard data-update pattern.

The GUI process talks to the backend via:

  • control_queue (it pushes MyControlMessage),
  • utility_queue (bidirectional UtilityMessage),
  • SharedRingBuffer (it pulls live sensor frames at 30 Hz).

main.py#

The launcher. Always tiny — just launch(...) with the right arguments.

from fes_framework.orchestrator import launch
from app.strategy import MyStrategy
from app.engine import MyEngine
from app.gui.main_window import gui_main

if __name__ == "__main__":
    launch(
        MyStrategy,
        engine_class=MyEngine,        # optional
        gui_runner=gui_main,          # optional
        lsl_enabled=True,             # optional
        external_input_streams=[...], # optional
    )

What you do not write#

You never write code for any of the following — the framework owns them:

  • Hardware connection / SDK initialisation
  • Frame parsing (ctypes → dataclasses)
  • Channel ID lookup (MuscleMap does it)
  • Looped haptic playable management
  • LSL outlet creation
  • Process forking / IPC plumbing
  • Cleanup on shutdown

If you find yourself writing any of this, look at the relevant concept page — there's almost certainly a framework hook for it.


Putting it on disk#

For a new application:

  1. Copy examples/elbow_flexion/ to a new directory.
  2. Rename elbow_* files to your application name.
  3. Replace the strategy logic in <your>_control_strategy.py.
  4. Adjust <your>_types.py (control message, shared-memory dtype).
  5. Edit the GUI tabs to expose your application's parameters.
  6. Test by running python -m my_fes_app.main.

There is no scaffold generator (RapidKit new my_app) yet. Copy-paste from an example is the supported workflow.


When to grow beyond this layout#

For most research applications the layout above is enough. Consider splitting further when:

  • The strategy file exceeds ~500 lines — split utility classes into app/utils/.
  • You have multiple operators with different needs — give each one its own GUI module under app/gui/operator_x/.
  • You're shipping the application as an executable — add a PyInstaller .spec file alongside main.py. See examples/elbow_flexion/main.spec for an example.

See also#