diff --git a/build.gradle b/build.gradle
index 04f07977..f2178d73 100644
--- a/build.gradle
+++ b/build.gradle
@@ -53,7 +53,7 @@ repositories {
maven { url "https://jitpack.io" }
}
-version = '0.6.0-alpha5'
+version = '0.6.0-alpha6'
//Java 19 is required for this version of the Project Panama preview/incubator feature
java {
diff --git a/src/main/java/io/github/dsheirer/buffer/DcCorrectionManager.java b/src/main/java/io/github/dsheirer/buffer/DcCorrectionManager.java
index 807ed9dc..f578f9aa 100644
--- a/src/main/java/io/github/dsheirer/buffer/DcCorrectionManager.java
+++ b/src/main/java/io/github/dsheirer/buffer/DcCorrectionManager.java
@@ -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);
diff --git a/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBuffer.java b/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBuffer.java
new file mode 100644
index 00000000..476a59dc
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBuffer.java
@@ -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
+ * ****************************************************************************
+ */
+
+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 iterator()
+ {
+ return new IteratorComplexSamples();
+ }
+
+ @Override
+ public Iterator iteratorInterleaved()
+ {
+ return new IteratorInterleaved();
+ }
+
+ @Override
+ public int sampleCount()
+ {
+ return mInterleavedSamples.length / 2;
+ }
+
+ /**
+ * Scalar implementation of complex samples buffer iterator
+ */
+ public class IteratorComplexSamples implements Iterator
+ {
+ private boolean mHasNext = true;
+
+ @Override
+ public boolean hasNext()
+ {
+ return mHasNext;
+ }
+
+ @Override
+ public ComplexSamples next()
+ {
+ 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
+ {
+ 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());
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBufferFactory.java b/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBufferFactory.java
new file mode 100644
index 00000000..8bb2f80a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/buffer/airspy/hf/AirspyHfNativeBufferFactory.java
@@ -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
+ * ****************************************************************************
+ */
+
+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);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java b/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java
index 51906891..90f2e638 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerClass.java
@@ -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 SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, HACKRF, RTL2832,
+ public static final EnumSet SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, AIRSPY_HF, HACKRF, RTL2832,
FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
public static final EnumSet 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;
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java b/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java
index 434c9d7f..c40f7fd9 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerFactory.java
@@ -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:
diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerType.java b/src/main/java/io/github/dsheirer/source/tuner/TunerType.java
index de36198e..7a0ab54b 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/TunerType.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/TunerType.java
@@ -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"),
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfSampleRate.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfSampleRate.java
new file mode 100644
index 00000000..25dc4060
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfSampleRate.java
@@ -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
+ * ****************************************************************************
+ */
+
+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)");
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTuner.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTuner.java
new file mode 100644
index 00000000..7c84dd72
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTuner.java
@@ -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
+ * ****************************************************************************
+ */
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerConfiguration.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerConfiguration.java
new file mode 100644
index 00000000..eb77bdf3
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerConfiguration.java
@@ -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
+ * ****************************************************************************
+ */
+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());
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java
new file mode 100644
index 00000000..3fbfcb79
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerController.java
@@ -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
+ * ****************************************************************************
+ */
+
+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 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 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);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerEditor.java
new file mode 100644
index 00000000..14155350
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/AirspyHfTunerEditor.java
@@ -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
+ * ****************************************************************************
+ */
+
+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
+{
+ private static final Logger mLog = LoggerFactory.getLogger(AirspyHfTunerEditor.class);
+ private JComboBox mSampleRateCombo;
+ private JComboBox mAttenuationCombo;
+ private JToggleButton mAgcToggleButton;
+ private JToggleButton mLnaToggleButton;
+
+ /**
+ * Constructs an instance
+ *
+ * @param userPreferences
+ * @param tunerManager for requesting configuration saves.
+ * @param discoveredTuner
+ */
+ public AirspyHfTunerEditor(UserPreferences userPreferences, TunerManager tunerManager, DiscoveredTuner discoveredTuner)
+ {
+ super(userPreferences, tunerManager, discoveredTuner);
+ init();
+ tunerStatusUpdated();
+ }
+
+ private void init()
+ {
+ setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
+ "[][][][][][][][][][][][][grow]"));
+
+ add(new JLabel("Tuner:"));
+ add(getTunerIdLabel(), "wrap");
+
+ add(new JLabel("Status:"));
+ add(getTunerStatusLabel(), "wrap");
+
+ add(getButtonPanel(), "span,align left");
+
+ add(new JSeparator(), "span,growx,push");
+
+ add(new JLabel("Frequency (MHz):"));
+ add(getFrequencyPanel(), "wrap");
+
+ add(new JLabel("Sample Rate:"));
+ add(getSampleRateCombo(), "wrap");
+
+ add(new JSeparator(), "span");
+
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.setLayout(new MigLayout("insets 0", "[left]", ""));
+ buttonPanel.add(getAgcToggleButton());
+ buttonPanel.add(getLnaToggleButton());
+ add(new JLabel(" "));
+ add(buttonPanel, "wrap");
+
+ add(new JLabel("Attenuation:"));
+ add(getAttenuationCombo(), "wrap");
+ }
+
+ @Override
+ protected void save()
+ {
+ 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 getSampleRateCombo()
+ {
+ if(mSampleRateCombo == null)
+ {
+ mSampleRateCombo = new JComboBox<>();
+ mSampleRateCombo.setEnabled(false);
+ mSampleRateCombo.addActionListener(e ->
+ {
+ if(!isLoading())
+ {
+ AirspyHfSampleRate sampleRate = (AirspyHfSampleRate)mSampleRateCombo.getSelectedItem();
+
+ if(sampleRate != null)
+ {
+ try
+ {
+ getTuner().getController().setSampleRate(sampleRate);
+ save();
+ }
+ catch(SourceException se)
+ {
+ mLog.error("Error setting Airspy Hf Sample Rate [" + sampleRate + "]", se);
+ JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
+ "Airspy Tuner Controller - couldn't apply the sample rate setting [" +
+ sampleRate + "] " + se.getLocalizedMessage());
+ }
+ }
+ }
+ });
+ }
+
+ return mSampleRateCombo;
+ }
+
+ /**
+ * Combobox with available attenuation items
+ * @return combo box
+ */
+ private JComboBox getAttenuationCombo()
+ {
+ if(mAttenuationCombo == null)
+ {
+ mAttenuationCombo = new JComboBox<>(Attenuation.values());
+ mAttenuationCombo.setEnabled(false);
+ mAttenuationCombo.addActionListener(e -> {
+ if(!isLoading())
+ {
+ Attenuation selected = (Attenuation)mAttenuationCombo.getSelectedItem();
+ try
+ {
+ getTuner().getController().setAttenuation(selected);
+ save();
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error setting Airspy Hf attenuation [" + selected + "]", ioe);
+ JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
+ "Airspy Tuner Controller - couldn't apply attenuation setting [" +
+ selected + "] " + ioe.getLocalizedMessage());
+ }
+ }
+ });
+ }
+
+ return mAttenuationCombo;
+ }
+
+ /**
+ * Automatic Gain Control (AGC) toggle button
+ */
+ private JToggleButton getAgcToggleButton()
+ {
+ if(mAgcToggleButton == null)
+ {
+ mAgcToggleButton = new JToggleButton("AGC");
+ mAgcToggleButton.setToolTipText("Automatic Gain Control");
+ mAgcToggleButton.setEnabled(false);
+ mAgcToggleButton.addActionListener(e -> {
+ if(!isLoading())
+ {
+ try
+ {
+ getTuner().getController().setAgc(mAgcToggleButton.isSelected());
+ save();
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error setting Airspy HF AGC", ioe);
+ JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
+ "Airspy HF Tuner Controller - couldn't change AGC setting" + ioe.getLocalizedMessage());
+ }
+ }
+ });
+ }
+
+ return mAgcToggleButton;
+ }
+
+ /**
+ * Low Noise Amplifier (LNA) toggle button
+ */
+ private JToggleButton getLnaToggleButton()
+ {
+ if(mLnaToggleButton == null)
+ {
+ mLnaToggleButton = new JToggleButton("LNA");
+ mLnaToggleButton.setToolTipText("Low Noise Amplifier");
+ mLnaToggleButton.setEnabled(false);
+ mLnaToggleButton.addActionListener(e -> {
+ if(!isLoading())
+ {
+ try
+ {
+ getTuner().getController().setLna(mLnaToggleButton.isSelected());
+ save();
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("Error setting Airspy HF LNA", ioe);
+ JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
+ "Airspy HF Tuner Controller - couldn't change LNA setting" + ioe.getLocalizedMessage());
+ }
+ }
+ });
+ }
+
+ return mLnaToggleButton;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Attenuation.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Attenuation.java
new file mode 100644
index 00000000..b970f10d
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Attenuation.java
@@ -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
+ * ****************************************************************************
+ */
+
+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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/BoardId.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/BoardId.java
new file mode 100644
index 00000000..aa15d3f3
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/BoardId.java
@@ -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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Request.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Request.java
new file mode 100644
index 00000000..236b7c37
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Request.java
@@ -0,0 +1,65 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.source.tuner.airspy.hf;
+
+/**
+ * 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;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Response.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Response.java
new file mode 100644
index 00000000..6f741c1f
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/hf/Response.java
@@ -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;
+ };
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfiguration.java b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfiguration.java
index 2827ea7d..d45c2ddf 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfiguration.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/configuration/TunerConfiguration.java
@@ -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"),
diff --git a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java
index f1e4efa3..768b996c 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/manager/TunerManager.java
@@ -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
diff --git a/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java b/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java
index a809483c..b2b9ecc4 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/ui/DiscoveredTunerModel.java
@@ -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)
{
diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java
index 1e529442..d6c2e8c7 100644
--- a/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java
+++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBTunerController.java
@@ -53,7 +53,7 @@ public abstract class USBTunerController extends TunerController
private static final int USB_INTERFACE = 0x0; //Common value for all currently supported devices
private static final int USB_CONFIGURATION = 0x1; //Common value for all currently supported devices
private static final int USB_BULK_TRANSFER_BUFFER_POOL_SIZE = 8;
- private static final byte USB_BULK_TRANSFER_ENDPOINT = (byte) 0x81;
+ protected static final byte USB_BULK_TRANSFER_ENDPOINT = (byte) 0x81;
private static final long USB_BULK_TRANSFER_TIMEOUT_MS = 2000l;
private int mBus;
@@ -355,9 +355,19 @@ 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
diff --git a/src/main/java/io/github/dsheirer/util/ByteUtil.java b/src/main/java/io/github/dsheirer/util/ByteUtil.java
new file mode 100644
index 00000000..3b630f38
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/util/ByteUtil.java
@@ -0,0 +1,65 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.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();
+ }
+}