Design Report — Rev 1.0

MPC-Augmented PI Controller with
Conditional Integration and Self-Learning
Disturbance Rejection

Embedded Thermal Control System for an Insulated Oven
ESP32 · MAX6675 Thermocouple · Phase-Angle Triac Drive · SIMC Auto-Tuning
Ömer Karaoğlu
Ömer Karaoğlu
System Designer & Author
April 15, 2026
Revision History
RevDateDescription
1.02026-04-15Initial release

Contents

  1. System Overview
  2. Main Loop Timing Architecture
  3. Temperature Filtering Pipeline
  4. Triac Phase-Angle Power Delivery
  5. Auto-Tuner State Machine
  6. SIMC Tuning Rules
  7. MPC Prediction Engine
  8. Self-Learning Cooling Rate
  9. Disturbance Rejection Logic
  10. Conditional Integral Switching
  11. Full PID Computation & Anti-Windup
  12. Parameter Reference

1. System Overview

The system is a single-loop thermal controller for an insulated oven, running on an ESP32 microcontroller. A MAX6675 thermocouple provides temperature feedback. A triac with phase-angle control modulates AC power to the heater. The controller combines model-predictive features with classical PI control, tuned via a built-in auto-tuner that identifies plant dynamics and applies SIMC rules.

flowchart LR
  subgraph HW["Hardware Layer"]
    TC["MAX6675\nThermocouple"]
    ZCD["Zero-Cross\nDetector"]
    TRIAC["Triac Gate\nDriver"]
    HEATER["Heating\nElement"]
  end

  subgraph SW["Software Layer (ESP32)"]
    FILT["Median + EMA\nFilter"]
    MPC["MPC Predictor\n& Cooling Model"]
    PID["PI Controller\nw/ Conditional\nIntegral"]
    LUT["True-RMS\nPower LUT"]
    AT["Auto-Tuner\n(SIMC)"]
  end

  TC -- raw °C --> FILT
  FILT -- filtered °C --> MPC
  MPC -- predicted °C --> PID
  PID -- power 0–1 --> LUT
  LUT -- delay µs --> ZCD
  ZCD -- trigger --> TRIAC
  TRIAC -- AC power --> HEATER
  HEATER -. thermal .-> TC

  AT -. "Kc, Ti" .-> PID

  style HW fill:#e8f4fd,stroke:#4a90d9
  style SW fill:#f0f1fa,stroke:#6c63ff
Figure 1 — High-level signal flow from sensor to heater

2. Main Loop Timing Architecture

The loop() function uses non-blocking timing to run three concurrent tasks at different rates, plus an event-driven serial command handler. No RTOS is used — all scheduling is cooperative via millis() comparisons.

flowchart TD
  LOOP["loop() entry"] --> T1{"250 ms\nelapsed?"}
  T1 -- Yes --> READ["Read thermocouple\nApply median + EMA filter"]
  READ --> BRANCH{"tune_state > 0 ?"}
  BRANCH -- Yes --> TUNE["runAutoTuner()"]
  BRANCH -- No --> CPID["computePID()"]
  TUNE --> T2
  CPID --> T2
  T1 -- No --> T2{"1000 ms\nelapsed?"}

  T2 -- Yes --> SLOPE["Compute slope\nover 10 s window"]
  SLOPE --> COOL["Update self-learning\ncooling rate"]
  COOL --> HIST["Record power_history\nand temp_history"]
  HIST --> T3
  T2 -- No --> T3{"500 ms\nelapsed?"}

  T3 -- Yes --> TEL["printTelemetry()"]
  T3 -- No --> T4
  TEL --> T4{"Serial\navailable?"}

  T4 -- Yes --> CMD["handleCommand()"]
  T4 -- No --> LOOP

  CMD --> LOOP

  style READ fill:#d4edda,stroke:#28a745
  style TUNE fill:#fff3cd,stroke:#ffc107
  style CPID fill:#d1ecf1,stroke:#17a2b8
  style SLOPE fill:#e2d5f1,stroke:#7c4dff
Figure 2 — Main loop timing structure (all times are non-blocking)
TaskPeriodPurpose
Control Loop250 msRead sensor, run filter, execute PID or auto-tuner
MPC Ledger1000 msCompute temperature slope, update cooling model, record power history
Telemetry500 msPrint system state to serial
Serial ParserEvent-drivenHandle SET, TUNE, LAMBDA, PLANT commands

