/* * Copyright (c) 2013, Will Szumski * Copyright (c) 2013, Doug Szumski * * This file is part of Cyclismo. * * Cyclismo is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Cyclismo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Cyclismo. If not, see <http://www.gnu.org/licenses/>. */ package org.cowboycoders.turbotrainers.bushido.brake; import org.cowboycoders.ant.Channel; import org.cowboycoders.ant.NetworkKeys; import org.cowboycoders.ant.Node; import org.cowboycoders.ant.events.BroadcastListener; import org.cowboycoders.ant.events.MessageCondition; import org.cowboycoders.ant.messages.ChannelMessage; import org.cowboycoders.ant.messages.SlaveChannelType; import org.cowboycoders.ant.messages.StandardMessage; import org.cowboycoders.ant.messages.data.BroadcastDataMessage; import org.cowboycoders.ant.utils.AntUtils; import org.cowboycoders.ant.utils.ArrayUtils; import org.cowboycoders.ant.utils.ChannelMessageSender; import org.cowboycoders.ant.utils.EnqueuedMessageSender; import org.cowboycoders.turbotrainers.AntTurboTrainer; import org.cowboycoders.turbotrainers.Mode; import org.cowboycoders.turbotrainers.Parameters.CommonParametersInterface; import org.cowboycoders.turbotrainers.TooFewAntChannelsAvailableException; import org.cowboycoders.turbotrainers.TurboTrainerDataListener; import org.fluxoid.utils.FixedPeriodUpdater; import org.fluxoid.utils.FixedPeriodUpdaterWithReset; import org.fluxoid.utils.IterationOperator; import org.fluxoid.utils.IterationUtils; import org.fluxoid.utils.UpdateCallback; import java.math.BigInteger; import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Formatter; import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; public class BushidoBrake extends AntTurboTrainer { private static final int UPDATE_PERIOD_MS = 1000; // must be > 2 seconds as this is the approximate update period from the brake private static final int RESET_PERIOD_MS = 5000; public static final Mode[] SUPPORTED_MODES = new Mode[]{ Mode.TARGET_SLOPE, Mode.TARGET_SPEED, Mode.TARGET_POWER }; { setSupportedModes(SUPPORTED_MODES); } public final static Logger LOGGER = Logger.getLogger(BushidoBrake.class .getName()); public static final byte[] PACKET_REQUEST_VERSION = AntUtils .padToDataLength(new int[]{(byte) 0xAc, 0x02}); public static final byte[] PACKET_START_CALIBRATION = AntUtils .padToDataLength(new int[]{(byte) 0x23, 0x63}); // public static final byte[] PACKET_REQUEST_CALIBRATION_STATUS = AntUtils // .padToDataLength(new int[] { (byte) 0x23, 0x58 }); public static final byte[] PACKET_REQUEST_CALIBRATION_VALUE = AntUtils .padToDataLength(new int[]{0x23, 0x4d}); public static final byte[] PACKET_POST_RESTART_CALIBRATION_RERSUME = AntUtils.padToDataLength(new int[]{0x23, 0x58}); private static final Byte[] PARTIAL_PACKET_CALIBRATION_STATUS = new Byte[]{0x22}; private final ArrayList<BroadcastListener<? extends ChannelMessage>> listeners = new ArrayList<BroadcastListener<? extends ChannelMessage>>(); private Channel channel; private EnqueuedMessageSender channelMessageSender; private BrakeModel model; private Lock requestDataLock = new ReentrantLock(); private boolean requestDataInProgess = false; private AbstractController resistanceController; private Runnable requestDataCallback = new Runnable() { @Override public void run() { try { requestDataLock.lock(); requestDataInProgess = false; } finally { requestDataLock.unlock(); } } }; private VersionRequestCallback versionRequestCallback; private TurboTrainerDataListener dispatchListener = new TurboTrainerDataListener() { /** * @param speed in km/h */ @Override public void onSpeedChange(double speed) { // allow hooks in controller to determine which speed we send final double speedToSend = resistanceController.onSpeedChange(speed); synchronized (model) { model.setActualSpeed(speed); model.setVirtualSpeed(speedToSend); } synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation( TurboTrainerDataListener dcl) { dcl.onSpeedChange(speedToSend); } }); } // We are integrating for distance. As onDistanceChange() doesn't // receive values directly // manually update with new value obtained through integration synchronized (model) { this.onDistanceChange(model.getVirtualDistance()); } } /** * @param power in watts */ @Override public void onPowerChange(final double power) { synchronized (model) { model.setPower(power); } synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation( TurboTrainerDataListener dcl) { double powerToSend = resistanceController.onPowerChange(power); dcl.onPowerChange(powerToSend); } }); } } /** * @param cadence in rpm */ @Override public void onCadenceChange(final double cadence) { synchronized (model) { model.setCadence(cadence); } synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation( TurboTrainerDataListener dcl) { double cadenceToSend = resistanceController.onCadenceChange(cadence); dcl.onCadenceChange(cadenceToSend); } }); } } /** * @param distance in m */ @Override public void onDistanceChange(final double distance) { // called from onSpeedChange synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation( TurboTrainerDataListener dcl) { synchronized (model) { double distanceToSend = resistanceController.onDistanceChange(distance); dcl.onDistanceChange(distanceToSend); } } }); } } /** * @param heartRate in bpm */ @Override public void onHeartRateChange(double heartRate) { } }; private FixedPeriodUpdater speedUpdater = new FixedPeriodUpdaterWithReset(0.0, new UpdateCallback() { @Override public void onUpdate(Object newValue) { dispatchListener.onSpeedChange((Double) newValue); } }, UPDATE_PERIOD_MS, RESET_PERIOD_MS) { @Override public Object getResetValue() { return 0.0; } }; private FixedPeriodUpdater powerUpdater = new FixedPeriodUpdaterWithReset(0.0, new UpdateCallback() { @Override public void onUpdate(Object newValue) { dispatchListener.onPowerChange((Double) newValue); } }, UPDATE_PERIOD_MS, RESET_PERIOD_MS) { @Override public Object getResetValue() { return 0.0; } }; private FixedPeriodUpdater cadenceUpdater = new FixedPeriodUpdaterWithReset(0.0, new UpdateCallback() { @Override public void onUpdate(Object newValue) { dispatchListener.onCadenceChange((Double) newValue); } }, UPDATE_PERIOD_MS, RESET_PERIOD_MS) { @Override public Object getResetValue() { return model.getHeartRate(); } }; public class BushidoUpdatesListener implements BushidoBrakeInternalListener { private final BrakeModel model; /** * Only route responses through this member */ private ChannelMessageSender channelSender; public BushidoUpdatesListener(BrakeModel model, ChannelMessageSender channelSender) { this.model = model; this.channelSender = channelSender; } @Override public void onRequestData(Byte[] data) { // We don't want thread's queueing up waiting to be serviced. // Subject to a race but we will respond to next request. try { requestDataLock.lock(); if (requestDataInProgess) return; requestDataInProgess = true; } finally { requestDataLock.unlock(); } byte[] bytes = null; synchronized (model) { bytes = model.getDataPacket(); } channelSender.sendMessage(AntUtils.buildBroadcastMessage(bytes), requestDataCallback); } @Override public void onSpeedChange(final double speed) { speedUpdater.update(speed); } @Override public void onPowerChange(final double power) { powerUpdater.update(power); } @Override public void onCadenceChange(final double cadence) { cadenceUpdater.update(cadence); } @Override public void onDistanceChange(final double distance) { if (resistanceController instanceof PowerPidBrakeController) { PowerPidBrakeController rc = (PowerPidBrakeController) resistanceController; rc.setTargetPower(model.getTargetPower()); } } @Override public void onChangeCounter(int counter) { synchronized (model) { model.setCounter(counter); } } @Override public void onChangeLeftPower(double power) { synchronized (model) { model.setPowerLeft(power); } } @Override public void onChangeRightPower(double power) { synchronized (model) { model.setPowerRight(power); } } @Override public void onReceiveSoftwareVersion(String version) { LOGGER.info("received bushido soft version:" + version); synchronized (BushidoBrake.this) { if (versionRequestCallback != null) { versionRequestCallback.onVersionReceived(version); // make available for garbage collection versionRequestCallback = null; } } } @Override public void onChangeBrakeTemperature(double temp) { synchronized (model) { model.setBrakeTemperature(temp); } } @Override public void onChangeBalance(int balance) { synchronized (model) { model.setPowerBalance(balance); } } @Override public void onHeartRateChange(double heartRate) { // not recieved } } /** * Call after start. */ private <V extends ChannelMessage> void registerChannelRxListener( BroadcastListener<V> listener, Class<V> clazz) { synchronized (listeners) { listeners.add(listener); channel.registerRxListener(listener, clazz); } } private <V extends ChannelMessage> void unregisterRxListener( BroadcastListener<? extends ChannelMessage> listener) { channel.removeRxListener(listener); } public void start() throws InterruptedException, TimeoutException { startConnection(); // resetOdometer(); // startCycling(); } private void validateController() { if (resistanceController.getMode() != getCurrentMode()) { throw new IllegalStateException(String.format("Invalid controller: %s for mode: %s. ", resistanceController.toString(), getCurrentMode().toString())); } } public void startConnection() throws InterruptedException, TimeoutException { Node node = getNode(); // FIXME: Sometimes the channels aren't freed after use. Stop the node as a workaround for now. node.stop(); node.start(); channel = node.getFreeChannel(); if (channel == null) { throw new TooFewAntChannelsAvailableException(); } channel.setName("C:BUSHIDO_BRAKE"); SlaveChannelType channelType = new SlaveChannelType(); channel.assign(NetworkKeys.ANT_PUBLIC, channelType); channel.setId(0, 0x51, 0, false); channel.setFrequency(60); channel.setPeriod(4096); channel.setSearchTimeout(Channel.SEARCH_TIMEOUT_NEVER); channel.open(); channelMessageSender = new EnqueuedMessageSender(channel); Mode mode = getCurrentMode(); switch (mode) { case TARGET_POWER: model = new TargetPowerModel(); break; case TARGET_SPEED: // TODO case TARGET_SLOPE: default: model = new TargetSlopeModel(); } if (resistanceController == null) { // This is normally the case unless the controller has been specifically set resistanceController = getDefaultResistanceController(mode); } validateController(); BushidoUpdatesListener updatesListener = new BushidoUpdatesListener( model, this.getMessageSender()); BushidoBrakeBroadcastDataListener dataListener = new BushidoBrakeBroadcastDataListener( updatesListener); this.registerChannelRxListener(dataListener, BroadcastDataMessage.class); resistanceController.start(model); //FIXME: these need to be called after the line above. DO NOT MOVE. //This is because start(model) sets the data model on the resistance controller. //These calls delegate to the resistance controller. If the resistance controller //calls getDataModel() we dereference a null pointer. speedUpdater.start(); powerUpdater.start(); cadenceUpdater.start(); } /** * Overrides the default resistance controller for the Bushido brake which is normally chosen * automatically as a function of the turbo mode. This may be useful for testing, or providing * alternative controllers to the defaults. * * @param resistanceController to override the default controller with. */ public void overrideDefaultResistanceController(AbstractController resistanceController) { this.resistanceController = resistanceController; } public static interface VersionRequestCallback { public void onVersionReceived(String versionString); } public void requestVersion(VersionRequestCallback versionRequestCallback) { if (versionRequestCallback == null) { throw new IllegalArgumentException("callback must be non-null"); } synchronized (this) { this.versionRequestCallback = versionRequestCallback; } try { getMessageSender().pause(true); BroadcastDataMessage msg = new BroadcastDataMessage(); msg.setData(BushidoBrake.PACKET_REQUEST_VERSION); channel.sendAndWaitForMessage(msg, AntUtils.CONDITION_CHANNEL_TX, 10L, TimeUnit.SECONDS, null); } catch (InterruptedException e) { LOGGER.warning("interrupted waiting for version"); } catch (TimeoutException e) { LOGGER.warning("timeout waiting for version"); } finally { getMessageSender().pause(false); } } /** * Callback to facilitate UI hooks * * @author will */ public interface CalibrationCallback { /** * Called when user is required to speed up to 40 km/h */ void onRequestStartPedalling(); /** * Called once user has reached 25 mph (40 kmh). Must instruct user to * stop pedalling until requested. */ void onReachedCalibrationSpeed(); /** * Called after onReachedCalibrationSpeed() and once user has slowed * down to zero kmh. User must start pedalling to receive new * calibration value */ void onRequestResumePedalling(); /** * Only called on success * * @param calibrationValue new calibration value */ void onSuccess(double calibrationValue); /** * Only called if calibration fails * * @param exception reason for failure, may be caused by: * InterruptedException, TimeoutException */ void onFailure(CalibrationException exception); /** * Called when below speed */ void onBelowSpeedReminder(double speed); } /** * Thrown on calibration failure * * @author will */ public static class CalibrationException extends Exception { private static final long serialVersionUID = -5196590652828662691L; private CalibrationException() { super(); } private CalibrationException(String message, Throwable cause) { super(message, cause); } private CalibrationException(String message) { super(message); } private CalibrationException(Throwable cause) { super(cause); } } /** * Compound class that controls CalibrationState transitions, as well as the "actions" that can * be performed * on that state. * * @author will */ private static abstract class CalibrationStateAction { private final CalibrationState state; private final boolean allowNullTransition; public CalibrationStateAction(CalibrationState state, boolean allowNullTransition) { this.state = state; this.allowNullTransition = allowNullTransition; } public CalibrationStateAction(CalibrationState state) { this(state, false); } public final CalibrationState getMappedState() { return state; } public final boolean shouldAllowNullTransition() { return allowNullTransition; } public final boolean performTransition(CalibrationState newState) throws IllegalStateException { if (newState == state) return false; if (!shouldAllowNullTransition() && newState == null) throw new IllegalStateException("Null state disallowed"); String newStateString = newState == null ? "null" : newState.toString(); String stateString = state == null ? "null" : state.toString(); LOGGER.info("state transition: " + stateString + " -> " + newStateString); return onPerformTransition(newState); } /** * Should perform any necessary operations to make the transition. * * @param newState target state * @return false if no transition made, but was expect state. True, otherwise. * @throws IllegalStateException thrown if unexpected state is encountered. */ public abstract boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException; /** * State dependent behaviour. Called once when transition to this state is first made. */ public abstract void onStateSelected(Byte[] data); } private class CalibrationController { private static final int RADIO_QUIET_TIMEOUT_MILLI_SECONDS = 1000; private boolean started = false; private long timeout; private Long startTimeStamp; private Timer timer; private CalibrationCallback callback; private CalibrationStateMachine stateMachine; private TimerTask timeOutMonitor; private TimerTask stopDetector; private Timer radioSilenceTimer; private abstract class AntHandler { //submit through single thread so we don't multiple threads trying to send at same time private Executor singleThreadPool = Executors.newSingleThreadExecutor(); public abstract void doSend() throws InterruptedException, TimeoutException; public void send() { try { doSend(); } catch (InterruptedException e) { LOGGER.warning("interrupted during calibration"); callback.onFailure(new CalibrationException(e)); stop(); return; } catch (TimeoutException e) { LOGGER.warning("timeout waiting for calibration to finish"); callback.onFailure(new CalibrationException(e)); stop(); return; } } public void sendOnDifferentThread() { singleThreadPool.execute(new Runnable() { public void run() { send(); } }); } } private class CalibrationStateMachine { private final Map<CalibrationState, CalibrationStateAction> wrappedStates = new HashMap<CalibrationState, CalibrationStateAction>(); private CalibrationState currentState = null; { addState(null, new CalibrationStateAction(null) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { // if not in calibration we need to request to switch to calibration mode if (newState == CalibrationState.NON_CALIBRATION_MODE) { final BroadcastDataMessage msg = new BroadcastDataMessage(); msg.setData(BushidoBrake.PACKET_START_CALIBRATION); final long duration = nanosRemaining(TimeUnit.SECONDS.toNanos(2)); new AntHandler() { @Override public void doSend() throws InterruptedException, TimeoutException { channel.sendAndWaitForMessage(msg, AntUtils.CONDITION_CHANNEL_TX, duration, TimeUnit.NANOSECONDS, null); } }.sendOnDifferentThread(); return false; } else if (newState != CalibrationState.CALIBRATION_MODE) { throw new IllegalStateException(); } return true; } @Override public void onStateSelected(Byte[] data) { // no processing needed } }); addState(CalibrationState.CALIBRATION_MODE, new CalibrationStateAction(CalibrationState .CALIBRATION_MODE) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { if (newState != CalibrationState.CALIBRATION_REQUESTED) throw new IllegalStateException(); return true; } @Override public void onStateSelected(Byte[] data) { LOGGER.info(CalibrationState.CALIBRATION_MODE.toString()); } }); addState(CalibrationState.CALIBRATION_REQUESTED, new CalibrationStateAction (CalibrationState.CALIBRATION_REQUESTED) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { if (newState != CalibrationState.UP_TO_SPEED) throw new IllegalStateException(); return true; } @Override public void onStateSelected(Byte[] data) { LOGGER.info(CalibrationState.CALIBRATION_REQUESTED.toString()); BushidoBrake.this.registerDataListener(speedListener); } }); addState(CalibrationState.UP_TO_SPEED, new CalibrationStateAction(CalibrationState .UP_TO_SPEED) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { if (newState != CalibrationState.NO_ERROR) throw new IllegalStateException(); return true; } @Override public void onStateSelected(Byte[] data) { LOGGER.info(CalibrationState.UP_TO_SPEED.toString()); } }); addState(CalibrationState.NO_ERROR, new CalibrationStateAction(CalibrationState.NO_ERROR) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { if (newState != CalibrationState.STOPPED) throw new IllegalStateException(); return true; } @Override public void onStateSelected(Byte[] data) { LOGGER.info(CalibrationState.NO_ERROR.toString()); synchronized (replyListener) { if (radioSilenceTimer == null) { newRadioSilenceTimer(); } } } }); addState(CalibrationState.STOPPED, new CalibrationStateAction(CalibrationState.STOPPED) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { if (newState != CalibrationState.NON_CALIBRATION_MODE) throw new IllegalStateException(); return true; } @Override public void onStateSelected(Byte[] data) { LOGGER.info(CalibrationState.STOPPED.toString()); callback.onRequestResumePedalling(); } }); addState(CalibrationState.NON_CALIBRATION_MODE, new CalibrationStateAction (CalibrationState.NON_CALIBRATION_MODE) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { if (newState != CalibrationState.CALIBRATION_VALUE_READY) throw new IllegalStateException(); return true; } @Override public void onStateSelected(Byte[] data) { LOGGER.info(CalibrationState.NON_CALIBRATION_MODE.toString()); //new StatusPoller().start(); final BroadcastDataMessage msg = new BroadcastDataMessage(); msg.setData(BushidoBrake.PACKET_POST_RESTART_CALIBRATION_RERSUME); new AntHandler() { @Override public void doSend() throws InterruptedException, TimeoutException { int RETRIES = 5; long duration = CalibrationController.this.nanosRemaining(TimeUnit.SECONDS .toNanos(10L)); duration = (long) ((double) duration / (double) RETRIES); //set a min timeout if (duration < TimeUnit.SECONDS.toNanos(1L)) { RETRIES = 1; duration = CalibrationController.this.nanosRemaining(TimeUnit.SECONDS.toNanos (2L)); } sendAndRetry(msg, AntUtils.CONDITION_CHANNEL_TX, RETRIES, duration, TimeUnit.NANOSECONDS); } }.sendOnDifferentThread(); } }); addState(CalibrationState.CALIBRATION_VALUE_READY, new CalibrationStateAction (CalibrationState.CALIBRATION_VALUE_READY) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { if (newState != CalibrationState.CALIBRATED) throw new IllegalStateException(); return true; } @Override public void onStateSelected(Byte[] data) { LOGGER.info(CalibrationState.CALIBRATION_VALUE_READY.toString()); LOGGER.info("Requesting calibration value"); final BroadcastDataMessage msg = new BroadcastDataMessage(); msg.setData(BushidoBrake.PACKET_REQUEST_CALIBRATION_VALUE); new AntHandler() { @Override public void doSend() throws InterruptedException, TimeoutException { long duration = CalibrationController.this.nanosRemaining(TimeUnit.SECONDS .toNanos(2L)); channel.sendAndWaitForMessage(msg, AntUtils.CONDITION_CHANNEL_TX, duration, TimeUnit.NANOSECONDS, null); } }.sendOnDifferentThread(); } }); addState(CalibrationState.CALIBRATED, new CalibrationStateAction(CalibrationState .CALIBRATED) { @Override public boolean onPerformTransition( org.cowboycoders.turbotrainers.bushido.brake.CalibrationState newState) throws IllegalStateException { // final state : no transitions should occur throw new IllegalStateException(); } @Override public void onStateSelected(Byte[] data) { LOGGER.info(CalibrationState.CALIBRATED.toString()); BigInteger val = new BigInteger(new byte[]{0x00, data[4], data[5]}); double calibrationValue = val.doubleValue() / 10.0; callback.onSuccess(calibrationValue); stop(); } }); } private void addState(CalibrationState state, CalibrationStateAction wrappedState) throws IllegalArgumentException { if (wrappedState.getMappedState() != state) throw new IllegalArgumentException("wrapped state must match state it is wrapping"); wrappedStates.put(state, wrappedState); } private final void setTargetState(CalibrationState newState, Byte[] data) { CalibrationStateAction wrappedState = wrappedStates.get(currentState); if (wrappedState == null) { throw new IllegalStateException("State must be mapped to a wrapped value. State: " + currentState); } if (wrappedState.performTransition(newState)) { currentState = newState; wrappedState = wrappedStates.get(newState); wrappedState.onStateSelected(data); } } public synchronized final void setTargetState(CalibrationState newState) { setTargetState(newState, null); } public synchronized final void setTargetState(Byte[] data) { CalibrationState newState = null; if (ArrayUtils.arrayStartsWith( PARTIAL_PACKET_CALIBRATION_STATUS, data)) { if (data[1] == 0x06 && data[3] == 0x02) { newState = CalibrationState.CALIBRATION_MODE; } else if (data[1] == 0x06 && data[3] == 0x05) { newState = CalibrationState.UP_TO_SPEED; } else if (data[1] == 0x06) { newState = CalibrationState.CALIBRATION_REQUESTED; } else if (data[1] == 0x03 && data[3] == 0x0c) { newState = CalibrationState.NO_ERROR; } else if (data[1] == 0x03 && data[3] == 0x42) { newState = CalibrationState.CALIBRATED; } else if (data[1] == 0x03 && data[3] == 0x4d) { // 22:03:00:4d:00:00:00:00 newState = CalibrationState.CALIBRATION_VALUE_READY; } else if (data[1] == 0x03) { // 22:03:00:00:00:00:00:00 newState = CalibrationState.NON_CALIBRATION_MODE; } else { StringBuilder packet = new StringBuilder(); Formatter formatter = null; try { formatter = new Formatter(packet); for (byte b : data) { formatter.format("%02x:", b); } } finally { if (formatter != null) formatter.close(); } LOGGER.warning("unknown status packet: " + packet.toString()); } // String stateAsString = newState == null ? "null" : newState // .toString(); // LOGGER.info("current state: " + stateAsString); setTargetState(newState, data); } } public synchronized void reset() { currentState = null; } } /** * Handles exceptions for state changes * * @author will */ private abstract class StateSetter { protected abstract void doStateChange() throws IllegalStateException; public final void changeState() { try { doStateChange(); } catch (IllegalStateException e) { // something went wrong which led to an unexpected state change. Assume calibration // irreversably failed // and restart with remaining time LOGGER.info("Illegal state transition: restarting calibration"); long timeLeft = CalibrationController.this.nanosRemaining(null); CalibrationCallback oldCallBack = callback; stop(); start(oldCallBack, timeLeft); } } } private void newRadioSilenceTimer() { radioSilenceTimer = new Timer(); stopDetector = new TimerTask() { @Override public void run() { // state : stopped new StateSetter() { @Override protected void doStateChange() throws IllegalStateException { stateMachine.setTargetState(CalibrationState.STOPPED); } }.changeState(); } }; timer.schedule(stopDetector, RADIO_QUIET_TIMEOUT_MILLI_SECONDS); } private final BroadcastListener<BroadcastDataMessage> replyListener = new BroadcastListener<BroadcastDataMessage>() { @Override public void receiveMessage(BroadcastDataMessage message) { // if radio not quiet start a new timer (we assume quiet when timer successfully elapses) synchronized (this) { if (radioSilenceTimer != null) { stopDetector.cancel(); radioSilenceTimer.cancel(); newRadioSilenceTimer(); } } final Byte[] data = message.getData(); new StateSetter() { @Override protected void doStateChange() throws IllegalStateException { stateMachine.setTargetState(data); } }.changeState(); } }; TurboTrainerDataListener speedListener = new TurboTrainerDataListener() { @Override public void onSpeedChange(double speed) { if (speed >= 40) { callback.onReachedCalibrationSpeed(); } else { callback.onBelowSpeedReminder(speed); } } @Override public void onPowerChange(double power) { // Not interested } @Override public void onCadenceChange(double cadence) { // Not interested } @Override public void onDistanceChange(double distance) { // Not interested } @Override public void onHeartRateChange(double heartRate) { // Not interested } }; public CalibrationController() { stateMachine = new CalibrationStateMachine(); } public long nanosRemaining(Long defaultIfLess) { long timeLeft; timeLeft = timeout - (System.nanoTime() - startTimeStamp); if (defaultIfLess != null && timeLeft > defaultIfLess) return defaultIfLess; return timeLeft; } public synchronized void start(final CalibrationCallback callback, long timeout) { if (started) throw new IllegalStateException( "calibration already in progress"); if (callback == null) throw new IllegalArgumentException("callback must be non null"); this.timeout = timeout; this.callback = callback; started = true; getMessageSender().pause(true); timer = new Timer(); timeOutMonitor = new TimerTask() { @Override public void run() { callback.onFailure(new CalibrationException("max calibration time expired")); stop(); } }; timer.schedule(timeOutMonitor, TimeUnit.NANOSECONDS.toMillis(timeout)); startTimeStamp = System.nanoTime(); channel.registerRxListener(replyListener, BroadcastDataMessage.class); callback.onRequestStartPedalling(); } public synchronized void stop() { if (!started) return; timer.cancel(); started = false; getMessageSender().pause(false); startTimeStamp = null; timer = null; channel.removeRxListener(replyListener); BushidoBrake.this.unregisterDataListener(speedListener); stateMachine.reset(); } } private CalibrationController calibrationController = new CalibrationController(); /** * Asynchronously calibrates the brake. * * @param callback various calibration hooks {@link CalibrationCallback ) * @param timeout timeout for calibration to complete (in seconds) * @return new calibration value (as would be displayed on headunit) */ public double calibrate(CalibrationCallback callback, long timeout) { double calibrationValue = 0; calibrationController .start(callback, TimeUnit.SECONDS.toNanos(timeout)); return calibrationValue; } private StandardMessage sendAndRetry(final ChannelMessage msg, final MessageCondition condition, final int maxRetries, final long timeoutPerRetry, final TimeUnit timeoutUnit) throws InterruptedException, TimeoutException { return AntUtils.sendAndRetry(channel, msg, condition, maxRetries, timeoutPerRetry, timeoutUnit); } public EnqueuedMessageSender getMessageSender() { return channelMessageSender; } public void stop() throws InterruptedException, TimeoutException { // cause related threads to be shutdown calibrationController.stop(); // make sure no-one sends and unpause which will cancel our stop request synchronized (listeners) { for (BroadcastListener<? extends ChannelMessage> listener : listeners) { unregisterRxListener(listener); } } if (resistanceController != null) { // If something goes wrong when the ANT connection is started this might not be initialised. resistanceController.stop(); } // disconnect(); if (channel != null) { channel.close(); channel.unassign(); getNode().freeChannel(channel); } // let external controller stop node // node.stop(); } @Override public boolean supportsSpeed() { return true; } @Override public boolean supportsPower() { return true; } @Override public boolean supportsCadence() { return true; } @Override public boolean supportsHeartRate() { return false; } @Override public void setParameters(CommonParametersInterface parameters) throws IllegalArgumentException { synchronized (model) { model.setParameters(parameters); } } /** * Returns a default controller for the specified turbo mode. These controllers are specific to * the Bushido. * * @param mode is the turbo mode as selected in the UI. * @return the controller for the turbo to realise the specified mode. */ private AbstractController getDefaultResistanceController(Mode mode) { AbstractController controller; switch (mode) { case TARGET_SLOPE: controller = new SpeedResistanceMapper(); break; case TARGET_SPEED: controller = new SpeedPidBrakeController(); break; case TARGET_POWER: controller = new PowerPidBrakeController(); break; default: throw new InvalidParameterException("Invalid mode"); } return controller; } }