/* * 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.headunit; 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.events.MessageConditionFactory; import org.cowboycoders.ant.messages.ChannelMessage; import org.cowboycoders.ant.messages.SlaveChannelType; import org.cowboycoders.ant.messages.StandardMessage; import org.cowboycoders.ant.messages.data.AcknowledgedDataMessage; import org.cowboycoders.ant.messages.data.BroadcastDataMessage; import org.cowboycoders.ant.messages.data.DataMessage; 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.IterationOperator; import org.fluxoid.utils.IterationUtils; import java.util.ArrayList; import java.util.Collections; import java.util.Set; import java.util.WeakHashMap; 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 BushidoHeadunit extends AntTurboTrainer { public final static Logger LOGGER = Logger.getLogger(BushidoHeadunit.class.getName()); public static final Mode[] SUPPORTED_MODES = new Mode[]{ Mode.TARGET_SLOPE }; { setSupportedModes(SUPPORTED_MODES); } private static final long TIMEOUT_DISTANCE_UPDATED = TimeUnit.SECONDS.toNanos(5); public static final byte[] PACKET_ALIVE = AntUtils.padToDataLength(new int[]{0xac, 0x03, 0x02}); public static final byte[] PACKET_RESET_ODOMETER = AntUtils.padToDataLength(new int[]{0xac, 0x03, 0x01}); public static final byte[] PACKET_DISCONNECT = AntUtils.padToDataLength(new int[]{0xac, 0x03}); public static final byte[] PACKET_INIT_CONNECTION = AntUtils.padToDataLength(new int[]{0xac, 0x03, 0x04}); public static final byte[] PACKET_START_CYLING = AntUtils.padToDataLength(new int[]{0xac, 0x03, 0x03}); private static final long RESET_ODOMETER_TIMEOUT = TimeUnit.SECONDS.toNanos(5); private static final Byte[] PARTIAL_PACKET_CONNECTION_SUCCESSFUL = new Byte[]{(byte) 0xad, 0x01, 0x04}; // this may not be NO_CONNECTION private static final Byte[] PARTIAL_PACKET_NO_CONNECTION = new Byte[]{(byte) 0xad, 0x01, 0x00}; // private static final MessageCondition AntUtils.CONDITION_CHANNEL_TX = // MessageConditionFactory.newResponseCondition(MessageId.EVENT, ResponseCode.EVENT_TX); private final ArrayList<BroadcastListener<? extends ChannelMessage>> listeners = new ArrayList<BroadcastListener<? extends ChannelMessage>>(); private Channel channel; // private ExecutorService channelExecutorService = Executors.newSingleThreadExecutor(); private EnqueuedMessageSender channelMessageSender; private AbstractBushidoModel model; // /** // * If locked should not send data packets; // */ // private Lock responseLock = new ReentrantLock(); // /** // * Weak set // */ // private Set<TurboTrainerDataListener> dataChangeListeners = Collections.newSetFromMap(new // WeakHashMap<TurboTrainerDataListener,Boolean>()); // /** * Weak set */ private final Set<BushidoButtonPressListener> buttonPressListeners = Collections.newSetFromMap (new WeakHashMap<BushidoButtonPressListener, Boolean>()); boolean distanceUpdated = false; private Lock requestPauseLock = new ReentrantLock(); private boolean requestPauseInProgess = false; private Lock requestDataLock = new ReentrantLock(); private boolean requestDataInProgess = false; private Runnable requestPauseCallback = new Runnable() { @Override public void run() { try { requestPauseLock.lock(); requestPauseInProgess = false; } finally { requestPauseLock.unlock(); } } }; private Runnable requestDataCallback = new Runnable() { @Override public void run() { try { requestDataLock.lock(); requestDataInProgess = false; } finally { requestDataLock.unlock(); } } }; //private boolean respond = true; public class BushidoUpdatesListener implements BushidoInternalListener { private final AbstractBushidoModel data; /** * Only route responses through this member */ private ChannelMessageSender channelSender; public BushidoUpdatesListener(AbstractBushidoModel model, ChannelMessageSender channelSender) { this.data = model; this.channelSender = channelSender; } @Override public void onRequestData() { // 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 (data) { bytes = data.getDataPacket(); } channelSender.sendMessage(AntUtils.buildBroadcastMessage(bytes), requestDataCallback); } @Override public void onRequestKeepAlive() { // TODO Auto-generated method stub } @Override public void onSpeedChange(final double speed) { synchronized (data) { data.setVirtualSpeed(speed); } synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation(TurboTrainerDataListener dcl) { dcl.onSpeedChange(speed); } }); } } @Override public void onPowerChange(final double power) { synchronized (data) { data.setPower(power); } synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation(TurboTrainerDataListener dcl) { dcl.onPowerChange(power); } }); } } @Override public void onCadenceChange(final double cadence) { synchronized (data) { data.setCadence(cadence); } synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation(TurboTrainerDataListener dcl) { dcl.onCadenceChange(cadence); } }); } } @Override public void onDistanceChange(final double distance) { synchronized (data) { data.setActualDistance(distance); distanceUpdated = true; synchronized (model) { model.notifyAll(); } } synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation(TurboTrainerDataListener dcl) { synchronized (data) { dcl.onDistanceChange(data.getVirtualDistance()); } } }); } } @Override public void onHeartRateChange(final double heartRate) { synchronized (data) { data.setHeartRate(heartRate); } synchronized (dataChangeListeners) { IterationUtils.operateOnAll(dataChangeListeners, new IterationOperator<TurboTrainerDataListener>() { @Override public void performOperation(TurboTrainerDataListener dcl) { dcl.onHeartRateChange(heartRate); } }); } } @Override public void onRequestPauseStatus() { try { requestPauseLock.lock(); if (requestPauseInProgess) return; requestPauseInProgess = true; } finally { requestPauseLock.unlock(); } BroadcastDataMessage msg = AntUtils.buildBroadcastMessage(PACKET_ALIVE); channelSender.sendMessage(msg, requestPauseCallback); } @Override public synchronized void onButtonPressFinished(final BushidoButtonPressDescriptor descriptor) { synchronized (buttonPressListeners) { IterationUtils.operateOnAll(buttonPressListeners, new IterationOperator<BushidoButtonPressListener>() { @Override public void performOperation(BushidoButtonPressListener bpl) { bpl.onButtonPressFinished(descriptor); } }); } } @Override public void onButtonPressActive(final BushidoButtonPressDescriptor descriptor) { synchronized (buttonPressListeners) { IterationUtils.operateOnAll(buttonPressListeners, new IterationOperator<BushidoButtonPressListener>() { @Override public void performOperation(BushidoButtonPressListener bpl) { bpl.onButtonPressActive(descriptor); } }); } } } ; /** * Stored in weak set, so keep a reference : no anonymous classes */ public void registerButtonPressListener(BushidoButtonPressListener listener) { synchronized (buttonPressListeners) { buttonPressListeners.add(listener); } } /** * Stored in weak set, so keep a reference : no anonymous classes */ public void registerDataListener(TurboTrainerDataListener listener) { synchronized (dataChangeListeners) { dataChangeListeners.add(listener); } } /** * As a opposed to that based on artificial speed used to compensate for negative gradients */ public double getRealDistance() { synchronized (model) { return model.getVirtualDistance(); } } /** * 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(); } public void startConnection() throws InterruptedException, TimeoutException { // check mode is valid Mode currentMode = getCurrentMode(); if (currentMode == null) { throw new IllegalStateException("must set a mode"); } Node node = getNode(); node.start(); channel = node.getFreeChannel(); if (channel == null) { throw new TooFewAntChannelsAvailableException(); } channel.setName("C:BUSHIDO"); SlaveChannelType channelType = new SlaveChannelType(); channel.assign(NetworkKeys.ANT_PUBLIC, channelType); channel.setId(0, 0x52, 0, false); channel.setFrequency(60); channel.setPeriod(4096); channel.setSearchTimeout(255); channel.open(); channelMessageSender = new EnqueuedMessageSender(channel); initConnection(); //startCycling(); BushidoUpdatesListener updatesListener = new BushidoUpdatesListener(model, this .getMessageSender()); BushidoBroadcastDataListener dataListener = new BushidoBroadcastDataListener(updatesListener); BushidoInternalButtonPressListener buttonListener = new BushidoInternalButtonPressListener (updatesListener); this.registerChannelRxListener(dataListener, BroadcastDataMessage.class); this.registerChannelRxListener(buttonListener, AcknowledgedDataMessage.class); } public void startCycling() throws InterruptedException, TimeoutException { BroadcastDataMessage msg = new BroadcastDataMessage(); msg.setData(BushidoHeadunit.PACKET_START_CYLING); channel.sendAndWaitForMessage(msg, AntUtils.CONDITION_CHANNEL_TX, 10L, TimeUnit.SECONDS, null); } private void initConnection() throws InterruptedException, TimeoutException { BroadcastDataMessage msg = new BroadcastDataMessage(); msg.setData(BushidoHeadunit.PACKET_INIT_CONNECTION); MessageCondition condition = new MessageCondition() { @Override public boolean test(StandardMessage msg) { if (!(msg instanceof DataMessage)) return false; DataMessage dataMessage = (DataMessage) msg; if (!ArrayUtils.arrayStartsWith(PARTIAL_PACKET_CONNECTION_SUCCESSFUL, dataMessage.getData ())) return false; return true; } }; sendAndRetry(msg, condition, 20, 1L, TimeUnit.SECONDS); } private void disconnect() throws InterruptedException, TimeoutException { BroadcastDataMessage msg = new BroadcastDataMessage(); msg.setData(BushidoHeadunit.PACKET_DISCONNECT); MessageCondition condition = new MessageCondition() { @Override public boolean test(StandardMessage msg) { if (!(msg instanceof DataMessage)) return false; DataMessage dataMessage = (DataMessage) msg; if (!ArrayUtils.arrayStartsWith(PARTIAL_PACKET_NO_CONNECTION, dataMessage.getData())) return false; return true; } }; MessageCondition chainedCondition = MessageConditionFactory.newChainedCondition(AntUtils .CONDITION_CHANNEL_TX, condition); sendAndRetry(msg, chainedCondition, 20, 1L, TimeUnit.SECONDS); } 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; } /** * If you don't call this after we start the headunit will remember the previously cycled distance */ public void resetOdometer() throws InterruptedException, TimeoutException { // stop replying to messages getMessageSender().pause(true); BroadcastDataMessage msg = new BroadcastDataMessage(); msg.setData(BushidoHeadunit.PACKET_RESET_ODOMETER); channel.sendAndWaitForMessage(msg, AntUtils.CONDITION_CHANNEL_TX, 10L, TimeUnit.SECONDS, null); long startTimeStamp = System.nanoTime(); synchronized (model) { while (!distanceUpdated) { long currentTimestamp = System.nanoTime(); long timeLeft = TIMEOUT_DISTANCE_UPDATED - (currentTimestamp - startTimeStamp); model.wait(TimeUnit.NANOSECONDS.toMillis(timeLeft)); } } if (System.nanoTime() - startTimeStamp > TIMEOUT_DISTANCE_UPDATED) { throw new TimeoutException("timeout waiting for distance to be updated"); } startTimeStamp = System.nanoTime(); synchronized (model) { while (model.getVirtualDistance() > 0.000001) { channel.sendAndWaitForMessage(msg, AntUtils.CONDITION_CHANNEL_TX, 10L, TimeUnit.SECONDS, null); long currentTimestamp = System.nanoTime(); long timeLeft = RESET_ODOMETER_TIMEOUT - (currentTimestamp - startTimeStamp); if (timeLeft <= 0) { throw new TimeoutException("timeout waiting for distance to be reset"); } model.wait(TimeUnit.NANOSECONDS.toMillis(timeLeft)); } } // start replying to messages again getMessageSender().pause(false); } public void stop() throws InterruptedException, TimeoutException { // make sure no-one sends and unpause which will cancel our stop request synchronized (listeners) { for (BroadcastListener<? extends ChannelMessage> listener : listeners) { unregisterRxListener(listener); } } disconnect(); channel.close(); channel.unassign(); getNode().freeChannel(channel); // let external controiller stop node //node.stop(); } // @Override // public void unregisterDataListener(TurboTrainerDataListener listener) { // synchronized (dataChangeListeners) { // dataChangeListeners.remove(listener); // } // } // public MessageMetaWrapper<StandardMessage> send(ChannelMessage msg) { // // return channel.send(msg); // } @Override public boolean supportsSpeed() { return true; } @Override public boolean supportsPower() { return true; } @Override public boolean supportsCadence() { return true; } @Override public boolean supportsHeartRate() { return true; } @Override public void setParameters(CommonParametersInterface parameters) throws IllegalArgumentException { model.setParameters(parameters); } @Override public void setMode(Mode mode) throws IllegalArgumentException { super.setMode(mode); if (mode == Mode.TARGET_SLOPE) { // already this model if (model instanceof BushidoTargetSlopeModel) { return; } model = new BushidoTargetSlopeModel(); } } }