/******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 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.ltrnet; import alias.Alias; import alias.AliasList; import alias.id.AliasIDType; import channel.metadata.AliasedIntegerAttributeMonitor; 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 util.StringUtils; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Iterator; import java.util.TreeSet; import java.util.concurrent.ScheduledExecutorService; public class LTRNetDecoderState extends DecoderState { private final static Logger mLog = LoggerFactory.getLogger(LTRNetDecoderState.class); private DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000"); private TreeSet<String> mTalkgroups = new TreeSet<String>(); private TreeSet<String> mTalkgroupsFirstHeard = new TreeSet<String>(); private TreeSet<String> mESNs = new TreeSet<String>(); private TreeSet<Integer> mUniqueIDs = new TreeSet<Integer>(); private TreeSet<String> mNeighborIDs = new TreeSet<String>(); private TreeSet<String> mSiteIDs = new TreeSet<String>(); private HashMap<Integer,Long> mReceiveFrequencies = new HashMap<Integer,Long>(); private HashMap<Integer,Long> mTransmitFrequencies = new HashMap<Integer,Long>(); private HashMap<Integer,String> mActiveCalls = new HashMap<Integer,String>(); private AliasedStringAttributeMonitor mToAttribute; private AliasedIntegerAttributeMonitor mFromUIDAttribute; private AliasedStringAttributeMonitor mESNAttribute; private String mMessage; private String mMessageType; private int mChannelNumber; private long mFrequency = 0; public LTRNetDecoderState(AliasList aliasList) { super(aliasList); mToAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, getAttributeChangeRequestListener(), getAliasList(), AliasIDType.TALKGROUP); mToAttribute.addIllegalValue("0-00-000"); mFromUIDAttribute = new AliasedIntegerAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO, getAttributeChangeRequestListener(), getAliasList(), AliasIDType.LTR_NET_UID); mESNAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_FROM, getAttributeChangeRequestListener(), getAliasList(), AliasIDType.ESN); } @Override public DecoderType getDecoderType() { return DecoderType.LTR_NET; } @Override public void receive(Message message) { if(message.isValid()) { State state = State.IDLE; if(message instanceof LTRNetOSWMessage) { LTRNetOSWMessage ltr = (LTRNetOSWMessage) message; switch(ltr.getMessageType()) { case CA_ENDD: if(mChannelNumber == 0) { setChannelNumber(ltr.getChannel()); } /* Process FCC Station ID Events */ if(ltr.getGroup() == 254) { if(mCurrentCallEvent == null || mCurrentCallEvent.getCallEventType() != CallEventType.STATION_ID) { mCurrentCallEvent = new LTRCallEvent.Builder( DecoderType.LTR_NET, CallEventType.STATION_ID) .aliasList(getAliasList()) .channel(String.valueOf(mChannelNumber)) .frequency(mFrequency) .to(ltr.getTalkgroupID()) .build(); broadcast(mCurrentCallEvent); broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); } else { broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA)); } } else { processCallEndMessage(ltr); } break; case CA_STRT: if(mChannelNumber == 0) { setChannelNumber(ltr.getChannel()); } /* If the call event channel matches our current channel * then it's a call, otherwise it's a call detect. */ if(ltr.getChannel() == mChannelNumber) { processCallMessage(ltr); } else { processCallDetectMessage(ltr); } break; case SY_IDLE: if(mChannelNumber != ltr.getChannel()) { setChannelNumber(ltr.getChannel()); } break; case MA_CHNH: break; case MA_CHNL: break; case FQ_RXHI: case FQ_RXLO: if(ltr.getFrequency() > 0) { mReceiveFrequencies.put(ltr.getHomeRepeater(), ltr.getFrequency()); } break; case FQ_TXHI: case FQ_TXLO: if(ltr.getFrequency() > 0) { mTransmitFrequencies.put(ltr.getHomeRepeater(), ltr.getFrequency()); } break; case ID_NBOR: String neighborID = ltr.getNeighborID(); if(neighborID != null) { mNeighborIDs.add(neighborID); } break; case ID_UNIQ: state = State.DATA; int uniqueID = ltr.getRadioUniqueID(); if(uniqueID != LTRNetOSWMessage.INT_NULL_VALUE) { mUniqueIDs.add(uniqueID); } if(getCurrentLTRCallEvent() == null) { mCurrentCallEvent = new LTRCallEvent.Builder( DecoderType.LTR_NET, CallEventType.REGISTER) .aliasList(getAliasList()) .channel(String.valueOf(mChannelNumber)) .frequency(mFrequency) .from(String.valueOf(uniqueID)) .build(); } else { mCurrentCallEvent.setFromID( String.valueOf(ltr.getRadioUniqueID())); mCurrentCallEvent.setDetails("Unique ID"); broadcast(mCurrentCallEvent); } mFromUIDAttribute.process(uniqueID); break; case ID_SITE: String siteID = ltr.getSiteID(); if(siteID != null) { mSiteIDs.add(siteID); } break; default: break; } } else if(message instanceof LTRNetISWMessage) { LTRNetISWMessage ltr = ((LTRNetISWMessage) message); switch(ltr.getMessageType()) { case CA_STRT: processCallMessage(ltr); break; case CA_ENDD: processCallEndMessage(ltr); break; case ID_ESNH: case ID_ESNL: state = State.DATA; String esn = ltr.getESN(); if(!esn.contains("xxxx")) { mESNs.add(ltr.getESN()); } setMessageType("ESN"); mESNAttribute.process(ltr.getESN()); broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA)); if(mCurrentCallEvent == null) { mCurrentCallEvent = new LTRCallEvent.Builder( DecoderType.LTR_NET, CallEventType.REGISTER_ESN) .aliasList(getAliasList()) .details("ESN:" + ltr.getESN()) .frequency(mFrequency) .from(ltr.getESN()) .build(); broadcast(mCurrentCallEvent); } break; case ID_UNIQ: state = State.DATA; int uniqueid = ltr.getRadioUniqueID(); if(uniqueid != LTRNetISWMessage.INT_NULL_VALUE) { mUniqueIDs.add(uniqueid); setMessageType("REGISTER UID"); mFromUIDAttribute.process(uniqueid); if(getCurrentLTRCallEvent() == null) { mCurrentCallEvent = new LTRCallEvent.Builder( DecoderType.LTR_NET, CallEventType.REGISTER) .aliasList(getAliasList()) .channel(String.valueOf(mChannelNumber)) .frequency(mFrequency) .from(String.valueOf(uniqueid)) .build(); } else { mCurrentCallEvent.setFromID( String.valueOf(ltr.getRadioUniqueID())); mCurrentCallEvent.setDetails("Unique ID"); broadcast(mCurrentCallEvent); } } break; default: break; } } } } public LTRCallEvent getCurrentLTRCallEvent() { if(mCurrentCallEvent != null) { return (LTRCallEvent) mCurrentCallEvent; } return null; } @Override public String getActivitySummary() { StringBuilder sb = new StringBuilder(); sb.append("Activity Summary\n"); sb.append("Decoder:\tLTR-Net\n\n"); if(mSiteIDs.isEmpty()) { sb.append("Site:\tUnknown\n"); } else { Iterator<String> it = mSiteIDs.iterator(); while(it.hasNext()) { sb.append("Site:\t"); String siteID = it.next(); sb.append(siteID); if(hasAliasList()) { Alias siteAlias = getAliasList().getSiteID(String.valueOf(siteID)); if(siteAlias != null) { sb.append(" "); sb.append(siteAlias.getName()); } } sb.append("\n"); } } sb.append("\nLCNs (transmit | receive)\n"); if(mReceiveFrequencies.isEmpty() && mTransmitFrequencies.isEmpty()) { sb.append(" None\n"); } else { for(int x = 1; x < 21; x++) { long rcv = 0; if(mReceiveFrequencies.containsKey(x)) { rcv = mReceiveFrequencies.get(x); } long xmt = 0; if(mTransmitFrequencies.containsKey(x)) { xmt = mTransmitFrequencies.get(x); } if(rcv > 0 || xmt > 0) { if(x < 10) { sb.append(" "); } sb.append(x); sb.append(": "); if(xmt == 0) { sb.append("---.-----"); } else { sb.append(mDecimalFormatter.format((double) xmt / 1E6d)); } sb.append(" | "); if(rcv == 0) { sb.append("---.-----"); } else { sb.append(mDecimalFormatter.format((double) rcv / 1E6d)); } if(x == mChannelNumber) { sb.append(" **"); } 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("\nRadio Unique IDs\n"); if(mUniqueIDs.isEmpty()) { sb.append(" None\n"); } else { Iterator<Integer> it = mUniqueIDs.iterator(); while(it.hasNext()) { int uid = it.next(); sb.append(" "); sb.append(uid); sb.append(" "); if(hasAliasList()) { Alias alias = getAliasList().getUniqueID(uid); if(alias != null) { sb.append(alias.getName()); } } sb.append("\n"); } } sb.append("\nESNs\n"); if(mESNs.isEmpty()) { sb.append(" None\n"); } else { Iterator<String> it = mESNs.iterator(); while(it.hasNext()) { String esn = it.next(); sb.append(" "); sb.append(esn); sb.append(" "); if(hasAliasList()) { Alias alias = getAliasList().getESNAlias(esn); if(alias != null) { sb.append(alias.getName()); } } sb.append("\n"); } } sb.append("\nNeighbor Sites\n"); if(mNeighborIDs.isEmpty()) { sb.append(" None\n"); } else { Iterator<String> it = mNeighborIDs.iterator(); while(it.hasNext()) { String neighbor = it.next(); sb.append(" "); sb.append(neighbor); sb.append(" "); if(hasAliasList()) { Alias alias = getAliasList().getSiteID(String.valueOf(neighbor)); if(alias != null) { sb.append(alias.getName()); } } sb.append("\n"); } } return sb.toString(); } /** * Call Detect - current channel messages indicate a call on another channel */ private void processCallDetectMessage(LTRNetMessage message) { if(!mActiveCalls.containsKey(message.getChannel()) || !mActiveCalls.get(message.getChannel()).contentEquals(message.getTalkgroupID())) { mActiveCalls.put(message.getChannel(), message.getTalkgroupID()); int channel = message.getChannel(); long frequency = 0; if(mTransmitFrequencies.containsKey(channel)) { frequency = mTransmitFrequencies.get(channel); } broadcast(new LTRCallEvent.Builder(DecoderType.LTR_NET, CallEventType.CALL_DETECT) .aliasList(getAliasList()) .channel(String.valueOf(message.getChannel())) .frequency(frequency) .to(message.getTalkgroupID()) .build()); } broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.IDLE)); } /** * 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); } private boolean isValidTalkgroup(String talkgroup) { return talkgroup != null && !talkgroup.matches("0-00-000"); } private void processCallMessage(LTRNetMessage message) { int group = message.getGroup(); /* Process call registration */ if(group == 253) { setMessageType("REGISTER"); broadcast(new DecoderStateEvent(this, Event.START, State.DATA)); } /* Process call */ else { String talkgroup = message.getTalkgroupID(); if(isValidTalkgroup(talkgroup)) { mToAttribute.process(talkgroup); final LTRCallEvent current = getCurrentLTRCallEvent(); //If this is a new call or the talkgroup is different from the current call, create a new call event if(current == null || isDifferentTalkgroup(talkgroup)) { /* Invalidate the current call */ if(current != null) { current.setValid(false); broadcast(current); } /* A talkgroup must be seen at least once before it will be added * to the mTalkgroups list that is used in the activity summary, * so that we don't pollute the summary with one-off error talkgroups */ if(mTalkgroupsFirstHeard.contains(talkgroup)) { mTalkgroups.add(talkgroup); } else { mTalkgroupsFirstHeard.add(talkgroup); } CallEvent callEvent = new LTRCallEvent.Builder(DecoderType.LTR_NET, CallEventType.CALL) .aliasList(getAliasList()) .channel(String.valueOf(message.getChannel())) .frequency(mFrequency) .to(message.getTalkgroupID()) .build(); broadcast(callEvent); mCurrentCallEvent = callEvent; broadcast(new DecoderStateEvent(this, Event.START, State.CALL)); } broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL)); } } } private void processCallEndMessage(LTRNetMessage message) { if(mCurrentCallEvent != null && mCurrentCallEvent.getCallEventType() == CallEventType.CALL) { mCurrentCallEvent.setEnd(System.currentTimeMillis()); broadcast(mCurrentCallEvent); } mCurrentCallEvent = null; broadcast(new DecoderStateEvent(this, Event.END, State.FADE)); } /** * Performs a full reset */ public void reset() { mChannelNumber = 0; mActiveCalls.clear(); mESNs.clear(); mNeighborIDs.clear(); mReceiveFrequencies.clear(); mSiteIDs.clear(); mTalkgroups.clear(); mTransmitFrequencies.clear(); mUniqueIDs.clear(); resetState(); } /** * Resets the decoder state after a call or other decode event */ private void resetState() { if(mCurrentCallEvent != null && mCurrentCallEvent .getCallEventType() == CallEventType.CALL) { mCurrentCallEvent.end(); broadcast(mCurrentCallEvent); } mCurrentCallEvent = null; mToAttribute.reset(); mESNAttribute.reset(); mFromUIDAttribute.reset(); mMessage = null; mMessageType = null; } public String getMessage() { return mMessage; } public void setMessage(String message) { if(!StringUtils.isEqual(mMessage, message)) { mMessage = message; broadcast(new AttributeChangeRequest<String>(Attribute.MESSAGE, mMessageType)); } } public String getMessageType() { return mMessageType; } private void setMessageType(String messageType) { if(!StringUtils.isEqual(mMessageType, messageType)) { mMessageType = messageType; broadcast(new AttributeChangeRequest<String>(Attribute.MESSAGE_TYPE, mMessageType)); } } public boolean hasChannelNumber() { return mChannelNumber != 0; } public int getChannelNumber() { return mChannelNumber; } public void setChannelNumber(int channelNumber) { if(mChannelNumber != channelNumber) { mChannelNumber = channelNumber; broadcast(new AttributeChangeRequest<String>(Attribute.CHANNEL_FREQUENCY_LABEL, "LCN:" + getChannelNumber())); } } @Override public void init() { /* No initialization steps required */ } @Override public void receiveDecoderStateEvent(DecoderStateEvent event) { switch(event.getEvent()) { case RESET: resetState(); break; case SOURCE_FREQUENCY: mFrequency = event.getFrequency(); break; default: break; } } @Override public void start(ScheduledExecutorService executor) { } @Override public void stop() { } }