mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
Updates pull request from Roman Tsypuk to convert the build system from
ant to gradle. Removes linux and windows application launch scripts and produces an executable jar with all resources included.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class AuxDecodeConfigurationEditor extends Editor<Channel>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private List<AuxDecoderCheckBox> mControls = new ArrayList<>();
|
||||
|
||||
public AuxDecodeConfigurationEditor()
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
public AuxDecodeConfiguration getConfiguration()
|
||||
{
|
||||
return hasItem() ? getItem().getAuxDecodeConfiguration() : null;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "fill,wrap 4", "", "[][grow]" ) );
|
||||
|
||||
List<DecoderType> decoders = DecoderType.getAuxDecoders();
|
||||
|
||||
Collections.sort( decoders );
|
||||
|
||||
for( DecoderType decoder: decoders )
|
||||
{
|
||||
AuxDecoderCheckBox control = new AuxDecoderCheckBox( decoder );
|
||||
control.setEnabled( false );
|
||||
control.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
add( control );
|
||||
mControls.add( control );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasItem() )
|
||||
{
|
||||
AuxDecodeConfiguration config = getItem().getAuxDecodeConfiguration();
|
||||
|
||||
config.clearAuxDecoders();
|
||||
|
||||
for( AuxDecoderCheckBox checkBox: mControls )
|
||||
{
|
||||
if( checkBox.isSelected() )
|
||||
{
|
||||
config.addAuxDecoder( checkBox.getDecoderType() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
|
||||
private void setControlsEnabled( boolean enabled )
|
||||
{
|
||||
for( AuxDecoderCheckBox box: mControls )
|
||||
{
|
||||
if( box.isEnabled() != enabled )
|
||||
{
|
||||
box.setEnabled( enabled );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AuxDecoderCheckBox extends JCheckBox
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private DecoderType mDecoderType;
|
||||
|
||||
public AuxDecoderCheckBox( DecoderType decoder )
|
||||
{
|
||||
super( decoder.getDisplayString() );
|
||||
|
||||
mDecoderType = decoder;
|
||||
}
|
||||
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return mDecoderType;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem( Channel channel )
|
||||
{
|
||||
super.setItem( channel );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
setControlsEnabled( true );
|
||||
|
||||
for( AuxDecoderCheckBox cb: mControls )
|
||||
{
|
||||
if( getItem().getAuxDecodeConfiguration().getAuxDecoders().contains( cb.getDecoderType() ) )
|
||||
{
|
||||
cb.setSelected( true );
|
||||
}
|
||||
else
|
||||
{
|
||||
cb.setSelected( false );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setControlsEnabled( false );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EditorValidationException;
|
||||
import io.github.dsheirer.gui.editor.EmptyValidatingEditor;
|
||||
import io.github.dsheirer.gui.editor.ValidatingEditor;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class DecodeConfigurationEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JComboBox<DecoderType> mComboDecoders;
|
||||
private ValidatingEditor<Channel> mCurrentEditor = new EmptyValidatingEditor<>( "a decoder" );
|
||||
private ChannelMapModel mChannelMapModel;
|
||||
|
||||
public DecodeConfigurationEditor( final ChannelMapModel channelMapModel )
|
||||
{
|
||||
mChannelMapModel = channelMapModel;
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "wrap 2", "[][grow,fill]", "[align top][grow,fill]" ) );
|
||||
|
||||
mComboDecoders = new JComboBox<DecoderType>();
|
||||
mComboDecoders.setEnabled( false );
|
||||
|
||||
DefaultComboBoxModel<DecoderType> model = new DefaultComboBoxModel<DecoderType>();
|
||||
|
||||
for( DecoderType type: DecoderType.getPrimaryDecoders() )
|
||||
{
|
||||
model.addElement( type );
|
||||
}
|
||||
|
||||
mComboDecoders.setModel( model );
|
||||
mComboDecoders.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
DecoderType selected = mComboDecoders.getItemAt( mComboDecoders.getSelectedIndex() );
|
||||
ValidatingEditor<Channel> editor = DecoderFactory.getEditor( selected, mChannelMapModel );
|
||||
setEditor( editor );
|
||||
}
|
||||
});
|
||||
|
||||
add( mComboDecoders );
|
||||
add( mCurrentEditor );
|
||||
}
|
||||
|
||||
private void setEditor( ValidatingEditor<Channel> editor )
|
||||
{
|
||||
if( mCurrentEditor != editor )
|
||||
{
|
||||
//Set channel to null in current editor to force a save prompt as required
|
||||
if( mCurrentEditor.isModified() )
|
||||
{
|
||||
mCurrentEditor.setItem( null );
|
||||
}
|
||||
|
||||
remove( mCurrentEditor );
|
||||
|
||||
mCurrentEditor = editor;
|
||||
mCurrentEditor.setSaveRequestListener( this );
|
||||
mCurrentEditor.setItem( getItem() );
|
||||
|
||||
add( mCurrentEditor );
|
||||
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasItem() )
|
||||
{
|
||||
mCurrentEditor.save();
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem( Channel channel )
|
||||
{
|
||||
super.setItem( channel );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
mComboDecoders.setEnabled( true );
|
||||
mComboDecoders.setSelectedItem( channel.getDecodeConfiguration().getDecoderType() );
|
||||
mCurrentEditor.setItem( channel );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComboDecoders.setEnabled( false );
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate( Editor<Channel> editor ) throws EditorValidationException
|
||||
{
|
||||
mCurrentEditor.validate( editor );
|
||||
}
|
||||
}
|
||||
85
src/main/java/io/github/dsheirer/module/decode/Decoder.java
Normal file
85
src/main/java/io/github/dsheirer/module/decode/Decoder.java
Normal file
@@ -0,0 +1,85 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode;
|
||||
|
||||
import io.github.dsheirer.message.IMessageProvider;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public abstract class Decoder extends Module
|
||||
implements IMessageProvider, Listener<Message>
|
||||
{
|
||||
/* This has to be a broadcaster in order for references to persist */
|
||||
protected Listener<Message> mMessageListener;
|
||||
|
||||
/**
|
||||
* Decoder - parent class for all decoders, demodulators and components.
|
||||
*/
|
||||
public Decoder()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the decoder type (ie protocol)
|
||||
*/
|
||||
public abstract DecoderType getDecoderType();
|
||||
|
||||
|
||||
/**
|
||||
* Adds a listener for receiving decoded messages from this decoder
|
||||
*/
|
||||
@Override
|
||||
public void setMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving decoded messages from all attached
|
||||
* decoders
|
||||
*/
|
||||
@Override
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the message to the registered message listener
|
||||
*/
|
||||
public void broadcast( Message message )
|
||||
{
|
||||
if( mMessageListener != null )
|
||||
{
|
||||
mMessageListener.receive( message );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( Message message )
|
||||
{
|
||||
broadcast( message );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.AudioModule;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState;
|
||||
import io.github.dsheirer.channel.traffic.TrafficChannelManager;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.ChannelModel;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMap;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.filter.AllPassFilter;
|
||||
import io.github.dsheirer.filter.FilterSet;
|
||||
import io.github.dsheirer.filter.IFilter;
|
||||
import io.github.dsheirer.gui.editor.EmptyValidatingEditor;
|
||||
import io.github.dsheirer.gui.editor.ValidatingEditor;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.module.decode.am.AMDecoder;
|
||||
import io.github.dsheirer.module.decode.am.AMDecoderEditor;
|
||||
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2Decoder;
|
||||
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderState;
|
||||
import io.github.dsheirer.module.decode.fleetsync2.FleetsyncMessageFilter;
|
||||
import io.github.dsheirer.module.decode.lj1200.LJ1200Decoder;
|
||||
import io.github.dsheirer.module.decode.lj1200.LJ1200DecoderState;
|
||||
import io.github.dsheirer.module.decode.lj1200.LJ1200MessageFilter;
|
||||
import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
|
||||
import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoder;
|
||||
import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderEditor;
|
||||
import io.github.dsheirer.module.decode.ltrnet.LTRNetDecoderState;
|
||||
import io.github.dsheirer.module.decode.ltrnet.LTRNetMessageFilter;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.LTRStandardDecoder;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.LTRStandardDecoderEditor;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.LTRStandardDecoderState;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.LTRStandardMessageFilter;
|
||||
import io.github.dsheirer.module.decode.mdc1200.MDCDecoder;
|
||||
import io.github.dsheirer.module.decode.mdc1200.MDCDecoderState;
|
||||
import io.github.dsheirer.module.decode.mdc1200.MDCMessageFilter;
|
||||
import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderEditor;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327DecoderState;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327MessageFilter;
|
||||
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
||||
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
|
||||
import io.github.dsheirer.module.decode.nbfm.NBFMDecoderEditor;
|
||||
import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1;
|
||||
import io.github.dsheirer.module.decode.p25.P25Decoder;
|
||||
import io.github.dsheirer.module.decode.p25.P25DecoderEditor;
|
||||
import io.github.dsheirer.module.decode.p25.P25DecoderState;
|
||||
import io.github.dsheirer.module.decode.p25.P25_C4FMDecoder;
|
||||
import io.github.dsheirer.module.decode.p25.P25_LSMDecoder;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25AudioModule;
|
||||
import io.github.dsheirer.module.decode.p25.message.filter.P25MessageFilterSet;
|
||||
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
|
||||
import io.github.dsheirer.module.decode.passport.PassportDecoder;
|
||||
import io.github.dsheirer.module.decode.passport.PassportDecoderEditor;
|
||||
import io.github.dsheirer.module.decode.passport.PassportDecoderState;
|
||||
import io.github.dsheirer.module.decode.passport.PassportMessageFilter;
|
||||
import io.github.dsheirer.module.decode.tait.Tait1200Decoder;
|
||||
import io.github.dsheirer.module.decode.tait.Tait1200DecoderState;
|
||||
import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule;
|
||||
import io.github.dsheirer.module.demodulate.audio.DemodulatedAudioFilterModule;
|
||||
import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DecoderFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DecoderFactory.class);
|
||||
|
||||
//Low-pass filter with ~60 dB attenuation and 89 taps
|
||||
private static final FIRFilterSpecification MPT1327_FILTER_SPECIFICATION = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate( 48000 ).gridDensity( 16 ).passBandCutoff( 3200 ).passBandAmplitude( 1.0 ).passBandRipple( 0.02 )
|
||||
.stopBandStart( 4000 ).stopBandAmplitude( 0.0 ).stopBandRipple( 0.03 ).build();
|
||||
|
||||
private static final FIRFilterSpecification P25_C4FM_IQ_SPECIFICATION = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(48000).gridDensity(16).passBandCutoff(6750).passBandAmplitude(1.0).passBandRipple(0.01)
|
||||
.stopBandStart(7000).stopBandAmplitude(0.0).stopBandRipple(0.008).build();
|
||||
|
||||
private static final FIRFilterSpecification P25_C4FM_DEMOD_SPECIFICATION = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(48000).gridDensity(16).passBandCutoff(2500).passBandAmplitude(1.0).passBandRipple(0.01)
|
||||
.stopBandStart(4000).stopBandAmplitude(0.0).stopBandRipple(0.008).build();
|
||||
|
||||
private static float[] MPT1327_LOWPASS_FILTER;
|
||||
private static float[] P25_C4FM_IQ_FILTER;
|
||||
private static float[] P25_C4FM_DEMOD_FILTER;
|
||||
|
||||
static
|
||||
{
|
||||
try
|
||||
{
|
||||
MPT1327_LOWPASS_FILTER = FilterFactory.getTaps(MPT1327_FILTER_SPECIFICATION);
|
||||
P25_C4FM_IQ_FILTER = FilterFactory.getTaps(P25_C4FM_IQ_SPECIFICATION);
|
||||
P25_C4FM_DEMOD_FILTER = FilterFactory.getTaps(P25_C4FM_DEMOD_SPECIFICATION);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't design startup filter(s)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of one primary decoder and any auxiliary decoders, as
|
||||
* specified in the configurations.
|
||||
*
|
||||
* @return list of configured decoders
|
||||
*/
|
||||
public static List<Module> getModules(ChannelModel channelModel,
|
||||
ChannelMapModel channelMapModel,
|
||||
ChannelProcessingManager channelProcessingManager,
|
||||
AliasModel aliasModel,
|
||||
Channel channel,
|
||||
Metadata metadata)
|
||||
{
|
||||
|
||||
/* Get the optional alias list for the decode modules to use */
|
||||
AliasList aliasList = aliasModel.getAliasList(channel.getAliasListName());
|
||||
|
||||
List<Module> modules = getPrimaryModules(channelModel, channelMapModel, channelProcessingManager, aliasList,
|
||||
channel, metadata);
|
||||
|
||||
modules.addAll(getAuxiliaryDecoders(channel.getAuxDecodeConfiguration(), aliasList));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a primary decoder as specified in the decode configuration
|
||||
*/
|
||||
public static List<Module> getPrimaryModules(ChannelModel channelModel,
|
||||
ChannelMapModel channelMapModel,
|
||||
ChannelProcessingManager channelProcessingManager,
|
||||
AliasList aliasList,
|
||||
Channel channel,
|
||||
Metadata metadata)
|
||||
{
|
||||
List<Module> modules = new ArrayList<Module>();
|
||||
|
||||
ChannelType channelType = channel.getChannelType();
|
||||
|
||||
/* Baseband low-pass filter pass and stop frequencies */
|
||||
DecodeConfiguration decodeConfig = channel.getDecodeConfiguration();
|
||||
|
||||
int iqPass = decodeConfig.getDecoderType().getChannelBandwidth() / 2;
|
||||
int iqStop = iqPass + 1250;
|
||||
|
||||
switch(decodeConfig.getDecoderType())
|
||||
{
|
||||
case AM:
|
||||
modules.add(new AMDecoder(decodeConfig));
|
||||
modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName()));
|
||||
modules.add(new AMDemodulatorModule());
|
||||
modules.add(new DemodulatedAudioFilterModule(4000, 6000));
|
||||
modules.add(new AudioModule(metadata));
|
||||
break;
|
||||
case NBFM:
|
||||
modules.add(new NBFMDecoder(decodeConfig));
|
||||
modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.NBFM, channel.getName()));
|
||||
modules.add(new FMDemodulatorModule(iqPass, iqStop));
|
||||
modules.add(new DemodulatedAudioFilterModule(4000, 6000));
|
||||
modules.add(new AudioModule(metadata));
|
||||
break;
|
||||
case LTR_STANDARD:
|
||||
MessageDirection direction = ((DecodeConfigLTRStandard) decodeConfig).getMessageDirection();
|
||||
modules.add(new LTRStandardDecoder(aliasList, direction));
|
||||
modules.add(new LTRStandardDecoderState(aliasList));
|
||||
modules.add(new FMDemodulatorModule(iqPass, iqStop));
|
||||
modules.add(new DemodulatedAudioFilterModule(4000, 6000));
|
||||
modules.add(new AudioModule(metadata));
|
||||
break;
|
||||
case LTR_NET:
|
||||
modules.add(new LTRNetDecoder((DecodeConfigLTRNet) decodeConfig, aliasList));
|
||||
modules.add(new LTRNetDecoderState(aliasList));
|
||||
modules.add(new FMDemodulatorModule(iqPass, iqStop));
|
||||
modules.add(new DemodulatedAudioFilterModule(4000, 6000));
|
||||
modules.add(new AudioModule(metadata));
|
||||
break;
|
||||
case MPT1327:
|
||||
DecodeConfigMPT1327 mptConfig = (DecodeConfigMPT1327) decodeConfig;
|
||||
|
||||
ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName());
|
||||
|
||||
MPT1327Decoder.Sync sync = mptConfig.getSync();
|
||||
|
||||
modules.add(new MPT1327Decoder(aliasList, sync));
|
||||
|
||||
|
||||
modules.add(new MPT1327DecoderState(aliasList, channelMap, channelType, mptConfig.getCallTimeout() * 1000));
|
||||
|
||||
if(channelType == ChannelType.STANDARD)
|
||||
{
|
||||
modules.add(new TrafficChannelManager(channelModel, decodeConfig, channel.getEventLogConfiguration(),
|
||||
channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(),
|
||||
(aliasList != null ? aliasList.getName() : null), mptConfig.getTrafficChannelPoolSize()));
|
||||
}
|
||||
|
||||
modules.add(new FMDemodulatorModule(iqPass, iqStop));
|
||||
modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f));
|
||||
modules.add(new AudioModule(metadata));
|
||||
break;
|
||||
case PASSPORT:
|
||||
modules.add(new PassportDecoder(decodeConfig, aliasList));
|
||||
modules.add(new PassportDecoderState(aliasList));
|
||||
modules.add(new FMDemodulatorModule(iqPass, iqStop));
|
||||
modules.add(new DemodulatedAudioFilterModule(4000, 6000));
|
||||
modules.add(new AudioModule(metadata));
|
||||
break;
|
||||
case P25_PHASE1:
|
||||
DecodeConfigP25Phase1 p25Config = (DecodeConfigP25Phase1) decodeConfig;
|
||||
|
||||
P25Decoder.Modulation modulation = p25Config.getModulation();
|
||||
|
||||
|
||||
switch(modulation)
|
||||
{
|
||||
case C4FM:
|
||||
modules.add(new FMDemodulatorModule(P25_C4FM_IQ_FILTER));
|
||||
modules.add(new DemodulatedAudioFilterModule(P25_C4FM_DEMOD_FILTER, 1.0f));
|
||||
modules.add(new P25_C4FMDecoder(aliasList, decodeConfig.getAFCMaximumCorrection()));
|
||||
modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.C4FM,
|
||||
p25Config.getIgnoreDataCalls()));
|
||||
break;
|
||||
case CQPSK:
|
||||
modules.add(new P25_LSMDecoder(aliasList));
|
||||
modules.add(new P25DecoderState(aliasList, channelType, P25Decoder.Modulation.CQPSK, p25Config.getIgnoreDataCalls()));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized P25 Phase 1 Modulation [" + modulation + "]");
|
||||
}
|
||||
|
||||
if(channelType == ChannelType.STANDARD)
|
||||
{
|
||||
modules.add(new TrafficChannelManager(channelModel, decodeConfig, channel.getEventLogConfiguration(),
|
||||
channel.getRecordConfiguration(), channel.getSystem(), channel.getSite(),
|
||||
(aliasList != null ? aliasList.getName() : null), p25Config.getTrafficChannelPoolSize()));
|
||||
}
|
||||
|
||||
modules.add(new P25AudioModule(metadata));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown decoder type [" + decodeConfig.getDecoderType().toString() + "]");
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a list of auxiliary decoders, as specified in the configuration
|
||||
*
|
||||
* @param config - auxiliary configuration
|
||||
* @param aliasList - optional alias list
|
||||
* @return - list of auxiliary decoders
|
||||
*/
|
||||
public static List<Module> getAuxiliaryDecoders(AuxDecodeConfiguration config,
|
||||
AliasList aliasList)
|
||||
{
|
||||
List<Module> modules = new ArrayList<>();
|
||||
|
||||
if(config != null)
|
||||
{
|
||||
for(DecoderType auxDecoder : config.getAuxDecoders())
|
||||
{
|
||||
switch(auxDecoder)
|
||||
{
|
||||
case FLEETSYNC2:
|
||||
modules.add(new Fleetsync2Decoder(aliasList));
|
||||
modules.add(new Fleetsync2DecoderState(aliasList));
|
||||
break;
|
||||
case MDC1200:
|
||||
modules.add(new MDCDecoder(aliasList));
|
||||
modules.add(new MDCDecoderState(aliasList));
|
||||
break;
|
||||
case LJ_1200:
|
||||
modules.add(new LJ1200Decoder(aliasList));
|
||||
modules.add(new LJ1200DecoderState(aliasList));
|
||||
break;
|
||||
case TAIT_1200:
|
||||
modules.add(new Tait1200Decoder(aliasList));
|
||||
modules.add(new Tait1200DecoderState(aliasList));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized auxiliary "
|
||||
+ "decoder type [" + auxDecoder + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles a filter set containing filters for the primary channel
|
||||
* decoder and each of the auxiliary decoders
|
||||
*/
|
||||
public static FilterSet<Message> getMessageFilters(List<Module> modules)
|
||||
{
|
||||
FilterSet<Message> filterSet = new FilterSet<>();
|
||||
|
||||
for(Module module : modules)
|
||||
{
|
||||
if(module instanceof Decoder)
|
||||
{
|
||||
filterSet.addFilters(getMessageFilter(
|
||||
((Decoder) module).getDecoderType()));
|
||||
}
|
||||
}
|
||||
|
||||
/* If we don't have any filters, add an ALL-PASS filter */
|
||||
if(filterSet.getFilters().isEmpty())
|
||||
{
|
||||
filterSet.addFilter(new AllPassFilter<Message>());
|
||||
}
|
||||
|
||||
return filterSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of IMessageFilter objects (FilterSets or Filters) that
|
||||
* can process all of the messages produced by the specified decoder type.
|
||||
*/
|
||||
public static List<IFilter<Message>> getMessageFilter(DecoderType decoder)
|
||||
{
|
||||
ArrayList<IFilter<Message>> filters = new ArrayList<IFilter<Message>>();
|
||||
|
||||
switch(decoder)
|
||||
{
|
||||
case FLEETSYNC2:
|
||||
filters.add(new FleetsyncMessageFilter());
|
||||
break;
|
||||
case LJ_1200:
|
||||
filters.add(new LJ1200MessageFilter());
|
||||
break;
|
||||
case LTR_NET:
|
||||
filters.add(new LTRNetMessageFilter());
|
||||
break;
|
||||
case LTR_STANDARD:
|
||||
filters.add(new LTRStandardMessageFilter());
|
||||
break;
|
||||
case MDC1200:
|
||||
filters.add(new MDCMessageFilter());
|
||||
break;
|
||||
case MPT1327:
|
||||
filters.add(new MPT1327MessageFilter());
|
||||
break;
|
||||
case P25_PHASE1:
|
||||
case P25_PHASE2:
|
||||
filters.add(new P25MessageFilterSet());
|
||||
break;
|
||||
case PASSPORT:
|
||||
filters.add(new PassportMessageFilter());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
public static DecodeConfiguration getDefaultDecodeConfiguration()
|
||||
{
|
||||
return getDecodeConfiguration(DecoderType.NBFM);
|
||||
}
|
||||
|
||||
public static DecodeConfiguration getDecodeConfiguration(DecoderType decoder)
|
||||
{
|
||||
DecodeConfiguration retVal;
|
||||
|
||||
switch(decoder)
|
||||
{
|
||||
case AM:
|
||||
return new DecodeConfigAM();
|
||||
case LTR_NET:
|
||||
return new DecodeConfigLTRNet();
|
||||
case LTR_STANDARD:
|
||||
return new DecodeConfigLTRStandard();
|
||||
case MPT1327:
|
||||
return new DecodeConfigMPT1327();
|
||||
case NBFM:
|
||||
return new DecodeConfigNBFM();
|
||||
case PASSPORT:
|
||||
return new DecodeConfigPassport();
|
||||
case P25_PHASE1:
|
||||
return new DecodeConfigP25Phase1();
|
||||
default:
|
||||
throw new IllegalArgumentException("DecodeConfigFactory - unknown decoder type [" + decoder.toString() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public static ValidatingEditor<Channel> getEditor(DecoderType type, ChannelMapModel model)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case AM:
|
||||
return new AMDecoderEditor();
|
||||
case LTR_NET:
|
||||
return new LTRNetDecoderEditor();
|
||||
case LTR_STANDARD:
|
||||
return new LTRStandardDecoderEditor();
|
||||
case MPT1327:
|
||||
return new MPT1327DecoderEditor(model);
|
||||
case NBFM:
|
||||
return new NBFMDecoderEditor();
|
||||
case P25_PHASE1:
|
||||
return new P25DecoderEditor();
|
||||
case PASSPORT:
|
||||
return new PassportDecoderEditor();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return new EmptyValidatingEditor<Channel>("a decoder");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the configuration
|
||||
*/
|
||||
public static DecodeConfiguration copy(DecodeConfiguration config)
|
||||
{
|
||||
if(config != null)
|
||||
{
|
||||
switch(config.getDecoderType())
|
||||
{
|
||||
case AM:
|
||||
DecodeConfigAM originalAM = (DecodeConfigAM) config;
|
||||
DecodeConfigAM copyAM = new DecodeConfigAM();
|
||||
copyAM.setAFC(originalAM.getAFC());
|
||||
copyAM.setAFCMaximumCorrection(originalAM.getAFCMaximumCorrection());
|
||||
return copyAM;
|
||||
case LTR_NET:
|
||||
DecodeConfigLTRNet originalLTRNet = (DecodeConfigLTRNet) config;
|
||||
DecodeConfigLTRNet copyLTRNet = new DecodeConfigLTRNet();
|
||||
copyLTRNet.setAFC(originalLTRNet.getAFC());
|
||||
copyLTRNet.setAFCMaximumCorrection(originalLTRNet.getAFCMaximumCorrection());
|
||||
copyLTRNet.setMessageDirection(originalLTRNet.getMessageDirection());
|
||||
return copyLTRNet;
|
||||
case LTR_STANDARD:
|
||||
DecodeConfigLTRStandard originalLTRStandard = (DecodeConfigLTRStandard) config;
|
||||
DecodeConfigLTRStandard copyLTRStandard = new DecodeConfigLTRStandard();
|
||||
copyLTRStandard.setAFC(originalLTRStandard.getAFC());
|
||||
copyLTRStandard.setAFCMaximumCorrection(originalLTRStandard.getAFCMaximumCorrection());
|
||||
copyLTRStandard.setMessageDirection(originalLTRStandard.getMessageDirection());
|
||||
return copyLTRStandard;
|
||||
case MPT1327:
|
||||
DecodeConfigMPT1327 originalMPT = (DecodeConfigMPT1327) config;
|
||||
DecodeConfigMPT1327 copyMPT = new DecodeConfigMPT1327();
|
||||
copyMPT.setAFC(originalMPT.getAFC());
|
||||
copyMPT.setAFCMaximumCorrection(originalMPT.getAFCMaximumCorrection());
|
||||
copyMPT.setCallTimeout(originalMPT.getCallTimeout());
|
||||
copyMPT.setChannelMapName(originalMPT.getChannelMapName());
|
||||
copyMPT.setSync(originalMPT.getSync());
|
||||
copyMPT.setTrafficChannelPoolSize(originalMPT.getTrafficChannelPoolSize());
|
||||
return copyMPT;
|
||||
case NBFM:
|
||||
DecodeConfigNBFM originalNBFM = (DecodeConfigNBFM) config;
|
||||
DecodeConfigNBFM copyNBFM = new DecodeConfigNBFM();
|
||||
copyNBFM.setAFC(originalNBFM.getAFC());
|
||||
copyNBFM.setAFCMaximumCorrection(originalNBFM.getAFCMaximumCorrection());
|
||||
return copyNBFM;
|
||||
case P25_PHASE1:
|
||||
DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1) config;
|
||||
DecodeConfigP25Phase1 copyP25 = new DecodeConfigP25Phase1();
|
||||
copyP25.setAFC(originalP25.getAFC());
|
||||
copyP25.setAFCMaximumCorrection(originalP25.getAFCMaximumCorrection());
|
||||
copyP25.setIgnoreDataCalls(originalP25.getIgnoreDataCalls());
|
||||
copyP25.setModulation(originalP25.getModulation());
|
||||
copyP25.setTrafficChannelPoolSize(originalP25.getTrafficChannelPoolSize());
|
||||
return copyP25;
|
||||
case PASSPORT:
|
||||
DecodeConfigPassport originalPass = (DecodeConfigPassport) config;
|
||||
DecodeConfigPassport copyPass = new DecodeConfigPassport();
|
||||
copyPass.setAFC(originalPass.getAFC());
|
||||
copyPass.setAFCMaximumCorrection(originalPass.getAFCMaximumCorrection());
|
||||
return copyPass;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized decoder configuration type:" + config.getDecoderType());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
130
src/main/java/io/github/dsheirer/module/decode/DecoderType.java
Normal file
130
src/main/java/io/github/dsheirer/module/decode/DecoderType.java
Normal file
@@ -0,0 +1,130 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Enumeration of decoder types
|
||||
*/
|
||||
public enum DecoderType
|
||||
{
|
||||
ACARS( "ACARS", "ACARS", "images/acars.png", 12500 ),
|
||||
AIS( "AIS", "AIS", "images/ais.png", 12500 ),
|
||||
AM( "AM", "AM", "images/am.png", 10000 ),
|
||||
APRS( "APRS", "APRS", "images/aprs.png", 12500 ),
|
||||
DMR( "DMR", "DMR", "images/dmr.png", 12500 ),
|
||||
FLEETSYNC2( "Fleetsync II", "Fsync2", "images/fm.png", 12500 ),
|
||||
LJ_1200( "LJ1200 173.075", "LJ1200", "images/lj1200.png", 12500 ),
|
||||
LTR_STANDARD( "LTR-Standard", "LTR", "images/ltr.png", 10000 ),
|
||||
LTR_NET( "LTR-Net", "LTR-Net", "images/ltr_net.png", 12500 ),
|
||||
MDC1200( "MDC1200", "MDC1200", "images/fm.png", 12500 ),
|
||||
MOTOROLA_TYPE_2( "MOTO T2", "MOTO T2", "images/motorola_type_2.png", 15000 ),
|
||||
MPT1327( "MPT1327", "MPT1327", "images/mpt1327.png", 12500 ),
|
||||
NXDN( "NXDN", "NXDN", "images/nxdn.png", 8000 ),
|
||||
NBFM( "NBFM", "NBFM", "images/fm.png", 12500 ),
|
||||
PASSPORT( "Passport", "Passport", "images/passport.png", 12500 ),
|
||||
P25_PHASE1( "P25 Phase I", "P25-1", "images/p25_1.png", 12500 ),
|
||||
P25_PHASE2( "P25 Phase II", "P25-2", "images/p25_2.png", 15000 ),
|
||||
TAIT_1200( "Tait 1200", "Tait 1200", "images/tait.png", 12500 );
|
||||
|
||||
private String mDisplayString;
|
||||
private String mShortDisplayString;
|
||||
private String mIconFilename;
|
||||
private int mChannelBandwidth;
|
||||
|
||||
DecoderType( String displayString, String shortDisplayString,
|
||||
String iconFilename, int bandwidth )
|
||||
{
|
||||
mDisplayString = displayString;
|
||||
mShortDisplayString = shortDisplayString;
|
||||
mIconFilename = iconFilename;
|
||||
mChannelBandwidth = bandwidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary decoders
|
||||
*/
|
||||
public static EnumSet<DecoderType> getPrimaryDecoders()
|
||||
{
|
||||
return EnumSet.of( DecoderType.AM,
|
||||
DecoderType.LTR_NET,
|
||||
DecoderType.LTR_STANDARD,
|
||||
DecoderType.MPT1327,
|
||||
DecoderType.NBFM,
|
||||
DecoderType.P25_PHASE1,
|
||||
DecoderType.PASSPORT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoders that can be used in the viewer application
|
||||
*/
|
||||
public static EnumSet<DecoderType> getInstrumentableDecoders()
|
||||
{
|
||||
return EnumSet.of( DecoderType.FLEETSYNC2,
|
||||
DecoderType.LJ_1200,
|
||||
DecoderType.LTR_NET,
|
||||
DecoderType.MDC1200,
|
||||
DecoderType.MPT1327,
|
||||
DecoderType.PASSPORT,
|
||||
DecoderType.P25_PHASE1,
|
||||
DecoderType.TAIT_1200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Available auxiliary decoders.
|
||||
*/
|
||||
public static ArrayList<DecoderType> getAuxDecoders()
|
||||
{
|
||||
ArrayList<DecoderType> decoders = new ArrayList<DecoderType>();
|
||||
|
||||
decoders.add( DecoderType.FLEETSYNC2 );
|
||||
decoders.add( DecoderType.LJ_1200 );
|
||||
decoders.add( DecoderType.MDC1200 );
|
||||
decoders.add( DecoderType.TAIT_1200 );
|
||||
|
||||
return decoders;
|
||||
}
|
||||
|
||||
public String getDisplayString()
|
||||
{
|
||||
return mDisplayString;
|
||||
}
|
||||
|
||||
public String getShortDisplayString()
|
||||
{
|
||||
return mShortDisplayString;
|
||||
}
|
||||
|
||||
public String getIconFilename()
|
||||
{
|
||||
return mIconFilename;
|
||||
}
|
||||
|
||||
public int getChannelBandwidth()
|
||||
{
|
||||
return mChannelBandwidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mDisplayString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 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 io.github.dsheirer.module.decode;
|
||||
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateProvider;
|
||||
import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
* Primary decoder adds the following functionality over the basic decoder:
|
||||
*
|
||||
* - Provides audio squelch control
|
||||
*/
|
||||
|
||||
public abstract class PrimaryDecoder extends Decoder implements ISquelchStateProvider
|
||||
{
|
||||
protected Listener<SquelchState> mSquelchStateListener;
|
||||
|
||||
protected DecodeConfiguration mDecodeConfiguration;
|
||||
|
||||
public PrimaryDecoder( DecodeConfiguration config )
|
||||
{
|
||||
mDecodeConfiguration = config;
|
||||
}
|
||||
|
||||
public DecodeConfiguration getDecodeConfiguration()
|
||||
{
|
||||
return mDecodeConfiguration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mSquelchStateListener = null;
|
||||
mDecodeConfiguration = null;
|
||||
}
|
||||
|
||||
public void broadcast( SquelchState state )
|
||||
{
|
||||
if( mSquelchStateListener != null )
|
||||
{
|
||||
mSquelchStateListener.receive( state );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSquelchStateListener( Listener<SquelchState> listener )
|
||||
{
|
||||
mSquelchStateListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSquelchStateListener()
|
||||
{
|
||||
mSquelchStateListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.am;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class AMDecoder extends PrimaryDecoder
|
||||
{
|
||||
public AMDecoder( DecodeConfiguration config )
|
||||
{
|
||||
super( config );
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.AM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.am;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EditorValidationException;
|
||||
import io.github.dsheirer.gui.editor.ValidatingEditor;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public class AMDecoderEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public AMDecoderEditor()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0", "", "" ) );
|
||||
add( new JLabel( "AM Decoder" ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate( Editor<Channel> editor ) throws EditorValidationException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem( Channel item )
|
||||
{
|
||||
super.setItem( item );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
if( getItem().getDecodeConfiguration() instanceof DecodeConfigAM )
|
||||
{
|
||||
setModified( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
setModified( true );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasItem() && isModified() )
|
||||
{
|
||||
getItem().setDecodeConfiguration( new DecodeConfigAM() );
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.am;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
public class DecodeConfigAM extends DecodeConfiguration
|
||||
{
|
||||
public DecodeConfigAM()
|
||||
{
|
||||
super( DecoderType.AM );
|
||||
|
||||
setAFC( false );
|
||||
}
|
||||
|
||||
public boolean supportsAFC()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.config;
|
||||
|
||||
import io.github.dsheirer.controller.config.Configuration;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class AuxDecodeConfiguration extends Configuration
|
||||
{
|
||||
ArrayList<DecoderType> mAuxDecoders = new ArrayList<DecoderType>();
|
||||
|
||||
public AuxDecodeConfiguration()
|
||||
{
|
||||
}
|
||||
|
||||
@XmlElement( name="aux_decoder" )
|
||||
public ArrayList<DecoderType> getAuxDecoders()
|
||||
{
|
||||
return mAuxDecoders;
|
||||
}
|
||||
|
||||
public void setAuxDecoders( ArrayList<DecoderType> decoders )
|
||||
{
|
||||
mAuxDecoders = decoders;
|
||||
}
|
||||
|
||||
public void addAuxDecoder( DecoderType decoder )
|
||||
{
|
||||
mAuxDecoders.add( decoder );
|
||||
}
|
||||
|
||||
public void removeAuxDecoder( DecoderType decoder )
|
||||
{
|
||||
mAuxDecoders.remove( decoder );
|
||||
}
|
||||
|
||||
public void clearAuxDecoders()
|
||||
{
|
||||
mAuxDecoders.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.config;
|
||||
|
||||
import io.github.dsheirer.controller.config.Configuration;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
|
||||
import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
|
||||
import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
|
||||
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
||||
import io.github.dsheirer.module.decode.p25.DecodeConfigP25Phase1;
|
||||
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.XmlSeeAlso;
|
||||
|
||||
@XmlSeeAlso( { DecodeConfigAM.class,
|
||||
DecodeConfigNBFM.class,
|
||||
DecodeConfigLTRNet.class,
|
||||
DecodeConfigLTRStandard.class,
|
||||
DecodeConfigMPT1327.class,
|
||||
DecodeConfigPassport.class,
|
||||
DecodeConfigP25Phase1.class } )
|
||||
@XmlRootElement( name = "decode_configuration" )
|
||||
public abstract class DecodeConfiguration extends Configuration
|
||||
{
|
||||
public static final int DEFAULT_CALL_TIMEOUT_SECONDS = 45;
|
||||
public static final int CALL_TIMEOUT_MINIMUM = 1;
|
||||
public static final int CALL_TIMEOUT_MAXIMUM = 300; //5 minutes
|
||||
|
||||
public static final int TRAFFIC_CHANNEL_LIMIT_DEFAULT = 3;
|
||||
public static final int TRAFFIC_CHANNEL_LIMIT_MINIMUM = 0;
|
||||
public static final int TRAFFIC_CHANNEL_LIMIT_MAXIMUM = 30;
|
||||
|
||||
public static final int DEFAULT_AFC_MAX_CORRECTION = 1000;
|
||||
|
||||
private DecoderType mDecoderType = DecoderType.NBFM;
|
||||
private boolean mAFCEnabled = true;
|
||||
private int mAFCMaxCorrection = DEFAULT_AFC_MAX_CORRECTION;
|
||||
|
||||
public DecodeConfiguration()
|
||||
{
|
||||
this( DecoderType.NBFM );
|
||||
}
|
||||
|
||||
public DecodeConfiguration( DecoderType type )
|
||||
{
|
||||
mDecoderType = type;
|
||||
}
|
||||
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return mDecoderType;
|
||||
}
|
||||
|
||||
@XmlAttribute( name = "afc" )
|
||||
public boolean getAFC()
|
||||
{
|
||||
return mAFCEnabled;
|
||||
}
|
||||
|
||||
public boolean isAFCEnabled()
|
||||
{
|
||||
return mAFCEnabled;
|
||||
}
|
||||
|
||||
public void setAFC( boolean enabled )
|
||||
{
|
||||
mAFCEnabled = enabled;
|
||||
}
|
||||
|
||||
@XmlAttribute( name = "AFCMaximumCorrection" )
|
||||
public int getAFCMaximumCorrection()
|
||||
{
|
||||
return mAFCMaxCorrection;
|
||||
}
|
||||
|
||||
public void setAFCMaximumCorrection( int max )
|
||||
{
|
||||
mAFCMaxCorrection = max;
|
||||
}
|
||||
|
||||
public boolean supportsAFC()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.event;
|
||||
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class ActivitySummaryFrame extends JFrame
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ActivitySummaryFrame( String summary )
|
||||
{
|
||||
this( summary, null );
|
||||
}
|
||||
|
||||
public ActivitySummaryFrame( String summary, Component displayOver )
|
||||
{
|
||||
setTitle( "Activity Summary" );
|
||||
setLocationRelativeTo( displayOver );
|
||||
setSize( 400, 400 );
|
||||
setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
|
||||
setLayout( new MigLayout( "", "[grow,fill]", "[grow,fill][]" ) );
|
||||
|
||||
JTextArea summaryText = new JTextArea( summary );
|
||||
|
||||
JScrollPane scroller = new JScrollPane( summaryText );
|
||||
scroller.setViewportView( summaryText );
|
||||
|
||||
add( scroller, "wrap" );
|
||||
|
||||
JButton close = new JButton( "Close" );
|
||||
close.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
dispose();
|
||||
}
|
||||
} );
|
||||
|
||||
add( close );
|
||||
|
||||
EventQueue.invokeLater( new Runnable()
|
||||
{
|
||||
public void run()
|
||||
{
|
||||
setVisible( true );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.event;
|
||||
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public interface ActivitySummaryProvider extends Listener<Message>
|
||||
{
|
||||
public String getActivitySummary();
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.event;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public abstract class CallEvent implements Comparable<CallEvent>
|
||||
{
|
||||
protected long mEventStart = System.currentTimeMillis();
|
||||
protected long mEventEnd;
|
||||
|
||||
private SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd");
|
||||
private SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("HHmmss");
|
||||
|
||||
protected DecoderType mDecoderType;
|
||||
protected CallEventType mCallEventType;
|
||||
protected AliasList mAliasList;
|
||||
protected String mFromID;
|
||||
protected String mToID;
|
||||
protected String mDetails;
|
||||
|
||||
private boolean mValid = true;
|
||||
|
||||
public CallEvent(DecoderType decoder, CallEventType callEventType, AliasList aliasList, String fromID,
|
||||
String toID, String details)
|
||||
{
|
||||
mDecoderType = decoder;
|
||||
mCallEventType = callEventType;
|
||||
mAliasList = aliasList;
|
||||
mFromID = fromID;
|
||||
mToID = toID;
|
||||
mDetails = details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this event is valid. Use this method to mark a previously
|
||||
* dispatched event as invalid and resend it so that any downstream
|
||||
* processing can remove the invalid event.
|
||||
*/
|
||||
public void setValid(boolean valid)
|
||||
{
|
||||
mValid = valid;
|
||||
}
|
||||
|
||||
public boolean isValid()
|
||||
{
|
||||
return mValid;
|
||||
}
|
||||
|
||||
public void setEnd(long end)
|
||||
{
|
||||
mEventEnd = end;
|
||||
}
|
||||
|
||||
public void end()
|
||||
{
|
||||
setEnd(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return mDecoderType;
|
||||
}
|
||||
|
||||
public CallEventType getCallEventType()
|
||||
{
|
||||
return mCallEventType;
|
||||
}
|
||||
|
||||
public void setCallEventType(CallEventType type)
|
||||
{
|
||||
mCallEventType = type;
|
||||
}
|
||||
|
||||
public AliasList getAliasList()
|
||||
{
|
||||
return mAliasList;
|
||||
}
|
||||
|
||||
public void setAliasList(AliasList aliasList)
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
}
|
||||
|
||||
public boolean hasAliasList()
|
||||
{
|
||||
return mAliasList != null;
|
||||
}
|
||||
|
||||
public long getEventStartTime()
|
||||
{
|
||||
return mEventStart;
|
||||
}
|
||||
|
||||
public long getEventEndTime()
|
||||
{
|
||||
return mEventEnd;
|
||||
}
|
||||
|
||||
public String getFromID()
|
||||
{
|
||||
return mFromID;
|
||||
}
|
||||
|
||||
public boolean hasFromID()
|
||||
{
|
||||
return mFromID != null;
|
||||
}
|
||||
|
||||
public void setFromID(String id)
|
||||
{
|
||||
mFromID = id;
|
||||
}
|
||||
|
||||
public String getToID()
|
||||
{
|
||||
return mToID;
|
||||
}
|
||||
|
||||
public boolean hasToID()
|
||||
{
|
||||
return mToID != null;
|
||||
}
|
||||
|
||||
public void setToID(String id)
|
||||
{
|
||||
mToID = id;
|
||||
}
|
||||
|
||||
public String getDetails()
|
||||
{
|
||||
return mDetails;
|
||||
}
|
||||
|
||||
public void setDetails(String details)
|
||||
{
|
||||
mDetails = details;
|
||||
}
|
||||
|
||||
public abstract Alias getFromIDAlias();
|
||||
|
||||
public abstract Alias getToIDAlias();
|
||||
|
||||
public abstract String getChannel();
|
||||
|
||||
public abstract long getFrequency();
|
||||
|
||||
public static String getCSVHeader()
|
||||
{
|
||||
return "START_DATE,START_TIME,END_DATE,END_TIME,DECODER,EVENT,FROM,FROM_ALIAS,TO,TO_ALIAS,CHANNEL," +
|
||||
"FREQUENCY,DETAILS";
|
||||
}
|
||||
|
||||
public String toCSV()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("'");
|
||||
|
||||
Date eventStart = new Date(getEventStartTime());
|
||||
sb.append(DATE_FORMATTER.format(eventStart));
|
||||
sb.append("','");
|
||||
sb.append(TIME_FORMATTER.format(eventStart));
|
||||
sb.append("','");
|
||||
if(mEventEnd != 0)
|
||||
{
|
||||
Date eventEnd = new Date(getEventEndTime());
|
||||
sb.append(DATE_FORMATTER.format(eventEnd));
|
||||
sb.append("','");
|
||||
sb.append(TIME_FORMATTER.format(eventEnd));
|
||||
sb.append("','");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("','',");
|
||||
}
|
||||
sb.append("','");
|
||||
sb.append(getDecoderType().toString());
|
||||
sb.append("','");
|
||||
sb.append(getCallEventType().toString());
|
||||
sb.append("',");
|
||||
if(getFromID() != null)
|
||||
{
|
||||
sb.append("'");
|
||||
sb.append(getFromID());
|
||||
sb.append("'");
|
||||
}
|
||||
sb.append(",");
|
||||
|
||||
Alias fromAlias = getFromIDAlias();
|
||||
|
||||
if(fromAlias != null)
|
||||
{
|
||||
sb.append("'");
|
||||
sb.append(fromAlias.getName());
|
||||
sb.append("'");
|
||||
}
|
||||
sb.append(",");
|
||||
|
||||
if(getToID() != null)
|
||||
{
|
||||
sb.append("'");
|
||||
sb.append(getToID());
|
||||
sb.append("'");
|
||||
}
|
||||
sb.append(",");
|
||||
|
||||
Alias toAlias = getToIDAlias();
|
||||
|
||||
if(toAlias != null)
|
||||
{
|
||||
sb.append("'");
|
||||
sb.append(toAlias.getName());
|
||||
sb.append("'");
|
||||
}
|
||||
sb.append(",'");
|
||||
sb.append(getChannel());
|
||||
sb.append("','");
|
||||
sb.append(getFrequency());
|
||||
sb.append("',");
|
||||
if(getDetails() != null)
|
||||
{
|
||||
sb.append("'");
|
||||
sb.append(getDetails());
|
||||
sb.append("'");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public enum CallEventType
|
||||
{
|
||||
ANNOUNCEMENT("Announcement"),
|
||||
CALL("Call"),
|
||||
CALL_ALERT("Call Alert"),
|
||||
CALL_DETECT("Call Detect"),
|
||||
CALL_DO_NOT_MONITOR("Call-Do Not Monitor"),
|
||||
CALL_END("Call End"),
|
||||
CALL_UNIQUE_ID("UID Call"),
|
||||
CALL_NO_TUNER("Call - No Tuner"),
|
||||
CALL_TIMEOUT("Call Timeout"),
|
||||
COMMAND("Command"),
|
||||
DATA_CALL("Data Call"),
|
||||
DEREGISTER("Deregister"),
|
||||
EMERGENCY("EMERGENCY"),
|
||||
ENCRYPTED_CALL("Encrypted Call"),
|
||||
FUNCTION("Function"),
|
||||
GPS("GPS"),
|
||||
GROUP_CALL("Group Call"),
|
||||
ID_ANI("ANI"),
|
||||
ID_UNIQUE("Unique ID"),
|
||||
NOTIFICATION("Notification"),
|
||||
PAGE("Page"),
|
||||
PATCH_GROUP_ADD("Patch Group Add"),
|
||||
PATCH_GROUP_CALL("Patch Group Call"),
|
||||
PATCH_GROUP_DELETE("Patch Group Delete"),
|
||||
QUERY("Query"),
|
||||
REGISTER("Register"),
|
||||
REGISTER_ESN("ESN"),
|
||||
RESPONSE("Response"),
|
||||
SDM("Short Data Message"),
|
||||
STATION_ID("Station ID"),
|
||||
STATUS("Status"),
|
||||
TELEPHONE_INTERCONNECT("Telephone Interconnect"),
|
||||
UNIT_TO_UNIT_CALL("Unit To Unit Call"),
|
||||
UNKNOWN("Unknown"),
|
||||
INVALID("Invalid");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
private CallEventType(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CallEvent other)
|
||||
{
|
||||
if(other != null && other == this)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Long.compare(mEventStart, other.getEventStartTime());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.event;
|
||||
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* Custom renderer for JTable cells that contain Alias objects. Renders the
|
||||
* alias name, and if there is an icon name, attempts to get that icon from
|
||||
* the settings manager and render it.
|
||||
*/
|
||||
public class CallEventAliasCellRenderer extends DefaultTableCellRenderer
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final static Logger mLog = LoggerFactory.getLogger(CallEventAliasCellRenderer.class);
|
||||
|
||||
private IconManager mIconManager;
|
||||
|
||||
public CallEventAliasCellRenderer(IconManager iconManager)
|
||||
{
|
||||
super();
|
||||
mIconManager = iconManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
|
||||
{
|
||||
JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
|
||||
if(value instanceof Alias)
|
||||
{
|
||||
Alias alias = (Alias)value;
|
||||
|
||||
label.setText(alias.getName());
|
||||
label.setForeground(alias.getDisplayColor());
|
||||
label.setIcon(mIconManager.getIcon(alias.getIconName(), IconManager.DEFAULT_ICON_SIZE));
|
||||
}
|
||||
else
|
||||
{
|
||||
label.setText("");
|
||||
label.setForeground(table.getForeground());
|
||||
label.setIcon(null);
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.event;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CallEventModel extends AbstractTableModel implements Listener<CallEvent>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static DecimalFormat mFrequencyFormatter = new DecimalFormat("0.000000");
|
||||
|
||||
public static final int TIME = 0;
|
||||
public static final int EVENT = 1;
|
||||
public static final int FROM_ID = 2;
|
||||
public static final int FROM_ALIAS = 3;
|
||||
public static final int TO_ID = 4;
|
||||
public static final int TO_ALIAS = 5;
|
||||
public static final int CHANNEL = 6;
|
||||
public static final int FREQUENCY = 7;
|
||||
public static final int DETAILS = 8;
|
||||
|
||||
protected int mMaxMessages = 500;
|
||||
|
||||
protected List<CallEvent> mEvents = new ArrayList<CallEvent>();
|
||||
|
||||
protected String[] mHeaders = new String[]{"Time",
|
||||
"Event",
|
||||
"From",
|
||||
"Alias",
|
||||
"To",
|
||||
"Alias",
|
||||
"Channel",
|
||||
"Frequency",
|
||||
"Details"};
|
||||
|
||||
private SimpleDateFormat mSDFTime = new SimpleDateFormat("HH:mm:ss");
|
||||
|
||||
public CallEventModel()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mEvents.clear();
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mEvents.clear();
|
||||
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
public int getMaxMessageCount()
|
||||
{
|
||||
return mMaxMessages;
|
||||
}
|
||||
|
||||
public void setMaxMessageCount(int count)
|
||||
{
|
||||
mMaxMessages = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds, updates or deletes the event from the model. Producers can send
|
||||
* the same call event multiple times to indicate that information in the
|
||||
* event is updated. Producers can also mark the event as invalid and the
|
||||
* event will be removed from the model.
|
||||
*/
|
||||
public void receive(final CallEvent event)
|
||||
{
|
||||
if(event.isValid())
|
||||
{
|
||||
if(!mEvents.contains(event))
|
||||
{
|
||||
mEvents.add(0, event);
|
||||
|
||||
fireTableRowsInserted(0, 0);
|
||||
|
||||
prune();
|
||||
}
|
||||
else
|
||||
{
|
||||
int row = mEvents.indexOf(event);
|
||||
|
||||
fireTableRowsUpdated(row, row);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mEvents.contains(event))
|
||||
{
|
||||
int row = mEvents.indexOf(event);
|
||||
|
||||
mEvents.remove(event);
|
||||
|
||||
fireTableRowsDeleted(row, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prune()
|
||||
{
|
||||
while(mEvents.size() > mMaxMessages)
|
||||
{
|
||||
int index = mEvents.size() - 1;
|
||||
mEvents.remove(index);
|
||||
|
||||
fireTableRowsDeleted(index, index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount()
|
||||
{
|
||||
return mEvents.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount()
|
||||
{
|
||||
return mHeaders.length;
|
||||
}
|
||||
|
||||
public String getColumnName(int column)
|
||||
{
|
||||
return mHeaders[column];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex)
|
||||
{
|
||||
synchronized(mEvents)
|
||||
{
|
||||
switch(columnIndex)
|
||||
{
|
||||
case TIME:
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append(mSDFTime.format(
|
||||
mEvents.get(rowIndex).getEventStartTime()));
|
||||
|
||||
if(mEvents.get(rowIndex).getEventEndTime() != 0)
|
||||
{
|
||||
sb.append(" - ");
|
||||
sb.append(mSDFTime.format(
|
||||
mEvents.get(rowIndex).getEventEndTime()));
|
||||
}
|
||||
else if(mEvents.get(rowIndex)
|
||||
.getCallEventType() == CallEvent.CallEventType.CALL)
|
||||
{
|
||||
sb.append(" - In Progress");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
case EVENT:
|
||||
return mEvents.get(rowIndex).getCallEventType();
|
||||
case FROM_ID:
|
||||
return mEvents.get(rowIndex).getFromID();
|
||||
case FROM_ALIAS:
|
||||
return mEvents.get(rowIndex).getFromIDAlias();
|
||||
case TO_ID:
|
||||
return mEvents.get(rowIndex).getToID();
|
||||
case TO_ALIAS:
|
||||
return mEvents.get(rowIndex).getToIDAlias();
|
||||
case CHANNEL:
|
||||
return mEvents.get(rowIndex).getChannel();
|
||||
case FREQUENCY:
|
||||
long frequency = mEvents.get(rowIndex).getFrequency();
|
||||
|
||||
if(frequency != 0)
|
||||
{
|
||||
return mFrequencyFormatter.format((double) frequency / 1E6d);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case DETAILS:
|
||||
return mEvents.get(rowIndex).getDetails();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int columnIndex)
|
||||
{
|
||||
if(columnIndex == FROM_ALIAS || columnIndex == TO_ALIAS)
|
||||
{
|
||||
return Alias.class;
|
||||
}
|
||||
|
||||
return super.getColumnClass(columnIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.event;
|
||||
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import io.github.dsheirer.module.ProcessingChain;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class CallEventPanel extends JPanel implements Listener<ProcessingChain>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private JTable mTable;
|
||||
private CallEventModel mEmptyCallEventModel = new CallEventModel();
|
||||
private JScrollPane mEmptyScroller;
|
||||
private CallEventAliasCellRenderer mRenderer;
|
||||
|
||||
/**
|
||||
* View for call event table
|
||||
* @param iconManager to display alias icons in table rows
|
||||
*/
|
||||
public CallEventPanel(IconManager iconManager)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
|
||||
mTable = new JTable(mEmptyCallEventModel);
|
||||
mTable.setAutoCreateRowSorter(true);
|
||||
mTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
|
||||
|
||||
mRenderer = new CallEventAliasCellRenderer(iconManager);
|
||||
|
||||
|
||||
mEmptyScroller = new JScrollPane(mTable);
|
||||
|
||||
add(mEmptyScroller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ProcessingChain processingChain)
|
||||
{
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mTable.setModel(processingChain != null ? processingChain.getCallEventModel() : mEmptyCallEventModel);
|
||||
|
||||
if(processingChain != null)
|
||||
{
|
||||
mTable.getColumnModel().getColumn(CallEventModel.FROM_ALIAS).setCellRenderer(mRenderer);
|
||||
mTable.getColumnModel().getColumn(CallEventModel.TO_ALIAS).setCellRenderer(mRenderer);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.github.dsheirer.module.decode.event;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public interface ICallEventListener
|
||||
{
|
||||
public Listener<CallEvent> getCallEventListener();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.github.dsheirer.module.decode.event;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public interface ICallEventProvider
|
||||
{
|
||||
public void addCallEventListener( Listener<CallEvent> listener );
|
||||
public void removeCallEventListener( Listener<CallEvent> listener );
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.event;
|
||||
|
||||
import io.github.dsheirer.filter.FilterSet;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.awt.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class MessageActivityModel extends AbstractTableModel
|
||||
implements Listener<Message>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final int TIME = 0;
|
||||
private static final int PROTOCOL = 1;
|
||||
private static final int ERROR_STATUS = 2;
|
||||
private static final int MESSAGE = 3;
|
||||
private static final int MESSAGE_BITS = 4;
|
||||
|
||||
protected int mMaxMessages = 500;
|
||||
protected LinkedList<Message> mMessages = new LinkedList<Message>();
|
||||
|
||||
protected int[] mColumnWidths = { 110, 110, 110, -1, -1 };
|
||||
|
||||
protected String[] mHeaders = new String[] { "Time",
|
||||
"Protocol",
|
||||
"Error Check",
|
||||
"Message",
|
||||
"Binary" };
|
||||
|
||||
private SimpleDateFormat mSDFTime = new SimpleDateFormat( "HH:mm:ss" );
|
||||
|
||||
private boolean mNewMessagesFirst = true;
|
||||
|
||||
private FilterSet<Message> mMessageFilter;
|
||||
|
||||
public MessageActivityModel( FilterSet<Message> messageFilter )
|
||||
{
|
||||
mMessageFilter = messageFilter;
|
||||
}
|
||||
|
||||
public MessageActivityModel()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all messages from history
|
||||
*/
|
||||
public void clear()
|
||||
{
|
||||
EventQueue.invokeLater( new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
int messageCount = mMessages.size();
|
||||
|
||||
mMessages.clear();
|
||||
|
||||
fireTableRowsDeleted( 0, messageCount - 1 );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public FilterSet<Message> getMessageFilter()
|
||||
{
|
||||
return mMessageFilter;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mMessages.clear();
|
||||
}
|
||||
|
||||
public int[] getColumnWidths()
|
||||
{
|
||||
return mColumnWidths;
|
||||
}
|
||||
|
||||
public void setColumnWidths( int[] widths )
|
||||
{
|
||||
if( widths.length != 5 )
|
||||
{
|
||||
throw new IllegalArgumentException( "MessageActivityModel - "
|
||||
+ "column widths array should have 5 elements" );
|
||||
}
|
||||
else
|
||||
{
|
||||
mColumnWidths = widths;
|
||||
}
|
||||
}
|
||||
|
||||
public int getMaxMessageCount()
|
||||
{
|
||||
return mMaxMessages;
|
||||
}
|
||||
|
||||
public void setMaxMessageCount( int count )
|
||||
{
|
||||
mMaxMessages = count;
|
||||
}
|
||||
|
||||
public void receive( final Message message )
|
||||
{
|
||||
if( message.isValid() && mMessageFilter.passes( message ) )
|
||||
{
|
||||
EventQueue.invokeLater( new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mMessages.addFirst( message );
|
||||
|
||||
MessageActivityModel.this.fireTableRowsInserted( 0, 0 );
|
||||
|
||||
prune();
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
private void prune()
|
||||
{
|
||||
while( mMessages.size() > mMaxMessages )
|
||||
{
|
||||
mMessages.removeLast();
|
||||
|
||||
super.fireTableRowsDeleted( mMessages.size() - 1, mMessages.size() - 1 );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount()
|
||||
{
|
||||
return mMessages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount()
|
||||
{
|
||||
return mHeaders.length;
|
||||
}
|
||||
|
||||
public String getColumnName( int column )
|
||||
{
|
||||
return mHeaders[ column ];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt( int rowIndex, int columnIndex )
|
||||
{
|
||||
if( 0 <= rowIndex && rowIndex < mMessages.size() )
|
||||
{
|
||||
Message message = mMessages.get( rowIndex );
|
||||
|
||||
switch( columnIndex )
|
||||
{
|
||||
case TIME:
|
||||
return mSDFTime.format( new Date( message.getTimeReceived() ) );
|
||||
case PROTOCOL:
|
||||
return message.getProtocol();
|
||||
case ERROR_STATUS:
|
||||
return message.getErrorStatus();
|
||||
case MESSAGE:
|
||||
return message.getMessage();
|
||||
case MESSAGE_BITS:
|
||||
return message.getBinaryMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.event;
|
||||
|
||||
import com.jidesoft.swing.JideButton;
|
||||
import com.jidesoft.swing.JideSplitButton;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.filter.FilterEditorPanel;
|
||||
import io.github.dsheirer.filter.FilterSet;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.ProcessingChain;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
|
||||
public class MessageActivityPanel extends JPanel implements Listener<ProcessingChain>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MessageActivityPanel.class);
|
||||
|
||||
private static MessageActivityModel EMPTY_MODEL = new MessageActivityModel();
|
||||
|
||||
private JTable mTable = new JTable(EMPTY_MODEL);
|
||||
|
||||
private MessageManagementPanel mManagementPanel = new MessageManagementPanel();
|
||||
|
||||
private ChannelProcessingManager mChannelProcessingManager;
|
||||
|
||||
public MessageActivityPanel(ChannelProcessingManager channelProcessingManager)
|
||||
{
|
||||
mChannelProcessingManager = channelProcessingManager;
|
||||
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[][grow,fill]", "[]0[grow,fill]"));
|
||||
|
||||
add(mManagementPanel, "span,growx");
|
||||
|
||||
add(new JScrollPane(mTable), "span,grow");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ProcessingChain processingChain)
|
||||
{
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mTable.setModel(processingChain != null ? processingChain.getMessageActivityModel() : EMPTY_MODEL);
|
||||
|
||||
if(processingChain != null)
|
||||
{
|
||||
mManagementPanel.enableButtons();
|
||||
}
|
||||
else
|
||||
{
|
||||
mManagementPanel.disableButtons();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void channelChanged(ChannelEvent event)
|
||||
// {
|
||||
// boolean changed = false;
|
||||
//
|
||||
// if(event.getEvent() == Event.NOTIFICATION_SELECTION_CHANGE &&
|
||||
// event.getChannel().isSelected())
|
||||
// {
|
||||
// if(mDisplayedChannel == null || (mDisplayedChannel != null && mDisplayedChannel != event.getChannel()))
|
||||
// {
|
||||
// mDisplayedChannel = event.getChannel();
|
||||
//
|
||||
// ProcessingChain chain = mChannelProcessingManager.getProcessingChain(mDisplayedChannel);
|
||||
//
|
||||
// if(chain != null)
|
||||
// {
|
||||
// mDisplayedModel = chain.getMessageActivityModel();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// mDisplayedModel = EMPTY_MODEL;
|
||||
// }
|
||||
//
|
||||
// mMessageTable.setModel(mDisplayedModel);
|
||||
//
|
||||
// if(mDisplayedChannel != null)
|
||||
// {
|
||||
// mManagementPanel.enableButtons();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// mManagementPanel.disableButtons();
|
||||
// }
|
||||
//
|
||||
// changed = true;
|
||||
// }
|
||||
// }
|
||||
// else if(event.getEvent() == Event.NOTIFICATION_PROCESSING_STOP || event.getEvent() == Event.REQUEST_DISABLE)
|
||||
// {
|
||||
// if(mDisplayedChannel != null && mDisplayedChannel == event.getChannel())
|
||||
// {
|
||||
// mDisplayedChannel = null;
|
||||
//
|
||||
// mMessageTable.setModel(EMPTY_MODEL);
|
||||
//
|
||||
// mManagementPanel.disableButtons();
|
||||
//
|
||||
// changed = true;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if(changed)
|
||||
// {
|
||||
// revalidate();
|
||||
// repaint();
|
||||
// }
|
||||
// }
|
||||
|
||||
public class MessageManagementPanel extends JPanel
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private MessageHistoryButton mHistoryButton = new MessageHistoryButton();
|
||||
private MessageFilterButton mFilterButton = new MessageFilterButton();
|
||||
|
||||
public MessageManagementPanel()
|
||||
{
|
||||
setLayout(new MigLayout("insets 2 2 5 5", "[]5[left,grow]", ""));
|
||||
|
||||
disableButtons();
|
||||
|
||||
add(mFilterButton);
|
||||
add(mHistoryButton);
|
||||
}
|
||||
|
||||
public void enableButtons()
|
||||
{
|
||||
mHistoryButton.setEnabled(true);
|
||||
mFilterButton.setEnabled(true);
|
||||
}
|
||||
|
||||
public void disableButtons()
|
||||
{
|
||||
mHistoryButton.setEnabled(false);
|
||||
mFilterButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageHistoryButton extends JideSplitButton
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MessageHistoryButton()
|
||||
{
|
||||
super("Clear");
|
||||
|
||||
JPanel historyPanel = new JPanel();
|
||||
|
||||
historyPanel.add(new JLabel("Message History:"));
|
||||
|
||||
final JSlider slider = new JSlider();
|
||||
slider.setMinimum(0);
|
||||
slider.setMaximum(2000);
|
||||
slider.setMajorTickSpacing(500);
|
||||
slider.setPaintTicks(true);
|
||||
slider.setPaintLabels(true);
|
||||
|
||||
slider.addMouseListener(new MouseListener()
|
||||
{
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent arg0)
|
||||
{
|
||||
if(SwingUtilities.isLeftMouseButton(arg0) && arg0.getClickCount() == 2)
|
||||
{
|
||||
slider.setValue(500); //default
|
||||
}
|
||||
}
|
||||
|
||||
public void mouseEntered(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mouseExited(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mousePressed(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mouseReleased(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
slider.setValue(((MessageActivityModel)mTable.getModel()).getMaxMessageCount());
|
||||
slider.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent arg0)
|
||||
{
|
||||
((MessageActivityModel)mTable.getModel()).setMaxMessageCount(slider.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
historyPanel.add(slider);
|
||||
|
||||
add(historyPanel);
|
||||
|
||||
/* Clear messages */
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
((MessageActivityModel)mTable.getModel()).clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageFilterButton extends JideButton
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public MessageFilterButton()
|
||||
{
|
||||
super("Filter");
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent arg0)
|
||||
{
|
||||
final JFrame editor = new JFrame();
|
||||
|
||||
editor.setTitle("Message Filter Editor");
|
||||
editor.setSize(600, 400);
|
||||
editor.setLocationRelativeTo(MessageFilterButton.this);
|
||||
editor.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
editor.setLayout(new MigLayout("", "[grow,fill]",
|
||||
"[grow,fill][][]"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
FilterSet<Message> filter = (FilterSet<Message>) ((MessageActivityModel)mTable.getModel()).getMessageFilter();
|
||||
|
||||
FilterEditorPanel<Message> panel = new FilterEditorPanel<Message>(filter);
|
||||
|
||||
JScrollPane scroller = new JScrollPane(panel);
|
||||
scroller.setViewportView(panel);
|
||||
|
||||
editor.add(scroller, "wrap");
|
||||
|
||||
editor.add(new JLabel("Right-click to select/deselect all nodes"), "wrap");
|
||||
|
||||
JButton close = new JButton("Close");
|
||||
close.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
editor.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
editor.add(close);
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
public void run()
|
||||
{
|
||||
editor.setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.fleetsync1;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.map.Plottable;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class Fleetsync1Message extends Message
|
||||
{
|
||||
private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" );
|
||||
|
||||
private BinaryMessage mMessage;
|
||||
|
||||
public Fleetsync1Message( BinaryMessage message )
|
||||
{
|
||||
mMessage = message;
|
||||
}
|
||||
|
||||
public boolean isValid()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( mSDF.format( new Date( System.currentTimeMillis() ) ) );
|
||||
sb.append( " " );
|
||||
sb.append( "FS1 [" + mMessage.toString() + "]" );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads spaces onto the end of the value to make it 'places' long
|
||||
*/
|
||||
public String pad( String value, int places, String padCharacter )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( value );
|
||||
|
||||
while( sb.length() < places )
|
||||
{
|
||||
sb.append( padCharacter );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads an integer value with additional zeroes to make it decimalPlaces long
|
||||
*/
|
||||
public String format( int number, int decimalPlaces )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() );
|
||||
|
||||
for( int x = 0; x < paddingRequired; x++)
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
|
||||
sb.append( number );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings( "unused" )
|
||||
private int getInt( int[] bits )
|
||||
{
|
||||
int retVal = 0;
|
||||
|
||||
for( int x = 0; x < bits.length; x++ )
|
||||
{
|
||||
if( mMessage.get( bits[ x ] ) )
|
||||
{
|
||||
retVal += 1<<x;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBinaryMessage()
|
||||
{
|
||||
return mMessage.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToID()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorStatus()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plottable getPlottable()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a listing of aliases contained in the message.
|
||||
*/
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<Alias>();
|
||||
|
||||
Alias from = getFromIDAlias();
|
||||
|
||||
if( from != null )
|
||||
{
|
||||
aliases.add( from );
|
||||
}
|
||||
|
||||
Alias to = getToIDAlias();
|
||||
|
||||
if( to != null )
|
||||
{
|
||||
aliases.add( to );
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.fleetsync1;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Fleetsync1MessageProcessor implements Listener<BinaryMessage>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( Fleetsync1MessageProcessor.class );
|
||||
|
||||
private Broadcaster<Message> mBroadcaster = new Broadcaster<Message>();
|
||||
|
||||
@Override
|
||||
public void receive( BinaryMessage buffer )
|
||||
{
|
||||
Fleetsync1Message message = new Fleetsync1Message( buffer );
|
||||
|
||||
mBroadcaster.receive( message );
|
||||
}
|
||||
|
||||
public void addMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mBroadcaster.addListener( listener );
|
||||
}
|
||||
|
||||
public void removeMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mBroadcaster.removeListener( listener );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.fleetsync2;
|
||||
|
||||
import io.github.dsheirer.bits.MessageFramer;
|
||||
import io.github.dsheirer.bits.SyncPattern;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.instrument.tap.stream.BinaryTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatBufferTap;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.fsk.FSK2Decoder;
|
||||
import io.github.dsheirer.dsp.fsk.FSK2Decoder.Output;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* Fleetsync II Decoder - 1200 baud 2FSK decoder that can process 48k sample rate
|
||||
* floating point samples and output fully framed Fleetsync II messages
|
||||
*/
|
||||
public class Fleetsync2Decoder extends Decoder implements IFilteredRealBufferListener,
|
||||
Instrumentable
|
||||
{
|
||||
/* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */
|
||||
private static final int sDECIMATED_SAMPLE_RATE = 24000;
|
||||
|
||||
/* Baud or Symbol Rate */
|
||||
private static final int sSYMBOL_RATE = 1200;
|
||||
|
||||
/* Message length - 5 x REVS + 16 x SYNC + 8 x 64Bit Blocks */
|
||||
private static final int sMESSAGE_LENGTH = 537;
|
||||
|
||||
/* Instrumentation Taps */
|
||||
private ArrayList<TapGroup> mAvailableTaps;
|
||||
private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD =
|
||||
"Tap Point: Bandpass Filter > < FSK2 Decoder";
|
||||
private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER =
|
||||
"Tap Point: FSK2 Decoder > < Message Framer";
|
||||
|
||||
private FSK2Decoder mFSKDecoder;
|
||||
private HalfBandFilter_RB_RB mDecimationFilter;
|
||||
private RealFIRFilter_RB_RB mBandPassFilter;
|
||||
private MessageFramer mMessageFramer;
|
||||
private Fleetsync2MessageProcessor mMessageProcessor;
|
||||
|
||||
public Fleetsync2Decoder( AliasList aliasList )
|
||||
{
|
||||
mDecimationFilter = new HalfBandFilter_RB_RB(
|
||||
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true );
|
||||
|
||||
mBandPassFilter = new RealFIRFilter_RB_RB(
|
||||
Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f );
|
||||
|
||||
mDecimationFilter.setListener( mBandPassFilter );
|
||||
|
||||
mFSKDecoder = new FSK2Decoder( sDECIMATED_SAMPLE_RATE,
|
||||
sSYMBOL_RATE, Output.INVERTED );
|
||||
mBandPassFilter.setListener( mFSKDecoder );
|
||||
|
||||
mMessageFramer = new MessageFramer(
|
||||
SyncPattern.FLEETSYNC2.getPattern(), sMESSAGE_LENGTH );
|
||||
mFSKDecoder.setListener( mMessageFramer );
|
||||
|
||||
mMessageProcessor = new Fleetsync2MessageProcessor( aliasList );
|
||||
mMessageFramer.addMessageListener( mMessageProcessor );
|
||||
|
||||
mMessageProcessor.setMessageListener( this );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mBandPassFilter.dispose();
|
||||
mDecimationFilter.dispose();
|
||||
mFSKDecoder.dispose();
|
||||
mMessageFramer.dispose();
|
||||
mMessageProcessor.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.FLEETSYNC2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<TapGroup>();
|
||||
|
||||
TapGroup group = new TapGroup( "Fleetsync 2 Decoder" );
|
||||
|
||||
group.add( new FloatBufferTap(
|
||||
INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) );
|
||||
group.add( new BinaryTap(
|
||||
INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) );
|
||||
|
||||
mAvailableTaps.add( group );
|
||||
|
||||
mAvailableTaps.addAll( mFSKDecoder.getTapGroups() );
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
{
|
||||
mFSKDecoder.registerTap( tap );
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD:
|
||||
FloatBufferTap bpTap = (FloatBufferTap)tap;
|
||||
mBandPassFilter.setListener( bpTap );
|
||||
bpTap.setListener( mFSKDecoder );
|
||||
break;
|
||||
case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER:
|
||||
BinaryTap decoderTap = (BinaryTap)tap;
|
||||
mFSKDecoder.setListener( decoderTap );
|
||||
decoderTap.setListener( mMessageFramer );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
{
|
||||
mFSKDecoder.unregisterTap( tap );
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD:
|
||||
mBandPassFilter.setListener( mFSKDecoder );
|
||||
break;
|
||||
case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER:
|
||||
mFSKDecoder.setListener( mMessageFramer );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getFilteredRealBufferListener()
|
||||
{
|
||||
return mDecimationFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.fleetsync2;
|
||||
|
||||
import io.github.dsheirer.channel.metadata.AliasedStringAttributeMonitor;
|
||||
import io.github.dsheirer.channel.metadata.Attribute;
|
||||
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.util.StringUtils;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class Fleetsync2DecoderState extends DecoderState
|
||||
{
|
||||
private TreeSet<String> mIdents = new TreeSet<String>();
|
||||
private TreeSet<String> mEmergencyIdents = new TreeSet<String>();
|
||||
|
||||
private AliasedStringAttributeMonitor mFromAttribute;
|
||||
private AliasedStringAttributeMonitor mToAttribute;
|
||||
private String mMessage;
|
||||
private String mMessageType;
|
||||
private long mFrequency;
|
||||
|
||||
public Fleetsync2DecoderState(AliasList aliasList)
|
||||
{
|
||||
super(aliasList);
|
||||
|
||||
mFromAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_FROM,
|
||||
getAttributeChangeRequestListener(), getAliasList(), AliasIDType.FLEETSYNC);
|
||||
mToAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_TO,
|
||||
getAttributeChangeRequestListener(), getAliasList(), AliasIDType.FLEETSYNC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.FLEETSYNC2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the overall decoder state and clears any accumulated event details
|
||||
*/
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mIdents.clear();
|
||||
mEmergencyIdents.clear();
|
||||
|
||||
resetState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets this decoder state
|
||||
*/
|
||||
public void resetState()
|
||||
{
|
||||
mFromAttribute.reset();
|
||||
mToAttribute.reset();
|
||||
mMessage = null;
|
||||
mMessageType = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Message message)
|
||||
{
|
||||
if(message instanceof FleetsyncMessage)
|
||||
{
|
||||
FleetsyncMessage fleetsync = (FleetsyncMessage) message;
|
||||
|
||||
if(fleetsync.isValid())
|
||||
{
|
||||
State state = State.CALL;
|
||||
|
||||
mFromAttribute.process(fleetsync.getFromID());
|
||||
mIdents.add(fleetsync.getFromID());
|
||||
|
||||
FleetsyncMessageType type = fleetsync.getMessageType();
|
||||
|
||||
if(type != FleetsyncMessageType.ANI)
|
||||
{
|
||||
mToAttribute.process(fleetsync.getToID());
|
||||
mIdents.add(fleetsync.getToID());
|
||||
}
|
||||
|
||||
setMessageType(type.getLabel());
|
||||
|
||||
switch(type)
|
||||
{
|
||||
case GPS:
|
||||
setMessage(fleetsync.getGPSLocation());
|
||||
state = State.DATA;
|
||||
break;
|
||||
case STATUS:
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(fleetsync.getStatus());
|
||||
|
||||
Alias status = fleetsync.getStatusAlias();
|
||||
|
||||
if(status != null)
|
||||
{
|
||||
sb.append("/");
|
||||
sb.append(status.getName());
|
||||
}
|
||||
|
||||
setMessage(sb.toString());
|
||||
state = State.DATA;
|
||||
break;
|
||||
case EMERGENCY:
|
||||
case LONE_WORKER_EMERGENCY:
|
||||
mEmergencyIdents.add(fleetsync.getFromID());
|
||||
state = State.DATA;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
FleetsyncCallEvent fsCallEvent =
|
||||
FleetsyncCallEvent.getFleetsync2Event(fleetsync, mFrequency);
|
||||
|
||||
fsCallEvent.setAliasList(getAliasList());
|
||||
|
||||
broadcast(fsCallEvent);
|
||||
|
||||
/* Broadcast decode event so that the channel state will
|
||||
* kick in and reset everything after a short delay */
|
||||
broadcast(new DecoderStateEvent(this, Event.DECODE, state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
public void setMessage(String message)
|
||||
{
|
||||
if(!StringUtils.isEqual(mMessage, message))
|
||||
{
|
||||
mMessage = message;
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.MESSAGE, mMessage));
|
||||
}
|
||||
}
|
||||
|
||||
public String getMessageType()
|
||||
{
|
||||
return "Fleetsync " + mMessageType;
|
||||
}
|
||||
|
||||
public void setMessageType(String type)
|
||||
{
|
||||
if(!StringUtils.isEqual(mMessageType, type))
|
||||
{
|
||||
mMessageType = type;
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.MESSAGE_TYPE, getMessageType()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to reset events issued by the channel state
|
||||
*/
|
||||
@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("=============================\n");
|
||||
sb.append("Decoder:\tFleetsync II\n\n");
|
||||
sb.append("Fleetsync Idents\n");
|
||||
|
||||
if(mIdents.isEmpty())
|
||||
{
|
||||
sb.append(" None\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Iterator<String> it = mIdents.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
String ident = it.next();
|
||||
|
||||
sb.append(" ");
|
||||
sb.append(formatIdent(ident));
|
||||
|
||||
if(hasAliasList())
|
||||
{
|
||||
Alias alias = getAliasList().getFleetsyncAlias(ident);
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
sb.append(" ");
|
||||
sb.append(alias.getName());
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\nFleetsync Emergency Activations\n");
|
||||
|
||||
if(mEmergencyIdents.isEmpty())
|
||||
{
|
||||
sb.append(" None\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Iterator<String> it = mEmergencyIdents.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
String ident = it.next();
|
||||
|
||||
sb.append(" ");
|
||||
sb.append(formatIdent(ident));
|
||||
|
||||
if(hasAliasList())
|
||||
{
|
||||
Alias alias = getAliasList().getFleetsyncAlias(ident);
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
sb.append(" ");
|
||||
sb.append(alias.getName());
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String formatIdent(String ident)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(ident.length() == 7)
|
||||
{
|
||||
sb.append(ident.substring(0, 3));
|
||||
sb.append("-");
|
||||
sb.append(ident.substring(3, 7));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return ident;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.fleetsync2;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
|
||||
public class Fleetsync2MessageProcessor implements Listener<BinaryMessage>
|
||||
{
|
||||
private Listener<Message> mMessageListener;
|
||||
|
||||
private AliasList mAliasList;
|
||||
|
||||
public Fleetsync2MessageProcessor( AliasList list )
|
||||
{
|
||||
mAliasList = list;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mMessageListener = null;
|
||||
mAliasList = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( BinaryMessage buffer )
|
||||
{
|
||||
FleetsyncMessage message = new FleetsyncMessage( buffer, mAliasList );
|
||||
|
||||
if( mMessageListener != null )
|
||||
{
|
||||
mMessageListener.receive( message );
|
||||
}
|
||||
}
|
||||
|
||||
public void setMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.fleetsync2;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
|
||||
public class FleetsyncCallEvent extends CallEvent
|
||||
{
|
||||
private long mFrequency = 0;
|
||||
|
||||
public FleetsyncCallEvent( CallEventType callEventType,
|
||||
AliasList aliasList,
|
||||
String fromID,
|
||||
String toID,
|
||||
String details,
|
||||
long frequency )
|
||||
{
|
||||
super( DecoderType.FLEETSYNC2, callEventType,
|
||||
aliasList, fromID, toID, details );
|
||||
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
private Alias getAlias( String ident )
|
||||
{
|
||||
if( hasAliasList() )
|
||||
{
|
||||
return getAliasList().getFleetsyncAlias( ident );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return getAlias( getFromID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return getAlias( getToID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannel()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
public static class Builder
|
||||
{
|
||||
/* Required parameters */
|
||||
private CallEventType mCallEventType;
|
||||
|
||||
/* Optional parameters */
|
||||
private AliasList mAliasList;
|
||||
private String mFromID;
|
||||
private String mToID;
|
||||
private String mDetails;
|
||||
private long mFrequency;
|
||||
|
||||
public Builder( CallEventType callEventType )
|
||||
{
|
||||
mCallEventType = callEventType;
|
||||
}
|
||||
|
||||
public Builder aliasList( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder frequency( long frequency )
|
||||
{
|
||||
mFrequency = frequency;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder from( String val )
|
||||
{
|
||||
mFromID = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder details( String details )
|
||||
{
|
||||
mDetails = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder to( String toID )
|
||||
{
|
||||
mToID = toID;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FleetsyncCallEvent build()
|
||||
{
|
||||
return new FleetsyncCallEvent( this );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor for the builder
|
||||
*/
|
||||
private FleetsyncCallEvent( Builder builder )
|
||||
{
|
||||
this( builder.mCallEventType,
|
||||
builder.mAliasList,
|
||||
builder.mFromID,
|
||||
builder.mToID,
|
||||
builder.mDetails,
|
||||
builder.mFrequency );
|
||||
}
|
||||
|
||||
public static FleetsyncCallEvent getFleetsync2Event(
|
||||
FleetsyncMessage message, long frequency )
|
||||
{
|
||||
CallEventType type = CallEventType.UNKNOWN;
|
||||
StringBuilder sbDetails = new StringBuilder();
|
||||
sbDetails.append( "Fleetsync " );
|
||||
|
||||
switch( message.getMessageType() )
|
||||
{
|
||||
case ACKNOWLEDGE:
|
||||
type = CallEventType.RESPONSE;
|
||||
sbDetails.append( "ACK" );
|
||||
break;
|
||||
case ANI:
|
||||
type = CallEventType.ID_ANI;
|
||||
break;
|
||||
case EMERGENCY:
|
||||
type = CallEventType.EMERGENCY;
|
||||
break;
|
||||
case GPS:
|
||||
type = CallEventType.GPS;
|
||||
sbDetails.append( message.getGPSLocation() );
|
||||
break;
|
||||
case LONE_WORKER_EMERGENCY:
|
||||
type = CallEventType.EMERGENCY;
|
||||
sbDetails.append( "LONE WORKER EMERGENCY ALERT" );
|
||||
break;
|
||||
case PAGING:
|
||||
type = CallEventType.PAGE;
|
||||
break;
|
||||
case STATUS:
|
||||
type = CallEventType.STATUS;
|
||||
sbDetails.append( "Status: " );
|
||||
sbDetails.append( message.getStatus() );
|
||||
|
||||
if( message.getStatusAlias() != null )
|
||||
{
|
||||
sbDetails.append( "/" );
|
||||
sbDetails.append( message.getStatusAlias().getName() );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if( type == CallEventType.ID_ANI ||
|
||||
type == CallEventType.EMERGENCY )
|
||||
{
|
||||
return new FleetsyncCallEvent.Builder( type )
|
||||
.details( sbDetails.toString() )
|
||||
.frequency( frequency )
|
||||
.from( message.getFromID() )
|
||||
.build();
|
||||
}
|
||||
else
|
||||
{
|
||||
return new FleetsyncCallEvent.Builder( type )
|
||||
.details( sbDetails.toString() )
|
||||
.frequency( frequency )
|
||||
.from( message.getFromID() )
|
||||
.to( message.getToID() )
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,894 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.fleetsync2;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.edac.CRCFleetsync;
|
||||
import io.github.dsheirer.map.Plottable;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import org.jdesktop.swingx.mapviewer.GeoPosition;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
|
||||
public class FleetsyncMessage extends Message
|
||||
{
|
||||
private static DecimalFormat mDecimalFormatter = new DecimalFormat("#.#####");
|
||||
private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd HHmmss" );
|
||||
|
||||
//Calendar to use in calculating time hacks
|
||||
Calendar mCalendar = new GregorianCalendar();
|
||||
|
||||
//Message parts are identified in big-endian order for correct translation
|
||||
|
||||
//Message Header
|
||||
private static int[] sRevs = { 4,3,2,1,0 };
|
||||
private static int[] sSync = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5 };
|
||||
|
||||
//Message Block 1
|
||||
private static int[] sStatusMessage = { 27,26,25,24,23,22,21 };
|
||||
private static int[] sMessageType = { 33,32,31,30,29 };
|
||||
|
||||
private static int sEmergencyFlag = 22;
|
||||
private static int sLoneWorkerFlag = 24;
|
||||
private static int sPagingFlag = 26;
|
||||
private static int sEndOfTransmissionFlag = 27;
|
||||
private static int sManualFlag = 28;
|
||||
private static int sANIFlag = 29;
|
||||
private static int sStatusFlag = 30;
|
||||
private static int sAcknowledgeFlag = 31;
|
||||
//32 - unknown - always 0
|
||||
//33 - unknown - always 0
|
||||
//34 - unknown - set for ACKNOWLEDGE
|
||||
private static int sGPSExtensionFlag = 35;
|
||||
private static int sFleetExtensionFlag = 36;
|
||||
private static int[] sFleetFrom = { 44,43,42,41,40,39,38,37 };
|
||||
private static int[] sIdentFrom = { 56,55,54,53,52,51,50,49,48,47,46,45 };
|
||||
private static int[] sIdentTo = { 68,67,66,65,64,63,62,61,60,59,58,57 };
|
||||
private static int[] sCRC1 = { 84,83,82,81,80,79,78,77,76,75,74,73,72,71,70,69 };
|
||||
|
||||
//Message Block 2
|
||||
private static int[] sFleetTo = { 92,91,90,89,88,87,86,85 };
|
||||
private static int[] sCRC2 = { 148,147,146,145,144,143,142,141,140,139,138,137,136,135,134,132 };
|
||||
|
||||
//Message Block 3
|
||||
private static int[] sGPSHours = { 176,175,174,173,172 };
|
||||
private static int[] sGPSMinutes = { 182,181,180,179,178,177 };
|
||||
private static int[] sGPSSeconds = { 188,187,186,185,184,183 };
|
||||
private static int[] sCRC3 = { 212,211,210,209,208,207,206,205,204,203,202,201,200,199,198,197 };
|
||||
|
||||
//Message Block 4
|
||||
private static int[] sGPSChecksum = { 220,219,218,217,216,215,214,213 };
|
||||
private static int[] sLatitudeDegreesMinutes = { 236,235,234,233,232,231,230,229,228,227,226,225,224,223,222,221 };
|
||||
private static int[] sLatitudeDecimalMinutes = { 251,250,249,248,247,246,245,244,243,242,241,240,239,238 };
|
||||
private static int[] sSpeed = { 276,275,274,273,272,271,270,269,268,267,266,265,264,263,262,261,260,259,258,257,256,255,254,253,252 };
|
||||
private static int[] sCRC4 = { 276,275,274,273,272,271,270,269,268,267,266,265,264,263,262,261 };
|
||||
|
||||
//Message Block 5
|
||||
private static int[] sGPSCentury = { 284,283,282,281,280,279,278,277 };
|
||||
private static int[] sGPSYear = { 291,290,289,288,287,286,285 };
|
||||
private static int[] sGPSMonth = { 295,294,293,292 };
|
||||
private static int[] sGPSDay = { 300,299,298,297,296 };
|
||||
private static int[] sLongitudeDegreesMinutes = { 316,315,314,313,312,311,310,309,308,307,306,305,304,303,302,301 };
|
||||
private static int[] sLongitudeDecimalMinutes = { 331,330,329,328,327,326,325,324,323,322,321,320,319,318 };
|
||||
private static int[] sCRC5 = { 340,339,338,337,336,335,334,333,332,331,330,329,328,327,326,325 };
|
||||
|
||||
//Message Block 6
|
||||
private static int[] sGPSUnknown1 = { 352,351,350,349 };
|
||||
private static int[] sGPSHeading = { 365,363,362,361,360,359,358,357,356,355,354,353 };
|
||||
private static int[] sCRC6 = { 404,403,402,401,400,399,398,397,396,395,394,393,392,391,390,389 };
|
||||
|
||||
//Message Block 7
|
||||
private static int[] sBLK7WhatIsIt = { 444,443,442,441,440,439,438,437,436,435,434,433,432,431,430,429 };
|
||||
private static int[] sCRC7 = { 468,467,466,465,464,463,462,461,460,459,458,457,456,455,454,453 };
|
||||
|
||||
//Message Block 8
|
||||
private static int[] sGPSSpeed = { 491,490,489,488,487,486,485,484 };
|
||||
private static int[] sGPSSpeedFractional = { 499,498,497,496,495,494,493,492 };
|
||||
private static int[] sCRC8 = { 532,531,530,529,528,527,526,525,524,523,522,521,520,519,518,517 };
|
||||
|
||||
private BinaryMessage mMessage;
|
||||
private CRC[] mCRC = new CRC[ 8 ];
|
||||
private AliasList mAliasList;
|
||||
|
||||
public FleetsyncMessage( BinaryMessage message, AliasList list )
|
||||
{
|
||||
mMessage = message;
|
||||
mAliasList = list;
|
||||
checkParity();
|
||||
}
|
||||
|
||||
private void checkParity()
|
||||
{
|
||||
//Check message block 1
|
||||
mCRC[ 0 ] = detectAndCorrect( 21, 85 );
|
||||
|
||||
//Only check subsequent blocks if we know block 1 is correct
|
||||
if( mCRC[ 0 ] == CRC.PASSED || mCRC[ 0 ] == CRC.CORRECTED )
|
||||
{
|
||||
if( hasFleetExtensionFlag() )
|
||||
{
|
||||
//Check message block 2
|
||||
mCRC[ 1 ] = detectAndCorrect( 85, 149 );
|
||||
}
|
||||
|
||||
if( hasGPSFlag() )
|
||||
{
|
||||
//Check message block 3
|
||||
mCRC[ 2 ] = detectAndCorrect( 149, 213 );
|
||||
//Check message block 4
|
||||
mCRC[ 3 ] = detectAndCorrect( 213, 277 );
|
||||
//Check message block 5
|
||||
mCRC[ 4 ] = detectAndCorrect( 277, 341 );
|
||||
//Check message block 6
|
||||
mCRC[ 5 ] = detectAndCorrect( 341, 405 );
|
||||
//Check message block 7
|
||||
mCRC[ 6 ] = detectAndCorrect( 405, 469 );
|
||||
//Check message block 8
|
||||
mCRC[ 7 ] = detectAndCorrect( 469, 533 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CRC detectAndCorrect( int start, int end )
|
||||
{
|
||||
BitSet original = mMessage.get( start, end );
|
||||
|
||||
CRC retVal = CRCFleetsync.check( original );
|
||||
|
||||
//Attempt to correct single-bit errors
|
||||
if( retVal == CRC.FAILED_PARITY )
|
||||
{
|
||||
int[] errorBitPositions = CRCFleetsync.findBitErrors( original );
|
||||
|
||||
if( errorBitPositions != null )
|
||||
{
|
||||
for( int errorBitPosition: errorBitPositions )
|
||||
{
|
||||
mMessage.flip( start + errorBitPosition );
|
||||
}
|
||||
|
||||
retVal = CRC.CORRECTED;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public boolean isValid()
|
||||
{
|
||||
boolean valid = true;
|
||||
|
||||
for( int x = 0; x < mCRC.length; x++ )
|
||||
{
|
||||
CRC crc = mCRC[ x ];
|
||||
|
||||
if( crc != null )
|
||||
{
|
||||
if( crc == CRC.FAILED_CRC ||
|
||||
crc == CRC.FAILED_PARITY ||
|
||||
crc == CRC.UNKNOWN )
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( mSDF.format( new Date( System.currentTimeMillis() ) ) );
|
||||
|
||||
sb.append( " FSync2 " + getParity() );
|
||||
|
||||
sb.append( getMessage() );
|
||||
|
||||
sb.append( " [" + mMessage.toString() + "]" );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads spaces onto the end of the value to make it 'places' long
|
||||
*/
|
||||
public String pad( String value, int places, String padCharacter )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( value );
|
||||
|
||||
while( sb.length() < places )
|
||||
{
|
||||
sb.append( padCharacter );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads an integer value with additional zeroes to make it decimalPlaces long
|
||||
*/
|
||||
public String format( int number, int decimalPlaces )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() );
|
||||
|
||||
for( int x = 0; x < paddingRequired; x++)
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
|
||||
sb.append( number );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public FleetsyncMessageType getMessageType()
|
||||
{
|
||||
if( hasStatusFlag() )
|
||||
{
|
||||
if( hasAcknowledgeFlag() )
|
||||
{
|
||||
return FleetsyncMessageType.ACKNOWLEDGE;
|
||||
}
|
||||
|
||||
if( hasGPSFlag() )
|
||||
{
|
||||
return FleetsyncMessageType.GPS;
|
||||
}
|
||||
else
|
||||
{
|
||||
return FleetsyncMessageType.STATUS;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( hasAcknowledgeFlag() )
|
||||
{
|
||||
return FleetsyncMessageType.ACKNOWLEDGE;
|
||||
}
|
||||
|
||||
if( hasANIFlag() )
|
||||
{
|
||||
return FleetsyncMessageType.ANI;
|
||||
}
|
||||
|
||||
if( hasGPSFlag() )
|
||||
{
|
||||
return FleetsyncMessageType.GPS;
|
||||
}
|
||||
|
||||
if( hasPagingFlag() )
|
||||
{
|
||||
return FleetsyncMessageType.PAGING;
|
||||
}
|
||||
|
||||
if( hasEmergencyFlag() )
|
||||
{
|
||||
if( hasLoneWorkerFlag() )
|
||||
{
|
||||
return FleetsyncMessageType.LONE_WORKER_EMERGENCY;
|
||||
}
|
||||
else
|
||||
return FleetsyncMessageType.EMERGENCY;
|
||||
}
|
||||
}
|
||||
|
||||
return FleetsyncMessageType.UNKNOWN;
|
||||
}
|
||||
|
||||
public int getStatus()
|
||||
{
|
||||
return getStatusNumber() + 9;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RAW status number. The actual status number should be
|
||||
* accessed via the getStatus() method.
|
||||
* @return
|
||||
*/
|
||||
public int getStatusNumber()
|
||||
{
|
||||
return getInt( sStatusMessage );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
*/
|
||||
public Alias getStatusAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getStatus( getStatus() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getFleetFrom()
|
||||
{
|
||||
return getInt( sFleetFrom ) + 99;
|
||||
}
|
||||
|
||||
public int getIdentifierFrom()
|
||||
{
|
||||
return getInt( sIdentFrom ) + 999;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverted Flags - 0 = flag is true
|
||||
* @return
|
||||
*/
|
||||
public boolean hasEndOfTransmissionFlag()
|
||||
{
|
||||
return !mMessage.get( sEndOfTransmissionFlag );
|
||||
}
|
||||
|
||||
public boolean hasEmergencyFlag()
|
||||
{
|
||||
return !mMessage.get( sEmergencyFlag );
|
||||
}
|
||||
|
||||
public boolean hasLoneWorkerFlag()
|
||||
{
|
||||
return !mMessage.get( sLoneWorkerFlag );
|
||||
}
|
||||
|
||||
public boolean hasPagingFlag()
|
||||
{
|
||||
return !mMessage.get( sPagingFlag );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normal Flags - 1 = flag is true
|
||||
* @return
|
||||
*/
|
||||
public boolean hasANIFlag()
|
||||
{
|
||||
return mMessage.get( sANIFlag );
|
||||
}
|
||||
|
||||
public boolean hasAcknowledgeFlag()
|
||||
{
|
||||
return mMessage.get( sAcknowledgeFlag );
|
||||
}
|
||||
|
||||
public boolean hasFleetExtensionFlag()
|
||||
{
|
||||
return mMessage.get( sFleetExtensionFlag );
|
||||
}
|
||||
|
||||
public boolean hasGPSFlag()
|
||||
{
|
||||
return mMessage.get( sGPSExtensionFlag );
|
||||
}
|
||||
|
||||
public boolean hasStatusFlag()
|
||||
{
|
||||
return mMessage.get( sStatusFlag );
|
||||
}
|
||||
|
||||
|
||||
public int getFleetTo()
|
||||
{
|
||||
if( hasFleetExtensionFlag() )
|
||||
{
|
||||
return getInt( sFleetTo ) + 99;
|
||||
}
|
||||
else
|
||||
{
|
||||
return getInt( sFleetFrom ) + 99;
|
||||
}
|
||||
}
|
||||
|
||||
public int getIdentifierTo()
|
||||
{
|
||||
return getInt( sIdentTo ) + 999;
|
||||
}
|
||||
|
||||
/**
|
||||
* GPS Heading
|
||||
* @return
|
||||
*/
|
||||
public double getHeading()
|
||||
{
|
||||
double retVal = 0.0;
|
||||
|
||||
int heading = getInt( sGPSHeading );
|
||||
|
||||
if( heading != 4095 )
|
||||
{
|
||||
retVal = (double)(heading / 10.0 );
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public int getBlock7WhatIsIt()
|
||||
{
|
||||
int value = getInt( sBLK7WhatIsIt );
|
||||
|
||||
if( value == 65535 )
|
||||
{
|
||||
value = -1;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speed in Kph - whole numbers and three decimal digits of precision
|
||||
* @return
|
||||
*/
|
||||
public double getSpeed()
|
||||
{
|
||||
double retVal = 0.0;
|
||||
|
||||
int temp = getInt( sSpeed );
|
||||
|
||||
if( temp != 0 )
|
||||
{
|
||||
retVal = (double)temp / 1000.0D;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public double getLatitude()
|
||||
{
|
||||
//TODO: determine the correct hemisphere indicator and replace this
|
||||
//hardcoded "0" with the correct hemisphere value
|
||||
|
||||
return convertDDMToDD( 0,
|
||||
getInt( sLatitudeDegreesMinutes ),
|
||||
getInt( sLatitudeDecimalMinutes ) );
|
||||
}
|
||||
|
||||
public double getLongitude()
|
||||
{
|
||||
//TODO: determine the correct hemisphere indicator and replace this
|
||||
//hardcoded "1" with the correct hemisphere value
|
||||
|
||||
return convertDDMToDD( 1,
|
||||
getInt( sLongitudeDegreesMinutes ),
|
||||
getInt( sLongitudeDecimalMinutes ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Degrees Decimal Minutes to Decimal Degrees
|
||||
*
|
||||
* Latitude and Longitude values are represented by:
|
||||
*
|
||||
* @param hemisphere - 0=North & East, 1=South & West
|
||||
* @param degreesMinutes - an integer value with the first 2-3 digits representing
|
||||
* the degrees and the last two digits representing the minutes
|
||||
*
|
||||
* @param decimalDegrees - an integer value representing the fractional
|
||||
* minutes
|
||||
*
|
||||
* @return - decimal degrees formatted value
|
||||
*/
|
||||
public double convertDDMToDD( int hemisphere, int degreesMinutes, int decimalDegrees )
|
||||
{
|
||||
double retVal = 0.0;
|
||||
|
||||
if( degreesMinutes != 0 )
|
||||
{
|
||||
//Degrees - divide value by 100 and retain the whole number value (ie degrees)
|
||||
retVal += (double)( degreesMinutes / 100 );
|
||||
|
||||
//Minutes - modulus by 100 to get the whole minutes value
|
||||
int wholeMinutes = degreesMinutes % 100;
|
||||
|
||||
if( wholeMinutes != 0 )
|
||||
{
|
||||
retVal += (double)( wholeMinutes / 60.0D );
|
||||
}
|
||||
}
|
||||
|
||||
if( decimalDegrees != 0 )
|
||||
{
|
||||
//Fractional Minutes - divide by 10,000 to get the decimal place correct
|
||||
//then divide by 60 (minutes) to get the decimal value
|
||||
//10,000 * 60 = 600,000
|
||||
retVal += (double)( decimalDegrees / 600000.0D );
|
||||
}
|
||||
|
||||
//Adjust the value +/- for the hemisphere
|
||||
if( hemisphere == 1 ) //South and West values
|
||||
{
|
||||
retVal = -retVal;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public String getGPSTime()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append( format( getInt( sGPSHours ), 2 ) );
|
||||
sb.append( ":" );
|
||||
sb.append( format( getInt( sGPSMinutes ), 2 ) );
|
||||
sb.append( ":" );
|
||||
sb.append( format( getInt( sGPSSeconds ), 2 ) );
|
||||
sb.append( "z" );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 7 bit checksum that is part of the GPGGA message, I think
|
||||
* @return
|
||||
*/
|
||||
public int getGPSChecksum()
|
||||
{
|
||||
return getInt( sGPSChecksum );
|
||||
}
|
||||
|
||||
public String getGPSChecksumString()
|
||||
{
|
||||
String sum = Integer.toHexString( getGPSChecksum() );
|
||||
|
||||
if( sum.length() == 1 )
|
||||
{
|
||||
return "*0" + sum;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "*" + sum;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 1-based calendar day of month
|
||||
* 1 = 1st day of month
|
||||
*/
|
||||
public int getGPSDay()
|
||||
{
|
||||
return getInt( sGPSDay ) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 0-based GPS Month
|
||||
* 0 = January
|
||||
*
|
||||
* Note: actual day of month value is 1-based, so we subtract 1 to get
|
||||
* the actual day of month value
|
||||
*/
|
||||
public int getGPSMonth()
|
||||
{
|
||||
return getInt( sGPSMonth );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns year, which is a combination of the century bit field + 1, times
|
||||
* 100, plus the year bit field
|
||||
*/
|
||||
public int getGPSYear()
|
||||
{
|
||||
// return ( ( getInt( sGPSCentury ) + 1 ) * 100 ) + getInt( sGPSYear );
|
||||
return ( 2000 ) + getInt( sGPSYear );
|
||||
}
|
||||
|
||||
public String getGPSLocation()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append( pad( String.valueOf( mDecimalFormatter.format( getLatitude() ) ), 8, "0" ) + " " );
|
||||
sb.append( pad( String.valueOf( mDecimalFormatter.format( getLongitude() ) ), 10, "0" ) );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
public String getGPSDate()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( getGPSYear() + "-" );
|
||||
|
||||
int month = getGPSMonth();
|
||||
|
||||
if( month < 10 )
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
|
||||
sb.append( month + "-" );
|
||||
|
||||
int day = getGPSDay();
|
||||
|
||||
if( day < 10 )
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
|
||||
sb.append( day );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private int getInt( int[] bits )
|
||||
{
|
||||
int retVal = 0;
|
||||
|
||||
for( int x = 0; x < bits.length; x++ )
|
||||
{
|
||||
if( mMessage.get( bits[ x ] ) )
|
||||
{
|
||||
retVal += 1<<x;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBinaryMessage()
|
||||
{
|
||||
return mMessage.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* String representing results of the parity check
|
||||
*
|
||||
* [P] = passes parity check
|
||||
* [f] = fails parity check
|
||||
* [C] = corrected message
|
||||
* [-] = message section not present
|
||||
*/
|
||||
public String getParity()
|
||||
{
|
||||
return "[" + CRC.format( mCRC ) + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol()
|
||||
{
|
||||
return "Fleetsync II";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType()
|
||||
{
|
||||
return getMessageType().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( getFleetFrom() );
|
||||
sb.append( "-" );
|
||||
sb.append( getIdentifierFrom() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getFleetsyncAlias( getFromID() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToID()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( getFleetTo() );
|
||||
sb.append( "-" );
|
||||
sb.append( getIdentifierTo() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getFleetsyncAlias( getToID() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getFromTo( boolean includeFrom, boolean includeTo )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if( includeFrom )
|
||||
{
|
||||
sb.append( "FM:" );
|
||||
sb.append( getFromID() );
|
||||
|
||||
Alias from = getFromIDAlias();
|
||||
|
||||
if( from != null )
|
||||
{
|
||||
sb.append( " " );
|
||||
sb.append( from.getName() );
|
||||
}
|
||||
}
|
||||
|
||||
if( includeFrom && includeTo )
|
||||
{
|
||||
sb.append( " " );
|
||||
}
|
||||
|
||||
if( includeTo )
|
||||
{
|
||||
sb.append( "TO:" );
|
||||
sb.append( getToID() );
|
||||
|
||||
Alias to = getToIDAlias();
|
||||
|
||||
if( to != null )
|
||||
{
|
||||
sb.append( " " );
|
||||
sb.append( to.getName() );
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
FleetsyncMessageType type = getMessageType();
|
||||
|
||||
switch( type )
|
||||
{
|
||||
case ACKNOWLEDGE:
|
||||
sb.append( "ACKNOWLEDGE " );
|
||||
sb.append( getFromTo( true, true ) );
|
||||
break;
|
||||
case ANI:
|
||||
sb.append( "ANI " );
|
||||
sb.append( getFromTo( true, false ) );
|
||||
break;
|
||||
case EMERGENCY:
|
||||
sb.append( "**EMERGENCY** " );
|
||||
sb.append( getFromTo( true, true ) );
|
||||
break;
|
||||
case GPS:
|
||||
sb.append( "GPS [" );
|
||||
sb.append( getGPSLocation() );
|
||||
sb.append( "] " );
|
||||
|
||||
sb.append( getFromTo( true, true ) );
|
||||
|
||||
sb.append( " HDG:" );
|
||||
sb.append( pad( String.valueOf( getHeading() ), 5, " " ) );
|
||||
|
||||
sb.append( " GPSDate[" );
|
||||
sb.append( getGPSDate() );
|
||||
sb.append( " " );
|
||||
sb.append( getGPSTime() );
|
||||
sb.append( "]" );
|
||||
break;
|
||||
case LONE_WORKER_EMERGENCY:
|
||||
sb.append( "**LONE WORKER EMERGENCY** " );
|
||||
sb.append( getFromTo( true, true ) );
|
||||
break;
|
||||
case PAGING:
|
||||
sb.append( "PAGING " );
|
||||
sb.append( getFromTo( true, true ) );
|
||||
break;
|
||||
case STATUS:
|
||||
sb.append( "STATUS: " + format( getStatus(), 2 ) );
|
||||
|
||||
Alias status = getStatusAlias();
|
||||
if( status != null )
|
||||
{
|
||||
sb.append( "/" );
|
||||
sb.append( status.getName() );
|
||||
}
|
||||
|
||||
sb.append( getFromTo( true, true ) );
|
||||
break;
|
||||
case UNKNOWN:
|
||||
default:
|
||||
sb.append( "*** UNKNOWN *** " );
|
||||
sb.append( getFromTo( true, true ) );
|
||||
break;
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorStatus()
|
||||
{
|
||||
return CRC.format( mCRC );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plottable getPlottable()
|
||||
{
|
||||
if( isValid() && getMessageType() == FleetsyncMessageType.GPS )
|
||||
{
|
||||
GeoPosition position =
|
||||
new GeoPosition( getLatitude(), getLongitude() );
|
||||
|
||||
Alias alias = mAliasList.getFleetsyncAlias( getFromID() );
|
||||
|
||||
return new Plottable( mTimeReceived, position, getFromID(), alias );
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a listing of aliases contained in the message.
|
||||
*/
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<Alias>();
|
||||
|
||||
Alias from = getFromIDAlias();
|
||||
|
||||
if( from != null )
|
||||
{
|
||||
aliases.add( from );
|
||||
}
|
||||
|
||||
Alias to = getToIDAlias();
|
||||
|
||||
if( to != null )
|
||||
{
|
||||
aliases.add( to );
|
||||
}
|
||||
|
||||
Alias status = getStatusAlias();
|
||||
|
||||
if( status != null )
|
||||
{
|
||||
aliases.add( status );
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package io.github.dsheirer.module.decode.fleetsync2;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class FleetsyncMessageFilter extends Filter<Message>
|
||||
{
|
||||
private HashMap<FleetsyncMessageType, FilterElement<FleetsyncMessageType>> mElements =
|
||||
new HashMap<FleetsyncMessageType, FilterElement<FleetsyncMessageType>>();
|
||||
|
||||
public FleetsyncMessageFilter()
|
||||
{
|
||||
super( "Fleetsync Message Filter" );
|
||||
|
||||
for( FleetsyncMessageType type: FleetsyncMessageType.values() )
|
||||
{
|
||||
if( type != FleetsyncMessageType.UNKNOWN )
|
||||
{
|
||||
mElements.put( type, new FilterElement<FleetsyncMessageType>( type ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes( Message message )
|
||||
{
|
||||
if( mEnabled && canProcess( message ) )
|
||||
{
|
||||
FleetsyncMessage fleet = (FleetsyncMessage)message;
|
||||
|
||||
if( mElements.containsKey( fleet.getMessageType() ) )
|
||||
{
|
||||
return mElements.get( fleet.getMessageType() ).isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess( Message message )
|
||||
{
|
||||
return message instanceof FleetsyncMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return new ArrayList<FilterElement<?>>( mElements.values() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.fleetsync2;
|
||||
|
||||
public enum FleetsyncMessageType
|
||||
{
|
||||
ACKNOWLEDGE( "ACK" ),
|
||||
ANI( "ANI" ),
|
||||
EMERGENCY( "EMERG" ),
|
||||
GPS( "GPS" ),
|
||||
LONE_WORKER_EMERGENCY( "LONE WORKER" ),
|
||||
PAGING( "PAGE" ),
|
||||
STATUS( "STATUS" ),
|
||||
UNKNOWN( "UNK" );
|
||||
|
||||
private String mLabel;
|
||||
|
||||
private FleetsyncMessageType( String label )
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.lj1200;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
|
||||
public class LJ1200CallEvent extends CallEvent
|
||||
{
|
||||
public LJ1200CallEvent( CallEventType callEventType,
|
||||
AliasList aliasList,
|
||||
String fromID,
|
||||
String toID,
|
||||
String details )
|
||||
{
|
||||
super( DecoderType.LJ_1200, callEventType,
|
||||
aliasList, fromID, toID, details );
|
||||
}
|
||||
|
||||
private Alias getAlias( String ident )
|
||||
{
|
||||
if( hasAliasList() )
|
||||
{
|
||||
return getAliasList().getFleetsyncAlias( ident );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return getAlias( getFromID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return getAlias( getToID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannel()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrequency()
|
||||
{
|
||||
return 173075000;
|
||||
}
|
||||
|
||||
public static class Builder
|
||||
{
|
||||
/* Required parameters */
|
||||
private CallEventType mCallEventType;
|
||||
|
||||
/* Optional parameters */
|
||||
private AliasList mAliasList;
|
||||
private String mFromID;
|
||||
private String mToID;
|
||||
private String mDetails;
|
||||
|
||||
public Builder( CallEventType callEventType )
|
||||
{
|
||||
mCallEventType = callEventType;
|
||||
}
|
||||
|
||||
public Builder aliasList( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder from( String val )
|
||||
{
|
||||
mFromID = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder details( String details )
|
||||
{
|
||||
mDetails = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder to( String toID )
|
||||
{
|
||||
mToID = toID;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LJ1200CallEvent build()
|
||||
{
|
||||
return new LJ1200CallEvent( this );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor for the builder
|
||||
*/
|
||||
private LJ1200CallEvent( Builder builder )
|
||||
{
|
||||
this( builder.mCallEventType,
|
||||
builder.mAliasList,
|
||||
builder.mFromID,
|
||||
builder.mToID,
|
||||
builder.mDetails );
|
||||
}
|
||||
|
||||
public static LJ1200CallEvent getLJ1200Event( LJ1200Message message )
|
||||
{
|
||||
CallEventType type = CallEventType.COMMAND;
|
||||
|
||||
return new LJ1200CallEvent.Builder( type )
|
||||
.to( message.getAddress() )
|
||||
.details( message.getMessage() )
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.lj1200;
|
||||
|
||||
import io.github.dsheirer.bits.MessageFramer;
|
||||
import io.github.dsheirer.bits.SyncPattern;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.instrument.tap.stream.BinaryTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatBufferTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatTap;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.fsk.FSK2Decoder;
|
||||
import io.github.dsheirer.dsp.fsk.FSK2Decoder.Output;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* LJ1200 - 1200 baud 2FSK decoder
|
||||
*/
|
||||
public class LJ1200Decoder extends Decoder implements IFilteredRealBufferListener,
|
||||
Instrumentable
|
||||
{
|
||||
/* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */
|
||||
private static final int DECIMATED_SAMPLE_RATE = 24000;
|
||||
|
||||
/* Baud or Symbol Rate */
|
||||
private static final int SYMBOL_RATE = 1200;
|
||||
|
||||
/* Message length - 16-bit sync plus 64 bit message */
|
||||
private static final int MESSAGE_LENGTH = 80;
|
||||
|
||||
/* Instrumentation Taps */
|
||||
private ArrayList<TapGroup> mAvailableTaps;
|
||||
private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD =
|
||||
"Tap Point: Bandpass Filter > < FSK2 Decoder";
|
||||
private static final String INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER =
|
||||
"Tap Point: FSK2 Decoder > < Message Framer";
|
||||
|
||||
private FSK2Decoder mFSKDecoder;
|
||||
private Broadcaster<Boolean> mFSKDecoderBroadcaster =
|
||||
new Broadcaster<Boolean>();
|
||||
|
||||
private HalfBandFilter_RB_RB mDecimationFilter;
|
||||
private RealFIRFilter_RB_RB mBandPassFilter;
|
||||
private MessageFramer mTowerMessageFramer;
|
||||
private MessageFramer mTransponderMessageFramer;
|
||||
private LJ1200MessageProcessor mMessageProcessor;
|
||||
|
||||
public LJ1200Decoder( AliasList aliasList )
|
||||
{
|
||||
mDecimationFilter = new HalfBandFilter_RB_RB(
|
||||
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true );
|
||||
|
||||
mBandPassFilter = new RealFIRFilter_RB_RB(
|
||||
Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f );
|
||||
mDecimationFilter.setListener( mBandPassFilter );
|
||||
|
||||
mFSKDecoder = new FSK2Decoder( DECIMATED_SAMPLE_RATE,
|
||||
SYMBOL_RATE, Output.INVERTED );
|
||||
mBandPassFilter.setListener( mFSKDecoder );
|
||||
|
||||
mFSKDecoder.setListener( mFSKDecoderBroadcaster );
|
||||
|
||||
mTowerMessageFramer = new MessageFramer(
|
||||
SyncPattern.LJ1200.getPattern(), MESSAGE_LENGTH );
|
||||
mTransponderMessageFramer = new MessageFramer(
|
||||
SyncPattern.LJ1200_TRANSPONDER.getPattern(), MESSAGE_LENGTH );
|
||||
mFSKDecoderBroadcaster.addListener( mTowerMessageFramer );
|
||||
mFSKDecoderBroadcaster.addListener( mTransponderMessageFramer );
|
||||
|
||||
mMessageProcessor = new LJ1200MessageProcessor( aliasList );
|
||||
mTowerMessageFramer.addMessageListener( mMessageProcessor );
|
||||
mTransponderMessageFramer.addMessageListener( mMessageProcessor );
|
||||
mMessageProcessor.setMessageListener( this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.LJ_1200;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mDecimationFilter.dispose();
|
||||
mBandPassFilter.dispose();
|
||||
mFSKDecoder.dispose();
|
||||
mTowerMessageFramer.dispose();
|
||||
mMessageProcessor.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a float listener interface for connecting this decoder to a
|
||||
* float stream provider
|
||||
*/
|
||||
public RealSampleListener getRealReceiver()
|
||||
{
|
||||
return (RealSampleListener)mDecimationFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<TapGroup>();
|
||||
|
||||
TapGroup group = new TapGroup( "LJ-1200 Decoder" );
|
||||
|
||||
group.add( new FloatTap(
|
||||
INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) );
|
||||
group.add( new BinaryTap(
|
||||
INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) );
|
||||
|
||||
mAvailableTaps.add( group );
|
||||
mAvailableTaps.addAll( mFSKDecoder.getTapGroups() );
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
{
|
||||
mFSKDecoder.registerTap( tap );
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD:
|
||||
FloatBufferTap bpTap = (FloatBufferTap)tap;
|
||||
mBandPassFilter.setListener( bpTap );
|
||||
bpTap.setListener( mFSKDecoder );
|
||||
break;
|
||||
case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER:
|
||||
BinaryTap decoderTap = (BinaryTap)tap;
|
||||
mFSKDecoder.setListener( decoderTap );
|
||||
decoderTap.setListener( mTowerMessageFramer );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
{
|
||||
mFSKDecoder.unregisterTap( tap );
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD:
|
||||
mBandPassFilter.setListener( mFSKDecoder );
|
||||
break;
|
||||
case INSTRUMENT_FSK2_DECODER_TO_MESSAGE_FRAMER:
|
||||
mFSKDecoder.setListener( mTowerMessageFramer );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getFilteredRealBufferListener()
|
||||
{
|
||||
return mDecimationFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.lj1200;
|
||||
|
||||
import io.github.dsheirer.channel.metadata.AliasedStringAttributeMonitor;
|
||||
import io.github.dsheirer.channel.metadata.Attribute;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class LJ1200DecoderState extends DecoderState
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(LJ1200DecoderState.class);
|
||||
|
||||
private Set<String> mAddresses = new TreeSet<String>();
|
||||
|
||||
private AliasedStringAttributeMonitor mAddressAttribute;
|
||||
|
||||
public LJ1200DecoderState(AliasList aliasList)
|
||||
{
|
||||
super(aliasList);
|
||||
|
||||
mAddressAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_TO,
|
||||
getAttributeChangeRequestListener(), getAliasList(), AliasIDType.SITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.LJ_1200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Message message)
|
||||
{
|
||||
if(message instanceof LJ1200Message)
|
||||
{
|
||||
LJ1200Message lj = (LJ1200Message) message;
|
||||
|
||||
if(lj.isValid())
|
||||
{
|
||||
String address = lj.getAddress();
|
||||
|
||||
mAddressAttribute.process(address);
|
||||
mAddresses.add(address);
|
||||
|
||||
broadcast(LJ1200CallEvent.getLJ1200Event(lj));
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA));
|
||||
}
|
||||
}
|
||||
else if(message instanceof LJ1200TransponderMessage)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("=============================\n");
|
||||
sb.append("Decoder:\tLJ-1200I\n\n");
|
||||
|
||||
if(!mAddresses.isEmpty())
|
||||
{
|
||||
sb.append("Transponder Addresses:\n");
|
||||
|
||||
List<String> addresses = new ArrayList<String>(mAddresses);
|
||||
|
||||
Collections.sort(addresses);
|
||||
|
||||
for(String address : addresses)
|
||||
{
|
||||
sb.append("\t");
|
||||
sb.append(address);
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
/* No initialization steps */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
if(event.getEvent() == Event.RESET)
|
||||
{
|
||||
resetState();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetState()
|
||||
{
|
||||
mAddressAttribute.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mAddresses.clear();
|
||||
resetState();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.lj1200;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.edac.CRCLJ;
|
||||
import io.github.dsheirer.map.Plottable;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LJ1200Message extends Message
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( LJ1200Message.class );
|
||||
|
||||
public static final String[] REPLY_CODE = { "0","1","2","3","4","5","6","7",
|
||||
"8","9","A","C","D","E","F","G","H","J","K","L","M","N","P","Q","R","S",
|
||||
"T","U","V","W","X","Y" };
|
||||
|
||||
public static int[] SYNC = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 };
|
||||
|
||||
public static int[] VRC = { 23,22,21,20,19,18,17,16 };
|
||||
|
||||
public static int[] LRC = { 31,30,29,28,27,26,25,24 };
|
||||
|
||||
public static int[] FUNCTION = { 35,34,33,32 };
|
||||
|
||||
public static int[] ADDRESS = { 63,62,61,60,59,58,57,56,55,54,53,52,51,50,
|
||||
49,48,47,46,45,44,43,42,41,40,39,38,37,36 };
|
||||
|
||||
public static int[] REPLY_1 = { 39,38,37,36,43 };
|
||||
public static int[] REPLY_2 = { 42,41,40,47,46 };
|
||||
public static int[] REPLY_3 = { 45,44,51,50,49 };
|
||||
public static int[] REPLY_4 = { 48,55,54,53,52 };
|
||||
public static int[] REPLY_5 = { 59,58,57,56,63 };
|
||||
|
||||
public static int[] MESSAGE_CRC = { 79,78,77,76,75,74,73,72,71,70,69,68,67,
|
||||
66,65,64 };
|
||||
|
||||
private BinaryMessage mMessage;
|
||||
private AliasList mAliasList;
|
||||
private CRC mCRC;
|
||||
|
||||
public LJ1200Message( BinaryMessage message, AliasList list )
|
||||
{
|
||||
mMessage = message;
|
||||
mAliasList = list;
|
||||
|
||||
checkCRC();
|
||||
|
||||
switch( mCRC )
|
||||
{
|
||||
case CORRECTED:
|
||||
mLog.debug( "CORR:" + message.toString() );
|
||||
break;
|
||||
case FAILED_CRC:
|
||||
mLog.debug( "FAIL:" + message.toString() );
|
||||
break;
|
||||
case PASSED:
|
||||
mLog.debug( "PASS:" + message.toString() );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkCRC()
|
||||
{
|
||||
mCRC = CRCLJ.checkAndCorrect( mMessage );
|
||||
}
|
||||
|
||||
public boolean isValid()
|
||||
{
|
||||
return mCRC == CRC.PASSED || mCRC == CRC.CORRECTED;
|
||||
}
|
||||
|
||||
public String getVRC()
|
||||
{
|
||||
return mMessage.getHex( VRC, 2 );
|
||||
}
|
||||
|
||||
public String getLRC()
|
||||
{
|
||||
return mMessage.getHex( LRC, 2 );
|
||||
}
|
||||
|
||||
public String getCRC()
|
||||
{
|
||||
return mMessage.getHex( MESSAGE_CRC, 4 );
|
||||
}
|
||||
|
||||
public Function getFunction()
|
||||
{
|
||||
return Function.fromValue( mMessage.getInt( FUNCTION ),
|
||||
mMessage.getInt( REPLY_3 ) );
|
||||
}
|
||||
|
||||
public String getAddress()
|
||||
{
|
||||
return mMessage.getHex( ADDRESS, 7 );
|
||||
}
|
||||
|
||||
public Alias getTransponderAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getESNAlias( getAddress() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Alias getFunctionAndIDAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getLoJackAlias( getFunction(), getReplyCode() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 5 character reply code for function E and F, transponder activation.
|
||||
* @return - reply code
|
||||
*/
|
||||
public String getReplyCode()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( REPLY_CODE[ mMessage.getInt( REPLY_1 ) ] );
|
||||
sb.append( REPLY_CODE[ mMessage.getInt( REPLY_2 ) ] );
|
||||
sb.append( REPLY_CODE[ mMessage.getInt( REPLY_3 ) ] );
|
||||
sb.append( REPLY_CODE[ mMessage.getInt( REPLY_4 ) ] );
|
||||
sb.append( REPLY_CODE[ mMessage.getInt( REPLY_5 ) ] );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
Function function = getFunction();
|
||||
|
||||
sb.append( "FUNCTION: " );
|
||||
sb.append( function.toString() );
|
||||
|
||||
switch( function )
|
||||
{
|
||||
case F1_SITE_ID:
|
||||
sb.append( " SITE [" );
|
||||
break;
|
||||
case F1_SPEED_UP:
|
||||
case F2_TEST:
|
||||
case F3_DEACTIVATE:
|
||||
case F4_ACTIVATE:
|
||||
case FF_TRACK_PULSE:
|
||||
default:
|
||||
sb.append( " REPLY CODE [" );
|
||||
break;
|
||||
}
|
||||
|
||||
sb.append( getReplyCode() );
|
||||
|
||||
Alias site = getFunctionAndIDAlias();
|
||||
|
||||
if( site != null )
|
||||
{
|
||||
sb.append( "/" );
|
||||
sb.append( site.getName() );
|
||||
}
|
||||
|
||||
sb.append( "] ADDRESS [" );
|
||||
sb.append( getAddress() );
|
||||
sb.append( "] VRC [" + getVRC() );
|
||||
sb.append( "] LRC [" + getLRC() );
|
||||
sb.append( "] CRC [" + getCRC() );
|
||||
sb.append( "]" );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads spaces onto the end of the value to make it 'places' long
|
||||
*/
|
||||
public String pad( String value, int places, String padCharacter )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( value );
|
||||
|
||||
while( sb.length() < places )
|
||||
{
|
||||
sb.append( padCharacter );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads an integer value with additional zeroes to make it decimalPlaces long
|
||||
*/
|
||||
public String format( int number, int decimalPlaces )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() );
|
||||
|
||||
for( int x = 0; x < paddingRequired; x++)
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
|
||||
sb.append( number );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBinaryMessage()
|
||||
{
|
||||
return mMessage.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol()
|
||||
{
|
||||
return "LJ-1200";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType()
|
||||
{
|
||||
if( getFunction() == Function.F1_SPEED_UP )
|
||||
{
|
||||
return "TRANSPONDER";
|
||||
}
|
||||
|
||||
return "TOWER";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
return getReplyCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return getFunctionAndIDAlias();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToID()
|
||||
{
|
||||
return getAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
return toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorStatus()
|
||||
{
|
||||
return mCRC.getDisplayText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plottable getPlottable()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public enum Function
|
||||
{
|
||||
/* Big Endian Format */
|
||||
F0_UNKNOWN( "0-UNKNOWN" ),
|
||||
F1_SITE_ID( "1Y-SITE ID" ),
|
||||
F1_SPEED_UP( "1-SPEED UP" ),
|
||||
F2_TEST( "2-TOWER TEST" ),
|
||||
F3_DEACTIVATE( "3-DEACTIVATE" ),
|
||||
F4_ACTIVATE( "4-ACTIVATE" ),
|
||||
F5_TRANSPONDER_TEST_REPLY( "5-TRANSPONDER TEST REPLY" ),
|
||||
F6_TRANSPONDER_TEST_COMMAND( "6-TRANSPONDER TEST COMMAND" ),
|
||||
F7_QUIET_COMMAND( "7-QUIET COMMAND" ),
|
||||
F8_UNKNOWN( "8-UNKNOWN" ),
|
||||
F9_UNKNOWN( "9-UNKNOWN" ),
|
||||
FA_UNKNOWN( "A-UNKNOWN" ),
|
||||
FB_UNKNOWN( "B-UNKNOWN" ),
|
||||
FC_ALT_DEACTIVATE( "C-ALT-DEACTIVATE" ),
|
||||
FD_UNKNOWN( "D-UNKNOWN" ),
|
||||
FE_UNKNOWN( "E-UNKNOWN" ),
|
||||
FF_TRACK_PULSE( "F-TRACK PULSE" ),
|
||||
|
||||
UNKNOWN( "UNKNOWN" );
|
||||
|
||||
private String mLabel;
|
||||
|
||||
private Function( String label )
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return getLabel();
|
||||
}
|
||||
|
||||
public static Function fromValue( int value, int replyCodeDigit3 )
|
||||
{
|
||||
switch( value )
|
||||
{
|
||||
case 0:
|
||||
return Function.F0_UNKNOWN;
|
||||
case 1:
|
||||
if( replyCodeDigit3 == 31 ) /* 'Y' middle character */
|
||||
{
|
||||
return Function.F1_SITE_ID;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Function.F1_SPEED_UP;
|
||||
}
|
||||
case 2:
|
||||
return Function.F2_TEST;
|
||||
case 3:
|
||||
return Function.F3_DEACTIVATE;
|
||||
case 4:
|
||||
return Function.F4_ACTIVATE;
|
||||
case 5:
|
||||
return Function.F5_TRANSPONDER_TEST_REPLY;
|
||||
case 6:
|
||||
return Function.F6_TRANSPONDER_TEST_COMMAND;
|
||||
case 7:
|
||||
return Function.F7_QUIET_COMMAND;
|
||||
case 8:
|
||||
return Function.F8_UNKNOWN;
|
||||
case 9:
|
||||
return Function.F9_UNKNOWN;
|
||||
case 10:
|
||||
return Function.FA_UNKNOWN;
|
||||
case 11:
|
||||
return Function.FB_UNKNOWN;
|
||||
case 12:
|
||||
return Function.FC_ALT_DEACTIVATE;
|
||||
case 13:
|
||||
return Function.FD_UNKNOWN;
|
||||
case 14:
|
||||
return Function.FE_UNKNOWN;
|
||||
case 15:
|
||||
return Function.FF_TRACK_PULSE;
|
||||
default:
|
||||
return Function.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a listing of aliases contained in the message.
|
||||
*/
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<Alias>();
|
||||
|
||||
Alias functionAndID = getFunctionAndIDAlias();
|
||||
|
||||
if( functionAndID != null )
|
||||
{
|
||||
aliases.add( functionAndID );
|
||||
}
|
||||
|
||||
Alias transponder = getTransponderAlias();
|
||||
|
||||
if( transponder != null )
|
||||
{
|
||||
aliases.add( transponder );
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.lj1200;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.Message;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class LJ1200MessageFilter extends Filter<Message>
|
||||
{
|
||||
|
||||
public LJ1200MessageFilter()
|
||||
{
|
||||
super( "LJ-1200 Message Filter" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes( Message message )
|
||||
{
|
||||
return mEnabled && canProcess( message );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess( Message message )
|
||||
{
|
||||
return message instanceof LJ1200Message ||
|
||||
message instanceof LJ1200TransponderMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.lj1200;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
|
||||
public class LJ1200MessageProcessor implements Listener<BinaryMessage>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( LJ1200MessageProcessor.class );
|
||||
|
||||
public static int[] SYNC = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 };
|
||||
|
||||
public static int SYNC_TOWER = 0x550F;
|
||||
public static int SYNC_TRANSPONDER = 0x2AD5;
|
||||
|
||||
private Listener<Message> mMessageListener;
|
||||
|
||||
private AliasList mAliasList;
|
||||
|
||||
public LJ1200MessageProcessor( AliasList list )
|
||||
{
|
||||
mAliasList = list;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mAliasList = null;
|
||||
mMessageListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( BinaryMessage buffer )
|
||||
{
|
||||
int sync = buffer.getInt( SYNC );
|
||||
|
||||
if( sync == SYNC_TOWER )
|
||||
{
|
||||
if( mMessageListener != null )
|
||||
{
|
||||
mMessageListener.receive( new LJ1200Message( buffer, mAliasList ) );
|
||||
}
|
||||
}
|
||||
else if( sync == SYNC_TRANSPONDER )
|
||||
{
|
||||
if( mMessageListener != null )
|
||||
{
|
||||
mMessageListener.receive( new LJ1200TransponderMessage( buffer, mAliasList ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.lj1200;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.map.Plottable;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
public class LJ1200TransponderMessage extends Message
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( LJ1200TransponderMessage.class );
|
||||
|
||||
public static int[] SYNC = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 };
|
||||
|
||||
public static int[] MESSAGE_CRC = { 64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79 };
|
||||
|
||||
private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd HHmmss" );
|
||||
|
||||
private BinaryMessage mMessage;
|
||||
private AliasList mAliasList;
|
||||
private CRC mCRC;
|
||||
|
||||
public LJ1200TransponderMessage( BinaryMessage message, AliasList list )
|
||||
{
|
||||
mMessage = message;
|
||||
mAliasList = list;
|
||||
|
||||
checkCRC();
|
||||
|
||||
switch( mCRC )
|
||||
{
|
||||
case CORRECTED:
|
||||
mLog.debug( "CORR:" + message.toString() );
|
||||
break;
|
||||
case FAILED_CRC:
|
||||
mLog.debug( "FAIL:" + message.toString() );
|
||||
break;
|
||||
case PASSED:
|
||||
mLog.debug( "PASS:" + message.toString() );
|
||||
break;
|
||||
case UNKNOWN:
|
||||
mLog.debug( "UNKNOWN:" + message.toString() );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkCRC()
|
||||
{
|
||||
mCRC = CRC.UNKNOWN;
|
||||
}
|
||||
|
||||
public boolean isValid()
|
||||
{
|
||||
// return mCRC == CRC.PASSED || mCRC == CRC.CORRECTED;
|
||||
|
||||
/* Override validity check until proper CRC is implemented */
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getCRC()
|
||||
{
|
||||
return mMessage.getHex( MESSAGE_CRC, 4 );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "TRANSPONDER TEST" );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads spaces onto the end of the value to make it 'places' long
|
||||
*/
|
||||
public String pad( String value, int places, String padCharacter )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( value );
|
||||
|
||||
while( sb.length() < places )
|
||||
{
|
||||
sb.append( padCharacter );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads an integer value with additional zeroes to make it decimalPlaces long
|
||||
*/
|
||||
public String format( int number, int decimalPlaces )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() );
|
||||
|
||||
for( int x = 0; x < paddingRequired; x++)
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
|
||||
sb.append( number );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBinaryMessage()
|
||||
{
|
||||
return mMessage.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol()
|
||||
{
|
||||
return "LJ-1200";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType()
|
||||
{
|
||||
return "TRANSPONDER";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToID()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
return toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorStatus()
|
||||
{
|
||||
return mCRC.getDisplayText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plottable getPlottable()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
public class DecodeConfigLTRNet extends DecodeConfiguration
|
||||
{
|
||||
private MessageDirection mMessageDirection = MessageDirection.OSW;
|
||||
|
||||
public DecodeConfigLTRNet()
|
||||
{
|
||||
super( DecoderType.LTR_NET );
|
||||
}
|
||||
|
||||
@XmlAttribute( name = "direction" )
|
||||
public MessageDirection getMessageDirection()
|
||||
{
|
||||
return mMessageDirection;
|
||||
}
|
||||
|
||||
public void setMessageDirection( MessageDirection direction )
|
||||
{
|
||||
mMessageDirection = direction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
|
||||
public class LTRCallEvent extends CallEvent
|
||||
{
|
||||
private String mChannel;
|
||||
private long mFrequency;
|
||||
|
||||
public LTRCallEvent( DecoderType type,
|
||||
CallEventType callEventType,
|
||||
AliasList aliasList,
|
||||
String fromID,
|
||||
String toID,
|
||||
String channel,
|
||||
long frequency,
|
||||
String details )
|
||||
{
|
||||
super( type, callEventType, aliasList, fromID, toID, details );
|
||||
|
||||
mChannel = channel;
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the message's talkgroup matches this call event. Counts the
|
||||
* number of messages that match this call event to use in determining if
|
||||
* a call event is valid or an isolated (errant) message.
|
||||
*
|
||||
* See: isValid()
|
||||
*/
|
||||
public boolean isMatchingTalkgroup( LTRNetMessage message )
|
||||
{
|
||||
String talkgroup = message.getTalkgroupID();
|
||||
|
||||
if( talkgroup != null && getToID() != null &&
|
||||
talkgroup.contentEquals( getToID() ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return isMatchingTalkgroup( message.getTalkgroupID() );
|
||||
}
|
||||
|
||||
public boolean isMatchingTalkgroup( String talkgroup )
|
||||
{
|
||||
return talkgroup != null && getToID() != null && talkgroup.contentEquals( getToID() );
|
||||
}
|
||||
|
||||
private Alias getAlias(String talkgroup )
|
||||
{
|
||||
if( hasAliasList() )
|
||||
{
|
||||
return getAliasList().getTalkgroupAlias( talkgroup );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
if( mCallEventType == CallEventType.CALL ||
|
||||
mCallEventType == CallEventType.REGISTER ||
|
||||
mCallEventType == CallEventType.ID_UNIQUE )
|
||||
{
|
||||
if( hasAliasList() )
|
||||
{
|
||||
try
|
||||
{
|
||||
int radioID = Integer.parseInt( getFromID() );
|
||||
|
||||
return mAliasList.getUniqueID( radioID );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
//Do nothing, we couldn't parse the int radio id value
|
||||
}
|
||||
}
|
||||
}
|
||||
else if( mCallEventType == CallEventType.REGISTER_ESN )
|
||||
{
|
||||
if( hasAliasList() )
|
||||
{
|
||||
return mAliasList.getESNAlias( getFromID() );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return getAlias( getFromID() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return getAlias( getToID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannel()
|
||||
{
|
||||
return mChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
public static class Builder
|
||||
{
|
||||
/* Required parameters */
|
||||
private DecoderType mDecoderType;
|
||||
private CallEventType mCallEventType;
|
||||
|
||||
/* Optional parameters */
|
||||
private AliasList mAliasList;
|
||||
private String mFromID;
|
||||
private String mToID;
|
||||
private String mDetails;
|
||||
private String mChannel;
|
||||
private long mFrequency;
|
||||
|
||||
public Builder( DecoderType decoderType, CallEventType callEventType )
|
||||
{
|
||||
mDecoderType = decoderType;
|
||||
mCallEventType = callEventType;
|
||||
}
|
||||
|
||||
public Builder aliasList( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder from( String val )
|
||||
{
|
||||
mFromID = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder channel( String channel )
|
||||
{
|
||||
mChannel = channel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder details( String details )
|
||||
{
|
||||
mDetails = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder frequency( long frequency )
|
||||
{
|
||||
mFrequency = frequency;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder to( String toID )
|
||||
{
|
||||
mToID = toID;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LTRCallEvent build()
|
||||
{
|
||||
return new LTRCallEvent( this );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor for the builder
|
||||
*/
|
||||
private LTRCallEvent( Builder builder )
|
||||
{
|
||||
this( builder.mDecoderType,
|
||||
builder.mCallEventType,
|
||||
builder.mAliasList,
|
||||
builder.mFromID,
|
||||
builder.mToID,
|
||||
builder.mChannel,
|
||||
builder.mFrequency,
|
||||
builder.mDetails );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.MessageFramer;
|
||||
import io.github.dsheirer.bits.SyncPattern;
|
||||
import io.github.dsheirer.dsp.fsk.LTRFSKDecoder;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class LTRNetDecoder extends Decoder
|
||||
implements IUnFilteredRealBufferListener, Instrumentable
|
||||
{
|
||||
public static final int LTR_STANDARD_MESSAGE_LENGTH = 40;
|
||||
private LTRFSKDecoder mLTRFSKDecoder;
|
||||
private MessageFramer mLTRMessageFramer;
|
||||
private LTRNetMessageProcessor mLTRMessageProcessor;
|
||||
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
|
||||
/**
|
||||
* LTR-Net Decoder. Decodes unfiltered (e.g. demodulated but with no DC or
|
||||
* audio filtering) samples and produces LTR-Net messages.
|
||||
*/
|
||||
public LTRNetDecoder( DecodeConfigLTRNet config, AliasList aliasList )
|
||||
{
|
||||
mLTRFSKDecoder = new LTRFSKDecoder();
|
||||
|
||||
|
||||
if( config.getMessageDirection() == MessageDirection.OSW )
|
||||
{
|
||||
mLTRMessageFramer =
|
||||
new MessageFramer( SyncPattern.LTR_STANDARD_OSW.getPattern(),
|
||||
LTR_STANDARD_MESSAGE_LENGTH );
|
||||
}
|
||||
else
|
||||
{
|
||||
mLTRMessageFramer =
|
||||
new MessageFramer( SyncPattern.LTR_STANDARD_ISW.getPattern(),
|
||||
LTR_STANDARD_MESSAGE_LENGTH );
|
||||
}
|
||||
|
||||
mLTRFSKDecoder.addListener( mLTRMessageFramer );
|
||||
|
||||
mLTRMessageProcessor = new LTRNetMessageProcessor(
|
||||
config.getMessageDirection(), aliasList );
|
||||
|
||||
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
|
||||
|
||||
mLTRMessageProcessor.setMessageListener( this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.LTR_NET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getUnFilteredRealBufferListener()
|
||||
{
|
||||
return mLTRFSKDecoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<TapGroup>();
|
||||
|
||||
mAvailableTaps.addAll( mLTRFSKDecoder.getTapGroups() );
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
{
|
||||
mLTRFSKDecoder.registerTap( tap );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
{
|
||||
mLTRFSKDecoder.unregisterTap( tap );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mLTRMessageFramer.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EditorValidationException;
|
||||
import io.github.dsheirer.gui.editor.ValidatingEditor;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class LTRNetDecoderEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JCheckBox mAFC;
|
||||
private JSlider mAFCMaximumCorrection;
|
||||
private JComboBox<MessageDirection> mComboDirection;
|
||||
|
||||
public LTRNetDecoderEditor()
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0,wrap 4", "[right][grow,fill][right][grow,fill]", "" ) );
|
||||
|
||||
mAFC = new JCheckBox( "AFC: 3000 Hz" );
|
||||
mAFC.setEnabled( false );
|
||||
mAFC.setToolTipText( "AFC automatically adjusts the center frequency of the channel to "
|
||||
+ "correct/compensate for inaccuracies and frequency drift in the tuner" );
|
||||
mAFC.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent arg0 )
|
||||
{
|
||||
setModified( true );
|
||||
|
||||
if( mAFC.isSelected() && !mAFCMaximumCorrection.isEnabled() )
|
||||
{
|
||||
mAFCMaximumCorrection.setEnabled( true );
|
||||
}
|
||||
else if( !mAFC.isSelected() && mAFCMaximumCorrection.isEnabled() )
|
||||
{
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
add( mAFC );
|
||||
|
||||
mAFCMaximumCorrection = new JSlider( 0, 7000, 3000 );
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
mAFCMaximumCorrection.setToolTipText( "Maximum AFC frequency correction (0 - 15kHz)" );
|
||||
mAFCMaximumCorrection.setMajorTickSpacing( 2000 );
|
||||
mAFCMaximumCorrection.setMinorTickSpacing( 1000 );
|
||||
mAFCMaximumCorrection.setPaintTicks( true );
|
||||
|
||||
mAFCMaximumCorrection.addChangeListener( new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged( ChangeEvent e )
|
||||
{
|
||||
mAFC.setText( "AFC: " + mAFCMaximumCorrection.getValue() + " Hz" );
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
add( mAFCMaximumCorrection );
|
||||
|
||||
mComboDirection = new JComboBox<MessageDirection>();
|
||||
mComboDirection.setModel( new DefaultComboBoxModel<MessageDirection>( MessageDirection.values() ) );
|
||||
mComboDirection.setSelectedItem( MessageDirection.OSW );
|
||||
mComboDirection.setEnabled( false );
|
||||
mComboDirection.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
|
||||
add( new JLabel( "Format:" ) );
|
||||
add( mComboDirection );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate( Editor<Channel> editor ) throws EditorValidationException
|
||||
{
|
||||
//No validation
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setItem( Channel item )
|
||||
{
|
||||
super.setItem( item );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
mAFC.setEnabled( true );
|
||||
mComboDirection.setEnabled( true );
|
||||
|
||||
DecodeConfiguration config = getItem().getDecodeConfiguration();
|
||||
|
||||
if( config instanceof DecodeConfigLTRNet )
|
||||
{
|
||||
DecodeConfigLTRNet ltr = (DecodeConfigLTRNet)config;
|
||||
mComboDirection.setSelectedItem( ltr.getMessageDirection() );
|
||||
mAFC.setSelected( ltr.isAFCEnabled() );
|
||||
mAFCMaximumCorrection.setValue( ltr.getAFCMaximumCorrection() );
|
||||
mAFCMaximumCorrection.setEnabled( ltr.isAFCEnabled() );
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComboDirection.setSelectedItem( MessageDirection.OSW );
|
||||
mAFC.setSelected( false );
|
||||
mAFCMaximumCorrection.setValue( DecodeConfiguration.DEFAULT_AFC_MAX_CORRECTION );
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
|
||||
setModified( true );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mAFC.setEnabled( false );
|
||||
mComboDirection.setEnabled( false );
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasItem() && isModified() )
|
||||
{
|
||||
DecodeConfigLTRNet ltr = new DecodeConfigLTRNet();
|
||||
|
||||
ltr.setAFC( mAFC.isSelected() );
|
||||
ltr.setAFCMaximumCorrection( mAFCMaximumCorrection.getValue() );
|
||||
ltr.setMessageDirection( (MessageDirection)mComboDirection.getSelectedItem() );
|
||||
|
||||
getItem().setDecodeConfiguration( ltr );
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.channel.metadata.AliasedIntegerAttributeMonitor;
|
||||
import io.github.dsheirer.channel.metadata.AliasedStringAttributeMonitor;
|
||||
import io.github.dsheirer.channel.metadata.Attribute;
|
||||
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
import io.github.dsheirer.util.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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() !=
|
||||
CallEvent.CallEventType.STATION_ID)
|
||||
{
|
||||
mCurrentCallEvent = new LTRCallEvent.Builder(
|
||||
DecoderType.LTR_NET, CallEvent.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, CallEvent.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, CallEvent.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, CallEvent.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, CallEvent.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, CallEvent.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() == CallEvent.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() == CallEvent.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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.edac.CRCLTR;
|
||||
import io.github.dsheirer.map.Plottable;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class LTRNetISWMessage extends LTRNetMessage
|
||||
{
|
||||
private static final int sCHIU_UNIQUE_ID = 24;
|
||||
private static final int sCHIU_ESN_LOW = 27;
|
||||
private static final int sCHIU_ESN_HIGH = 29;
|
||||
|
||||
private static final int sFREE_CALL = 21;
|
||||
private static final int sFREE_END_CALL = 23;
|
||||
private static final int sFREE_REQUEST_ACCESS = 31;
|
||||
|
||||
|
||||
private LTRNetISWMessage mAuxMessage;
|
||||
|
||||
public LTRNetISWMessage( BinaryMessage message, AliasList list )
|
||||
{
|
||||
super( message, MessageDirection.ISW, list );
|
||||
|
||||
/**
|
||||
* If the CRC fails, test for a transmitted CRC of 127 and then check
|
||||
* the Free field for special messages
|
||||
*/
|
||||
if( mCRC == CRC.FAILED_CRC && CRCLTR.getTransmittedChecksum( mMessage ) == 127 )
|
||||
{
|
||||
if( getFree() == sFREE_REQUEST_ACCESS ||
|
||||
getFree() == sFREE_END_CALL )
|
||||
{
|
||||
mCRC = CRC.PASSED;
|
||||
}
|
||||
}
|
||||
|
||||
mMessageType = getMessageType();
|
||||
}
|
||||
|
||||
public void setAuxiliaryMessage( LTRNetISWMessage message )
|
||||
{
|
||||
if( message != null )
|
||||
{
|
||||
mAuxMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
public String getESN()
|
||||
{
|
||||
if( mMessageType == MessageType.ID_ESNH )
|
||||
{
|
||||
if( mAuxMessage != null &&
|
||||
mAuxMessage.getMessageType() == MessageType.ID_ESNL )
|
||||
{
|
||||
return getESNHigh() + mAuxMessage.getESNLow();
|
||||
}
|
||||
else
|
||||
{
|
||||
return getESNHigh() + "xxxx";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( mAuxMessage != null &&
|
||||
mAuxMessage.getMessageType() == MessageType.ID_ESNH )
|
||||
{
|
||||
return mAuxMessage.getESNHigh() + getESNLow();
|
||||
}
|
||||
else
|
||||
{
|
||||
return "xxxx" + getESNLow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Alias getESNAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getESNAlias( getESN() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getESNHigh()
|
||||
{
|
||||
int esnHigh = getInt( sSIXTEEN_BITS );
|
||||
|
||||
return String.format("%04X", esnHigh & 0xFFFF );
|
||||
}
|
||||
|
||||
public String getESNLow()
|
||||
{
|
||||
int esnLow = getInt( sSIXTEEN_BITS );
|
||||
|
||||
return String.format("%04X", esnLow & 0xFFFF );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append( mDatestampFormatter.format( new Date( System.currentTimeMillis() ) ) );
|
||||
sb.append( " LTRNet ISW [" );
|
||||
sb.append( mCRC.getAbbreviation() );
|
||||
sb.append( "] " );
|
||||
sb.append( getMessage() );
|
||||
pad( sb, 100 );
|
||||
sb.append( mMessage.toString() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
switch( mMessageType )
|
||||
{
|
||||
case CA_STRT:
|
||||
sb.append( "CALL LCN:" );
|
||||
sb.append( format( getChannel(), 2 ) );
|
||||
sb.append( " TG [ " );
|
||||
sb.append( getTalkgroupID() );
|
||||
sb.append( "/" );
|
||||
sb.append( getTalkgroupIDAlias() );
|
||||
sb.append( " ] FREE:" );
|
||||
sb.append( format( getFree(), 2 ) );
|
||||
break;
|
||||
case CA_ENDD:
|
||||
sb.append( "END* AREA:" );
|
||||
sb.append( getArea() );
|
||||
sb.append( " LCN:" );
|
||||
sb.append( format( getChannel(), 2 ) );
|
||||
sb.append( " TG [" );
|
||||
sb.append( getTalkgroupID() );
|
||||
sb.append( "/" );
|
||||
sb.append( getTalkgroupIDAlias() );
|
||||
sb.append( "] FREE:" );
|
||||
sb.append( format( getFree(), 2 ) );
|
||||
break;
|
||||
case RQ_ACCE:
|
||||
sb.append( "REQUEST ACCESS LCN:" );
|
||||
sb.append( format( getChannel(), 2 ) );
|
||||
sb.append( " TG [ " );
|
||||
sb.append( getTalkgroupID() );
|
||||
sb.append( "/" );
|
||||
sb.append( getTalkgroupIDAlias() );
|
||||
sb.append( " ] FREE:" );
|
||||
sb.append( format( getFree(), 2 ) );
|
||||
break;
|
||||
case ID_ESNL:
|
||||
sb.append( "ESN LOW " );
|
||||
sb.append( getESN() );
|
||||
sb.append( "/" );
|
||||
sb.append( getESNAlias() );
|
||||
break;
|
||||
case ID_ESNH:
|
||||
sb.append( "ESN HIGH " );
|
||||
sb.append( getESN() );
|
||||
sb.append( "/" );
|
||||
sb.append( getESNAlias() );
|
||||
break;
|
||||
case ID_UNIQ:
|
||||
sb.append( "RADIO UNIQUE ID " );
|
||||
sb.append( getRadioUniqueID() );
|
||||
|
||||
if( getRadioUniqueIDAlias() != null )
|
||||
{
|
||||
sb.append( "/" );
|
||||
sb.append( getRadioUniqueIDAlias().getName() );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sb.append( "UNKNOWN " );
|
||||
sb.append( getArea() );
|
||||
sb.append( " " );
|
||||
sb.append( format( getChannel(), 2 ) );
|
||||
sb.append( " " );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( " " );
|
||||
sb.append( format( getGroup(), 3 ) );
|
||||
sb.append( " " );
|
||||
sb.append( format( getFree(), 2 ) );
|
||||
sb.append( " CRC:" );
|
||||
sb.append( CRCLTR.getTransmittedChecksum( mMessage ) );
|
||||
sb.append( " CALC:" );
|
||||
sb.append( CRCLTR.getCalculatedChecksum( mMessage ) );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public int getRadioUniqueID()
|
||||
{
|
||||
int retVal = INT_NULL_VALUE;
|
||||
|
||||
if( mMessageType == MessageType.ID_UNIQ )
|
||||
{
|
||||
retVal = getInt( sSIXTEEN_BITS );
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public Alias getRadioUniqueIDAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getUniqueID( getRadioUniqueID() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public MessageType getMessageType()
|
||||
{
|
||||
MessageType retVal = MessageType.UN_KNWN;
|
||||
|
||||
int channel = getChannel();
|
||||
|
||||
if( channel == 31 )
|
||||
{
|
||||
retVal = MessageType.CA_ENDD;
|
||||
}
|
||||
else if( channel > 20 )
|
||||
{
|
||||
switch( channel )
|
||||
{
|
||||
case sCHIU_UNIQUE_ID:
|
||||
retVal = MessageType.ID_UNIQ;
|
||||
break;
|
||||
case sCHIU_ESN_LOW:
|
||||
retVal = MessageType.ID_ESNH;
|
||||
break;
|
||||
case sCHIU_ESN_HIGH:
|
||||
retVal = MessageType.ID_ESNL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if( channel > 0 )
|
||||
{
|
||||
int free = getFree();
|
||||
|
||||
switch( free )
|
||||
{
|
||||
case sFREE_CALL:
|
||||
retVal = MessageType.CA_STRT;
|
||||
break;
|
||||
case sFREE_END_CALL:
|
||||
retVal = MessageType.CA_ENDD;
|
||||
break;
|
||||
case sFREE_REQUEST_ACCESS:
|
||||
retVal = MessageType.RQ_ACCE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuxiliaryMessage( LTRNetOSWMessage message )
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plottable getPlottable()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a listing of aliases contained in the message.
|
||||
*/
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<Alias>();
|
||||
|
||||
Alias from = getFromIDAlias();
|
||||
|
||||
if( from != null )
|
||||
{
|
||||
aliases.add( from );
|
||||
}
|
||||
|
||||
Alias to = getToIDAlias();
|
||||
|
||||
if( to != null )
|
||||
{
|
||||
aliases.add( to );
|
||||
}
|
||||
|
||||
Alias esn = getESNAlias();
|
||||
|
||||
if( esn != null )
|
||||
{
|
||||
aliases.add( esn );
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.edac.CRCLTR;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
public abstract class LTRNetMessage extends Message
|
||||
{
|
||||
protected static final String sUNKNOWN = "**UNKNOWN**";
|
||||
protected static final int INT_NULL_VALUE = -1;
|
||||
protected static final double sDOUBLE_NULL_VALUE = -1.0D;
|
||||
protected SimpleDateFormat mDatestampFormatter =
|
||||
new SimpleDateFormat( "yyyyMMdd HHmmss" );
|
||||
protected DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000");
|
||||
|
||||
protected static final int[] sSYNC = { 8,7,6,5,4,3,2,1,0 };
|
||||
protected static final int[] sAREA = { 9 };
|
||||
protected static final int[] sCHANNEL = { 14,13,12,11,10 };
|
||||
protected static final int[] sHOME_REPEATER = { 19,18,17,16,15 };
|
||||
protected static final int[] sGROUP = { 27,26,25,24,23,22,21,20 };
|
||||
protected static final int[] sFREE = { 32,31,30,29,28 };
|
||||
protected static final int[] sCRC = { 39,38,37,36,35,34,33 };
|
||||
protected static final int[] sSIXTEEN_BITS = { 32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17 };
|
||||
protected static final int[] sMANUFACTURER_ID = { 23,22,21,20,19,18,17 };
|
||||
|
||||
protected BinaryMessage mMessage;
|
||||
protected MessageType mMessageType;
|
||||
protected CRC mCRC;
|
||||
protected AliasList mAliasList;
|
||||
|
||||
public LTRNetMessage( BinaryMessage message,
|
||||
MessageDirection direction,
|
||||
AliasList list )
|
||||
{
|
||||
mMessage = message;
|
||||
mCRC = CRCLTR.check( message, direction );
|
||||
mAliasList = list;
|
||||
}
|
||||
|
||||
public MessageType getMessageType()
|
||||
{
|
||||
return mMessageType;
|
||||
}
|
||||
|
||||
public abstract void setAuxiliaryMessage( LTRNetOSWMessage message );
|
||||
|
||||
public boolean isValid()
|
||||
{
|
||||
return mCRC != CRC.FAILED_CRC && mCRC != CRC.FAILED_PARITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends spaces to the end of the stringbuilder to make it length long
|
||||
*/
|
||||
protected void pad( StringBuilder sb, int length )
|
||||
{
|
||||
while( sb.length() < length )
|
||||
{
|
||||
sb.append( " " );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBinaryMessage()
|
||||
{
|
||||
return mMessage.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a value formatted in hex notation.
|
||||
*
|
||||
* @param value - integer value
|
||||
* @return - hex digits returned
|
||||
*/
|
||||
protected static String getHex( int value, int digits )
|
||||
{
|
||||
return String.format( "%0" + digits + "X", value );
|
||||
}
|
||||
|
||||
protected int getInt( int[] bits )
|
||||
{
|
||||
int retVal = 0;
|
||||
|
||||
for( int x = 0; x < bits.length; x++ )
|
||||
{
|
||||
if( mMessage.get( bits[ x ] ) )
|
||||
{
|
||||
retVal += 1<<x;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads an integer value with additional zeroes to make it decimalPlaces long
|
||||
*/
|
||||
public String format( int number, int decimalPlaces )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() );
|
||||
|
||||
for( int x = 0; x < paddingRequired; x++)
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
|
||||
sb.append( number );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String format( String val, int places )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( val );
|
||||
|
||||
while( sb.length() < places )
|
||||
{
|
||||
sb.append( " " );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public CRC getCRC()
|
||||
{
|
||||
return mCRC;
|
||||
}
|
||||
|
||||
public int getArea()
|
||||
{
|
||||
return getInt( sAREA );
|
||||
}
|
||||
|
||||
public int getChannel()
|
||||
{
|
||||
return getInt( sCHANNEL );
|
||||
}
|
||||
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return getInt( sHOME_REPEATER );
|
||||
}
|
||||
|
||||
public int getGroup()
|
||||
{
|
||||
return getInt( sGROUP );
|
||||
}
|
||||
|
||||
public String getTalkgroupID()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( getArea() );
|
||||
sb.append( "-" );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( "-" );
|
||||
sb.append( format( getGroup(), 3 ) );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public Alias getTalkgroupIDAlias()
|
||||
{
|
||||
return mAliasList.getTalkgroupAlias( getTalkgroupID() );
|
||||
}
|
||||
|
||||
public int getFree()
|
||||
{
|
||||
return getInt( sFREE );
|
||||
}
|
||||
|
||||
public int getCRCChecksum()
|
||||
{
|
||||
return getInt( sCRC );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol()
|
||||
{
|
||||
return DecoderType.LTR_NET.getDisplayString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType()
|
||||
{
|
||||
if( mMessageType != null )
|
||||
{
|
||||
return mMessageType.getDisplayText();
|
||||
}
|
||||
else
|
||||
{
|
||||
return MessageType.UN_KNWN.getDisplayText();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToID()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorStatus()
|
||||
{
|
||||
return mCRC.getDisplayText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class LTRNetMessageFilter extends Filter<Message>
|
||||
{
|
||||
private HashMap<MessageType, FilterElement<MessageType>> mElements =
|
||||
new HashMap<MessageType, FilterElement<MessageType>>();
|
||||
|
||||
public LTRNetMessageFilter()
|
||||
{
|
||||
super( "LTR-Net Message Filter" );
|
||||
|
||||
mElements.put( MessageType.CA_STRT,
|
||||
new FilterElement<MessageType>( MessageType.CA_STRT ) );
|
||||
mElements.put( MessageType.CA_ENDD,
|
||||
new FilterElement<MessageType>( MessageType.CA_ENDD ) );
|
||||
mElements.put( MessageType.SY_IDLE,
|
||||
new FilterElement<MessageType>( MessageType.SY_IDLE ) );
|
||||
mElements.put( MessageType.ID_UNIQ,
|
||||
new FilterElement<MessageType>( MessageType.ID_UNIQ ) );
|
||||
mElements.put( MessageType.ID_ESNH,
|
||||
new FilterElement<MessageType>( MessageType.ID_ESNH ) );
|
||||
mElements.put( MessageType.ID_ESNL,
|
||||
new FilterElement<MessageType>( MessageType.ID_ESNL ) );
|
||||
mElements.put( MessageType.RQ_ACCE,
|
||||
new FilterElement<MessageType>( MessageType.RQ_ACCE ) );
|
||||
mElements.put( MessageType.ID_SITE,
|
||||
new FilterElement<MessageType>( MessageType.ID_SITE ) );
|
||||
mElements.put( MessageType.FQ_RXHI,
|
||||
new FilterElement<MessageType>( MessageType.FQ_RXHI ) );
|
||||
mElements.put( MessageType.FQ_RXLO,
|
||||
new FilterElement<MessageType>( MessageType.FQ_RXLO ) );
|
||||
mElements.put( MessageType.FQ_TXHI,
|
||||
new FilterElement<MessageType>( MessageType.FQ_TXHI ) );
|
||||
mElements.put( MessageType.FQ_TXLO,
|
||||
new FilterElement<MessageType>( MessageType.FQ_TXLO ) );
|
||||
mElements.put( MessageType.ID_NBOR,
|
||||
new FilterElement<MessageType>( MessageType.ID_NBOR ) );
|
||||
mElements.put( MessageType.MA_CHNH,
|
||||
new FilterElement<MessageType>( MessageType.MA_CHNH ) );
|
||||
mElements.put( MessageType.MA_CHNL,
|
||||
new FilterElement<MessageType>( MessageType.MA_CHNL ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes( Message message )
|
||||
{
|
||||
if( mEnabled && canProcess( message ) )
|
||||
{
|
||||
LTRNetMessage ltr = (LTRNetMessage)message;
|
||||
|
||||
if( mElements.containsKey( ltr.getMessageType() ) )
|
||||
{
|
||||
return mElements.get( ltr.getMessageType() ).isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess( Message message )
|
||||
{
|
||||
return message instanceof LTRNetMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return new ArrayList<FilterElement<?>>( mElements.values() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class LTRNetMessageProcessor implements Listener<BinaryMessage>
|
||||
{
|
||||
private Listener<Message> mMessageListener;
|
||||
|
||||
private HashMap<Integer,LTRNetOSWMessage> mReceiveHighAuxMessages =
|
||||
new HashMap<Integer,LTRNetOSWMessage>();
|
||||
private HashMap<Integer,LTRNetOSWMessage> mReceiveLowAuxMessages =
|
||||
new HashMap<Integer,LTRNetOSWMessage>();
|
||||
|
||||
private HashMap<Integer,LTRNetOSWMessage> mTransmitHighAuxMessages =
|
||||
new HashMap<Integer,LTRNetOSWMessage>();
|
||||
private HashMap<Integer,LTRNetOSWMessage> mTransmitLowAuxMessages =
|
||||
new HashMap<Integer,LTRNetOSWMessage>();
|
||||
|
||||
private LTRNetISWMessage mESNHighMessage;
|
||||
private LTRNetISWMessage mESNLowMessage;
|
||||
|
||||
private MessageDirection mDirection;
|
||||
private AliasList mAliasList;
|
||||
|
||||
public LTRNetMessageProcessor( MessageDirection direction, AliasList list )
|
||||
{
|
||||
mDirection = direction;
|
||||
mAliasList = list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( BinaryMessage buffer )
|
||||
{
|
||||
if( mMessageListener != null )
|
||||
{
|
||||
LTRNetMessage message;
|
||||
|
||||
if( mDirection == MessageDirection.OSW )
|
||||
{
|
||||
message = new LTRNetOSWMessage( buffer, mAliasList );
|
||||
|
||||
if( message.isValid() )
|
||||
{
|
||||
if( message.getMessageType() == MessageType.FQ_TXHI )
|
||||
{
|
||||
mTransmitHighAuxMessages.put( message.getHomeRepeater(), (LTRNetOSWMessage)message );
|
||||
|
||||
message.setAuxiliaryMessage( mTransmitLowAuxMessages.get(message.getHomeRepeater() ) );
|
||||
}
|
||||
else if( message.getMessageType() == MessageType.FQ_TXLO )
|
||||
{
|
||||
mTransmitLowAuxMessages.put( message.getHomeRepeater(),
|
||||
(LTRNetOSWMessage)message );
|
||||
|
||||
message.setAuxiliaryMessage( mTransmitHighAuxMessages.get(
|
||||
message.getHomeRepeater() ) );
|
||||
}
|
||||
else if( message.getMessageType() == MessageType.FQ_RXHI )
|
||||
{
|
||||
mReceiveHighAuxMessages.put( message.getHomeRepeater(),
|
||||
(LTRNetOSWMessage)message );
|
||||
|
||||
message.setAuxiliaryMessage( mReceiveLowAuxMessages.get(
|
||||
message.getHomeRepeater() ) );
|
||||
}
|
||||
else if( message.getMessageType() == MessageType.FQ_RXLO )
|
||||
{
|
||||
mReceiveLowAuxMessages.put( message.getHomeRepeater(),
|
||||
(LTRNetOSWMessage)message );
|
||||
message.setAuxiliaryMessage( mReceiveHighAuxMessages.get(
|
||||
message.getHomeRepeater() ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.flip( 0, 40 );
|
||||
|
||||
message = new LTRNetISWMessage( buffer, mAliasList );
|
||||
|
||||
if( message.isValid() )
|
||||
{
|
||||
/**
|
||||
* Catch ESN High messages to marry up with ESN Low messages. Purge
|
||||
* the ESN high message after 2 messages, so that it doesn't get
|
||||
* paired with the wrong esn low message
|
||||
*/
|
||||
if( message.getMessageType() == MessageType.ID_ESNH )
|
||||
{
|
||||
mESNHighMessage = (LTRNetISWMessage)message;
|
||||
|
||||
if( mESNLowMessage != null )
|
||||
{
|
||||
/* Check that the stored message is not older than
|
||||
* 5 seconds */
|
||||
if( System.currentTimeMillis() -
|
||||
mESNLowMessage.getTimeReceived() < 5000 )
|
||||
{
|
||||
((LTRNetISWMessage)message).setAuxiliaryMessage( mESNLowMessage );
|
||||
}
|
||||
else
|
||||
{
|
||||
mESNLowMessage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if( message.getMessageType() == MessageType.ID_ESNL )
|
||||
{
|
||||
mESNLowMessage = (LTRNetISWMessage)message;
|
||||
|
||||
if( mESNHighMessage != null )
|
||||
{
|
||||
/* Check that the stored message is not older than 5 seconds */
|
||||
if( System.currentTimeMillis() - mESNHighMessage.getTimeReceived() < 5000 )
|
||||
{
|
||||
((LTRNetISWMessage)message).setAuxiliaryMessage( mESNHighMessage );
|
||||
}
|
||||
else
|
||||
{
|
||||
mESNHighMessage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mMessageListener.receive( message );
|
||||
}
|
||||
}
|
||||
|
||||
public void setMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.ltrnet;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.map.Plottable;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class LTRNetOSWMessage extends LTRNetMessage
|
||||
{
|
||||
private static final int CHANNEL_FREQUENCY_MESSAGE_TYPE_BIT = 20;
|
||||
private static final int CHANNEL_MAP_MESSAGE_TYPE_BIT = 17;
|
||||
|
||||
private static final int CHIU_UNIQUE_ID = 17;
|
||||
private static final int CHIU_SITE_ID = 18;
|
||||
private static final int CHIU_TRANSMIT_FREQUENCY = 24;
|
||||
private static final int CHIU_RECEIVE_FREQUENCY = 25;
|
||||
private static final int CHIU_NEIGHBOR = 26;
|
||||
private static final int CHIU_MAP = 28;
|
||||
private static final int CHIU_CALL_END = 31;
|
||||
|
||||
private static final int HOME_DIRECTED_GROUP_CALL = 29;
|
||||
private static final int HOME_CHANNEL_IN_USE_MUTE_AUDIO = 30;
|
||||
|
||||
private static final int GROUP_DO_NOTHING = 253;
|
||||
private static final int GROUP_CWID = 254;
|
||||
private static final int GROUP_CHANNEL_IDLE = 255;
|
||||
|
||||
private static final int FREE_ALL_CHANNELS_BUSY = 0;
|
||||
private static final int FREE_LTR_GROUP_CALL = 30;
|
||||
private static final int FREE_LTRNET_GROUP_CALL = 31;
|
||||
|
||||
private LTRNetOSWMessage mAuxMessage;
|
||||
|
||||
public LTRNetOSWMessage( BinaryMessage message, AliasList list )
|
||||
{
|
||||
super( message, MessageDirection.OSW, list );
|
||||
mMessageType = getType();
|
||||
}
|
||||
|
||||
public void setAuxiliaryMessage( LTRNetOSWMessage message )
|
||||
{
|
||||
if( message != null )
|
||||
{
|
||||
mAuxMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
public String getRadioUniqueIDText()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int radioId = getRadioUniqueID();
|
||||
|
||||
if( radioId != INT_NULL_VALUE )
|
||||
{
|
||||
sb.append( " RADIO UNIQUE ID: " );
|
||||
sb.append( radioId );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append( mDatestampFormatter.format(
|
||||
new Date( System.currentTimeMillis() ) ) );
|
||||
sb.append( " LTRNet OSW [" );
|
||||
sb.append( mCRC.getAbbreviation() );
|
||||
sb.append( "] " );
|
||||
sb.append( getMessage() );
|
||||
pad( sb, 100 );
|
||||
sb.append( mMessage.toString() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
switch( mMessageType )
|
||||
{
|
||||
case SY_IDLE:
|
||||
sb.append( "IDLE AREA:" );
|
||||
sb.append( getArea() );
|
||||
sb.append( " LCN:" );
|
||||
sb.append( format( getChannel(), 2 ) );
|
||||
sb.append( " HOME:" );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( " GRP:" );
|
||||
sb.append( format( getGroup(), 3 ) );
|
||||
sb.append( " FREE:" );
|
||||
sb.append( format( getFree(), 2 ) );
|
||||
break;
|
||||
case CA_STRT:
|
||||
sb.append( "CALL LCN:" );
|
||||
sb.append( format( getChannel(), 2 ) );
|
||||
sb.append( " TG [ " );
|
||||
sb.append( getTalkgroupID() );
|
||||
sb.append( "/" );
|
||||
sb.append( getTalkgroupIDAlias() );
|
||||
sb.append( " ] FREE:" );
|
||||
sb.append( format( getFree(), 2 ) );
|
||||
break;
|
||||
case CA_ENDD:
|
||||
sb.append( "END* AREA:" );
|
||||
sb.append( getArea() );
|
||||
sb.append( " LCN:" );
|
||||
sb.append( format( getChannel(), 2 ) );
|
||||
sb.append( " TG [" );
|
||||
sb.append( getTalkgroupID() );
|
||||
sb.append( "/" );
|
||||
sb.append( getTalkgroupIDAlias() );
|
||||
sb.append( "] FREE:" );
|
||||
sb.append( format( getFree(), 2 ) );
|
||||
break;
|
||||
case MA_CHNL:
|
||||
sb.append( "CHAN MAP LOW LCN [" );
|
||||
sb.append( getChannelMapAsString( getChannelMapLow() ) );
|
||||
sb.append( "]" );
|
||||
break;
|
||||
case MA_CHNH:
|
||||
sb.append( "CHAN MAP HIGH LCN [" );
|
||||
sb.append( getChannelMapAsString( getChannelMapHigh() ) );
|
||||
sb.append( "]" );
|
||||
break;
|
||||
case FQ_RXLO:
|
||||
sb.append( "CHAN RX LOW LCN:" );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( " = " );
|
||||
long rxFreqLo = getFrequency();
|
||||
if( rxFreqLo > 0 )
|
||||
{
|
||||
sb.append( mDecimalFormatter.format( (double)rxFreqLo / 1E6d ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "000.00000" );
|
||||
}
|
||||
sb.append( " MHz" );
|
||||
break;
|
||||
case FQ_RXHI:
|
||||
sb.append( "CHAN RX HIGH LCN:" );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( " = " );
|
||||
long rxfreqhi = getFrequency();
|
||||
if( rxfreqhi > 0 )
|
||||
{
|
||||
sb.append( mDecimalFormatter.format( (double)rxfreqhi / 1E6d ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "000.00000" );
|
||||
}
|
||||
sb.append( " MHz" );
|
||||
break;
|
||||
case FQ_TXHI:
|
||||
sb.append( "CHAN TX LOW LCN:" );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( " = " );
|
||||
long txfreq = getFrequency();
|
||||
if( txfreq > 0 )
|
||||
{
|
||||
sb.append( mDecimalFormatter.format( (double)txfreq / 1E6d ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "000.00000" );
|
||||
}
|
||||
sb.append( " MHz" );
|
||||
break;
|
||||
case FQ_TXLO:
|
||||
sb.append( "CHAN TX HIGH LCN:" );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( " = " );
|
||||
long txFreqLo = getFrequency();
|
||||
if( txFreqLo > 0 )
|
||||
{
|
||||
sb.append( mDecimalFormatter.format( (double)txFreqLo / 1E6d ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "000.00000" );
|
||||
}
|
||||
sb.append( " MHz" );
|
||||
break;
|
||||
case ID_SITE:
|
||||
String siteId = getSiteID();
|
||||
if( siteId != null )
|
||||
{
|
||||
sb.append( "SITE ID: " );
|
||||
sb.append( siteId );
|
||||
sb.append( " " );
|
||||
sb.append( getSiteIDAlias() );
|
||||
}
|
||||
break;
|
||||
case ID_NBOR:
|
||||
int neighborRank = getNeighborRank();
|
||||
String neighborId = getNeighborID();
|
||||
|
||||
if( neighborRank != INT_NULL_VALUE &&
|
||||
neighborId != null )
|
||||
{
|
||||
sb.append( "NEIGHBOR SITE " );
|
||||
sb.append( neighborRank );
|
||||
sb.append( ": " );
|
||||
sb.append( neighborId );
|
||||
sb.append( " " );
|
||||
sb.append( getNeighborIDAlias() );
|
||||
}
|
||||
break;
|
||||
case ID_UNIQ:
|
||||
sb.append( "REGISTER RADIO UNIQUE ID:" );
|
||||
sb.append( getRadioUniqueID() );
|
||||
break;
|
||||
default:
|
||||
sb.append( "UNKNOWN " );
|
||||
sb.append( getArea() );
|
||||
sb.append( " " + format( getChannel(), 2 ) );
|
||||
sb.append( " " + format( getHomeRepeater(), 2 ) );
|
||||
sb.append( " " + format( getGroup(), 3 ) );
|
||||
sb.append( " " + format( getFree(), 2 ) );
|
||||
sb.append( " A:" );
|
||||
sb.append( getArea() );
|
||||
sb.append( " C:" );
|
||||
sb.append( format( getChannel(), 2 ) );
|
||||
sb.append( " H:" );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( " G:" );
|
||||
sb.append( format( getGroup(), 3 ) );
|
||||
sb.append( " F:" );
|
||||
sb.append( format( getFree(), 2 ) );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String getTalkgroupID()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( getArea() );
|
||||
sb.append( "-" );
|
||||
sb.append( format( getHomeRepeater(), 2 ) );
|
||||
sb.append( "-" );
|
||||
sb.append( format( getGroup(), 3 ) );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public Alias getTalkgroupIDAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getTalkgroupAlias( getTalkgroupID() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ArrayList<Integer> getChannelMapLow()
|
||||
{
|
||||
ArrayList<Integer> retVal = new ArrayList<Integer>();
|
||||
|
||||
if( mMessageType == MessageType.MA_CHNL )
|
||||
{
|
||||
for( int x = 27; x >= 18; x-- )
|
||||
{
|
||||
if( mMessage.get( x ) )
|
||||
{
|
||||
retVal.add( 28 - x );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public ArrayList<Integer> getChannelMapHigh()
|
||||
{
|
||||
ArrayList<Integer> retVal = new ArrayList<Integer>();
|
||||
|
||||
if( mMessageType == MessageType.MA_CHNH )
|
||||
{
|
||||
for( int x = 27; x >= 18; x-- )
|
||||
{
|
||||
if( mMessage.get( x ) )
|
||||
{
|
||||
retVal.add( 38 - x );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public String getChannelMapAsString( ArrayList<Integer> channels )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for( Integer channel: channels )
|
||||
{
|
||||
sb.append( channel );
|
||||
sb.append( " " );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public int getRadioUniqueID()
|
||||
{
|
||||
int retVal = INT_NULL_VALUE;
|
||||
|
||||
if( mMessageType == MessageType.ID_UNIQ )
|
||||
{
|
||||
retVal = mMessage.getInt( 17, 32 );
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public Alias getRadioUniqueIDAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getUniqueID( getRadioUniqueID() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getSiteID()
|
||||
{
|
||||
if( mMessageType == MessageType.ID_SITE )
|
||||
{
|
||||
return String.valueOf( mMessage.getInt( 23, 32 ) );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Alias getSiteIDAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getSiteID( getSiteID() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getNeighborID()
|
||||
{
|
||||
if( mMessageType == MessageType.ID_NBOR )
|
||||
{
|
||||
int id = mMessage.getInt( 23, 32 );
|
||||
|
||||
if( id > 0 )
|
||||
{
|
||||
return String.valueOf( id );
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Alias getNeighborIDAlias()
|
||||
{
|
||||
if( mAliasList != null )
|
||||
{
|
||||
return mAliasList.getSiteID( getNeighborID() );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getNeighborRank()
|
||||
{
|
||||
int retVal = INT_NULL_VALUE;
|
||||
|
||||
if( mMessageType == MessageType.ID_NBOR )
|
||||
{
|
||||
retVal = mMessage.getInt( 15, 18 ) + 1;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public long getFrequency()
|
||||
{
|
||||
long retVal = 0;
|
||||
|
||||
if( mAuxMessage != null )
|
||||
{
|
||||
int high;
|
||||
int low;
|
||||
|
||||
MessageType type = getMessageType();
|
||||
|
||||
if( type == MessageType.FQ_RXHI || type == MessageType.FQ_TXHI )
|
||||
{
|
||||
high = getHighChannelUnits();
|
||||
low = mAuxMessage.getLowChannelUnits();
|
||||
}
|
||||
else
|
||||
{
|
||||
high = mAuxMessage.getHighChannelUnits();
|
||||
low = getLowChannelUnits();
|
||||
}
|
||||
|
||||
if( high != INT_NULL_VALUE && low != INT_NULL_VALUE )
|
||||
{
|
||||
retVal = ( high + low ) * 1250 + 150000000;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the high order (15 - 12) frequency bits which are
|
||||
* the lower 4 bits of the Free field, left shifted by 12, representing the
|
||||
* frequency value in units of .00125 MHz
|
||||
*/
|
||||
public int getHighChannelUnits()
|
||||
{
|
||||
int retVal = INT_NULL_VALUE;
|
||||
|
||||
if( mAuxMessage != null &&
|
||||
( mMessageType == MessageType.FQ_RXHI ||
|
||||
mMessageType == MessageType.FQ_TXHI ) )
|
||||
{
|
||||
retVal = mMessage.getInt( 29, 32 );
|
||||
|
||||
retVal = Integer.rotateLeft( retVal, 12 );
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the low order (11 - 0) frequency bits which are
|
||||
* contained in the Group and Free fields representing the frequency value
|
||||
* units of .00125 MHz
|
||||
*/
|
||||
public int getLowChannelUnits()
|
||||
{
|
||||
int retVal = INT_NULL_VALUE;
|
||||
|
||||
if( mMessageType == MessageType.FQ_RXLO ||
|
||||
mMessageType == MessageType.FQ_TXLO )
|
||||
{
|
||||
retVal = mMessage.getInt( 21, 32 );
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public MessageType getType()
|
||||
{
|
||||
MessageType retVal = MessageType.UN_KNWN;
|
||||
|
||||
int channel = getChannel();
|
||||
|
||||
int home = getHomeRepeater();
|
||||
|
||||
//LTR-Net messages
|
||||
if( home != 31 && ( channel > 20 || home > 20 ) )
|
||||
{
|
||||
switch( channel )
|
||||
{
|
||||
case CHIU_UNIQUE_ID:
|
||||
retVal = MessageType.ID_UNIQ;
|
||||
break;
|
||||
case CHIU_SITE_ID:
|
||||
retVal = MessageType.ID_SITE;
|
||||
break;
|
||||
case CHIU_TRANSMIT_FREQUENCY:
|
||||
if( getHomeRepeater() == 24 )
|
||||
{
|
||||
retVal = MessageType.ID_UNIQ;
|
||||
}
|
||||
else
|
||||
{
|
||||
if( mMessage.get( CHANNEL_FREQUENCY_MESSAGE_TYPE_BIT ) )
|
||||
{
|
||||
retVal = MessageType.FQ_TXHI;
|
||||
}
|
||||
else
|
||||
{
|
||||
retVal = MessageType.FQ_TXLO;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHIU_RECEIVE_FREQUENCY:
|
||||
if( mMessage.get( CHANNEL_FREQUENCY_MESSAGE_TYPE_BIT ) )
|
||||
{
|
||||
retVal = MessageType.FQ_RXHI;
|
||||
}
|
||||
else
|
||||
{
|
||||
retVal = MessageType.FQ_RXLO;
|
||||
}
|
||||
break;
|
||||
case CHIU_NEIGHBOR:
|
||||
retVal = MessageType.ID_NBOR;
|
||||
break;
|
||||
case CHIU_MAP:
|
||||
if( mMessage.get( CHANNEL_MAP_MESSAGE_TYPE_BIT ) )
|
||||
{
|
||||
retVal = MessageType.MA_CHNH;
|
||||
}
|
||||
else
|
||||
{
|
||||
retVal = MessageType.MA_CHNL;
|
||||
}
|
||||
break;
|
||||
case CHIU_CALL_END:
|
||||
retVal = MessageType.CA_ENDD;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int group = getGroup();
|
||||
|
||||
if( group == GROUP_CHANNEL_IDLE )
|
||||
{
|
||||
retVal = MessageType.SY_IDLE;
|
||||
}
|
||||
else
|
||||
{
|
||||
retVal = MessageType.CA_STRT;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
if( mMessageType == MessageType.CA_STRT ||
|
||||
mMessageType == MessageType.CA_ENDD )
|
||||
{
|
||||
return getTalkgroupID();
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
if( mMessageType == MessageType.CA_STRT ||
|
||||
mMessageType == MessageType.CA_ENDD )
|
||||
{
|
||||
return getTalkgroupIDAlias();
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plottable getPlottable()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a listing of aliases contained in the message.
|
||||
*/
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<Alias>();
|
||||
|
||||
Alias from = getFromIDAlias();
|
||||
|
||||
if( from != null )
|
||||
{
|
||||
aliases.add( from );
|
||||
}
|
||||
|
||||
Alias to = getToIDAlias();
|
||||
|
||||
if( to != null )
|
||||
{
|
||||
aliases.add( to );
|
||||
}
|
||||
|
||||
Alias neighbor = getNeighborIDAlias();
|
||||
|
||||
if( neighbor != null )
|
||||
{
|
||||
aliases.add( neighbor );
|
||||
}
|
||||
|
||||
Alias uid = getRadioUniqueIDAlias();
|
||||
|
||||
if( uid != null )
|
||||
{
|
||||
aliases.add( uid );
|
||||
}
|
||||
|
||||
Alias site = getSiteIDAlias();
|
||||
|
||||
if( site != null )
|
||||
{
|
||||
aliases.add( site );
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.ltrstandard;
|
||||
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
public class DecodeConfigLTRStandard extends DecodeConfiguration
|
||||
{
|
||||
private MessageDirection mMessageDirection = MessageDirection.OSW;
|
||||
|
||||
public DecodeConfigLTRStandard()
|
||||
{
|
||||
super( DecoderType.LTR_STANDARD );
|
||||
}
|
||||
|
||||
@XmlAttribute( name = "direction" )
|
||||
public MessageDirection getMessageDirection()
|
||||
{
|
||||
return mMessageDirection;
|
||||
}
|
||||
|
||||
public void setMessageDirection( MessageDirection direction )
|
||||
{
|
||||
mMessageDirection = direction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.ltrstandard;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.MessageFramer;
|
||||
import io.github.dsheirer.bits.SyncPattern;
|
||||
import io.github.dsheirer.dsp.fsk.LTRFSKDecoder;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IUnFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class LTRStandardDecoder extends Decoder implements IUnFilteredRealBufferListener
|
||||
{
|
||||
public static final int LTR_STANDARD_MESSAGE_LENGTH = 40;
|
||||
|
||||
private LTRFSKDecoder mLTRFSKDecoder;
|
||||
private MessageFramer mLTRMessageFramer;
|
||||
private LTRStandardMessageProcessor mLTRMessageProcessor;
|
||||
|
||||
/**
|
||||
* LTR Decoder. Decodes unfiltered (e.g. demodulated but with no DC or
|
||||
* audio filtering) samples and produces LTR Standard messages.
|
||||
*/
|
||||
public LTRStandardDecoder( AliasList aliasList,
|
||||
MessageDirection direction )
|
||||
{
|
||||
mLTRFSKDecoder = new LTRFSKDecoder();
|
||||
|
||||
if( direction == MessageDirection.OSW )
|
||||
{
|
||||
mLTRMessageFramer =
|
||||
new MessageFramer( SyncPattern.LTR_STANDARD_OSW.getPattern(),
|
||||
LTR_STANDARD_MESSAGE_LENGTH );
|
||||
}
|
||||
else
|
||||
{
|
||||
mLTRMessageFramer =
|
||||
new MessageFramer( SyncPattern.LTR_STANDARD_ISW.getPattern(),
|
||||
LTR_STANDARD_MESSAGE_LENGTH );
|
||||
}
|
||||
|
||||
mLTRFSKDecoder.addListener( mLTRMessageFramer );
|
||||
|
||||
mLTRMessageProcessor = new LTRStandardMessageProcessor( direction, aliasList );
|
||||
mLTRMessageFramer.addMessageListener( mLTRMessageProcessor );
|
||||
mLTRMessageProcessor.setMessageListener( this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.LTR_STANDARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getUnFilteredRealBufferListener()
|
||||
{
|
||||
return mLTRFSKDecoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mLTRMessageFramer.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.ltrstandard;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EditorValidationException;
|
||||
import io.github.dsheirer.gui.editor.ValidatingEditor;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class LTRStandardDecoderEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JCheckBox mAFC;
|
||||
private JSlider mAFCMaximumCorrection;
|
||||
private JComboBox<MessageDirection> mComboDirection;
|
||||
|
||||
public LTRStandardDecoderEditor()
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0,wrap 4", "[right][grow,fill][right][grow,fill]", "" ) );
|
||||
|
||||
mAFC = new JCheckBox( "AFC: 3000 Hz" );
|
||||
mAFC.setEnabled( false );
|
||||
mAFC.setToolTipText( "AFC automatically adjusts the center frequency of the channel to "
|
||||
+ "correct/compensate for inaccuracies and frequency drift in the tuner" );
|
||||
mAFC.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent arg0 )
|
||||
{
|
||||
setModified( true );
|
||||
|
||||
if( mAFC.isSelected() && !mAFCMaximumCorrection.isEnabled() )
|
||||
{
|
||||
mAFCMaximumCorrection.setEnabled( true );
|
||||
}
|
||||
else if( !mAFC.isSelected() && mAFCMaximumCorrection.isEnabled() )
|
||||
{
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
add( mAFC );
|
||||
|
||||
mAFCMaximumCorrection = new JSlider( 0, 7000, 3000 );
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
mAFCMaximumCorrection.setToolTipText( "Maximum AFC frequency correction (0 - 15kHz)" );
|
||||
mAFCMaximumCorrection.setMajorTickSpacing( 2000 );
|
||||
mAFCMaximumCorrection.setMinorTickSpacing( 1000 );
|
||||
mAFCMaximumCorrection.setPaintTicks( true );
|
||||
|
||||
mAFCMaximumCorrection.addChangeListener( new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged( ChangeEvent e )
|
||||
{
|
||||
mAFC.setText( "AFC: " + mAFCMaximumCorrection.getValue() + " Hz" );
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
add( mAFCMaximumCorrection );
|
||||
|
||||
mComboDirection = new JComboBox<MessageDirection>();
|
||||
mComboDirection.setModel( new DefaultComboBoxModel<MessageDirection>( MessageDirection.values() ) );
|
||||
mComboDirection.setSelectedItem( MessageDirection.OSW );
|
||||
mComboDirection.setEnabled( false );
|
||||
mComboDirection.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
|
||||
add( new JLabel( "Format:" ) );
|
||||
add( mComboDirection );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate( Editor<Channel> editor ) throws EditorValidationException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem( Channel item )
|
||||
{
|
||||
super.setItem( item );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
mAFC.setEnabled( true );
|
||||
mComboDirection.setEnabled( true );
|
||||
|
||||
DecodeConfiguration config = getItem().getDecodeConfiguration();
|
||||
|
||||
if( config instanceof DecodeConfigLTRStandard )
|
||||
{
|
||||
DecodeConfigLTRStandard ltr = (DecodeConfigLTRStandard)config;
|
||||
mComboDirection.setSelectedItem( ltr.getMessageDirection() );
|
||||
mAFC.setSelected( ltr.isAFCEnabled() );
|
||||
mAFCMaximumCorrection.setValue( ltr.getAFCMaximumCorrection() );
|
||||
mAFCMaximumCorrection.setEnabled( ltr.isAFCEnabled() );
|
||||
setModified( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComboDirection.setSelectedItem( MessageDirection.OSW );
|
||||
mAFC.setSelected( false );
|
||||
mAFCMaximumCorrection.setValue( DecodeConfiguration.DEFAULT_AFC_MAX_CORRECTION );
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
|
||||
setModified( true );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mAFC.setEnabled( false );
|
||||
mComboDirection.setEnabled( false );
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasItem() && isModified() )
|
||||
{
|
||||
DecodeConfigLTRStandard ltr = new DecodeConfigLTRStandard();
|
||||
|
||||
ltr.setAFC( mAFC.isSelected() );
|
||||
ltr.setAFCMaximumCorrection( mAFCMaximumCorrection.getValue() );
|
||||
ltr.setMessageDirection( (MessageDirection)mComboDirection.getSelectedItem() );
|
||||
|
||||
getItem().setDecodeConfiguration( ltr );
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.ltrstandard;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.channel.metadata.AliasedStringAttributeMonitor;
|
||||
import io.github.dsheirer.channel.metadata.Attribute;
|
||||
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
import io.github.dsheirer.module.decode.ltrnet.LTRCallEvent;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.CallEndMessage;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.CallMessage;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.IdleMessage;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.LTRStandardMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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 ? CallEvent.CallEventType.CALL : CallEvent.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.github.dsheirer.module.decode.ltrstandard;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.LTRStandardMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class LTRStandardMessageFilter extends Filter<Message>
|
||||
{
|
||||
private HashMap<MessageType, FilterElement<MessageType>> mElements =
|
||||
new HashMap<MessageType, FilterElement<MessageType>>();
|
||||
|
||||
public LTRStandardMessageFilter()
|
||||
{
|
||||
super( "LTR Message Filter" );
|
||||
|
||||
mElements.put( MessageType.CA_STRT,
|
||||
new FilterElement<MessageType>( MessageType.CA_STRT ) );
|
||||
mElements.put( MessageType.CA_ENDD,
|
||||
new FilterElement<MessageType>( MessageType.CA_ENDD ) );
|
||||
mElements.put( MessageType.SY_IDLE,
|
||||
new FilterElement<MessageType>( MessageType.SY_IDLE ) );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes( Message message )
|
||||
{
|
||||
if( mEnabled && canProcess( message ) )
|
||||
{
|
||||
LTRStandardMessage ltr = (LTRStandardMessage)message;
|
||||
|
||||
if( mElements.containsKey( ltr.getMessageType() ) )
|
||||
{
|
||||
return mElements.get( ltr.getMessageType() ).isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess( Message message )
|
||||
{
|
||||
return message instanceof LTRStandardMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return new ArrayList<FilterElement<?>>( mElements.values() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.ltrstandard;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.edac.CRCLTR;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.CallEndMessage;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.CallMessage;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.IdleMessage;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.LTRStandardMessage;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.message.UnknownMessage;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public class LTRStandardMessageProcessor implements Listener<BinaryMessage>
|
||||
{
|
||||
private MessageDirection mDirection;
|
||||
private AliasList mAliasList;
|
||||
private Listener<Message> mMessageListener;
|
||||
|
||||
/**
|
||||
* Processes raw binary messages and converts them to the correct message class
|
||||
*
|
||||
* @param direction - inbound (ISW) or outbound (OSW)
|
||||
* @param list - alias list
|
||||
*/
|
||||
public LTRStandardMessageProcessor( MessageDirection direction, AliasList list )
|
||||
{
|
||||
mDirection = direction;
|
||||
mAliasList = list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( BinaryMessage binaryMessage )
|
||||
{
|
||||
if( mMessageListener != null )
|
||||
{
|
||||
//Inbound Status Word (ISW) is a bit-flipped version of the Outbound
|
||||
//(OSW), so flip the bits and process it as an OSW
|
||||
if( mDirection == MessageDirection.ISW )
|
||||
{
|
||||
binaryMessage.flip( 0, 40 );
|
||||
}
|
||||
|
||||
CRC crc = CRCLTR.check( binaryMessage, mDirection );
|
||||
|
||||
if( crc != CRC.FAILED_CRC &&
|
||||
crc != CRC.FAILED_PARITY )
|
||||
{
|
||||
LTRStandardMessage message;
|
||||
|
||||
int channel = binaryMessage.getInt( LTRStandardMessage.CHANNEL );
|
||||
int home = binaryMessage.getInt( LTRStandardMessage.HOME_REPEATER );
|
||||
int free = binaryMessage.getInt( LTRStandardMessage.FREE );
|
||||
int group = binaryMessage.getInt( LTRStandardMessage.GROUP );
|
||||
|
||||
if( isValidChannel( channel ) &&
|
||||
isValidChannel( home ) &&
|
||||
isValidChannel( free ) )
|
||||
{
|
||||
if( channel == free && group == 255 )
|
||||
{
|
||||
message = new IdleMessage( binaryMessage, mDirection, mAliasList, crc );
|
||||
}
|
||||
else
|
||||
{
|
||||
message = new CallMessage( binaryMessage, mDirection, mAliasList, crc );
|
||||
}
|
||||
}
|
||||
else if( channel == 31 &&
|
||||
isValidChannel( home ) &&
|
||||
isValidChannel( free ) )
|
||||
{
|
||||
message = new CallEndMessage( binaryMessage, mDirection, mAliasList, crc );
|
||||
}
|
||||
else
|
||||
{
|
||||
message = new UnknownMessage( binaryMessage, mDirection, mAliasList, crc );
|
||||
}
|
||||
|
||||
mMessageListener.receive( message );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the channel (LCN) number to ensure it is in the range 1 -20
|
||||
*/
|
||||
private boolean isValidChannel( int channel )
|
||||
{
|
||||
return ( 1 <= channel && channel <= 20 );
|
||||
}
|
||||
|
||||
public void setMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.ltrstandard.message;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
|
||||
public class CallEndMessage extends LTRStandardMessage
|
||||
{
|
||||
public CallEndMessage(BinaryMessage message, MessageDirection direction,
|
||||
AliasList list, CRC crc )
|
||||
{
|
||||
super( message, direction, list, crc );
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType getMessageType()
|
||||
{
|
||||
return MessageType.CA_ENDD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "END* AREA:" );
|
||||
sb.append( getArea() );
|
||||
sb.append( " LCN:" );
|
||||
sb.append( getChannelFormatted() );
|
||||
sb.append( " TG [" );
|
||||
sb.append( getToID() );
|
||||
sb.append( "/" );
|
||||
Alias endAlias = getToIDAlias();
|
||||
if( endAlias != null )
|
||||
{
|
||||
sb.append( getToIDAlias().getName() );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "UNKNOWN" );
|
||||
}
|
||||
sb.append( "] FREE:" );
|
||||
sb.append( getFreeFormatted() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.ltrstandard.message;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
|
||||
public class CallMessage extends LTRStandardMessage
|
||||
{
|
||||
public CallMessage(BinaryMessage message, MessageDirection direction, AliasList list, CRC crc )
|
||||
{
|
||||
super( message, direction, list, crc );
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType getMessageType()
|
||||
{
|
||||
return MessageType.CA_STRT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "CALL LCN:" );
|
||||
sb.append( getChannelFormatted() );
|
||||
sb.append( " TG [ " );
|
||||
sb.append( getToID() );
|
||||
sb.append( "/" );
|
||||
|
||||
Alias alias = getToIDAlias();
|
||||
|
||||
if( alias != null )
|
||||
{
|
||||
sb.append( getToIDAlias().getName() );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "UNKNOWN" );
|
||||
}
|
||||
|
||||
sb.append( " ] FREE:" );
|
||||
sb.append( getFreeFormatted() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.ltrstandard.message;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
|
||||
public class IdleMessage extends LTRStandardMessage
|
||||
{
|
||||
public IdleMessage(BinaryMessage message, MessageDirection direction,
|
||||
AliasList list, CRC crc )
|
||||
{
|
||||
super( message, direction, list, crc );
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType getMessageType()
|
||||
{
|
||||
return MessageType.SY_IDLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "IDLE AREA:" );
|
||||
sb.append( getArea() );
|
||||
sb.append( " LCN:" );
|
||||
sb.append( getChannelFormatted() );
|
||||
sb.append( " HOME:" );
|
||||
sb.append( getHomeRepeaterFormatted() );
|
||||
sb.append( " GRP:" );
|
||||
sb.append( getGroupFormatted() );
|
||||
sb.append( " FREE:" );
|
||||
sb.append( getFreeFormatted() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.ltrstandard.message;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class LTRStandardMessage extends Message
|
||||
{
|
||||
protected SimpleDateFormat mDatestampFormatter =
|
||||
new SimpleDateFormat( "yyyyMMdd HHmmss" );
|
||||
|
||||
public static final int[] SYNC = { 0,1,2,3,4,5,6,7,8 };
|
||||
public static final int[] AREA = { 9 };
|
||||
public static final int[] CHANNEL = { 10,11,12,13,14 };
|
||||
public static final int[] HOME_REPEATER = { 15,16,17,18,19 };
|
||||
public static final int[] GROUP = { 20,21,22,23,24,25,26,27 };
|
||||
public static final int[] FREE = { 28,29,30,31,32 };
|
||||
public static final int[] CHECKSUM = { 33,34,35,36,37,38,39 };
|
||||
|
||||
protected BinaryMessage mMessage;
|
||||
protected MessageDirection mMessageDirection;
|
||||
protected AliasList mAliasList;
|
||||
protected CRC mCRC;
|
||||
|
||||
public LTRStandardMessage( BinaryMessage message,
|
||||
MessageDirection direction,
|
||||
AliasList list,
|
||||
CRC crc )
|
||||
{
|
||||
mMessage = message;
|
||||
mMessageDirection = direction;
|
||||
mAliasList = list;
|
||||
mCRC = crc;
|
||||
}
|
||||
|
||||
public abstract MessageType getMessageType();
|
||||
|
||||
public boolean isValid()
|
||||
{
|
||||
return mCRC != CRC.FAILED_CRC && mCRC != CRC.FAILED_PARITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append( mDatestampFormatter.format(
|
||||
new Date( System.currentTimeMillis() ) ) );
|
||||
sb.append( " LTR " );
|
||||
sb.append( mMessageDirection.name() );
|
||||
sb.append( " [" );
|
||||
sb.append( mCRC.getAbbreviation() );
|
||||
sb.append( "] " );
|
||||
sb.append( getMessage() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBinaryMessage()
|
||||
{
|
||||
return mMessage.toString();
|
||||
}
|
||||
|
||||
public CRC getCRC()
|
||||
{
|
||||
return mCRC;
|
||||
}
|
||||
|
||||
public int getArea()
|
||||
{
|
||||
return mMessage.getInt( AREA );
|
||||
}
|
||||
|
||||
public int getChannel()
|
||||
{
|
||||
return mMessage.getInt( CHANNEL );
|
||||
}
|
||||
|
||||
public String getChannelFormatted()
|
||||
{
|
||||
return StringUtils.leftPad( String.valueOf( getChannel() ), 2, "0" );
|
||||
}
|
||||
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return mMessage.getInt( HOME_REPEATER );
|
||||
}
|
||||
|
||||
public String getHomeRepeaterFormatted()
|
||||
{
|
||||
return StringUtils.leftPad( String.valueOf( getHomeRepeater() ), 2, "0" );
|
||||
}
|
||||
|
||||
public int getGroup()
|
||||
{
|
||||
return mMessage.getInt( GROUP );
|
||||
}
|
||||
|
||||
public String getGroupFormatted()
|
||||
{
|
||||
return StringUtils.leftPad( String.valueOf( getGroup() ), 3, "0" );
|
||||
}
|
||||
|
||||
public int getFree()
|
||||
{
|
||||
return mMessage.getInt( FREE );
|
||||
}
|
||||
|
||||
public String getFreeFormatted()
|
||||
{
|
||||
return StringUtils.leftPad( String.valueOf( getFree() ), 2, "0" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToID()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( getArea() );
|
||||
sb.append( "-" );
|
||||
sb.append( getHomeRepeaterFormatted() );
|
||||
sb.append( "-" );
|
||||
sb.append( getGroupFormatted() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return mAliasList.getTalkgroupAlias( getToID() );
|
||||
}
|
||||
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
Alias talkgroupAlias = getToIDAlias();
|
||||
|
||||
if( talkgroupAlias != null )
|
||||
{
|
||||
aliases.add( talkgroupAlias );
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
public int getCRCChecksum()
|
||||
{
|
||||
return mMessage.getInt( CHECKSUM );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol()
|
||||
{
|
||||
return DecoderType.LTR_STANDARD.getDisplayString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType()
|
||||
{
|
||||
return getMessageType().getDisplayText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorStatus()
|
||||
{
|
||||
return mCRC.getDisplayText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.ltrstandard.message;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.message.MessageDirection;
|
||||
import io.github.dsheirer.message.MessageType;
|
||||
|
||||
public class UnknownMessage extends LTRStandardMessage
|
||||
{
|
||||
public UnknownMessage(BinaryMessage message, MessageDirection direction, AliasList list, CRC crc )
|
||||
{
|
||||
super( message, direction, list, crc );
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageType getMessageType()
|
||||
{
|
||||
return MessageType.UN_KNWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "UNKNOWN AREA:" );
|
||||
sb.append( getArea() );
|
||||
sb.append( " LCN:" );
|
||||
sb.append( getChannelFormatted() );
|
||||
sb.append( " HOME:" );
|
||||
sb.append( getHomeRepeaterFormatted() );
|
||||
sb.append( " GRP:" );
|
||||
sb.append( getGroupFormatted() );
|
||||
sb.append( " FREE:" );
|
||||
sb.append( getFreeFormatted() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.mdc1200;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
|
||||
public class MDCCallEvent extends CallEvent
|
||||
{
|
||||
public MDCCallEvent( CallEventType callEventType,
|
||||
AliasList aliasList,
|
||||
String fromID,
|
||||
String toID,
|
||||
String details )
|
||||
{
|
||||
super( DecoderType.MDC1200, callEventType,
|
||||
aliasList, fromID, toID, details );
|
||||
}
|
||||
|
||||
private Alias getAlias( String ident )
|
||||
{
|
||||
if( hasAliasList() )
|
||||
{
|
||||
return getAliasList().getMDC1200Alias( ident );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return getAlias( getFromID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return getAlias( getToID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannel()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrequency()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static class Builder
|
||||
{
|
||||
/* Required parameters */
|
||||
private CallEventType mCallEventType;
|
||||
|
||||
/* Optional parameters */
|
||||
private AliasList mAliasList;
|
||||
private String mFromID;
|
||||
private String mToID;
|
||||
private String mDetails;
|
||||
|
||||
public Builder( CallEventType callEventType )
|
||||
{
|
||||
mCallEventType = callEventType;
|
||||
}
|
||||
|
||||
public Builder aliasList( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder from( String val )
|
||||
{
|
||||
mFromID = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder details( String details )
|
||||
{
|
||||
mDetails = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder to( String toID )
|
||||
{
|
||||
mToID = toID;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MDCCallEvent build()
|
||||
{
|
||||
return new MDCCallEvent( this );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor for the builder
|
||||
*/
|
||||
private MDCCallEvent( Builder builder )
|
||||
{
|
||||
this( builder.mCallEventType,
|
||||
builder.mAliasList,
|
||||
builder.mFromID,
|
||||
builder.mToID,
|
||||
builder.mDetails );
|
||||
}
|
||||
|
||||
public static MDCCallEvent
|
||||
getMDCCallEvent( MDCMessage message )
|
||||
{
|
||||
CallEventType type = CallEventType.UNKNOWN;
|
||||
StringBuilder sbDetails = new StringBuilder();
|
||||
sbDetails.append( "MDC-1200 OPCODE:" );
|
||||
sbDetails.append( String.valueOf( message.getOpcode() ) );
|
||||
|
||||
switch( message.getMessageType() )
|
||||
{
|
||||
case ACKNOWLEDGE:
|
||||
type = CallEventType.RESPONSE;
|
||||
break;
|
||||
case ANI:
|
||||
type = CallEventType.ID_ANI;
|
||||
break;
|
||||
case EMERGENCY:
|
||||
type = CallEventType.EMERGENCY;
|
||||
break;
|
||||
case PAGING:
|
||||
type = CallEventType.PAGE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return new MDCCallEvent.Builder( type )
|
||||
.details( sbDetails.toString() )
|
||||
.from( message.getFromID() )
|
||||
.to( message.getToID() )
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.mdc1200;
|
||||
|
||||
import io.github.dsheirer.bits.MessageFramer;
|
||||
import io.github.dsheirer.bits.SyncPattern;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.instrument.tap.stream.BinaryTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatBufferTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatTap;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.dsp.NRZDecoder;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.fsk.FSK2Decoder;
|
||||
import io.github.dsheirer.dsp.fsk.FSK2Decoder.Output;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* MDC1200 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate
|
||||
* complex or floating point samples and output fully framed MDC1200 messages
|
||||
*/
|
||||
public class MDCDecoder extends Decoder implements IFilteredRealBufferListener,
|
||||
Instrumentable
|
||||
{
|
||||
/* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */
|
||||
private static final int sDECIMATED_SAMPLE_RATE = 24000;
|
||||
|
||||
/* Baud or Symbol Rate */
|
||||
private static final int sSYMBOL_RATE = 1200;
|
||||
|
||||
/* Message length */
|
||||
private static final int sMESSAGE_LENGTH = 304;
|
||||
|
||||
/* Instrumentation Taps */
|
||||
private ArrayList<TapGroup> mAvailableTaps;
|
||||
private static final String INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD =
|
||||
"Tap Point: Bandpass Filter > < FSK2 Decoder";
|
||||
private static final String INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER =
|
||||
"Tap Point: FSK2 Decoder > < NRZI Decoder";
|
||||
private static final String INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER =
|
||||
"Tap Point: NRZ Decoder > < Message Framer";
|
||||
|
||||
private FSK2Decoder mFSKDecoder;
|
||||
private HalfBandFilter_RB_RB mDecimationFilter;
|
||||
private RealFIRFilter_RB_RB mBandPassFilter;
|
||||
private NRZDecoder mNRZDecoder;
|
||||
private MessageFramer mMessageFramer;
|
||||
private MDCMessageProcessor mMessageProcessor;
|
||||
|
||||
public MDCDecoder( AliasList aliasList )
|
||||
{
|
||||
/* Decimation filter: 48000 / 2 = 24000 */
|
||||
mDecimationFilter = new HalfBandFilter_RB_RB(
|
||||
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0f, true );
|
||||
|
||||
/* Bandpass filter - predefined */
|
||||
mBandPassFilter = new RealFIRFilter_RB_RB(
|
||||
Filters.FIRBP_1200FSK_24000FS.getCoefficients(), 1.02f );
|
||||
mDecimationFilter.setListener( mBandPassFilter );
|
||||
|
||||
/* 2FSK Decoder */
|
||||
mFSKDecoder = new FSK2Decoder( sDECIMATED_SAMPLE_RATE,
|
||||
sSYMBOL_RATE, Output.NORMAL );
|
||||
mBandPassFilter.setListener( mFSKDecoder );
|
||||
|
||||
/* NRZ Decoder */
|
||||
mNRZDecoder = new NRZDecoder( NRZDecoder.MODE_INVERTED );
|
||||
mFSKDecoder.setListener( mNRZDecoder );
|
||||
|
||||
/* Message Framer */
|
||||
mMessageFramer = new MessageFramer( SyncPattern.MDC1200.getPattern(),
|
||||
sMESSAGE_LENGTH );
|
||||
mNRZDecoder.setListener( mMessageFramer );
|
||||
|
||||
/* Message Processor */
|
||||
mMessageProcessor = new MDCMessageProcessor( aliasList );
|
||||
mMessageFramer.addMessageListener( mMessageProcessor );
|
||||
mMessageProcessor.addMessageListener( this );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mDecimationFilter.dispose();
|
||||
mBandPassFilter.dispose();
|
||||
mFSKDecoder.dispose();
|
||||
mNRZDecoder.dispose();
|
||||
mMessageFramer.dispose();
|
||||
mMessageProcessor.dispose();
|
||||
}
|
||||
|
||||
/* Instrumentation */
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<TapGroup>();
|
||||
|
||||
TapGroup group = new TapGroup( "MDC-1200 Decoder" );
|
||||
|
||||
group.add(
|
||||
new FloatTap( INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD, 0, 0.5f ) );
|
||||
group.add(
|
||||
new BinaryTap( INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER, 0, 0.025f ) );
|
||||
group.add(
|
||||
new BinaryTap( INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER, 0, 0.025f ) );
|
||||
|
||||
mAvailableTaps.add( group );
|
||||
mAvailableTaps.addAll( mFSKDecoder.getTapGroups() );
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
{
|
||||
mFSKDecoder.registerTap( tap );
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD:
|
||||
/* wire the tap between the bandpass filter and the demod */
|
||||
FloatBufferTap floatTap = (FloatBufferTap)tap;
|
||||
mBandPassFilter.setListener( floatTap );
|
||||
floatTap.setListener( mFSKDecoder );
|
||||
break;
|
||||
case INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER:
|
||||
/* wire between FSK2 decoder and NRZ decoder */
|
||||
BinaryTap binTap = (BinaryTap)tap;
|
||||
mFSKDecoder.setListener( binTap );
|
||||
binTap.setListener( mNRZDecoder );
|
||||
break;
|
||||
case INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER:
|
||||
/* wire between demod and framer */
|
||||
BinaryTap binInvTap = (BinaryTap)tap;
|
||||
mNRZDecoder.setListener( binInvTap );
|
||||
binInvTap.setListener( mMessageFramer );
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
{
|
||||
mFSKDecoder.unregisterTap( tap );
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_BANDPASS_FILTER_TO_FSK2_DEMOD:
|
||||
mBandPassFilter.setListener( mFSKDecoder );
|
||||
break;
|
||||
case INSTRUMENT_FSK2_DECODER_TO_NRZ_DECODER:
|
||||
mFSKDecoder.setListener( mNRZDecoder );
|
||||
break;
|
||||
case INSTRUMENT_NRZI_DECODER_TO_MESSAGE_FRAMER:
|
||||
mNRZDecoder.setListener( mMessageFramer );
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.MDC1200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getFilteredRealBufferListener()
|
||||
{
|
||||
return mDecimationFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.mdc1200;
|
||||
|
||||
import io.github.dsheirer.channel.metadata.AliasedStringAttributeMonitor;
|
||||
import io.github.dsheirer.channel.metadata.Attribute;
|
||||
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.util.StringUtils;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class MDCDecoderState extends DecoderState
|
||||
{
|
||||
private TreeSet<String> mIdents = new TreeSet<String>();
|
||||
private TreeSet<String> mEmergencyIdents = new TreeSet<String>();
|
||||
|
||||
private AliasedStringAttributeMonitor mFromAttribute;
|
||||
private String mMessage;
|
||||
private String mMessageType;
|
||||
|
||||
public MDCDecoderState(AliasList aliasList)
|
||||
{
|
||||
super(aliasList);
|
||||
|
||||
mFromAttribute = new AliasedStringAttributeMonitor(Attribute.SECONDARY_ADDRESS_FROM,
|
||||
getAttributeChangeRequestListener(), getAliasList(), AliasIDType.MDC1200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.MDC1200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mIdents.clear();
|
||||
mEmergencyIdents.clear();
|
||||
|
||||
resetState();
|
||||
}
|
||||
|
||||
private void resetState()
|
||||
{
|
||||
mFromAttribute.reset();
|
||||
mMessage = null;
|
||||
mMessageType = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Message message)
|
||||
{
|
||||
if(message instanceof MDCMessage)
|
||||
{
|
||||
MDCMessage mdc = (MDCMessage) message;
|
||||
|
||||
mIdents.add(mdc.getUnitID());
|
||||
|
||||
if(mdc.isEmergency())
|
||||
{
|
||||
mEmergencyIdents.add(mdc.getUnitID());
|
||||
}
|
||||
|
||||
mFromAttribute.process(mdc.getFromID());
|
||||
|
||||
MDCMessageType type = mdc.getMessageType();
|
||||
|
||||
setMessageType(type.getLabel());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("OPCODE ");
|
||||
sb.append(String.valueOf(mdc.getOpcode()));
|
||||
|
||||
if(mdc.isBOT())
|
||||
{
|
||||
sb.append(" TYPE:BOT");
|
||||
}
|
||||
|
||||
if(mdc.isEOT())
|
||||
{
|
||||
sb.append(" TYPE:EOT");
|
||||
}
|
||||
|
||||
setMessage(sb.toString());
|
||||
|
||||
MDCCallEvent event = MDCCallEvent.getMDCCallEvent(mdc);
|
||||
event.setAliasList(getAliasList());
|
||||
broadcast(event);
|
||||
|
||||
switch(type)
|
||||
{
|
||||
case ANI:
|
||||
broadcast(new DecoderStateEvent(this, Event.DECODE, State.CALL));
|
||||
break;
|
||||
case ACKNOWLEDGE:
|
||||
case EMERGENCY:
|
||||
case PAGING:
|
||||
case STATUS:
|
||||
default:
|
||||
broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA));
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
public void setMessage(String message)
|
||||
{
|
||||
if(!StringUtils.isEqual(mMessage, message))
|
||||
{
|
||||
mMessage = message;
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.MESSAGE, mMessage));
|
||||
}
|
||||
}
|
||||
|
||||
public String getMessageType()
|
||||
{
|
||||
return mMessageType;
|
||||
}
|
||||
|
||||
public void setMessageType(String type)
|
||||
{
|
||||
if(!StringUtils.isEqual(mMessageType, type))
|
||||
{
|
||||
mMessageType = type;
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.MESSAGE_TYPE, "MDC:" + mMessageType));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("=============================\n");
|
||||
sb.append("Decoder:\tMDC-1200\n\n");
|
||||
sb.append("MDC-1200 Idents\n");
|
||||
|
||||
if(mIdents.isEmpty())
|
||||
{
|
||||
sb.append(" None\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Iterator<String> it = mIdents.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
String ident = it.next();
|
||||
|
||||
sb.append(" ");
|
||||
sb.append(ident);
|
||||
sb.append(" ");
|
||||
|
||||
if(hasAliasList())
|
||||
{
|
||||
Alias alias = getAliasList().getMDC1200Alias(ident);
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
sb.append(alias.getName());
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("MDC-1200 Emergency Idents\n");
|
||||
|
||||
if(mEmergencyIdents.isEmpty())
|
||||
{
|
||||
sb.append(" None\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
Iterator<String> it = mEmergencyIdents.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
String ident = it.next();
|
||||
|
||||
sb.append(" ");
|
||||
sb.append(ident);
|
||||
sb.append(" ");
|
||||
|
||||
if(hasAliasList())
|
||||
{
|
||||
Alias alias = getAliasList().getMDC1200Alias(ident);
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
sb.append(alias.getName());
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
/* No initialization required */
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to reset events issued by the channel state
|
||||
*/
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case RESET:
|
||||
resetState();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.mdc1200;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.map.Plottable;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class MDCMessage extends Message
|
||||
{
|
||||
private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd HHmmss" );
|
||||
|
||||
private static int[] sSYNC1 = { 0,1,2,3,4,5,6,7,8,9,
|
||||
10,11,12,13,14,15,16,17,18,19,
|
||||
20,21,22,23,24,25,26,27,28,29,
|
||||
30,31,32,33,34,35,36,37,38,39 };
|
||||
|
||||
private static int[] sOPCODE = { 47,46,45,44,43,42,41,40 };
|
||||
private static int sANI_FLAG = 40;
|
||||
private static int sDIRECTION_FLAG = 45;
|
||||
private static int sACKNOWLEDGE_REQUIRED_FLAG = 46;
|
||||
private static int sPACKET_TYPE_FLAG = 47;
|
||||
private static int sEMERGENCY_FLAG = 48;
|
||||
private static int[] sARGUMENT = { 49,50,51,52,53,54 };
|
||||
private static int sBOT_EOT_FLAG = 55;
|
||||
private static int[] sDIGIT_2 = { 59,58,57,56 };
|
||||
private static int[] sDIGIT_1 = { 63,62,61,60 };
|
||||
private static int[] sDIGIT_4 = { 67,66,65,64 };
|
||||
private static int[] sDIGIT_3 = { 71,70,69,68 };
|
||||
|
||||
private BinaryMessage mMessage;
|
||||
private AliasList mAliasList;
|
||||
|
||||
public MDCMessage( BinaryMessage message, AliasList list )
|
||||
{
|
||||
mMessage = message;
|
||||
mAliasList = list;
|
||||
}
|
||||
|
||||
public boolean isValid()
|
||||
{
|
||||
//TODO: add CRC and/or convolution decoding/repair
|
||||
return true;
|
||||
}
|
||||
|
||||
public PacketType getPacketType()
|
||||
{
|
||||
if( mMessage.get( sPACKET_TYPE_FLAG ) )
|
||||
{
|
||||
return PacketType.DATA;
|
||||
}
|
||||
else
|
||||
{
|
||||
return PacketType.CMND;
|
||||
}
|
||||
}
|
||||
|
||||
public Acknowledge getResponse()
|
||||
{
|
||||
if( mMessage.get( sACKNOWLEDGE_REQUIRED_FLAG ) )
|
||||
{
|
||||
return Acknowledge.YES;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Acknowledge.NO;
|
||||
}
|
||||
}
|
||||
|
||||
public Direction getDirection()
|
||||
{
|
||||
if( mMessage.get( sDIRECTION_FLAG ) )
|
||||
{
|
||||
return Direction.OUT;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Direction.IN;
|
||||
}
|
||||
}
|
||||
|
||||
public int getOpcode()
|
||||
{
|
||||
return mMessage.getInt( sOPCODE );
|
||||
}
|
||||
|
||||
public int getArgument()
|
||||
{
|
||||
return mMessage.getInt( sARGUMENT );
|
||||
}
|
||||
|
||||
public boolean isEmergency()
|
||||
{
|
||||
return mMessage.get( sEMERGENCY_FLAG );
|
||||
}
|
||||
|
||||
|
||||
public boolean isANI()
|
||||
{
|
||||
return mMessage.get( sANI_FLAG );
|
||||
}
|
||||
|
||||
public boolean isBOT()
|
||||
{
|
||||
return mMessage.get( sBOT_EOT_FLAG );
|
||||
}
|
||||
|
||||
public boolean isEOT()
|
||||
{
|
||||
return !mMessage.get( sBOT_EOT_FLAG );
|
||||
}
|
||||
|
||||
public String getUnitID()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( Integer.toHexString( mMessage.getInt( sDIGIT_1 ) ).toUpperCase() );
|
||||
sb.append( Integer.toHexString( mMessage.getInt( sDIGIT_2 ) ).toUpperCase() );
|
||||
sb.append( Integer.toHexString( mMessage.getInt( sDIGIT_3 ) ).toUpperCase() );
|
||||
sb.append( Integer.toHexString( mMessage.getInt( sDIGIT_4 ) ).toUpperCase() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public MDCMessageType getMessageType()
|
||||
{
|
||||
switch( getOpcode() )
|
||||
{
|
||||
case 0:
|
||||
if( isEmergency() )
|
||||
{
|
||||
return MDCMessageType.EMERGENCY;
|
||||
}
|
||||
case 1:
|
||||
return MDCMessageType.ANI;
|
||||
default:
|
||||
return MDCMessageType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( mSDF.format( new Date( System.currentTimeMillis() ) ) );
|
||||
|
||||
sb.append( getMessage() );
|
||||
|
||||
sb.append( " [" + mMessage.toString() + "]" );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads spaces onto the end of the value to make it 'places' long
|
||||
*/
|
||||
public String pad( String value, int places, String padCharacter )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( value );
|
||||
|
||||
while( sb.length() < places )
|
||||
{
|
||||
sb.append( padCharacter );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads an integer value with additional zeroes to make it decimalPlaces long
|
||||
*/
|
||||
public String format( int number, int decimalPlaces )
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() );
|
||||
|
||||
for( int x = 0; x < paddingRequired; x++)
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
|
||||
sb.append( number );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings( "unused" )
|
||||
private int getInt( int[] bits )
|
||||
{
|
||||
int retVal = 0;
|
||||
|
||||
for( int x = 0; x < bits.length; x++ )
|
||||
{
|
||||
if( mMessage.get( bits[ x ] ) )
|
||||
{
|
||||
retVal += 1<<x;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBinaryMessage()
|
||||
{
|
||||
return mMessage.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol()
|
||||
{
|
||||
return "MDC-1200";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
return getUnitID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return mAliasList.getMDC1200Alias( getFromID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToID()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "MDC1200 UNIT:" + getUnitID() );
|
||||
if( isEmergency() )
|
||||
{
|
||||
sb.append( " **EMERGENCY**" );
|
||||
}
|
||||
|
||||
if( isBOT() )
|
||||
{
|
||||
sb.append( " BOT" );
|
||||
}
|
||||
|
||||
if( isEOT() )
|
||||
{
|
||||
sb.append( " EOT" );
|
||||
}
|
||||
|
||||
sb.append( " OPCODE:" + format( getOpcode(), 2 ) );
|
||||
sb.append( " ARG:" + format( getArgument(), 3 ) );
|
||||
sb.append( " TYPE:" + getPacketType().toString() );
|
||||
sb.append( " ACK:" + getResponse().toString() );
|
||||
sb.append( " DIR:" + pad( getDirection().toString(), 3, " " ) );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorStatus()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plottable getPlottable()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a listing of aliases contained in the message.
|
||||
*/
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<Alias>();
|
||||
|
||||
Alias from = getFromIDAlias();
|
||||
|
||||
if( from != null )
|
||||
{
|
||||
aliases.add( from );
|
||||
}
|
||||
|
||||
Alias to = getToIDAlias();
|
||||
|
||||
if( to != null )
|
||||
{
|
||||
aliases.add( to );
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
private enum PacketType { CMND, DATA };
|
||||
private enum Acknowledge { YES, NO };
|
||||
private enum Direction { IN, OUT };
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package io.github.dsheirer.module.decode.mdc1200;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class MDCMessageFilter extends Filter<Message>
|
||||
{
|
||||
private HashMap<MDCMessageType, FilterElement<MDCMessageType>> mElements =
|
||||
new HashMap<MDCMessageType, FilterElement<MDCMessageType>>();
|
||||
|
||||
public MDCMessageFilter()
|
||||
{
|
||||
super( "MDC-1200 Message Filter" );
|
||||
|
||||
for( MDCMessageType type: MDCMessageType.values() )
|
||||
{
|
||||
if( type != MDCMessageType.UNKNOWN )
|
||||
{
|
||||
mElements.put( type, new FilterElement<MDCMessageType>( type ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes( Message message )
|
||||
{
|
||||
if( mEnabled && canProcess( message ) )
|
||||
{
|
||||
MDCMessage mdc = (MDCMessage)message;
|
||||
|
||||
if( mElements.containsKey( mdc.getMessageType() ) )
|
||||
{
|
||||
return mElements.get( mdc.getMessageType() ).isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess( Message message )
|
||||
{
|
||||
return message instanceof MDCMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return new ArrayList<FilterElement<?>>( mElements.values() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.mdc1200;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
|
||||
public class MDCMessageProcessor implements Listener<BinaryMessage>
|
||||
{
|
||||
private static int sMESSAGE_LENGTH = 112;
|
||||
|
||||
private Broadcaster<Message> mBroadcaster = new Broadcaster<Message>();
|
||||
private AliasList mAliasList;
|
||||
|
||||
public MDCMessageProcessor( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mBroadcaster.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( BinaryMessage buffer )
|
||||
{
|
||||
/**
|
||||
* De-interleave the 112 bits of message one, starting after the sync
|
||||
* pattern
|
||||
*/
|
||||
deinterleave( buffer, 40 );
|
||||
|
||||
/**
|
||||
* For simplicity, we also deinterleave message 2, which may or may not
|
||||
* be there, starting after the initial sync ( 0 - 39 ) and the initial
|
||||
* message ( 40 - 151 ) and the second sync ( 152 - 191 ).
|
||||
*/
|
||||
deinterleave( buffer, 192 );
|
||||
|
||||
/**
|
||||
* Wrap the buffer in a message along with the designated alias list
|
||||
* and send it on its merry way
|
||||
*/
|
||||
MDCMessage message = new MDCMessage( buffer, mAliasList );
|
||||
|
||||
mBroadcaster.receive( message );
|
||||
}
|
||||
|
||||
public void addMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mBroadcaster.addListener( listener );
|
||||
}
|
||||
|
||||
public void removeMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mBroadcaster.removeListener( listener );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes NRZ-I encoding from the entire buffer. Assumes that the initial
|
||||
* bit will be a 0 after decode, despite what is currently in the buffer,
|
||||
* in order to maintain correct polarity of the decoded message.
|
||||
*/
|
||||
private void removeNRZIEncoding( BinaryMessage buffer )
|
||||
{
|
||||
/**
|
||||
* Clear bit position 0, the first bit of the sync pattern, to start
|
||||
* the decode with the correct polarity
|
||||
*/
|
||||
buffer.clear( 0 );
|
||||
|
||||
/**
|
||||
* Set each bit according to the state of the previous bit using XOR
|
||||
* as follows:
|
||||
*
|
||||
* 0 = same as previous state
|
||||
* 1 = inverse of previous state
|
||||
*/
|
||||
for( int x = 1; x < buffer.size(); x++ )
|
||||
{
|
||||
if( buffer.get( x - 1 ) ^ buffer.get( x ) )
|
||||
{
|
||||
buffer.set( x );
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.clear( x );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deinterleaves a 112-bit packet, starting at the offset into the buffer
|
||||
*/
|
||||
private void deinterleave( BinaryMessage buffer, int offset )
|
||||
{
|
||||
if( buffer.size() < sMESSAGE_LENGTH + offset )
|
||||
{
|
||||
throw new IllegalArgumentException( "MDCMessageProcessor - "
|
||||
+ "cannot deinterleave message - message buffer too short" );
|
||||
}
|
||||
|
||||
BinaryMessage deinterleaved = new BinaryMessage( 112 );
|
||||
|
||||
int deinterleavedPointer = 0;
|
||||
|
||||
for( int column = 0; column < 16; column++ )
|
||||
{
|
||||
for( int row = 0; row < 7; row++ )
|
||||
{
|
||||
if( buffer.get( ( row * 16 ) + column + offset ) )
|
||||
{
|
||||
deinterleaved.set( deinterleavedPointer );
|
||||
}
|
||||
|
||||
deinterleavedPointer++;
|
||||
}
|
||||
}
|
||||
|
||||
//Overlay the deinterleaved bits back onto the source message and
|
||||
//return it
|
||||
buffer.clear( offset, offset + sMESSAGE_LENGTH );
|
||||
|
||||
for( int x = 0; x < sMESSAGE_LENGTH; x++ )
|
||||
{
|
||||
if( deinterleaved.get( x ) )
|
||||
{
|
||||
buffer.set( x + offset );
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.clear( x + offset );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.mdc1200;
|
||||
|
||||
public enum MDCMessageType
|
||||
{
|
||||
ACKNOWLEDGE( "Acknowledge" ),
|
||||
ANI( "ANI" ),
|
||||
EMERGENCY( "Emergency" ),
|
||||
PAGING( "Paging" ),
|
||||
STATUS( "Status" ),
|
||||
UNKNOWN( "Unk" );
|
||||
|
||||
private String mLabel;
|
||||
|
||||
private MDCMessageType( String label )
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.mpt1327;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327Decoder.Sync;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
|
||||
public class DecodeConfigMPT1327 extends DecodeConfiguration
|
||||
{
|
||||
private String mChannelMapName;
|
||||
private Sync mSync = Sync.NORMAL;
|
||||
|
||||
private int mCallTimeout = DEFAULT_CALL_TIMEOUT_SECONDS;
|
||||
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
|
||||
|
||||
public DecodeConfigMPT1327()
|
||||
{
|
||||
super( DecoderType.MPT1327 );
|
||||
}
|
||||
|
||||
@XmlElement
|
||||
public String getChannelMapName()
|
||||
{
|
||||
return mChannelMapName;
|
||||
}
|
||||
|
||||
public void setChannelMapName( String name )
|
||||
{
|
||||
mChannelMapName = name;
|
||||
}
|
||||
|
||||
@XmlAttribute( name="sync" )
|
||||
public Sync getSync()
|
||||
{
|
||||
return mSync;
|
||||
}
|
||||
|
||||
public void setSync( Sync sync )
|
||||
{
|
||||
mSync = sync;
|
||||
}
|
||||
|
||||
@XmlAttribute( name="call_timeout" )
|
||||
public int getCallTimeout()
|
||||
{
|
||||
return mCallTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the call timeout value in seconds ( 10 - 600 );
|
||||
* @param timeout
|
||||
*/
|
||||
public void setCallTimeout( int timeout )
|
||||
{
|
||||
if( CALL_TIMEOUT_MINIMUM <= timeout && timeout <= CALL_TIMEOUT_MAXIMUM )
|
||||
{
|
||||
mCallTimeout = timeout;
|
||||
}
|
||||
else
|
||||
{
|
||||
mCallTimeout = DEFAULT_CALL_TIMEOUT_SECONDS;
|
||||
}
|
||||
}
|
||||
|
||||
@XmlAttribute( name="traffic_channel_pool_size" )
|
||||
public int getTrafficChannelPoolSize()
|
||||
{
|
||||
return mTrafficChannelPoolSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the traffic channel pool size which is the maximum number of
|
||||
* simultaneous traffic channels that can be allocated.
|
||||
*
|
||||
* This limits the maximum calls so that busy systems won't cause more
|
||||
* traffic channels to be allocated than the decoder/software/host computer
|
||||
* can support.
|
||||
*/
|
||||
public void setTrafficChannelPoolSize( int size )
|
||||
{
|
||||
mTrafficChannelPoolSize = size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.mpt1327;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
|
||||
public class MPT1327CallEvent extends CallEvent
|
||||
{
|
||||
private String mChannel;
|
||||
private long mFrequency;
|
||||
|
||||
public MPT1327CallEvent( CallEventType callEventType,
|
||||
AliasList aliasList,
|
||||
String fromID,
|
||||
String toID,
|
||||
String channel,
|
||||
long frequency,
|
||||
String details )
|
||||
{
|
||||
super( DecoderType.MPT1327, callEventType,
|
||||
aliasList, fromID, toID, details );
|
||||
|
||||
mChannel = channel;
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
private Alias getAlias(String mpt1327Talkgroup )
|
||||
{
|
||||
if( hasAliasList() )
|
||||
{
|
||||
return getAliasList().getMPT1327Alias( mpt1327Talkgroup );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return getAlias( getFromID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return getAlias( getToID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannel()
|
||||
{
|
||||
return mChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
public static class Builder
|
||||
{
|
||||
/* Required parameters */
|
||||
private CallEventType mCallEventType;
|
||||
|
||||
/* Optional parameters */
|
||||
private AliasList mAliasList;
|
||||
private String mFromID;
|
||||
private String mToID;
|
||||
private String mDetails;
|
||||
private String mChannel;
|
||||
private long mFrequency;
|
||||
|
||||
public Builder( CallEventType callEventType )
|
||||
{
|
||||
mCallEventType = callEventType;
|
||||
}
|
||||
|
||||
public Builder aliasList( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder from( String val )
|
||||
{
|
||||
mFromID = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder channel( String channel )
|
||||
{
|
||||
mChannel = channel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder details( String details )
|
||||
{
|
||||
mDetails = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder frequency( long frequency )
|
||||
{
|
||||
mFrequency = frequency;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder to( String toID )
|
||||
{
|
||||
mToID = toID;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MPT1327CallEvent build()
|
||||
{
|
||||
return new MPT1327CallEvent( this );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor for the builder
|
||||
*/
|
||||
private MPT1327CallEvent( Builder builder )
|
||||
{
|
||||
this( builder.mCallEventType,
|
||||
builder.mAliasList,
|
||||
builder.mFromID,
|
||||
builder.mToID,
|
||||
builder.mChannel,
|
||||
builder.mFrequency,
|
||||
builder.mDetails );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.mpt1327;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.MessageFramer;
|
||||
import io.github.dsheirer.bits.SyncPattern;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB;
|
||||
import io.github.dsheirer.dsp.fsk.FSK2Decoder;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.instrument.tap.stream.BinaryTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatBufferTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatTap;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* MPT1327 Decoder - 1200 baud 2FSK decoder that can process 48k sample rate
|
||||
* complex or floating point samples and output fully framed MPT1327 control and
|
||||
* traffic messages.
|
||||
*/
|
||||
public class MPT1327Decoder extends Decoder implements IFilteredRealBufferListener, Instrumentable
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MPT1327Decoder.class);
|
||||
|
||||
private static final FIRFilterSpecification HIGH_PASS_SPECIFICATION = FIRFilterSpecification.highPassBuilder()
|
||||
.sampleRate( 24000 )
|
||||
.stopBandCutoff( 800 )
|
||||
.stopBandAmplitude( 0.0 )
|
||||
.stopBandRipple( 0.03 )
|
||||
.passBandStart( 1000 )
|
||||
.passBandAmplitude( 1.0 )
|
||||
.passBandRipple( 0.08 )
|
||||
.build();
|
||||
|
||||
private static float[] HIGHPASS_FILTER;
|
||||
|
||||
static
|
||||
{
|
||||
try
|
||||
{
|
||||
HIGHPASS_FILTER = FilterFactory.getTaps(HIGH_PASS_SPECIFICATION);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't design MPT1327 highpass filter");
|
||||
}
|
||||
}
|
||||
|
||||
/* Decimated sample rate ( 48,000 / 2 = 24,000 ) feeding the decoder */
|
||||
private static final int sDECIMATED_SAMPLE_RATE = 24000;
|
||||
|
||||
/* Baud or Symbol Rate */
|
||||
private static final int sSYMBOL_RATE = 1200;
|
||||
|
||||
/* Message length -- longest possible message is:
|
||||
* 4xREVS + 16xSYNC + 64xADD1 + 64xDCW1 + 64xDCW2 + 64xDCW3 + 64xDCW4 */
|
||||
private static final int sMESSAGE_LENGTH = 350;
|
||||
|
||||
/* Instrumentation Taps */
|
||||
private static final String INSTRUMENT_HB1_FILTER_TO_HIGH_PASS =
|
||||
"Tap Point: Half-band Decimation Filter > < Low Pass Filter";
|
||||
private static final String INSTRUMENT_HIGH_PASS_TO_DECODER =
|
||||
"Tap Point: Low Pass Filter > < Decoder";
|
||||
private static final String INSTRUMENT_DECODER_TO_FRAMER =
|
||||
"Tap Point: Decoder > < Sync Detect/Message Framer";
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
|
||||
private HalfBandFilter_RB_RB mDecimationFilter;
|
||||
private RealFIRFilter_RB_RB mHighPassFilter;
|
||||
private FSK2Decoder mFSKDecoder;
|
||||
private Broadcaster<Boolean> mSymbolBroadcaster;
|
||||
private MessageFramer mControlMessageFramer;
|
||||
private MessageFramer mTrafficMessageFramer;
|
||||
private MPT1327MessageProcessor mMessageProcessor;
|
||||
|
||||
public MPT1327Decoder(AliasList aliasList, Sync sync)
|
||||
{
|
||||
/**
|
||||
* Normal: 2FSK Decoder with inverted output
|
||||
* French: 2FSK Decoder with normal output
|
||||
*/
|
||||
if(sync == Sync.NORMAL)
|
||||
{
|
||||
mFSKDecoder = new FSK2Decoder(24000, 1200, FSK2Decoder.Output.INVERTED);
|
||||
}
|
||||
else if(sync == Sync.FRENCH)
|
||||
{
|
||||
mFSKDecoder = new FSK2Decoder(24000, 1200, FSK2Decoder.Output.NORMAL);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalArgumentException("MPT1327 Decoder - unrecognized Sync type");
|
||||
}
|
||||
|
||||
/* Decimation filter - 48000 / 2 = 24000 output */
|
||||
mDecimationFilter = new HalfBandFilter_RB_RB(Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 1.0002f, true);
|
||||
|
||||
//High-pass filter the audio to remove any DC component or CTCSS tones. The audio has already been low-pass
|
||||
//filtered by the AudioDemodulation filter cutoff of 3400 Hz.
|
||||
mHighPassFilter = new RealFIRFilter_RB_RB(HIGHPASS_FILTER, 1.0f);
|
||||
mDecimationFilter.setListener(mHighPassFilter);
|
||||
|
||||
mHighPassFilter.setListener(mFSKDecoder);
|
||||
|
||||
mSymbolBroadcaster = new Broadcaster<Boolean>();
|
||||
mFSKDecoder.setListener(mSymbolBroadcaster);
|
||||
|
||||
/* Message framer for control channel messages */
|
||||
mControlMessageFramer = new MessageFramer(sync.getControlSyncPattern().getPattern(), sMESSAGE_LENGTH);
|
||||
mSymbolBroadcaster.addListener(mControlMessageFramer);
|
||||
|
||||
/* Message framer for traffic channel massages */
|
||||
mTrafficMessageFramer = new MessageFramer(sync.getTrafficSyncPattern().getPattern(), sMESSAGE_LENGTH);
|
||||
mSymbolBroadcaster.addListener(mTrafficMessageFramer);
|
||||
|
||||
/* Fully decoded and framed messages processor */
|
||||
mMessageProcessor = new MPT1327MessageProcessor(aliasList);
|
||||
mMessageProcessor.setMessageListener(this);
|
||||
mControlMessageFramer.addMessageListener(mMessageProcessor);
|
||||
mTrafficMessageFramer.addMessageListener(mMessageProcessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getFilteredRealBufferListener()
|
||||
{
|
||||
return mDecimationFilter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.MPT1327;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup method
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mSymbolBroadcaster.dispose();
|
||||
|
||||
mControlMessageFramer.dispose();
|
||||
|
||||
mFSKDecoder.dispose();
|
||||
|
||||
mDecimationFilter.dispose();
|
||||
|
||||
mMessageProcessor.dispose();
|
||||
|
||||
mTrafficMessageFramer.dispose();
|
||||
}
|
||||
|
||||
/* Instrumentation Taps */
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if(mAvailableTaps == null)
|
||||
{
|
||||
mAvailableTaps = new ArrayList<TapGroup>();
|
||||
|
||||
TapGroup group = new TapGroup("MPT-1327 Decoder");
|
||||
|
||||
group.add(new FloatTap(INSTRUMENT_HB1_FILTER_TO_HIGH_PASS, 0, 0.5f));
|
||||
group.add(new FloatTap(INSTRUMENT_HIGH_PASS_TO_DECODER, 0, 0.5f));
|
||||
group.add(new BinaryTap(INSTRUMENT_DECODER_TO_FRAMER, 0, 0.025f));
|
||||
|
||||
mAvailableTaps.add(group);
|
||||
|
||||
/* Add the taps from the FSK decoder */
|
||||
mAvailableTaps.addAll(mFSKDecoder.getTapGroups());
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap(Tap tap)
|
||||
{
|
||||
/* Send request to decoder */
|
||||
mFSKDecoder.registerTap(tap);
|
||||
|
||||
switch(tap.getName())
|
||||
{
|
||||
case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS:
|
||||
FloatBufferTap hb1Tap = (FloatBufferTap) tap;
|
||||
mDecimationFilter.setListener(hb1Tap);
|
||||
hb1Tap.setListener(mHighPassFilter);
|
||||
break;
|
||||
case INSTRUMENT_HIGH_PASS_TO_DECODER:
|
||||
FloatBufferTap lowTap = (FloatBufferTap) tap;
|
||||
mHighPassFilter.setListener(lowTap);
|
||||
lowTap.setListener(mFSKDecoder);
|
||||
break;
|
||||
case INSTRUMENT_DECODER_TO_FRAMER:
|
||||
BinaryTap decoderTap = (BinaryTap) tap;
|
||||
mFSKDecoder.setListener(decoderTap);
|
||||
decoderTap.setListener(mSymbolBroadcaster);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap(Tap tap)
|
||||
{
|
||||
mFSKDecoder.unregisterTap(tap);
|
||||
|
||||
switch(tap.getName())
|
||||
{
|
||||
case INSTRUMENT_HB1_FILTER_TO_HIGH_PASS:
|
||||
mDecimationFilter.setListener(mHighPassFilter);
|
||||
break;
|
||||
case INSTRUMENT_HIGH_PASS_TO_DECODER:
|
||||
mHighPassFilter.setListener(mFSKDecoder);
|
||||
break;
|
||||
case INSTRUMENT_DECODER_TO_FRAMER:
|
||||
mFSKDecoder.setListener(mSymbolBroadcaster);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Sync
|
||||
{
|
||||
NORMAL("Normal", SyncPattern.MPT1327_CONTROL, SyncPattern.MPT1327_TRAFFIC),
|
||||
FRENCH("French", SyncPattern.MPT1327_CONTROL_FRENCH, SyncPattern.MPT1327_TRAFFIC_FRENCH);
|
||||
|
||||
private String mLabel;
|
||||
private SyncPattern mControlSyncPattern;
|
||||
private SyncPattern mTrafficSyncPattern;
|
||||
|
||||
private Sync(String label, SyncPattern controlPattern, SyncPattern trafficPattern)
|
||||
{
|
||||
mLabel = label;
|
||||
mControlSyncPattern = controlPattern;
|
||||
mTrafficSyncPattern = trafficPattern;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public SyncPattern getControlSyncPattern()
|
||||
{
|
||||
return mControlSyncPattern;
|
||||
}
|
||||
|
||||
public SyncPattern getTrafficSyncPattern()
|
||||
{
|
||||
return mTrafficSyncPattern;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return getLabel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
mControlMessageFramer.reset();
|
||||
mTrafficMessageFramer.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.mpt1327;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMap;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMapManagerFrame;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EditorValidationException;
|
||||
import io.github.dsheirer.gui.editor.ValidatingEditor;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.List;
|
||||
|
||||
public class MPT1327DecoderEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JCheckBox mAFC;
|
||||
private JSlider mAFCMaximumCorrection;
|
||||
private JComboBox<ChannelMap> mComboChannelMaps;
|
||||
private JLabel mCallTimeoutLabel;
|
||||
private JSlider mCallTimeout;
|
||||
private JLabel mTrafficChannelPoolSizeLabel;
|
||||
private JSlider mTrafficChannelPoolSize;
|
||||
private ChannelMapModel mChannelMapModel;
|
||||
|
||||
public MPT1327DecoderEditor(ChannelMapModel channelMapModel)
|
||||
{
|
||||
mChannelMapModel = channelMapModel;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0,wrap 4", "[right][grow,fill][right][grow,fill]", ""));
|
||||
|
||||
mAFC = new JCheckBox("AFC: 3000 Hz");
|
||||
mAFC.setEnabled(false);
|
||||
mAFC.setToolTipText("AFC automatically adjusts the center frequency of the channel to "
|
||||
+ "correct/compensate for inaccuracies and frequency drift in the tuner");
|
||||
mAFC.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent arg0)
|
||||
{
|
||||
setModified(true);
|
||||
|
||||
if(mAFC.isSelected() && !mAFCMaximumCorrection.isEnabled())
|
||||
{
|
||||
mAFCMaximumCorrection.setEnabled(true);
|
||||
}
|
||||
else if(!mAFC.isSelected() && mAFCMaximumCorrection.isEnabled())
|
||||
{
|
||||
mAFCMaximumCorrection.setEnabled(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
add(mAFC);
|
||||
|
||||
mAFCMaximumCorrection = new JSlider(0, 7000, 3000);
|
||||
mAFCMaximumCorrection.setEnabled(false);
|
||||
mAFCMaximumCorrection.setToolTipText("Maximum AFC frequency correction (0 - 15kHz)");
|
||||
mAFCMaximumCorrection.setMajorTickSpacing(2000);
|
||||
mAFCMaximumCorrection.setMinorTickSpacing(1000);
|
||||
mAFCMaximumCorrection.setPaintTicks(true);
|
||||
|
||||
mAFCMaximumCorrection.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mAFC.setText("AFC: " + mAFCMaximumCorrection.getValue() + " Hz");
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
add(mAFCMaximumCorrection);
|
||||
|
||||
JButton channelMaps = new JButton("Channel Maps");
|
||||
channelMaps.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
final ChannelMapManagerFrame manager =
|
||||
new ChannelMapManagerFrame(mChannelMapModel);
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
manager.setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
add(channelMaps, "span 2");
|
||||
|
||||
List<ChannelMap> maps = mChannelMapModel.getChannelMaps();
|
||||
ChannelMap[] mapArray = maps.toArray(new ChannelMap[maps.size()]);
|
||||
|
||||
mComboChannelMaps = new JComboBox<ChannelMap>();
|
||||
mComboChannelMaps.setModel(new DefaultComboBoxModel<ChannelMap>(mapArray));
|
||||
mComboChannelMaps.setEnabled(false);
|
||||
add(new JLabel("Channel Map:"));
|
||||
add(mComboChannelMaps, "span 3,grow");
|
||||
|
||||
mTrafficChannelPoolSize = new JSlider(JSlider.HORIZONTAL, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MINIMUM,
|
||||
DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MAXIMUM, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT);
|
||||
mTrafficChannelPoolSize.setEnabled(false);
|
||||
mTrafficChannelPoolSize.setMajorTickSpacing(10);
|
||||
mTrafficChannelPoolSize.setMinorTickSpacing(5);
|
||||
mTrafficChannelPoolSize.setPaintTicks(true);
|
||||
mTrafficChannelPoolSizeLabel = new JLabel("Traffic Pool: " +
|
||||
mTrafficChannelPoolSize.getValue() + " ");
|
||||
|
||||
mTrafficChannelPoolSize.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mTrafficChannelPoolSizeLabel.setText("Traffic Pool: " +
|
||||
mTrafficChannelPoolSize.getValue());
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
|
||||
add(mTrafficChannelPoolSizeLabel);
|
||||
add(mTrafficChannelPoolSize);
|
||||
|
||||
mCallTimeout = new JSlider(JSlider.HORIZONTAL, DecodeConfiguration.CALL_TIMEOUT_MINIMUM,
|
||||
DecodeConfiguration.CALL_TIMEOUT_MAXIMUM, DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS);
|
||||
mCallTimeout.setEnabled(false);
|
||||
mCallTimeout.setMajorTickSpacing(100);
|
||||
mCallTimeout.setMinorTickSpacing(50);
|
||||
mCallTimeout.setPaintTicks(true);
|
||||
|
||||
mCallTimeoutLabel = new JLabel("Timeout: " + mCallTimeout.getValue() + " ");
|
||||
|
||||
mCallTimeout.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mCallTimeoutLabel.setText("Timeout: " + mCallTimeout.getValue());
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
|
||||
add(mCallTimeoutLabel);
|
||||
add(mCallTimeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if(hasItem() && isModified())
|
||||
{
|
||||
DecodeConfigMPT1327 mpt = new DecodeConfigMPT1327();
|
||||
mpt.setAFC(mAFC.isSelected());
|
||||
mpt.setAFCMaximumCorrection(mAFCMaximumCorrection.getValue());
|
||||
mpt.setTrafficChannelPoolSize(mTrafficChannelPoolSize.getValue());
|
||||
mpt.setCallTimeout(mCallTimeout.getValue());
|
||||
|
||||
if(mComboChannelMaps.getSelectedItem() != null)
|
||||
{
|
||||
ChannelMap map = (ChannelMap)mComboChannelMaps.getSelectedItem();
|
||||
|
||||
mpt.setChannelMapName(map.getName());
|
||||
}
|
||||
|
||||
getItem().setDecodeConfiguration(mpt);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Editor<Channel> editor) throws EditorValidationException
|
||||
{
|
||||
//No validation required
|
||||
}
|
||||
|
||||
private void setControlsEnabled(boolean enabled)
|
||||
{
|
||||
if(mAFC.isEnabled() != enabled)
|
||||
{
|
||||
mAFC.setEnabled(enabled);
|
||||
}
|
||||
|
||||
if(mComboChannelMaps.isEnabled() != enabled)
|
||||
{
|
||||
mComboChannelMaps.setEnabled(enabled);
|
||||
}
|
||||
|
||||
if(mTrafficChannelPoolSize.isEnabled() != enabled)
|
||||
{
|
||||
mTrafficChannelPoolSize.setEnabled(enabled);
|
||||
}
|
||||
|
||||
if(mCallTimeout.isEnabled() != enabled)
|
||||
{
|
||||
mCallTimeout.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(Channel item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
if(hasItem())
|
||||
{
|
||||
setControlsEnabled(true);
|
||||
|
||||
DecodeConfiguration config = getItem().getDecodeConfiguration();
|
||||
|
||||
if(config instanceof DecodeConfigMPT1327)
|
||||
{
|
||||
DecodeConfigMPT1327 mpt = (DecodeConfigMPT1327)config;
|
||||
mAFC.setSelected(mpt.isAFCEnabled());
|
||||
mAFCMaximumCorrection.setValue(mpt.getAFCMaximumCorrection());
|
||||
mAFCMaximumCorrection.setEnabled(mpt.isAFCEnabled());
|
||||
|
||||
ChannelMap map = mChannelMapModel.getChannelMap(mpt.getChannelMapName());
|
||||
|
||||
if(map != null)
|
||||
{
|
||||
mComboChannelMaps.setSelectedItem(map);
|
||||
}
|
||||
|
||||
mTrafficChannelPoolSize.setValue(mpt.getTrafficChannelPoolSize());
|
||||
mCallTimeout.setValue(mpt.getCallTimeout());
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
mAFC.setSelected(false);
|
||||
mAFCMaximumCorrection.setValue(DecodeConfiguration.DEFAULT_AFC_MAX_CORRECTION);
|
||||
mAFCMaximumCorrection.setEnabled(false);
|
||||
mTrafficChannelPoolSize.setValue(DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT);
|
||||
mCallTimeout.setValue(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS);
|
||||
|
||||
setModified(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setControlsEnabled(false);
|
||||
setModified(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.mpt1327;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.channel.metadata.AliasedStringAttributeMonitor;
|
||||
import io.github.dsheirer.channel.metadata.Attribute;
|
||||
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
|
||||
import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.channel.traffic.TrafficChannelAllocationEvent;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMap;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
import io.github.dsheirer.util.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class MPT1327DecoderState extends DecoderState
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MPT1327DecoderState.class);
|
||||
|
||||
private TreeSet<String> mIdents = new TreeSet<String>();
|
||||
private HashMap<String,ArrayList<String>> mGroups = new HashMap<String,ArrayList<String>>();
|
||||
|
||||
private String mSite;
|
||||
private AliasedStringAttributeMonitor mFromAttribute;
|
||||
private AliasedStringAttributeMonitor mToAttribute;
|
||||
|
||||
private int mChannelNumber;
|
||||
private ChannelType mChannelType;
|
||||
private ChannelMap mChannelMap;
|
||||
|
||||
private long mFrequency = 0;
|
||||
private long mCallTimeout;
|
||||
|
||||
public MPT1327DecoderState(AliasList aliasList, ChannelMap channelMap, ChannelType channelType, long callTimeout)
|
||||
{
|
||||
super(aliasList);
|
||||
|
||||
mChannelMap = channelMap;
|
||||
mChannelType = channelType;
|
||||
mCallTimeout = callTimeout;
|
||||
mFromAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_FROM,
|
||||
getAttributeChangeRequestListener(), getAliasList(), AliasIDType.MPT1327);
|
||||
mToAttribute = new AliasedStringAttributeMonitor(Attribute.PRIMARY_ADDRESS_TO,
|
||||
getAttributeChangeRequestListener(), getAliasList(), AliasIDType.MPT1327);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.MPT1327;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
|
||||
public ChannelType getChannelType()
|
||||
{
|
||||
return mChannelType;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Message message)
|
||||
{
|
||||
if(message.isValid())
|
||||
{
|
||||
if(message instanceof MPT1327Message)
|
||||
{
|
||||
MPT1327Message mpt = ((MPT1327Message) message);
|
||||
|
||||
switch(mpt.getMessageType())
|
||||
{
|
||||
case ACK:
|
||||
mIdents.add(mpt.getFromID());
|
||||
|
||||
MPT1327Message.IdentType identType = mpt.getIdent1Type();
|
||||
|
||||
if(identType == MPT1327Message.IdentType.REGI)
|
||||
{
|
||||
broadcast(new MPT1327CallEvent.Builder(CallEvent.CallEventType.REGISTER)
|
||||
.aliasList(getAliasList())
|
||||
.channel(String.valueOf(mChannelNumber))
|
||||
.details("REGISTERED ON NETWORK")
|
||||
.frequency(mFrequency)
|
||||
.from(mpt.getToID())
|
||||
.to(mpt.getFromID())
|
||||
.build());
|
||||
}
|
||||
else
|
||||
{
|
||||
broadcast(new MPT1327CallEvent.Builder(CallEvent.CallEventType.RESPONSE)
|
||||
.aliasList(getAliasList())
|
||||
.channel(String.valueOf(mChannelNumber))
|
||||
.details("ACK " + identType.getLabel())
|
||||
.frequency(mFrequency)
|
||||
.from(mpt.getFromID())
|
||||
.to(mpt.getToID())
|
||||
.build());
|
||||
}
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL));
|
||||
break;
|
||||
case ACKI:
|
||||
mIdents.add(mpt.getFromID());
|
||||
mIdents.add(mpt.getToID());
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL));
|
||||
break;
|
||||
case AHYC:
|
||||
mIdents.add(mpt.getToID());
|
||||
|
||||
broadcast(new MPT1327CallEvent.Builder(CallEvent.CallEventType.COMMAND)
|
||||
.aliasList(getAliasList())
|
||||
.channel(String.valueOf(mChannelNumber))
|
||||
.details(((MPT1327Message) message).getRequestString())
|
||||
.frequency(mFrequency)
|
||||
.from(mpt.getFromID())
|
||||
.to(mpt.getToID())
|
||||
.build());
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL));
|
||||
break;
|
||||
case AHYQ:
|
||||
broadcast(new MPT1327CallEvent.Builder(CallEvent.CallEventType.STATUS)
|
||||
.aliasList(getAliasList())
|
||||
.channel(String.valueOf(mChannelNumber))
|
||||
.details(mpt.getStatusMessage())
|
||||
.frequency(mFrequency)
|
||||
.from(mpt.getFromID())
|
||||
.to(mpt.getToID())
|
||||
.build());
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL));
|
||||
break;
|
||||
case ALH:
|
||||
setSite(mpt.getSiteID());
|
||||
broadcast(new DecoderStateEvent(this, Event.START, State.CONTROL));
|
||||
break;
|
||||
case GTC:
|
||||
String from = mpt.getFromID();
|
||||
String to = mpt.getToID();
|
||||
|
||||
if(from != null)
|
||||
{
|
||||
mIdents.add(from);
|
||||
}
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
mIdents.add(to);
|
||||
}
|
||||
|
||||
/* Capture the idents that talk to each group */
|
||||
if(from != null && to != null)
|
||||
{
|
||||
if(mGroups.containsKey(to))
|
||||
{
|
||||
ArrayList<String> talkgroups = mGroups.get(to);
|
||||
|
||||
if(!talkgroups.contains(from))
|
||||
{
|
||||
talkgroups.add(from);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ArrayList<String> talkgroups = new ArrayList<String>();
|
||||
talkgroups.add(from);
|
||||
|
||||
mGroups.put(to, talkgroups);
|
||||
}
|
||||
}
|
||||
|
||||
int channel = mpt.getChannel();
|
||||
|
||||
long frequency = 0;
|
||||
|
||||
if(getChannelMap() != null)
|
||||
{
|
||||
frequency = getChannelMap().getFrequency(channel);
|
||||
}
|
||||
|
||||
CallEvent event = new MPT1327CallEvent.Builder(CallEvent.CallEventType.CALL)
|
||||
.aliasList(getAliasList())
|
||||
.channel(String.valueOf(channel))
|
||||
.details("GTC")
|
||||
.frequency(frequency)
|
||||
.from(mpt.getFromID())
|
||||
.to(mpt.getToID())
|
||||
.build();
|
||||
|
||||
broadcast(new TrafficChannelAllocationEvent(this, event));
|
||||
break;
|
||||
case HEAD_PLUS1:
|
||||
case HEAD_PLUS2:
|
||||
case HEAD_PLUS3:
|
||||
case HEAD_PLUS4:
|
||||
broadcast(new MPT1327CallEvent.Builder(CallEvent.CallEventType.SDM)
|
||||
.aliasList(getAliasList())
|
||||
.details(mpt.getMessage())
|
||||
.from(mpt.getFromID())
|
||||
.to(mpt.getToID())
|
||||
.build());
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL));
|
||||
break;
|
||||
|
||||
/* Traffic Channel Events */
|
||||
case CLEAR:
|
||||
mChannelNumber = mpt.getChannel();
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.END, State.FADE));
|
||||
break;
|
||||
case MAINT:
|
||||
if(mChannelType == ChannelType.STANDARD)
|
||||
{
|
||||
/**
|
||||
* When we receive a MAINT message and we're configured
|
||||
* as a standard channel, we need to apply the call
|
||||
* timeout specified by the user. Otherwise we'll
|
||||
* be using the shorter default call timeout
|
||||
*/
|
||||
broadcast(new ChangeChannelTimeoutEvent(this,
|
||||
mChannelType, mCallTimeout));
|
||||
|
||||
if(mCurrentCallEvent == null)
|
||||
{
|
||||
mCurrentCallEvent = new MPT1327CallEvent.Builder(CallEvent.CallEventType.CALL)
|
||||
.aliasList(getAliasList())
|
||||
.channel(String.valueOf(mChannelNumber))
|
||||
.details("MONITORED TRAFFIC CHANNEL")
|
||||
.frequency(mFrequency)
|
||||
.to(mpt.getToID())
|
||||
.build();
|
||||
|
||||
broadcast(mCurrentCallEvent);
|
||||
}
|
||||
|
||||
mToAttribute.process(mpt.getToID());
|
||||
|
||||
broadcast(new DecoderStateEvent(this, Event.START, State.CALL));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mIdents.clear();
|
||||
|
||||
resetState();
|
||||
}
|
||||
|
||||
private void resetState()
|
||||
{
|
||||
mFromAttribute.reset();
|
||||
mToAttribute.reset();
|
||||
|
||||
/**
|
||||
* If this is a standard channel, reset the fade timeout to the default
|
||||
* timeout. Once processing is underway, if we get a MAINT message,
|
||||
* this indicates we're processing a traffic channel as a standard
|
||||
* channel, so we'll issue a different call timeout then.
|
||||
*/
|
||||
if(mChannelType == ChannelType.STANDARD)
|
||||
{
|
||||
broadcast(new ChangeChannelTimeoutEvent(this, mChannelType,
|
||||
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000));
|
||||
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
mCurrentCallEvent.end();
|
||||
broadcast(mCurrentCallEvent);
|
||||
mCurrentCallEvent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getSite()
|
||||
{
|
||||
return mSite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the site number.
|
||||
*/
|
||||
public void setSite(String site)
|
||||
{
|
||||
if(!StringUtils.isEqual(mSite, site))
|
||||
{
|
||||
mSite = site;
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.NETWORK_ID_1, "SITE:" + getSite(),
|
||||
getSiteAlias()));
|
||||
}
|
||||
}
|
||||
|
||||
public Alias getSiteAlias()
|
||||
{
|
||||
if(hasAliasList())
|
||||
{
|
||||
return getAliasList().getSiteID(mSite);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ChannelMap getChannelMap()
|
||||
{
|
||||
return mChannelMap;
|
||||
}
|
||||
|
||||
public void setChannelMap(ChannelMap channelMap)
|
||||
{
|
||||
mChannelMap = channelMap;
|
||||
}
|
||||
|
||||
public int getChannelNumber()
|
||||
{
|
||||
return mChannelNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the channel number. This is used primarily for traffic channels since
|
||||
* the channel will already have been identified prior to the traffic
|
||||
* channel being created.
|
||||
*/
|
||||
public void setChannelNumber(int channel)
|
||||
{
|
||||
if(mChannelNumber != channel)
|
||||
{
|
||||
mChannelNumber = channel;
|
||||
broadcast(new AttributeChangeRequest<String>(Attribute.CHANNEL_FREQUENCY_LABEL, String.valueOf(mChannelNumber)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Activity Summary - Decoder: MPT-1327\n");
|
||||
sb.append(DIVIDER1);
|
||||
sb.append("Site: ");
|
||||
|
||||
if(mSite != null)
|
||||
{
|
||||
sb.append(mSite);
|
||||
|
||||
if(hasAliasList())
|
||||
{
|
||||
Alias siteAlias = getAliasList().getSiteID(mSite);
|
||||
|
||||
if(siteAlias != null)
|
||||
{
|
||||
sb.append(" ").append(siteAlias.getName()).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("Unknown\n");
|
||||
}
|
||||
|
||||
sb.append(DIVIDER2).append("Talkgroups: ");
|
||||
|
||||
if(mGroups.isEmpty())
|
||||
{
|
||||
sb.append("None\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
List<String> talkgroups = new ArrayList<String>(mGroups.keySet());
|
||||
Collections.sort(talkgroups);
|
||||
|
||||
for(String talkgroup : talkgroups)
|
||||
{
|
||||
sb.append("\n ").append(talkgroup);
|
||||
|
||||
if(hasAliasList())
|
||||
{
|
||||
Alias alias = getAliasList().getMPT1327Alias(talkgroup);
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
sb.append(" ");
|
||||
sb.append(alias.getName());
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
|
||||
ArrayList<String> members = mGroups.get(talkgroup);
|
||||
Collections.sort(members);
|
||||
|
||||
for(String member : members)
|
||||
{
|
||||
sb.append(" >");
|
||||
sb.append(member);
|
||||
|
||||
if(hasAliasList())
|
||||
{
|
||||
Alias alias = getAliasList().getMPT1327Alias(member);
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
sb.append(" ");
|
||||
sb.append(alias.getName());
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.append(DIVIDER2).append("All Idents: ");
|
||||
|
||||
if(mIdents.isEmpty())
|
||||
{
|
||||
sb.append("None\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("\n");
|
||||
|
||||
Iterator<String> it = mIdents.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
String ident = it.next();
|
||||
|
||||
sb.append("");
|
||||
sb.append(ident);
|
||||
sb.append(" ");
|
||||
|
||||
if(hasAliasList())
|
||||
{
|
||||
Alias alias = getAliasList().getMPT1327Alias(ident);
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
sb.append(alias.getName());
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
/* No initialization required */
|
||||
}
|
||||
|
||||
@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, event.getFrequency()));
|
||||
break;
|
||||
case TRAFFIC_CHANNEL_ALLOCATION:
|
||||
if(event.getSource() != MPT1327DecoderState.this)
|
||||
{
|
||||
if(event instanceof TrafficChannelAllocationEvent)
|
||||
{
|
||||
TrafficChannelAllocationEvent allocationEvent =
|
||||
(TrafficChannelAllocationEvent) event;
|
||||
|
||||
String channel = allocationEvent.getCallEvent().getChannel();
|
||||
|
||||
if(channel != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
setChannelNumber(Integer.valueOf(channel));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the channel number
|
||||
}
|
||||
}
|
||||
|
||||
mFrequency = allocationEvent.getCallEvent().getFrequency();
|
||||
broadcast(new AttributeChangeRequest<Long>(Attribute.CHANNEL_FREQUENCY,
|
||||
allocationEvent.getCallEvent().getFrequency()));
|
||||
|
||||
mFromAttribute.process(allocationEvent.getCallEvent().getFromID());
|
||||
mToAttribute.process(allocationEvent.getCallEvent().getToID());
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
package io.github.dsheirer.module.decode.mpt1327;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.mpt1327.MPT1327Message.MPTMessageType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class MPT1327MessageFilter extends Filter<Message>
|
||||
{
|
||||
private HashMap<MPTMessageType,FilterElement<MPTMessageType>> mFilterElements =
|
||||
new HashMap<MPTMessageType,FilterElement<MPTMessageType>>();
|
||||
|
||||
public MPT1327MessageFilter()
|
||||
{
|
||||
super( "MPT1327 Message Type Filter" );
|
||||
|
||||
for( MPTMessageType type: MPT1327Message.MPTMessageType.values() )
|
||||
{
|
||||
if( type != MPTMessageType.UNKN )
|
||||
{
|
||||
mFilterElements.put( type,
|
||||
new FilterElement<MPTMessageType>( type ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes( Message message )
|
||||
{
|
||||
if( mEnabled && canProcess( message ) )
|
||||
{
|
||||
MPT1327Message mpt = (MPT1327Message)message;
|
||||
|
||||
FilterElement<MPTMessageType> element =
|
||||
mFilterElements.get( mpt.getMessageType() );
|
||||
|
||||
if( element != null )
|
||||
{
|
||||
return element.isEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess( Message message )
|
||||
{
|
||||
return message instanceof MPT1327Message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return new ArrayList<FilterElement<?>>( mFilterElements.values() );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.mpt1327;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MPT1327MessageProcessor implements Listener<BinaryMessage>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( MPT1327MessageProcessor.class );
|
||||
|
||||
private Listener<Message> mMessageListener;
|
||||
|
||||
private AliasList mAliasList;
|
||||
|
||||
public MPT1327MessageProcessor( AliasList list )
|
||||
{
|
||||
mAliasList = list;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( BinaryMessage buffer )
|
||||
{
|
||||
if( mMessageListener != null )
|
||||
{
|
||||
MPT1327Message message = new MPT1327Message( buffer, mAliasList );
|
||||
|
||||
mMessageListener.receive( message );
|
||||
}
|
||||
}
|
||||
|
||||
public void setMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.nbfm;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
public class DecodeConfigNBFM extends DecodeConfiguration
|
||||
{
|
||||
public DecodeConfigNBFM()
|
||||
{
|
||||
super( DecoderType.NBFM );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.nbfm;
|
||||
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class NBFMDecoder extends PrimaryDecoder
|
||||
{
|
||||
public NBFMDecoder( DecodeConfiguration config )
|
||||
{
|
||||
super( config );
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.NBFM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.nbfm;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EditorValidationException;
|
||||
import io.github.dsheirer.gui.editor.ValidatingEditor;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class NBFMDecoderEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JCheckBox mAFC;
|
||||
private JSlider mAFCMaximumCorrection;
|
||||
|
||||
public NBFMDecoderEditor()
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0,wrap 2", "[right][grow,fill]", "" ) );
|
||||
|
||||
mAFC = new JCheckBox( "AFC: 3000 Hz" );
|
||||
mAFC.setEnabled( false );
|
||||
mAFC.setToolTipText( "AFC automatically adjusts the center frequency of the channel to "
|
||||
+ "correct/compensate for inaccuracies and frequency drift in the tuner" );
|
||||
mAFC.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent arg0 )
|
||||
{
|
||||
setModified( true );
|
||||
|
||||
if( mAFC.isSelected() && !mAFCMaximumCorrection.isEnabled() )
|
||||
{
|
||||
mAFCMaximumCorrection.setEnabled( true );
|
||||
}
|
||||
else if( !mAFC.isSelected() && mAFCMaximumCorrection.isEnabled() )
|
||||
{
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
add( mAFC );
|
||||
|
||||
mAFCMaximumCorrection = new JSlider( 0, 7000, 3000 );
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
mAFCMaximumCorrection.setToolTipText( "Maximum AFC frequency correction (0 - 15kHz)" );
|
||||
mAFCMaximumCorrection.setMajorTickSpacing( 2000 );
|
||||
mAFCMaximumCorrection.setMinorTickSpacing( 1000 );
|
||||
mAFCMaximumCorrection.setPaintTicks( true );
|
||||
|
||||
mAFCMaximumCorrection.addChangeListener( new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged( ChangeEvent e )
|
||||
{
|
||||
mAFC.setText( "AFC: " + mAFCMaximumCorrection.getValue() + " Hz" );
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
add( mAFCMaximumCorrection );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate( Editor<Channel> editor ) throws EditorValidationException
|
||||
{
|
||||
//No validation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasItem() && isModified() )
|
||||
{
|
||||
DecodeConfigNBFM nbfm = new DecodeConfigNBFM();
|
||||
|
||||
nbfm.setAFC( mAFC.isSelected() );
|
||||
nbfm.setAFCMaximumCorrection( mAFCMaximumCorrection.getValue() );
|
||||
|
||||
getItem().setDecodeConfiguration( nbfm );
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
|
||||
private void setControlsEnabled( boolean enabled )
|
||||
{
|
||||
if( mAFC.isEnabled() != enabled )
|
||||
{
|
||||
mAFC.setEnabled( enabled );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem( Channel item )
|
||||
{
|
||||
super.setItem( item );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
setControlsEnabled( true );
|
||||
|
||||
DecodeConfiguration config = getItem().getDecodeConfiguration();
|
||||
|
||||
if( config instanceof DecodeConfigNBFM )
|
||||
{
|
||||
DecodeConfigNBFM nbfm = (DecodeConfigNBFM)config;
|
||||
|
||||
mAFC.setSelected( nbfm.isAFCEnabled() );
|
||||
mAFCMaximumCorrection.setValue( nbfm.getAFCMaximumCorrection() );
|
||||
mAFCMaximumCorrection.setEnabled( nbfm.isAFCEnabled() );
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
mAFC.setSelected( false );
|
||||
mAFCMaximumCorrection.setValue( DecodeConfiguration.DEFAULT_AFC_MAX_CORRECTION );
|
||||
mAFCMaximumCorrection.setEnabled( false );
|
||||
|
||||
setModified( true );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setControlsEnabled( false );
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
|
||||
public interface C4FMFrameListener
|
||||
{
|
||||
/**
|
||||
* Listener interface to receive the output of the C4FMMessageFramer.
|
||||
*
|
||||
* @param buffer - framed message without the sync pattern
|
||||
* @param inverted - flag indicating if the message was received inverted
|
||||
*/
|
||||
public void receive( BinaryMessage buffer, boolean inverted );
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
|
||||
/**
|
||||
* C4FM slicer to convert the output stream of the C4FMSymbolFilter into a
|
||||
* stream of C4FM symbols.
|
||||
*
|
||||
* Supports registering listener(s) to receive normal and/or inverted symbol
|
||||
* output streams.
|
||||
*/
|
||||
public class C4FMSlicer implements RealSampleListener
|
||||
{
|
||||
private static final float THRESHOLD = 2.0f;
|
||||
|
||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mBroadcaster.dispose();
|
||||
mBroadcaster = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary method for receiving output from the C4FMSymbolFilter. Slices
|
||||
* (converts) the filtered sample value into a C4FMSymbol decision.
|
||||
*/
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
if( sample > 0.0 )
|
||||
{
|
||||
if( sample >= THRESHOLD )
|
||||
{
|
||||
dispatch( Dibit.D01_PLUS_3 );
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatch( Dibit.D00_PLUS_1 );
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( sample > -THRESHOLD )
|
||||
{
|
||||
dispatch( Dibit.D10_MINUS_1 );
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatch( Dibit.D11_MINUS_3 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the symbol decision to any registered listeners
|
||||
*/
|
||||
private void dispatch( Dibit symbol )
|
||||
{
|
||||
mBroadcaster.receive( symbol );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive the normal (non-inverted) C4FM symbol
|
||||
* stream.
|
||||
*/
|
||||
public void addListener( Listener<Dibit> listener )
|
||||
{
|
||||
mBroadcaster.addListener( listener );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener
|
||||
*/
|
||||
public void removeListener( Listener<Dibit> listener )
|
||||
{
|
||||
mBroadcaster.removeListener( listener );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.instrument.tap.stream.AdditiveFloatTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatTap;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
import io.github.dsheirer.sample.real.RealSampleProvider;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.dsp.gain.DirectGainControl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class C4FMSymbolFilter implements Listener<RealBuffer>, IFrequencyChangeListener, RealSampleProvider, Instrumentable
|
||||
{
|
||||
private static final float TAPS[][] =
|
||||
{
|
||||
{ 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 0/128
|
||||
{ -1.54700e-04f, 8.53777e-04f, -2.76968e-03f, 7.89295e-03f, 9.98534e-01f, -5.41054e-03f, 1.24642e-03f, -1.98993e-04f }, // 1/128
|
||||
{ -3.09412e-04f, 1.70888e-03f, -5.55134e-03f, 1.58840e-02f, 9.96891e-01f, -1.07209e-02f, 2.47942e-03f, -3.96391e-04f }, // 2/128
|
||||
{ -4.64053e-04f, 2.56486e-03f, -8.34364e-03f, 2.39714e-02f, 9.95074e-01f, -1.59305e-02f, 3.69852e-03f, -5.92100e-04f }, // 3/128
|
||||
{ -6.18544e-04f, 3.42130e-03f, -1.11453e-02f, 3.21531e-02f, 9.93082e-01f, -2.10389e-02f, 4.90322e-03f, -7.86031e-04f }, // 4/128
|
||||
{ -7.72802e-04f, 4.27773e-03f, -1.39548e-02f, 4.04274e-02f, 9.90917e-01f, -2.60456e-02f, 6.09305e-03f, -9.78093e-04f }, // 5/128
|
||||
{ -9.26747e-04f, 5.13372e-03f, -1.67710e-02f, 4.87921e-02f, 9.88580e-01f, -3.09503e-02f, 7.26755e-03f, -1.16820e-03f }, // 6/128
|
||||
{ -1.08030e-03f, 5.98883e-03f, -1.95925e-02f, 5.72454e-02f, 9.86071e-01f, -3.57525e-02f, 8.42626e-03f, -1.35627e-03f }, // 7/128
|
||||
{ -1.23337e-03f, 6.84261e-03f, -2.24178e-02f, 6.57852e-02f, 9.83392e-01f, -4.04519e-02f, 9.56876e-03f, -1.54221e-03f }, // 8/128
|
||||
{ -1.38589e-03f, 7.69462e-03f, -2.52457e-02f, 7.44095e-02f, 9.80543e-01f, -4.50483e-02f, 1.06946e-02f, -1.72594e-03f }, // 9/128
|
||||
{ -1.53777e-03f, 8.54441e-03f, -2.80746e-02f, 8.31162e-02f, 9.77526e-01f, -4.95412e-02f, 1.18034e-02f, -1.90738e-03f }, // 10/128
|
||||
{ -1.68894e-03f, 9.39154e-03f, -3.09033e-02f, 9.19033e-02f, 9.74342e-01f, -5.39305e-02f, 1.28947e-02f, -2.08645e-03f }, // 11/128
|
||||
{ -1.83931e-03f, 1.02356e-02f, -3.37303e-02f, 1.00769e-01f, 9.70992e-01f, -5.82159e-02f, 1.39681e-02f, -2.26307e-03f }, // 12/128
|
||||
{ -1.98880e-03f, 1.10760e-02f, -3.65541e-02f, 1.09710e-01f, 9.67477e-01f, -6.23972e-02f, 1.50233e-02f, -2.43718e-03f }, // 13/128
|
||||
{ -2.13733e-03f, 1.19125e-02f, -3.93735e-02f, 1.18725e-01f, 9.63798e-01f, -6.64743e-02f, 1.60599e-02f, -2.60868e-03f }, // 14/128
|
||||
{ -2.28483e-03f, 1.27445e-02f, -4.21869e-02f, 1.27812e-01f, 9.59958e-01f, -7.04471e-02f, 1.70776e-02f, -2.77751e-03f }, // 15/128
|
||||
{ -2.43121e-03f, 1.35716e-02f, -4.49929e-02f, 1.36968e-01f, 9.55956e-01f, -7.43154e-02f, 1.80759e-02f, -2.94361e-03f }, // 16/128
|
||||
{ -2.57640e-03f, 1.43934e-02f, -4.77900e-02f, 1.46192e-01f, 9.51795e-01f, -7.80792e-02f, 1.90545e-02f, -3.10689e-03f }, // 17/128
|
||||
{ -2.72032e-03f, 1.52095e-02f, -5.05770e-02f, 1.55480e-01f, 9.47477e-01f, -8.17385e-02f, 2.00132e-02f, -3.26730e-03f }, // 18/128
|
||||
{ -2.86289e-03f, 1.60193e-02f, -5.33522e-02f, 1.64831e-01f, 9.43001e-01f, -8.52933e-02f, 2.09516e-02f, -3.42477e-03f }, // 19/128
|
||||
{ -3.00403e-03f, 1.68225e-02f, -5.61142e-02f, 1.74242e-01f, 9.38371e-01f, -8.87435e-02f, 2.18695e-02f, -3.57923e-03f }, // 20/128
|
||||
{ -3.14367e-03f, 1.76185e-02f, -5.88617e-02f, 1.83711e-01f, 9.33586e-01f, -9.20893e-02f, 2.27664e-02f, -3.73062e-03f }, // 21/128
|
||||
{ -3.28174e-03f, 1.84071e-02f, -6.15931e-02f, 1.93236e-01f, 9.28650e-01f, -9.53307e-02f, 2.36423e-02f, -3.87888e-03f }, // 22/128
|
||||
{ -3.41815e-03f, 1.91877e-02f, -6.43069e-02f, 2.02814e-01f, 9.23564e-01f, -9.84679e-02f, 2.44967e-02f, -4.02397e-03f }, // 23/128
|
||||
{ -3.55283e-03f, 1.99599e-02f, -6.70018e-02f, 2.12443e-01f, 9.18329e-01f, -1.01501e-01f, 2.53295e-02f, -4.16581e-03f }, // 24/128
|
||||
{ -3.68570e-03f, 2.07233e-02f, -6.96762e-02f, 2.22120e-01f, 9.12947e-01f, -1.04430e-01f, 2.61404e-02f, -4.30435e-03f }, // 25/128
|
||||
{ -3.81671e-03f, 2.14774e-02f, -7.23286e-02f, 2.31843e-01f, 9.07420e-01f, -1.07256e-01f, 2.69293e-02f, -4.43955e-03f }, // 26/128
|
||||
{ -3.94576e-03f, 2.22218e-02f, -7.49577e-02f, 2.41609e-01f, 9.01749e-01f, -1.09978e-01f, 2.76957e-02f, -4.57135e-03f }, // 27/128
|
||||
{ -4.07279e-03f, 2.29562e-02f, -7.75620e-02f, 2.51417e-01f, 8.95936e-01f, -1.12597e-01f, 2.84397e-02f, -4.69970e-03f }, // 28/128
|
||||
{ -4.19774e-03f, 2.36801e-02f, -8.01399e-02f, 2.61263e-01f, 8.89984e-01f, -1.15113e-01f, 2.91609e-02f, -4.82456e-03f }, // 29/128
|
||||
{ -4.32052e-03f, 2.43930e-02f, -8.26900e-02f, 2.71144e-01f, 8.83893e-01f, -1.17526e-01f, 2.98593e-02f, -4.94589e-03f }, // 30/128
|
||||
{ -4.44107e-03f, 2.50946e-02f, -8.52109e-02f, 2.81060e-01f, 8.77666e-01f, -1.19837e-01f, 3.05345e-02f, -5.06363e-03f }, // 31/128
|
||||
{ -4.55932e-03f, 2.57844e-02f, -8.77011e-02f, 2.91006e-01f, 8.71305e-01f, -1.22047e-01f, 3.11866e-02f, -5.17776e-03f }, // 32/128
|
||||
{ -4.67520e-03f, 2.64621e-02f, -9.01591e-02f, 3.00980e-01f, 8.64812e-01f, -1.24154e-01f, 3.18153e-02f, -5.28823e-03f }, // 33/128
|
||||
{ -4.78866e-03f, 2.71272e-02f, -9.25834e-02f, 3.10980e-01f, 8.58189e-01f, -1.26161e-01f, 3.24205e-02f, -5.39500e-03f }, // 34/128
|
||||
{ -4.89961e-03f, 2.77794e-02f, -9.49727e-02f, 3.21004e-01f, 8.51437e-01f, -1.28068e-01f, 3.30021e-02f, -5.49804e-03f }, // 35/128
|
||||
{ -5.00800e-03f, 2.84182e-02f, -9.73254e-02f, 3.31048e-01f, 8.44559e-01f, -1.29874e-01f, 3.35600e-02f, -5.59731e-03f }, // 36/128
|
||||
{ -5.11376e-03f, 2.90433e-02f, -9.96402e-02f, 3.41109e-01f, 8.37557e-01f, -1.31581e-01f, 3.40940e-02f, -5.69280e-03f }, // 37/128
|
||||
{ -5.21683e-03f, 2.96543e-02f, -1.01915e-01f, 3.51186e-01f, 8.30432e-01f, -1.33189e-01f, 3.46042e-02f, -5.78446e-03f }, // 38/128
|
||||
{ -5.31716e-03f, 3.02507e-02f, -1.04150e-01f, 3.61276e-01f, 8.23188e-01f, -1.34699e-01f, 3.50903e-02f, -5.87227e-03f }, // 39/128
|
||||
{ -5.41467e-03f, 3.08323e-02f, -1.06342e-01f, 3.71376e-01f, 8.15826e-01f, -1.36111e-01f, 3.55525e-02f, -5.95620e-03f }, // 40/128
|
||||
{ -5.50931e-03f, 3.13987e-02f, -1.08490e-01f, 3.81484e-01f, 8.08348e-01f, -1.37426e-01f, 3.59905e-02f, -6.03624e-03f }, // 41/128
|
||||
{ -5.60103e-03f, 3.19495e-02f, -1.10593e-01f, 3.91596e-01f, 8.00757e-01f, -1.38644e-01f, 3.64044e-02f, -6.11236e-03f }, // 42/128
|
||||
{ -5.68976e-03f, 3.24843e-02f, -1.12650e-01f, 4.01710e-01f, 7.93055e-01f, -1.39767e-01f, 3.67941e-02f, -6.18454e-03f }, // 43/128
|
||||
{ -5.77544e-03f, 3.30027e-02f, -1.14659e-01f, 4.11823e-01f, 7.85244e-01f, -1.40794e-01f, 3.71596e-02f, -6.25277e-03f }, // 44/128
|
||||
{ -5.85804e-03f, 3.35046e-02f, -1.16618e-01f, 4.21934e-01f, 7.77327e-01f, -1.41727e-01f, 3.75010e-02f, -6.31703e-03f }, // 45/128
|
||||
{ -5.93749e-03f, 3.39894e-02f, -1.18526e-01f, 4.32038e-01f, 7.69305e-01f, -1.42566e-01f, 3.78182e-02f, -6.37730e-03f }, // 46/128
|
||||
{ -6.01374e-03f, 3.44568e-02f, -1.20382e-01f, 4.42134e-01f, 7.61181e-01f, -1.43313e-01f, 3.81111e-02f, -6.43358e-03f }, // 47/128
|
||||
{ -6.08674e-03f, 3.49066e-02f, -1.22185e-01f, 4.52218e-01f, 7.52958e-01f, -1.43968e-01f, 3.83800e-02f, -6.48585e-03f }, // 48/128
|
||||
{ -6.15644e-03f, 3.53384e-02f, -1.23933e-01f, 4.62289e-01f, 7.44637e-01f, -1.44531e-01f, 3.86247e-02f, -6.53412e-03f }, // 49/128
|
||||
{ -6.22280e-03f, 3.57519e-02f, -1.25624e-01f, 4.72342e-01f, 7.36222e-01f, -1.45004e-01f, 3.88454e-02f, -6.57836e-03f }, // 50/128
|
||||
{ -6.28577e-03f, 3.61468e-02f, -1.27258e-01f, 4.82377e-01f, 7.27714e-01f, -1.45387e-01f, 3.90420e-02f, -6.61859e-03f }, // 51/128
|
||||
{ -6.34530e-03f, 3.65227e-02f, -1.28832e-01f, 4.92389e-01f, 7.19116e-01f, -1.45682e-01f, 3.92147e-02f, -6.65479e-03f }, // 52/128
|
||||
{ -6.40135e-03f, 3.68795e-02f, -1.30347e-01f, 5.02377e-01f, 7.10431e-01f, -1.45889e-01f, 3.93636e-02f, -6.68698e-03f }, // 53/128
|
||||
{ -6.45388e-03f, 3.72167e-02f, -1.31800e-01f, 5.12337e-01f, 7.01661e-01f, -1.46009e-01f, 3.94886e-02f, -6.71514e-03f }, // 54/128
|
||||
{ -6.50285e-03f, 3.75341e-02f, -1.33190e-01f, 5.22267e-01f, 6.92808e-01f, -1.46043e-01f, 3.95900e-02f, -6.73929e-03f }, // 55/128
|
||||
{ -6.54823e-03f, 3.78315e-02f, -1.34515e-01f, 5.32164e-01f, 6.83875e-01f, -1.45993e-01f, 3.96678e-02f, -6.75943e-03f }, // 56/128
|
||||
{ -6.58996e-03f, 3.81085e-02f, -1.35775e-01f, 5.42025e-01f, 6.74865e-01f, -1.45859e-01f, 3.97222e-02f, -6.77557e-03f }, // 57/128
|
||||
{ -6.62802e-03f, 3.83650e-02f, -1.36969e-01f, 5.51849e-01f, 6.65779e-01f, -1.45641e-01f, 3.97532e-02f, -6.78771e-03f }, // 58/128
|
||||
{ -6.66238e-03f, 3.86006e-02f, -1.38094e-01f, 5.61631e-01f, 6.56621e-01f, -1.45343e-01f, 3.97610e-02f, -6.79588e-03f }, // 59/128
|
||||
{ -6.69300e-03f, 3.88151e-02f, -1.39150e-01f, 5.71370e-01f, 6.47394e-01f, -1.44963e-01f, 3.97458e-02f, -6.80007e-03f }, // 60/128
|
||||
{ -6.71985e-03f, 3.90083e-02f, -1.40136e-01f, 5.81063e-01f, 6.38099e-01f, -1.44503e-01f, 3.97077e-02f, -6.80032e-03f }, // 61/128
|
||||
{ -6.74291e-03f, 3.91800e-02f, -1.41050e-01f, 5.90706e-01f, 6.28739e-01f, -1.43965e-01f, 3.96469e-02f, -6.79662e-03f }, // 62/128
|
||||
{ -6.76214e-03f, 3.93299e-02f, -1.41891e-01f, 6.00298e-01f, 6.19318e-01f, -1.43350e-01f, 3.95635e-02f, -6.78902e-03f }, // 63/128
|
||||
{ -6.77751e-03f, 3.94578e-02f, -1.42658e-01f, 6.09836e-01f, 6.09836e-01f, -1.42658e-01f, 3.94578e-02f, -6.77751e-03f }, // 64/128
|
||||
{ -6.78902e-03f, 3.95635e-02f, -1.43350e-01f, 6.19318e-01f, 6.00298e-01f, -1.41891e-01f, 3.93299e-02f, -6.76214e-03f }, // 65/128
|
||||
{ -6.79662e-03f, 3.96469e-02f, -1.43965e-01f, 6.28739e-01f, 5.90706e-01f, -1.41050e-01f, 3.91800e-02f, -6.74291e-03f }, // 66/128
|
||||
{ -6.80032e-03f, 3.97077e-02f, -1.44503e-01f, 6.38099e-01f, 5.81063e-01f, -1.40136e-01f, 3.90083e-02f, -6.71985e-03f }, // 67/128
|
||||
{ -6.80007e-03f, 3.97458e-02f, -1.44963e-01f, 6.47394e-01f, 5.71370e-01f, -1.39150e-01f, 3.88151e-02f, -6.69300e-03f }, // 68/128
|
||||
{ -6.79588e-03f, 3.97610e-02f, -1.45343e-01f, 6.56621e-01f, 5.61631e-01f, -1.38094e-01f, 3.86006e-02f, -6.66238e-03f }, // 69/128
|
||||
{ -6.78771e-03f, 3.97532e-02f, -1.45641e-01f, 6.65779e-01f, 5.51849e-01f, -1.36969e-01f, 3.83650e-02f, -6.62802e-03f }, // 70/128
|
||||
{ -6.77557e-03f, 3.97222e-02f, -1.45859e-01f, 6.74865e-01f, 5.42025e-01f, -1.35775e-01f, 3.81085e-02f, -6.58996e-03f }, // 71/128
|
||||
{ -6.75943e-03f, 3.96678e-02f, -1.45993e-01f, 6.83875e-01f, 5.32164e-01f, -1.34515e-01f, 3.78315e-02f, -6.54823e-03f }, // 72/128
|
||||
{ -6.73929e-03f, 3.95900e-02f, -1.46043e-01f, 6.92808e-01f, 5.22267e-01f, -1.33190e-01f, 3.75341e-02f, -6.50285e-03f }, // 73/128
|
||||
{ -6.71514e-03f, 3.94886e-02f, -1.46009e-01f, 7.01661e-01f, 5.12337e-01f, -1.31800e-01f, 3.72167e-02f, -6.45388e-03f }, // 74/128
|
||||
{ -6.68698e-03f, 3.93636e-02f, -1.45889e-01f, 7.10431e-01f, 5.02377e-01f, -1.30347e-01f, 3.68795e-02f, -6.40135e-03f }, // 75/128
|
||||
{ -6.65479e-03f, 3.92147e-02f, -1.45682e-01f, 7.19116e-01f, 4.92389e-01f, -1.28832e-01f, 3.65227e-02f, -6.34530e-03f }, // 76/128
|
||||
{ -6.61859e-03f, 3.90420e-02f, -1.45387e-01f, 7.27714e-01f, 4.82377e-01f, -1.27258e-01f, 3.61468e-02f, -6.28577e-03f }, // 77/128
|
||||
{ -6.57836e-03f, 3.88454e-02f, -1.45004e-01f, 7.36222e-01f, 4.72342e-01f, -1.25624e-01f, 3.57519e-02f, -6.22280e-03f }, // 78/128
|
||||
{ -6.53412e-03f, 3.86247e-02f, -1.44531e-01f, 7.44637e-01f, 4.62289e-01f, -1.23933e-01f, 3.53384e-02f, -6.15644e-03f }, // 79/128
|
||||
{ -6.48585e-03f, 3.83800e-02f, -1.43968e-01f, 7.52958e-01f, 4.52218e-01f, -1.22185e-01f, 3.49066e-02f, -6.08674e-03f }, // 80/128
|
||||
{ -6.43358e-03f, 3.81111e-02f, -1.43313e-01f, 7.61181e-01f, 4.42134e-01f, -1.20382e-01f, 3.44568e-02f, -6.01374e-03f }, // 81/128
|
||||
{ -6.37730e-03f, 3.78182e-02f, -1.42566e-01f, 7.69305e-01f, 4.32038e-01f, -1.18526e-01f, 3.39894e-02f, -5.93749e-03f }, // 82/128
|
||||
{ -6.31703e-03f, 3.75010e-02f, -1.41727e-01f, 7.77327e-01f, 4.21934e-01f, -1.16618e-01f, 3.35046e-02f, -5.85804e-03f }, // 83/128
|
||||
{ -6.25277e-03f, 3.71596e-02f, -1.40794e-01f, 7.85244e-01f, 4.11823e-01f, -1.14659e-01f, 3.30027e-02f, -5.77544e-03f }, // 84/128
|
||||
{ -6.18454e-03f, 3.67941e-02f, -1.39767e-01f, 7.93055e-01f, 4.01710e-01f, -1.12650e-01f, 3.24843e-02f, -5.68976e-03f }, // 85/128
|
||||
{ -6.11236e-03f, 3.64044e-02f, -1.38644e-01f, 8.00757e-01f, 3.91596e-01f, -1.10593e-01f, 3.19495e-02f, -5.60103e-03f }, // 86/128
|
||||
{ -6.03624e-03f, 3.59905e-02f, -1.37426e-01f, 8.08348e-01f, 3.81484e-01f, -1.08490e-01f, 3.13987e-02f, -5.50931e-03f }, // 87/128
|
||||
{ -5.95620e-03f, 3.55525e-02f, -1.36111e-01f, 8.15826e-01f, 3.71376e-01f, -1.06342e-01f, 3.08323e-02f, -5.41467e-03f }, // 88/128
|
||||
{ -5.87227e-03f, 3.50903e-02f, -1.34699e-01f, 8.23188e-01f, 3.61276e-01f, -1.04150e-01f, 3.02507e-02f, -5.31716e-03f }, // 89/128
|
||||
{ -5.78446e-03f, 3.46042e-02f, -1.33189e-01f, 8.30432e-01f, 3.51186e-01f, -1.01915e-01f, 2.96543e-02f, -5.21683e-03f }, // 90/128
|
||||
{ -5.69280e-03f, 3.40940e-02f, -1.31581e-01f, 8.37557e-01f, 3.41109e-01f, -9.96402e-02f, 2.90433e-02f, -5.11376e-03f }, // 91/128
|
||||
{ -5.59731e-03f, 3.35600e-02f, -1.29874e-01f, 8.44559e-01f, 3.31048e-01f, -9.73254e-02f, 2.84182e-02f, -5.00800e-03f }, // 92/128
|
||||
{ -5.49804e-03f, 3.30021e-02f, -1.28068e-01f, 8.51437e-01f, 3.21004e-01f, -9.49727e-02f, 2.77794e-02f, -4.89961e-03f }, // 93/128
|
||||
{ -5.39500e-03f, 3.24205e-02f, -1.26161e-01f, 8.58189e-01f, 3.10980e-01f, -9.25834e-02f, 2.71272e-02f, -4.78866e-03f }, // 94/128
|
||||
{ -5.28823e-03f, 3.18153e-02f, -1.24154e-01f, 8.64812e-01f, 3.00980e-01f, -9.01591e-02f, 2.64621e-02f, -4.67520e-03f }, // 95/128
|
||||
{ -5.17776e-03f, 3.11866e-02f, -1.22047e-01f, 8.71305e-01f, 2.91006e-01f, -8.77011e-02f, 2.57844e-02f, -4.55932e-03f }, // 96/128
|
||||
{ -5.06363e-03f, 3.05345e-02f, -1.19837e-01f, 8.77666e-01f, 2.81060e-01f, -8.52109e-02f, 2.50946e-02f, -4.44107e-03f }, // 97/128
|
||||
{ -4.94589e-03f, 2.98593e-02f, -1.17526e-01f, 8.83893e-01f, 2.71144e-01f, -8.26900e-02f, 2.43930e-02f, -4.32052e-03f }, // 98/128
|
||||
{ -4.82456e-03f, 2.91609e-02f, -1.15113e-01f, 8.89984e-01f, 2.61263e-01f, -8.01399e-02f, 2.36801e-02f, -4.19774e-03f }, // 99/128
|
||||
{ -4.69970e-03f, 2.84397e-02f, -1.12597e-01f, 8.95936e-01f, 2.51417e-01f, -7.75620e-02f, 2.29562e-02f, -4.07279e-03f }, // 100/128
|
||||
{ -4.57135e-03f, 2.76957e-02f, -1.09978e-01f, 9.01749e-01f, 2.41609e-01f, -7.49577e-02f, 2.22218e-02f, -3.94576e-03f }, // 101/128
|
||||
{ -4.43955e-03f, 2.69293e-02f, -1.07256e-01f, 9.07420e-01f, 2.31843e-01f, -7.23286e-02f, 2.14774e-02f, -3.81671e-03f }, // 102/128
|
||||
{ -4.30435e-03f, 2.61404e-02f, -1.04430e-01f, 9.12947e-01f, 2.22120e-01f, -6.96762e-02f, 2.07233e-02f, -3.68570e-03f }, // 103/128
|
||||
{ -4.16581e-03f, 2.53295e-02f, -1.01501e-01f, 9.18329e-01f, 2.12443e-01f, -6.70018e-02f, 1.99599e-02f, -3.55283e-03f }, // 104/128
|
||||
{ -4.02397e-03f, 2.44967e-02f, -9.84679e-02f, 9.23564e-01f, 2.02814e-01f, -6.43069e-02f, 1.91877e-02f, -3.41815e-03f }, // 105/128
|
||||
{ -3.87888e-03f, 2.36423e-02f, -9.53307e-02f, 9.28650e-01f, 1.93236e-01f, -6.15931e-02f, 1.84071e-02f, -3.28174e-03f }, // 106/128
|
||||
{ -3.73062e-03f, 2.27664e-02f, -9.20893e-02f, 9.33586e-01f, 1.83711e-01f, -5.88617e-02f, 1.76185e-02f, -3.14367e-03f }, // 107/128
|
||||
{ -3.57923e-03f, 2.18695e-02f, -8.87435e-02f, 9.38371e-01f, 1.74242e-01f, -5.61142e-02f, 1.68225e-02f, -3.00403e-03f }, // 108/128
|
||||
{ -3.42477e-03f, 2.09516e-02f, -8.52933e-02f, 9.43001e-01f, 1.64831e-01f, -5.33522e-02f, 1.60193e-02f, -2.86289e-03f }, // 109/128
|
||||
{ -3.26730e-03f, 2.00132e-02f, -8.17385e-02f, 9.47477e-01f, 1.55480e-01f, -5.05770e-02f, 1.52095e-02f, -2.72032e-03f }, // 110/128
|
||||
{ -3.10689e-03f, 1.90545e-02f, -7.80792e-02f, 9.51795e-01f, 1.46192e-01f, -4.77900e-02f, 1.43934e-02f, -2.57640e-03f }, // 111/128
|
||||
{ -2.94361e-03f, 1.80759e-02f, -7.43154e-02f, 9.55956e-01f, 1.36968e-01f, -4.49929e-02f, 1.35716e-02f, -2.43121e-03f }, // 112/128
|
||||
{ -2.77751e-03f, 1.70776e-02f, -7.04471e-02f, 9.59958e-01f, 1.27812e-01f, -4.21869e-02f, 1.27445e-02f, -2.28483e-03f }, // 113/128
|
||||
{ -2.60868e-03f, 1.60599e-02f, -6.64743e-02f, 9.63798e-01f, 1.18725e-01f, -3.93735e-02f, 1.19125e-02f, -2.13733e-03f }, // 114/128
|
||||
{ -2.43718e-03f, 1.50233e-02f, -6.23972e-02f, 9.67477e-01f, 1.09710e-01f, -3.65541e-02f, 1.10760e-02f, -1.98880e-03f }, // 115/128
|
||||
{ -2.26307e-03f, 1.39681e-02f, -5.82159e-02f, 9.70992e-01f, 1.00769e-01f, -3.37303e-02f, 1.02356e-02f, -1.83931e-03f }, // 116/128
|
||||
{ -2.08645e-03f, 1.28947e-02f, -5.39305e-02f, 9.74342e-01f, 9.19033e-02f, -3.09033e-02f, 9.39154e-03f, -1.68894e-03f }, // 117/128
|
||||
{ -1.90738e-03f, 1.18034e-02f, -4.95412e-02f, 9.77526e-01f, 8.31162e-02f, -2.80746e-02f, 8.54441e-03f, -1.53777e-03f }, // 118/128
|
||||
{ -1.72594e-03f, 1.06946e-02f, -4.50483e-02f, 9.80543e-01f, 7.44095e-02f, -2.52457e-02f, 7.69462e-03f, -1.38589e-03f }, // 119/128
|
||||
{ -1.54221e-03f, 9.56876e-03f, -4.04519e-02f, 9.83392e-01f, 6.57852e-02f, -2.24178e-02f, 6.84261e-03f, -1.23337e-03f }, // 120/128
|
||||
{ -1.35627e-03f, 8.42626e-03f, -3.57525e-02f, 9.86071e-01f, 5.72454e-02f, -1.95925e-02f, 5.98883e-03f, -1.08030e-03f }, // 121/128
|
||||
{ -1.16820e-03f, 7.26755e-03f, -3.09503e-02f, 9.88580e-01f, 4.87921e-02f, -1.67710e-02f, 5.13372e-03f, -9.26747e-04f }, // 122/128
|
||||
{ -9.78093e-04f, 6.09305e-03f, -2.60456e-02f, 9.90917e-01f, 4.04274e-02f, -1.39548e-02f, 4.27773e-03f, -7.72802e-04f }, // 123/128
|
||||
{ -7.86031e-04f, 4.90322e-03f, -2.10389e-02f, 9.93082e-01f, 3.21531e-02f, -1.11453e-02f, 3.42130e-03f, -6.18544e-04f }, // 124/128
|
||||
{ -5.92100e-04f, 3.69852e-03f, -1.59305e-02f, 9.95074e-01f, 2.39714e-02f, -8.34364e-03f, 2.56486e-03f, -4.64053e-04f }, // 125/128
|
||||
{ -3.96391e-04f, 2.47942e-03f, -1.07209e-02f, 9.96891e-01f, 1.58840e-02f, -5.55134e-03f, 1.70888e-03f, -3.09412e-04f }, // 126/128
|
||||
{ -1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f }, // 127/128
|
||||
{ 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 128/128
|
||||
};
|
||||
|
||||
/* Instrumentation Taps */
|
||||
private static final String INSTRUMENT_SYMBOL_SPREAD = "Tap Point: Symbol Spread (Goal=2.0)";
|
||||
private FloatTap mSymbolSpreadTap;
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
|
||||
private static final int NUMBER_FILTER_TAPS = 8;
|
||||
private static final int NUMBER_FILTER_STEPS = 128;
|
||||
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
private static final int SYMBOL_RATE = 4800;
|
||||
|
||||
/* Tracking loop gain constant */
|
||||
private static final double K_SYMBOL_SPREAD = 0.0100;
|
||||
|
||||
/* Constraints on symbol spreading */
|
||||
private static final float SYMBOL_SPREAD_MAX = 2.4f; // upper range limit: +20%
|
||||
private static final float SYMBOL_SPREAD_MIN = 1.6f; // lower range limit: -20%
|
||||
|
||||
/* Symbol clock tracking loop gain */
|
||||
private static final double K_SYMBOL_TIMING = 0.025;
|
||||
|
||||
/* Coarse and fine frequency tracking constants */
|
||||
private static final double K_COARSE_FREQUENCY = 0.00125;
|
||||
private static final double K_FINE_FREQUENCY = 0.125;
|
||||
|
||||
/* Frequency correction broadcast threshold */
|
||||
// private static final double COARSE_FREQUENCY_DEADBAND = 1.66;
|
||||
private static final double COARSE_FREQUENCY_THRESHOLD = 1.20;
|
||||
|
||||
/* 2.0 symbol spread gives -3, -1, 1, 3 */
|
||||
private float mSymbolSpread = 2.0f;
|
||||
private float mSymbolClock = 0.0f;
|
||||
private float mSymbolTime = (float)SYMBOL_RATE / (float)SAMPLE_RATE;
|
||||
|
||||
private float mFineFrequencyCorrection = 0.0f;
|
||||
private float mCoarseFrequencyCorrection = 0.0f;
|
||||
|
||||
private float mHistory[] = new float[ NUMBER_FILTER_TAPS ];
|
||||
private int mHistoryLast = 0;
|
||||
|
||||
private RealSampleListener mListener;
|
||||
|
||||
private DirectGainControl mGainController = new DirectGainControl( 15.0f, 0.1f, 35.0f, 0.3f );
|
||||
|
||||
private int mFrequencyAdjustmentRequested = 0;
|
||||
private int mFrequencyCorrection = 0;
|
||||
private int mFrequencyCorrectionMaximum = 3000;
|
||||
private boolean mResetFrequencyTracker = false;
|
||||
private FrequencyCorrectionProcessor mFrequencyCorrectionProcessor;
|
||||
private Listener<FrequencyChangeEvent> mFrequencyChangeListener;
|
||||
|
||||
/**
|
||||
* C4FM Symbol Filter
|
||||
*
|
||||
* Sample Gain values - the gain of incoming sample values will critically
|
||||
* impact the performance of this filter. Gain should be adjusted to
|
||||
* optimize the value of mSymbolSpread toward a value of 2.0. If the value
|
||||
* is less than 2.0, then increase gain. If the value is over 2.0, then
|
||||
* decrease gain.
|
||||
*
|
||||
* When preceded by the RealAutomaticGainFilter class, an optimal gain
|
||||
* setting for this filter is 15.4 and that yields a symbol spread that
|
||||
* is centered on 2.0, ranging 1.92 to 2.08.
|
||||
*/
|
||||
public C4FMSymbolFilter( int frequencyCorrectionMaximum )
|
||||
{
|
||||
mFrequencyCorrectionMaximum = frequencyCorrectionMaximum;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mGainController = null;
|
||||
// mFrequencyCorrectionControl = null;
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
for( float sample: buffer.getSamples() )
|
||||
{
|
||||
receive( sample );
|
||||
}
|
||||
|
||||
/* If a frequency correction was requested during the processing of this
|
||||
* buffer, we'll apply the change and it will be reflected in the next
|
||||
* arriving buffer. Reset the lock on frequency correction and reset
|
||||
* the internal frequency correction tracker */
|
||||
if( mFrequencyAdjustmentRequested != 0 )
|
||||
{
|
||||
int correction = mFrequencyCorrection + mFrequencyAdjustmentRequested;
|
||||
|
||||
if( correction > mFrequencyCorrectionMaximum )
|
||||
{
|
||||
correction = mFrequencyCorrectionMaximum;
|
||||
}
|
||||
else if( correction < -mFrequencyCorrectionMaximum )
|
||||
{
|
||||
correction = -mFrequencyCorrectionMaximum;
|
||||
}
|
||||
|
||||
broadcast( new FrequencyChangeEvent(Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, correction ) );
|
||||
|
||||
mFrequencyAdjustmentRequested = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void receive( float sample )
|
||||
{
|
||||
sample = mGainController.correct( sample );
|
||||
|
||||
if( mResetFrequencyTracker )
|
||||
{
|
||||
mCoarseFrequencyCorrection = 0.0f;
|
||||
mFineFrequencyCorrection = 0.0f;
|
||||
|
||||
broadcast( new FrequencyChangeEvent(
|
||||
Event.REQUEST_CHANNEL_FREQUENCY_CORRECTION_CHANGE, 0 ));
|
||||
|
||||
mResetFrequencyTracker = false;
|
||||
}
|
||||
|
||||
mSymbolClock += mSymbolTime;
|
||||
|
||||
mHistory[ mHistoryLast++ ] = sample;
|
||||
|
||||
mHistoryLast %= NUMBER_FILTER_TAPS;
|
||||
|
||||
if( mSymbolClock > 1.0f )
|
||||
{
|
||||
mSymbolClock -= 1.0f;
|
||||
|
||||
int imu = (int)Math.floor( 0.5 +
|
||||
( (float)NUMBER_FILTER_STEPS * ( mSymbolClock / mSymbolTime ) ) );
|
||||
|
||||
if( imu >= NUMBER_FILTER_STEPS )
|
||||
{
|
||||
imu = NUMBER_FILTER_STEPS - 1;
|
||||
}
|
||||
|
||||
int imu_p1 = imu + 1;
|
||||
|
||||
int j = mHistoryLast;
|
||||
|
||||
double interp = 0.0;
|
||||
double interp_p1 = 0.0;
|
||||
|
||||
for( int i = 0; i < NUMBER_FILTER_TAPS; i++ )
|
||||
{
|
||||
interp += TAPS[ imu][ i ] * mHistory[ j ];
|
||||
interp_p1 += TAPS[ imu_p1 ][ i ] * mHistory[ j ];
|
||||
|
||||
j = ( j + 1 ) % NUMBER_FILTER_TAPS;
|
||||
}
|
||||
|
||||
/* Output symbol will be interpolated value corrected for symbol
|
||||
* spread and frequency offset */
|
||||
interp -= mFineFrequencyCorrection;
|
||||
interp_p1 -= mFineFrequencyCorrection;
|
||||
|
||||
/* Correct output for symbol deviation (spread) */
|
||||
float output = (float)( 2.0 * interp / mSymbolSpread );
|
||||
|
||||
/* Detect received symbol error: basically use a hard decision and
|
||||
* subtract off expected position nominal symbol level which will be
|
||||
* +/- 0.5 * symbol spread and +/- 1.5 symbol spread. Remember that
|
||||
* nominal symbol spread will be 2.0 */
|
||||
double symbolError;
|
||||
|
||||
if( interp < -mSymbolSpread )
|
||||
{
|
||||
/* symbol is -3: Expected at -1.5 * symbol spread */
|
||||
symbolError = interp + ( 1.5 * mSymbolSpread );
|
||||
mSymbolSpread -= ( symbolError * 0.5 * K_SYMBOL_SPREAD );
|
||||
}
|
||||
else if( interp < 0.0 )
|
||||
{
|
||||
/* symbol is -1: Expected at -0.5 * symbol_spread */
|
||||
symbolError = interp + (0.5 * mSymbolSpread );
|
||||
mSymbolSpread -= ( symbolError * K_SYMBOL_SPREAD );
|
||||
}
|
||||
else if( interp < mSymbolSpread )
|
||||
{
|
||||
/* symbol is +1: Expected at +0.5 * symbol_spread */
|
||||
symbolError = interp - ( 0.5 * mSymbolSpread );
|
||||
mSymbolSpread += ( symbolError * K_SYMBOL_SPREAD );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* symbol is +3: Expected at +1.5 * symbol_spread */
|
||||
symbolError = interp - ( 1.5 * mSymbolSpread );
|
||||
mSymbolSpread += ( symbolError * 0.5 * K_SYMBOL_SPREAD );
|
||||
}
|
||||
|
||||
/* Symbol clock tracking loop adjustment */
|
||||
if( interp_p1 < interp )
|
||||
{
|
||||
mSymbolClock += symbolError * K_SYMBOL_TIMING;
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolClock -= symbolError * K_SYMBOL_TIMING;
|
||||
}
|
||||
|
||||
if( mSymbolSpread < SYMBOL_SPREAD_MIN )
|
||||
{
|
||||
mGainController.increase();
|
||||
|
||||
mSymbolSpread = SYMBOL_SPREAD_MIN;
|
||||
}
|
||||
else if( mSymbolSpread > SYMBOL_SPREAD_MAX )
|
||||
{
|
||||
mGainController.decrease();
|
||||
|
||||
mSymbolSpread = SYMBOL_SPREAD_MAX;
|
||||
}
|
||||
|
||||
mCoarseFrequencyCorrection += ( ( mFineFrequencyCorrection -
|
||||
mCoarseFrequencyCorrection ) * K_COARSE_FREQUENCY );
|
||||
|
||||
mFineFrequencyCorrection += ( symbolError * K_FINE_FREQUENCY );
|
||||
|
||||
/* Queue a frequency adjustment (once per buffer) as needed */
|
||||
if( Math.abs( mCoarseFrequencyCorrection ) > COARSE_FREQUENCY_THRESHOLD )
|
||||
{
|
||||
mFrequencyAdjustmentRequested =
|
||||
500 * ( mCoarseFrequencyCorrection > 0 ? 1 : -1 );
|
||||
}
|
||||
|
||||
if( mSymbolSpreadTap != null )
|
||||
{
|
||||
mSymbolSpreadTap.receive( mSymbolSpread );
|
||||
}
|
||||
|
||||
/* dispatch the interpolated value to the listener */
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( output );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setListener( RealSampleListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener( RealSampleListener listener )
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<>();
|
||||
|
||||
/*
|
||||
* Since the target is 2.0, we subtract 2.0 from the value so that
|
||||
* a good value is plotted right on the zero center line
|
||||
*/
|
||||
TapGroup group = new TapGroup( "C4FM Symbol Filter" );
|
||||
|
||||
group.add(
|
||||
new AdditiveFloatTap( INSTRUMENT_SYMBOL_SPREAD, 0, 0.1f, -2.0f ) );
|
||||
|
||||
mAvailableTaps.add( group );
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
{
|
||||
if( tap.getName().contentEquals( INSTRUMENT_SYMBOL_SPREAD ) )
|
||||
{
|
||||
mSymbolSpreadTap = (FloatTap)tap;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
{
|
||||
if( tap.getName().contentEquals( INSTRUMENT_SYMBOL_SPREAD ) )
|
||||
{
|
||||
mSymbolSpreadTap = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a frequency change event to the registered listener
|
||||
* @param event
|
||||
*/
|
||||
public void broadcast( FrequencyChangeEvent event )
|
||||
{
|
||||
if( mFrequencyChangeListener != null )
|
||||
{
|
||||
mFrequencyChangeListener.receive( event );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener for frequency change events.
|
||||
* @param listener
|
||||
*/
|
||||
public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener )
|
||||
{
|
||||
mFrequencyChangeListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
if( mFrequencyCorrectionProcessor == null )
|
||||
{
|
||||
mFrequencyCorrectionProcessor = new FrequencyCorrectionProcessor();
|
||||
}
|
||||
|
||||
return mFrequencyCorrectionProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives notifications that the channel frequency correction has been
|
||||
* applied and updates internal tracking value.
|
||||
*/
|
||||
public class FrequencyCorrectionProcessor implements Listener<FrequencyChangeEvent>
|
||||
{
|
||||
@Override
|
||||
public void receive( FrequencyChangeEvent event )
|
||||
{
|
||||
switch( event.getEvent() )
|
||||
{
|
||||
case NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE:
|
||||
mFrequencyCorrection = event.getValue().intValue();
|
||||
|
||||
//Reset internal frequency tracking
|
||||
mCoarseFrequencyCorrection = 0.0f;
|
||||
mFineFrequencyCorrection = 0.0f;
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
mResetFrequencyTracker = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.p25;
|
||||
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
|
||||
public class DecodeConfigP25Phase1 extends DecodeConfiguration
|
||||
{
|
||||
private P25_LSMDecoder.Modulation mModulation = P25Decoder.Modulation.C4FM;
|
||||
|
||||
private int mCallTimeout = 1;
|
||||
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
|
||||
private boolean mIgnoreDataCalls = true;
|
||||
|
||||
public DecodeConfigP25Phase1()
|
||||
{
|
||||
super( DecoderType.P25_PHASE1 );
|
||||
|
||||
setAFC( false );
|
||||
}
|
||||
|
||||
public boolean supportsAFC()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@XmlAttribute( name = "modulation" )
|
||||
public P25_LSMDecoder.Modulation getModulation()
|
||||
{
|
||||
return mModulation;
|
||||
}
|
||||
|
||||
public void setModulation( P25_LSMDecoder.Modulation modulation )
|
||||
{
|
||||
mModulation = modulation;
|
||||
}
|
||||
|
||||
@XmlAttribute( name = "ignore_data_calls" )
|
||||
public boolean getIgnoreDataCalls()
|
||||
{
|
||||
return mIgnoreDataCalls;
|
||||
}
|
||||
|
||||
public void setIgnoreDataCalls( boolean ignore )
|
||||
{
|
||||
mIgnoreDataCalls = ignore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this field is now deprecated.
|
||||
* @return
|
||||
*/
|
||||
@XmlTransient
|
||||
@Deprecated
|
||||
public int getCallTimeout()
|
||||
{
|
||||
return mCallTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the call timeout value in seconds ( 10 - 600 );
|
||||
* @param timeout
|
||||
*/
|
||||
@Deprecated
|
||||
public void setCallTimeout( int timeout )
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@XmlAttribute( name="traffic_channel_pool_size" )
|
||||
public int getTrafficChannelPoolSize()
|
||||
{
|
||||
return mTrafficChannelPoolSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the traffic channel pool size which is the maximum number of
|
||||
* simultaneous traffic channels that can be allocated.
|
||||
*
|
||||
* This limits the maximum calls so that busy systems won't cause more
|
||||
* traffic channels to be allocated than the decoder/software/host computer
|
||||
* can support.
|
||||
*/
|
||||
public void setTrafficChannelPoolSize( int size )
|
||||
{
|
||||
mTrafficChannelPoolSize = size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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 io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
|
||||
public class P25CallEvent extends CallEvent
|
||||
{
|
||||
private String mChannel;
|
||||
private long mFrequency;
|
||||
|
||||
public P25CallEvent( CallEventType callEventType,
|
||||
AliasList aliasList,
|
||||
String fromID,
|
||||
String toID,
|
||||
String channel,
|
||||
long frequency,
|
||||
String details )
|
||||
{
|
||||
super( DecoderType.P25_PHASE1, callEventType,
|
||||
aliasList, fromID, toID, details );
|
||||
|
||||
mChannel = channel;
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
private Alias getAlias( String talkgroup )
|
||||
{
|
||||
if( hasAliasList() )
|
||||
{
|
||||
return getAliasList().getTalkgroupAlias( talkgroup );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
return getAlias( getFromID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
return getAlias( getToID() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChannel()
|
||||
{
|
||||
return mChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "P25 " );
|
||||
sb.append( getCallEventType().name() );
|
||||
sb.append( " CHAN[" );
|
||||
sb.append( getChannel() );
|
||||
sb.append( "] TO[" );
|
||||
sb.append( getToID() );
|
||||
sb.append( "] FROM[" );
|
||||
sb.append( getFromID() );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static class Builder
|
||||
{
|
||||
/* Required parameters */
|
||||
private CallEventType mCallEventType;
|
||||
|
||||
/* Optional parameters */
|
||||
private AliasList mAliasList;
|
||||
private String mFromID;
|
||||
private String mToID;
|
||||
private String mDetails;
|
||||
private String mChannel;
|
||||
private long mFrequency;
|
||||
|
||||
public Builder( CallEventType callEventType )
|
||||
{
|
||||
mCallEventType = callEventType;
|
||||
}
|
||||
|
||||
public Builder aliasList( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder from( String val )
|
||||
{
|
||||
mFromID = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder channel( String channel )
|
||||
{
|
||||
mChannel = channel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder details( String details )
|
||||
{
|
||||
mDetails = details;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder frequency( long frequency )
|
||||
{
|
||||
mFrequency = frequency;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder to( String toID )
|
||||
{
|
||||
mToID = toID;
|
||||
return this;
|
||||
}
|
||||
|
||||
public P25CallEvent build()
|
||||
{
|
||||
return new P25CallEvent( this );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor for the builder
|
||||
*/
|
||||
private P25CallEvent( Builder builder )
|
||||
{
|
||||
this( builder.mCallEventType,
|
||||
builder.mAliasList,
|
||||
builder.mFromID,
|
||||
builder.mToID,
|
||||
builder.mChannel,
|
||||
builder.mFrequency,
|
||||
builder.mDetails );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.instrument.Instrumentable;
|
||||
import io.github.dsheirer.module.decode.Decoder;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProvider;
|
||||
|
||||
public abstract class P25Decoder extends Decoder implements IFrequencyChangeListener, IFrequencyChangeProvider, Instrumentable
|
||||
{
|
||||
private P25MessageProcessor mMessageProcessor;
|
||||
|
||||
public P25Decoder( AliasList aliasList )
|
||||
{
|
||||
mMessageProcessor = new P25MessageProcessor( aliasList );
|
||||
mMessageProcessor.setMessageListener( this );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mMessageProcessor.dispose();
|
||||
mMessageProcessor = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.P25_PHASE1;
|
||||
}
|
||||
|
||||
protected P25MessageProcessor getMessageProcessor()
|
||||
{
|
||||
return mMessageProcessor;
|
||||
}
|
||||
|
||||
public abstract Modulation getModulation();
|
||||
|
||||
public enum Modulation
|
||||
{
|
||||
CQPSK( "Simulcast (LSM)", "LSM" ),
|
||||
C4FM( "Normal (C4FM)", "C4FM" );
|
||||
|
||||
private String mLabel;
|
||||
private String mShortLabel;
|
||||
|
||||
private Modulation( String label, String shortLabel )
|
||||
{
|
||||
mLabel = label;
|
||||
mShortLabel = shortLabel;
|
||||
}
|
||||
|
||||
public String getLabel()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public String getShortLabel()
|
||||
{
|
||||
return mShortLabel;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return getLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*******************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EditorValidationException;
|
||||
import io.github.dsheirer.gui.editor.ValidatingEditor;
|
||||
import io.github.dsheirer.module.decode.AuxDecodeConfigurationEditor;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.source.SourceConfigurationEditor;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class P25DecoderEditor extends ValidatingEditor<Channel>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( P25DecoderEditor.class );
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private JComboBox<P25_LSMDecoder.Modulation> mComboModulation;
|
||||
private JCheckBox mIgnoreDataCalls;
|
||||
private JLabel mTrafficChannelPoolSizeLabel;
|
||||
private JSlider mTrafficChannelPoolSize;
|
||||
|
||||
public P25DecoderEditor()
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0,wrap 3", "[right][grow,fill][]", "" ) );
|
||||
|
||||
mComboModulation = new JComboBox<P25_LSMDecoder.Modulation>();
|
||||
mComboModulation.setModel( new DefaultComboBoxModel<P25_LSMDecoder.Modulation>(
|
||||
P25_LSMDecoder.Modulation.values() ) );
|
||||
mComboModulation.setEnabled( false );
|
||||
mComboModulation.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
|
||||
add( new JLabel( "Modulation:" ) );
|
||||
add( mComboModulation );
|
||||
|
||||
mIgnoreDataCalls = new JCheckBox( "Ignore Data Calls" );
|
||||
mIgnoreDataCalls.setEnabled( false );
|
||||
mIgnoreDataCalls.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
|
||||
add( mIgnoreDataCalls );
|
||||
|
||||
mTrafficChannelPoolSize = new JSlider( JSlider.HORIZONTAL, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MINIMUM,
|
||||
DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MAXIMUM, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT );
|
||||
mTrafficChannelPoolSize.setEnabled( false );
|
||||
mTrafficChannelPoolSize.setMajorTickSpacing( 10 );
|
||||
mTrafficChannelPoolSize.setMinorTickSpacing( 5 );
|
||||
mTrafficChannelPoolSize.setPaintTicks( true );
|
||||
mTrafficChannelPoolSize.addChangeListener( new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged( ChangeEvent e )
|
||||
{
|
||||
mTrafficChannelPoolSizeLabel.setText( "Traffic Pool: " + mTrafficChannelPoolSize.getValue() );
|
||||
setModified( true );
|
||||
}
|
||||
} );
|
||||
|
||||
mTrafficChannelPoolSizeLabel = new JLabel( "Traffic Pool: " + mTrafficChannelPoolSize.getValue() + " " );
|
||||
|
||||
add( mTrafficChannelPoolSizeLabel );
|
||||
add( mTrafficChannelPoolSize );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasItem() && isModified() )
|
||||
{
|
||||
DecodeConfigP25Phase1 p25 = new DecodeConfigP25Phase1();
|
||||
|
||||
p25.setModulation( (P25Decoder.Modulation)mComboModulation.getSelectedItem() );
|
||||
p25.setIgnoreDataCalls( mIgnoreDataCalls.isSelected() );
|
||||
p25.setTrafficChannelPoolSize( mTrafficChannelPoolSize.getValue() );
|
||||
|
||||
getItem().setDecodeConfiguration( p25 );
|
||||
}
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate( Editor<Channel> editor ) throws EditorValidationException
|
||||
{
|
||||
/**
|
||||
* Enforce CQPSK modulation uses a Tuner Source for I/Q sample data
|
||||
*/
|
||||
if( editor instanceof SourceConfigurationEditor &&
|
||||
((P25Decoder.Modulation)mComboModulation.getSelectedItem()) == P25Decoder.Modulation.CQPSK )
|
||||
{
|
||||
SourceConfiguration config = ((SourceConfigurationEditor)editor).getSourceConfiguration();
|
||||
|
||||
if( config != null && !( config instanceof SourceConfigTuner ) )
|
||||
{
|
||||
throw new EditorValidationException( editor,
|
||||
"<html><body width='175'><h3>LSM Simulcast</h3>"
|
||||
+ "<p>P25 LSM Simulcast decoder can only be used with "
|
||||
+ "a tuner source. Please change the Source to use a tuner"
|
||||
+ " or change the P25 Decoder to C4FM modulation</p>" );
|
||||
}
|
||||
}
|
||||
|
||||
if( editor instanceof AuxDecodeConfigurationEditor)
|
||||
{
|
||||
AuxDecodeConfiguration config = ((AuxDecodeConfigurationEditor)editor).getConfiguration();
|
||||
|
||||
if( config != null && !config.getAuxDecoders().isEmpty() )
|
||||
{
|
||||
throw new EditorValidationException( editor,
|
||||
"<html><body width='175'><h3>Auxilary Decoders</h3>"
|
||||
+ "<p>The auxiliary decoders work with analog audio and are not "
|
||||
+ "compatible with the P25 Decoder. Please uncheck any auxiliary "
|
||||
+ "decoders that you have selected for this channel</p>" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setControlsEnabled( boolean enabled )
|
||||
{
|
||||
if( mComboModulation.isEnabled() != enabled )
|
||||
{
|
||||
mComboModulation.setEnabled( enabled );
|
||||
}
|
||||
|
||||
if( mTrafficChannelPoolSize.isEnabled() != enabled )
|
||||
{
|
||||
mTrafficChannelPoolSize.setEnabled( enabled );
|
||||
}
|
||||
|
||||
if( mIgnoreDataCalls.isEnabled() != enabled )
|
||||
{
|
||||
mIgnoreDataCalls.setEnabled( enabled );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem( Channel item )
|
||||
{
|
||||
super.setItem( item );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
setControlsEnabled( true );
|
||||
|
||||
DecodeConfiguration config = getItem().getDecodeConfiguration();
|
||||
|
||||
if( config instanceof DecodeConfigP25Phase1 )
|
||||
{
|
||||
DecodeConfigP25Phase1 p25 = (DecodeConfigP25Phase1)config;
|
||||
|
||||
mComboModulation.setSelectedItem( p25.getModulation() );
|
||||
mIgnoreDataCalls.setSelected( p25.getIgnoreDataCalls() );
|
||||
mTrafficChannelPoolSize.setValue( p25.getTrafficChannelPoolSize() );
|
||||
setModified( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComboModulation.setSelectedItem( P25Decoder.Modulation.C4FM );
|
||||
mIgnoreDataCalls.setSelected( false );
|
||||
mTrafficChannelPoolSize.setValue( DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT );
|
||||
setModified( true );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setControlsEnabled( false );
|
||||
setModified( false );
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.BitSet;
|
||||
|
||||
/**
|
||||
* Utility class to process interleave of P25 Voice and Data messages.
|
||||
*/
|
||||
public class P25Interleave
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( P25Interleave.class );
|
||||
|
||||
private static int[] DATA_INTERLEAVE = new int[] { 0,1,2,3,52,53,54,55,100,
|
||||
101,102,103,148,149,150,151,4,5,6,7,56,57,58,59,104,105,106,107,152,153,
|
||||
154,155,8,9,10,11,60,61,62,63,108,109,110,111,156,157,158,159,12,13,14,
|
||||
15,64,65,66,67,112,113,114,115,160,161,162,163,16,17,18,19,68,69,70,71,
|
||||
116,117,118,119,164,165,166,167,20,21,22,23,72,73,74,75,120,121,122,123,
|
||||
168,169,170,171,24,25,26,27,76,77,78,79,124,125,126,127,172,173,174,175,
|
||||
28,29,30,31,80,81,82,83,128,129,130,131,176,177,178,179,32,33,34,35,84,
|
||||
85,86,87,132,133,134,135,180,181,182,183,36,37,38,39,88,89,90,91,136,
|
||||
137,138,139,184,185,186,187,40,41,42,43,92,93,94,95,140,141,142,143,188,
|
||||
189,190,191,44,45,46,47,96,97,98,99,144,145,146,147,192,193,194,195,48,
|
||||
49,50,51 };
|
||||
|
||||
private static int[] DATA_DEINTERLEAVE = new int[] { 0,1,2,3,16,17,18,19,32,
|
||||
33,34,35,48,49,50,51,64,65,66,67,80,81,82,83,96,97,98,99,112,113,114,115,
|
||||
128,129,130,131,144,145,146,147,160,161,162,163,176,177,178,179,192,193,
|
||||
194,195,4,5,6,7,20,21,22,23,36,37,38,39,52,53,54,55,68,69,70,71,84,85,
|
||||
86,87,100,101,102,103,116,117,118,119,132,133,134,135,148,149,150,151,
|
||||
164,165,166,167,180,181,182,183,8,9,10,11,24,25,26,27,40,41,42,43,56,57,
|
||||
58,59,72,73,74,75,88,89,90,91,104,105,106,107,120,121,122,123,136,137,
|
||||
138,139,152,153,154,155,168,169,170,171,184,185,186,187,12,13,14,15,28,
|
||||
29,30,31,44,45,46,47,60,61,62,63,76,77,78,79,92,93,94,95,108,109,110,
|
||||
111,124,125,126,127,140,141,142,143,156,157,158,159,172,173,174,175,188,
|
||||
189,190,191 };
|
||||
|
||||
private static int[] VOICE_INTERLEAVE = new int[] { 0,24,48,72,96,120,25,
|
||||
1,73,49,121,97,2,26,50,74,98,122,27,3,75,51,123,99,4,28,52,76,100,124,
|
||||
29,5,77,53,125,101,6,30,54,78,102,126,31,7,79,55,127,103,8,32,56,80,104,
|
||||
128,33,9,81,57,129,105,10,34,58,82,106,130,35,11,83,59,131,107,12,36,60,
|
||||
84,108,132,37,13,85,61,133,109,14,38,62,86,110,134,39,87,15,63,135,111,
|
||||
16,40,64,88,112,136,41,17,89,65,137,113,18,42,66,90,114,138,43,19,91,
|
||||
67,139,115,20,44,68,92,116,140,45,21,93,69,141,117,22,46,70,94,119,142,
|
||||
47,23,95,71,143,118 };
|
||||
|
||||
private static int[] VOICE_DEINTERLEAVE = new int[] { 0,7,12,19,24,31,36,43,
|
||||
48,55,60,67,72,79,84,91,96,103,108,115,120,127,132,139,1,6,13,18,25,30,
|
||||
37,42,49,54,61,66,73,78,85,90,97,102,109,114,121,126,133,138,2,9,14,21,
|
||||
26,33,38,45,50,57,62,69,74,81,86,93,98,105,110,117,122,129,134,141,3,8,
|
||||
15,20,27,32,39,44,51,56,63,68,75,80,87,92,99,104,111,116,123,128,135,
|
||||
140,4,11,16,23,28,35,40,47,52,59,64,71,76,83,88,95,100,107,112,119,124,
|
||||
131,136,143,5,10,17,22,29,34,41,46,53,58,65,70,77,82,89,94,101,106,113,
|
||||
118,125,130,137,142 };
|
||||
/**
|
||||
* Deinterleaves the 196-bit block in message, identified by start and end
|
||||
* bit positions. Note: end index (exclusive) should be one more than the
|
||||
* last bit in the block.
|
||||
*
|
||||
* @param message - source message to deinterleave
|
||||
* @param start - starting bit index for the block
|
||||
* @param end - ending bit index for the block, plus 1
|
||||
*/
|
||||
public static BinaryMessage deinterleaveData( BinaryMessage message,
|
||||
int start, int end )
|
||||
{
|
||||
return deinterleave( DATA_DEINTERLEAVE, message, start, end );
|
||||
}
|
||||
|
||||
public static BinaryMessage deinterleaveVoice( BinaryMessage message,
|
||||
int start, int end )
|
||||
{
|
||||
return deinterleave( VOICE_DEINTERLEAVE, message, start, end );
|
||||
}
|
||||
|
||||
public static BinaryMessage deinterleave( int[] pattern, BinaryMessage message,
|
||||
int start, int end )
|
||||
{
|
||||
BitSet original = message.get( start, end );
|
||||
|
||||
/* Clear block bits in source message */
|
||||
message.clear( start, end );
|
||||
|
||||
/* Iterate only the set bits in the original message and apply
|
||||
* the deinterleave -- we don't have to evaluate the 0 bits */
|
||||
for (int i = original.nextSetBit( 0 );
|
||||
i >= 0 && i < pattern.length;
|
||||
i = original.nextSetBit( i + 1 ) )
|
||||
{
|
||||
message.set( start + pattern[ i ] );
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interleaves the 196-bit block in message, identified by start and end
|
||||
* bit positions. Note: end index (exclusive) should be one more than the
|
||||
* last bit in the block.
|
||||
*
|
||||
* @param message - source message to interleave
|
||||
* @param start - starting bit index for the block
|
||||
* @param end - ending bit index for the block, plus 1
|
||||
*/
|
||||
public static BinaryMessage interleaveData( BinaryMessage message,
|
||||
int start, int end )
|
||||
{
|
||||
return interleave( DATA_INTERLEAVE, message, start, end );
|
||||
}
|
||||
|
||||
public static BinaryMessage interleaveVoice( BinaryMessage message,
|
||||
int start, int end )
|
||||
{
|
||||
return interleave( VOICE_INTERLEAVE, message, start, end );
|
||||
}
|
||||
|
||||
public static BinaryMessage interleave( int[] pattern, BinaryMessage message,
|
||||
int start, int end )
|
||||
{
|
||||
BitSet original = message.get( start, end );
|
||||
|
||||
/* Clear block bits in source message */
|
||||
message.clear( start, end );
|
||||
|
||||
/* Iterate only the set bits in the original message and apply
|
||||
* the deinterleave -- we don't have to evaluate the 0 bits */
|
||||
for (int i = original.nextSetBit( 0 );
|
||||
i >= 0 && i < pattern.length;
|
||||
i = original.nextSetBit( i + 1 ) )
|
||||
{
|
||||
message.set( start + pattern[ i ] );
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.BitSetFullException;
|
||||
import io.github.dsheirer.bits.ISyncDetectListener;
|
||||
import io.github.dsheirer.bits.MultiSyncPatternMatcher;
|
||||
import io.github.dsheirer.bits.SoftSyncDetector;
|
||||
import io.github.dsheirer.bits.SyncDetector;
|
||||
import io.github.dsheirer.dsp.psk.LSMDemodulator;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.dsp.symbol.FrameSync;
|
||||
import io.github.dsheirer.edac.BCH_63_16_11;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.edac.CRCP25;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.P25Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.hdu.HDUMessage;
|
||||
import io.github.dsheirer.module.decode.p25.message.ldu.LDU1Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.ldu.LDU2Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.ldu.lc.LDULCMessageFactory;
|
||||
import io.github.dsheirer.module.decode.p25.message.pdu.PDUMessage;
|
||||
import io.github.dsheirer.module.decode.p25.message.pdu.PDUMessageFactory;
|
||||
import io.github.dsheirer.module.decode.p25.message.pdu.confirmed.PDUConfirmedMessage;
|
||||
import io.github.dsheirer.module.decode.p25.message.pdu.confirmed.PDUConfirmedMessageFactory;
|
||||
import io.github.dsheirer.module.decode.p25.message.tdu.TDUMessage;
|
||||
import io.github.dsheirer.module.decode.p25.message.tdu.lc.TDULCMessageFactory;
|
||||
import io.github.dsheirer.module.decode.p25.message.tdu.lc.TDULinkControlMessage;
|
||||
import io.github.dsheirer.module.decode.p25.message.tsbk.TSBKMessage;
|
||||
import io.github.dsheirer.module.decode.p25.message.tsbk.TSBKMessageFactory;
|
||||
import io.github.dsheirer.module.decode.p25.message.vselp.VSELP1Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.vselp.VSELP2Message;
|
||||
import io.github.dsheirer.module.decode.p25.reference.DataUnitID;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class P25MessageFramer implements Listener<Dibit>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( P25MessageFramer.class );
|
||||
|
||||
/* Determines the threshold for sync pattern soft matching */
|
||||
private static final int SYNC_MATCH_THRESHOLD = 2;
|
||||
private static final int SYNC_IN_CALL_THRESHOLD = 4;
|
||||
|
||||
/* Costas Loop phase lock error correction values. A phase lock error of
|
||||
* 90 degrees requires a correction of 1/4 of the symbol rate (1200Hz). An
|
||||
* error of 180 degrees requires a correction of 1/2 of the symbol rate */
|
||||
public static final double SYMBOL_FREQUENCY = 2.0d * Math.PI * 4800.0d / 48000.0d;
|
||||
public static final double PHASE_CORRECTION_90_DEGREES = SYMBOL_FREQUENCY / 4.0d;
|
||||
public static final double PHASE_CORRECTION_180_DEGREES = SYMBOL_FREQUENCY / 2.0d;
|
||||
public static final int TSBK_BEGIN = 64;
|
||||
public static final int TSBK_CRC_START = 144;
|
||||
public static final int TSBK_END = 260;
|
||||
public static final int TSBK_DECODED_END = 160;
|
||||
|
||||
public static final int PDU0_BEGIN = 64;
|
||||
public static final int PDU0_CRC_BEGIN = 144;
|
||||
public static final int PDU0_END = 260;
|
||||
public static final int PDU1_BEGIN = 160;
|
||||
public static final int PDU1_END = 356;
|
||||
public static final int PDU2_BEGIN = 256;
|
||||
public static final int PDU2_END = 452;
|
||||
public static final int PDU3_BEGIN = 352;
|
||||
public static final int PDU3_END = 548;
|
||||
public static final int PDU3_DECODED_END = 448;
|
||||
|
||||
private SoftSyncDetector mPrimarySyncDetector = new SoftSyncDetector(
|
||||
FrameSync.P25_PHASE1_NORMAL.getSync(), SYNC_MATCH_THRESHOLD );
|
||||
|
||||
private MultiSyncPatternMatcher mMatcher = new MultiSyncPatternMatcher( 48 );
|
||||
private ArrayList<P25MessageAssembler> mAssemblers =
|
||||
new ArrayList<P25MessageAssembler>();
|
||||
|
||||
private Listener<Message> mListener;
|
||||
private AliasList mAliasList;
|
||||
|
||||
private Trellis_1_2_Rate mHalfRate = new Trellis_1_2_Rate();
|
||||
private Trellis_3_4_Rate mThreeQuarterRate = new Trellis_3_4_Rate();
|
||||
private BCH_63_16_11 mNIDDecoder = new BCH_63_16_11();
|
||||
|
||||
/**
|
||||
* Constructs a P25 message framer to receive a stream of symbols and
|
||||
* detect the sync pattern then capture the following stream of symbols up
|
||||
* to the message length, and then broadcast that bit buffer to the registered
|
||||
* listener.
|
||||
*/
|
||||
public P25MessageFramer( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
|
||||
mPrimarySyncDetector.setListener( new ISyncDetectListener()
|
||||
{
|
||||
@Override
|
||||
public void syncDetected()
|
||||
{
|
||||
for( P25MessageAssembler assembler: mAssemblers )
|
||||
{
|
||||
if( !assembler.isActive() )
|
||||
{
|
||||
assembler.setActive( true );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
mMatcher.add( mPrimarySyncDetector );
|
||||
|
||||
/**
|
||||
* We use two message assemblers to catch any sync detections, so that
|
||||
* if we have a false trigger then we still have a chance to catch the
|
||||
* valid message. If we have two false triggers before the arrival of
|
||||
* the actual message sync, then the third or subsequent real or false
|
||||
* sync pattern will produce a debug message indicating no assemblers
|
||||
* are available..
|
||||
*/
|
||||
mAssemblers.add( new P25MessageAssembler() );
|
||||
mAssemblers.add( new P25MessageAssembler() );
|
||||
}
|
||||
|
||||
public P25MessageFramer( AliasList aliasList, LSMDemodulator demodulator )
|
||||
{
|
||||
this( aliasList );
|
||||
|
||||
if( demodulator != null )
|
||||
{
|
||||
/* For CQPSK, we include 3 additional sync detectors to watch for and
|
||||
* correct +/-90 and 180 degree costas loop phase lock errors */
|
||||
mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_90_CCW,
|
||||
demodulator, PHASE_CORRECTION_90_DEGREES ) );
|
||||
|
||||
mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_90_CW,
|
||||
demodulator, -PHASE_CORRECTION_90_DEGREES ) );
|
||||
|
||||
mMatcher.add( new CostasPhaseErrorDetector( FrameSync.P25_PHASE1_ERROR_180,
|
||||
demodulator, PHASE_CORRECTION_180_DEGREES ) );
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
for( P25MessageAssembler a: mAssemblers )
|
||||
{
|
||||
a.dispose();
|
||||
}
|
||||
|
||||
mAssemblers.clear();
|
||||
|
||||
mListener = null;
|
||||
mAliasList = null;
|
||||
|
||||
mMatcher.dispose();
|
||||
mMatcher = null;
|
||||
|
||||
mPrimarySyncDetector.dispose();
|
||||
mPrimarySyncDetector = null;
|
||||
|
||||
}
|
||||
|
||||
private void dispatch( Message message )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( message );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( Dibit symbol )
|
||||
{
|
||||
for( P25MessageAssembler assembler: mAssemblers )
|
||||
{
|
||||
if( assembler.isActive() )
|
||||
{
|
||||
assembler.receive( symbol );
|
||||
|
||||
if( assembler.complete() )
|
||||
{
|
||||
assembler.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mMatcher.receive( symbol.getBit1(), symbol.getBit2() );
|
||||
}
|
||||
|
||||
public void setListener( Listener<Message> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void removeListener( Listener<Message> listener )
|
||||
{
|
||||
mListener = null;
|
||||
}
|
||||
|
||||
private class P25MessageAssembler
|
||||
{
|
||||
/* Starting position of the status symbol counter is 24 symbols to
|
||||
* account for the 48-bit sync pattern which is not included in message */
|
||||
private int mStatusSymbolPointer = 24;
|
||||
private BinaryMessage mMessage;
|
||||
private int mMessageLength;
|
||||
private boolean mComplete = false;
|
||||
private boolean mActive = false;
|
||||
private DataUnitID mDUID = DataUnitID.NID;
|
||||
|
||||
public P25MessageAssembler()
|
||||
{
|
||||
mMessageLength = mDUID.getMessageLength();
|
||||
mMessage = new BinaryMessage( mMessageLength );
|
||||
reset();
|
||||
}
|
||||
|
||||
public void receive( Dibit dibit )
|
||||
{
|
||||
if( mActive )
|
||||
{
|
||||
if( mStatusSymbolPointer == 35 )
|
||||
{
|
||||
mStatusSymbolPointer = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
mStatusSymbolPointer++;
|
||||
|
||||
try
|
||||
{
|
||||
mMessage.add( dibit.getBit1() );
|
||||
mMessage.add( dibit.getBit2() );
|
||||
}
|
||||
catch( BitSetFullException e )
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Check the message for complete */
|
||||
if( mMessage.isFull() )
|
||||
{
|
||||
checkComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
mDUID = DataUnitID.NID;
|
||||
mMessage.setSize( mDUID.getMessageLength() );
|
||||
mMessage.clear();
|
||||
mStatusSymbolPointer = 24;
|
||||
mComplete = false;
|
||||
mActive = false;
|
||||
}
|
||||
|
||||
public void setActive( boolean active )
|
||||
{
|
||||
mActive = active;
|
||||
}
|
||||
|
||||
private void setDUID( DataUnitID id )
|
||||
{
|
||||
mDUID = id;
|
||||
mMessageLength = id.getMessageLength();
|
||||
mMessage.setSize( mMessageLength );
|
||||
}
|
||||
|
||||
private void checkComplete()
|
||||
{
|
||||
switch( mDUID )
|
||||
{
|
||||
case NID:
|
||||
mMessage = mNIDDecoder.correctNID( mMessage );
|
||||
|
||||
if( mMessage.getCRC() != CRC.FAILED_CRC )
|
||||
{
|
||||
int value = mMessage.getInt( P25Message.DUID );
|
||||
|
||||
DataUnitID duid = DataUnitID.fromValue( value );
|
||||
|
||||
if( duid != DataUnitID.UNKN )
|
||||
{
|
||||
setDUID( duid );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
dispatch( new P25Message( mMessage.copy(), mDUID, mAliasList ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
break;
|
||||
case HDU:
|
||||
mComplete = true;
|
||||
dispatch( new HDUMessage( mMessage.copy(), mDUID, mAliasList ) );
|
||||
|
||||
/* We're in a call now, lower the sync match threshold */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
|
||||
break;
|
||||
case LDU1:
|
||||
mComplete = true;
|
||||
|
||||
LDU1Message ldu1 = new LDU1Message( mMessage.copy(),
|
||||
mDUID, mAliasList );
|
||||
|
||||
/* Convert the LDU1 message into a link control LDU1 message */
|
||||
dispatch( LDULCMessageFactory.getMessage( ldu1 ) );
|
||||
|
||||
/* We're in a call now, lower the sync match threshold */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
|
||||
break;
|
||||
case LDU2:
|
||||
mComplete = true;
|
||||
dispatch( new LDU2Message( mMessage.copy(), mDUID, mAliasList ) );
|
||||
|
||||
/* We're in a call now, lower the sync match threshold */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_IN_CALL_THRESHOLD );
|
||||
break;
|
||||
case PDU0:
|
||||
|
||||
/* Remove interleaving */
|
||||
P25Interleave.deinterleaveData( mMessage, PDU0_BEGIN, PDU0_END );
|
||||
|
||||
/* Remove trellis encoding - abort processing if we have an
|
||||
* unsuccessful decode due to excessive errors */
|
||||
if( mHalfRate.decode( mMessage, PDU0_BEGIN, PDU0_END ) )
|
||||
{
|
||||
mMessage = CRCP25.correctCCITT80( mMessage,
|
||||
PDU0_BEGIN, PDU0_CRC_BEGIN );
|
||||
|
||||
|
||||
if( mMessage.getCRC() != CRC.FAILED_CRC )
|
||||
{
|
||||
boolean confirmed = mMessage.get(
|
||||
PDUMessage.CONFIRMATION_REQUIRED_INDICATOR );
|
||||
|
||||
if( confirmed )
|
||||
{
|
||||
setDUID( DataUnitID.PDUC );
|
||||
}
|
||||
else
|
||||
{
|
||||
setDUID( DataUnitID.PDU1 );
|
||||
}
|
||||
|
||||
mMessage.setPointer( PDU1_BEGIN );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
|
||||
break;
|
||||
case PDU1:
|
||||
/* Remove interleaving */
|
||||
P25Interleave.deinterleaveData( mMessage, PDU1_BEGIN, PDU1_END );
|
||||
|
||||
/* Remove trellis encoding - abort processing if we have an
|
||||
* unsuccessful decode due to excessive errors */
|
||||
if( mHalfRate.decode( mMessage, PDU1_BEGIN, PDU1_END ) )
|
||||
{
|
||||
if( mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ) == 1 )
|
||||
{
|
||||
mMessage.setSize( PDU2_BEGIN );
|
||||
|
||||
PDUMessage pduMessage1 = PDUMessageFactory.getMessage(
|
||||
mMessage.copy(), DataUnitID.PDU1, mAliasList );
|
||||
|
||||
dispatch( pduMessage1 );
|
||||
|
||||
mComplete = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
setDUID( DataUnitID.PDU2 );
|
||||
|
||||
mMessage.setPointer( PDU2_BEGIN );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
break;
|
||||
case PDU2:
|
||||
/* Remove interleaving */
|
||||
P25Interleave.deinterleaveData( mMessage, PDU2_BEGIN, PDU2_END );
|
||||
|
||||
/* Remove trellis encoding - abort processing if we have an
|
||||
* unsuccessful decode due to excessive errors */
|
||||
if( mHalfRate.decode( mMessage, PDU2_BEGIN, PDU2_END ) )
|
||||
{
|
||||
if( mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW ) == 2 )
|
||||
{
|
||||
mMessage.setSize( PDU3_BEGIN );
|
||||
|
||||
PDUMessage pduMessage2 = PDUMessageFactory.getMessage(
|
||||
mMessage.copy(), DataUnitID.PDU2, mAliasList );
|
||||
|
||||
dispatch( pduMessage2 );
|
||||
|
||||
mComplete = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
setDUID( DataUnitID.PDU3 );
|
||||
|
||||
mMessage.setPointer( PDU3_BEGIN );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case PDU3:
|
||||
/* Remove interleaving */
|
||||
P25Interleave.deinterleaveData( mMessage, PDU3_BEGIN, PDU3_END );
|
||||
|
||||
/* Remove trellis encoding - abort processing if we have an
|
||||
* unsuccessful decode due to excessive errors */
|
||||
if( mHalfRate.decode( mMessage, PDU3_BEGIN, PDU3_END ) )
|
||||
{
|
||||
mMessage.setSize( PDU3_DECODED_END );
|
||||
|
||||
PDUMessage pduMessage3 = PDUMessageFactory.getMessage(mMessage.copy(), DataUnitID.PDU3, mAliasList );
|
||||
|
||||
dispatch( pduMessage3 );
|
||||
}
|
||||
|
||||
mComplete = true;
|
||||
break;
|
||||
case PDUC:
|
||||
/* De-interleave the latest block*/
|
||||
P25Interleave.deinterleaveData( mMessage,
|
||||
mMessage.size() - 196, mMessage.size() );
|
||||
|
||||
/* Decode 3/4 rate convolutional encoding from latest block */
|
||||
if( mThreeQuarterRate.decode( mMessage,
|
||||
mMessage.size() - 196, mMessage.size() ) )
|
||||
{
|
||||
/* Resize the message and adjust the message pointer
|
||||
* to account for removing 48 + 4 parity bits */
|
||||
mMessage.setSize( mMessage.size() - 52 );
|
||||
mMessage.adjustPointer( -52 );
|
||||
|
||||
int blocks = mMessage.getInt( PDUMessage.BLOCKS_TO_FOLLOW );
|
||||
|
||||
int current = ( mMessage.size() - 160 ) / 144;
|
||||
|
||||
if( current < blocks )
|
||||
{
|
||||
mMessage.setSize( mMessage.size() + 196 );
|
||||
mMessageLength = mMessage.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
PDUConfirmedMessage pducm =
|
||||
new PDUConfirmedMessage( mMessage.copy(),
|
||||
mAliasList );
|
||||
|
||||
/* Translate into correct subclass */
|
||||
dispatch( PDUConfirmedMessageFactory.getMessage( pducm ) );
|
||||
|
||||
mComplete = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
PDUConfirmedMessage pducm =
|
||||
new PDUConfirmedMessage( mMessage.copy(),
|
||||
mAliasList );
|
||||
|
||||
/* Translate into correct subclass */
|
||||
dispatch( PDUConfirmedMessageFactory.getMessage( pducm ) );
|
||||
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case TDU:
|
||||
dispatch( new TDUMessage( mMessage.copy(), mDUID, mAliasList ) );
|
||||
mComplete = true;
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case TDULC:
|
||||
TDULinkControlMessage tdulc = new TDULinkControlMessage(
|
||||
mMessage.copy(), mDUID, mAliasList );
|
||||
|
||||
/* Convert to an appropriate link control message */
|
||||
tdulc = TDULCMessageFactory.getMessage( tdulc );
|
||||
|
||||
dispatch( tdulc );
|
||||
mComplete = true;
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case TSBK1:
|
||||
/* Remove interleaving */
|
||||
P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
|
||||
|
||||
/* Remove trellis encoding - abort processing if we have an
|
||||
* unsuccessful decode due to excessive errors */
|
||||
|
||||
if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
|
||||
{
|
||||
mMessage = CRCP25.correctCCITT80( mMessage,
|
||||
TSBK_BEGIN, TSBK_CRC_START );
|
||||
|
||||
if( mMessage.getCRC() != CRC.FAILED_CRC )
|
||||
{
|
||||
BinaryMessage tsbkBuffer1 = mMessage.copy();
|
||||
tsbkBuffer1.setSize( TSBK_DECODED_END );
|
||||
|
||||
TSBKMessage tsbkMessage1 = TSBKMessageFactory.getMessage(
|
||||
tsbkBuffer1, DataUnitID.TSBK1, mAliasList );
|
||||
|
||||
if( tsbkMessage1.isLastBlock() )
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
setDUID( DataUnitID.TSBK2 );
|
||||
mMessage.setPointer( TSBK_BEGIN );
|
||||
}
|
||||
|
||||
dispatch( tsbkMessage1 );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
|
||||
/* Set sync match threshold to normal */
|
||||
mPrimarySyncDetector.setThreshold( SYNC_MATCH_THRESHOLD );
|
||||
break;
|
||||
case TSBK2:
|
||||
/* Remove interleaving */
|
||||
P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
|
||||
|
||||
/* Remove trellis encoding - abort processing if we have an
|
||||
* unsuccessful decode due to excessive errors */
|
||||
if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
|
||||
{
|
||||
mMessage = CRCP25.correctCCITT80( mMessage,
|
||||
TSBK_BEGIN, TSBK_CRC_START );
|
||||
|
||||
if( mMessage.getCRC() != CRC.FAILED_CRC )
|
||||
{
|
||||
BinaryMessage tsbkBuffer2 = mMessage.copy();
|
||||
tsbkBuffer2.setSize( TSBK_DECODED_END );
|
||||
|
||||
TSBKMessage tsbkMessage2 = TSBKMessageFactory.getMessage(
|
||||
tsbkBuffer2, DataUnitID.TSBK2, mAliasList );
|
||||
|
||||
if( tsbkMessage2.isLastBlock() )
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
setDUID( DataUnitID.TSBK3 );
|
||||
mMessage.setPointer( TSBK_BEGIN );
|
||||
}
|
||||
|
||||
dispatch( tsbkMessage2 );
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mComplete = true;
|
||||
}
|
||||
break;
|
||||
case TSBK3:
|
||||
/* Remove interleaving */
|
||||
P25Interleave.deinterleaveData( mMessage, TSBK_BEGIN, TSBK_END );
|
||||
|
||||
/* Remove trellis encoding - abort processing if we have an
|
||||
* unsuccessful decode due to excessive errors */
|
||||
if( mHalfRate.decode( mMessage, TSBK_BEGIN, TSBK_END ) )
|
||||
{
|
||||
mMessage = CRCP25.correctCCITT80( mMessage,
|
||||
TSBK_BEGIN, TSBK_CRC_START );
|
||||
|
||||
if( mMessage.getCRC() != CRC.FAILED_CRC )
|
||||
{
|
||||
BinaryMessage tsbkBuffer3 = mMessage.copy();
|
||||
tsbkBuffer3.setSize( TSBK_DECODED_END );
|
||||
|
||||
TSBKMessage tsbkMessage3 = TSBKMessageFactory.getMessage(
|
||||
tsbkBuffer3, DataUnitID.TSBK3, mAliasList );
|
||||
|
||||
dispatch( tsbkMessage3 );
|
||||
}
|
||||
}
|
||||
|
||||
mComplete = true;
|
||||
break;
|
||||
case VSELP1:
|
||||
mComplete = true;
|
||||
dispatch( new VSELP1Message( mMessage.copy(), mDUID, mAliasList ) );
|
||||
break;
|
||||
case VSELP2:
|
||||
mComplete = true;
|
||||
dispatch( new VSELP2Message( mMessage.copy(), mDUID, mAliasList ) );
|
||||
break;
|
||||
case UNKN:
|
||||
mComplete = true;
|
||||
dispatch( new P25Message( mMessage.copy(), mDUID, mAliasList ) );
|
||||
break;
|
||||
default:
|
||||
mComplete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mMessage = null;
|
||||
mHalfRate.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to indicate when this assembler has received all of the bits it
|
||||
* is looking for (ie message length), and should then be removed from
|
||||
* receiving any more bits
|
||||
*/
|
||||
public boolean complete()
|
||||
{
|
||||
return mComplete;
|
||||
}
|
||||
|
||||
public boolean isActive()
|
||||
{
|
||||
return mActive;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync pattern detector to listen for costas loop phase lock errors and
|
||||
* apply a phase correction to the costas loop so that we don't miss any
|
||||
* messages.
|
||||
*
|
||||
* When the costas loop locks with a +/- 90 degree or 180 degree phase
|
||||
* error, the slicer will incorrectly apply the symbol pattern rotated left
|
||||
* or right by the phase error. However, we can detect these rotated sync
|
||||
* patterns and apply immediate phase correction so that message processing
|
||||
* can continue.
|
||||
*/
|
||||
public class CostasPhaseErrorDetector extends SyncDetector
|
||||
{
|
||||
private LSMDemodulator mDemodulator;
|
||||
private double mCorrection;
|
||||
|
||||
public CostasPhaseErrorDetector( FrameSync frameSync, LSMDemodulator demodulator, double correction )
|
||||
{
|
||||
super( frameSync.getSync() );
|
||||
|
||||
mDemodulator = demodulator;
|
||||
|
||||
mCorrection = correction;
|
||||
|
||||
setListener( new ISyncDetectListener()
|
||||
{
|
||||
@Override
|
||||
public void syncDetected()
|
||||
{
|
||||
mDemodulator.correctPhaseError( mCorrection );
|
||||
|
||||
/* Since we detected a sync pattern, start a message assembler */
|
||||
for( P25MessageAssembler assembler: mAssemblers )
|
||||
{
|
||||
if( !assembler.isActive() )
|
||||
{
|
||||
assembler.setActive( true );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.IBandIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.message.IdentifierReceiver;
|
||||
import io.github.dsheirer.module.decode.p25.message.ldu.LDUMessage;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class P25MessageProcessor implements Listener<Message>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( P25MessageProcessor.class );
|
||||
|
||||
private Listener<Message> mMessageListener;
|
||||
|
||||
/* Map of up to 16 band identifiers per RFSS. These identifier update
|
||||
* messages are inserted into any message that conveys channel information
|
||||
* so that the uplink/downlink frequencies can be calculated */
|
||||
private HashMap<Integer,IBandIdentifier> mBandIdentifierMap =
|
||||
new HashMap<Integer,IBandIdentifier>();
|
||||
|
||||
private AliasList mAliasList;
|
||||
|
||||
public P25MessageProcessor( AliasList aliasList )
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( Message message )
|
||||
{
|
||||
/**
|
||||
* Capture frequency band identifier messages and inject them into any
|
||||
* messages that require band information in order to calculate the
|
||||
* up-link and down-link frequencies for any numeric channel references
|
||||
* contained within the message.
|
||||
*/
|
||||
if( message.isValid() )
|
||||
{
|
||||
/* Insert band identifier update messages into channel-type messages */
|
||||
if( message instanceof IdentifierReceiver )
|
||||
{
|
||||
IdentifierReceiver receiver = (IdentifierReceiver)message;
|
||||
|
||||
int[] identifiers = receiver.getIdentifiers();
|
||||
|
||||
for( int identifier: identifiers )
|
||||
{
|
||||
receiver.setIdentifierMessage( identifier,
|
||||
mBandIdentifierMap.get( identifier ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Store band identifiers so that they can be injected into channel
|
||||
* type messages */
|
||||
if( message instanceof IBandIdentifier )
|
||||
{
|
||||
IBandIdentifier bandIdentifier = (IBandIdentifier)message;
|
||||
|
||||
mBandIdentifierMap.put( bandIdentifier.getIdentifier(),
|
||||
bandIdentifier );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast all valid messages and any LDU voice messages regardless if
|
||||
* they are valid or not, so that we don't miss any voice frames
|
||||
*/
|
||||
if( mMessageListener != null && message.isValid() || message instanceof LDUMessage)
|
||||
{
|
||||
mMessageListener.receive( message );
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mBandIdentifierMap.clear();
|
||||
mMessageListener = null;
|
||||
}
|
||||
|
||||
public void setMessageListener( Listener<Message> listener )
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.instrument.tap.stream.DibitTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.FloatTap;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class P25_C4FMDecoder extends P25Decoder implements IFilteredRealBufferListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( P25_C4FMDecoder.class );
|
||||
|
||||
/* Instrumentation Taps */
|
||||
private static final String INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT = "Tap Point: Symbol Filter Output";
|
||||
private static final String INSTRUMENT_C4FM_SLICER_OUTPUT = "Tap Point: C4FM Slicer Output";
|
||||
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
private C4FMSymbolFilter mSymbolFilter;
|
||||
private C4FMSlicer mC4FMSlicer;
|
||||
private P25MessageFramer mMessageFramer;
|
||||
|
||||
/**
|
||||
* P25 Phase 1 C4FM Decoder processes real buffers of un-filtered,
|
||||
* demodulated audio and produces decoded P25 Phase 1 Messages.
|
||||
*
|
||||
* Provides Frequency Control to steer external tuner source and incorporates
|
||||
* internal gain control.
|
||||
*
|
||||
* Note: use the P25AudioModule to convert the decoded messages into audio.
|
||||
*
|
||||
* @param aliasList - optional (can be null) list of alias values for network
|
||||
* infrastructure and subscriber identities that will be included in each
|
||||
* decoded message
|
||||
*/
|
||||
public P25_C4FMDecoder(AliasList aliasList, int frequencyCorrectionMaximum )
|
||||
{
|
||||
super( aliasList );
|
||||
|
||||
/* Shape gain and frequency offsets to optimize sample stream */
|
||||
mSymbolFilter = new C4FMSymbolFilter( frequencyCorrectionMaximum );
|
||||
|
||||
/* Convert samples to symbols */
|
||||
mC4FMSlicer = new C4FMSlicer();
|
||||
mSymbolFilter.setListener( mC4FMSlicer );
|
||||
|
||||
/* Sync pattern detection and message construction */
|
||||
mMessageFramer = new P25MessageFramer( aliasList );
|
||||
mC4FMSlicer.addListener( mMessageFramer );
|
||||
|
||||
/* Process and broadcast messages */
|
||||
mMessageFramer.setListener( getMessageProcessor() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mSymbolFilter.dispose();
|
||||
mSymbolFilter = null;
|
||||
|
||||
mMessageFramer.dispose();
|
||||
mMessageFramer = null;
|
||||
|
||||
mC4FMSlicer.dispose();
|
||||
mC4FMSlicer = null;
|
||||
}
|
||||
|
||||
public Modulation getModulation()
|
||||
{
|
||||
return Modulation.C4FM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getFilteredRealBufferListener()
|
||||
{
|
||||
return mSymbolFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instrumentation taps for monitoring internal processing
|
||||
*/
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<>();
|
||||
|
||||
TapGroup group = new TapGroup( "P25 C4FM Decoder" );
|
||||
|
||||
group.add( new FloatTap( INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT, 0, 0.1f ) );
|
||||
group.add( new DibitTap( INSTRUMENT_C4FM_SLICER_OUTPUT, 0, 0.1f ) );
|
||||
|
||||
mAvailableTaps.add( group );
|
||||
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
mAvailableTaps.addAll( mSymbolFilter.getTapGroups() );
|
||||
}
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an instrumentation tap to monitor internal processing.
|
||||
*/
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
{
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
mSymbolFilter.registerTap( tap );
|
||||
}
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
|
||||
FloatTap symbolTap = (FloatTap)tap;
|
||||
mSymbolFilter.setListener( symbolTap );
|
||||
symbolTap.setListener( mC4FMSlicer );
|
||||
break;
|
||||
case INSTRUMENT_C4FM_SLICER_OUTPUT:
|
||||
DibitTap slicerTap = (DibitTap)tap;
|
||||
|
||||
if( mC4FMSlicer != null )
|
||||
{
|
||||
mC4FMSlicer.addListener( slicerTap );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the instrumentation tap.
|
||||
*/
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
{
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
mSymbolFilter.unregisterTap( tap );
|
||||
}
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_C4FM_SYMBOL_FILTER_OUTPUT:
|
||||
mSymbolFilter.setListener( mC4FMSlicer );
|
||||
break;
|
||||
case INSTRUMENT_C4FM_SLICER_OUTPUT:
|
||||
DibitTap slicerTap = (DibitTap)tap;
|
||||
|
||||
if( mC4FMSlicer != null )
|
||||
{
|
||||
mC4FMSlicer.removeListener( slicerTap );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException( "Unrecognized tap: " +
|
||||
tap.getName() );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyChangeListener( Listener<FrequencyChangeEvent> listener )
|
||||
{
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
mSymbolFilter.setFrequencyChangeListener( listener );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
mSymbolFilter.setFrequencyChangeListener( null );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
if( mSymbolFilter != null )
|
||||
{
|
||||
return mSymbolFilter.getFrequencyChangeListener();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.instrument.tap.Tap;
|
||||
import io.github.dsheirer.instrument.tap.TapGroup;
|
||||
import io.github.dsheirer.instrument.tap.stream.ComplexSampleTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.ComplexTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.DibitTap;
|
||||
import io.github.dsheirer.instrument.tap.stream.QPSKTap;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexBufferToStreamConverter;
|
||||
import io.github.dsheirer.sample.complex.IComplexBufferListener;
|
||||
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
|
||||
import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
|
||||
import io.github.dsheirer.dsp.psk.LSMDemodulator;
|
||||
import io.github.dsheirer.dsp.psk.QPSKPolarSlicer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class P25_LSMDecoder extends P25Decoder implements IComplexBufferListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( P25_LSMDecoder.class );
|
||||
|
||||
/* Instrumentation Taps */
|
||||
private static final String INSTRUMENT_BASEBAND_FILTER_OUTPUT = "Tap Point: Baseband Filter Output";
|
||||
private static final String INSTRUMENT_AGC_OUTPUT = "Tap Point: AGC Output";
|
||||
private static final String INSTRUMENT_LSM_DEMODULATOR_OUTPUT = "Tap Point: LSM Demodulator Output";
|
||||
private static final String INSTRUMENT_QPSK_SLICER_OUTPUT = "Tap Point: QPSK Slicer Output";
|
||||
|
||||
private List<TapGroup> mAvailableTaps;
|
||||
|
||||
private ComplexFIRFilter_CB_CB mBasebandFilter;
|
||||
private ComplexBufferToStreamConverter mStreamConverter = new ComplexBufferToStreamConverter();
|
||||
private ComplexFeedForwardGainControl mAGC =
|
||||
new ComplexFeedForwardGainControl( 32 );
|
||||
private LSMDemodulator mLSMDemodulator = new LSMDemodulator();
|
||||
private QPSKPolarSlicer mQPSKSlicer = new QPSKPolarSlicer();
|
||||
private P25MessageFramer mMessageFramer;
|
||||
|
||||
public P25_LSMDecoder( AliasList aliasList )
|
||||
{
|
||||
super( aliasList );
|
||||
|
||||
mBasebandFilter = new ComplexFIRFilter_CB_CB( FilterFactory.getLowPass(
|
||||
48000, 7250, 8000, 60, WindowType.HANNING, true ), 1.0f );
|
||||
|
||||
mBasebandFilter.setListener( mStreamConverter );
|
||||
|
||||
mStreamConverter.setListener( mAGC );
|
||||
|
||||
mAGC.setListener( mLSMDemodulator );
|
||||
|
||||
mLSMDemodulator.setSymbolListener( mQPSKSlicer );
|
||||
|
||||
mMessageFramer = new P25MessageFramer( aliasList, mLSMDemodulator );
|
||||
mQPSKSlicer.addListener( mMessageFramer );
|
||||
|
||||
mMessageFramer.setListener( getMessageProcessor() );
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
|
||||
mBasebandFilter.dispose();
|
||||
mBasebandFilter = null;
|
||||
|
||||
mStreamConverter.dispose();
|
||||
mStreamConverter = null;
|
||||
|
||||
mAGC.dispose();
|
||||
mAGC = null;
|
||||
|
||||
mLSMDemodulator.dispose();
|
||||
mLSMDemodulator = null;
|
||||
|
||||
mQPSKSlicer.dispose();
|
||||
mQPSKSlicer = null;
|
||||
|
||||
mMessageFramer.dispose();
|
||||
mMessageFramer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrequencyChangeListener(Listener<FrequencyChangeEvent> listener)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFrequencyChangeListener()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
|
||||
{
|
||||
return new Listener<FrequencyChangeEvent>()
|
||||
{
|
||||
@Override
|
||||
public void receive(FrequencyChangeEvent frequencyChangeEvent)
|
||||
{
|
||||
//Ignored
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<ComplexBuffer> getComplexBufferListener()
|
||||
{
|
||||
return mBasebandFilter;
|
||||
}
|
||||
|
||||
public Modulation getModulation()
|
||||
{
|
||||
return Modulation.CQPSK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a list of instrumentation taps for monitoring internal processing
|
||||
*/
|
||||
@Override
|
||||
public List<TapGroup> getTapGroups()
|
||||
{
|
||||
if( mAvailableTaps == null )
|
||||
{
|
||||
mAvailableTaps = new ArrayList<>();
|
||||
|
||||
TapGroup group = new TapGroup( "P25 LSM Decoder" );
|
||||
|
||||
group.add( new ComplexTap( INSTRUMENT_BASEBAND_FILTER_OUTPUT, 0, 1.0f ) );
|
||||
group.add( new ComplexTap( INSTRUMENT_AGC_OUTPUT, 0, 1.0f ) );
|
||||
group.add( new QPSKTap( INSTRUMENT_LSM_DEMODULATOR_OUTPUT, 0, 1.0f ) );
|
||||
group.add( new DibitTap( INSTRUMENT_QPSK_SLICER_OUTPUT, 0, 0.1f ) );
|
||||
|
||||
mAvailableTaps.add( group );
|
||||
|
||||
if( mLSMDemodulator != null )
|
||||
{
|
||||
mAvailableTaps.addAll( mLSMDemodulator.getTapGroups() );
|
||||
}
|
||||
}
|
||||
|
||||
return mAvailableTaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the instrumentation tap
|
||||
*/
|
||||
@Override
|
||||
public void registerTap( Tap tap )
|
||||
{
|
||||
if( mLSMDemodulator != null )
|
||||
{
|
||||
mLSMDemodulator.registerTap( tap );
|
||||
}
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
|
||||
ComplexSampleTap baseband = (ComplexSampleTap)tap;
|
||||
mStreamConverter.setListener( baseband );
|
||||
baseband.setListener( mAGC );
|
||||
break;
|
||||
case INSTRUMENT_AGC_OUTPUT:
|
||||
ComplexSampleTap agcSymbol = (ComplexSampleTap)tap;
|
||||
mAGC.setListener( agcSymbol );
|
||||
agcSymbol.setListener( mLSMDemodulator );
|
||||
break;
|
||||
case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
|
||||
QPSKTap qpsk = (QPSKTap)tap;
|
||||
mLSMDemodulator.setSymbolListener( qpsk );
|
||||
qpsk.setListener( mQPSKSlicer );
|
||||
break;
|
||||
case INSTRUMENT_QPSK_SLICER_OUTPUT:
|
||||
mQPSKSlicer.addListener( (DibitTap)tap );
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException( "Unrecognized tap: " +
|
||||
tap.getName() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the instrumentation tap
|
||||
*/
|
||||
@Override
|
||||
public void unregisterTap( Tap tap )
|
||||
{
|
||||
if( mLSMDemodulator != null )
|
||||
{
|
||||
mLSMDemodulator.unregisterTap( tap );
|
||||
}
|
||||
|
||||
switch( tap.getName() )
|
||||
{
|
||||
case INSTRUMENT_BASEBAND_FILTER_OUTPUT:
|
||||
mStreamConverter.setListener( mAGC );
|
||||
break;
|
||||
case INSTRUMENT_AGC_OUTPUT:
|
||||
mAGC.setListener( mLSMDemodulator );
|
||||
break;
|
||||
case INSTRUMENT_LSM_DEMODULATOR_OUTPUT:
|
||||
mLSMDemodulator.setSymbolListener( mQPSKSlicer );
|
||||
break;
|
||||
case INSTRUMENT_QPSK_SLICER_OUTPUT:
|
||||
mQPSKSlicer.removeListener( (DibitTap)tap );
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException( "Unrecognized tap: " +
|
||||
tap.getName() );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start( ScheduledExecutorService executor )
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PatchGroup
|
||||
{
|
||||
private long mExpireThreshold = 5000; //5 seconds
|
||||
private long mLastUpdated;
|
||||
private String mPatchGroupID;
|
||||
private List<String> mPatchedGroups = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs a patch group with the specified patch group ID
|
||||
* @param patchGroupID that identifies this patch group
|
||||
*/
|
||||
public PatchGroup(String patchGroupID)
|
||||
{
|
||||
mPatchGroupID = patchGroupID;
|
||||
mLastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch group ID for this patch group
|
||||
*/
|
||||
public String getPatchGroupID()
|
||||
{
|
||||
return mPatchGroupID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the patched group to this patch group. Duplicate patched groups are ignored.
|
||||
*
|
||||
* Note: the patch group ID will not be added as a patched group, to avoid circular dependencies.
|
||||
*
|
||||
* @return true if the patched group was added to this patch group
|
||||
*/
|
||||
public boolean addPatchedGroup(String patchedGroup)
|
||||
{
|
||||
if(patchedGroup != null && !patchedGroup.equals(mPatchGroupID) && !mPatchedGroups.contains(patchedGroup))
|
||||
{
|
||||
mPatchedGroups.add(patchedGroup);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patched talkgroups that are part of this patch group.
|
||||
*/
|
||||
public List<String> getPatchedGroups()
|
||||
{
|
||||
return mPatchedGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the timestamp for this patch group.
|
||||
*/
|
||||
public void updateTimestamp()
|
||||
{
|
||||
mLastUpdated = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this patch group is expired, meaning that it hasn't been updated in the past (expire
|
||||
* threshold) seconds.
|
||||
*/
|
||||
public boolean isExpired()
|
||||
{
|
||||
return System.currentTimeMillis() - mLastUpdated > mExpireThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the patch group expiration threshold (default = 5000 ms). A patch group is considered expired if
|
||||
* no new updates have been received within the past (expire threshold) milliseconds
|
||||
*
|
||||
* @param expireThreshold in milliseconds
|
||||
*/
|
||||
public void setExpireThreshold(long expireThreshold)
|
||||
{
|
||||
mExpireThreshold = expireThreshold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.module.decode.event.CallEvent;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.PatchGroupAlias;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupID;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class PatchGroupManager
|
||||
{
|
||||
private AliasList mAliasList;
|
||||
private List<PatchGroup> mPatchGroups = new ArrayList<>();
|
||||
private Broadcaster<CallEvent> mCallEventBroadcaster;
|
||||
|
||||
/**
|
||||
* Manages active P25 patch groups and auto-expires patch groups if not updated every 5 seconds.
|
||||
*
|
||||
* @param aliasList to keep updated with patch group changes
|
||||
* @param broadcaster for rebroadcasting patch group related call events
|
||||
*/
|
||||
public PatchGroupManager(AliasList aliasList, Broadcaster<CallEvent> broadcaster)
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
mCallEventBroadcaster = broadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the patch group.
|
||||
*
|
||||
* @param patchGroupID of the patch group to remove
|
||||
*/
|
||||
public void removePatchGroup(String patchGroupID)
|
||||
{
|
||||
removePatchGroup(patchGroupID, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the patch group.
|
||||
*
|
||||
* @param patchGroupID of the patch group to remove
|
||||
* @param expired true if the patch group is being removed because it expired, false if we receive a
|
||||
* message commanding commanding a patch group delete.
|
||||
*/
|
||||
private void removePatchGroup(String patchGroupID, boolean expired)
|
||||
{
|
||||
PatchGroup patchGroup = getPatchGroup(patchGroupID);
|
||||
|
||||
if(patchGroup != null)
|
||||
{
|
||||
//Cleanup the alias list
|
||||
if(mAliasList != null)
|
||||
{
|
||||
Alias alias = mAliasList.getTalkgroupAlias(patchGroupID);
|
||||
|
||||
if(alias instanceof PatchGroupAlias)
|
||||
{
|
||||
PatchGroupAlias patchGroupAlias = (PatchGroupAlias)alias;
|
||||
|
||||
mAliasList.removeAlias(patchGroupAlias);
|
||||
|
||||
//Replace our temporary patch group alias with the original alias for the patch group ID
|
||||
if(patchGroupAlias.hasPatchGroupAlias())
|
||||
{
|
||||
mAliasList.addAlias(patchGroupAlias.getPatchGroupAlias());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mPatchGroups.remove(patchGroup);
|
||||
|
||||
if(mCallEventBroadcaster != null)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append((expired ? "EXPIRED " : "DELETED "));
|
||||
sb.append("PATCH GROUP:").append(patchGroup.getPatchGroupID());
|
||||
sb.append(" ").append(patchGroup.getPatchedGroups());
|
||||
|
||||
|
||||
mCallEventBroadcaster.broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.PATCH_GROUP_DELETE)
|
||||
.to(patchGroup.getPatchGroupID())
|
||||
.details(sb.toString())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds/updates a patch group alias to the alias list containing aliases for each of the patched talkgroups. If the
|
||||
* patch group alias already exists, any patched talkgroups will be removed from the existing patch group alias and
|
||||
* replaced with the aliases corresponding to the patched talkgroup aliases.
|
||||
*
|
||||
* @param patchGroupID for the patch group
|
||||
* @param patchedTalkgroups containing the talkgroup IDs for each of the patched talkgroups
|
||||
*/
|
||||
public void updatePatchGroup(String patchGroupID, List<String> patchedTalkgroups)
|
||||
{
|
||||
boolean updated = false;
|
||||
|
||||
PatchGroup patchGroup = getPatchGroup(patchGroupID);
|
||||
|
||||
if(patchGroup == null)
|
||||
{
|
||||
//Remove any expired patch groups before we add a new one
|
||||
cleanupPatchGroups();
|
||||
patchGroup = new PatchGroup(patchGroupID);
|
||||
mPatchGroups.add(patchGroup);
|
||||
updated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
patchGroup.updateTimestamp();
|
||||
}
|
||||
|
||||
for(String patchedGroup : patchedTalkgroups)
|
||||
{
|
||||
if(patchGroup.addPatchedGroup(patchedGroup))
|
||||
{
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
//If we make any changes to the patch group, update the patch group alias
|
||||
if(updated)
|
||||
{
|
||||
PatchGroupAlias patchGroupAlias = null;
|
||||
|
||||
//Check for an existing alias for the patch talkgroup - do not include wildcard aliases
|
||||
if(mAliasList != null)
|
||||
{
|
||||
Alias existingAlias = mAliasList.getTalkgroupAlias(patchGroupID, false);
|
||||
|
||||
if(existingAlias instanceof PatchGroupAlias)
|
||||
{
|
||||
patchGroupAlias = (PatchGroupAlias)existingAlias;
|
||||
}
|
||||
else
|
||||
{
|
||||
patchGroupAlias = new PatchGroupAlias();
|
||||
|
||||
patchGroupAlias.addAliasID(new TalkgroupID(patchGroupID));
|
||||
|
||||
if(existingAlias != null)
|
||||
{
|
||||
mAliasList.removeAlias(existingAlias);
|
||||
|
||||
patchGroupAlias.setPatchGroupAlias(existingAlias);
|
||||
}
|
||||
|
||||
mAliasList.addAlias(patchGroupAlias);
|
||||
}
|
||||
|
||||
if(patchGroupAlias != null)
|
||||
{
|
||||
patchGroupAlias.setPatchedTalkgroupIDs(patchGroup.getPatchedGroups());
|
||||
|
||||
patchGroupAlias.clearPatchedAliases();
|
||||
|
||||
for(String patchedTalkgroup : patchGroup.getPatchedGroups())
|
||||
{
|
||||
Alias patchedAlias = mAliasList.getTalkgroupAlias(patchedTalkgroup);
|
||||
|
||||
if(patchedAlias != null)
|
||||
{
|
||||
patchGroupAlias.addPatchedAlias(patchedAlias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mCallEventBroadcaster != null)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("PATCH GROUP:").append(patchGroup.getPatchGroupID());
|
||||
sb.append(" ").append(patchGroup.getPatchedGroups());
|
||||
|
||||
mCallEventBroadcaster.broadcast(new P25CallEvent.Builder(CallEvent.CallEventType.PATCH_GROUP_ADD)
|
||||
.aliasList(mAliasList)
|
||||
.details(sb.toString())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the patch group matching the patch group ID or null
|
||||
*/
|
||||
private PatchGroup getPatchGroup(String patchGroupID)
|
||||
{
|
||||
for(PatchGroup patchGroup : mPatchGroups)
|
||||
{
|
||||
if(patchGroup.getPatchGroupID().equals(patchGroupID))
|
||||
{
|
||||
return patchGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the last updated timestamp on all patch groups and removes any patch groups that have not been
|
||||
* updated in the past 5 seconds
|
||||
*/
|
||||
public void cleanupPatchGroups()
|
||||
{
|
||||
Iterator<PatchGroup> it = mPatchGroups.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
PatchGroup patchGroup = it.next();
|
||||
|
||||
if(patchGroup.isExpired())
|
||||
{
|
||||
it.remove();
|
||||
|
||||
removePatchGroup(patchGroup.getPatchGroupID(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Random;
|
||||
|
||||
public class Trellis_1_2_Rate
|
||||
{
|
||||
public final static int MAX_ERROR_THRESHOLD = 7;
|
||||
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( Trellis_1_2_Rate.class );
|
||||
|
||||
private ArrayList<ConstellationNode> mConstellationNodes =
|
||||
new ArrayList<ConstellationNode>();
|
||||
|
||||
private static final int[][] CONSTELLATION_COSTS =
|
||||
{ { 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 },
|
||||
{ 1,0,2,1,2,1,3,2,2,1,3,2,3,2,4,3 },
|
||||
{ 1,2,0,1,2,3,1,2,2,3,1,2,3,4,2,3 },
|
||||
{ 2,1,1,0,3,2,2,1,3,2,2,1,4,3,3,2 },
|
||||
{ 1,2,2,3,0,1,1,2,2,3,3,4,1,2,2,3 },
|
||||
{ 2,1,3,2,1,0,2,1,3,2,4,3,2,1,3,2 },
|
||||
{ 2,3,1,2,1,2,0,1,3,4,2,3,2,3,1,2 },
|
||||
{ 3,2,2,1,2,1,1,0,4,3,3,2,3,2,2,1 },
|
||||
{ 1,2,2,3,2,3,3,4,0,1,1,2,1,2,2,3 },
|
||||
{ 2,1,3,2,3,2,4,3,1,0,2,1,2,1,3,2 },
|
||||
{ 2,3,1,2,3,4,2,3,1,2,0,1,2,3,1,2 },
|
||||
{ 3,2,2,1,4,3,3,2,2,1,1,0,3,2,2,1 },
|
||||
{ 2,3,3,4,1,2,2,3,1,2,2,3,0,1,1,2 },
|
||||
{ 3,2,4,3,2,1,3,2,2,1,3,2,1,0,2,1 },
|
||||
{ 3,4,2,3,2,3,1,2,2,3,1,2,1,2,0,1 },
|
||||
{ 4,3,3,2,3,2,2,1,3,2,2,1,2,1,1,0 } };
|
||||
|
||||
public Trellis_1_2_Rate()
|
||||
{
|
||||
/* Create 49 x 4-bit constellation nodes to handle 196-bit blocks */
|
||||
ConstellationNode previous = null;
|
||||
|
||||
for( int x = 0; x < 49; x++ )
|
||||
{
|
||||
ConstellationNode node = new ConstellationNode();
|
||||
|
||||
if( previous != null )
|
||||
{
|
||||
previous.connect( node );
|
||||
}
|
||||
|
||||
previous = node;
|
||||
|
||||
mConstellationNodes.add( node );
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
for( ConstellationNode node: mConstellationNodes )
|
||||
{
|
||||
node.dispose();
|
||||
}
|
||||
|
||||
mConstellationNodes.clear();
|
||||
}
|
||||
|
||||
// private String constellationsToString( String label )
|
||||
// {
|
||||
// StringBuilder sb = new StringBuilder();
|
||||
//
|
||||
// sb.append( label );
|
||||
//
|
||||
// for( ConstellationNode node: mConstellationNodes )
|
||||
// {
|
||||
// sb.append( " " + node.getConstellation().name() );
|
||||
// }
|
||||
//
|
||||
// return sb.toString();
|
||||
// }
|
||||
//
|
||||
// private String inputsToString()
|
||||
// {
|
||||
// StringBuilder sb = new StringBuilder();
|
||||
//
|
||||
// sb.append( " INPUTS: " );
|
||||
//
|
||||
// for( ConstellationNode node: mConstellationNodes )
|
||||
// {
|
||||
// sb.append( " " );
|
||||
// sb.append( ( node.getConstellation().getInput().getBit1() ? "1" : "0" ) );
|
||||
// sb.append( ( node.getConstellation().getInput().getBit2() ? "1" : "0" ) );
|
||||
// }
|
||||
//
|
||||
// return sb.toString();
|
||||
// }
|
||||
|
||||
public boolean decode( BinaryMessage message, int start, int end )
|
||||
{
|
||||
/* load each of the nodes with de-interleaved constellations */
|
||||
for( int index = 0; index < 49; index++ )
|
||||
{
|
||||
Constellation c = getConstellation( message, start + index * 4 );
|
||||
|
||||
mConstellationNodes.get( index ).setConstellation( c );
|
||||
}
|
||||
|
||||
/* test to see if constellations are correct - otherwise correct them */
|
||||
ConstellationNode firstNode = mConstellationNodes.get( 0 );
|
||||
|
||||
int errorCount = firstNode.getErrorCount();
|
||||
|
||||
if( errorCount > 0 )
|
||||
{
|
||||
if( errorCount < MAX_ERROR_THRESHOLD )
|
||||
{
|
||||
/* repair the errors */
|
||||
firstNode.correctTo( Dibit.D00_PLUS_1 );
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* clear constellations from original message */
|
||||
message.clear( start, end );
|
||||
|
||||
/* replace with decoded values from the constellation nodes */
|
||||
for( int index = 0; index < 49; index++ )
|
||||
{
|
||||
ConstellationNode node = mConstellationNodes.get( index );
|
||||
|
||||
Dibit input = node.getInputDibit();
|
||||
|
||||
if( input.getBit1() )
|
||||
{
|
||||
message.set( start + ( index * 2 ) );
|
||||
}
|
||||
if( input.getBit2() )
|
||||
{
|
||||
message.set( start + ( index * 2 ) + 1 );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Constellation getConstellation( BinaryMessage message, int index )
|
||||
{
|
||||
int transmittedValue = 0;
|
||||
|
||||
for( int x = 0; x < 4; x++ )
|
||||
{
|
||||
if( message.get( index + x ) )
|
||||
{
|
||||
transmittedValue += ( 1 << ( 3 - x ) );
|
||||
}
|
||||
}
|
||||
|
||||
return Constellation.fromTransmittedValue( transmittedValue );
|
||||
}
|
||||
|
||||
public class ConstellationNode
|
||||
{
|
||||
private ConstellationNode mConnectedNode;
|
||||
private Constellation mConstellation;
|
||||
private boolean mCorrect;
|
||||
|
||||
public ConstellationNode()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mConnectedNode = null;
|
||||
}
|
||||
|
||||
public Constellation getConstellation()
|
||||
{
|
||||
return mConstellation;
|
||||
}
|
||||
|
||||
public Dibit getInputDibit()
|
||||
{
|
||||
return mConstellation.getInput();
|
||||
}
|
||||
|
||||
public boolean hasStateDibit( Dibit dibit )
|
||||
{
|
||||
return mConstellation.getState() == dibit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a correction down the line of connected nodes. Only nodes
|
||||
* with the mCorrect flag set to false will be corrected.
|
||||
*
|
||||
* Note: Assumes that the starting node's value is 0. Initiate the
|
||||
* corrective sequence by invoking this method with Dibit.D0 on the
|
||||
* first node.
|
||||
*
|
||||
* @param stateDibit to use for the left side.
|
||||
*/
|
||||
public void correctTo( Dibit stateDibit )
|
||||
{
|
||||
if( mCorrect && mConstellation.getState() == stateDibit )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( isCurrentConnectionCorrect() )
|
||||
{
|
||||
mConstellation = Constellation.fromStateAndInputDibits( stateDibit,
|
||||
mConstellation.getInput() );
|
||||
|
||||
mCorrect = true;
|
||||
|
||||
if( mConnectedNode != null )
|
||||
{
|
||||
/* the next node's state was this node's input */
|
||||
mConnectedNode.correctTo( mConstellation.getInput() );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Constellation cheapestConstellation = mConstellation;
|
||||
|
||||
int cheapestCost = 100; //arbitrary
|
||||
|
||||
for( Dibit testInput: Dibit.values() )
|
||||
{
|
||||
Constellation testConstellation = Constellation
|
||||
.fromStateAndInputDibits( stateDibit, testInput );
|
||||
|
||||
int testCost = mConstellation.costTo( testConstellation ) +
|
||||
mConnectedNode.costTo( testInput );
|
||||
|
||||
if( testCost < cheapestCost )
|
||||
{
|
||||
cheapestCost = testCost;
|
||||
cheapestConstellation = testConstellation;
|
||||
}
|
||||
}
|
||||
|
||||
mConstellation = cheapestConstellation;
|
||||
|
||||
mConnectedNode.correctTo( mConstellation.getInput() );
|
||||
|
||||
mCorrect = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the cost (hamming distance) of using the argument as the
|
||||
* state dibit for the current node, and recursively finding the
|
||||
* cheapest corresponding input dibit.
|
||||
*
|
||||
* @param stateTest
|
||||
* @return
|
||||
*/
|
||||
public int costTo( Dibit stateTest )
|
||||
{
|
||||
if( isCurrentConnectionCorrect() )
|
||||
{
|
||||
Constellation c = Constellation.fromStateAndInputDibits(
|
||||
stateTest, mConstellation.getInput() );
|
||||
|
||||
return mConstellation.costTo( c );
|
||||
}
|
||||
else
|
||||
{
|
||||
int cheapestCost = 100; //arbitrary
|
||||
|
||||
for( Dibit inputTest: Dibit.values() )
|
||||
{
|
||||
Constellation constellationTest =
|
||||
Constellation.fromStateAndInputDibits( stateTest, inputTest );
|
||||
|
||||
int cost = mConnectedNode.costTo( inputTest ) +
|
||||
mConstellation.costTo( constellationTest );
|
||||
|
||||
if( cost < cheapestCost )
|
||||
{
|
||||
cheapestCost = cost;
|
||||
}
|
||||
}
|
||||
|
||||
return cheapestCost;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the immediate connection to the right is correct
|
||||
*/
|
||||
public boolean isCurrentConnectionCorrect()
|
||||
{
|
||||
return ( mConnectedNode == null ||
|
||||
mConstellation.getInput() == mConnectedNode.getStateDibit() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a recursive call to all nodes to the right, setting the
|
||||
* mCorrect flag on all nodes to true, if the node's connection to the
|
||||
* right node is correct and all nodes to the right are correct. Or,
|
||||
* false if this node's connection, or any node's connection to the
|
||||
* right is incorrect.
|
||||
*
|
||||
* @return - true - all node connections to the right are correct
|
||||
* false - one or more nodes to the right are incorrect
|
||||
*/
|
||||
public int getErrorCount()
|
||||
{
|
||||
if( mConnectedNode == null )
|
||||
{
|
||||
mCorrect = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
mCorrect = mConstellation.getInput() == mConnectedNode.getStateDibit();
|
||||
|
||||
return mConnectedNode.getErrorCount() + ( mCorrect ? 0 : 1 );
|
||||
}
|
||||
|
||||
public Dibit getStateDibit()
|
||||
{
|
||||
return mConstellation.getState();
|
||||
}
|
||||
|
||||
public void setConstellation( Constellation constellation )
|
||||
{
|
||||
mConstellation = constellation;
|
||||
mCorrect = false;
|
||||
}
|
||||
|
||||
public void connect( ConstellationNode node )
|
||||
{
|
||||
mConnectedNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Constellation
|
||||
{
|
||||
/* State (previous), Input (next), Transmit 1, Transmit 2 */
|
||||
CB( Dibit.D01_PLUS_3, Dibit.D01_PLUS_3, Dibit.D00_PLUS_1, Dibit.D00_PLUS_1, 0 ),
|
||||
CC( Dibit.D00_PLUS_1, Dibit.D10_MINUS_1, Dibit.D00_PLUS_1, Dibit.D01_PLUS_3, 1 ),
|
||||
C0( Dibit.D00_PLUS_1, Dibit.D00_PLUS_1, Dibit.D00_PLUS_1, Dibit.D10_MINUS_1, 2 ),
|
||||
C7( Dibit.D01_PLUS_3, Dibit.D11_MINUS_3, Dibit.D00_PLUS_1, Dibit.D11_MINUS_3, 3 ),
|
||||
CE( Dibit.D10_MINUS_1, Dibit.D11_MINUS_3, Dibit.D01_PLUS_3, Dibit.D00_PLUS_1, 4 ),
|
||||
C9( Dibit.D11_MINUS_3, Dibit.D00_PLUS_1, Dibit.D01_PLUS_3, Dibit.D01_PLUS_3, 5 ),
|
||||
C5( Dibit.D11_MINUS_3, Dibit.D10_MINUS_1, Dibit.D01_PLUS_3, Dibit.D10_MINUS_1, 6 ),
|
||||
C2( Dibit.D10_MINUS_1, Dibit.D01_PLUS_3, Dibit.D01_PLUS_3, Dibit.D11_MINUS_3, 7 ),
|
||||
CA( Dibit.D11_MINUS_3, Dibit.D11_MINUS_3, Dibit.D10_MINUS_1, Dibit.D00_PLUS_1, 8 ),
|
||||
CD( Dibit.D10_MINUS_1, Dibit.D00_PLUS_1, Dibit.D10_MINUS_1, Dibit.D01_PLUS_3, 9 ),
|
||||
C1( Dibit.D10_MINUS_1, Dibit.D10_MINUS_1, Dibit.D10_MINUS_1, Dibit.D10_MINUS_1, 10 ),
|
||||
C6( Dibit.D11_MINUS_3, Dibit.D01_PLUS_3, Dibit.D10_MINUS_1, Dibit.D11_MINUS_3, 11 ),
|
||||
CF( Dibit.D00_PLUS_1, Dibit.D01_PLUS_3, Dibit.D11_MINUS_3, Dibit.D00_PLUS_1, 12 ),
|
||||
C8( Dibit.D01_PLUS_3, Dibit.D10_MINUS_1, Dibit.D11_MINUS_3, Dibit.D01_PLUS_3, 13 ),
|
||||
C4( Dibit.D01_PLUS_3, Dibit.D00_PLUS_1, Dibit.D11_MINUS_3, Dibit.D10_MINUS_1, 14 ),
|
||||
C3( Dibit.D00_PLUS_1, Dibit.D11_MINUS_3, Dibit.D11_MINUS_3, Dibit.D11_MINUS_3, 15 );
|
||||
|
||||
private Dibit mStateDibit;
|
||||
private Dibit mInputDibit;
|
||||
private Dibit mTransmitDibit1;
|
||||
private Dibit mTransmitDibit2;
|
||||
private int mTransmittedValue;
|
||||
|
||||
private Constellation( Dibit stateDibit,
|
||||
Dibit inputDibit,
|
||||
Dibit transmitDibit1,
|
||||
Dibit transmitDibit2,
|
||||
int transmittedValue )
|
||||
{
|
||||
mStateDibit = stateDibit;
|
||||
mInputDibit = inputDibit;
|
||||
mTransmitDibit1 = transmitDibit1;
|
||||
mTransmitDibit2 = transmitDibit2;
|
||||
mTransmittedValue = transmittedValue;
|
||||
}
|
||||
|
||||
public Dibit getState()
|
||||
{
|
||||
return mStateDibit;
|
||||
}
|
||||
|
||||
public Dibit getInput()
|
||||
{
|
||||
return mInputDibit;
|
||||
}
|
||||
|
||||
public Dibit getTransmitDibit1()
|
||||
{
|
||||
return mTransmitDibit1;
|
||||
}
|
||||
|
||||
public Dibit getTransmitDibit2()
|
||||
{
|
||||
return mTransmitDibit2;
|
||||
}
|
||||
|
||||
public int getTransmittedValue()
|
||||
{
|
||||
return mTransmittedValue;
|
||||
}
|
||||
|
||||
public static Constellation fromTransmittedValue( int value )
|
||||
{
|
||||
if( 0 <= value && value <= 15 )
|
||||
{
|
||||
return values()[ value ];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Constellation fromTransmittedDibits( Dibit left, Dibit right )
|
||||
{
|
||||
return fromTransmittedValue( left.getHighValue() + right.getLowValue() );
|
||||
}
|
||||
|
||||
public static Constellation fromStateAndInputDibits( Dibit state, Dibit input )
|
||||
{
|
||||
switch( state )
|
||||
{
|
||||
case D00_PLUS_1:
|
||||
switch( input )
|
||||
{
|
||||
case D00_PLUS_1:
|
||||
return C0;
|
||||
case D01_PLUS_3:
|
||||
return CF;
|
||||
case D10_MINUS_1:
|
||||
return CC;
|
||||
case D11_MINUS_3:
|
||||
return C3;
|
||||
}
|
||||
break;
|
||||
case D01_PLUS_3:
|
||||
switch( input )
|
||||
{
|
||||
case D00_PLUS_1:
|
||||
return C4;
|
||||
case D01_PLUS_3:
|
||||
return CB;
|
||||
case D10_MINUS_1:
|
||||
return C8;
|
||||
case D11_MINUS_3:
|
||||
return C7;
|
||||
}
|
||||
break;
|
||||
case D10_MINUS_1:
|
||||
switch( input )
|
||||
{
|
||||
case D00_PLUS_1:
|
||||
return CD;
|
||||
case D01_PLUS_3:
|
||||
return C2;
|
||||
case D10_MINUS_1:
|
||||
return C1;
|
||||
case D11_MINUS_3:
|
||||
return CE;
|
||||
}
|
||||
break;
|
||||
case D11_MINUS_3:
|
||||
switch( input )
|
||||
{
|
||||
case D00_PLUS_1:
|
||||
return C9;
|
||||
case D01_PLUS_3:
|
||||
return C6;
|
||||
case D10_MINUS_1:
|
||||
return C5;
|
||||
case D11_MINUS_3:
|
||||
return CA;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* Should never get to here */
|
||||
return C0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cost or hamming distance to the other constellation using
|
||||
* the values from the constellation costs table
|
||||
*/
|
||||
public int costTo( Constellation other )
|
||||
{
|
||||
return CONSTELLATION_COSTS[ getTransmittedValue() ][ other.getTransmittedValue() ];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test harness
|
||||
*/
|
||||
public static void main( String[] args )
|
||||
{
|
||||
Random random = new Random();
|
||||
|
||||
BinaryMessage buffer = new BinaryMessage( 196 );
|
||||
|
||||
try
|
||||
{
|
||||
for( int x = 0; x < 196; x++ )
|
||||
{
|
||||
int number = random.nextInt( 2 );
|
||||
|
||||
if( number == 1 )
|
||||
{
|
||||
buffer.add( false );
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.add( true );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
mLog.debug( " BUFFER: " + buffer.toString() );
|
||||
|
||||
Trellis_1_2_Rate t = new Trellis_1_2_Rate();
|
||||
|
||||
t.decode( buffer, 0, 196 );
|
||||
mLog.debug( " DECODED: " + buffer.toString() );
|
||||
mLog.debug( "Finished!" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class Trellis_3_4_Rate
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( Trellis_3_4_Rate.class );
|
||||
|
||||
/* Hamming distance (bit match count) between constellation pairs */
|
||||
private static final int[][] CONSTELLATION_METRICS =
|
||||
{ { 4,3,3,2,3,2,2,1,3,2,2,1,2,1,1,0 },
|
||||
{ 3,4,2,3,2,3,1,2,2,3,1,2,1,2,0,1 },
|
||||
{ 3,2,4,3,2,1,3,2,2,1,3,2,1,0,2,1 },
|
||||
{ 2,3,3,4,1,2,2,3,1,2,2,3,0,1,1,2 },
|
||||
{ 3,2,2,1,4,3,3,2,2,1,1,0,3,2,2,1 },
|
||||
{ 2,3,1,2,3,4,2,3,1,2,0,1,2,3,1,2 },
|
||||
{ 2,1,3,2,3,2,4,3,1,0,2,1,2,1,3,2 },
|
||||
{ 1,2,2,3,2,3,3,4,0,1,1,2,1,2,2,3 },
|
||||
{ 3,2,2,1,2,1,1,0,4,3,3,2,3,2,2,1 },
|
||||
{ 2,3,1,2,1,2,0,1,3,4,2,3,2,3,1,2 },
|
||||
{ 2,1,3,2,1,0,2,1,3,2,4,3,2,1,3,2 },
|
||||
{ 1,2,2,3,0,1,1,2,2,3,3,4,1,2,2,3 },
|
||||
{ 2,1,1,0,3,2,2,1,3,2,2,1,4,3,3,2 },
|
||||
{ 1,2,0,1,2,3,1,2,2,3,1,2,3,4,2,3 },
|
||||
{ 1,0,2,1,2,1,3,2,2,1,3,2,3,2,4,3 },
|
||||
{ 0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4 } };
|
||||
|
||||
/* Even valued state tribits can only produce even valued constellations
|
||||
* and odd valued state tribits can only produce odd constellations. */
|
||||
public static Con[] EVEN_CONSTELLATIONS = new Con[] { Con.C0,Con.C2,Con.C4,
|
||||
Con.C6,Con.C8,Con.CA,Con.CC,Con.CE };
|
||||
public static Con[] ODD_CONSTELLATIONS = new Con[] { Con.C1,Con.C3,Con.C5,
|
||||
Con.C7,Con.C9,Con.CB,Con.CD,Con.CF };
|
||||
|
||||
/* Constellation and state tribit lookup map to find the input tribit */
|
||||
public static HashMap<Con,Tribit[]> INPUT_FROM_CONSTELLATION_MAP;
|
||||
|
||||
private ArrayList<Con> mTransmittedConstellations = new ArrayList<Con>();
|
||||
private ArrayList<Path> mSurvivorPaths = new ArrayList<Path>();
|
||||
private ArrayList<Path> mNewPaths = new ArrayList<Path>();
|
||||
private PathMetrics mPathMetrics = new PathMetrics();
|
||||
|
||||
/**
|
||||
* Implements the Viterbi algorithm to decode 3/4 rate trellis encoded 196-bit
|
||||
* packet data messages.
|
||||
*/
|
||||
public Trellis_3_4_Rate()
|
||||
{
|
||||
createConstellationToTribitMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lookup map for state to input tribit values for a given
|
||||
* constellation. Input tribits are contained in the array using the state
|
||||
* tribit's value as the lookup index. Null values indicate illegal state
|
||||
* and input combinations for the specified constellation.
|
||||
*/
|
||||
private void createConstellationToTribitMap()
|
||||
{
|
||||
INPUT_FROM_CONSTELLATION_MAP = new HashMap<Con,Tribit[]>();
|
||||
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.CB, new Tribit[] { null, null, Tribit.T5, Tribit.T3, Tribit.T1, Tribit.T7, null, null } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.CC, new Tribit[] { Tribit.T3, Tribit.T1, null, null, null, null, Tribit.T7, Tribit.T5 } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C0, new Tribit[] { Tribit.T0, Tribit.T6, null, null, null, null, Tribit.T4, Tribit.T2 } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C7, new Tribit[] { null, null, Tribit.T6, Tribit.T4, Tribit.T2, Tribit.T0, null, null } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.CE, new Tribit[] { Tribit.T7, Tribit.T5, null, null, null, null, Tribit.T3, Tribit.T1 } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C9, new Tribit[] { null, null, Tribit.T1, Tribit.T7, Tribit.T5, Tribit.T3, null, null } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C5, new Tribit[] { null, null, Tribit.T2, Tribit.T0, Tribit.T6, Tribit.T4, null, null } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C2, new Tribit[] { Tribit.T4, Tribit.T2, null, null, null, null, Tribit.T0, Tribit.T6 } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.CA, new Tribit[] { Tribit.T5, Tribit.T3, null, null, null, null, Tribit.T1, Tribit.T7 } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.CD, new Tribit[] { null, null, Tribit.T3, Tribit.T1, Tribit.T7, Tribit.T5, null, null } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C1, new Tribit[] { null, null, Tribit.T0, Tribit.T6, Tribit.T4, Tribit.T2, null, null } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C6, new Tribit[] { Tribit.T6, Tribit.T4, null, null, null, null, Tribit.T2, Tribit.T0 } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.CF, new Tribit[] { null, null, Tribit.T7, Tribit.T5, Tribit.T3, Tribit.T1, null, null } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C8, new Tribit[] { Tribit.T1, Tribit.T7, null, null, null, null, Tribit.T5, Tribit.T3 } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C4, new Tribit[] { Tribit.T2, Tribit.T0, null, null, null, null, Tribit.T6, Tribit.T4 } );
|
||||
INPUT_FROM_CONSTELLATION_MAP.put( Con.C3, new Tribit[] { null, null, Tribit.T4, Tribit.T2, Tribit.T0, Tribit.T6, null, null } );;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member object cleanup prior to deleting
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a 196-bit 3/4 rate convolutional encoded message located between
|
||||
* start and end indexes and returns the decoded 144-bit message overlayed
|
||||
* upon the original message with the remaining 52 bits cleared to zero.
|
||||
*
|
||||
* @return - original message with decoded message bits..
|
||||
*/
|
||||
public boolean decode( BinaryMessage message, int start, int end )
|
||||
{
|
||||
reset();
|
||||
|
||||
/* Load and decode each of the transmitted constellations */
|
||||
for( int index = 0; index < 49; index++ )
|
||||
{
|
||||
Con c = getConstellation( message, start + index * 4 );
|
||||
|
||||
add( c );
|
||||
}
|
||||
|
||||
/* The final decoded survivor path should be at path metrics tribit 0 */
|
||||
Path path = mPathMetrics.getPath( Tribit.T0 );
|
||||
|
||||
/* We should have a path with 50 nodes, counting the starting 000 node
|
||||
* and the flushing 000 node, otherwise there was an error */
|
||||
if( path != null && path.getNodes().size() == 50 )
|
||||
{
|
||||
/* Clear the original message bits */
|
||||
message.clear( start, end );
|
||||
|
||||
List<Node> nodes = path.getNodes();
|
||||
|
||||
/* Load each of the nodes' state tribit values into the original message */
|
||||
for( int x = 1; x < 50; x++ )
|
||||
{
|
||||
message.load( start + ( ( x - 1 ) * 3 ), 3,
|
||||
nodes.get( x ).getState().getValue() );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the decoder before decoding a new data packet.
|
||||
*/
|
||||
private void reset()
|
||||
{
|
||||
mTransmittedConstellations.clear();
|
||||
mNewPaths.clear();
|
||||
|
||||
mPathMetrics.reset();
|
||||
|
||||
mSurvivorPaths.clear();
|
||||
|
||||
/* Tribit 000 is the only legal start point */
|
||||
mSurvivorPaths.add( new Path( new Node( 0, Tribit.T0, Con.C0 ) ) );
|
||||
}
|
||||
|
||||
private void add( Con con )
|
||||
{
|
||||
mTransmittedConstellations.add( con );
|
||||
|
||||
/* Add in any newly created survivor paths */
|
||||
mSurvivorPaths.addAll( mNewPaths );
|
||||
mNewPaths.clear();
|
||||
|
||||
/* Set a culling threshold for any path metrics 3 below the best path */
|
||||
int survivorThreshold = mPathMetrics.getSurvivorThreshold();
|
||||
|
||||
/* Reset path metrics */
|
||||
mPathMetrics.reset();
|
||||
|
||||
/* Add constellation to each survivor path. If a survivor path metric
|
||||
* falls below the culling threshold, remove it */
|
||||
Iterator<Path> it = mSurvivorPaths.iterator();
|
||||
|
||||
Path path;
|
||||
|
||||
while( it.hasNext() )
|
||||
{
|
||||
path = it.next();
|
||||
|
||||
if( path.isDead() || path.getPathMetric() < survivorThreshold )
|
||||
{
|
||||
it.remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
path.add( con );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Con getConstellation( BinaryMessage message, int index )
|
||||
{
|
||||
int value = message.getInt( index, index + 3 );
|
||||
return Con.fromTransmittedValue( value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Path metrics maintains a map of the current best path metrics that end
|
||||
* at each of the 8 valid tribit values. Each new path is evaluated against
|
||||
* the current best path for retention (survivor) or disposal (dead). A
|
||||
* best metric is maintained to evaluate the Walking Dead for disposal.
|
||||
* Walking dead is a path lagging the best path metric by more than 3.
|
||||
*/
|
||||
public class PathMetrics
|
||||
{
|
||||
private HashMap<Tribit,Path> mMetrics = new HashMap<Tribit,Path>();
|
||||
|
||||
private int mBestMetric = 0;
|
||||
|
||||
public PathMetrics()
|
||||
{
|
||||
}
|
||||
|
||||
public int getSurvivorThreshold()
|
||||
{
|
||||
return mBestMetric - 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all path metrics
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mMetrics.clear();
|
||||
mBestMetric = 0;
|
||||
}
|
||||
|
||||
public int getMetric( Tribit tribit )
|
||||
{
|
||||
if( mMetrics.containsKey( tribit ) )
|
||||
{
|
||||
return mMetrics.get( tribit ).getPathMetric();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Path getPath( Tribit tribit )
|
||||
{
|
||||
return mMetrics.get( tribit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the path's metric against the current best path metric for
|
||||
* the final state tribit of the path. If the path's metric is higher
|
||||
* than the current best path metric, the current path is marked for
|
||||
* termination and the new path is promoted to best path. If the new
|
||||
* path's metric is less than the best path metric, then the new path
|
||||
* metric is marked for termination. If the metrics are equal, the new
|
||||
* path is allowed to survive.
|
||||
*
|
||||
* @param path/metric to evaluate
|
||||
*/
|
||||
public void evalute( Path path )
|
||||
{
|
||||
Node lastNode = path.getLastNode();
|
||||
|
||||
Path bestPath = mMetrics.get( lastNode.getState() );
|
||||
|
||||
if( bestPath == null )
|
||||
{
|
||||
mMetrics.put( lastNode.getState(), path );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( path.getPathMetric() < bestPath.getPathMetric() )
|
||||
{
|
||||
path.setSurvivor( false );
|
||||
}
|
||||
else if( path.getPathMetric() > bestPath.getPathMetric() )
|
||||
{
|
||||
mMetrics.put( lastNode.getState(), path );
|
||||
|
||||
bestPath.setSurvivor( false );
|
||||
}
|
||||
|
||||
/* Equal metric paths are allowed to survive */
|
||||
}
|
||||
|
||||
/* Capture the best path metric */
|
||||
if( path.getPathMetric() > mBestMetric )
|
||||
{
|
||||
mBestMetric = path.getPathMetric();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Path is a sequence of nodes where the starting node always contains
|
||||
* tribit 0 and constellation 0. Path maintains a running path metric
|
||||
* derived from the sum of each of the node's branch metrics.
|
||||
*/
|
||||
public class Path
|
||||
{
|
||||
private boolean mSurvivor = true;
|
||||
private int mPathMetric = 0;
|
||||
private ArrayList<Node> mNodes = new ArrayList<Node>();
|
||||
|
||||
public Path( Node first )
|
||||
{
|
||||
mNodes.add( first );
|
||||
|
||||
mPathMetric = first.getBranchMetric();
|
||||
|
||||
mPathMetrics.evalute( this );
|
||||
}
|
||||
|
||||
public Path( ArrayList<Node> nodes, int metric )
|
||||
{
|
||||
mNodes = nodes;
|
||||
mPathMetric = metric;
|
||||
}
|
||||
|
||||
public List<Node> getNodes()
|
||||
{
|
||||
return mNodes;
|
||||
}
|
||||
|
||||
public int getBitErrorCount()
|
||||
{
|
||||
return ( getLastNode().getIndex() * 4 ) - mPathMetric;
|
||||
}
|
||||
|
||||
public void setSurvivor( boolean survivor )
|
||||
{
|
||||
mSurvivor = survivor;
|
||||
}
|
||||
|
||||
public boolean isSurvivor()
|
||||
{
|
||||
return mSurvivor;
|
||||
}
|
||||
|
||||
public boolean isDead()
|
||||
{
|
||||
return !mSurvivor;
|
||||
}
|
||||
|
||||
@SuppressWarnings( "unchecked" )
|
||||
public Path copyOf()
|
||||
{
|
||||
return new Path( (ArrayList<Node>)mNodes.clone(), mPathMetric );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the constellation and returns any new paths created as a result
|
||||
* of the constellation being detected as an errant constellation.
|
||||
*/
|
||||
public void add( Con con )
|
||||
{
|
||||
Node current = getLastNode();
|
||||
|
||||
Tribit input = INPUT_FROM_CONSTELLATION_MAP
|
||||
.get( con )[ current.getState().getValue() ];
|
||||
|
||||
/* A non-null input tribit from the lookup table is valid, otherwise
|
||||
* create 8 alternate branches from the original path to explore
|
||||
* where the error occurred. */
|
||||
if( input != null )
|
||||
{
|
||||
add( new Node( mNodes.size(), input, con ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Create new paths for each of the valid constellations that
|
||||
* correspond to the parity of the current state tribit */
|
||||
Con[] constellations = current.getState().getType() ==
|
||||
Type.EVEN ? EVEN_CONSTELLATIONS : ODD_CONSTELLATIONS;
|
||||
|
||||
for( int x = 0; x < 8; x++ )
|
||||
{
|
||||
/* Get the input tribit corresponding to the state tribit
|
||||
* and constellation combination */
|
||||
Tribit tribit = INPUT_FROM_CONSTELLATION_MAP
|
||||
.get( constellations[ x ] )[ current.getState().getValue() ];
|
||||
|
||||
Node candidate = new Node( mNodes.size(), tribit, constellations[ x ] );
|
||||
|
||||
/* If the current path metric plus the candidate node's
|
||||
* branch metric is equal or better than the current best
|
||||
* metric for the given tribit, add it as a potential path,
|
||||
* otherwise discard it */
|
||||
if( this.getPathMetric() + candidate.getBranchMetric() >=
|
||||
mPathMetrics.getMetric( tribit ) )
|
||||
{
|
||||
/* If this is the final constellation of the set, simply
|
||||
* add it to the existing path ... */
|
||||
if( x == 7 )
|
||||
{
|
||||
add( candidate );
|
||||
}
|
||||
/* ... otherwise create a copy of the current path */
|
||||
else
|
||||
{
|
||||
Path path = this.copyOf();
|
||||
|
||||
path.add( candidate );
|
||||
|
||||
mNewPaths.add( path );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void add( Node node )
|
||||
{
|
||||
mNodes.add( node );
|
||||
|
||||
mPathMetric += node.getBranchMetric();
|
||||
|
||||
mPathMetrics.evalute( this );
|
||||
}
|
||||
|
||||
public int getPathMetric()
|
||||
{
|
||||
return mPathMetric;
|
||||
}
|
||||
|
||||
public Node getLastNode()
|
||||
{
|
||||
return mNodes.get( mNodes.size() - 1 );
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "PATH [" );
|
||||
sb.append( mNodes.size() );
|
||||
sb.append( "-" );
|
||||
sb.append( mPathMetric );
|
||||
sb.append( "] " );
|
||||
|
||||
for( Node node: mNodes )
|
||||
{
|
||||
sb.append( node.getState().name() );
|
||||
sb.append( "/" );
|
||||
sb.append( node.getConstellation().name() );
|
||||
sb.append( " " );
|
||||
}
|
||||
|
||||
sb.append( mSurvivor ? "SURVIVOR" : "DEAD" );
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trellis node containing the constellation used to produce the node and
|
||||
* the input tribit for the constellation.
|
||||
*/
|
||||
public class Node
|
||||
{
|
||||
private int mIndex;
|
||||
private int mBranchMetric;
|
||||
private Tribit mTribit;
|
||||
private Con mConstellation;
|
||||
|
||||
public Node( int index, Tribit tribit, Con constellation )
|
||||
{
|
||||
mIndex = index;
|
||||
mTribit = tribit;
|
||||
mConstellation = constellation;
|
||||
|
||||
if( mIndex > 0 )
|
||||
{
|
||||
Con transmitted = mTransmittedConstellations.get( index - 1 );
|
||||
|
||||
if( transmitted == null )
|
||||
{
|
||||
throw new IllegalArgumentException( "Cannot calculate "
|
||||
+ "metric - transmitted constellation symbol is null" );
|
||||
}
|
||||
|
||||
mBranchMetric = transmitted.getMetricTo( constellation );
|
||||
}
|
||||
}
|
||||
|
||||
public int getIndex()
|
||||
{
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
public Tribit getState()
|
||||
{
|
||||
return mTribit;
|
||||
}
|
||||
|
||||
public Con getConstellation()
|
||||
{
|
||||
return mConstellation;
|
||||
}
|
||||
|
||||
public int getBranchMetric()
|
||||
{
|
||||
return mBranchMetric;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append( "Node " );
|
||||
sb.append( mIndex );
|
||||
sb.append( " Metric:" );
|
||||
sb.append( mBranchMetric );
|
||||
sb.append( " Con:" );
|
||||
sb.append( mConstellation.name() );
|
||||
sb.append( " State:" );
|
||||
if( mTribit == null )
|
||||
{
|
||||
sb.append( "null" );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( mTribit.name() );
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constellation Type. Constellations are classified as even or add based
|
||||
* on the the composition/comparison of the transmitted (representative)
|
||||
* dibits:
|
||||
*
|
||||
* EVEN: - first dibit differs from second dibit in exactly one bit position
|
||||
*
|
||||
* ODD: - first dibit differs from second dibit in zero or two bit positions
|
||||
* following three patterns:
|
||||
* 1) repeat (10 10), mirror (01 10), or invert(00 11)
|
||||
*/
|
||||
public enum Type { EVEN, ODD };
|
||||
|
||||
/**
|
||||
* Constellations, ordered by transmitted value. Transmitted value is the
|
||||
* value of the dibit pair that represents the constellation when transmitted.
|
||||
*/
|
||||
public enum Con
|
||||
{
|
||||
CB( 0 ),
|
||||
CC( 1 ),
|
||||
C0( 2 ),
|
||||
C7( 3 ),
|
||||
CE( 4 ),
|
||||
C9( 5 ),
|
||||
C5( 6 ),
|
||||
C2( 7 ),
|
||||
CA( 8 ),
|
||||
CD( 9 ),
|
||||
C1( 10 ),
|
||||
C6( 11 ),
|
||||
CF( 12 ),
|
||||
C8( 13 ),
|
||||
C4( 14 ),
|
||||
C3( 15 );
|
||||
|
||||
private int mTransmittedValue;
|
||||
|
||||
private Con( int transmittedValue )
|
||||
{
|
||||
mTransmittedValue = transmittedValue;
|
||||
}
|
||||
|
||||
public int getTransmittedValue()
|
||||
{
|
||||
return mTransmittedValue;
|
||||
}
|
||||
|
||||
public static Con fromTransmittedValue( int value )
|
||||
{
|
||||
if( 0 <= value && value <= 15 )
|
||||
{
|
||||
return values()[ value ];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Con fromTransmittedDibits( Dibit left, Dibit right )
|
||||
{
|
||||
return fromTransmittedValue( left.getHighValue() + right.getLowValue() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the metric or hamming distance to the other constellation
|
||||
* using the values from the constellation costs table. A perfect match
|
||||
* has a metric of 4 and a complete miss has a value of 0.
|
||||
*/
|
||||
public int getMetricTo( Con other )
|
||||
{
|
||||
return CONSTELLATION_METRICS[ getTransmittedValue() ][ other.getTransmittedValue() ];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tribit. Used to define both the state and the input bit sequences that
|
||||
* produce the transmitted constellation bit sequences. Each tribit is
|
||||
* either even or odd valued producing a direct correlation to the types
|
||||
* of valid constellations containing the tribit as the state tribit.
|
||||
*/
|
||||
public enum Tribit
|
||||
{
|
||||
T0( 0, Type.EVEN ),
|
||||
T1( 1, Type.EVEN ),
|
||||
T2( 2, Type.ODD ),
|
||||
T3( 3, Type.ODD ),
|
||||
T4( 4, Type.ODD ),
|
||||
T5( 5, Type.ODD ),
|
||||
T6( 6, Type.EVEN ),
|
||||
T7( 7, Type.EVEN );
|
||||
|
||||
private int mValue;
|
||||
private Type mType;
|
||||
|
||||
private Tribit( int value, Type type )
|
||||
{
|
||||
mValue = value;
|
||||
mType = type;
|
||||
}
|
||||
|
||||
public Type getType()
|
||||
{
|
||||
return mType;
|
||||
}
|
||||
|
||||
public int getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test harness
|
||||
*/
|
||||
public static void main( String[] args )
|
||||
{
|
||||
String raw = "12:58:51.635 DEBUG dsp.fsk.P25MessageFramer - AFTER DEINTERLEAVE: 00100110000011001010010101111101000101010110100111111100101011000111011011000110000000000000000000000111111101011000001000001111000110000000000010101011110110010000000001011011000000010100011110110001000010100100011111000000000100000000000000000000010000000000001100000000000000011010101010101010101010100010001010000110010111111101101000001010000010100000101000001010000010100000101000001010000010100000101000001010000010100000101000001010000010100000010111001001000011100101011010101010101001010011";
|
||||
|
||||
BinaryMessage message = BinaryMessage.load( raw );
|
||||
|
||||
mLog.debug( "MSG: " + message.toString() );
|
||||
|
||||
Trellis_3_4_Rate t = new Trellis_3_4_Rate();
|
||||
t.decode( message, 0, 196 );
|
||||
|
||||
mLog.debug( "DEC: " + message.toString() );
|
||||
|
||||
mLog.debug( "Finished!" );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package io.github.dsheirer.module.decode.p25.audio;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.audio.IAudioPacketProvider;
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
|
||||
import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.message.IMessageListener;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import jmbe.iface.AudioConversionLibrary;
|
||||
import jmbe.iface.AudioConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.dsp.gain.NonClippingGain;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.module.decode.p25.message.hdu.HDUMessage;
|
||||
import io.github.dsheirer.module.decode.p25.message.ldu.LDU1Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.ldu.LDU2Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.ldu.LDUMessage;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class P25AudioModule extends Module implements Listener<Message>, IAudioPacketProvider, IMessageListener,
|
||||
ISquelchStateListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(P25AudioModule.class);
|
||||
|
||||
private static final String IMBE_CODEC = "IMBE";
|
||||
private static boolean mLibraryLoadStatusLogged = false;
|
||||
|
||||
private boolean mCanConvertAudio = false;
|
||||
private boolean mEncryptedCall = false;
|
||||
private boolean mEncryptedCallStateEstablished = false;
|
||||
|
||||
private AudioConverter mAudioConverter;
|
||||
private Listener<AudioPacket> mAudioPacketListener;
|
||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private NonClippingGain mGain = new NonClippingGain(5.0f, 0.95f);
|
||||
private Metadata mMetadata;
|
||||
private LDU1Message mCachedLDU1Message = null;
|
||||
|
||||
public P25AudioModule(Metadata metadata)
|
||||
{
|
||||
mMetadata = metadata;
|
||||
loadConverter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<Message> getMessageListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SquelchState> getSquelchStateListener()
|
||||
{
|
||||
return mSquelchStateListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mAudioConverter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes call header (HDU) and voice frame (LDU1/LDU2) messages to decode audio and to determine the
|
||||
* encrypted audio status of a call event. Only the HDU and LDU2 messages convey encrypted call status. If an
|
||||
* LDU1 message is received without a preceding HDU message, then the LDU1 message is cached until the first
|
||||
* LDU2 message is received and the encryption state can be determined. Both the LDU1 and the LDU2 message are
|
||||
* then processed for audio if the call is unencrypted.
|
||||
*/
|
||||
public void receive(Message message)
|
||||
{
|
||||
if(mCanConvertAudio && mAudioPacketListener != null)
|
||||
{
|
||||
if(mEncryptedCallStateEstablished)
|
||||
{
|
||||
if(message instanceof LDUMessage)
|
||||
{
|
||||
processAudio((LDUMessage)message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(message instanceof HDUMessage)
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = ((HDUMessage)message).isEncryptedAudio();
|
||||
}
|
||||
else if(message instanceof LDU1Message)
|
||||
{
|
||||
//When we receive an LDU1 message with first receiving the HDU message, cache the LDU1 Message
|
||||
//until we can determine the encrypted call state from the next LDU2 message
|
||||
mCachedLDU1Message = (LDU1Message)message;
|
||||
}
|
||||
else if(message instanceof LDU2Message)
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
LDU2Message ldu2 = (LDU2Message)message;
|
||||
mEncryptedCall = ldu2.isEncryptedAudio();
|
||||
|
||||
if(mCachedLDU1Message != null)
|
||||
{
|
||||
processAudio(mCachedLDU1Message);
|
||||
mCachedLDU1Message = null;
|
||||
}
|
||||
|
||||
processAudio(ldu2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an audio packet by decoding the IMBE audio frames and rebroadcasting them as PCM audio packets.
|
||||
*/
|
||||
private void processAudio(LDUMessage ldu)
|
||||
{
|
||||
if(!mEncryptedCall)
|
||||
{
|
||||
for(byte[] frame : ldu.getIMBEFrames())
|
||||
{
|
||||
float[] audio = mAudioConverter.decode(frame);
|
||||
|
||||
audio = mGain.apply(audio);
|
||||
|
||||
mAudioPacketListener.receive(new AudioPacket(audio, mMetadata.copyOf()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Encrypted audio processing not implemented
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads audio frame processing chain. Constructs an imbe targetdataline
|
||||
* to receive the raw imbe frames. Adds an IMBE to 8k PCM format conversion
|
||||
* stream wrapper. Finally, adds an upsampling (8k to 48k) stream wrapper.
|
||||
*/
|
||||
private void loadConverter()
|
||||
{
|
||||
AudioConversionLibrary library = null;
|
||||
|
||||
try
|
||||
{
|
||||
@SuppressWarnings("rawtypes")
|
||||
Class temp = Class.forName("jmbe.JMBEAudioLibrary");
|
||||
|
||||
library = (AudioConversionLibrary)temp.newInstance();
|
||||
|
||||
if((library.getMajorVersion() == 0 && library.getMinorVersion() >= 3 &&
|
||||
library.getBuildVersion() >= 3) || library.getMajorVersion() >= 1)
|
||||
{
|
||||
mAudioConverter = library.getAudioConverter(IMBE_CODEC, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO);
|
||||
|
||||
if(mAudioConverter != null)
|
||||
{
|
||||
mCanConvertAudio = true;
|
||||
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("JMBE audio conversion library [");
|
||||
sb.append(library.getVersion());
|
||||
sb.append("] successfully loaded - P25 audio will be available");
|
||||
|
||||
mLog.info(sb.toString());
|
||||
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
mLog.info("JMBE audio conversion library NOT FOUND");
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.warn("JMBE library version 0.3.3 or higher is required - found: " + library.getVersion());
|
||||
}
|
||||
}
|
||||
catch(ClassNotFoundException e1)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
mLog.error("Couldn't find/load JMBE audio conversion library");
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
catch(InstantiationException e1)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
mLog.error("Couldn't instantiate JMBE audio conversion library class");
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
catch(IllegalAccessException e1)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged)
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library due to "
|
||||
+ "security restrictions");
|
||||
mLibraryLoadStatusLogged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAudioPacketListener(Listener<AudioPacket> listener)
|
||||
{
|
||||
mAudioPacketListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAudioPacketListener()
|
||||
{
|
||||
mAudioPacketListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for squelch state to process end of call actions. At call end the encrypted call state established
|
||||
* flag is reset so that the encrypted audio state for the next call can be properly detected and we send an
|
||||
* END audio packet so that downstream processors like the audio recorder can properly close out a call sequence.
|
||||
*/
|
||||
public class SquelchStateListener implements Listener<SquelchState>
|
||||
{
|
||||
@Override
|
||||
public void receive(SquelchState state)
|
||||
{
|
||||
if(state == SquelchState.SQUELCH)
|
||||
{
|
||||
if(mAudioPacketListener != null)
|
||||
{
|
||||
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
|
||||
}
|
||||
|
||||
mEncryptedCallStateEstablished = false;
|
||||
mEncryptedCall = false;
|
||||
mCachedLDU1Message = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.github.dsheirer.module.decode.p25.message;
|
||||
|
||||
/**
|
||||
* Interface for adjacent site (ie neighbor) messages
|
||||
*/
|
||||
public interface IAdjacentSite
|
||||
{
|
||||
public String getUniqueID();
|
||||
public String getRFSS();
|
||||
public String getSystemID();
|
||||
public String getSiteID();
|
||||
public String getLRA();
|
||||
public String getSystemServiceClass();
|
||||
|
||||
public String getDownlinkChannel();
|
||||
public long getDownlinkFrequency();
|
||||
|
||||
public String getUplinkChannel();
|
||||
public long getUplinkFrequency();
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.p25.message;
|
||||
|
||||
public interface IBandIdentifier
|
||||
{
|
||||
|
||||
/**
|
||||
* (Band) Identifier
|
||||
*/
|
||||
public abstract int getIdentifier();
|
||||
|
||||
/**
|
||||
* Channel spacing in hertz
|
||||
*/
|
||||
public abstract long getChannelSpacing();
|
||||
|
||||
/**
|
||||
* Base frequency in hertz
|
||||
*/
|
||||
public abstract long getBaseFrequency();
|
||||
|
||||
|
||||
/**
|
||||
* Channel bandwidth in hertz
|
||||
*/
|
||||
public abstract int getBandwidth();
|
||||
|
||||
/**
|
||||
* Transmit offset in hertz
|
||||
*/
|
||||
public abstract long getTransmitOffset();
|
||||
|
||||
/**
|
||||
* Downlink (tower to mobile) Frequency for the specified channel
|
||||
*/
|
||||
public abstract long getDownlinkFrequency(int channelNumber);
|
||||
|
||||
/**
|
||||
* Uplink (mobile to tower) Frequency for the specified channel
|
||||
*/
|
||||
public abstract long getUplinkFrequency(int channelNumber);
|
||||
|
||||
/**
|
||||
* Indicates if this band is an FDMA (false) or TDMA (true) band
|
||||
*/
|
||||
public boolean isTDMA();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.github.dsheirer.module.decode.p25.message;
|
||||
|
||||
|
||||
/**
|
||||
* Interface to allow messages to be augmented with IdentiferUpdateXXX type
|
||||
* messages that provide the channel information necessary to calculate the
|
||||
* uplink and downlink frequency for the channel.
|
||||
*/
|
||||
public interface IdentifierReceiver
|
||||
{
|
||||
public void setIdentifierMessage( int identifier, IBandIdentifier message );
|
||||
|
||||
public int[] getIdentifiers();
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*******************************************************************************
|
||||
* 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 io.github.dsheirer.module.decode.p25.message;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import io.github.dsheirer.map.Plottable;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.p25.reference.DataUnitID;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
public class P25Message extends Message
|
||||
{
|
||||
public enum DuplexMode {HALF, FULL};
|
||||
public enum SessionMode {PACKET, CIRCUIT};
|
||||
|
||||
public static final int[] NAC = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
|
||||
public static final int[] DUID = {12, 13, 14, 15};
|
||||
public static final int[] BCH = {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
|
||||
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63};
|
||||
|
||||
protected SimpleDateFormat mTimeDurationFormat = new SimpleDateFormat("HH:mm:ss.SSS");
|
||||
|
||||
protected BinaryMessage mMessage;
|
||||
protected DataUnitID mDUID;
|
||||
protected AliasList mAliasList;
|
||||
|
||||
protected CRC[] mCRC;
|
||||
|
||||
public P25Message(BinaryMessage message, DataUnitID duid, AliasList aliasList)
|
||||
{
|
||||
super();
|
||||
|
||||
mMessage = message;
|
||||
mDUID = duid;
|
||||
mAliasList = aliasList;
|
||||
}
|
||||
|
||||
public CRC[] getCRCResults()
|
||||
{
|
||||
return mCRC;
|
||||
}
|
||||
|
||||
public BinaryMessage getSourceMessage()
|
||||
{
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
public AliasList getAliasList()
|
||||
{
|
||||
return mAliasList;
|
||||
}
|
||||
|
||||
public String getNAC()
|
||||
{
|
||||
return mMessage.getHex(NAC, 3);
|
||||
}
|
||||
|
||||
public DataUnitID getDUID()
|
||||
{
|
||||
return mDUID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorStatus()
|
||||
{
|
||||
if(mCRC == null)
|
||||
{
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
return CRC.format(mCRC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("NAC:");
|
||||
sb.append(getNAC());
|
||||
sb.append(" ");
|
||||
sb.append(getDUID().getLabel());
|
||||
sb.append(" DATA UNIT ID:" + mMessage.getInt(DUID));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBinaryMessage()
|
||||
{
|
||||
return mMessage.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol()
|
||||
{
|
||||
return "P25 Phase 1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType()
|
||||
{
|
||||
return mDUID.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFromID()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getFromIDAlias()
|
||||
{
|
||||
if(mAliasList != null)
|
||||
{
|
||||
return mAliasList.getTalkgroupAlias(getFromID());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToID()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Alias getToIDAlias()
|
||||
{
|
||||
if(mAliasList != null)
|
||||
{
|
||||
return mAliasList.getTalkgroupAlias(getToID());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Plottable getPlottable()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
if(mCRC != null)
|
||||
{
|
||||
for(CRC crc : mCRC)
|
||||
{
|
||||
if(crc == CRC.FAILED_CRC)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.github.dsheirer.module.decode.p25.message.filter;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.hdu.HDUMessage;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class HDUMessageFilter extends Filter<Message>
|
||||
{
|
||||
public HDUMessageFilter()
|
||||
{
|
||||
super( "Header Data Unit" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes( Message message )
|
||||
{
|
||||
return mEnabled && canProcess( message );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess( Message message )
|
||||
{
|
||||
return message instanceof HDUMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.github.dsheirer.module.decode.p25.message.filter;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.Message;
|
||||
import io.github.dsheirer.module.decode.p25.message.ldu.LDUMessage;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class LDUMessageFilter extends Filter<Message>
|
||||
{
|
||||
public LDUMessageFilter()
|
||||
{
|
||||
super( "Link Data Unit" );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes( Message message )
|
||||
{
|
||||
return mEnabled && canProcess( message );
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess( Message message )
|
||||
{
|
||||
return message instanceof LDUMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user