/* This file is part of Wattzap Community Edition.
*
* Wattzap Community Edtion 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.
*
* Wattzap Community Edition 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 Wattzap. If not, see <http://www.gnu.org/licenses/>.
*/
package org.cowboycoders.ant.interfaces;
/**
* Copyright (c) 2012-2013, Will Szumski, David George
*
* This file is part of formicidae.
*
* formicidae 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.
*
* formicidae 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 formicidae. If not, see <http://www.gnu.org/licenses/>.
*/
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.usb.UsbClaimException;
import javax.usb.UsbConst;
import javax.usb.UsbDevice;
import javax.usb.UsbDisconnectedException;
import javax.usb.UsbEndpoint;
import javax.usb.UsbException;
import javax.usb.UsbHostManager;
import javax.usb.UsbHub;
import javax.usb.UsbInterface;
import javax.usb.UsbInterfacePolicy;
import javax.usb.UsbNotActiveException;
import javax.usb.UsbNotOpenException;
import javax.usb.UsbPipe;
import javax.usb.UsbServices;
import org.cowboycoders.ant.messages.StandardMessage;
import org.cowboycoders.ant.messages.commands.ResetMessage;
import org.cowboycoders.ant.utils.ByteUtils;
import org.cowboycoders.ant.utils.UsbUtils;
public class AntTransceiver extends AbstractAntTransceiver {
public final static Logger LOGGER = Logger.getLogger(AntTransceiver.class
.getName());
public static final Level LOG_LEVEL = Level.SEVERE;
static {
// set logging level
AntTransceiver.LOGGER.setLevel(LOG_LEVEL);
}
/**
* usb device id
*/
private static final short DEVICE_ID = 0x1008;
public static final short ANTUSBM_ID = 0x1009;
/**
* usb vendor
*/
private static final short VENDOR_ID = 0x0fcf;
/**
* sync byte
*/
private static byte MESSAGE_TX_SYNC = (byte) 0xA4;
/**
* Used to set read buffer size
*/
private static final int MAX_MSG_LENGTH = 23;
// private static final int MESSAGE_OFFSET_SYNC = 0;
private static final int MESSAGE_OFFSET_MSG_LENGTH = 1;
/**
* Usb Interface
*/
private UsbInterface _interface;
/**
* opened
*/
private boolean running = false;
/**
* class lock
*/
private ReentrantLock lock = new ReentrantLock();
/**
* interface claimed lock
*/
private ReentrantLock interfaceLock = new ReentrantLock();
private boolean readEndpoint = true;
private UsbEndpoint endpointIn;
private UsbEndpoint endpointOut;
private UsbDevice device;
UsbPipe inPipe = null;
private UsbReader usbReader;
// private int deviceNumber;
public AntTransceiver(int deviceNumber, short deviceId) {
// this.deviceNumber = deviceNumber;
doInit(deviceNumber, deviceId);
}
public AntTransceiver(int deviceNumber) {
// this.deviceNumber = deviceNumber;
doInit(deviceNumber, DEVICE_ID);
}
/**
* Testing only
*/
AntTransceiver() {
}
private void doInit(int deviceNumber, short deviceId) {
UsbServices usbServices = null;
UsbHub rootHub;
try {
usbServices = UsbHostManager.getUsbServices();
rootHub = usbServices.getRootUsbHub();
} catch (SecurityException e) {
throw new AntCommunicationException(e);
} catch (UsbException e) {
throw new AntCommunicationException(e);
}
List<UsbDevice> devices = UsbUtils.getUsbDevicesWithId(rootHub,
VENDOR_ID, deviceId);
// devices = UsbUtils.getAllUsbDevices(rootHub);
// causes javax.usb.UsbException: Strings not supported by device
// for (UsbDevice d : devices) {
// try {
// LOGGER.finer("Found device: " + d.getProductString());
// } catch (UnsupportedEncodingException e) {
// e.printStackTrace();
// } catch (UsbDisconnectedException e) {
// e.printStackTrace();
// } catch (UsbException e) {
//
// //e.printStackTrace();
// }
// }
LOGGER.finer("Number of devices: " + devices.size());
if (devices.size() < deviceNumber + 1) {
throw new AntCommunicationException("Device not found");
}
UsbDevice device = devices.get(deviceNumber);
this.device = device;
}
private void logData(Level level, byte[] data, String tag) {
StringBuffer logBuffer = new StringBuffer();
for (Byte b : data) {
logBuffer.append(String.format("%x ", b));
}
logBuffer.append((String.format("\n")));
LOGGER.log(level, tag + " : " + logBuffer);
}
public class UsbReader extends Thread {
private byte[] last;
private static final int BUFFER_SIZE = 64;
/**
* @param data
* buffer to check
* @return data remaining
*/
private byte[] lookForSync(byte[] data, int len) {
if (data == null || data.length < 1) {
return new byte[0];
}
if (data[0] != MESSAGE_TX_SYNC) {
int index = -1;
for (int i = 0; i < len; i++) {
if (data[i] == MESSAGE_TX_SYNC) {
index = i;
break;
}
}
// not found
if (index < 0) {
LOGGER.warning("data read from usb endpoint does not contain a sync byte : ignoring");
return new byte[0]; // zero length array
}
LOGGER.info("found non-zero sync byte index");
data = Arrays.copyOfRange(data, index, data.length);
}
return data;
}
// All this array copying inefficient but simple (could keep reference
// to current index instead)
private byte[] skipCurrentSync(byte[] data) {
if (data.length < 1) {
return new byte[0];
}
data = Arrays.copyOfRange(data, 1, data.length);
return data;
}
/**
* Gets the next message and notifies interested listeners.
*
* @param data
* - message data
* @param len
* - message length
*/
void processBuffer(byte[] data, int len) {
while (len > 0) {
data = lookForSync(data, len);
if (data.length <= MESSAGE_OFFSET_MSG_LENGTH) {
LOGGER.info("data length too small, checking next packet");
// assume rest will arrive in next packet
last = data;
break;
}
int msgLength = data[MESSAGE_OFFSET_MSG_LENGTH];
// negative length does not make sense
if (msgLength < 0) {
LOGGER.warning("msgLength appears to be incorrect (ignorning). Length : "
+ msgLength);
data = skipCurrentSync(data);
continue;
}
int checkSumIndex = msgLength + 3;
if (checkSumIndex >= data.length) {
// unreasonably large checkSumIndex (dont span multiple
// buffers)
if (checkSumIndex >= BUFFER_SIZE - 1) {
LOGGER.warning("msgLength appears to be incorrect (ignorning). Length : "
+ msgLength);
data = skipCurrentSync(data);
continue;
}
// we try assume continued in next buffer
last = data;
break;
}
// data minus sync and checksum
byte[] cleanData = new byte[msgLength + 2];
for (int i = 0; i < msgLength + 2; i++) {
cleanData[i] = data[i + 1];
}
if (getChecksum(cleanData) != data[checkSumIndex]) {
LOGGER.warning("checksum incorrect : ignoring");
data = skipCurrentSync(data);
continue;
}
AntTransceiver.this.broadcastRxMessage(cleanData);
// cleandata length + sync + checksum
len -= (cleanData.length + 2);
data = Arrays.copyOfRange(data, cleanData.length + 2,
data.length);
}
}
/*
* Two Modifications (David George - 11/June/2013)
*
* 1. continue if we get a USB Exception on read from lower layers, this
* is a timeout and we don't care
*
* 2. use returned data length to make code more efficient (hopefully).
* No more searching for SYNC bytes in zero data
*/
@Override
public void run() {
try {
while (readEndpoint) {
try {
// interfaceLock.lock();
// byte [] data = new byte[MAX_MSG_LENGTH];
byte[] data = new byte[BUFFER_SIZE];
int len;
try {
// inPipe.open();
LOGGER.finest("pre read");
len = inPipe.syncSubmit(data);
// System.out.println("received " + len);
} catch (UsbException e) {
// Timeouts are expected in some implementations - these manifest
// themselves as UsbExceptions. We should continue, but log the error
// in case it indicates something more serious.
LOGGER.warning(e.getMessage());
continue;
} finally {
// inPipe.close();
}
logData(Level.FINER, data, "read");
// process remaining bytes from last buffer
if (last != null) {
// TODO len is bigger due to remaining bytes
len += last.length;
data = ByteUtils.joinArray(last, data);
last = null;
}
processBuffer(data, len);
} finally {
// interfaceLock.unlock();
}
}
} catch (UsbNotActiveException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UsbNotOpenException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UsbDisconnectedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
LOGGER.finest(this.getClass().toString() + " killed");
}
}
/**
*
* @param _interface
* interface to claim / release
* @param claim
* true to claim, false to release
*/
private void claimInterface(UsbInterface _interface, boolean claim) {
try {
interfaceLock.lock();
if (claim) {
//_interface.claim();
_interface.claim(new UsbInterfacePolicy() {
@Override
public boolean forceClaim(UsbInterface usbInterface) {
System.out.println(">>> forcing claim");
return true;
}
});
} else {
if (_interface.isClaimed()) {
_interface.release();
}
}
} catch (UsbClaimException e) {
e.printStackTrace();
throw new AntCommunicationException(e);
} catch (UsbNotActiveException e) {
throw new AntCommunicationException(e);
} catch (UsbDisconnectedException e) {
throw new AntCommunicationException(e);
} catch (UsbException e) {
throw new AntCommunicationException(e);
} finally {
interfaceLock.unlock();
}
}
// FIXME : TAKES an age to start with reference javax.usb implementation
@Override
public boolean start() {
try {
lock.lock();
// already started
if (running)
return true;
if (!device.isConfigured()) {
throw new AntCommunicationException(
"Ant stick not configured by OS");
}
UsbInterface _interface = device.getActiveUsbConfiguration()
.getUsbInterface((byte) 0);
this._interface = _interface;
claimInterface(_interface, true);
@SuppressWarnings("unchecked")
List<UsbEndpoint> endpoints = _interface.getUsbEndpoints();
if (endpoints.size() != 2) {
throw new AntCommunicationException(
"Unexpected number of endpoints");
}
for (UsbEndpoint endpoint : endpoints) {
if (endpoint.getDirection() == UsbConst.ENDPOINT_DIRECTION_IN)
this.endpointIn = endpoint;
else
this.endpointOut = endpoint;
}
if (this.endpointOut == null || this.endpointIn == null) {
throw new AntCommunicationException("Endpoints not found");
}
// FIXME: without these two // don't seem to receive replies to
// first few messages
// StandardMessage msg = new ResetMessage();
// send(msg.encode());
// FIXME: if we don't write some garbage it doesn't response to
// first few
// messages
try {
write(new byte[128]);
} catch (UsbException e) {
LOGGER.finest("device wake up failed");
}
inPipe = endpointIn.getUsbPipe();
try {
inPipe.open();
} catch (UsbException e) {
throw new AntCommunicationException("Error opening inPipe");
}
readEndpoint = true;
this.usbReader = new UsbReader();
// SharedThreadPool.getThreadPool().execute(this.usbReader);
this.usbReader.start();
running = true;
} catch (RuntimeException e) {
e.printStackTrace();
claimInterface(_interface, false);
throw e;
} finally {
lock.unlock();
}
// TODO Auto-generated method stub
return true;
}
// private static class TransferKiller extends Thread {
//
// private boolean running = false;
//
// private boolean stop = false;
//
// /**
// * should use lock
// * @return the running
// */
// public boolean isRunning() {
// return running;
// }
//
// /**
// * stops the killer
// */
// public void kill() {
// this.stop = true;
// }
//
// /**
// * @return the lock
// */
// public Lock getLock() {
// return lock;
// }
//
// /**
// * @return the statusChanged
// */
// public Condition getStatusChanged() {
// return statusChanged;
// }
//
// private Lock lock = new ReentrantLock();
//
// private Condition statusChanged = lock.newCondition();
//
// private UsbPipe pipe = null;
//
// public TransferKiller(UsbPipe pipe) {
// this.pipe = pipe;
// }
//
// @Override
// public void run() {
//
// try{
//
// lock.lock();
// running = true;
// statusChanged.signalAll();
// AntTransceiver.LOGGER.finest("TransferKiller: started");
// } finally {
// lock.unlock();
// }
//
// try {
// while(!stop) {
// try {
// lock.lock();
// AntTransceiver.LOGGER.finest("TransferKiller: abortAllSubmissions");
// pipe.abortAllSubmissions();
// } finally {
// lock.unlock();
// }
// Thread.sleep(10);
// }
// } catch (InterruptedException e) {
// try {
// lock.lock();
// stop = true;
// } finally {
// lock.unlock();
// }
// }
//
// try {
// lock.lock();
// running = false;
// AntTransceiver.LOGGER.finest("TransferKiller: killed");
// statusChanged.signalAll();
//
// } finally {
// lock.unlock();
// }
//
//
//
// }
//
//
// }
// @Override
// public void stop() {
// try {
// lock.lock();
private void killUsbReader() {
readEndpoint = false;
// Doesn't seem to work so we send a message instead
// inPipe.abortAllSubmissions();
StandardMessage msg = new ResetMessage();
send(msg.encode());
try {
usbReader.join();
} catch (InterruptedException e) {
LOGGER.severe("interrupted waiting to shutdown device");
}
}
@Override
public void stop() {
try {
lock.lock();
if (!running)
return;
// TransferKiller inPipeKiller = new TransferKiller(inPipe);
// inPipeKiller.start();
killUsbReader();
// try {
// interfaceLock.lock();
// try {
// inPipeKiller.getLock().lock();
// inPipeKiller.kill();
// while(inPipeKiller.isRunning()) {
// inPipeKiller.getStatusChanged().await();
// }
//
// } catch (InterruptedException e) {
// LOGGER.severe("interrupted waiting for killer to stop");
// } finally {
// inPipeKiller.getLock().unlock();
//
// }
try {
// inPipe.abortAllSubmissions();
inPipe.close();
} catch (UsbException e) {
throw new AntCommunicationException("Error closing inPipe", e);
}
_interface.release();
// } finally {
// interfaceLock.unlock();
// }
// _interface.release();
running = false;
} catch (UsbClaimException e) {
throw new AntCommunicationException(e);
} catch (UsbNotActiveException e) {
throw new AntCommunicationException(e);
} catch (UsbDisconnectedException e) {
throw new AntCommunicationException(e);
} catch (UsbException e) {
throw new AntCommunicationException(e);
} finally {
lock.unlock();
}
}
private void write(byte[] data) throws UsbNotActiveException,
UsbNotOpenException, IllegalArgumentException,
UsbDisconnectedException, UsbException {
UsbPipe pipe = null;
try {
lock.lock();
pipe = endpointOut.getUsbPipe();
if (!pipe.isOpen())
pipe.open();
LOGGER.finest("pre submit");
pipe.syncSubmit(data);
logData(Level.FINER, data, "wrote");
} finally {
if (pipe != null) {
pipe.close();
}
lock.unlock();
}
}
@Override
public void send(byte[] message) throws AntCommunicationException {
try {
if (!running)
throw new AntCommunicationException(
"AntTransceiver not running. Use start()");
write(addExtras(message));
} catch (UsbNotActiveException e) {
throw new AntCommunicationException(e);
} catch (UsbNotOpenException e) {
throw new AntCommunicationException(e);
} catch (IllegalArgumentException e) {
throw new AntCommunicationException(e);
} catch (UsbDisconnectedException e) {
throw new AntCommunicationException(e);
} catch (UsbException e) {
throw new AntCommunicationException(e);
} finally {
}
}
public byte getChecksum(byte[] nocheck) {
byte checksum = 0;
checksum = MESSAGE_TX_SYNC;
for (byte b : nocheck) {
checksum ^= b % 0xff;
}
return checksum;
}
public byte[] addExtras(byte[] nocheck) {
byte[] data = new byte[nocheck.length + 2];
data[0] = MESSAGE_TX_SYNC;
for (int i = 1; i < data.length - 1; i++) {
data[i] = nocheck[i - 1];
}
data[data.length - 1] = getChecksum(nocheck);
return data;
}
@Override
public boolean isRunning() {
try {
lock.lock();
return running;
} finally {
lock.unlock();
}
}
}