/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package source.tuner.usb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.DeviceHandle;
import org.usb4java.LibUsb;
import org.usb4java.LibUsbException;
import org.usb4java.Transfer;
import org.usb4java.TransferCallback;
import sample.Broadcaster;
import sample.Listener;
import sample.OverflowableTransferQueue;
import sample.adapter.ISampleAdapter;
import sample.complex.ComplexBuffer;
import sample.real.IOverflowListener;
import source.tuner.TunerManager;
import util.ThreadPool;
import javax.usb.UsbException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class USBTransferProcessor implements TransferCallback
{
private final static Logger mLog = LoggerFactory.getLogger(USBTransferProcessor.class);
private static final byte USB_BULK_TRANSFER_ENDPOINT = (byte) 0x81;
private static final long USB_TIMEOUT_MS = 2000l; //milliseconds
//Number of native byte buffers to allocate for transferring data from the USB device
private static final int TRANSFER_BUFFER_POOL_SIZE = 16;
//Maximum number of filled buffers for the blocking queue
private static final int FILLED_BUFFER_MAX_CAPACITY = 300;
//Threshold for resetting buffer overflow condition
private static final int FILLED_BUFFER_OVERFLOW_RESET_THRESHOLD = 100;
private String mDeviceName;
//Handle to the USB bulk transfer device
private DeviceHandle mDeviceHandle;
//Tuner format-specific byte to IQ float sample converter
private ISampleAdapter mSampleAdapter;
//Byte array transfer buffers size in bytes
private int mBufferSize;
private Broadcaster<ComplexBuffer> mComplexBufferBroadcaster = new Broadcaster<>();
private OverflowableTransferQueue<byte[]> mFilledBuffers;
private LinkedTransferQueue<Transfer> mAvailableTransfers = new LinkedTransferQueue<>();
private LinkedTransferQueue<Transfer> mTransfersInProgress = new LinkedTransferQueue<>();
private AtomicBoolean mRunning = new AtomicBoolean();
private ByteBuffer mLibUsbHandlerStatus = ByteBuffer.allocateDirect(4);
private BufferDispatcher mBufferDispatcher = new BufferDispatcher();
private ScheduledFuture mBufferDispatcherFuture;
/**
* Manages stream of USB transfer buffers and converts buffers to complex buffer samples for distribution to
* any registered listeners.
*
* @param deviceName to use when logging information or errors
* @param deviceHandle to the USB bulk transfer device
* @param sampleAdapter specific to the tuner's byte buffer format for converting to floating point I/Q samples
* @param bufferSize in bytes. Should be a multiple of two: 65536, 131072 or 262144.
*/
public USBTransferProcessor(String deviceName,
DeviceHandle deviceHandle,
ISampleAdapter sampleAdapter,
int bufferSize)
{
mDeviceName = deviceName;
mDeviceHandle = deviceHandle;
mSampleAdapter = sampleAdapter;
mBufferSize = bufferSize;
mFilledBuffers = new OverflowableTransferQueue<>(FILLED_BUFFER_MAX_CAPACITY, FILLED_BUFFER_OVERFLOW_RESET_THRESHOLD);
mFilledBuffers.setOverflowListener(new IOverflowListener()
{
@Override
public void sourceOverflow(boolean overflow)
{
if(overflow)
{
mLog.debug(mDeviceName + " - buffer overflow - temporary pause until processing catches up");
}
else
{
mLog.debug(mDeviceName + " - buffer overflow cleared - resuming normal processing");
}
}
});
}
/**
* Start USB transfer buffer processing. Subsequent calls to this method after started will be ignored.
*/
private void start()
{
if(mRunning.compareAndSet(false, true))
{
prepareDeviceStart();
prepareTransfers();
while(!mAvailableTransfers.isEmpty())
{
Transfer transfer = mAvailableTransfers.poll();
if(transfer != null)
{
int result = LibUsb.submitTransfer(transfer);
if(result == LibUsb.SUCCESS)
{
mTransfersInProgress.add(transfer);
}
else if(result == LibUsb.ERROR_PIPE)
{
int resetResult = LibUsb.clearHalt(mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT);
if(resetResult == LibUsb.SUCCESS)
{
int resubmitResult = LibUsb.submitTransfer(transfer);
if(resubmitResult == LibUsb.SUCCESS)
{
mTransfersInProgress.add(transfer);
}
else
{
mLog.error(mDeviceName + " - error resubmitting transfer after endpoint clear halt");
}
}
else
{
mLog.error(mDeviceName + " - unable to clear device endpoint halt");
}
}
else
{
//TODO: broadcast to each listener that this source has an error and is shutting down
mLog.error(mDeviceName + "- error submitting transfer [" + LibUsb.errorName(result) + "]");
}
}
}
//Start transferred buffer dispatcher
mBufferDispatcherFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mBufferDispatcher,
0, 11, TimeUnit.MILLISECONDS);
//Register with LibUSB processor so that it auto-starts LibUSB processing
TunerManager.LIBUSB_TRANSFER_PROCESSOR.registerTransferProcessor(this);
}
}
/**
* Stop USB transfer buffer processing. Subsequent calls to this method after stopped will be ignored.
*/
private void stop()
{
if(mRunning.compareAndSet(true, false))
{
mBufferDispatcherFuture.cancel(true);
mFilledBuffers.clear();
//Cancel the lib usb process timer
for(Transfer transfer : mTransfersInProgress)
{
LibUsb.cancelTransfer(transfer);
}
//Unregister from LibUSB processor so that it auto-stops LibUSB processing
TunerManager.LIBUSB_TRANSFER_PROCESSOR.registerTransferProcessor(this);
//Directly invoke the timeout handler to ensure that our cancelled transfer buffers are flushed.
int result = LibUsb.handleEventsTimeoutCompleted(null, USB_TIMEOUT_MS,
mLibUsbHandlerStatus.asIntBuffer());
if(result != LibUsb.SUCCESS)
{
mLog.error(mDeviceName + " - error while cancelling transfer buffers during shutdown/pause - error code:" + result);
}
mLibUsbHandlerStatus.rewind();
executeDeviceStop();
}
}
/**
* Allows sub-class implementations to execute any device-specific operations to prepare for starting USB transfers
*/
protected void prepareDeviceStart()
{
}
/**
* Allows sub-class implementations to execute any device-specific operations after stopping USB transfers
*/
protected void executeDeviceStop()
{
}
/**
* Indicates if there are complex buffer listeners registered with this processor
*/
public boolean hasListeners()
{
return mComplexBufferBroadcaster.hasListeners();
}
public void addListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.addListener(listener);
start();
}
public void removeListener(Listener<ComplexBuffer> listener)
{
mComplexBufferBroadcaster.removeListener(listener);
if(!hasListeners())
{
stop();
}
}
public void removeAllListeners()
{
mComplexBufferBroadcaster.clear();
stop();
}
/**
* Prepares (allocates) a set of transfer buffers for use in transferring data from the USB device via the bulk
* interface. Since we're using direct allocation (native), buffers are retained and reused across multiple
* start/stop cycles.
*/
private void prepareTransfers() throws LibUsbException
{
while(mAvailableTransfers.size() < TRANSFER_BUFFER_POOL_SIZE)
{
Transfer transfer = LibUsb.allocTransfer();
if(transfer == null)
{
throw new LibUsbException("Couldn't allocate USB transfer buffer", LibUsb.ERROR_NO_MEM);
}
final ByteBuffer buffer = ByteBuffer.allocateDirect(mBufferSize);
LibUsb.fillBulkTransfer(transfer, mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT, buffer, this,
"Buffer", USB_TIMEOUT_MS);
mAvailableTransfers.add(transfer);
}
}
/**
* Process a filled transfer buffer received back from the USB device
*/
@Override
public void processTransfer(Transfer transfer)
{
mTransfersInProgress.remove(transfer);
switch(transfer.status())
{
case LibUsb.TRANSFER_COMPLETED:
case LibUsb.TRANSFER_STALL:
case LibUsb.TRANSFER_TIMED_OUT:
if(transfer.actualLength() > 0)
{
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[transfer.actualLength()];
buffer.get(data);
buffer.rewind();
if(mRunning.get())
{
mFilledBuffers.offer(data);
}
}
break;
case LibUsb.TRANSFER_CANCELLED:
break;
default:
//Unexpected transfer error
mLog.error(mDeviceName + " - transfer error [" + getTransferStatus(transfer.status()) +
"] transferred actual: " + transfer.actualLength());
}
if(mRunning.get())
{
int result = LibUsb.submitTransfer(transfer);
if(result == LibUsb.SUCCESS)
{
mTransfersInProgress.add(transfer);
}
else if(result == LibUsb.ERROR_PIPE)
{
int resetResult = LibUsb.clearHalt(mDeviceHandle, USB_BULK_TRANSFER_ENDPOINT);
if(resetResult == LibUsb.SUCCESS)
{
int resubmitResult = LibUsb.submitTransfer(transfer);
if(resubmitResult == LibUsb.SUCCESS)
{
mTransfersInProgress.add(transfer);
}
else
{
mLog.error(mDeviceName + " - error resubmitting transfer after endpoint clear halt");
}
}
else
{
mLog.error(mDeviceName + " - unable to clear device endpoint halt");
}
}
else
{
mAvailableTransfers.add(transfer);
mLog.error(mDeviceName + " - error submitting transfer [" + LibUsb.errorName(result) + "]");
if(mTransfersInProgress.isEmpty())
{
mLog.warn(mDeviceName + " - all transfer buffer processing is stopped");
//TODO: no transfers are in progress ... need to alert the user and the registered complex
//TODO: buffer listeners so that they can respond accordingly
}
}
}
else
{
//We're stopping - park the transfer buffers
if(!mAvailableTransfers.contains(transfer))
{
mAvailableTransfers.add(transfer);
}
}
}
/**
* Converts the USB transfer status number into a descriptive label
*/
public static String getTransferStatus(int status)
{
switch(status)
{
case 0:
return "TRANSFER COMPLETED (0)";
case 1:
return "TRANSFER ERROR (1)";
case 2:
return "TRANSFER TIMED OUT (2)";
case 3:
return "TRANSFER CANCELLED (3)";
case 4:
return "TRANSFER STALL (4)";
case 5:
return "TRANSFER NO DEVICE (5)";
case 6:
return "TRANSFER OVERFLOW (6)";
default:
return "UNKNOWN TRANSFER STATUS (" + status + ")";
}
}
/**
* Fetches byte[] chunks from the raw sample buffer. Converts each byte
* array and broadcasts the array to all registered listeners
*/
public class BufferDispatcher implements Runnable
{
private List<byte[]> mBuffersToDispatch = new ArrayList<>();
@Override
public void run()
{
try
{
mFilledBuffers.drainTo(mBuffersToDispatch, 15);
for(byte[] buffer : mBuffersToDispatch)
{
float[] complexSamples = mSampleAdapter.convert(buffer);
mComplexBufferBroadcaster.broadcast(new ComplexBuffer(complexSamples));
}
}
catch(Exception e)
{
mLog.error(mDeviceName + " - error while dispatching complex IQ buffer samples", e);
}
mBuffersToDispatch.clear();
}
}
}