3. Temperature Filtering Pipeline

Raw thermocouple readings are noisy. The system applies a two-stage filter: first a 7-sample median filter to reject spike outliers, then an exponential moving average (EMA) to smooth the result. This produces a clean current_temp value used by all downstream logic.

flowchart LR
  RAW["Raw reading\nfrom MAX6675"] --> NAN{"isnan()?"}
  NAN -- Yes --> HALT["pid_output = 0\nSENSOR ERROR"]
  NAN -- No --> BUF["Insert into\n7-sample\nring buffer"]
  BUF --> FULL{"Buffer\nfilled?"}
  FULL -- No --> PASS["current_temp\n= raw_temp\n(passthrough)"]
  FULL -- Yes --> MED["Median of\n7 samples"]
  MED --> EMA["EMA\nα = 0.1"]
  EMA --> OUT["current_temp"]

  style HALT fill:#f8d7da,stroke:#dc3545
  style OUT fill:#d4edda,stroke:#28a745
  style MED fill:#e2d5f1,stroke:#7c4dff
  style EMA fill:#d1ecf1,stroke:#17a2b8
Figure 3 — Two-stage temperature filtering (median → EMA)
EMA:  Tfiltered = α · Tmedian + (1 − α) · Tprev   where α = 0.1

The median filter uses a simple bubble sort on a copy of the 7-element window, returning the middle value. This is effective at rejecting single-sample spikes common with thermocouple interfaces.

4. Triac Phase-Angle Power Delivery

Power is modulated via phase-angle control: the triac fires at a variable delay after each AC zero-crossing. A pre-computed lookup table maps desired power (0–100%) to a firing delay in microseconds. The mapping is based on the true-RMS relationship between firing angle and delivered power, not a simple linear approximation.

flowchart LR
  PID_OUT["pid_output\n(0.0 – 1.0)"] --> IDX["Map to\nindex 0–100"]
  IDX --> LUT["power_to_delay_lut\n(pre-computed\nat boot)"]
  LUT --> DELAY["delay_µs =\n230 + LUT value"]

  ZC["AC Zero-Cross\nInterrupt\n(FALLING)"] --> CHECK{"pid_output\n> 0.02?"}
  CHECK -- No --> SKIP["No fire\nthis half-cycle"]
  CHECK -- Yes --> TIMER["Start hw_timer\nfor delay_µs"]
  TIMER --> FIRE["Triac ISR:\nGPIO HIGH 150µs\nthen LOW"]

  style ZC fill:#fff3cd,stroke:#ffc107
  style FIRE fill:#f8d7da,stroke:#dc3545
  style LUT fill:#e2d5f1,stroke:#7c4dff
Figure 4 — Zero-cross detection → phase-angle triac firing
LUT Build (bisection):  P(α) = 1 − α/π + sin(2α) / 2π
For each target power 0–100%, solve for α via 15 iterations of bisection, then: delay = (α / π) × 9500 µs

The 230 µs offset accounts for zero-cross detector latency. The 150 µs gate pulse is sufficient to latch the triac for the remainder of the half-cycle.

5. Auto-Tuner State Machine

The auto-tuner identifies the plant as an integrating process (no self-regulation — the heater has no natural equilibrium). It applies a step test and measures the dead time and maximum heating slope to build a first-order-plus-dead-time (FOPDT) integrating model. The procedure runs in three states.

stateDiagram-v2
  [*] --> Idle: System boots\nor tune completes

  Idle --> State1_Wait: User sends\n"TUNE" command

  State1_Wait: State 1 — Wait for Steady State
  State1_Wait --> State1_Wait: |ΔT| > 0.15°C → reset timer
  State1_Wait --> State2_DeadTime: 60 s stable\n(ΔT ≤ 0.15°C)

  State2_DeadTime: State 2 — Find Dead Time (θ)
  State2_DeadTime --> State3_Slope: T rises ≥ 0.5°C\nabove start

  State3_Slope: State 3 — Measure Max Slope
  State3_Slope --> Idle: After 60 s observation\n→ Compute K', θ → SIMC

  note right of State1_Wait
    pid_output = 0
    Heater OFF
    Recording baseline
  end note

  note right of State2_DeadTime
    pid_output = tune_step_power (30%)
    Step applied, waiting for
    first measurable response
  end note

  note right of State3_Slope
    Step power continues
    ΔT over 60 s → S_max
    K' = S_max / step_power
  end note
