TeslasuitDocumentation
Frameworks

Step 4 — Calibration gate

Goal: refuse to start the control loop until calibration succeeds and meets a quality bar. You'll use: CalibrationAPI, ClosedLoopEngine lifecycle hooks Builds on: Step 3 Anchored example: examples/atomic/calibration_gate.py


What you're adding#

So far you've trusted the SDK's joint-angle data to be accurate. It isn't, until calibration runs. The framework exposes calibration via engine.calibration and adds a quality assessment so you can refuse to start the loop on a bad calibration.

You'll subclass ClosedLoopEngine, override on_start(), run calibration there, and call self.stop() if quality is too low. The engine's main loop will see the stop request and exit cleanly before ever delivering stimulation.


Code#

# app/engine.py
from fes_framework.engine import ClosedLoopEngine


class CalibratedEngine(ClosedLoopEngine):
    """Refuse to start until calibration succeeds and quality is acceptable."""

    def on_start(self) -> None:
        # Enable biomechanical-angle collection as well, since most
        # calibrated applications use joint angles.
        self.data_streamer.set_biomech_collection(True)

        input("[CalibratedEngine] Stand in I-pose. Press Enter to calibrate... ")

        result = self.calibration.calibrate()
        print(f"[CalibratedEngine] {result.message}")
        if not result.success:
            print("[CalibratedEngine] Calibration failed — stopping.")
            self.stop()
            return

        quality = self.calibration.check_quality()
        print(
            f"[CalibratedEngine] quality={quality.overall_score:.0%} "
            f"({quality.sensors_reporting}/{quality.sensors_expected} sensors, "
            f"symmetry={quality.symmetry_score:.0%})"
        )
        if quality.issues:
            for issue in quality.issues:
                print(f"  ! {issue}")
        if not quality.is_acceptable:
            print("[CalibratedEngine] Quality below threshold — stopping.")
            self.stop()
            return

        # Optional: persist the reference frame to disk.
        self.calibration.export(path="./data")

        print("[CalibratedEngine] Calibration OK. Running.")
# app/main.py
from fes_framework.orchestrator import launch
from app.engine import CalibratedEngine
from app.strategy import StanceQuadStrategy   # from Step 3

if __name__ == "__main__":
    launch(StanceQuadStrategy, engine_class=CalibratedEngine)

Walkthrough#

CalibratedEngine(ClosedLoopEngine) — subclass the engine when you need lifecycle hooks. There's no separate "calibration manager" class to plug in; the engine is the place.

on_start() — fires after strategy.setup() and before the first cycle. Anything you do here runs while the suit is connected and streaming, but no stimulation has happened yet. That makes it the right place for any pre-flight check.

self.calibration.calibrate() — the SDK call. Blocks for ~1 second while the SDK captures the I-pose reference frame. The returned CalibrationResult reports success and includes the raw reference frame for offline analysis.

self.calibration.check_quality() — runs three heuristic checks (sensor reporting, left-right symmetry, plausible IMU ranges) and returns a CalibrationQuality with an overall score 0.0–1.0. Default acceptance threshold is 0.7. See Calibration → Quality checks.

self.stop() — flips an internal flag the main loop checks at the top of every iteration. The next cycle won't run; the engine falls through to _cleanup(). No stimulation has been delivered.

self.calibration.export(path="./data") — writes a timestamped CSV with the reference frame. Useful when you want to reproduce a session offline.


Why on_start() and not __init__()?#

__init__() runs while the engine is being constructed — possibly before the suit is fully streaming, definitely before the strategy has been wired up. By the time on_start() runs, all the auto-built components are in place and the suit is producing data. It's the correct place for any operation that depends on hardware state.


What calibration.check_quality() looks for#

CheckCommon failureWhat it means
Sensor reportingOne sensor returns all-zeroA suit segment is loose, unplugged, or damaged
Left-right symmetryBilateral cosine similarity < 0.7The user wasn't actually in I-pose (arms crossed, leaning)
Plausible rangesAccelerometer magnitude not ~1 GGross IMU miscalibration

quality.issues is a list of human-readable descriptions, suitable for display in a GUI or printing to console.


Verify#

Run the app. Expected output:

[CalibratedEngine] Stand in I-pose. Press Enter to calibrate...
[CalibratedEngine] Calibration completed successfully
[CalibratedEngine] quality=92% (20/20 sensors, symmetry=97%)
[CalibratedEngine] Calibration OK. Running.
[Orchestrator] Running headless (Ctrl-C to stop)

If quality is too low, simulate it: stand with your feet wide apart or arms folded when you press Enter. You should see:

[CalibratedEngine] quality=58% (20/20 sensors, symmetry=42%)
  ! Poor left-right symmetry (score: 0.42). Subject may not have been in proper I-pose.
[CalibratedEngine] Quality below threshold — stopping.
ClosedLoopEngine stopped after 0 cycles

The "0 cycles" confirms no stimulation was delivered.


Tuning the threshold#

CalibrationAPI.QUALITY_THRESHOLD is 0.7 by default. To tighten or loosen:

class CalibratedEngine(ClosedLoopEngine):
    def on_start(self) -> None:
        self.calibration.QUALITY_THRESHOLD = 0.85   # stricter
        ...

For early development, a lower threshold (0.5) lets you iterate without re-fitting the suit between runs. For sessions that matter, push it higher (0.85–0.9).


What you've learned#

  • ClosedLoopEngine subclassing + on_start() is the canonical hook for pre-flight gating.
  • engine.calibration.calibrate() + .check_quality() is the standard pattern; the framework gives you both the calibration itself and a quality score.
  • self.stop() from on_start() is safe — no stimulation happens if you stop before the first cycle.

Next#

Step 5 — Runtime parameters. You'll let the operator tune thresholds and amplitudes from a controller without restarting the application.