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