Figure 5 — Auto-tuner state machine (3 states)

State Transitions in Detail

StateEntry ConditionActionExit Condition
1 — Stabilize "TUNE" command received Heater OFF. Monitor temperature drift. Reset 60 s timer any time |ΔT| > 0.15°C 60 s pass with temp within ±0.15°C
2 — Dead Time Steady state confirmed Apply step power (30%). Record start temp and time. Watch for first response. Temperature rises 0.5°C above start
3 — Slope Dead time measured Continue step power. Measure total ΔT over next 60 s. Compute Smax = ΔT / 60 60 s elapsed → compute plant model → SIMC → return to Idle
Plant identification:
θ = time from step application to first 0.5°C rise
Smax = ΔT / 60   (°C/s, slope over 60 s after dead time)
K' = Smax / ustep   (integrating gain, °C/s per unit power)
τ = 0   (integrating process — no self-regulation)

6. SIMC Tuning Rules

Once the plant model is identified, the Skogestad IMC (SIMC) rules compute PI controller gains. The system supports both integrating-process and FOPDT models, though the auto-tuner always identifies an integrating process (τ = 0). A user-adjustable λ (lambda) parameter sets the desired closed-loop speed.

flowchart TD
  PLANT["Plant Parameters\nK', θ, τ"] --> LAMBDA["Compute actual λ\nλ_actual = θ × (λ_setting × 0.33)"]
  LAMBDA --> CHECK{"τ == 0 ?"}

  CHECK -- "Yes (Integrating)" --> INT_KC["Kc = 1 / (K' × (λ_actual + θ))"]
  INT_KC --> INT_TI["Ti = 4 × (λ_actual + θ)"]

  CHECK -- "No (FOPDT)" --> FOPDT_KC["Kc = (1/K) × (τ / (λ_actual + θ))"]
  FOPDT_KC --> FOPDT_TI["Ti = min(τ, 4×(λ_actual + θ))"]

  INT_TI --> SAVE["Save Kc, Ti, K', θ, τ\nto NVS (Preferences)"]
  FOPDT_TI --> SAVE

  style INT_KC fill:#d4edda,stroke:#28a745
  style INT_TI fill:#d4edda,stroke:#28a745
  style FOPDT_KC fill:#d1ecf1,stroke:#17a2b8
  style FOPDT_TI fill:#d1ecf1,stroke:#17a2b8
  style SAVE fill:#fff3cd,stroke:#ffc107
