diff --git a/src/main/java/io/github/dsheirer/channel/metadata/AttributeMonitor.java b/src/main/java/io/github/dsheirer/channel/metadata/AttributeMonitor.java index 2a7bc581..6bc79da3 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/AttributeMonitor.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/AttributeMonitor.java @@ -123,6 +123,11 @@ public class AttributeMonitor { mCurrentValue = null; mOccurrenceCounts.clear(); + + if(mListener != null) + { + mListener.receive(new AttributeChangeRequest(mAttribute, null, null)); + } } /** diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java index b78129a2..f340e92b 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java @@ -25,32 +25,36 @@ import io.github.dsheirer.controller.channel.ChannelProcessingManager; import io.github.dsheirer.controller.channel.ChannelUtils; import io.github.dsheirer.icon.IconManager; import io.github.dsheirer.module.ProcessingChain; -import io.github.dsheirer.properties.SystemProperties; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import net.miginfocom.swing.MigLayout; import org.apache.commons.lang3.Validate; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.swing.*; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.SwingConstants; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableCellRenderer; -import java.awt.*; +import java.awt.Color; +import java.awt.Component; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.util.HashMap; +import java.util.Map; public class ChannelMetadataPanel extends JPanel implements ListSelectionListener { - private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataPanel.class); - - private static final String PROPERTY_PREFIX_BACKGROUND = "channel.metadata.panel.state.color."; - private static final String PROPERTY_PREFIX_FOREGROUND = "channel.metadata.panel.text.color."; private ChannelProcessingManager mChannelProcessingManager; private IconManager mIconManager; private JTable mTable; private Broadcaster mSelectedProcessingChainBroadcaster = new Broadcaster<>(); + private Map mBackgroundColors = new HashMap<>(); + private Map mForegroundColors = new HashMap<>(); /** * Table view for currently decoding channel metadata @@ -94,7 +98,34 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene add(scrollPane); -// new TableColumnWidthMonitor(mTable, PROPERTY_PREFIX, new int[] {15,15,15,15,25,25,25,25,15,40}); + setColors(); + } + + /** + * Setup the background and foreground color palette for the various channel states. + */ + private void setColors() + { + mBackgroundColors.put(State.ACTIVE, Color.CYAN); + mForegroundColors.put(State.ACTIVE, Color.YELLOW); + mBackgroundColors.put(State.CALL, Color.BLUE); + mForegroundColors.put(State.CALL, Color.YELLOW); + mBackgroundColors.put(State.CONTROL, Color.ORANGE); + mForegroundColors.put(State.CONTROL, Color.BLUE); + mBackgroundColors.put(State.DATA, Color.GREEN); + mForegroundColors.put(State.DATA, Color.BLUE); + mBackgroundColors.put(State.ENCRYPTED, Color.MAGENTA); + mForegroundColors.put(State.ENCRYPTED, Color.WHITE); + mBackgroundColors.put(State.FADE, Color.LIGHT_GRAY); + mForegroundColors.put(State.FADE, Color.DARK_GRAY); + mBackgroundColors.put(State.IDLE, Color.WHITE); + mForegroundColors.put(State.IDLE, Color.DARK_GRAY); + mBackgroundColors.put(State.OVERFLOW, Color.RED); + mForegroundColors.put(State.OVERFLOW, Color.YELLOW); + mBackgroundColors.put(State.RESET, Color.PINK); + mForegroundColors.put(State.RESET, Color.YELLOW); + mBackgroundColors.put(State.TEARDOWN, Color.DARK_GRAY); + mForegroundColors.put(State.TEARDOWN, Color.WHITE); } @Override @@ -185,44 +216,14 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene State state = (State)value; label.setText(state.getDisplayValue()); - switch(state) + if(mBackgroundColors.containsKey(state)) { - case CALL: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "call", Color.BLUE); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "call", Color.YELLOW); - break; - case CONTROL: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "control", Color.ORANGE); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "control", Color.BLUE); - break; - case DATA: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "data", Color.GREEN); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "data", Color.BLUE); - break; - case ENCRYPTED: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "encrypted", Color.MAGENTA); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "encrypted", Color.WHITE); - break; - case FADE: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "fade", Color.LIGHT_GRAY.BLUE); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "fade", Color.YELLOW); - break; - case IDLE: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "idle", Color.WHITE); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "idle", Color.BLUE); - break; - case OVERFLOW: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "overflow", Color.RED); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "overflow", Color.YELLOW); - break; - case RESET: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "reset", Color.PINK); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "reset", Color.YELLOW); - break; - case TEARDOWN: - background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "teardown", Color.DARK_GRAY); - foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "teardown", Color.WHITE); - break; + background = mBackgroundColors.get(state); + } + + if(mForegroundColors.containsKey(state)) + { + foreground = mForegroundColors.get(state); } } else diff --git a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java index bc71e4f1..1d192bc6 100644 --- a/src/main/java/io/github/dsheirer/channel/state/ChannelState.java +++ b/src/main/java/io/github/dsheirer/channel/state/ChannelState.java @@ -303,6 +303,11 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder { switch(state) { + case ACTIVE: + broadcast(SquelchState.SQUELCH); + updateFadeTimeout(); + mState = state; + break; case CONTROL: //Don't allow traffic channels to be control channels, otherwise they can't transition to teardown if(isStandardChannel()) 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 a4047e7f..b4b9a6df 100644 --- a/src/main/java/io/github/dsheirer/channel/state/State.java +++ b/src/main/java/io/github/dsheirer/channel/state/State.java @@ -24,54 +24,114 @@ import java.util.EnumSet; */ public enum State { - IDLE("IDLE") + /** + * Active state indicates that valid messages are being decoded that do not indicate any call or data + * activity. For example, this is used for P25 when a channel is actively transmitting Terminator Data + * Units in between data packets or at the beginning or end of a call. + */ + ACTIVE("ACTIVE") { @Override public boolean canChangeTo(State state) { - return state != TEARDOWN && state != RESET && state != OVERFLOW; + return state == CALL | + state == CONTROL || + state == DATA || + state == ENCRYPTED || + state == FADE || + state == TEARDOWN; } }, + /** + * Indicates that call messages are being decoded, or a call is in progress (ie producing audio). + */ CALL("CALL") { @Override public boolean canChangeTo(State state) { - return state == CONTROL || state == DATA || state == ENCRYPTED || state == FADE || state == TEARDOWN; - } - }, - DATA("DATA") - { - @Override - public boolean canChangeTo(State state) - { - return state == CALL || state == CONTROL || state == ENCRYPTED || state == FADE || state == TEARDOWN; - } - }, - ENCRYPTED("ENCRYPTED") - { - @Override - public boolean canChangeTo(State state) - { - return state == FADE || state == TEARDOWN; + return state == ACTIVE || + state == CONTROL || + state == DATA || + state == ENCRYPTED || + state == FADE || + state == TEARDOWN; } }, + /** + * Indicates a trunking control channel is being decoded. + */ CONTROL("CONTROL") { @Override public boolean canChangeTo(State state) { - return state == IDLE || state == FADE; + return state == IDLE || + state == FADE; } }, + /** + * Indicates data packets are being decoded + */ + DATA("DATA") + { + @Override + public boolean canChangeTo(State state) + { + return state == ACTIVE || + state == CALL || + state == CONTROL || + state == ENCRYPTED || + state == FADE || + state == TEARDOWN; + } + }, + /** + * Indicates encrypted audio is detected. + */ + ENCRYPTED("ENCRYPTED") + { + @Override + public boolean canChangeTo(State state) + { + return state == FADE || + state == TEARDOWN; + } + }, + /** + * Fade state occurs when the channel is no longer actively decoding an messages. This is a transitory + * state before transition to IDLE or back to an active state. + */ FADE("FADE") { @Override public boolean canChangeTo(State state) { - return state != FADE && state != RESET && state != OVERFLOW; + return state != FADE && + state != RESET && + state != OVERFLOW; } }, + /** + * Indicates the channel is idle and not actively decoding any messages + */ + IDLE("IDLE") + { + @Override + public boolean canChangeTo(State state) + { + return state != TEARDOWN && + state != RESET && + state != OVERFLOW; + } + }, + /** + * Indicates that the channel's sample buffer has overflowed and new buffer samples are no longer being + * delivered to the channel. This state persists until the processing catches up and reduces the number + * of samples in the input buffer below a threshold value. Transition to and from this state is exclusively + * managed by the Channel State and no other channel states can be processed from the channel decoders + * until the channel state is taken out of overflow. + */ OVERFLOW("OVERFLOW") { @Override @@ -80,6 +140,9 @@ public enum State return false; } }, + /** + * Indicates that the channel has been reset and is ready to use for a new decoding session. + */ RESET("RESET") { @Override @@ -88,6 +151,10 @@ public enum State return state == IDLE; } }, + /** + * Call or data teardown. This state is used by traffic channels to indicate that a call or data session + * has concluded and the channel is ready for tear down and reuse. + */ TEARDOWN("TEARDOWN") { @Override @@ -99,7 +166,7 @@ public enum State private String mDisplayValue; - public static final EnumSet CALL_STATES = EnumSet.of(CALL, CONTROL, DATA, ENCRYPTED); + public static final EnumSet CALL_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED); public static final EnumSet IDLE_STATES = EnumSet.of(IDLE, FADE); private State(String displayValue) diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderState.java index d2331b60..2fcdd2f3 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderState.java @@ -254,6 +254,7 @@ public class P25DecoderState extends DecoderState if(mCurrentCallEvent != null) { mCurrentCallEvent.end(); + broadcast(mCurrentCallEvent); } mCurrentCallEvent = null; @@ -395,6 +396,10 @@ public class P25DecoderState extends DecoderState */ private void processTDU(TDUMessage tdu) { + if(mChannelType == ChannelType.STANDARD) + { + broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE)); + } } /** @@ -432,19 +437,21 @@ public class P25DecoderState extends DecoderState */ private void processTDULC(TDULinkControlMessage tdulc) { + if(mCurrentCallEvent != null) + { + mCurrentCallEvent.end(); + broadcast(mCurrentCallEvent); + mCurrentCallEvent = null; + } + if(tdulc.getOpcode() == LinkControlOpcode.CALL_TERMINATION_OR_CANCELLATION) { broadcast(new DecoderStateEvent(this, Event.END, State.FADE)); - if(mCurrentCallEvent != null) - { - mCurrentCallEvent.end(); - mCurrentCallEvent = null; - } } else { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); + broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE)); } switch(tdulc.getOpcode()) @@ -495,22 +502,17 @@ public class P25DecoderState extends DecoderState /* This opcode as handled at the beginning of the method */ break; case CHANNEL_IDENTIFIER_UPDATE: - //TODO: does the activity summary need this message? - /* This message is handled by the P25MessageProcessor and * inserted into any channels needing frequency band info */ break; case CHANNEL_IDENTIFIER_UPDATE_EXPLICIT: - //TODO: does the activity summary need this message? - /* This message is handled by the P25MessageProcessor and * inserted into any channels needing frequency band info */ break; case EXTENDED_FUNCTION_COMMAND: if(tdulc instanceof ExtendedFunctionCommand) { - ExtendedFunctionCommand efc = - (ExtendedFunctionCommand)tdulc; + ExtendedFunctionCommand efc = (ExtendedFunctionCommand)tdulc; broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.COMMAND) .aliasList(getAliasList()) @@ -606,42 +608,25 @@ public class P25DecoderState extends DecoderState } break; case GROUP_VOICE_CHANNEL_USER: - /* Used on a traffic channel to reflect current call entities */ + //Used on a traffic channel to reflect that the channel is currently being used by the + //TO talkgroup, but that channel is currently quiet (ie no voice) */ if(tdulc instanceof GroupVoiceChannelUser) { - GroupVoiceChannelUser gvcuser = - (GroupVoiceChannelUser)tdulc; + GroupVoiceChannelUser gvcuser = (GroupVoiceChannelUser)tdulc; - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE)); + + //The from value should always be "000000" indicating that nobody is currently talking + mFromTalkgroupMonitor.reset(); - String from = gvcuser.getSourceAddress(); String to = gvcuser.getGroupAddress(); - - mFromTalkgroupMonitor.process(from); mToTalkgroupMonitor.process(to); - - if(mCurrentCallEvent == null) - { - mCurrentCallEvent = new P25CallEvent.Builder(CallEvent.CallEventType.GROUP_CALL) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details("TDULC GROUP VOICE CHANNEL USER" + - (gvcuser.isEncrypted() ? "ENCRYPTED " : "") + - (gvcuser.isEmergency() ? "EMERGENCY " : "")) - .frequency(mCurrentChannelFrequency) - .from(from) - .to(to) - .build(); - - broadcast(mCurrentCallEvent); - } } break; case MESSAGE_UPDATE: if(tdulc instanceof MessageUpdate) { - MessageUpdate mu = - (MessageUpdate)tdulc; + MessageUpdate mu = (MessageUpdate)tdulc; broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.SDM) .aliasList(getAliasList()) @@ -748,8 +733,7 @@ public class P25DecoderState extends DecoderState io.github.dsheirer.module.decode.p25.message.tdu.lc.SecondaryControlChannelBroadcast sccb = (io.github.dsheirer.module.decode.p25.message.tdu.lc.SecondaryControlChannelBroadcast)tdulc; - String site = sccb.getRFSubsystemID() + "-" + - sccb.getSiteID(); + String site = sccb.getRFSubsystemID() + "-" + sccb.getSiteID(); mSiteAttributeMonitor.process(site); } @@ -761,11 +745,9 @@ public class P25DecoderState extends DecoderState case SECONDARY_CONTROL_CHANNEL_BROADCAST_EXPLICIT: if(tdulc instanceof SecondaryControlChannelBroadcastExplicit) { - SecondaryControlChannelBroadcastExplicit sccb = - (SecondaryControlChannelBroadcastExplicit)tdulc; + SecondaryControlChannelBroadcastExplicit sccb = (SecondaryControlChannelBroadcastExplicit)tdulc; - String site = sccb.getRFSubsystemID() + "-" + - sccb.getSiteID(); + String site = sccb.getRFSubsystemID() + "-" + sccb.getSiteID(); mSiteAttributeMonitor.process(site); } @@ -777,8 +759,7 @@ public class P25DecoderState extends DecoderState case STATUS_QUERY: if(tdulc instanceof StatusQuery) { - StatusQuery sq = - (StatusQuery)tdulc; + StatusQuery sq = (StatusQuery)tdulc; broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.QUERY) .aliasList(getAliasList()) @@ -816,8 +797,7 @@ public class P25DecoderState extends DecoderState case TELEPHONE_INTERCONNECT_ANSWER_REQUEST: if(tdulc instanceof TelephoneInterconnectAnswerRequest) { - TelephoneInterconnectAnswerRequest tiar = - (TelephoneInterconnectAnswerRequest)tdulc; + TelephoneInterconnectAnswerRequest tiar = (TelephoneInterconnectAnswerRequest)tdulc; broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.PAGE) .aliasList(getAliasList()) @@ -835,8 +815,7 @@ public class P25DecoderState extends DecoderState if(mChannelType == ChannelType.STANDARD && tdulc instanceof TelephoneInterconnectVoiceChannelUser) { - TelephoneInterconnectVoiceChannelUser tivcu = - (TelephoneInterconnectVoiceChannelUser)tdulc; + TelephoneInterconnectVoiceChannelUser tivcu = (TelephoneInterconnectVoiceChannelUser)tdulc; String to = tivcu.getAddress(); @@ -860,8 +839,7 @@ public class P25DecoderState extends DecoderState case UNIT_AUTHENTICATION_COMMAND: if(tdulc instanceof UnitAuthenticationCommand) { - UnitAuthenticationCommand uac = - (UnitAuthenticationCommand)tdulc; + UnitAuthenticationCommand uac = (UnitAuthenticationCommand)tdulc; broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.COMMAND) .aliasList(getAliasList()) @@ -877,8 +855,7 @@ public class P25DecoderState extends DecoderState case UNIT_REGISTRATION_COMMAND: if(tdulc instanceof UnitRegistrationCommand) { - UnitRegistrationCommand urc = - (UnitRegistrationCommand)tdulc; + UnitRegistrationCommand urc = (UnitRegistrationCommand)tdulc; broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.COMMAND) .aliasList(getAliasList()) @@ -894,8 +871,7 @@ public class P25DecoderState extends DecoderState case UNIT_TO_UNIT_ANSWER_REQUEST: if(tdulc instanceof UnitToUnitAnswerRequest) { - UnitToUnitAnswerRequest uuar = - (UnitToUnitAnswerRequest)tdulc; + UnitToUnitAnswerRequest uuar = (UnitToUnitAnswerRequest)tdulc; broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.PAGE) .aliasList(getAliasList()) @@ -913,27 +889,12 @@ public class P25DecoderState extends DecoderState /* Used on traffic channels to indicate the current call entities */ if(tdulc instanceof UnitToUnitVoiceChannelUser) { - UnitToUnitVoiceChannelUser uuvcu = - (UnitToUnitVoiceChannelUser)tdulc; + UnitToUnitVoiceChannelUser uuvcu = (UnitToUnitVoiceChannelUser)tdulc; - String from = uuvcu.getSourceAddress(); - mFromTalkgroupMonitor.process(from); + mFromTalkgroupMonitor.reset(); String to = uuvcu.getTargetAddress(); mToTalkgroupMonitor.process(to); - - if(mCurrentCallEvent != null) - { - mCurrentCallEvent = new P25CallEvent.Builder(CallEvent.CallEventType.UNIT_TO_UNIT_CALL) - .aliasList(getAliasList()) - .channel(mCurrentChannel) - .details((uuvcu.isEncrypted() ? "ENCRYPTED " : "") + - (uuvcu.isEmergency() ? "EMERGENCY " : "")) - .frequency(mCurrentChannelFrequency) - .from(from) - .to(to) - .build(); - } } break; default: @@ -1166,9 +1127,17 @@ public class P25DecoderState extends DecoderState (io.github.dsheirer.module.decode.p25.message.ldu.lc.GroupVoiceChannelUser)ldu; mFromTalkgroupMonitor.process(gvcuser.getSourceAddress()); + mCurrentCallEvent.setFromID(mFromTalkgroupMonitor.getValue()); mToTalkgroupMonitor.process(gvcuser.getGroupAddress()); + String to = mToTalkgroupMonitor.getValue(); + + if(mCurrentCallEvent.getToID() == null || !mCurrentCallEvent.getToID().contentEquals(to)) + { + mCurrentCallEvent.setToID(to); + } + if(mChannelType == ChannelType.STANDARD) { if(mCurrentCallEvent.getCallEventType() != CallEvent.CallEventType.GROUP_CALL) @@ -1631,14 +1600,13 @@ public class P25DecoderState extends DecoderState */ private void processPDU(PDUMessage pdu) { + broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); + if(pdu instanceof PacketData || pdu instanceof PDUTypeUnknown) { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); } else if(pdu instanceof PDUConfirmedMessage) { - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); - PDUConfirmedMessage pduc = (PDUConfirmedMessage)pdu; switch(pduc.getPDUType()) @@ -1723,6 +1691,11 @@ public class P25DecoderState extends DecoderState sbTo.append(sud.getUDPDestinationPort()); } + mFromTalkgroupMonitor.reset(); + mFromTalkgroupMonitor.process(sbFrom.toString()); + mToTalkgroupMonitor.reset(); + mToTalkgroupMonitor.process(pduc.getLogicalLinkID()); + broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.DATA_CALL) @@ -1750,9 +1723,6 @@ public class P25DecoderState extends DecoderState } else { - /* These are alternate trunking control blocks in PDU format */ - broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL)); - switch(pdu.getOpcode()) { case GROUP_DATA_CHANNEL_GRANT: