diff --git a/src/main/java/io/github/dsheirer/alias/id/radio/Radio.java b/src/main/java/io/github/dsheirer/alias/id/radio/Radio.java index e3d0d8df..f266faa3 100644 --- a/src/main/java/io/github/dsheirer/alias/id/radio/Radio.java +++ b/src/main/java/io/github/dsheirer/alias/id/radio/Radio.java @@ -78,8 +78,8 @@ public class Radio extends AliasID return false; } - TalkgroupFormat talkgroupFormat = TalkgroupFormat.get(mProtocol); - return talkgroupFormat.getMinimumValidValue() <= mValue && mValue <= talkgroupFormat.getMaximumValidValue(); + RadioFormat radioFormat = RadioFormat.get(mProtocol); + return radioFormat.getMinimumValidValue() <= mValue && mValue <= radioFormat.getMaximumValidValue(); } public String toString() diff --git a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java index f3ce2ba4..a253ff17 100644 --- a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java @@ -39,7 +39,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen private static final int MAX_SEGMENT_AUDIO_SAMPLE_LENGTH = 8000 * 60 * 1; //8 kHz - 1 minute private Listener mAudioSegmentListener; - private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection(); + protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection(); private Broadcaster mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>(); private AliasList mAliasList; private AudioSegment mAudioSegment; diff --git a/src/main/java/io/github/dsheirer/audio/AudioSegment.java b/src/main/java/io/github/dsheirer/audio/AudioSegment.java index 3d6fed49..1d605bbd 100644 --- a/src/main/java/io/github/dsheirer/audio/AudioSegment.java +++ b/src/main/java/io/github/dsheirer/audio/AudioSegment.java @@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -408,11 +409,28 @@ public class AudioSegment implements Listener public void receive(IdentifierUpdateNotification identifierUpdateNotification) { //Only process add updates that match this timeslot - if(identifierUpdateNotification.getTimeslot() == getTimeslot() && - (identifierUpdateNotification.isAdd() || identifierUpdateNotification.isSilentAdd())) + if(identifierUpdateNotification.getTimeslot() == getTimeslot()) { - addIdentifier(identifierUpdateNotification.getIdentifier()); + if(identifierUpdateNotification.isAdd() || identifierUpdateNotification.isSilentAdd()) + { + addIdentifier(identifierUpdateNotification.getIdentifier()); + } + mIdentifierUpdateNotificationBroadcaster.broadcast(identifierUpdateNotification); } } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("Audio Segment\n Timeslot:").append(getTimeslot()).append("\n"); + sb.append(" Start Time:").append(new Date(getStartTimestamp())); + sb.append(" Linked:").append(isLinked()).append("\n"); + sb.append(" Recordable:").append(recordAudioProperty().get()).append("\n"); + sb.append("Do Not Monitor:").append(isDoNotMonitor()).append("\n"); + sb.append(" Stream To:").append(getBroadcastChannels()).append("\n"); + sb.append(" Identifiers:\n").append(getIdentifierCollection()).append("\n"); + return sb.toString(); + } } diff --git a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java index a2bbf690..53010b59 100644 --- a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java @@ -105,7 +105,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS //Set the proxy as a listener so that echo'd updates are broadcast externally mutableIdentifierCollection.setIdentifierUpdateListener(mIdentifierUpdateNotificationProxy); - StateMachine stateMachine = new StateMachine(timeslot); + StateMachine stateMachine = new StateMachine(timeslot, State.MULTI_CHANNEL_ACTIVE_STATES); mStateMachineMap.put(timeslot, stateMachine); stateMachine.addListener(this); @@ -133,7 +133,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS public void stateChanged(State state, int timeslot) { //Broadcast current channel state so that channel rotation monitor can track - if(state.isActiveState()) + if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state)) { broadcast(DecoderStateEvent.activeState(timeslot)); } @@ -148,9 +148,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS switch(state) { - case IDLE: - broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE)); - break; case RESET: reset(timeslot); mStateMachineMap.get(timeslot).setState(State.IDLE); @@ -173,19 +170,49 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS */ private void checkTeardown() { - boolean teardown = true; + boolean teardown = false; + boolean active = false; for(StateMachine stateMachine: mStateMachineMap.values()) { - if(stateMachine.getState().isActiveState()) + State state = stateMachine.getState(); + + //If we have an active state in either timeslot, don't teardown. IDLE is a special state that is active + //but doesn't prevent a teardown when the other timeslot is in TEARDOWN + if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state) && state != State.IDLE) { - teardown = false; + active = true; + } + else if(state == State.TEARDOWN) + { + teardown = true; } } - if(teardown) + if(teardown && !active) { - broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE)); + if(mChannel.isTrafficChannel()) + { + broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE)); + } + else + { + for(StateMachine stateMachine: mStateMachineMap.values()) + { + stateMachine.setState(State.RESET); + } + } + } + //If one timeslot is teardown but the other is still active, reset the teardown timeslot to IDLE + else if(teardown && active) + { + for(StateMachine stateMachine: mStateMachineMap.values()) + { + if(stateMachine.getState() == State.TEARDOWN) + { + stateMachine.setState(State.RESET); + } + } } } @@ -340,11 +367,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++) { mIdentifierCollectionMap.get(timeslot).broadcastIdentifiers(); - - if(mChannel.getChannelType() == ChannelType.TRAFFIC) - { - mStateMachineMap.get(timeslot).setState(State.ACTIVE); - } + mStateMachineMap.get(timeslot).setState(State.RESET); } } @@ -476,7 +499,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS //in an active (ie sync locked) state. for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++) { - if(mStateMachineMap.get(timeslot).getState().isActiveState()) + if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState())) { broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), mChannel.getChannelType().name())); @@ -511,7 +534,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS case CONTINUATION: case DECODE: case START: - if(State.CALL_STATES.contains(event.getState())) + if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(event.getState())) { mStateMachineMap.get(event.getTimeslot()).setState(event.getState()); } @@ -519,11 +542,11 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS case END: if(mChannel.isTrafficChannel()) { - mStateMachineMap.get(event.getTimeslot()).setState(State.TEARDOWN); + mStateMachineMap.get(event.getTimeslot()).setState(event.getState()); } else { - mStateMachineMap.get(event.getTimeslot()).setState(State.FADE); + mStateMachineMap.get(event.getTimeslot()).setState(event.getState()); } break; case RESET: diff --git a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java index d1391f5e..9574be6e 100644 --- a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java @@ -94,7 +94,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver(); private SourceEventListener mInternalSourceEventListener; private ChannelMetadata mChannelMetadata; - private StateMachine mStateMachine = new StateMachine(0); + private StateMachine mStateMachine = new StateMachine(0, State.SINGLE_CHANNEL_ACTIVE_STATES); private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0); public SingleChannelState(Channel channel, AliasModel aliasModel) @@ -123,7 +123,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder public void stateChanged(State state, int timeslot) { //Broadcast current channel state so that channel rotation monitor can track - if(state.isActiveState()) + if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(state)) { broadcast(DecoderStateEvent.activeState(timeslot)); } @@ -377,7 +377,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder case NOTIFICATION_MEASURED_FREQUENCY_ERROR: //Rebroadcast frequency error measurements to external listener if we're currently //in an active (ie sync locked) state. - if(mStateMachine.getState().isActiveState()) + if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState())) { broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), mChannel.getChannelType().name())); @@ -411,7 +411,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder case CONTINUATION: case DECODE: case START: - if(State.CALL_STATES.contains(event.getState())) + if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(event.getState())) { mStateMachine.setState(event.getState()); } diff --git a/src/main/java/io/github/dsheirer/channel/state/State.java b/src/main/java/io/github/dsheirer/channel/state/State.java index 63b356d2..ae4ed374 100644 --- a/src/main/java/io/github/dsheirer/channel/state/State.java +++ b/src/main/java/io/github/dsheirer/channel/state/State.java @@ -43,6 +43,7 @@ public enum State state == DATA || state == ENCRYPTED || state == FADE || + state == IDLE || state == TEARDOWN || state == RESET; } @@ -60,6 +61,7 @@ public enum State state == DATA || state == ENCRYPTED || state == FADE || + state == IDLE || state == TEARDOWN || state == RESET; } @@ -121,7 +123,8 @@ public enum State } }, /** - * Indicates the channel is idle and not actively decoding any messages + * Indicates the channel is idle and not actively decoding any messages or in a multiple timeslot decoder this + * state reflects active decoding that indicates the timeslot is idle. */ IDLE("IDLE") { @@ -159,8 +162,8 @@ public enum State private String mDisplayValue; - public static final EnumSet CALL_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED); - public static final EnumSet IDLE_STATES = EnumSet.of(IDLE, FADE); + public static final EnumSet SINGLE_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED); + public static final EnumSet MULTI_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED, IDLE); private State(String displayValue) { @@ -169,17 +172,6 @@ public enum State public abstract boolean canChangeTo(State state); - /** - * Indicates that this state is an active (ie decoding) state and is one of the states - * enumerated in the CALL_STATES enumeration set. - * - * @return true if this state is an active state - */ - public boolean isActiveState() - { - return CALL_STATES.contains(this); - } - public String getDisplayValue() { return mDisplayValue; diff --git a/src/main/java/io/github/dsheirer/channel/state/StateMachine.java b/src/main/java/io/github/dsheirer/channel/state/StateMachine.java index c08bf1b2..0b022bbc 100644 --- a/src/main/java/io/github/dsheirer/channel/state/StateMachine.java +++ b/src/main/java/io/github/dsheirer/channel/state/StateMachine.java @@ -30,8 +30,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; +/** + * State machine for tracking a channel state. + */ public class StateMachine { private final static Logger mLog = LoggerFactory.getLogger(StateMachine.class); @@ -42,13 +46,21 @@ public class StateMachine protected long mEndTimeout; protected long mEndTimeoutBuffer = 0; private int mTimeslot; + private EnumSet mActiveStates; private Channel.ChannelType mChannelType = Channel.ChannelType.STANDARD; private List mStateMachineListeners = new ArrayList<>(); private Listener mIdentifierUpdateListener; - public StateMachine(int timeslot) + /** + * Constructs an instance + * + * @param timeslot for this state machine + * @param activeStates set of states considered active for updating the fade timeout + */ + public StateMachine(int timeslot, EnumSet activeStates) { mTimeslot = timeslot; + mActiveStates = activeStates; } public void addListener(IStateMachineListener listener) @@ -73,7 +85,7 @@ public class StateMachine public void checkState() { - if(mState.isActiveState() && mFadeTimeout <= System.currentTimeMillis()) + if(mActiveStates.contains(mState) && mFadeTimeout <= System.currentTimeMillis()) { setState(State.FADE); } @@ -92,18 +104,22 @@ public class StateMachine { if(state == mState) { - if(State.CALL_STATES.contains(state)) + if(mActiveStates.contains(state)) { updateFadeTimeout(); } } else if(mState.canChangeTo(state)) { + if(mActiveStates.contains(state)) + { + updateFadeTimeout(); + } + switch(state) { case ACTIVE: mState = state; - updateFadeTimeout(); broadcast(ChannelStateIdentifier.ACTIVE); break; case CONTROL: @@ -111,23 +127,19 @@ public class StateMachine if(mChannelType == Channel.ChannelType.STANDARD) { mState = state; - updateFadeTimeout(); broadcast(ChannelStateIdentifier.CONTROL); } break; case DATA: mState = state; - updateFadeTimeout(); broadcast(ChannelStateIdentifier.DATA); break; case ENCRYPTED: mState = state; - updateFadeTimeout(); broadcast(ChannelStateIdentifier.ENCRYPTED); break; case CALL: mState = state; - updateFadeTimeout(); broadcast(ChannelStateIdentifier.CALL); break; case FADE: diff --git a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java index faf941cd..5a41e15e 100644 --- a/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java +++ b/src/main/java/io/github/dsheirer/dsp/filter/channelizer/ComplexPolyphaseChannelizerM2.java @@ -28,6 +28,7 @@ import org.jtransforms.fft.FloatFFT_1D; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.text.DecimalFormat; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -62,7 +63,7 @@ import java.util.List; public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer { private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class); - + private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0"); private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 25000; //Sized at 152 buffers a second where max = 5 seconds and reset = 2 seconds worth of buffers @@ -154,7 +155,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann channels--; } - mLog.debug("Sample Rate [" + sampleRate + "] providing [" + channels + "] channels at [" + (sampleRate / (double)channels) + "] Hz each"); + mLog.info("Sample Rate [" + DECIMAL_FORMAT.format(sampleRate) + "] providing [" + channels + + "] channels at [" + DECIMAL_FORMAT.format(sampleRate / (double)channels) + "] Hz each"); + return channels; } diff --git a/src/main/java/io/github/dsheirer/identifier/radio/RadioIdentifier.java b/src/main/java/io/github/dsheirer/identifier/radio/RadioIdentifier.java index 077e10d8..f3dfb96b 100644 --- a/src/main/java/io/github/dsheirer/identifier/radio/RadioIdentifier.java +++ b/src/main/java/io/github/dsheirer/identifier/radio/RadioIdentifier.java @@ -37,6 +37,6 @@ public abstract class RadioIdentifier extends IntegerIdentifier @Override public boolean isValid() { - return getValue() > 0; + return getValue() >= 0; } } diff --git a/src/main/java/io/github/dsheirer/identifier/tone/CallProgressIdentifier.java b/src/main/java/io/github/dsheirer/identifier/tone/CallProgressIdentifier.java deleted file mode 100644 index 80763c8a..00000000 --- a/src/main/java/io/github/dsheirer/identifier/tone/CallProgressIdentifier.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.identifier.tone; - -import com.google.common.base.Joiner; -import io.github.dsheirer.identifier.Form; -import io.github.dsheirer.identifier.IdentifierClass; -import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.string.StringListIdentifier; - -import java.util.List; - -/** - * Call Progress (ie dial tone, busy tone) tones identifier - */ -public abstract class CallProgressIdentifier extends StringListIdentifier -{ - public CallProgressIdentifier(List values) - { - super(values, IdentifierClass.USER, Form.CALL_PROGRESS_TONE, Role.TO); - } - - @Override - public String toString() - { - return "CALL:" + Joiner.on("").join(getValue()); - } -} diff --git a/src/main/java/io/github/dsheirer/identifier/tone/DtmfIdentifier.java b/src/main/java/io/github/dsheirer/identifier/tone/DtmfIdentifier.java deleted file mode 100644 index d54396c3..00000000 --- a/src/main/java/io/github/dsheirer/identifier/tone/DtmfIdentifier.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.identifier.tone; - -import com.google.common.base.Joiner; -import io.github.dsheirer.identifier.Form; -import io.github.dsheirer.identifier.IdentifierClass; -import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.string.StringListIdentifier; - -import java.util.List; - -/** - * Dual-Tone Multi-Frequency (DTMF) tones identifier - */ -public abstract class DtmfIdentifier extends StringListIdentifier -{ - public DtmfIdentifier(List values) - { - super(values, IdentifierClass.USER, Form.DTMF, Role.TO); - } - - @Override - public String toString() - { - return "DTMF:" + Joiner.on("").join(getValue()); - } -} diff --git a/src/main/java/io/github/dsheirer/identifier/tone/KnoxIdentifier.java b/src/main/java/io/github/dsheirer/identifier/tone/KnoxIdentifier.java deleted file mode 100644 index c7de91f7..00000000 --- a/src/main/java/io/github/dsheirer/identifier/tone/KnoxIdentifier.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.identifier.tone; - -import com.google.common.base.Joiner; -import io.github.dsheirer.identifier.Form; -import io.github.dsheirer.identifier.IdentifierClass; -import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.string.StringListIdentifier; - -import java.util.List; - -/** - * Knox tones identifier - */ -public abstract class KnoxIdentifier extends StringListIdentifier -{ - public KnoxIdentifier(List values) - { - super(values, IdentifierClass.USER, Form.KNOX_TONE, Role.TO); - } - - @Override - public String toString() - { - return "KNOX:" + Joiner.on("").join(getValue()); - } -} diff --git a/src/main/java/io/github/dsheirer/identifier/tone/P25CallProgressIdentifier.java b/src/main/java/io/github/dsheirer/identifier/tone/P25CallProgressIdentifier.java deleted file mode 100644 index 129c37eb..00000000 --- a/src/main/java/io/github/dsheirer/identifier/tone/P25CallProgressIdentifier.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.identifier.tone; - -import io.github.dsheirer.protocol.Protocol; - -import java.util.List; - -public class P25CallProgressIdentifier extends CallProgressIdentifier -{ - public P25CallProgressIdentifier(List values) - { - super(values); - } - - @Override - public Protocol getProtocol() - { - return Protocol.APCO25; - } - - public static P25CallProgressIdentifier create(List values) - { - return new P25CallProgressIdentifier(values); - } -} diff --git a/src/main/java/io/github/dsheirer/identifier/tone/P25DtmfIdentifier.java b/src/main/java/io/github/dsheirer/identifier/tone/P25DtmfIdentifier.java deleted file mode 100644 index 1f3ed7d2..00000000 --- a/src/main/java/io/github/dsheirer/identifier/tone/P25DtmfIdentifier.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.identifier.tone; - -import io.github.dsheirer.protocol.Protocol; - -import java.util.List; - -public class P25DtmfIdentifier extends DtmfIdentifier -{ - public P25DtmfIdentifier(List values) - { - super(values); - } - - @Override - public Protocol getProtocol() - { - return Protocol.APCO25; - } - - public static P25DtmfIdentifier create(List values) - { - return new P25DtmfIdentifier(values); - } -} diff --git a/src/main/java/io/github/dsheirer/identifier/tone/P25KnoxIdentifier.java b/src/main/java/io/github/dsheirer/identifier/tone/P25KnoxIdentifier.java deleted file mode 100644 index e952fdc8..00000000 --- a/src/main/java/io/github/dsheirer/identifier/tone/P25KnoxIdentifier.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * - * * ****************************************************************************** - * * 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 - * * ***************************************************************************** - * - * - */ - -package io.github.dsheirer.identifier.tone; - -import io.github.dsheirer.protocol.Protocol; - -import java.util.List; - -public class P25KnoxIdentifier extends KnoxIdentifier -{ - public P25KnoxIdentifier(List values) - { - super(values); - } - - @Override - public Protocol getProtocol() - { - return Protocol.APCO25; - } - - public static P25KnoxIdentifier create(List values) - { - return new P25KnoxIdentifier(values); - } -} diff --git a/src/main/java/io/github/dsheirer/identifier/tone/P25ToneIdentifier.java b/src/main/java/io/github/dsheirer/identifier/tone/P25ToneIdentifier.java index 26ef0b6e..489e41b2 100644 --- a/src/main/java/io/github/dsheirer/identifier/tone/P25ToneIdentifier.java +++ b/src/main/java/io/github/dsheirer/identifier/tone/P25ToneIdentifier.java @@ -26,11 +26,14 @@ import io.github.dsheirer.protocol.Protocol; import java.util.List; +/** + * P25 Phase II AMBE audio codec tone metadata + */ public class P25ToneIdentifier extends ToneIdentifier { - public P25ToneIdentifier(List values) + public P25ToneIdentifier(String value) { - super(values); + super(value); } @Override @@ -39,8 +42,8 @@ public class P25ToneIdentifier extends ToneIdentifier return Protocol.APCO25; } - public static P25ToneIdentifier create(List values) + public static P25ToneIdentifier create(String value) { - return new P25ToneIdentifier(values); + return new P25ToneIdentifier(value); } } diff --git a/src/main/java/io/github/dsheirer/identifier/tone/ToneIdentifier.java b/src/main/java/io/github/dsheirer/identifier/tone/ToneIdentifier.java index 39a5799f..767177bc 100644 --- a/src/main/java/io/github/dsheirer/identifier/tone/ToneIdentifier.java +++ b/src/main/java/io/github/dsheirer/identifier/tone/ToneIdentifier.java @@ -22,27 +22,18 @@ package io.github.dsheirer.identifier.tone; -import com.google.common.base.Joiner; import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.string.StringListIdentifier; - -import java.util.List; +import io.github.dsheirer.identifier.string.StringIdentifier; /** * Pass-band audio tones identifier */ -public abstract class ToneIdentifier extends StringListIdentifier +public abstract class ToneIdentifier extends StringIdentifier { - public ToneIdentifier(List values) + public ToneIdentifier(String value) { - super(values, IdentifierClass.USER, Form.TONE, Role.TO); - } - - @Override - public String toString() - { - return "TONES:" + Joiner.on("").join(getValue()); + super(value, IdentifierClass.USER, Form.TONE, Role.FROM); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java index 91256415..bddc36b0 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java @@ -572,7 +572,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv if(type == null) { - mLog.debug("Unrecognized opcode for determining decode event type: " + opcode.name()); + mLog.error("Unrecognized opcode for determining decode event type: " + opcode.name()); type = DecodeEventType.CALL; } @@ -697,7 +697,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv //Issue a disable request for each traffic channel for(Channel channel: channels) { - mLog.debug("Stopping traffic channel: " + channel); + mLog.info("Stopping traffic channel: " + channel); broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE)); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java index cb23c63c..de3cae04 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/audio/P25P2AudioModule.java @@ -19,6 +19,7 @@ package io.github.dsheirer.module.decode.p25.audio; +import com.google.common.base.Joiner; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule; import io.github.dsheirer.audio.squelch.SquelchState; @@ -27,9 +28,7 @@ import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierUpdateNotification; import io.github.dsheirer.identifier.IdentifierUpdateProvider; -import io.github.dsheirer.identifier.tone.P25CallProgressIdentifier; -import io.github.dsheirer.identifier.tone.P25DtmfIdentifier; -import io.github.dsheirer.identifier.tone.P25KnoxIdentifier; +import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.tone.P25ToneIdentifier; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence; @@ -51,17 +50,9 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat { private final static Logger mLog = LoggerFactory.getLogger(P25P2AudioModule.class); - private static final String METADATA_TYPE_DTMF = "DTMF"; - private static final String METADATA_TYPE_KNOX = "KNOX"; - private static final String METADATA_TYPE_TONE = "TONE"; - private static final String METADATA_TYPE_CALL_PROGRESS = "CALL PROGRESS"; - private Listener mIdentifierUpdateNotificationListener; private SquelchStateListener mSquelchStateListener = new SquelchStateListener(); - private MetadataProcessor mDtmfMetadataProcessor; - private MetadataProcessor mKnoxMetadataProcessor; - private MetadataProcessor mToneMetadataProcessor; - private MetadataProcessor mCallProcessMetadataProcessor; + private ToneMetadataProcessor mToneMetadataProcessor = new ToneMetadataProcessor(); private int mTimeslot; private Queue mQueuedAudioTimeslots = new ArrayDeque<>(); private boolean mEncryptedCallStateEstablished = false; @@ -92,10 +83,10 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat @Override public void reset() { - mCallProcessMetadataProcessor = null; - mDtmfMetadataProcessor = null; - mKnoxMetadataProcessor = null; - mToneMetadataProcessor = null; + //Explicitly clear FROM identifiers to ensure previous call TONE identifiers are cleared. + mIdentifierCollection.remove(Role.FROM); + + mToneMetadataProcessor.reset(); mQueuedAudioTimeslots.clear(); //Reset encrypted call handling flags @@ -179,107 +170,44 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat { byte[] voiceFrameBytes = voiceFrame.getBytes(); - IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes); - processMetadata(audioWithMetadata); - addAudio(audioWithMetadata.getAudio()); + try + { + IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes); + addAudio(audioWithMetadata.getAudio()); + processMetadata(audioWithMetadata); + } + catch(Exception e) + { + mLog.error("Error synthesizing AMBE audio - continuing [" + e.getLocalizedMessage() + "]"); + } } } } /** - * Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) + * Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) so that the + * tone metadata can be converted into a FROM identifier and included with any call segment. */ private void processMetadata(IAudioWithMetadata audioWithMetadata) { - if(mIdentifierUpdateNotificationListener != null) + if(audioWithMetadata.hasMetadata() && mIdentifierUpdateNotificationListener != null) { - if(audioWithMetadata.hasMetadata()) + for(Map.Entry entry: audioWithMetadata.getMetadata().entrySet()) { - Map metadata = audioWithMetadata.getMetadata(); + //Each metadata map entry contains a tone-type (key) and tone (value) + Identifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue()); - if(metadata.containsKey(METADATA_TYPE_DTMF)) + if(metadataIdentifier != null) { - String dtmf = metadata.get(METADATA_TYPE_DTMF); - - if(dtmf != null) - { - if(mDtmfMetadataProcessor == null) - { - mDtmfMetadataProcessor = new MetadataProcessor(); - } - - List dtmfTones = mDtmfMetadataProcessor.process(dtmf); - broadcast(P25DtmfIdentifier.create(dtmfTones)); - } - } - else if(metadata.containsKey(METADATA_TYPE_KNOX)) - { - String knox = metadata.get(METADATA_TYPE_KNOX); - - if(knox != null) - { - if(mKnoxMetadataProcessor == null) - { - mKnoxMetadataProcessor = new MetadataProcessor(); - } - - List knoxTones = mKnoxMetadataProcessor.process(knox); - broadcast(P25KnoxIdentifier.create(knoxTones)); - } - } - else if(metadata.containsKey(METADATA_TYPE_TONE)) - { - String tone = metadata.get(METADATA_TYPE_TONE); - - if(tone != null) - { - if(mToneMetadataProcessor == null) - { - mToneMetadataProcessor = new MetadataProcessor(); - } - - List tones = mToneMetadataProcessor.process(tone); - broadcast(P25ToneIdentifier.create(tones)); - } - } - else if(metadata.containsKey(METADATA_TYPE_CALL_PROGRESS)) - { - String callProgressTone = metadata.get(METADATA_TYPE_CALL_PROGRESS); - - if(callProgressTone != null) - { - if(mCallProcessMetadataProcessor == null) - { - mCallProcessMetadataProcessor = new MetadataProcessor(); - } - - List tones = mCallProcessMetadataProcessor.process(callProgressTone); - broadcast(P25CallProgressIdentifier.create(tones)); - } - } - } - else - { - if(mDtmfMetadataProcessor != null) - { - mDtmfMetadataProcessor.noMetadata(); - } - if(mKnoxMetadataProcessor != null) - { - mKnoxMetadataProcessor.noMetadata(); - } - if(mCallProcessMetadataProcessor != null) - { - mCallProcessMetadataProcessor.noMetadata(); - } - if(mToneMetadataProcessor != null) - { - mToneMetadataProcessor.noMetadata(); + broadcast(metadataIdentifier); } } } } + /** + * Broadcasts the identifier to a registered listener + */ private void broadcast(Identifier identifier) { if(mIdentifierUpdateNotificationListener != null) @@ -289,12 +217,18 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat } } + /** + * Registers the listener to receive identifier updates + */ @Override public void setIdentifierUpdateListener(Listener listener) { mIdentifierUpdateNotificationListener = listener; } + /** + * Unregisters a listener from receiving identifier updates + */ @Override public void removeIdentifierUpdateListener() { @@ -302,40 +236,124 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat } /** - * Processes metadata string values to provide a de-duplicated list of metadata values. - * - * This is necessary for metadata such as DTMF where a tone can span multiple 20ms voice frames but is intended - * to be represented as a single tone. An empty (no-metadata) frame causes the repeat checker to stop and allow - * the next metadata value to be added to the list. + * Process AMBE audio frame tone metadata. Tracks the count of sequential frames containing tone metadata to + * provide a list of each unique tone and a time duration (milliseconds) for the tone. Tones are concatenated into + * a comma separated list and included as call segment metadata. */ - public class MetadataProcessor + public class ToneMetadataProcessor { - private List mValues = new ArrayList<>(); - private String mLastInput; + private List mToneMetadata = new ArrayList<>(); - public List process(String value) + /** + * Resets or clears any accumulated call tone metadata to prepare for the next call. + */ + public void reset() { - if(value != null) - { - if(mLastInput != null && mLastInput.equalsIgnoreCase(value)) - { - //Suppress the repeat and return the current value - return mValues; - } - else - { - mLastInput = value; - mValues.add(value); - return mValues; - } - } - - return mValues; + mToneMetadata.clear(); } - public void noMetadata() + /** + * Process the tone metadata + * @param type of tone + * @param value of tone + * @return an identifier with the accumulated tone metadata set + */ + public Identifier process(String type, String value) { - mLastInput = null; + if(type == null || value == null) + { + return null; + } + + ToneMetadata toneMetadata = (mToneMetadata.size() > 0 ? mToneMetadata.get(mToneMetadata.size() - 1) : null); + + if(toneMetadata == null || !toneMetadata.matches(type, value)) + { + toneMetadata = new ToneMetadata(type, value); + mToneMetadata.add(toneMetadata); + } + + toneMetadata.incrementCount(); + + return P25ToneIdentifier.create(Joiner.on(",").join(mToneMetadata)); + } + } + + /** + * Metadata about the tone type being transmitted and the duration. + * + * Note: each AMBE frame is 20 milliseconds in duration, so total duration is the count of 20 ms tone frames + * of the same metadata type. + */ + public class ToneMetadata + { + private String mType; + private String mValue; + private int mCount; + + /** + * Constructs an instance + * @param type of tone + * @param value of the tone + */ + public ToneMetadata(String type, String value) + { + mType = type; + mValue = value; + mCount = 0; + } + + /** + * Indicates if this tone metadata matches the tone metadata represented by the arguments + * @param type of tone + * @param value of tone + * @return true if they match + */ + public boolean matches(String type, String value) + { + return type != null && value != null && type.matches(mType) && value.matches(mValue); + } + + /** + * Type of tone + */ + public String getType() + { + return mType; + } + + /** + * Value of tone + */ + public String getValue() + { + return mValue; + } + + /** + * Number of times this tone has occurred sequentially + */ + public int getCount() + { + return mCount; + } + + /** + * Increments the count of the number of sequential frames that contained the tone + */ + public void incrementCount() + { + mCount++; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append(mType); + sb.append(" ").append(mValue); + sb.append(" (").append(mCount * 20).append("ms)"); + return sb.toString(); } } @@ -359,5 +377,4 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat } } } - } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java index 6bc7c597..3083373b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java @@ -43,6 +43,7 @@ import io.github.dsheirer.module.decode.event.DecodeEventType; import io.github.dsheirer.module.decode.p25.P25DecodeEvent; import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel; import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier; +import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacPduType; @@ -102,6 +103,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie private PatchGroupManager mPatchGroupManager = new PatchGroupManager(); private P25P2NetworkConfigurationMonitor mNetworkConfigurationMonitor = new P25P2NetworkConfigurationMonitor(); private DecodeEvent mCurrentCallEvent; + private int mEndPttOnFacchCounter = 0; /** * Constructs an APCO-25 decoder state for a traffic channel. @@ -138,7 +140,8 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie protected void resetState() { super.resetState(); - closeCurrentCallEvent(System.currentTimeMillis(), true); + closeCurrentCallEvent(System.currentTimeMillis(), true, MacPduType.MAC_3_IDLE); + mEndPttOnFacchCounter = 0; } /** @@ -151,28 +154,68 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie { if(message instanceof MacMessage) { - processMacMessage((MacMessage)message); + MacMessage macMessage = (MacMessage)message; + processMacMessage(macMessage); + + MacPduType macPduType = macMessage.getMacPduType(); + + //Ignore End PTT - this is handled in the processMacMessage() method + if(macPduType != MacPduType.MAC_2_END_PTT) + { + mEndPttOnFacchCounter = 0; + continueState(getStateFromPduType(macPduType)); + } + + if(macPduType == MacPduType.MAC_3_IDLE) + { + closeCurrentCallEvent(message.getTimestamp(), true, macPduType); + } } else if(message instanceof AbstractVoiceTimeslot) { - if(isEncrypted()) + if(mCurrentCallEvent != null) { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ENCRYPTED, getTimeslot())); - } - else - { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot())); + if(isEncrypted()) + { + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ENCRYPTED, getTimeslot())); + } + else + { + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot())); + } } updateCurrentCall(null, null, message.getTimestamp()); } else if(message instanceof EncryptionSynchronizationSequence) { + //We don't send any state events for this message since it can only occur in conjunction with + //an audio frame that already sends the call state event getIdentifierCollection().update(message.getIdentifiers()); } } } + /** + * Updates the channel state according to the PDU type + */ + private static State getStateFromPduType(MacPduType macPduType) + { + switch(macPduType) + { + case MAC_1_PTT: + return State.CALL; + case MAC_2_END_PTT: + return State.TEARDOWN; + case MAC_4_ACTIVE: + case MAC_6_HANGTIME: + return State.ACTIVE; + case MAC_3_IDLE: + default: + return State.IDLE; + } + } + /** * Adds the current channel to the local identifier collection which will cause it to be broadcast to all of the * other listeners and will allow both timeslots on this channel to receive it and update accordingly. @@ -215,30 +258,67 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie { //Add the set of identifiers before we close out the call event to ensure they're captured in //the closing event. - for(Identifier identifier : mac.getIdentifiers()) + if(mCurrentCallEvent != null) { - if(identifier.getRole() == Role.FROM && identifier instanceof APCO25RadioIdentifier) + for(Identifier identifier : mac.getIdentifiers()) { - int value = ((APCO25RadioIdentifier)identifier).getValue(); - - if(value == SYSTEM_CONTROLLER) + if(identifier.getRole() == Role.FROM) { - //ignore so we don't overwrite the real from radio identifier + if(identifier instanceof APCO25RadioIdentifier) + { + int value = ((APCO25RadioIdentifier)identifier).getValue(); + + //Group call End PTT uses a FROM value of 0xFFFFFF - don't overwrite the correct id + if(value != SYSTEM_CONTROLLER) + { + getIdentifierCollection().update(mPatchGroupManager.update(identifier)); + } + } + } + else if(identifier.getRole() == Role.TO) + { + if(identifier instanceof APCO25Talkgroup) + { + int value = ((APCO25Talkgroup)identifier).getValue(); + + //Individual call End PTT uses a TO value of 0 - don't overwrite the correct id + if(value != 0) + { + getIdentifierCollection().update(mPatchGroupManager.update(identifier)); + } + } } else { - //Add to the identifier collection after filtering through the patch group manager getIdentifierCollection().update(mPatchGroupManager.update(identifier)); } } - else - { - //Add to the identifier collection after filtering through the patch group manager - getIdentifierCollection().update(mPatchGroupManager.update(identifier)); - } } - closeCurrentCallEvent(message.getTimestamp(), true); + if(message.getDataUnitID().isFACCH()) + { + mEndPttOnFacchCounter++; + + //FNE sending 2 or more End PTT in FACCH timeslots indicates a channel teardown event. + if(mEndPttOnFacchCounter > 1) + { + if(mChannelType == ChannelType.TRAFFIC) + { + closeCurrentCallEvent(message.getTimestamp(), true, MacPduType.MAC_2_END_PTT); + //Don't send a continue state here ... the close() method will send the TEARDOWN state + } + //Otherwise the channel state is set to ACTIVE in anticipation of further call activity + else + { + closeCurrentCallEvent(message.getTimestamp(), true, MacPduType.MAC_4_ACTIVE); + continueState(State.RESET); + } + } + } + else + { + closeCurrentCallEvent(message.getTimestamp(), true, MacPduType.MAC_4_ACTIVE); + } } break; case TDMA_0_NULL_INFORMATION_MESSAGE: @@ -246,15 +326,13 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie if(type == MacPduType.MAC_3_IDLE || type == MacPduType.MAC_6_HANGTIME) { - closeCurrentCallEvent(message.getTimestamp(), true); + closeCurrentCallEvent(message.getTimestamp(), true, type); } - continueState(State.ACTIVE); break; case TDMA_1_GROUP_VOICE_CHANNEL_USER_ABBREVIATED: if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME) { - closeCurrentCallEvent(message.getTimestamp(), false); - getIdentifierCollection().remove(Role.FROM); + closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME); for(Identifier identifier : mac.getIdentifiers()) { @@ -291,8 +369,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie case TDMA_33_GROUP_VOICE_CHANNEL_USER_EXTENDED: if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME) { - closeCurrentCallEvent(message.getTimestamp(), false); - getIdentifierCollection().remove(Role.FROM); + closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME); for(Identifier identifier : mac.getIdentifiers()) { @@ -329,8 +406,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie case TDMA_2_UNIT_TO_UNIT_VOICE_CHANNEL_USER: if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME) { - closeCurrentCallEvent(message.getTimestamp(), false); - getIdentifierCollection().remove(Role.FROM); + closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME); for(Identifier identifier : mac.getIdentifiers()) { @@ -367,8 +443,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie case TDMA_34_UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED: if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME) { - closeCurrentCallEvent(message.getTimestamp(), false); - getIdentifierCollection().remove(Role.FROM); + closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME); for(Identifier identifier : mac.getIdentifiers()) { @@ -405,8 +480,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie case TDMA_3_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER: if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME) { - closeCurrentCallEvent(message.getTimestamp(), false); - getIdentifierCollection().remove(Role.FROM); + closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME); for(Identifier identifier : mac.getIdentifiers()) { @@ -439,7 +513,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie } } } - continueState(State.ACTIVE); break; case TDMA_5_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE: if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantUpdateMultiple) @@ -461,7 +534,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie broadcastCurrentChannel(gvcgum.getChannelC()); } } - continueState(State.ACTIVE); break; case TDMA_37_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE_EXPLICIT: if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantUpdateMultipleExplicit) @@ -478,7 +550,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie broadcastCurrentChannel(gvcgume.getChannelB()); } } - continueState(State.ACTIVE); break; case PHASE1_64_GROUP_VOICE_CHANNEL_GRANT_ABBREVIATED: if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantAbbreviated) @@ -490,7 +561,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie broadcastCurrentChannel(gvcga.getChannel()); } } - continueState(State.ACTIVE); break; case PHASE1_66_GROUP_VOICE_CHANNEL_GRANT_UPDATE: if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantUpdate) @@ -507,7 +577,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie broadcastCurrentChannel(gvcgu.getChannelB()); } } - continueState(State.ACTIVE); break; case PHASE1_70_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_ABBREVIATED: if(getCurrentChannel() == null && mac instanceof UnitToUnitVoiceChannelGrantAbbreviated) @@ -519,7 +588,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie broadcastCurrentChannel(uuvcga.getChannel()); } } - continueState(State.ACTIVE); break; case PHASE1_195_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantUpdateExplicit) @@ -531,7 +599,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie broadcastCurrentChannel(gvcgue.getChannel()); } } - continueState(State.ACTIVE); break; case PHASE1_198_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_EXTENDED: if(getCurrentChannel() == null && mac instanceof UnitToUnitVoiceChannelGrantUpdateExtended) @@ -543,7 +610,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie broadcastCurrentChannel(uuvcgue.getChannel()); } } - continueState(State.ACTIVE); break; case TDMA_17_INDIRECT_GROUP_PAGING: MutableIdentifierCollection icGroupPaging = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); @@ -556,7 +622,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .details("GROUP PAGE") .identifiers(icGroupPaging) .build()); - continueState(State.ACTIVE); break; case TDMA_18_INDIVIDUAL_PAGING_MESSAGE_WITH_PRIORITY: if(mac instanceof IndividualPagingMessage) @@ -575,7 +640,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icIndividualPaging) .build()); } - continueState(State.ACTIVE); break; case TDMA_48_POWER_CONTROL_SIGNAL_QUALITY: if(mac instanceof PowerControlSignalQuality) @@ -593,7 +657,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icPowerControl) .build()); } - continueState(State.ACTIVE); break; case TDMA_49_MAC_RELEASE: if(mac instanceof MacRelease) @@ -604,7 +667,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie icMacRelease.remove(IdentifierClass.USER); icMacRelease.update(mac.getIdentifiers()); - closeCurrentCallEvent(message.getTimestamp(), true); + closeCurrentCallEvent(message.getTimestamp(), true, message.getMacPduType()); broadcast(P25DecodeEvent.builder(message.getTimestamp()) .channel(getCurrentChannel()) @@ -630,7 +693,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icMacRelease) .build()); } - continueState(State.ACTIVE); break; case PHASE1_68_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_ABBREVIATED: if(mac instanceof UnitToUnitVoiceChannelGrantAbbreviated) @@ -647,7 +709,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icGrant) .build()); } - continueState(State.ACTIVE); break; case PHASE1_69_UNIT_TO_UNIT_ANSWER_REQUEST_ABBREVIATED: if(mac instanceof UnitToUnitAnswerRequestAbbreviated) @@ -664,7 +725,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icRequest) .build()); } - continueState(State.ACTIVE); break; case PHASE1_74_TELEPHONE_INTERCONNECT_ANSWER_REQUEST: if(mac instanceof TelephoneInterconnectAnswerRequest) @@ -681,7 +741,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icRequest) .build()); } - continueState(State.ACTIVE); break; case PHASE1_76_RADIO_UNIT_MONITOR_COMMAND_ABBREVIATED: if(mac instanceof RadioUnitMonitorCommand) @@ -699,7 +758,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icRequest) .build()); } - continueState(State.ACTIVE); break; case PHASE1_84_SNDCP_DATA_CHANNEL_GRANT: if(mac instanceof SNDCPDataChannelGrant) @@ -716,7 +774,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icGrant) .build()); } - continueState(State.ACTIVE); break; case PHASE1_85_SNDCP_DATA_PAGE_REQUEST: if(mac instanceof SNDCPDataPageRequest) @@ -733,7 +790,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icPage) .build()); } - continueState(State.ACTIVE); break; case PHASE1_88_STATUS_UPDATE_ABBREVIATED: if(mac instanceof StatusUpdateAbbreviated) @@ -750,7 +806,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icStatusUpdate) .build()); } - continueState(State.ACTIVE); break; case PHASE1_90_STATUS_QUERY_ABBREVIATED: if(mac instanceof StatusQueryAbbreviated) @@ -766,7 +821,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icStatusUpdate) .build()); } - continueState(State.ACTIVE); break; case PHASE1_92_MESSAGE_UPDATE_ABBREVIATED: if(mac instanceof MessageUpdateAbbreviated) @@ -783,7 +837,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icStatusUpdate) .build()); } - continueState(State.ACTIVE); break; case PHASE1_94_RADIO_UNIT_MONITOR_COMMAND_ENHANCED: if(mac instanceof RadioUnitMonitorCommandEnhanced) @@ -802,7 +855,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icRequest) .build()); } - continueState(State.ACTIVE); break; case PHASE1_95_CALL_ALERT_ABBREVIATED: MutableIdentifierCollection icCallAlert = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); @@ -814,7 +866,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .eventDescription(DecodeEventType.CALL_ALERT.toString()) .identifiers(icCallAlert) .build()); - continueState(State.ACTIVE); break; case PHASE1_96_ACK_RESPONSE: if(mac instanceof AcknowledgeResponse) @@ -831,7 +882,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icAckResponse) .build()); } - continueState(State.ACTIVE); break; case PHASE1_97_QUEUED_RESPONSE: if(mac instanceof QueuedResponse) @@ -850,7 +900,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icQueuedResponse) .build()); } - continueState(State.ACTIVE); break; case PHASE1_100_EXTENDED_FUNCTION_COMMAND_ABBREVIATED: if(mac instanceof ExtendedFunctionCommand) @@ -868,7 +917,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icExtendedFunction) .build()); } - continueState(State.ACTIVE); break; case PHASE1_103_DENY_RESPONSE: if(mac instanceof DenyResponse) @@ -885,7 +933,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icDenyResponse) .build()); } - continueState(State.ACTIVE); break; case PHASE1_106_GROUP_AFFILIATION_QUERY_ABBREVIATED: MutableIdentifierCollection icGroupAffiliationQuery = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); @@ -898,7 +945,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .details("GROUP AFFILIATION") .identifiers(icGroupAffiliationQuery) .build()); - continueState(State.ACTIVE); break; case PHASE1_109_UNIT_REGISTRATION_COMMAND_ABBREVIATED: MutableIdentifierCollection icUnitRegisterCommand = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); @@ -911,7 +957,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .details("UNIT REGISTRATION") .identifiers(icUnitRegisterCommand) .build()); - continueState(State.ACTIVE); break; case PHASE1_197_UNIT_TO_UNIT_ANSWER_REQUEST_EXTENDED: if(mac instanceof UnitToUnitAnswerRequestExtended) @@ -930,7 +975,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icUnitAnswerRequestExtended) .build()); } - continueState(State.ACTIVE); break; case PHASE1_204_RADIO_UNIT_MONITOR_COMMAND_EXTENDED: if(mac instanceof RadioUnitMonitorCommandExtended) @@ -948,7 +992,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icRequest) .build()); } - continueState(State.ACTIVE); break; case PHASE1_216_STATUS_UPDATE_EXTENDED: if(mac instanceof StatusUpdateExtended) @@ -965,7 +1008,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icStatusUpdate) .build()); } - continueState(State.ACTIVE); break; case PHASE1_218_STATUS_QUERY_EXTENDED: MutableIdentifierCollection icStatusQueryExtended = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); @@ -978,7 +1020,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .details("STATUS QUERY") .identifiers(icStatusQueryExtended) .build()); - continueState(State.ACTIVE); break; case PHASE1_220_MESSAGE_UPDATE_EXTENDED: if(mac instanceof MessageUpdateExtended) @@ -995,7 +1036,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icStatusUpdate) .build()); } - continueState(State.ACTIVE); break; case PHASE1_223_CALL_ALERT_EXTENDED: if(mac instanceof CallAlertExtended) @@ -1010,7 +1050,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icCallAlertExtended) .build()); } - continueState(State.ACTIVE); break; case PHASE1_228_EXTENDED_FUNCTION_COMMAND_EXTENDED: if(mac instanceof ExtendedFunctionCommandExtended) @@ -1028,7 +1067,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icExtendedFunction) .build()); } - continueState(State.ACTIVE); break; case PHASE1_234_GROUP_AFFILIATION_QUERY_EXTENDED: if(mac instanceof GroupAffiliationQueryExtended) @@ -1045,7 +1083,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .identifiers(icGroupAffiliationQueryExtended) .build()); } - continueState(State.ACTIVE); break; case PHASE1_115_IDENTIFIER_UPDATE_TDMA: @@ -1071,8 +1108,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie case PHASE1_196_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_EXTENDED: case UNKNOWN: default: - continueState(State.ACTIVE); - //ignore break; } } @@ -1135,7 +1170,15 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie .build(); broadcast(mCurrentCallEvent); - broadcast(new DecoderStateEvent(this, Event.START, State.CALL, getTimeslot())); + + if(isEncrypted()) + { + broadcast(new DecoderStateEvent(this, Event.START, State.ENCRYPTED, getTimeslot())); + } + else + { + broadcast(new DecoderStateEvent(this, Event.START, State.CALL, getTimeslot())); + } } else { @@ -1152,7 +1195,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie mCurrentCallEvent.setIdentifierCollection(getIdentifierCollection().copyOf()); mCurrentCallEvent.end(timestamp); broadcast(mCurrentCallEvent); - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot())); } } @@ -1160,8 +1202,10 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie * Ends/closes the current call event. * * @param timestamp of the message that indicates the event has ended. + * @param resetIdentifiers to reset the FROM/TO identifiers + * @param pduType of the message that caused the close call event - to determine channel state after call */ - private void closeCurrentCallEvent(long timestamp, boolean resetIdentifiers) + private void closeCurrentCallEvent(long timestamp, boolean resetIdentifiers, MacPduType pduType) { if(mCurrentCallEvent != null) { @@ -1169,21 +1213,21 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie broadcast(mCurrentCallEvent); mCurrentCallEvent = null; - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot())); - } - else - { - continueState(State.ACTIVE); - } + broadcast(new DecoderStateEvent(this, Event.END, getStateFromPduType(pduType), getTimeslot())); - if(resetIdentifiers) - { - getIdentifierCollection().remove(IdentifierClass.USER); + if(resetIdentifiers) + { + getIdentifierCollection().remove(IdentifierClass.USER); + } + else + { + //Only clear the from identifier at this point ... the channel may still be allocated to the TO talkgroup + getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM); + } } - else + else if(pduType == MacPduType.MAC_2_END_PTT) { - //Only clear the from identifier at this point ... the channel may still be allocated to the TO talkgroup - getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM); + broadcast(new DecoderStateEvent(this, Event.END, getStateFromPduType(pduType), getTimeslot())); } } @@ -1215,14 +1259,17 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { - switch(event.getEvent()) + if(event.getTimeslot() == getTimeslot()) { - case RESET: - resetState(); - mNetworkConfigurationMonitor.reset(); - break; - default: - break; + switch(event.getEvent()) + { + case RESET: + resetState(); + mNetworkConfigurationMonitor.reset(); + break; + default: + break; + } } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java index 5b1bb2d0..9fb9a5fa 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageFramer.java @@ -224,25 +224,30 @@ public class P25P2MessageFramer implements Listener public static void main(String[] args) { UserPreferences userPreferences = new UserPreferences(); - userPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(Path.of("/home/denny/JMBE/jmbe-1.0.2.jar")); + userPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(Path.of("/home/denny/JMBE/jmbe-1.0.3.jar")); ApplicationLog applicationLog = new ApplicationLog(userPreferences); applicationLog.start(); +// Path directory = Paths.get("/home/denny/SDRTrunk/recordings"); Path directory = Paths.get("/home/denny/temp/Harris P25-2 Logs/bits"); // Path directory = Paths.get("/media/denny/500G1EXT4/PBITRecordings"); +// ScrambleParameters scrambleParameters = new ScrambleParameters(781824, 1186, 1189); //Nugent ScrambleParameters scrambleParameters = new ScrambleParameters(123654, 813, 10); //Hills // ScrambleParameters scrambleParameters = new ScrambleParameters(1, 972, 972)); -// ScrambleParameters scrambleParameters = new ScrambleParameters(781824, 686, 677)); //CNYICC - Rome +// ScrambleParameters scrambleParameters = new ScrambleParameters(781824, 686, 677); //CNYICC - Rome + +// Channel channel = new Channel("Phase 2 Test", Channel.ChannelType.STANDARD); + Channel channel = new Channel("Phase 2 Test", Channel.ChannelType.TRAFFIC); - Channel channel = new Channel("Phase 2 Test"); channel.setDecodeConfiguration(new DecodeConfigP25Phase2()); AliasList aliasList = new AliasList("Test Alias List"); - Alias alias = new Alias("TG Range 1-65535"); - alias.addAliasID(new Record()); +// Alias alias = new Alias("TG Range 1-65535"); +// alias.addAliasID(new Record()); // alias.addAliasID(new TalkgroupRange(Protocol.APCO25, 1, 65535)); - alias.addAliasID(new Talkgroup(Protocol.APCO25, 11185)); - aliasList.addAlias(alias); +// alias.addAliasID(new Talkgroup(Protocol.APCO25, 11857)); +// alias.addAliasID(new Talkgroup(Protocol.APCO25, 12601)); +// aliasList.addAlias(alias); AudioRecordingManager recordingManager = new AudioRecordingManager(userPreferences); recordingManager.start(); ProcessingChain processingChain = new ProcessingChain(channel, new AliasModel()); @@ -254,20 +259,21 @@ public class P25P2MessageFramer implements Listener processingChain.addModule(new P25P2AudioModule(userPreferences, 1, aliasList)); MessageProviderModule messageProviderModule = new MessageProviderModule(); processingChain.addModule(messageProviderModule); - processingChain.start(); try(OutputStream logOutput = Files.newOutputStream(directory.resolve("log.txt"))) { try { - DirectoryStream stream = Files.newDirectoryStream(directory, "*PHASE2_Hills*TRAFFIC.bits"); +// DirectoryStream stream = Files.newDirectoryStream(directory, "*042102*TRAFFIC.bits"); +// DirectoryStream stream = Files.newDirectoryStream(directory, "*042102*.bits"); + DirectoryStream stream = Files.newDirectoryStream(directory, "*214601*Hills*TRAFFIC.bits"); stream.forEach(new Consumer() { @Override public void accept(Path path) { - mLog.debug("Processing:" + path.toString()); + mLog.debug("\n\nProcessing:" + path.toString() + "\n\n"); try { logOutput.write(("Processing:" + path.toString() + "\n").getBytes()); @@ -277,6 +283,8 @@ public class P25P2MessageFramer implements Listener mLog.error("Error", ioe); } + processingChain.start(); + P25P2MessageFramer messageFramer = new P25P2MessageFramer(null, DecoderType.P25_PHASE2.getProtocol().getBitRate()); messageFramer.setScrambleParameters(scrambleParameters); P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor(); @@ -313,6 +321,12 @@ public class P25P2MessageFramer implements Listener { ioe.printStackTrace(); } + + mLog.debug("**STOPPING PROCESSING CHAIN**"); + + processingChain.stop(); + + mLog.debug("\n============================================================================================================================\n"); } } ); @@ -326,5 +340,7 @@ public class P25P2MessageFramer implements Listener { mLog.error("Error", ioe); } + + recordingManager.stop(); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/DataUnitID.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/DataUnitID.java index 1f5387c5..2cfd5a79 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/DataUnitID.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/enumeration/DataUnitID.java @@ -65,6 +65,22 @@ public enum DataUnitID return mValueWithParity; } + /** + * Indicates if this DUID is a SACCH, either scrambled or unscrambled + */ + public boolean isSACCH() + { + return this == SCRAMBLED_SACCH || this == UNSCRAMBLED_SACCH; + } + + /** + * Indicates if this DUID is a FACCH, either scrambled or unscrambled + */ + public boolean isFACCH() + { + return this == SCRAMBLED_FACCH || this == UNSCRAMBLED_FACCH; + } + @Override public String toString() { diff --git a/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java b/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java index 90100242..9a0d623b 100644 --- a/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java +++ b/src/main/java/io/github/dsheirer/record/AudioRecordingManager.java @@ -39,7 +39,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.ScheduledFuture; @@ -55,6 +54,8 @@ public class AudioRecordingManager implements Listener private ScheduledFuture mQueueProcessorHandle; private UserPreferences mUserPreferences; private int mUnknownAudioRecordingIndex = 1; + private int mDuplicateAudioRecordingSuffix = 1; + private String mPreviousRecordingPath = null; /** * Constructs an instance @@ -133,28 +134,26 @@ public class AudioRecordingManager implements Listener */ private void processAudioSegments() { - List audioSegments = new ArrayList<>(); - mCompletedAudioSegmentQueue.drainTo(audioSegments); + RecordFormat recordFormat = mUserPreferences.getRecordPreference().getAudioRecordFormat(); + AudioSegment audioSegment = mCompletedAudioSegmentQueue.poll(); - if(!audioSegments.isEmpty()) + while(audioSegment != null) { - RecordFormat recordFormat = mUserPreferences.getRecordPreference().getAudioRecordFormat(); + Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat); - for(AudioSegment audioSegment: audioSegments) + try { - Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat); - - try - { - AudioSegmentRecorder.record(audioSegment, path, recordFormat); - } - catch(IOException ioe) - { - mLog.error("Error recording audio segment to [" + path.toString() + "]"); - } - - audioSegment.decrementConsumerCount(); + AudioSegmentRecorder.record(audioSegment, path, recordFormat); } + catch(IOException ioe) + { + mLog.error("Error recording audio segment to [" + path.toString() + "]"); + } + + audioSegment.decrementConsumerCount(); + + //Grab the next one to record + audioSegment = mCompletedAudioSegmentQueue.poll(); } } @@ -210,7 +209,7 @@ public class AudioRecordingManager implements Listener if(!toIdentifiers.isEmpty()) { - sb.append("_TO_").append(toIdentifiers.get(0)).toString(); + sb.append("_TO_").append(toIdentifiers.get(0)); } } @@ -227,7 +226,14 @@ public class AudioRecordingManager implements Listener if(!fromIdentifiers.isEmpty()) { - sb.append("_FROM_").append(fromIdentifiers.get(0)).toString(); + for(Identifier identifier: fromIdentifiers) + { + if(identifier.getForm() != Form.TONE) + { + sb.append("_FROM_").append(identifier); + break; + } + } } } } @@ -246,6 +252,26 @@ public class AudioRecordingManager implements Listener //Remove any illegal filename characters sbFinal.append(StringUtils.replaceIllegalCharacters(sb.toString())); + + if(mPreviousRecordingPath != null && mPreviousRecordingPath.contentEquals(sbFinal.toString())) + { + sbFinal.append("_V").append(mDuplicateAudioRecordingSuffix++); + } + else + { + mDuplicateAudioRecordingSuffix = 2; + mPreviousRecordingPath = sbFinal.toString(); + } + + for(Identifier identifier: identifierCollection.getIdentifiers(Role.FROM)) + { + if(identifier.getForm() == Form.TONE) + { + sbFinal.append("_WITH_TONES"); + break; + } + } + sbFinal.append(recordFormat.getExtension()); return getRecordingBasePath().resolve(sbFinal.toString()); diff --git a/src/main/java/io/github/dsheirer/source/tuner/usb/USBMasterProcessor.java b/src/main/java/io/github/dsheirer/source/tuner/usb/USBMasterProcessor.java index bcab6755..42ac28e5 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/usb/USBMasterProcessor.java +++ b/src/main/java/io/github/dsheirer/source/tuner/usb/USBMasterProcessor.java @@ -81,7 +81,7 @@ public class USBMasterProcessor { if(mRunning.compareAndSet(false, true)) { - mLog.warn("Starting USB master processor thread"); + mLog.info("Starting USB master processor thread"); if(mProcessorThread == null) { mProcessor.reset(); @@ -110,7 +110,7 @@ public class USBMasterProcessor { if(mRunning.compareAndSet(true, false)) { - mLog.warn("Stopping USB master processor thread"); + mLog.info("Stopping USB master processor thread"); if(mProcessorThread != null) { mProcessor.stop(); diff --git a/src/main/java/io/github/dsheirer/util/StringUtils.java b/src/main/java/io/github/dsheirer/util/StringUtils.java index df85edce..503ea64f 100644 --- a/src/main/java/io/github/dsheirer/util/StringUtils.java +++ b/src/main/java/io/github/dsheirer/util/StringUtils.java @@ -21,7 +21,7 @@ package io.github.dsheirer.util; public class StringUtils { private static final String[] ILLEGAL_FILENAME_CHARACTERS = {"#", "%", "&", "{", "}", "\\", "<", ">", - "*", "?", "/", " ", "$", "!", "'", "\"", ":", "@", "+", "`", "|", "="}; + "*", "?", "/", " ", "$", "!", "'", "\"", ":", "@", "+", "`", "|", "=", ","}; /** * Compares string a to b and returns true if a is non-null and non-empty, b is non-null and non-empty, and