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
SimplePIDclass. - Antagonist pair stimulation — signed PID error drives biceps (for flexion) or triceps (for extension).
- Custom
ControlMessage—ElbowControlMessagecarries 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 plotsHow the closed loop works#
Each cycle inside the strategy:
- Read the active arm's elbow flexion angle (
self.joints.ElbowFlexExtRorElbowFlexExtL). - If mirror mode is on, set the target = opposite arm's measured angle. Otherwise use the slider's setpoint.
- Compute the PID output:
error = target - measured, run throughSimplePID. - 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.
- Positive output (need flexion) → write biceps
- 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.mainWorkflow:
- Stand/sit in T-pose, click Calibrate (T-pose).
- Pick the active arm (Left or Right radio).
- Set the desired angle slider.
- Lower the amplitude slider to its minimum before enabling FES (the example loads at 100%).
- Tick Enable FES (master), then raise amplitude slowly while watching the wearer.
- 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. - 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:
| Concern | Raw SDK code | This example |
|---|---|---|
| Connection | TsApi() + get_or_wait_last_device_attached() + … | (auto by ClosedLoopEngine) |
| Joint-angle access | parse get_biomechanical_angles_on_ready() ctypes | self.joints.ElbowFlexExtR |
| Stimulation | haptic.create_touch(...) + create_touch_parameters(...) | self.ems_output.biceps_left = EMSParamData(...) |
| Per-cycle pacing | manual timer | hardware-paced |
| Parameter tuning | restart per change | live ControlMessage updates |
| Calibration quality | manual check | engine.calibration.check_quality() |
| GUI ↔ control loop | bespoke threading | multiprocessing.Queue + SharedRingBuffer |
| Recording | bespoke file writer | lsl_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 application | Copy |
|---|---|
| PID with anti-windup clamp | elbow_utils.SimplePID |
| FFT-based auto-tune state machine | elbow_utils.FFTAutoTuner |
| Antagonist-pair stimulation pattern | elbow_control_strategy.process() |
| Calibration gate UI | gui/tabs/control_tab.py |
| Real-time pyqtgraph plotting | gui/tabs/plot_tab.py |
| Shared-memory frame layout | elbow_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 withself.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#
examples/elbow_flexion/README.md— operational README- Implementation Guide → Step 9 — what a full application looks like
- Concepts → ControlStrategy
- Concepts → Messaging
