#1455 Add Airspy HF+/Discovery Tuner Support (#1524)

* #1455 Add support for airspy hf+/discovery tuners

* #1455 testing updates

---------

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2023-04-15 07:18:37 -04:00
committed by GitHub
parent df3d3b5ddc
commit f877354a11
21 changed files with 1870 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* 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
@@ -84,7 +84,7 @@ public class DcCorrectionManager
{
mDcCalculationsRemaining--;
}
//
// mLog.info("DC: " + mDecimalFormat.format(mAverageDc) +
// " RES:" + mDecimalFormat.format(residualOffsetToRemove) +
// " REM:" + mDcCalculationsRemaining);

View File

@@ -0,0 +1,132 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy.hf;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
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 extends AbstractNativeBuffer
{
public static final float SCALE = 1.0f / 32768.0f;
private short[] mInterleavedSamples;
private float mAverageDc;
/**
* Constructs an instance.
* @param timestamp of the first sample of the buffer
* @param interleavedSamples pairs of 16-bit complex samples.
*/
public AirspyHfNativeBuffer(long timestamp, float samplesPerMillisecond, float averageDc, short[] interleavedSamples)
{
super(timestamp, samplesPerMillisecond);
mInterleavedSamples = interleavedSamples;
mAverageDc = averageDc;
}
@Override
public Iterator<ComplexSamples> iterator()
{
return new IteratorComplexSamples();
}
@Override
public Iterator<InterleavedComplexSamples> iteratorInterleaved()
{
return new IteratorInterleaved();
}
@Override
public int sampleCount()
{
return mInterleavedSamples.length / 2;
}
/**
* Scalar implementation of complex samples buffer iterator
*/
public class IteratorComplexSamples implements Iterator<ComplexSamples>
{
private boolean mHasNext = true;
@Override
public boolean hasNext()
{
return mHasNext;
}
@Override
public ComplexSamples next()
{
int length = mInterleavedSamples.length / 2;
float[] i = new float[length];
float[] q = new float[length];
int index = 0;
for(int x = 0; x < mInterleavedSamples.length; x += 2)
{
index = x / 2;
//Native ordering is in Q, I, Q, I ... reverse it to I, Q, I, Q
i[index] = (mInterleavedSamples[x + 1] * SCALE) - mAverageDc;
q[index] = (mInterleavedSamples[x] * SCALE) - mAverageDc;
}
mHasNext = false;
return new ComplexSamples(i, q, getTimestamp());
}
}
/**
* Scalar implementation of interleaved sample buffer iterator.
*/
public class IteratorInterleaved implements Iterator<InterleavedComplexSamples>
{
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 += 2)
{
//Native ordering is in Q, I, Q, I ... reverse it to I, Q, I, Q
samples[x] = (mInterleavedSamples[x + 1] * SCALE) - mAverageDc;
samples[x + 1] = (mInterleavedSamples[x] * SCALE) - mAverageDc;
}
mHasNext = false;
return new InterleavedComplexSamples(samples, getTimestamp());
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy.hf;
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
import io.github.dsheirer.buffer.DcCorrectionManager;
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
{
private DcCorrectionManager mDcCorrectionManager = new DcCorrectionManager();
/**
* Constructs an instance
*/
public AirspyHfNativeBufferFactory()
{
}
/**
* Converts the samples byte buffer into a native buffer and calculates DC offset
* @param samples byte array copied from native memory
* @param timestamp of the samples
* @return constructed native buffer
*/
@Override
public INativeBuffer getBuffer(ByteBuffer samples, long timestamp)
{
ShortBuffer shortBuffer = samples.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short[] converted = new short[shortBuffer.capacity()];
shortBuffer.get(converted);
if(mDcCorrectionManager.shouldCalculateDc())
{
calculateDc(converted);
}
return new AirspyHfNativeBuffer(timestamp, getSamplesPerMillisecond(), mDcCorrectionManager.getAverageDc(), converted);
}
/**
* Calculates the average DC in the sample stream so that it can be subtracted from the samples when the
* native buffer is used.
* @param samples containing DC offset
*/
private void calculateDc(short[] samples)
{
float dcAccumulator = 0;
for(short sample: samples)
{
dcAccumulator += sample;
}
dcAccumulator /= samples.length;
mDcCorrectionManager.adjust(dcAccumulator * AirspyHfNativeBuffer.SCALE);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* 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
@@ -26,6 +26,7 @@ import java.util.EnumSet;
public enum TunerClass
{
AIRSPY("Airspy"),
AIRSPY_HF("Airspy HF+"),
FUNCUBE_DONGLE_PRO("Funcube Dongle Pro" ),
FUNCUBE_DONGLE_PRO_PLUS("Funcube Dongle Pro+" ),
HACKRF("HackRF" ),
@@ -52,7 +53,7 @@ public enum TunerClass
return mDescription;
}
public static final EnumSet<TunerClass> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, HACKRF, RTL2832,
public static final EnumSet<TunerClass> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, AIRSPY_HF, HACKRF, RTL2832,
FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
public static final EnumSet<TunerClass> FUNCUBE_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
@@ -122,6 +123,8 @@ public enum TunerClass
return HACKRF;
case 0x1D5060A1:
return AIRSPY;
case 0x03EB800C:
return AIRSPY_HF;
}
return TunerClass.UNKNOWN;

View File

@@ -28,6 +28,10 @@ import io.github.dsheirer.source.tuner.airspy.AirspyTuner;
import io.github.dsheirer.source.tuner.airspy.AirspyTunerConfiguration;
import io.github.dsheirer.source.tuner.airspy.AirspyTunerController;
import io.github.dsheirer.source.tuner.airspy.AirspyTunerEditor;
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;
@@ -323,6 +327,8 @@ public class TunerFactory
{
case AIRSPY:
return new AirspyTuner(new AirspyTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case AIRSPY_HF:
return new AirspyHfTuner(new AirspyHfTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case FUNCUBE_DONGLE_PRO:
TargetDataLine tdl1 = MixerManager.getTunerTargetDataLine(MixerTunerType.FUNCUBE_DONGLE_PRO);
if(tdl1 != null)
@@ -372,6 +378,8 @@ public class TunerFactory
{
switch(type)
{
case AIRSPY_HF_PLUS:
return new AirspyHfTunerConfiguration(uniqueID);
case AIRSPY_R820T:
return new AirspyTunerConfiguration(uniqueID);
case ELONICS_E4000:
@@ -415,6 +423,8 @@ public class TunerFactory
{
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:

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* 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
@@ -26,6 +26,7 @@ import java.util.EnumSet;
public enum TunerType
{
AIRSPY_R820T("R820T"),
AIRSPY_HF_PLUS("HF+"),
ELONICS_E4000("E4000"),
FCI_FC2580("FC2580"),
FITIPOWER_FC0012("FC0012"),

View File

@@ -0,0 +1,77 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.airspy.hf;
import java.text.DecimalFormat;
/**
* Airspy HF sample rate.
*/
public class AirspyHfSampleRate
{
private static final DecimalFormat KILOHERTZ_FORMATTER = new DecimalFormat("0.0");
private int mIndex;
private int mSampleRate;
private boolean mLowIf;
/**
* Constructs an instance
* @param sampleRate in Hertz
* @param lowIf true if Low IF (LIF) or (default) false if Zero IF (ZIF)
*/
public AirspyHfSampleRate(int index, int sampleRate, boolean lowIf)
{
mIndex = index;
mSampleRate = sampleRate;
mLowIf = lowIf;
}
/**
* Index for the sample rate in the tuner's sample rate structure.
*/
public short getIndex()
{
return (short)mIndex;
}
/**
* Value of sample rate
* @return rate in Hertz
*/
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 mLowIf;
}
@Override
public String toString()
{
return KILOHERTZ_FORMATTER.format(mSampleRate / 1E3) + " kHz " + (isLowIf() ? "(Low IF)" : "(Zero IF)");
}
}

View File

@@ -0,0 +1,85 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.airspy.hf;
import io.github.dsheirer.preference.source.ChannelizerType;
import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.Tuner;
import io.github.dsheirer.source.tuner.TunerClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Airspy HF+Tuner
*/
public class AirspyHfTuner extends Tuner
{
private final static Logger mLog = LoggerFactory.getLogger(AirspyHfTuner.class);
/**
* Constructs an instance
* @param controller for the Airspy HF
* @param tunerErrorListener to listen for errors from this tuner
* @param channelizerType for the channelizer
*/
public AirspyHfTuner(AirspyHfTunerController controller, ITunerErrorListener tunerErrorListener,
ChannelizerType channelizerType)
{
super(controller, tunerErrorListener, channelizerType);
}
@Override
public String getPreferredName()
{
return "Airspy HF+";
}
/**
* Airspy tuner controller
*/
public AirspyHfTunerController getController()
{
return (AirspyHfTunerController)getTunerController();
}
@Override
public String getUniqueID()
{
return getController().getSerialNumber();
}
@Override
public TunerClass getTunerClass()
{
return TunerClass.AIRSPY_HF;
}
@Override
public double getSampleSize()
{
return 18.0;
}
@Override
public int getMaximumUSBBitsPerSecond()
{
//4-bytes per sample = 32 bits times 912 kSps = 29,184,000 bits per second
return 29_184_000;
}
}

View File

@@ -0,0 +1,154 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.airspy.hf;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
/**
* Tuner configuration for Airspy HF+ & Discovery tuners
*/
public class AirspyHfTunerConfiguration extends TunerConfiguration
{
private int mSampleRate;
private boolean mAgc;
private boolean mLna;
private int mAttenuation;
/**
* Default constructor for JAXB
*/
public AirspyHfTunerConfiguration()
{
}
/**
* Constructs an instance for the unique ID.
* @param uniqueID of the tuner (ie serial number)
*/
public AirspyHfTunerConfiguration(String uniqueID)
{
super(uniqueID);
}
@Override
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
public TunerType getTunerType()
{
return TunerType.AIRSPY_HF_PLUS;
}
/**
* Saved sample rate setting
* @return sample rate in Hertz
*/
@JacksonXmlProperty(isAttribute = true, localName = "sample_rate")
public int getSampleRate()
{
return mSampleRate;
}
/**
* Sets the sample rate
* @param sampleRate in Hertz
*/
public void setSampleRate(int sampleRate)
{
mSampleRate = sampleRate;
}
/**
* Indicates if the AGC is enabled
* @return AGC enabled
*/
@JacksonXmlProperty(isAttribute = true, localName = "agc")
public boolean isAgc()
{
return mAgc;
}
/**
* Sets the AGC enabled state.
* @param agc enabled
*/
public void setAgc(boolean agc)
{
mAgc = agc;
}
/**
* Indicates if the LNA is enabled
* @return LNA enabled state
*/
@JacksonXmlProperty(isAttribute = true, localName = "lna")
public boolean isLna()
{
return mLna;
}
/**
* Sets the LNA enabled state
* @param lna enabled
*/
public void setLna(boolean lna)
{
mLna = lna;
}
/**
* Attenuation setting.
* @return attenuation setting index value.
*/
@JacksonXmlProperty(isAttribute = true, localName = "attenuation")
public int getAttenuationValue()
{
return mAttenuation;
}
/**
* Sets the attenuation index value.
* @param attenuation
*/
public void setAttenuationValue(int attenuation)
{
mAttenuation = attenuation;
}
/**
* Attenuation setting
* @return attenuation
*/
@JsonIgnore
public Attenuation getAttenuation()
{
return Attenuation.fromValue(getAttenuationValue());
}
/**
* Sets the attentuation
* @param attenuation to set
*/
@JsonIgnore
public void setAttenuation(Attenuation attenuation)
{
setAttenuationValue(attenuation.getValue());
}
}

View File

@@ -0,0 +1,668 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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 java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.LibUsb;
/**
* 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(0, 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 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<AirspyHfSampleRate> 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;
private static final int CALIBRATION_MAGIC = 0xA5CA71B0;
private int mCalibrationRecordMagicNumber;
private int mCalibrationRecordPPB;
private int mCalibrationRecordVctcxo;
/**
* 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);
setMinimumFrequency(MINIMUM_FREQUENCY_HZ);
setMaximumFrequency(MAXIMUM_FREQUENCY_HZ);
setUsableBandwidthPercentage(.9); //90% usable after filter rolloff
setMiddleUnusableHalfBandwidth(3000);
}
@Override
public void apply(TunerConfiguration config) throws SourceException
{
super.apply(config);
if(config instanceof AirspyHfTunerConfiguration airspyConfig)
{
int sampleRate = airspyConfig.getSampleRate();
boolean found = false;
//The tuner supports several sample rates, but in testing the only rate that seems to function
//correctly is the 912kHz rate, which should be index 0.
// for(AirspyHfSampleRate rate: getAvailableSampleRates())
// {
// if(rate.getSampleRate() == sampleRate)
// {
// setSampleRate(rate);
// found = true;
// break;
// }
// }
if(!found)
{
setSampleRate(getAvailableSampleRates().get(0));
}
try
{
setAgc(airspyConfig.isAgc());
}
catch(IOException ioe)
{
mLog.error("Error setting AGC enabled [" + airspyConfig.isAgc() + "]");
}
try
{
setLna(airspyConfig.isLna());
}
catch(IOException ioe)
{
mLog.error("Error setting LNA enabled [" + airspyConfig.isLna() + "]");
}
try
{
setAttenuation(airspyConfig.getAttenuation());
}
catch(IOException ioe)
{
mLog.error("Error setting attenuation [" + airspyConfig.getAttenuation() + "]");
}
}
}
@Override
public int getBufferSampleCount()
{
return 1024;
}
@Override
public long getTunedFrequency() throws SourceException
{
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
{
double if_shift = mCurrentSampleRate.isLowIf() ? IF_SHIFT_ZIF : IF_SHIFT_LIF;
double adjusted_freq_hz = frequency * (1.0e9 + mCalibrationRecordPPB) * 1.0e-9;
int freq_khz = (int)((adjusted_freq_hz + if_shift) * 1e-3);
ByteBuffer buffer = ByteBuffer.allocateDirect(4);
buffer.putInt(freq_khz).flip();
try
{
writeData(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.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()))
{
getNativeBufferFactory().setSamplesPerMillisecond((float)sampleRate.getSampleRate() / 1000.0f);
LibUsb.clearHalt(getDeviceHandle(), USB_BULK_TRANSFER_ENDPOINT);
int status = writeIndex(Request.SET_SAMPLE_RATE, sampleRate.getIndex());
if(status != 0)
{
throw new SourceException("Unable to set Airspy HF sample rate to: " + sampleRate);
}
mFrequencyController.setSampleRate(sampleRate.getSampleRate());
getNativeBufferFactory().setSamplesPerMillisecond(sampleRate.getSampleRate() / 1000.0f);
mCurrentSampleRate = sampleRate;
//Set the frequency again to adjust for any switch between ZIF & LIF
setTunedFrequency(mTunedFrequency);
}
}
}
/**
* 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<AirspyHfSampleRate> 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()
{
return mNativeBufferFactory;
}
/**
* Byte size of USB transfer buffers
*/
@Override
protected int getTransferBufferSize()
{
return BUFFER_BYTE_SIZE;
}
/**
* Implements additional device-specific start operations.
* @throws SourceException if there is an issue
*/
@Override
protected void deviceStart() throws SourceException
{
int status = LibUsb.setInterfaceAltSetting(getDeviceHandle(), 0, 1);
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Can't set Airspy HF interface 0 alternate setting 1 - " + LibUsb.errorName(status));
}
loadBoardIdAndSerialNumber();
loadSampleRates();
try
{
loadDeviceConfig();
}
catch(IOException ioe)
{
mLog.info("Error getting device configuration and calibration info", ioe);
}
}
/**
* Implements additional device-specific stop operations.
* @throws SourceException if there is an issue
*/
@Override
protected void deviceStop()
{
try
{
setReceiverMode(false);
}
catch(Exception e)
{
mLog.error("Error setting Airspy HF tuner receiver mode to false for device stop.");
}
}
/**
* 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);
}
catch(SourceException ioe)
{
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 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 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.getRequest(), (short)0,
(short)count, buffer, 0);
if(status < 0)
{
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 write data to the tuner with a value of zero and an index of zero.
* @param request to submit
* @param dataBuffer to write
* @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 writeData(Request request, ByteBuffer dataBuffer) throws IOException
{
if(!dataBuffer.isDirect())
{
throw new IllegalArgumentException("Cannot write - must use a direct/native byte buffer");
}
int status = LibUsb.controlTransfer(getDeviceHandle(), REQUEST_TYPE_OUT, request.getRequest(), (short)0, (short)0,
dataBuffer, 0);
if(status < 0)
{
throw new IOException("Unable to complete write request [" + request.name() + "] - libusb status [" +
status + "] - " + LibUsb.errorName(status));
}
}
/**
* Writes the request with the specified value using an index of zero.
* @param request type
* @param value value to write
* @return status code of the write operation.
*/
private int writeValue(Request request, short value)
{
return LibUsb.controlTransfer(getDeviceHandle(), REQUEST_TYPE_OUT, request.getRequest(), value, (short)0,
ByteBuffer.allocateDirect(0), 0);
}
/**
* Writes the request with the specified index with a value of zero.
* @param request type
* @param index to write
* @return status code of the write operation.
*/
private int writeIndex(Request request, short index)
{
return LibUsb.controlTransfer(getDeviceHandle(), REQUEST_TYPE_OUT, request.getRequest(), (short)0, index,
ByteBuffer.allocateDirect(0), 0);
}
/**
* Sets the receiver mode to start/stop the sample stream
* @param running true to start and false to stop.
* @throws SourceException if there is an error
*/
private void setReceiverMode(boolean running) throws SourceException
{
int status = writeValue(Request.RECEIVER_MODE, running ? (short)1 : (short)0);
if(status != LibUsb.SUCCESS)
{
throw new SourceException("Unable to set receiver mode started to " + running + " - status [" + status +
"] " + LibUsb.errorName(status));
}
}
/**
* 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()
{
if(mAvailableSampleRates == null)
{
mAvailableSampleRates = new ArrayList<>();
try
{
//Read the first 4 bytes to get the count of sample rates.
byte[] bytes = read(Request.GET_SAMPLE_RATES, 0, 4);
int count = ByteUtil.toInteger(bytes, 0);
if(count > 0)
{
//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));
boolean lowIf = architectures[x] == (byte)0x1;
mAvailableSampleRates.add(new AirspyHfSampleRate(x, sampleRate, lowIf));
}
}
}
catch(Exception e)
{
mLog.error("Error reading sample rates from Airspy HF tuner", e);
}
//If we can't read sample rates from tuner, use the default sample rate.
if(mAvailableSampleRates.isEmpty())
{
mAvailableSampleRates.add(DEFAULT_SAMPLE_RATE);
}
}
}
/**
* 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
{
int status = writeValue(Request.SET_HF_AGC, enabled ? (short)1 : (short)0);
if(status != 0)
{
throw new IOException("Unable to set AGC enabled [" + enabled + "] - status code: " + status);
}
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
{
int status = writeValue(Request.SET_HF_LNA, enabled ? (short)1 : (short)0);
if(status != 0)
{
throw new IOException("Unable to set LNA enabled [" + enabled + "] - status code: " + status);
}
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
{
int status = writeValue(Request.SET_HF_ATT, attenuation.getValue());
if(status != 0)
{
throw new IOException("Unable to set attenuation [" + attenuation + "] - status code: " + status);
}
mAttenuation = attenuation;
}
/**
* Reads the device configuration from the tuner
* @return byte array with configuration contents.
*/
private void loadDeviceConfig() throws IOException
{
byte[] bytes = read(Request.CONFIG_READ, 0, 256);
IntBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
mCalibrationRecordMagicNumber = buffer.get(0);
mCalibrationRecordPPB = buffer.get(1);
mCalibrationRecordVctcxo = buffer.get(2);
}
}

View File

@@ -0,0 +1,330 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
import javax.swing.SpinnerNumberModel;
/**
* Tuner editor for Airspy HF+/Discovery tuners
*/
public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTunerConfiguration>
{
private static final Logger mLog = LoggerFactory.getLogger(AirspyHfTunerEditor.class);
private JComboBox<AirspyHfSampleRate> mSampleRateCombo;
private JComboBox<Attenuation> 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()
{
if(hasConfiguration() && !isLoading())
{
getConfiguration().setFrequency(getFrequencyControl().getFrequency());
double value = ((SpinnerNumberModel) getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue();
getConfiguration().setFrequencyCorrection(value);
getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
getConfiguration().setSampleRate((int)getTuner().getController().getSampleRate());
Attenuation attenuation = (Attenuation)getAttenuationCombo().getSelectedItem();
getConfiguration().setAttenuation(attenuation);
getConfiguration().setAgc(getAgcToggleButton().isSelected());
getConfiguration().setLna(getLnaToggleButton().isSelected());
saveConfiguration();
}
}
@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());
//Permanently disable the sample rates combo and force 912kHz usage - during dev testing, attempts to use
//the other sample rates produced inconsistent results.
getSampleRateCombo().setEnabled(false);
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<AirspyHfSampleRate> 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<Attenuation> 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;
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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 short getValue()
{
return (short)mValue;
}
@Override
public String toString()
{
return mLabel;
}
/**
* Lookup the attenuation entry from the index value.
* @param value to lookup
* @return attenuation.
*/
public static Attenuation fromValue(int value)
{
for(Attenuation attenuation: Attenuation.values())
{
if(attenuation.mValue == value)
{
return attenuation;
}
}
return Attenuation.A0;
}
}

View File

@@ -0,0 +1,48 @@
package io.github.dsheirer.source.tuner.airspy.hf;
/**
* Airspy HF models
*/
public enum BoardId
{
UNKNOWN(0, "UNKNOWN"),
HF_REV_A(1, "HF+"),
HF_DISCOVERY_REV_A(2, "HF Discovery"),
INVALID(0xFF, "INVALID");
private int mValue;
private String mLabel;
/**
* Constructs an instance
* @param value of the entry
* @param label for display
*/
BoardId(int value, String label)
{
mValue = value;
mLabel = label;
}
/**
* Lookup a board ID from a value
* @param value to lookup
* @return matching entry or INVALID
*/
public static BoardId fromValue(int value)
{
return switch(value)
{
case 0 -> UNKNOWN;
case 1 -> HF_REV_A;
case 2 -> HF_DISCOVERY_REV_A;
default -> INVALID;
};
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.airspy.hf;
/**
* Requests/Commands supported by the Airspy HF
*/
public enum Request
{
INVALID(0),
RECEIVER_MODE(1),
SET_FREQUENCY(2),
GET_SAMPLE_RATES(3),
SET_SAMPLE_RATE(4),
CONFIG_READ(5),
CONFIG_WRITE(6),
GET_SERIAL_NUMBER_BOARD_ID(7),
SET_USER_OUTPUT(8),
GET_VERSION_STRING(9),
SET_HF_AGC(10),
SET_HF_AGC_THRESHOLD(11),
SET_HF_ATT(12),
SET_HF_LNA(13),
GET_SAMPLE_RATE_ARCHITECTURES(14),
GET_FILTER_GAIN(15),
GET_FREQUENCY_DELTA(16),
SET_VCTCXO_CALIBRATION(17);
private int mValue;
/**
* Constructs an instance
* @param value of the entry
*/
Request(int value)
{
mValue = value;
}
/**
* Value associated with the entry
* @return value cast to a byte.
*/
public byte getRequest()
{
return (byte)mValue;
}
}

View File

@@ -0,0 +1,33 @@
package io.github.dsheirer.source.tuner.airspy.hf;
/**
* Responses to Airspy HF requests
*/
public enum Response
{
SUCCESS(0),
ERROR(-1),
UNSUPPORTED(-2);
private int mValue;
Response(int value)
{
mValue = value;
}
/**
* Lookup the enumeration entry from the value.
* @param value to lookup
* @return entry or UNSUPPORTED if there are no matches.
*/
public static Response fromValue(int value)
{
return switch(value)
{
case 0 -> SUCCESS;
case -1 -> ERROR;
default -> UNSUPPORTED;
};
}
}

View File

@@ -25,6 +25,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.airspy.AirspyTunerConfiguration;
import io.github.dsheirer.source.tuner.airspy.hf.AirspyHfTunerConfiguration;
import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerConfiguration;
import io.github.dsheirer.source.tuner.fcd.proplusV2.FCD2TunerConfiguration;
import io.github.dsheirer.source.tuner.hackrf.HackRFTunerConfiguration;
@@ -39,6 +40,7 @@ import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = AirspyTunerConfiguration.class, name = "airspyTunerConfiguration"),
@JsonSubTypes.Type(value = AirspyHfTunerConfiguration.class, name = "airspyHfTunerConfiguration"),
@JsonSubTypes.Type(value = E4KTunerConfiguration.class, name = "e4KTunerConfiguration"),
@JsonSubTypes.Type(value = FCD1TunerConfiguration.class, name = "fcd1TunerConfiguration"),
@JsonSubTypes.Type(value = FCD2TunerConfiguration.class, name = "fcd2TunerConfiguration"),

View File

@@ -45,13 +45,6 @@ import io.github.dsheirer.source.tuner.sdrplay.api.device.DeviceInfo;
import io.github.dsheirer.source.tuner.sdrplay.rspDuo.DiscoveredRspDuoTuner1;
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerModel;
import io.github.dsheirer.util.ThreadPool;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.Context;
@@ -62,6 +55,14 @@ import org.usb4java.HotplugCallback;
import org.usb4java.HotplugCallbackHandle;
import org.usb4java.LibUsb;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Tuner manager provides access to tuners using USB, recording, sound-card and system-daemon accessible devices. This
* manager also supports hot-plug detection and black-listing of discovered tuners so that they can be used with other

View File

@@ -367,6 +367,12 @@ public class DiscoveredTunerModel extends AbstractTableModel implements Listener
EventQueue.invokeLater(() -> fireTableRowsUpdated(row, row));
return;
}
else if(current == TunerStatus.DISABLED)
{
int row = mDiscoveredTuners.indexOf(discoveredTuner);
EventQueue.invokeLater(() -> fireTableRowsUpdated(row, row));
return;
}
if(current == TunerStatus.REMOVED)
{

View File

@@ -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 @@ public abstract class USBTunerController extends TunerController
//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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.util;
/**
* Utilities for working with bytes and byte arrays
*/
public class ByteUtil
{
/**
* Converts a byte array to an integer using big endian format.
* @param bytes containing four bytes.
* @param offset into the byte array to start parsing.
* @return signed integer value.
*/
public static int toInteger(byte[] bytes, int offset)
{
if(bytes == null || bytes.length < (offset + 4))
{
throw new IllegalArgumentException("Conversion to integer requires byte array with at least 4 bytes - " +
"length:" + bytes.length + " offset:" + offset);
}
int value = (bytes[offset + 3] & 0xFF) << 24;
value += (bytes[offset + 2] & 0xFF) << 16;
value += (bytes[offset + 1] & 0xFF) << 8;
value += (bytes[offset] & 0xFF);
return value;
}
/**
* Formats the byte array as a string of hexadecimal values.
* @param bytes to format
* @return hex string
*/
public static String toHexString(byte[] bytes)
{
StringBuilder sb = new StringBuilder();
for(byte b : bytes)
{
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}