TeslasuitDocumentation
Frameworks

Elbow flexion (PID, antagonist pair, GUI)

A complete PID-controlled FES application for elbow flexion. Uses biceps and triceps as an agonist/antagonist pair, an FFT-based auto-tuner, and a PyQt5 GUI with control sliders and live plots.

Source: examples/elbow_flexion/ README: examples/elbow_flexion/README.md


What it shows#

This is the framework's reference for continuous joint-angle tracking with antagonist muscle pairs. Where the Walking FES example is event-driven (gait phase transitions), this one is signal-driven (continuous joint-angle feedback).

Capabilities demonstrated:

  • PID control with a self-contained SimplePID class.
  • Antagonist pair stimulation — signed PID error drives biceps (for flexion) or triceps (for extension).
  • Custom ControlMessageElbowControlMessage carries PID gains, target angle, active-arm flag, mirror toggle, and an auto-tune request.
  • FFT-based PID auto-tuner — ramps Kp until sustained oscillation, then estimates Ku/Tu from the dominant FFT frequency and applies Ziegler–Nichols gains.
  • Opposite-arm mirroring — one arm acts as the setpoint source for the other.
  • Dual-process GUI — PyQt5 main window with a control tab (sliders) and a plot tab (pyqtgraph angle / pulse-width traces).
  • Calibration gate — pre-launch quality check.

Project layout#

examples/elbow_flexion/
├── main.py                          ── dual-process launcher
├── backend_mainloop.py              ── ElbowBackendMainloop(ClosedLoopEngine)
├── elbow_control_strategy.py        ── ElbowFlexionPIDStrategy(ControlStrategyBase)
├── elbow_utils.py                   ── SimplePID + FFTAutoTuner
├── elbow_types.py                   ── ElbowControlMessage + shared-memory dtype
├── elbow_init_utils.py              ── factory + frame packer
└── gui/
    ├── main_window.py               ── two-tab MainWindow
    ├── data_handler.py              ── reads SharedRingBuffer
    └── tabs/
        ├── control_tab.py           ── sliders, radios, auto-tune button
        └── plot_tab.py              ── angle + pulse-width plots

How the closed loop works#

Each cycle inside the strategy:

  1. Read the active arm's elbow flexion angle (self.joints.ElbowFlexExtR or ElbowFlexExtL).
  2. If mirror mode is on, set the target = opposite arm's measured angle. Otherwise use the slider's setpoint.
  3. Compute the PID output: error = target - measured, run through SimplePID.
  4. Map signed PID output to two muscles:
    • Positive output (need flexion) → write biceps EMSParamData, mute triceps.
    • Negative output (need extension) → write triceps EMSParamData, mute biceps.
  5. Clamp pulse-width at 140 μs.

The auto-tuner runs as a state machine in FFTAutoTuner. When the operator clicks "Auto-tune," the strategy ramps Kp upward each cycle while watching the elbow angle's FFT. When sustained oscillation is detected, it computes Ku and Tu and writes Ziegler–Nichols gains back into ElbowControlMessage.


Running it#

Hardware:

  • Teslasuit 4.x or XR5, connected, powered.
  • Subject seated, elbows free to move 0–135°.
python -m examples.elbow_flexion.main

Workflow:

  1. Stand/sit in T-pose, click Calibrate (T-pose).
  2. Pick the active arm (Left or Right radio).
  3. Set the desired angle slider.
  4. Lower the amplitude slider to its minimum before enabling FES (the example loads at 100%).
  5. Tick Enable FES (master), then raise amplitude slowly while watching the wearer.
  6. Either tune Kp/Ki/Kd manually with the sliders or click Auto-tune PID — when the status shows DONE, the suggested gains have been applied.
  7. Optionally enable Mirror opposite arm — the active arm now tracks the contralateral arm's angle.

What's clinically interesting here#

Compare this to writing the same thing on the raw Teslasuit SDK:

ConcernRaw SDK codeThis example
ConnectionTsApi() + get_or_wait_last_device_attached() + …(auto by ClosedLoopEngine)
Joint-angle accessparse get_biomechanical_angles_on_ready() ctypesself.joints.ElbowFlexExtR
Stimulationhaptic.create_touch(...) + create_touch_parameters(...)self.ems_output.biceps_left = EMSParamData(...)
Per-cycle pacingmanual timerhardware-paced
Parameter tuningrestart per changelive ControlMessage updates
Calibration qualitymanual checkengine.calibration.check_quality()
GUI ↔ control loopbespoke threadingmultiprocessing.Queue + SharedRingBuffer
Recordingbespoke file writerlsl_enabled=True + LabRecorder

The strategy file is ~250 lines. The same functionality on raw SDK would be ~1,500.


Things you can copy from this example#

For your applicationCopy
PID with anti-windup clampelbow_utils.SimplePID
FFT-based auto-tune state machineelbow_utils.FFTAutoTuner
Antagonist-pair stimulation patternelbow_control_strategy.process()
Calibration gate UIgui/tabs/control_tab.py
Real-time pyqtgraph plottinggui/tabs/plot_tab.py
Shared-memory frame layoutelbow_types.shared_memory_frame_elbow

The strategy and utility classes are deliberately self-contained — nothing imports from walking_fes/ or other example folders. You can lift them wholesale into your own application directory and start modifying.


Safety notes#

⚠️ The example loads at 100% amplitude. 100% is not a safe starting point. Always lower the amplitude slider to its minimum before ticking Enable FES, then raise it slowly while watching the wearer. The example should default to a low amplitude; until it does, treat lowering it as a mandatory first step. Never run on an uncalibrated suit.

  • Default amplitude in this example is 100%; dial it down before enabling FES for the first time.
  • The master FES toggle routes through the framework's kill switch — when off, all stimulation is muted at the engine level.
  • The auto-tuner caps Kp at 3.0 and has a 30-second hard time limit.
  • Pulse width is clamped to 140 μs in the strategy.

See Safety for the framework's safety guarantees.


Heritage#

Ported from a standalone research prototype that built directly on the Teslasuit SDK. The framework integration:

  • replaced raw-quaternion math with BiomechanicalData.ElbowFlexExt*,
  • replaced haptic_play_touch() calls with self.ems_output.* writes,
  • folded three QTimers into one engine cycle.

This is exactly the kind of "extract to framework" exercise the framework was designed for. If you're porting your own SDK-direct application, this is the template.


See also#