mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
* #1455 Add support for airspy hf+/discovery tuners * #1455 testing updates --------- Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
65
src/main/java/io/github/dsheirer/util/ByteUtil.java
Normal file
65
src/main/java/io/github/dsheirer/util/ByteUtil.java
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user