diff --git a/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBuffer.java b/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBuffer.java
new file mode 100644
index 000000000..5df8617c0
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBuffer.java
@@ -0,0 +1,131 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 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
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.buffer.airspy.hf;
+
+import io.github.dsheirer.buffer.INativeBuffer;
+import io.github.dsheirer.sample.complex.ComplexSamples;
+import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
+import java.util.Iterator;
+
+/**
+ * Native buffer implementation for Airspy HF+ & Discovery tuners.
+ */
+public class AirspyHfNativeBuffer implements INativeBuffer
+{
+ private static final float SCALE = 1.0f / 32768.0f;
+ private long mTimestamp;
+ private short[] mInterleavedSamples;
+
+ /**
+ * Constructs an instance.
+ * @param timestamp of the first sample of the buffer
+ * @param interleavedSamples pairs of 16-bit complex samples.
+ */
+ public AirspyHfNativeBuffer(long timestamp, short[] interleavedSamples)
+ {
+ mTimestamp = timestamp;
+ mInterleavedSamples = interleavedSamples;
+ }
+
+ @Override
+ public Iterator iterator()
+ {
+ return new IteratorComplexSamples();
+ }
+
+ @Override
+ public Iterator iteratorInterleaved()
+ {
+ return new IteratorInterleaved();
+ }
+
+ @Override
+ public int sampleCount()
+ {
+ return mInterleavedSamples.length / 2;
+ }
+
+ @Override
+ public long getTimestamp()
+ {
+ return mTimestamp;
+ }
+
+ /**
+ * Scalar implementation of complex samples buffer iterator
+ */
+ public class IteratorComplexSamples implements Iterator
+ {
+ private boolean mHasNext = true;
+
+ @Override
+ public boolean hasNext()
+ {
+ return mHasNext;
+ }
+
+ @Override
+ public ComplexSamples next()
+ {
+ float[] i = new float[mInterleavedSamples.length / 2];
+ float[] q = new float[mInterleavedSamples.length / 2];
+
+ for(int x = 0; x < mInterleavedSamples.length; x += 2)
+ {
+ int index = mInterleavedSamples.length / 2;
+ i[index] = mInterleavedSamples[x] * SCALE;
+ q[index] = mInterleavedSamples[x + 1] * SCALE;
+ }
+
+ mHasNext = false;
+
+ return new ComplexSamples(i, q, mTimestamp);
+ }
+ }
+
+ /**
+ * Scalar implementation of interleaved sample buffer iterator.
+ */
+ public class IteratorInterleaved implements Iterator
+ {
+ private boolean mHasNext = true;
+
+ @Override
+ public boolean hasNext()
+ {
+ return mHasNext;
+ }
+
+ @Override
+ public InterleavedComplexSamples next()
+ {
+ float[] samples = new float[mInterleavedSamples.length];
+
+ for(int x = 0; x < mInterleavedSamples.length; x++)
+ {
+ samples[x] = mInterleavedSamples[x] * SCALE;
+ }
+
+ mHasNext = false;
+
+ return new InterleavedComplexSamples(samples, mTimestamp);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBufferFactory.java b/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBufferFactory.java
new file mode 100644
index 000000000..a1ae45c61
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBufferFactory.java
@@ -0,0 +1,50 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 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
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.buffer.airspy.hf;
+
+import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
+import io.github.dsheirer.buffer.INativeBuffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.ShortBuffer;
+
+/**
+ * Airspy HF+ native buffer factory.
+ *
+ * Note: the tuner provides 1024 samples in each transfer where samples are 16-bit interleaved complex.
+ */
+public class AirspyHfNativeBufferFactory extends AbstractNativeBufferFactory
+{
+ /**
+ * Constructs an instance
+ */
+ public AirspyHfNativeBufferFactory()
+ {
+ }
+
+ @Override
+ public INativeBuffer getBuffer(ByteBuffer samples, long timestamp)
+ {
+ ShortBuffer shortBuffer = samples.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
+ short[] converted = new short[shortBuffer.capacity() / 2];
+ shortBuffer.get(converted);
+ return new AirspyHfNativeBuffer(timestamp, converted);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java b/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java
index d543b174d..d9e9660c3 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java
@@ -31,6 +31,7 @@
import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTuner;
import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTunerConfiguration;
import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTunerController;
+import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTunerEditor;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.fcd.FCDTuner;
import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerConfiguration;
@@ -412,6 +413,8 @@ public static TunerEditor getEditor(UserPreferences userPreferences, DiscoveredT
{
case AIRSPY:
return new AirspyTunerEditor(userPreferences, tunerManager, discoveredTuner);
+ case AIRSPY_HF:
+ return new AirspyHfTunerEditor(userPreferences, tunerManager, discoveredTuner);
case FUNCUBE_DONGLE_PRO:
return new FCD1TunerEditor(userPreferences, tunerManager, discoveredTuner);
case FUNCUBE_DONGLE_PRO_PLUS:
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfSampleRate.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfSampleRate.java
index e826eaa94..607965283 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfSampleRate.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfSampleRate.java
@@ -1,3 +1,22 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 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
+ * ****************************************************************************
+ */
+
package io.github.dsheirer.source.tuner.airspy.hf;
import java.text.DecimalFormat;
@@ -8,25 +27,41 @@
public class AirspyHfSampleRate
{
private static final DecimalFormat KILOHERTZ_FORMATTER = new DecimalFormat("0.0");
- private int mValue;
+ private int mSampleRate;
+ private boolean mLowIf;
- public AirspyHfSampleRate(int value)
+ /**
+ * Constructs an instance
+ * @param sampleRate in Hertz
+ * @param lowIf true if Low IF (LIF) or (default) false if Zero IF (ZIF)
+ */
+ public AirspyHfSampleRate(int sampleRate, boolean lowIf)
{
- mValue = value;
+ mSampleRate = sampleRate;
+ mLowIf = lowIf;
}
/**
* Value of sample rate
* @return rate in Hertz
*/
- public int getValue()
+ public int getSampleRate()
+ {
+ return mSampleRate;
+ }
+
+ /**
+ * Indicates if the sample rate uses Low IF (LIF) versus Zero IF (ZIF)
+ * @return true if LIF
+ */
+ public boolean isLowIf()
{
- return mValue;
+ return mLowIf;
}
@Override
public String toString()
{
- return KILOHERTZ_FORMATTER.format(mValue / 1E3) + " kHz";
+ return KILOHERTZ_FORMATTER.format(mSampleRate / 1E3) + " kHz " + (isLowIf() ? "(Low IF)" : "(Zero IF)");
}
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTuner.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTuner.java
index afe1d0780..ca296f68d 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTuner.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTuner.java
@@ -84,13 +84,13 @@ public TunerClass getTunerClass()
@Override
public double getSampleSize()
{
- return 13.0;
+ return 18.0;
}
@Override
public int getMaximumUSBBitsPerSecond()
{
- //4-bytes per sample = 32 bits times 10 MSps = 320,000,000 bits per second
- return 320000000;
+ //4-bytes per sample = 32 bits times 912 kSps = 29,184,000 bits per second
+ return 29_184_000;
}
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java
index d2640e635..05b81dac1 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java
@@ -20,36 +20,53 @@
package io.github.dsheirer.source.tuner.airspy.hf;
import io.github.dsheirer.buffer.INativeBufferFactory;
+import io.github.dsheirer.buffer.airspy.hf.AirspyHfNativeBufferFactory;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.usb.USBTunerController;
import io.github.dsheirer.util.ByteUtil;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.usb4java.LibUsb;
-
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.usb4java.LibUsb;
/**
- * Airspy HF+ and Discovery tuner controller.
+ * Airspy HF+ and HF Discovery tuner controller.
*/
public class AirspyHfTunerController extends USBTunerController
{
private static final Logger mLog = LoggerFactory.getLogger(AirspyHfTunerController.class);
- private static final AirspyHfSampleRate DEFAULT_SAMPLE_RATE = new AirspyHfSampleRate(768_000);
+ private static final AirspyHfSampleRate DEFAULT_SAMPLE_RATE = new AirspyHfSampleRate(768_000, false);
+ private static final long IF_SHIFT_LIF = 0;
+ private static final long IF_SHIFT_ZIF = 5_000;
private static final long MINIMUM_FREQUENCY_HZ = 500_000;
private static final long MAXIMUM_FREQUENCY_HZ = 260_000_000;
private static final byte REQUEST_TYPE_IN = LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE;
private static final byte REQUEST_TYPE_OUT = LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE;
- private String mSerialNumber;
+ private static final int BUFFER_SAMPLE_COUNT = 1024;
+ private static final int BUFFER_BYTE_SIZE = BUFFER_SAMPLE_COUNT * 2 * 2; //2x 16-bit samples (4-bytes total) per complex.
+ private AirspyHfNativeBufferFactory mNativeBufferFactory = new AirspyHfNativeBufferFactory();
private List mAvailableSampleRates;
private AirspyHfSampleRate mCurrentSampleRate = DEFAULT_SAMPLE_RATE;
+ private BoardId mBoardId;
+ private String mSerialNumber;
+ private Attenuation mAttenuation = Attenuation.A0;
+ private boolean mAgcEnabled;
+ private boolean mLnaEnabled;
+ private long mTunedFrequency;
+ /**
+ * Constructs an instance
+ * @param bus for USB
+ * @param portAddress for USB
+ * @param tunerErrorListener to receive tuner error notifications.
+ */
public AirspyHfTunerController(int bus, String portAddress, ITunerErrorListener tunerErrorListener)
{
super(bus, portAddress, tunerErrorListener);
@@ -70,51 +87,191 @@ public void apply(TunerConfiguration config) throws SourceException
@Override
public int getBufferSampleCount()
{
- //TODO:
- return 0;
+ return 1024;
}
@Override
public long getTunedFrequency() throws SourceException
{
- //TODO:
- return 0;
+ return mTunedFrequency;
+ }
+
+ /**
+ * Serial number of the tuner.
+ * @return serial number
+ */
+ public String getSerialNumber()
+ {
+ return mSerialNumber;
+ }
+
+ /**
+ * Board identifier
+ * @return board ID
+ */
+ public BoardId getBoardId()
+ {
+ return mBoardId;
}
+ /**
+ * Sets the tuned frequency.
+ * @param frequency to set
+ * @throws SourceException if there is an error
+ */
@Override
public void setTunedFrequency(long frequency) throws SourceException
{
- //TODO:
+ long shiftedFrequency = frequency + (mCurrentSampleRate.isLowIf() ? IF_SHIFT_LIF : IF_SHIFT_ZIF);
+ //Convert to kHz, cast to int causing round up/down to kHz unit
+ int frequency_khz = (int)(shiftedFrequency / 1E3d);
+ ByteBuffer buffer = ByteBuffer.allocateDirect(4).order(ByteOrder.LITTLE_ENDIAN).putInt(frequency_khz);
+ buffer.flip();
+
+ try
+ {
+ write(Request.SET_FREQUENCY, buffer);
+ mTunedFrequency = frequency;
+ }
+ catch(IOException ioe)
+ {
+ mLog.info("Error setting tuned frequency to " + frequency, ioe);
+ throw new SourceException("Error setting frequency", ioe);
+ }
+
+ try
+ {
+ byte[] deltaBytes = read(Request.GET_FREQUENCY_DELTA, 0, 4);
+ int delta = ByteUtil.toInteger(deltaBytes, 0);
+
+ if(delta != 0)
+ {
+ mLog.warn("Frequency delta after setting tuned frequency [" + frequency + "] is: " + delta);
+ }
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error reading frequency delta from Airspy HF+ tuner", ioe);
+ }
}
@Override
public double getCurrentSampleRate() throws SourceException
{
loadSampleRates();
- return mCurrentSampleRate.getValue();
+ return mCurrentSampleRate.getSampleRate();
}
+ /**
+ * Access the current sample rate object.
+ * @return current sample rate.
+ */
+ public AirspyHfSampleRate getCurrentAirspySampleRate()
+ {
+ return mCurrentSampleRate;
+ }
+
+ /**
+ * Sets the sample rate for the tuner.
+ * @param sampleRate to apply.
+ */
+ public void setSampleRate(AirspyHfSampleRate sampleRate) throws SourceException
+ {
+ if(sampleRate != null)
+ {
+ if(isSupportedSampleRate(sampleRate.getSampleRate()))
+ {
+ LibUsb.clearHalt(getDeviceHandle(), USB_BULK_TRANSFER_ENDPOINT);
+ ByteBuffer buffer = ByteBuffer.allocateDirect(4)
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .putInt(sampleRate.getSampleRate())
+ .flip();
+
+ try
+ {
+ write(Request.SET_SAMPLE_RATE, buffer);
+ mFrequencyController.setSampleRate(sampleRate.getSampleRate());
+ getNativeBufferFactory().setSamplesPerMillisecond(sampleRate.getSampleRate() / 1000.0f);
+ mCurrentSampleRate = sampleRate;
+ }
+ catch(IOException ioe)
+ {
+ throw new SourceException("Unable to set samle rate to: " + sampleRate, ioe);
+ }
+ }
+ }
+ }
+
+ /**
+ * Indicates if the sample rate is supported.
+ * @param sampleRate to check
+ * @return true if the rate matches one of the available sample rates.
+ */
+ public boolean isSupportedSampleRate(int sampleRate)
+ {
+ if(mAvailableSampleRates != null && !mAvailableSampleRates.isEmpty())
+ {
+ for(AirspyHfSampleRate availableRate: mAvailableSampleRates)
+ {
+ if(availableRate.getSampleRate() == sampleRate)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Available/supported sample rates.
+ * @return available sample rates
+ * @throws IllegalStateException if the device has not yet been started
+ */
+ public List getAvailableSampleRates()
+ {
+ if(mAvailableSampleRates == null || mAvailableSampleRates.isEmpty())
+ {
+ throw new IllegalStateException("Device must be started before accessing available sample rates");
+ }
+
+ return mAvailableSampleRates;
+ }
+
+ /**
+ * Tuner type.
+ * @return for this tuner
+ */
@Override
public TunerType getTunerType()
{
return TunerType.AIRSPY_HF_PLUS;
}
+ /**
+ * Native buffer factory for creating native buffers from USB transfer buffers
+ * @return factory
+ */
@Override
protected INativeBufferFactory getNativeBufferFactory()
{
- //TODO:
- return null;
+ return mNativeBufferFactory;
}
+ /**
+ * Byte size of USB transfer buffers
+ */
@Override
protected int getTransferBufferSize()
{
- //TODO:
- return 0;
+ return BUFFER_BYTE_SIZE;
}
+ /**
+ * Implements additional device-specific start operations.
+ * @throws SourceException if there is an issue
+ */
@Override
protected void deviceStart() throws SourceException
{
@@ -125,62 +282,125 @@ protected void deviceStart() throws SourceException
throw new SourceException("Can't set Airspy HF interface 0 alternate setting 1 - " + LibUsb.errorName(status));
}
- mSerialNumber = LibUsb.getStringDescriptor(getDeviceHandle(), getDeviceDescriptor().iSerialNumber());
-
+ loadBoardIdAndSerialNumber();
loadSampleRates();
+ setSampleRate(DEFAULT_SAMPLE_RATE);
+ setTunedFrequency(mFrequencyController.getTunedFrequency());
+
+ try
+ {
+ setAgc(false);
+ setLna(false);
+ }
+ catch(IOException ioe)
+ {
+ mLog.info("Error setting AGC and LNA to false", ioe);
+ }
}
+ /**
+ * Implements additional device-specific stop operations.
+ * @throws SourceException if there is an issue
+ */
@Override
protected void deviceStop()
+ {
+ }
+
+ /**
+ * Preparation operations for starting sample stream.
+ */
+ @Override
+ protected void prepareStreaming()
+ {
+ try
+ {
+ setReceiverMode(false);
+ }
+ catch(SourceException ioe)
+ {
+ mLog.error("Error setting Airspy HF tuner receiver mode to false to reset before we start streaming.");
+ setErrorMessage("Unable to set receiver mode off");
+ return;
+ }
+
+ LibUsb.clearHalt(getDeviceHandle(), USB_BULK_TRANSFER_ENDPOINT);
+
+ try
+ {
+ setReceiverMode(true);
+ }
+ catch(SourceException ioe)
+ {
+ mLog.error("Error setting Airspy HF tuner receiver mode to true to start streaming.");
+ setErrorMessage("Unable to set receiver mode on");
+ }
+ }
+
+ /**
+ * Cleanup operations for shutting down sample stream.
+ */
+ @Override
+ protected void streamingCleanup()
{
try
{
-// setReceiverMode(false);
+ setReceiverMode(false);
}
- catch(Exception e)
+ catch(SourceException ioe)
{
- mLog.error("Error stopping device", e);
+ mLog.error("Error setting Airspy HF tuner receiver mode to false to stop streaming.");
}
}
/**
* LibUsb control transfer to request data from the tuner.
* @param request to submit
- * @param length of bytes to read
+ * @param count parameter associated with the request
+ * @param bufferLength is the byte size of the buffer to receive the read data.
* @return byte array containing the response value
* @throws IOException if there is an error or the request cannot be completed.
*/
- private byte[] read(Request request, int length, int bufferLength) throws IOException
+ private byte[] read(Request request, int count, int bufferLength) throws IOException
{
+ //Allocate a native/direct byte buffer outside of the JVM
ByteBuffer buffer = ByteBuffer.allocateDirect(bufferLength);
int status = LibUsb.controlTransfer(getDeviceHandle(), REQUEST_TYPE_IN, request.getValue(), (short)0,
- (short)length, buffer, 0);
+ (short)count, buffer, 0);
- if(status >= 0)
+ if(status < 0)
{
- byte[] result = new byte[bufferLength];
- buffer.get(result);
- return result;
+ throw new IOException("Unable to complete read request [" + request.name() + "] - libusb status [" + status +
+ "] - " + LibUsb.errorName(status));
}
- throw new IOException("Unable to complete read request [" + request.name() + "] - libusb status [" + status +
- "] - " + LibUsb.errorName(status));
+ byte[] result = new byte[bufferLength];
+ buffer.get(result);
+ return result;
}
/**
- * LibUsb control transfer to send data from the tuner.
+ * LibUsb control transfer to send data to the tuner.
* @param request to submit
- * @return byte array containing the response value
+ * @return direct byte buffer containing the value(s) to write in little-endian format
* @throws IOException if there is an error or the request cannot be completed.
*/
private void write(Request request, ByteBuffer buffer) throws IOException
{
+ if(!buffer.isDirect())
+ {
+ throw new IllegalArgumentException("Cannot write - must use a direct/native byte buffer");
+ }
+
int status = LibUsb.controlTransfer(getDeviceHandle(), REQUEST_TYPE_OUT, request.getValue(), (short)0, (short)0,
buffer, 0);
- if(status != LibUsb.SUCCESS)
+ mLog.info("Status [" + status + "] for write [" + request + "]");
+
+ if(status < 0)
{
- throw new IOException("Unable to complete write request [" + request.name() + "] - libusb status: " + status);
+ throw new IOException("Unable to complete write request [" + request.name() + "] - libusb status [" +
+ status + "] - " + LibUsb.errorName(status));
}
}
@@ -202,7 +422,40 @@ private void setReceiverMode(boolean started) throws SourceException
}
/**
- * Loads the sample rates from the tuner's firmware or uses the default sample rate of 768 kHz.
+ * Reads the board ID and serial number from the device.
+ * @throws IOException if there is an issue
+ */
+ private void loadBoardIdAndSerialNumber()
+ {
+ try
+ {
+ byte[] bytes = read(Request.GET_SERIAL_NUMBER_BOARD_ID, 0, 20);
+
+ int boardId = ByteUtil.toInteger(bytes, 0);
+ mBoardId = BoardId.fromValue(boardId);
+
+ int serial1 = ByteUtil.toInteger(bytes, 4);
+ int serial2 = ByteUtil.toInteger(bytes, 8);
+ int serial3 = ByteUtil.toInteger(bytes, 12);
+ int serial4 = ByteUtil.toInteger(bytes, 16);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(Integer.toHexString(serial1));
+ sb.append(Integer.toHexString(serial2));
+ sb.append(Integer.toHexString(serial3));
+ sb.append(Integer.toHexString(serial4));
+ mSerialNumber = sb.toString().toUpperCase();
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error reading board ID and serial number from device", ioe);
+ mBoardId = BoardId.UNKNOWN;
+ mSerialNumber = "UNKNOWN";
+ }
+ }
+
+ /**
+ * Loads the sample rates and LIF/ZIF modes from the tuner's firmware or uses the default sample rate of 768 kHz.
*/
private void loadSampleRates()
{
@@ -222,11 +475,17 @@ private void loadSampleRates()
//Read the count (again) plus the number of 4-byte integer values.
bytes = read(Request.GET_SAMPLE_RATES, count, count * 4);
+ //Read the architectures array.
+ byte[] architectures = read(Request.GET_SAMPLE_RATE_ARCHITECTURES, count, count);
+
for(int x = 0; x < count; x++)
{
int sampleRate = ByteUtil.toInteger(bytes, (x * 4));
- mAvailableSampleRates.add(new AirspyHfSampleRate(sampleRate));
+ boolean lowIf = architectures[x] == (byte)0x1;
+ mAvailableSampleRates.add(new AirspyHfSampleRate(sampleRate, lowIf));
}
+
+ mLog.info("Sample Rates: " + mAvailableSampleRates);
}
}
catch(Exception e)
@@ -241,4 +500,79 @@ private void loadSampleRates()
}
}
}
+
+ /**
+ * Indicates if the Automatic Gain Control (AGC) is enabled.
+ * @return enabled state, true or false.
+ */
+ public boolean getAgc()
+ {
+ return mAgcEnabled;
+ }
+
+ /**
+ * Sets the Automatic Gain Control (AGC)
+ * @param enabled true to turn on AGC or false to turn off AGC
+ * @throws IOException if there is an error
+ */
+ public void setAgc(boolean enabled) throws IOException
+ {
+ write(Request.SET_HF_AGC, getFlagBuffer(enabled));
+ mAgcEnabled = enabled;
+ }
+
+ /**
+ * Indicates if the Low Noise Amplifier (LNA) is enabled.
+ * @return enabled state of the LNA
+ */
+ public boolean getLna()
+ {
+ return mLnaEnabled;
+ }
+
+ /**
+ * Sets the Low Noise Amplifier (LNA)
+ * @param enabled true to turn on or false to turn off
+ * @throws IOException if there is an error
+ */
+ public void setLna(boolean enabled) throws IOException
+ {
+ write(Request.SET_HF_LNA, getFlagBuffer(enabled));
+ mLnaEnabled = enabled;
+ }
+
+ /**
+ * Current attenuation value.
+ */
+ public Attenuation getAttenuation()
+ {
+ return mAttenuation;
+ }
+
+ /**
+ * Sets the attenuation value.
+ * @param attenuation value to set: 0 - 8. Increases attenuation (ie decrease gain) by 6dB with each step.
+ * @throws IOException if there is an error
+ */
+ public void setAttenuation(Attenuation attenuation) throws IOException
+ {
+ ByteBuffer buffer = ByteBuffer.allocateDirect(1);
+ buffer.put(attenuation.getValue());
+ buffer.flip();
+ write(Request.SET_HF_ATT, buffer);
+ mAttenuation = attenuation;
+ }
+
+ /**
+ * Creates a direct byte buffer and loads the flag value where true=1 and false=0
+ * @param flag to load
+ * @return direct byte buffer.
+ */
+ private ByteBuffer getFlagBuffer(boolean flag)
+ {
+ ByteBuffer buffer = ByteBuffer.allocateDirect(1);
+ buffer.put(flag ? (byte)0x01 : (byte)0x00);
+ buffer.flip();
+ return buffer;
+ }
}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerEditor.java
new file mode 100644
index 000000000..3ab1886b2
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerEditor.java
@@ -0,0 +1,312 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 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
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.source.tuner.airspy.hf;
+
+import io.github.dsheirer.preference.UserPreferences;
+import io.github.dsheirer.source.SourceException;
+import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
+import io.github.dsheirer.source.tuner.manager.TunerManager;
+import io.github.dsheirer.source.tuner.ui.TunerEditor;
+import java.io.IOException;
+import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JToggleButton;
+
+/**
+ * Tuner editor for Airspy HF+/Discovery tuners
+ */
+public class AirspyHfTunerEditor extends TunerEditor
+{
+ private static final Logger mLog = LoggerFactory.getLogger(AirspyHfTunerEditor.class);
+ private JComboBox mSampleRateCombo;
+ private JComboBox mAttenuationCombo;
+ private JToggleButton mAgcToggleButton;
+ private JToggleButton mLnaToggleButton;
+
+ /**
+ * Constructs an instance
+ *
+ * @param userPreferences
+ * @param tunerManager for requesting configuration saves.
+ * @param discoveredTuner
+ */
+ public AirspyHfTunerEditor(UserPreferences userPreferences, TunerManager tunerManager, DiscoveredTuner discoveredTuner)
+ {
+ super(userPreferences, tunerManager, discoveredTuner);
+ init();
+ tunerStatusUpdated();
+ }
+
+ private void init()
+ {
+ setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
+ "[][][][][][][][][][][][][grow]"));
+
+ add(new JLabel("Tuner:"));
+ add(getTunerIdLabel(), "wrap");
+
+ add(new JLabel("Status:"));
+ add(getTunerStatusLabel(), "wrap");
+
+ add(getButtonPanel(), "span,align left");
+
+ add(new JSeparator(), "span,growx,push");
+
+ add(new JLabel("Frequency (MHz):"));
+ add(getFrequencyPanel(), "wrap");
+
+ add(new JLabel("Sample Rate:"));
+ add(getSampleRateCombo(), "wrap");
+
+ add(new JSeparator(), "span");
+
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.setLayout(new MigLayout("insets 0", "[left]", ""));
+ buttonPanel.add(getAgcToggleButton());
+ buttonPanel.add(getLnaToggleButton());
+ add(new JLabel(" "));
+ add(buttonPanel, "wrap");
+
+ add(new JLabel("Attenuation:"));
+ add(getAttenuationCombo(), "wrap");
+ }
+
+ @Override
+ protected void save()
+ {
+
+ }
+
+ @Override
+ protected void tunerStatusUpdated()
+ {
+ setLoading(true);
+
+ String status = getDiscoveredTuner().getTunerStatus().toString();
+ if(getDiscoveredTuner().hasErrorMessage())
+ {
+ status += " - " + getDiscoveredTuner().getErrorMessage();
+ }
+ getTunerStatusLabel().setText(status);
+ getButtonPanel().updateControls();
+ getFrequencyPanel().updateControls();
+
+ if(hasTuner())
+ {
+ getTunerIdLabel().setText("Airspy " + getTuner().getController().getBoardId() + " SER# " +
+ getTuner().getController().getSerialNumber());
+ getSampleRateCombo().setEnabled(true);
+ getSampleRateCombo().removeAllItems();
+ for(AirspyHfSampleRate sampleRate: getTuner().getController().getAvailableSampleRates())
+ {
+ getSampleRateCombo().addItem(sampleRate);
+ }
+ getAgcToggleButton().setEnabled(true);
+ getAgcToggleButton().setSelected(getTuner().getController().getAgc());
+ getLnaToggleButton().setEnabled(true);
+ getLnaToggleButton().setSelected(getTuner().getController().getLna());
+ getSampleRateCombo().setSelectedItem(getTuner().getController().getCurrentAirspySampleRate());
+ getAttenuationCombo().setEnabled(true);
+ getAttenuationCombo().setSelectedItem(getTuner().getController().getAttenuation());
+ }
+ else
+ {
+ getTunerIdLabel().setText("Airspy HF+");
+ getAgcToggleButton().setEnabled(false);
+ getAgcToggleButton().setSelected(false);
+ getLnaToggleButton().setEnabled(false);
+ getLnaToggleButton().setSelected(false);
+ getSampleRateCombo().setEnabled(false);
+ getAttenuationCombo().setEnabled(false);
+ getAttenuationCombo().setSelectedItem(Attenuation.A0);
+ }
+
+ updateSampleRateToolTip();
+
+ setLoading(false);
+ }
+
+ /**
+ * Updates the sample rate tooltip according to the tuner controller's lock state.
+ */
+ private void updateSampleRateToolTip()
+ {
+ if(hasTuner() && getTuner().getTunerController().isLockedSampleRate())
+ {
+ getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
+ }
+ else if(hasTuner())
+ {
+ getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
+ }
+ else
+ {
+ getSampleRateCombo().setToolTipText("No tuner available");
+ }
+ }
+
+ @Override
+ public void setTunerLockState(boolean locked)
+ {
+ getFrequencyPanel().updateControls();
+ getSampleRateCombo().setEnabled(!locked);
+ updateSampleRateToolTip();
+ }
+
+ /**
+ * Sample rate combo for selecting among available sample rates.
+ * @return sample rate combo
+ */
+ private JComboBox getSampleRateCombo()
+ {
+ if(mSampleRateCombo == null)
+ {
+ mSampleRateCombo = new JComboBox<>();
+ mSampleRateCombo.setEnabled(false);
+ mSampleRateCombo.addActionListener(e ->
+ {
+ if(!isLoading())
+ {
+ AirspyHfSampleRate sampleRate = (AirspyHfSampleRate)mSampleRateCombo.getSelectedItem();
+
+ if(sampleRate != null)
+ {
+ try
+ {
+ getTuner().getController().setSampleRate(sampleRate);
+ save();
+ }
+ catch(SourceException se)
+ {
+ mLog.error("Error setting Airspy Hf Sample Rate [" + sampleRate + "]", se);
+ JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
+ "Airspy Tuner Controller - couldn't apply the sample rate setting [" +
+ sampleRate + "] " + se.getLocalizedMessage());
+ }
+ }
+ }
+ });
+ }
+
+ return mSampleRateCombo;
+ }
+
+ /**
+ * Combobox with available attenuation items
+ * @return combo box
+ */
+ private JComboBox getAttenuationCombo()
+ {
+ if(mAttenuationCombo == null)
+ {
+ mAttenuationCombo = new JComboBox<>(Attenuation.values());
+ mAttenuationCombo.setEnabled(false);
+ mAttenuationCombo.addActionListener(e -> {
+ if(!isLoading())
+ {
+ Attenuation selected = (Attenuation)mAttenuationCombo.getSelectedItem();
+ try
+ {
+ getTuner().getController().setAttenuation(selected);
+ save();
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error setting Airspy Hf attenuation [" + selected + "]", ioe);
+ JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
+ "Airspy Tuner Controller - couldn't apply attenuation setting [" +
+ selected + "] " + ioe.getLocalizedMessage());
+ }
+ }
+ });
+ }
+
+ return mAttenuationCombo;
+ }
+
+ /**
+ * Automatic Gain Control (AGC) toggle button
+ */
+ private JToggleButton getAgcToggleButton()
+ {
+ if(mAgcToggleButton == null)
+ {
+ mAgcToggleButton = new JToggleButton("AGC");
+ mAgcToggleButton.setToolTipText("Automatic Gain Control");
+ mAgcToggleButton.setEnabled(false);
+ mAgcToggleButton.addActionListener(e -> {
+ if(!isLoading())
+ {
+ try
+ {
+ getTuner().getController().setAgc(mAgcToggleButton.isSelected());
+ save();
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error setting Airspy HF AGC", ioe);
+ JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
+ "Airspy HF Tuner Controller - couldn't change AGC setting" + ioe.getLocalizedMessage());
+ }
+ }
+ });
+ }
+
+ return mAgcToggleButton;
+ }
+
+ /**
+ * Low Noise Amplifier (LNA) toggle button
+ */
+ private JToggleButton getLnaToggleButton()
+ {
+ if(mLnaToggleButton == null)
+ {
+ mLnaToggleButton = new JToggleButton("LNA");
+ mLnaToggleButton.setToolTipText("Low Noise Amplifier");
+ mLnaToggleButton.setEnabled(false);
+ mLnaToggleButton.addActionListener(e -> {
+ if(!isLoading())
+ {
+ try
+ {
+ getTuner().getController().setLna(mLnaToggleButton.isSelected());
+ save();
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error setting Airspy HF LNA", ioe);
+ JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
+ "Airspy HF Tuner Controller - couldn't change LNA setting" + ioe.getLocalizedMessage());
+ }
+ }
+ });
+ }
+
+ return mLnaToggleButton;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Attenuation.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Attenuation.java
new file mode 100644
index 000000000..89634f4c6
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Attenuation.java
@@ -0,0 +1,65 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 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
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.source.tuner.airspy.hf;
+
+/**
+ * Attenuation levels for the Airspy HF+ tuner
+ */
+public enum Attenuation
+{
+ A0(0, "0 dB"),
+ A1(1, "6 dB"),
+ A2(2, "12 dB"),
+ A3(3, "18 dB"),
+ A4(4, "24 dB"),
+ A5(5, "30 dB"),
+ A6(6, "36 dB"),
+ A7(7, "42 dB"),
+ A8(8, "48 dB");
+
+ private int mValue;
+ private String mLabel;
+
+ /**
+ * Constructs an instance
+ * @param value for the attenuation level
+ * @param label to display in the UI
+ */
+ Attenuation(int value, String label)
+ {
+ mValue = value;
+ mLabel = label;
+ }
+
+ /**
+ * Byte value setting
+ * @return value
+ */
+ public byte getValue()
+ {
+ return (byte)(mValue & 0xFF);
+ }
+
+ @Override
+ public String toString()
+ {
+ return mLabel;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java
index 9e6082092..bc9aa49e3 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/rtl/r820t/R820TTunerEditor.java
@@ -123,7 +123,6 @@ private void init()
@Override
protected void tunerStatusUpdated()
{
- mLog.info("Tuner status was updated.");
setLoading(true);
if(hasTuner())
diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java
index f04074113..4dd33292a 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java
@@ -53,7 +53,7 @@ public abstract class USBTunerController extends TunerController
private static final int USB_INTERFACE = 0x0; //Common value for all currently supported devices
private static final int USB_CONFIGURATION = 0x1; //Common value for all currently supported devices
private static final int USB_BULK_TRANSFER_BUFFER_POOL_SIZE = 8;
- private static final byte USB_BULK_TRANSFER_ENDPOINT = (byte) 0x81;
+ protected static final byte USB_BULK_TRANSFER_ENDPOINT = (byte) 0x81;
private static final long USB_BULK_TRANSFER_TIMEOUT_MS = 2000l;
private int mBus;
@@ -355,9 +355,19 @@ private void stopStreaming()
//Perform final event processing iteration so LibUsb returns all of our cancelled tranfers
mEventProcessor.handleFinalEvents();
+
+ streamingCleanup();
}
}
+ /**
+ * Post streaming cleanup actions. This method can be overridden by sub-class to implement additional actions
+ * needed to cleanup after streaming stops.
+ */
+ protected void streamingCleanup()
+ {
+ }
+
/**
* Finds the USB device for this tuner at the specified USB bus and port.
* @return discovered USB device
diff --git a/src/main/java/io/github/dsheirer/util/ByteUtil.java b/src/main/java/io/github/dsheirer/util/ByteUtil.java
index e549e3788..ea24c3fc2 100644
--- a/src/main/java/io/github/dsheirer/util/ByteUtil.java
+++ b/src/main/java/io/github/dsheirer/util/ByteUtil.java
@@ -1,3 +1,22 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 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
+ * ****************************************************************************
+ */
+
package io.github.dsheirer.util;
/**