content: /*******************************************************************************
   *     SDR Trunk 
   *     Copyright (C) 2014 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 device.fcd;
  
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.nio.ByteOrder;
  import java.nio.IntBuffer;
  
  import javax.usb.UsbClaimException;
  import javax.usb.UsbException;
  
  import org.usb4java.Device;
  import org.usb4java.DeviceDescriptor;
  import org.usb4java.DeviceHandle;
  import org.usb4java.LibUsb;
  import org.usb4java.LibUsbException;
  
  import common.Log;
  import device.DeviceException;
  import device.fcd.FCD1TunerController.Block;
  
  public abstract class FCDTunerController extends device.TunerController
  {
  	public static final double USABLE_BANDWIDTH_PERCENT = 1.0;
  	public static final int DC_SPIKE_AVOID_BUFFER = 5000;
  	
  	public final static byte FCD_INTERFACE = (byte)0x2;
  	public final static byte FCD_ENDPOINT_IN = (byte)0x82;
  	public final static byte FCD_ENDPOINT_OUT = (byte)0x2;
  	
  	private Device mDevice;
  	private DeviceDescriptor mDeviceDescriptor;
  	private DeviceHandle mDeviceHandle;
  	
  	private FCDConfiguration mConfiguration = new FCDConfiguration();
  	
  	/**
  	 * Generic FCD tuner controller - contains functionality common across both
  	 * funcube dongle tuners 
  	 * 
  	 * @param device
  	 * @param descriptor
  	 * @param minTunableFrequency
  	 * @param maxTunableFrequency
  	 * @throws DeviceException 
  	 */
  	public FCDTunerController( String name, Device device, 
  							   DeviceDescriptor descriptor,
  							   int sampleRate,
  							   long minTunableFrequency,
  							   long maxTunableFrequency ) throws DeviceException {
  		super(name, minTunableFrequency, maxTunableFrequency);
  		
  		SAMPLE_RATE = sampleRate;
  		mDevice = device;
  		mDeviceDescriptor = descriptor;
  	}
  
  	
  	public void setMixerGain( boolean enabled ) throws DeviceException {
  		try {
          	send( FCDCommand.APP_SET_MIXER_GAIN, enabled ? 1 : 0 );
  		}
          catch ( Exception e ) {
          	Log.println( "error while setting Mixer Gain: " + e.getMessage() );
          	throw e;
          }
  	}
  	public boolean getMixerGain() throws DeviceException {
  		try {
          	ByteBuffer buffer = send( FCDCommand.APP_GET_MIXER_GAIN );
  			buffer.order( ByteOrder.LITTLE_ENDIAN );
  			int ret = buffer.get(2);
  			if (ret > 0 )
  				return true;
          }
          catch ( Exception e ) {
          	Log.println( "error while getting Mixer Gain: " + e.getMessage() );
          	throw e;
          }
  		return false;
  	}
  	
  	public boolean isConnected() { if (mDevice !=null) return true; return false; }
  
  	
  	/**
  	 * Initializes the controller by opening the USB device and claiming the
  	 * HID interface.
  	 * 
  	 * Invoke this method after constructing this class to setup the
  	 * controller.
  	 * 
  	 * @throws DeviceException if cannot open and claim the USB device
  	 */
  	public void init() throws DeviceException {
  		mDeviceHandle = new DeviceHandle();
  		
  		int result = LibUsb.open( mDevice, mDeviceHandle );
  
  		if( result != LibUsb.SUCCESS ) {
  			throw new DeviceException( "libusb couldn't open funcube usb "
  					+ "device [" + LibUsb.errorName( result ) + "]" );
  
  		}
  
  		claimInterface();
  	}
  
  	/**
  	 * Disposes of resources.  Closes the USB device and interface.
  	 */
  	public void dispose()
  	{
  		if( mDeviceHandle != null )
  		{
  			try
  			{
  				LibUsb.close( mDeviceHandle );
  			}
  			catch( Exception e )
  			{
  				Log.errorDialog( "error while closing device handle", e.getMessage() );
  			}
  			
  			mDeviceHandle = null;
  		}
  		
  		mDeviceDescriptor = null;
  		mDevice = null;
  	}
  
  	/**
  	 * Sample rate of the tuner
  	 */
  	public abstract int getCurrentSampleRate() throws DeviceException;
  	
  	/**
  	 * USB address (bus/port)
  	 */
  	public String getUSBAddress()
  	{
  		if( mDevice != null )
  		{
  			StringBuilder sb = new StringBuilder();
  			
  			sb.append( "Bus:" );
  			int bus = LibUsb.getBusNumber( mDevice );
  			sb.append( bus );
  			
  			sb.append( " Port:" );
  			int port = LibUsb.getPortNumber( mDevice );
  			sb.append( port );
  			
  			return sb.toString();
  		}
  
  		return "UNKNOWN";
  	}
  
  	/**
  	 * USB Vendor and Product ID 
  	 */
  	public String getUSBID()
  	{
  		if( mDeviceDescriptor != null )
  		{
  			StringBuilder sb = new StringBuilder();
  			
  			sb.append( String.format( "%04X", 
  					(int)( mDeviceDescriptor.idVendor() & 0xFFFF ) ) );
  
  			sb.append( ":" );
  			
  			sb.append( String.format( "%04X", 
  					(int)( mDeviceDescriptor.idProduct() & 0xFFFF ) ) );
  
  			return sb.toString();
  		}
  
  		return "UNKNOWN";
  	}
  
  	/**
  	 * USB Port Speed.  Should be 2.0 for both types of funcube dongles
  	 */
  	public String getUSBSpeed()
  	{
  		if( mDevice != null )
  		{
  			int speed = LibUsb.getDeviceSpeed( mDevice );
  
  			switch( speed )
  			{
  				case 0:
  					return "1.1 LOW";
  				case 1:
  					return "1.1 FULL";
  				case 2:
  					return "2.0 HIGH";
  				case 3:
  					return "3.0 SUPER";
  				default:
  			}
  		}
  		
  		return "UNKNOWN";
  	}
  	
  	/**
  	 * Set fcd interface mode
  	 */
  	public void setFCDMode( Mode mode ) throws UsbException, UsbClaimException
  	{
  		ByteBuffer response = null;
  		
  		switch( mode )
  		{
  			case APPLICATION:
  				response = send( FCDCommand.BL_QUERY );
  				break;
  			case BOOTLOADER:
  				response = send( FCDCommand.APP_RESET );
  				break;
  			default:
  				break;
  		}
  		
  		if( response != null )
  		{
  			mConfiguration.set( response );
  		}
  		else
  		{
  			mConfiguration.setModeUnknown();
  		}
  	}
  
  	/**
  	 * Sets the actual (uncorrected) device frequency
  	 */
  	public int setFrequency( long frequency ) throws DeviceException
  	{
  		try {
  			send( FCDCommand.APP_SET_FREQUENCY_HZ, frequency );
  		}
  		catch( Exception e ) {
  			Log.println( "Couldn't set FCD Local " +
  					"Oscillator Frequency [" + frequency + "]" +e.getMessage() );
  			throw e;
  		}
  		return 0;
  	}
  
  	/**
  	 * Gets the actual (uncorrected) device frequency
  	 */
  	public long getTunedFrequency() throws DeviceException
  	{
  		try {
  			ByteBuffer buffer = send( FCDCommand.APP_GET_FREQUENCY_HZ );
  			
  			buffer.order( ByteOrder.LITTLE_ENDIAN );
  			
  			return (int)( buffer.getInt( 2 ) & 0xFFFFFFFF );
  		}
  		catch( Exception e ) {
  			Log.println( "FCDTunerController - "
  					+ "couldn't get LO frequency" + e.getMessage() );
  			throw e;
  		}
  	}
  	
  	/**
  	 * Returns the FCD device configuration
  	 */
  	public FCDConfiguration getConfiguration()
  	{
  		return mConfiguration;
  	}
  
  	/**
  	 * Claims the USB interface.  Attempts to detach the active kernel driver if
  	 * one is currently attached.
  	 */
  	private void claimInterface() throws DeviceException
  	{
  		if( mDeviceHandle != null )
  		{
  			int result = LibUsb.kernelDriverActive( mDeviceHandle, FCD_INTERFACE );
  			
  			if( result == 1 )
  			{
  				result = LibUsb.detachKernelDriver( mDeviceHandle, 
  						FCD_INTERFACE );
  
  				if( result != LibUsb.SUCCESS )
  				{
  					Log.errorDialog( "ERROR", "failed attempt to detach kernel driver [" + 
  					LibUsb.errorName( result ) + "]" );
  				}
  			}
  			
  			result = LibUsb.claimInterface( mDeviceHandle, FCD_INTERFACE );
  			
  			if( result != LibUsb.SUCCESS )
  			{
  				throw new DeviceException( "couldn't claim usb interface [" + 
  					LibUsb.errorName( result ) + "]" );
  			}
  		}
  		else
  		{
  			throw new DeviceException( "couldn't claim usb hid interface - no "
  					+ "device handle" );
  		}
  	}
  
  	/**
  	 * Performs an interrupt write to the OUT endpoint.
  	 * @param buffer - direct allocated buffer.  Must be 64 bytes in length.
  	 * 
  	 * @throws LibUsbException on error
  	 */
  	private void write( ByteBuffer buffer ) throws LibUsbException
  	{
  		if( mDeviceHandle != null )
  		{
  			IntBuffer transferred = IntBuffer.allocate( 1 );
  			
  			int result = LibUsb.interruptTransfer( mDeviceHandle,
  												   FCD_ENDPOINT_OUT,
  												   buffer, 
  												   transferred,
  												   500l );
  			
  			if( result != LibUsb.SUCCESS )
  			{
  				throw new LibUsbException( "error writing byte buffer", 
  								result );
  			}
  //			else if( transferred.get() != buffer.capacity() )
  //			{
  //				throw new LibUsbException( "transferred bytes [" + 
  //						transferred.get( 0 ) + "] is not what was expected [" + 
  //						buffer.capacity() + "]", result );
  //			}
  		}
  		else
  		{
  			throw new LibUsbException( "device handle is null", 
  							LibUsb.ERROR_NO_DEVICE );
  		}
  		// Test the exception handling and retry
  //		throw new LibUsbException( "TEST error writing byte buffer", 
  //				-7 );
  	}
  	
  	/**
  	 * Performs an interrupt write to the OUT endpoint for the FCD command.
  	 * @param command - no-argument command to write
  	 * @throws LibUsbException - on error
  	 */
  	private void write( FCDCommand command ) throws LibUsbException
  	{
  		ByteBuffer buffer = ByteBuffer.allocateDirect( 64 );
  
  		buffer.put( 0, command.getCommand() );
  		buffer.put( 1, (byte)0x00 );
  
  		write( buffer );
  	}
  
  	/**
  	 * Convenience logger for debugging read/write operations
  	 */
  	@SuppressWarnings( "unused" )
      private void log( String label, ByteBuffer buffer )
  	{
  		StringBuilder sb = new StringBuilder();
  
  		sb.append( label );
  		
  		sb.append( " " );
  
  		sb.append( buffer.get( 0 ) );
  		sb.append( " | " );
  		
  		for( int x = 0; x < 64; x++ )
  		{
  			sb.append( String.format( "%02X", (int)( buffer.get( x ) & 0xFF ) ) );
  			sb.append( " " );
  		}
  		
  		Log.println( sb.toString() );
  	}
  	
  	/**
  	 * Performs an interrupt write to the OUT endpoint for the FCD command.
  	 * @param command - command to write
  	 * @param argument - value to write with the command
  	 * @throws LibUsbException - on error
  	 */
  	private void write( FCDCommand command, long argument ) throws LibUsbException
  	{
  		ByteBuffer buffer = ByteBuffer.allocateDirect( 64 );
  
  		/* The FCD expects little-endian formatted values */
  		buffer.order( ByteOrder.LITTLE_ENDIAN );
  
  		buffer.put( 0, command.getCommand() );
  		buffer.putLong( 1, argument );
  
  		write( buffer );
  	}
  	
  	/**
  	 * Performs an interrupt read against the endpoint
  	 * @return buffer read from FCD
  	 * @throws LibUsbException on error
  	 */
  	private ByteBuffer read() throws LibUsbException
  	{
  		if( mDeviceHandle != null )
  		{
  			ByteBuffer buffer = ByteBuffer.allocateDirect( 64 );
  
  			IntBuffer transferred = IntBuffer.allocate( 1 );
  			
  
  			int result = LibUsb.interruptTransfer( mDeviceHandle,
  												   FCD_ENDPOINT_IN,
  												   buffer, 
  												   transferred,
  												   500l );
  			
  			if( result != LibUsb.SUCCESS )
  			{
  				throw new LibUsbException( "error reading byte buffer", 
  								result );
  			}
  			else if( transferred.get( 0 ) != buffer.capacity() )
  			{
  				throw new LibUsbException( "received bytes [" + 
  						transferred.get( 0 ) + "] didn't match expected "
  						+ "length [" + buffer.capacity() + "]", result );
  			}
  //			throw new LibUsbException( "TEST error reading byte buffer", 
  //					-7 );
  			return buffer;
  		}
  		
  		throw new LibUsbException( "device handle is null", 
  				LibUsb.ERROR_NO_DEVICE );
  	}
  	
  	/**
  	 * Sends the FCD command and argument.  Performs a read to complete the
  	 * command.
  	 * 
  	 * @param command - command to send
  	 * @param argument - command argument to send
  	 * @throws LibUsbException - on error
  	 */
  	protected void send( FCDCommand command, long argument ) throws LibUsbException
  	{
  		write( command, argument );
  		read();
  	}
  		
  	/**
  	 * Sends the no-argument FCD command.  Performs a read to complete the
  	 * command.
  	 * 
  	 * @param command - command to send
  	 * @throws LibUsbException - on error
  	 */
  	protected ByteBuffer send( FCDCommand command ) throws LibUsbException
  	{
  		write( command );
  		return read();
  	}
  
  	/**
  	 * FCD configuration string parsing  class
  	 */
  	public class FCDConfiguration
  	{
  		private String mConfig;
  		private Mode mMode;
  
  		public FCDConfiguration()
  		{
  			mConfig = null;
  			mMode = Mode.UNKNOWN;
  		}
  		
  		private void setModeUnknown()
  		{
  			mConfig = null;
  			mMode = Mode.UNKNOWN;
  		}
  		
  		/**
  		 * Extracts the configuration string from the buffer
  		 */
  		public void set( ByteBuffer buffer )
  		{
  			if( buffer.capacity() == 64 )
  			{
  				byte[] data = new byte[ 64 ];
  				
  				for( int x = 0; x < 64; x++ )
  				{
  					data[ x ] = buffer.get( x );
  				}
  
  				mConfig = new String( data );
  				mMode = Mode.getMode( mConfig );
  			}
  			else
  			{
  				mConfig = null;
  				mMode = Mode.ERROR;
  			}
  		}
  		
  		public Mode getMode()
  		{
  			return mMode;
  		}
  		
          public FCDModel getModel()
  		{
  			FCDModel retVal = FCDModel.FUNCUBE_UNKNOWN;
  			
  			switch( mMode )
  			{
  				case APPLICATION:
  					retVal = FCDModel.getFCD( mConfig.substring( 15, 22 ) );
  					break;
  				case BOOTLOADER:
  				case UNKNOWN:
  				case ERROR:
  					break;
  			}
  			
  			return retVal;
  		}
  		
  		public Block getBandBlocking()
  		{
  			Block retVal = Block.UNKNOWN;
  			
  			switch( mMode )
  			{
  				case APPLICATION:
  					retVal = Block.getBlock( mConfig.substring( 23, 33 ).trim() );
  					break;
  				case BOOTLOADER:
  				case UNKNOWN:
  				case ERROR:
  				break;
  			}
  			
  			return retVal;
  		}
  
  		public String getFirmware()
  		{
  			String retVal = null;
  			
  			switch( mMode )
  			{
  				case APPLICATION:
  					retVal = mConfig.substring( 9, 14 );
  					break;
  				case BOOTLOADER:
  				case UNKNOWN:
  				case ERROR:
  					break;
  			}
  
  			return retVal;
  		}
  
  		public String toString()
  		{
  			return getModel().getLabel();
  		}
  	}
  	
  	public enum Mode 
  	{ 
  		APPLICATION,
  		BOOTLOADER,
  		ERROR,
  		UNKNOWN;
  
  		public static Mode getMode( String config )
  		{
  			Mode retVal = UNKNOWN;
  
  			if( config == null )
  			{
  				retVal = ERROR;
  			}
  			else
  			{
  				if( config.length() >= 8 )
  				{
  					String mode = config.substring( 2, 8 ).trim();
  
  					if( mode.equalsIgnoreCase( "FCDAPP" ) )
  					{
  						retVal = APPLICATION;
  					}
  					else if( mode.equalsIgnoreCase( "FCDBL" ) )
  					{
  						retVal = BOOTLOADER;
  					}
  				}
  			}
  			
  			return retVal;
  		}
  	}
  	
  	public void cleanup() throws IOException, DeviceException {
  		//Leave this connected for stability
  	}
  }
  