diff --git a/src/main/java/io/github/dsheirer/gui/control/FrequencyTextField.java b/src/main/java/io/github/dsheirer/gui/control/FrequencyTextField.java new file mode 100644 index 00000000..e986f693 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/control/FrequencyTextField.java @@ -0,0 +1,192 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 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.gui.control; + +import java.awt.EventQueue; +import net.miginfocom.swing.MigLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.JFrame; +import javax.swing.JTextField; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.DocumentFilter; +import javax.swing.text.PlainDocument; + +/** + * Swing text field control for entering frequency (MHz) values. + */ +public class FrequencyTextField extends JTextField +{ + private static final Logger LOGGER = LoggerFactory.getLogger(FrequencyTextField.class); + private static final String REGEX = "[1-9][0-9]{0,3}(\\.[0-9]{0,6})?"; + private double mMinimum; + private double mMaximum; + + /** + * Constructs an instance + * + * @param minimum allowable frequency value in Hertz + * @param maximum allowable frequency value in Hertz + * @param current frequency value in Hertz + */ + public FrequencyTextField(long minimum, long maximum, long current) + { + super(8); + mMinimum = minimum / 1E6d; + mMaximum = maximum / 1E6d; + + PlainDocument document = (PlainDocument)this.getDocument(); + document.setDocumentFilter(new FrequencyFilter()); + setFrequency(current); + } + + /** + * Current frequency value + * @return frequency in Hertz + */ + public long getFrequency() + { + String value = getText(); + + if(value == null || value.isEmpty()) + { + return 0; + } + + try + { + return (long)(Double.parseDouble(getText()) * 1E6d); + } + catch(Exception e) + { + LOGGER.error("Unable to parse frequency value from text [" + value + "] " + e.getLocalizedMessage()); + } + + return 0; + } + + /** + * Sets the current frequency value + * @param frequency in Hertz + */ + public void setFrequency(long frequency) + { + double frequencyMHz = frequency / 1E6d; + + if(isValid(String.valueOf(frequencyMHz))) + { + setText(String.valueOf(frequencyMHz)); + } + else + { + LOGGER.warn("Can't set frequency [" + frequency + "Hz / " + frequencyMHz + "MHz] with current minimum [" + mMinimum + "MHz] and maximum [" + mMaximum + "MHz] limits"); + } + } + + /** + * Indicates if the value is a valid double value that is between the minimum and maximum extents. + * @param value to test + * @return true if it is valid. + */ + private boolean isValid(String value) + { + if(value == null || value.isEmpty()) + { + return true; + } + + if(value.matches(REGEX)) + { + try + { + double frequency = Double.parseDouble(value); + return mMinimum <= frequency && frequency <= mMaximum; + } + catch(NumberFormatException e) + { + return false; + } + } + + return false; + } + + /** + * Input filter for user entered values. + */ + class FrequencyFilter extends DocumentFilter + { + @Override + public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr) throws BadLocationException + { + Document doc = fb.getDocument(); + StringBuilder sb = new StringBuilder(); + sb.append(doc.getText(0, doc.getLength())); + sb.insert(offset, string); + + if(isValid(sb.toString())) + { + super.insertString(fb, offset, string, attr); + } + } + + @Override + public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException + { + Document doc = fb.getDocument(); + StringBuilder sb = new StringBuilder(); + sb.append(doc.getText(0, doc.getLength())); + sb.replace(offset, offset + length, text); + + if(isValid(sb.toString())) + { + super.replace(fb, offset, length, text, attrs); + } + } + + @Override + public void remove(FilterBypass fb, int offset, int length) throws BadLocationException + { + Document doc = fb.getDocument(); + StringBuilder sb = new StringBuilder(); + sb.append(doc.getText(0, doc.getLength())); + sb.delete(offset, offset + length); + + if(isValid(sb.toString())) + { + super.remove(fb, offset, length); + } + } + } + + public static void main(String[] args) + { + JFrame frame = new JFrame("Frequency Control Test"); + frame.setSize(300, 200); + FrequencyTextField ftf = new FrequencyTextField(20, 2_000_000_000, 101_100_000); + frame.setLayout(new MigLayout()); + frame.add(ftf); + + EventQueue.invokeLater(() -> frame.setVisible(true)); + } +} diff --git a/src/main/java/io/github/dsheirer/gui/control/IntegerFormatter.java b/src/main/java/io/github/dsheirer/gui/control/IntegerFormatter.java index 9d522e1d..1e013e23 100644 --- a/src/main/java/io/github/dsheirer/gui/control/IntegerFormatter.java +++ b/src/main/java/io/github/dsheirer/gui/control/IntegerFormatter.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -19,20 +19,18 @@ package io.github.dsheirer.gui.control; -import io.github.dsheirer.gui.playlist.alias.AliasItemEditor; +import java.util.function.UnaryOperator; import javafx.scene.control.TextFormatter; import javafx.util.converter.IntegerStringConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.function.UnaryOperator; - /** * Text formatter for integer values that constrains values to specified minimum and maximum valid values. */ public class IntegerFormatter extends TextFormatter { - private static final Logger mLog = LoggerFactory.getLogger(AliasItemEditor.class); + private static final Logger mLog = LoggerFactory.getLogger(IntegerFormatter.class); /** * Constructs an instance diff --git a/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java b/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java index 7e00f853..cd5bbf77 100644 --- a/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java +++ b/src/main/java/io/github/dsheirer/gui/control/JFrequencyControl.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -434,8 +434,7 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor if(se instanceof InvalidFrequencyException ife) { - JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() + - "] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner."); + JOptionPane.showMessageDialog(this, ife.getMessage() + " for this tuner."); } } } diff --git a/src/main/java/io/github/dsheirer/gui/control/LongFormatter.java b/src/main/java/io/github/dsheirer/gui/control/LongFormatter.java new file mode 100644 index 00000000..94171233 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/control/LongFormatter.java @@ -0,0 +1,111 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 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.gui.control; + +import java.util.function.UnaryOperator; +import javafx.scene.control.TextFormatter; +import javafx.util.converter.LongStringConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Text formatter for long values that constrains values to specified minimum and maximum valid values. + */ +public class LongFormatter extends TextFormatter +{ + private static final Logger mLog = LoggerFactory.getLogger(LongFormatter.class); + + /** + * Constructs an instance + * @param minimum allowed value + * @param maximum allowed value + */ + public LongFormatter(int minimum, int maximum) + { + super(new LongStringConverter(), null, new LongFilter(minimum, maximum)); + } + + /** + * Formatted text change filter that only allows hexadecimal characters where the converted decimal value + * is also constrained within minimum and maximum valid values. + */ + public static class LongFilter implements UnaryOperator + { + private String DECIMAL_REGEX = "\\-?[0-9].*"; + private int mMinimum; + private int mMaximum; + + /** + * Constructs an instance + * @param minimum value + * @param maximum value + */ + public LongFilter(int minimum, int maximum) + { + mMinimum = minimum; + mMaximum = maximum; + } + + /** + * Indicates if the value argument is parsable as an integer, or is empty or null. + */ + private boolean isValid(String value) + { + if(value == null || value.isEmpty()) + { + return true; + } + + try + { + long parsed = Long.parseLong(value); + return mMinimum <= parsed && parsed <= mMaximum; + } + catch(Exception e) + { + //no-op + } + + return false; + } + + @Override + public Change apply(Change change) + { + //Only validate if the user added text to the control. Otherwise, allow it to go through + if(change.getText() != null) + { + String updatedText = change.getControlNewText(); + + if(updatedText == null || updatedText.isEmpty()) + { + return change; + } + + if(!updatedText.matches(DECIMAL_REGEX) || !isValid(updatedText)) + { + return null; + } + } + + return change; + } + } +} diff --git a/src/main/java/io/github/dsheirer/source/InvalidFrequencyException.java b/src/main/java/io/github/dsheirer/source/InvalidFrequencyException.java index bc30a29a..fcaebaa9 100644 --- a/src/main/java/io/github/dsheirer/source/InvalidFrequencyException.java +++ b/src/main/java/io/github/dsheirer/source/InvalidFrequencyException.java @@ -1,6 +1,6 @@ -/******************************************************************************* - * sdrtrunk - * Copyright (C) 2014-2016 Dennis Sheirer +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 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 @@ -14,8 +14,8 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see - * - ******************************************************************************/ + * **************************************************************************** + */ package io.github.dsheirer.source; public class InvalidFrequencyException extends SourceException @@ -54,11 +54,4 @@ public class InvalidFrequencyException extends SourceException { return mValidFrequency; } - - @Override - public String getMessage() - { - return super.getMessage() + " Invalid Frequency: " + getInvalidFrequency() + - " - current valid frequency:" + getValidFrequency(); - } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java index 46c8dde4..88816da9 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/TunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/TunerController.java @@ -74,6 +74,17 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, mFrequencyErrorCorrectionManager = new FrequencyErrorCorrectionManager(this); } + /** + * Updates the frequency controller with the new minimum and maximum values. + * @param minimum frequency Hertz + * @param maximum frequency Hertz + */ + public void setFrequencyExtents(long minimum, long maximum) + { + mFrequencyController.setMinimumFrequency(minimum); + mFrequencyController.setMaximumFrequency(maximum); + } + /** * Lock for the frequency controller. This should only be used by the channel source manager to lock access to the * frequency controller while creating a channel source, to block multi-threaded access to the frequency controller @@ -175,7 +186,31 @@ public abstract class TunerController implements Tunable, ISourceEventProcessor, */ public void apply(TunerConfiguration config) throws SourceException { - setFrequency(config.getFrequency()); + //Set the frequency from the config. If the config frequency generates an error, try setting the default + //frequency before we give up on the tuner as having an error. + try + { + setFrequency(config.getFrequency()); + } + catch(Exception e) + { + mLog.warn("Unable to restore previous frequency [" + config.getFrequency() + "] trying default frequency"); + if(config.getFrequency() != TunerConfiguration.DEFAULT_FREQUENCY) + { + setFrequency(TunerConfiguration.DEFAULT_FREQUENCY); + //If we're successful to here, update the config with the default frequency. + config.setFrequency(TunerConfiguration.DEFAULT_FREQUENCY); + } + } + + if(config.getMinimumFrequency() > 0) + { + setMinimumFrequency(config.getMinimumFrequency()); + } + if(config.getMaximumFrequency() > 0) + { + setMaximumFrequency(config.getMaximumFrequency()); + } setFrequencyCorrection(config.getFrequencyCorrection()); getFrequencyErrorCorrectionManager().setEnabled(config.getAutoPPMCorrectionEnabled()); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerConfiguration.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerConfiguration.java index 05248028..2aac26c4 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerConfiguration.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerConfiguration.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -38,6 +38,7 @@ public class AirspyTunerConfiguration extends TunerConfiguration */ public AirspyTunerConfiguration() { + super(AirspyTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ, AirspyTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ); } @Override diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java index 7c74f998..0faa1171 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerController.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -59,8 +59,8 @@ public class AirspyTunerController extends USBTunerController public static final int IF_GAIN_MIN = 0; public static final int IF_GAIN_MAX = 15; public static final int IF_GAIN_DEFAULT = 9; - public static final long FREQUENCY_MIN = 24000000l; - public static final long FREQUENCY_MAX = 1800000000l; + public static final long MINIMUM_TUNABLE_FREQUENCY_HZ = 24000000l; + public static final long MAXIMUM_TUNABLE_FREQUENCY_HZ = 1800000000l; public static final long FREQUENCY_DEFAULT = 101100000; public static final double USABLE_BANDWIDTH_PERCENT = 0.90; private static final long USB_TIMEOUT_MS = 2000l; //milliseconds @@ -82,7 +82,7 @@ public class AirspyTunerController extends USBTunerController */ public AirspyTunerController(int bus, String portAddress, ITunerErrorListener tunerErrorListener) { - super(bus, portAddress, FREQUENCY_MIN, FREQUENCY_MAX, 0, USABLE_BANDWIDTH_PERCENT, tunerErrorListener); + super(bus, portAddress, MINIMUM_TUNABLE_FREQUENCY_HZ, MAXIMUM_TUNABLE_FREQUENCY_HZ, 0, USABLE_BANDWIDTH_PERCENT, tunerErrorListener); } @Override @@ -233,7 +233,7 @@ public class AirspyTunerController extends USBTunerController @Override public synchronized void setTunedFrequency(long frequency) throws SourceException { - if(FREQUENCY_MIN <= frequency && frequency <= FREQUENCY_MAX) + if(MINIMUM_TUNABLE_FREQUENCY_HZ <= frequency && frequency <= MAXIMUM_TUNABLE_FREQUENCY_HZ) { ByteBuffer buffer = ByteBuffer.allocateDirect(4); buffer.order(ByteOrder.LITTLE_ENDIAN); @@ -252,7 +252,7 @@ public class AirspyTunerController extends USBTunerController } else { - throw new SourceException("Frequency [" + frequency + "] outside " + "of tunable range " + FREQUENCY_MIN + "-" + FREQUENCY_MAX); + throw new SourceException("Frequency [" + frequency + "] outside " + "of tunable range " + MINIMUM_TUNABLE_FREQUENCY_HZ + "-" + MAXIMUM_TUNABLE_FREQUENCY_HZ); } } diff --git a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java index d1f72188..ed32e804 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/airspy/AirspyTunerEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -80,6 +80,18 @@ public class AirspyTunerEditor extends TunerEditor mMaximumFrequency) + if(frequency > mMaximumFrequency) { - throw new InvalidFrequencyException("Requested frequency not valid", frequency, mMaximumFrequency); + throw new InvalidFrequencyException("Frequency [" + frequency + "] is above the maximum [" + + mMaximumFrequency + "]", frequency, mMaximumFrequency); } mFrequency = frequency; - - mTunedFrequency = tunedFrequency; + mTunedFrequency = getTunedFrequency(frequency); if(mTunable != null) { @@ -230,7 +229,7 @@ public class FrequencyController */ public void setMinimumFrequency(long minimum) { - mMaximumFrequency = minimum; + mMinimumFrequency = minimum; } /** @@ -252,15 +251,12 @@ public class FrequencyController } /** - * Calculate the tuned frequency by adding frequency correction to the - * corrected frequency. - * + * Calculate the tuned frequency by adding frequency correction to the corrected frequency. * @param correctedFrequency */ private long getTunedFrequency(long correctedFrequency) { - return (long)((double)correctedFrequency / - (1.0 + (mFrequencyCorrection / 1000000.0))); + return (long)((double)correctedFrequency / (1.0 + (mFrequencyCorrection / 1000000.0))); } /** diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerConfiguration.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerConfiguration.java index a9e328e5..9213577c 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerConfiguration.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerConfiguration.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -39,6 +39,7 @@ public class HackRFTunerConfiguration extends TunerConfiguration */ public HackRFTunerConfiguration() { + super(HackRFTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ, HackRFTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ); } public HackRFTunerConfiguration(String uniqueID) diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java index 3d20b81a..55e3313d 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerController.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2022 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -47,8 +47,8 @@ public class HackRFTunerController extends USBTunerController public static final byte REQUEST_TYPE_IN = (byte)(LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE); public static final byte REQUEST_TYPE_OUT = (byte)(LibUsb.ENDPOINT_OUT | LibUsb.REQUEST_TYPE_VENDOR | LibUsb.RECIPIENT_DEVICE); - public static final long MINIMUM_TUNABLE_FREQUENCY = 10000000l; - public static final long MAXIMUM_TUNABLE_FREQUENCY = 6000000000l; + public static final long MINIMUM_TUNABLE_FREQUENCY_HZ = 10000000l; + public static final long MAXIMUM_TUNABLE_FREQUENCY_HZ = 6000000000l; public static final long DEFAULT_FREQUENCY = 101100000; public static final double USABLE_BANDWIDTH = 0.90; public static final int DC_HALF_BANDWIDTH = 5000; @@ -64,7 +64,7 @@ public class HackRFTunerController extends USBTunerController */ public HackRFTunerController(int bus, String portAddress, ITunerErrorListener tunerErrorListener) { - super(bus, portAddress, MINIMUM_TUNABLE_FREQUENCY, MAXIMUM_TUNABLE_FREQUENCY, DC_HALF_BANDWIDTH, USABLE_BANDWIDTH, + super(bus, portAddress, MINIMUM_TUNABLE_FREQUENCY_HZ, MAXIMUM_TUNABLE_FREQUENCY_HZ, DC_HALF_BANDWIDTH, USABLE_BANDWIDTH, tunerErrorListener); } diff --git a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java index 388ff63c..4190e677 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/hackrf/HackRFTunerEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -65,6 +65,18 @@ public class HackRFTunerEditor extends TunerEditor extends TunerCon implements IDeviceEventListener, IStreamListener { private static final Logger mLog = LoggerFactory.getLogger(RspTunerController.class); - protected static final long MINIMUM_FREQUENCY = 100_000; - protected static final long MAXIMUM_FREQUENCY = 2_000_000_000; + protected static final long MINIMUM_TUNABLE_FREQUENCY_HZ = 100_000; + protected static final long MAXIMUM_TUNABLE_FREQUENCY_HZ = 2_000_000_000; protected static final int MIDDLE_UNUSABLE_BANDWIDTH = 0; private I mControlRsp; private RspNativeBufferFactory mNativeBufferFactory = new RspNativeBufferFactory(RspSampleRate.RATE_8_000); @@ -66,8 +66,8 @@ public abstract class RspTunerController extends TunerCon //Register this controller to receive device events and sample streams when startStream() is invoked. mControlRsp.resister(this, this); - setMinimumFrequency(MINIMUM_FREQUENCY); - setMaximumFrequency(MAXIMUM_FREQUENCY); + setMinimumFrequency(MINIMUM_TUNABLE_FREQUENCY_HZ); + setMaximumFrequency(MAXIMUM_TUNABLE_FREQUENCY_HZ); setMiddleUnusableHalfBandwidth(MIDDLE_UNUSABLE_BANDWIDTH); setUsableBandwidthPercentage(1.0); //Initial value } diff --git a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/RspTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/RspTunerEditor.java index 6921d37b..d70f1393 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/RspTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/RspTunerEditor.java @@ -73,6 +73,18 @@ public abstract class RspTunerEditor extends Tu return (RspTunerController) getTuner().getTunerController(); } + @Override + public long getMinimumTunableFrequency() + { + return RspTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ; + } + + @Override + public long getMaximumTunableFrequency() + { + return RspTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ; + } + /** * Gain controls panel * @return gain panel diff --git a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1/Rsp1TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1/Rsp1TunerEditor.java index ea3a5093..b8278f9b 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1/Rsp1TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1/Rsp1TunerEditor.java @@ -156,6 +156,8 @@ public class Rsp1TunerEditor extends RspTunerEditor if(hasConfiguration() && !isLoading()) { getConfiguration().setFrequency(getFrequencyControl().getFrequency()); + getConfiguration().setMinimumFrequency(getMinimumFrequencyTextField().getFrequency()); + getConfiguration().setMaximumFrequency(getMaximumFrequencyTextField().getFrequency()); double value = ((SpinnerNumberModel) getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue(); getConfiguration().setFrequencyCorrection(value); getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected()); @@ -186,6 +188,8 @@ public class Rsp1TunerEditor extends RspTunerEditor try { getTunerController().setSampleRate(selected); + //Adjust the min/max values for the sample rate. + adjustForSampleRate((int)selected.getSampleRate()); save(); } catch(SDRPlayException se) diff --git a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1a/Rsp1aTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1a/Rsp1aTunerEditor.java index 4650c13a..6cdeb1d5 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1a/Rsp1aTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1a/Rsp1aTunerEditor.java @@ -197,6 +197,8 @@ public class Rsp1aTunerEditor extends RspTunerEditor if(hasConfiguration() && !isLoading()) { getConfiguration().setFrequency(getFrequencyControl().getFrequency()); + getConfiguration().setMinimumFrequency(getMinimumFrequencyTextField().getFrequency()); + getConfiguration().setMaximumFrequency(getMaximumFrequencyTextField().getFrequency()); double value = ((SpinnerNumberModel) getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue(); getConfiguration().setFrequencyCorrection(value); getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected()); @@ -230,6 +232,8 @@ public class Rsp1aTunerEditor extends RspTunerEditor try { getTunerController().setSampleRate(selected); + //Adjust the min/max values for the sample rate. + adjustForSampleRate((int)selected.getSampleRate()); save(); } catch(SDRPlayException se) diff --git a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1b/Rsp1bTunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1b/Rsp1bTunerEditor.java index 5150db63..86d8f449 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1b/Rsp1bTunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp1b/Rsp1bTunerEditor.java @@ -196,6 +196,8 @@ public class Rsp1bTunerEditor extends RspTunerEditor if(hasConfiguration() && !isLoading()) { getConfiguration().setFrequency(getFrequencyControl().getFrequency()); + getConfiguration().setMinimumFrequency(getMinimumFrequencyTextField().getFrequency()); + getConfiguration().setMaximumFrequency(getMaximumFrequencyTextField().getFrequency()); double value = ((SpinnerNumberModel) getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue(); getConfiguration().setFrequencyCorrection(value); getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected()); @@ -229,6 +231,8 @@ public class Rsp1bTunerEditor extends RspTunerEditor try { getTunerController().setSampleRate(selected); + //Adjust the min/max values for the sample rate. + adjustForSampleRate((int)selected.getSampleRate()); save(); } catch(SDRPlayException se) diff --git a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp2/Rsp2TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp2/Rsp2TunerEditor.java index 2b483df0..b5f383a0 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp2/Rsp2TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rsp2/Rsp2TunerEditor.java @@ -211,6 +211,8 @@ public class Rsp2TunerEditor extends RspTunerEditor if(hasConfiguration() && !isLoading()) { getConfiguration().setFrequency(getFrequencyControl().getFrequency()); + getConfiguration().setMinimumFrequency(getMinimumFrequencyTextField().getFrequency()); + getConfiguration().setMaximumFrequency(getMaximumFrequencyTextField().getFrequency()); double value = ((SpinnerNumberModel) getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue(); getConfiguration().setFrequencyCorrection(value); getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected()); @@ -244,6 +246,8 @@ public class Rsp2TunerEditor extends RspTunerEditor try { getTunerController().setSampleRate(selected); + //Adjust the min/max values for the sample rate. + adjustForSampleRate((int)selected.getSampleRate()); save(); } catch(SDRPlayException se) diff --git a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rspDuo/RspDuoTuner1Editor.java b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rspDuo/RspDuoTuner1Editor.java index a4a75701..8d1d4aa1 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rspDuo/RspDuoTuner1Editor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/sdrplay/rspDuo/RspDuoTuner1Editor.java @@ -239,6 +239,8 @@ public class RspDuoTuner1Editor extends RspTunerEditor if(hasConfiguration() && !isLoading()) { getConfiguration().setFrequency(getFrequencyControl().getFrequency()); + getConfiguration().setMinimumFrequency(getMinimumFrequencyTextField().getFrequency()); + getConfiguration().setMaximumFrequency(getMaximumFrequencyTextField().getFrequency()); double value = ((SpinnerNumberModel) getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue(); getConfiguration().setFrequencyCorrection(value); getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected()); @@ -265,6 +267,8 @@ public class RspDxTunerEditor extends RspTunerEditor try { getTunerController().setSampleRate(selected); + //Adjust the min/max values for the sample rate. + adjustForSampleRate((int)selected.getSampleRate()); save(); } catch(SDRPlayException se) diff --git a/src/main/java/io/github/dsheirer/source/tuner/ui/TunerEditor.java b/src/main/java/io/github/dsheirer/source/tuner/ui/TunerEditor.java index d0d11e65..a09f1436 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/ui/TunerEditor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/ui/TunerEditor.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 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 @@ -18,6 +18,7 @@ */ package io.github.dsheirer.source.tuner.ui; +import io.github.dsheirer.gui.control.FrequencyTextField; import io.github.dsheirer.gui.control.JFrequencyControl; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.properties.SystemProperties; @@ -37,6 +38,8 @@ import io.github.dsheirer.spectrum.SpectralDisplayPanel; import io.github.dsheirer.util.SwingUtils; import io.github.dsheirer.util.ThreadPool; import java.awt.EventQueue; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; import java.text.CharacterIterator; import java.text.DecimalFormat; import java.text.StringCharacterIterator; @@ -47,6 +50,7 @@ import org.slf4j.LoggerFactory; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.JToggleButton; @@ -63,6 +67,8 @@ public abstract class TunerEditor implements IDiscoveredTunerStatusListener, Listener { private Logger mLog = LoggerFactory.getLogger(TunerEditor.class); + private static final long DEFAULT_MINIMUM_FREQUENCY = 1_000_000; + private static final long DEFAULT_MAXIMUM_FREQUENCY = 9_999_999_999l; private static final String BUTTON_STATUS_ENABLE = "Enable"; private static final String BUTTON_STATUS_DISABLE = "Disable"; private static final long serialVersionUID = 1L; @@ -86,6 +92,9 @@ public abstract class TunerEditor private JLabel mRecordingStatusLabel; private JLabel mTunerStatusLabel; private JLabel mTunerLockedStatusLabel; + private FrequencyTextField mMinimumFrequencyTextField; + private FrequencyTextField mMaximumFrequencyTextField; + private JButton mResetFrequenciesButton; private boolean mLoading = false; /** @@ -119,6 +128,32 @@ public abstract class TunerEditor } } + /** + * Minimum tunable frequency supported by the tuner. + * @return minimum frequency hertz + */ + public abstract long getMinimumTunableFrequency(); + + /** + * Maximum tunable frequency supported by the tuner. + * @return maximum frequency hertz + */ + public abstract long getMaximumTunableFrequency(); + + /** + * Current sample rate for the tuner. + * @return sample rate in hertz. + */ + public int getCurrentSampleRate() + { + if(hasTuner()) + { + return (int)getTuner().getTunerController().getSampleRate(); + } + + return 0; + } + /**r * Indicates if the controls are currently being loaded with values. */ @@ -284,6 +319,235 @@ public abstract class TunerEditor return mFrequencyControl; } + /** + * Minimum frequency value text field + */ + protected FrequencyTextField getMinimumFrequencyTextField() + { + if(mMinimumFrequencyTextField == null) + { + mMinimumFrequencyTextField = new FrequencyTextField(DEFAULT_MINIMUM_FREQUENCY, DEFAULT_MAXIMUM_FREQUENCY, + getMinimumTunableFrequency()); + mMinimumFrequencyTextField.setToolTipText("Sets or changes the minimum frequency value that this tuner will support."); + mMinimumFrequencyTextField.addFocusListener(new FocusListener() + { + private long mExistingFrequency; + + @Override + public void focusGained(FocusEvent e) + { + mExistingFrequency = getMinimumFrequencyTextField().getFrequency(); + } + + @Override + public void focusLost(FocusEvent e) + { + if(!isLoading()) + { + setLoading(true); + + long minimum = getMinimumFrequencyTextField().getFrequency(); + long maximum = getMaximumFrequencyTextField().getFrequency(); + + if(minimum < getMinimumTunableFrequency()) + { + JOptionPane.showMessageDialog(TunerEditor.this, "Frequency value [" + + getMinimumFrequencyTextField().getText() + "] is below the supported frequency range for this tuner", + "Invalid Frequency", JOptionPane.ERROR_MESSAGE); + getMinimumFrequencyTextField().setFrequency(mExistingFrequency); + return; + } + + if((minimum + getCurrentSampleRate()) > maximum) + { + long newMaximum = minimum + getCurrentSampleRate(); + + if(newMaximum <= getMaximumTunableFrequency()) + { + maximum = newMaximum; + getMaximumFrequencyTextField().setFrequency(maximum); + } + else + { + JOptionPane.showMessageDialog(TunerEditor.this, "Frequency value [" + + getMinimumFrequencyTextField().getText() + "] is invalid for current sample rate " + + "and maximum supported frequency for this tuner", "Invalid Frequency", + JOptionPane.ERROR_MESSAGE); + getMinimumFrequencyTextField().setFrequency(mExistingFrequency); + return; + } + } + + if(hasTuner()) + { + getTuner().getTunerController().setFrequencyExtents(minimum, maximum); + } + + adjustFrequencyControl(minimum, maximum); + setLoading(false); + save(); + } + } + }); + } + + return mMinimumFrequencyTextField; + } + + /** + * Adjusts the frequency control to be within the min-max range. + * @param minimum frequency value. + * @param maximum frequency value. + */ + private void adjustFrequencyControl(long minimum, long maximum) + { + if(hasTuner()) + { + try + { + if(getFrequencyControl().getFrequency() < minimum) + { + getTuner().getTunerController().setFrequency(minimum); + } + else if(getFrequencyControl().getFrequency() > maximum) + { + getTuner().getTunerController().setFrequency(maximum); + } + } + catch(SourceException se) + { + mLog.error("Error adjusting frequency", se); + } + } + } + + /** + * Adjusts the minimum and maximum frequency values to ensure the gap is wide enough for the sample rate. + * @param sampleRate to adjust for. + */ + protected void adjustForSampleRate(int sampleRate) + { + long minimum = getMinimumFrequencyTextField().getFrequency(); + long maximum = getMaximumFrequencyTextField().getFrequency(); + + if(maximum - minimum < sampleRate) + { + long newMaximum = minimum + sampleRate; + + if(newMaximum <= getMaximumTunableFrequency()) + { + getMaximumFrequencyTextField().setFrequency(newMaximum); + } + else + { + long newMinimum = maximum - sampleRate; + + if(newMinimum >= getMinimumTunableFrequency()) + { + getMinimumFrequencyTextField().setFrequency(newMinimum); + } + else + { + JOptionPane.showMessageDialog(TunerEditor.this, "Unable to adjust tuner's " + + "minimum and maximum frequency values to accommodate new sample rate [" + sampleRate + "]", + "Frequency Error", JOptionPane.ERROR_MESSAGE); + } + } + } + } + + /** + * Maximum frequency value text field + */ + protected FrequencyTextField getMaximumFrequencyTextField() + { + if(mMaximumFrequencyTextField == null) + { + mMaximumFrequencyTextField = new FrequencyTextField(DEFAULT_MINIMUM_FREQUENCY, DEFAULT_MAXIMUM_FREQUENCY, + getMaximumTunableFrequency()); + mMaximumFrequencyTextField.setToolTipText("Sets or changes the maximum frequency value that this tuner will support."); + mMaximumFrequencyTextField.addFocusListener(new FocusListener() + { + private long mExistingFrequency; + + @Override + public void focusGained(FocusEvent e) + { + mExistingFrequency = getMaximumFrequencyTextField().getFrequency(); + } + + @Override + public void focusLost(FocusEvent e) + { + if(!isLoading()) + { + + setLoading(true); + long minimum = getMinimumFrequencyTextField().getFrequency(); + long maximum = getMaximumFrequencyTextField().getFrequency(); + + if(maximum > getMaximumTunableFrequency()) + { + JOptionPane.showMessageDialog(TunerEditor.this, "Frequency value [" + + getMaximumFrequencyTextField().getText() + "] is above the supported frequency " + + "range for this tuner", "Invalid Frequency", JOptionPane.ERROR_MESSAGE); + getMaximumFrequencyTextField().setFrequency(mExistingFrequency); + return; + } + + if((maximum - getCurrentSampleRate()) < minimum) + { + long newMinimum = maximum - getCurrentSampleRate(); + + if(newMinimum >= getMinimumTunableFrequency()) + { + minimum = newMinimum; + getMinimumFrequencyTextField().setFrequency(minimum); + } + else + { + JOptionPane.showMessageDialog(TunerEditor.this, "Frequency value [" + + getMaximumFrequencyTextField().getText() + "] is invalid for current sample rate " + + "and minimum supported frequency for this tuner", "Invalid Frequency", + JOptionPane.ERROR_MESSAGE); + getMaximumFrequencyTextField().setFrequency(mExistingFrequency); + return; + } + } + + if(hasTuner()) + { + getTuner().getTunerController().setFrequencyExtents(minimum, maximum); + } + + adjustFrequencyControl(minimum, maximum); + setLoading(false); + save(); + } + } + }); + } + + return mMaximumFrequencyTextField; + } + + /** + * Resets the minimum and maximum frequency values. + */ + protected JButton getResetFrequenciesButton() + { + if(mResetFrequenciesButton == null) + { + mResetFrequenciesButton = new JButton("Reset"); + mResetFrequenciesButton.addActionListener(e -> { + getMinimumFrequencyTextField().setFrequency(getMinimumTunableFrequency()); + getMaximumFrequencyTextField().setFrequency(getMaximumTunableFrequency()); + }); + } + + return mResetFrequenciesButton; + } + /** * Button requesting to show tuner in new spectral display */ @@ -603,12 +867,22 @@ public abstract class TunerEditor { public FrequencyPanel() { - setLayout(new MigLayout("insets 0,fill", "[][][][][grow,fill]", "")); + setLayout(new MigLayout("insets 0,fill", "[][][][grow,fill]", "")); add(getFrequencyControl(), "spany 2"); add(new JLabel("PPM:")); add(getFrequencyCorrectionSpinner()); add(getMeasuredPPMLabel(), "wrap"); add(getAutoPPMCheckBox(), "span"); + + JPanel minMaxPanel = new JPanel(); + minMaxPanel.setLayout(new MigLayout("insets 0", "[][][][][][grow,fill]", "")); + minMaxPanel.add(new JLabel("Minimum:")); + minMaxPanel.add(getMinimumFrequencyTextField()); + minMaxPanel.add(new JLabel("Maximum:")); + minMaxPanel.add(getMaximumFrequencyTextField()); + minMaxPanel.add(getResetFrequenciesButton()); + add(minMaxPanel, "span"); + add(getTunerLockedStatusLabel(), "span"); } @@ -619,7 +893,11 @@ public abstract class TunerEditor { getFrequencyControl().clearListeners(); getFrequencyControl().addListener(mFrequencyAndCorrectionChangeListener); - getFrequencyControl().setEnabled(hasTuner() && !getTuner().getTunerController().isLockedSampleRate()); + boolean hasTunerUnlocked = hasTuner() && !getTuner().getTunerController().isLockedSampleRate(); + getFrequencyControl().setEnabled(hasTunerUnlocked); + getMinimumFrequencyTextField().setEnabled(hasTunerUnlocked); + getMaximumFrequencyTextField().setEnabled(hasTunerUnlocked); + getResetFrequenciesButton().setEnabled(hasTunerUnlocked); getTunerLockedStatusLabel().setVisible(hasTuner() && getTuner().getTunerController().isLockedSampleRate()); getFrequencyCorrectionSpinner().setEnabled(hasTuner()); getAutoPPMCheckBox().setEnabled(hasTuner()); @@ -629,6 +907,8 @@ public abstract class TunerEditor if(tuner != null) { getFrequencyControl().setFrequency(tuner.getTunerController().getFrequency(), false); + getMinimumFrequencyTextField().setFrequency(tuner.getTunerController().getMinimumFrequency()); + getMaximumFrequencyTextField().setFrequency(tuner.getTunerController().getMaximumFrequency()); getFrequencyCorrectionSpinner().setValue(tuner.getTunerController().getFrequencyCorrection()); getAutoPPMCheckBox().setSelected(tuner.getTunerController().getFrequencyErrorCorrectionManager().isEnabled()); getFrequencyControl().addListener(getTuner().getTunerController());