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(); + } +}