#7 NXDN Decoder support. Redesigns tuner and channel frequency error management. (#2431)

Closes #7 

NXDN Decoder

* Implements: NXDN4800, NXDN9600, Type-C and Type-D decoding and trunk tracking.
* Redesigns tuner and channel frequency error management.
This commit is contained in:
Denny Sheirer
2026-07-08 03:44:08 -04:00
committed by GitHub
parent a053315675
commit 7945f0ce63
445 changed files with 30854 additions and 2053 deletions

View File

@@ -1,252 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.carrier;
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.window.WindowFactory;
import io.github.dsheirer.dsp.window.WindowType;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
import org.jtransforms.fft.FloatFFT_1D;
/**
* Calculates the carrier offset from channel center. Processes incoming Complex Sample buffers to detect the carrier
* signal and calculate a carrier offset measurement.
* <p>
* Calculates once a second by performing a 128-point FFT on each consecutive complex buffer. Once it detects a
* sequence of 5 consecutive buffers with sufficient SNR (>15 dB), it calculates the average offset across those 5
* buffers and then averages across the 10 most recent high-SNR sequences.
*/
public class CarrierOffsetProcessor
{
private static final int FFT_BIN_SIZE = 128;
private static final int MEASUREMENT_COUNT_THRESHOLD = 5;
private static final int SEARCH_WINDOW_MIDDLE = FFT_BIN_SIZE / 2;
private static final long CALCULATION_TIME_INTERVAL_MS = 1000;
private static final float CENTER_INDEX = FFT_BIN_SIZE / 2.0f - 1;
private static final float[] WINDOW = WindowFactory.getWindow(WindowType.BLACKMAN_HARRIS_7, FFT_BIN_SIZE * 2);
private static final FloatFFT_1D FFT = new FloatFFT_1D(FFT_BIN_SIZE);
private final FloatAveragingBuffer mAveragingBuffer = new FloatAveragingBuffer(5);
private final FloatAveragingBuffer mOffsetAverage = new FloatAveragingBuffer(10, 3);
private final StandardDeviation mStandardDeviation = new StandardDeviation();
private float mCarrierOffset;
private float mResolution;
private int mHalfWidth;
private int mMeasurementCount = 0;
private int mSearchWindowMin;
private int mSearchWindowMax;
private long mEstimatedOffset;
private long mLastCalculationTimestamp = 0;
/**
* Constructs an instance.
*/
public CarrierOffsetProcessor()
{
setSampleRate(50000.0);
}
/**
* Update the current channel sample rate.
* @param sampleRate in Hertz
*/
public void setSampleRate(double sampleRate)
{
mResolution = (float)sampleRate / FFT_BIN_SIZE;
mHalfWidth = (int)Math.floor(12500.0 / mResolution / 2.0); //Half of 12.5 kHz signal width
mSearchWindowMin = SEARCH_WINDOW_MIDDLE - mHalfWidth;
mSearchWindowMax = SEARCH_WINDOW_MIDDLE + mHalfWidth;
}
/**
* Resets this processor whenever the source tuner PPM value is changed or updated, so that we can immediately
* recalculate the carrier offset with the updated settings.
*/
public void reset()
{
mMeasurementCount = 0;
mLastCalculationTimestamp = 0;
mEstimatedOffset = 0;
mAveragingBuffer.reset();
mStandardDeviation.clear();
mOffsetAverage.reset();
}
/**
* Estimated carrier frequency offset averaged across buffers that have sufficient SNR (>15 dB).
* @return estimated average carrier offset. Note: this method can be accessed after each invocation of process()
* method.
*/
public long getEstimatedOffset()
{
return mEstimatedOffset;
}
/**
* Indicates if a signal was detected in the most recently processed sample buffer. A value of false indicates the
* signal was either not present, or the SNR was less than 15 dB and therefore this processor could not calculate
* the carrier offest estimation.
* @return true if the most recent buffer had a signal that was also strong enough to process.
*/
public boolean hasCarrier()
{
return mCarrierOffset != Float.MAX_VALUE;
}
/**
* Processes the complex sample buffer by calculating the detected carrier offset and mixing the buffer samples by
* remove the calculated offset. The current detected carrier offset is available via the getEstimatedOffset()
* method.
*
* @return true if the estimated carrier offset value was updated.
*/
public boolean process(ComplexSamples samples)
{
if(mLastCalculationTimestamp + CALCULATION_TIME_INTERVAL_MS < samples.timestamp())
{
int bufferOffset = 0;
while((bufferOffset + FFT_BIN_SIZE) <= samples.length())
{
mCarrierOffset = calculateCarrierOffset(samples, bufferOffset);
//Float max value indicates no signal or low SNR - reset the counter so that we can keep trying.
if(mCarrierOffset == Float.MAX_VALUE)
{
mMeasurementCount = 0;
mStandardDeviation.clear();
}
else
{
mMeasurementCount++;
mAveragingBuffer.add(mCarrierOffset);
}
if(mMeasurementCount >= MEASUREMENT_COUNT_THRESHOLD)
{
if(mStandardDeviation.getResult() < 5.0)
{
mOffsetAverage.add(mAveragingBuffer.getAverage());
mLastCalculationTimestamp = samples.timestamp();
//Set the buffer offset to the sample length to exit the loop
bufferOffset = samples.length();
}
mMeasurementCount = 0;
mStandardDeviation.clear();
}
bufferOffset += FFT_BIN_SIZE;
}
mEstimatedOffset = (long)mOffsetAverage.getAverage();
return true;
}
return false;
}
/**
* Calculate the detected peak signal offset from the sample buffer.
* @param complexSamples to process
* @param offset into the complex samples buffer to calculate
* @return estimated carrier offset from center of the channel.
*/
private float calculateCarrierOffset(ComplexSamples complexSamples, int offset)
{
float[] samples = complexSamples.toInterleaved(offset, FFT_BIN_SIZE).samples();
WindowFactory.apply(WINDOW, samples);
FFT.complexForward(samples);
float[] magnitudes = ComplexDecibelConverter.convert(samples);
float peakIndex = findPeak(magnitudes);
//Return float max value for Low SNR or no signal
if(peakIndex == Float.MAX_VALUE)
{
return Float.MAX_VALUE;
}
mStandardDeviation.increment(peakIndex);
return mResolution * (peakIndex - CENTER_INDEX);
}
/**
* Finds the FFT bin with the largest magnitude within a window of +/- 12.5 kHz of the center bin.
* @param magnitudes from the FFT measurement.
* @return center bin or Float.MAX_VALUE to indicate no signal or low SNR.
*/
private float findPeak(float[] magnitudes)
{
float peakValue = -150.0f;
int peakIndex = 0;
int min = mSearchWindowMin;
int max = mSearchWindowMax;
int half = mHalfWidth;
//Find the peak magnitude within twice the search window width of the center indices.
for(int x = min; x <= max; x++)
{
if(magnitudes[x] > peakValue)
{
peakValue = magnitudes[x];
peakIndex = x;
}
}
//From there, find the center point that best fits the signal to the search window width. We need a minimum
// SNR for this to work.
int left = peakIndex - half;
int right = peakIndex + half;
//Do we have enough SNR?
float snr = peakValue - ((magnitudes[left] + magnitudes[right]) / 2.0f);
if(snr > 15.0f)
{
float minimumSearchThreshold = peakValue - 16.0f; //Stop at bin values 16 db lower.
while(magnitudes[left] < minimumSearchThreshold && left < peakIndex)
{
left++;
}
while(magnitudes[right] < minimumSearchThreshold && right > peakIndex)
{
right--;
}
int binWidth = right - left;
if(binWidth == 0)
{
return peakIndex;
}
else
{
return left + (binWidth / 2.0f);
}
}
//No signal or low SNR - return MAX VALUE to indicate no signal.
return Float.MAX_VALUE;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* 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
@@ -81,6 +81,12 @@ import io.github.dsheirer.module.decode.mpt1327.Sync;
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
import io.github.dsheirer.module.decode.nbfm.NBFMDecoderState;
import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
import io.github.dsheirer.module.decode.nxdn.NXDNDecoder;
import io.github.dsheirer.module.decode.nxdn.NXDNDecoderState;
import io.github.dsheirer.module.decode.nxdn.NXDNTrafficChannelManager;
import io.github.dsheirer.module.decode.nxdn.audio.NXDNAudioModule;
import io.github.dsheirer.module.decode.nxdn.layer3.filter.NXDNMessageFilterSet;
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
@@ -171,9 +177,6 @@ public class DecoderFactory
processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig,
trafficChannelManager, channelDescriptor);
break;
case NBFM:
processNBFM(channel, modules, aliasList, decodeConfig);
break;
case LTR:
processLTRStandard(channel, modules, aliasList, (DecodeConfigLTRStandard) decodeConfig);
break;
@@ -184,6 +187,12 @@ public class DecoderFactory
processMPT1327(channelMapModel, channel, modules, aliasList, channelType,
(DecodeConfigMPT1327) decodeConfig, userPreferences);
break;
case NBFM:
processNBFM(channel, modules, aliasList, decodeConfig);
break;
case NXDN:
processNXDN(channel, userPreferences, modules, aliasList, decodeConfig, trafficChannelManager, channelDescriptor);
break;
case PASSPORT:
processPassport(channel, modules, aliasList, decodeConfig);
break;
@@ -462,6 +471,55 @@ public class DecoderFactory
}
}
/**
* Creates decoder modules for the NXDN decoder.
* @param channel configuration with center frequency
* @param modules to receive the decoder modules
* @param aliasList for aliases.
* @param decodeConfig with details
*/
private static void processNXDN(Channel channel, UserPreferences userPreferences, List<Module> modules,
AliasList aliasList, DecodeConfiguration decodeConfig,
TrafficChannelManager trafficChannelManager, IChannelDescriptor channelDescriptor)
{
if(decodeConfig instanceof DecodeConfigNXDN configNXDN)
{
//Add a channel rotation monitor when we have multiple control channel frequencies specified
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency sctmf &&
sctmf.hasMultipleFrequencies())
{
List<State> activeStates = new ArrayList<>();
activeStates.add(State.CONTROL);
modules.add(new ChannelRotationMonitor(activeStates, sctmf.getFrequencyRotationDelay(), userPreferences));
}
modules.add(new NXDNDecoder(configNXDN));
modules.add(new NXDNAudioModule(userPreferences, aliasList));
if(channel.getChannelType() == ChannelType.STANDARD)
{
NXDNTrafficChannelManager primaryTCM = new NXDNTrafficChannelManager(channel);
modules.add(primaryTCM);
modules.add(new NXDNDecoderState(channel, primaryTCM));
}
else if(trafficChannelManager instanceof NXDNTrafficChannelManager parentTCM)
{
NXDNDecoderState decoderState = new NXDNDecoderState(channel, parentTCM);
decoderState.setCurrentChannel(channelDescriptor);
modules.add(decoderState);
}
else
{
mLog.warn("Expected non-null NXDN traffic channel manager for channel " + channel.getName());
}
}
else
{
throw new IllegalArgumentException("Can't create NXDN decoder - unrecognized config: " + decodeConfig);
}
}
/**
* Creates modules for DMR decoder setup.
*
@@ -622,9 +680,9 @@ public class DecoderFactory
for(Module module : modules)
{
if(module instanceof Decoder)
if(module instanceof Decoder decoder)
{
filterSet.addFilters(getMessageFilter(((Decoder)module).getDecoderType()));
filterSet.addFilters(getMessageFilter(decoder.getDecoderType()));
}
}
@@ -668,6 +726,9 @@ public class DecoderFactory
case MPT1327:
filters.add(new MPT1327MessageFilter());
break;
case NXDN:
filters.add(new NXDNMessageFilterSet());
break;
case P25_PHASE1:
filters.add(new P25P1MessageFilterSet());
break;
@@ -708,6 +769,8 @@ public class DecoderFactory
return new DecodeConfigMPT1327();
case NBFM:
return new DecodeConfigNBFM();
case NXDN:
return new DecodeConfigNXDN();
case PASSPORT:
return new DecodeConfigPassport();
case P25_PHASE1:
@@ -767,6 +830,13 @@ public class DecoderFactory
copyNBFM.setSquelchNoiseCloseThreshold(origNBFM.getSquelchNoiseCloseThreshold());
copyNBFM.setTalkgroup(origNBFM.getTalkgroup());
return copyNBFM;
case NXDN:
DecodeConfigNXDN origNXDN = (DecodeConfigNXDN)config;
DecodeConfigNXDN copyNXDN = new DecodeConfigNXDN();
copyNXDN.setChannelMap(origNXDN.getChannelMap());
copyNXDN.setTransmissionMode(origNXDN.getTransmissionMode());
copyNXDN.setTrafficChannelPoolSize(origNXDN.getTrafficChannelPoolSize());
return copyNXDN;
case P25_PHASE1:
DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1)config;
DecodeConfigP25Phase1 copyP25 = new DecodeConfigP25Phase1();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -28,11 +28,12 @@ public enum DecoderType
{
//Primary Decoders
AM("AM", "AM", Protocol.UNKNOWN),
DMR("DMR","DMR", Protocol.DMR),
DMR("DMR", "DMR", Protocol.DMR),
LTR("LTR", "LTR", Protocol.LTR),
LTR_NET("LTR-Net", "LTR-Net", Protocol.LTR_NET),
MPT1327("MPT1327", "MPT1327", Protocol.MPT1327),
NBFM("NBFM", "NBFM", Protocol.NBFM),
NXDN("NXDN", "NXDN", Protocol.NXDN),
PASSPORT("Passport", "Passport", Protocol.PASSPORT),
P25_PHASE1("P25 Phase 1", "P25-1", Protocol.APCO25),
P25_PHASE2("P25 Phase 2", "P25-2", Protocol.APCO25_PHASE2),
@@ -44,9 +45,9 @@ public enum DecoderType
MDC1200("MDC1200", "MDC1200", Protocol.MDC1200),
TAIT_1200("Tait 1200", "Tait 1200", Protocol.TAIT1200);
private String mDisplayString;
private String mShortDisplayString;
private Protocol mProtocol;
private final String mDisplayString;
private final String mShortDisplayString;
private final Protocol mProtocol;
DecoderType(String displayString, String shortDisplayString, Protocol protocol)
{
@@ -58,38 +59,23 @@ public enum DecoderType
/**
* Primary decoders that operate on I/Q sample streams
*/
public static EnumSet<DecoderType> PRIMARY_DECODERS =
EnumSet.of(DecoderType.AM,
DecoderType.DMR,
DecoderType.LTR,
DecoderType.LTR_NET,
DecoderType.MPT1327,
DecoderType.NBFM,
DecoderType.P25_PHASE1,
DecoderType.P25_PHASE2,
DecoderType.PASSPORT);
public static final EnumSet<DecoderType> PRIMARY_DECODERS = EnumSet.of(AM, DMR, LTR, LTR_NET, MPT1327, NBFM, NXDN,
P25_PHASE1, P25_PHASE2, PASSPORT);
/**
* Auxiliary decoders that operate on in-band signalling in the decoded audio channel
*/
public static final EnumSet<DecoderType> AUX_DECODERS =
EnumSet.of(DecoderType.DCS,
DecoderType.FLEETSYNC2,
DecoderType.LJ_1200,
DecoderType.MDC1200,
DecoderType.TAIT_1200);
public static final EnumSet<DecoderType> AUX_DECODERS = EnumSet.of(DCS, FLEETSYNC2, LJ_1200, MDC1200, TAIT_1200);
/**
* Decoders that produce a (recordable) bitstream
*/
public static final EnumSet<DecoderType> BITSTREAM_DECODERS = EnumSet.of(DecoderType.DMR,
DecoderType.MPT1327, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
public static final EnumSet<DecoderType> BITSTREAM_DECODERS = EnumSet.of(DMR, MPT1327, NXDN, P25_PHASE1, P25_PHASE2);
/**
* Decoders that produce (recordable) MBE audio codec frames
*/
public static final EnumSet<DecoderType> MBE_AUDIO_CODEC_DECODERS =
EnumSet.of(DecoderType.DMR, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
public static final EnumSet<DecoderType> MBE_AUDIO_CODEC_DECODERS = EnumSet.of(DMR, NXDN, P25_PHASE1, P25_PHASE2);
public Protocol getProtocol()
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* 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
@@ -29,8 +29,19 @@ import io.github.dsheirer.source.SourceEvent;
*/
public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceEventProvider
{
private static final double TWO_PI = 2 * Math.PI;
private Listener<SourceEvent> mSourceEventListener;
private Listener<Float> mSymbolListener;
private double mSampleRate;
/**
* Sets the decimated final sample rate that will be used for reporting decoder PLL errors.
* @param rate in Hertz
*/
protected void setDecimatedSampleRate(double rate)
{
mSampleRate = rate;
}
/**
* Protocol description suitable for display in the user interface
@@ -68,13 +79,19 @@ public abstract class FeedbackDecoder extends PrimaryDecoder implements ISourceE
}
/**
* Processes the current phase-locked loop (PLL) measurement and sends to the parent tuner for self correction.
* @param pllError as measured in the decoder.
* Processes the current phase-locked loop (PLL) measurement and sends to the channel frequency error manager to
* correct the error by mixing the incoming sample stream to remove the error.
*
* Note: this source event error measurement is rebroadcast by the ProcessingChain and received by the
* ChannelFrequencyErrorManager that lives within the TunerChannelSource where it adjusts the channel's mixer to
* incrementally correct and drive the error toward zero in concert with the parent tuner's auto PPM correction.
*
* @param pllError as measured in the decoder, in Radians.
*/
public void processPLLError(float pllError, int baudRate)
public void processPLLError(float pllError)
{
long frequencyError = (long)(baudRate / 2.0 * (pllError / Math.PI));
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(frequencyError, "Decoder measured error sync locked"));
long correctionRequestHertz = (long)(mSampleRate / TWO_PI * pllError);
broadcast(SourceEvent.frequencyCorrectionRequest(correctionRequestHertz));
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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,7 +18,7 @@
*/
package io.github.dsheirer.module.decode.analog;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State;
@@ -42,7 +42,7 @@ public abstract class AnalogDecoderState extends DecoderState implements ISource
private final static Logger mLog = LoggerFactory.getLogger(AnalogDecoderState.class);
private Listener<SourceEvent> mSourceEventListener = new SourceEventListener();
private DecodeEvent mDecodeEvent;
private IChannelDescriptor mChannelDescriptor = null;
private IBandChannelDescriptor mChannelDescriptor = null;
public AnalogDecoderState()
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* 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
@@ -31,6 +31,7 @@ import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25;
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
@@ -39,13 +40,14 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DecodeConfigAM.class, name = "decodeConfigAM"),
@JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR"),
@JsonSubTypes.Type(value = DecodeConfigLTRNet.class, name = "decodeConfigLTRNet"),
@JsonSubTypes.Type(value = DecodeConfigLTRStandard.class, name = "decodeConfigLTRStandard"),
@JsonSubTypes.Type(value = DecodeConfigMPT1327.class, name = "decodeConfigMPT1327"),
@JsonSubTypes.Type(value = DecodeConfigNBFM.class, name = "decodeConfigNBFM"),
@JsonSubTypes.Type(value = DecodeConfigNXDN.class, name = "decodeConfigNXDN"),
@JsonSubTypes.Type(value = DecodeConfigP25.class, name = "decodeConfigP25"),
@JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport"),
@JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR")
@JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport")
})
@JacksonXmlRootElement(localName = "decode_configuration")
public abstract class DecodeConfiguration extends Configuration
@@ -76,4 +78,10 @@ public abstract class DecodeConfiguration extends Configuration
{
return new int[0];
}
@JsonIgnore
public int getBitRate()
{
return getDecoderType().getProtocol().getBitRate();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* 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
@@ -28,10 +28,8 @@ import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFactory;
import io.github.dsheirer.dsp.psk.demod.DifferentialDemodulatorFloat;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.carrier.CarrierOffsetProcessor;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule;
@@ -48,7 +46,6 @@ import io.github.dsheirer.sample.buffer.IByteBufferProvider;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import java.io.File;
@@ -82,8 +79,8 @@ import org.slf4j.LoggerFactory;
* The DMRMessageProcessor processes messages from the message framer to extract and reassemble link control and
* embedded link control parameters.
*/
public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener, ISourceEventListener,
ISourceEventProvider, Listener<ComplexSamples>
public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener,
ISourceEventListener, Listener<ComplexSamples>
{
private static final Logger LOGGER = LoggerFactory.getLogger(DMRDecoder.class);
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##");
@@ -100,8 +97,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
private IRealDecimationFilter mDecimationFilterQ;
private RealFIRFilter mRRCFilterI;
private RealFIRFilter mRRCFilterQ;
private final PowerMonitor mPowerMonitor = new PowerMonitor();
private final CarrierOffsetProcessor mCarrierOffsetProcessor = new CarrierOffsetProcessor();
/**
* Constructs an instance
@@ -140,9 +135,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
SYMBOL_RATE + " symbol rate)");
}
mPowerMonitor.setSampleRate((int)sampleRate);
mCarrierOffsetProcessor.setSampleRate(sampleRate);
mIBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
mQBasebandFilter = FilterFactory.getRealFilter(getBasebandFilter(sampleRate));
@@ -158,6 +150,10 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
float decimatedSampleRate = (float)sampleRate / decimation;
//Set the decimated sample rate to use for PLL error reporting.
setDecimatedSampleRate(decimatedSampleRate);
float rrcAlpha = Math.abs((float)(5760.0 / decimatedSampleRate));
int symbolLength = (int)Math.floor((-44 * rrcAlpha) + 33);
symbolLength += symbolLength % 2; //Make the symbol length even
@@ -190,9 +186,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
float[] i = mIBasebandFilter.filter(samples.i());
float[] q = mQBasebandFilter.filter(samples.q());
//Process buffer for power measurements
mPowerMonitor.process(i, q);
i = mDecimationFilterI.decimateReal(i);
q = mDecimationFilterQ.decimateReal(q);
@@ -201,25 +194,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
float[] demodulated = mDemodulator.demodulate(i, q);
mSymbolProcessor.receive(demodulated);
//Estimate carrier offset and broadcast at each update. This value is used in the channel spectral display,
// and it's also processed by the tuner's PPM error monitor to auto-adjust the tuner PPM value.
if(mCarrierOffsetProcessor.process(samples))
{
//Tuner PPM Monitor - negate the value to indicate channel error from tuner's PPM that's causing the offset
mPowerMonitor.broadcast(SourceEvent.frequencyErrorMeasurement(-mCarrierOffsetProcessor.getEstimatedOffset()));
//Channel spectral display - when there's a carrier send the estimate, otherwise send a zero to cause the
//display to blank the carrier offset measurement indicator line
if(mCarrierOffsetProcessor.hasCarrier())
{
mPowerMonitor.broadcast(SourceEvent.carrierOffsetMeasurement(mCarrierOffsetProcessor.getEstimatedOffset()));
}
else
{
mPowerMonitor.broadcast(SourceEvent.carrierOffsetMeasurement(0));
}
}
}
/**
@@ -299,21 +273,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
return "DMR C4FM";
}
/**
* Sets the source event listener to receive source events from this decoder.
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mPowerMonitor.setSourceEventListener(listener);
}
@Override
public void removeSourceEventListener()
{
mPowerMonitor.setSourceEventListener(null);
}
@Override
public void start()
{
@@ -340,9 +299,6 @@ public class DMRDecoder extends FeedbackDecoder implements IByteBufferProvider,
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
mCarrierOffsetProcessor.reset();
break;
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* 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
@@ -67,7 +67,6 @@ public class DMRSoftSymbolProcessor
private static final int BUFFER_PROTECTED_REGION_DIBITS = 92;
private static final int BUFFER_WORKSPACE_LENGTH_DIBITS = 25; //This can be adjusted for efficiency
private static final int BUFFER_LENGTH_DIBITS = BUFFER_PROTECTED_REGION_DIBITS + BUFFER_WORKSPACE_LENGTH_DIBITS;
private static final int SYMBOL_RATE = 4800;
private static final float EQUALIZER_LOOP_GAIN = 0.15f;
private static final float MAXIMUM_EQUALIZER_BALANCE = (float)(Math.PI / 3.0);
private static final float MAXIMUM_EQUALIZER_GAIN = 1.25f;
@@ -565,7 +564,7 @@ public class DMRSoftSymbolProcessor
mEqualizerBalance = Math.min(mEqualizerBalance, MAXIMUM_EQUALIZER_BALANCE);
mEqualizerBalance = Math.max(mEqualizerBalance, -MAXIMUM_EQUALIZER_BALANCE);
mFeedbackDecoder.processPLLError(mEqualizerBalance, SYMBOL_RATE);
mFeedbackDecoder.processPLLError(mEqualizerBalance);
//Constrain gain between 1.0f and 1.35f
mEqualizerGain = Math.min(mEqualizerGain, MAXIMUM_EQUALIZER_GAIN);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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,7 +19,7 @@
package io.github.dsheirer.module.decode.dmr;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelConversionRequest;
@@ -225,7 +225,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
* @param currentFrequency of the standard channel
* @param restChannel to identify the new channel frequency to start
*/
public void convertToTrafficChannel(Channel channel, long currentFrequency, IChannelDescriptor restChannel,
public void convertToTrafficChannel(Channel channel, long currentFrequency, IBandChannelDescriptor restChannel,
DMRNetworkConfigurationMonitor networkConfigurationMonitor)
{
mLock.lock();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* 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
@@ -162,7 +162,7 @@ public class DecodeConfigDMR extends DecodeConfiguration
@Override
public ChannelSpecification getChannelSpecification()
{
return new ChannelSpecification(50000.0, 12500, 6500.0, 7200.0);
return new ChannelSpecification(24000.0, 12500, 6500.0, 7200.0);
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 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,7 +19,7 @@
package io.github.dsheirer.module.decode.dmr.channel;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
@@ -35,7 +35,7 @@ import org.apache.commons.lang3.Validate;
*
* Note: timeslots are tracked as 1 and 2
*/
public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor, ITimeslotFrequencyReceiver
public abstract class DMRChannel extends IntegerIdentifier implements IBandChannelDescriptor, ITimeslotFrequencyReceiver
{
private TimeslotFrequency mTimeslotFrequency;
private int mTimeslot;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* 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
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25;
package io.github.dsheirer.module.decode.event;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.identifier.Form;
@@ -28,33 +28,33 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wrapper to track the state of a traffic channel event to manage updates from the control channel and the traffic
* channel and to assist in determining when the communicants of a traffic channel have changed, indicating the need
* for a new event.
* Base channel event tracker
* @param <T> type of decode event being tracked.
*/
public class P25TrafficChannelEventTracker
public abstract class ChannelEventTracker<T extends DecodeEvent>
{
private static final Logger LOGGER = LoggerFactory.getLogger(P25TrafficChannelEventTracker.class);
private static final long STALE_EVENT_THRESHOLD_MS = 2000;
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelEventTracker.class);
private static final long STALE_EVENT_THRESHOLD_MS = 3000;
private static final long MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS = 15000;
private P25ChannelGrantEvent mEvent;
private boolean mStarted = false;
private boolean mComplete = false;
private final T mEvent;
/**
* Constructs an instance
* @param event to track for the traffic channel.
* @param event to be tracked
*/
public P25TrafficChannelEventTracker(P25ChannelGrantEvent event)
public ChannelEventTracker(T event)
{
mEvent = event;
}
/**
* Access the underlying traffic channel event that is being tracked.
*
* @return event.
*/
public P25ChannelGrantEvent getEvent()
public T getEvent()
{
return mEvent;
}
@@ -62,6 +62,7 @@ public class P25TrafficChannelEventTracker
/**
* Indicates if this event is stale relative to the provided timestamp. Staleness is determined by the time delta
* between the event's end time and timestamp argument.
*
* @param timestamp to check for staleness
* @return true if the delta time exceeds a threshold.
*/
@@ -76,23 +77,23 @@ public class P25TrafficChannelEventTracker
}
/**
* Indicates if the TDMA data channel duration exceeds the threshold (15 seconds)
* Indicates if the data channel duration exceeds the threshold (15 seconds)
*/
public boolean exceedsMaxTDMADataDuration()
public boolean exceedsMaxDataDuration()
{
return getEvent().getDuration() > MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS;
}
/**
* Adds the identifier to the tracked event if the event's identifier collection does not already have it.
*
* @param identifier to add
*/
public void addIdentifierIfMissing(Identifier identifier)
{
if(identifier != null && !getEvent().getIdentifierCollection().hasIdentifier(identifier))
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(getEvent().getIdentifierCollection()
.getIdentifiers());
MutableIdentifierCollection mic = new MutableIdentifierCollection(getEvent().getIdentifierCollection().getIdentifiers());
mic.update(identifier);
getEvent().setIdentifierCollection(mic);
}
@@ -101,7 +102,8 @@ public class P25TrafficChannelEventTracker
/**
* Adds the additional details to the tracked event if the current event details are null or if they do not contain
* the additional details.
* @param additionalDetails
*
* @param additionalDetails for the event
*/
public void addDetailsIfMissing(String additionalDetails)
{
@@ -135,6 +137,7 @@ public class P25TrafficChannelEventTracker
/**
* Indicates if the tracked event from identifier is non null and that it is different to the from argument.
*
* @param fromToCompare against the current event from identifier.
* @return true if they are different.
*/
@@ -153,6 +156,7 @@ public class P25TrafficChannelEventTracker
/**
* Checks the call for staleness and verifies that the call event TO identifier is equal to the TO identifier
* in the provided identifier collection.
*
* @param toCompareIC containing a TO and optionally a FROM identifier to compare.
* @param timestamp to check for staleness
* @return true if the call identifiers are the same and the call is not stale.
@@ -195,6 +199,7 @@ public class P25TrafficChannelEventTracker
/**
* Indicates if the event has been marked as complete by traffic channel signalling.
*
* @return complete status.
*/
public boolean isComplete()
@@ -204,6 +209,7 @@ public class P25TrafficChannelEventTracker
/**
* Indicates if the event has been marked as started by the traffic channel for HDU or LDU signalling.
*
* @return started status.
*/
public boolean isStarted()
@@ -236,6 +242,7 @@ public class P25TrafficChannelEventTracker
*
* Note: once the event is being updated from the traffic channel, any attempts to update from the control channel
* are ignored.
*
* @param timestamp to assign.
*/
public void updateDurationTraffic(long timestamp)
@@ -255,6 +262,7 @@ public class P25TrafficChannelEventTracker
* Mark the event as complete and assign final end timestamp to the event.
*
* Note: further attempts to complete an already complete event are ignored.
*
* @param timestamp to assign.
* @return true if the timestamp was updated
*/
@@ -272,6 +280,7 @@ public class P25TrafficChannelEventTracker
/**
* Updates the details for the tracked event.
*
* @param details to update
*/
public void setDetails(String details)
@@ -279,8 +288,46 @@ public class P25TrafficChannelEventTracker
getEvent().setDetails(details);
}
/**
* Prefixes the details the current event details text
* @param details to prefix
*/
public void prefixDetails(String details)
{
if(getEvent().getDetails() == null)
{
getEvent().setDetails(details);
}
else
{
if(!getEvent().getDetails().contains(details))
{
getEvent().setDetails(details + " " + getEvent().getDetails());
}
}
}
/**
* Adds the details to the end of the existing event text or sets the event details.
*/
public void addDetails(String details)
{
if(getEvent().getDetails() == null)
{
getEvent().setDetails(details);
}
else
{
if(!getEvent().getDetails().contains(details))
{
getEvent().setDetails(getEvent().getDetails() + " " + details);
}
}
}
/**
* Updates the channel descriptor for the tracked event.
*
* @param channelDescriptor to update.
*/
public void addChannelDescriptorIfMissing(IChannelDescriptor channelDescriptor)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* 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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* 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
@@ -101,12 +101,19 @@ public class DecodeEventDuplicateDetector
}
/**
* Generates a unique event key that is a combinatio of the TO identifier value and the details of the event.
* Generates a unique event key that is a combination of the TO identifier value and the details of the event.
* @param event for the key
* @return generated key.
*/
private static String getKey(IDecodeEvent event)
{
if(event.getIdentifierCollection().getFromIdentifier() != null)
{
return event.getIdentifierCollection().getToIdentifier().toString() +
event.getIdentifierCollection().getFromIdentifier().toString() +
event.getDetails();
}
return event.getIdentifierCollection().getToIdentifier().toString() + event.getDetails();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* 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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -59,24 +59,24 @@ import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
/**
* Decode event table view
*/
public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain>
{
private static final long serialVersionUID = 1L;
private final static Logger mLog = LoggerFactory.getLogger(DecodeEventPanel.class);
private static final String TABLE_PREFERENCE_KEY = "decode.event.panel";
private JTable mTable;
private JTableColumnWidthMonitor mTableColumnWidthMonitor;
private DecodeEventModel mEventModel = new DecodeEventModel();
private final JTable mTable;
private final DecodeEventModel mEventModel = new DecodeEventModel();
private DecodeEventHistory mCurrentEventHistory;
private JScrollPane mEmptyScroller;
private IconModel mIconModel;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private TimestampCellRenderer mTimestampCellRenderer;
private FilterSet<IDecodeEvent> mFilterSet = new DecodeEventFilterSet();
private TableRowSorter<TableModel> mTableRowSorter;
private HistoryManagementPanel<IDecodeEvent> mHistoryManagementPanel;
private final IconModel mIconModel;
private final AliasModel mAliasModel;
private final UserPreferences mUserPreferences;
private final TimestampCellRenderer mTimestampCellRenderer;
private final FilterSet<IDecodeEvent> mFilterSet = new DecodeEventFilterSet();
private final HistoryManagementPanel<IDecodeEvent> mHistoryManagementPanel;
/**
@@ -93,19 +93,19 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
mUserPreferences = userPreferences;
mTimestampCellRenderer = new TimestampCellRenderer();
mTable = new JTable(mEventModel);
mTableRowSorter = new TableRowSorter<>(mEventModel);
mTableRowSorter.setRowFilter(new EventRowFilter());
mTable.setRowSorter(mTableRowSorter);
mTableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
TableRowSorter<TableModel> tableRowSorter = new TableRowSorter<>(mEventModel);
tableRowSorter.setRowFilter(new EventRowFilter());
mTable.setRowSorter(tableRowSorter);
JTableColumnWidthMonitor tableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
updateCellRenderers();
mHistoryManagementPanel = new HistoryManagementPanel<>(mEventModel, "Event Filter Editor");
mHistoryManagementPanel.updateFilterSet(mFilterSet);
add(mHistoryManagementPanel, "span,growx");
mEmptyScroller = new JScrollPane(mTable);
add(mEmptyScroller);
JScrollPane emptyScroller = new JScrollPane(mTable);
add(emptyScroller);
//Register filter change listener to refresh the table any time the event filters are changed.
mFilterSet.register(() -> mEventModel.fireTableDataChanged());
mFilterSet.register(mEventModel::fireTableDataChanged);
}
public void dispose()
@@ -122,7 +122,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
{
if(preferenceType == PreferenceType.DECODE_EVENT || preferenceType == PreferenceType.TALKGROUP_FORMAT)
{
EventQueue.invokeLater(() -> mTimestampCellRenderer.updatePreferences());
EventQueue.invokeLater(mTimestampCellRenderer::updatePreferences);
}
}
@@ -215,7 +215,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
for(Identifier identifier: identifiers)
{
if(sb.length() > 0)
if(!sb.isEmpty())
{
sb.append(",");
}
@@ -262,9 +262,8 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
ImageIcon icon = null;
String text = null;
if(value instanceof IdentifierCollection)
if(value instanceof IdentifierCollection identifierCollection)
{
IdentifierCollection identifierCollection = (IdentifierCollection)value;
List<Identifier> identifiers = identifierCollection.getIdentifiers(mRole);
if(identifiers != null && !identifiers.isEmpty())
@@ -281,13 +280,13 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
if(!aliases.isEmpty())
{
if(sb.length() > 0)
if(!sb.isEmpty())
{
sb.append(",");
}
sb.append(Joiner.on(", ").skipNulls().join(aliases));
color = aliases.get(0).getDisplayColor();
icon = mIconModel.getIcon(aliases.get(0).getIconName(), IconModel.DEFAULT_ICON_SIZE);
color = aliases.getFirst().getDisplayColor();
icon = mIconModel.getIcon(aliases.getFirst().getIconName(), IconModel.DEFAULT_ICON_SIZE);
}
}
@@ -337,9 +336,9 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
}
}
public class DurationCellRenderer extends DefaultTableCellRenderer
public static class DurationCellRenderer extends DefaultTableCellRenderer
{
private DecimalFormat mDecimalFormat = new DecimalFormat("0.0");
private final DecimalFormat mDecimalFormat = new DecimalFormat("0.0");
public DurationCellRenderer()
{
@@ -372,9 +371,9 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
/**
* Frequency value cell renderer
*/
public class FrequencyCellRenderer extends DefaultTableCellRenderer
public static class FrequencyCellRenderer extends DefaultTableCellRenderer
{
private DecimalFormat mFrequencyFormatter = new DecimalFormat("0.00000");
private final DecimalFormat mFrequencyFormatter = new DecimalFormat("0.00000");
public FrequencyCellRenderer()
{
@@ -388,10 +387,8 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
String formatted = null;
if(value instanceof IChannelDescriptor)
if(value instanceof IChannelDescriptor channelDescriptor)
{
IChannelDescriptor channelDescriptor = (IChannelDescriptor)value;
long frequency = channelDescriptor.getDownlinkFrequency();
if(frequency > 0)
@@ -409,7 +406,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
/**
* Channel descriptor value cell renderer
*/
public class ChannelDescriptorCellRenderer extends DefaultTableCellRenderer
public static class ChannelDescriptorCellRenderer extends DefaultTableCellRenderer
{
public ChannelDescriptorCellRenderer()
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 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,7 +18,7 @@
*/
package io.github.dsheirer.module.decode.ltrnet;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
@@ -97,7 +97,7 @@ public class LTRNetDecoderState extends DecoderState
}
@Override
protected IChannelDescriptor getCurrentChannel()
protected IBandChannelDescriptor getCurrentChannel()
{
if(mCurrentChannelNumber > 0)
{

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2025 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
@@ -15,16 +14,16 @@
*
* 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.ltrnet.channel;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
public class LtrNetChannel implements IChannelDescriptor, Comparable<LtrNetChannel>
public class LtrNetChannel implements IBandChannelDescriptor, Comparable<LtrNetChannel>
{
private long mDownlink;
private long mUplink;

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2025 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
@@ -15,16 +14,16 @@
*
* 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.ltrstandard.channel;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
public class LtrChannel implements IChannelDescriptor, Comparable<LtrChannel>
public class LtrChannel implements IBandChannelDescriptor, Comparable<LtrChannel>
{
private long mDownlink;
private int mChannel;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 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,7 +18,7 @@
*/
package io.github.dsheirer.module.decode.mpt1327;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.protocol.Protocol;
@@ -52,7 +52,7 @@ public class MPT1327ChannelGrantEvent extends MPT1327DecodeEvent
protected long mDuration;
protected DecodeEventType mDecodeEventType;
protected IdentifierCollection mIdentifierCollection;
protected IChannelDescriptor mChannelDescriptor;
protected IBandChannelDescriptor mChannelDescriptor;
protected String mDetails;
/**
@@ -90,7 +90,7 @@ public class MPT1327ChannelGrantEvent extends MPT1327DecodeEvent
* Sets the channel descriptor for this event
* @param channelDescriptor
*/
public MPT1327ChannelGrantDecodeEventBuilder channel(IChannelDescriptor channelDescriptor)
public MPT1327ChannelGrantDecodeEventBuilder channel(IBandChannelDescriptor channelDescriptor)
{
mChannelDescriptor = channelDescriptor;
return this;

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2025 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
@@ -15,22 +14,21 @@
*
* 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.mpt1327.channel;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
import java.util.Objects;
/**
* MPT-1327 Channel
*/
public class MPT1327Channel implements IChannelDescriptor
public class MPT1327Channel implements IBandChannelDescriptor
{
private int mChannelNumber;
private ChannelMap mChannelMap;

View File

@@ -0,0 +1,218 @@
/*
* *****************************************************************************
* 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.nxdn;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.Encoding;
import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import java.util.ArrayList;
import java.util.List;
/**
* NXDN decoder configuration
*/
public class DecodeConfigNXDN extends DecodeConfiguration
{
public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 200;
public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500;
public static final int CHANNEL_ROTATION_DELAY_MAXIMUM_MS = 2000;
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
private boolean mIgnoreDataCalls = false;
private boolean mIgnoreEncryptedCalls = false;
private static final ChannelSpecification CHANNEL_4800 = new ChannelSpecification(12000.0, 6250, 3000.0, 3125.0);
private static final ChannelSpecification CHANNEL_9600 = new ChannelSpecification(24000.0, 12500, 5200.0, 6250.0);
private TransmissionMode mTransmissionMode = TransmissionMode.M4800;
private List<ChannelFrequency> mChannelMap = new ArrayList<>();
private Encoding mEncoding = Encoding.UTF8;
/**
* Default constructor for Jackson.
*/
public DecodeConfigNXDN()
{
}
/**
* Override the parent method to indicate 4800 or 9600 bits per second, depending on the mode.
*/
@Override
public int getBitRate()
{
return getTransmissionMode().getSymbolRate() * 2;
}
/**
* Constructs an instance
* @param transmissionMode for the system
*/
public DecodeConfigNXDN(TransmissionMode transmissionMode)
{
mTransmissionMode = transmissionMode;
}
/**
* Optional user-provided channel to frequency mapping
* @return map
*/
@JacksonXmlProperty(localName = "channelMap")
public List<ChannelFrequency> getChannelMap()
{
return mChannelMap;
}
/**
* Sets the optional user-provided channel to frequency mapping
* @param channelMap with entries to configure channels
*/
public void setChannelMap(List<ChannelFrequency> channelMap)
{
mChannelMap = channelMap;
}
/**
* Adds the channel frequency entry to the map
* @param channelFrequency to add
*/
public void add(ChannelFrequency channelFrequency)
{
mChannelMap.add(channelFrequency);
}
/**
* Ignore data calls
* @return true if ignore
*/
@JacksonXmlProperty(isAttribute = true, localName = "ignore_data_calls")
public boolean isIgnoreDataCalls()
{
return mIgnoreDataCalls;
}
/**
* Set ignore data calls
* @param ignore data calls
*/
public void setIgnoreDataCalls(boolean ignore)
{
mIgnoreDataCalls = ignore;
}
/**
* Ignore encrypted calls
* @return true if ignore
*/
@JacksonXmlProperty(isAttribute = true, localName = "ignore_encrypted_calls")
public boolean isIgnoreEncryptedCalls()
{
return mIgnoreEncryptedCalls;
}
/**
* Set ignore encrypted calls
* @param ignore encrypted calls
*/
public void setIgnoreEncryptedCalls(boolean ignore)
{
mIgnoreEncryptedCalls = ignore;
}
/**
* Traffic channel pool size.
* @return
*/
@JacksonXmlProperty(isAttribute = true, localName = "traffic_channel_pool_size")
public int getTrafficChannelPoolSize()
{
return mTrafficChannelPoolSize;
}
/**
* Sets the traffic channel pool size which is the maximum number of
* simultaneous traffic channels that can be allocated.
*
* This limits the maximum calls so that busy systems won't cause more
* traffic channels to be allocated than the decoder/software/host computer
* can support.
*/
public void setTrafficChannelPoolSize(int size)
{
mTrafficChannelPoolSize = size;
}
/**
* Transmission mode for this system.
*/
@JacksonXmlProperty(isAttribute = true, localName = "mode")
public TransmissionMode getTransmissionMode()
{
return mTransmissionMode;
}
/**
* Sets/changes the transmission mode
* @param transmissionMode to use
*/
public void setTransmissionMode(TransmissionMode transmissionMode)
{
mTransmissionMode = transmissionMode;
}
/**
* Character set for encoding talker alias values
*/
@JacksonXmlProperty(isAttribute = true, localName = "charset")
public Encoding getEncoding()
{
return mEncoding;
}
/**
* Sets the character set for talker alias encoding
* @param encoding to use
*/
public void setEncoding(Encoding encoding)
{
mEncoding = encoding;
}
@JsonIgnore
@Override
public DecoderType getDecoderType()
{
return DecoderType.NXDN;
}
@JsonIgnore
@Override
public ChannelSpecification getChannelSpecification()
{
return switch(getTransmissionMode())
{
case M4800, TYPE_D -> CHANNEL_4800;
case M9600 -> CHANNEL_9600;
};
}
}

View File

@@ -0,0 +1,60 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.module.decode.event.ChannelEventTracker;
import io.github.dsheirer.module.decode.event.DecodeEvent;
/**
* Wrapper to track the state of a traffic channel event to manage updates from the control channel and the traffic
* channel and to assist in determining when the communicants of a traffic channel have changed, indicating the need
* for a new event.
*/
public class NXDNChannelEventTracker extends ChannelEventTracker<DecodeEvent>
{
private boolean mTrafficChannelAllocated = false;
/**
* Constructs an instance
*
* @param event to track for the traffic channel.
*/
public NXDNChannelEventTracker(DecodeEvent event)
{
super(event);
}
/**
* Indicates if a traffic channel was allocated for this event
*/
public boolean isTrafficChannelAllocated()
{
return mTrafficChannelAllocated;
}
/**
* Sets flag to indicate if traffic channel was allocated
* @param allocated as the flag
*/
public void setTrafficChannelAllocated(boolean allocated)
{
mTrafficChannelAllocated = allocated;
}
}

View File

@@ -0,0 +1,43 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.controller.channel.event.PreloadDataContent;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.List;
/**
* Channel access info and optional channel frequencies to preload into a traffic channel.
*/
public class NXDNChannelInfoPreloadData extends PreloadDataContent<ChannelInformation>
{
/**
* Constructs an instance
*
* @param info from the channel to preload
* @param frequencies optional provided by the user in the config
*/
public NXDNChannelInfoPreloadData(ChannelAccessInformation info, List<ChannelFrequency> frequencies)
{
super(new ChannelInformation(info, frequencies));
}
}

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.protocol.Protocol;
/**
* NXDN Decode Event
*/
public class NXDNDecodeEvent extends DecodeEvent
{
/**
* Constructs a decode event
* @param decodeEventType for the event
* @param start timestamp
*/
public NXDNDecodeEvent(DecodeEventType decodeEventType, long start)
{
super(decodeEventType, start);
setProtocol(Protocol.NXDN);
}
/**
* Creates a new decode event builder with the specified start timestamp.
* @param timeStart for the event
* @return builder
*/
public static DecodeEventBuilder builder(DecodeEventType decodeEventType, long timeStart)
{
DecodeEventBuilder decodeEventBuilder = new DecodeEventBuilder(decodeEventType, timeStart);
decodeEventBuilder.protocol(Protocol.NXDN);
decodeEventBuilder.timeslot(0);
return decodeEventBuilder;
}
}

View File

@@ -0,0 +1,404 @@
/*
* *****************************************************************************
* 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.nxdn;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.fm.FmDemodulatorFactory;
import io.github.dsheirer.dsp.fm.IDemodulator;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* NXDN Decoder for 4800, 9600 and Type-C/D systems.
*/
public class NXDNDecoder extends FeedbackDecoder implements IByteBufferProvider, IComplexSamplesListener,
ISourceEventListener, Listener<ComplexSamples>
{
private static final Logger LOGGER = LoggerFactory.getLogger(NXDNDecoder.class);
private static final Map<String,float[]> BASEBAND_FILTERS = new HashMap<>();
private final DecodeConfigNXDN mConfig;
private final NXDNSymbolProcessor mSymbolProcessor;
private final NXDNMessageFramer mMessageFramer;
private final NXDNMessageProcessor mMessageProcessor;
private IRealDecimationFilter mDecimationFilterI;
private IRealDecimationFilter mDecimationFilterQ;
private IRealFilter mBasebandFilterI;
private IRealFilter mBasebandFilterQ;
private IRealFilter mPulseShapingFilter;
private IDemodulator mDemodulator;
@Override
public DecoderType getDecoderType()
{
return DecoderType.NXDN;
}
/**
* Constructs an instance
* @param config for the decoder
*/
public NXDNDecoder(DecodeConfigNXDN config)
{
mConfig = config;
mMessageProcessor = new NXDNMessageProcessor(config);
mMessageProcessor.setMessageListener(getMessageListener());
mMessageFramer = new NXDNMessageFramer(mMessageProcessor, config.getTransmissionMode());
mSymbolProcessor = new NXDNSymbolProcessor(mMessageFramer, this);
}
/**
* Preloads NXDN channel details passed from the control channel used to resolve channel frequency information
* in traffic channel broadcast messaging.
*
* @param preloadData containing a DMR network configuration monitor.
*/
@Subscribe
public void preload(NXDNChannelInfoPreloadData preloadData)
{
if(mMessageProcessor != null)
{
mMessageProcessor.preload(preloadData);
}
}
@Override
public String getProtocolDescription()
{
return "NXDN " + mConfig.getTransmissionMode();
}
/**
* Sets the sample rate and configures internal decoder components.
* @param sampleRate of the channel to decode
*/
public void setSampleRate(double sampleRate)
{
float samplesPerSymbol = (float)sampleRate / mConfig.getTransmissionMode().getSymbolRate();
if(samplesPerSymbol <= 4.0f)
{
throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be at least " + (mConfig.getTransmissionMode().getSymbolRate() * 4) + " Hz");
}
int decimation = 1;
//Identify decimation that gets us as close to 5.0 Samples Per Symbol as possible
while(samplesPerSymbol >= 10.0)
{
decimation *= 2;
samplesPerSymbol /= 2;
}
mDecimationFilterI = DecimationFilterFactory.getRealDecimationFilter(decimation);
mDecimationFilterQ = DecimationFilterFactory.getRealDecimationFilter(decimation);
float decimatedSampleRate = (float)sampleRate / decimation;
//Set the decimated sample rate to use for PLL error reporting.
setDecimatedSampleRate(decimatedSampleRate);
int symbolLength = 26;
float rrcAlpha = 0.2f;
float[] rrcTaps = FilterFactory.getRRC(samplesPerSymbol, symbolLength, rrcAlpha);
mPulseShapingFilter = new RealFIRFilter(rrcTaps);
float[] basebandTaps = getBasebandFilter(decimatedSampleRate, mConfig);
mBasebandFilterI = FilterFactory.getRealFilter(basebandTaps);
mBasebandFilterQ = FilterFactory.getRealFilter(basebandTaps);
mDemodulator = FmDemodulatorFactory.getFmDemodulator();
mSymbolProcessor.setSamplesPerSymbol(samplesPerSymbol);
mMessageProcessor.setMessageListener(getMessageListener());
}
/**
* Primary method for processing incoming complex sample buffers
* @param samples containing channelized complex samples
*/
@Override
public void receive(ComplexSamples samples)
{
//Update the message framer with the timestamp from the incoming sample buffer.
mMessageFramer.setTimestamp(samples.timestamp());
float[] i = mDecimationFilterI.decimateReal(samples.i());
float[] q = mDecimationFilterQ.decimateReal(samples.q());
i = mBasebandFilterI.filter(i);
q = mBasebandFilterQ.filter(q);
float[] demodulated = mDemodulator.demodulate(i, q);
demodulated = mPulseShapingFilter.filter(demodulated);
//Process demodulated samples into symbols and apply message sync detection and framing.
mSymbolProcessor.process(demodulated);
}
/**
* Constructs a baseband filter for this decoder using the current sample rate
*/
private float[] getBasebandFilter(double sampleRate, DecodeConfigNXDN config)
{
String mapKey = config.getTransmissionMode().name() + sampleRate;
float[] coefficients = null;
if(BASEBAND_FILTERS.containsKey(mapKey))
{
coefficients = BASEBAND_FILTERS.get(mapKey);
}
else
{
ChannelSpecification channel = config.getChannelSpecification();
FIRFilterSpecification specification = FIRFilterSpecification
.lowPassBuilder()
.sampleRate(sampleRate)
.passBandCutoff(channel.getPassFrequency())
.passBandAmplitude(1.0).passBandRipple(0.01)
.stopBandAmplitude(0.0).stopBandStart(channel.getStopFrequency())
.stopBandRipple(0.01).build();
try
{
coefficients = FilterFactory.getTaps(specification);
if(coefficients != null)
{
BASEBAND_FILTERS.put(mapKey, coefficients);
}
}
catch(Exception fde) //FilterDesignException
{
System.out.println("Error");
}
}
if(coefficients == null)
{
throw new IllegalStateException("Unable to design low pass filter for sample rate [" + sampleRate + "]");
}
return coefficients;
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public void setBufferListener(Listener<ByteBuffer> listener)
{
mSymbolProcessor.setBufferListener(listener);
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public void removeBufferListener(Listener<ByteBuffer> listener)
{
mSymbolProcessor.setBufferListener(null);
}
/**
* Implements the IByteBufferProvider interface - delegates to the symbol processor
*/
@Override
public boolean hasBufferListeners()
{
return mSymbolProcessor.hasBufferListener();
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return this::process;
}
@Override
public void stop()
{
super.stop();
mSymbolProcessor.dispose();
}
/**
* Process source events
*/
private void process(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
mSymbolProcessor.resetBalance();
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
setSampleRate(sourceEvent.getValue().doubleValue());
break;
}
}
@Override
public Listener<ComplexSamples> getComplexSamplesListener()
{
return this;
}
/**
* Listener implementation used in the main method for offline demodulating an I/Q file..
*/
public static class DebugListener implements Listener<IMessage>
{
private int mInvalidMessages;
private int mValidMessages;
private int mValidMessageBitErrors;
public void log()
{
System.out.println("Invalid: " + mInvalidMessages + " Valid: " + mValidMessages + " Valid-Bit Errors: " + mValidMessageBitErrors);
}
@Override
public void receive(IMessage message)
{
if(message instanceof NXDNMessage nxdn)
{
if(nxdn.isValid())
{
System.out.println("BE:" + nxdn.getMessage().getCorrectedBitCount() + " " + nxdn);
mValidMessages++;
mValidMessageBitErrors += nxdn.getMessage().getCorrectedBitCount();
}
else
{
System.out.println("\t\tBE:" + nxdn.getMessage().getCorrectedBitCount() + " " + nxdn);
mInvalidMessages++;
}
}
else
{
System.out.println(message.toString());
}
}
}
public static void main(String[] args)
{
LOGGER.info("Starting ...");
DecodeConfigNXDN config = null;
// String directory = "D:\\Recordings\\NXDN\\"; //Windows
String directory = "/run/media/denny/T9/Recordings/NXDN/"; //Linux
//NXDN 9600 Channels
// config = new DecodeConfigNXDN(TransmissionMode.M9600);
// config.add(new ChannelFrequency(105, 451887500, 0));
// String file = directory + "20251127_063701_451887500_Bush-NXDN-96_Sentinel-Heights_Control_45_baseband.wav";
//Weird 938 MHz site in Springfield, VA that is all CRC errors.
// String file = directory + "20260324_033833_938736125_SYSTEM_SITE_null_47_baseband.wav";
//NXDN 4800 Channels
config = new DecodeConfigNXDN(TransmissionMode.M4800);
// String file = directory + "20251128_052514_150845000_MobileTech-NXDN-48_Fulton_Control_47_baseband.wav";
//Lower SNR recordings
String file = directory + "20260430_023716_150867500_Mobiletech-Communications-(NXDN)_Pulaski_Pulaski_11_baseband.wav";
// String file = directory + "20260430_023556_152982500_Mobiletech-Communications-(NXDN)_Manlius_Manlius_15_baseband.wav";
//This traffic channel sample Has Sync Detects between 1,096,430 - 1,111,430 samples (decimated sample rate: 12,500 Hz)
// String file = directory + "20260104_065056_153582500_Mobiletech-Communications-(NXDN)_Fulton_LCN-3_50_baseband.wav";
// String file = directory + "20260104_065240_153582500_Mobiletech-Communications-(NXDN)_Fulton_LCN-3_50_baseband.wav";
// String file = directory + "AceVentura Sample 1/20260122_071724_154987500_SCFR_SITE_null_1_baseband.wav";
// String file = directory + "AceVentura Sample 2/20260122_073239_154987500_SCFR_SITE_null_1_baseband.wav";
// String file = directory + "20260322_063443_150845000_Mobiletech-Communications-(NXDN)_Fulton_Fulton_2_baseband.wav";
//Type-D Channels
// config = new DecodeConfigNXDN(TransmissionMode.TYPE_D);
// String file = directory + "Type C and D/Action Radio Hennepin tadscottsmith/20260402_152326_464125000_Action-Radio---Hennepin_LCN-1_103_baseband.wav";
// String file = directory + "Type C and D/Action Radio Hennepin 2 tadscottsmith/recordings/20260330_221120_464650000_Action-Radio_Hennepin_LCN-4_92_baseband.wav";
// String file = directory + "Type C and D/20260314_091952_451775000_Heimes_Omaha_TYPE_D_114_baseband.wav";
// String file = directory + "Type C and D/20260318_034719_452675000_Heimes_Omaha_TYPE_D_0_baseband.wav";
// String file = directory + "Type C and D/20260320_065339_451450000_Heimes_Omaha_TYPE_D_0_baseband.wav";
//This file has PPM mis-adjusted by 8.0 to test for equalizer balance
// String file = directory + "20260119_044740_150845000_Mobiletech-Communications-(NXDN)_Oswego_Control_2_baseband.wav";
System.out.println("Processing File: " + file);
boolean autoReplay = false;
NXDNDecoder decoder = new NXDNDecoder(config);
decoder.start();
DebugListener listener = new DebugListener();
decoder.setMessageListener(listener);
try(ComplexWaveSource source = new ComplexWaveSource(new File(file), autoReplay))
{
source.setListener(iNativeBuffer -> {
Iterator<ComplexSamples> it = iNativeBuffer.iterator();
while(it.hasNext())
{
decoder.receive(it.next());
}
});
source.start();
decoder.setSampleRate(source.getSampleRate());
System.out.println("Recording File Sample Rate: " + source.getSampleRate());
while(true)
{
source.next(2048, true);
}
}
catch(IOException ioe)
{
LOGGER.error("Error", ioe);
}
listener.log();
LOGGER.info("Finished");
}
}

View File

@@ -0,0 +1,738 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent;
import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignment;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignmentDuplicateControl;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallReceptionRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequestWithESN;
import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallRequestHeader;
import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusInquiryRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusInquiryResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignment;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateControl;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateTraffic;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallConnectionResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallReceptionRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.data.GPS;
import io.github.dsheirer.module.decode.nxdn.layer3.data.NXDNPacketMessage;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryRequestMultiSystem;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryResponseMultiSystem;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationParameterInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationResponseTypeD;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationClearResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationCommand;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationResponseTypeD;
import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAliasComplete;
import io.github.dsheirer.protocol.Protocol;
import java.util.Collections;
import java.util.List;
/**
* NXDN decoder state
*/
public class NXDNDecoderState extends DecoderState
{
private static final int IDLE_DURING_CALL_MAX_COUNT = 5;
private final Channel mChannel;
private final NXDNNetworkConfigurationMonitor mNetworkConfigurationMonitor = new NXDNNetworkConfigurationMonitor();
private final NXDNTrafficChannelManager mTrafficChannelManager;
private boolean mEncryptedCallStateDetermined = false;
private boolean mEncryptedCall = false;
private int mDisconnectResponseCount = 0;
private int mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT;
/**
* Constructs an instance
* @param channel for this state
* @param trafficChannelManager shared with this channel
*/
public NXDNDecoderState(Channel channel, NXDNTrafficChannelManager trafficChannelManager)
{
mChannel = channel;
mTrafficChannelManager = trafficChannelManager;
}
/**
* Indicates if we have a non-null traffic channel manager
*/
private boolean hasTrafficChannelManager()
{
return mTrafficChannelManager != null;
}
@Override
public void init()
{
}
@Override
public void start()
{
super.start();
//Change the default (45-second) traffic channel timeout to 1 second
if(mChannel.isTrafficChannel())
{
broadcast(new ChangeChannelTimeoutEvent(this, Channel.ChannelType.TRAFFIC, 1000));
}
}
@Override
public DecoderType getDecoderType()
{
return DecoderType.NXDN;
}
@Override
public void receiveDecoderStateEvent(DecoderStateEvent event)
{
switch(event.getEvent())
{
case REQUEST_RESET:
resetState();
break;
case NOTIFICATION_SOURCE_FREQUENCY:
setCurrentFrequency(event.getFrequency());
//Only update the traffic channel manager if we're not a traffic channel.
if(hasTrafficChannelManager() && mChannel.isStandardChannel())
{
mTrafficChannelManager.setCurrentControlFrequency(getCurrentFrequency(), mChannel);
}
break;
default:
break;
}
}
@Override
public String getActivitySummary()
{
StringBuilder sb = new StringBuilder();
sb.append("NXDN Channel Activity Summary\n");
if(mChannel.getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN)
{
sb.append("Transmission Mode: ").append(configNXDN.getTransmissionMode()).append("\n");
}
sb.append(mNetworkConfigurationMonitor.getSummary()).append("\n");
sb.append(mTrafficChannelManager.getTalkerAliasManager().getAliasSummary()).append("\n");
return sb.toString();
}
@Override
public void receive(IMessage iMessage)
{
if(iMessage instanceof NXDNMessage nxdn && nxdn.isValid())
{
if(nxdn instanceof NXDNLayer3Message layer3)
{
processLayer3(layer3);
}
else if(nxdn instanceof Audio audio)
{
processAudio(audio);
}
else if(nxdn instanceof NXDNPacketMessage packetMessage)
{
processPacketMessage(packetMessage);
}
}
}
/**
* Broadcasts the arguments as a new decode event
* @param identifiers involved in the event
* @param timestamp of the message/event
* @param decodeEventType of the event
* @param details for the event
*/
private void broadcastEvent(List<Identifier> identifiers, long timestamp, DecodeEventType decodeEventType, String details)
{
MutableIdentifierCollection mic = getMutableIdentifierCollection(identifiers);
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic);
broadcast(NXDNDecodeEvent.builder(decodeEventType, timestamp)
.channel(getCurrentChannel())
.details(details)
.identifiers(mic)
.build());
}
/**
* Creates a copy of the current identifier collection, removes any USER identifiers and adds the argument identifiers
* passing each identifier through the patch group manager to replace with a patch group if it exists
* @param identifiers to add to the collection copy
* @return collection
*/
private MutableIdentifierCollection getMutableIdentifierCollection(List<Identifier> identifiers)
{
MutableIdentifierCollection requestCollection = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
requestCollection.remove(IdentifierClass.USER);
for(Identifier identifier: identifiers)
{
requestCollection.update(identifier);
}
return requestCollection;
}
/**
* Creates a copy of the current identifier collection, removes any USER identifiers and adds the argument identifier
* passed through the patch group manager to replace with a patch group if it exists
* @param identifier to add to the collection copy
* @return collection
*/
private MutableIdentifierCollection getMutableIdentifierCollection(Identifier identifier)
{
return getMutableIdentifierCollection(Collections.singletonList(identifier));
}
/**
* Process layer 3 messages
* @param layer3 message to process
*/
private void processLayer3(NXDNLayer3Message layer3)
{
State state = layer3.getMessageType().isControl() ? State.CONTROL : State.ACTIVE;
DecoderStateEvent.Event event = DecoderStateEvent.Event.DECODE;
switch(layer3.getMessageType())
{
case CONTROL_OUT_01_CC_VOICE_CALL_RESPONSE:
case TYPE_D_OUT_00_CC_CALL_RESPONSE:
if(layer3 instanceof VoiceCallResponse vcr)
{
//Controller responds to a voice call request with either a channel grant, or this message if
//it can't support the call or the called party is not available
broadcastEvent(vcr.getIdentifiers(), vcr.getTimestamp(), DecodeEventType.RESPONSE,
"VOICE CALL REQUEST - RESPONSE:" + vcr.getCause().toString());
}
break;
case TRAFFIC_OUT_01_CC_VOICE_CALL:
case TYPE_D_OUT_01_CC_VOICE_CALL:
if(layer3 instanceof VoiceCall vc)
{
mIdleDuringCallCount = 0;
getIdentifierCollection().update(vc.getIdentifiers());
mEncryptedCallStateDetermined = true;
mEncryptedCall = vc.getEncryptionKeyIdentifier().isEncrypted();
state = mEncryptedCall ? State.ENCRYPTED : State.CALL;
event = DecoderStateEvent.Event.START;
mTrafficChannelManager.processVoiceCall(vc, getCurrentChannel());
}
break;
case CONTROL_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST:
case TRAFFIC_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST:
if(layer3 instanceof VoiceCallReceptionRequest vcrr)
{
broadcastEvent(vcrr.getIdentifiers(), vcrr.getTimestamp(), DecodeEventType.PAGE,
"VOICE CALL RECEPTION REQUEST");
}
break;
case CONTROL_OUT_03_CC_VOICE_CALL_CONNECTION_RESPONSE:
if(layer3 instanceof VoiceCallConnectionResponse vccr)
{
//Controller responds to a voice call connection request for the called party with either a channel
//grant, or this message if it can't support the call or connect the called party
broadcastEvent(vccr.getIdentifiers(), vccr.getTimestamp(), DecodeEventType.RESPONSE,
"CALL CONNECTION REQUEST - RESPONSE:" + vccr.getCause().toString());
}
break;
case TRAFFIC_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TYPE_D_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
state = State.CALL;
event = DecoderStateEvent.Event.CONTINUATION;
mIdleDuringCallCount = 0;
break;
case CONTROL_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
case TRAFFIC_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
case TYPE_D_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
if(layer3 instanceof VoiceCallAssignment vca)
{
//Decode event is created by the traffic channel manager
mTrafficChannelManager.processVoiceCallAssignment(vca);
}
break;
case CONTROL_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE:
if(layer3 instanceof VoiceCallAssignmentDuplicateControl vcadc)
{
//This informs when there are 2-calls ongoing where a radio can participate in either call
mTrafficChannelManager.processVoiceCallAssignment(vcadc);
}
break;
case TRAFFIC_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE:
if(layer3 instanceof VoiceCallAssignmentDuplicateTraffic vcadt)
{
//This informs when there are 2-calls ongoing where a radio can participate in either call
mTrafficChannelManager.processVoiceCallAssignment(vcadt);
}
break;
case TRAFFIC_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION:
case TRAFFIC_OUT_08_CC_TRANSMISSION_RELEASE:
case TYPE_D_OUT_08_CC_TRANSMISSION_RELEASE:
mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp());
mEncryptedCallStateDetermined = false;
mEncryptedCall = false;
mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT;
//Only remove the FROM identifier, anticipating a continuation call for the same talkgroup
getIdentifierCollection().remove(Role.FROM);
break;
case CONTROL_OUT_09_CC_DATA_CALL_RESPONSE:
if(layer3 instanceof DataCallResponse dcr)
{
//Controller responds to a data call request with either a channel grant, or this message if
//it can't support the call or the called party is not available
broadcastEvent(dcr.getIdentifiers(), dcr.getTimestamp(), DecodeEventType.RESPONSE,
"DATA CALL REQUEST - RESPONSE:" + dcr.getCause().toString());
}
break;
case TRAFFIC_OUT_09_CC_DATA_CALL_HEADER:
case TYPE_D_OUT_09_CC_DATA_CALL_HEADER:
state = State.DATA;
break;
case CONTROL_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST:
if(layer3 instanceof DataCallReceptionRequest dcrr)
{
broadcastEvent(dcrr.getIdentifiers(), dcrr.getTimestamp(), DecodeEventType.PAGE,
"DATA CALL RECEPTION REQUEST");
}
break;
case TRAFFIC_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST:
break;
case TRAFFIC_OUT_11_CC_DATA_CALL_BLOCK:
case TYPE_D_OUT_11_CC_DATA_CALL_BLOCK:
state = State.DATA;
break;
case TRAFFIC_OUT_12_CC_DATA_CALL_ACKNOWLEDGE:
case TYPE_D_OUT_12_CC_DATA_CALL_ACKNOWLEDGE:
break;
case CONTROL_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE:
if(layer3 instanceof DataCallAssignmentDuplicateControl dcadc)
{
mTrafficChannelManager.processDataCallAssignment(dcadc);
}
break;
case TRAFFIC_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE:
break;
case CONTROL_OUT_14_CC_DATA_CALL_ASSIGNMENT:
if(layer3 instanceof DataCallAssignment dca)
{
mTrafficChannelManager.processDataCallAssignment(dca);
}
break;
case TRAFFIC_OUT_14_CC_DATA_CALL_ASSIGNMENT:
break;
case TRAFFIC_OUT_15_CC_HEADER_DELAY:
case TYPE_D_OUT_15_CC_HEADER_DELAY:
break;
case CONTROL_OUT_16_CC_IDLE:
break;
case TRAFFIC_OUT_16_CC_IDLE:
case TYPE_D_OUT_16_CC_IDLE:
//Hack: on both 4800 and 9600 systems there can be instances during a call where they send IDLE frames
//We'll track when we're in a call and continue a call state until the sequence of IDLE messages
//exceeds a threshold and then the state will be flipped to ACTIVE from CALL.
mIdleDuringCallCount++;
if(mIdleDuringCallCount < IDLE_DURING_CALL_MAX_COUNT)
{
state = State.CALL;
}
else
{
mEncryptedCallStateDetermined = false;
mEncryptedCall = false;
}
break;
case CONTROL_OUT_17_CC_DISCONNECT:
//Ignore. If this happens on the current/active control channel, in a multi-channel configuration,
//the frequency rotation manager will simply move to the next channel.
break;
case TRAFFIC_OUT_17_CC_DISCONNECT:
case TYPE_D_OUT_17_CC_DISCONNECT:
mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp());
mEncryptedCallStateDetermined = false;
mEncryptedCall = false;
getIdentifierCollection().remove(IdentifierClass.USER);
mDisconnectResponseCount++;
//Delay channel teardown until after we receive at least 2x disconnect responses
if(mChannel.isTrafficChannel() && mDisconnectResponseCount >= 2)
{
//Signal that this traffic channel has ended.
event = DecoderStateEvent.Event.END;
state = State.FADE;
mDisconnectResponseCount = 0;
}
break;
//Broadcast messages
case CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
case CONTROL_OUT_24_BC_SITE_INFORMATION:
case CONTROL_OUT_25_BC_SERVICE_INFORMATION:
case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION:
case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION:
mNetworkConfigurationMonitor.process(layer3);
getIdentifierCollection().remove(IdentifierClass.USER);
break;
case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
mNetworkConfigurationMonitor.process(layer3);
if(layer3 instanceof ControlChannelInformation cci)
{
setCurrentChannel(cci.getChannel1());
}
break;
case TRAFFIC_OUT_24_BC_SITE_INFORMATION:
if(layer3 instanceof SiteInformation si)
{
//Update traffic channel manager so it can use this when allocation traffic channels.
mTrafficChannelManager.setChannelAccessInformation(si.getChannelAccessInformation());
}
//Deliberate fall through
case TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
case TYPE_D_OUT_23_BC_DIGITAL_STATION_ID:
case TRAFFIC_OUT_25_BC_SERVICE_INFORMATION:
case TYPE_D_OUT_25_BC_SERVICE_INFORMATION:
case TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
case TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION:
case TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION:
case TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION:
mNetworkConfigurationMonitor.process(layer3);
break;
case CONTROL_OUT_32_MM_REGISTRATION_RESPONSE:
if(layer3 instanceof RegistrationResponse rr)
{
broadcastEvent(rr.getIdentifiers(), rr.getTimestamp(), DecodeEventType.REGISTER, rr.getCause().toString());
}
break;
case TYPE_D_OUT_32_MM_REGISTRATION_RESPONSE:
if(layer3 instanceof RegistrationResponseTypeD rr)
{
broadcastEvent(rr.getIdentifiers(), rr.getTimestamp(), DecodeEventType.REGISTER, rr.getCause().toString());
}
break;
case CONTROL_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE:
case TYPE_D_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE:
if(layer3 instanceof RegistrationClearResponse rcr)
{
broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.RESPONSE,
"CLEAR REGISTRATION REQUEST - RESPONSE:" + rcr.getCause());
}
break;
case CONTROL_OUT_35_MM_REGISTRATION_COMMAND:
if(layer3 instanceof RegistrationCommand rc)
{
broadcastEvent(rc.getIdentifiers(), rc.getTimestamp(), DecodeEventType.REGISTER,
rc.getRegistrationOption().isPriorityStation() ? "PRIORITY STATION" : "");
}
break;
case CONTROL_OUT_36_MM_GROUP_REGISTRATION_RESPONSE:
if(layer3 instanceof GroupRegistrationResponse grr)
{
broadcastEvent(grr.getIdentifiers(), grr.getTimestamp(), DecodeEventType.RESPONSE,
"GROUP REGISTRATION REQUEST - RESPONSE:" + grr.getCause());
}
break;
case TYPE_D_OUT_36_MM_GROUP_REGISTRATION_RESPONSE:
if(layer3 instanceof GroupRegistrationResponseTypeD grr)
{
broadcastEvent(grr.getIdentifiers(), grr.getTimestamp(), DecodeEventType.RESPONSE,
"GROUP REGISTRATION REQUEST - RESPONSE:" + grr.getCause());
}
break;
case TYPE_D_OUT_38_MM_AUTHENTICATION_PARAMETER_INFORMATION:
if(layer3 instanceof AuthenticationParameterInformation api)
{
broadcastEvent(api.getIdentifiers(), api.getTimestamp(), DecodeEventType.REQUEST,
"AUTHENTICATE: " + api.getAuthenticationParameter());
}
break;
case CONTROL_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST:
case TYPE_D_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST:
if(layer3 instanceof AuthenticationInquiryRequest air)
{
broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.REQUEST,
"AUTHENTICATE:" + air.getAuthenticationParameter());
}
break;
case TYPE_D_OUT_41_MM_AUTHENTICATION_INQUIRY_RESPONSE:
if(layer3 instanceof AuthenticationInquiryResponse air)
{
broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.RESPONSE,
"AUTHENTICATION:" + air.getAuthenticationValue());
}
break;
case CONTROL_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
case TRAFFIC_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
if(layer3 instanceof AuthenticationInquiryRequestMultiSystem air)
{
broadcastEvent(air.getIdentifiers(), air.getTimestamp(), DecodeEventType.REQUEST,
"AUTHENTICATE (MULTI-SYSTEM):" + air.getAuthenticationParameter());
}
break;
case TRAFFIC_OUT_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM:
if(layer3 instanceof AuthenticationInquiryResponseMultiSystem airms)
{
broadcast(NXDNDecodeEvent.builder(DecodeEventType.RESPONSE, layer3.getTimestamp())
.channel(getCurrentChannel())
.identifiers(getMutableIdentifierCollection(airms.getIdentifiers()))
.details("AUTHENTICATION: " + airms.getAuthenticationValue())
.build());
}
break;
case TYPE_D_OUT_43_MM_DATA_WRITE_HEADER:
case TYPE_D_OUT_44_MM_DATA_WRITE_DATA:
state = State.DATA;
break;
case CONTROL_OUT_48_CC_STATUS_INQUIRY_REQUEST:
case TRAFFIC_OUT_48_CC_STATUS_INQUIRY_REQUEST:
case TYPE_D_OUT_48_CC_STATUS_INQUIRY_REQUEST:
if(layer3 instanceof StatusInquiryRequest sir)
{
broadcastEvent(sir.getIdentifiers(), sir.getTimestamp(), DecodeEventType.REQUEST,
"STATUS INQUIRY");
}
break;
case CONTROL_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
case TRAFFIC_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
case TYPE_D_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
if(layer3 instanceof StatusInquiryResponse sir)
{
broadcastEvent(sir.getIdentifiers(), sir.getTimestamp(), DecodeEventType.RESPONSE,
"STATUS INQUIRY:" + sir.getStatus() + ":" + sir.getStatusValue() + " CAUSE:" + sir.getCause());
}
break;
case CONTROL_OUT_50_CC_STATUS_REQUEST:
case TRAFFIC_OUT_50_CC_STATUS_REQUEST:
case TYPE_D_OUT_50_CC_STATUS_REQUEST:
if(layer3 instanceof StatusRequest sr)
{
broadcastEvent(sr.getIdentifiers(), sr.getTimestamp(), DecodeEventType.REQUEST,
"STATUS REQUEST:" + sr.getStatus());
}
break;
case CONTROL_OUT_51_CC_STATUS_RESPONSE:
case TRAFFIC_OUT_51_CC_STATUS_RESPONSE:
case TYPE_D_OUT_51_CC_STATUS_RESPONSE:
if(layer3 instanceof StatusResponse sr)
{
broadcastEvent(sr.getIdentifiers(), sr.getTimestamp(), DecodeEventType.REQUEST,
"STATUS RESPONSE:" + sr.getCause());
}
break;
case CONTROL_OUT_52_CC_REMOTE_CONTROL_REQUEST:
case TRAFFIC_OUT_52_CC_REMOTE_CONTROL_REQUEST:
case TYPE_D_OUT_52_CC_REMOTE_CONTROL_REQUEST:
if(layer3 instanceof RemoteControlRequest rcr)
{
broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.REQUEST,
"REMOTE CONTROL REQUEST - PARAMETERS:" + rcr.getControlParameters());
}
break;
case CONTROL_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
case TRAFFIC_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
case TYPE_D_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
if(layer3 instanceof RemoteControlResponse rcr)
{
broadcastEvent(rcr.getIdentifiers(), rcr.getTimestamp(), DecodeEventType.REQUEST,
"REMOTE CONTROL " + rcr.getControlCommand() + " " + rcr.getCause());
}
break;
case CONTROL_OUT_54_CC_REMOTE_CONTROL_REQUEST_WITH_ESN:
if(layer3 instanceof RemoteControlRequestWithESN rcrwe)
{
broadcastEvent(rcrwe.getIdentifiers(), rcrwe.getTimestamp(), DecodeEventType.REQUEST,
"REMOTE CONTROL REQUEST WITH ESN - PARAMETERS:" + rcrwe.getAuthenticationParameter());
}
break;
case CONTROL_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
case TRAFFIC_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
case TYPE_D_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
if(layer3 instanceof ShortDataCallRequestHeader sdcrh)
{
broadcast(NXDNDecodeEvent.builder(DecodeEventType.REQUEST, layer3.getTimestamp())
.channel(getCurrentChannel())
.identifiers(getMutableIdentifierCollection(sdcrh.getIdentifiers()))
.details("SHORT DATA CALL")
.build());
state = State.DATA;
}
break;
case TRAFFIC_OUT_57_CC_SHORT_DATA_CALL_BLOCK:
case TYPE_D_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA:
state = State.DATA;
case CONTROL_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA:
break;
case TRAFFIC_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
case TYPE_D_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
state = State.DATA;
case CONTROL_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
break;
case CONTROL_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
case TRAFFIC_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
case TYPE_D_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
if(layer3 instanceof ShortDataCallResponse sdcr)
{
broadcast(NXDNDecodeEvent.builder(DecodeEventType.RESPONSE, layer3.getTimestamp())
.channel(getCurrentChannel())
.identifiers(getMutableIdentifierCollection(sdcr.getIdentifiers()))
.details("SHORT DATA CALL: " + sdcr.getCause())
.build());
}
break;
case PROPRIETARY_FORM:
break;
case TALKER_ALIAS:
state = State.CALL;
event = DecoderStateEvent.Event.CONTINUATION;
break;
case TALKER_ALIAS_COMPLETE:
if(layer3 instanceof TalkerAliasComplete tac)
{
state = State.CALL;
event = DecoderStateEvent.Event.CONTINUATION;
var radio = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
if(radio instanceof RadioIdentifier ri)
{
mTrafficChannelManager.processTalkerAlias(getCurrentChannel(), tac.getTalkerAlias(), ri, tac.getTimestamp());
}
}
break;
case TRAFFIC_IN_01_CC_VOICE_CALL:
state = State.CALL;
break;
case TRAFFIC_IN_02_CC_VOICE_CALL_RECEPTION_RESPONSE:
case TRAFFIC_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
state = State.CALL;
break;
case TRAFFIC_IN_08_CC_TRANSMISSION_RELEASE:
case TRAFFIC_IN_09_CC_DATA_CALL_HEADER:
state = State.DATA;
break;
case TRAFFIC_IN_11_CC_DATA_CALL_BLOCK:
state = State.DATA;
break;
case TRAFFIC_IN_12_CC_DATA_CALL_ACKNOWLEDGE:
case TRAFFIC_IN_15_CC_HEADER_DELAY:
case TRAFFIC_IN_16_CC_IDLE:
case TRAFFIC_IN_17_CC_DISCONNECT_REQUEST:
case TRAFFIC_IN_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
case TRAFFIC_IN_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM:
case TRAFFIC_IN_48_CC_STATUS_INQUIRY_REQUEST:
case TRAFFIC_IN_49_CC_STATUS_INQUIRY_RESPONSE:
case TRAFFIC_IN_50_CC_STATUS_REQUEST:
case TRAFFIC_IN_51_CC_STATUS_RESPONSE:
case TRAFFIC_IN_52_CC_REMOTE_CONTROL_REQUEST:
case TRAFFIC_IN_53_CC_REMOTE_CONTROL_RESPONSE:
case TRAFFIC_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
case TRAFFIC_IN_57_CC_SHORT_DATA_CALL_BLOCK:
state = State.DATA;
break;
case TRAFFIC_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
state = State.DATA;
break;
case UNKNOWN:
break;
}
broadcast(new DecoderStateEvent(this, event, state));
}
/**
* Process an audio message
* @param audio to process
*/
private void processAudio(Audio audio)
{
mIdleDuringCallCount = 0;
State state = State.CALL;
if(mEncryptedCallStateDetermined && mEncryptedCall)
{
state = State.ENCRYPTED;
}
mTrafficChannelManager.processCallProgressUpdate(getCurrentChannel(), audio.getTimestamp());
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, state));
}
/**
* Process packet-based messages
* @param message to process
*/
private void processPacketMessage(NXDNPacketMessage message)
{
if(message instanceof GPS gps)
{
PlottableDecodeEvent plottableEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, gps.getTimestamp())
.identifiers(new IdentifierCollection(message.getPacketSequence().getHeader().getIdentifiers()))
.channel(getCurrentChannel())
.details(gps.getLocationFormatted() + " HDG: " + gps.getHeading() + " SPD:" + gps.getSpeed() +
" ELE:" + gps.getElevation())
.protocol(Protocol.NXDN)
.location(gps.getLocation())
.heading(gps.getHeading())
.speed(gps.getSpeed())
.build();
broadcast(plottableEvent);
}
}
}

View File

@@ -0,0 +1,155 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.protocol.Protocol;
/**
* Base NXDN message implementation
*/
public abstract class NXDNMessage implements IMessage
{
protected static int OCTET_0 = 0;
protected static int OCTET_1 = 8;
protected static int OCTET_2 = 16;
protected static int OCTET_3 = 24;
protected static int OCTET_4 = 32;
protected static int OCTET_5 = 40;
protected static int OCTET_6 = 48;
protected static int OCTET_7 = 56;
protected static int OCTET_8 = 64;
protected static int OCTET_9 = 72;
protected static int OCTET_10 = 80;
protected static int OCTET_11 = 88;
protected static int OCTET_12 = 96;
protected static int OCTET_13 = 104;
protected static int OCTET_14 = 112;
protected static int OCTET_15 = 120;
protected static int OCTET_16 = 128;
protected static int OCTET_17 = 136;
protected static int OCTET_18 = 144;
protected static int OCTET_19 = 152;
protected static int OCTET_20 = 160;
protected final int mRAN;
protected final LICH mLICH;
private CorrectedBinaryMessage mMessage;
private long mTimestamp;
private boolean mValid = true;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
*/
public NXDNMessage(CorrectedBinaryMessage message, long timestamp, int ran, LICH lich)
{
mMessage = message;
mTimestamp = timestamp;
mRAN = ran;
mLICH = lich;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
/**
* Underlying binary message
*/
public CorrectedBinaryMessage getMessage()
{
return mMessage;
}
@Override
public boolean isValid()
{
return mValid;
}
/**
* Marks this message as valid or invalid.
*
* @param valid flag
*/
public void setValid(boolean valid)
{
mValid = valid;
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
@Override
public int getTimeslot()
{
return 0;
}
/**
* Formatted message prefix indicating CRC status, RAN and RF Channel.
*/
public StringBuilder getMessageBuilder()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
sb.append(getLICH().getRFChannel().name());
if(hasRAN())
{
sb.append(" RAN:").append(getRAN());
}
sb.append(" ");
return sb;
}
public boolean hasRAN()
{
return mRAN != 0;
}
/**
* Radio access network (RAN) from the frame carrier
*/
public int getRAN()
{
return mRAN;
}
/**
* Link Information Channel (LICH) from the frame carrier
*/
public LICH getLICH()
{
return mLICH;
}
}

View File

@@ -0,0 +1,251 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.decode.nxdn.layer1.Frame;
import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNHardSyncDetector;
import io.github.dsheirer.module.decode.nxdn.layer1.sync.NXDNSyncDetector;
import io.github.dsheirer.module.decode.nxdn.layer2.LICHTracker;
import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.sample.Listener;
import java.util.Arrays;
/**
* Provides message framing for the demodulated dibit stream. This framer is notified by an external sync detection
* process using the two syncDetected() methods below to indicate if the NID that follows the sync was correctly error
* detected and corrected. When the NID does not pass error correction, we use a PLACEHOLDER data unit ID to allow the
* uncertain message to assemble and then we'll inspect before and after data unit IDs and the quantity of captured
* dibits to make a best guess on what the assembled message represents.
*/
public class NXDNMessageFramer
{
private static final int FRAME_LENGTH = 192;
private static final int SYNC_LENGTH = 10;
private static final int PAYLOAD_LENGTH = FRAME_LENGTH - SYNC_LENGTH;
private static final int SYNC_DETECTION_BIT_ERROR_THRESHOLD = 8;
private final Dibit[] mDibitBuffer = new Dibit[192];
private final LICHTracker mLICHTracker;
private final Listener<IMessage> mMessageListener;
private final NXDNHardSyncDetector mHardSyncDetector = new NXDNHardSyncDetector();
private final int mSymbolsPerSecond;
private final int mCoarseSyncLossThreshold;
private boolean mSyncDetected = false;
private int mDibitBufferPointer = 0;
private int mDibitSinceTimestampCounter = 0;
private int mDibitsSinceLastMessage = FRAME_LENGTH;
private long mReferenceTimestamp = 0;
/**
* Constructs an instance
* @param messageListener to receive framed messages
* @param transmissionMode to indicate how many symbols per second for accounting
*/
public NXDNMessageFramer(Listener<IMessage> messageListener, TransmissionMode transmissionMode)
{
mMessageListener = messageListener;
mLICHTracker = new LICHTracker(transmissionMode);
mSymbolsPerSecond = transmissionMode.getSymbolRate();
mCoarseSyncLossThreshold = mSymbolsPerSecond + FRAME_LENGTH;
Arrays.fill(mDibitBuffer, Dibit.D00_PLUS_1);
}
/**
* Sets or updates the current dibit stream time from an incoming sample buffer.
* @param time to use as a reference timestamp.
*/
public void setTimestamp(long time)
{
mReferenceTimestamp = time;
mDibitSinceTimestampCounter = 0;
}
/**
* Calculates the timestamp accurate to the currently received dibit.
* @return timestamp in milliseconds.
*/
private long getTimestamp()
{
if(mReferenceTimestamp > 0)
{
return mReferenceTimestamp + (long)(1000.0 * mDibitSinceTimestampCounter / 4800);
}
else
{
mDibitSinceTimestampCounter = 0;
return System.currentTimeMillis();
}
}
/**
* Dispatches a currently assembling message once it's complete.
* @return true if any of the messages from the frame are valid.
*/
private boolean dispatchMessage()
{
CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(PAYLOAD_LENGTH * 2);
//The buffer pointer is the next symbol after the completed message, ie start of sync
int bufferDibitPointer = mDibitBufferPointer + SYNC_LENGTH;
bufferDibitPointer %= FRAME_LENGTH;
int messageDibitPointer = 0;
Dibit dibit;
while(messageDibitPointer < PAYLOAD_LENGTH)
{
dibit = mDibitBuffer[bufferDibitPointer++];
cbm.add(dibit.getBit1(), dibit.getBit2());
bufferDibitPointer %= FRAME_LENGTH;
messageDibitPointer++;
}
boolean validMessage = false;
Frame frame = new Frame(cbm, getTimestamp(), mLICHTracker);
for(NXDNMessage message: frame.getMessages())
{
validMessage |= message.isValid();
dispatch(message);
}
mDibitsSinceLastMessage = 0;
return validMessage;
}
/**
* Tests if the previous 10 symbols in the dibit buffer match the sync pattern. This uses the
* buffer wrapping to determine if the just collected dibit sync sequence contains a sync pattern but was not
* triggered by the sync detector. This should only be used at the message interval when we anticipate arrival of
* the succeeding message.
*
* @return bit errors for the received dibit sequence against the sync pattern
*/
private int checkSync()
{
long value = 0;
int dibitBufferPointer = mDibitBufferPointer - SYNC_LENGTH;
if(dibitBufferPointer < 0)
{
dibitBufferPointer += FRAME_LENGTH;
}
for(int x = 0; x < SYNC_LENGTH; x++)
{
value <<= 2;
value += mDibitBuffer[dibitBufferPointer++].getValue();
dibitBufferPointer %= FRAME_LENGTH;
}
return Long.bitCount(value ^ NXDNSyncDetector.SYNC_PATTERN);
}
/**
* Dispatches the message to an optional listener
* @param message to dispatch
*/
private void dispatch(IMessage message)
{
if(mMessageListener != null)
{
mMessageListener.receive(message);
}
}
/**
* Notification that the sync pattern was detected. The most recently received dibit should be the last symbol
* in the sync pattern and the next dibit received should be the start of the frame content.
*/
public void syncDetected()
{
//Ignore false/extra sync when we're already in sync detection
if(!mSyncDetected)
{
mSyncDetected = true;
//Update sync loss accounting when we have more/less than the expected sync pattern symbol count.
if(mDibitsSinceLastMessage > SYNC_LENGTH)
{
dispatch(new SyncLossMessage(getTimestamp(),
(mDibitsSinceLastMessage - SYNC_LENGTH) * 2, Protocol.NXDN));
mDibitsSinceLastMessage = SYNC_LENGTH;
}
else if(mDibitsSinceLastMessage < SYNC_LENGTH)
{
mDibitsSinceLastMessage = SYNC_LENGTH;
}
}
}
/**
* Process symbol decision without sync detection.
* @param symbol that was demodulated.
*/
public boolean process(Dibit symbol)
{
//Check when we didn't get a sync detect, and count-wise, we're at the next frame's end-sync location
if(!mSyncDetected && mDibitsSinceLastMessage == SYNC_LENGTH)
{
int bitErrors = checkSync();
if(bitErrors <= SYNC_DETECTION_BIT_ERROR_THRESHOLD)
{
mSyncDetected = true;
}
}
mDibitBuffer[mDibitBufferPointer++] = symbol;
mDibitBufferPointer %= FRAME_LENGTH;
mDibitsSinceLastMessage++;
mDibitSinceTimestampCounter++;
if(mSyncDetected && mDibitsSinceLastMessage == FRAME_LENGTH)
{
mSyncDetected = false;
return dispatchMessage();
}
else if(mDibitsSinceLastMessage >= mCoarseSyncLossThreshold)
{
mDibitsSinceLastMessage -= mSymbolsPerSecond;
dispatch(new SyncLossMessage(getTimestamp(), mSymbolsPerSecond * 2, Protocol.NXDN));
}
return false;
}
/**
* Process symbol decision with (hard) sync detection
* @param symbol to process
*/
public void processWithHardSyncDetection(Dibit symbol)
{
process(symbol);
if(mHardSyncDetector.process(symbol))
{
syncDetected();
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelInformation;
import io.github.dsheirer.module.decode.nxdn.layer2.SACCHAssembler;
import io.github.dsheirer.module.decode.nxdn.layer2.SACCHFragment;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.IChannelInformationReceiver;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.Idle;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.call.Disconnect;
import io.github.dsheirer.module.decode.nxdn.layer3.call.IPacketHeader;
import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataInitializationVector;
import io.github.dsheirer.module.decode.nxdn.layer3.call.UserData;
import io.github.dsheirer.module.decode.nxdn.layer3.data.PacketSequenceAssembler;
import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAlias;
import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAliasAssembler;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import io.github.dsheirer.sample.Listener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* NXDN Message Processor.
*
* Performs post-message creation processing and enrichment before the message is sent downstream.
*/
public class NXDNMessageProcessor implements Listener<IMessage>
{
private final static Logger LOGGER = LoggerFactory.getLogger(NXDNMessageProcessor.class);
private ChannelAccessInformation mChannelAccessInformation;
private final Map<Integer, ChannelFrequency> mChannelFrequencyMap = new HashMap<>();
private final PacketSequenceAssembler mPacketSequenceAssembler = new PacketSequenceAssembler();
private final SACCHAssembler mSACCHAssembler = new SACCHAssembler();
private final TalkerAliasAssembler mTalkerAliasAssembler;
/**
* Sets channel preload information. This is used to preload a traffic channel with system channel information
* from the control channel when a traffic channel is allocated, on startup.
* @param data to preload
*/
public void preload(NXDNChannelInfoPreloadData data)
{
ChannelInformation channelInformation = data.getData();
if(mChannelAccessInformation == null)
{
mChannelAccessInformation = channelInformation.getChannelAccessInformation();
}
for(ChannelFrequency channelFrequency: channelInformation.getChannelFrequencies())
{
if(!mChannelFrequencyMap.containsKey(channelFrequency.getChannel()))
{
mChannelFrequencyMap.put(channelFrequency.getChannel(), channelFrequency);
}
}
}
/**
* Downstream message listener
*/
private Listener<IMessage> mMessageListener;
/**
* Constructs an instance
* @param configNXDN that optionally contains a channel map for channel mode systems
*/
public NXDNMessageProcessor(DecodeConfigNXDN configNXDN)
{
for(ChannelFrequency channelFrequency: configNXDN.getChannelMap())
{
mChannelFrequencyMap.put(channelFrequency.getChannel(), channelFrequency);
}
mTalkerAliasAssembler = new TalkerAliasAssembler(configNXDN.getEncoding());
}
/**
* Processes the message for enrichment or reassembly of fragments and sends the enriched message and any additional
* messages that were created during the enrichment to the registered message listener.
* @param message to process
*/
@Override
public void receive(IMessage message)
{
List<NXDNMessage> additionalMessagesToDispatch = null;
if(message != null && message.isValid())
{
//Capture channel access information from Site Information message
if(message instanceof SiteInformation siteInformation)
{
mChannelAccessInformation = siteInformation.getChannelAccessInformation();
}
//Enrich messages that carry channel information with the channel access info.
if(mChannelAccessInformation != null && message instanceof IChannelInformationReceiver receiver)
{
receiver.receive(mChannelAccessInformation, mChannelFrequencyMap);
}
//SACCH fragments are either standalone 4/4, or a sequence of 4 in a superframe. Reassemble them.
if(message instanceof SACCHFragment fragment)
{
dispatch(message);
message = null;
//Publish the completed SACCH message to this method so that it can be processed correctly and dispatch
//the original fragment and nullify the message to ensure correct sequencing where fragment 4 is
//dispatched first, and then the assembled SACCH message.
receive(mSACCHAssembler.process(fragment));
}
if(message instanceof TalkerAlias fragment)
{
dispatch(message);
//Assign the completed alias to the message to dispatch below, to ensure proper message output sequence
message = mTalkerAliasAssembler.process(fragment);
}
else if(message instanceof Idle || message instanceof Disconnect)
{
//Clear any captured talker alias fragments at the end of the call or idle mode
mTalkerAliasAssembler.reset();
}
else if(message instanceof IPacketHeader header)
{
mPacketSequenceAssembler.process(header);
}
else if(message instanceof UserData userDataBlock)
{
additionalMessagesToDispatch = mPacketSequenceAssembler.process(userDataBlock);
}
else if(message instanceof ShortDataInitializationVector iv)
{
mPacketSequenceAssembler.process(iv);
}
}
dispatch(message);
if(additionalMessagesToDispatch != null)
{
for(NXDNMessage additional: additionalMessagesToDispatch)
{
dispatch(additional);
}
}
}
/**
* Post-process the message for frequency band details.
* @param message to post process and dispatch
*/
private void dispatch(IMessage message)
{
if(message != null && mMessageListener != null)
{
mMessageListener.receive(message);
}
}
/**
* Prepares for disposal of this instance.
*/
public void dispose()
{
mMessageListener = null;
}
/**
* Sets the message listener to receive the output from this processor.
* @param listener to receive output messages
*/
public void setMessageListener(Listener<IMessage> listener)
{
mMessageListener = listener;
}
/**
* Clears a registered message listener.
*/
public void removeMessageListener()
{
mMessageListener = null;
}
}

View File

@@ -0,0 +1,249 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.AdjacentSiteInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.AdjacentSiteInformationTypeD;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.DigitalStationIDInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.FailureStatusInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.Neighbor;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ServiceInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.RepeaterIdle;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.SiteID;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Processes NXDN Layer 3 messages to assemble a snapshot of the site's broadcast configuration details
*/
public class NXDNNetworkConfigurationMonitor
{
private ControlChannelInformation mControlChannelInformation;
private DigitalStationIDInformation mDigitalStationIDInformation;
private FailureStatusInformation mFailureStatusInformation;
private ServiceInformation mServiceInformation;
private SiteInformation mSiteInformation;
private Map<Integer, Neighbor> mNeighborMap = new HashMap<>();
private AdjacentSiteInformationTypeD mTypeDNeighborA;
private AdjacentSiteInformationTypeD mTypeDNeighborB;
private Integer mTypeDRepeater;
private List<Integer> mTypeDObservedRepeaters = new ArrayList<>();
private SiteID mTypeDSiteID;
/**
* Constructs an instance
*/
public NXDNNetworkConfigurationMonitor()
{
}
public String getSummary()
{
StringBuilder sb = new StringBuilder();
if(mDigitalStationIDInformation != null)
{
sb.append("Current Channel Station ID\n ").append(mDigitalStationIDInformation).append("\n");
}
if(mSiteInformation != null)
{
sb.append("\nCurrent Site\n ").append(mSiteInformation).append("\n");
}
else if(mTypeDSiteID != null)
{
sb.append("\nCurrent Site\n ").append(mTypeDSiteID).append("\n");
}
if(mServiceInformation != null)
{
sb.append("\nCurrent Site Services\n ").append(mServiceInformation).append("\n");
}
if(mFailureStatusInformation != null)
{
sb.append("\nFailure Status\n ").append(mFailureStatusInformation).append("\n");
}
if(mControlChannelInformation != null)
{
sb.append("\nCurrent Site Control Channel\n ").append(mControlChannelInformation).append("\n");
}
if(mTypeDRepeater != null)
{
sb.append("\nType-D Current Repeater: ").append(mTypeDRepeater).append("\n");
}
if(!mTypeDObservedRepeaters.isEmpty())
{
Collections.sort(mTypeDObservedRepeaters);
sb.append("\nType-D Observed Repeater Numbers:").append(mTypeDObservedRepeaters).append("\n");
}
if(!mNeighborMap.isEmpty())
{
sb.append("\nNeighbor Sites\n");
List<Integer> ids = new ArrayList<>(mNeighborMap.keySet());
Collections.sort(ids);
for(Integer id : ids)
{
sb.append(" ").append(mNeighborMap.get(id)).append("\n");
}
}
else if(mTypeDNeighborA != null || mTypeDNeighborB != null)
{
sb.append("\nNeighbor Sites\n");
if(mTypeDNeighborA != null)
{
sb.append(" ").append(mTypeDNeighborA.getSystemID1()).append(" SITE:").append(mTypeDNeighborA.getSite1()).append("\n");
if(mTypeDNeighborA.hasSite2())
{
sb.append(" ").append(mTypeDNeighborA.getSystemID2()).append(" SITE:").append(mTypeDNeighborA.getSite2()).append("\n");
}
}
if(mTypeDNeighborB != null)
{
sb.append(" ").append(mTypeDNeighborB.getSystemID1()).append(" SITE:").append(mTypeDNeighborB.getSite1()).append("\n");
if(mTypeDNeighborB.hasSite2())
{
sb.append(" ").append(mTypeDNeighborB.getSystemID2()).append(" SITE:").append(mTypeDNeighborB.getSite2()).append("\n");
}
}
}
else
{
sb.append("\nNeighbor Sites\n NONE\n");
}
return sb.toString();
}
public void process(NXDNLayer3Message layer3)
{
switch(layer3.getMessageType())
{
case CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
case TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
case TYPE_D_OUT_23_BC_DIGITAL_STATION_ID:
if(layer3 instanceof DigitalStationIDInformation dsii)
{
mDigitalStationIDInformation = dsii;
}
break;
case CONTROL_OUT_24_BC_SITE_INFORMATION:
case TRAFFIC_OUT_24_BC_SITE_INFORMATION:
mSiteInformation = (SiteInformation) layer3;
break;
case CONTROL_OUT_25_BC_SERVICE_INFORMATION:
case TRAFFIC_OUT_25_BC_SERVICE_INFORMATION:
case TYPE_D_OUT_25_BC_SERVICE_INFORMATION:
if(layer3 instanceof ServiceInformation sii)
{
mServiceInformation = sii;
}
break;
case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
case TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
mControlChannelInformation = (ControlChannelInformation) layer3;
break;
case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION:
case TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION:
mFailureStatusInformation = (FailureStatusInformation) layer3;
break;
case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION:
case TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION:
if(layer3 instanceof AdjacentSiteInformation adjacent)
{
if(adjacent.hasChannel1())
{
Neighbor n1 = adjacent.getNeighbor1();
if(n1 != null)
{
mNeighborMap.put(n1.id(), n1);
if(adjacent.hasChannel2())
{
Neighbor n2 = adjacent.getNeighbor2();
if(n2 != null)
{
mNeighborMap.put(n2.id(), n2);
if(adjacent.hasChannel3())
{
Neighbor n3 = adjacent.getNeighbor3();
if(n3 != null)
{
mNeighborMap.put(n3.id(), n3);
if(adjacent.hasChannel4())
{
Neighbor n4 = adjacent.getNeighbor4();
if(n4 != null)
{
mNeighborMap.put(n4.id(), n4);
}
}
}
}
}
}
}
}
}
case TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION:
if(layer3 instanceof AdjacentSiteInformationTypeD atd)
{
if(atd.isIndex())
{
mTypeDNeighborB = atd;
}
else
{
mTypeDNeighborA = atd;
}
}
break;
case TYPE_D_SCCH_OUT_INFO_4_REPEATER_IDLE:
if(layer3 instanceof RepeaterIdle ri)
{
mTypeDRepeater = ri.getRepeater();
if(!mTypeDObservedRepeaters.contains(ri.getRepeater2()))
{
mTypeDObservedRepeaters.add(ri.getRepeater2());
}
}
break;
case TYPE_D_SCCH_OUT_INFO_4_SITE_ID:
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,794 @@
/*
* *****************************************************************************
* 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.nxdn;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.IChannelEventListener;
import io.github.dsheirer.controller.channel.IChannelEventProvider;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.alias.TalkerAliasManager;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.event.DecodeEventDuplicateDetector;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.module.decode.event.IDecodeEventProvider;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannel;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelFake;
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkerAliasIdentifier;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignment;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignment;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateTraffic;
import io.github.dsheirer.module.decode.nxdn.layer3.type.AudioCodec;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CallTimer;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CallType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.DataCallOption;
import io.github.dsheirer.module.decode.nxdn.layer3.type.Duplex;
import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
import io.github.dsheirer.module.decode.nxdn.layer3.type.VoiceCallOption;
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.config.SourceConfigTuner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages trunked system traffic channel activations
*/
public class NXDNTrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider,
IChannelEventListener, IChannelEventProvider
{
private static final Logger LOGGER = LoggerFactory.getLogger(NXDNTrafficChannelManager.class);
public static final String MAX_TRAFFIC_CHANNELS_EXCEEDED = "MAX TRAFFIC CHANNELS EXCEEDED";
public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED";
private final Channel mParentChannel;
private final DecodeEventDuplicateDetector mDuplicateDetector = new DecodeEventDuplicateDetector();
private final List<ChannelFrequency> mChannelFrequencies = new ArrayList<>();
private final Lock mLock = new ReentrantLock();
private final Map<Long, Channel> mAllocatedTrafficChannelMap = new HashMap<>();
private final Map<Long, NXDNChannelEventTracker> mEventTrackerMap = new HashMap<>();
private final Queue<Channel> mAvailableTrafficChannelQueue = new LinkedTransferQueue<>();
private final TalkerAliasManager mTalkerAliasManager = new TalkerAliasManager();
private final TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor();
private List<Channel> mManagedTrafficChannels;
private Listener<IDecodeEvent> mDecodeEventListener;
private Listener<ChannelEvent> mChannelEventListener;
private ChannelAccessInformation mChannelAccessInformation;
private boolean mIgnoreDataCalls;
private boolean mIgnoreEncryptedCalls;
/**
* Constructs an instance
*
* @param parentChannel configuration for the parent channel
*/
public NXDNTrafficChannelManager(Channel parentChannel)
{
mParentChannel = parentChannel;
if(parentChannel.getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN)
{
mIgnoreDataCalls = configNXDN.isIgnoreDataCalls();
mIgnoreEncryptedCalls = configNXDN.isIgnoreEncryptedCalls();
mChannelFrequencies.addAll(configNXDN.getChannelMap());
createTrafficChannels(configNXDN);
}
}
/**
* Talker alias manager
*/
public TalkerAliasManager getTalkerAliasManager()
{
return mTalkerAliasManager;
}
/**
* Sets the channel access info from the control channel.
*
* @param info from the control channel
*/
public void setChannelAccessInformation(ChannelAccessInformation info)
{
if(mChannelAccessInformation == null)
{
mChannelAccessInformation = info;
}
}
/**
* Creates up to the maximum number of traffic channels for use in allocating traffic channels.
*
* @param decodeConfig to use for each traffic channel
*/
private void createTrafficChannels(DecodeConfigNXDN decodeConfig)
{
if(mManagedTrafficChannels == null)
{
int trafficChannelPoolSize = decodeConfig.getTrafficChannelPoolSize();
List<Channel> trafficChannelList = new ArrayList<>();
if(trafficChannelPoolSize > 0)
{
for(int x = 0; x < trafficChannelPoolSize; x++)
{
Channel trafficChannel = new Channel("T-" + mParentChannel.getName(), Channel.ChannelType.TRAFFIC);
trafficChannel.setAliasListName(mParentChannel.getAliasListName());
trafficChannel.setSystem(mParentChannel.getSystem());
trafficChannel.setSite(mParentChannel.getSite());
trafficChannel.setDecodeConfiguration(decodeConfig);
trafficChannel.setEventLogConfiguration(mParentChannel.getEventLogConfiguration());
trafficChannel.setRecordConfiguration(mParentChannel.getRecordConfiguration());
trafficChannelList.add(trafficChannel);
}
}
mAvailableTrafficChannelQueue.addAll(trafficChannelList);
mManagedTrafficChannels = Collections.unmodifiableList(trafficChannelList);
}
}
/**
* Retrieves the current event tracker for the specified channel and if the tracker is stale relative to the
* timestamp, returns null, otherwise returns the current tracker.
*
* @param frequency to look up the tracker
* @param timestamp to compare for staleness
* @return tracker or null
*/
private NXDNChannelEventTracker getTrackerRemoveIfStale(long frequency, long timestamp)
{
NXDNChannelEventTracker tracker = mEventTrackerMap.get(frequency);
if(tracker != null && tracker.isStale(timestamp))
{
removeTracker(frequency);
tracker = null;
}
return tracker;
}
/**
* Broadcasts the decode event from the tracker.
*
* @param tracker containing a decode event.
*/
public void broadcast(NXDNChannelEventTracker tracker)
{
broadcast(tracker.getEvent());
}
/**
* Broadcasts an initial or update decode event to any registered listener.
*/
public void broadcast(DecodeEvent decodeEvent)
{
if(mDecodeEventListener != null)
{
if(decodeEvent.getEventType() == DecodeEventType.DATA_CALL && mDuplicateDetector.isDuplicate(decodeEvent,
System.currentTimeMillis()))
{
return;
}
mDecodeEventListener.receive(decodeEvent);
}
else
{
System.out.println("Decode event listener is null");
}
}
/**
* Selects the decode event type based on the call type and encryption.
*
* @param callType for the call
* @param encryption for the call
* @return decode event type
*/
private DecodeEventType getType(CallType callType, EncryptionKeyIdentifier encryption)
{
boolean encrypted = encryption.isEncrypted();
return switch(callType)
{
case GROUP_BROADCAST, GROUP_CONFERENCE ->
encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP;
case INDIVIDUAL ->
encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT;
case INTERCONNECT, SPEED_DIAL ->
encrypted ? DecodeEventType.CALL_INTERCONNECT_ENCRYPTED : DecodeEventType.CALL_INTERCONNECT;
default -> encrypted ? DecodeEventType.CALL_ENCRYPTED : DecodeEventType.CALL;
};
}
/**
* Creates a traffic channel event tracker
*
* @param eventType for the decode event
* @param ic collection of from/to identifiers
* @param channel to track
* @param timestamp for the event
* @return tracker
*/
private NXDNChannelEventTracker createTracker(DecodeEventType eventType, IdentifierCollection ic,
NXDNChannel channel, long timestamp)
{
long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
DecodeEvent event = NXDNDecodeEvent.builder(eventType, timestamp)
.timeslot(0)
.channel(channel)
.identifiers(ic)
.build();
NXDNChannelEventTracker tracker = new NXDNChannelEventTracker(event);
mEventTrackerMap.put(frequency, tracker);
return tracker;
}
/**
* Process a data call assignment
*
* @param dca to process
*/
public void processDataCallAssignment(DataCallAssignment dca)
{
if(dca.hasChannel() && dca.getChannel().getDownlinkFrequency() > 0)
{
DecodeEventType eventType = dca.getEncryptionKeyIdentifier().isEncrypted() ?
DecodeEventType.DATA_CALL_ENCRYPTED : DecodeEventType.DATA_CALL;
processDataCall(dca.getIdentifiers(), dca.getChannel(), eventType, dca.getTimestamp(), dca.getCallOption(),
dca.getCallTimer());
}
}
/**
* Process a voice call assignment or notification
* @param identifiers for the call
* @param channel for the call
*/
private void processDataCall(List<Identifier> identifiers, NXDNChannel channel, DecodeEventType eventType,
long timestamp, DataCallOption dco, CallTimer callTimer)
{
if(channel.getDownlinkFrequency() > 0)
{
mLock.lock();
try
{
NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(channel.getDownlinkFrequency(), timestamp);
MutableIdentifierCollection ic = new MutableIdentifierCollection(identifiers);
if(tracker != null)
{
if(tracker.isSameCallCheckingToAndFrom(ic, timestamp))
{
tracker.updateDurationControl(timestamp);
broadcast(tracker);
}
else
{
removeTracker(channel.getDownlinkFrequency());
tracker = null;
}
}
if(tracker == null)
{
tracker = createTracker(eventType, ic, channel, timestamp);
Duplex duplex = dco.getDuplex();
TransmissionMode mode = dco.getTransmissionMode();
tracker.addDetails("TIMER:" + callTimer + " " + duplex + " " + mode);
}
if(mIgnoreDataCalls)
{
tracker.prefixDetails("IGNORED DATA");
}
else
{
if(!tracker.isTrafficChannelAllocated() && channel.isValid() &&
channel.getDownlinkFrequency() != getCurrentControlFrequency() &&
!mAllocatedTrafficChannelMap.containsKey(channel.getDownlinkFrequency()))
{
//Retrieve a channel from the traffic channel queue
Channel traffic = mAvailableTrafficChannelQueue.poll();
if(traffic != null)
{
requestTrafficChannelStart(traffic, channel, ic, tracker);
}
else
{
tracker.addDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " " + tracker.getEvent().getDetails());
}
}
}
broadcast(tracker);
}
finally
{
mLock.unlock();
}
}
}
/**
* Process a voice call assignment indicating there is a call on another channel.
*
* @param vca to process
*/
public void processVoiceCallAssignment(VoiceCallAssignment vca)
{
if(vca.hasChannel() && vca.getChannel().getDownlinkFrequency() > 0)
{
processVoiceCall(vca.getIdentifiers(), vca.getChannel(), vca.getCallType(), vca.getEncryptionKeyIdentifier(),
vca.getTimestamp(), vca.getCallOption(), vca.getCallTimer());
}
}
/**
* Process a voice call assignment duplicate notification from a traffic channel
*
* @param vca to process
*/
public void processVoiceCallAssignment(VoiceCallAssignmentDuplicateTraffic vca)
{
NXDNChannel channel = vca.getChannel();
if(channel == null || channel.getDownlinkFrequency() == 0)
{
//If the channel is null or not configured, use the talkgroup value so we can track the call event
channel = new NXDNChannelFake(vca.getDestination().getValue());
}
processVoiceCall(vca.getIdentifiers(), channel, vca.getCallType(), vca.getEncryptionKeyIdentifier(),
vca.getTimestamp(), vca.getCallOption(), vca.getCallTimer());
}
/**
* Process a traffic channel voice call message
* @param voiceCall message
* @param channel for the call, can be null
*/
public void processVoiceCall(VoiceCall voiceCall, IChannelDescriptor channel)
{
NXDNChannel nxdn = null;
if(channel instanceof NXDNChannel nxdnChannel)
{
nxdn = nxdnChannel;
}
processVoiceCall(voiceCall.getIdentifiers(), nxdn, voiceCall.getCallType(), voiceCall.getEncryptionKeyIdentifier(),
voiceCall.getTimestamp(), voiceCall.getCallOption(), CallTimer.UNSPECIFIED);
}
/**
* Adds or updates the talker alias for the tracked call event.
* @param channel for the call
* @param talkerAlias to add or update
* @param timestamp for the talker alias message
*/
public void processTalkerAlias(IChannelDescriptor channel, NXDNTalkerAliasIdentifier talkerAlias,
RadioIdentifier radio, long timestamp)
{
mLock.lock();
try
{
long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp);
if(tracker != null)
{
tracker.addIdentifierIfMissing(talkerAlias);
broadcast(tracker);
}
getTalkerAliasManager().update(radio, talkerAlias);
}
finally
{
mLock.unlock();
}
}
/**
* Updates the end time for an ongoing call to reflect progress
* @param channel descriptor for the traffic channel
*/
public void processCallProgressUpdate(IChannelDescriptor channel, long timestamp)
{
mLock.lock();
try
{
long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp);
if(tracker != null)
{
tracker.updateDurationTraffic(timestamp);
broadcast(tracker);
}
}
finally
{
mLock.unlock();
}
}
/**
* Ends the current call and removes the event tracker
* @param channel for the call
* @param timestamp for the end call
*/
public void processEndCall(IChannelDescriptor channel, long timestamp)
{
mLock.lock();
try
{
long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp);
if(tracker != null)
{
tracker.completeTraffic(timestamp);
broadcast(tracker);
removeTracker(frequency);
}
}
finally
{
mLock.unlock();
}
}
/**
* Process a voice call assignment or notification
* @param identifiers for the call
* @param channel for the call
*/
private void processVoiceCall(List<Identifier> identifiers, NXDNChannel channel, CallType callType,
EncryptionKeyIdentifier encryption, long timestamp, VoiceCallOption vco,
CallTimer callTimer)
{
mLock.lock();
long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
try
{
NXDNChannelEventTracker tracker = getTrackerRemoveIfStale(frequency, timestamp);
MutableIdentifierCollection ic = new MutableIdentifierCollection(identifiers);
mTalkerAliasManager.enrich(ic);
if(tracker != null)
{
if(tracker.isSameCallCheckingToAndFrom(ic, timestamp))
{
tracker.updateDurationControl(timestamp);
}
else
{
removeTracker(frequency);
tracker = null;
}
}
if(tracker == null)
{
DecodeEventType eventType = getType(callType, encryption);
tracker = createTracker(eventType, ic, channel, timestamp);
AudioCodec audioCodec = vco.getCodec();
TransmissionMode mode = vco.getTransmissionMode();
if(encryption.isEncrypted())
{
tracker.addDetails(encryption + " TIMER:" + callTimer + " " + audioCodec + " " + mode);
}
else
{
tracker.addDetails("TIMER:" + callTimer + " " + audioCodec + " " + mode);
}
}
boolean ignoreEncrypted = mIgnoreEncryptedCalls && encryption.isEncrypted();
if(ignoreEncrypted)
{
tracker.prefixDetails("IGNORED ENCRYPTED");
}
else if(!tracker.isTrafficChannelAllocated() && channel != null && channel.isValid() &&
channel.getDownlinkFrequency() != getCurrentControlFrequency() &&
!mAllocatedTrafficChannelMap.containsKey(frequency))
{
//Retrieve a channel from the traffic channel queue
Channel traffic = mAvailableTrafficChannelQueue.poll();
if(traffic != null)
{
requestTrafficChannelStart(traffic, channel, ic, tracker);
}
else
{
tracker.addDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED + " " + tracker.getEvent().getDetails());
}
}
broadcast(tracker);
}
finally
{
mLock.unlock();
}
}
/**
* Sends a channel start request to the ChannelProcessingManager.
* Note: this method is not thread safe and the calling method must protect access using mLock.
*
* @param trafficChannel to use for the traffic channel
* @param nxdnChannel that describes the traffic channel downlink frequency
* @param ic containing identifiers for the call
* @param tracker to update with channel allocation flag
*/
private void requestTrafficChannelStart(Channel trafficChannel, NXDNChannel nxdnChannel,
IdentifierCollection ic, NXDNChannelEventTracker tracker)
{
if(nxdnChannel != null && nxdnChannel.getDownlinkFrequency() > 0 && getInterModuleEventBus() != null)
{
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(nxdnChannel.getDownlinkFrequency());
if(mParentChannel.getSourceConfiguration() instanceof SourceConfigTuner parentConfigTuner)
{
sourceConfig.setPreferredTuner(parentConfigTuner.getPreferredTuner());
}
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(nxdnChannel.getDownlinkFrequency(), trafficChannel);
ChannelStartProcessingRequest startChannelRequest = new ChannelStartProcessingRequest(trafficChannel,
nxdnChannel, ic, this);
startChannelRequest.addPreloadDataContent(new NXDNChannelInfoPreloadData(mChannelAccessInformation, mChannelFrequencies));
getInterModuleEventBus().post(startChannelRequest);
tracker.setTrafficChannelAllocated(true);
}
else
{
//Return the channel to the traffic channel pool if we didn't start it.
mAvailableTrafficChannelQueue.add(trafficChannel);
}
}
@Override
public Listener<ChannelEvent> getChannelEventListener()
{
return mTrafficChannelTeardownMonitor;
}
/**
* Broadcasts the channel event ot an optionally registered listener
* @param event to broadcast
*/
private void broadcast(ChannelEvent event)
{
if(mChannelEventListener != null)
{
mChannelEventListener.receive(event);
}
}
@Override
public void setChannelEventListener(Listener<ChannelEvent> listener)
{
mChannelEventListener = listener;
}
@Override
public void removeChannelEventListener()
{
mChannelEventListener = null;
}
/**
* Process the current control frequency to ensure we don't allocated traffic channels for the same frequency.
* @param previous frequency for the control channel (to remove from allocated channels)
* @param current frequency for the control channel (to add to allocated channels)
* @param channel for the current control channel
*/
@Override
protected void processControlFrequencyUpdate(long previous, long current, Channel channel)
{
mLock.lock();
try
{
//Clear any traffic channel that is already allocated on the new/current control frequency
if(mAllocatedTrafficChannelMap.containsKey(current))
{
broadcast(new ChannelEvent(mAllocatedTrafficChannelMap.get(current), ChannelEvent.Event.REQUEST_DISABLE));
}
}
finally
{
mLock.unlock();
}
}
@Override
public void reset()
{
}
@Override
public void start()
{
}
@Override
public void stop()
{
mLock.lock();
try
{
mAvailableTrafficChannelQueue.clear();
List<Channel> channels = new ArrayList<>(mAllocatedTrafficChannelMap.values());
//Issue a disable request for each traffic channel
for(Channel channel: channels)
{
LOGGER.info("Stopping NXDN traffic channel: " + channel);
broadcast(new ChannelEvent(channel, ChannelEvent.Event.REQUEST_DISABLE));
}
}
finally
{
mLock.unlock();
}
}
@Override
public void addDecodeEventListener(Listener<IDecodeEvent> listener)
{
mDecodeEventListener = listener;
}
@Override
public void removeDecodeEventListener(Listener<IDecodeEvent> listener)
{
mDecodeEventListener = null;
}
/**
* Remotes the event tracker from the tracker map
* @param frequency for the tracker
*/
private void removeTracker(long frequency)
{
mLock.lock();
try
{
mEventTrackerMap.remove(frequency);
}
finally
{
mLock.unlock();
}
}
/**
* Monitors channel teardown events to detect when traffic channel processing has ended or channel start has been
* rejected. Reclaims the traffic channel instance for reuse by future traffic channel grants.
*/
public class TrafficChannelTeardownMonitor implements Listener<ChannelEvent>
{
/**
* Process channel events from the ChannelProcessingManager to account for owned child traffic channels.
* Note: this method sees events for ALL channels and not just P25 channels managed by this instance.
*
* @param channelEvent to process
*/
@Override
public void receive(ChannelEvent channelEvent)
{
Channel channel = channelEvent.getChannel();
if(mManagedTrafficChannels.contains(channel))
{
mLock.lock();
try
{
switch(channelEvent.getEvent())
{
case NOTIFICATION_PROCESSING_STOP:
mAllocatedTrafficChannelMap.entrySet()
.stream()
.filter(entry -> entry.getValue() == channel)
.map(Map.Entry::getKey)
.findFirst()
.ifPresent(frequency -> {
mAllocatedTrafficChannelMap.remove(frequency);
//Don't remove the tracker. There's a chance the control channel can still
//reference the now terminating traffic channel and cause a residual event
//creation and channel allocation and we'll use the tracker to keep that from
//happening.
//removeTracker(frequency);
mAvailableTrafficChannelQueue.add(channel);
});
break;
case NOTIFICATION_PROCESSING_START_REJECTED:
mAllocatedTrafficChannelMap.entrySet().stream()
.filter(entry -> entry.getValue() == channel)
.map(Map.Entry::getKey)
.findFirst()
.ifPresent(rejectedFrequency -> {
mAllocatedTrafficChannelMap.remove(rejectedFrequency);
mAvailableTrafficChannelQueue.add(channel);
//Leave the event in the map so that it doesn't get recreated. The channel
//processing manager set the 'tuner not available' in the details already
NXDNChannelEventTracker tracker = mEventTrackerMap.get(rejectedFrequency);
if(tracker != null)
{
if(!tracker.getEvent().getDetails().contains(CHANNEL_START_REJECTED))
{
tracker.addDetails(CHANNEL_START_REJECTED + " " + channelEvent.getDescription() +
(tracker.getEvent().getDetails() != null ? " - " + tracker.getEvent().getDetails() : ""));
}
tracker.setTrafficChannelAllocated(false);
broadcast(tracker);
}
});
break;
}
}
finally
{
mLock.unlock();
}
}
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* *****************************************************************************
* 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.nxdn;
/**
* Tracks sync detections for fixed length messages
*/
public class SyncTracker
{
private final int mSymbolsPerMessage;
private final int mSyncLossThreshold;
private final int mMaxSyncWindowSize;
private int mSyncCount = 0;
private int mSymbolCount;
/**
* Constructs an instance
* @param symbolsPerMessage a count of how many symbols per message, between each sync interval (ie message length).
* @param messageWindowSize window size for retaining sync across messages sequences
*/
public SyncTracker(int symbolsPerMessage, int messageWindowSize)
{
mSymbolsPerMessage = symbolsPerMessage;
mSyncLossThreshold = symbolsPerMessage * 2;
mMaxSyncWindowSize = messageWindowSize;
}
/**
* Increments the symbol count.
*/
public void symbol()
{
mSymbolCount++;
if(mSymbolCount >= mSyncLossThreshold)
{
mSymbolCount -= mSymbolsPerMessage;
mSyncCount--;
mSyncCount = Math.max(mSyncCount, 0);
}
}
/**
* Indicates when a sync pattern is detected.
*/
public void syncDetected()
{
mSyncCount++;
mSyncCount = Math.min(mSyncCount, mMaxSyncWindowSize);
mSymbolCount -= mSymbolsPerMessage;
mSymbolCount = Math.max(mSymbolCount, 0);
}
/**
* Indicates if there has been at least one sync detection in the past maxSyncSequence message windows.
* @return true if there is at least one sync detection
*/
public boolean isSynchronized()
{
return mSyncCount > 0;
}
}

View File

@@ -0,0 +1,175 @@
/*
* *****************************************************************************
* 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.nxdn.audio;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule;
import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.dsp.gain.NonClippingGain;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio;
import io.github.dsheirer.module.decode.nxdn.layer3.call.Disconnect;
import io.github.dsheirer.module.decode.nxdn.layer3.call.TransmissionRelease;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallWithOptionalLocation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.AudioCodec;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.List;
/**
* NXDN AMBE audio module
*/
public class NXDNAudioModule extends AmbeAudioModule
{
private final SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private final NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f);
private final List<Audio> mCachedAudioMessages = new ArrayList<>();
private boolean mEncryptedCall = false;
private boolean mEncryptedCallStateEstablished = false;
private AudioCodec mAudioCodec;
/**
* Constructs an instance
* @param userPreferences component
* @param aliasList for the current channel
*/
public NXDNAudioModule(UserPreferences userPreferences, AliasList aliasList)
{
super(userPreferences, aliasList, 0);
}
@Override
public Listener<SquelchStateEvent> getSquelchStateListener()
{
return mSquelchStateListener;
}
@Override
public void reset()
{
getIdentifierCollection().clear();
}
@Override
public void start()
{
}
/**
* Processes audio and layer 3 messages to decode audio and to determine the encrypted status of a call event.
*/
public void receive(IMessage message)
{
if(hasAudioCodec())
{
if(message instanceof Audio audio)
{
if(mEncryptedCallStateEstablished)
{
processAudio(audio);
}
else
{
//Cache audio until we can determine the encryption state
mCachedAudioMessages.add(audio);
}
}
else if(message.isValid())
{
if(message instanceof VoiceCall voiceCall)
{
mEncryptedCall = voiceCall.getEncryptionKeyIdentifier().isEncrypted();
mEncryptedCallStateEstablished = true;
mAudioCodec = voiceCall.getCallOption().getCodec();
processCachedAudio();
}
else if(message instanceof VoiceCallWithOptionalLocation voiceCall)
{
mEncryptedCall = voiceCall.getEncryptionKeyIdentifier().isEncrypted();
mEncryptedCallStateEstablished = true;
mAudioCodec = voiceCall.getCallOption().getCodec();
processCachedAudio();
}
else if(message instanceof Disconnect || message instanceof TransmissionRelease)
{
closeAudioSegment();
mCachedAudioMessages.clear();
mEncryptedCall = false;
mEncryptedCallStateEstablished = false;
}
}
}
}
/**
* Processes any cached audio frames that were pending an encryption state determination.
*/
private void processCachedAudio()
{
for(Audio audio : mCachedAudioMessages)
{
processAudio(audio);
}
mCachedAudioMessages.clear();
}
/**
* Processes an audio packet by decoding the IMBE audio frames and rebroadcasting them as PCM audio packets.
*/
private void processAudio(Audio audio)
{
if(!mEncryptedCall && mAudioCodec != null && mAudioCodec.equals(AudioCodec.HALF_RATE)) //Full rate not yet supported
{
for(byte[] frame : audio.getAudioFrames())
{
float[] generatedAudio = getAudioCodec().getAudio(frame);
generatedAudio = mGain.apply(generatedAudio);
addAudio(generatedAudio);
}
}
else
{
//Encrypted audio processing not supported
}
}
/**
* Wrapper for squelch state to process end of call actions. At call end the encrypted call state established
* flag is reset so that the encrypted audio state for the next call can be properly detected and we send an
* END audio packet so that downstream processors like the audio recorder can properly close out a call sequence.
*/
public class SquelchStateListener implements Listener<SquelchStateEvent>
{
@Override
public void receive(SquelchStateEvent event)
{
if(event.getSquelchState() == SquelchState.SQUELCH)
{
closeAudioSegment();
mEncryptedCallStateEstablished = false;
mEncryptedCall = false;
mCachedAudioMessages.clear();
}
}
}
}

View File

@@ -0,0 +1,225 @@
/*
* *****************************************************************************
* 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.nxdn.audio;
import io.github.dsheirer.audio.codec.mbe.MBECallSequence;
import io.github.dsheirer.audio.codec.mbe.MBECallSequenceRecorder;
import io.github.dsheirer.audio.codec.mbe.VoiceFrame;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.nxdn.NXDNMessage;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallInitializationVector;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressDestinationInfo2;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressDestinationInfo4;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressSourceID;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInfo;
import io.github.dsheirer.preference.UserPreferences;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* NXDN AMBE+ Frame recorder generates call sequence recordings containing JSON representations of audio
* frames, optional encryption and call identifiers.
*/
public class NXDNCallSequenceRecorder extends MBECallSequenceRecorder
{
private final static Logger mLog = LoggerFactory.getLogger(NXDNCallSequenceRecorder.class);
public static final String PROTOCOL = "NXDN";
private MBECallSequence mCallSequence;
/**
* Constructs an instance
*
* @param userPreferences to obtain the recording directory
* @param channelFrequency for the channel to record
* @param system defined by the user
* @param site defined by the user
*/
public NXDNCallSequenceRecorder(UserPreferences userPreferences, long channelFrequency, String system, String site)
{
super(userPreferences, channelFrequency, system, site);
}
/**
* Stops and flushes any partial frame sequence from the processors
*/
@Override
public void stop()
{
flush();
}
/**
* Primary message interface for receiving frames and metadata messages to record
*/
@Override
public void receive(IMessage message)
{
if(message instanceof NXDNMessage nxdn)
{
if(nxdn.isValid())
{
if(nxdn instanceof Audio audio)
{
process(audio);
}
else if(nxdn instanceof NXDNLayer3Message layer3)
{
process(layer3);
}
}
}
}
/**
* Gets and optionally creates a new call sequence.
*/
private MBECallSequence getCallSequence()
{
if(mCallSequence == null)
{
mCallSequence = new MBECallSequence(PROTOCOL);
}
return mCallSequence;
}
/**
* Flushes any partial call sequence
*/
public void flush()
{
if(mCallSequence != null)
{
writeCallSequence(mCallSequence);
mCallSequence = null;
}
}
/**
* Processes any NXDN layer 3 message
*/
public void process(NXDNLayer3Message layer3)
{
switch(layer3.getMessageType())
{
case TRAFFIC_IN_01_CC_VOICE_CALL:
case TRAFFIC_OUT_01_CC_VOICE_CALL:
case TYPE_D_IN_01_CC_VOICE_CALL:
case TYPE_D_OUT_01_CC_VOICE_CALL:
if(layer3 instanceof VoiceCall vc)
{
getCallSequence().setCallType(vc.getCallType().toString());
getCallSequence().setFromIdentifier(vc.getSource());
getCallSequence().setToIdentifier(vc.getDestination());
if(vc.getEncryptionKeyIdentifier().isEncrypted())
{
getCallSequence().setEncrypted(true);
}
}
break;
case TRAFFIC_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TRAFFIC_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TYPE_D_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TYPE_D_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
if(layer3 instanceof VoiceCallInitializationVector iv)
{
//TODO: implement superframes with IV injected into each
}
break;
case TRAFFIC_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION:
case TRAFFIC_OUT_08_CC_TRANSMISSION_RELEASE:
case TYPE_D_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION:
case TYPE_D_OUT_08_CC_TRANSMISSION_RELEASE:
writeCallSequence(mCallSequence);
mCallSequence = null;
break;
case TYPE_D_SCCH_IN_INFO_4_CALL_IN_PROGRESS_DESTINATION:
case TYPE_D_SCCH_OUT_INFO_4_CALL_IN_PROGRESS_DESTINATION:
if(layer3 instanceof CallInProgressDestinationInfo4 info4)
{
getCallSequence().setToIdentifier(info4.getDestination());
}
break;
case TYPE_D_SCCH_IN_INFO_3_CALL_IN_PROGRESS_SOURCE:
case TYPE_D_SCCH_OUT_INFO_3_CALL_IN_PROGRESS_SOURCE:
if(layer3 instanceof CallInProgressSourceID source)
{
getCallSequence().setFromIdentifier(source.getSource());
}
break;
case TYPE_D_SCCH_IN_INFO_2_CALL_IN_PROGRESS_DESTINATION:
case TYPE_D_SCCH_OUT_INFO_2_CALL_IN_PROGRESS_DESTINATION:
if(layer3 instanceof CallInProgressDestinationInfo2 info2)
{
getCallSequence().setToIdentifier(info2.getDestination());
}
break;
case TYPE_D_SCCH_IN_INFO_1_CALL_INFO:
case TYPE_D_SCCH_OUT_INFO_1_CALL_INFO:
if(layer3 instanceof CallInfo ci && ci.getEncryptionKey().isEncrypted())
{
getCallSequence().setEncrypted(true);
}
break;
}
}
/**
* Process audio messages
*/
private void process(Audio audio)
{
List<byte[]> voiceFrames = audio.getAudioFrames();
long timestamp = audio.getTimestamp();
for(int frame = 0; frame < voiceFrames.size(); frame++)
{
if(frame == 0)
{
BinaryMessage frameBits = BinaryMessage.from(voiceFrames.get(frame));
VoiceFrame voiceFrame = new VoiceFrame(timestamp, frameBits.toHexString());
if(audio.hasSACCHFragment())
{
voiceFrame.setTag(audio.getSACCHFragment().getStructure().toString());
}
getCallSequence().add(voiceFrame);
//Voice frames are 20 milliseconds each, so we increment the timestamp by 20 for each one
timestamp += 20;
}
else
{
BinaryMessage frameBits = BinaryMessage.from(voiceFrames.get(frame));
getCallSequence().addVoiceFrame(timestamp, frameBits.toHexString());
//Voice frames are 20 milliseconds each, so we increment the timestamp by 20 for each one
timestamp += 20;
}
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* *****************************************************************************
* 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.nxdn.channel;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
/**
* NXDN Channel to Frequency mapping.
*/
public class ChannelFrequency
{
private int mChannel;
private long mDownlink;
private long mUplink;
/**
* Constructs an instance
* @param number for the channel (1 - 2048)
* @param down link frequency in Hertz
* @param up link frequency in Hertz
*/
public ChannelFrequency(int number, long down, long up)
{
mChannel = number;
mDownlink = down;
mUplink = up;
}
/**
* Empty constructor for Jackson deserialization
*/
public ChannelFrequency()
{
//Empty Jackson constructor
}
/**
* Channel number
* @return channel number 1-2048
*/
@JacksonXmlProperty(isAttribute = true, localName = "channel")
public int getChannel()
{
return mChannel;
}
/**
* Sets the channel number
* @param channel in range 1 to 2048
*/
public void setChannel(int channel)
{
mChannel = channel;
}
/**
* Downlink frequency
* @return frequency in Hertz
*/
@JacksonXmlProperty(isAttribute = true, localName = "downlink")
public long getDownlink()
{
return mDownlink;
}
/**
* Sets the downlink frequency
* @param downlink frequency in Hertz
*/
public void setDownlink(long downlink)
{
mDownlink = downlink;
}
/**
* Uplink frequency
* @return frequency in Hertz
*/
@JacksonXmlProperty(isAttribute = true, localName = "uplink")
public long getUplink()
{
return mUplink;
}
/**
* Sets the uplink frequency
* @param uplink frequency in Hertz
*/
public void setUplink(long uplink)
{
mUplink = uplink;
}
}

View File

@@ -0,0 +1,61 @@
/*
* *****************************************************************************
* 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.nxdn.channel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.List;
/**
* Channel access and channel map information
*/
public class ChannelInformation
{
private final ChannelAccessInformation mChannelAccessInformation;
private final List<ChannelFrequency> mChannelFrequencies;
/**
* Constructs an instance
* @param access info
* @param map of optional channels/frequencies
*/
public ChannelInformation(ChannelAccessInformation access, List<ChannelFrequency> channels)
{
mChannelAccessInformation = access;
mChannelFrequencies = channels;
}
/**
* Channel access info from the control channel
* @return access info
*/
public ChannelAccessInformation getChannelAccessInformation()
{
return mChannelAccessInformation;
}
/**
* Channel frequencies
* @return list
*/
public List<ChannelFrequency> getChannelFrequencies()
{
return mChannelFrequencies;
}
}

View File

@@ -0,0 +1,80 @@
/*
* *****************************************************************************
* 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.nxdn.channel;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.IChannelInformationReceiver;
import io.github.dsheirer.protocol.Protocol;
/**
* Base NXDN channel
*/
public abstract class NXDNChannel implements IChannelDescriptor, IChannelInformationReceiver
{
/**
* Constructs an instance
*/
public NXDNChannel()
{
}
/**
* Indicates if this channel has a non-zero downlink frequency.
*/
public boolean isValid()
{
return getDownlinkFrequency() > 0;
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
@Override
public int getTimeslotCount()
{
return 1;
}
@Override
public boolean isTDMAChannel()
{
return false;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("DN:").append(getDownlinkFrequency() / 1E6D);
long up = getUplinkFrequency();
if(up > 0)
{
sb.append(" UP:").append(up / 1E6D);
}
sb.append(" MHZ");
return sb.toString();
}
}

View File

@@ -0,0 +1,74 @@
/*
* *****************************************************************************
* 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.nxdn.channel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.Bandwidth;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.Map;
/**
* NXDN channel that uses Direct Frequency Assignment.
*/
public class NXDNChannelDFA extends NXDNChannel
{
private ChannelAccessInformation mChannelAccessInformation;
private final int mOutboundChannelNumber;
private final int mInboundChannelNumber;
/**
* Constructs an instance
* @param outboundChannelNumber from message
* @param inboundChannelNumber from message
* @param bandwidth from message
*/
public NXDNChannelDFA(int outboundChannelNumber, int inboundChannelNumber, Bandwidth bandwidth)
{
mOutboundChannelNumber = outboundChannelNumber;
mInboundChannelNumber = inboundChannelNumber;
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
mChannelAccessInformation = channelAccessInformation;
}
@Override
public long getDownlinkFrequency()
{
if(mChannelAccessInformation != null)
{
return mChannelAccessInformation.getFrequency(mOutboundChannelNumber);
}
return 0;
}
@Override
public long getUplinkFrequency()
{
if(mChannelAccessInformation != null)
{
return mChannelAccessInformation.getFrequency(mInboundChannelNumber);
}
return 0;
}
}

View File

@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* 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.nxdn.channel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.Map;
/**
* Fake channel that uses a talkgroup value for tracking call detection events by frequency, where the talkgroup value
* represents the tracked frequency.
*/
public class NXDNChannelFake extends NXDNChannel
{
private int mTalkgroup;
public NXDNChannelFake(int talkgroup)
{
mTalkgroup = talkgroup;
}
/**
* Always return false to indicate this a a non-value, fake channel.
*/
@Override
public boolean isValid()
{
return false;
}
@Override
public long getDownlinkFrequency()
{
return mTalkgroup;
}
@Override
public long getUplinkFrequency()
{
return 0;
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
//No-op.
}
@Override
public String toString()
{
return "UNKNOWN";
}
}

View File

@@ -0,0 +1,103 @@
/*
* *****************************************************************************
* 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.nxdn.channel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.Map;
/**
* NXDN channel that uses a lookup table for the frequency values.
*/
public class NXDNChannelLookup extends NXDNChannel
{
private ChannelFrequency mChannelFrequency;
private final int mChannelNumber;
/**
* Constructs an instance
* @param channelNumber for this channel referenced against a global system channel lookup table.
*/
public NXDNChannelLookup(int channelNumber)
{
mChannelNumber = channelNumber;
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
mChannelFrequency = channelFrequencyMap.get(mChannelNumber);
}
/**
* Channel number
*/
public int getChannelNumber()
{
return mChannelNumber;
}
@Override
public long getDownlinkFrequency()
{
if(mChannelFrequency != null)
{
return mChannelFrequency.getDownlink();
}
return 0;
}
@Override
public long getUplinkFrequency()
{
if(mChannelFrequency != null)
{
return mChannelFrequency.getUplink();
}
return 0;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(mChannelFrequency != null)
{
sb.append("DN:").append(getDownlinkFrequency() / 1E6D);
long up = getUplinkFrequency();
if(up > 0)
{
sb.append(" UP:").append(up / 1E6D);
}
sb.append(" MHZ");
}
else
{
sb.append("CHANNEL-MODE [" + mChannelNumber + "] IS MISSING CHANNEL:FREQUENCY MAPPING");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,97 @@
/*
* *****************************************************************************
* 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.nxdn.channel;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.util.Callback;
/**
* JavaFX wrapper for the channel frequency object to make the fields observable.
*/
public class ObservableChannelFrequency
{
private IntegerProperty mChannel = new SimpleIntegerProperty();
private LongProperty mDownlink = new SimpleLongProperty();
private LongProperty mUplink = new SimpleLongProperty();
/**
* Constructs an instance
* @param channelFrequency object with configuration details
*/
public ObservableChannelFrequency(ChannelFrequency channelFrequency)
{
mChannel.set(channelFrequency.getChannel());
mDownlink.set(channelFrequency.getDownlink());
mUplink.set(channelFrequency.getUplink());
}
/**
* Constructs an instance with no values.
*/
public ObservableChannelFrequency()
{
}
/**
* Creates a ChannelFrequency instance from this observable wrapper.
*/
public ChannelFrequency getChannel()
{
return new ChannelFrequency(mChannel.get(), mDownlink.get(), mUplink.get());
}
/**
* Channel number
*/
public IntegerProperty channelProperty()
{
return mChannel;
}
/**
* Downlink frequency in Hertz
* @return
*/
public LongProperty downlinkProperty()
{
return mDownlink;
}
/**
* Uplink frequency in Hertz
*/
public LongProperty uplinkProperty()
{
return mUplink;
}
/**
* Creates an observable property extractor for use with observable lists to detect changes internal to this object.
*/
public static Callback<ObservableChannelFrequency, Observable[]> extractor()
{
return (ObservableChannelFrequency ocf) -> new Observable[] {ocf.channelProperty(), ocf.downlinkProperty(),
ocf.uplinkProperty()};
}
}

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* 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.nxdn.identifier;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.StringIdentifier;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationCategory;
import io.github.dsheirer.protocol.Protocol;
/**
* Indicates the network location category
*/
public class NXDNCategory extends StringIdentifier
{
public NXDNCategory(String value, IdentifierClass identifierClass, Form form, Role role)
{
super(value, identifierClass, form, role);
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
/**
* Creates an instance
* @param category for the system/site values
* @return category identifier
*/
public static NXDNCategory create(LocationCategory category)
{
return new NXDNCategory(category.toString(), IdentifierClass.NETWORK, Form.CATEGORY, Role.BROADCAST);
}
}

View File

@@ -0,0 +1,87 @@
/*
* *****************************************************************************
* 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.nxdn.identifier;
import io.github.dsheirer.identifier.encryption.EncryptionKey;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CipherType;
import io.github.dsheirer.module.decode.p25.reference.Encryption;
/**
* NXDN Encryption Parameters
*/
public class NXDNEncryptionKey extends EncryptionKey
{
/**
* Constructs an instance
* @param algorithm id
* @param keyId value
*/
public NXDNEncryptionKey(int algorithm, int keyId)
{
super(algorithm, keyId);
}
/**
* Cipher type for the call.
* @return type
*/
public CipherType getCipherType()
{
return CipherType.fromValue(getAlgorithm());
}
public String toString()
{
StringBuilder sb = new StringBuilder();
if(isEncrypted())
{
sb.append(getCipherType());
sb.append(" KEY:").append(getKey());
}
else
{
sb.append("UNENCRYPTED");
}
return sb.toString();
}
@Override
public boolean isEncrypted()
{
return getCipherType() != CipherType.UNENCRYPTED;
}
/**
* Creates a new NXDN encryption algorithm
*/
public static NXDNEncryptionKey create(int algorithm, int keyId)
{
return new NXDNEncryptionKey(algorithm, keyId);
}
/**
* Creates a new NXDN encryption algorithm
*/
public static NXDNEncryptionKey create(Encryption encryption, int keyId)
{
return create(encryption.getValue(), keyId);
}
}

View File

@@ -0,0 +1,62 @@
package io.github.dsheirer.module.decode.nxdn.identifier;
import io.github.dsheirer.identifier.Role;
/**
* Inter-system, ISSI or Roaming NXDN radio identifier.
*/
public class NXDNFullyQualifiedRadioIdentifier extends NXDNRadioIdentifier
{
private int mSystem;
/**
* Constructs an instance
* @param system that is home for the radio
* @param radio identifier
* @param role for the identifier
*/
public NXDNFullyQualifiedRadioIdentifier(int system, int radio, Role role)
{
super(radio, role);
mSystem = system;
}
@Override
public String toString()
{
return mSystem + "." + super.toString();
}
/**
* Creates a fully qualified radio identifier with a role of TO
* @param system that is home for the radio
* @param radio identifier
* @return identifier
*/
public static NXDNFullyQualifiedRadioIdentifier createTo(int system, int radio)
{
return new NXDNFullyQualifiedRadioIdentifier(system, radio, Role.TO);
}
/**
* Creates a fully qualified radio identifier with a role of FRON
* @param system that is home for the radio
* @param radio identifier
* @return identifier
*/
public static NXDNFullyQualifiedRadioIdentifier createFrom(int system, int radio)
{
return new NXDNFullyQualifiedRadioIdentifier(system, radio, Role.FROM);
}
/**
* Creates a fully qualified radio identifier with a role of ANY
* @param system that is home for the radio
* @param radio identifier
* @return identifier
*/
public static NXDNFullyQualifiedRadioIdentifier createAny(int system, int radio)
{
return new NXDNFullyQualifiedRadioIdentifier(system, radio, Role.ANY);
}
}

View File

@@ -0,0 +1,42 @@
package io.github.dsheirer.module.decode.nxdn.identifier;
import io.github.dsheirer.identifier.Role;
public class NXDNFullyQualifiedTalkgroupIdentifier extends NXDNTalkgroupIdentifier
{
private int mSystem;
/**
* Constructs an instance
*
* @param value for the talkgroup
* @param role for the talkgroup
*/
public NXDNFullyQualifiedTalkgroupIdentifier(int system, int value, Role role)
{
super(value, role);
mSystem = system;
}
/**
* Creates an instance with a role of TO
* @param system that is home for the talkgroup
* @param talkgroup value
* @return TO instance
*/
public static NXDNFullyQualifiedTalkgroupIdentifier createTo(int system, int talkgroup)
{
return new NXDNFullyQualifiedTalkgroupIdentifier(system, talkgroup, Role.TO);
}
/**
* Creates an instance with a role of ANY
* @param system that is home for the talkgroup
* @param talkgroup value
* @return ANY instance
*/
public static NXDNFullyQualifiedTalkgroupIdentifier createAny(int system, int talkgroup)
{
return new NXDNFullyQualifiedTalkgroupIdentifier(system, talkgroup, Role.ANY);
}
}

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.nxdn.identifier;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.protocol.Protocol;
/**
* NXDN Radio Access Number (RAN)
*/
public class NXDNRadioAccessNumber extends IntegerIdentifier
{
public NXDNRadioAccessNumber(int ran)
{
super(ran, IdentifierClass.NETWORK, Form.RAN, Role.BROADCAST);
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
/**
* Creates the identifier
* @param ran value
* @return identifier
*/
public static NXDNRadioAccessNumber create(int ran)
{
return new NXDNRadioAccessNumber(ran);
}
}

View File

@@ -0,0 +1,170 @@
/*
* *****************************************************************************
* 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.nxdn.identifier;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.protocol.Protocol;
import java.text.DecimalFormat;
/**
* NXDN Radio Identifier
*/
public class NXDNRadioIdentifier extends RadioIdentifier
{
private static final DecimalFormat REPEATER_FORMAT = new DecimalFormat("00");
private static final DecimalFormat RADIO_FORMAT = new DecimalFormat("0000");
private boolean mTypeD = false;
/**
* Constructs an instance
* @param value for the ID
* @param role played by the ID
*/
public NXDNRadioIdentifier(Integer value, Role role)
{
super(value, role);
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
/**
* Indicates if the identifier is for a Type-D system.
*/
public boolean isTypeD()
{
return mTypeD;
}
/**
* Home repeater value for a Type-D identifier.
*/
public int getTypeDHomeRepeater()
{
return (getValue() >> 11) & 0x1F;
}
/**
* Radio ID for a Type-D identifier.
* @return radio ID.
*/
public int getTypeDRadio()
{
return getValue() & 0x7FF;
}
/**
* Sets the Type-D flag for this ID.
* @param typeD true to indicate that this is a Type-D identifier.
*/
public void setTypeD(boolean typeD)
{
mTypeD = typeD;
}
@Override
public String toString()
{
if(isTypeD())
{
return REPEATER_FORMAT.format(getTypeDHomeRepeater()) + "-" + RADIO_FORMAT.format(getTypeDRadio());
}
return switch(getValue())
{
case 0xFFF0 -> "0xFFF0 TRUNKING CONTROLLER";
case 0xFFF1 -> "0xFFF1 PSTN ID";
case 0xFFF2 -> "0xFFF2 SPECIAL ID";
case 0xFFF3 -> "0xFFF3 SPECIAL ID";
case 0xFFF4 -> "0xFFF4 SPECIAL ID";
case 0xFFF5 -> "0xFFF5 CONVENTIONAL PSTN ID";
case 0xFFFF -> "0xFFFF ALL UNITS";
default -> super.toString();
};
}
/**
* Creates an NXDN radio identifier with the FROM role.
* @param value of the radio ID
* @return identifier
*/
public static NXDNRadioIdentifier createFrom(int value)
{
return new NXDNRadioIdentifier(value, Role.FROM);
}
/**
* Creates an NXDN radio identifier with the TO role.
* @param value of the radio ID
* @return identifier
*/
public static NXDNRadioIdentifier createTo(int value)
{
return new NXDNRadioIdentifier(value, Role.TO);
}
/**
* Creates an NXDN radio identifier with the ANY role.
* @param value of the radio ID
* @return identifier
*/
public static NXDNRadioIdentifier createAny(int value)
{
return new NXDNRadioIdentifier(value, Role.ANY);
}
/**
* Creates a Type-D radio identifier with the FROM role.
* @param radio value that combines the home repeater and the radio
* @return identifier
*/
public static NXDNRadioIdentifier createTypeDFrom(int radio)
{
NXDNRadioIdentifier identifier = new NXDNRadioIdentifier(radio, Role.FROM);
identifier.setTypeD(true);
return identifier;
}
/**
* Creates a Type-D radio identifier with the TO role.
* @param radio value that combines the home repeater and the radio
* @return identifier
*/
public static NXDNRadioIdentifier createTypeDTo(int radio)
{
NXDNRadioIdentifier identifier = new NXDNRadioIdentifier(radio, Role.TO);
identifier.setTypeD(true);
return identifier;
}
/**
* Creates a Type-D radio identifier with the ANY role.
* @param radio value that combines the home repeater and the radio
* @return identifier
*/
public static NXDNRadioIdentifier createTypeDAny(int radio)
{
NXDNRadioIdentifier identifier = new NXDNRadioIdentifier(radio, Role.ANY);
identifier.setTypeD(true);
return identifier;
}
}

View File

@@ -0,0 +1,47 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.nxdn.identifier;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.protocol.Protocol;
public class NXDNSite extends IntegerIdentifier
{
public NXDNSite(int value)
{
super(value, IdentifierClass.NETWORK, Form.SITE, Role.BROADCAST);
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
/**
* Creates a new site identifier
*/
public static NXDNSite create(int site)
{
return new NXDNSite(site);
}
}

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.nxdn.identifier;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.protocol.Protocol;
/**
* NXDN trunking system ID
*/
public class NXDNSystem extends IntegerIdentifier
{
public NXDNSystem(int value)
{
super(value, IdentifierClass.NETWORK, Form.SYSTEM, Role.BROADCAST);
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
/**
* Creates a new system identifier
*/
public static NXDNSystem create(int system)
{
return new NXDNSystem(system);
}
}

View File

@@ -0,0 +1,43 @@
/*
* *****************************************************************************
* 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.nxdn.identifier;
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
import io.github.dsheirer.protocol.Protocol;
/**
* NXDN talker alias identifier
*/
public class NXDNTalkerAliasIdentifier extends TalkerAliasIdentifier
{
/**
* Constructs an instance
* @param alias value
*/
public NXDNTalkerAliasIdentifier(String alias)
{
super(alias);
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
}

View File

@@ -0,0 +1,126 @@
/*
* *****************************************************************************
* 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.nxdn.identifier;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.protocol.Protocol;
import java.text.DecimalFormat;
/**
* NXDN Talkgroup Identifier
*/
public class NXDNTalkgroupIdentifier extends TalkgroupIdentifier
{
private static final DecimalFormat REPEATER_FORMAT = new DecimalFormat("00");
private static final DecimalFormat TALKGROUP_FORMAT = new DecimalFormat("0000");
private boolean mTypeD = false;
/**
* Constructs an instance
*
* @param value for the talkgroup
* @param role for the talkgroup
*/
public NXDNTalkgroupIdentifier(Integer value, Role role)
{
super(value, role);
}
@Override
public Protocol getProtocol()
{
return Protocol.NXDN;
}
/**
* Indicates if the identifier is for a Type-D system.
*/
public boolean isTypeD()
{
return mTypeD;
}
/**
* Sets the Type-D flag for this ID.
* @param typeD true to indicate that this is a Type-D identifier.
*/
public void setTypeD(boolean typeD)
{
mTypeD = typeD;
}
/**
* Home repeater value for a Type-D identifier.
*/
public int getTypeDHomeRepeater()
{
return (getValue() >> 11) & 0x1F;
}
/**
* Talkgroup ID for a Type-D identifier.
* @return radio ID.
*/
public int getTypeDTalkgroup()
{
return getValue() & 0x7FF;
}
@Override
public String toString()
{
if(isTypeD())
{
return REPEATER_FORMAT.format(getTypeDHomeRepeater()) + "-" + TALKGROUP_FORMAT.format(getTypeDTalkgroup());
}
return switch(getValue())
{
case 0x0 -> "0x0000 NULL GROUP";
case 0xFFF0 -> "0xFFF0 RESERVED GROUP";
case 0xFFFF -> "0xFFFF ALL GROUPS";
default -> super.toString();
};
}
/**
* Creates an NXDN talkgroup identifier with the TO role.
* @param value of the talkgroup
* @return identifier
*/
public static NXDNTalkgroupIdentifier createTo(int value)
{
return new NXDNTalkgroupIdentifier(value, Role.TO);
}
/**
* Creates a Type-D talkgroup identifier with the TO role.
* @param value for the home repeater and talkgroup combined into a 16-bit integer
* @return identifier
*/
public static NXDNTalkgroupIdentifier createTypeDTo(int value)
{
NXDNTalkgroupIdentifier identifier = new NXDNTalkgroupIdentifier(value, Role.TO);
identifier.setTypeD(true);
return identifier;
}
}

View File

@@ -0,0 +1,320 @@
/*
* *****************************************************************************
* 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.nxdn.layer1;
import io.github.dsheirer.dsp.symbol.Dibit;
import java.text.DecimalFormat;
import java.util.Arrays;
import org.apache.commons.math3.stat.descriptive.moment.Variance;
/**
* LMS adaptive equalizer for real-valued C4FM symbols. Trains taps on externally triggered sync detection combined
* with dynamic tap updates for a fixed symbol count and then freezes the taps until the error variance between sync
* detections exceeds a fixed threshold, whereby a tap retraining is enqueued for the next sync detection.
*/
public class C4FMEqualizer
{
private static final DecimalFormat DF = new DecimalFormat("0.0000");
//Maximum possible error is PI/4 = 0.78, but we clip it to less than half as the allowable error for tap updates
private static final float MAX_SYMBOL_ERROR = 0.3f;
private static final double TRAINING_IMPROVEMENT_GOAL_PER_ITERATION = 0.001f;
private static final double TRAINING_CONVERGENCE_THRESHOLD = 0.16f;
private static final double TRAINING_SKIP_VARIANCE_THRESHOLD = 0.05;
private static final int TRAINING_ITERATIONS_MAX = 30;
private final Variance mVariance = new Variance();
private final float[] mBuffer;
private final float[] mReversedSyncSymbols;
private final float[] mTaps;
private final int mCenterTap;
private final int mDelaySymbolCount;
private final int mTapUpdateSymbolCount;
private final int mTrainingSkipCountThreshold;
private boolean mEnabled = false;
private boolean mSyncDetected = false;
private int mSymbolsUntilTraining = 0;
private int mTapUpdateSymbolsRemaining = 0;
private int mSkippedTrainingCount = 0;
/**
* Constructs an instance
*
* @param syncSymbols sync pattern that will be used to train the equalizer on sync detections
* @param symbolLength for the equalizer - will be made to odd length, increasing by one if necessary (5-11)
* @param tapUpdateSymbolCount symbol count following sync detection and training to continue tap updates.
* @param maxSkipTrainingCount maximum number of consecutive sync detections and skipped trainings that can occur without retraining.
*/
public C4FMEqualizer(float[] syncSymbols, int symbolLength, int tapUpdateSymbolCount, int maxSkipTrainingCount)
{
//Store the sync pattern in reverse order
mReversedSyncSymbols = new float[syncSymbols.length];
for(int x = 0; x < syncSymbols.length; x++)
{
mReversedSyncSymbols[syncSymbols.length - x - 1] = syncSymbols[x];
}
//Make the symbol count odd length for equal delay on either side of the center tap
symbolLength += (symbolLength % 2 == 0) ? 1 : 0;
mCenterTap = symbolLength; //This works since we're going to double the buffer length
mDelaySymbolCount = symbolLength / 2; //Delay until new symbol is aligned to center tap
mTaps = new float[symbolLength * 2];
mTaps[mCenterTap] = 1.0f;
int bufferLength = mReversedSyncSymbols.length * 2 + mTaps.length - 2;
mBuffer = new float[bufferLength];
mTapUpdateSymbolCount = tapUpdateSymbolCount;
mTrainingSkipCountThreshold = maxSkipTrainingCount;
}
/**
* Description of the current tap values
*/
public String getTapsDescription()
{
StringBuilder sb = new StringBuilder();
sb.append("<html>EQ Taps<br>");
for(int i = 0; i < mTaps.length; i++)
{
sb.append(i).append(": ").append(DF.format(mTaps[i])).append("<br>");
}
return sb.toString();
}
/**
* Symbol processing delay.
* @return processing delay in symbols.
*/
public int getDelaySymbolCount()
{
return mDelaySymbolCount;
}
/**
* Length of the equalizer in symbols. Note: the internal tap count is twice as long since this
* is a fractional (x2) equalizer.
* @return length in symbols.
*/
public int getLength()
{
return mTaps.length / 2;
}
/**
* Enable the equalizer when a valid sync pattern is detected
*/
public void enable()
{
mEnabled = true;
}
/**
* Indicates the enabled state.
* @return enabled state
*/
public boolean isEnabled()
{
return mEnabled;
}
/**
* Disable the equalizer when the signal is lost so that noise doesn't contaminate the equalizer taps.
*/
public void disable()
{
mEnabled = false;
}
/**
* Notifies the equalizer that a sync pattern is detected and queues a training sequence once a couple more
* symbols arrive once the equalizer delay line has aligned to output the sync pattern.
*/
public void syncDetected()
{
mSyncDetected = true;
mSymbolsUntilTraining = mDelaySymbolCount;
}
/**
* Indicates that the sync patter is lost and the equalizer should reset.
*/
public void syncLost()
{
reset();
disable();
}
/*
* Resets the equalizer taps
*/
public void reset()
{
Arrays.fill(mTaps, 0f);
mTaps[mCenterTap] = 1.0f;
mVariance.clear();
}
/**
* Process the samples and produce a delayed, equalized soft symbol ready for symbol decision. Updates the
* equalizer taps to correct for the error in the equalized sample relative to the hard symbol decision.
*
* @param fractional sample that falls halfway between symbols
* @param symbol to equalize
* @return equalized symbol.
*/
public float process(float fractional, float symbol)
{
//Shift buffer samples to the right two places and add new sample and symbol.
System.arraycopy(mBuffer, 0, mBuffer, 2, mBuffer.length - 2);
mBuffer[0] = fractional;
mBuffer[1] = symbol;
//Since there's a buffer delay, count down from sync detection until the pattern is aligned with the buffer
// contents and we can start training
if(mSyncDetected)
{
if(--mSymbolsUntilTraining <= 0)
{
train();
}
}
float equalized = 0f;
if(mEnabled)
{
int tapIndex;
for(tapIndex = 0; tapIndex < mTaps.length; tapIndex++)
{
equalized += mTaps[tapIndex] * mBuffer[tapIndex];
}
if(mTapUpdateSymbolsRemaining > 0)
{
mTapUpdateSymbolsRemaining--;
float error = Dibit.getError(equalized);
error = Math.clamp(error, -MAX_SYMBOL_ERROR, MAX_SYMBOL_ERROR);
//Step size (mu) in range 0.0001 to 0.01
float stepSize = 0.01f;
for(tapIndex = 0; tapIndex < mTaps.length; tapIndex++)
{
if(tapIndex != mCenterTap)
{
mTaps[tapIndex] += stepSize * error * mBuffer[tapIndex];
// mTaps[tapIndex] += stepSize * error * mBuffer[tapIndex] * TAP_LEAKAGE;
}
}
}
//Track the residual error that remains after equalization
mVariance.increment(Dibit.getError(equalized));
}
else
{
//Return the uncorrected, delayed symbol when we're disabled
equalized = mBuffer[mCenterTap];
}
return equalized;
}
/**
* Trains the equalizer against the buffered detected sync pattern. Trains against the sync pattern sequence
* multiple times while the per-iteration improvement continues to become smaller and stops training once the
* improvement falls below a threshold goal or exceeds a maximum iterations. If the training converges to
* below a threshold, we snapshot and use the trained taps, otherwise fallback to the previous training snapshot.
*/
private void train()
{
mSyncDetected = false;
//Skip retrain when we're still doing dynamic tap adjustments following the previous training session.
if(mTapUpdateSymbolsRemaining > 0)
{
mVariance.clear();
return;
}
//Ensure we've collected enough variance samples in the buffer and we have sufficient variance
if(mVariance.getN() <= mDelaySymbolCount || mVariance.getResult() > TRAINING_SKIP_VARIANCE_THRESHOLD ||
++mSkippedTrainingCount > mTrainingSkipCountThreshold)
{
mSkippedTrainingCount = 0;
//If the current variance tracked since the last training session using dynamic tap adjustments at each
// symbol is excessive, fallback to the most recent training snapshot as a better starting point
if(mVariance.getResult() > 0.225)
{
reset();
}
float accumulator, error;
double improvement, previousVariance;
int symbolIndex, index, trainingIterationCount = 0;
Variance variance = new Variance();
//Step size (mu) in range 0.0001 to 0.01
float stepSize = 0.005f;
do
{
previousVariance = variance.getN() == 0 ? 20.0 : variance.getResult();
variance.clear();
for(symbolIndex = 0; symbolIndex < mReversedSyncSymbols.length; symbolIndex++)
{
accumulator = 0.0f;
for(index = 0; index < mTaps.length; index++)
{
accumulator += mTaps[index] * mBuffer[index + (2 * symbolIndex)];
}
error = mReversedSyncSymbols[symbolIndex] - accumulator;
variance.increment(error);
for(index = 0; index < mTaps.length; index++)
{
if(index != mCenterTap)
{
mTaps[index] += error * stepSize * mBuffer[index + (2 * symbolIndex)];
}
}
}
trainingIterationCount++;
improvement = previousVariance - variance.getResult();
}
while(trainingIterationCount < TRAINING_ITERATIONS_MAX && improvement > TRAINING_IMPROVEMENT_GOAL_PER_ITERATION);
// System.out.println("Training Iterations:" + trainingIterationCount + " End Variance:" + DF.format(variance.getResult()));
//If we failed to converge, reset the taps to default and use blind training
if(variance.getResult() > TRAINING_CONVERGENCE_THRESHOLD)
{
reset();
}
mTapUpdateSymbolsRemaining = mTapUpdateSymbolCount;
}
mVariance.clear();
}
}

View File

@@ -0,0 +1,403 @@
/*
* *****************************************************************************
* 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.nxdn.layer1;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.FragmentedIntField;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.module.decode.nxdn.NXDNMessage;
import io.github.dsheirer.module.decode.nxdn.layer2.Interleaver;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer2.LICHTracker;
import io.github.dsheirer.module.decode.nxdn.layer2.RFChannel;
import io.github.dsheirer.module.decode.nxdn.layer2.SACCHFragment;
import io.github.dsheirer.module.decode.nxdn.layer2.Scrambler;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageFactory;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.call.Audio;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.Convolution;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.NXDNCRC;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.PunctureProvider;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.PunctureProviderCACAndFACCH2_UDCH;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.PunctureProviderFACCH1_UDCH2_FACCH3;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.PunctureProviderLongCAC;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.PunctureProviderNone;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.PunctureProviderSACCHAndSCCH;
import io.github.dsheirer.module.decode.nxdn.layer3.type.AudioCodec;
import java.util.ArrayList;
import java.util.List;
/**
* NXDN layer 1 frame.
*/
public class Frame
{
private static final BinaryMessage SCRAMBLE_SEQUENCE_LICH = Scrambler.generate(16);
private static final BinaryMessage SCRAMBLE_SEQUENCE_FULL = Scrambler.generate(364);
private static final BinaryMessage SCRAMBLE_SEQUENCE_CONTROL_INBOUND = Scrambler.generate(268);
private static final BinaryMessage SCRAMBLE_SEQUENCE_CONTROL_OUTBOUND = Scrambler.generate(340);
private static final Interleaver INTERLEAVER_16_9 = new Interleaver(16, 9);
private static final Interleaver INTERLEAVER_12_29 = new Interleaver(12, 29);
private static final Interleaver INTERLEAVER_12_25 = new Interleaver(12, 25);
private static final Interleaver INTERLEAVER_12_21 = new Interleaver(12, 21);
private static final Interleaver INTERLEAVER_12_5 = new Interleaver(12, 5);
private static final PunctureProvider PUNCTURE_PROVIDER_CAC_FACCH2_UDCH = new PunctureProviderCACAndFACCH2_UDCH();
private static final PunctureProvider PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2 = new PunctureProviderFACCH1_UDCH2_FACCH3();
private static final PunctureProvider PUNCTURE_PROVIDER_LONG_CAC = new PunctureProviderLongCAC();
private static final PunctureProvider PUNCTURE_PROVIDER_NONE = new PunctureProviderNone();
private static final PunctureProvider PUNCTURE_PROVIDER_SACCH_SCCH = new PunctureProviderSACCHAndSCCH();
private static final IntField STRUCTURE = IntField.length2(0);
private static final IntField RADIO_ACCESS_NUMBER = IntField.length6(2);
private static final FragmentedIntField LICH_FIELD = FragmentedIntField.of(0, 2, 4, 6, 8, 10, 12);
private static final FragmentedIntField LICH_PARITY_FIELD = FragmentedIntField.of(0, 2, 4, 6);
private static final int LICH_EVEN_PARITY_CHECK_BIT = 14;
private static final int RAN_TYPE_D = 0;
static
{
//Clear the LICH bits in the complete scramble sequences so that we don't double-scramble the bits.
SCRAMBLE_SEQUENCE_FULL.xor(SCRAMBLE_SEQUENCE_LICH);
SCRAMBLE_SEQUENCE_CONTROL_INBOUND.xor(SCRAMBLE_SEQUENCE_LICH);
SCRAMBLE_SEQUENCE_CONTROL_OUTBOUND.xor(SCRAMBLE_SEQUENCE_LICH);
}
private LICH mLICH;
private List<NXDNMessage> mMessages = new ArrayList<>();
/**
* Constructs a frame. Note: channel and direction are used as fallback values when we can't exactly match the
* LICH value for this frame due to decoding errors.
* @param message with frame bits
* @param timestamp for the frame.
* @param tracker for accessing tracked RF channel type and direction.
*/
public Frame(CorrectedBinaryMessage message, long timestamp, LICHTracker tracker)
{
message.xor(SCRAMBLE_SEQUENCE_LICH);
boolean lichParityValid = checkLICHParity(message);
int lichValue = message.getInt(LICH_FIELD);
mLICH = LICH.fromValue(lichValue, tracker.isTypeD());
//If failed parity check or UNKNOWN LICH, force the highest occurring channel/direction from the tracker
if((!lichParityValid && tracker.getChannel() != RFChannel.UNKNOWN) || mLICH == LICH.UNKNOWN)
{
mLICH = LICH.fromValue(lichValue, tracker.getChannel(), tracker.getDirection());
}
else
{
tracker.track(mLICH);
}
//Descramble the message according to the RF Channel Type and repeater direction. Control channel uses the
//full scramble sequence truncated based on repeater direction. All others use a full scramble sequence.
//RDCH, RTCH and RTCH2
if(mLICH.getRFChannel() == RFChannel.RCCH)
{
if(mLICH.isOutbound())
{
message.xor(SCRAMBLE_SEQUENCE_CONTROL_OUTBOUND);
CorrectedBinaryMessage payload = decodeCAC(message);
int ran = payload.getInt(RADIO_ACCESS_NUMBER);
CorrectedBinaryMessage cac = payload.getSubMessage(8, 152);
cac.setCorrectedBitCount(payload.getCorrectedBitCount());
NXDNMessageType type = NXDNMessageType.getControl(cac, mLICH);
NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, cac, timestamp, ran, mLICH);
layer3.setValid(NXDNCRC.checkCAC(payload));
mMessages.add(layer3);
}
else
{
message.xor(SCRAMBLE_SEQUENCE_CONTROL_INBOUND);
CorrectedBinaryMessage payload = mLICH.isLongCAC() ? decodeLongCAC(message) : decodeShortCAC(message);
int ran = payload.getInt(RADIO_ACCESS_NUMBER);
CorrectedBinaryMessage cac = mLICH.isLongCAC() ? payload.getSubMessage(8, 136) : payload.getSubMessage(8, 104);
cac.setCorrectedBitCount(payload.getCorrectedBitCount());
NXDNMessageType type = NXDNMessageType.getControl(cac, mLICH);
NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, cac, timestamp, ran, mLICH);
layer3.setValid(mLICH.isLongCAC() ? NXDNCRC.checkLongCAC(payload) : NXDNCRC.checkShortCAC(payload));
mMessages.add(layer3);
}
}
else
{
message.xor(SCRAMBLE_SEQUENCE_FULL);
if(mLICH.hasSACCH()) //SACCH + Voice/FACCH frames
{
CorrectedBinaryMessage sacch = decodeSACCH(message);
SACCHFragment sacchFragment = new SACCHFragment(sacch, timestamp, mLICH);
sacchFragment.setValid(NXDNCRC.checkSACCH(sacch));
mMessages.add(sacchFragment);
if(mLICH.hasAudio())
{
List<byte[]> frames = new ArrayList<>();
if(mLICH.isVoiceFirst())
{
frames.add(message.getSubMessage(76, 148).toByteArray());
frames.add(message.getSubMessage(148, 220).toByteArray());
}
if(mLICH.isVoiceSecond())
{
frames.add(message.getSubMessage(220, 292).toByteArray());
frames.add(message.getSubMessage(292, 364).toByteArray());
}
Audio audio = new Audio(AudioCodec.HALF_RATE, frames, timestamp, sacchFragment.getRAN(), sacchFragment.getLICH());
audio.setSACCHFragment(sacchFragment);
mMessages.add(audio);
}
if(mLICH.isFACCH1First())
{
CorrectedBinaryMessage payload = decodeFACCH1First(message);
NXDNMessageType type = NXDNMessageType.getTraffic(payload, mLICH);
NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, payload, timestamp, sacchFragment.getRAN(), mLICH);
layer3.setValid(NXDNCRC.checkFACCH1(payload));
mMessages.add(layer3);
}
if(mLICH.isFACCH1Second())
{
CorrectedBinaryMessage payload = decodeFACCH1Second(message);
NXDNMessageType type = NXDNMessageType.getTraffic(payload, mLICH);
NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, payload, timestamp, sacchFragment.getRAN(), mLICH);
layer3.setValid(NXDNCRC.checkFACCH1(payload));
mMessages.add(layer3);
}
}
else if(mLICH.hasSCCH()) //Type-D segments with SCCH
{
CorrectedBinaryMessage scch = decodeSCCH(message);
NXDNMessageType typeSCCH = NXDNMessageType.getSCCH(scch, mLICH);
NXDNLayer3Message scchMessage = NXDNMessageFactory.get(typeSCCH, scch, timestamp, RAN_TYPE_D, mLICH);
scchMessage.setValid(NXDNCRC.checkSCCH(scch));
mMessages.add(scchMessage);
if(mLICH.isFACCH1First())
{
CorrectedBinaryMessage payload = decodeFACCH1First(message);
NXDNMessageType type = NXDNMessageType.getTypeD(payload, mLICH);
NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, payload, timestamp, RAN_TYPE_D, mLICH);
layer3.setValid(NXDNCRC.checkFACCH1(payload));
mMessages.add(layer3);
}
if(mLICH.isFACCH1Second())
{
CorrectedBinaryMessage payload = decodeFACCH1Second(message);
NXDNMessageType type = NXDNMessageType.getTypeD(payload, mLICH);
NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, payload, timestamp, RAN_TYPE_D, mLICH);
layer3.setValid(NXDNCRC.checkFACCH1(payload));
mMessages.add(layer3);
}
if(mLICH.isData()) //FACCH3 or UDCH2
{
CorrectedBinaryMessage payloadA = decodeUDCH2AndFACCH3A(message);
CorrectedBinaryMessage payloadB = decodeUDCH2AndFACCH3B(message);
boolean validA = NXDNCRC.checkFACCH3(payloadA);
boolean validB = NXDNCRC.checkFACCH3(payloadB);
CorrectedBinaryMessage combined = new CorrectedBinaryMessage(160);
NXDNMessageType type = NXDNMessageType.getTypeD(combined, mLICH);
NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, combined, timestamp, RAN_TYPE_D, mLICH);
layer3.setValid(validA && validB);
mMessages.add(layer3);
}
if(mLICH.hasAudio())
{
List<byte[]> frames = new ArrayList<>();
if(mLICH.isVoiceFirst())
{
frames.add(message.getSubMessage(76, 148).toByteArray());
frames.add(message.getSubMessage(148, 220).toByteArray());
}
if(mLICH.isVoiceSecond())
{
frames.add(message.getSubMessage(220, 292).toByteArray());
frames.add(message.getSubMessage(292, 364).toByteArray());
}
mMessages.add(new Audio(AudioCodec.HALF_RATE, frames, timestamp, RAN_TYPE_D, mLICH));
}
}
else if(mLICH.getRFChannel() != RFChannel.RTCH2) //UDCH or FACCH2 frames on non-Type-D
{
CorrectedBinaryMessage payload = decodeUDCHAndFACCH2(message);
int ran = payload.getInt(RADIO_ACCESS_NUMBER);
CorrectedBinaryMessage facch2 = payload.getSubMessage(8, 184);
facch2.setCorrectedBitCount(payload.getCorrectedBitCount());
NXDNMessageType type = NXDNMessageType.getTraffic(facch2, mLICH);
NXDNLayer3Message layer3 = NXDNMessageFactory.get(type, facch2, timestamp, ran, mLICH);
layer3.setValid(NXDNCRC.checkFACCH2(payload));
mMessages.add(layer3);
}
}
}
/**
* Indicates if the LICH field passes the even parity check.
* @param message to test
* @return true if the parity check passes.
*/
private static boolean checkLICHParity(CorrectedBinaryMessage message)
{
int onesBitCount = Integer.bitCount(message.getInt(LICH_PARITY_FIELD));
return (onesBitCount % 2 == 1) == message.get(LICH_EVEN_PARITY_CHECK_BIT);
}
/**
* Decodes the frame payload
* @param message from the frame
* @param offset to the start of the payload
* @param interleaver for the payload type
* @param punctureProvider for the payload type
* @return decoded message
*/
private static CorrectedBinaryMessage decode(CorrectedBinaryMessage message, int offset, Interleaver interleaver, PunctureProvider punctureProvider)
{
CorrectedBinaryMessage deinterleaved = interleaver.deinterleave(message, offset);
return Convolution.decode(punctureProvider.depuncture(deinterleaved), punctureProvider);
}
/**
* Decodes the first FACCH1 frame
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeFACCH1First(CorrectedBinaryMessage message)
{
return decode(message, 76, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2);
}
/**
* Decodes the second FACCH1 frame
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeFACCH1Second(CorrectedBinaryMessage message)
{
return decode(message, 220, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2);
}
/**
* Decodes the FACCH2 frame
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeUDCHAndFACCH2(CorrectedBinaryMessage message)
{
return decode(message, 16, INTERLEAVER_12_29, PUNCTURE_PROVIDER_CAC_FACCH2_UDCH);
}
/**
* Decodes the first half of the FACCH3 and UDCH2 frame on Type-D systems
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeUDCH2AndFACCH3A(CorrectedBinaryMessage message)
{
return decode(message, 76, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2);
}
/**
* Decodes the second half of the FACCH3 and UDCH2 frame on Type-D systems
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeUDCH2AndFACCH3B(CorrectedBinaryMessage message)
{
return decode(message, 220, INTERLEAVER_16_9, PUNCTURE_PROVIDER_FACCH1_FACCH3_UDCH2);
}
/**
* Decodes the SACCH frame
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeSACCH(CorrectedBinaryMessage message)
{
return decode(message, 16, INTERLEAVER_12_5, PUNCTURE_PROVIDER_SACCH_SCCH);
}
/**
* Decodes the SCCH frame in a Type-D system
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeSCCH(CorrectedBinaryMessage message)
{
return decode(message, 16, INTERLEAVER_12_5, PUNCTURE_PROVIDER_SACCH_SCCH);
}
/**
* Decodes the outbound control frame
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeCAC(CorrectedBinaryMessage message)
{
return decode(message, 16, INTERLEAVER_12_25, PUNCTURE_PROVIDER_CAC_FACCH2_UDCH);
}
/**
* Decodes the inbound long control frame
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeLongCAC(CorrectedBinaryMessage message)
{
return decode(message, 16, INTERLEAVER_12_21, PUNCTURE_PROVIDER_LONG_CAC);
}
/**
* Decodes the inbound short control frame
* @param message to decode
* @return decoded message
*/
public static CorrectedBinaryMessage decodeShortCAC(CorrectedBinaryMessage message)
{
return decode(message, 16, INTERLEAVER_12_21, PUNCTURE_PROVIDER_NONE);
}
/**
* Message(s( from this frame.
*/
public List<NXDNMessage> getMessages()
{
return mMessages;
}
/**
* Link information channel (LICH)
* @return LICH
*/
public LICH getLICH()
{
return mLICH;
}
}

View File

@@ -0,0 +1,59 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
import io.github.dsheirer.dsp.symbol.Dibit;
/**
* NXDN sync pattern detector that processes hard symbol (ie dibit) decisions.
*/
public class NXDNHardSyncDetector extends NXDNSyncDetector
{
private static final int MAXIMUM_BIT_ERROR = 2;
private long mValue;
/**
* Constructs an instance
*/
public NXDNHardSyncDetector()
{
}
/**
* Processes the dibit.
*
* @param dibit to process
* @return true if a sync pattern is detected.
*/
public boolean process(Dibit dibit)
{
mValue = (Long.rotateLeft(mValue, 2) & SYNC_MASK) + dibit.getValue();
return Long.bitCount(mValue ^ SYNC_PATTERN) <= MAXIMUM_BIT_ERROR;
}
/**
* Bit error count when comparing the current buffer contents to the known sync pattern.
*/
public int getCurrentBitErrors()
{
return Long.bitCount(mValue ^ SYNC_PATTERN);
}
}

View File

@@ -0,0 +1,71 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
import java.util.Arrays;
/**
* NXDN Sync Detector. Uses soft demodulated symbols to detect sync patterns in the demodulated symbol stream.
*/
public abstract class NXDNSoftSyncDetector extends NXDNSyncDetector
{
//symbol buffer is extended by 6x to support SIMD 512 calculations
protected float[] mSymbols = new float[SYNC_DIBIT_LENGTH * 2 + 6];
protected int mSymbolPointer = 0;
/**
* Constructs an instance
*/
public NXDNSoftSyncDetector()
{
}
/**
* Resets this sync detector and flushes any stored soft symbols from the buffer.
*/
public void reset()
{
Arrays.fill(mSymbols, 0.0f);
mSymbolPointer = 0;
}
/**
* Worker method to be implemented by the subclass implementation.
*
* @return sync correlation score.
*/
public abstract float calculate();
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @param dibitSymbol to process that represents the soft demodulated symbol value in phase angle/radians.
* @return correlation score.
*/
public float process(float dibitSymbol)
{
mSymbols[mSymbolPointer] = dibitSymbol;
mSymbols[mSymbolPointer + SYNC_DIBIT_LENGTH] = dibitSymbol;
mSymbolPointer++;
mSymbolPointer %= SYNC_DIBIT_LENGTH;
return calculate();
}
}

View File

@@ -0,0 +1,46 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
/**
* Scalar implementation of NXDN Soft Sync Detector.
*/
public class NXDNSoftSyncDetectorScalar extends NXDNSoftSyncDetector
{
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
float symbol;
float score = 0;
for(int x = 0; x < SYNC_DIBIT_LENGTH; x++)
{
symbol = mSymbols[mSymbolPointer + x];
score += SYMBOLS[x] * symbol;
}
return score;
}
}

View File

@@ -0,0 +1,58 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* SIMD Vector 128 implementation of NXDN Soft Sync Detector.
*/
public class NXDNSoftSyncDetectorVector128 extends NXDNSoftSyncDetector
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
private static final float[] SYNC_EXTENDED = new float[4];
public NXDNSoftSyncDetectorVector128()
{
//Create an array of 4x elements containing the final 2x symbols for SIMD loading.
SYNC_EXTENDED[2] = SYMBOLS[8];
SYNC_EXTENDED[3] = SYMBOLS[9];
}
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 10 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYMBOLS, 0)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer), accumulator);
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYMBOLS, 4)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 4), accumulator);
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_EXTENDED, 0)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 6), accumulator);
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,56 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* SIMD Vector 256 implementation of NXDN Soft Sync Detector.
*/
public class NXDNSoftSyncDetectorVector256 extends NXDNSoftSyncDetector
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
private static final float[] SYNC_EXTENDED = new float[8];
public NXDNSoftSyncDetectorVector256()
{
//Create an array of 8x elements containing the final 2x symbols for SIMD loading.
SYNC_EXTENDED[2] = SYMBOLS[8];
SYNC_EXTENDED[3] = SYMBOLS[9];
}
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 10 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYMBOLS, 0)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer), accumulator);
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYNC_EXTENDED, 0)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + 6), accumulator);
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,48 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* SIMD Vector 512 implementation of NXDN Soft Sync Detector.
*/
public class NXDNSoftSyncDetectorVector512 extends NXDNSoftSyncDetector
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
//Extend the symbols out to 16x using zero padding after the 10th symbol
private static final float[] SYNC_EXTENDED = Arrays.copyOf(SYMBOLS, 16);
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
return FloatVector.fromArray(VECTOR_SPECIES, SYNC_EXTENDED, 0)
.mul(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer))
.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,51 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* SIMD Vector 64 implementation of NXDN Soft Sync Detector.
*/
public class NXDNSoftSyncDetectorVector64 extends NXDNSoftSyncDetector
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
/**
* Processes the demodulated soft symbol value and returns a correlation value against the preceding 24 soft
* symbols that include this recent soft symbol.
*
* @return correlation score.
*/
public float calculate()
{
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
for(int x = 0; x < SYNC_DIBIT_LENGTH; x += VECTOR_SPECIES.length())
{
accumulator = FloatVector.fromArray(VECTOR_SPECIES, SYMBOLS, x)
.fma(FloatVector.fromArray(VECTOR_SPECIES, mSymbols, mSymbolPointer + x), accumulator);
}
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,173 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
import io.github.dsheirer.dsp.symbol.Dibit;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.math3.analysis.interpolation.SplineInterpolator;
import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction;
/**
* Base implementation of NXDN sync detector.
*/
public abstract class NXDNSyncDetector
{
public static final long SYNC_PATTERN = 0xCDF59;
public static final long SYNC_MASK = 0xFFFFF;
public static final int SYNC_DIBIT_LENGTH = 10;
public static final Dibit[] DIBITS = toDibits(SYNC_PATTERN, SYNC_DIBIT_LENGTH);
public static final float[] SYMBOLS = toSymbols(DIBITS);
private static Map<Integer,ReferenceSyncWaveform> sReferenceWaveformMap = new TreeMap<>();
/**
* Sync pattern length in dibits.
*/
public int getSyncPatternDibitLength()
{
return SYNC_DIBIT_LENGTH;
}
/**
* Dibits array for the sync pattern
* @return dibits array.
*/
public Dibit[] getSyncDibits()
{
return DIBITS;
}
/**
* Symbols array for the sync pattern.
* @return array of (ideal) soft symbol values.
*/
public float[] getSyncSymbols()
{
return SYMBOLS;
}
/**
* Converts the sync pattern to dibits.
* @param pattern to convert
* @param dibitLength in dibits
* @return array of dibits for the sync pattern.
*/
protected static Dibit[] toDibits(long pattern, int dibitLength)
{
Dibit[] dibits = new Dibit[dibitLength];
long mask = 3;
int dibitValue;
for(int x = 0; x < dibitLength; x++)
{
dibitValue = (int)((pattern & mask) >> (2 * x));
dibits[dibitLength - x - 1] = switch(dibitValue)
{
case 0 -> Dibit.D00_PLUS_1;
case 1 -> Dibit.D01_PLUS_3;
case 2 -> Dibit.D10_MINUS_1;
default -> Dibit.D11_MINUS_3;
};
mask = mask << 2;
}
return dibits;
}
/**
* Utility method to convert a dibit array into an array of ideal soft symbol values.
* @param dibits for the sync pattern
* @return ideal soft symbol array.
*/
protected static float[] toSymbols(Dibit[] dibits)
{
float[] symbols = new float[dibits.length];
for(int x = 0; x < dibits.length; x++)
{
symbols[x] = dibits[x].getIdealPhase();
}
return symbols;
}
/**
* Creates a reference waveform representation of the sync pattern that can be used to correlate against a
* candidate detected sync waveform to discriminate between a valid and invalid sync detection. The generated
* waveform will be aligned where the final sample is time-aligned to the final symbol.
*
* @param samplesPerSymbol count
* @return a reference waveform and ideal correlation score.
*/
public ReferenceSyncWaveform getReferenceWaveform(double samplesPerSymbol)
{
int key = (int)(samplesPerSymbol * 1000.0);
if(sReferenceWaveformMap.containsKey(key))
{
return sReferenceWaveformMap.get(key);
}
float[] symbols = getSyncSymbols();
int sampleCount = (int)Math.ceil(samplesPerSymbol * (symbols.length - 1)) + 1;
float[] samples = new float[sampleCount];
double sampleInterval = 1.0 / samplesPerSymbol;
double[] x = new double[symbols.length + 2];
double[] y = new double[symbols.length + 2];
for(int i = -1; i < x.length - 1; i++)
{
x[i + 1] = i;
if(0 <= i && i < symbols.length)
{
y[i + 1] = symbols[i];
}
}
double start = symbols.length - 1;
int samplePointer = sampleCount - 1;
SplineInterpolator interpolator = new SplineInterpolator();
PolynomialSplineFunction function = interpolator.interpolate(x, y);
float accumulator = 0;
float sample;
float max = 0;
while(samplePointer >= 0)
{
sample = (float)function.value(start);
if(Math.abs(sample) > max)
{
max = Math.abs(sample);
}
samples[samplePointer--] = sample;
accumulator += sample * sample;
start -= sampleInterval;
}
ReferenceSyncWaveform referenceWaveform = new ReferenceSyncWaveform(samples, accumulator, max);
sReferenceWaveformMap.put(key, referenceWaveform);
return referenceWaveform;
}
}

View File

@@ -0,0 +1,58 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Factory for creating NXDN Soft Sync Detectors
*/
public class NXDNSyncDetectorFactory
{
private static final Logger mLog = LoggerFactory.getLogger(NXDNSyncDetectorFactory.class);
/**
* Creates the implementation using calibration data to select the optimal implementation from scalar and vector options.
* @return demodulator instance
*/
public static NXDNSoftSyncDetector getStandardDetector()
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.NXDN_SOFT_SYNC_DETECTOR);
switch(implementation)
{
case VECTOR_SIMD_64:
return new NXDNSoftSyncDetectorVector64();
case VECTOR_SIMD_128:
return new NXDNSoftSyncDetectorVector128();
case VECTOR_SIMD_256:
return new NXDNSoftSyncDetectorVector256();
case VECTOR_SIMD_512:
return new NXDNSoftSyncDetectorVector512();
case SCALAR:
default:
return new NXDNSoftSyncDetectorScalar();
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* *****************************************************************************
* 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.nxdn.layer1.sync;
/**
* Waveform representing the ideal sync pattern, resampled at the sample rate.
* @param samples in samples
* @param idealCorrelationScore as dot-product of the ideal multiplied by itself.
* @param maxSample maximum allowable sample value
*/
public record ReferenceSyncWaveform(float[] samples, float idealCorrelationScore, float maxSample){};

View File

@@ -0,0 +1,28 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.nxdn.layer2;
/**
* Channel direction indicator where inbound is SU to repeater and outbound is repeater to SU
*/
public enum Direction
{
INBOUND, OUTBOUND;
}

View File

@@ -0,0 +1,30 @@
/*
* *****************************************************************************
* 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.nxdn.layer2;
/**
* NXDN frame structure
*/
public enum Framing
{
SUPER,
SINGLE,
UNKNOWN;
}

View File

@@ -0,0 +1,94 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.nxdn.layer2;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* Base NXDN interleaver configuration.
*/
public class Interleaver
{
private int[] mInterleaveIndexes;
private int mWidth;
private int mDepth;
/**
* Constructs an instance
* @param width of the interleave
* @param depth of the interleave
*/
public Interleaver(int width, int depth)
{
mWidth = width;
mDepth = depth;
mInterleaveIndexes = new int[width * depth];
int pointer = 0;
for(int column = 0; column < width; column++)
{
for(int row = 0; row < depth; row++)
{
mInterleaveIndexes[pointer++] = (row * mWidth) + column;
}
}
}
/**
* Performs interleave of the original message
* @param message original
* @param offset into the output message where the interleaved data should be placed
* @return output message with interleaved payload positioned starting at the offset.
*/
public CorrectedBinaryMessage interleave(CorrectedBinaryMessage message, int offset)
{
CorrectedBinaryMessage interleaved = new CorrectedBinaryMessage(mInterleaveIndexes.length + offset);
for(int index = 0; index < mInterleaveIndexes.length; index++)
{
if(message.get(index + offset))
{
interleaved.set(offset + mInterleaveIndexes[index]);
}
}
return interleaved;
}
/**
* Performs deinterleave of the interleaved message
* @param interleaved containing the interleaved payload starting at the offset
* @param offset into the message to the start of the interleaved payload.
* @return deinterleaved message starting at index 0
*/
public CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage interleaved, int offset)
{
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(mInterleaveIndexes.length);
for(int index = 0; index < mInterleaveIndexes.length; index++)
{
deinterleaved.set(mInterleaveIndexes[index], interleaved.get(index + offset));
}
return deinterleaved;
}
}

View File

@@ -0,0 +1,365 @@
/*
* *****************************************************************************
* 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.nxdn.layer2;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Link Information Channel (LICH) enumeration identifies the channel configuration and direction and optional values.
*/
public enum LICH
{
//Trunked - Control
RCCH_INBOUND_SINGLE_CAC_SHORT(0x18, RFChannel.RCCH, Framing.SINGLE, Structure.CAC_SHORT, Direction.INBOUND),
RCCH_INBOUND_SINGLE_CAC_LONG(0x08, RFChannel.RCCH, Framing.SINGLE, Structure.CAC_LONG, Direction.INBOUND),
RCCH_INBOUND_SINGLE_UNKNOWN(-1, RFChannel.RCCH, Framing.UNKNOWN, Structure.UNKNOWN, Direction.INBOUND),
RCCH_OUTBOUND_SINGLE_CAC_NORMAL(0x01, RFChannel.RCCH, Framing.SINGLE, Structure.CAC_NORMAL, Direction.OUTBOUND),
RCCH_OUTBOUND_SINGLE_CAC_IDLE(0x03, RFChannel.RCCH, Framing.SINGLE, Structure.CAC_IDLE, Direction.OUTBOUND),
RCCH_OUTBOUND_SINGLE_CAC_COMMON(0x05, RFChannel.RCCH, Framing.SINGLE, Structure.CAC_COMMON, Direction.OUTBOUND),
RCCH_OUTBOUND_SINGLE_UNKNOWN(-1, RFChannel.RCCH, Framing.SINGLE, Structure.CAC_NORMAL, Direction.OUTBOUND),
//Trunked - Traffic
RTCH_INBOUND_SUPER_VOICE_VOICE(0x36, RFChannel.RTCH, Framing.SUPER, Structure.VOICE_VOICE, Direction.INBOUND),
RTCH_INBOUND_SUPER_VOICE_FACCH1(0x34, RFChannel.RTCH, Framing.SUPER, Structure.VOICE_FACCH1, Direction.INBOUND),
RTCH_INBOUND_SUPER_FACCH1_VOICE(0x32, RFChannel.RTCH, Framing.SUPER, Structure.FACCH1_VOICE, Direction.INBOUND),
RTCH_INBOUND_SUPER_FACCH1_FACCH1(0x30, RFChannel.RTCH, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.INBOUND),
RTCH_INBOUND_SINGLE_FACCH1_FACCH1(0x20, RFChannel.RTCH, Framing.SINGLE, Structure.FACCH1_FACCH1, Direction.INBOUND),
RTCH_INBOUND_SUPER_IDLE(0x38, RFChannel.RTCH, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.INBOUND),
RTCH_INBOUND_SINGLE_UDCH(0x2E, RFChannel.RTCH, Framing.SINGLE, Structure.UDCH, Direction.INBOUND),
RTCH_INBOUND_SINGLE_FACCH2(0x28, RFChannel.RTCH, Framing.SINGLE, Structure.FACCH2, Direction.INBOUND),
RTCH_INBOUND_UNKNOWN(-1, RFChannel.RTCH, Framing.UNKNOWN, Structure.UNKNOWN, Direction.INBOUND),
RTCH_OUTBOUND_SUPER_VOICE_VOICE(0x37, RFChannel.RTCH, Framing.SUPER, Structure.VOICE_VOICE, Direction.OUTBOUND),
RTCH_OUTBOUND_SUPER_VOICE_FACCH1(0x35, RFChannel.RTCH, Framing.SUPER, Structure.VOICE_FACCH1, Direction.OUTBOUND),
RTCH_OUTBOUND_SUPER_FACCH1_VOICE(0x33, RFChannel.RTCH, Framing.SUPER, Structure.FACCH1_VOICE, Direction.OUTBOUND),
RTCH_OUTBOUND_SUPER_FACCH1_FACCH1(0x31, RFChannel.RTCH, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RTCH_OUTBOUND_SINGLE_FACCH1_FACCH1(0x21, RFChannel.RTCH, Framing.SINGLE, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RTCH_OUTBOUND_SUPER_IDLE(0x39, RFChannel.RTCH, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RTCH_OUTBOUND_SINGLE_UDCH(0x2F, RFChannel.RTCH, Framing.SINGLE, Structure.UDCH, Direction.OUTBOUND),
RTCH_OUTBOUND_SINGLE_FACCH2(0x29, RFChannel.RTCH, Framing.SINGLE, Structure.FACCH2, Direction.OUTBOUND),
RTCH_OUTBOUND_UNKNOWN(-1, RFChannel.RTCH, Framing.UNKNOWN, Structure.UNKNOWN, Direction.OUTBOUND),
//Conventional repeater or direct SU-to-SU
RDCH_INBOUND_SUPER_VOICE_VOICE(0x56, RFChannel.RDCH, Framing.SUPER, Structure.VOICE_VOICE, Direction.INBOUND),
RDCH_INBOUND_SUPER_VOICE_FACCH1(0x54, RFChannel.RDCH, Framing.SUPER, Structure.VOICE_FACCH1, Direction.INBOUND),
RDCH_INBOUND_SUPER_FACCH1_VOICE(0x52, RFChannel.RDCH, Framing.SUPER, Structure.FACCH1_VOICE, Direction.INBOUND),
RDCH_INBOUND_SUPER_FACCH1_FACCH1(0x50, RFChannel.RDCH, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.INBOUND),
RDCH_INBOUND_SINGLE_FACCH1_FACCH1(0x40, RFChannel.RDCH, Framing.SINGLE, Structure.FACCH1_FACCH1, Direction.INBOUND),
RDCH_INBOUND_SUPER_IDLE(0x58, RFChannel.RDCH, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.INBOUND),
RDCH_INBOUND_SINGLE_UDCH(0x4E, RFChannel.RDCH, Framing.SINGLE, Structure.UDCH, Direction.INBOUND),
RDCH_INBOUND_SINGLE_FACCH2(0x48, RFChannel.RDCH, Framing.SINGLE, Structure.FACCH2, Direction.INBOUND),
RDCH_INBOUND_UNKNOWN(-1, RFChannel.RDCH, Framing.UNKNOWN, Structure.UNKNOWN, Direction.INBOUND),
RDCH_OUTBOUND_SUPER_VOICE_VOICE(0x57, RFChannel.RDCH, Framing.SUPER, Structure.VOICE_VOICE, Direction.OUTBOUND),
RDCH_OUTBOUND_SUPER_VOICE_FACCH1(0x55, RFChannel.RDCH, Framing.SUPER, Structure.VOICE_FACCH1, Direction.OUTBOUND),
RDCH_OUTBOUND_SUPER_FACCH1_VOICE(0x53, RFChannel.RDCH, Framing.SUPER, Structure.FACCH1_VOICE, Direction.OUTBOUND),
RDCH_OUTBOUND_SUPER_FACCH1_FACCH1(0x51, RFChannel.RDCH, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RDCH_OUTBOUND_SINGLE_FACCH1_FACCH1(0x41, RFChannel.RDCH, Framing.SINGLE, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RDCH_OUTBOUND_SUPER_IDLE(0x59, RFChannel.RDCH, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RDCH_OUTBOUND_SINGLE_UDCH(0x4F, RFChannel.RDCH, Framing.SINGLE, Structure.UDCH, Direction.OUTBOUND),
RDCH_OUTBOUND_SINGLE_FACCH2(0x49, RFChannel.RDCH, Framing.SINGLE, Structure.FACCH2, Direction.OUTBOUND),
RDCH_OUTBOUND_UNKNOWN(-1, RFChannel.RDCH, Framing.UNKNOWN, Structure.UNKNOWN, Direction.OUTBOUND),
//Trunked Composite Control
RTCH_C_OUTBOUND_SUPER_VOICE_VOICE(0x77, RFChannel.RTCHC, Framing.SUPER, Structure.VOICE_VOICE, Direction.OUTBOUND),
RTCH_C_OUTBOUND_SUPER_VOICE_FACCH1(0x75, RFChannel.RTCHC, Framing.SUPER, Structure.VOICE_FACCH1, Direction.OUTBOUND),
RTCH_C_OUTBOUND_SUPER_FACCH1_VOICE(0x73, RFChannel.RTCHC, Framing.SUPER, Structure.FACCH1_VOICE, Direction.OUTBOUND),
RTCH_C_OUTBOUND_SUPER_FACCH1_FACCH1(0x71, RFChannel.RTCHC, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RTCH_C_OUTBOUND_SINGLE_FACCH1_FACCH1(0x61, RFChannel.RTCHC, Framing.SINGLE, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RTCH_C_OUTBOUND_SUPER_IDLE(0x79, RFChannel.RTCHC, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RTCH_C_OUTBOUND_SINGLE_UDCH(0x6F, RFChannel.RTCHC, Framing.SINGLE, Structure.UDCH, Direction.OUTBOUND),
RTCH_C_OUTBOUND_SINGLE_FACCH2(0x69, RFChannel.RTCHC, Framing.SINGLE, Structure.FACCH2, Direction.OUTBOUND),
RTCH_C_OUTBOUND_UNKNOWN(-1, RFChannel.RTCHC, Framing.UNKNOWN, Structure.UNKNOWN, Direction.OUTBOUND),
//Type-D Trunking (RTCH2)
RTCH_2_INBOUND_SUPER_VOICE_VOICE(0x76, RFChannel.RTCH2, Framing.SUPER, Structure.VOICE_VOICE, Direction.INBOUND),
RTCH_2_INBOUND_SUPER_VOICE_FACCH1(0x74, RFChannel.RTCH2, Framing.SUPER, Structure.VOICE_FACCH1, Direction.INBOUND),
RTCH_2_INBOUND_SUPER_FACCH1_VOICE(0x72, RFChannel.RTCH2, Framing.SUPER, Structure.FACCH1_VOICE, Direction.INBOUND),
RTCH_2_INBOUND_SUPER_FACCH1_FACCH1(0x70, RFChannel.RTCH2, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.INBOUND),
RTCH_2_INBOUND_SINGLE_UDCH2(0x6E, RFChannel.RTCH2, Framing.SINGLE, Structure.UDCH2, Direction.INBOUND),
RTCH_2_INBOUND_SINGLE_FACCH3(0x68, RFChannel.RTCH2, Framing.SINGLE, Structure.FACCH3, Direction.INBOUND),
RTCH_2_INBOUND_SINGLE_FACCH1_GUARD(0x62, RFChannel.RTCH2, Framing.SINGLE, Structure.FACCH1_GUARD, Direction.INBOUND),
RTCH_2_INBOUND_SINGLE_FACCH1_FACCH1(0x60, RFChannel.RTCH2, Framing.SINGLE, Structure.FACCH1_FACCH1, Direction.INBOUND),
RTCH_2_INBOUND_UNKNOWN(-1, RFChannel.RTCH2, Framing.UNKNOWN, Structure.UNKNOWN, Direction.INBOUND),
RTCH_2_OUTBOUND_SUPER_VOICE_VOICE(0x77, RFChannel.RTCH2, Framing.SUPER, Structure.VOICE_VOICE, Direction.OUTBOUND),
RTCH_2_OUTBOUND_SUPER_VOICE_FACCH1(0x75, RFChannel.RTCH2, Framing.SUPER, Structure.VOICE_FACCH1, Direction.OUTBOUND),
RTCH_2_OUTBOUND_SUPER_FACCH1_VOICE(0x73, RFChannel.RTCH2, Framing.SUPER, Structure.FACCH1_VOICE, Direction.OUTBOUND),
RTCH_2_OUTBOUND_SUPER_FACCH1_FACCH1(0x71, RFChannel.RTCH2, Framing.SUPER, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RTCH_2_OUTBOUND_SINGLE_UDCH2(0x6F, RFChannel.RTCH2, Framing.SINGLE, Structure.UDCH2, Direction.OUTBOUND),
RTCH_2_OUTBOUND_SINGLE_FACCH3(0x69, RFChannel.RTCH2, Framing.SINGLE, Structure.FACCH3, Direction.OUTBOUND),
RTCH_2_OUTBOUND_SINGLE_FACCH1_GUARD(0x63, RFChannel.RTCH2, Framing.SINGLE, Structure.FACCH1_GUARD, Direction.OUTBOUND),
RTCH_2_OUTBOUND_SINGLE_FACCH1_FACCH1(0x61, RFChannel.RTCH2, Framing.SINGLE, Structure.FACCH1_FACCH1, Direction.OUTBOUND),
RTCH_2_OUTBOUND_UNKNOWN(-1, RFChannel.RTCH2, Framing.UNKNOWN, Structure.UNKNOWN, Direction.OUTBOUND),
UNKNOWN(-1, RFChannel.UNKNOWN, Framing.UNKNOWN, Structure.UNKNOWN, Direction.OUTBOUND);
private int mValue;
private RFChannel mRFChannel;
private Framing mFraming;
private Structure mStructure;
private Direction mDirection;
private static final Map<Integer, LICH> LOOKUP_MAP_STANDARD = new TreeMap<>();
private static final Map<Integer, LICH> LOOKUP_MAP_TYPE_D = new TreeMap<>();
static
{
for(LICH lich : LICH.values())
{
if(lich.getRFChannel() == RFChannel.RTCH2)
{
LOOKUP_MAP_TYPE_D.put(lich.getValue(), lich);
}
else
{
LOOKUP_MAP_STANDARD.put(lich.getValue(), lich);
}
}
}
/**
* Constructs an instance
* @param value as transmitted
* @param rfChannel channel type
* @param framing structure
* @param structure options for the channel
* @param direction inbound or outbound
*/
LICH(int value, RFChannel rfChannel, Framing framing, Structure structure, Direction direction)
{
mValue = value;
mRFChannel = rfChannel;
mFraming = framing;
mStructure = structure;
mDirection = direction;
}
/**
* Indicates if this frame contains a FACCH1 message in the first half of the frame
*/
public boolean isFACCH1First()
{
return getStructure().isFACCH1First();
}
/**
* Indicates if this frame contains a FACCH1 message in the second half of the frame
*/
public boolean isFACCH1Second()
{
return getStructure().isFACCH1Second();
}
/**
* Indicates if this frame contains a data fragment (FACCH2, FACCH3, UDCH or UDCH2)
*/
public boolean isData()
{
return getStructure().isData();
}
/**
* Indicates if the frame has audio frames in either or both of the payloads.
*/
public boolean hasAudio()
{
return getStructure().isVoice();
}
/**
* Indicates if this frame has voice data in the first half of the frame.
*/
public boolean isVoiceFirst()
{
return getStructure() == Structure.VOICE_VOICE || getStructure() == Structure.VOICE_FACCH1;
}
/**
* Indicates if this frame has voice data in the second half of the frame.
*/
public boolean isVoiceSecond()
{
return getStructure() == Structure.VOICE_VOICE || getStructure() == Structure.FACCH1_VOICE;
}
/**
* Indicates if the RDCH or RTCH frame has a SACCH field.
*/
public boolean hasSACCH()
{
return getRFChannel() != RFChannel.RTCH2 && (getStructure().isFACCH1() || getStructure().isVoice());
}
/**
* Indicates if this is a Type-D frame with a SCCH segment.
*/
public boolean hasSCCH()
{
return getRFChannel() == RFChannel.RTCH2;
}
/**
* Indicates if this is a (4x) super frame (true) or single frame (false)
*/
public boolean isSuperFrame()
{
return getFraming() == Framing.SUPER;
}
/**
* Lookup the LICH from the value.
* @param value to lookup
* @param isTypeD to differentiate between Type-D versus Standard systems.
* @return matching entry or UNKNOWN if the value doesn't match any LICH values.
*/
public static LICH fromValue(int value, boolean isTypeD)
{
if(isTypeD && LOOKUP_MAP_TYPE_D.containsKey(value))
{
return LOOKUP_MAP_TYPE_D.get(value);
}
else if(!isTypeD && LOOKUP_MAP_STANDARD.containsKey(value))
{
return LOOKUP_MAP_STANDARD.get(value);
}
return UNKNOWN;
}
/**
* Look up the matching entry from the transmitted value. The channel and direction parameters should be from
* a tracker for the majority of observed values for the channel since the channel type and direction normally
* won't change within the transmission. These two additional values are for instance when the LICH has bit
* errors that prevent a clean match up.
* @param value transmitted
* @param channel as tracked
* @param direction as tracked
* @return matching entry or UNKNOWN
*/
public static LICH fromValue(int value, RFChannel channel, Direction direction)
{
if(channel == RFChannel.RTCH2)
{
if(LOOKUP_MAP_TYPE_D.containsKey(value))
{
return LOOKUP_MAP_TYPE_D.get(value);
}
}
else
{
if(LOOKUP_MAP_STANDARD.containsKey(value))
{
return LOOKUP_MAP_STANDARD.get(value);
}
}
switch(channel)
{
case RCCH:
return direction == Direction.OUTBOUND ? RCCH_OUTBOUND_SINGLE_UNKNOWN : RCCH_INBOUND_SINGLE_UNKNOWN;
case RTCH:
return direction == Direction.OUTBOUND ? RTCH_OUTBOUND_UNKNOWN : RTCH_INBOUND_UNKNOWN;
case RTCH2:
return direction == Direction.OUTBOUND ? RTCH_2_OUTBOUND_UNKNOWN : RTCH_2_INBOUND_UNKNOWN;
case RDCH:
return direction == Direction.OUTBOUND ? RDCH_OUTBOUND_UNKNOWN : RDCH_INBOUND_UNKNOWN;
case RTCHC:
return RTCH_C_OUTBOUND_UNKNOWN;
default:
return UNKNOWN;
}
}
/**
* Transmitted value.
*/
public int getValue()
{
return mValue;
}
/**
* RF channel type
*/
public RFChannel getRFChannel()
{
return mRFChannel;
}
/**
* Framing
*/
public Framing getFraming()
{
return mFraming;
}
/**
* Options for the channel
*/
public Structure getStructure()
{
return mStructure;
}
/**
* Channel direction
*/
public Direction getDirection()
{
return mDirection;
}
/**
* Indicates if the channel direction is outbound (true) or inbound (false).
* @return true if outbound
*/
public boolean isOutbound()
{
return getDirection().equals(Direction.OUTBOUND);
}
/**
* Indicates if the channel is carrying a long CAC (true) or short CAC (false)
* @return true if long CAC
*/
public boolean isLongCAC()
{
return this.equals(LICH.RCCH_INBOUND_SINGLE_CAC_LONG);
}
static void main()
{
List<LICH> liches = new ArrayList<>(Arrays.stream(LICH.values()).toList());
liches.sort(Comparator.comparingInt(LICH::getValue));
for(LICH setting : liches)
{
System.out.println(Integer.toHexString(setting.getValue()).toUpperCase() + ": " + setting);
}
}
}

View File

@@ -0,0 +1,219 @@
/*
* *****************************************************************************
* 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.nxdn.layer2;
import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tracks the observed channel type and direction occurrence counts.
*/
public class LICHTracker
{
private static final Logger LOG = LoggerFactory.getLogger(LICHTracker.class);
private static final int BUFFER_SIZE = 5;
private final RFChannel[] mRFChannelBuffer = new RFChannel[BUFFER_SIZE];
private final TransmissionMode mTransmissionMode;
private int mBufferPointer = 0;
private RFChannel mTrackedChannel = RFChannel.RCCH;
private Direction mTrackedDirection = Direction.OUTBOUND;
private List<Tracker> mTrackers = new ArrayList<>();
private Map<RFChannel,Tracker> mTrackerMap = new HashMap<>();
private int mCountOutbound;
private int mCountInbound;
/**
* Constructs an instance
*/
public LICHTracker(TransmissionMode transmissionMode)
{
mTransmissionMode = transmissionMode;
for(RFChannel channel : RFChannel.values())
{
Tracker tracker = new Tracker(channel);
mTrackers.add(tracker);
mTrackerMap.put(channel, tracker);
}
reset();
}
/**
* Indicates if the transmission mode is Type D
*/
public boolean isTypeD()
{
return mTransmissionMode.isTypeD();
}
/**
* Resets the channel tracked values.
*/
public void reset()
{
Arrays.fill(mRFChannelBuffer, RFChannel.UNKNOWN);
mTrackedChannel = RFChannel.UNKNOWN;
mTrackedDirection = Direction.OUTBOUND;
mTrackers.forEach(Tracker::reset);
}
/**
* RF channel with the highest observed counts.
*/
public RFChannel getChannel()
{
return mTrackedChannel;
}
/**
* Channel direction with the highest observed counts
*/
public Direction getDirection()
{
return mTrackedDirection;
}
/**
* Tracks the decoded LICH
* @param lich to track
*/
public void track(LICH lich)
{
if(lich == null)
{
return;
}
try
{
//Update RF channel count and tracked RF channel as necessary.
mTrackerMap.get(mRFChannelBuffer[mBufferPointer]).decrement();
mRFChannelBuffer[mBufferPointer++] = lich.getRFChannel();
mBufferPointer %= BUFFER_SIZE;
mTrackerMap.get(lich.getRFChannel()).increment();
if(mTrackedChannel != lich.getRFChannel())
{
Collections.sort(mTrackers);
mTrackedChannel = mTrackers.getLast().getRFChannel();
}
if(lich.getDirection() == Direction.OUTBOUND)
{
mCountOutbound = Math.min(++mCountOutbound, BUFFER_SIZE);
mCountInbound = Math.max(--mCountInbound, 0);
}
else
{
mCountInbound = Math.min(++mCountInbound, BUFFER_SIZE);
mCountOutbound = Math.max(--mCountOutbound, 0);
}
if(lich.getDirection() != mTrackedDirection)
{
mTrackedDirection = (mCountOutbound > mCountInbound) ? Direction.OUTBOUND : Direction.INBOUND;
}
}
catch(Exception e)
{
LOG.error("Error while updating LICH tracker for LICH: {}", lich);
}
}
/**
* Tracks the occurrence count of an RF channel
*/
static class Tracker implements Comparable<Tracker>
{
private final RFChannel mChannel;
private int mCount;
/**
* Constructs an instance
*
* @param channel to track occurrence counts
*/
public Tracker(RFChannel channel)
{
mChannel = channel;
}
/**
* Tracked channel
*/
public RFChannel getRFChannel()
{
return mChannel;
}
/**
* Count of occurrences
*/
public int getCount()
{
return mCount;
}
/**
* Increments the counter
*/
public void increment()
{
mCount = Math.min(++mCount, BUFFER_SIZE);
}
/**
* Decrements the counter
*/
public void decrement()
{
mCount = Math.max(--mCount, 0);
}
@Override
public int compareTo(Tracker o)
{
return Integer.compare(mCount, o.mCount);
}
/**
* Resets the counter to zero
*/
public void reset()
{
mCount = 0;
}
@Override
public String toString()
{
return "COUNT: " + mCount + " " + mChannel.toString();
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* *****************************************************************************
* 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.nxdn.layer2;
/**
* Layer 2 RF Channel Type enumeration
*/
public enum RFChannel
{
RCCH("CONTROL CHANNEL"),
RTCH("TRAFFIC CHANNEL"),
RTCH2("TRAFFIC CHANNEL 2"),
RDCH("DATA CHANNEL"),
RTCHC("COMPOSITE CONTROL CHANNEL"),
UNKNOWN("UNKNOWN");
private final String mLabel;
RFChannel(String label)
{
mLabel = label;
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -0,0 +1,99 @@
/*
* *****************************************************************************
* 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.nxdn.layer2;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageFactory;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
/**
* Assembles SACCH fragments into a Layer 3 message
*/
public class SACCHAssembler
{
private SACCHFragment mFragment1;
private SACCHFragment mFragment2;
private SACCHFragment mFragment3;
private SACCHFragment mFragment4;
/**
* Processes the SACCH fragment sequence to reassemble the embedded layer 3 message
* @param fragment to process
* @return assembled layer 3 message or null.
*/
public NXDNLayer3Message process(SACCHFragment fragment)
{
if(fragment.isValid())
{
switch(fragment.getStructure())
{
case SACCH_1_OF_4:
mFragment1 = fragment;
break;
case SACCH_2_OF_4:
mFragment2 = fragment;
break;
case SACCH_3_OF_4:
mFragment3 = fragment;
break;
case SACCH_4_OF_4_LAST_OR_SINGLE:
mFragment4 = fragment;
return assemble();
}
}
return null;
}
/**
* Assembles the fragments into a complete message
* @return
*/
private NXDNLayer3Message assemble()
{
NXDNLayer3Message layer3 = null;
if(mFragment1 != null && mFragment2 != null && mFragment3 != null && mFragment4 != null && mFragment4.isSuperFrame())
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(72);
message.load(0, mFragment1.getFragment());
message.load(18, mFragment2.getFragment());
message.load(36, mFragment3.getFragment());
message.load(54, mFragment4.getFragment());
NXDNMessageType type = NXDNMessageType.getTraffic(message, mFragment4.getLICH());
layer3 = NXDNMessageFactory.get(type, message, mFragment4.getTimestamp(), mFragment4.getRAN(), mFragment4.getLICH());
}
else if(mFragment4 != null && !mFragment4.isSuperFrame())
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(18);
message.load(0, mFragment4.getFragment());
NXDNMessageType type = NXDNMessageType.getTraffic(message, mFragment4.getLICH());
layer3 = NXDNMessageFactory.get(type, message, mFragment4.getTimestamp(), mFragment4.getRAN(), mFragment4.getLICH());
}
mFragment1 = null;
mFragment2 = null;
mFragment3 = null;
mFragment4 = null;
return layer3;
}
}

View File

@@ -0,0 +1,96 @@
/*
* *****************************************************************************
* 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.nxdn.layer2;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.NXDNMessage;
import io.github.dsheirer.module.decode.nxdn.layer3.type.Structure;
import java.util.List;
/**
* SACCH message fragment
*/
public class SACCHFragment extends NXDNMessage
{
private static final IntField STRUCTURE = IntField.length2(0);
private static final IntField RADIO_ACCESS_NUMBER = IntField.length6(2);
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
*/
public SACCHFragment(CorrectedBinaryMessage message, long timestamp, LICH lich)
{
super(message, timestamp, message.getInt(RADIO_ACCESS_NUMBER), lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append(getStructure());
sb.append(" MSG:").append(getMessage().toHexString());
sb.append(" LICH:").append(getLICH());
return sb.toString();
}
/**
* SACCH message fragment
*/
public CorrectedBinaryMessage getFragment()
{
return getMessage().getSubMessage(8, 26);
}
/**
* Indicates if the SACCH is standalone (false) or a superframe fragment (true).
*/
public boolean isSuperFrame()
{
return getLICH().isSuperFrame();
}
/**
* Structure of the SACCH message
* @return
*/
public Structure getStructure()
{
return Structure.fromTrafficValue(getMessage().getInt(STRUCTURE));
}
/**
* Radio Access Network (RAN) code
*/
public int getRAN()
{
return getMessage().getInt(RADIO_ACCESS_NUMBER);
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -0,0 +1,76 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.nxdn.layer2;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import java.util.Arrays;
/**
* NXDN Scrambler.
*
* Note: The first four bits of the scramble sequence are all zeros meaning that the first two symbols of the LICH do
* not get scrambled. This aligns with the ICD hinting that you could use the first two symbols of the LICH that
* represent the RF channel type as part of the sync pattern for sync detection, once you concretely identify the
* RF channel type.
*/
public class Scrambler
{
private static final int INITIAL_FILL = 0x0E4;
/**
* Generates the scrambler sequence for the requested length.
* @param length of scramble sequence
* @return generated scramble sequence.
*/
public static BinaryMessage generate(int length)
{
BinaryMessage sequence = new BinaryMessage(length);
int register = INITIAL_FILL;
boolean output;
int x = 0;
while(x < length)
{
output = (register & 1) == 1;
if(output)
{
sequence.set(x);
}
x +=2;
//XOR output with 5-4 tap for the feedback bit
if(output ^ ((register & 0x10) == 0x10))
{
//Set the high order feedback bit before we right shift it into place.
register |= 0x200;
}
//Unsigned right shift by 1 with no integer shifting wrap-around
register >>>= 1;
}
return sequence;
}
}

View File

@@ -0,0 +1,100 @@
/*
* *****************************************************************************
* 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.nxdn.layer2;
import java.util.EnumSet;
/**
* LICH structure enumeration
*/
public enum Structure
{
BEACON,
CAC_NORMAL,
CAC_IDLE,
CAC_COMMON,
CAC_LONG,
CAC_SHORT,
VOICE_VOICE,
FACCH1_VOICE,
VOICE_FACCH1,
FACCH1_FACCH1,
FACCH1_GUARD,
FACCH2,
FACCH3,
UDCH,
UDCH2,
UNKNOWN;
/**
* Data carrying structures
*/
private static final EnumSet<Structure> DATA = EnumSet.of(FACCH2, FACCH3, UDCH, UDCH2);
/**
* FACCH1 in first fragment
*/
private static final EnumSet<Structure> FACCH1_FIRST = EnumSet.of(FACCH1_FACCH1, FACCH1_GUARD, FACCH1_VOICE);
/**
* FACCH1 in second fragment
*/
private static final EnumSet<Structure> FACCH1_SECOND = EnumSet.of(FACCH1_FACCH1, VOICE_FACCH1);
/**
* Indicates if the frame contains audio frames
*/
public boolean isVoice()
{
return this.equals(VOICE_VOICE) || this.equals(FACCH1_VOICE) || this.equals(VOICE_FACCH1);
}
/**
* Indicates if this structure contains a FACCH1 frame
*/
public boolean isFACCH1()
{
return FACCH1_FIRST.contains(this) || FACCH1_SECOND.contains(this);
}
/**
* Indicates if a FACCH1 is in the first half of the frame
*/
public boolean isFACCH1First()
{
return FACCH1_FIRST.contains(this);
}
/**
* Indicates if a FACCH1 is in the second half of the frame
*/
public boolean isFACCH1Second()
{
return FACCH1_SECOND.contains(this);
}
/**
* Indicates if this is a data structure
*/
public boolean isData()
{
return DATA.contains(this);
}
}

View File

@@ -0,0 +1,80 @@
/*
* *****************************************************************************
* 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.nxdn.layer3;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.module.decode.nxdn.NXDNMessage;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
/**
* Base NXDN layer 3 message implementation
*/
public abstract class NXDNLayer3Message extends NXDNMessage
{
protected static int FLAG_1_INDEX = 0;
protected static int FLAG_2_INDEX = 1;
protected static final IntField MESSAGE_TYPE = IntField.length6(OCTET_0 + 2);
protected static final IntField IDENTIFIER_OCTET_3 = IntField.length16(OCTET_3);
protected static final IntField IDENTIFIER_OCTET_5 = IntField.length16(OCTET_5);
private final NXDNMessageType mType;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
*/
public NXDNLayer3Message(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, ran, lich);
mType = type;
}
/**
* Identifies the message type value from the binary message.
*
* @param message with message type value.
* @return value.
*/
public static int getTypeValue(CorrectedBinaryMessage message)
{
return message.getInt(MESSAGE_TYPE);
}
/**
* Message type
*
* @return message type
*/
public NXDNMessageType getMessageType()
{
return mType;
}
/**
* Indicates if this is a Type-D message type
*/
public boolean isTypeD()
{
return getMessageType().isTypeD();
}
}

View File

@@ -0,0 +1,422 @@
/*
* *****************************************************************************
* 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.nxdn.layer3;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.AdjacentSiteInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.AdjacentSiteInformationTypeD;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.DigitalStationIDInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.FailureStatusInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.Idle;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ServiceInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ServiceInformationTypeD;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.call.CallConnectionResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAcknowledge;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignment;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignmentDuplicateControl;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallAssignmentDuplicateTraffic;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallBlock;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallHeader;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallReceptionRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallReceptionResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DataCallResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.Disconnect;
import io.github.dsheirer.module.decode.nxdn.layer3.call.DisconnectRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.HeaderDelay;
import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlRequestWithESN;
import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.RemoteControlResponseWithESN;
import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallBlock;
import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallRequestHeader;
import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataCallResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.ShortDataInitializationVector;
import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusInquiryRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusInquiryResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.StatusResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.TransmissionRelease;
import io.github.dsheirer.module.decode.nxdn.layer3.call.TransmissionReleaseExtension;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCall;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignment;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateControl;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallAssignmentDuplicateTraffic;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallConnectionRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallConnectionResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallInitializationVector;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallReceptionRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallReceptionResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.call.VoiceCallResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.data.GPS;
import io.github.dsheirer.module.decode.nxdn.layer3.data.NXDNPacketMessage;
import io.github.dsheirer.module.decode.nxdn.layer3.data.PacketSequence;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryRequestMultiSystem;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationInquiryResponseMultiSystem;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationParameterInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.AuthenticationResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.DataWriteAcknowledge;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.DataWriteBlock;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.DataWriteHeader;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.GroupRegistrationResponseTypeD;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationClearRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationClearResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationCommand;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationRequest;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationResponse;
import io.github.dsheirer.module.decode.nxdn.layer3.mobility.RegistrationResponseTypeD;
import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.ProprietaryForm;
import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.TalkerAlias;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallCompleteDestinationInfo4;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressDestinationInfo2;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressDestinationInfo4;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInProgressSourceID;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.CallInfo;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.InitializationVectorPart1;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.InitializationVectorPart2;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.RepeaterFree;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.RepeaterHaltCWID;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.RepeaterIdle;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.SiteID;
/**
* Factory for creating NXDN messages
*/
public class NXDNMessageFactory
{
/**
* Creates an NXDN message parser for the packet sequence.
* @param packetSequence to wrap in a message parser
* @return message parser or null
*/
public static NXDNPacketMessage get(PacketSequence packetSequence)
{
if(GPS.isGPS(packetSequence))
{
return new GPS(packetSequence);
}
return null;
}
/**
* Creates an NXDN message parser for a message on the outbound control channel (CC)
* @param message content
* @param timestamp of the message
* @param ran value
* @param lich info
* @return message parser
*/
public static NXDNLayer3Message get(NXDNMessageType type, CorrectedBinaryMessage message, long timestamp, int ran, LICH lich)
{
switch (type)
{
case CONTROL_IN_01_CC_VOICE_CALL_REQUEST:
case TYPE_D_IN_00_CC_CALL_REQUEST: //Plain call request is handled as voice call request
case TYPE_D_IN_04_CC_VOICE_CALL_REQUEST:
return new VoiceCallRequest(message, timestamp, type, ran, lich);
case CONTROL_OUT_01_CC_VOICE_CALL_RESPONSE:
case TYPE_D_OUT_00_CC_CALL_RESPONSE:
return new VoiceCallResponse(message, timestamp, type, ran, lich);
case TRAFFIC_IN_01_CC_VOICE_CALL:
case TRAFFIC_OUT_01_CC_VOICE_CALL:
case TYPE_D_IN_01_CC_VOICE_CALL:
case TYPE_D_OUT_01_CC_VOICE_CALL:
return new VoiceCall(message, timestamp, type, ran, lich);
case CONTROL_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST:
case TRAFFIC_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST:
return new VoiceCallReceptionRequest(message, timestamp, type, ran, lich);
case CONTROL_IN_02_CC_VOICE_CALL_RECEPTION_RESPONSE:
case TRAFFIC_IN_02_CC_VOICE_CALL_RECEPTION_RESPONSE:
return new VoiceCallReceptionResponse(message, timestamp, type, ran, lich);
case CONTROL_IN_03_CC_VOICE_CALL_CONNECTION_REQUEST:
return new VoiceCallConnectionRequest(message, timestamp, type, ran, lich);
case CONTROL_OUT_03_CC_VOICE_CALL_CONNECTION_RESPONSE:
return new VoiceCallConnectionResponse(message, timestamp, type, ran, lich);
case TRAFFIC_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TRAFFIC_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TYPE_D_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
case TYPE_D_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR:
return new VoiceCallInitializationVector(message, timestamp, type, ran, lich);
case CONTROL_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
case TRAFFIC_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
case TYPE_D_OUT_04_CC_VOICE_CALL_ASSIGNMENT:
return new VoiceCallAssignment(message, timestamp, type, ran, lich);
case CONTROL_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE:
return new VoiceCallAssignmentDuplicateControl(message, timestamp, type, ran, lich);
case TRAFFIC_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE:
return new VoiceCallAssignmentDuplicateTraffic(message, timestamp, type, ran, lich);
case TYPE_D_OUT_06_CC_CALL_CONNECTION_RESPONSE:
return new CallConnectionResponse(message, timestamp, type, ran, lich);
case TRAFFIC_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION:
case TYPE_D_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION:
return new TransmissionReleaseExtension(message, timestamp, type, ran, lich);
case TRAFFIC_IN_08_CC_TRANSMISSION_RELEASE:
case TRAFFIC_OUT_08_CC_TRANSMISSION_RELEASE:
case TYPE_D_IN_08_CC_TRANSMISSION_RELEASE:
case TYPE_D_OUT_08_CC_TRANSMISSION_RELEASE:
return new TransmissionRelease(message, timestamp, type, ran, lich);
case CONTROL_IN_09_CC_DATA_CALL_REQUEST:
return new DataCallRequest(message, timestamp, type, ran, lich);
case CONTROL_OUT_09_CC_DATA_CALL_RESPONSE:
return new DataCallResponse(message, timestamp, type, ran, lich);
case TRAFFIC_IN_09_CC_DATA_CALL_HEADER:
case TRAFFIC_OUT_09_CC_DATA_CALL_HEADER:
case TYPE_D_IN_09_CC_DATA_CALL_HEADER:
case TYPE_D_OUT_09_CC_DATA_CALL_HEADER:
return new DataCallHeader(message, timestamp, type, ran, lich);
case CONTROL_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST:
case TRAFFIC_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST:
return new DataCallReceptionRequest(message, timestamp, type, ran, lich);
case CONTROL_IN_10_CC_DATA_CALL_RECEPTION_RESPONSE:
return new DataCallReceptionResponse(message, timestamp, type, ran, lich);
case TRAFFIC_IN_11_CC_DATA_CALL_BLOCK:
case TRAFFIC_OUT_11_CC_DATA_CALL_BLOCK:
case TYPE_D_IN_11_CC_DATA_CALL_BLOCK:
case TYPE_D_OUT_11_CC_DATA_CALL_BLOCK:
return new DataCallBlock(message, timestamp, type, ran, lich);
case TRAFFIC_IN_12_CC_DATA_CALL_ACKNOWLEDGE:
case TRAFFIC_OUT_12_CC_DATA_CALL_ACKNOWLEDGE:
case TYPE_D_IN_12_CC_DATA_CALL_ACKNOWLEDGE:
case TYPE_D_OUT_12_CC_DATA_CALL_ACKNOWLEDGE:
return new DataCallAcknowledge(message, timestamp, type, ran, lich);
case CONTROL_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE:
return new DataCallAssignmentDuplicateControl(message, timestamp, type, ran, lich);
case TRAFFIC_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE:
return new DataCallAssignmentDuplicateTraffic(message, timestamp, type, ran, lich);
case CONTROL_OUT_14_CC_DATA_CALL_ASSIGNMENT:
case TRAFFIC_OUT_14_CC_DATA_CALL_ASSIGNMENT:
return new DataCallAssignment(message, timestamp, type, ran, lich);
case TRAFFIC_IN_15_CC_HEADER_DELAY:
case TRAFFIC_OUT_15_CC_HEADER_DELAY:
case TYPE_D_IN_15_CC_HEADER_DELAY:
case TYPE_D_OUT_15_CC_HEADER_DELAY:
return new HeaderDelay(message, timestamp, type, ran, lich);
case CONTROL_OUT_16_CC_IDLE:
case TRAFFIC_OUT_16_CC_IDLE:
case TYPE_D_IN_16_CC_IDLE:
case TYPE_D_OUT_16_CC_IDLE:
return new Idle(message, timestamp, type, ran, lich);
case CONTROL_OUT_17_CC_DISCONNECT:
case TRAFFIC_OUT_17_CC_DISCONNECT:
case TYPE_D_OUT_17_CC_DISCONNECT:
return new Disconnect(message, timestamp, type, ran, lich);
case CONTROL_IN_17_CC_DISCONNECT_REQUEST:
case TRAFFIC_IN_17_CC_DISCONNECT_REQUEST:
case TYPE_D_IN_17_CC_DISCONNECT_REQUEST:
return new DisconnectRequest(message, timestamp, type, ran, lich);
//Broadcast Messages
case CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
case TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION:
case TYPE_D_OUT_23_BC_DIGITAL_STATION_ID:
return new DigitalStationIDInformation(message, timestamp, type, ran, lich);
case CONTROL_OUT_24_BC_SITE_INFORMATION:
case TRAFFIC_OUT_24_BC_SITE_INFORMATION:
return new SiteInformation(message, timestamp, type, ran, lich);
case CONTROL_OUT_25_BC_SERVICE_INFORMATION:
case TRAFFIC_OUT_25_BC_SERVICE_INFORMATION:
return new ServiceInformation(message, timestamp, type, ran, lich);
case TYPE_D_OUT_25_BC_SERVICE_INFORMATION:
return new ServiceInformationTypeD(message, timestamp, type, ran, lich);
case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
case TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
return new ControlChannelInformation(message, timestamp, type, ran, lich);
case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION:
case TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION:
return new AdjacentSiteInformation(message, timestamp, type, ran, lich);
case TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION:
return new AdjacentSiteInformationTypeD(message, timestamp, type, ran, lich);
case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION:
case TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION:
return new FailureStatusInformation(message, timestamp, type, ran, lich);
//Mobility Management Messages
case CONTROL_IN_32_MM_REGISTRATION_REQUEST:
case TYPE_D_IN_32_MM_REGISTRATION_REQUEST:
return new RegistrationRequest(message, timestamp, type, ran, lich);
case CONTROL_OUT_32_MM_REGISTRATION_RESPONSE:
return new RegistrationResponse(message, timestamp, type, ran, lich);
case TYPE_D_OUT_32_MM_REGISTRATION_RESPONSE:
return new RegistrationResponseTypeD(message, timestamp, type, ran, lich);
case CONTROL_IN_34_MM_REGISTRATION_CLEAR_REQUEST:
case TYPE_D_IN_34_MM_REGISTRATION_CLEAR_REQUEST:
return new RegistrationClearRequest(message, timestamp, type, ran, lich);
case CONTROL_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE:
case TYPE_D_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE:
return new RegistrationClearResponse(message, timestamp, type, ran, lich);
case CONTROL_OUT_35_MM_REGISTRATION_COMMAND:
return new RegistrationCommand(message, timestamp, type, ran, lich);
case CONTROL_IN_36_MM_GROUP_REGISTRATION_REQUEST:
case TYPE_D_IN_36_MM_GROUP_REGISTRATION_REQUEST:
return new GroupRegistrationRequest(message, timestamp, type, ran, lich);
case CONTROL_OUT_36_MM_GROUP_REGISTRATION_RESPONSE:
return new GroupRegistrationResponse(message, timestamp, type, ran, lich);
case TYPE_D_OUT_36_MM_GROUP_REGISTRATION_RESPONSE:
return new GroupRegistrationResponseTypeD(message, timestamp, type, ran, lich);
case TYPE_D_OUT_38_MM_AUTHENTICATION_PARAMETER_INFORMATION:
return new AuthenticationParameterInformation(message, timestamp, type, ran, lich);
case TYPE_D_IN_39_MM_AUTHENTICATION_RESPONSE:
return new AuthenticationResponse(message, timestamp, type, ran, lich);
case CONTROL_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST:
case TYPE_D_IN_40_MM_AUTHENTICATION_INQUIRY_REQUEST:
case TYPE_D_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST:
return new AuthenticationInquiryRequest(message, timestamp, type, ran, lich);
case CONTROL_IN_41_MM_AUTHENTICATION_INQUIRY_RESPONSE:
case TYPE_D_IN_41_MM_AUTHENTICATION_INQUIRY_RESPONSE:
case TYPE_D_OUT_41_MM_AUTHENTICATION_INQUIRY_RESPONSE:
return new AuthenticationInquiryResponse(message, timestamp, type, ran, lich);
case CONTROL_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
case TRAFFIC_IN_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
case TRAFFIC_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM:
return new AuthenticationInquiryRequestMultiSystem(message, timestamp, type, ran, lich);
case CONTROL_IN_43_MM_AUTHENTICATION_INQUIRY_RESPONSE_MULTI_SYSTEM:
case TRAFFIC_IN_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM:
case TRAFFIC_OUT_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM:
return new AuthenticationInquiryResponseMultiSystem(message, timestamp, type, ran, lich);
case TYPE_D_OUT_43_MM_DATA_WRITE_HEADER:
return new DataWriteHeader(message, timestamp, type, ran, lich);
case TYPE_D_OUT_44_MM_DATA_WRITE_DATA:
return new DataWriteBlock(message, timestamp, type, ran, lich);
case TYPE_D_IN_45_MM_DATA_WRITE_ACKNOWLEDGE:
return new DataWriteAcknowledge(message, timestamp, type, ran, lich);
case CONTROL_IN_48_CC_STATUS_INQUIRY_REQUEST:
case CONTROL_OUT_48_CC_STATUS_INQUIRY_REQUEST:
case TRAFFIC_IN_48_CC_STATUS_INQUIRY_REQUEST:
case TRAFFIC_OUT_48_CC_STATUS_INQUIRY_REQUEST:
case TYPE_D_IN_48_CC_STATUS_INQUIRY_REQUEST:
case TYPE_D_OUT_48_CC_STATUS_INQUIRY_REQUEST:
return new StatusInquiryRequest(message, timestamp, type, ran, lich);
case CONTROL_IN_49_CC_STATUS_INQUIRY_RESPONSE:
case CONTROL_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
case TRAFFIC_IN_49_CC_STATUS_INQUIRY_RESPONSE:
case TRAFFIC_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
case TYPE_D_IN_49_CC_STATUS_INQUIRY_RESPONSE:
case TYPE_D_OUT_49_CC_STATUS_INQUIRY_RESPONSE:
return new StatusInquiryResponse(message, timestamp, type, ran, lich);
case CONTROL_IN_50_CC_STATUS_REQUEST:
case CONTROL_OUT_50_CC_STATUS_REQUEST:
case TRAFFIC_IN_50_CC_STATUS_REQUEST:
case TRAFFIC_OUT_50_CC_STATUS_REQUEST:
case TYPE_D_IN_50_CC_STATUS_REQUEST:
case TYPE_D_OUT_50_CC_STATUS_REQUEST:
return new StatusRequest(message, timestamp, type, ran, lich);
case CONTROL_IN_51_CC_STATUS_RESPONSE:
case CONTROL_OUT_51_CC_STATUS_RESPONSE:
case TRAFFIC_IN_51_CC_STATUS_RESPONSE:
case TRAFFIC_OUT_51_CC_STATUS_RESPONSE:
case TYPE_D_IN_51_CC_STATUS_RESPONSE:
case TYPE_D_OUT_51_CC_STATUS_RESPONSE:
return new StatusResponse(message, timestamp, type, ran, lich);
case CONTROL_IN_52_CC_REMOTE_CONTROL_REQUEST:
case CONTROL_OUT_52_CC_REMOTE_CONTROL_REQUEST:
case TRAFFIC_IN_52_CC_REMOTE_CONTROL_REQUEST:
case TRAFFIC_OUT_52_CC_REMOTE_CONTROL_REQUEST:
case TYPE_D_IN_52_CC_REMOTE_CONTROL_REQUEST:
case TYPE_D_OUT_52_CC_REMOTE_CONTROL_REQUEST:
return new RemoteControlRequest(message, timestamp, type, ran, lich);
case CONTROL_IN_53_CC_REMOTE_CONTROL_RESPONSE:
case CONTROL_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
case TRAFFIC_IN_53_CC_REMOTE_CONTROL_RESPONSE:
case TRAFFIC_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
case TYPE_D_IN_53_CC_REMOTE_CONTROL_RESPONSE:
case TYPE_D_OUT_53_CC_REMOTE_CONTROL_RESPONSE:
return new RemoteControlResponse(message, timestamp, type, ran, lich);
case CONTROL_OUT_54_CC_REMOTE_CONTROL_REQUEST_WITH_ESN:
return new RemoteControlRequestWithESN(message, timestamp, type, ran, lich);
case CONTROL_IN_55_CC_REMOTE_CONTROL_RESPONSE_WITH_ESN:
return new RemoteControlResponseWithESN(message, timestamp, type, ran, lich);
case CONTROL_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
case CONTROL_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
case TRAFFIC_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
case TRAFFIC_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
case TYPE_D_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
case TYPE_D_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER:
return new ShortDataCallRequestHeader(message, timestamp, type, ran, lich);
case CONTROL_IN_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA:
case CONTROL_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA:
case TRAFFIC_IN_57_CC_SHORT_DATA_CALL_BLOCK:
case TRAFFIC_OUT_57_CC_SHORT_DATA_CALL_BLOCK:
case TYPE_D_IN_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA:
case TYPE_D_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA:
return new ShortDataCallBlock(message, timestamp, type, ran, lich);
case CONTROL_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
case CONTROL_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
case TRAFFIC_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
case TRAFFIC_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
case TYPE_D_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
case TYPE_D_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR:
return new ShortDataInitializationVector(message, timestamp, type, ran, lich);
case CONTROL_IN_59_CC_SHORT_DATA_CALL_RESPONSE:
case CONTROL_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
case TRAFFIC_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
case TRAFFIC_IN_59_CC_SHORT_DATA_CALL_RESPONSE:
case TYPE_D_IN_59_CC_SHORT_DATA_CALL_RESPONSE:
case TYPE_D_OUT_59_CC_SHORT_DATA_CALL_RESPONSE:
return new ShortDataCallResponse(message, timestamp, type, ran, lich);
case PROPRIETARY_FORM:
return new ProprietaryForm(message, timestamp, type, ran, lich);
case TALKER_ALIAS:
return new TalkerAlias(message, timestamp, type, ran, lich);
//NXDN Tpe-D SCCH messages
case TYPE_D_SCCH_IN_INFO_4_CALL_IN_PROGRESS_DESTINATION:
case TYPE_D_SCCH_OUT_INFO_4_CALL_IN_PROGRESS_DESTINATION:
return new CallInProgressDestinationInfo4(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_IN_INFO_4_CALL_COMPLETE_DESTINATION:
case TYPE_D_SCCH_OUT_INFO_4_CALL_COMPLETE_DESTINATION:
return new CallCompleteDestinationInfo4(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_OUT_INFO_4_REPEATER_IDLE:
return new RepeaterIdle(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_OUT_INFO_4_REPEATER_FREE:
return new RepeaterFree(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_OUT_INFO_4_REPEATER_HALT:
return new RepeaterHaltCWID(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_OUT_INFO_4_SITE_ID:
return new SiteID(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_IN_INFO_3_INITIALIZATION_VECTOR_PART2:
case TYPE_D_SCCH_OUT_INFO_3_INITIALIZATION_VECTOR_PART2:
return new InitializationVectorPart2(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_IN_INFO_3_CALL_IN_PROGRESS_SOURCE:
case TYPE_D_SCCH_OUT_INFO_3_CALL_IN_PROGRESS_SOURCE:
return new CallInProgressSourceID(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_IN_INFO_2_CALL_IN_PROGRESS_DESTINATION:
case TYPE_D_SCCH_OUT_INFO_2_CALL_IN_PROGRESS_DESTINATION:
return new CallInProgressDestinationInfo2(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_IN_INFO_1_CALL_INFO:
case TYPE_D_SCCH_OUT_INFO_1_CALL_INFO:
return new CallInfo(message, timestamp, type, ran, lich);
case TYPE_D_SCCH_IN_INFO_1_INITIALIZATION_VECTOR_PART1:
case TYPE_D_SCCH_OUT_INFO_1_INITIALIZATION_VECTOR_PART1:
return new InitializationVectorPart1(message, timestamp, type, ran, lich);
}
return new UnknownMessage(message, timestamp, type, ran, lich);
}
}

View File

@@ -0,0 +1,707 @@
/*
* *****************************************************************************
* 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.nxdn.layer3;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.ProprietaryForm;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.Information1;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.Information3;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.Information4;
import io.github.dsheirer.module.decode.nxdn.layer3.scch.SCCH;
import io.github.dsheirer.module.decode.nxdn.layer3.type.Vendor;
import java.util.EnumSet;
/**
* Enumeration of NXDN message types.
*/
public enum NXDNMessageType
{
//NOTE: do not modify entry order. We use EnumSet.range() below that expects the current ordering.
//Call Control
CONTROL_IN_01_CC_VOICE_CALL_REQUEST(1, "VOICE CALL REQUEST"),
CONTROL_IN_02_CC_VOICE_CALL_RECEPTION_RESPONSE(2, "VOICE CALL RECEPTION RESPONSE"),
CONTROL_IN_03_CC_VOICE_CALL_CONNECTION_REQUEST(3, "VOICE CALL CONNECTION REQUEST"),
CONTROL_IN_09_CC_DATA_CALL_REQUEST(9, "DATA CALL REQUEST"),
CONTROL_IN_10_CC_DATA_CALL_RECEPTION_RESPONSE(10, "DATA CALL RECEPTION RESPONSE"),
CONTROL_IN_17_CC_DISCONNECT_REQUEST(17, "DISCONNECT REQUEST"),
CONTROL_IN_48_CC_STATUS_INQUIRY_REQUEST(48, "STATUS INQUIRY REQUEST"),
CONTROL_IN_49_CC_STATUS_INQUIRY_RESPONSE(49, "STATUS INQUIRY RESPONSE"),
CONTROL_IN_50_CC_STATUS_REQUEST(50, "STATUS REQUEST"),
CONTROL_IN_51_CC_STATUS_RESPONSE(51, "STATUS RESPONSE"),
CONTROL_IN_52_CC_REMOTE_CONTROL_REQUEST(52, "REMOTE CONTROL REQUEST"),
CONTROL_IN_53_CC_REMOTE_CONTROL_RESPONSE(53, "REMOTE CONTROL RESPONSE"),
CONTROL_IN_55_CC_REMOTE_CONTROL_RESPONSE_WITH_ESN(55, "REMOTE CONTROL E RESPONSE"),
CONTROL_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER(56, "SHORT DATA CALL REQUEST HEADER"),
CONTROL_IN_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA(57, "SHORT DATA CALL REQUEST USER DATA"),
CONTROL_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR(58, "SHORT DATA CALL INITIALIZATION VECTOR"),
CONTROL_IN_59_CC_SHORT_DATA_CALL_RESPONSE(59, "SHORT DATA CALL RESPONSE"),
//Mobility Management
CONTROL_IN_32_MM_REGISTRATION_REQUEST(32, "REGISTRATION REQUEST"),
CONTROL_IN_34_MM_REGISTRATION_CLEAR_REQUEST(34, "REGISTRATION C REQUEST"),
CONTROL_IN_36_MM_GROUP_REGISTRATION_REQUEST(36, "GROUP REGISTRATION REQUEST"),
CONTROL_IN_41_MM_AUTHENTICATION_INQUIRY_RESPONSE(41, "AUTHORIZATION INQUIRY RESPONSE"),
CONTROL_IN_43_MM_AUTHENTICATION_INQUIRY_RESPONSE_MULTI_SYSTEM(43, "AUTHORIZATION INQUIRY RESPONSE MULTI-SYSTEM"),
//Call Control
CONTROL_OUT_01_CC_VOICE_CALL_RESPONSE(1, "VOICE CALL RESPONSE"),
CONTROL_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST(2, "VOICE CALL RECEPTION REQUEST"),
CONTROL_OUT_03_CC_VOICE_CALL_CONNECTION_RESPONSE(3, "VOICE CALL CONNECTION RESPONSE"),
CONTROL_OUT_04_CC_VOICE_CALL_ASSIGNMENT(4, "VOICE CALL ASSIGNMENT"),
CONTROL_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE(5, "VOICE CALL ASSIGNMENT DUPLICATE"),
CONTROL_OUT_09_CC_DATA_CALL_RESPONSE(9, "DATA CALL RESPONSE"),
CONTROL_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST(10, "DATA CALL RECEPTION REQUEST"),
CONTROL_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE(13, "DATA CALL ASSIGNMENT DUPLICATE"),
CONTROL_OUT_14_CC_DATA_CALL_ASSIGNMENT(14, "DATA CALL ASSIGNMENT"),
CONTROL_OUT_16_CC_IDLE(16, "IDLE"),
CONTROL_OUT_17_CC_DISCONNECT(17, "DISCONNECT"),
CONTROL_OUT_48_CC_STATUS_INQUIRY_REQUEST(48, "STATUS INQUIRY REQUEST"),
CONTROL_OUT_49_CC_STATUS_INQUIRY_RESPONSE(49, "STATUS INQUIRY RESPONSE"),
CONTROL_OUT_50_CC_STATUS_REQUEST(50, "STATUS REQUEST"),
CONTROL_OUT_51_CC_STATUS_RESPONSE(51, "STATUS RESPONSE"),
CONTROL_OUT_52_CC_REMOTE_CONTROL_REQUEST(52, "REMOTE CONTROL REQUEST"),
CONTROL_OUT_53_CC_REMOTE_CONTROL_RESPONSE(53, "REMOTE CONTROL RESPONSE"),
CONTROL_OUT_54_CC_REMOTE_CONTROL_REQUEST_WITH_ESN(54, "REMOTE CONTROL E REQUEST"),
CONTROL_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER(56, "SHORT DATA CALL REQUEST HEADER"),
CONTROL_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA(57, "SHORT DATA CALL REQUEST USER DATA"),
CONTROL_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR(58, "SHORT DATA CALL INITIALIZATION VECTOR"),
CONTROL_OUT_59_CC_SHORT_DATA_CALL_RESPONSE(59, "SHORT DATA CALL RESPONSE"),
//Broadcast
CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION(23, "DIGITAL STATION ID"),
CONTROL_OUT_24_BC_SITE_INFORMATION(24, "SITE INFORMATION"),
CONTROL_OUT_25_BC_SERVICE_INFORMATION(25, "SERVICE INFORMATION"),
CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION(26, "CONTROL CHANNEL INFORMATION"),
CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION(27, "ADJACENT SITE INFORMATION"),
CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION(28, "FAILURE STATUS INFORMATION"),
//Mobility Management
CONTROL_OUT_32_MM_REGISTRATION_RESPONSE(32, "REGISTRATION RESPONSE"),
CONTROL_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE(34, "REGISTRATION C RESPONSE"),
CONTROL_OUT_35_MM_REGISTRATION_COMMAND(35, "REG C RESPONSE"),
CONTROL_OUT_36_MM_GROUP_REGISTRATION_RESPONSE(36, "GROUP REGISTRATION RESPONSE"),
CONTROL_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST(40, "AUTHORIZATION INQUIRY REQUEST"),
CONTROL_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM(42, "AUTHORIZATION INQUIRY REQUEST MULTI-SYSTEM"),
//Call Control
TRAFFIC_IN_01_CC_VOICE_CALL(1, "VOICE CALL"),
TRAFFIC_IN_02_CC_VOICE_CALL_RECEPTION_RESPONSE(2, "VOICE CALL RECEPTION RESPONSE"),
TRAFFIC_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR(3, "VOICE CALL INITIALIZATION VECTOR"),
TRAFFIC_IN_08_CC_TRANSMISSION_RELEASE(8, "TRANSMISSION RELEASE"),
TRAFFIC_IN_09_CC_DATA_CALL_HEADER(9, "DATA CALL HEADER"),
TRAFFIC_IN_11_CC_DATA_CALL_BLOCK(11, "DATA CALL BLOCK"),
TRAFFIC_IN_12_CC_DATA_CALL_ACKNOWLEDGE(12, "DATA CALL ACKNOWLEDGE"),
TRAFFIC_IN_15_CC_HEADER_DELAY(15, "HEADER DELAY"),
TRAFFIC_IN_16_CC_IDLE(16, "IDLE"),
TRAFFIC_IN_17_CC_DISCONNECT_REQUEST(17, "DISCONNECT REQUEST"),
TRAFFIC_IN_48_CC_STATUS_INQUIRY_REQUEST(48, "STATUS INQUIRY REQUEST"),
TRAFFIC_IN_49_CC_STATUS_INQUIRY_RESPONSE(49, "STATUS INQUIRY RESPONSE"),
TRAFFIC_IN_50_CC_STATUS_REQUEST(50, "STATUS REQUEST"),
TRAFFIC_IN_51_CC_STATUS_RESPONSE(51, "STATUS RESPONSE"),
TRAFFIC_IN_52_CC_REMOTE_CONTROL_REQUEST(52, "REMOTE CONTROL REQUEST"),
TRAFFIC_IN_53_CC_REMOTE_CONTROL_RESPONSE(53, "REMOTE CONTROL RESPONSE"),
TRAFFIC_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER(56, "SHORT DATA CALL REQUEST HEADER"),
TRAFFIC_IN_57_CC_SHORT_DATA_CALL_BLOCK(57, "SHORT DATA CALL BLOCK"),
TRAFFIC_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR(58, "SHORT DATA CALL INITIALIZATION VECTOR"),
TRAFFIC_IN_59_CC_SHORT_DATA_CALL_RESPONSE(59, "SHORT DATA CALL RESPONSE"),
//Mobility Management
TRAFFIC_IN_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM(42, "AUTHORIZATION INQUIRY REQUEST MULTI-SYSTEM"),
TRAFFIC_IN_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM(43, "AUTHORIZATION INQUIRY RESPONSE MULTI-SYSTEM"),
//Call Control
TRAFFIC_OUT_01_CC_VOICE_CALL(1, "VOICE CALL"),
TRAFFIC_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST(2, "VOICE CALL RECEPTION REQUEST"),
TRAFFIC_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR(3, "VOICE CALL INITIALIZATION VECTOR"),
TRAFFIC_OUT_04_CC_VOICE_CALL_ASSIGNMENT(4, "VOICE CALL ASSIGNMENT"),
TRAFFIC_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE(5, "VOICE CALL ASSIGNMENT DUP"),
TRAFFIC_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION(7, "TRANSMISSION RELEASE EXTENSION"),
TRAFFIC_OUT_08_CC_TRANSMISSION_RELEASE(8, "TRANSMISSION RELEASE"),
TRAFFIC_OUT_09_CC_DATA_CALL_HEADER(9, "DATA CALL HEADER"),
TRAFFIC_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST(10, "DATA CALL RECEPTION REQUEST"),
TRAFFIC_OUT_11_CC_DATA_CALL_BLOCK(11, "DATA CALL USER DATA"),
TRAFFIC_OUT_12_CC_DATA_CALL_ACKNOWLEDGE(12, "DATA CALL ACKNOWLEDGE"),
TRAFFIC_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE(13, "DATA CALL ASSIGNMENT DUPLICATE"),
TRAFFIC_OUT_14_CC_DATA_CALL_ASSIGNMENT(14, "DATA CALL ASSIGNMENT DUPLICATE"),
TRAFFIC_OUT_15_CC_HEADER_DELAY(15, "HEADER DELAY"),
TRAFFIC_OUT_16_CC_IDLE(16, "IDLE"),
TRAFFIC_OUT_17_CC_DISCONNECT(17, "DISCONNECT"),
TRAFFIC_OUT_48_CC_STATUS_INQUIRY_REQUEST(48, "STATUS INQUIRY REQUEST"),
TRAFFIC_OUT_49_CC_STATUS_INQUIRY_RESPONSE(49, "STATUS INQUIRY RESPONSE"),
TRAFFIC_OUT_50_CC_STATUS_REQUEST(50, "STATUS REQUEST"),
TRAFFIC_OUT_51_CC_STATUS_RESPONSE(51, "STATUS RESPONSE"),
TRAFFIC_OUT_52_CC_REMOTE_CONTROL_REQUEST(52, "REMOTE CONTROL REQUEST"),
TRAFFIC_OUT_53_CC_REMOTE_CONTROL_RESPONSE(53, "REMOTE CONTROL RESPONSE"),
TRAFFIC_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER(56, "SHORT DATA CALL REQUEST HEADER"),
TRAFFIC_OUT_57_CC_SHORT_DATA_CALL_BLOCK(57, "SHORT DATA CALL REQUEST USER"),
TRAFFIC_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR(58, "SHORT DATA CALL INITIALIZATION VECTOR"),
TRAFFIC_OUT_59_CC_SHORT_DATA_CALL_RESPONSE(59, "SHORT DATA CALL RESPONSE"),
//Broadcast
TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION(23, "DIGITAL STATION ID"),
TRAFFIC_OUT_24_BC_SITE_INFORMATION(24, "SITE INFORMATION"),
TRAFFIC_OUT_25_BC_SERVICE_INFORMATION(25, "SERVICE INFORMATION"),
TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION(26, "CONTROL CHANNEL INFORMATION"),
TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION(27, "ADJACENT SITE INFORMATION"),
TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION(28, "FAILURE STATUS INFORMATION"),
//Mobility Management
TRAFFIC_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM(42, "AUTHORIZATION REQUEST MULTI-SYSTEM"),
TRAFFIC_OUT_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM(43, "AUTHORIZATION RESPONSE MULTI-SYSTEM"),
//Miscellanous
PROPRIETARY_FORM(63, "PROPRIETARY FORM"),
TALKER_ALIAS(0x82, "TALKER ALIAS"),
TALKER_ALIAS_COMPLETE(0x0, "TALKER ALIAS COMPLETE"),
//Type D SCCH messages
TYPE_D_SCCH_IN_INFO_4_CALL_IN_PROGRESS_DESTINATION(-1, "CALL IN PROGRESS - DESTINATION INFO 4"),
TYPE_D_SCCH_IN_INFO_4_CALL_COMPLETE_DESTINATION(-1, "CALL COMPLETE - DESTINATION INFO 4"),
TYPE_D_SCCH_IN_INFO_3_CALL_IN_PROGRESS_SOURCE(-1, "CALL IN PROGRESS - SOURCE ID"),
TYPE_D_SCCH_IN_INFO_3_INITIALIZATION_VECTOR_PART2(-1, "INITIALIZATION VECTOR PART 2"),
TYPE_D_SCCH_IN_INFO_2_CALL_IN_PROGRESS_DESTINATION(-1, "CALL IN PROGRESS DESTINATION INFO 2"),
TYPE_D_SCCH_IN_INFO_1_CALL_INFO(-1, "CALL INFO"),
TYPE_D_SCCH_IN_INFO_1_INITIALIZATION_VECTOR_PART1(-1, "INITIALIZATION VECTOR PART 1"),
TYPE_D_SCCH_IN_UNKNOWN(-1, "UNKNOWN"),
TYPE_D_SCCH_OUT_INFO_4_CALL_IN_PROGRESS_DESTINATION(-1, "CALL IN PROGRESS - DESTINATION INFO 4"),
TYPE_D_SCCH_OUT_INFO_4_CALL_COMPLETE_DESTINATION(-1, "CALL COMPLETE - DESTINATION INFO 4"),
TYPE_D_SCCH_OUT_INFO_4_REPEATER_IDLE(-1, "REPEATER IDLE"),
TYPE_D_SCCH_OUT_INFO_4_REPEATER_HALT(-1, "REPEATER HALT"),
TYPE_D_SCCH_OUT_INFO_4_REPEATER_FREE(-1, "REPEATER FREE"),
TYPE_D_SCCH_OUT_INFO_4_SITE_ID(-1, "SITE ID"),
TYPE_D_SCCH_OUT_INFO_3_CALL_IN_PROGRESS_SOURCE(-1, "CALL IN PROGRESS - SOURCE ID"),
TYPE_D_SCCH_OUT_INFO_3_INITIALIZATION_VECTOR_PART2(-1, "INITIALIZATION VECTOR PART 2"),
TYPE_D_SCCH_OUT_INFO_2_CALL_IN_PROGRESS_DESTINATION(-1, "CALL IN PROGRESS DESTINATION INFO 2"),
TYPE_D_SCCH_OUT_INFO_1_CALL_INFO(-1, "CALL INFO"),
TYPE_D_SCCH_OUT_INFO_1_INITIALIZATION_VECTOR_PART1(-1, "INITIALIZATION VECTOR PART 1"),
TYPE_D_SCCH_OUT_UNKNOWN(-1, "UNKNOWN"),
//Call Control
TYPE_D_IN_00_CC_CALL_REQUEST(0x00, "CALL REQUEST"),
TYPE_D_IN_01_CC_VOICE_CALL(0x01, "VOICE CALL"),
TYPE_D_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR(0x03, "VOICE CALL INITIALIZATION VECTOR"),
TYPE_D_IN_04_CC_VOICE_CALL_REQUEST(0x04, "VOICE CALL REQUEST"),
TYPE_D_IN_08_CC_TRANSMISSION_RELEASE(0x08, "TRANSMISSION RELEASE"),
TYPE_D_IN_09_CC_DATA_CALL_HEADER(0x09, "DATA CALL HEADER"),
TYPE_D_IN_11_CC_DATA_CALL_BLOCK(0x0B, "DATA CALL BLOCK"),
TYPE_D_IN_12_CC_DATA_CALL_ACKNOWLEDGE(0x0C, "DATA CALL ACKNOWLEDGE"),
TYPE_D_IN_15_CC_HEADER_DELAY(0x0F, "HEADER DELAY"),
TYPE_D_IN_16_CC_IDLE(0x10, "IDLE"),
TYPE_D_IN_17_CC_DISCONNECT_REQUEST(0x11, "DISCONNECT REQUEST"),
TYPE_D_IN_48_CC_STATUS_INQUIRY_REQUEST(0x30, "STATUS INQUIRY REQUEST"),
TYPE_D_IN_49_CC_STATUS_INQUIRY_RESPONSE(0x31, "STATUS INQUIRY RESPONSE"),
TYPE_D_IN_50_CC_STATUS_REQUEST(0x32, "STATUS REQUEST"),
TYPE_D_IN_51_CC_STATUS_RESPONSE(0x33, "STATUS RESPONSE"),
TYPE_D_IN_52_CC_REMOTE_CONTROL_REQUEST(0x34, "STATUS REQUEST"),
TYPE_D_IN_53_CC_REMOTE_CONTROL_RESPONSE(0x35, "STATUS RESPONSE"),
TYPE_D_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER(0x38, "SHORT DATA CALL REQUEST HEADER FORMAT"),
TYPE_D_IN_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA(0x39, "SHORT DATA CALL REQUEST USER DATA FORMAT"),
TYPE_D_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR(0x3A, "SHORT DATA CALL INITIALIZATION VECTOR"),
TYPE_D_IN_59_CC_SHORT_DATA_CALL_RESPONSE(0x3B, "SHORT DATA CALL RESPONSE"),
//Mobility Management
TYPE_D_IN_32_MM_REGISTRATION_REQUEST(0x20, "REGISTRATION REQUEST"),
TYPE_D_IN_34_MM_REGISTRATION_CLEAR_REQUEST(0x22, "REGISTRATION CLEAR REQUEST"),
TYPE_D_IN_36_MM_GROUP_REGISTRATION_REQUEST(0x24, "GROUP REGISTRATION REQUEST"),
TYPE_D_IN_39_MM_AUTHENTICATION_RESPONSE(0x27, "AUTHENTICATION RESPONSE"),
TYPE_D_IN_40_MM_AUTHENTICATION_INQUIRY_REQUEST(0x28, "AUTHENTICATION INQUIRY REQUEST"),
TYPE_D_IN_41_MM_AUTHENTICATION_INQUIRY_RESPONSE(0x29, "AUTHENTICATION INQUIRY RESPONSE"),
TYPE_D_IN_45_MM_DATA_WRITE_ACKNOWLEDGE(0x2D, "DATA WRITE ACKNOWLEDGE"),
TYPE_D_IN_UNKNOWN(-1, "UNKNOWN"),
//Call Control
TYPE_D_OUT_00_CC_CALL_RESPONSE(0x00, "CALL RESPONSE"),
TYPE_D_OUT_01_CC_VOICE_CALL(0x01, "VOICE CALL"),
TYPE_D_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR(0x03, "VOICE CALL INITIALIZATION VECTOR"),
TYPE_D_OUT_04_CC_VOICE_CALL_ASSIGNMENT(0x04, "VOICE CALL ASSIGNMENT"),
TYPE_D_OUT_06_CC_CALL_CONNECTION_RESPONSE(0x06, "CALL CONNECTION RESPONSE"),
TYPE_D_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION(0x07, "TRANSMISSION RELEASE EXTENSION"),
TYPE_D_OUT_08_CC_TRANSMISSION_RELEASE(0x08, "TRANSMISSION RELEASE"),
TYPE_D_OUT_09_CC_DATA_CALL_HEADER(0x09, "DATA CALL HEADER"),
TYPE_D_OUT_11_CC_DATA_CALL_BLOCK(0x0B, "DATA CALL BLOCK"),
TYPE_D_OUT_12_CC_DATA_CALL_ACKNOWLEDGE(0x0C, "DATA CALL ACKNOWLEDGE"),
TYPE_D_OUT_15_CC_HEADER_DELAY(0x0F, "HEADER DELAY"),
TYPE_D_OUT_16_CC_IDLE(0x10, "IDLE"),
TYPE_D_OUT_17_CC_DISCONNECT(0x11, "DISCONNECT"),
TYPE_D_OUT_48_CC_STATUS_INQUIRY_REQUEST(0x30, "STATUS INQUIRY REQUEST"),
TYPE_D_OUT_49_CC_STATUS_INQUIRY_RESPONSE(0x31, "STATUS INQUIRY RESPONSE"),
TYPE_D_OUT_50_CC_STATUS_REQUEST(0x32, "STATUS REQUEST"),
TYPE_D_OUT_51_CC_STATUS_RESPONSE(0x33, "STATUS RESPONSE"),
TYPE_D_OUT_52_CC_REMOTE_CONTROL_REQUEST(0x34, "STATUS REQUEST"),
TYPE_D_OUT_53_CC_REMOTE_CONTROL_RESPONSE(0x35, "STATUS RESPONSE"),
TYPE_D_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER(0x38, "SHORT DATA CALL REQUEST HEADER FORMAT"),
TYPE_D_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA(0x39, "SHORT DATA CALL REQUEST USER DATA FORMAT"),
TYPE_D_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR(0x3A, "SHORT DATA CALL INITIALIZATION VECTOR"),
TYPE_D_OUT_59_CC_SHORT_DATA_CALL_RESPONSE(0x3B, "SHORT DATA CALL RESPONSE"),
//Broadcast
TYPE_D_OUT_23_BC_DIGITAL_STATION_ID(0x17, "DIGITAL STATION ID"),
TYPE_D_OUT_25_BC_SERVICE_INFORMATION(0x19, "SERVICE INFORMATION"),
TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION(0x1B, "ADJACENT SITE INFORMATION"),
//Mobility Management
TYPE_D_OUT_32_MM_REGISTRATION_RESPONSE(0x20, "REGISTRATION RESPONSE"),
TYPE_D_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE(0x22, "REGISTRATION CLEAR RESPONSE"),
TYPE_D_OUT_36_MM_GROUP_REGISTRATION_RESPONSE(0x24, "GROUP REGISTRATION RESPONSE"),
TYPE_D_OUT_38_MM_AUTHENTICATION_PARAMETER_INFORMATION(0x26, "AUTHENTICATION PARAMETER INFORMATION"),
TYPE_D_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST(0x28, "AUTHENTICATION INQUIRY REQUEST"),
TYPE_D_OUT_41_MM_AUTHENTICATION_INQUIRY_RESPONSE(0x29, "AUTHENTICATION INQUIRY RESPONSE"),
TYPE_D_OUT_43_MM_DATA_WRITE_HEADER(0x2B, "DATA WRITE HEADER FORMAT"),
TYPE_D_OUT_44_MM_DATA_WRITE_DATA(0x2C, "DATA WRITE DATA FORMAT"),
TYPE_D_OUT_UNKNOWN(-1, "UNKNOWN"),
UNKNOWN(-1, "UNKNOWN");
private final int mValue;
private final String mLabel;
/**
* Constructs an instance
*
* @param value for the entry
* @param label to display
*/
NXDNMessageType(int value, String label)
{
mValue = value;
mLabel = label;
}
/**
* Control channel message types
*/
public static final EnumSet<NXDNMessageType> CONTROL = EnumSet.range(CONTROL_IN_01_CC_VOICE_CALL_REQUEST, CONTROL_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM);
/**
* Traffic channel message types
*/
public static final EnumSet<NXDNMessageType> TRAFFIC = EnumSet.range(TRAFFIC_IN_01_CC_VOICE_CALL, TRAFFIC_OUT_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM);
/**
* Type-D message types
*/
public static final EnumSet<NXDNMessageType> TYPE_D = EnumSet.range(TYPE_D_SCCH_IN_INFO_4_CALL_IN_PROGRESS_DESTINATION, TYPE_D_OUT_UNKNOWN);
/**
* Broadcast message types
*/
public static final EnumSet<NXDNMessageType> BROADCAST = EnumSet.of(CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION,
CONTROL_OUT_24_BC_SITE_INFORMATION, CONTROL_OUT_25_BC_SERVICE_INFORMATION,
CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION, CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION,
CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION, TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION,
TRAFFIC_OUT_24_BC_SITE_INFORMATION, TRAFFIC_OUT_25_BC_SERVICE_INFORMATION,
TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION, TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION,
TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION, TYPE_D_OUT_23_BC_DIGITAL_STATION_ID,
TYPE_D_OUT_25_BC_SERVICE_INFORMATION, TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION);
public static final EnumSet<NXDNMessageType> CONTROL_IN_CALL_CONTROL = EnumSet.range(CONTROL_IN_01_CC_VOICE_CALL_REQUEST, CONTROL_IN_59_CC_SHORT_DATA_CALL_RESPONSE);
public static final EnumSet<NXDNMessageType> CONTROL_IN_MOBILITY_MANAGEMENT = EnumSet.range(CONTROL_IN_32_MM_REGISTRATION_REQUEST, CONTROL_IN_43_MM_AUTHENTICATION_INQUIRY_RESPONSE_MULTI_SYSTEM);
public static final EnumSet<NXDNMessageType> CONTROL_OUT_BROADCAST = EnumSet.range(CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION, CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION);
public static final EnumSet<NXDNMessageType> CONTROL_OUT_CALL_CONTROL = EnumSet.range(CONTROL_OUT_01_CC_VOICE_CALL_RESPONSE, CONTROL_OUT_59_CC_SHORT_DATA_CALL_RESPONSE);
public static final EnumSet<NXDNMessageType> CONTROL_OUT_MOBILITY_MANAGEMENT = EnumSet.range(CONTROL_OUT_32_MM_REGISTRATION_RESPONSE, CONTROL_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM);
public static final EnumSet<NXDNMessageType> TRAFFIC_IN_CALL_CONTROL = EnumSet.range(TRAFFIC_IN_01_CC_VOICE_CALL, TRAFFIC_IN_59_CC_SHORT_DATA_CALL_RESPONSE);
public static final EnumSet<NXDNMessageType> TRAFFIC_IN_MOBILITY_MANAGEMENT = EnumSet.range(TRAFFIC_IN_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM, TRAFFIC_IN_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM);
public static final EnumSet<NXDNMessageType> TRAFFIC_OUT_BROADCAST = EnumSet.range(TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION, TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION);
public static final EnumSet<NXDNMessageType> TRAFFIC_OUT_CALL_CONTROL = EnumSet.range(TRAFFIC_OUT_01_CC_VOICE_CALL, TRAFFIC_OUT_59_CC_SHORT_DATA_CALL_RESPONSE);
public static final EnumSet<NXDNMessageType> TRAFFIC_OUT_MOBILITY_MANAGEMENT = EnumSet.range(TRAFFIC_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM, TRAFFIC_OUT_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM);
public static final EnumSet<NXDNMessageType> TYPE_D_IN_SCCH = EnumSet.range(TYPE_D_SCCH_IN_INFO_4_CALL_IN_PROGRESS_DESTINATION, TYPE_D_SCCH_IN_UNKNOWN);
public static final EnumSet<NXDNMessageType> TYPE_D_OUT_SCCH = EnumSet.range(TYPE_D_SCCH_OUT_INFO_4_CALL_IN_PROGRESS_DESTINATION, TYPE_D_SCCH_OUT_UNKNOWN);
public static final EnumSet<NXDNMessageType> TYPE_D_IN_CALL_CONTROL = EnumSet.range(TYPE_D_IN_00_CC_CALL_REQUEST, TYPE_D_IN_59_CC_SHORT_DATA_CALL_RESPONSE);
public static final EnumSet<NXDNMessageType> TYPE_D_IN_MOBILITY_MANAGEMENT = EnumSet.range(TYPE_D_IN_32_MM_REGISTRATION_REQUEST, TYPE_D_IN_UNKNOWN);
public static final EnumSet<NXDNMessageType> TYPE_D_OUT_BROADCAST = EnumSet.range(TYPE_D_OUT_23_BC_DIGITAL_STATION_ID, TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION);
public static final EnumSet<NXDNMessageType> TYPE_D_OUT_CALL_CONTROL = EnumSet.range(TYPE_D_OUT_00_CC_CALL_RESPONSE, TYPE_D_OUT_59_CC_SHORT_DATA_CALL_RESPONSE);
public static final EnumSet<NXDNMessageType> TYPE_D_OUT_MOBILITY_MANAGEMENT = EnumSet.range(TYPE_D_OUT_32_MM_REGISTRATION_RESPONSE, TYPE_D_OUT_UNKNOWN);
public static final EnumSet<NXDNMessageType> OTHER = EnumSet.of(PROPRIETARY_FORM, TALKER_ALIAS, TALKER_ALIAS_COMPLETE);
/**
* Call control message types
*/
public static final EnumSet<NXDNMessageType> CALL_CONTROL = EnumSet.of(TRAFFIC_IN_01_CC_VOICE_CALL);
/**
* Indicates if this message type is a broadcast message
*/
public boolean isBroadcast()
{
return BROADCAST.contains(this);
}
/**
* Indicates if this message type is a control channel message
*/
public boolean isControl()
{
return CONTROL.contains(this);
}
/**
* Indicates if this message type is a traffic channel message
*/
public boolean isTraffic()
{
return TRAFFIC.contains(this);
}
/**
* Indicates if the message type is a Type-D message
*/
public boolean isTypeD()
{
return TYPE_D.contains(this);
}
/**
* Value for the enumeration type
*
* @return value
*/
public int getValue()
{
return mValue;
}
/**
* Display label
*
* @return label
*/
@Override
public String toString()
{
return mLabel;
}
/**
* Utility method to lookup a vendor proprietary message type
*
* @param vendor for the message
* @param value of the message type
* @return message type or UNKNOWN
*/
public static NXDNMessageType getProprietary(Vendor vendor, int value)
{
switch(vendor)
{
case JVC:
switch(value)
{
case 0x82:
return TALKER_ALIAS;
}
default:
}
return PROPRIETARY_FORM;
}
/**
* Utility method to lookup the message type for the proprietary form message
*
* @param message in proprietary form
* @return message type or UNKNOWN
*/
public static NXDNMessageType getProprietary(CorrectedBinaryMessage message)
{
Vendor vendor = ProprietaryForm.getVendor(message);
int type = ProprietaryForm.getTypeValue(message);
return getProprietary(vendor, type);
}
/**
* Utility method to look up the inbound control channel message type.
*
* @param message containing the type value to lookup
* @param lich to determine inbound versus outbound
* @return matching entry or UNKNOWN if the value is not recognized.
*/
public static NXDNMessageType getControl(CorrectedBinaryMessage message, LICH lich)
{
int value = NXDNLayer3Message.getTypeValue(message);
if(lich.isOutbound())
{
return switch(value)
{
case 1 -> CONTROL_OUT_01_CC_VOICE_CALL_RESPONSE;
case 2 -> CONTROL_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST;
case 3 -> CONTROL_OUT_03_CC_VOICE_CALL_CONNECTION_RESPONSE;
case 4 -> CONTROL_OUT_04_CC_VOICE_CALL_ASSIGNMENT;
case 5 -> CONTROL_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE;
case 9 -> CONTROL_OUT_09_CC_DATA_CALL_RESPONSE;
case 10 -> CONTROL_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST;
case 13 -> CONTROL_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE;
case 14 -> CONTROL_OUT_14_CC_DATA_CALL_ASSIGNMENT;
case 16 -> CONTROL_OUT_16_CC_IDLE;
case 17 -> CONTROL_OUT_17_CC_DISCONNECT;
case 23 -> CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION;
case 24 -> CONTROL_OUT_24_BC_SITE_INFORMATION;
case 25 -> CONTROL_OUT_25_BC_SERVICE_INFORMATION;
case 26 -> CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION;
case 27 -> CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION;
case 32 -> CONTROL_OUT_32_MM_REGISTRATION_RESPONSE;
case 34 -> CONTROL_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE;
case 35 -> CONTROL_OUT_35_MM_REGISTRATION_COMMAND;
case 36 -> CONTROL_OUT_36_MM_GROUP_REGISTRATION_RESPONSE;
case 40 -> CONTROL_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST;
case 42 -> CONTROL_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM;
case 48 -> CONTROL_OUT_48_CC_STATUS_INQUIRY_REQUEST;
case 49 -> CONTROL_OUT_49_CC_STATUS_INQUIRY_RESPONSE;
case 50 -> CONTROL_OUT_50_CC_STATUS_REQUEST;
case 51 -> CONTROL_OUT_51_CC_STATUS_RESPONSE;
case 52 -> CONTROL_OUT_52_CC_REMOTE_CONTROL_REQUEST;
case 53 -> CONTROL_OUT_53_CC_REMOTE_CONTROL_RESPONSE;
case 54 -> CONTROL_OUT_54_CC_REMOTE_CONTROL_REQUEST_WITH_ESN;
case 56 -> CONTROL_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER;
case 57 -> CONTROL_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA;
case 58 -> CONTROL_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR;
case 59 -> CONTROL_OUT_59_CC_SHORT_DATA_CALL_RESPONSE;
case 63 -> getProprietary(message);
default -> UNKNOWN;
};
}
else
{
return switch(value)
{
case 1 -> CONTROL_IN_01_CC_VOICE_CALL_REQUEST;
case 2 -> CONTROL_IN_02_CC_VOICE_CALL_RECEPTION_RESPONSE;
case 3 -> CONTROL_IN_03_CC_VOICE_CALL_CONNECTION_REQUEST;
case 9 -> CONTROL_IN_09_CC_DATA_CALL_REQUEST;
case 10 -> CONTROL_IN_10_CC_DATA_CALL_RECEPTION_RESPONSE;
case 17 -> CONTROL_IN_17_CC_DISCONNECT_REQUEST;
case 32 -> CONTROL_IN_32_MM_REGISTRATION_REQUEST;
case 34 -> CONTROL_IN_34_MM_REGISTRATION_CLEAR_REQUEST;
case 36 -> CONTROL_IN_36_MM_GROUP_REGISTRATION_REQUEST;
case 41 -> CONTROL_IN_41_MM_AUTHENTICATION_INQUIRY_RESPONSE;
case 43 -> CONTROL_IN_43_MM_AUTHENTICATION_INQUIRY_RESPONSE_MULTI_SYSTEM;
case 48 -> CONTROL_IN_48_CC_STATUS_INQUIRY_REQUEST;
case 49 -> CONTROL_IN_49_CC_STATUS_INQUIRY_RESPONSE;
case 50 -> CONTROL_IN_50_CC_STATUS_REQUEST;
case 51 -> CONTROL_IN_51_CC_STATUS_RESPONSE;
case 52 -> CONTROL_IN_52_CC_REMOTE_CONTROL_REQUEST;
case 53 -> CONTROL_IN_53_CC_REMOTE_CONTROL_RESPONSE;
case 55 -> CONTROL_IN_55_CC_REMOTE_CONTROL_RESPONSE_WITH_ESN;
case 56 -> CONTROL_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER;
case 57 -> CONTROL_IN_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA;
case 58 -> CONTROL_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR;
case 59 -> CONTROL_IN_59_CC_SHORT_DATA_CALL_RESPONSE;
case 63 -> getProprietary(message);
default -> UNKNOWN;
};
}
}
/**
* Utility method to look up the traffic channel message type.
*
* @param message containing the type value to lookup
* @param lich to determine inbound versus outbout
* @return matching entry or UNKNOWN if the value is not recognized.
*/
public static NXDNMessageType getTraffic(CorrectedBinaryMessage message, LICH lich)
{
int value = NXDNLayer3Message.getTypeValue(message);
if(lich.isOutbound())
{
return switch(value)
{
case 1 -> TRAFFIC_OUT_01_CC_VOICE_CALL;
case 2 -> TRAFFIC_OUT_02_CC_VOICE_CALL_RECEPTION_REQUEST;
case 3 -> TRAFFIC_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR;
case 4 -> TRAFFIC_OUT_04_CC_VOICE_CALL_ASSIGNMENT;
case 5 -> TRAFFIC_OUT_05_CC_VOICE_CALL_ASSIGNMENT_DUPLICATE;
case 7 -> TRAFFIC_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION;
case 8 -> TRAFFIC_OUT_08_CC_TRANSMISSION_RELEASE;
case 9 -> TRAFFIC_OUT_09_CC_DATA_CALL_HEADER;
case 10 -> TRAFFIC_OUT_10_CC_DATA_CALL_RECEPTION_REQUEST;
case 11 -> TRAFFIC_OUT_11_CC_DATA_CALL_BLOCK;
case 12 -> TRAFFIC_OUT_12_CC_DATA_CALL_ACKNOWLEDGE;
case 13 -> TRAFFIC_OUT_13_CC_DATA_CALL_ASSIGNMENT_DUPLICATE;
case 14 -> TRAFFIC_OUT_14_CC_DATA_CALL_ASSIGNMENT;
case 15 -> TRAFFIC_OUT_15_CC_HEADER_DELAY;
case 16 -> TRAFFIC_OUT_16_CC_IDLE;
case 17 -> TRAFFIC_OUT_17_CC_DISCONNECT;
case 23 -> TRAFFIC_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION;
case 25 -> TRAFFIC_OUT_25_BC_SERVICE_INFORMATION;
case 26 -> TRAFFIC_OUT_26_BC_CONTROL_CHANNEL_INFORMATION;
case 27 -> TRAFFIC_OUT_27_BC_ADJACENT_SITE_INFORMATION;
case 28 -> TRAFFIC_OUT_28_BC_FAILURE_STATUS_INFORMATION;
case 42 -> TRAFFIC_OUT_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM;
case 43 -> TRAFFIC_OUT_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM;
case 48 -> TRAFFIC_OUT_48_CC_STATUS_INQUIRY_REQUEST;
case 49 -> TRAFFIC_OUT_49_CC_STATUS_INQUIRY_RESPONSE;
case 50 -> TRAFFIC_OUT_50_CC_STATUS_REQUEST;
case 51 -> TRAFFIC_OUT_51_CC_STATUS_RESPONSE;
case 52 -> TRAFFIC_OUT_52_CC_REMOTE_CONTROL_REQUEST;
case 53 -> TRAFFIC_OUT_53_CC_REMOTE_CONTROL_RESPONSE;
case 56 -> TRAFFIC_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER;
case 57 -> TRAFFIC_OUT_57_CC_SHORT_DATA_CALL_BLOCK;
case 58 -> TRAFFIC_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR;
case 59 -> TRAFFIC_OUT_59_CC_SHORT_DATA_CALL_RESPONSE;
case 63 -> getProprietary(message);
default -> UNKNOWN;
};
}
else
{
return switch(value)
{
case 1 -> TRAFFIC_IN_01_CC_VOICE_CALL;
case 3 -> TRAFFIC_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR;
case 8 -> TRAFFIC_IN_08_CC_TRANSMISSION_RELEASE;
case 9 -> TRAFFIC_IN_09_CC_DATA_CALL_HEADER;
case 11 -> TRAFFIC_IN_11_CC_DATA_CALL_BLOCK;
case 12 -> TRAFFIC_IN_12_CC_DATA_CALL_ACKNOWLEDGE;
case 15 -> TRAFFIC_IN_15_CC_HEADER_DELAY;
case 16 -> TRAFFIC_IN_16_CC_IDLE;
case 17 -> TRAFFIC_IN_17_CC_DISCONNECT_REQUEST;
case 23 -> CONTROL_OUT_23_BC_DIGITAL_STATION_ID_INFORMATION;
case 42 -> TRAFFIC_IN_42_MM_AUTHENTICATION_INQUIRY_REQUEST_MULTI_SYSTEM;
case 43 -> TRAFFIC_IN_43_MM_AUTHORIZATION_INQUIRY_RESPONSE_MULTI_SYSTEM;
case 48 -> TRAFFIC_IN_48_CC_STATUS_INQUIRY_REQUEST;
case 49 -> TRAFFIC_IN_49_CC_STATUS_INQUIRY_RESPONSE;
case 50 -> TRAFFIC_IN_50_CC_STATUS_REQUEST;
case 51 -> TRAFFIC_IN_51_CC_STATUS_RESPONSE;
case 52 -> TRAFFIC_IN_52_CC_REMOTE_CONTROL_REQUEST;
case 53 -> TRAFFIC_IN_53_CC_REMOTE_CONTROL_RESPONSE;
case 56 -> TRAFFIC_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER;
case 57 -> TRAFFIC_IN_57_CC_SHORT_DATA_CALL_BLOCK;
case 58 -> TRAFFIC_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR;
case 59 -> TRAFFIC_IN_59_CC_SHORT_DATA_CALL_RESPONSE;
case 63 -> getProprietary(message);
default -> UNKNOWN;
};
}
}
/**
* NXDN Type-D SCCH message type lookup
*
* @param message that is SCCH
* @return message type
*/
public static NXDNMessageType getSCCH(CorrectedBinaryMessage message, LICH lich)
{
int structure = SCCH.getStructure(message);
return switch(SCCH.getStructure(message))
{
case 0 -> Information4.getMessageType(message, lich);
case 1 -> Information3.getMessageType(message, lich);
case 2 -> lich.isOutbound() ? NXDNMessageType.TYPE_D_SCCH_OUT_INFO_2_CALL_IN_PROGRESS_DESTINATION : NXDNMessageType.TYPE_D_SCCH_IN_INFO_2_CALL_IN_PROGRESS_DESTINATION;
case 3 -> Information1.getMessageType(message, lich);
default -> lich.isOutbound() ? NXDNMessageType.TYPE_D_SCCH_OUT_UNKNOWN : NXDNMessageType.TYPE_D_SCCH_IN_UNKNOWN;
};
}
/**
* NXDN Type-D message type lookup
*
* @param message containing a message type value
* @param lich to determine inbound vs outbound
* @return message type
*/
public static NXDNMessageType getTypeD(CorrectedBinaryMessage message, LICH lich)
{
int value = NXDNLayer3Message.getTypeValue(message);
if(lich.isOutbound())
{
return switch(value)
{
case 0 -> TYPE_D_OUT_00_CC_CALL_RESPONSE;
case 1 -> TYPE_D_OUT_01_CC_VOICE_CALL;
case 2 -> TYPE_D_OUT_03_CC_VOICE_CALL_INITIALIZATION_VECTOR;
case 3 -> TYPE_D_OUT_04_CC_VOICE_CALL_ASSIGNMENT;
case 6 -> TYPE_D_OUT_06_CC_CALL_CONNECTION_RESPONSE;
case 7 -> TYPE_D_OUT_07_CC_TRANSMISSION_RELEASE_EXTENSION;
case 8 -> TYPE_D_OUT_08_CC_TRANSMISSION_RELEASE;
case 9 -> TYPE_D_OUT_09_CC_DATA_CALL_HEADER;
case 11 -> TYPE_D_OUT_11_CC_DATA_CALL_BLOCK;
case 12 -> TYPE_D_OUT_12_CC_DATA_CALL_ACKNOWLEDGE;
case 15 -> TYPE_D_OUT_15_CC_HEADER_DELAY;
case 16 -> TYPE_D_OUT_16_CC_IDLE;
case 17 -> TYPE_D_OUT_17_CC_DISCONNECT;
case 23 -> TYPE_D_OUT_23_BC_DIGITAL_STATION_ID;
case 25 -> TYPE_D_OUT_25_BC_SERVICE_INFORMATION;
case 27 -> TYPE_D_OUT_27_BC_ADJACENT_SITE_INFORMATION;
case 32 -> TYPE_D_OUT_32_MM_REGISTRATION_RESPONSE;
case 34 -> TYPE_D_OUT_34_MM_REGISTRATION_CLEAR_RESPONSE;
case 36 -> TYPE_D_OUT_36_MM_GROUP_REGISTRATION_RESPONSE;
case 38 -> TYPE_D_OUT_38_MM_AUTHENTICATION_PARAMETER_INFORMATION;
case 40 -> TYPE_D_OUT_40_MM_AUTHENTICATION_INQUIRY_REQUEST;
case 41 -> TYPE_D_OUT_41_MM_AUTHENTICATION_INQUIRY_RESPONSE;
case 43 -> TYPE_D_OUT_43_MM_DATA_WRITE_HEADER;
case 44 -> TYPE_D_OUT_44_MM_DATA_WRITE_DATA;
case 48 -> TYPE_D_OUT_48_CC_STATUS_INQUIRY_REQUEST;
case 49 -> TYPE_D_OUT_49_CC_STATUS_INQUIRY_RESPONSE;
case 50 -> TYPE_D_OUT_50_CC_STATUS_REQUEST;
case 51 -> TYPE_D_OUT_51_CC_STATUS_RESPONSE;
case 52 -> TYPE_D_OUT_52_CC_REMOTE_CONTROL_REQUEST;
case 53 -> TYPE_D_OUT_53_CC_REMOTE_CONTROL_RESPONSE;
case 56 -> TYPE_D_OUT_56_CC_SHORT_DATA_CALL_REQUEST_HEADER;
case 57 -> TYPE_D_OUT_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA;
case 58 -> TYPE_D_OUT_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR;
case 59 -> TYPE_D_OUT_59_CC_SHORT_DATA_CALL_RESPONSE;
case 63 -> getProprietary(message);
default -> TYPE_D_OUT_UNKNOWN;
};
}
else
{
return switch(value)
{
case 0 -> TYPE_D_IN_00_CC_CALL_REQUEST;
case 1 -> TYPE_D_IN_01_CC_VOICE_CALL;
case 3 -> TYPE_D_IN_03_CC_VOICE_CALL_INITIALIZATION_VECTOR;
case 4 -> TYPE_D_IN_04_CC_VOICE_CALL_REQUEST;
case 8 -> TYPE_D_IN_08_CC_TRANSMISSION_RELEASE;
case 9 -> TYPE_D_IN_09_CC_DATA_CALL_HEADER;
case 11 -> TYPE_D_IN_11_CC_DATA_CALL_BLOCK;
case 12 -> TYPE_D_IN_12_CC_DATA_CALL_ACKNOWLEDGE;
case 15 -> TYPE_D_IN_15_CC_HEADER_DELAY;
case 16 -> TYPE_D_IN_16_CC_IDLE;
case 17 -> TYPE_D_IN_17_CC_DISCONNECT_REQUEST;
case 32 -> TYPE_D_IN_32_MM_REGISTRATION_REQUEST;
case 34 -> TYPE_D_IN_34_MM_REGISTRATION_CLEAR_REQUEST;
case 36 -> TYPE_D_IN_36_MM_GROUP_REGISTRATION_REQUEST;
case 39 -> TYPE_D_IN_39_MM_AUTHENTICATION_RESPONSE;
case 40 -> TYPE_D_IN_40_MM_AUTHENTICATION_INQUIRY_REQUEST;
case 41 -> TYPE_D_IN_41_MM_AUTHENTICATION_INQUIRY_RESPONSE;
case 45 -> TYPE_D_IN_45_MM_DATA_WRITE_ACKNOWLEDGE;
case 48 -> TYPE_D_IN_48_CC_STATUS_INQUIRY_REQUEST;
case 49 -> TYPE_D_IN_49_CC_STATUS_INQUIRY_RESPONSE;
case 50 -> TYPE_D_IN_50_CC_STATUS_REQUEST;
case 51 -> TYPE_D_IN_51_CC_STATUS_RESPONSE;
case 52 -> TYPE_D_IN_52_CC_REMOTE_CONTROL_REQUEST;
case 53 -> TYPE_D_IN_53_CC_REMOTE_CONTROL_RESPONSE;
case 56 -> TYPE_D_IN_56_CC_SHORT_DATA_CALL_REQUEST_HEADER;
case 57 -> TYPE_D_IN_57_CC_SHORT_DATA_CALL_REQUEST_USER_DATA;
case 58 -> TYPE_D_IN_58_CC_SHORT_DATA_CALL_INITIALIZATION_VECTOR;
case 59 -> TYPE_D_IN_59_CC_SHORT_DATA_CALL_RESPONSE;
case 63 -> getProprietary(message);
default -> TYPE_D_IN_UNKNOWN;
};
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* *****************************************************************************
* 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.nxdn.layer3;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import java.util.List;
/**
* Unknown message type
*/
public class UnknownMessage extends NXDNLayer3Message
{
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param ran value
* @param lich info
*/
public UnknownMessage(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append("UNKNOWN TYPE ").append(getMessageType());
sb.append(" MSG:").append(getMessage().toHexString());
sb.append(" LICH:").append(getLICH());
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -0,0 +1,393 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannel;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelDFA;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelLookup;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.Bandwidth;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationID;
import java.util.List;
import java.util.Map;
/**
* Adjacent (ie neighbor) site information
*/
public class AdjacentSiteInformation extends NXDNLayer3Message implements IChannelInformationReceiver
{
private static final int LOCATION_ID_1 = OCTET_1;
private static final IntField NEIGHBOR_ID_1 = IntField.length4(OCTET_4 + 2);
//Channel Mode Fields
private static final IntField CHANNEL_1 = IntField.length10(OCTET_4 + 6);
private static final int LOCATION_ID_2 = OCTET_6;
private static final IntField NEIGHBOR_ID_2 = IntField.length4(OCTET_9 + 2);
private static final IntField CHANNEL_2 = IntField.length10(OCTET_9 + 6);
private static final int LOCATION_ID_3 = OCTET_11;
private static final IntField NEIGHBOR_ID_3 = IntField.length4(OCTET_14 + 2);
private static final IntField CHANNEL_3 = IntField.length10(OCTET_14 + 6);
private static final int LOCATION_ID_4 = OCTET_16;
private static final IntField NEIGHBOR_ID_4 = IntField.length4(OCTET_19 + 2);
private static final IntField CHANNEL_4 = IntField.length10(OCTET_19 + 6);
//DFA Mode Fields
private static final IntField DFA_BANDWIDTH_1 = IntField.length2(OCTET_4 + 6);
private static final IntField DFA_CHANNEL_1 = IntField.length16(OCTET_5);
private static final int DFA_LOCATION_ID_2 = OCTET_7;
private static final IntField DFA_NEIGHBOR_ID_2 = IntField.length4(OCTET_10 + 2);
private static final IntField DFA_BANDWIDTH_2 = IntField.length2(OCTET_10 + 6);
private static final IntField DFA_CHANNEL_2 = IntField.length16(OCTET_11);
private static final int DFA_LOCATION_ID_3 = OCTET_13;
private static final IntField DFA_NEIGHBOR_ID_3 = IntField.length4(OCTET_16 + 2);
private static final IntField DFA_BANDWIDTH_3 = IntField.length2(OCTET_16 + 6);
private static final IntField DFA_CHANNEL_3 = IntField.length16(OCTET_17);
private LocationID mLocationID1;
private LocationID mLocationID2;
private LocationID mLocationID3;
private LocationID mLocationID4;
private NXDNChannel mChannel1;
private NXDNChannel mChannel2;
private NXDNChannel mChannel3;
private NXDNChannel mChannel4;
private int mNeighborId2;
private int mNeighborId3;
private int mNeighborId4;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public AdjacentSiteInformation(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append("ADJACENT SITE ").append(getNeighborId1()).append(" [").append(getLocation1());
if(hasChannel1())
{
sb.append(" CHAN:").append(getChannel1());
if(hasChannel2())
{
sb.append("] SITE ").append(getNeighborId2()).append(" [").append(getLocation2());
sb.append(" CHAN:").append(getChannel2());
if(hasChannel3())
{
sb.append("] SITE ").append(getNeighborId3()).append(" [").append(getLocation3());
sb.append(" CHAN:").append(getChannel3());
if(hasChannel4())
{
sb.append("] SITE ").append(getNeighborId4()).append(" [").append(getLocation4());
sb.append(" CHAN:").append(getChannel4());
}
else
{
sb.append("]");
}
}
else
{
sb.append("]");
}
}
else
{
sb.append("]");
}
}
return sb.toString();
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
if(channelAccessInformation != null)
{
if(channelAccessInformation.isChannel()) //Channel Mode
{
mChannel1 = new NXDNChannelLookup(getMessage().getInt(CHANNEL_1));
mChannel1.receive(channelAccessInformation, channelFrequencyMap);
mNeighborId2 = getMessage().getInt(NEIGHBOR_ID_2);
if(mNeighborId2 > 0)
{
mLocationID2 = new LocationID(getMessage(), LOCATION_ID_2);
mChannel2 = new NXDNChannelLookup(getMessage().getInt(CHANNEL_2));
mChannel2.receive(channelAccessInformation, channelFrequencyMap);
mNeighborId3 = getMessage().getInt(NEIGHBOR_ID_3);
if(mNeighborId3 > 0)
{
mLocationID3 = new LocationID(getMessage(), LOCATION_ID_3);
mChannel3 = new NXDNChannelLookup(getMessage().getInt(CHANNEL_3));
mChannel3.receive(channelAccessInformation, channelFrequencyMap);
mNeighborId4 = getMessage().getInt(NEIGHBOR_ID_4);
if(mNeighborId4 > 0)
{
mLocationID4 = new LocationID(getMessage(), LOCATION_ID_4);
mChannel4 = new NXDNChannelLookup(getMessage().getInt(CHANNEL_4));
mChannel4.receive(channelAccessInformation, channelFrequencyMap);
}
}
}
}
else //DFA Mode
{
mChannel1 = new NXDNChannelDFA(getMessage().getInt(DFA_CHANNEL_1), 0,
Bandwidth.fromValue(getMessage().getInt(DFA_BANDWIDTH_1)));
mChannel1.receive(channelAccessInformation, channelFrequencyMap);
mNeighborId2 = getMessage().getInt(DFA_NEIGHBOR_ID_2);
if(mNeighborId2 > 0)
{
mLocationID2 = new LocationID(getMessage(), DFA_LOCATION_ID_2);
mChannel2 = new NXDNChannelDFA(getMessage().getInt(DFA_CHANNEL_2), 0,
Bandwidth.fromValue(getMessage().getInt(DFA_BANDWIDTH_2)));
mChannel2.receive(channelAccessInformation, channelFrequencyMap);
mNeighborId3 = getMessage().getInt(DFA_NEIGHBOR_ID_3);
if(mNeighborId3 > 0)
{
mLocationID3 = new LocationID(getMessage(), DFA_LOCATION_ID_3);
mChannel3 = new NXDNChannelDFA(getMessage().getInt(DFA_CHANNEL_3), 0,
Bandwidth.fromValue(getMessage().getInt(DFA_BANDWIDTH_3)));
mChannel3.receive(channelAccessInformation, channelFrequencyMap);
}
}
}
}
}
public Neighbor getNeighbor1()
{
if(hasChannel1())
{
return new Neighbor(getNeighborId1(), getLocation1(), getChannel1());
}
return null;
}
public Neighbor getNeighbor2()
{
if(hasChannel2())
{
return new Neighbor(getNeighborId2(), getLocation2(), getChannel2());
}
return null;
}
public Neighbor getNeighbor3()
{
if(hasChannel3())
{
return new Neighbor(getNeighborId3(), getLocation3(), getChannel3());
}
return null;
}
public Neighbor getNeighbor4()
{
if(hasChannel4())
{
return new Neighbor(getNeighborId4(), getLocation4(), getChannel4());
}
return null;
}
/**
* Neighbor ID for neighbor 1
*/
public int getNeighborId1()
{
return getMessage().getInt(NEIGHBOR_ID_1);
}
/**
* Neighbor ID for neighbor 2
*/
public int getNeighborId2()
{
return mNeighborId2;
}
/**
* Neighbor ID for neighbor 3
*/
public int getNeighborId3()
{
return mNeighborId3;
}
/**
* Neighbor ID for neighbor 4
*/
public int getNeighborId4()
{
return mNeighborId4;
}
/**
* Location 1
* @return location
*/
public LocationID getLocation1()
{
if(mLocationID1 == null)
{
mLocationID1 = new LocationID(getMessage(), LOCATION_ID_1);
}
return mLocationID1;
}
/**
* Location 2
* @return location or null
*/
public LocationID getLocation2()
{
return mLocationID2;
}
/**
* Location 3
* @return location or null
*/
public LocationID getLocation3()
{
return mLocationID3;
}
/**
* Location 4
* @return location or null
*/
public LocationID getLocation4()
{
return mLocationID4;
}
/**
* Indicates if the channel is available. Note: channel info isn't available until the channel access info is set.
*/
public boolean hasChannel1()
{
return mChannel1 != null;
}
/**
* Channel 1
* @return channel or null.
*/
public NXDNChannel getChannel1()
{
return mChannel1;
}
/**
* Channel 2
* @return channel or null.
*/
public NXDNChannel getChannel2()
{
return mChannel2;
}
/**
* Channel 3
* @return channel or null.
*/
public NXDNChannel getChannel3()
{
return mChannel3;
}
/**
* Channel 4
* @return channel or null.
*/
public NXDNChannel getChannel4()
{
return mChannel4;
}
/**
* Indicates if the channel is available. Note: channel info isn't available until the channel access info is set.
*/
public boolean hasChannel2()
{
return mChannel2 != null;
}
/**
* Indicates if the channel is available. Note: channel info isn't available until the channel access info is set.
*/
public boolean hasChannel3()
{
return mChannel3 != null;
}
/**
* Indicates if the channel is available. Note: channel info isn't available until the channel access info is set.
*/
public boolean hasChannel4()
{
return mChannel4 != null;
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -0,0 +1,193 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNSite;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.AdjacentSiteOption;
import io.github.dsheirer.module.decode.nxdn.layer3.type.SystemID;
import java.util.List;
/**
* Type-D adjacent (ie neighbor) site information
*/
public class AdjacentSiteInformationTypeD extends NXDNLayer3Message
{
private static final int INDEX = OCTET_1 + 7;
private static final IntField ADJACENT_SITE_OPTION_1 = IntField.length5(OCTET_2);
private static final int ADJACENT_SITE_SYSTEM_ID_1 = OCTET_2 + 5;
private static final IntField ADJACENT_SITE_CODE_1 = IntField.length8(OCTET_5);
private static final IntField ADJACENT_SITE_OPTION_2 = IntField.length5(OCTET_6);
private static final int ADJACENT_SITE_SYSTEM_ID_2 = OCTET_6 + 5;
private static final IntField ADJACENT_SITE_CODE_2 = IntField.length8(OCTET_9);
private AdjacentSiteOption mSiteOption1;
private AdjacentSiteOption mSiteOption2;
private SystemID mSystemID1;
private SystemID mSystemID2;
private NXDNSite mSite1;
private NXDNSite mSite2;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type message
* @param ran is always 0
* @param lich for the fragment
*/
public AdjacentSiteInformationTypeD(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append("TYPE-D NEIGHBOR-").append(isIndex() ? "3:" : "1:");
sb.append(getSystemID1()).append(" SITE:").append(getSite1());
if(getSiteOption1().isIsolatedSite())
{
sb.append(" (ISOLATED)");
}
if(hasSite2())
{
sb.append(" NEIGHBOR-").append(isIndex() ? "4:" : "2:");
sb.append(getSystemID2()).append(" SITE:").append(getSite2());
if(getSiteOption2().isIsolatedSite())
{
sb.append(" (ISOLATED)");
}
}
return sb.toString();
}
/**
* Indicates if the site 2 field is empty (false) or not (true)
*/
public boolean hasSite2()
{
return getSystemID2().getSystem().getValue() != 0 && getSite2().getValue() != 0;
}
/**
* Indicates if this is adjacent site information message 1 (false) or 2 (true) when transmitting up
* to four adjacent sites with two sites carried in message 1 and two carried in message 2.
*/
public boolean isIndex()
{
return getMessage().get(INDEX);
}
/**
* Adjacent site option 1
*/
public AdjacentSiteOption getSiteOption1()
{
if(mSiteOption1 == null)
{
mSiteOption1 = new AdjacentSiteOption(getMessage().getInt(ADJACENT_SITE_OPTION_1));
}
return mSiteOption1;
}
/**
* Adjacent site option 2
*/
public AdjacentSiteOption getSiteOption2()
{
if(mSiteOption2 == null)
{
mSiteOption2 = new AdjacentSiteOption(getMessage().getInt(ADJACENT_SITE_OPTION_2));
}
return mSiteOption2;
}
/**
* Adjacent site system ID 1
*/
public SystemID getSystemID1()
{
if(mSystemID1 == null)
{
mSystemID1 = new SystemID(getMessage(), ADJACENT_SITE_SYSTEM_ID_1);
}
return mSystemID1;
}
/**
* Adjacent site system ID 2
*/
public SystemID getSystemID2()
{
if(mSystemID2 == null)
{
mSystemID2 = new SystemID(getMessage(), ADJACENT_SITE_SYSTEM_ID_2);
}
return mSystemID2;
}
/**
* Adjacent site 1 ID
*/
public NXDNSite getSite1()
{
if(mSite1 == null)
{
mSite1 = NXDNSite.create(getMessage().getInt(ADJACENT_SITE_CODE_1));
}
return mSite1;
}
/**
* Adjacent site 2 ID
*/
public NXDNSite getSite2()
{
if(mSite2 == null)
{
mSite2 = NXDNSite.create(getMessage().getInt(ADJACENT_SITE_CODE_2));
}
return mSite2;
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -0,0 +1,212 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannel;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelDFA;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelLookup;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.Bandwidth;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelNotification;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationID;
import java.util.List;
import java.util.Map;
/**
* Control Channel information for a site
*/
public class ControlChannelInformation extends NXDNLayer3Message implements IChannelInformationReceiver
{
private static final int LOCATION_ID = OCTET_1;
private LocationID mLocationID;
private static final IntField FLAGS = IntField.length4(OCTET_4);
//Channel Access = Channel Mode Fields
private static final IntField CONTROL_CHANNEL_1 = IntField.length10(OCTET_4 + 6);
private static final IntField CONTROL_CHANNEL_2 = IntField.length10(OCTET_6 + 6);
//Channel Access = DFA Fields
private static final int HAS_CHANNEL_1 = OCTET_4 + 4;
private static final int HAS_CHANNEL_2 = OCTET_4 + 5;
private static final IntField BANDWIDTH_1 = IntField.length2(OCTET_4 + 6);
private static final IntField CONTROL_CHANNEL_1_OFN = IntField.length16(OCTET_5);
private static final IntField CONTROL_CHANNEL_1_IFN = IntField.length16(OCTET_7);
private static final IntField BANDWIDTH_2 = IntField.length2(OCTET_9 + 6);
private static final IntField CONTROL_CHANNEL_2_OFN = IntField.length16(OCTET_10);
private static final IntField CONTROL_CHANNEL_2_IFN = IntField.length16(OCTET_12);
private NXDNChannel mChannel1;
private NXDNChannel mChannel2;
/**
* Constructs an instance
* @param message content
* @param timestamp of the message
* @param type of message
* @param ran value
* @param lich info
*/
public ControlChannelInformation(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append(getLocationID());
sb.append(" ").append(getFlags()).append(" CONTROL");
if(hasChannel1())
{
sb.append(" 1 ").append(getChannel1());
}
else
{
sb.append(" 1:NOT YET CONFIGURED");
}
if(hasChannel2())
{
sb.append(" 2 ").append(getChannel2());
}
return sb.toString();
}
/**
* Indicates if this message has a channel 1 value.
*/
public boolean hasChannel1()
{
return mChannel1 != null;
}
/**
* Indicates if this message has a channel 2 value.
*/
public boolean hasChannel2()
{
return mChannel2 != null;
}
/**
* Channel 1
* @return null if channel access info hasn't been received, or the channel 1 instance.
*/
public NXDNChannel getChannel1()
{
if(mChannel1 == null)
{
return new NXDNChannelLookup(0);
}
return mChannel1;
}
/**
* Channel 2
* @return null if channel access info hasn't been received, message doesn't have a channel 2, or the channel 2 instance.
*/
public NXDNChannel getChannel2()
{
if(mChannel2 == null)
{
return new NXDNChannelLookup(0);
}
return mChannel2;
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
if(channelAccessInformation != null)
{
if(channelAccessInformation.isDFA())
{
if(getMessage().get(HAS_CHANNEL_1))
{
int ofn = getMessage().getInt(CONTROL_CHANNEL_1_OFN);
int ifn = getMessage().getInt(CONTROL_CHANNEL_1_IFN);
Bandwidth bandwidth = Bandwidth.fromValue(getMessage().getInt(BANDWIDTH_1));
mChannel1 = new NXDNChannelDFA(ofn, ifn, bandwidth);
mChannel1.receive(channelAccessInformation, channelFrequencyMap);
}
if(getMessage().get(HAS_CHANNEL_2))
{
int ofn = getMessage().getInt(CONTROL_CHANNEL_2_OFN);
int ifn = getMessage().getInt(CONTROL_CHANNEL_2_IFN);
Bandwidth bandwidth = Bandwidth.fromValue(getMessage().getInt(BANDWIDTH_2));
mChannel2 = new NXDNChannelDFA(ofn, ifn, bandwidth);
mChannel2.receive(channelAccessInformation, channelFrequencyMap);
}
}
else //Channel mode
{
int channel1 = getMessage().getInt(CONTROL_CHANNEL_1);
mChannel1 = new NXDNChannelLookup(channel1);
mChannel1.receive(channelAccessInformation, channelFrequencyMap);
int channel2 = getMessage().getInt(CONTROL_CHANNEL_2);
if(channel2 > 0)
{
mChannel2 = new NXDNChannelLookup(channel2);
mChannel2.receive(channelAccessInformation, channelFrequencyMap);
}
}
}
}
/**
* Location ID field
*/
public LocationID getLocationID()
{
if(mLocationID == null)
{
mLocationID = new LocationID(getMessage(), LOCATION_ID);
}
return mLocationID;
}
/**
* Flags that amplify the meaning of the control channel information.
*/
public ChannelNotification getFlags()
{
return ChannelNotification.fromValue(getMessage().getInt(FLAGS));
}
@Override
public List<Identifier> getIdentifiers()
{
return getLocationID().getIdentifiers();
}
}

View File

@@ -0,0 +1,131 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.coding.NXDNCRC;
import io.github.dsheirer.module.decode.nxdn.layer3.type.StationIDOption;
import java.util.Collections;
import java.util.List;
/**
* Digital Station ID information (e.g. U.S. FCC Call sign)
*/
public class DigitalStationIDInformation extends NXDNLayer3Message
{
private static final IntField OPTION = IntField.length8(OCTET_1);
private StationIDOption mStationIDOption;
private String mCharacters;
private Boolean mCharacterCRC;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public DigitalStationIDInformation(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append("DIGITAL STATION ID");
if(getStationIDOption().isComplete())
{
sb.append(":").append(getCharacters());
sb.append(" CRC:").append(isValidCharacterCRC() ? "PASS" : "FAIL");
}
else
{
sb.append(" PARTIAL:").append(getCharacters());
}
sb.append(" ").append(getStationIDOption());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
public boolean isValidCharacterCRC()
{
if(mCharacterCRC == null)
{
mCharacterCRC = NXDNCRC.checkMessage(getMessage().getSubMessage(OCTET_2, getMessage().size()), getStationIDOption().getValue() * 8);
}
return mCharacterCRC;
}
/**
* Station ID option that specifies message sequencing and character length.
*/
public StationIDOption getStationIDOption()
{
if(mStationIDOption == null)
{
mStationIDOption = new StationIDOption(getMessage().getInt(OPTION));
}
return mStationIDOption;
}
/**
* Character fragment from this message.
*/
public String getCharacters()
{
if(mCharacters == null)
{
StringBuilder sb = new StringBuilder();
int pointer = OCTET_2;
int length = OCTET_2 + (8 * getStationIDOption().getValue());
length = Math.min(length, getMessage().size());
while(pointer < length)
{
IntField field = IntField.length8(pointer);
sb.append((char)(0xFF & getMessage().getInt(field)));
pointer += 8;
}
mCharacters = sb.toString().trim();
}
return mCharacters;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,90 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CallTimer;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationID;
import java.util.Collections;
import java.util.List;
/**
* Site failure status information
*/
public class FailureStatusInformation extends NXDNLayer3Message
{
private static final int LOCATION_ID = OCTET_1;
private static final IntField CALL_TIMER = IntField.length6(OCTET_5);
private LocationID mLocationID;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public FailureStatusInformation(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append("FAIL-SOFT MODE ").append(getLocationID()).append(" CALL TIMER:").append(getCallTimer());
return sb.toString();
}
/**
* Location that is in failsoft
*/
public LocationID getLocationID()
{
if(mLocationID == null)
{
mLocationID = new LocationID(getMessage(), LOCATION_ID);
}
return mLocationID;
}
/**
* Call timer.
*/
public CallTimer getCallTimer()
{
return CallTimer.fromValue(getMessage().getInt(CALL_TIMER));
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,39 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.Map;
/**
* Interface for messages that require a channel access information structure when calculating channel frequency values.
*
* Note: the channel access information object is from the SiteInformation broadcast message.
*/
public interface IChannelInformationReceiver
{
/**
* Provides the latest channel access information and any optional channel:frequency mapping entries.
* @param channelAccessInformation from the control channel
* @param channelFrequencyMap provided by the user that maps each channel number to a frequency.
*/
void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap);
}

View File

@@ -0,0 +1,65 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import java.util.List;
/**
* Channel idle message
*/
public class Idle extends NXDNLayer3Message
{
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type
* @param ran value
* @param lich info
*/
public Idle(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
if(isTypeD())
{
sb.append("TYPE-D ");
}
sb.append("IDLE");
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -0,0 +1,39 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannel;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationID;
/**
* Neighbor site summary
* @param locationID
* @param channel
*/
public record Neighbor(int id, LocationID locationID, NXDNChannel channel)
{
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("ID: ").append(id).append(" ").append(locationID).append(" ").append(channel);
return sb.toString();
}
}

View File

@@ -0,0 +1,113 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationID;
import io.github.dsheirer.module.decode.nxdn.layer3.type.RestrictionInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ServiceInfo;
import java.util.Collections;
import java.util.List;
/**
* Service information
*/
public class ServiceInformation extends NXDNLayer3Message
{
private static final int LOCATION_ID = OCTET_1;
private static final IntField SERVICE_INFORMATION = IntField.length16(OCTET_4);
private static final int RESTRICTION_INFORMATION = OCTET_6;
private LocationID mLocationID;
private ServiceInfo mServiceInfo;
private RestrictionInformation mRestrictionInformation;
/**
* Constructs an instance
* @param message content
* @param timestamp of the message
* @param type of message
* @param ran value
* @param lich info
*/
public ServiceInformation(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append(getLocationID());
sb.append(" ").append(getServiceInformation());
sb.append(" ").append(getRestrictionInformation());
return sb.toString();
}
/**
* Location ID field
*/
public LocationID getLocationID()
{
if(mLocationID == null)
{
mLocationID = new LocationID(getMessage(), LOCATION_ID);
}
return mLocationID;
}
/**
* Service information field.
*/
public ServiceInfo getServiceInformation()
{
if(mServiceInfo == null)
{
mServiceInfo = new ServiceInfo(getMessage().getInt(SERVICE_INFORMATION));
}
return mServiceInfo;
}
/**
* Restriction information field.
*/
public RestrictionInformation getRestrictionInformation()
{
if(mRestrictionInformation == null)
{
mRestrictionInformation = new RestrictionInformation(getMessage(), RESTRICTION_INFORMATION);
}
return mRestrictionInformation;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,130 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNSite;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.RestrictionInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ServiceInfo;
import io.github.dsheirer.module.decode.nxdn.layer3.type.SystemID;
import java.util.Collections;
import java.util.List;
/**
* Service information for Type-D system
*/
public class ServiceInformationTypeD extends NXDNLayer3Message
{
private static final int SYSTEM_ID = OCTET_1 + 5;
private static final IntField SITE_CODE = IntField.length8(OCTET_4);
private static final IntField SERVICE_INFORMATION = IntField.length16(OCTET_5);
private static final int RESTRICTION_INFORMATION = OCTET_7;
private NXDNSite mSite;
private SystemID mSystemID;
private ServiceInfo mServiceInfo;
private RestrictionInformation mRestrictionInformation;
/**
* Constructs an instance
* @param message content
* @param timestamp of the message
* @param type of message
* @param ran value
* @param lich info
*/
public ServiceInformationTypeD(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append("TYPE-D ").append(getSystemID());
sb.append(" SITE:").append(getSite());
sb.append(" ").append(getServiceInformation());
sb.append(" ").append(getRestrictionInformation());
return sb.toString();
}
/**
* Site information
*/
public NXDNSite getSite()
{
if(mSite == null)
{
mSite = NXDNSite.create(getMessage().getInt(SITE_CODE));
}
return mSite;
}
/**
* Location ID field
*/
public SystemID getSystemID()
{
if(mSystemID == null)
{
mSystemID = new SystemID(getMessage(), SYSTEM_ID);
}
return mSystemID;
}
/**
* Service information field.
*/
public ServiceInfo getServiceInformation()
{
if(mServiceInfo == null)
{
mServiceInfo = new ServiceInfo(getMessage().getInt(SERVICE_INFORMATION));
}
return mServiceInfo;
}
/**
* Restriction information field.
*/
public RestrictionInformation getRestrictionInformation()
{
if(mRestrictionInformation == null)
{
mRestrictionInformation = new RestrictionInformation(getMessage(), RESTRICTION_INFORMATION);
}
return mRestrictionInformation;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,252 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.broadcast;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelLookup;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelStructure;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationID;
import io.github.dsheirer.module.decode.nxdn.layer3.type.RestrictionInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ServiceInfo;
import java.util.List;
import java.util.Map;
/**
* Site information
*/
public class SiteInformation extends NXDNLayer3Message implements IChannelInformationReceiver
{
private static final int LOCATION_ID = OCTET_1;
private static final int CHANNEL_STRUCTURE = OCTET_4;
private static final IntField SERVICE_INFORMATION = IntField.length16(OCTET_6);
private static final int RESTRICTION_INFORMATION = OCTET_8;
private static final int CHANNEL_ACCESS_INFORMATION = OCTET_11;
private static final IntField VERSION_NUMBER = IntField.length8(OCTET_14);
private static final IntField ADJACENT_SITE_ALLOCATION = IntField.length4(OCTET_15);
private static final IntField CONTROL_CHANNEL_1 = IntField.range(OCTET_15 + 4, OCTET_16 + 5);
private static final IntField CONTROL_CHANNEL_2 = IntField.range(OCTET_16 + 6, OCTET_17 + 7);
private LocationID mLocationID;
private ChannelStructure mChannelStructure;
private ServiceInfo mServiceInfo;
private RestrictionInformation mRestrictionInformation;
private ChannelAccessInformation mChannelAccessInformation;
private NXDNChannelLookup mChannel1;
private NXDNChannelLookup mChannel2;
/**
* Constructs an instance
* @param message content
* @param timestamp of the message
* @param type of message
* @param ran value
* @param lich info
*/
public SiteInformation(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append(getLocationID());
if(hasChannel1())
{
sb.append(" CONTROL CHANNEL 1:").append(getChannel1());
if(hasChannel2())
{
sb.append(" CONTROL CHANNEL 2:").append(getChannel2());
}
}
else
{
sb.append(" SITE USES DIRECT FREQUENCY ALLOCATION (DFA) CHANNELS");
}
sb.append(" ADJACENT SITES:").append(getAdjacentSiteAllocation());
sb.append(" NXDN VER:").append(getVersionNumber());
sb.append(" ").append(getServiceInformation());
sb.append(" ").append(getRestrictionInformation());
return sb.toString();
}
/**
* Control channel 1
* @return channel instance or null if the site is configured for DFA
*/
public NXDNChannelLookup getChannel1()
{
if(mChannel1 == null && getChannelAccessInformation().isChannel())
{
mChannel1 = new NXDNChannelLookup(getMessage().getInt(CONTROL_CHANNEL_1));
}
return mChannel1;
}
/**
* Indicates if this message has a first control channel value.
*/
public boolean hasChannel1()
{
return getChannel1() != null;
}
/**
* Control channel 2
* @return channel instance or null if the site is configured for DFA
*/
public NXDNChannelLookup getChannel2()
{
if(mChannel2 == null && getChannelAccessInformation().isChannel())
{
int channelNumber = getMessage().getInt(CONTROL_CHANNEL_2);
if(channelNumber > 0)
{
mChannel2 = new NXDNChannelLookup(channelNumber);
}
}
return mChannel2;
}
/**
* Indicates if this message has a first control channel value.
*/
public boolean hasChannel2()
{
return getChannel2() != null;
}
/**
* Location ID field
*/
public LocationID getLocationID()
{
if(mLocationID == null)
{
mLocationID = new LocationID(getMessage(), LOCATION_ID);
}
return mLocationID;
}
/**
* Channel structure field.
*/
public ChannelStructure getChannelStructure()
{
if(mChannelStructure == null)
{
mChannelStructure = new ChannelStructure(getMessage(), CHANNEL_STRUCTURE);
}
return mChannelStructure;
}
/**
* Service information field.
*/
public ServiceInfo getServiceInformation()
{
if(mServiceInfo == null)
{
mServiceInfo = new ServiceInfo(getMessage().getInt(SERVICE_INFORMATION));
}
return mServiceInfo;
}
/**
* Restriction information field.
*/
public RestrictionInformation getRestrictionInformation()
{
if(mRestrictionInformation == null)
{
mRestrictionInformation = new RestrictionInformation(getMessage(), RESTRICTION_INFORMATION);
}
return mRestrictionInformation;
}
/**
* Channel access information field.
*/
public ChannelAccessInformation getChannelAccessInformation()
{
if(mChannelAccessInformation == null)
{
mChannelAccessInformation = new ChannelAccessInformation(getMessage(), CHANNEL_ACCESS_INFORMATION);
}
return mChannelAccessInformation;
}
/**
* NXDN version number. Note: version numbers are not further defined.
*/
public int getVersionNumber()
{
return getMessage().getInt(VERSION_NUMBER);
}
/**
* Number of adjacent trunking sites.
*/
public int getAdjacentSiteAllocation()
{
return getMessage().getInt(ADJACENT_SITE_ALLOCATION);
}
@Override
public List<Identifier> getIdentifiers()
{
return getLocationID().getIdentifiers();
}
/**
* Provides the latest channel access information and any optional channel:frequency mapping entries.
* @param channelAccessInformation from the control channel
* @param channelFrequencyMap provided by the user that maps each channel number to a frequency.
*/
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
if(hasChannel1())
{
getChannel1().receive(channelAccessInformation, channelFrequencyMap);
}
if(hasChannel2())
{
getChannel2().receive(channelAccessInformation, channelFrequencyMap);
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.call;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.FragmentedIntField;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.edac.Golay23;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.NXDNMessage;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer2.SACCHFragment;
import io.github.dsheirer.module.decode.nxdn.layer3.type.AudioCodec;
import io.github.dsheirer.util.ByteUtil;
import java.nio.ByteOrder;
import java.util.Collections;
import java.util.List;
/**
* NXDN audio frame data
*/
public class Audio extends NXDNMessage
{
private static final FragmentedIntField VECTOR_C0 = FragmentedIntField.of(0, 4, 8, 12, 16, 20, 24, 28, 32,
36, 40, 44, 48, 52, 56, 60, 64, 68, 1, 5, 9, 13, 17, 21);
private static final FragmentedIntField VECTOR_C1 = FragmentedIntField.of(25, 29, 33, 37, 41, 45, 49, 53,
57, 61, 65, 69, 2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42);
private static final IntField VECTOR_U0 = IntField.length12(0);
private final AudioCodec mAudioCodec;
private final List<byte[]> mAudioFrames;
private SACCHFragment mSACCHFragment;
/**
* Constructs an instance
*
* @param audioCodec code used for the channel
* @param frames of raw AMBE audio
* @param timestamp for the message
* @param ran from the SR field
* @param lich from the SR field
*/
public Audio(AudioCodec audioCodec, List<byte[]> frames, long timestamp, int ran, LICH lich)
{
super(new CorrectedBinaryMessage(0), timestamp, ran, lich);
mAudioCodec = audioCodec;
mAudioFrames = frames;
boolean valid = false;
//Check that at least 1 audio frame is valid.
for(byte[] frame: frames)
{
valid |= isValid(frame);
}
setValid(valid);
}
/**
* Optional SACCH fragment
*/
public SACCHFragment getSACCHFragment()
{
return mSACCHFragment;
}
/**
* Indicates if this audio has an optional SACCH fragment
*/
public boolean hasSACCHFragment()
{
return mSACCHFragment != null;
}
/**
* Sets the optional SACCH fragment
*/
public void setSACCHFragment(SACCHFragment fragment)
{
mSACCHFragment = fragment;
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder().append("AUDIO FRAMES");
for(byte[] frame: mAudioFrames)
{
sb.append(" ").append(ByteUtil.toHexString(frame));
}
return sb.toString();
}
/**
* Audio frame data
*/
public List<byte[]> getAudioFrames()
{
return mAudioFrames;
}
/**
* AMBE+ audio codec format
* @return full or half rate
*/
public AudioCodec getAudioCodec()
{
return mAudioCodec;
}
/**
* Performs error detection and correction to determine if the frame is valid
*/
public static boolean isValid(byte[] audio)
{
CorrectedBinaryMessage cbm = CorrectedBinaryMessage.from(audio, ByteOrder.LITTLE_ENDIAN);
CorrectedBinaryMessage vc0 = new CorrectedBinaryMessage(24);
vc0.load(0, 24, cbm.getInt(VECTOR_C0));
//TODO: use Golay24 here, but the Golay24 implementation isn't as robust as the Golay23 implementation
int error1 = Golay23.checkAndCorrect(vc0, 0);
if(error1 < 4)
{
CorrectedBinaryMessage vc1 = new CorrectedBinaryMessage(23);
vc1.load(0, 23, cbm.getInt(VECTOR_C1));
CorrectedBinaryMessage modulationVector = getModulationVector(vc0.getInt(VECTOR_U0));
vc1.xor(modulationVector);
int error2 = Golay23.checkAndCorrect(vc1, 0);
return error2 < 4;
}
return false;
}
/**
* Generates the bit modulation vector that is xor'd to vector C1 using U0 as the seed.
*
* @param seed for the modulation vector
* @return modulation vector bit sequence to descramble vector C1
*/
private static CorrectedBinaryMessage getModulationVector(int seed)
{
CorrectedBinaryMessage modulationVector = new CorrectedBinaryMessage(23);
//alg 52
int prX = 16 * seed;
for(int x = 0; x < 23; x++)
{
//alg 53 - simplified [... - 65536 * floor((173 * pr(n-1) + 13849) / 65536)] to modulus operation
prX = (173 * prX + 13849) % 65536;
//alg 54 - values 32768 and above are a 1 and below is a 0 (default)
if(prX >= 32768)
{
modulationVector.set(x);
}
}
return modulationVector;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,73 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.call;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNEncryptionKey;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CallOption;
import io.github.dsheirer.protocol.Protocol;
/**
* Base call message
*/
public abstract class Call extends CallControl
{
protected static final IntField CALL_OPTION = IntField.length5(OCTET_2 + 3);
private static final IntField CIPHER_TYPE = IntField.length2(OCTET_7);
private static final IntField KEY_ID = IntField.length6(OCTET_7 + 2);
private EncryptionKeyIdentifier mEncryptionKeyIdentifier;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public Call(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
/**
* Call options for this call.
*/
public abstract CallOption getCallOption();
/**
* Encryption algorithm and key ID.
*/
public EncryptionKeyIdentifier getEncryptionKeyIdentifier()
{
if(mEncryptionKeyIdentifier == null)
{
mEncryptionKeyIdentifier = EncryptionKeyIdentifier.create(Protocol.NXDN, NXDNEncryptionKey.create(getMessage().getInt(CIPHER_TYPE), getMessage().getInt(KEY_ID)));
}
return mEncryptionKeyIdentifier;
}
}

View File

@@ -0,0 +1,81 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.call;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CallOption;
import io.github.dsheirer.module.decode.nxdn.layer3.type.VoiceCallOption;
import java.util.List;
/**
* Type-D call connection response to respond to an SU call connection across sites.
*/
public class CallConnectionResponse extends CallWithOptionalLocation
{
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public CallConnectionResponse(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append("CALL CONNECTION RESPONSE FROM:").append(getSource());
sb.append(" TO:").append(getDestination());
if(getEncryptionKeyIdentifier().isEncrypted())
{
sb.append(" ").append(getEncryptionKeyIdentifier());
}
return sb.toString();
}
@Override
protected int getLocationOffset()
{
return OCTET_8;
}
@Override
public CallOption getCallOption()
{
return new VoiceCallOption(0);
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of(getSource(), getDestination());
}
}

View File

@@ -0,0 +1,132 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.call;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNRadioIdentifier;
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkgroupIdentifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CallControlOption;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CallType;
/**
* Base call control message
*/
public abstract class CallControl extends NXDNLayer3Message
{
private static final IntField CC_OPTION = IntField.length8(OCTET_1);
private static final IntField CALL_TYPE = IntField.length3(OCTET_2);
private CallControlOption mCallControlOption;
protected NXDNRadioIdentifier mSourceIdentifier;
protected IntegerIdentifier mDestinationIdentifier;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public CallControl(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
/**
* Call control options for the call
* @return options
*/
public CallControlOption getCallControlOption()
{
if(mCallControlOption == null)
{
mCallControlOption = new CallControlOption(getMessage().getInt(CC_OPTION));
}
return mCallControlOption;
}
/**
* Call type for this voice call.
* @return type
*/
public CallType getCallType()
{
return CallType.fromValue(getMessage().getInt(CALL_TYPE));
}
/**
* Source radio ID
* @return source identifier.
*/
public NXDNRadioIdentifier getSource()
{
if(mSourceIdentifier == null)
{
if(isTypeD())
{
mSourceIdentifier = NXDNRadioIdentifier.createTypeDFrom(getMessage().getInt(IDENTIFIER_OCTET_3));
}
else
{
mSourceIdentifier = NXDNRadioIdentifier.createFrom(getMessage().getInt(IDENTIFIER_OCTET_3));
}
}
return mSourceIdentifier;
}
/**
* Destination identifier, either talkgroup or radio.
* @return destination identifier
*/
public IntegerIdentifier getDestination()
{
if(mDestinationIdentifier == null)
{
if(isTypeD())
{
mDestinationIdentifier = switch (getCallType())
{
case GROUP_BROADCAST, GROUP_CONFERENCE -> NXDNTalkgroupIdentifier
.createTypeDTo(getMessage().getInt(IDENTIFIER_OCTET_5));
default -> NXDNRadioIdentifier.createTypeDTo(getMessage().getInt(IDENTIFIER_OCTET_5));
};
}
else
{
mDestinationIdentifier = switch (getCallType())
{
case GROUP_BROADCAST, GROUP_CONFERENCE -> NXDNTalkgroupIdentifier
.createTo(getMessage().getInt(IDENTIFIER_OCTET_5));
default -> NXDNRadioIdentifier.createTo(getMessage().getInt(IDENTIFIER_OCTET_5));
};
}
}
return mDestinationIdentifier;
}
}

View File

@@ -0,0 +1,118 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.call;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNRadioIdentifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationID;
import io.github.dsheirer.module.decode.nxdn.layer3.type.LocationOrSystemIDOption;
import io.github.dsheirer.module.decode.nxdn.layer3.type.SystemID;
/**
* Call message with optional location field for fully qualified source/destination identifiers.
*/
public abstract class CallWithOptionalLocation extends Call
{
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public CallWithOptionalLocation(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
/**
* Offset to the location ID and option field within the message. This is implemented by the subclass.
*/
protected abstract int getLocationOffset();
/**
* Source identifier. Overrides the default method to optionally create a fully qualified identifier that includes
* the visitor source system ID.
*/
@Override
public NXDNRadioIdentifier getSource()
{
//Disabling fully qualified identifier until we're sure we need this.
// if(mSourceIdentifier == null && getCallControlOption().hasLocationId() && getLocationIDOption().isSource())
// {
// mSourceIdentifier = NXDNFullyQualifiedRadioIdentifier.createFrom(getLocationID().getSystem().getValue(),
// getMessage().getInt(IDENTIFIER_OCTET_3));
// mSourceIdentifier = NXDNRadioIdentifier.createFrom(getMessage().getInt(IDENTIFIER_OCTET_3));
// }
return super.getSource();
}
/**
* Destination identifier. Overrides the default method to optionally create a fully qualified identifier that
* includes the visitor source system ID.
*/
@Override
public IntegerIdentifier getDestination()
{
//Disabling fully qualified identifier until we're sure we need this.
// if(mDestinationIdentifier == null && getCallControlOption().hasLocationId() && getLocationIDOption().isDestination())
// {
// mDestinationIdentifier = switch (getCallType())
// {
// case GROUP_BROADCAST, GROUP_CONFERENCE -> NXDNFullyQualifiedTalkgroupIdentifier
// .createTo(getLocationID().getSystem().getValue(), getMessage().getInt(IDENTIFIER_OCTET_5));
// default -> NXDNFullyQualifiedRadioIdentifier.createTo(getLocationID().getSystem().getValue(),
// getMessage().getInt(IDENTIFIER_OCTET_5));
// };
// }
return super.getDestination();
}
/**
* Location ID Option field.
*/
public LocationOrSystemIDOption getLocationIDOption()
{
IntField field = IntField.length2(getLocationOffset());
return LocationOrSystemIDOption.fromValue(getMessage().getInt(field));
}
/**
* Creates a location ID field parser.
*/
public LocationID getLocationID()
{
//In Type-D systems, this is a System ID field
if(isTypeD())
{
return new SystemID(getMessage(), getLocationOffset() + 5);
}
return new LocationID(getMessage(), getLocationOffset() + 5, true);
}
}

View File

@@ -0,0 +1,60 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.call;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.DataCallOption;
/**
* Data call base implementation
*/
public abstract class DataCall extends Call
{
private DataCallOption mDataCallOption;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public DataCall(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
/**
* Data call options
*/
public DataCallOption getCallOption()
{
if(mDataCallOption == null)
{
mDataCallOption = new DataCallOption(getMessage().getInt(CALL_OPTION));
}
return mDataCallOption;
}
}

View File

@@ -0,0 +1,100 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.call;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ErrorBlockFlag;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ResponseInformation;
import java.util.List;
/**
* Response to confirmed data call
*/
public class DataCallAcknowledge extends DataCall
{
private static final IntField RESPONSE_INFORMATION = IntField.length16(OCTET_7);
private static final IntField ERROR_BLOCK_FLAG = IntField.length16(OCTET_9);
private ResponseInformation mResponseInformation;
private ErrorBlockFlag mErrorBlockFlag;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public DataCallAcknowledge(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
sb.append("DATA CALL ACKNOWLEDGE");
sb.append(" FROM:").append(getSource());
sb.append(" TO:").append(getDestination());
sb.append(" ").append(getResponseInformation());
sb.append(" ").append(getErrorBlockFlag());
return sb.toString();
}
/**
* Response with acknowledge or no-acknowledge.
* @return response information.
*/
public ResponseInformation getResponseInformation()
{
if(mResponseInformation == null)
{
mResponseInformation = new ResponseInformation(getMessage().getInt(RESPONSE_INFORMATION));
}
return mResponseInformation;
}
/**
* Flags indicating error state with any received blocks.
* @return flags
*/
public ErrorBlockFlag getErrorBlockFlag()
{
if(mErrorBlockFlag == null)
{
mErrorBlockFlag = new ErrorBlockFlag(getMessage().getInt(ERROR_BLOCK_FLAG));
}
return mErrorBlockFlag;
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of(getSource(), getDestination(), getEncryptionKeyIdentifier());
}
}

View File

@@ -0,0 +1,151 @@
/*
* *****************************************************************************
* 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.nxdn.layer3.call;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannel;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelDFA;
import io.github.dsheirer.module.decode.nxdn.channel.NXDNChannelLookup;
import io.github.dsheirer.module.decode.nxdn.layer2.LICH;
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNMessageType;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.IChannelInformationReceiver;
import io.github.dsheirer.module.decode.nxdn.layer3.type.Bandwidth;
import io.github.dsheirer.module.decode.nxdn.layer3.type.CallTimer;
import io.github.dsheirer.module.decode.nxdn.layer3.type.ChannelAccessInformation;
import java.util.List;
import java.util.Map;
/**
* Data call assignment
*/
public class DataCallAssignment extends DataCallWithOptionalLocation implements IChannelInformationReceiver
{
private static final IntField CALL_TIMER = IntField.length6(OCTET_7);
private static final IntField CHANNEL_NUMBER = IntField.length10(OCTET_7 + 6);
private static final IntField BANDWIDTH = IntField.length2(OCTET_7 + 6);
private static final IntField OFN = IntField.length16(OCTET_8);
private static final IntField IFN = IntField.length16(OCTET_10);
private static final int LOCATION_ID_OFFSET = OCTET_12;
private NXDNChannel mChannel;
/**
* Constructs an instance
*
* @param message with binary data
* @param timestamp for the message
* @param type of message
* @param ran value
* @param lich info
*/
public DataCallAssignment(CorrectedBinaryMessage message, long timestamp, NXDNMessageType type, int ran, LICH lich)
{
super(message, timestamp, type, ran, lich);
}
@Override
protected int getLocationOffset()
{
return LOCATION_ID_OFFSET;
}
@Override
public String toString()
{
StringBuilder sb = getMessageBuilder();
if(getCallControlOption().isEmergency())
{
sb.append("EMERGENCY ");
}
if(getCallControlOption().isPriorityPaging())
{
sb.append("PRIORITY PAGING ");
}
sb.append(getCallType()).append(" DATA CALL ASSIGNMENT");
sb.append(" FROM:").append(getSource());
sb.append(" TO:").append(getDestination());
sb.append(" ").append(getEncryptionKeyIdentifier());
sb.append(getCallOption());
if(hasChannel())
{
sb.append(" CHANNEL:").append(getChannel());
}
else
{
sb.append(" CHANNEL: NOT CONFIGURED");
}
sb.append(" TIMER:").append(getCallTimer());
return sb.toString();
}
@Override
public void receive(ChannelAccessInformation channelAccessInformation, Map<Integer, ChannelFrequency> channelFrequencyMap)
{
if(mChannel == null)
{
if(channelAccessInformation.isChannel())
{
mChannel = new NXDNChannelLookup(getMessage().getInt(CHANNEL_NUMBER));
}
else
{
mChannel = new NXDNChannelDFA(getMessage().getInt(OFN), getMessage().getInt(IFN),
Bandwidth.fromValue(getMessage().getInt(BANDWIDTH)));
}
}
mChannel.receive(channelAccessInformation, channelFrequencyMap);
}
/**
* Indicates if the channel information is configured.
*/
public boolean hasChannel()
{
return getChannel() != null;
}
/**
* Channel for the call
*/
public NXDNChannel getChannel()
{
return mChannel;
}
/**
* Call timer.
*/
public CallTimer getCallTimer()
{
return CallTimer.fromValue(getMessage().getInt(CALL_TIMER));
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of(getSource(), getDestination(), getEncryptionKeyIdentifier());
}
}

Some files were not shown because too many files have changed in this diff Show More