/******************************************************************************* * 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 channel.traffic; import alias.Alias; import alias.id.priority.Priority; import channel.state.DecoderStateEvent; import channel.state.IDecoderStateEventListener; import controller.channel.Channel; import controller.channel.Channel.ChannelType; import controller.channel.ChannelEvent; import controller.channel.ChannelEvent.Event; import controller.channel.ChannelModel; import controller.channel.ChannelProcessingManager; import controller.channel.TrafficChannelEvent; import module.Module; import module.decode.config.DecodeConfiguration; import module.decode.event.CallEvent; import module.decode.event.CallEvent.CallEventType; import module.decode.event.ICallEventProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import record.config.RecordConfiguration; import sample.Listener; import source.config.SourceConfigTuner; import source.tuner.TunerChannel; import source.tuner.TunerChannel.Type; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener { private final static Logger mLog = LoggerFactory.getLogger(TrafficChannelManager.class); public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED"; public static final String NO_TUNER_AVAILABLE = "NO TUNER AVAILABLE"; public static final String UNKNOWN_FREQUENCY = "UNKNOWN FREQUENCY"; private int mTrafficChannelPoolMaximumSize = DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT; private List<Channel> mTrafficChannelPool = new ArrayList<Channel>(); private Map<String,Channel> mTrafficChannelsInUse = new ConcurrentHashMap<String,Channel>(); private DecoderStateEventListener mEventListener = new DecoderStateEventListener(); private Listener<CallEvent> mCallEventListener; private ChannelModel mChannelModel; private DecodeConfiguration mDecodeConfiguration; private RecordConfiguration mRecordConfiguration; private String mSystem; private String mSite; private String mAliasListName; /** * Monitors call events and allocates traffic decoder channels in response * to traffic channel allocation call events. Manages a pool of reusable * traffic channel allocations. * * @param channelModel containing channels currently in use * @param decodeConfiguration - decoder configuration to use for each * traffic channel allocation. * @param recordConfiguration - recording options for each traffic channel * @param system label to use to describe the system * @param site label to use to describe the site * @param aliasListName designated for the channel * @param trafficChannelPoolSize - maximum number of allocated traffic channels * in the pool */ public TrafficChannelManager(ChannelModel channelModel, DecodeConfiguration decodeConfiguration, RecordConfiguration recordConfiguration, String system, String site, String aliasListName, int trafficChannelPoolSize) { mChannelModel = channelModel; mDecodeConfiguration = decodeConfiguration; mRecordConfiguration = recordConfiguration; mSystem = system; mSite = site; mAliasListName = aliasListName; mTrafficChannelPoolMaximumSize = trafficChannelPoolSize; } @Override public void dispose() { for(Channel trafficChannel : mTrafficChannelPool) { mChannelModel.broadcast(new ChannelEvent(trafficChannel, Event.REQUEST_DISABLE)); } mTrafficChannelPool.clear(); mTrafficChannelsInUse.clear(); mCallEventListener = null; mDecodeConfiguration = null; } /** * Provides a channel by either reusing an existing channel or constructing * a new one, limited by the total number of constructed channels allowed. * * Note: you must enforce thread safety on the mTrafficChannelsInUse * external to this method. * * @param channelNumber * @param tunerChannel * @return */ private Channel getChannel(String channelNumber, TunerChannel tunerChannel) { Channel channel = null; if(!mTrafficChannelsInUse.containsKey(channelNumber)) { for(Channel configuredChannel : mTrafficChannelPool) { if(!configuredChannel.getEnabled()) { channel = configuredChannel; break; } } if(channel == null && mTrafficChannelPool.size() < mTrafficChannelPoolMaximumSize) { channel = new Channel("Traffic", ChannelType.TRAFFIC); channel.setDecodeConfiguration(mDecodeConfiguration); channel.setRecordConfiguration(mRecordConfiguration); channel.setAliasListName(mAliasListName); mChannelModel.addChannel(channel); mTrafficChannelPool.add(channel); } /* If we have a configured channel, update metadata */ if(channel != null) { channel.setSourceConfiguration(new SourceConfigTuner(tunerChannel)); channel.setSystem(mSystem); channel.setSite(mSite); channel.setName(channelNumber); mChannelModel.broadcast(new ChannelEvent(channel, Event.NOTIFICATION_CONFIGURATION_CHANGE)); } } return channel; } /** * Processes the event and creates a traffic channel is resources are * available */ private void process(TrafficChannelAllocationEvent event) { CallEvent callEvent = event.getCallEvent(); /* Check for duplicate events and suppress */ synchronized(mTrafficChannelsInUse) { if(mTrafficChannelsInUse.containsKey(callEvent.getChannel())) { return; } long frequency = callEvent.getFrequency(); if(frequency > 0) { Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC, frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth())); if(channel != null) { TrafficChannelEvent trafficChannelEvent = new TrafficChannelEvent(this, channel, Event.REQUEST_ENABLE, callEvent); //Request to enable the channel mChannelModel.broadcast(trafficChannelEvent); if(channel.getEnabled()) { mTrafficChannelsInUse.put(callEvent.getChannel(), channel); } else { callEvent.setCallEventType(CallEventType.CALL_DETECT); String details = callEvent.getDetails(); if(details == null || details.isEmpty()) { callEvent.setDetails(CHANNEL_START_REJECTED); } else if(!details.contains(CHANNEL_START_REJECTED)) { callEvent.setDetails(new StringBuilder(CHANNEL_START_REJECTED).append(" : ") .append(callEvent.getDetails()).toString()); } } } else { callEvent.setCallEventType(CallEventType.CALL_DETECT); String details = callEvent.getDetails(); if(details == null || details.isEmpty()) { callEvent.setDetails(NO_TUNER_AVAILABLE); } else if(!details.contains(NO_TUNER_AVAILABLE)) { callEvent.setDetails(new StringBuilder(NO_TUNER_AVAILABLE).append(" : ") .append(callEvent.getDetails()).toString()); } } } else { callEvent.setCallEventType(CallEventType.CALL_DETECT); String details = callEvent.getDetails(); if(details == null || details.isEmpty()) { callEvent.setDetails(UNKNOWN_FREQUENCY); } else if(!details.contains(UNKNOWN_FREQUENCY)) { callEvent.setDetails(new StringBuilder(UNKNOWN_FREQUENCY).append(" : ") .append(callEvent.getDetails()).toString()); } } final Listener<CallEvent> listener = mCallEventListener; if(listener != null) { listener.receive(callEvent); } } } /** * Compares the call type, channel and to fields for equivalence and the * from field for either both null, or equivalence. * * @param e1 * @param e2 * @return */ public static boolean isSameCallEvent(CallEvent e1, CallEvent e2) { if(e1 == null || e2 == null) { return false; } if(e1.getCallEventType() != e2.getCallEventType()) { return false; } if(e1.getChannel() == null || e2.getChannel() == null || !e1.getChannel().contentEquals(e2.getChannel())) { return false; } if(e1.getToID() == null || e2.getToID() == null || !e1.getToID().contentEquals(e2.getToID())) { return false; } if(e1.getFromID() == null || e2.getFromID() == null) { return (e1.getFromID() == null && e2.getFromID() == null); } else if(e1.getFromID().contentEquals(e2.getFromID())) { return true; } return false; } @Override public Listener<DecoderStateEvent> getDecoderStateListener() { return mEventListener; } @Override public void addCallEventListener(Listener<CallEvent> listener) { mCallEventListener = listener; } @Override public void removeCallEventListener(Listener<CallEvent> listener) { mCallEventListener = null; } @Override public void reset() { } @Override public void start(ScheduledExecutorService executor) { } @Override public void stop() { if(!mTrafficChannelsInUse.isEmpty()) { List<String> channels = new ArrayList<>(); /* Copy the keyset so we don't get concurrent modification of the map */ channels.addAll(mTrafficChannelsInUse.keySet()); for(String channel : channels) { callEnd(channel); } } } /** * Callback used by the TrafficChannelStatusListener class to signal the * end of a an allocated traffic channel call event * * @param channelNumber - channel number from the call event that signaled * the start of a traffic channel allocation */ public void callEnd(String channelNumber) { synchronized(mTrafficChannelsInUse) { if(channelNumber != null && mTrafficChannelsInUse.containsKey(channelNumber)) { Channel channel = mTrafficChannelsInUse.get(channelNumber); mChannelModel.broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE)); mTrafficChannelsInUse.remove(channelNumber); } } } /** * Wrapper class for the decoder state event listener interface to catch * traffic channel allocation requests */ public class DecoderStateEventListener implements Listener<DecoderStateEvent> { @Override public void receive(DecoderStateEvent event) { switch(event.getEvent()) { case TRAFFIC_CHANNEL_ALLOCATION: process((TrafficChannelAllocationEvent) event); break; default: break; } } } }