package org.cowboycoders.turbotrainers.bushido.brake; import org.cowboycoders.turbotrainers.Mode; import org.cowboycoders.turbotrainers.PowerModel; import org.fluxoid.utils.Conversions; import org.fluxoid.utils.FixedPeriodUpdater; import org.fluxoid.utils.SimpleCsvLogger; import org.fluxoid.utils.UpdateCallback; import java.io.File; /** * @author www.cowboycoders.org * * A brake resistance controller which maps a speed, brake resistance * and power. * * The mapping is implemented using a surface fit to calibration data * generated from measuring power as a function of actual wheel speed * and brake resistance. */ public class SpeedResistancePowerMapper extends AbstractController { private static final Mode SUPPORTED_MODE = Mode.TARGET_SLOPE; // Period at which the virtual speed will be updated (ms) private static final int POWER_MODEL_UPDATE_PERIOD_MS = 100; // Brake resistance upon power up of the brake private static final int INITIAL_BRAKE_RESISTANCE = 100; // Below this limit power-speed characteristics // don' change appreciably for the brake private static final int MINIMUM_BRAKE_RESISTANCE = 100; // It becomes very hard to pedal around ~700 // and the tyre starts slipping even with high tension in the brake. private static final int MAXIMUM_BRAKE_RESISTANCE = 1000; // Minimum simulation speed. Used in conjunction with minimum simulation power. // Below this pedaling speed we drop to minimum resistance // Currently the surface fit doesn't map well below about this value for lowe powers and // the brake also switches off at some speed > 0 private static final int MINIMUM_SIMULATION_SPEED = 10; // The surface fit works at higher power for slow speeds. This is private static final int MINIMUM_SIMULATION_SPEED_OVERRIDE_POWER = 250; // 16 coefficients from polynomial surface fit mapping speed and power to // brake resistance private static final double[] SURF_COEFFS = { 7.62670239e+02, 8.36403498e+00, -1.93426172e-02, 1.19616898e-05, -1.26187201e+02, -3.08275665e-02, 5.64269493e-04, -4.42943697e-07, 3.74310573e+00, -5.51901879e-03, -2.52126663e-06, 4.64788059e-09, -4.24931219e-02, 1.11216056e-04, -8.58263602e-08, 1.58166813e-11 }; // Model to estimate speed from power private final PowerModel powerModel = new PowerModel(); /** * Calculates a brake resistance from the current virtual speed. * * The virtual speed is calculated using a power model taking into account * rider and bike weight (total weight), the current slope, etc. * * The polynomial fit has been generated from data based on the relationship * between the brake resistance and the virtual speed on the head unit. * * @return - estimated brake resistance */ public final double getBrakeResistanceFromSurfaceFit() { final BrakeModel bushidoDataModel = getDataModel(); // We have a virtual speed calculated from the actual power being // produced and we // want to set the brake resistance so that it is appropriate for these // two quantities. final double actualPower = bushidoDataModel.getPower(); final double virtualSpeed = bushidoDataModel.getVirtualSpeed(); // If the speed is too low, just send back the minimum resistance. // Only do this if the power is also low if ((virtualSpeed < MINIMUM_SIMULATION_SPEED) && (actualPower < MINIMUM_SIMULATION_SPEED_OVERRIDE_POWER)) { return MINIMUM_BRAKE_RESISTANCE; } // Order of the polynomial fit final int order = (int) Math.sqrt(SURF_COEFFS.length) - 1; // Calculate the brake resistance from virtual speed and actual power // using the // surface fit double brakeResistance = 0.0; int k = 0; for (int i = 0; i < order + 1; ++i) { for (int j = 0; j < order + 1; ++j) { brakeResistance += SURF_COEFFS[k++] * Math.pow(virtualSpeed, i) * Math.pow(actualPower, j); } } return brakeResistance; } /** * Periodically updates the virtual speed which is required for estimating * the brake resistance * * This is called frequently for two reasons: * * 1. We want to ensure that the most recent resistance estimate is always * available to be sent to the brake. 2. The power model requires updating * in sub-second intervals for the integration technique to work with * reasonable accuracy. */ private final UpdateCallback updateVirtualSpeed = new UpdateCallback() { @Override public void onUpdate(final Object newValue) { final BrakeModel bushidoDataModel = getDataModel(); powerModel.setGradientAsPercentage(bushidoDataModel.getSlope()); final double virtualSpeed = powerModel.updatePower((Double) newValue) * Conversions.METRES_PER_SECOND_TO_KM_PER_HOUR; bushidoDataModel.setVirtualSpeed(virtualSpeed); // Update the brake resistance from the current virtual speed bushidoDataModel .setAbsoluteResistance((int) getBrakeResistanceFromSurfaceFit()); logToCsv(VIRTUAL_SPEED_HEADING, virtualSpeed); logToCsv(ABSOLUTE_RESISTANCE_HEADING, bushidoDataModel.getAbsoluteResistance()); } }; private final FixedPeriodUpdater powerModelUpdater = new FixedPeriodUpdater( new Double(0), updateVirtualSpeed, POWER_MODEL_UPDATE_PERIOD_MS); @Override public final double onPowerChange(final double power) { // Update the power with which the power model is updated with powerModelUpdater.update(new Double(power)); // Only starts once powerModelUpdater.start(); return power; } @Override public final double onSpeedChange(final double speed) { logToCsv(ACTUAL_SPEED_HEADING, speed); return getDataModel().getVirtualSpeed(); } @Override public Mode getMode() { return SUPPORTED_MODE; } @Override public final void onStart() { getDataModel().setResistanceBounds(MINIMUM_BRAKE_RESISTANCE, MAXIMUM_BRAKE_RESISTANCE); getDataModel().setResistance(INITIAL_BRAKE_RESISTANCE); } @Override public final void onStop() { powerModelUpdater.stop(); } @Override protected SimpleCsvLogger getCsvLogger(File file) { SimpleCsvLogger logger = new SimpleCsvLogger(file, ACTUAL_SPEED_HEADING, VIRTUAL_SPEED_HEADING, ABSOLUTE_RESISTANCE_HEADING); logger.addTime(true); logger.append(true); return logger; } }