Adds 'ACTIVE' as a valid channel state to indicate that the channel is

actively decoding, but no call or data is being transmitted.  This is
primarily used for P25 channels when TDU Link Control messages are
being transmitted in between voice or data transmissions on a channel.

Updated several of the P25 decoder state message processing sections to
keep the channel state UI updated with current call identifiers and
states.

Hard-coded channel state colors to remove inefficiency in the way that
channel state coloration was being handled.
This commit is contained in:
Denny
2018-08-11 05:36:30 -04:00
parent 9176ca610f
commit 8e4e4129ca
5 changed files with 195 additions and 147 deletions

View File

@@ -123,6 +123,11 @@ public class AttributeMonitor<T>
{
mCurrentValue = null;
mOccurrenceCounts.clear();
if(mListener != null)
{
mListener.receive(new AttributeChangeRequest(mAttribute, null, null));
}
}
/**

View File

@@ -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<ProcessingChain> mSelectedProcessingChainBroadcaster = new Broadcaster<>();
private Map<State,Color> mBackgroundColors = new HashMap<>();
private Map<State,Color> 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

View File

@@ -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())

View File

@@ -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<State> CALL_STATES = EnumSet.of(CALL, CONTROL, DATA, ENCRYPTED);
public static final EnumSet<State> CALL_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
public static final EnumSet<State> IDLE_STATES = EnumSet.of(IDLE, FADE);
private State(String displayValue)

View File

@@ -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: