Compare commits

..

2 Commits

Author SHA1 Message Date
denny
5d9d40836b #2566 P25P1 Adjacent Status Broadcast message update. 2026-11-08 04:51:47 -05:00
Denny Sheirer
ec7efa4d17 #536 Add Bias-T support for Airspy, Airspy HF and Funcube Dongles. (#2599)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-08 04:05:58 -05:00
30 changed files with 604 additions and 2022 deletions

View File

@@ -129,7 +129,6 @@ dependencies {
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jhackrf:2026.01.3' //HackRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jhydrasdr:1.1.4' //HydraSDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jlimesdr:25.1.0.1' //LimeSDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jrtl:1.4.0.1' //RTL-SDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jsdrplay:3.15.3' //SDRplay RSP tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'

View File

@@ -49,14 +49,6 @@ dependencyResolutionManagement {
content { includeModule 'io.github.dsheirer', 'jhydrasdr' }
}
//jLimeSDR is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jLimeSDR/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jlimesdr' }
}
//jsdrplay is published as a GitHub release. The jar does not contain the SDRplay API native library, which
//users install separately from sdrplay.com
ivy {

View File

@@ -101,7 +101,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCIn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMessageUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMotorolaGroupRegroupChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCProtectionParameterBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCStatusUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCTelephoneInterconnectChannelGrant;
@@ -599,6 +598,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
//Network configuration messages
case OSP_ADJACENT_STATUS_BROADCAST:
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
mNetworkConfigurationMonitor.process(ambtc);
break;
case OSP_NETWORK_STATUS_BROADCAST:
@@ -678,14 +678,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
mu.getShortDataMessage());
}
break;
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
if(ambtc instanceof AMBTCProtectionParameterBroadcast ppb)
{
broadcastEvent(ambtc, DecodeEventType.RESPONSE, "USE ENCRYPTION " + ppb.getEncryptionKey() +
" OUTBOUND MI:" + ppb.getOutboundMessageIndicator() +
" INBOUND MI:" + ppb.getInboundMessageIndicator());
}
break;
case OSP_ROAMING_ADDRESS_UPDATE:
broadcastEvent(ambtc.getIdentifiers(), ambtc.getTimestamp(), DecodeEventType.RESPONSE,
"ROAMING ADDRESS UPDATE");

View File

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSeconda
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSystemServiceBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.AMBTCMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcastUncoordinated;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
@@ -198,6 +199,7 @@ public class P25P1NetworkConfigurationMonitor
switch(ambtc.getHeader().getOpcode())
{
case OSP_ADJACENT_STATUS_BROADCAST:
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
if(ambtc instanceof AMBTCAdjacentStatusBroadcast)
{
AMBTCAdjacentStatusBroadcast aasb = (AMBTCAdjacentStatusBroadcast)ambtc;
@@ -502,14 +504,24 @@ public class P25P1NetworkConfigurationMonitor
if(mAMBTCNeighborSites.containsKey(site))
{
AMBTCAdjacentStatusBroadcast ambtc = mAMBTCNeighborSites.get(site);
sb.append(" SYSTEM:").append(format(ambtc.getSystem(), 3));
sb.append(" ");
if(ambtc instanceof AMBTCAdjacentStatusBroadcastUncoordinated unc)
{
sb.append("WACN:").append(unc.getWacn()).append(" ");
}
sb.append("SYSTEM:").append(format(ambtc.getSystem(), 3));
sb.append(" NAC:").append(format(ambtc.getNAC(), 3));
sb.append(" RFSS:").append(format(ambtc.getRfss(), 2));
sb.append(" SITE:").append(format(ambtc.getSite(), 2));
sb.append(" LRA:").append(format(ambtc.getLocationRegistrationArea(), 2));
sb.append(" CHANNEL:").append(ambtc.getChannel());
sb.append(" DOWNLINK:").append(ambtc.getChannel().getDownlinkFrequency());
sb.append(" UPLINK:").append(ambtc.getChannel().getUplinkFrequency()).append("\n");
sb.append(" UPLINK:").append(ambtc.getChannel().getUplinkFrequency());
if(ambtc instanceof AMBTCAdjacentStatusBroadcastUncoordinated unc)
{
sb.append(" SERVICES:").append(unc.getSystemServiceClass());
}
sb.append("\n");
}
if(mLCNeighborSites.containsKey(site))
{

View File

@@ -37,6 +37,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUnitToUnitAnswerResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.isp.AMBTCUnitToUnitVoiceServiceRequest;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCAdjacentStatusBroadcastUncoordinated;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCCallAlert;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCFrequencyBandUpdateTDMA;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCGroupAffiliationQuery;
@@ -47,7 +48,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCIn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMessageUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCMotorolaGroupRegroupChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCProtectionParameterBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRoamingAddressResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRoamingAddressUpdate;
@@ -337,7 +337,7 @@ public class PDUMessageFactory
case OSP_NETWORK_STATUS_BROADCAST:
return new AMBTCNetworkStatusBroadcast(pduSequence, nac, timestamp);
case OSP_ADJACENT_STATUS_BROADCAST_UNCOORDINATED_BAND_PLAN:
return new AMBTCProtectionParameterBroadcast(pduSequence, nac, timestamp);
return new AMBTCAdjacentStatusBroadcastUncoordinated(pduSequence, nac, timestamp);
case OSP_RFSS_STATUS_BROADCAST:
return new AMBTCRFSSStatusBroadcast(pduSequence, nac, timestamp);
case OSP_ROAMING_ADDRESS_UPDATE:

View File

@@ -0,0 +1,146 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.APCO25Wacn;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25ExplicitChannel;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUSequence;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.block.UnconfirmedDataBlock;
import io.github.dsheirer.module.decode.p25.reference.SystemServiceClass;
/**
* Adjacent Status Broadcast - Uncoordinated Band Plan (opcode 0x3E), extended/multi-block (AMBTC) format.
*
* Announces a neighbor site that may belong to a different WACN/system (and band plan) than the current site.
* Opcode 0x3E was previously defined as Protection Parameter Broadcast; current TIA-102.AABC revisions reassign it.
*
* Header: LRA (24-31), flags (32-35), SYSTEM (36-47), RFSS (64-71), SITE (72-79) -- same as opcode 0x3C
* Block 0: TRANSMIT CHANNEL (0-15), RECEIVE CHANNEL (16-31), SYSTEM SERVICE CLASS (32-39), WACN (40-59)
*
* A receive channel of 0xFFFF indicates that no separate receive channel is specified.
*/
public class AMBTCAdjacentStatusBroadcastUncoordinated extends AMBTCAdjacentStatusBroadcast
{
private static final int[] BLOCK_0_DOWNLINK_FREQUENCY_BAND = {0, 1, 2, 3};
private static final int[] BLOCK_0_DOWNLINK_CHANNEL_NUMBER = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] BLOCK_0_UPLINK_CHANNEL = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] BLOCK_0_UPLINK_FREQUENCY_BAND = {16, 17, 18, 19};
private static final int[] BLOCK_0_UPLINK_CHANNEL_NUMBER = {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] BLOCK_0_SYSTEM_SERVICE_CLASS = {32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] BLOCK_0_WACN = {40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
58, 59};
private static final int UNSPECIFIED_CHANNEL = 0xFFFF;
private IBandChannelDescriptor mChannel;
private Identifier mWacn;
private SystemServiceClass mSystemServiceClass;
public AMBTCAdjacentStatusBroadcastUncoordinated(PDUSequence PDUSequence, int nac, long timestamp)
{
super(PDUSequence, nac, timestamp);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
sb.append(" WACN:").append(getWacn());
sb.append(" SYSTEM:").append(getSystem());
sb.append(" LRA:").append(getLocationRegistrationArea());
sb.append(" RFSS:").append(getRfss());
sb.append(" SITE:").append(getSite());
sb.append(" CHANNEL:").append(getChannel());
sb.append(" SERVICES:").append(getSystemServiceClass());
return sb.toString();
}
/**
* WACN of the neighbor site
*/
public Identifier getWacn()
{
if(mWacn == null && hasDataBlock(0))
{
mWacn = APCO25Wacn.create(getDataBlock(0).getMessage().getInt(BLOCK_0_WACN));
}
return mWacn;
}
/**
* Services offered by the neighbor site
*/
public SystemServiceClass getSystemServiceClass()
{
if(mSystemServiceClass == null)
{
mSystemServiceClass = new SystemServiceClass(hasDataBlock(0) ?
getDataBlock(0).getMessage().getInt(BLOCK_0_SYSTEM_SERVICE_CLASS) : 0);
}
return mSystemServiceClass;
}
private boolean hasUplinkChannel()
{
return hasDataBlock(0) && getDataBlock(0).getMessage().getInt(BLOCK_0_UPLINK_CHANNEL) != UNSPECIFIED_CHANNEL;
}
@Override
public boolean isExtendedChannel()
{
return hasUplinkChannel() && super.isExtendedChannel();
}
@Override
public IBandChannelDescriptor getChannel()
{
if(mChannel == null)
{
if(hasDataBlock(0))
{
UnconfirmedDataBlock block = getDataBlock(0);
if(isExtendedChannel())
{
mChannel = APCO25ExplicitChannel.create(block.getMessage().getInt(BLOCK_0_DOWNLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_DOWNLINK_CHANNEL_NUMBER),
block.getMessage().getInt(BLOCK_0_UPLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_UPLINK_CHANNEL_NUMBER));
}
else
{
mChannel = APCO25Channel.create(block.getMessage().getInt(BLOCK_0_DOWNLINK_FREQUENCY_BAND),
block.getMessage().getInt(BLOCK_0_DOWNLINK_CHANNEL_NUMBER));
}
}
else
{
mChannel = APCO25Channel.create(-1, 0);
}
}
return mChannel;
}
}

View File

@@ -32,7 +32,6 @@ public enum TunerClass
FUNCUBE_DONGLE_PRO_PLUS("Funcube Dongle Pro+" ),
HACKRF("HackRF" ),
HYDRASDR("HydraSDR"),
LIMESDR("LimeSDR"),
RTL2832("RTL-2832"),
RSP("RSP"),
TEST_TUNER("Test"),
@@ -57,7 +56,7 @@ public enum TunerClass
}
public static final EnumSet<TunerClass> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY, AIRSPY_HF, BLADERF, HACKRF,
HYDRASDR, LIMESDR, RTL2832, FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
HYDRASDR, RTL2832, FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
public static final EnumSet<TunerClass> FUNCUBE_TUNERS = EnumSet.of(FUNCUBE_DONGLE_PRO, FUNCUBE_DONGLE_PRO_PLUS);
@@ -147,9 +146,6 @@ public enum TunerClass
return BLADERF;
case 0x38AF0001: //HYDRASDR RFONE (official VID 38af, PID 0001)
return HYDRASDR;
case 0x1D506108: //LIMESDR-USB
case 0x0403601F: //LIMESDR MINI (FTDI FT601)
return LIMESDR;
case 0x1D5060A1:
return AIRSPY;
case 0x03EB800C:

View File

@@ -54,10 +54,6 @@ import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTuner;
import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTunerConfiguration;
import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTunerController;
import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTunerEditor;
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTuner;
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTunerConfiguration;
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTunerController;
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTunerEditor;
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration;
@@ -317,8 +313,6 @@ public class TunerFactory
return new HackRFTuner(new HackRFTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case HYDRASDR:
return new HydraSdrTuner(new HydraSdrTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case LIMESDR:
return new LimeSdrTuner(new LimeSdrTunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
case RTL2832:
return new RTL2832Tuner(new RTL2832TunerController(bus, portAddress, tunerErrorListener), tunerErrorListener, channelizerType);
default:
@@ -358,8 +352,6 @@ public class TunerFactory
return new HackRFTunerConfiguration(uniqueID);
case HYDRASDR_R828D:
return new HydraSdrTunerConfiguration(uniqueID);
case LIMESDR:
return new LimeSdrTunerConfiguration(uniqueID);
case RAFAELMICRO_R820T:
return new R820TTunerConfiguration(uniqueID);
case RAFAELMICRO_R828D:
@@ -407,8 +399,6 @@ public class TunerFactory
return new HackRFTunerEditor(userPreferences, tunerManager, discoveredTuner);
case HYDRASDR:
return new HydraSdrTunerEditor(userPreferences, tunerManager, discoveredTuner);
case LIMESDR:
return new LimeSdrTunerEditor(userPreferences, tunerManager, discoveredTuner);
case RSP:
if(discoveredTuner instanceof DiscoveredRspTuner discoveredRspTuner)
{

View File

@@ -39,7 +39,6 @@ public enum TunerType
HACKRF_JAWBREAKER("Jawbreaker"),
HACKRF_RAD1O("RAD1O"),
HYDRASDR_R828D("R828D"),
LIMESDR("LimeSDR"),
RAFAELMICRO_R820T("R820T"),
RAFAELMICRO_R828D("R828D"),
RSP_1("RSP1"),
@@ -77,7 +76,7 @@ public enum TunerType
* Supported USB tuner types
*/
public static final EnumSet<TunerType> SUPPORTED_USB_TUNERS = EnumSet.of(AIRSPY_R820T, BLADERF, ELONICS_E4000, FITIPOWER_FC0012,
HACKRF_JAWBREAKER, HACKRF_RAD1O, HACKRF_ONE, HACKRF_PRO, HYDRASDR_R828D, LIMESDR, RAFAELMICRO_R820T);
HACKRF_JAWBREAKER, HACKRF_RAD1O, HACKRF_ONE, HACKRF_PRO, HYDRASDR_R828D, RAFAELMICRO_R820T);
/**
* Supported sound card tuner types

View File

@@ -32,6 +32,7 @@ public class AirspyTunerConfiguration extends TunerConfiguration
private int mLNAGain = AirspyTunerController.LNA_GAIN_DEFAULT;
private boolean mMixerAGC = false;
private boolean mLNAAGC = false;
private boolean mBiasT = false;
/**
* Default constructor for JAXB
@@ -129,4 +130,15 @@ public class AirspyTunerConfiguration extends TunerConfiguration
{
mLNAAGC = enabled;
}
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
}

View File

@@ -183,13 +183,22 @@ public class AirspyTunerController extends TunerController
}
/**
* Closes the device, which also stops streaming.
* Turns off the bias-T and closes the device, which also stops streaming.
* @param device to close, or null
*/
private static void closeDevice(AirspyDevice device)
{
if(device != null)
{
try
{
device.setBiasTee(false);
}
catch(AirspyException | RuntimeException e)
{
mLog.warn("Error turning off Airspy bias-T before closing the device - " + e.getMessage());
}
try
{
device.close();
@@ -462,6 +471,7 @@ public class AirspyTunerController extends TunerController
setLNAGain(config.getLNAGain());
setMixerAGC(config.isMixerAGC());
setLNAAGC(config.isLNAAGC());
setBiasT(config.isBiasT());
//Set the gain mode last, so custom values are already set, and linearity and sensitivity modes will
//automatically override the custom values.
@@ -620,6 +630,17 @@ public class AirspyTunerController extends TunerController
execute(device -> device.setLnaAgc(enabled));
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
*
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException on error
*/
public void setBiasT(boolean enabled) throws SourceException
{
execute(device -> device.setBiasTee(enabled));
}
public void setGain(Gain gain) throws SourceException
{
if(gain != Gain.CUSTOM)

View File

@@ -66,6 +66,7 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
private JLabel mMixerGainValueLabel;
private JCheckBox mLNAAGCCheckBox;
private JCheckBox mMixerAGCCheckBox;
private JCheckBox mBiasTCheckBox;
/**
* Constructs an instance
@@ -116,6 +117,8 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(hasTuner() && !getTuner().getTunerController().isLockedSampleRate());
getTunerInfoButton().setEnabled(hasTuner());
getBiasTCheckBox().setEnabled(hasTuner());
getBiasTCheckBox().setSelected(hasTuner() && hasConfiguration() && getConfiguration().isBiasT());
updateGainComponents((hasTuner() && hasConfiguration()) ? getConfiguration().getGain() : null);
if(hasTuner())
@@ -159,6 +162,9 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel(""));
add(getBiasTCheckBox(), "wrap");
add(new JSeparator(), "span,growx,push");
add(new JLabel("Gain Control"), "wrap");
@@ -182,6 +188,37 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
add(getLNAGainValueLabel());
}
/**
* Checkbox to enable/disable Bias-T power for active antennas
*/
private JCheckBox getBiasTCheckBox()
{
if(mBiasTCheckBox == null)
{
mBiasTCheckBox = new JCheckBox("Bias-T");
mBiasTCheckBox.setEnabled(false);
mBiasTCheckBox.setToolTipText("Enable Bias-T power output for active antennas");
mBiasTCheckBox.addActionListener(e ->
{
if(hasTuner() && !isLoading())
{
try
{
getTuner().getController().setBiasT(mBiasTCheckBox.isSelected());
save();
}
catch(Exception e1)
{
mLog.error("Error setting Bias-T", e1);
JOptionPane.showMessageDialog(mBiasTCheckBox, "Couldn't set Bias-T: " + e1.getMessage());
}
}
});
}
return mBiasTCheckBox;
}
private JCheckBox getLNAAGCCheckBox()
{
if(mLNAAGCCheckBox == null)
@@ -607,6 +644,7 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
getConfiguration().setLNAGain(getLNAGainSlider().getValue());
getConfiguration().setMixerAGC(getMixerAGCCheckBox().isSelected());
getConfiguration().setLNAAGC(getLNAAGCCheckBox().isSelected());
getConfiguration().setBiasT(getBiasTCheckBox().isSelected());
saveConfiguration();
}
}

View File

@@ -32,6 +32,7 @@ public class AirspyHfTunerConfiguration extends TunerConfiguration
private boolean mAgc;
private boolean mLna;
private int mAttenuation;
private boolean mBiasT;
/**
* Default constructor for JAXB
@@ -114,6 +115,25 @@ public class AirspyHfTunerConfiguration extends TunerConfiguration
mLna = lna;
}
/**
* Indicates if the Bias-T power output is enabled. Only applies to devices whose firmware supports a bias-T.
* @return bias-T enabled state
*/
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
/**
* Sets the Bias-T power output enabled state
* @param enabled true to enable
*/
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
/**
* Attenuation setting.
* @return attenuation setting index value.

View File

@@ -73,6 +73,8 @@ public class AirspyHfTunerController extends TunerController
private Attenuation mAttenuation = Attenuation.A0;
private boolean mAgcEnabled;
private boolean mLnaEnabled;
private boolean mBiasTSupported;
private boolean mBiasTEnabled;
private volatile float mSamplesPerMillisecond = DEFAULT_SAMPLE_RATE.getSampleRate() / 1000.0f;
private volatile int mBufferSampleCount = DEFAULT_BUFFER_SAMPLE_COUNT;
private boolean mStreaming = false;
@@ -116,6 +118,8 @@ public class AirspyHfTunerController extends TunerController
try
{
mBiasTSupported = false;
mBiasTEnabled = false;
mDevice = AirspyHf.open(serialNumber);
BoardInfo boardInfo = mDevice.boardInfo();
@@ -139,6 +143,8 @@ public class AirspyHfTunerController extends TunerController
}
mAvailableSampleRates = Collections.unmodifiableList(sampleRates);
mBiasTSupported = detectBiasTSupport(mDevice);
mBiasTEnabled = false;
//Force the sample rate to be applied, since the rate discovery leaves the device at the last rate.
mCurrentSampleRate = null;
@@ -161,6 +167,7 @@ public class AirspyHfTunerController extends TunerController
public void stop()
{
AirspyHfDevice device;
boolean biasTEnabled;
getLock().lock();
@@ -169,6 +176,8 @@ public class AirspyHfTunerController extends TunerController
device = mDevice;
mDevice = null;
mStreaming = false;
biasTEnabled = mBiasTEnabled;
mBiasTEnabled = false;
}
finally
{
@@ -176,9 +185,58 @@ public class AirspyHfTunerController extends TunerController
}
mNativeBufferBroadcaster.clear();
if(device != null && biasTEnabled)
{
try
{
device.setBiasTee(0);
}
catch(AirspyHfException | RuntimeException e)
{
mLog.warn("Error turning off Airspy HF bias-T before closing the device - " + e.getMessage());
}
}
closeDevice(device);
}
/**
* Detects if the device firmware supports a bias-T. Firmware without bias-T support rejects the bias-T commands.
* Some firmware (HF+ Discovery R5.0.1) rejects the bias-T count request, so this falls back to turning the bias-T
* off, which is a safe probe since the bias-T should be off at startup anyway.
* @param device to probe
* @return true if the device accepts bias-T commands
*/
private static boolean detectBiasTSupport(AirspyHfDevice device)
{
try
{
if(device.biasTeeCount() > 0)
{
device.setBiasTee(0);
return true;
}
return false;
}
catch(AirspyHfException | RuntimeException e)
{
//Bias-T count not supported - fall through and probe with a bias-T off command.
}
try
{
device.setBiasTee(0);
return true;
}
catch(AirspyHfException | RuntimeException e)
{
mLog.debug("Airspy HF firmware doesn't support bias-T - " + e.getMessage());
return false;
}
}
/**
* Closes the device, which also stops streaming.
* @param device to close, or null
@@ -448,6 +506,11 @@ public class AirspyHfTunerController extends TunerController
setLna(airspyConfig.isLna());
setAttenuation(airspyConfig.getAttenuation());
setAgc(airspyConfig.isAgc());
if(isBiasTSupported())
{
setBiasT(airspyConfig.isBiasT());
}
}
catch(SourceException se)
{
@@ -670,4 +733,38 @@ public class AirspyHfTunerController extends TunerController
execute(device -> device.setAttenuationStep(attenuation.getValue()));
mAttenuation = attenuation;
}
/**
* Indicates if the device firmware supports a bias-T, available once the tuner is started.
* @return true if supported
*/
public boolean isBiasTSupported()
{
return mBiasTSupported;
}
/**
* Indicates if the Bias-T power output is enabled.
* @return enabled state
*/
public boolean getBiasT()
{
return mBiasTEnabled;
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException if the device doesn't support a bias-T or there is an error
*/
public void setBiasT(boolean enabled) throws SourceException
{
if(!mBiasTSupported)
{
throw new SourceException("Airspy HF firmware doesn't support bias-T");
}
execute(device -> device.setBiasTee(enabled ? 1 : 0));
mBiasTEnabled = enabled;
}
}

View File

@@ -46,6 +46,7 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
private JComboBox<Attenuation> mAttenuationCombo;
private JToggleButton mAgcToggleButton;
private JToggleButton mLnaToggleButton;
private JToggleButton mBiasTToggleButton;
/**
* Constructs an instance
@@ -100,6 +101,7 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
buttonPanel.setLayout(new MigLayout("insets 0", "[left]", ""));
buttonPanel.add(getAgcToggleButton());
buttonPanel.add(getLnaToggleButton());
buttonPanel.add(getBiasTToggleButton());
add(new JLabel(" "));
add(buttonPanel, "wrap");
@@ -125,6 +127,12 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getConfiguration().setAgc(getAgcToggleButton().isSelected());
getConfiguration().setLna(getLnaToggleButton().isSelected());
//Only update bias-T when the device supports it, so the saved setting isn't lost on unsupported firmware.
if(getTuner().getController().isBiasTSupported())
{
getConfiguration().setBiasT(getBiasTToggleButton().isSelected());
}
saveConfiguration();
}
}
@@ -158,6 +166,11 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getAgcToggleButton().setSelected(getTuner().getController().getAgc());
getLnaToggleButton().setEnabled(true);
getLnaToggleButton().setSelected(getTuner().getController().getLna());
boolean biasTSupported = getTuner().getController().isBiasTSupported();
getBiasTToggleButton().setEnabled(biasTSupported);
getBiasTToggleButton().setSelected(biasTSupported && getTuner().getController().getBiasT());
getBiasTToggleButton().setToolTipText(biasTSupported ? "Enable Bias-T power output for active antennas" :
"Bias-T not supported by this device or firmware");
getSampleRateCombo().setSelectedItem(getTuner().getController().getCurrentAirspySampleRate());
getAttenuationCombo().setEnabled(!getTuner().getController().getAgc());
getAttenuationCombo().setSelectedItem(getTuner().getController().getAttenuation());
@@ -169,6 +182,9 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
getAgcToggleButton().setSelected(false);
getLnaToggleButton().setEnabled(false);
getLnaToggleButton().setSelected(false);
getBiasTToggleButton().setEnabled(false);
getBiasTToggleButton().setSelected(false);
getBiasTToggleButton().setToolTipText("No tuner available");
getSampleRateCombo().setEnabled(false);
getAttenuationCombo().setEnabled(false);
getAttenuationCombo().setSelectedItem(Attenuation.A0);
@@ -344,4 +360,36 @@ public class AirspyHfTunerEditor extends TunerEditor<AirspyHfTuner,AirspyHfTuner
return mLnaToggleButton;
}
/**
* Bias-T toggle button, enabled only when the device firmware supports a bias-T.
*/
private JToggleButton getBiasTToggleButton()
{
if(mBiasTToggleButton == null)
{
mBiasTToggleButton = new JToggleButton("Bias-T");
mBiasTToggleButton.setToolTipText("Enable Bias-T power output for active antennas");
mBiasTToggleButton.setEnabled(false);
mBiasTToggleButton.addActionListener(e -> {
if(hasTuner() && !isLoading())
{
try
{
getTuner().getController().setBiasT(mBiasTToggleButton.isSelected());
save();
}
catch(SourceException se)
{
mLog.error("Error setting Airspy HF Bias-T", se);
mBiasTToggleButton.setSelected(getTuner().getController().getBiasT());
JOptionPane.showMessageDialog(AirspyHfTunerEditor.this,
"Airspy HF Tuner Controller - couldn't change Bias-T setting " + se.getLocalizedMessage());
}
}
});
}
return mBiasTToggleButton;
}
}

View File

@@ -31,7 +31,6 @@ 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;
import io.github.dsheirer.source.tuner.hydrasdr.HydraSdrTunerConfiguration;
import io.github.dsheirer.source.tuner.limesdr.LimeSdrTunerConfiguration;
import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.fc0012.FC0012TunerConfiguration;
@@ -57,7 +56,6 @@ import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
@JsonSubTypes.Type(value = FCD2TunerConfiguration.class, name = "fcd2TunerConfiguration"),
@JsonSubTypes.Type(value = HackRFTunerConfiguration.class, name = "hackRFTunerConfiguration"),
@JsonSubTypes.Type(value = HydraSdrTunerConfiguration.class, name = "hydraSdrTunerConfiguration"),
@JsonSubTypes.Type(value = LimeSdrTunerConfiguration.class, name = "limeSdrTunerConfiguration"),
@JsonSubTypes.Type(value = RecordingTunerConfiguration.class, name = "recordingTunerConfiguration"),
@JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"),
@JsonSubTypes.Type(value = R828DTunerConfiguration.class, name = "r828DTunerConfiguration"),

View File

@@ -35,6 +35,7 @@ public class FCD1TunerConfiguration extends TunerConfiguration
private LNAGain mLNAGain = LNAGain.LNA_GAIN_PLUS_20_0;
private LNAEnhance mLNAEnhance = LNAEnhance.LNA_ENHANCE_OFF;
private MixerGain mMixerGain = MixerGain.MIXER_GAIN_PLUS_12_0;
private boolean mBiasT = false;
/**
* Default constructor for JAXB
@@ -132,4 +133,15 @@ public class FCD1TunerConfiguration extends TunerConfiguration
{
mGainCorrection = value;
}
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
}

View File

@@ -49,6 +49,7 @@ public class FCD1TunerController extends FCDTunerController
private LNAGain mLNAGain;
private LNAEnhance mLNAEnhance;
private MixerGain mMixerGain;
private boolean mBiasTEnabled = false;
/**
* Constructs an instance
@@ -123,9 +124,61 @@ public class FCD1TunerController extends FCDTunerController
//Re-cast any usb exceptions as source exceptions
throw new SourceException("FCD 1 Controller error applying tuner config - " + e.getLocalizedMessage());
}
//Apply the bias-T separately so that a bias-T error doesn't prevent the tuner from starting.
try
{
setBiasT(fcd1.isBiasT());
}
catch(SourceException se)
{
mLog.warn("FCD 1 Controller couldn't apply bias-T setting - " + se.getMessage());
}
}
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
*
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException on error
*/
public void setBiasT(boolean enabled) throws SourceException
{
try
{
send(FCDCommand.APP_SET_BIAS_TEE, enabled ? 1 : 0);
mBiasTEnabled = enabled;
}
catch(Exception e)
{
throw new SourceException("Couldn't set FCD Pro bias-T", e);
}
}
/**
* Turns off the bias-T, when enabled, before closing the device.
*/
@Override
public void stop()
{
if(mBiasTEnabled)
{
try
{
setBiasT(false);
}
catch(Exception e)
{
mLog.warn("Error turning off FCD Pro bias-T before closing the device - " + e.getMessage());
}
mBiasTEnabled = false;
}
super.stop();
}
/**
* Gets the current LNA Gain value from the controller and stores it
*/

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.source.tuner.fcd.proV1;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.fcd.FCDTuner;
import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerController.LNAEnhance;
import io.github.dsheirer.source.tuner.fcd.proV1.FCD1TunerController.LNAGain;
@@ -32,6 +33,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
@@ -52,6 +54,7 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
private JComboBox<LNAGain> mLnaGainCombo;
private JComboBox<LNAEnhance> mLnaEnhanceCombo;
private JComboBox<MixerGain> mMixerGainCombo;
private JCheckBox mBiasTCheckBox;
private CorrectionSpinner mCorrectionDCI;
private CorrectionSpinner mCorrectionDCQ;
private CorrectionSpinner mGainCorrectionSpinner;
@@ -121,6 +124,7 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
getMixerGainCombo().setSelectedItem(getConfiguration().getMixerGain());
getLnaEnhanceCombo().setSelectedItem(getConfiguration().getLNAEnhance());
getLnaGainCombo().setSelectedItem(getConfiguration().getLNAGain());
getBiasTCheckBox().setSelected(getConfiguration().isBiasT());
getDcCorrectionSpinnerI().setValue(getConfiguration().getInphaseDCCorrection());
getDcCorrectionSpinnerQ().setValue(getConfiguration().getQuadratureDCCorrection());
getPhaseCorrectionSpinner().setValue(getConfiguration().getGainCorrection());
@@ -130,6 +134,7 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
getMixerGainCombo().setEnabled(hasTuner());
getLnaEnhanceCombo().setEnabled(hasTuner());
getLnaGainCombo().setEnabled(hasTuner());
getBiasTCheckBox().setEnabled(hasTuner());
getDcCorrectionSpinnerI().setEnabled(hasTuner());
getDcCorrectionSpinnerQ().setEnabled(hasTuner());
getPhaseCorrectionSpinner().setEnabled(hasTuner());
@@ -164,6 +169,8 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
add(getLnaEnhanceCombo(), "wrap");
add(new JLabel("Mixer:"));
add(getMixerGainCombo(), "wrap");
add(new JLabel(""));
add(getBiasTCheckBox(), "wrap");
add(new JSeparator(), "span,growx,push");
add(new JLabel("Correction"), "span,align center");
@@ -209,6 +216,37 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
return mLnaGainCombo;
}
/**
* Checkbox to enable/disable Bias-T power for active antennas
*/
public JCheckBox getBiasTCheckBox()
{
if(mBiasTCheckBox == null)
{
mBiasTCheckBox = new JCheckBox("Bias-T");
mBiasTCheckBox.setEnabled(false);
mBiasTCheckBox.setToolTipText("Enable Bias-T power output for active antennas");
mBiasTCheckBox.addActionListener(event ->
{
if(hasTuner() && !isLoading())
{
try
{
getController().setBiasT(mBiasTCheckBox.isSelected());
save();
}
catch(SourceException e)
{
mLog.error("Couldn't set bias-T for FCD Pro", e);
JOptionPane.showMessageDialog(FCD1TunerEditor.this, "Couldn't set Bias-T: " + e.getMessage());
}
}
});
}
return mBiasTCheckBox;
}
public JComboBox getLnaEnhanceCombo()
{
if(mLnaEnhanceCombo == null)
@@ -386,6 +424,7 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
getConfiguration().setLNAEnhance((LNAEnhance) mLnaEnhanceCombo.getSelectedItem());
getConfiguration().setLNAGain((LNAGain) mLnaGainCombo.getSelectedItem());
getConfiguration().setMixerGain((MixerGain) mMixerGainCombo.getSelectedItem());
getConfiguration().setBiasT(getBiasTCheckBox().isSelected());
double dci = ((SpinnerNumberModel)mCorrectionDCI.getModel()).getNumber().doubleValue();
getConfiguration().setInphaseDCCorrection(dci);
double dcq = ((SpinnerNumberModel)mCorrectionDCQ.getModel()).getNumber().doubleValue();

View File

@@ -27,6 +27,7 @@ public class FCD2TunerConfiguration extends TunerConfiguration
{
private boolean mGainLNA = true;
private boolean mGainMixer = true;
private boolean mBiasT = false;
/**
* Default constructor for JAXB
@@ -69,4 +70,15 @@ public class FCD2TunerConfiguration extends TunerConfiguration
{
mGainMixer = gain;
}
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasT;
}
public void setBiasT(boolean enabled)
{
mBiasT = enabled;
}
}

View File

@@ -43,6 +43,7 @@ public class FCD2TunerController extends FCDTunerController
public static final int MINIMUM_TUNABLE_FREQUENCY_HZ = 150000;
public static final int MAXIMUM_TUNABLE_FREQUENCY_HZ = 2050000000;
public static final int SAMPLE_RATE = 192000;
private boolean mBiasTEnabled = false;
/**
* Constructs an instance
@@ -98,6 +99,48 @@ public class FCD2TunerController extends FCDTunerController
send(FCDCommand.APP_SET_MIXER_GAIN, enabled ? 1 : 0);
}
/**
* Enables/disables the Bias-T (RF bias tee) power output for active antennas
*
* @param enabled true to enable Bias-T, false to disable
* @throws SourceException on error
*/
public void setBiasT(boolean enabled) throws SourceException
{
try
{
send(FCDCommand.APP_SET_BIAS_TEE, enabled ? 1 : 0);
mBiasTEnabled = enabled;
}
catch(Exception e)
{
throw new SourceException("Couldn't set FCD Pro Plus bias-T", e);
}
}
/**
* Turns off the bias-T, when enabled, before closing the device.
*/
@Override
public void stop()
{
if(mBiasTEnabled)
{
try
{
setBiasT(false);
}
catch(Exception e)
{
mLog.warn("Error turning off FCD Pro Plus bias-T before closing the device - " + e.getMessage());
}
mBiasTEnabled = false;
}
super.stop();
}
@Override
public void apply(TunerConfiguration config) throws SourceException
{
@@ -108,6 +151,7 @@ public class FCD2TunerController extends FCDTunerController
{
setLNAGain(fcd2.getGainLNA());
setMixerGain(fcd2.getGainMixer());
setBiasT(fcd2.isBiasT());
}
}

View File

@@ -32,6 +32,7 @@ import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SpinnerNumberModel;
@@ -45,6 +46,7 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
private JButton mTunerInfoButton;
private JCheckBox mLnaGainCheckBox;
private JCheckBox mMixerGainCheckBox;
private JCheckBox mBiasTCheckBox;
/**
* Constructs an instance
@@ -106,11 +108,13 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
getTunerInfoButton().setEnabled(hasTuner());
getLnaGainCheckBox().setEnabled(hasTuner());
getMixerGainCheckBox().setEnabled(hasTuner());
getBiasTCheckBox().setEnabled(hasTuner());
if(hasTuner() && hasConfiguration())
{
getLnaGainCheckBox().setSelected(getConfiguration().getGainLNA());
getMixerGainCheckBox().setSelected(getConfiguration().getGainMixer());
getBiasTCheckBox().setSelected(getConfiguration().isBiasT());
}
setLoading(false);
@@ -135,8 +139,12 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
add(getFrequencyPanel(), "wrap");
add(new JSeparator(), "span,growx,push");
add(getLnaGainCheckBox());
add(getMixerGainCheckBox());
JPanel checkBoxPanel = new JPanel();
checkBoxPanel.setLayout(new MigLayout("insets 0", "[left][left][left]", ""));
checkBoxPanel.add(getLnaGainCheckBox());
checkBoxPanel.add(getMixerGainCheckBox());
checkBoxPanel.add(getBiasTCheckBox());
add(checkBoxPanel, "span,align left");
}
public JButton getTunerInfoButton()
@@ -204,6 +212,37 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
return mMixerGainCheckBox;
}
/**
* Checkbox to enable/disable Bias-T power for active antennas
*/
public JCheckBox getBiasTCheckBox()
{
if(mBiasTCheckBox == null)
{
mBiasTCheckBox = new JCheckBox("Bias-T");
mBiasTCheckBox.setEnabled(false);
mBiasTCheckBox.setToolTipText("Enable Bias-T power output for active antennas");
mBiasTCheckBox.addActionListener(event ->
{
if(hasTuner() && !isLoading())
{
try
{
getController().setBiasT(getBiasTCheckBox().isSelected());
save();
}
catch(SourceException e)
{
mLog.error("Couldn't set bias-T for FCD2", e);
JOptionPane.showMessageDialog(mBiasTCheckBox, "Couldn't set Bias-T: " + e.getMessage());
}
}
});
}
return mBiasTCheckBox;
}
@Override
public void setTunerLockState(boolean locked)
{
@@ -255,6 +294,7 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
getConfiguration().setGainLNA(getLnaGainCheckBox().isSelected());
getConfiguration().setGainMixer(getMixerGainCheckBox().isSelected());
getConfiguration().setBiasT(getBiasTCheckBox().isSelected());
saveConfiguration();
}
}

View File

@@ -1,35 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
/**
* LimeSDR device details for display.
* @param serial board serial number, 16 hex digits
* @param board name, such as LimeSDR-USB or LimeSDR-Mini
* @param firmware version
* @param hardware version
* @param gateware (FPGA) version
* @param connection USB link, such as USB3.0
* @param library version of LimeSuiteNG
*/
public record LimeSdrDeviceInformation(String serial, String board, String firmware, String hardware, String gateware,
String connection, String library)
{
}

View File

@@ -1,112 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
import java.util.ArrayList;
import java.util.List;
/**
* LimeSDR sample rates offered to the user. The LMS7002M supports any rate in its range, so these are rates that divide
* evenly into polyphase channelizer channels.
*/
public enum LimeSdrSampleRate
{
RATE_1_25(1_250_000, "1.25 MHz"),
RATE_2_5(2_500_000, "2.50 MHz"),
RATE_5_0(5_000_000, "5.00 MHz"),
RATE_7_5(7_500_000, "7.50 MHz"),
RATE_10_0(10_000_000, "10.00 MHz"),
RATE_12_5(12_500_000, "12.50 MHz"),
RATE_15_0(15_000_000, "15.00 MHz"),
RATE_20_0(20_000_000, "20.00 MHz"),
RATE_25_0(25_000_000, "25.00 MHz"),
RATE_30_0(30_000_000, "30.00 MHz"),
RATE_30_72(30_720_000, "30.72 MHz"),
RATE_40_0(40_000_000, "40.00 MHz"),
RATE_50_0(50_000_000, "50.00 MHz"),
RATE_60_0(60_000_000, "60.00 MHz"),
RATE_61_44(61_440_000, "61.44 MHz");
public static final LimeSdrSampleRate DEFAULT = RATE_10_0;
private final int mRate;
private final String mLabel;
LimeSdrSampleRate(int rate, String label)
{
mRate = rate;
mLabel = label;
}
/**
* Sample rate in Hertz
*/
public int getRate()
{
return mRate;
}
@Override
public String toString()
{
return mLabel;
}
/**
* Sample rates within the device range, which is the lower of what the board and its USB connection support.
* @param minimumRate in Hertz
* @param maximumRate in Hertz
* @return rates, lowest to highest
*/
public static List<LimeSdrSampleRate> getRates(double minimumRate, double maximumRate)
{
List<LimeSdrSampleRate> rates = new ArrayList<>();
for(LimeSdrSampleRate rate : values())
{
if(rate.getRate() >= minimumRate && rate.getRate() <= maximumRate)
{
rates.add(rate);
}
}
return rates;
}
/**
* Finds the highest rate from the list that doesn't exceed the requested rate.
* @param rates to search, lowest to highest
* @param requested rate in Hertz
* @return closest rate, or the lowest rate when the requested rate is below all of them.
*/
public static LimeSdrSampleRate getClosest(List<LimeSdrSampleRate> rates, int requested)
{
LimeSdrSampleRate closest = rates.getFirst();
for(LimeSdrSampleRate rate : rates)
{
if(rate.getRate() <= requested)
{
closest = rate;
}
}
return closest;
}
}

View File

@@ -1,86 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
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;
/**
* LimeSDR tuner
*/
public class LimeSdrTuner extends Tuner
{
//The USB link carries 12-bit I and Q samples, packed into 3 bytes
private static final int LINK_BITS_PER_SAMPLE = 24;
private static final double SAMPLE_SIZE_12_BIT = 12.0;
/**
* Constructs an instance
* @param controller for the LimeSDR
* @param tunerErrorListener to listen for errors from this tuner
* @param channelizerType for the channelizer
*/
public LimeSdrTuner(LimeSdrTunerController controller, ITunerErrorListener tunerErrorListener,
ChannelizerType channelizerType)
{
super(controller, tunerErrorListener, channelizerType);
}
/**
* LimeSDR tuner controller
*/
public LimeSdrTunerController getController()
{
return (LimeSdrTunerController)getTunerController();
}
@Override
public String getPreferredName()
{
LimeSdrDeviceInformation info = getController().getDeviceInfo();
return (info != null ? info.board() : "LimeSDR") + " " + getUniqueID();
}
@Override
public String getUniqueID()
{
return getController().getDeviceInfo().serial();
}
@Override
public TunerClass getTunerClass()
{
return TunerClass.LIMESDR;
}
@Override
public double getSampleSize()
{
return SAMPLE_SIZE_12_BIT;
}
@Override
public int getMaximumUSBBitsPerSecond()
{
return LimeSdrSampleRate.RATE_61_44.getRate() * LINK_BITS_PER_SAMPLE;
}
}

View File

@@ -1,97 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
/**
* LimeSDR tuner configuration
*/
public class LimeSdrTunerConfiguration extends TunerConfiguration
{
private int mSampleRate = LimeSdrSampleRate.DEFAULT.getRate();
private int mGain = LimeSdrTunerController.GAIN_DEFAULT;
private String mAntenna = LimeSdrTunerController.ANTENNA_DEFAULT;
/**
* Default constructor for Jackson
*/
public LimeSdrTunerConfiguration()
{
super(LimeSdrTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ, LimeSdrTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ);
}
/**
* Constructs an instance
* @param uniqueID for the tuner
*/
public LimeSdrTunerConfiguration(String uniqueID)
{
super(uniqueID);
}
@Override
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
public TunerType getTunerType()
{
return TunerType.LIMESDR;
}
@JacksonXmlProperty(isAttribute = true, localName = "sample_rate")
public int getSampleRate()
{
return mSampleRate;
}
public void setSampleRate(int sampleRate)
{
mSampleRate = sampleRate;
}
/**
* Combined receive gain in dB, 0 to 73.
*/
@JacksonXmlProperty(isAttribute = true, localName = "gain")
public int getGain()
{
return mGain;
}
public void setGain(int gain)
{
mGain = gain;
}
/**
* Receive antenna port name, such as LNAW.
*/
@JacksonXmlProperty(isAttribute = true, localName = "antenna")
public String getAntenna()
{
return mAntenna;
}
public void setAntenna(String antenna)
{
mAntenna = antenna;
}
}

View File

@@ -1,363 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.manager.TunerStatus;
import io.github.dsheirer.source.tuner.ui.TunerEditor;
import java.io.Serial;
import java.util.List;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.SpinnerNumberModel;
/**
* LimeSDR tuner editor
*/
public class LimeSdrTunerEditor extends TunerEditor<LimeSdrTuner, LimeSdrTunerConfiguration>
{
@Serial
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger(LimeSdrTunerEditor.class);
private JButton mTunerInfoButton;
private JComboBox<LimeSdrSampleRate> mSampleRateCombo;
private JComboBox<String> mAntennaCombo;
private JSlider mGainSlider;
private JLabel mGainValueLabel;
/**
* Constructs an instance
* @param userPreferences for wide-band recordings
* @param tunerManager for saving tuner configuration
* @param discoveredTuner for the optionally usable LimeSDR tuner and controller
*/
public LimeSdrTunerEditor(UserPreferences userPreferences, TunerManager tunerManager, DiscoveredTuner discoveredTuner)
{
super(userPreferences, tunerManager, discoveredTuner);
init();
tunerStatusUpdated();
}
@Override
public long getMinimumTunableFrequency()
{
return hasTuner() ? getTuner().getController().getMinimumTunableFrequency() :
LimeSdrTunerController.MINIMUM_TUNABLE_FREQUENCY_HZ;
}
@Override
public long getMaximumTunableFrequency()
{
return hasTuner() ? getTuner().getController().getMaximumTunableFrequency() :
LimeSdrTunerController.MAXIMUM_TUNABLE_FREQUENCY_HZ;
}
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", "[][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
add(getTunerIdLabel());
add(getTunerInfoButton());
add(new JLabel("Status:"));
add(getTunerStatusLabel(), "wrap");
add(getButtonPanel(), "span,align left");
add(new JSeparator(), "span,growx");
add(new JLabel("Frequency (MHz):"));
add(getFrequencyPanel(), "wrap");
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel("Antenna:"));
add(getAntennaCombo(), "wrap");
add(new JSeparator(), "span,growx");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
//Empty row that takes all extra vertical space so that the controls stay stacked at the top of the editor.
add(new JLabel(), "span,pushy");
}
@Override
protected void tunerStatusUpdated()
{
setLoading(true);
boolean started = hasStartedTuner();
getTunerIdLabel().setText(started ? getTuner().getPreferredName() : getDiscoveredTuner().getId());
String status = getDiscoveredTuner().getTunerStatus().toString();
if(getDiscoveredTuner().hasErrorMessage())
{
status += " - " + getDiscoveredTuner().getErrorMessage();
}
getTunerStatusLabel().setText(status);
getButtonPanel().updateControls();
getFrequencyPanel().updateControls();
getTunerInfoButton().setEnabled(started);
if(started)
{
LimeSdrTunerController controller = getTuner().getController();
List<LimeSdrSampleRate> rates = controller.getSampleRates();
getSampleRateCombo().setModel(new DefaultComboBoxModel<>(rates.toArray(new LimeSdrSampleRate[0])));
getSampleRateCombo().setSelectedItem(controller.getLimeSdrSampleRate());
getSampleRateCombo().setEnabled(!controller.isLockedSampleRate());
List<String> antennas = controller.getAntennas();
getAntennaCombo().setModel(new DefaultComboBoxModel<>(antennas.toArray(new String[0])));
getAntennaCombo().setSelectedItem(controller.getAntenna());
getAntennaCombo().setEnabled(true);
getGainSlider().setValue(controller.getGain());
getGainSlider().setEnabled(true);
getGainValueLabel().setEnabled(true);
}
else
{
getSampleRateCombo().setModel(new DefaultComboBoxModel<>());
getSampleRateCombo().setEnabled(false);
getAntennaCombo().setModel(new DefaultComboBoxModel<>());
getAntennaCombo().setEnabled(false);
getGainSlider().setEnabled(false);
getGainValueLabel().setEnabled(false);
}
updateGainLabel();
setLoading(false);
}
/**
* Indicates if the tuner has finished starting, since a status update can arrive while it's starting or restarting.
*/
private boolean hasStartedTuner()
{
return hasTuner() && getTuner().getController().getAntenna() != null;
}
private void updateGainLabel()
{
getGainValueLabel().setText(getGainSlider().getValue() + " dB");
}
/**
* Button that provides tuner information
*/
private JButton getTunerInfoButton()
{
if(mTunerInfoButton == null)
{
mTunerInfoButton = new JButton("Info");
mTunerInfoButton.setEnabled(false);
mTunerInfoButton.addActionListener(_ -> JOptionPane.showMessageDialog(LimeSdrTunerEditor.this,
getTunerInfo(), "Tuner Info", JOptionPane.INFORMATION_MESSAGE));
}
return mTunerInfoButton;
}
private JComboBox<LimeSdrSampleRate> getSampleRateCombo()
{
if(mSampleRateCombo == null)
{
mSampleRateCombo = new JComboBox<>();
mSampleRateCombo.setEnabled(false);
mSampleRateCombo.addActionListener(_ ->
{
if(hasTuner() && !isLoading())
{
LimeSdrSampleRate rate = (LimeSdrSampleRate)mSampleRateCombo.getSelectedItem();
try
{
getTuner().getController().setSampleRate(rate);
//Adjust the min/max values for the sample rate.
adjustForSampleRate(rate.getRate());
save();
}
catch(Exception e1)
{
mLog.error("Error setting LimeSDR sample rate", e1);
JOptionPane.showMessageDialog(LimeSdrTunerEditor.this, "Couldn't set sample rate to " + rate);
}
}
});
}
return mSampleRateCombo;
}
private JComboBox<String> getAntennaCombo()
{
if(mAntennaCombo == null)
{
mAntennaCombo = new JComboBox<>();
mAntennaCombo.setEnabled(false);
mAntennaCombo.setToolTipText("Receive port: LNAW wideband (default), LNAL low band, LNAH high band");
mAntennaCombo.addActionListener(_ ->
{
if(hasTuner() && !isLoading())
{
String antenna = (String)mAntennaCombo.getSelectedItem();
try
{
getTuner().getController().setAntenna(antenna);
save();
}
catch(Exception e1)
{
mLog.error("Error setting LimeSDR antenna", e1);
JOptionPane.showMessageDialog(mAntennaCombo, "Couldn't set antenna to " + antenna);
}
}
});
}
return mAntennaCombo;
}
private JSlider getGainSlider()
{
if(mGainSlider == null)
{
mGainSlider = new JSlider(JSlider.HORIZONTAL, LimeSdrTunerController.GAIN_MINIMUM,
LimeSdrTunerController.GAIN_MAXIMUM, LimeSdrTunerController.GAIN_DEFAULT);
mGainSlider.setEnabled(false);
mGainSlider.setMajorTickSpacing(5);
mGainSlider.setPaintTicks(true);
mGainSlider.addChangeListener(_ ->
{
int gain = mGainSlider.getValue();
if(hasTuner() && !isLoading())
{
try
{
getTuner().getController().setGain(gain);
save();
}
catch(Exception e)
{
mLog.error("Couldn't set LimeSDR gain to:" + gain, e);
JOptionPane.showMessageDialog(mGainSlider, "Couldn't set gain value to " + gain);
}
}
updateGainLabel();
});
}
return mGainSlider;
}
private JLabel getGainValueLabel()
{
if(mGainValueLabel == null)
{
mGainValueLabel = new JLabel("0 dB");
mGainValueLabel.setEnabled(false);
}
return mGainValueLabel;
}
@Override
public void save()
{
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());
if(getSampleRateCombo().getSelectedItem() instanceof LimeSdrSampleRate rate)
{
getConfiguration().setSampleRate(rate.getRate());
}
if(getAntennaCombo().getSelectedItem() instanceof String antenna)
{
getConfiguration().setAntenna(antenna);
}
getConfiguration().setGain(getGainSlider().getValue());
saveConfiguration();
}
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
getSampleRateCombo().setToolTipText(locked ? "Sample Rate is locked. Disable decoding channels to unlock." :
"Select a sample rate for the tuner");
}
private String getTunerInfo()
{
if(getDiscoveredTuner().getTunerStatus() == TunerStatus.ERROR)
{
return getDiscoveredTuner().getErrorMessage();
}
if(hasTuner())
{
LimeSdrDeviceInformation info = getTuner().getController().getDeviceInfo();
StringBuilder sb = new StringBuilder();
sb.append("<html><h3>LimeSDR Tuner</h3>");
sb.append("<b>Board: </b>").append(info.board()).append("<br>");
sb.append("<b>Serial: </b>").append(info.serial()).append("<br>");
sb.append("<b>Hardware: </b>").append(info.hardware()).append("<br>");
sb.append("<b>Firmware: </b>").append(info.firmware()).append("<br>");
sb.append("<b>Gateware: </b>").append(info.gateware()).append("<br>");
sb.append("<b>Connection: </b>").append(info.connection()).append("<br>");
sb.append("<b>LimeSuiteNG: </b>").append(info.library()).append("<br>");
return sb.toString();
}
return null;
}
}

View File

@@ -1,7 +0,0 @@
# LimeSDR udev rules
# Grants access to the logged-in user (uaccess) and to the plugdev group.
# LimeSDR-USB
SUBSYSTEM=="usb", ATTR{idVendor}=="1d50", ATTR{idProduct}=="6108", MODE="0660", GROUP="plugdev", TAG+="uaccess"
# LimeSDR Mini and LimeSDR Mini 2.0
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="601f", MODE="0660", GROUP="plugdev", TAG+="uaccess"

View File

@@ -1,204 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.limesdr;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.dsp.filter.channelizer.ComplexPolyphaseChannelizerM2;
import io.github.dsheirer.limesdr.DeviceEntry;
import io.github.dsheirer.limesdr.LogLevel;
import io.github.dsheirer.limesdr.Range;
import io.github.dsheirer.limesdr.StreamConfig;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerClass;
import io.github.dsheirer.source.tuner.TunerFactory;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import java.util.List;
import org.junit.jupiter.api.Test;
/**
* Tests for the parts of the LimeSDR tuner that don't require a device.
*/
class LimeSdrTunerTest
{
private static final DeviceEntry USB_A = DeviceEntry.parse("LimeSDR-USB, media=USB3.0, addr=1d50:6108, serial=0009060B00462008");
private static final DeviceEntry USB_B = DeviceEntry.parse("LimeSDR-USB, media=USB3.0, addr=1d50:6108, serial=0009060B00471B22");
private static final DeviceEntry MINI = DeviceEntry.parse("LimeSDR-Mini, media=USB3.0, addr=0403:601f, serial=1D3AC9A5C7B4D2");
@Test
void lookupFindsEachUsbProduct()
{
assertEquals(TunerClass.LIMESDR, TunerClass.lookup((short)0x1D50, (short)0x6108));
assertEquals(TunerClass.LIMESDR, TunerClass.lookup((short)0x0403, (short)0x601F));
assertTrue(TunerClass.LIMESDR.isSupportedUsbTuner());
assertTrue(TunerType.LIMESDR.isSupportedUsbTuner());
}
@Test
void scalesSamplesAndRemovesHalfBitOffset()
{
short[] samples = {2047, -2048, -1, 0};
assertArrayEquals(new float[]{2047.5f / 2048, -2047.5f / 2048, -0.5f / 2048, 0.5f / 2048},
LimeSdrTunerController.scale(samples));
}
@Test
void sampleRatesAreLimitedByTheDevice()
{
//LimeSDR-USB on USB 3.0
assertEquals(List.of(LimeSdrSampleRate.values()), LimeSdrSampleRate.getRates(100_000, 61_440_000));
//LimeSDR Mini
assertEquals(LimeSdrSampleRate.RATE_30_72, LimeSdrSampleRate.getRates(100_000, 30_720_000).getLast());
}
@Test
void usb2LimitsTheSampleRate()
{
Range range = new Range(100_000, 61_440_000, 0);
assertEquals(61_440_000, LimeSdrTunerController.getMaximumSampleRate(range, "USB3.0"));
double usb2 = LimeSdrTunerController.getMaximumSampleRate(range, "USB2.0");
assertEquals(LimeSdrSampleRate.RATE_10_0, LimeSdrSampleRate.getRates(range.min(), usb2).getLast());
}
@Test
void savedRateAboveTheDeviceMaximumIsReduced()
{
List<LimeSdrSampleRate> rates = LimeSdrSampleRate.getRates(100_000, 30_720_000);
assertEquals(LimeSdrSampleRate.RATE_30_72, LimeSdrSampleRate.getClosest(rates, LimeSdrSampleRate.RATE_61_44.getRate()));
assertEquals(LimeSdrSampleRate.RATE_1_25, LimeSdrSampleRate.getClosest(rates, 1));
}
@Test
void sampleRatesDivideIntoPolyphaseChannels()
{
for(LimeSdrSampleRate rate : LimeSdrSampleRate.values())
{
int channelCount = ComplexPolyphaseChannelizerM2.getChannelCount(rate.getRate());
assertEquals(0, channelCount % 2, rate.toString());
assertTrue((double)rate.getRate() / channelCount >= 25_000.0, rate.toString());
}
}
@Test
void maximumUsbRateDoesNotOverflow()
{
assertTrue(LimeSdrSampleRate.RATE_61_44.getRate() * 24L <= Integer.MAX_VALUE);
}
@Test
void antennaListExcludesNoneAndLoopbackPorts()
{
List<String> ports = List.of("None", "LNAH", "LNAL", "LNAW", "LB1", "LB2");
List<String> antennas = LimeSdrTunerController.getSelectableAntennas(ports);
assertEquals(List.of("LNAH", "LNAL", "LNAW"), antennas);
assertEquals("LNAW", LimeSdrTunerController.getDefaultAntenna(antennas));
assertEquals("LNAH", LimeSdrTunerController.getDefaultAntenna(List.of("LNAH")));
}
@Test
void deviceIsFoundBySerialNumber() throws SourceException
{
LimeSdrTunerController.UsbIdentity identity = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108,
"0009060B00471B22");
assertEquals(USB_B, LimeSdrTunerController.findDevice(identity, List.of(MINI, USB_A, USB_B)));
}
@Test
void deviceWithoutSerialIsFoundWhenItIsTheOnlyOneOfItsKind() throws SourceException
{
LimeSdrTunerController.UsbIdentity mini = new LimeSdrTunerController.UsbIdentity(0x0403, 0x601F, null);
LimeSdrTunerController.UsbIdentity usb = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108, null);
assertEquals(MINI, LimeSdrTunerController.findDevice(mini, List.of(USB_A, MINI)));
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(usb, List.of(USB_A, USB_B, MINI)));
}
@Test
void deviceThatLimeSuiteCantSeeIsAnError()
{
LimeSdrTunerController.UsbIdentity identity = new LimeSdrTunerController.UsbIdentity(0x1D50, 0x6108,
"0009060B00462008");
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(identity, List.of(MINI)));
assertThrows(SourceException.class, () -> LimeSdrTunerController.findDevice(identity, List.of(USB_B)));
}
@Test
void configurationSurvivesSerialization() throws Exception
{
LimeSdrTunerConfiguration configuration = new LimeSdrTunerConfiguration("test");
configuration.setSampleRate(LimeSdrSampleRate.RATE_5_0.getRate());
configuration.setGain(55);
configuration.setAntenna("LNAH");
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String json = mapper.writeValueAsString(configuration);
TunerConfiguration restored = mapper.readValue(json, TunerConfiguration.class);
LimeSdrTunerConfiguration limeSdr = assertInstanceOf(LimeSdrTunerConfiguration.class, restored);
assertEquals(TunerType.LIMESDR, limeSdr.getTunerType());
assertEquals(LimeSdrSampleRate.RATE_5_0.getRate(), limeSdr.getSampleRate());
assertEquals(55, limeSdr.getGain());
assertEquals("LNAH", limeSdr.getAntenna());
assertEquals("test", limeSdr.getUniqueID());
}
@Test
void factoryCreatesTheConfiguration()
{
assertInstanceOf(LimeSdrTunerConfiguration.class, TunerFactory.getTunerConfiguration(TunerType.LIMESDR, "id"));
}
@Test
void fifoHoldsAQuarterSecondOfSamples()
{
assertEquals(StreamConfig.DEFAULT_FIFO_SIZE, LimeSdrTunerController.getFifoSize(LimeSdrSampleRate.RATE_1_25));
assertEquals(10_000_000, LimeSdrTunerController.getFifoSize(LimeSdrSampleRate.RATE_40_0));
}
@Test
void debugMessagesAreDiscarded()
{
LimeSdrTunerController.log(LogLevel.DEBUG, "Loss: pkt:4622555 exp: 000000017a863dc0, got: 000000017a864860");
LimeSdrTunerController.log(LogLevel.VERBOSE, "Set Rx LO frequency (450100180 Hz)");
}
@Test
void droppedSamplesAreFoundFromTimestampGaps()
{
assertEquals(0, LimeSdrTunerController.getTimestampGap(-1, 5_000_000));
assertEquals(0, LimeSdrTunerController.getTimestampGap(65536, 65536));
assertEquals(718_080, LimeSdrTunerController.getTimestampGap(32_309_248, 33_027_328));
assertEquals(0, LimeSdrTunerController.getTimestampGap(32_309_248, 0));
}
}