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),