Figure 6 — SIMC tuning rule selection based on plant model type
Lambda scaling: The code applies a 0.33× multiplier to λ before using it. So the user-facing λ range of 1–5 actually maps to an effective closed-loop time constant of 0.33θ to 1.65θ. Lower λ → faster but more aggressive response.
Integrating SIMC (τ = 0):
Kc = 1 / [ K' × (λactual + θ) ]
Ti = 4 × (λactual + θ)

FOPDT SIMC (τ > 0):
Kc = (1/K) × τ / (λactual + θ)
Ti = min( τ, 4×(λactual + θ) )

7. MPC Prediction Engine

The controller uses a model-predictive approach to estimate where the temperature will be after the dead time θ, accounting for power already injected but not yet visible in the sensor reading. This predicted temperature drives the proportional term and (conditionally) the integral term.

flowchart TD
  subgraph Inputs
    CT["current_temp"]
    CS["current_slope\n(°C/s over 10 s)"]
    PH["power_history[]\n(300 s buffer)"]
    PP["plant_K, plant_theta"]
    NCR["natural_cooling_rate\n(self-learned)"]
  end

  subgraph Disturbance["Disturbance Filter"]
    CS --> NEGCHECK{"slope < 0 ?"}
    NEGCHECK -- No --> PASS_S["momentum_slope\n= current_slope"]
    NEGCHECK -- Yes --> NATCHECK{"|slope| ≤ 1.1 ×\ncooling_rate?"}
    NATCHECK -- "Yes (natural)" --> ZERO["momentum_slope = 0\n(ignore — it's expected)"]
    NATCHECK -- "No (crash!)" --> EXCESS["momentum_slope =\nslope + cooling_rate\n(keep only the excess)"]
  end

  subgraph Prediction["Forward Projection"]
    PH --> UOLD["u_old = power\nθ seconds ago"]
    UOLD --> PSUM["power_sum = Σ of\n(power[i] − u_old)\nover θ-second window"]
  end

  CT --> PRED
  PASS_S --> PRED
  ZERO --> PRED
  EXCESS --> PRED
  PSUM --> PRED

  PRED["predicted_temp =\ncurrent_temp\n+ momentum_slope × θ\n+ K' × power_sum"]

  PRED --> PID_USE["→ Used by\nProportional Term\n& Conditional Integral"]

  style PRED fill:#d4edda,stroke:#28a745
  style ZERO fill:#d1ecf1,stroke:#17a2b8
  style EXCESS fill:#f8d7da,stroke:#dc3545
  style PID_USE fill:#fff3cd,stroke:#ffc107
Figure 7 — MPC prediction: combine momentum, injected energy, and disturbance filtering
Prediction equation:
T̂ = Tcurrent + slopeadjusted × θ + K' × Σ(u[i] − uold)

The power_sum term captures energy that has been injected into the system during the dead time window but has not yet reached the sensor. This forward-looking estimate prevents the controller from over-driving during ramp-up, which is the primary cause of overshoot in pure-PI systems controlling thermal processes with significant transport delay.

8. Self-Learning Cooling Rate

The system continuously learns the natural cooling rate of the thermal mass. This is critical for the disturbance rejection logic: the controller needs to distinguish between normal heat loss and genuine disturbances like a door opening or cold fluid injection. When the drop is natural, the momentum projection is zeroed — the proportional term does not react. Instead, the steady correction for normal heat loss is handled entirely by the conditional integral operating in REAL mode. When the drop is anomalous, the excess beyond the learned rate is projected forward, and the proportional term engages.

flowchart TD
  CHECK1{"pid_output\n== 0 ?"}
  CHECK1 -- No --> SKIP["Don't update\n(heater is on)"]
  CHECK1 -- Yes --> CHECK2{"current_slope\n< 0 ?"}
  CHECK2 -- No --> SKIP2["Don't update\n(temp not dropping)"]
  CHECK2 -- Yes --> CHECK3{"|slope| < K' × 0.5 ?"}
  CHECK3 -- No --> SKIP3["Don't update\n(impossibly fast —\nsensor glitch?)"]
  CHECK3 -- Yes --> LEARN["Update cooling rate:\nrate = 0.1 × |slope|\n+ 0.9 × rate_prev"]

  LEARN --> USE["Used by MPC\nto classify\ndropping temp as\nnatural vs anomalous"]

  style LEARN fill:#d4edda,stroke:#28a745
  style USE fill:#d1ecf1,stroke:#17a2b8
  style SKIP fill:#f0f0f0,stroke:#999
  style SKIP2 fill:#f0f0f0,stroke:#999
  style SKIP3 fill:#f8d7da,stroke:#dc3545
Figure 8 — Self-learning natural cooling rate (updated every 1 s)
Learning rule (EMA):  rcool = 0.1 × |slope| + 0.9 × rcool,prev
Guard:  Only when heater OFF, temp falling, and |slope| < K' × 0.5

The 0.5 × K' guard rejects sensor errors or sudden physical disturbances from corrupting the learned rate. The slow EMA (α = 0.1) means the rate adapts over minutes, not seconds, giving a stable baseline.

9. Disturbance Rejection Logic

Before feeding the temperature slope into the predictor, the system classifies any negative slope as either natural cooling or a genuine disturbance. When classified as natural, the momentum slope is zeroed so the proportional term does not project a crash into the future — the gradual heat loss correction is left to the conditional integral in REAL mode, which is the appropriate mechanism for slow, expected drift. When classified as anomalous, only the excess beyond the learned natural rate is projected forward, engaging the proportional term proportionally to the severity of the disturbance.

flowchart TD
  S["current_slope"] --> NEG{"slope < 0 ?"}
  NEG -- No --> POS["momentum_slope\n= current_slope\n(positive or zero —\nno filtering needed)"]

  NEG -- Yes --> MAG{"|slope| ≤ 1.1 ×\nnatural_cooling_rate ?"}
  MAG -- Yes --> NAT["momentum_slope = 0\n\nVerdict: NATURAL COOLING\nP-term does not react.\nCorrection left to the\nconditional integral (REAL mode)."]

  MAG -- No --> CRASH["momentum_slope =\nslope + cooling_rate\n\nVerdict: ANOMALOUS DROP\nOnly the excess beyond\nnatural cooling is projected\ninto the future."]

  POS --> PRED["→ predicted_temp"]
  NAT --> PRED
  CRASH --> PRED

  style NAT fill:#d4edda,stroke:#28a745
  style CRASH fill:#f8d7da,stroke:#dc3545
  style POS fill:#d1ecf1,stroke:#17a2b8
Figure 9 — Disturbance classification with 10% noise margin
The 1.1× margin (10% above learned cooling rate) prevents sensor noise from falsely triggering disturbance rejection. Only drops that meaningfully exceed the learned natural rate trigger corrective prediction.

10. Conditional Integral Switching

This is the most nuanced part of the controller. The integral term's error source switches between the real temperature and the predicted temperature depending on the system's operating mode. This prevents integral windup during ramp-up while still eliminating steady-state error once near setpoint.

flowchart TD
  START["Evaluate conditions"] --> C1{"|slope| ≤ flat_threshold\nAND error ≤ 10% SP\nAND error > 1.5% SP ?"}

  C1 -- Yes --> MODE_A1["MODE A: Near Steady-State\ni_error = SP − T_real\nflatFlag = REAL\n\nSlope is flat, close to target.\nUse real temp to kill\nsteady-state offset."]

  C1 -- No --> C2{"error ≤ 1.5% SP\nAND T < SP ?"}
  C2 -- Yes --> MODE_A2["MODE A: Very Close\ni_error = SP − T_real\nflatFlag = REAL\n\nWithin 1.5% of target.\nAlways use real temp\nfor fine correction."]

  C2 -- No --> C3{"T ≥ SP AND\nT_predicted ≥ 90% SP ?"}
  C3 -- Yes --> MODE_A3["MODE A: Above Setpoint\ni_error = SP − T_real\nflatFlag = REAL\n\nTemp at or above target\nand predictor agrees.\nUse real temp."]

  C3 -- No --> C4{"T ≥ SP AND\nT_predicted < 90% SP ?"}
  C4 -- Yes --> MODE_B1["MODE B: Overshoot Warning\ni_error = SP − T_predicted\nflatFlag = PRED\n\nTemp is above SP but\npredictor sees a crash\ncoming. Use predicted\nto prepare."]

  C4 -- No --> MODE_B2["MODE B: Active Ramp\ni_error = SP − T_predicted\nflatFlag = PRED\n\nActively heating up.\nUse predicted temp to\nprevent integral preloading."]

  MODE_A1 --> INT["error_integral +=\ni_error × dt"]
  MODE_A2 --> INT
  MODE_A3 --> INT
  MODE_B1 --> INT
  MODE_B2 --> INT

  INT --> CLAMP["Clamp integral:\nmax = Ti / Kc\nmin = 0"]

  style MODE_A1 fill:#d4edda,stroke:#28a745
  style MODE_A2 fill:#d4edda,stroke:#28a745
  style MODE_A3 fill:#d4edda,stroke:#28a745
  style MODE_B1 fill:#fff3cd,stroke:#ffc107
  style MODE_B2 fill:#d1ecf1,stroke:#17a2b8
Figure 10 — Conditional integral mode selection (5 branches)
Flat threshold:  flat_threshold = K' × 0.1 × 0.3 = K' × 0.03
Integral clamp:  0 ≤ integral ≤ Ti / Kc

Why Two Modes?

During active ramp-up (Mode B), the real temperature lags far behind the setpoint, which would cause massive integral accumulation if used directly. By feeding the predicted temperature error into the integral, the system accounts for energy already in the pipeline. Once near steady-state (Mode A), the real temperature is used so the integral can eliminate the last fraction of a degree of offset — something the predictor alone cannot resolve due to model inaccuracies.

The flatThresholdFlag shown in telemetry as [REAL] or [PRED] indicates which mode is active, giving the operator real-time visibility into the integral's behavior.

11. Full PID Computation & Anti-Windup

The complete control computation combines the MPC prediction, proportional action, conditional integral, output clamping, and anti-windup. There is no derivative term — the predictive model serves that role.

flowchart TD
  GUARD{"plant_K == 0 ?"}
  GUARD -- Yes --> OFF["pid_output = 0\n(no plant model yet)"]
  GUARD -- No --> PREDICT["Compute predicted_temp\n(see §7 MPC Predictor)"]

  PREDICT --> PTERM["P-term = Kc × (SP − T_predicted)"]
  PREDICT --> ISELECT["Select i_error source\n(see §10 Conditional Integral)"]
  ISELECT --> ITERM["I-term = Kc × (1/Ti) × ∫ i_error · dt"]

  PTERM --> SUM["u = P-term + I-term"]
  ITERM --> SUM

  SUM --> UCHECK{"u ≥ 1.0 ?"}
  UCHECK -- Yes --> SAT_HI["u = 1.0\nAnti-windup:\nundo last integral\nstep if i_error > 0"]
  UCHECK -- No --> LCHECK{"u ≤ 0.0 ?"}
  LCHECK -- Yes --> SAT_LO["u = 0.0"]
  LCHECK -- No --> OK["u = u (no clamping)"]

  SAT_HI --> FLOOR
  SAT_LO --> FLOOR
  OK --> FLOOR

  FLOOR["Floor: integral ≥ 0\n(no negative windup)"]
  FLOOR --> OUTPUT["pid_output = u"]

  style PREDICT fill:#e2d5f1,stroke:#7c4dff
  style PTERM fill:#d1ecf1,stroke:#17a2b8
  style ITERM fill:#d4edda,stroke:#28a745
  style SAT_HI fill:#f8d7da,stroke:#dc3545
  style OUTPUT fill:#fff3cd,stroke:#ffc107
Figure 11 — Full PI computation with MPC prediction and anti-windup
Control law:  u = Kc × (SP − T̂) + Kc × (1/Ti) × ∫ ei · dt

Anti-windup (high):  If u ≥ 1 and ei > 0, undo: integral −= ei × dt
Anti-windup (low):  integral is floored at 0 (heater cannot cool)
Key design choice: The proportional term always uses the predicted temperature, not the real temperature. This gives early anticipation of overshoot. The integral switches between real and predicted based on operating regime (§10). There is no derivative term — the MPC momentum projection serves the same anticipatory function with better noise immunity.

12. Parameter Reference

Timing Constants

ParameterValueDescription
Control loop period250 msPID/filter execution rate (dt = 0.25 s)
MPC ledger period1000 msSlope and power history update rate
Telemetry period500 msSerial output rate
Median window7 samplesCovers 1.75 s of readings
Slope window10 samples10 s rolling window for °C/s computation
Power history buffer300 samples5 minutes of power at 1 s resolution

Filter Parameters

ParameterValueDescription
EMA α (temperature)0.1Post-median smoothing factor (slow, heavy filtering)
EMA α (cooling rate)0.1Learning rate for natural cooling model
Cooling rate guardK' × 0.5Max credible cooling slope
Disturbance margin1.1×10% tolerance above learned cooling rate

Auto-Tuner Parameters

ParameterValueDescription
Stabilization time60 sMinimum quiet period before step
Stability threshold±0.15°CMax drift during stabilization
Step power30%Open-loop excitation level
Dead time trigger+0.5°CTemperature rise that marks end of dead time
Slope observation60 sDuration of slope measurement after θ

SIMC / Controller Parameters (User-Adjustable)

ParameterRangeDefaultDescription
Setpoint20–200°C50°CTarget temperature
Lambda (λ)1.0–5.03.0Closed-loop aggressiveness (lower = faster)
KcAuto-tuned1.0Proportional gain
TiAuto-tuned100.0Integral time (seconds)

Hardware

ParameterValueDescription
Triac gate pulse150 µsDuration of gate drive pulse
ZCD offset230 µsCompensation for zero-cross detector latency
Half-cycle period~9500 µsAssumed 50 Hz mains (10 ms half-cycle minus margins)
Minimum output2%Below this, triac is not fired (noise floor)

NVS Persistent Storage

KeyTypeDescription
setpointFloatTarget temperature
lambdaFloatClosed-loop tuning aggressiveness
KFloatPlant integrating gain
tauFloatPlant time constant (0 for integrating)
thetaFloatPlant dead time
KcFloatController proportional gain
TiFloatController integral time