/******************************************************************************* * 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.passport; 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; import module.decode.event.CallEvent.CallEventType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.TreeSet; import java.util.concurrent.ScheduledExecutorService; public class PassportDecoderState extends DecoderState { private final static Logger mLog = LoggerFactory.getLogger(PassportDecoderState.class); private HashSet<String> mTalkgroupsFirstHeard = new HashSet<String>(); private TreeSet<String> mTalkgroups = new TreeSet<String>(); private TreeSet<String> mMobileIDs = new TreeSet<String>(); private HashMap<Integer,String> mSiteLCNs = new HashMap<Integer,String>(); private HashMap<Integer,String> mNeighborLCNs = new HashMap<Integer,String>(); private AliasedStringAttributeMonitor mFromMobileIDAttribute; private AliasedStringAttributeMonitor mToTalkgroupAttribute; private int mChannelNumber; private int mSiteNumber; private PassportBand mSiteBand; private HashMap<Integer,String> mActiveCalls = new HashMap<Integer,String>(); private long mFrequency; public PassportDecoderState(AliasList aliasList) { super(aliasList); mFromMobileIDAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_FROM, getAttributeChangeRequestListener(), getAliasList(), AliasIDType.MIN); mToTalkgroupAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); } @Override public DecoderType getDecoderType() { return DecoderType.PASSPORT; } @Override public void start(ScheduledExecutorService executor) { } @Override public void init() { } @Override public void stop() { } private void logTalkgroup(String talkgroup) { if(mTalkgroupsFirstHeard.contains(talkgroup)) { mTalkgroups.add(talkgroup); } else { mTalkgroupsFirstHeard.add(talkgroup); } } private PassportCallEvent getCurrentCallEvent() { return (PassportCallEvent) mCurrentCallEvent; } /** * Indicates if the talkgroup is different than the talkgroup specified in * the current call event */ private boolean isDifferentTalkgroup(String talkgroup) { return talkgroup != null && mCurrentCallEvent != null && mCurrentCallEvent.getToID() != null && !mCurrentCallEvent.getToID().contentEquals(talkgroup); } @Override public void receive(Message message) { if(message instanceof PassportMessage) { PassportMessage passport = (PassportMessage) message; if(passport.isValid()) { switch(passport.getMessageType()) { case CA_STRT: mSiteLCNs.put(passport.getLCN(), passport.getLCNFrequencyFormatted()); String talkgroup = String.valueOf(passport.getTalkgroupID()); logTalkgroup(talkgroup); if(mChannelNumber == 0) { setChannelNumber(passport.getLCN()); } /* Call on this channel */ if(passport.getLCN() == mChannelNumber) { mToTalkgroupAttribute.process(talkgroup); PassportCallEvent current = getCurrentCallEvent(); /* If we're already in a call event, add the message * to the current call event ... if false, then we * have a different call ... cleanup the old one. */ if(current != null && isDifferentTalkgroup(talkgroup)) { mCurrentCallEvent.end(); mCurrentCallEvent = null; } if(mCurrentCallEvent == null) { mCurrentCallEvent = new PassportCallEvent .Builder(CallEventType.CALL) .aliasList(getAliasList()) .channel(String.valueOf(mChannelNumber)) .frequency(passport.getLCNFrequency()) .to(String.valueOf(passport.getTalkgroupID())) .build(); broadcast(mCurrentCallEvent); broadcast(new DecoderStateEvent(this, Event.START, State.CALL)); } else { broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); } } else { //Call Detection int lcn = passport.getLCN(); String tg = String.valueOf(passport.getTalkgroupID()); if(!mActiveCalls.containsKey(lcn) || !mActiveCalls.get(lcn).contentEquals(tg)) { mActiveCalls.put(lcn, tg); broadcast(new PassportCallEvent .Builder(CallEventType.CALL_DETECT) .aliasList(getAliasList()) .channel(String.valueOf(lcn)) .details("Site: " + passport.getSite()) .frequency(passport.getLCNFrequency()) .to(tg) .build()); } } break; case CA_ENDD: String endTalkgroup = String.valueOf(passport.getTalkgroupID()); logTalkgroup(endTalkgroup); mToTalkgroupAttribute.process(endTalkgroup); broadcast(new DecoderStateEvent(this, Event.END, State.CALL)); if(mCurrentCallEvent != null) { mCurrentCallEvent.end(); broadcast(mCurrentCallEvent); mCurrentCallEvent = null; broadcast(new DecoderStateEvent(this, Event.RESET, State.FADE)); } break; case DA_STRT: mSiteLCNs.put(passport.getLCN(), passport.getLCNFrequencyFormatted()); String dataTalkgroup = String.valueOf(passport.getTalkgroupID()); logTalkgroup(dataTalkgroup); if(mChannelNumber == 0) { setChannelNumber(passport.getLCN()); } /* Data call on this channel */ if(passport.getLCN() == mChannelNumber) { mToTalkgroupAttribute.process(dataTalkgroup); PassportCallEvent current = getCurrentCallEvent(); /* If we're already in a call event, add the message * to the current call event ... if false, then we * have a different call ... cleanup the old one. */ if(current != null && isDifferentTalkgroup(dataTalkgroup)) { mCurrentCallEvent.end(); mCurrentCallEvent = null; } if(mCurrentCallEvent == null) { mCurrentCallEvent = new PassportCallEvent .Builder(CallEventType.DATA_CALL) .aliasList(getAliasList()) .channel(String.valueOf(mChannelNumber)) .frequency(passport.getLCNFrequency()) .to(String.valueOf(passport.getTalkgroupID())) .build(); broadcast(mCurrentCallEvent); broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); } else { broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); } } else { //Call Detection int lcn = passport.getLCN(); String tg = String.valueOf(passport.getTalkgroupID()); if(!mActiveCalls.containsKey(lcn) || !mActiveCalls.get(lcn).contentEquals(tg)) { mActiveCalls.put(lcn, tg); broadcast(new PassportCallEvent .Builder(CallEventType.DATA_CALL) .aliasList(getAliasList()) .channel(String.valueOf(lcn)) .details("Site: " + passport.getSite()) .frequency(passport.getLCNFrequency()) .to(tg) .build()); } } break; case DA_ENDD: String dataEndTalkgroup = String.valueOf(passport.getTalkgroupID()); logTalkgroup(dataEndTalkgroup); mToTalkgroupAttribute.process(dataEndTalkgroup); broadcast(new DecoderStateEvent(this, Event.END, State.DATA)); if(mCurrentCallEvent != null) { mCurrentCallEvent.end(); broadcast(mCurrentCallEvent); mCurrentCallEvent = null; broadcast(new DecoderStateEvent(this, Event.RESET, State.FADE)); } break; case ID_RDIO: String min = passport.getMobileID(); if(min != null) { mMobileIDs.add(min); } mFromMobileIDAttribute.process(min); final CallEvent current = getCurrentCallEvent(); if(current != null) { current.setFromID(min); broadcast(current); } break; case RA_REGI: if(mCurrentCallEvent == null || mCurrentCallEvent.getCallEventType() != CallEventType.REGISTER) { mCurrentCallEvent = new PassportCallEvent .Builder(CallEventType.REGISTER) .aliasList(getAliasList()) .channel(String.valueOf(passport.getLCN())) .frequency(passport.getLCNFrequency()) .to(passport.getToID()) .build(); broadcast(mCurrentCallEvent); broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); } else { broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); } break; case SY_IDLE: if(passport.getFree() != 0) { mNeighborLCNs.put(passport.getFree(), passport.getFreeFrequencyFormatted()); } setSiteNumber(passport.getSite()); PassportBand band = passport.getSiteBand(); setSiteBand(band); setChannelNumber(getSiteBand().getChannel(mFrequency)); break; default: break; } } } } public static String formatTalkgroup(String talkgroup) { StringBuilder sb = new StringBuilder(); if(talkgroup.length() == 6) { sb.append(talkgroup.substring(0, 1)); sb.append("-"); sb.append(talkgroup.substring(1, 3)); sb.append("-"); sb.append(talkgroup.substring(3, 6)); return sb.toString(); } else { return talkgroup; } } @Override public String getActivitySummary() { StringBuilder sb = new StringBuilder(); sb.append("Activity Summary\n"); sb.append("Decoder:\tPassport\n\n"); sb.append("Site Channels\n"); if(mSiteLCNs.isEmpty()) { sb.append(" None\n"); } else { ArrayList<Integer> channels = new ArrayList<>(mSiteLCNs.keySet()); Collections.sort(channels); for(Integer channel : channels) { sb.append(" " + channel); sb.append("\t" + mSiteLCNs.get(channel)); sb.append("\n"); } } sb.append("\nNeighbor Channels\n"); if(mNeighborLCNs.isEmpty()) { sb.append(" None\n"); } else { ArrayList<Integer> channels = new ArrayList<>(mNeighborLCNs.keySet()); Collections.sort(channels); for(Integer channel : channels) { sb.append(" " + channel); sb.append("\t" + mNeighborLCNs.get(channel)); sb.append("\n"); } } sb.append("\nTalkgroups\n"); if(mTalkgroups.isEmpty()) { sb.append(" None\n"); } else { Iterator<String> it = mTalkgroups.iterator(); while(it.hasNext()) { String tgid = it.next(); sb.append(" "); sb.append(tgid); sb.append(" "); if(hasAliasList()) { Alias alias = getAliasList().getTalkgroupAlias(tgid); if(alias != null) { sb.append(alias.getName()); } } sb.append("\n"); } } sb.append("\nMobile ID Numbers\n"); if(mMobileIDs.isEmpty()) { sb.append(" None\n"); } else { Iterator<String> it = mMobileIDs.iterator(); while(it.hasNext()) { String min = it.next(); sb.append(" "); sb.append(min); sb.append(" "); if(hasAliasList()) { Alias alias = getAliasList().getMobileIDNumberAlias(min); if(alias != null) { sb.append(alias.getName()); } } sb.append("\n"); } } return sb.toString(); } public void reset() { mTalkgroupsFirstHeard.clear(); mTalkgroups.clear(); mMobileIDs.clear(); mSiteLCNs.clear(); mNeighborLCNs.clear(); resetState(); } private void resetState() { mToTalkgroupAttribute.reset(); mFromMobileIDAttribute.reset(); } public int getChannelNumber() { return mChannelNumber; } private void setChannelNumber(int channel) { if(mChannelNumber != channel) { mChannelNumber = channel; broadcast(new AttributeChangeRequest<String>(Attribute.CHANNEL_FREQUENCY_LABEL, "CHAN:" + mChannelNumber)); } } public int getSiteNumber() { return mSiteNumber; } private void setSiteNumber(int site) { if(mSiteNumber != site) { mSiteNumber = site; Alias alias = hasAliasList() ? getAliasList().getSiteID(String.valueOf(getSiteNumber())) : null; broadcast(new AttributeChangeRequest<String>(Attribute.NETWORK_ID_1, "SITE:" + mSiteNumber, alias)); } } private PassportBand getSiteBand() { return mSiteBand; } private void setSiteBand(PassportBand band) { if(mSiteBand == null) { mSiteBand = band; broadcast(new AttributeChangeRequest<String>(Attribute.NETWORK_ID_2, "BAND:" + band.getDescription())); } } @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { switch(event.getEvent()) { case RESET: resetState(); break; case SOURCE_FREQUENCY: mFrequency = event.getFrequency(); broadcast(new AttributeChangeRequest<Long>(Attribute.CHANNEL_FREQUENCY, mFrequency)); break; default: break; } } }