package org.cowboycoders.turbotrainers.bushido.brake;
import org.cowboycoders.pid.GainController;
import org.cowboycoders.pid.GainParameters;
import org.cowboycoders.pid.OutputControlParameters;
import org.cowboycoders.pid.OutputController;
import org.cowboycoders.pid.PidController;
import org.cowboycoders.pid.PidParameterController;
import org.cowboycoders.pid.ProcessVariableProvider;
import org.cowboycoders.turbotrainers.Mode;
import org.cowboycoders.turbotrainers.PowerModel;
import org.cowboycoders.turbotrainers.PowerModelManipulator;
import org.fluxoid.utils.Conversions;
import org.fluxoid.utils.FixedPeriodUpdater;
import org.fluxoid.utils.SimpleCsvLogger;
import org.fluxoid.utils.SlopeTimeAverager;
import org.fluxoid.utils.UpdateCallback;
import java.io.File;
public class SpeedPidBrakeController extends AbstractController {
private static final Mode SUPPORTED_MODE = Mode.TARGET_SPEED;
private static final int POWER_MODEL_UPDATE_PERIOD_MS = 100; // milli-seconds
private static final double PID_DERIVATIVE_GAIN = 2;
private static final double PID_INTEGRAL_GAIN = 0.5;
private static final double PID_PROPORTIONAL_GAIN = 0.5;
private static final int STARTING_RESISTANCE = 0; // %
// if acceleration is +/- this value we assume we have reach a steady state.
// This is when we try and sync actual wheel speed to prdicted wheel speed
private static final Double ACTUAL_SPEED_STEADY_STATE_THRESHOLD = 0.28; // m/s^2
private static final int MIN_SLOPE_SAMPLES = 5;
// Don't try and simulate speeds below this due to inaccuracies in readings at low speed
// Rolling resistance on turbo becomes excessively large such that lowest resistance setting is
// unrealistically hard
private static final double LOW_SPEED_LIMIT = 1.8; // m/s ~ 4 mph
private PowerModel powerModel = new PowerModel();
private volatile double predictedSpeed = -1; // metres/s
private volatile double actualSpeed = -1; // metres/s
private SlopeTimeAverager predictedSpeedSlopeAverager = new SlopeTimeAverager();
private SlopeTimeAverager actualSpeedSlopeAverager = new SlopeTimeAverager();
// public SpeedPidBrakeController(BrakeModel bushidoModel) {
// this.bushidoDataModel = bushidoModel;
// bushidoDataModel.setResistance(getEstimatedResistance());
// actualSpeedSlopeAverager.setThreshold(ACTUAL_SPEED_STEADY_STATE_THRESHOLD,
// -ACTUAL_SPEED_STEADY_STATE_THRESHOLD);
// }
private UpdateCallback powerModelUpdateCallback = new UpdateCallback() {
@Override
public void onUpdate(Object newValue) {
powerModel.setGradientAsPercentage(getDataModel().getSlope());
double predictedSpeed = powerModel.updatePower((Double) newValue);
//predictedSpeed = Conversions.METRES_PER_SECOND_TO_KM_PER_HOUR * predictedSpeed;
setPredictedSpeed(predictedSpeed);
}
};
private FixedPeriodUpdater powerModelUpdater = new FixedPeriodUpdater(new Double(0),
powerModelUpdateCallback, POWER_MODEL_UPDATE_PERIOD_MS);
private ProcessVariableProvider actualSpeedProvider = new ProcessVariableProvider() {
@Override
public double getProcessVariable() {
return actualSpeed;
}
};
private OutputController resistanceOutputController = new OutputController() {
@Override
public void setOutput(double resistance) {
BrakeModel bushidoDataModel = getDataModel();
// enforce estimated resistance for first update
if (needsSync) {
bushidoDataModel.setResistance(getEstimatedResistance());
needsSync = false;
return;
}
logToCsv(ABSOLUTE_RESISTANCE_HEADING, bushidoDataModel.getAbsoluteResistance());
bushidoDataModel.setResistance(resistance + getEstimatedResistance());
}
/**
* Centred on getEstimatedResistance()
*/
@Override
public double getMaxOutput() {
return BrakeModel.RESISTANCE_MAX - getEstimatedResistance();
}
/**
* Centred on getEstimatedResistance()
*/
@Override
public double getMinOutput() {
return BrakeModel.RESISTANCE_MIN - getEstimatedResistance();
}
};
private GainController resistanceGainController = new GainController() {
@Override
public GainParameters getGain(OutputControlParameters parameters) {
GainParameters defaultParameters = new GainParameters(PID_PROPORTIONAL_GAIN,
PID_INTEGRAL_GAIN, PID_DERIVATIVE_GAIN);
return defaultParameters;
}
};
private PidController resistancePidController = new PidController(actualSpeedProvider,
resistanceOutputController, resistanceGainController);
private boolean needsSync = true;
@Override
public double onSpeedChange(double speed) {
BrakeModel bushidoDataModel = getDataModel();
setActualSpeed(speed * Conversions.KM_PER_HOUR_TO_METRES_PER_SECOND);
double predictedSpeed = getPredictedSpeed();
// test for stationary -> non-stationary transition
// speed is on average increasing and is below our threshold
if (predictedSpeed <= LOW_SPEED_LIMIT
&& predictedSpeedSlopeAverager.getAverage() > 0) {
// only sync if actualSpeed is higher : actualSpeed will be lower than predicted if slowing
// down
// as real speed drops faster than model predicts
//if (actualSpeed > predictedSpeed) {
// resync
needsSync = true;
resistancePidController.reset();
// use high resistance to force sync at lower speed
bushidoDataModel.setResistance(getEstimatedResistance());
//}
}
// don't mess with brake resistance until reached steady state
if (!needsSync || actualSpeedSlopeAverager.getNumberOfSamples() >= MIN_SLOPE_SAMPLES &&
actualSpeedSlopeAverager.isWithinThreshold()) {
if (needsSync) {
double spd = actualSpeed;
powerModel.setVelocity(spd);
setPredictedSpeed(spd);
}
resistancePidController.adjustSetpoint(getPredictedSpeed());
}
return speed;
//actualSpeedUpdated = true;
}
/**
* Estimated resistance for given gradient
*/
protected double getEstimatedResistance() {
return STARTING_RESISTANCE + powerModel.getGradientAsPercentage();
}
@Override
public double onPowerChange(double power) {
logToCsv(POWER_HEADING, power);
powerModelUpdater.update(new Double(power));
// only starts once
powerModelUpdater.start();
return power;
}
protected double getPredictedSpeed() {
return predictedSpeed;
}
protected void setPredictedSpeed(double newValue) {
logToCsv(VIRTUAL_SPEED_HEADING, newValue);
SpeedPidBrakeController.this.predictedSpeed = newValue;
// gradient averager
predictedSpeedSlopeAverager.add(predictedSpeed);
}
protected void setActualSpeed(double newValue) {
logToCsv(ACTUAL_SPEED_HEADING, newValue);
SpeedPidBrakeController.this.actualSpeed = newValue;
actualSpeedSlopeAverager.add(actualSpeed);
}
public PowerModelManipulator getPowerModelManipulator() {
return powerModel;
}
public PidParameterController getPidParameterController() {
return resistancePidController;
}
@Override
public Mode getMode() {
return SUPPORTED_MODE;
}
@Override
public void onStart() {
BrakeModel bushidoDataModel = getDataModel();
bushidoDataModel.setResistance(getEstimatedResistance());
actualSpeedSlopeAverager.setThreshold(ACTUAL_SPEED_STEADY_STATE_THRESHOLD,
-ACTUAL_SPEED_STEADY_STATE_THRESHOLD);
}
@Override
protected SimpleCsvLogger getCsvLogger(File file) {
SimpleCsvLogger logger = new SimpleCsvLogger(file, ACTUAL_SPEED_HEADING,
VIRTUAL_SPEED_HEADING, POWER_HEADING, ABSOLUTE_RESISTANCE_HEADING);
logger.addTime(true);
logger.append(true);
return logger;
}
@Override
public void onStop() {
powerModelUpdater.stop();
}
}