package source.tuner.airspy; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.EnumSet; 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; import javax.usb.UsbException; import org.apache.commons.io.EndianUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.usb4java.Device; 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.complex.ComplexBuffer; import source.SourceException; import source.tuner.TunerController; import source.tuner.configuration.TunerConfiguration; import controller.ThreadPoolManager; import controller.ThreadPoolManager.ThreadType; import dsp.filter.dc.DCRemovalFilter_RB; import dsp.filter.hilbert.HilbertTransform; /** * SDR Trunk * Copyright (C) 2015-2016 Dennis Sheirer * * Ported from libairspy at: * https://github.com/airspy/host/tree/master/libairspy * ----------------------------------------------------------------------------- * Copyright (c) 2013, Michael Ossmann * Copyright (c) 2012, Jared Boone * Copyright (c) 2014, Youssef Touil * Copyright (c) 2014, Benjamin Vernoux * Copyright (c) 2015, Ian Gilmour * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * Neither the name of AirSpy nor the names of its contributors may be used to * endorse or promote products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ public class AirspyTunerController extends TunerController { public static final Gain LINEARITY_GAIN_DEFAULT = Gain.LINEARITY_14; public static final Gain SENSITIVITY_GAIN_DEFAULT = Gain.SENSITIVITY_10; public static final int GAIN_MIN = 1; public static final int GAIN_MAX = 22; public static final int LNA_GAIN_MIN = 0; public static final int LNA_GAIN_MAX = 14; public static final int LNA_GAIN_DEFAULT = 7; public static final int MIXER_GAIN_MIN = 0; public static final int MIXER_GAIN_MAX = 15; public static final int MIXER_GAIN_DEFAULT = 9; public static final int IF_GAIN_MIN = 0; public static final int IF_GAIN_MAX = 15; public static final int IF_GAIN_DEFAULT = 9; public static final long FREQUENCY_MIN = 24000000l; public static final long FREQUENCY_MAX = 1800000000l; public static final long FREQUENCY_DEFAULT = 101100000; public static final double USABLE_BANDWIDTH_PERCENT = 0.90; public static final AirspySampleRate DEFAULT_SAMPLE_RATE = new AirspySampleRate( 0, 10000000, "10.00 MHz" ); public static final long USB_TIMEOUT_MS = 2000l; //milliseconds public static final byte USB_ENDPOINT = (byte)0x81; public static final byte USB_INTERFACE = (byte)0x0; public static final int TRANSFER_BUFFER_POOL_SIZE = 16; public static final byte USB_REQUEST_IN = (byte)( LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE ); public static final byte USB_REQUEST_OUT = (byte)( LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE ); public static final DecimalFormat MHZ_FORMATTER = new DecimalFormat( "#.00 MHz" ); private final static Logger mLog = LoggerFactory.getLogger( AirspyTunerController.class ); private Device mDevice; private DeviceHandle mDeviceHandle; private ThreadPoolManager mThreadPoolManager; private LinkedTransferQueue mFilledBuffers = new LinkedTransferQueue(); private Broadcaster mComplexBufferBroadcaster = new Broadcaster<>(); private int mBufferSize = 262144; private BufferProcessor mBufferProcessor = new BufferProcessor(); private AirspySampleAdapter mSampleAdapter = new AirspySampleAdapter(); private DCRemovalFilter_RB mDCFilter = new DCRemovalFilter_RB( 0.01f ); private HilbertTransform mHilbertTransform = new HilbertTransform(); private AirspyDeviceInformation mDeviceInfo; private List mSampleRates = new ArrayList<>(); private int mSampleRate = 0; public AirspyTunerController( Device device, ThreadPoolManager threadPoolManager ) throws SourceException { super( FREQUENCY_MIN, FREQUENCY_MAX, 0, USABLE_BANDWIDTH_PERCENT ); mDevice = device; mThreadPoolManager = threadPoolManager; } public void init() throws SourceException { mDeviceHandle = new DeviceHandle(); int result = LibUsb.open( mDevice, mDeviceHandle ); if( result != 0 ) { if( result == LibUsb.ERROR_ACCESS ) { mLog.error( "Unable to access Airspy - insufficient permissions." + " If you are running a Linux OS, have you installed the " + "airspy rules file in \\etc\\udev\\rules.d ??" ); } throw new SourceException( "Couldn't open airspy device - " + LibUsb.strError( result ) ); } try { claimInterface(); } catch( Exception e ) { throw new SourceException( "Airspy Tuner Controller - error while " + "setting USB configuration, claiming USB interface or " + "reset the kernel mode driver", e ); } try { setSamplePacking( false ); } catch( LibUsbException | UsbException | UnsupportedOperationException e ) { mLog.info( "Sample packing is not supported by airspy firmware" ); } try { setReceiverMode( true ); } catch( Exception e ) { mLog.error( "Couldn't enable airspy receiver mode", e ); } setFrequency( FREQUENCY_DEFAULT ); try { determineAvailableSampleRates(); } catch ( LibUsbException | UsbException e ) { mLog.error( "Error identifying available samples rates", e ); } try { setSampleRate( DEFAULT_SAMPLE_RATE ); } catch ( IllegalArgumentException | LibUsbException | UsbException e ) { mLog.error( "Setting sample rate is not supported by firmware", e ); } } /** * Claims the USB interface. If another application currently has * the interface claimed, the USB_FORCE_CLAIM_INTERFACE setting * will dictate if the interface is forcibly claimed from the other * application */ private void claimInterface() throws SourceException { if( mDeviceHandle != null ) { int result = LibUsb.kernelDriverActive( mDeviceHandle, USB_INTERFACE ); if( result == 1 ) { result = LibUsb.detachKernelDriver( mDeviceHandle, USB_INTERFACE ); if( result != LibUsb.SUCCESS ) { mLog.error( "failed attempt to detach kernel driver [" + LibUsb.errorName( result ) + "]" ); throw new SourceException( "couldn't detach kernel driver " + "from device" ); } } result = LibUsb.setConfiguration( mDeviceHandle, 1 ); if( result != LibUsb.SUCCESS ) { throw new SourceException( "couldn't set USB configuration 1 [" + LibUsb.errorName( result ) + "]" ); } result = LibUsb.claimInterface( mDeviceHandle, USB_INTERFACE ); if( result != LibUsb.SUCCESS ) { throw new SourceException( "couldn't claim usb interface [" + LibUsb.errorName( result ) + "]" ); } } else { throw new SourceException( "couldn't claim usb interface - no " + "device handle" ); } } 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 + ")"; } } @Override public void apply( TunerConfiguration config ) throws SourceException { if( config instanceof AirspyTunerConfiguration ) { AirspyTunerConfiguration airspy = (AirspyTunerConfiguration)config; int sampleRate = airspy.getSampleRate(); AirspySampleRate rate = getSampleRate( sampleRate ); if( rate == null ) { if( !mSampleRates.isEmpty() ) { rate = mSampleRates.get( 0 ); } else { rate = DEFAULT_SAMPLE_RATE; } } try { setSampleRate( rate ); } catch( UsbException e ) { throw new SourceException( "Couldn't set sample rate [" + rate.toString() + "]", e ); } try { setIFGain( airspy.getIFGain() ); setMixerGain( airspy.getMixerGain() ); setLNAGain( airspy.getLNAGain() ); setMixerAGC( airspy.isMixerAGC() ); setLNAAGC( airspy.isLNAAGC() ); //Set the gain mode last, so custom values are already set, and //linearity and sensitivity modes will automatically override //the custom values. setGain( airspy.getGain() ); setFrequencyCorrection( airspy.getFrequencyCorrection() ); } catch( Exception e ) { throw new SourceException( "Couldn't apply gain settings from " + "airspy config", e ); } try { setFrequency( airspy.getFrequency() ); } catch( SourceException se ) { //Do nothing, we couldn't set the frequency } } else { throw new IllegalArgumentException( "Invalid tuner config:" + config.getClass() ); } } @Override public long getTunedFrequency() throws SourceException { return mFrequencyController.getTunedFrequency(); } @Override public void setTunedFrequency( long frequency ) throws SourceException { mLog.debug("Setting tuned frequency to: " + frequency ); if( FREQUENCY_MIN <= frequency && frequency <= FREQUENCY_MAX ) { ByteBuffer buffer = ByteBuffer.allocateDirect( 4 ); buffer.order( ByteOrder.LITTLE_ENDIAN ); buffer.putInt( (int)frequency ); buffer.rewind(); try { write( Command.SET_FREQUENCY, 0, 0, buffer ); } catch( UsbException e ) { mLog.error( "error setting frequency [" + frequency + "]", e ); throw new SourceException( "error setting frequency [" + frequency + "]", e ); } } else { throw new SourceException( "Frequency [" + frequency + "] outside " + "of tunable range " + FREQUENCY_MIN + "-" + FREQUENCY_MAX ); } } @Override public int getCurrentSampleRate() throws SourceException { return mSampleRate; } /** * Sets the sample rate to the rate specified by the index value in the * available sample rates map * * @param index to a sample rate in the available samples rates map. * * @throws IllegalArgumentException if index is not a valid rate index * @throws LibUsbException if there was a read error or if this operation * is not supported by the current firmware * * @throws UsbException if there was a USB error */ public void setSampleRate( AirspySampleRate rate ) throws LibUsbException, UsbException { if( rate.getRate() != mSampleRate ) { int result = readByte( Command.SET_SAMPLE_RATE, 0, rate.getIndex(), true ); if( result != 1 ) { throw new UsbException( "Error setting sample rate [" + rate + "] rate - return value [" + result + "]" ); } else { mSampleRate = rate.getRate(); mFrequencyController.setSampleRate( mSampleRate ); } } } /** * Returns a list of sample rates supported by the firmware version */ public List getSampleRates() { return mSampleRates; } /** * Airspy sample rate object that matches the current sample rate setting. */ public AirspySampleRate getAirspySampleRate() { return getSampleRate( mSampleRate ); } /** * Airspy sample rate object that matches the specified rate in hertz, or * null if there are no available sample rates for the tuner that match the * argument value. */ public AirspySampleRate getSampleRate( int rate ) { for( AirspySampleRate sampleRate: mSampleRates ) { if( sampleRate.getRate() == rate ) { return sampleRate; } } //We should never get to here ... return null; } /** * Enables/Disables sample packing to allow two 12-bit samples to be packed * into 3 bytes (enabled) or 4 bytes (disabled). * * @param enabled * * @throws UsbException if sample packing is not supported by the current * device firmware or if there were usb communication issues */ public void setSamplePacking( boolean enabled ) throws LibUsbException, UsbException { int result = readByte( Command.SET_PACKING, 0, ( enabled ? 1 : 0 ), true ); if( result != 1 ) { throw new UsbException( "Couldnt set sample packing enabled: " + enabled ); } /* If we didn't throw an exception above, then update the sample adapter * to process samples accordingly */ mSampleAdapter.setSamplePacking( enabled ); } /** * Enables/disables the mixer automatic gain setting * * @param enabled * * @throws LibUsbException on unsuccessful read operation * @throws UsbException on USB error */ public void setMixerAGC( boolean enabled ) throws LibUsbException, UsbException { int result = readByte( Command.SET_MIXER_AGC, 0, ( enabled ? 1 : 0 ), true ); if( result != LibUsb.SUCCESS ) { throw new UsbException( "Couldnt set mixer AGC enabled: " + enabled ); } } /** * Enables/disables the low noise amplifier automatic gain setting * * @param enabled * * @throws LibUsbException on unsuccessful read operation * @throws UsbException on USB error */ public void setLNAAGC( boolean enabled ) throws LibUsbException, UsbException { int result = readByte( Command.SET_LNA_AGC, 0, ( enabled ? 1 : 0 ), true ); if( result != LibUsb.SUCCESS ) { throw new UsbException( "Couldnt set LNA AGC enabled: " + enabled ); } } public void setGain( Gain gain ) throws UsbException { if( gain != Gain.CUSTOM ) { setMixerAGC( false ); setLNAAGC( false ); setLNAGain( gain.getLNA() ); setMixerGain( gain.getMixer() ); setIFGain( gain.getIF() ); } } /** * Sets LNA gain * * @param gain - value within range of LNA_GAIN_MIN to LNA_GAIN_MAX * * @throws LibUsbException on error in java USB wrapper * @throws UsbException on error in USB transfer * @throws IllegalArgumentException if gain value is invalid */ public void setLNAGain( int gain ) throws LibUsbException, UsbException, IllegalArgumentException { if( LNA_GAIN_MIN <= gain && gain <= LNA_GAIN_MAX ) { int result = readByte( Command.SET_LNA_GAIN, 0, gain, true ); if( result != LibUsb.SUCCESS ) { throw new UsbException( "Couldnt set LNA gain to: " + gain ); } } else { throw new IllegalArgumentException( "LNA gain value [" + gain + "] is outside value range: " + LNA_GAIN_MIN + "-" + LNA_GAIN_MAX ); } } /** * Sets Mixer gain * * @param gain - value within range of MIXER_GAIN_MIN to MIXER_GAIN_MAX * * @throws LibUsbException on error in java USB wrapper * @throws UsbException on error in USB transfer * @throws IllegalArgumentException if gain value is invalid */ public void setMixerGain( int gain ) throws LibUsbException, UsbException, IllegalArgumentException { if( MIXER_GAIN_MIN <= gain && gain <= MIXER_GAIN_MAX ) { int result = readByte( Command.SET_MIXER_GAIN, 0, gain, true ); if( result != LibUsb.SUCCESS ) { throw new UsbException( "Couldnt set mixer gain to: " + gain ); } } else { throw new IllegalArgumentException( "Mixer gain value [" + gain + "] is outside value range: " + MIXER_GAIN_MIN + "-" + MIXER_GAIN_MAX ); } } /** * Sets IF (VGA) gain * * @param gain - value within range of VGA_GAIN_MIN to VGA_GAIN_MAX * * @throws LibUsbException on error in java USB wrapper * @throws UsbException on error in USB transfer * @throws IllegalArgumentException if gain value is invalid */ public void setIFGain( int gain ) throws LibUsbException, UsbException, IllegalArgumentException { if( IF_GAIN_MIN <= gain && gain <= IF_GAIN_MAX ) { int result = readByte( Command.SET_VGA_GAIN, 0, gain, true ); if( result != LibUsb.SUCCESS ) { throw new UsbException( "Couldnt set VGA gain to: " + gain ); } } else { throw new IllegalArgumentException( "VGA gain value [" + gain + "] is outside value range: " + IF_GAIN_MIN + "-" + IF_GAIN_MAX ); } } public void setReceiverMode( boolean enabled ) throws LibUsbException, UsbException { //Empty buffer to throw away ByteBuffer buffer = ByteBuffer.allocateDirect( 0 ); write( Command.RECEIVER_MODE, ( enabled ? 1 : 0 ), 0, buffer ); } /** * Queries the device for available sample rates. Will always provide at * least the default 10 MHz sample rate. */ private void determineAvailableSampleRates() throws LibUsbException, UsbException { mSampleRates.clear(); //Get a count of available sample rates. If we get an exception, then //we're using an older firmware revision and only the default 10 MHz //rate is supported try { byte[] rawCount = readArray( Command.GET_SAMPLE_RATES, 0, 0, 4 ); if( rawCount != null ) { int count = EndianUtils.readSwappedInteger( rawCount, 0 ); byte[] rawRates = readArray( Command.GET_SAMPLE_RATES, 0, count, ( count * 4 ) ); for( int x = 0; x < count; x++ ) { int rate = EndianUtils.readSwappedInteger( rawRates, ( x * 4 ) ); if( rate != DEFAULT_SAMPLE_RATE.getRate() ) { mSampleRates.add( new AirspySampleRate( x, rate, formatSampleRate( rate ) ) ); } } } } catch( LibUsbException e ) { //Press on, nothing else to do here .. } if( mSampleRates.isEmpty()) { mSampleRates.add( DEFAULT_SAMPLE_RATE ); } } /** * Formats the rate in hertz for display as megahertz */ private static String formatSampleRate( int rate ) { return MHZ_FORMATTER.format( (double)rate / 1E6d ); } /** * Device information */ public AirspyDeviceInformation getDeviceInfo() { //Lazy initialization if( mDeviceInfo == null ) { readDeviceInfo(); } return mDeviceInfo; } /** * Reads version information from the device and populates the info object */ private void readDeviceInfo() { if( mDeviceInfo == null ) { mDeviceInfo = new AirspyDeviceInformation(); } /* Board ID */ try { int boardID = readByte( Command.BOARD_ID_READ, 0, 0, true ); mDeviceInfo.setBoardID( boardID ); } catch ( LibUsbException | UsbException e ) { mLog.error( "Error reading airspy board ID", e ); } /* Version String */ try { //NOTE: libairspy is internally reading 127 bytes, however airspy_info //script is telling it to read 255 bytes ... things that make you go hmmmm byte[] version = readArray( Command.VERSION_STRING_READ, 0, 0, 127 ); mDeviceInfo.setVersion( version ); } catch ( LibUsbException | UsbException e ) { mLog.error( "Error reading airspy version string", e ); } /* Part ID and Serial Number */ try { //Read 6 x 32-bit integers = 24 bytes byte[] serial = readArray( Command.BOARD_PART_ID_SERIAL_NUMBER_READ, 0, 0, 24 ); mDeviceInfo.setPartAndSerialNumber( serial ); } catch ( LibUsbException | UsbException e ) { mLog.error( "Error reading airspy version string", e ); } } /** * Reads a single byte value from the device. * * @param command - airspy command * * @param value - value field for usb setup packet * @param index - index field for usb setup packet * * @return - byte value as an integer * * @throws LibUsbException if the operation is unsuccesful * @throws UsbException on any usb errors */ private int readByte( Command command, int value, int index, boolean signed ) throws LibUsbException, UsbException { if( mDeviceHandle != null ) { ByteBuffer buffer = ByteBuffer.allocateDirect( 1 ); int transferred = LibUsb.controlTransfer( mDeviceHandle, USB_REQUEST_IN, command.getValue(), (short)value, (short)index, buffer, USB_TIMEOUT_MS ); if( transferred < 0 ) { throw new LibUsbException( "read error", transferred ); } byte result = buffer.get( 0 ); if( signed ) { return ( result & 0xFF ); } else { return result; } } else { throw new LibUsbException( "device handle is null", LibUsb.ERROR_NO_DEVICE ); } } /** * Reads a multi-byte value from the device * * @param command - airspy command * @param value - usb packet value * @param index - usb packet index * @param length - number of bytes to read * @return - bytes read from the device * * @throws LibUsbException if quantity of bytes read doesn't equal the * requested number of bytes * @throws UsbException on error communicating with the device */ private byte[] readArray( Command command, int value, int index, int length ) throws LibUsbException, UsbException { if( mDeviceHandle != null ) { ByteBuffer buffer = ByteBuffer.allocateDirect( length ); int transferred = LibUsb.controlTransfer( mDeviceHandle, USB_REQUEST_IN, command.getValue(), (short)value, (short)index, buffer, USB_TIMEOUT_MS ); if( transferred < 0 ) { throw new LibUsbException( "read error", transferred ); } byte[] results = new byte[ transferred ]; buffer.get( results ); return results; } else { throw new LibUsbException( "device handle is null", LibUsb.ERROR_NO_DEVICE ); } } /** * Writes the buffer contents to the device * * @param command - airspy command * @param value - usb packet value * @param index - usb packet index * @param buffer - data to write to the device * @throws UsbException on error */ public void write( Command command, int value, int index, ByteBuffer buffer ) throws UsbException { if( mDeviceHandle != null ) { int transferred = LibUsb.controlTransfer( mDeviceHandle, USB_REQUEST_OUT, command.getValue(), (short)value, (short)index, buffer, USB_TIMEOUT_MS ); if( transferred < 0 ) { throw new LibUsbException( "error writing byte buffer", transferred ); } else if( transferred != buffer.capacity() ) { throw new LibUsbException( "transferred bytes [" + transferred + "] is not what was expected [" + buffer.capacity() + "]", transferred ); } } else { throw new LibUsbException( "device handle is null", LibUsb.ERROR_NO_DEVICE ); } } public enum GainMode { LINEARITY, SENSITIVITY, CUSTOM; } public enum Gain { LINEARITY_1( 1, 4, 0, 0 ), LINEARITY_2( 2, 5, 0, 0 ), LINEARITY_3( 3, 6, 1, 0 ), LINEARITY_4( 4, 7, 1, 0 ), LINEARITY_5( 5, 8, 1, 0 ), LINEARITY_6( 6, 9, 1, 0 ), LINEARITY_7( 7, 10, 2, 0 ), LINEARITY_8( 8, 10, 2, 1 ), LINEARITY_9( 9, 10, 0, 3 ), LINEARITY_10( 10, 10, 0, 5 ), LINEARITY_11( 11, 10, 1, 6 ), LINEARITY_12( 12, 10, 0, 8 ), LINEARITY_13( 13, 10, 0, 9 ), LINEARITY_14( 14, 10, 5, 8 ), LINEARITY_15( 15, 10, 6, 9 ), LINEARITY_16( 16, 11, 6, 9 ), LINEARITY_17( 17, 11, 7, 10 ), LINEARITY_18( 18, 11, 8, 12 ), LINEARITY_19( 19, 11, 9, 13 ), LINEARITY_20( 20, 11, 11, 14 ), LINEARITY_21( 21, 12, 12, 14 ), LINEARITY_22( 22, 13, 12, 14 ), SENSITIVITY_1( 1, 4, 0, 0 ), SENSITIVITY_2( 2, 4, 0, 1 ), SENSITIVITY_3( 3, 4, 0, 2 ), SENSITIVITY_4( 4, 4, 0, 3 ), SENSITIVITY_5( 5, 4, 1, 5 ), SENSITIVITY_6( 6, 4, 2, 6 ), SENSITIVITY_7( 7, 4, 2, 7 ), SENSITIVITY_8( 8, 4, 3, 8 ), SENSITIVITY_9( 9, 4, 4, 9 ), SENSITIVITY_10( 10, 5, 4, 9 ), SENSITIVITY_11( 11, 5, 4, 12 ), SENSITIVITY_12( 12, 5, 7, 12 ), SENSITIVITY_13( 13, 5, 8, 13 ), SENSITIVITY_14( 14, 5, 9, 14 ), SENSITIVITY_15( 15, 6, 9, 14 ), SENSITIVITY_16( 16, 7, 10, 14 ), SENSITIVITY_17( 17, 8, 10, 14 ), SENSITIVITY_18( 18, 9, 11, 14 ), SENSITIVITY_19( 19, 10, 12, 14 ), SENSITIVITY_20( 20, 11, 12, 14 ), SENSITIVITY_21( 21, 12, 12, 14 ), SENSITIVITY_22( 22, 13, 12, 14 ), CUSTOM( 1, 0, 0, 0 ); private int mValue; private int mIF; private int mMixer; private int mLNA; private Gain( int value, int ifGain, int mixer, int lna ) { mValue = value; mIF = ifGain; mMixer = mixer; mLNA = lna; } public int getValue() { return mValue; } public int getIF() { return mIF; } public int getMixer() { return mMixer; } public int getLNA() { return mLNA; } public static Gain getGain( GainMode mode, int value ) { assert( GAIN_MIN <= value && value <= GAIN_MAX ); switch( mode ) { case LINEARITY: for( Gain gain: getLinearityGains() ) { if( gain.getValue() == value ) { return gain; } } return LINEARITY_GAIN_DEFAULT; case SENSITIVITY: for( Gain gain: getSensitivityGains() ) { if( gain.getValue() == value ) { return gain; } } return SENSITIVITY_GAIN_DEFAULT; case CUSTOM: default: return Gain.CUSTOM; } } public static GainMode getGainMode( Gain gain ) { if( gain == CUSTOM ) { return GainMode.CUSTOM; } else if( getLinearityGains().contains( gain ) ) { return GainMode.LINEARITY; } else if( getSensitivityGains().contains( gain ) ) { return GainMode.SENSITIVITY; } return GainMode.CUSTOM; } public static EnumSet getLinearityGains() { return EnumSet.range( LINEARITY_1, LINEARITY_22 ); } public static EnumSet getSensitivityGains() { return EnumSet.range( SENSITIVITY_1, SENSITIVITY_22 ); } } /** * Airspy Board Identifier */ public enum BoardID { AIRSPY( 0, "Airspy" ), UNKNOWN( -1, "Unknown" ); private int mValue; private String mLabel; private BoardID( int value, String label ) { mValue = value; mLabel = label; } public int getValue() { return mValue; } public String getLabel() { return mLabel; } public static BoardID fromValue( int value ) { if( value == 0 ) { return AIRSPY; } return UNKNOWN; } } /** * Airspy Commands */ public enum Command { INVALID( 0 ), RECEIVER_MODE( 1 ), SI5351C_WRITE( 2 ), SI5351C_READ( 3 ), R820T_WRITE( 4 ), R820T_READ( 5 ), SPIFLASH_ERASE( 6 ), SPIFLASH_WRITE( 7 ), SPIFLASH_READ( 8 ), BOARD_ID_READ( 9 ), VERSION_STRING_READ( 10 ), BOARD_PART_ID_SERIAL_NUMBER_READ( 11 ), SET_SAMPLE_RATE( 12 ), SET_FREQUENCY( 13 ), SET_LNA_GAIN( 14 ), SET_MIXER_GAIN( 15 ), SET_VGA_GAIN( 16 ), SET_LNA_AGC( 17 ), SET_MIXER_AGC( 18 ), MS_VENDOR_COMMAND( 19 ), SET_RF_BIAS_COMMAND( 20 ), GPIO_WRITE( 21 ), GPIO_READ( 22 ), GPIO_DIR__WRITE( 23 ), GPIO_DIR_READ( 24 ), GET_SAMPLE_RATES( 25 ), SET_PACKING( 26 ); private int mValue; private Command( int value ) { mValue = value; } public byte getValue() { return (byte)mValue; } public static Command fromValue( int value ) { if( 0 <= value && value <= 25 ) { return Command.values()[ value ]; } return INVALID; } } public enum ReceiverMode { OFF( 0 ), ON( 1 ); private int mValue; private ReceiverMode( int value ) { mValue = value; } public int getValue() { return mValue; } } /** * General Purpose Input/Output Ports (accessible on the airspy board) */ public enum GPIOPort { PORT_0( 0 ), PORT_1( 1 ), PORT_2( 2 ), PORT_3( 3 ), PORT_4( 4 ), PORT_5( 5 ), PORT_6( 6 ), PORT_7( 7 ); private int mValue; private GPIOPort( int value ) { mValue = value; } public int getValue() { return mValue; } } /** * General Purpose Input/Output Pins (accessible on the airspy board) */ public enum GPIOPin { PIN_0( 0 ), PIN_1( 1 ), PIN_2( 2 ), PIN_3( 3 ), PIN_4( 4 ), PIN_5( 5 ), PIN_6( 6 ), PIN_7( 7 ), PIN_8( 8 ), PIN_9( 9 ), PIN_10( 10 ), PIN_11( 11 ), PIN_12( 12 ), PIN_13( 13 ), PIN_14( 14 ), PIN_15( 15 ), PIN_16( 16 ), PIN_17( 17 ), PIN_18( 18 ), PIN_19( 19 ), PIN_20( 20 ), PIN_21( 21 ), PIN_22( 22 ), PIN_23( 23 ), PIN_24( 24 ), PIN_25( 25 ), PIN_26( 26 ), PIN_27( 27 ), PIN_28( 28 ), PIN_29( 29 ), PIN_30( 30 ), PIN_31( 31 ); private int mValue; private GPIOPin( int value ) { mValue = value; } public int getValue() { return mValue; } } /** * Adds a sample listener. If the buffer processing thread is * not currently running, starts it running in a new thread. */ public void addListener( Listener listener ) { synchronized ( mComplexBufferBroadcaster ) { mComplexBufferBroadcaster.addListener( listener ); if( mBufferProcessor == null || !mBufferProcessor.isRunning() ) { mBufferProcessor = new BufferProcessor(); Thread thread = new Thread( mBufferProcessor ); thread.setDaemon( true ); thread.setName( "Airspy Buffer Processor" ); thread.start(); } } } /** * Removes the sample listener. If this is the last registered listener, * shuts down the buffer processing thread. */ public void removeListener( Listener listener ) { synchronized ( mComplexBufferBroadcaster ) { mComplexBufferBroadcaster.removeListener( listener ); if( !mComplexBufferBroadcaster.hasListeners() ) { mBufferProcessor.stop(); } } } /** * Buffer processing thread. Fetches samples from the Airspy Tuner and * dispatches them to all registered listeners */ public class BufferProcessor implements Runnable, TransferCallback { private ScheduledFuture mSampleDispatcherTask; private LinkedTransferQueue mAvailableTransfers; private LinkedTransferQueue mTransfersInProgress = new LinkedTransferQueue<>(); private AtomicBoolean mRunning = new AtomicBoolean(); private ByteBuffer mLibUsbHandlerStatus; private boolean mCancel = false; @Override public void run() { if( mRunning.compareAndSet( false, true ) ) { prepareTransfers(); mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate( ThreadType.SOURCE_SAMPLE_PROCESSING, new BufferDispatcher(), 20, TimeUnit.MILLISECONDS ); mLibUsbHandlerStatus = ByteBuffer.allocateDirect( 4 ); List transfers = new ArrayList<>(); mCancel = false; while( mRunning.get() ) { mAvailableTransfers.drainTo( transfers ); for( Transfer transfer: transfers ) { int result = LibUsb.submitTransfer( transfer ); if( result == LibUsb.SUCCESS ) { mTransfersInProgress.add( transfer ); } else { mLog.error( "error submitting transfer [" + LibUsb.errorName( result ) + "]" ); } } int result = LibUsb.handleEventsTimeoutCompleted( null, USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() ); if( result != LibUsb.SUCCESS ) { mLog.error( "error handling events for libusb" ); } transfers.clear(); mLibUsbHandlerStatus.rewind(); } if( mCancel ) { for( Transfer transfer: mTransfersInProgress ) { LibUsb.cancelTransfer( transfer ); } int result = LibUsb.handleEventsTimeoutCompleted( null, USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() ); if( result != LibUsb.SUCCESS ) { mLog.error( "error handling events for libusb during cancel" ); } mLibUsbHandlerStatus.rewind(); } } } /** * Stops the sample fetching thread */ public void stop() { mCancel = true; if( mRunning.compareAndSet( true, false ) ) { if( mSampleDispatcherTask != null ) { mSampleDispatcherTask.cancel( true ); mFilledBuffers.clear(); } } } /** * Indicates if this thread is running */ public boolean isRunning() { return mRunning.get(); } @Override public void processTransfer( Transfer transfer ) { mTransfersInProgress.remove( transfer ); switch( transfer.status() ) { case LibUsb.TRANSFER_COMPLETED: case LibUsb.TRANSFER_STALL: if( transfer.actualLength() > 0 ) { ByteBuffer buffer = transfer.buffer(); byte[] data = new byte[ transfer.actualLength() ]; buffer.get( data ); buffer.rewind(); if( isRunning() ) { mFilledBuffers.add( data ); } } break; case LibUsb.TRANSFER_CANCELLED: break; default: /* unexpected error */ mLog.error( "transfer error [" + getTransferStatus( transfer.status() ) + "] transferred actual: " + transfer.actualLength() ); } mAvailableTransfers.add( transfer ); } /** * Prepares (allocates) a set of transfer buffers for use in * transferring data from the tuner via the bulk interface */ private void prepareTransfers() throws LibUsbException { if( mAvailableTransfers == null ) { mAvailableTransfers = new LinkedTransferQueue<>(); for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ ) { Transfer transfer = LibUsb.allocTransfer(); if( transfer == null ) { throw new LibUsbException( "couldn't allocate transfer", LibUsb.ERROR_NO_MEM ); } final ByteBuffer buffer = ByteBuffer.allocateDirect( mBufferSize ); LibUsb.fillBulkTransfer( transfer, mDeviceHandle, USB_ENDPOINT, buffer, BufferProcessor.this, "Buffer", USB_TIMEOUT_MS ); mAvailableTransfers.add( transfer ); } } } } /** * 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 { @Override public void run() { try { ArrayList buffers = new ArrayList<>(); mFilledBuffers.drainTo( buffers ); for( byte[] buffer: buffers ) { float[] realSamples = mSampleAdapter.convert( buffer ); realSamples = mDCFilter.filter( realSamples ); float[] quadratureSamples = mHilbertTransform.filter( realSamples ); mComplexBufferBroadcaster.broadcast( new ComplexBuffer( quadratureSamples ) ); } } catch( Exception e ) { mLog.error( "error during Airspy buffer dispatching", e ); } } } }