diff --git a/src/main/java/io/github/dsheirer/identifier/dcs/DCSIdentifier.java b/src/main/java/io/github/dsheirer/identifier/dcs/DCSIdentifier.java
new file mode 100644
index 00000000..6c1bca4a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/identifier/dcs/DCSIdentifier.java
@@ -0,0 +1,48 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.identifier.dcs;
+
+import io.github.dsheirer.identifier.Form;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierClass;
+import io.github.dsheirer.identifier.Role;
+import io.github.dsheirer.module.decode.dcs.DCSCode;
+import io.github.dsheirer.protocol.Protocol;
+
+/**
+ * Digital Coded Squelch code identifier. This is decoded from the transmitted signal and represents the FROM role.
+ */
+public class DCSIdentifier extends Identifier
+{
+ /**
+ * Constructs an instance.
+ * @param code detected.
+ */
+ public DCSIdentifier(DCSCode code)
+ {
+ super(code, IdentifierClass.USER, Form.TONE, Role.FROM);
+ }
+
+ @Override
+ public Protocol getProtocol()
+ {
+ return Protocol.DCS;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
index 9189b7c8..551f5994 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
@@ -39,6 +39,9 @@ import io.github.dsheirer.module.decode.am.AMDecoderState;
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
+import io.github.dsheirer.module.decode.dcs.DCSDecoder;
+import io.github.dsheirer.module.decode.dcs.DCSDecoderState;
+import io.github.dsheirer.module.decode.dcs.DCSMessageFilter;
import io.github.dsheirer.module.decode.dmr.DMRDecoder;
import io.github.dsheirer.module.decode.dmr.DMRDecoderState;
import io.github.dsheirer.module.decode.dmr.DMRTrafficChannelManager;
@@ -456,6 +459,10 @@ public class DecoderFactory
{
switch(auxDecoder)
{
+ case DCS:
+ modules.add(new DCSDecoder());
+ modules.add(new DCSDecoderState());
+ break;
case FLEETSYNC2:
modules.add(new Fleetsync2Decoder());
modules.add(new Fleetsync2DecoderState());
@@ -516,6 +523,9 @@ public class DecoderFactory
switch(decoder)
{
+ case DCS:
+ filters.add(new DCSMessageFilter());
+ break;
case FLEETSYNC2:
filters.add(new FleetsyncMessageFilter());
break;
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
index 118632cd..02311d9c 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2022 Dennis Sheirer
+ * Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,7 +19,6 @@
package io.github.dsheirer.module.decode;
import io.github.dsheirer.protocol.Protocol;
-
import java.util.EnumSet;
/**
@@ -39,6 +38,7 @@ public enum DecoderType
P25_PHASE2("P25 Phase 2", "P25-2", Protocol.APCO25_PHASE2),
//Auxiliary Decoders
+ DCS("Digital Coded Squelch (DCS)", "DCS", Protocol.DCS),
FLEETSYNC2("Fleetsync II", "Fleetsync2", Protocol.FLEETSYNC),
LJ_1200("LJ1200 173.075", "LJ1200", Protocol.LOJACK),
MDC1200("MDC1200", "MDC1200", Protocol.MDC1200),
@@ -73,7 +73,8 @@ public enum DecoderType
* Auxiliary decoders that operate on in-band signalling in the decoded audio channel
*/
public static final EnumSet AUX_DECODERS =
- EnumSet.of(DecoderType.FLEETSYNC2,
+ EnumSet.of(DecoderType.DCS,
+ DecoderType.FLEETSYNC2,
DecoderType.LJ_1200,
DecoderType.MDC1200,
DecoderType.TAIT_1200);
diff --git a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java
index dcfb966a..b07fd1f4 100644
--- a/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java
+++ b/src/main/java/io/github/dsheirer/module/decode/analog/AnalogDecoderState.java
@@ -121,6 +121,7 @@ public abstract class AnalogDecoderState extends DecoderState implements ISource
startCallEvent();
}
+ getIdentifierCollection().update(getTalkgroupIdentifier());
mDecodeEvent.update(System.currentTimeMillis());
broadcast(mDecodeEvent);
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dcs/DCSCode.java b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSCode.java
new file mode 100644
index 00000000..822b1b84
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSCode.java
@@ -0,0 +1,281 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.dcs;
+
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * DCS codes as defined in ETSI TS 103 236 V1.1.1
+ *
+ * Code values are the bit reversed integer values of the 23-bit binary sequence representation. The values are bit
+ * reversed for normal (N) codes and as specified in the ETSI chart for the inverted (I) codes to align with the
+ * transmitted format.
+ */
+public enum DCSCode
+{
+ //Inverted codes
+ I023(7747603),
+ I025(7043093),
+ I026(6674454),
+ I031(5371929),
+ I032(6248474),
+ I043(5990435),
+ I047(1038375),
+ I051(8169513),
+ I054(7292972),
+ I065(6101045),
+ I071(6789177),
+ I072(6895674),
+ I073(3041339),
+ I074(7632956),
+ I114(3532876),
+ I115(7518285),
+ I116(8132686),
+ I125(505941),
+ I131(4012121),
+ I132(3381338),
+ I134(3070044),
+ I143(3647587),
+ I152(2017386),
+ I155(4511853),
+ I156(4880494),
+ I162(7063666),
+ I165(3266677),
+ I172(391290),
+ I174(1620092),
+ I205(7248005),
+ I223(6875283),
+ I226(8063126),
+ I243(4569251),
+ I244(2074788),
+ I245(5830821),
+ I251(6453417),
+ I261(1538225),
+ I263(6195379),
+ I265(4442293),
+ I271(7948473),
+ I306(850118),
+ I311(3725513),
+ I315(7104717),
+ I331(2353369),
+ I343(2717923),
+ I346(3840230),
+ I351(964841),
+ I364(6838516),
+ I365(3082485),
+ I371(1411321),
+ I411(7825673),
+ I412(7981322),
+ I413(4102411),
+ I423(4954387),
+ I431(7100697),
+ I432(6486298),
+ I445(8096037),
+ I464(2615604),
+ I465(6338869),
+ I466(7215414),
+ I503(3959107),
+ I506(3115334),
+ I516(4307278),
+ I532(932186),
+ I546(1698150),
+ I565(817525),
+ I606(6134150),
+ I612(6756746),
+ I624(1005972),
+ I627(129431),
+ I631(7506329),
+ I632(8137114),
+ I654(4995500),
+ I662(2390450),
+ I664(3750324),
+ I703(2275779),
+ I712(776650),
+ I723(3770835),
+ I731(1984985),
+ I732(1108442),
+ I734(895452),
+ I743(1366499),
+ I754(2161132),
+
+ //Normal codes
+ N023(6557239),
+ N025(5508971),
+ N026(3411411),
+ N031(4984773),
+ N032(2887037),
+ N043(6425453),
+ N047(7474680),
+ N051(4852383),
+ N054(1706363),
+ N065(5639261),
+ N071(5115123),
+ N072(3018315),
+ N073(7211834),
+ N074(1969943),
+ N114(1641430),
+ N115(5836455),
+ N116(3738655),
+ N125(5574384),
+ N131(5049950),
+ N132(2952422),
+ N134(1904058),
+ N143(6490870),
+ N152(2820540),
+ N155(5967249),
+ N156(3870505),
+ N162(2558443),
+ N165(5705158),
+ N172(3084240),
+ N174(2035340),
+ N205(5278907),
+ N223(6589323),
+ N226(3442799),
+ N243(6459089),
+ N244(1215228),
+ N245(5410701),
+ N251(4886307),
+ N261(4624244),
+ N263(6719677),
+ N265(5671393),
+ N271(5146959),
+ N306(3248024),
+ N311(4820366),
+ N315(5868315),
+ N331(5082082),
+ N343(6524746),
+ N346(3378350),
+ N351(4951736),
+ N364(1543435),
+ N365(5736570),
+ N371(5212372),
+ N411(4737911),
+ N412(2640335),
+ N413(6835390),
+ N423(6573289),
+ N431(5000475),
+ N432(2903971),
+ N445(5392623),
+ N464(1461234),
+ N465(5656195),
+ N466(3558459),
+ N503(6376222),
+ N506(3229946),
+ N516(3755713),
+ N532(2969144),
+ N546(3361740),
+ N565(5721880),
+ N606(3198173),
+ N612(2673779),
+ N624(1363320),
+ N627(7655360),
+ N631(5032103),
+ N632(2935327),
+ N654(1756697),
+ N662(2543378),
+ N664(1494606),
+ N703(6409890),
+ N712(2739688),
+ N723(6670542),
+ N731(5097788),
+ N732(3001220),
+ N734(1952472),
+ N743(6540692),
+ N754(1822594),
+ UNKNOWN(0);
+
+ private int mValue;
+
+ DCSCode(int value)
+ {
+ mValue = value;
+ }
+
+ /**
+ * Inverted DCS codes
+ */
+ private static final EnumSet INVERTED_CODES = EnumSet.range(I023, I754);
+
+ /**
+ * Lookup map for quickly finding a DCS code from the transmitted value.
+ */
+ private static final Map CODE_MAP = new HashMap<>();
+
+ static
+ {
+ for(DCSCode code: EnumSet.range(I023, N754))
+ {
+ CODE_MAP.put(code.getValue(), code);
+ }
+ }
+
+ /**
+ * Integer value of the code from transmitted bits.
+ * @return value
+ */
+ public int getValue()
+ {
+ return mValue;
+ }
+
+ /**
+ * Indicates if this is an inverted DCS code (true) or a normal DCS code (false).
+ * @return true if inverted.
+ */
+ public boolean isInverted()
+ {
+ return INVERTED_CODES.contains(this);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "DCS-" + name().substring(1, 4) + (isInverted() ? " Inverted" : "");
+ }
+
+ /**
+ * Indicates if the value maps to a known DCS code.
+ * @param value to test
+ * @return true if the value matches a DCS code.
+ */
+ public static boolean hasValue(int value)
+ {
+ return CODE_MAP.containsKey(value);
+ }
+
+ /**
+ * Get the DCS code that matches the value, otherwise return UNKNOWN.
+ * @param value to lookup
+ * @return matching DCS code or UNKNOWN.
+ */
+ public static DCSCode fromValue(int value)
+ {
+ DCSCode code = CODE_MAP.get(value);
+
+ if(code == null)
+ {
+ code = UNKNOWN;
+ }
+
+ return code;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dcs/DCSDecoder.java b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSDecoder.java
new file mode 100644
index 00000000..f2f535f7
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSDecoder.java
@@ -0,0 +1,293 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.dcs;
+
+import io.github.dsheirer.dsp.filter.FilterFactory;
+import io.github.dsheirer.dsp.filter.design.FilterDesignException;
+import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
+import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
+import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
+import io.github.dsheirer.module.decode.Decoder;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.real.IRealBufferListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Digital Coded Squelch (DCS) demodulator/decoder designed to work with FM demodulated 8 kHz input audio samples and
+ * decode 134.4-baud signalling present in the unfiltered demodulated audio stream.
+ *
+ * This decoder monitors the incoming sample stream looking for either a positive or negative sequence of samples where
+ * the slope of these values exceeds a threshold and causes the symbol state to flip. The symbol state remains across
+ * successive baud periods until the next opposing slope state change sequence is detected.
+ *
+ * This decoder uses a slope detector to decode the 134.4 bits per second stream and identify the DCS code. It is
+ * designed to work with 8 kHz audio sample rate. Each baud period is (8,000 / 134.4) = 59.52 samples per symbol and
+ * a full DCS tone sequence is 23 bits * 59.52 samples/bit = 1369 samples. The DCS tone sequence is continuously
+ * transmitted at a rate of 134.4 bits-per-second / 23 = 5.84 repeats per second while the source continues transmitting.
+ */
+public class DCSDecoder extends Decoder implements IRealBufferListener, Listener
+{
+ private static final Logger mLog = LoggerFactory.getLogger(DCSDecoder.class);
+ //Slope calculation sumXX value is always the same for 59 evenly spaced samples
+ private static final double SLOPE_CALCULATION_SUM_XX = 2247.5;
+ private static final float BAUD_LENGTH = 8000.0f / 134.4f;
+ private static final float SLOPE_THRESHOLD = 0.002750f;
+ private static final int MAX_ONES_SEQUENCE = 6;
+ private static final int OVERLAP = (int)Math.ceil(BAUD_LENGTH);
+ private static final int POST_TRANSITION_SAMPLES_TO_SKIP = 30;
+ private static final int SLOPE_CALCULATION_PERIOD = 30;
+ private static final int IDEAL_SYMBOL_TRANSITION_MIN = 11;
+ private static final int IDEAL_SYMBOL_TRANSITION_MAX = 19;
+ private static final int CODE_MASK = 0x7FFFFF; //23-bit mask
+ private static float[] sLowPassFilterCoefficients;
+ private boolean mSymbol = false;
+ private float mBaudCounter = 0f;
+ private float mMaxSlope = 0;
+ private float[] mResidual = new float[OVERLAP];
+ private int mCode = 0;
+ private int mExcessiveOneSequenceCounter = 0;
+
+ static
+ {
+ FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
+ .sampleRate(8000)
+ .gridDensity(16)
+ .oddLength(true)
+ .passBandCutoff(200)
+ .passBandAmplitude(1.0)
+ .passBandRipple(0.01)
+ .stopBandStart(300)
+ .stopBandAmplitude(0.0)
+ .stopBandRipple(0.03) //Approximately 60 dB attenuation
+ .build();
+
+ try
+ {
+ RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
+
+ if(designer.isValid())
+ {
+ sLowPassFilterCoefficients = designer.getImpulseResponse();
+ }
+ }
+ catch(FilterDesignException fde)
+ {
+ mLog.error("Filter design error", fde);
+ }
+ }
+
+ private IRealFilter mLowPassFilter = FilterFactory.getRealFilter(sLowPassFilterCoefficients);
+
+ /**
+ * Constructs an instance
+ */
+ public DCSDecoder()
+ {
+ }
+
+ /**
+ * Decoder type
+ */
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.DCS;
+ }
+
+ /**
+ * Implementation of the IRealBufferListener interface
+ */
+ @Override
+ public Listener getBufferListener()
+ {
+ return this::receive;
+ }
+
+ /**
+ * Processes the demodulated 8 kHz audio samples to extract the 300-baud LTR signalling and delivers the decoded
+ * symbol array to the registered listener.
+ * @param samples to demodulate.
+ */
+ public void receive(float[] samples)
+ {
+ if(getMessageListener() != null)
+ {
+ float[] filtered = mLowPassFilter.filter(samples);
+ float[] buffer = new float[filtered.length + OVERLAP];
+ int samplesToSkip = 0;
+ int timingAdjust = 0;
+ float slope = 0;
+
+ try
+ {
+ System.arraycopy(mResidual, 0, buffer, 0, mResidual.length);
+ System.arraycopy(filtered, 0, buffer, mResidual.length, filtered.length);
+ System.arraycopy(filtered, filtered.length - OVERLAP, mResidual, 0, OVERLAP);
+
+ for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer++)
+ {
+ //Don't calculate slope if we're within the baud period following the last symbol transition
+ if(samplesToSkip > 0)
+ {
+ samplesToSkip--;
+ }
+ else
+ {
+ slope = calculateSlope(buffer, bufferPointer);
+
+ if(mSymbol)
+ {
+ //Look for crest of slope at lowest point below the negative slope threshold for transition
+ if(slope > mMaxSlope && mMaxSlope < -SLOPE_THRESHOLD)
+ {
+ mSymbol = false;
+
+ //Coarse symbol timing adjust - ideal toggle is middle of baud, between 11 & 19 samples
+ if(mBaudCounter < IDEAL_SYMBOL_TRANSITION_MIN)
+ {
+ timingAdjust = 1;
+ }
+ else if(mBaudCounter > IDEAL_SYMBOL_TRANSITION_MAX)
+ {
+ timingAdjust = -1;
+ }
+ else
+ {
+ timingAdjust = 0;
+ }
+
+ mBaudCounter += timingAdjust;
+ samplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
+ }
+ else if(slope < mMaxSlope)
+ {
+ mMaxSlope = slope;
+ }
+ }
+ else
+ {
+ //Loop for crest of slope at the highest point above the positive threshold
+ if(slope < mMaxSlope && mMaxSlope > SLOPE_THRESHOLD)
+ {
+ mSymbol = true;
+
+ //Coarse symbol timing adjust - ideal toggle is middle of baud, between 11 & 19 samples
+ if(mBaudCounter < IDEAL_SYMBOL_TRANSITION_MIN)
+ {
+ timingAdjust = 1;
+ }
+ else if(mBaudCounter > IDEAL_SYMBOL_TRANSITION_MAX)
+ {
+ timingAdjust = -1;
+ }
+ else
+ {
+ timingAdjust = 0;
+ }
+
+ mBaudCounter += timingAdjust;
+ samplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
+ }
+ else if(slope > mMaxSlope)
+ {
+ mMaxSlope = slope;
+ }
+ }
+ }
+
+ mBaudCounter++;
+
+ //Update the current code value once we've processed a baud/symbol period of samples
+ if(mBaudCounter > BAUD_LENGTH)
+ {
+ //Update the code value adding in the current symbol
+ mCode = (Integer.rotateLeft(mCode, 1) + (mSymbol ? 1 : 0)) & CODE_MASK;
+ if(DCSCode.hasValue(mCode))
+ {
+ getMessageListener().receive(new DCSMessage(DCSCode.fromValue(mCode), System.currentTimeMillis()));
+ }
+
+ mBaudCounter -= BAUD_LENGTH;
+
+ //Check for a continuous sequence of ones that exceeds the max (6) for any DCS code and
+ //flip/revert the symbol state back to zero.
+ if(mSymbol)
+ {
+ mExcessiveOneSequenceCounter++;
+
+ if(mExcessiveOneSequenceCounter > MAX_ONES_SEQUENCE)
+ {
+ mSymbol = false;
+ mMaxSlope = -1f;
+ mExcessiveOneSequenceCounter = 0;
+ }
+ }
+ else
+ {
+ mExcessiveOneSequenceCounter = 0;
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ mLog.warn("Unexpected error while processing DCS samples", e);
+ }
+ }
+ }
+
+ /**
+ * Calculates the slope of the 30 samples in the values array starting at the specified offset. The baud period
+ * is slightly more than 59 samples, however the voltage state transition occurs over a 30-sample period.
+ *
+ * Note: code modified from Apache Commons Math library, SimpleRegression class.
+ * @param samples to load from
+ * @param offset to the sample start for the slope calculation
+ */
+ public float calculateSlope(float[] samples, int offset)
+ {
+ /** mean of accumulated x & y values, used in updating formulas */
+ double xbar = 0;
+ double ybar = samples[offset];
+
+// double sumXX = 0;
+ double sumXY = 0;
+ double fact1, dx, dy;
+
+ for(int x = 1; x < SLOPE_CALCULATION_PERIOD; x++)
+ {
+ fact1 = 1.0 + x;
+// final double fact2 = x / (1.0 + x);
+ dx = x - xbar;
+ dy = samples[offset + x] - ybar;
+// sumXX += dx * dx * fact2;
+ sumXY += dx * dy * (x / (1.0 + x));
+ xbar += dx / fact1;
+ ybar += dy / fact1;
+ }
+
+ //Transfer the sumXX value to SLOPE_CALCULATION_SUM_XX since it's a constant value
+// mLog.info("SumXX: " + sumXX);
+
+ return (float)(sumXY / SLOPE_CALCULATION_SUM_XX);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dcs/DCSDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSDecoderState.java
new file mode 100644
index 00000000..763fca01
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSDecoderState.java
@@ -0,0 +1,123 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.dcs;
+
+import io.github.dsheirer.channel.state.DecoderState;
+import io.github.dsheirer.channel.state.DecoderStateEvent;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.DecoderType;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Maintains the decoder state from the stream of decoded DCS code messages.
+ */
+public class DCSDecoderState extends DecoderState
+{
+ private Map mDcsCodeCountsMap = new HashMap<>();
+ private DCSCode mCurrentCode = DCSCode.UNKNOWN;
+
+ /**
+ * Constructs an instance
+ */
+ public DCSDecoderState()
+ {
+ }
+
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.DCS;
+ }
+
+
+ @Override
+ public void receive(IMessage message)
+ {
+ if(message instanceof DCSMessage dcsMessage)
+ {
+ mCurrentCode = dcsMessage.getDCSCode();
+
+ if(!mDcsCodeCountsMap.containsKey(mCurrentCode))
+ {
+ mDcsCodeCountsMap.put(mCurrentCode, 1);
+ }
+ else
+ {
+ mDcsCodeCountsMap.put(mCurrentCode, mDcsCodeCountsMap.get(mCurrentCode) + 1);
+ }
+
+ getIdentifierCollection().update(dcsMessage.getIdentifiers());
+ }
+ }
+
+ @Override
+ public void receiveDecoderStateEvent(DecoderStateEvent event)
+ {
+ switch(event.getEvent())
+ {
+ case REQUEST_RESET:
+ resetState();
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public String getActivitySummary()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("=============================\n");
+ sb.append("Decoder:\tDigital Coded Squelch (DCS)\n\n");
+ if(mDcsCodeCountsMap.isEmpty())
+ {
+ sb.append(" Detected Codes: (none)\n");
+ }
+ else
+ {
+ for(Map.Entry entry: mDcsCodeCountsMap.entrySet())
+ {
+ sb.append(" ").append(entry.getKey()).append(" - Count: ").append(entry.getValue());
+
+ if(entry.getKey().equals(mCurrentCode))
+ {
+ sb.append(" - Current\n");
+ }
+ else
+ {
+ sb.append("\n");
+ }
+ }
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ public void init()
+ {
+ }
+
+ @Override
+ public void stop()
+ {
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dcs/DCSMessage.java b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSMessage.java
new file mode 100644
index 00000000..e8107d52
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSMessage.java
@@ -0,0 +1,79 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.dcs;
+
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.dcs.DCSIdentifier;
+import io.github.dsheirer.message.Message;
+import io.github.dsheirer.protocol.Protocol;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Digital Coded Squelch (DCS) tone detected message
+ */
+public class DCSMessage extends Message
+{
+ private final DCSCode mDCSCode;
+
+ /**
+ * Constructs an instance
+ * @param code that was detected
+ * @param timestamp when the code was detected
+ */
+ public DCSMessage(DCSCode code, long timestamp)
+ {
+ super(timestamp);
+ mDCSCode = code;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Digital Coded Squelch (DCS) Detected: " + mDCSCode.name();
+ }
+
+ /**
+ * The DCS code that was detected.
+ * @return code
+ */
+ public DCSCode getDCSCode()
+ {
+ return mDCSCode;
+ }
+
+ @Override
+ public boolean isValid()
+ {
+ return true; //We only send a message when the tone was valid
+ }
+
+ @Override
+ public Protocol getProtocol()
+ {
+ return Protocol.DCS;
+ }
+
+ @Override
+ public List getIdentifiers()
+ {
+ return Collections.singletonList(new DCSIdentifier(mDCSCode));
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dcs/DCSMessageFilter.java b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSMessageFilter.java
new file mode 100644
index 00000000..ea5a64f6
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dcs/DCSMessageFilter.java
@@ -0,0 +1,66 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.dcs;
+
+import io.github.dsheirer.filter.Filter;
+import io.github.dsheirer.filter.FilterElement;
+import io.github.dsheirer.message.IMessage;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Message filter for Digital Coded Squelch (DCS)
+ */
+public class DCSMessageFilter extends Filter
+{
+ private Map> mElements = new HashMap<>();
+ private static final String DCS_MESSAGE_FILTER_TAG = "DCS";
+
+ public DCSMessageFilter()
+ {
+ super("DCS Message Filter");
+ mElements.put(DCS_MESSAGE_FILTER_TAG, new FilterElement<>("DCS Code", true));
+ }
+
+ @Override
+ public boolean passes(IMessage message)
+ {
+ if(mEnabled && canProcess(message))
+ {
+ return mElements.get(DCS_MESSAGE_FILTER_TAG).isEnabled();
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean canProcess(IMessage message)
+ {
+ return message instanceof DCSMessage;
+ }
+
+ @Override
+ public List> getFilterElements()
+ {
+ return new ArrayList>(mElements.values());
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java
index d1bb8b7d..2a6ba768 100644
--- a/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java
+++ b/src/main/java/io/github/dsheirer/module/decode/fleetsync2/Fleetsync2DecoderState.java
@@ -1,23 +1,20 @@
/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
*
- * * ******************************************************************************
- * * Copyright (C) 2014-2019 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
- * * *****************************************************************************
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
*/
package io.github.dsheirer.module.decode.fleetsync2;
@@ -33,13 +30,16 @@ import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
import io.github.dsheirer.module.decode.fleetsync2.identifier.FleetsyncIdentifier;
import io.github.dsheirer.module.decode.fleetsync2.message.Fleetsync2Message;
import io.github.dsheirer.protocol.Protocol;
-
-import java.util.Set;
-import java.util.TreeSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
public class Fleetsync2DecoderState extends DecoderState
{
- private Set mIdents = new TreeSet<>();
+ private Map mFromIdentCountsMap = new HashMap<>();
+ private Map mToIdentCountsMap = new HashMap<>();
public Fleetsync2DecoderState()
{
@@ -52,16 +52,6 @@ public class Fleetsync2DecoderState extends DecoderState
return DecoderType.FLEETSYNC2;
}
- /**
- * Resets the overall decoder state and clears any accumulated event details
- */
- @Override
- public void reset()
- {
- super.reset();
- mIdents.clear();
- }
-
@Override
public void start()
{
@@ -87,8 +77,30 @@ public class Fleetsync2DecoderState extends DecoderState
getIdentifierCollection().update(fleetsync.getIdentifiers());
- mIdents.add(fleetsync.getFromIdentifier());
- mIdents.add(fleetsync.getToIdentifier());
+ FleetsyncIdentifier from = fleetsync.getFromIdentifier();
+
+ if(mFromIdentCountsMap.containsKey(from))
+ {
+ mFromIdentCountsMap.put(from, mFromIdentCountsMap.get(from) + 1);
+ }
+ else
+ {
+ mFromIdentCountsMap.put(from, 1);
+ }
+
+ FleetsyncIdentifier to = fleetsync.getToIdentifier();
+
+ if(to != null)
+ {
+ if(mToIdentCountsMap.containsKey(to))
+ {
+ mToIdentCountsMap.put(to, mToIdentCountsMap.get(to) + 1);
+ }
+ else
+ {
+ mToIdentCountsMap.put(to, 1);
+ }
+ }
switch(fleetsync.getMessageType())
{
@@ -158,17 +170,30 @@ public class Fleetsync2DecoderState extends DecoderState
sb.append("=============================\n");
sb.append("Decoder:\tFleetsync II\n\n");
- sb.append("Fleetsync Idents\n");
- if(mIdents.isEmpty())
+ if(mFromIdentCountsMap.isEmpty() && mToIdentCountsMap.isEmpty())
{
+ sb.append("Fleetsync Idents\n");
sb.append(" None\n");
}
else
{
+ sb.append("Fleetsync From Idents\n");
- for (FleetsyncIdentifier mIdent : mIdents) {
- sb.append(" ").append(mIdent.formatted()).append("\n");
+ List fromIdents = new ArrayList<>(mFromIdentCountsMap.keySet());
+ Collections.sort(fromIdents);
+ for(FleetsyncIdentifier from: fromIdents)
+ {
+ sb.append(" ").append(from.formatted()).append(" - Count:").append(mFromIdentCountsMap.get(from)).append("\n");
+ }
+
+ sb.append("\nFleetsync To Idents\n");
+
+ List toIdents = new ArrayList<>(mToIdentCountsMap.keySet());
+ Collections.sort(toIdents);
+ for(FleetsyncIdentifier to: toIdents)
+ {
+ sb.append(" ").append(to.formatted()).append(" - Count:").append(mToIdentCountsMap.get(to)).append("\n");
}
}
diff --git a/src/main/java/io/github/dsheirer/protocol/Protocol.java b/src/main/java/io/github/dsheirer/protocol/Protocol.java
index 8e4c6950..fd3a8771 100644
--- a/src/main/java/io/github/dsheirer/protocol/Protocol.java
+++ b/src/main/java/io/github/dsheirer/protocol/Protocol.java
@@ -30,6 +30,7 @@ public enum Protocol
APCO25_PHASE2("APCO-25 P2", "APCO25PHASE2", 12000),
ARS("ARS", "ARS", 0),
CELLOCATOR("CELLOCATOR", "CELLOCATOR", 0),
+ DCS("DCS", "DCS", 134),
DMR("DMR", "DMR", 9600),
FLEETSYNC("Fleetsync", "FLEETSYNC", 1200),
IPV4("IPV4", "IPV4", 0),