/******************************************************************************* * sdrtrunk * Copyright (C) 2014-2017 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 <http://www.gnu.org/licenses/> * ******************************************************************************/ package module.decode.ltrstandard; import alias.Alias; import alias.AliasList; import alias.id.AliasIDType; import channel.metadata.AliasedStringAttributeMonitor; import channel.metadata.Attribute; import channel.metadata.AttributeChangeRequest; import channel.state.DecoderState; import channel.state.DecoderStateEvent; import channel.state.DecoderStateEvent.Event; import channel.state.State; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; import util.StringUtils; 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 java.util.concurrent.ScheduledExecutorService; public class LTRStandardDecoderState extends DecoderState { private final static Logger mLog = LoggerFactory.getLogger(LTRStandardDecoderState.class); private Map<Integer,LTRCallEvent> mActiveCalls = new HashMap<>(); private Set<String> mTalkgroupsFirstHeard = new HashSet<>(); private Set<String> mTalkgroups = new TreeSet<>(); private AliasedStringAttributeMonitor mTalkgroupAttribute; private long mFrequency; private LCNTracker mLCNTracker = new LCNTracker(); public LTRStandardDecoderState(AliasList aliasList) { super(aliasList); mTalkgroupAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); } @Override public DecoderType getDecoderType() { return DecoderType.LTR_STANDARD; } @Override public void start(ScheduledExecutorService executor) { // TODO Auto-generated method stub } @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(); setChannelNumber(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)) { String talkgroup = start.getToID(); mTalkgroupAttribute.process(talkgroup); processTalkgroup(talkgroup); } } 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(); setChannelNumber(repeater); if(mLCNTracker.isCurrentChannel(repeater)) { String talkgroup = end.getToID(); mTalkgroupAttribute.process(talkgroup); processTalkgroup(talkgroup); 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(); mTalkgroupAttribute.reset(); } public boolean hasChannelNumber() { return mLCNTracker.getCurrentChannel() != 0; } public int getChannelNumber() { return mLCNTracker.getCurrentChannel(); } private void setChannelNumber(int channel) { int original = mLCNTracker.getCurrentChannel(); mLCNTracker.processCallChannel(channel); if(mLCNTracker.getCurrentChannel() != original) { broadcast(new AttributeChangeRequest<String>(Attribute.CHANNEL_FREQUENCY_LABEL, "LCN:" + 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<Integer> 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; } } } public void processFreeChannel(int channel) { if(1 <= channel && channel <= 20) { mFreeLCNCounts[channel]++; if(mFreeLCNCounts[channel] > mFreeHighestCount) { mFreeHighestCount = mFreeLCNCounts[channel]; } } } public List<Integer> getActiveLCNs() { List<Integer> 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; } } }