/******************************************************************************* * SDR Trunk * Copyright (C) 2014-2016 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 module.decode.ltrstandard; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import message.Message; import module.decode.DecoderType; import module.decode.event.CallEvent.CallEventType; import module.decode.ltrnet.LTRCallEvent; import module.decode.ltrstandard.message.CallEndMessage; import module.decode.ltrstandard.message.CallMessage; import module.decode.ltrstandard.message.IdleMessage; import module.decode.ltrstandard.message.LTRStandardMessage; import module.decode.state.ChangedAttribute; import module.decode.state.DecoderState; import module.decode.state.DecoderStateEvent; import module.decode.state.DecoderStateEvent.Event; import module.decode.state.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import alias.Alias; import alias.AliasList; import audio.metadata.Metadata; import audio.metadata.MetadataType; public class LTRStandardDecoderState extends DecoderState { private final static Logger mLog = LoggerFactory.getLogger( LTRStandardDecoderState.class ); private Map mActiveCalls = new HashMap<>(); private Set mTalkgroupsFirstHeard = new HashSet<>(); private Set mTalkgroups = new TreeSet<>(); private String mTalkgroup; private Alias mTalkgroupAlias; private long mFrequency; private LCNTracker mLCNTracker = new LCNTracker(); public LTRStandardDecoderState( AliasList aliasList ) { super( aliasList ); } @Override public DecoderType getDecoderType() { return DecoderType.LTR_STANDARD; } @Override public void start() { } @Override public void stop() { } @Override public void receive( Message message ) { if( message.isValid() && message instanceof LTRStandardMessage ) { switch( ((LTRStandardMessage)message).getMessageType() ) { case CA_STRT: CallMessage start = (CallMessage)message; int channel = start.getChannel(); mLCNTracker.processCallChannel( channel ); mLCNTracker.processFreeChannel( start.getFree() ); //Only process calls on this LCN, or call detects for //talkgroups that are homed on this LCN if( mLCNTracker.isValidChannel( channel ) && ( mLCNTracker.isCurrentChannel( channel ) || mLCNTracker.isCurrentChannel( start.getHomeRepeater() ) ) ) { LTRCallEvent event = mActiveCalls.get( channel ); if( event == null || !event.isMatchingTalkgroup( start.getToID() ) ) { //Check for different talkgroup if( event != null ) { event.end(); mActiveCalls.remove( channel ); } boolean current = mLCNTracker.isCurrentChannel( channel ); event = new LTRCallEvent.Builder( DecoderType.LTR_STANDARD, current ? CallEventType.CALL : CallEventType.CALL_DETECT ) .to( start.getToID() ) .aliasList( getAliasList() ) .channel( start.getChannelFormatted() ) .frequency( current ? mFrequency : 0 ) .build(); mActiveCalls.put( channel, event ); broadcast( event ); if( mLCNTracker.isCurrentChannel( channel ) ) { mTalkgroup = start.getToID(); broadcast( ChangedAttribute.TO_TALKGROUP ); processTalkgroup( mTalkgroup ); mTalkgroupAlias = start.getToIDAlias(); broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); broadcast( new Metadata( MetadataType.TO, mTalkgroup, mTalkgroupAlias, true ) ); } } broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CALL ) ); } break; case CA_ENDD: CallEndMessage end = (CallEndMessage)message; //Home channel is 31 for call end -- use the free channel //as the call end channel int repeater = end.getFree(); mLCNTracker.processCallChannel( repeater ); if( mLCNTracker.isCurrentChannel( repeater ) ) { mTalkgroup = end.getToID(); broadcast( ChangedAttribute.TO_TALKGROUP ); processTalkgroup( mTalkgroup ); mTalkgroupAlias = end.getToIDAlias(); broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); LTRCallEvent event = mActiveCalls.remove( repeater ); if( event != null ) { event.end(); broadcast( event ); broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) ); } } break; case SY_IDLE: IdleMessage idle = (IdleMessage)message; int lcn = idle.getChannel(); mLCNTracker.processCallChannel( lcn ); break; case UN_KNWN: default: break; } } } private void processTalkgroup( String talkgroup ) { if( mTalkgroupsFirstHeard.contains( talkgroup ) ) { mTalkgroups.add( talkgroup ); } else { mTalkgroupsFirstHeard.add( talkgroup ); } } /** * Performs a full reset */ public void reset() { mActiveCalls.clear(); mTalkgroupsFirstHeard.clear(); mTalkgroups.clear(); mLCNTracker.reset(); resetState(); } /** * Performs a temporal reset following a call or other decode event */ private void resetState() { for( Integer key: mActiveCalls.keySet() ) { LTRCallEvent event = mActiveCalls.get( key ); if( event != null ) { event.end(); broadcast( event ); } } mActiveCalls.clear(); mTalkgroup = null; broadcast( ChangedAttribute.TO_TALKGROUP ); mTalkgroupAlias = null; broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS ); } public String getTalkgroup() { return mTalkgroup; } public Alias getTalkgroupAlias() { return mTalkgroupAlias; } public boolean hasChannelNumber() { return mLCNTracker.getCurrentChannel() != 0; } public int getChannelNumber() { return mLCNTracker.getCurrentChannel(); } @Override public void init() { } @Override public void receiveDecoderStateEvent( DecoderStateEvent event ) { switch( event.getEvent() ) { case RESET: resetState(); break; case SOURCE_FREQUENCY: mFrequency = event.getFrequency(); break; default: break; } } @Override public String getActivitySummary() { StringBuilder sb = new StringBuilder(); sb.append( "Activity Summary\n\n" ); sb.append( "Decoder:\tLTR-Standard\n" ); sb.append( "Monitored LCN: " ); if( hasChannelNumber() ) { sb.append( getChannelNumber() ); } else { sb.append( "*Insufficient Data*" ); } sb.append( "\n" ); sb.append( "Active LCNs:\t" ); List lcns = mLCNTracker.getActiveLCNs(); if( lcns.size() > 0 ) { sb.append( mLCNTracker.getActiveLCNs() ); } else { sb.append( "*Insufficient Data*" ); } sb.append( "\n\n" ); sb.append( "Talkgroups\n" ); if( mTalkgroups.isEmpty() ) { sb.append( " None\n" ); } else { for( String talkgroup: mTalkgroups ) { sb.append( " " ); sb.append( talkgroup ); sb.append( " " ); if( hasAliasList() ) { Alias alias = getAliasList().getTalkgroupAlias( talkgroup ); if( alias != null ) { sb.append( alias.getName() ); } } sb.append( "\n" ); } } return sb.toString(); } /** * Tracks the set of call and free channels for a system in order to * dynamically determine the current LCN and minimize false triggers from * bogus decoded LTR messages. Tracks the number of occurances of each * LCN and uses a dynamic threshold to determine LCN validity. */ public class LCNTracker { private static final int DEFAULT_COUNT = 10; private int[] mCallLCNCounts; private int[] mFreeLCNCounts; private int mCallHighestCount; private int mFreeHighestCount; private int mCurrentLCN; public LCNTracker() { reset(); } public void logStatistics() { for( int x = 1; x <= 20; x++ ) { mLog.debug( "Call " + x + ": " + mCallLCNCounts[ x ] ); } mLog.debug( "Call Highest Count: " + mCallHighestCount ); for( int x = 1; x <= 20; x++ ) { mLog.debug( "Free " + x + ": " + mFreeLCNCounts[ x ] ); } mLog.debug( "Free Highest Count: " + mFreeHighestCount ); mLog.debug( "Current LCN: " + mCurrentLCN ); } public void reset() { //Dim channel arrays to 21 -- ignore the 0 index mCallLCNCounts = new int[ 21 ]; mFreeLCNCounts = new int[ 21 ]; mCallHighestCount = 3; mFreeHighestCount = DEFAULT_COUNT; mCurrentLCN = 0; } public boolean isCurrentChannel( int channel ) { if( mCurrentLCN == 0 ) { return true; } else { return channel == mCurrentLCN; } } public int getCurrentChannel() { return mCurrentLCN; } /** * Indicates if the channel is valid based on the observed current and * free channels reported by the system. Tracks the count of each * reported free channel the highest free channel count is used as a * threshold where a channel is valid when its count exceeds 20% of the * highest free channel count. A channel is also valid when it is * the currently monitored channel. */ public boolean isValidChannel( int channel ) { if( isCurrentChannel( channel ) ) { return true; } int count = mFreeLCNCounts[ channel ]; int threshold = (int)( (double)mFreeHighestCount * 0.2 ); return count >= threshold; } public void processCallChannel( int channel ) { if( 1 <= channel && channel <= 20 ) { mCallLCNCounts[ channel ]++; if( mCallLCNCounts[ channel ] > mCallHighestCount ) { mCallHighestCount = mCallLCNCounts[ channel ]; mCurrentLCN = channel; broadcast( ChangedAttribute.CHANNEL_NUMBER ); } } } public void processFreeChannel( int channel ) { if( 1 <= channel && channel <= 20 ) { mFreeLCNCounts[ channel ]++; if( mFreeLCNCounts[ channel ] > mFreeHighestCount ) { mFreeHighestCount = mFreeLCNCounts[ channel ]; } } } public List getActiveLCNs() { List active = new ArrayList<>(); if( mFreeHighestCount > DEFAULT_COUNT ) { for( int x = 1; x <= 20; x++ ) { if( mCurrentLCN != 0 && isCurrentChannel( x ) ) { active.add( x ); } else if( isValidChannel( x ) ) { active.add( x ); } } } return active; } } }