using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace HC_APTBS.Services.Impl
{
///
/// PID controller for smooth RPM ramping on the test bench motor.
/// Runs a background loop that reads actual RPM, computes the PID output,
/// and writes the corresponding motor voltage via a callback.
///
///
/// All values are normalized to 0–1.0 internally. The output is scaled back
/// to the configured voltage range before calling the write delegate.
///
///
public sealed class BenchPidController : IDisposable
{
// ── Gains ────────────────────────────────────────────────────────────────
private readonly double _kp;
private readonly double _ki;
private readonly double _kd;
// ── Ranges ───────────────────────────────────────────────────────────────
/// Maximum process variable (RPM).
private readonly double _pvMax;
/// Maximum output variable (voltage).
private readonly double _outMax;
// ── Delegates ────────────────────────────────────────────────────────────
private readonly Func _readActualRpm;
private readonly Func _readTargetRpm;
private readonly Action _sendVoltage;
// ── Loop state ───────────────────────────────────────────────────────────
private readonly int _intervalMs;
private CancellationTokenSource? _cts;
private Task? _loopTask;
// ── PID state ────────────────────────────────────────────────────────────
private double _errSum;
private double _lastPv;
private Stopwatch _sw = new();
/// True when the PID loop is actively running.
public bool IsRunning { get; private set; }
// ── Constructor ──────────────────────────────────────────────────────────
///
/// Creates a new PID controller for bench RPM control.
///
/// Proportional gain.
/// Integral gain.
/// Derivative gain.
/// Loop interval in milliseconds.
/// Maximum process variable (RPM). Minimum is always 0.
/// Maximum output (voltage). Minimum is always 0.
/// Delegate to read the current bench RPM.
/// Delegate to read the desired RPM setpoint.
/// Delegate to write the output voltage to the CAN bus.
public BenchPidController(
double kp, double ki, double kd, int intervalMs,
double pvMax, double outMax,
Func readActualRpm,
Func readTargetRpm,
Action sendVoltage)
{
_kp = kp;
_ki = ki;
_kd = kd;
_intervalMs = Math.Max(1, intervalMs);
_pvMax = pvMax;
_outMax = outMax;
_readActualRpm = readActualRpm;
_readTargetRpm = readTargetRpm;
_sendVoltage = sendVoltage;
}
// ── Public API ───────────────────────────────────────────────────────────
///
/// Starts the PID loop with bumpless transfer from the given initial conditions.
/// If already running, stops first before restarting.
///
/// Current motor voltage (for bumpless startup).
/// Current actual RPM (for bumpless startup).
public void Start(double initialVoltage, double initialRpm)
{
if (IsRunning) Stop();
// Bumpless startup: pre-initialize integral sum so the first output
// matches the current voltage, avoiding a step response.
double normalizedOutput = Scale(initialVoltage, 0, _outMax, 0, 1.0);
_errSum = _ki != 0 ? normalizedOutput / _ki : 0;
_lastPv = Scale(Clamp(initialRpm, 0, _pvMax), 0, _pvMax, 0, 1.0);
_sw = Stopwatch.StartNew();
IsRunning = true;
_cts = new CancellationTokenSource();
var ct = _cts.Token;
_loopTask = Task.Run(async () =>
{
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_intervalMs));
try
{
while (IsRunning && await timer.WaitForNextTickAsync(ct))
{
Compute();
}
}
catch (OperationCanceledException) { }
}, ct);
}
///
/// Stops the PID loop and sends 0 V to the motor.
///
public void Stop()
{
if (!IsRunning) return;
IsRunning = false;
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
_sendVoltage(0);
}
// ── PID core ─────────────────────────────────────────────────────────────
private void Compute()
{
double pv = _readActualRpm();
double sp = _readTargetRpm();
// Clamp and normalize to 0–1.0
pv = Scale(Clamp(pv, 0, _pvMax), 0, _pvMax, 0, 1.0);
sp = Scale(Clamp(sp, 0, _pvMax), 0, _pvMax, 0, 1.0);
double err = sp - pv;
// Time delta in seconds
double dt = _sw.Elapsed.TotalSeconds;
_sw.Restart();
if (dt <= 0) dt = _intervalMs / 1000.0;
// P term
double pTerm = _kp * err;
// I term (with anti-windup: only integrate when PV is in-range)
double partialSum = _errSum;
double iTerm = 0;
if (pv >= 0 && pv <= 1.0)
{
partialSum = _errSum + dt * err;
iTerm = _ki * partialSum;
}
// D term (derivative-on-measurement, not on error)
double dTerm = 0;
if (dt > 0)
dTerm = _kd * (pv - _lastPv) / dt;
// Combine, clamp, scale to output voltage range
double output = Clamp(pTerm + iTerm + dTerm, 0, 1.0);
double voltage = Scale(output, 0, 1.0, 0, _outMax);
_sendVoltage(voltage);
// Update state
_errSum = partialSum;
_lastPv = pv;
}
// ── Helpers ──────────────────────────────────────────────────────────────
private static double Clamp(double value, double min, double max)
=> value > max ? max : value < min ? min : value;
private static double Scale(double value, double fromMin, double fromMax, double toMin, double toMax)
{
if (Math.Abs(fromMax - fromMin) < 1e-12) return toMin;
double pct = (value - fromMin) / (fromMax - fromMin);
return toMin + pct * (toMax - toMin);
}
///
public void Dispose()
{
IsRunning = false;
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
}
}