#1304 P25 Phase 1/2 decoder enhancements. (#1898)

* P25 Phase 2 now supports control channel decoding and decoder was enhanced to improve decodes.
* Radio reference site editor updated for phase 2 sites to allow user to select FDMA or TDMA control.
* Decode events are now minimized and de-duplicated.
* Fully parsing all phase 1/2 messages from the latest ICD.
* Vendor specific messaging recoveries (Moto & Harris).
* Patch group management enhancements with stale patch detection.
* Tuner editor now fully resets the min/max frequency values
* Default traffic channel count increased to 20 (from 3).
* ISSI roaming radio and talkgroup values now displayed and aliasable as fully qualified values (e.g. 1234(123.456.7890)
* Moto emergency alarm activation messaging support
* Expanded list of P25 encryption algorithms
* Message (bits) viewer enhanced for P25 phase 1 and phase 2
* IPV4 addresses and UDP ports no longer classified as user values - won't display in event views.
* New framework for describing message binary fields that should reduce overall memory footprint and enhance code readability

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2024-04-26 13:55:17 -04:00
committed by GitHub
parent f5938fa8e2
commit 7e709d9d6f
430 changed files with 30098 additions and 13300 deletions

View File

@@ -29,6 +29,7 @@
<w>talkgroups</w>
<w>tgid</w>
<w>ultravox</w>
<w>unsynchronized</w>
<w>wacn</w>
<w>xtea</w>
</words>

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -663,11 +663,13 @@ public class AliasList
public Alias getAlias(TalkgroupIdentifier identifier)
{
//Attempt to do a fully qualified identifier match first.
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
{
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
return mFullyQualifiedTalkgroupAliasMap.get(fqti.getFullyQualifiedTalkgroupAddress());
}
//Then try to match it by it's locally assigned (temporary) address.
int value = identifier.getValue();
Alias mapValue = mTalkgroupAliasMap.get(value);
@@ -676,6 +678,7 @@ public class AliasList
return mapValue;
}
//Finally, match the locally assigned address against any talkgroup ranges
for(Map.Entry<TalkgroupRange, Alias> entry : mTalkgroupRangeAliasMap.entrySet())
{
if(entry.getKey().contains(value))
@@ -780,11 +783,13 @@ public class AliasList
public Alias getAlias(RadioIdentifier identifier)
{
//Attempt to do a fully qualified identifier match first.
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
{
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
return mFullyQualifiedRadioAliasMap.get(fqri.getFullyQualifiedRadioAddress());
}
//Then match against the locally assigned (temporary) address
int value = identifier.getValue();
Alias mapValue = mRadioAliasMap.get(value);
@@ -793,6 +798,7 @@ public class AliasList
return mapValue;
}
//Finally, attempt to match the locally assigned (temporary) address against any radio ranges.
for(Map.Entry<RadioRange, Alias> entry : mRadioRangeAliasMap.entrySet())
{
if(entry.getKey().contains(value))

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -451,6 +451,203 @@ public class BinaryMessage extends BitSet
return value;
}
/**
* Returns the integer value of the message field described by the field argument.
* @param intField with start and end indices (inclusive).
* @return integer field value.
*/
public int getInt(IntField intField)
{
int value = 0;
for(int index = intField.start(); index <= intField.end(); index++)
{
value = Integer.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Returns the integer value of the message field described by the field argument.
* @param fragmentedField with an array of message indices
* @return integer field value.
*/
public int getInt(FragmentedIntField fragmentedField)
{
int value = 0;
for(int index: fragmentedField.indices())
{
value = Integer.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
* @param intField to inspect
* @return true if any of the bits are set, indicating a non-zero value.
*/
public boolean hasInt(IntField intField)
{
return nextSetBit(intField.start()) <= intField.end();
}
/**
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
* @param intField to inspect
* @return true if any of the bits are set, indicating a non-zero value.
*/
public boolean hasInt(FragmentedIntField fragmentedField)
{
for(int index: fragmentedField.indices())
{
if(get(index))
{
return true;
}
}
return false;
}
/**
* Returns the integer value of the message field described by the field argument where the message start index is
* offset within this binary message.
* @param intField with start and end indices (inclusive).
* @param offset to the start of the first message index.
* @return integer field value.
*/
public int getInt(IntField intField, int offset)
{
int value = 0;
for(int index = intField.start() + offset; index <= intField.end() + offset; index++)
{
value = Integer.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Returns the integer value of the message field described by the field argument where the message start index is
* offset within this binary message.
* @param fragmentedField with an array of field indices
* @param offset to the start of the first message index.
* @return integer field value.
*/
public int getInt(FragmentedIntField fragmentedField, int offset)
{
int value = 0;
for(int index: fragmentedField.indices())
{
value = Integer.rotateLeft(value, 1);
if(get(index + offset))
{
value++;
}
}
return value;
}
/**
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
* @param intField to inspect
* @param offset to the first bit of the message.
* @return true if any of the bits are set, indicating a non-zero value.
*/
public boolean hasInt(IntField intField, int offset)
{
return nextSetBit(intField.start() + offset) <= (intField.end() + offset);
}
/**
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
* @param intField to inspect
* @param offset to the first bit of the message.
* @return true if any of the bits are set, indicating a non-zero value.
*/
public boolean hasInt(FragmentedIntField fragmentedField, int offset)
{
for(int index: fragmentedField.indices())
{
if(get(index + offset))
{
return true;
}
}
return false;
}
/**
* Returns the long value of the message field described by the field argument.
* @param intField with start and end indices (inclusive).
* @return integer field value.
*/
public long getLong(LongField intField)
{
long value = 0;
for(int index = intField.start(); index <= intField.end(); index++)
{
value = Long.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Returns the long value of the message field described by the field argument where the message start index is
* offset within this binary message.
* @param intField with start and end indices (inclusive).
* @param offset to the start of the first message index.
* @return field value.
*/
public long getLong(LongField intField, int offset)
{
long value = 0;
for(int index = intField.start() + offset; index <= intField.end() + offset; index++)
{
value = Long.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Returns the integer value represented by the bit array
*
@@ -742,6 +939,18 @@ public class BinaryMessage extends BitSet
return sb.toString();
}
/**
* Returns the integer field formatted as a hex value using zero prefixes to pad the hex character count to fully
* represent the size (width) of the field.
* @param field to parse as hex
* @return hex value.
*/
public String getHex(IntField field)
{
int width = Math.ceilDiv(field.width(), 4);
return String.format("%0" + width + "X", getInt(field));
}
/**
* Format the byte value that starts at the specified index as hexadecimal. If the length of the message is less
* than the start index plus 7 bits, then the value represents those bits as high-order bits with zero padding in

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.bits;
@@ -84,9 +81,9 @@ public class CorrectedBinaryMessage extends BinaryMessage
/**
* Returns a new binary message containing the bits from (inclusive) to end (exclusive).
*
* @param start bit
* @param end bit
* @return message
* @param start bit inclusive
* @param end bit exclusive
* @return message with sub message bits
*/
public CorrectedBinaryMessage getSubMessage(int start, int end)
{

View File

@@ -0,0 +1,45 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.bits;
/**
* Defines a fragmented or non-contiguous bit field within a binary message.
* @param indices for the bits in the field.
*/
public record FragmentedIntField(int... indices)
{
public FragmentedIntField
{
if(indices.length > 32)
{
throw new IllegalArgumentException("Integer field indices size [" + indices.length + "] cannot exceed 32-bits for an integer");
}
}
/**
* Utility constructor method.
* @param indices (inclusive)
* @return constructed fragmented integer field.
*/
public static FragmentedIntField of(int... indices)
{
return new FragmentedIntField(indices);
}
}

View File

@@ -0,0 +1,131 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.bits;
/**
* Defines a contiguous bit field within a binary message.
* @param start bit index, inclusive.
* @param end bit index, inclusive.
*/
public record IntField(int start, int end)
{
public IntField
{
if(end - start > 32)
{
throw new IllegalArgumentException("Integer field length [" + (end - start) + "] cannot exceed 32-bits for an integer");
}
if(end < start)
{
throw new IllegalArgumentException("Integer field start index must be less than end index");
}
}
/**
* Width of the field in bit positions
* @return width
*/
public int width()
{
return end() - start() + 1;
}
/**
* Utility constructor method.
* @param start index (inclusive)
* @param end index (inclusive)
* @return constructed bit field.
*/
public static IntField range(int start, int end)
{
return new IntField(start, end);
}
/**
* Utility constructor method for a field with one four bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length4(int start)
{
return new IntField(start, (start + 3));
}
/**
* Utility constructor method for a field with one octet of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length8(int start)
{
return new IntField(start, (start + 7));
}
/**
* Utility constructor method for a field with 12 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length12(int start)
{
return new IntField(start, (start + 11));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length16(int start)
{
return new IntField(start, (start + 15));
}
/**
* Utility constructor method for a field with 20 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length20(int start)
{
return new IntField(start, (start + 19));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length24(int start)
{
return new IntField(start, (start + 23));
}
/**
* Utility constructor method for a field with three octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length32(int start)
{
return new IntField(start, (start + 31));
}
}

View File

@@ -0,0 +1,122 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.bits;
/**
* Defines a contiguous bit field within a binary message.
* @param start bit index, inclusive.
* @param end bit index, inclusive.
*/
public record LongField(int start, int end)
{
public LongField
{
if(end - start > 64)
{
throw new IllegalArgumentException("Long field length [" + (end - start) + "] cannot exceed 64-bits for a long");
}
if(end < start)
{
throw new IllegalArgumentException("Long field start index must be less than end index");
}
}
/**
* Utility constructor method.
* @param start index (inclusive)
* @param end index (inclusive)
* @return constructed bit field.
*/
public static LongField range(int start, int end)
{
return new LongField(start, end);
}
/**
* Utility constructor method for a field with one four bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length4(int start)
{
return new LongField(start, (start + 3));
}
/**
* Utility constructor method for a field with one octet of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length8(int start)
{
return new LongField(start, (start + 7));
}
/**
* Utility constructor method for a field with 12 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length12(int start)
{
return new LongField(start, (start + 11));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length16(int start)
{
return new LongField(start, (start + 15));
}
/**
* Utility constructor method for a field with 20 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length20(int start)
{
return new LongField(start, (start + 19));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length24(int start)
{
return new LongField(start, (start + 23));
}
/**
* Utility constructor method for a field with three octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length32(int start)
{
return new LongField(start, (start + 31));
}
}

View File

@@ -26,6 +26,7 @@ import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.event.ActivitySummaryProvider;
import io.github.dsheirer.module.decode.event.DecodeEventDuplicateDetector;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.module.decode.event.IDecodeEventProvider;
import io.github.dsheirer.sample.Broadcaster;
@@ -44,6 +45,7 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
protected Broadcaster<IDecodeEvent> mDecodeEventBroadcaster = new Broadcaster<>();
protected Listener<DecoderStateEvent> mDecoderStateListener;
private DecoderStateEventListener mDecoderStateEventListener = new DecoderStateEventListener();
private static final DecodeEventDuplicateDetector mDuplicateEventDetector = new DecodeEventDuplicateDetector();
private boolean mRunning;
public abstract DecoderType getDecoderType();
@@ -105,7 +107,10 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
*/
protected void broadcast(IDecodeEvent event)
{
mDecodeEventBroadcaster.broadcast(event);
if(!mDuplicateEventDetector.isDuplicate(event, System.currentTimeMillis()))
{
mDecodeEventBroadcaster.broadcast(event);
}
}
/**
@@ -161,6 +166,9 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
mDecoderStateListener = null;
}
/**
* Wrapper that implements the IDecoderStateEventListener interface
*/
private class DecoderStateEventListener implements Listener<DecoderStateEvent>
{
@Override
@@ -169,23 +177,4 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
receiveDecoderStateEvent(event);
}
}
/**
* Message listener that only passes messages while we're running. This is important because each of the decoders
* can process blocks of samples and that can result in additional messages being generated even after shutdown
* and so we shut off the processing of decoded messages when we're commanded to stop. The sample processing thread
* cannot be shutdown or forcefully interrupted because downstream decoder and channel states may have acquired
* locks that have to be properly released.
*/
private class MessageListener implements Listener<IMessage>
{
@Override
public void receive(IMessage message)
{
if(isRunning())
{
receive(message);
}
}
}
}

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.configuration.ChannelDescriptorConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.DecoderTypeConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.sample.Listener;
/**
@@ -38,13 +39,21 @@ public abstract class DecoderState extends AbstractDecoderState
private MutableIdentifierCollection mIdentifierCollection;
protected Listener<IdentifierUpdateNotification> mConfigurationIdentifierListener;
protected IChannelDescriptor mCurrentChannel;
private long mCurrentFrequency;
/**
* Constructs an instance
* @param mutableIdentifierCollection to preload into this decoder state
*/
public DecoderState(MutableIdentifierCollection mutableIdentifierCollection)
{
mIdentifierCollection = mutableIdentifierCollection;
mIdentifierCollection.update(new DecoderTypeConfigurationIdentifier(getDecoderType()));
}
/**
* Constructs an instance using an empty identifier collection.
*/
public DecoderState()
{
this(new MutableIdentifierCollection());
@@ -59,6 +68,23 @@ public abstract class DecoderState extends AbstractDecoderState
mIdentifierCollection.broadcastIdentifiers();
}
/**
* Current frequency for this channel.
*/
public long getCurrentFrequency()
{
return mCurrentFrequency;
}
/**
* Sets the current frequency for this channel.
* @param frequency to set.
*/
public void setCurrentFrequency(long frequency)
{
mCurrentFrequency = frequency;
}
/**
* Registers the listener to receive identifier update notifications
*/
@@ -153,9 +179,13 @@ public abstract class DecoderState extends AbstractDecoderState
{
getIdentifierCollection().receive(identifierUpdateNotification);
if(identifierUpdateNotification.getIdentifier() instanceof ChannelDescriptorConfigurationIdentifier)
if(identifierUpdateNotification.getIdentifier() instanceof ChannelDescriptorConfigurationIdentifier cdci)
{
setCurrentChannel(((ChannelDescriptorConfigurationIdentifier)identifierUpdateNotification.getIdentifier()).getValue());
setCurrentChannel(cdci.getValue());
}
else if(identifierUpdateNotification.getIdentifier() instanceof FrequencyConfigurationIdentifier fci)
{
setCurrentFrequency(fci.getValue());
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -26,12 +26,20 @@ import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.configuration.ChannelDescriptorConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.sample.Listener;
/**
* Base decoder state implementation for a multi-timeslot protocol that tracks the state for a single timeslot.
*/
public abstract class TimeslotDecoderState extends DecoderState
{
private int mTimeslot;
/**
* Constructs an instance
* @param timeslot to monitor/maintain state.
*/
public TimeslotDecoderState(int timeslot)
{
super(new MutableIdentifierCollection(timeslot));
@@ -39,6 +47,9 @@ public abstract class TimeslotDecoderState extends DecoderState
mConfigurationIdentifierListener = new TimeslotConfigurationIdentifierListener();
}
/**
* Monitored timeslot for this decoder state instance.
*/
protected int getTimeslot()
{
return mTimeslot;
@@ -82,6 +93,10 @@ public abstract class TimeslotDecoderState extends DecoderState
{
setCurrentChannel((IChannelDescriptor)identifier);
}
else if(identifier instanceof FrequencyConfigurationIdentifier fci)
{
setCurrentFrequency(fci.getValue());
}
}
}
}

View File

@@ -279,7 +279,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
break;
case REQUEST_DISABLE:
case NOTIFICATION_DELETE:
if(channel.isProcessing())
if(channel != null && channel.isProcessing())
{
try
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -19,6 +19,7 @@
package io.github.dsheirer.edac;
import io.github.dsheirer.log.LoggingSuppressor;
import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
@@ -32,6 +33,7 @@ import org.slf4j.LoggerFactory;
public class BerlekempMassey
{
private final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
/* Golay field size GF( 2 ** MM ) */
private int MM;
@@ -234,7 +236,16 @@ public class BerlekempMassey
/* put recd[i] into index form (ie as powers of alpha) */
for(int i = 0; i < NN; i++)
{
output[i] = index_of[input[i]];
try
{
output[i] = index_of[input[i]];
}
catch(Exception e)
{
LOGGING_SUPPRESSOR.error(getClass().toString(), 2, "Reed Solomon Decoder error for " +
"class [" + getClass() + "] there may be an issue with the message parser class indices - " +
"ensure the hex bit values are not larger 63", e);
}
}
/* first form the syndromes */

View File

@@ -0,0 +1,285 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.edac;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.IntField;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* P25 Phase 2 Binary (40, 9, 16) Code for IISCH in the Super Frame Fragment.
*/
public class Binary40_9_16
{
private final static Logger mLog = LoggerFactory.getLogger(Binary40_9_16.class);
private static final long[] CHECKSUMS = {0x8816CE36D7l, 0x201DFD4F64l, 0x100F4B1758l, 0x0C00DED18El, 0x020807F7FFl,
0x09048D9B72l, 0x009DA3A171l, 0x0058CBAA4El, 0x00343D8597l};
private static final long CODE_WORD_OFFSET = 0x184229d461l;
private static final Map<Long,BinaryMessage> CODEWORD_MAP_TS1 = new TreeMap<>();
private static final Map<Long,BinaryMessage> CODEWORD_MAP_TS2 = new TreeMap<>();
private static final IntField CHANNEL_NUMBER = IntField.range(2, 3);
private static final IntField ISCH_LOCATION = IntField.range(4, 5);
private static final IntField WORD = IntField.range(0, 8);
public Binary40_9_16()
{
for(int x = 0; x < 512; x++)
{
BinaryMessage message = new BinaryMessage(9);
message.load(0, 9, x);
int channelNumber = message.getInt(CHANNEL_NUMBER) + 1;
int ischLocation = message.getInt(ISCH_LOCATION);
long codeword = getCodeWord(message);
codeword ^= CODE_WORD_OFFSET;
if(ischLocation != 3)
{
if(channelNumber == 1)
{
CODEWORD_MAP_TS1.put(codeword, message);
}
else if(channelNumber == 2)
{
CODEWORD_MAP_TS2.put(codeword, message);
}
}
}
List<Long> keys = new ArrayList<>(CODEWORD_MAP_TS1.keySet());
Collections.sort(keys);
for(Long key: keys)
{
BinaryMessage message = CODEWORD_MAP_TS1.get(key);
int distance = getDistance(key, keys);
}
List<Long> keys2 = new ArrayList<>(CODEWORD_MAP_TS2.keySet());
Collections.sort(keys2);
for(Long key: keys2)
{
BinaryMessage message = CODEWORD_MAP_TS2.get(key);
int distance = getDistance(key, keys);
}
}
/**
* Calculates the closest distance between the value and the values in the value set.
* @param value to test
* @param valueSet to test against
* @return closest distance of the value to the values in the value set, ignoring any equivalent values.
*/
public static int getDistance(long value, List<Long> valueSet)
{
int distance = 40;
for(Long candidate: valueSet)
{
if(candidate != value)
{
long syndrome = candidate ^ value;
int candidateDistance = Long.bitCount(syndrome);
if(candidateDistance < distance)
{
distance = candidateDistance;
}
}
}
return distance;
}
/**
* Creates a codeword from the binary message where each set bit in the binary message corresponds to one of the
* words in the generator and we XOR the words together.
* @param message for generating a codeword
* @return codeword
*/
public static long getCodeWord(BinaryMessage message)
{
long codeword = 0;
for(int x = 0; x < message.length(); x++)
{
if(message.get(x))
{
codeword ^= CHECKSUMS[x];
}
}
return codeword;
}
/**
* Calculates the checksum (ie codeword) from the set bits in the message.
* @param message with set bits, 0-8
* @return
*/
private static long calculateChecksum(BinaryMessage message)
{
long calculated = 0; //Starting value
/* Iterate the set bits and XOR running checksum with lookup value */
for(int i = message.nextSetBit(0); i < 9; i = message.nextSetBit(i + 1))
{
calculated ^= CHECKSUMS[i];
}
return calculated;
}
// /**
// * Performs error detection and returns a corrected copy of the 24-bit
// * message that starts at the start index.
// *
// * @param message - source message containing startIndex + 24 bits length
// * @return - corrected 24-bit galois value
// */
// public static int checkAndCorrect(CorrectedBinaryMessage message)
// {
// boolean parityError = message.cardinality() % 2 != 0;
//
// long syndrome = getSyndrome(message);
//
// /* No errors */
// if(syndrome == 0)
// {
// if(parityError)
// {
// message.flip(23);
// message.incrementCorrectedBitCount(1);
// return 1;
// }
//
// return 0;
// }
//
// /* Get original message value */
// int original = message.getInt(0, 22);
//
// int index = -1;
// int syndromeWeight = 3;
// int errors = 0;
//
// while(index < 23)
// {
// if(index != -1)
// {
// /* restore the previous flipped bit */
// if(index > 0)
// {
// message.flip(index - 1);
// }
//
// message.flip(index);
//
// syndromeWeight = 2;
// }
//
// syndrome = getSyndrome(message);
//
// if(syndrome > 0)
// {
// for(int i = 0; i < 23; i++)
// {
//
// errors = Long.bitCount(syndrome);
//
// if(errors <= syndromeWeight)
// {
// message.xor(12, 11, syndrome);
//
// message.rotateRight(i, 0, 22);
//
// if(index >= 0)
// {
// errors++;
// }
//
// int corrected = message.getInt(0, 22);
//
// if(Integer.bitCount(original ^ corrected) > 3)
// {
// return 2;
// }
//
// return 1;
// }
// else
// {
// message.rotateLeft(0, 22);
// syndrome = getSyndrome(message);
// }
// }
//
// index++;
// }
// }
//
// return 2;
// }
private static long getSyndrome(BinaryMessage message)
{
long calculated = calculateChecksum(message);
long checksum = message.getInt(12, 22);
return (checksum ^ calculated);
}
public static void main(String[] args)
{
// Binary40_9_16 edac = new Binary40_9_16();
long mask = 0xFFFFFFFFFFl;
long sync = 0x575D57F7FFl;
long syncLeft = Long.rotateLeft(sync, 2) & mask;
long syncRight = Long.rotateRight(sync, 2) & mask;
System.out.println("Mask: " + Long.toBinaryString(sync));
System.out.println("Left: " + Long.toBinaryString(syncLeft));
System.out.println("Right: " + Long.toBinaryString(syncRight));
int distanceLeft = Long.bitCount(sync ^ syncLeft);
int distanceRight = Long.bitCount( sync ^ syncRight);
System.out.println("Left: " + distanceLeft);
System.out.println("Right: " + distanceRight);
System.out.println("Finished!");
// CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("F3BB20"));
// CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("F0C5C0"));
// CorrectedBinaryMessage bm = new CorrectedBinaryMessage(BinaryMessage.loadHex("AFAC00"));
//
// System.out.println("M:" + bm.toHexString());
// int a = Binary40_9_16.checkAndCorrect(bm);
// System.out.println("M:" + bm.toHexString());
//
// System.out.println("A:" + a);
}
}

View File

@@ -1,22 +1,26 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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 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.
* 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/>
*
******************************************************************************/
* 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.edac;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -216,6 +220,63 @@ public class CRCP25
0x20000000l, 0x40000000l, 0x80000000l
};
/**
* CRC-12 checksums for P25 Phase 2 MAC PDU Contents. This is used for a 144-bit message and 12-bits of checksum
* using a polynomial of 0x1897 and generated by:
* <p>
* long[] checksums = CRCUtil.generate(144, 12, 0x1897l, 0x0l, false);
*/
private static int[] CRC_12_FACCH = new int[]{0x256, 0x12B, 0xCDE, 0x66F, 0xF7C, 0x7BE, 0x3DF, 0xDA4, 0x6D2, 0x369,
0xDFF, 0xAB4, 0x55A, 0x2AD, 0xD1D, 0xAC5, 0x929, 0x8DF, 0x824, 0x412, 0x209, 0xD4F, 0xAEC, 0x576, 0x2BB,
0xD16, 0x68B, 0xF0E, 0x787, 0xF88, 0x7C4, 0x3E2, 0x1F1, 0xCB3, 0xA12, 0x509, 0xECF, 0xB2C, 0x596, 0x2CB,
0xD2E, 0x697, 0xF00, 0x780, 0x3C0, 0x1E0, 0xF0, 0x78, 0x3C, 0x1E, 0xF, 0xC4C, 0x626, 0x313, 0xDC2, 0x6E1,
0xF3B, 0xBD6, 0x5EB, 0xEBE, 0x75F, 0xFE4, 0x7F2, 0x3F9, 0xDB7, 0xA90, 0x548, 0x2A4, 0x152, 0xA9, 0xC1F,
0xA44, 0x522, 0x291, 0xD03, 0xACA, 0x565, 0xEF9, 0xB37, 0x9D0, 0x4E8, 0x274, 0x13A, 0x9D, 0xC05, 0xA49,
0x96F, 0x8FC, 0x47E, 0x23F, 0xD54, 0x6AA, 0x355, 0xDE1, 0xABB, 0x916, 0x48B, 0xE0E, 0x707, 0xFC8, 0x7E4,
0x3F2, 0x1F9, 0xCB7, 0xA10, 0x508, 0x284, 0x142, 0xA1, 0xC1B, 0xA46, 0x523, 0xEDA, 0x76D, 0xFFD, 0xBB5,
0x991, 0x883, 0x80A, 0x405, 0xE49, 0xB6F, 0x9FC, 0x4FE, 0x27F, 0xD74, 0x6BA, 0x35D, 0xDE5, 0xAB9, 0x917,
0x8C0, 0x460, 0x230, 0x118, 0x8C, 0x46, 0x23, 0xC5A, 0x62D, 0xF5D, 0xBE5, 0x9B9, 0x897};
/**
* CRC-12 checksums for P25 Phase 2 MAC PDU Contents. This is used for a 168-bit message and 12-bits of checksum
* using a polynomial of 0x1897 and generated by:
* <p>
* long[] checksums = CRCUtil.generate(168, 12, 0x1897l, 0x0l, false);
*/
private static int[] CRC_12_SACCH = new int[]{0x47B, 0xE76, 0x73B, 0xFD6, 0x7EB, 0xFBE, 0x7DF, 0xFA4, 0x7D2, 0x3E9,
0xDBF, 0xA94, 0x54A, 0x2A5, 0xD19, 0xAC7, 0x928, 0x494, 0x24A, 0x125, 0xCD9, 0xA27, 0x958, 0x4AC, 0x256,
0x12B, 0xCDE, 0x66F, 0xF7C, 0x7BE, 0x3DF, 0xDA4, 0x6D2, 0x369, 0xDFF, 0xAB4, 0x55A, 0x2AD, 0xD1D, 0xAC5,
0x929, 0x8DF, 0x824, 0x412, 0x209, 0xD4F, 0xAEC, 0x576, 0x2BB, 0xD16, 0x68B, 0xF0E, 0x787, 0xF88, 0x7C4,
0x3E2, 0x1F1, 0xCB3, 0xA12, 0x509, 0xECF, 0xB2C, 0x596, 0x2CB, 0xD2E, 0x697, 0xF00, 0x780, 0x3C0, 0x1E0,
0xF0, 0x78, 0x3C, 0x1E, 0xF, 0xC4C, 0x626, 0x313, 0xDC2, 0x6E1, 0xF3B, 0xBD6, 0x5EB, 0xEBE, 0x75F, 0xFE4,
0x7F2, 0x3F9, 0xDB7, 0xA90, 0x548, 0x2A4, 0x152, 0xA9, 0xC1F, 0xA44, 0x522, 0x291, 0xD03, 0xACA, 0x565,
0xEF9, 0xB37, 0x9D0, 0x4E8, 0x274, 0x13A, 0x9D, 0xC05, 0xA49, 0x96F, 0x8FC, 0x47E, 0x23F, 0xD54, 0x6AA,
0x355, 0xDE1, 0xABB, 0x916, 0x48B, 0xE0E, 0x707, 0xFC8, 0x7E4, 0x3F2, 0x1F9, 0xCB7, 0xA10, 0x508, 0x284,
0x142, 0xA1, 0xC1B, 0xA46, 0x523, 0xEDA, 0x76D, 0xFFD, 0xBB5, 0x991, 0x883, 0x80A, 0x405, 0xE49, 0xB6F,
0x9FC, 0x4FE, 0x27F, 0xD74, 0x6BA, 0x35D, 0xDE5, 0xAB9, 0x917, 0x8C0, 0x460, 0x230, 0x118, 0x8C, 0x46, 0x23,
0xC5A, 0x62D, 0xF5D, 0xBE5, 0x9B9, 0x897,};
/**
* CRC-16 checksums for P25 Phase 2 LCCH MAC PDU Contents. This is used for a 164-bit message and 16-bits of
* checksum using a polynomial of 0x11021 and generated by:
* <p>
* long[] checksums = CRCUtil.generate(164, 16, 0x11021l, 0x0l, true);
*/
private static int[] CRC_16_LCCH = new int[]{0xCF76, 0x67BB, 0xBBCD, 0xD5F6, 0x6AFB, 0xBD6D, 0xD6A6, 0x6B53, 0xBDB9,
0xD6CC, 0x6B66, 0x35B3, 0x92C9, 0xC174, 0x60BA, 0x305D, 0x903E, 0x481F, 0xAC1F, 0xDE1F, 0xE71F, 0xFB9F,
0xF5DF, 0xF2FF, 0xF16F, 0xF0A7, 0xF043, 0xF031, 0xF008, 0x7804, 0x3C02, 0x1E01, 0x8710, 0x4388, 0x21C4,
0x10E2, 0x871, 0x8C28, 0x4614, 0x230A, 0x1185, 0x80D2, 0x4069, 0xA824, 0x5412, 0x2A09, 0x9D14, 0x4E8A,
0x2745, 0x9BB2, 0x4DD9, 0xAEFC, 0x577E, 0x2BBF, 0x9DCF, 0xC6F7, 0xEB6B, 0xFDA5, 0xF6C2, 0x7B61, 0xB5A0,
0x5AD0, 0x2D68, 0x16B4, 0xB5A, 0x5AD, 0x8AC6, 0x4563, 0xAAA1, 0xDD40, 0x6EA0, 0x3750, 0x1BA8, 0xDD4, 0x6EA,
0x375, 0x89AA, 0x44D5, 0xAA7A, 0x553D, 0xA28E, 0x5147, 0xA0B3, 0xD849, 0xE434, 0x721A, 0x390D, 0x9496,
0x4A4B, 0xAD35, 0xDE8A, 0x6F45, 0xBFB2, 0x5FD9, 0xA7FC, 0x53FE, 0x29FF, 0x9CEF, 0xC667, 0xEB23, 0xFD81,
0xF6D0, 0x7B68, 0x3DB4, 0x1EDA, 0xF6D, 0x8FA6, 0x47D3, 0xABF9, 0xDDEC, 0x6EF6, 0x377B, 0x93AD, 0xC1C6,
0x60E3, 0xB861, 0xD420, 0x6A10, 0x3508, 0x1A84, 0xD42, 0x6A1, 0x8B40, 0x45A0, 0x22D0, 0x1168, 0x8B4, 0x45A,
0x22D, 0x8906, 0x4483, 0xAA51, 0xDD38, 0x6E9C, 0x374E, 0x1BA7, 0x85C3, 0xCAF1, 0xED68, 0x76B4, 0x3B5A,
0x1DAD, 0x86C6, 0x4363, 0xA9A1, 0xDCC0, 0x6E60, 0x3730, 0x1B98, 0xDCC, 0x6E6, 0x373, 0x89A9, 0xCCC4, 0x6662,
0x3331, 0x9188, 0x48C4, 0x2462, 0x1231, 0x8108, 0x4084, 0x2042, 0x1021, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20,
0x40, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000};
/**
* Performs error detection and single-bit error correction against the
* data blocks of a PDU1 message.
@@ -402,11 +463,8 @@ public class CRCP25
}
int checksum = message.getInt(messageStart + 7, messageStart + 15);
int residual = calculated ^ checksum;
// mLog.debug( "CALC:" + calculated + " CHECK:" + checksum + " RESID:" + residual );
if(residual == 0 || residual == 0x1FF)
{
return CRC.PASSED;
@@ -441,12 +499,80 @@ public class CRCP25
return passes;
}
/**
* Calculates the CRC-12 checksum for the P25 Phase 2 MAC PDU Contents for S-OEMI/FACCH and compares it to the
* transmitted checksum to verify that the message is correct.
* Note: this uses the pre-calculated checksums in the CRC_12_FACCH array.
*
* @param message to verify
* @return true if the message passes the CRC-12 check.
*/
public static boolean crc12_FACCH(CorrectedBinaryMessage message)
{
int calculated = 0xFFF; //Initial fill of all ones.
//Iterate the set bits and XOR the running checksum with pre-calculated lookup value
for(int i = message.nextSetBit(0); i >= 0 && i < 144; i = message.nextSetBit(i + 1))
{
calculated ^= CRC_12_FACCH[i];
}
int checksum = message.getInt(144, 155); //12-bit transmitted checksum
int residual = calculated ^ checksum;
return residual == 0;
}
/**
* Calculates the CRC-12 checksum for the P25 Phase 2 MAC PDU Contents for I-OEMI/SACCH and compares it to the
* transmitted checksum to verify that the message is correct.
* Note: this uses the pre-calculated checksums in the CRC_12_SACCH array.
*
* @param message to verify
* @return true if the message passes the CRC-12 check.
*/
public static boolean crc12_SACCH(CorrectedBinaryMessage message)
{
int calculated = 0xFFF; //Initial fill of all ones.
//Iterate the set bits and XOR the running checksum with pre-calculated lookup value
for(int i = message.nextSetBit(0); i >= 0 && i < 168; i = message.nextSetBit(i + 1))
{
calculated ^= CRC_12_SACCH[i];
}
int checksum = message.getInt(168, 179); //12-bit transmitted checksum
int residual = calculated ^ checksum;
return residual == 0;
}
/**
* Calculates the CRC-16 checksum for the P25 Phase 2 MAC PDU Contents for I-OECI/LCCH and compares it to the
* transmitted checksum to verify that the message is correct.
* Note: this uses the pre-calculated checksums in the CRC_16_LCCH array.
*
* @param message to verify
* @return true if the message passes the CRC-16 check.
*/
public static boolean crc16_LCCH(CorrectedBinaryMessage message)
{
int calculated = 0xFFFF; //Initial fill of all ones.
//Iterate the set bits and XOR the running checksum with pre-calculated lookup value
for(int i = message.nextSetBit(0); i >= 0 && i < 164; i = message.nextSetBit(i + 1))
{
calculated ^= CRC_16_LCCH[i];
}
int checksum = message.getInt(164, 179); //16-bit transmitted checksum
int residual = calculated ^ checksum;
return residual == 0;
}
/**
* Calculates the value of the message checksum as a long
*/
public static long getLongChecksum(BinaryMessage message,
int crcStart, int crcLength)
public static long getLongChecksum(BinaryMessage message, int crcStart, int crcLength)
{
return message.getLong(crcStart, crcStart + crcLength - 1);
}
@@ -454,8 +580,7 @@ public class CRCP25
/**
* Calculates the value of the message checksum as an integer
*/
public static int getIntChecksum(BinaryMessage message,
int crcStart, int crcLength)
public static int getIntChecksum(BinaryMessage message, int crcStart, int crcLength)
{
return message.getInt(crcStart, crcStart + crcLength - 1);
}
@@ -494,16 +619,19 @@ public class CRCP25
public static void main(String[] args)
{
String raw = "000000001000001100000001010001111011000100001010010001111100000000000101000000000000000001000000000000110000000000000001101010101010101010101010";
long[] checksums = CRCUtil.generate(144, 12, 0x1897l, 0x0l, false);
System.out.println("Checksums:" + Arrays.toString(checksums));
BinaryMessage message = BinaryMessage.load(raw);
String hex12 = "7C0000000000000000000000000000000000CA7";
mLog.debug("MSG:" + message.toString());
CRC results = checkCRC9(message, 0);
mLog.debug("COR:" + message.toString());
mLog.debug("Results: " + results.getDisplayText());
// BinaryMessage message = BinaryMessage.load(raw);
//
// mLog.debug("MSG:" + message.toString());
//
// CRC results = checkCRC9(message, 0);
//
// mLog.debug("COR:" + message.toString());
//
// mLog.debug("Results: " + results.getDisplayText());
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -19,6 +19,7 @@
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,8 +33,7 @@ public class CRCUtil
long initialFill,
boolean includeCRCBitErrors)
{
long[] crcTable = new long[messageSize +
(includeCRCBitErrors ? crcSize : 0)];
long[] crcTable = new long[messageSize + (includeCRCBitErrors ? crcSize : 0)];
int[] checksumIndexes = new int[crcSize];
@@ -132,20 +132,12 @@ public class CRCUtil
* @return message with all message bits zeroed out, and the remainder
* placed in the crc field which starts at index messageLength
*/
public static BinaryMessage decode(BinaryMessage message,
int messageStart,
int messageSize,
long polynomial,
int crcSize)
public static BinaryMessage decode(BinaryMessage message, int messageStart, int messageSize, long polynomial, int crcSize)
{
for(int i = message.nextSetBit(messageStart);
i >= messageStart && i < messageSize;
i = message.nextSetBit(i + 1))
for(int i = message.nextSetBit(messageStart); i >= messageStart && i < messageSize; i = message.nextSetBit(i + 1))
{
BinaryMessage polySet = new BinaryMessage(crcSize + i + 1);
polySet.load(i, crcSize + 1, polynomial);
message.xor(polySet);
System.out.println(message.toString());
}
@@ -226,8 +218,11 @@ public class CRCUtil
{
mLog.debug("Starting");
long poly = 0x13l;
long[] checksums = generate(32, 4, poly, 0, true);
int messageSize = 164;
int crcSize = 16;
long poly = 0x11021l;
long initialFill = 0x0l;
long[] checksums = CRCUtil.generate(messageSize, crcSize, poly, initialFill, true);
StringBuilder sb = new StringBuilder();
sb.append("private static int[] CHECKSUMS = new int[]{");
@@ -238,7 +233,29 @@ public class CRCUtil
}
sb.append("};");
System.out.println("Checksums:\n" + sb);
String hex = "7CFC0039420170452A4F9970000000000000000000D05";
CorrectedBinaryMessage message = new CorrectedBinaryMessage(BinaryMessage.loadHex(hex));
int calculated = 0xFFF; //Initial fill of all ones
int messageStart = 0;
/* Iterate the set bits and XOR running checksum with lookup value */
for(int i = message.nextSetBit(messageStart);
i >= messageStart && i < messageSize;
i = message.nextSetBit(i + 1))
{
System.out.println("Bit [" + i + "] is set");
calculated ^= checksums[i];
}
int checksum = message.getInt(messageSize, messageSize + crcSize - 1);
int residual = calculated ^ checksum;
mLog.debug("CALC:" + Integer.toHexString(calculated).toUpperCase() +
" CHECK:" + Integer.toHexString(checksum).toUpperCase() +
" RESIDUAL:" + Integer.toHexString(residual).toUpperCase());
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -35,7 +35,7 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.RecordingViewer;
import io.github.dsheirer.gui.viewer.MessageRecordingViewer;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor;
@@ -84,7 +84,7 @@ public class JavaFxWindowManager extends Application
private TunerManager mTunerManager;
private UserPreferences mUserPreferences;
private UserPreferencesEditor mUserPreferencesEditor;
private RecordingViewer mRecordingViewer;
private MessageRecordingViewer mMessageRecordingViewer;
private Stage mChannelMapStage;
private Stage mIconManagerStage;
@@ -216,14 +216,14 @@ public class JavaFxWindowManager extends Application
return mRecordingViewerStage;
}
public RecordingViewer getRecordingViewer()
public MessageRecordingViewer getRecordingViewer()
{
if(mRecordingViewer == null)
if(mMessageRecordingViewer == null)
{
mRecordingViewer = new RecordingViewer();
mMessageRecordingViewer = new MessageRecordingViewer();
}
return mRecordingViewer;
return mMessageRecordingViewer;
}
public Stage getIconManagerStage()

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -647,11 +647,13 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
public NewP25P2ChannelMenu()
{
setText(DecoderType.P25_PHASE2.getDisplayString());
MenuItem trunked = new MenuItem("Trunked System");
trunked.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE1));
MenuItem channel = new MenuItem("Individual Channel");
MenuItem trunkedP1 = new MenuItem("Trunked System - FDMA Phase 1 Control Channel");
trunkedP1.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE1));
MenuItem trunkedP2 = new MenuItem("Trunked System - TDMA Phase 2 Control Channel");
trunkedP2.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE2));
MenuItem channel = new MenuItem("Individual Phase 2 Channel");
channel.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE2));
getItems().addAll(trunked, channel);
getItems().addAll(trunkedP1, trunkedP2, channel);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -113,7 +113,7 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
if(mDecoderPane == null)
{
mDecoderPane = new TitledPane();
mDecoderPane.setText("Decoder: P25 Phase 1 (or P25 Phase 2 Control Channel)");
mDecoderPane.setText("Decoder: P25 Phase 1 (also used for P25 Phase 2 system with FDMA control channels)");
mDecoderPane.setExpanded(true);
GridPane gridPane = new GridPane();
@@ -355,9 +355,9 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
{
DecodeConfigP25Phase1 config;
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase1)
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase1 p1)
{
config = (DecodeConfigP25Phase1)getItem().getDecodeConfiguration();
config = p1;
}
else
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -42,9 +42,13 @@ import java.util.List;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -64,6 +68,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
private IntegerTextField mWacnTextField;
private IntegerTextField mSystemTextField;
private IntegerTextField mNacTextField;
private ToggleSwitch mIgnoreDataCallsButton;
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
/**
* Constructs an instance
@@ -102,7 +108,7 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
if(mDecoderPane == null)
{
mDecoderPane = new TitledPane();
mDecoderPane.setText("Decoder: P25 Phase 2");
mDecoderPane.setText("Decoder: P25 Phase 2 for TDMA control or TDMA traffic channels.");
mDecoderPane.setExpanded(true);
GridPane gridPane = new GridPane();
@@ -112,6 +118,22 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
int row = 0;
Label poolSizeLabel = new Label("Max Traffic Channels");
GridPane.setHalignment(poolSizeLabel, HPos.RIGHT);
GridPane.setConstraints(poolSizeLabel, 0, row);
gridPane.getChildren().add(poolSizeLabel);
GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 1, row);
gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner());
GridPane.setConstraints(getIgnoreDataCallsButton(), 2, row);
gridPane.getChildren().add(getIgnoreDataCallsButton());
Label directionLabel = new Label("Ignore Data Calls");
GridPane.setHalignment(directionLabel, HPos.LEFT);
GridPane.setConstraints(directionLabel, 3, row);
gridPane.getChildren().add(directionLabel);
Label wacnLabel = new Label("WACN");
GridPane.setHalignment(wacnLabel, HPos.RIGHT);
GridPane.setConstraints(wacnLabel, 0, ++row);
@@ -122,20 +144,27 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
Label systemLabel = new Label("System");
GridPane.setHalignment(systemLabel, HPos.RIGHT);
GridPane.setConstraints(systemLabel, 0, ++row);
GridPane.setConstraints(systemLabel, 2, row);
gridPane.getChildren().add(systemLabel);
GridPane.setConstraints(getSystemTextField(), 1, row);
GridPane.setConstraints(getSystemTextField(), 3, row);
gridPane.getChildren().add(getSystemTextField());
Label nacLabel = new Label("NAC");
GridPane.setHalignment(nacLabel, HPos.RIGHT);
GridPane.setConstraints(nacLabel, 0, ++row);
GridPane.setConstraints(nacLabel, 4, row);
gridPane.getChildren().add(nacLabel);
GridPane.setConstraints(getNacTextField(), 1, row);
GridPane.setConstraints(getNacTextField(), 5, row);
gridPane.getChildren().add(getNacTextField());
Label noteLabel = new Label("Note: WACN/System/NAC values are auto-detected (ie not required) from " +
"the control channel and are only required when decoding individual traffic channels");
GridPane.setHalignment(noteLabel, HPos.LEFT);
GridPane.setConstraints(noteLabel, 1, ++row, 6, 1);
gridPane.getChildren().add(noteLabel);
mDecoderPane.setContent(gridPane);
}
@@ -203,6 +232,37 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
return mEventLogConfigurationEditor;
}
private ToggleSwitch getIgnoreDataCallsButton()
{
if(mIgnoreDataCallsButton == null)
{
mIgnoreDataCallsButton = new ToggleSwitch();
mIgnoreDataCallsButton.setDisable(true);
mIgnoreDataCallsButton.selectedProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mIgnoreDataCallsButton;
}
private Spinner<Integer> getTrafficChannelPoolSizeSpinner()
{
if(mTrafficChannelPoolSizeSpinner == null)
{
mTrafficChannelPoolSizeSpinner = new Spinner();
mTrafficChannelPoolSizeSpinner.setDisable(true);
mTrafficChannelPoolSizeSpinner.setTooltip(
new Tooltip("Maximum number of traffic channels that can be created by the decoder"));
mTrafficChannelPoolSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
SpinnerValueFactory<Integer> svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 50);
mTrafficChannelPoolSizeSpinner.setValueFactory(svf);
mTrafficChannelPoolSizeSpinner.getValueFactory().valueProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mTrafficChannelPoolSizeSpinner;
}
private IntegerTextField getWacnTextField()
{
if(mWacnTextField == null)
@@ -247,6 +307,9 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
types.add(RecorderType.BASEBAND);
types.add(RecorderType.DEMODULATED_BIT_STREAM);
types.add(RecorderType.MBE_CALL_SEQUENCE);
types.add(RecorderType.TRAFFIC_BASEBAND);
types.add(RecorderType.TRAFFIC_DEMODULATED_BIT_STREAM);
types.add(RecorderType.TRAFFIC_MBE_CALL_SEQUENCE);
mRecordConfigurationEditor = new RecordConfigurationEditor(types);
mRecordConfigurationEditor.setDisable(true);
mRecordConfigurationEditor.modifiedProperty()
@@ -259,9 +322,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
@Override
protected void setDecoderConfiguration(DecodeConfiguration config)
{
if(config instanceof DecodeConfigP25Phase2)
if(config instanceof DecodeConfigP25Phase2 decodeConfig)
{
DecodeConfigP25Phase2 decodeConfig = (DecodeConfigP25Phase2)config;
getWacnTextField().setDisable(false);
getSystemTextField().setDisable(false);
getNacTextField().setDisable(false);
@@ -280,6 +342,11 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
getSystemTextField().set(0);
getNacTextField().set(0);
}
getIgnoreDataCallsButton().setDisable(false);
getIgnoreDataCallsButton().setSelected(decodeConfig.getIgnoreDataCalls());
getTrafficChannelPoolSizeSpinner().setDisable(false);
getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(decodeConfig.getTrafficChannelPoolSize());
}
else
{
@@ -289,6 +356,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
getWacnTextField().setDisable(true);
getSystemTextField().setDisable(true);
getNacTextField().setDisable(true);
getIgnoreDataCallsButton().setDisable(true);
getTrafficChannelPoolSizeSpinner().setDisable(true);
}
}
@@ -297,20 +366,23 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
{
DecodeConfigP25Phase2 config;
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase2)
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase2 p2)
{
config = (DecodeConfigP25Phase2)getItem().getDecodeConfiguration();
config.setAutoDetectScrambleParameters(false);
int wacn = getWacnTextField().get();
int system = getSystemTextField().get();
int nac = getNacTextField().get();
config.setScrambleParameters(new ScrambleParameters(wacn, system, nac));
config = p2;
}
else
{
config = new DecodeConfigP25Phase2();
}
config.setAutoDetectScrambleParameters(false);
int wacn = getWacnTextField().get();
int system = getSystemTextField().get();
int nac = getNacTextField().get();
config.setScrambleParameters(new ScrambleParameters(wacn, system, nac));
config.setIgnoreDataCalls(getIgnoreDataCallsButton().isSelected());
config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
getItem().setDecodeConfiguration(config);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -25,13 +25,15 @@ import io.github.dsheirer.rrapi.type.Site;
/**
* Wrapper class to join a Site and a corresponding County Info
*/
public class EnrichedSite
public class EnrichedSite implements Comparable<EnrichedSite>
{
private static final String PHASE_2_TDMA_MODULATION = "TDMA";
private Site mSite;
private CountyInfo mCountyInfo;
/**
* Constructs an instance
*
* @param site object
* @param countyInfo optional
*/
@@ -41,8 +43,16 @@ public class EnrichedSite
mCountyInfo = countyInfo;
}
public static String format(int value)
{
StringBuilder sb = new StringBuilder();
sb.append(value).append(" (").append(Integer.toHexString(value).toUpperCase()).append(")");
return sb.toString();
}
/**
* Site instance
*
* @return site or null
*/
public Site getSite()
@@ -52,6 +62,7 @@ public class EnrichedSite
/**
* Sets the site instance
*
* @param site or null
*/
public void setSite(Site site)
@@ -61,6 +72,7 @@ public class EnrichedSite
/**
* County information
*
* @return county info or null
*/
public CountyInfo getCountyInfo()
@@ -70,6 +82,7 @@ public class EnrichedSite
/**
* Sets the county info
*
* @param countyInfo or null
*/
public void setCountyInfo(CountyInfo countyInfo)
@@ -78,13 +91,26 @@ public class EnrichedSite
}
/**
* Optional site number
* Formatted system identity
*
* @return
*/
public Integer getSiteNumber()
public String getSystemFormatted()
{
if(mSite != null)
{
return mSite.getSiteNumber();
//System number is stored in the zone number field.
return format(mSite.getZoneNumber());
}
return null;
}
public String getSiteFormatted()
{
if(mSite != null)
{
return format(mSite.getSiteNumber());
}
return null;
@@ -93,11 +119,11 @@ public class EnrichedSite
/**
* Optional site RFSS value
*/
public Integer getRfss()
public String getRfssFormatted()
{
if(mSite != null)
{
return mSite.getRfss();
return format(mSite.getRfss());
}
return null;
@@ -128,4 +154,23 @@ public class EnrichedSite
return null;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
String system = getSystemFormatted();
sb.append(system == null ? "-" : system).append(" ");
String rfss = getRfssFormatted();
sb.append(rfss == null ? "-" : rfss).append(" ");
String site = getSiteFormatted();
sb.append(site == null ? "-" : site);
return sb.toString();
}
@Override
public int compareTo(EnrichedSite o)
{
return this.toString().compareTo(o.toString());
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -449,6 +449,16 @@ public class RadioReferenceDecoder
* @return decoder type or null.
*/
public DecoderType getDecoderType(System system)
{
return getDecoderType(system, null);
}
/**
* Decoder type for the specified system, if supported.
* @param system requiring a decoder type
* @return decoder type or null.
*/
public DecoderType getDecoderType(System system, Site site)
{
Type type = getType(system);
Flavor flavor = getFlavor(system);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.rrapi.type.Flavor;
import io.github.dsheirer.rrapi.type.RadioNetwork;
import io.github.dsheirer.rrapi.type.Site;
import io.github.dsheirer.rrapi.type.SiteFrequency;
@@ -42,6 +43,13 @@ import io.github.dsheirer.rrapi.type.System;
import io.github.dsheirer.rrapi.type.SystemInformation;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import javafx.animation.RotateTransition;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
@@ -74,14 +82,6 @@ import org.controlsfx.control.SegmentedButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
public class SiteEditor extends GridPane
{
private static final Logger mLog = LoggerFactory.getLogger(SiteEditor.class);
@@ -90,6 +90,8 @@ public class SiteEditor extends GridPane
private static final String PRIMARY_CONTROL_CHANNEL = "d";
private static final String TOGGLE_BUTTON_CONTROL = "Control";
private static final String TOGGLE_BUTTON_P25_VOICE = "All P25 Voice";
private static final String PHASE_2_TDMA_MODULATION = "TDMA";
private static final String PHASE_2_FLAVOR = "Phase II";
private UserPreferences mUserPreferences;
private PlaylistManager mPlaylistManager;
@@ -115,6 +117,10 @@ public class SiteEditor extends GridPane
private SystemInformation mCurrentSystemInformation;
private ComboBox mAliasListNameComboBox;
private Button mNewAliasListButton;
private Label mP25ControlLabel;
private SegmentedButton mP25ControlSegmentedButton;
private ToggleButton mFdmaControlToggleButton;
private ToggleButton mTdmaControlToggleButton;
public SiteEditor(UserPreferences userPreferences, PlaylistManager playlistManager)
{
@@ -163,6 +169,13 @@ public class SiteEditor extends GridPane
GridPane.setConstraints(getConfigurationsSegmentedButton(), 2, row);
getChildren().add(getConfigurationsSegmentedButton());
GridPane.setConstraints(getP25ControlLabel(), 1, ++row);
GridPane.setHalignment(getP25ControlLabel(), HPos.RIGHT);
getChildren().add(getP25ControlLabel());
GridPane.setConstraints(getP25ControlSegmentedButton(), 2, row);
getChildren().add(getP25ControlSegmentedButton());
Label systemLabel = new Label("System");
GridPane.setConstraints(systemLabel, 1, ++row);
GridPane.setHalignment(systemLabel, HPos.RIGHT);
@@ -206,7 +219,7 @@ public class SiteEditor extends GridPane
createBox.setAlignment(Pos.CENTER_LEFT);
createBox.setSpacing(10);
createBox.getChildren().addAll(getCreateChannelConfigurationButton(), getGoToChannelEditorCheckBox());
GridPane.setConstraints(createBox, 2, ++row + 1);
GridPane.setConstraints(createBox, 2, ++row);
getChildren().addAll(createBox);
//Note: the following label node is added to the same location as the buttons and visibility is toggled
@@ -215,6 +228,13 @@ public class SiteEditor extends GridPane
getChildren().add(getProtocolNotSupportedLabel());
}
/**
* Creates a channel decode configuration for the specified site.
* @param decoderType to create
* @param site information and frequencies
* @param systemInformation with frequency mapping
* @return decode configuration
*/
private DecodeConfiguration getDecodeConfiguration(DecoderType decoderType, Site site, SystemInformation systemInformation)
{
if(decoderType == null)
@@ -298,7 +318,13 @@ public class SiteEditor extends GridPane
}
}
/**
* Loads the user selected site info into this editor.
* @param site to load
* @param system for the site
* @param systemInformation for the site
* @param decoder to lookup supplemental information
*/
public void setSite(EnrichedSite site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
{
mCurrentSite = site;
@@ -346,6 +372,29 @@ public class SiteEditor extends GridPane
}
else
{
Flavor flavor = decoder.getFlavor(system);
if(flavor != null && flavor.getName() != null && flavor.getName().equals(PHASE_2_FLAVOR))
{
getP25ControlLabel().setVisible(true);
getTdmaControlToggleButton().setVisible(true);
getFdmaControlToggleButton().setVisible(true);
if(site.getSite().getModulation() != null && site.getSite().getModulation().contains(PHASE_2_TDMA_MODULATION))
{
getTdmaControlToggleButton().setSelected(true);
}
else
{
getFdmaControlToggleButton().setSelected(true);
}
}
else
{
getP25ControlLabel().setVisible(false);
getTdmaControlToggleButton().setVisible(false);
getFdmaControlToggleButton().setVisible(false);
}
getCreateChannelConfigurationButton().setVisible(true);
getGoToChannelEditorCheckBox().setVisible(true);
getSystemTextField().setText(system.getName());
@@ -514,10 +563,13 @@ public class SiteEditor extends GridPane
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
//Change a phase 2 system to use the phase 1 control channel
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
if(decoderType == DecoderType.P25_PHASE2)
{
decoderType = DecoderType.P25_PHASE1;
if(getFdmaControlToggleButton().isSelected())
{
decoderType = DecoderType.P25_PHASE1;
}
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(), mCurrentSystemInformation));
@@ -587,10 +639,13 @@ public class SiteEditor extends GridPane
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
//Change a phase 2 system to use the phase 1 control channel
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
if(decoderType == DecoderType.P25_PHASE2)
{
decoderType = DecoderType.P25_PHASE1;
if(getFdmaControlToggleButton().isSelected())
{
decoderType = DecoderType.P25_PHASE1;
}
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
@@ -685,6 +740,16 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
if(decoderType == DecoderType.P25_PHASE2)
{
if(getFdmaControlToggleButton().isSelected())
{
decoderType = DecoderType.P25_PHASE1;
}
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
@@ -750,6 +815,70 @@ public class SiteEditor extends GridPane
return (long)(siteFrequency.getFrequency() * 1E6);
}
/**
* P25 Phase 2 control channel label.
*/
private Label getP25ControlLabel()
{
if(mP25ControlLabel == null)
{
mP25ControlLabel = new Label("Control");
mP25ControlLabel.setVisible(false);
}
return mP25ControlLabel;
}
/**
* P25 Phase 2 control channel type (TDMA vs FDMA) selection buttons.
*/
private SegmentedButton getP25ControlSegmentedButton()
{
if(mP25ControlSegmentedButton == null)
{
mP25ControlSegmentedButton = new SegmentedButton(getFdmaControlToggleButton(), getTdmaControlToggleButton());
mP25ControlSegmentedButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
mP25ControlSegmentedButton.getToggleGroup().selectedToggleProperty()
.addListener((observable, oldValue, newValue) -> {
//Ensure that one button is always selected
if(newValue == null)
{
oldValue.setSelected(true);
}
});
}
return mP25ControlSegmentedButton;
}
/**
* P25 Phase 2 with FDMA (phase 1) control channel selection button.
*/
private ToggleButton getFdmaControlToggleButton()
{
if(mFdmaControlToggleButton == null)
{
mFdmaControlToggleButton = new ToggleButton("FDMA Phase 1");
mFdmaControlToggleButton.setVisible(false);
}
return mFdmaControlToggleButton;
}
/**
* P25 Phase 2 with TDMA (phase 2) control channel selection button.
*/
private ToggleButton getTdmaControlToggleButton()
{
if(mTdmaControlToggleButton == null)
{
mTdmaControlToggleButton = new ToggleButton("TDMA Phase 2");
mTdmaControlToggleButton.setVisible(false);
}
return mTdmaControlToggleButton;
}
/**
* Flashes the alias list combobox to let the user know that they must select an alias list
*/

View File

@@ -22,6 +22,7 @@
package io.github.dsheirer.gui.playlist.radioreference;
import com.google.common.collect.Ordering;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.rrapi.type.Flavor;
@@ -42,6 +43,8 @@ import javafx.scene.layout.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -126,6 +129,7 @@ public class SystemSiteSelectionEditor extends GridPane
getFlavorLabel().setText(flavor != null ? flavor.getName() : null);
Voice voice = decoder.getVoice(system);
getVoiceLabel().setText(voice != null ? voice.getName() : null);
Collections.sort(sites, Ordering.natural());
getSiteTableView().getItems().addAll(sites);
setLoading(false);
}
@@ -212,27 +216,33 @@ public class SystemSiteSelectionEditor extends GridPane
mSiteTableView = new TableView<>();
mSiteTableView.setPlaceholder(getPlaceholderLabel());
TableColumn numberColumn = new TableColumn();
numberColumn.setText("Site");
numberColumn.setCellValueFactory(new PropertyValueFactory<>("siteNumber"));
numberColumn.setPrefWidth(75);
TableColumn systemColumn = new TableColumn();
systemColumn.setText("System");
systemColumn.setCellValueFactory(new PropertyValueFactory<>("systemFormatted"));
systemColumn.setPrefWidth(60);
TableColumn rfssColumn = new TableColumn();
rfssColumn.setText("RFSS");
rfssColumn.setCellValueFactory(new PropertyValueFactory<>("rfss"));
rfssColumn.setCellValueFactory(new PropertyValueFactory<>("rfssFormatted"));
rfssColumn.setPrefWidth(60);
TableColumn countyColumn = new TableColumn();
countyColumn.setText("County");
countyColumn.setCellValueFactory(new PropertyValueFactory<>("countyName"));
countyColumn.setPrefWidth(125);
TableColumn siteColumn = new TableColumn();
siteColumn.setText("Site");
siteColumn.setCellValueFactory(new PropertyValueFactory<>("siteFormatted"));
siteColumn.setPrefWidth(75);
TableColumn countyNameColumn = new TableColumn();
countyNameColumn.setText("County");
countyNameColumn.setCellValueFactory(new PropertyValueFactory<>("countyName"));
countyNameColumn.setPrefWidth(125);
TableColumn descriptionColumn = new TableColumn();
descriptionColumn.setText("Name");
descriptionColumn.setCellValueFactory(new PropertyValueFactory<>("description"));
descriptionColumn.setPrefWidth(400);
mSiteTableView.getColumns().addAll(numberColumn, rfssColumn, countyColumn, descriptionColumn);
mSiteTableView.getColumns().addAll(systemColumn, rfssColumn, siteColumn, countyNameColumn,
descriptionColumn);
mSiteTableView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
mSiteTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, selected) ->

View File

@@ -0,0 +1,234 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.gui.viewer;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
import javafx.geometry.HPos;
import javafx.scene.control.Label;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
/**
* Viewer for channel start processing requests
*/
public class ChannelStartProcessingRequestViewer extends HBox
{
private Label mChannelConfigLabel;
private Label mChannelDescriptorLabel;
private Label mTrafficChannelManagerLabel;
private Label mPreloadDataContentLabel;
private Label mParentDecodeHistoryLabel;
private Label mChildDecodeHistoryLabel;
private IdentifierCollectionViewer mIdentifierCollectionViewer;
/**
* Constructs an instance
*/
public ChannelStartProcessingRequestViewer()
{
GridPane gridPane = new GridPane();
gridPane.setVgap(5);
gridPane.setHgap(5);
gridPane.add(new Label("Configuration:"), 0, 0);
GridPane.setHgrow(getChannelConfigLabel(), Priority.ALWAYS);
gridPane.add(getChannelConfigLabel(), 1, 0);
gridPane.add(new Label("Channel:"), 0, 1);
GridPane.setHgrow(getChannelDescriptorLabel(), Priority.ALWAYS);
gridPane.add(getChannelDescriptorLabel(), 1, 1);
gridPane.add(new Label("TCM:"), 0, 2);
GridPane.setHgrow(getTrafficChannelManagerLabel(), Priority.ALWAYS);
gridPane.add(getTrafficChannelManagerLabel(), 1, 2);
gridPane.add(new Label("Preload Items:"), 0, 3);
GridPane.setHgrow(getPreloadDataContentLabel(), Priority.ALWAYS);
gridPane.add(getPreloadDataContentLabel(), 1, 3);
gridPane.add(new Label("Parent History:"), 0, 4);
GridPane.setHgrow(getParentDecodeHistoryLabel(), Priority.ALWAYS);
gridPane.add(getParentDecodeHistoryLabel(), 1, 4);
gridPane.add(new Label("Child History:"), 0, 5);
GridPane.setHgrow(getChildDecodeHistoryLabel(), Priority.ALWAYS);
gridPane.add(getChildDecodeHistoryLabel(), 1, 5);
gridPane.add(new Label("Identifiers"), 0, 6);
GridPane.setHgrow(getIdentifierCollectionViewer(), Priority.ALWAYS);
getIdentifierCollectionViewer().setPrefHeight(120);
gridPane.add(getIdentifierCollectionViewer(), 0, 7, 2, 1);
ColumnConstraints cc0 = new ColumnConstraints();
cc0.setHalignment(HPos.RIGHT);
gridPane.getColumnConstraints().add(cc0);
getChildren().add(gridPane);
}
public void set(ChannelStartProcessingRequest request)
{
if(request != null)
{
if(request.getChannel() != null)
{
getChannelConfigLabel().setText(request.getChannel().toString());
}
else
{
getChannelConfigLabel().setText("None");
}
if(request.getChannelDescriptor() != null)
{
getChannelDescriptorLabel().setText(request.getChannelDescriptor().toString() + " " +
request.getChannelDescriptor().getDownlinkFrequency() + " MHz");
}
else
{
getChannelDescriptorLabel().setText("None");
}
if(request.getChildDecodeEventHistory() != null)
{
getChildDecodeHistoryLabel().setText(request.getChildDecodeEventHistory().getItems().size() + " items");
}
else
{
getChildDecodeHistoryLabel().setText("None");
}
if(request.getParentDecodeEventHistory() != null)
{
getParentDecodeHistoryLabel().setText(request.getParentDecodeEventHistory().getItems().size() + " items");
}
else
{
getParentDecodeHistoryLabel().setText("None");
}
if(request.getPreloadDataContents() != null)
{
getPreloadDataContentLabel().setText(request.getPreloadDataContents().size() + " items");
}
else
{
getPreloadDataContentLabel().setText("None");
}
if(request.getTrafficChannelManager() != null)
{
getTrafficChannelManagerLabel().setText(request.getTrafficChannelManager().getClass().getName());
}
else
{
getTrafficChannelManagerLabel().setText("None:");
}
if(request.getIdentifierCollection() != null)
{
getIdentifierCollectionViewer().set(request.getIdentifierCollection());
}
}
else
{
getChannelDescriptorLabel().setText(null);
getChildDecodeHistoryLabel().setText(null);
getChannelConfigLabel().setText(null);
getIdentifierCollectionViewer().set(null);
getParentDecodeHistoryLabel().setText(null);
getPreloadDataContentLabel().setText(null);
getTrafficChannelManagerLabel().setText(null);
}
}
public Label getChannelConfigLabel()
{
if(mChannelConfigLabel == null)
{
mChannelConfigLabel = new Label();
}
return mChannelConfigLabel;
}
public Label getChannelDescriptorLabel()
{
if(mChannelDescriptorLabel == null)
{
mChannelDescriptorLabel = new Label();
}
return mChannelDescriptorLabel;
}
private Label getTrafficChannelManagerLabel()
{
if(mTrafficChannelManagerLabel == null)
{
mTrafficChannelManagerLabel = new Label();
}
return mTrafficChannelManagerLabel;
}
private Label getPreloadDataContentLabel()
{
if(mPreloadDataContentLabel == null)
{
mPreloadDataContentLabel = new Label();
}
return mPreloadDataContentLabel;
}
private Label getParentDecodeHistoryLabel()
{
if(mParentDecodeHistoryLabel == null)
{
mParentDecodeHistoryLabel = new Label();
}
return mParentDecodeHistoryLabel;
}
private Label getChildDecodeHistoryLabel()
{
if(mChildDecodeHistoryLabel == null)
{
mChildDecodeHistoryLabel = new Label();
}
return mChildDecodeHistoryLabel;
}
private IdentifierCollectionViewer getIdentifierCollectionViewer()
{
if(mIdentifierCollectionViewer == null)
{
mIdentifierCollectionViewer = new IdentifierCollectionViewer();
}
return mIdentifierCollectionViewer;
}
}

View File

@@ -0,0 +1,90 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.gui.viewer;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
/**
* JavaFX identifier collection viewer
*/
public class IdentifierCollectionViewer extends VBox
{
private TableView<Identifier> mIdentifierTableView;
/**
* Constructs an instance
*/
public IdentifierCollectionViewer()
{
GridPane gridPane = new GridPane();
GridPane.setHgrow(getIdentifierTableView(), Priority.ALWAYS);
gridPane.add(getIdentifierTableView(), 0, 0);
getChildren().add(gridPane);
}
public void set(IdentifierCollection identifierCollection)
{
getIdentifierTableView().getItems().clear();
if(identifierCollection != null)
{
getIdentifierTableView().getItems().addAll(identifierCollection.getIdentifiers());
}
}
public TableView<Identifier> getIdentifierTableView()
{
if(mIdentifierTableView == null)
{
mIdentifierTableView = new TableView<>();
TableColumn classColumn = new TableColumn();
classColumn.setPrefWidth(110);
classColumn.setText("Class");
classColumn.setCellValueFactory(new PropertyValueFactory<>("identifierClass"));
TableColumn formColumn = new TableColumn();
formColumn.setPrefWidth(160);
formColumn.setText("Form");
formColumn.setCellValueFactory(new PropertyValueFactory<>("form"));
TableColumn roleColumn = new TableColumn();
roleColumn.setPrefWidth(110);
roleColumn.setText("Role");
roleColumn.setCellValueFactory(new PropertyValueFactory<>("role"));
TableColumn valueColumn = new TableColumn();
valueColumn.setPrefWidth(160);
valueColumn.setText("Value");
valueColumn.setCellValueFactory(new PropertyValueFactory<>("value"));
mIdentifierTableView.getColumns().addAll(classColumn, formColumn, roleColumn, valueColumn);
}
return mIdentifierTableView;
}
}

View File

@@ -0,0 +1,159 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.gui.viewer;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import java.util.ArrayList;
import java.util.List;
/**
* Wrapper class that combines a single message with any decoder state events and decoded events that were produced by a
* decoder state.
*/
public class MessagePackage
{
private IMessage mMessage;
private List<DecoderStateEvent> mDecoderStateEvents = new ArrayList<>();
private List<DecodeEventSnapshot> mDecodeEvents = new ArrayList<>();
private ChannelStartProcessingRequest mChannelStartProcessingRequest;
/**
* Constructs an instance
* @param message for this instance
*/
public MessagePackage(IMessage message)
{
mMessage = message;
}
/**
* Message for this instance
*/
public IMessage getMessage()
{
return mMessage;
}
/**
* List of decoder state events
*/
public List<DecoderStateEvent> getDecoderStateEvents()
{
return mDecoderStateEvents;
}
/**
* Adds the decoder state event to this instance
*/
public void add(DecoderStateEvent event)
{
mDecoderStateEvents.add(event);
}
/**
* List of decode event snapshots
*/
public List<DecodeEventSnapshot> getDecodeEvents()
{
return mDecodeEvents;
}
/**
* Adds the decode event to this instance
*/
public void add(DecodeEventSnapshot event)
{
mDecodeEvents.add(event);
}
/**
* Adds the channel start processing request
*/
public void add(ChannelStartProcessingRequest request)
{
mChannelStartProcessingRequest = request;
}
/**
* Message timeslot
*/
public int getTimeslot()
{
return getMessage().getTimeslot();
}
/**
* Message timestamp
*/
public long getTimestamp()
{
return getMessage().getTimestamp();
}
/**
* Message validity flag
*/
public boolean isValid()
{
return getMessage().isValid();
}
/**
* Message string representation
*/
@Override
public String toString()
{
return getMessage().toString();
}
/**
* Count of decode events
*/
public int getDecodeEventCount()
{
return mDecodeEvents.size();
}
/**
* Count of decoder state events
*/
public int getDecoderStateEventCount()
{
return mDecoderStateEvents.size();
}
/**
* Count (0 or 1) of channel start processing requests.
*/
public int getChannelStartProcessingRequestCount()
{
return mChannelStartProcessingRequest == null ? 0 : 1;
}
public ChannelStartProcessingRequest getChannelStartProcessingRequest()
{
return mChannelStartProcessingRequest;
}
}

View File

@@ -0,0 +1,225 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.gui.viewer;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
/**
* JavaFX message package details viewer
*/
public class MessagePackageViewer extends VBox
{
private Label mMessageLabel;
private TableView<DecoderStateEvent> mDecoderStateEventTableView;
private TableView<DecodeEventSnapshot> mDecodeEventTableView;
private IdentifierCollectionViewer mIdentifierCollectionViewer;
private ChannelStartProcessingRequestViewer mChannelStartProcessingRequestViewer;
/**
* Constructs an instance
*/
public MessagePackageViewer()
{
GridPane gridPane = new GridPane();
gridPane.setHgap(5);
gridPane.setVgap(5);
Label messageLabel = new Label("Message:");
gridPane.add(messageLabel, 0, 0);
GridPane.setHgrow(getMessageLabel(), Priority.ALWAYS);
gridPane.add(getMessageLabel(), 1, 0);
gridPane.add(new Label("Decoder State Events"), 0, 1);
getDecoderStateEventTableView().setPrefHeight(120);
GridPane.setHgrow(getDecoderStateEventTableView(), Priority.NEVER);
gridPane.add(getDecoderStateEventTableView(), 0, 2);
gridPane.add(new Label("Decode Events"), 1, 1);
getDecodeEventTableView().setPrefHeight(120);
GridPane.setHgrow(getDecodeEventTableView(), Priority.ALWAYS);
gridPane.add(getDecodeEventTableView(), 1, 2);
gridPane.add(new Label("Channel Start Processing Request"), 0, 3);
gridPane.add(getChannelStartProcessingRequestViewer(), 0, 4);
getIdentifierCollectionViewer().setPrefHeight(120);
gridPane.add(new Label("Selected Decode Event Identifiers"), 1, 3);
gridPane.add(getIdentifierCollectionViewer(), 1, 4);
getChildren().add(gridPane);
}
public void set(MessagePackage messagePackage)
{
getMessageLabel().setText(null);
getDecoderStateEventTableView().getItems().clear();
getDecodeEventTableView().getItems().clear();
getChannelStartProcessingRequestViewer().set(null);
if(messagePackage != null)
{
String message = messagePackage.toString();
if(message.length() > 40)
{
message = message.substring(0, 40) + "...";
}
getMessageLabel().setText(message);
getDecoderStateEventTableView().getItems().addAll(messagePackage.getDecoderStateEvents());
getDecodeEventTableView().getItems().addAll(messagePackage.getDecodeEvents());
getChannelStartProcessingRequestViewer().set(messagePackage.getChannelStartProcessingRequest());
if(getDecodeEventTableView().getItems().size() > 0)
{
getDecodeEventTableView().getSelectionModel().select(0);
}
}
}
private IdentifierCollectionViewer getIdentifierCollectionViewer()
{
if(mIdentifierCollectionViewer == null)
{
mIdentifierCollectionViewer = new IdentifierCollectionViewer();
}
return mIdentifierCollectionViewer;
}
private Label getMessageLabel()
{
if(mMessageLabel == null)
{
mMessageLabel = new Label();
mMessageLabel.setMaxWidth(Double.MAX_VALUE);
}
return mMessageLabel;
}
public TableView<DecoderStateEvent> getDecoderStateEventTableView()
{
if(mDecoderStateEventTableView == null)
{
mDecoderStateEventTableView = new TableView<>();
TableColumn timeslotColumn = new TableColumn();
timeslotColumn.setPrefWidth(110);
timeslotColumn.setText("Timeslot");
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
TableColumn stateColumn = new TableColumn();
stateColumn.setPrefWidth(110);
stateColumn.setText("State");
stateColumn.setCellValueFactory(new PropertyValueFactory<>("state"));
TableColumn eventColumn = new TableColumn();
eventColumn.setPrefWidth(110);
eventColumn.setText("Event");
eventColumn.setCellValueFactory(new PropertyValueFactory<>("event"));
TableColumn frequencyColumn = new TableColumn();
frequencyColumn.setPrefWidth(110);
frequencyColumn.setText("Frequency");
frequencyColumn.setCellValueFactory(new PropertyValueFactory<>("frequency"));
mDecoderStateEventTableView.getColumns().addAll(timeslotColumn, stateColumn, eventColumn, frequencyColumn);
}
return mDecoderStateEventTableView;
}
private TableView<DecodeEventSnapshot> getDecodeEventTableView()
{
if(mDecodeEventTableView == null)
{
mDecodeEventTableView = new TableView<>();
mDecodeEventTableView.setMaxWidth(Double.MAX_VALUE);
TableColumn startTimeColumn = new TableColumn();
startTimeColumn.setPrefWidth(110);
startTimeColumn.setText("Start");
startTimeColumn.setCellValueFactory(new PropertyValueFactory<>("timeStart"));
TableColumn durationColumn = new TableColumn();
durationColumn.setPrefWidth(110);
durationColumn.setText("Duration");
durationColumn.setCellValueFactory(new PropertyValueFactory<>("duration"));
TableColumn typeColumn = new TableColumn();
typeColumn.setPrefWidth(130);
typeColumn.setText("Type");
typeColumn.setCellValueFactory(new PropertyValueFactory<>("eventType"));
TableColumn channelDescriptorColumn = new TableColumn();
channelDescriptorColumn.setPrefWidth(110);
channelDescriptorColumn.setText("Channel");
channelDescriptorColumn.setCellValueFactory(new PropertyValueFactory<>("channelDescriptor"));
TableColumn frequencyColumn = new TableColumn();
frequencyColumn.setPrefWidth(110);
frequencyColumn.setText("Frequency");
frequencyColumn.setCellValueFactory(new PropertyValueFactory<>("frequency"));
TableColumn hashcodeColumn = new TableColumn();
hashcodeColumn.setPrefWidth(100);
hashcodeColumn.setText("Hash ID");
hashcodeColumn.setCellValueFactory(new PropertyValueFactory<>("originalHashCode"));
TableColumn detailsColumn = new TableColumn();
detailsColumn.setPrefWidth(500);
detailsColumn.setText("Details");
detailsColumn.setCellValueFactory(new PropertyValueFactory<>("details"));
mDecodeEventTableView.getColumns().addAll(startTimeColumn, durationColumn, typeColumn, channelDescriptorColumn,
frequencyColumn, hashcodeColumn, detailsColumn);
mDecodeEventTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->
{
if(newValue != null)
{
getIdentifierCollectionViewer().set(newValue.getIdentifierCollection());
}
else
{
getIdentifierCollectionViewer().set(null);
}
});
}
return mDecodeEventTableView;
}
private ChannelStartProcessingRequestViewer getChannelStartProcessingRequestViewer()
{
if(mChannelStartProcessingRequestViewer == null)
{
mChannelStartProcessingRequestViewer = new ChannelStartProcessingRequestViewer();
}
return mChannelStartProcessingRequestViewer;
}
}

View File

@@ -0,0 +1,107 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.gui.viewer;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.module.decode.event.IDecodeEventListener;
/**
* Utility for combining a message and decoder state events.
*/
public class MessagePackager
{
private MessagePackage mMessagePackage;
/**
* Constructs an instance
*/
public MessagePackager()
{
}
/**
* Adds the message and creates a new MessageWithEvents instance, wrapping the message, ready to also receive any
* decode events and decoder state events. The previous message with events is overwritten.
* @param message to wrap.
*/
public void add(IMessage message)
{
mMessagePackage = new MessagePackage(message);
}
/**
* Access the current message with events.
*/
public MessagePackage getMessageWithEvents()
{
return mMessagePackage;
}
/**
* Adds the decoder state event to the current message with events.
* @param event to add
*/
public void add(DecoderStateEvent event)
{
if(mMessagePackage != null)
{
mMessagePackage.add(event);
}
}
/**
* Adds the decode event to the current message with events.
* @param event to add
*/
public void add(IDecodeEvent event)
{
if(mMessagePackage != null)
{
if(event instanceof DecodeEvent decodeEvent)
{
try
{
DecodeEventSnapshot snapshot = decodeEvent.getSnapshot();
mMessagePackage.add(snapshot);
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
}
/**
* Subscription to receive channel start processing requests via the event bus from the traffic channel manager
* @param request sent from the traffic channel manager.
*/
@Subscribe
public void process(ChannelStartProcessingRequest request)
{
mMessagePackage.add(request);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -41,20 +41,21 @@ import org.slf4j.LoggerFactory;
/**
* Utility application to load and view .bits recording file with the messages fully parsed.
*
* Supported Protocols: DMR and APCO25 Phase 1.
* Supported Protocols: DMR, APCO25 Phase 1 and Phase 2.
*/
public class RecordingViewer extends VBox
public class MessageRecordingViewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class);
private static final Logger mLog = LoggerFactory.getLogger(MessageRecordingViewer.class);
private MenuBar mMenuBar;
private TabPane mTabPane;
private int mTabCounterDmr = 1;
private int mTabCounterP25P1 = 1;
private int mTabCounterP25P2 = 1;
/**
* Constructs an instance
*/
public RecordingViewer()
public MessageRecordingViewer()
{
VBox.setVgrow(getTabPane(), Priority.ALWAYS);
getChildren().addAll(getMenuBar(), getTabPane());
@@ -74,13 +75,19 @@ public class RecordingViewer extends VBox
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
MenuItem p25p1MenuItem = new MenuItem("P25P1");
MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1");
p25p1MenuItem.onActionProperty().set(event -> {
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer());
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
MenuItem p25p2MenuItem = new MenuItem("P25 Phase 2");
p25p2MenuItem.onActionProperty().set(event -> {
Tab tab = new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer());
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem, p25p2MenuItem);
MenuItem exitMenu = new MenuItem("Exit");
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
@@ -102,6 +109,7 @@ public class RecordingViewer extends VBox
mTabPane.setMaxHeight(Double.MAX_VALUE);
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()));
mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer()));
}
return mTabPane;
@@ -151,11 +159,6 @@ public class RecordingViewer extends VBox
}
});
}
public LabeledTab(String label)
{
this(label, null);
}
}
public static void main(String[] args)
@@ -165,7 +168,7 @@ public class RecordingViewer extends VBox
@Override
public void start(Stage primaryStage) throws Exception
{
Scene scene = new Scene(new RecordingViewer(), 1100, 800);
Scene scene = new Scene(new MessageRecordingViewer(), 1100, 800);
primaryStage.setTitle("Message Recording Viewer (.bits)");
primaryStage.setScene(scene);
primaryStage.show();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -19,8 +19,14 @@
package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.message.IMessage;
import com.google.common.eventbus.EventBus;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.message.StuffBitsMessage;
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DecoderState;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor;
import io.github.dsheirer.record.binary.BinaryReader;
@@ -33,8 +39,11 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -71,17 +80,20 @@ public class P25P1Viewer extends VBox
private static final Logger mLog = LoggerFactory.getLogger(P25P1Viewer.class);
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.p25p1";
private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
private Preferences mPreferences = Preferences.userNodeForPackage(P25P1Viewer.class);
private Button mSelectFileButton;
private Label mSelectedFileLabel;
private TableView<IMessage> mMessageTableView;
private ObservableList<IMessage> mMessages = FXCollections.observableArrayList();
private FilteredList<IMessage> mFilteredMessages = new FilteredList<>(mMessages);
private TableView<MessagePackage> mMessagePackageTableView;
private ObservableList<MessagePackage> mMessagePackages = FXCollections.observableArrayList();
private FilteredList<MessagePackage> mFilteredMessagePackages = new FilteredList<>(mMessagePackages);
private TextField mSearchText;
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
private MessagePackageViewer mMessagePackageViewer;
private StringProperty mLoadedFile = new SimpleStringProperty();
public P25P1Viewer()
{
@@ -115,9 +127,11 @@ public class P25P1Viewer extends VBox
VBox.setVgrow(fileBox, Priority.NEVER);
VBox.setVgrow(filterBox, Priority.NEVER);
VBox.setVgrow(getMessageTableView(), Priority.ALWAYS);
VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
getChildren().addAll(fileBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
getChildren().addAll(fileBox, filterBox, getMessageTableView());
}
/**
@@ -132,19 +146,54 @@ public class P25P1Viewer extends VBox
{
if(file != null && file.exists())
{
mMessages.clear();
mLoadedFile.set(file.toString());
mMessagePackages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
ThreadPool.CACHED.submit(() -> {
List<IMessage> messages = new ArrayList<>();
List<MessagePackage> messagePackages = new ArrayList<>();
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
messageFramer.setListener(messageProcessor);
Channel empty = new Channel("Empty");
empty.setDecodeConfiguration(new DecodeConfigP25Phase1());
MessagePackager messagePackager = new MessagePackager();
//Setup a temporary event bus to capture channel start processing requests
EventBus eventBus = new EventBus("debug");
eventBus.register(messagePackager);
P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(empty);
trafficChannelManager.setInterModuleEventBus(eventBus);
//Register to receive events
trafficChannelManager.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
P25P1DecoderState decoderState = new P25P1DecoderState(empty, trafficChannelManager);
decoderState.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
decoderState.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
decoderState.start();
long frequency = getFrequencyFromFile(mLoadedFile.get());
if(frequency > 0)
{
trafficChannelManager.setCurrentControlFrequency(frequency, empty);
FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency);
decoderState.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
IdentifierUpdateNotification.Operation.ADD, 1));
}
messageProcessor.setMessageListener(message -> {
if(!(message instanceof StuffBitsMessage))
{
messages.add(message);
//Add the initial message to the packager so that it can be combined with any decoder state events.
messagePackager.add(message);
decoderState.receive(message);
//Collect the packaged message with events
messagePackages.add(messagePackager.getMessageWithEvents());
}
});
@@ -164,13 +213,47 @@ public class P25P1Viewer extends VBox
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessages.addAll(messages);
getMessageTableView().scrollTo(0);
mMessagePackages.addAll(messagePackages);
getMessagePackageTableView().scrollTo(0);
});
});
}
}
/**
* Extracts the channel frequency value from the bits file name to broadcast as the current frequency for each of
* the decoder states.
* @param file name to parse
* @return parsed frequency or zero.
*/
private static long getFrequencyFromFile(String file)
{
if(file == null || file.isEmpty())
{
return 0;
}
if(file.matches(FILE_FREQUENCY_REGEX))
{
Pattern p = Pattern.compile(FILE_FREQUENCY_REGEX);
Matcher m = p.matcher(file);
if(m.find())
{
try
{
String raw = m.group(1);
return Long.parseLong(raw);
}
catch(Exception e)
{
mLog.error("Couldn't parse frequency from bits file [" + file + "]");
}
}
}
return 0;
}
/**
* Updates the filter(s) applies to the list of messages
*/
@@ -180,12 +263,12 @@ public class P25P1Viewer extends VBox
if(filterText != null && !filterText.isEmpty())
{
Predicate<IMessage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
mFilteredMessages.setPredicate(textPredicate);
Predicate<MessagePackage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
mFilteredMessagePackages.setPredicate(textPredicate);
}
else
{
mFilteredMessages.setPredicate(null);
mFilteredMessagePackages.setPredicate(null);
}
}
@@ -197,12 +280,12 @@ public class P25P1Viewer extends VBox
{
if(text != null && !text.isEmpty())
{
for(IMessage message: mFilteredMessages)
for(MessagePackage messagePackage: mFilteredMessagePackages)
{
if(message.toString().toLowerCase().contains(text.toLowerCase()))
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
getMessagePackageTableView().getSelectionModel().select(messagePackage);
getMessagePackageTableView().scrollTo(messagePackage);
return;
}
}
@@ -217,7 +300,7 @@ public class P25P1Viewer extends VBox
{
if(text != null && !text.isEmpty())
{
IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem();
MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem();
if(selected == null)
{
@@ -225,18 +308,18 @@ public class P25P1Viewer extends VBox
return;
}
int row = mFilteredMessages.indexOf(selected);
int row = mFilteredMessagePackages.indexOf(selected);
for(int x = row + 1; x < mFilteredMessages.size(); x++)
for(int x = row + 1; x < mFilteredMessagePackages.size(); x++)
{
if(x < mFilteredMessages.size())
if(x < mFilteredMessagePackages.size())
{
IMessage message = mFilteredMessages.get(x);
MessagePackage messagePackage = mFilteredMessagePackages.get(x);
if(message.toString().toLowerCase().contains(text.toLowerCase()))
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
getMessagePackageTableView().getSelectionModel().select(messagePackage);
getMessagePackageTableView().scrollTo(messagePackage);
return;
}
}
@@ -244,25 +327,40 @@ public class P25P1Viewer extends VBox
}
}
/**
* List view control with DMR messages
*/
private TableView<IMessage> getMessageTableView()
private MessagePackageViewer getMessagePackageViewer()
{
if(mMessageTableView == null)
if(mMessagePackageViewer == null)
{
mMessageTableView = new TableView<>();
mMessageTableView.setPlaceholder(getLoadingIndicator());
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
mMessageTableView.setItems(sortedList);
mMessagePackageViewer = new MessagePackageViewer();
mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE);
mMessageTableView.setOnKeyPressed(event ->
//Register for table selection events to display the selected value.
getMessagePackageTableView().getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> getMessagePackageViewer().set(newValue));
}
return mMessagePackageViewer;
}
/**
* List view control with messages
*/
private TableView<MessagePackage> getMessagePackageTableView()
{
if(mMessagePackageTableView == null)
{
mMessagePackageTableView = new TableView<>();
mMessagePackageTableView.setPlaceholder(getLoadingIndicator());
SortedList<MessagePackage> sortedList = new SortedList<>(mFilteredMessagePackages);
sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty());
mMessagePackageTableView.setItems(sortedList);
mMessagePackageTableView.setOnKeyPressed(event ->
{
if(KEY_CODE_COPY.match(event))
{
final Set<Integer> rows = new TreeSet<>();
for (final TablePosition tablePosition : mMessageTableView.getSelectionModel().getSelectedCells())
for (final TablePosition tablePosition : mMessagePackageTableView.getSelectionModel().getSelectedCells())
{
rows.add(tablePosition.getRow());
}
@@ -282,7 +380,7 @@ public class P25P1Viewer extends VBox
boolean firstCol = true;
for (final TableColumn<?, ?> column : mMessageTableView.getColumns())
for (final TableColumn<?, ?> column : mMessagePackageTableView.getColumns())
{
if(firstCol)
{
@@ -313,23 +411,44 @@ public class P25P1Viewer extends VBox
validColumn.setText("Valid");
validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
TableColumn timeslotColumn = new TableColumn();
timeslotColumn.setPrefWidth(35);
timeslotColumn.setText("TS");
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
TableColumn messageColumn = new TableColumn();
messageColumn.setPrefWidth(1000);
messageColumn.setPrefWidth(900);
messageColumn.setText("Message");
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
SimpleStringProperty property = new SimpleStringProperty();
if(param.getValue() instanceof IMessage message)
if(param.getValue() instanceof MessagePackage messagePackage)
{
property.set(message.toString());
property.set(messagePackage.toString());
}
return property;
});
mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn);
TableColumn decodeEventCountColumn = new TableColumn();
decodeEventCountColumn.setPrefWidth(50);
decodeEventCountColumn.setText("Events");
decodeEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decodeEventCount"));
TableColumn decoderStateEventCountColumn = new TableColumn();
decoderStateEventCountColumn.setPrefWidth(50);
decoderStateEventCountColumn.setText("States");
decoderStateEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decoderStateEventCount"));
TableColumn channelStartCountColumn = new TableColumn();
channelStartCountColumn.setPrefWidth(50);
channelStartCountColumn.setText("Starts");
channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount"));
mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn,
decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn);
}
return mMessageTableView;
return mMessagePackageTableView;
}
/**
@@ -343,7 +462,7 @@ public class P25P1Viewer extends VBox
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select DMR .bits Recording");
fileChooser.setTitle("Select P25 Phase 1 .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
{

View File

@@ -0,0 +1,738 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.gui.viewer;
import com.google.common.eventbus.EventBus;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroupManager;
import io.github.dsheirer.message.StuffBitsMessage;
import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent;
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
import io.github.dsheirer.module.decode.p25.phase1.message.P25FrequencyBand;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
import io.github.dsheirer.module.decode.p25.phase2.P25P2DecoderState;
import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageFramer;
import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageProcessor;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.util.ThreadPool;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* APCO25 Phase 2 viewer panel
*/
public class P25P2Viewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(P25P2Viewer.class);
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.p25p2";
private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
private Preferences mPreferences = Preferences.userNodeForPackage(P25P2Viewer.class);
private Button mSelectFileButton;
private Label mSelectedFileLabel;
private TableView<MessagePackage> mMessagePackageTableView;
private ObservableList<MessagePackage> mMessagePackages = FXCollections.observableArrayList();
private FilteredList<MessagePackage> mFilteredMessagePackages = new FilteredList<>(mMessagePackages);
private CheckBox mShowTS0;
private CheckBox mShowTS1;
private CheckBox mShowTS2;
private TextField mSearchText;
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
private IntegerTextField mWACNTextField;
private IntegerTextField mSystemTextField;
private IntegerTextField mNACTextField;
private Button mReloadButton;
private StringProperty mLoadedFile = new SimpleStringProperty();
private MessagePackageViewer mMessagePackageViewer;
public P25P2Viewer()
{
setPadding(new Insets(5));
setSpacing(5);
HBox fileBox = new HBox();
fileBox.setMaxWidth(Double.MAX_VALUE);
fileBox.setAlignment(Pos.CENTER_LEFT);
fileBox.setSpacing(5);
getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
HBox scrambleSettingsBox = new HBox();
scrambleSettingsBox.setAlignment(Pos.BASELINE_LEFT);
scrambleSettingsBox.setSpacing(5);
Label wacnLabel = new Label("WACN:");
Label systemLabel = new Label("SYSTEM:");
Label nacLabel = new Label("NAC:");
scrambleSettingsBox.getChildren().addAll(wacnLabel, getWACNTextField(), systemLabel, getSystemTextField(),
nacLabel, getNACTextField(), getReloadButton());
HBox filterBox = new HBox();
filterBox.setMaxWidth(Double.MAX_VALUE);
filterBox.setAlignment(Pos.BASELINE_CENTER);
filterBox.setSpacing(5);
Label searchLabel = new Label("Message Filter:");
HBox.setMargin(searchLabel, new Insets(0,0,0,15));
Label findLabel = new Label("Find:");
HBox.setHgrow(getFindText(), Priority.ALWAYS);
HBox.setHgrow(getSearchText(), Priority.ALWAYS);
Label showLabel = new Label("Show:");
HBox.setMargin(showLabel, new Insets(0,0,0,15));
filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
getSearchText(), showLabel, getShowTS0(), getShowTS1(), getShowTS2());
VBox.setVgrow(fileBox, Priority.NEVER);
VBox.setVgrow(filterBox, Priority.NEVER);
VBox.setVgrow(scrambleSettingsBox, Priority.NEVER);
VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
getChildren().addAll(fileBox, scrambleSettingsBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
}
/**
* Extracts the channel frequency value from the bits file name to broadcast as the current frequency for each of
* the decoder states.
* @param file name to parse
* @return parsed frequency or zero.
*/
private static long getFrequencyFromFile(String file)
{
if(file == null || file.isEmpty())
{
return 0;
}
if(file.matches(FILE_FREQUENCY_REGEX))
{
Pattern p = Pattern.compile(FILE_FREQUENCY_REGEX);
Matcher m = p.matcher(file);
if(m.find())
{
try
{
String raw = m.group(1);
return Long.parseLong(raw);
}
catch(Exception e)
{
mLog.error("Couldn't parse frequency from bits file [" + file + "]");
}
}
}
return 0;
}
/**
* Processes the recording file and loads the content into the viewer
*
* Note: invoke this method off of the UI thread in a thread pool executor and the results will be loaded into the
* message table back on the JavaFX UI thread.
*
* @param file containing a .bits recording of decoded data.
*/
private void load(File file)
{
mLog.info("Loading File: " + file);
if(file != null && file.exists())
{
mLoadedFile.set(file.toString());
mMessagePackages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
int wacn = getWACNTextField().get();
int system = getSystemTextField().get();
int nac = getNACTextField().get();
ScrambleParameters scrambleParameters = new ScrambleParameters(wacn, system, nac);
ThreadPool.CACHED.submit(() -> {
List<MessagePackage> messages = new ArrayList<>();
P25P2MessageFramer messageFramer = new P25P2MessageFramer(null, 9600);
messageFramer.setScrambleParameters(scrambleParameters);
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
messageFramer.setListener(messageProcessor);
Channel empty = new Channel("Empty");
empty.setDecodeConfiguration(new DecodeConfigP25Phase2());
MessagePackager messagePackager = new MessagePackager();
//Setup a temporary event bus to capture channel start processing requests
EventBus eventBus = new EventBus("debug");
eventBus.register(messagePackager);
P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(empty);
trafficChannelManager.setInterModuleEventBus(eventBus);
//Register to receive events
trafficChannelManager.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
PatchGroupManager patchGroupManager = new PatchGroupManager();
P25P2DecoderState decoderState1 = new P25P2DecoderState(empty, 1, trafficChannelManager,
patchGroupManager);
decoderState1.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
decoderState1.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
P25P2DecoderState decoderState2 = new P25P2DecoderState(empty, 2, trafficChannelManager,
patchGroupManager);
decoderState2.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
decoderState2.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
decoderState1.start();
decoderState2.start();
long frequency = getFrequencyFromFile(mLoadedFile.get());
if(frequency > 0)
{
trafficChannelManager.setCurrentControlFrequency(frequency, empty);
FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency);
decoderState1.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
IdentifierUpdateNotification.Operation.ADD, 1));
decoderState2.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
IdentifierUpdateNotification.Operation.ADD, 2));
}
//TODO: testing use - PCWIN TDMA Frequency Band as preload data
//ID:2 OFFSET:-45000000 SPACING:12500 BASE:851012500 TDMA BW:12500 TIMESLOTS:2 VOCODER:HALF_RATE
P25FrequencyBand band = new P25FrequencyBand(2, 851012500l, -45000000l, 12500, 12500, 2);
P25FrequencyBandPreloadDataContent content = new P25FrequencyBandPreloadDataContent(Collections.singleton(band));
messageProcessor.preload(content);
messageProcessor.setMessageListener(message -> {
if(!(message instanceof StuffBitsMessage))
{
// System.out.println(message);
//Add the initial message to the packager so that it can be combined with any decoder state events.
messagePackager.add(message);
if(message.getTimeslot() == P25P1Message.TIMESLOT_1)
{
decoderState1.receive(message);
}
else if(message.getTimeslot() == P25P1Message.TIMESLOT_2)
{
decoderState2.receive(message);
}
//Collect the packaged message with events
messages.add(messagePackager.getMessageWithEvents());
}
});
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{
ByteBuffer buffer = reader.next();
// System.out.println("Processing Bytes " + buffer.capacity() + " / " + reader.getByteCounter());
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessagePackages.addAll(messages);
getMessagePackageTableView().scrollTo(0);
});
});
}
else
{
mLog.info("Can't load file: " + file);
}
}
/**
* Updates the filter(s) applies to the list of messages
*/
private void updateFilters()
{
Predicate<MessagePackage> timeslotPredicate = message ->
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
(getShowTS2().isSelected() && (message.getTimeslot() == 2));
String filterText = getSearchText().getText();
if(filterText == null || filterText.isEmpty())
{
mFilteredMessagePackages.setPredicate(timeslotPredicate);
}
else
{
Predicate<MessagePackage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
mFilteredMessagePackages.setPredicate(timeslotPredicate.and(textPredicate));
}
}
/**
* Finds and selects the first row containing the text argument.
* @param text to search for.
*/
private void find(String text)
{
if(text != null && !text.isEmpty())
{
for(MessagePackage messagePackage: mFilteredMessagePackages)
{
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessagePackageTableView().getSelectionModel().select(messagePackage);
getMessagePackageTableView().scrollTo(messagePackage);
return;
}
}
}
}
/**
* Finds and selects the first row containing the text argument, after the currently selected row.
* @param text to search for.
*/
private void findNext(String text)
{
if(text != null && !text.isEmpty())
{
MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem();
if(selected == null)
{
find(text);
return;
}
int row = mFilteredMessagePackages.indexOf(selected);
for(int x = row + 1; x < mFilteredMessagePackages.size(); x++)
{
if(x < mFilteredMessagePackages.size())
{
MessagePackage messagePackage = mFilteredMessagePackages.get(x);
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessagePackageTableView().getSelectionModel().select(messagePackage);
getMessagePackageTableView().scrollTo(messagePackage);
return;
}
}
}
}
}
private MessagePackageViewer getMessagePackageViewer()
{
if(mMessagePackageViewer == null)
{
mMessagePackageViewer = new MessagePackageViewer();
mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE);
//Register for table selection events to display the selected value.
getMessagePackageTableView().getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> getMessagePackageViewer().set(newValue));
}
return mMessagePackageViewer;
}
private IntegerTextField getWACNTextField()
{
if(mWACNTextField == null)
{
mWACNTextField = new IntegerTextField();
}
return mWACNTextField;
}
private IntegerTextField getSystemTextField()
{
if(mSystemTextField == null)
{
mSystemTextField = new IntegerTextField();
}
return mSystemTextField;
}
private IntegerTextField getNACTextField()
{
if(mNACTextField == null)
{
mNACTextField = new IntegerTextField();
}
return mNACTextField;
}
private Button getReloadButton()
{
if(mReloadButton == null)
{
mReloadButton = new Button("Reload");
mReloadButton.disableProperty().bind(Bindings.isNull(mLoadedFile));
mReloadButton.setOnAction(event -> load(new File(mLoadedFile.get())));
}
return mReloadButton;
}
/**
* List view control with message packages
*/
private TableView<MessagePackage> getMessagePackageTableView()
{
if(mMessagePackageTableView == null)
{
mMessagePackageTableView = new TableView<>();
mMessagePackageTableView.setPlaceholder(getLoadingIndicator());
SortedList<MessagePackage> sortedList = new SortedList<>(mFilteredMessagePackages);
sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty());
mMessagePackageTableView.setItems(sortedList);
mMessagePackageTableView.setOnKeyPressed(event ->
{
if(KEY_CODE_COPY.match(event))
{
final Set<Integer> rows = new TreeSet<>();
for (final TablePosition tablePosition : mMessagePackageTableView.getSelectionModel().getSelectedCells())
{
rows.add(tablePosition.getRow());
}
final StringBuilder sb = new StringBuilder();
boolean firstRow = true;
for (final Integer row : rows)
{
if(firstRow)
{
firstRow = false;
}
else
{
sb.append('\n');
}
boolean firstCol = true;
for (final TableColumn<?, ?> column : mMessagePackageTableView.getColumns())
{
if(firstCol)
{
firstCol = false;
}
else
{
sb.append('\t');
}
final Object cellData = column.getCellData(row);
sb.append(cellData == null ? "" : cellData.toString());
}
}
final ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(sb.toString());
Clipboard.getSystemClipboard().setContent(clipboardContent);
}
});
TableColumn timestampColumn = new TableColumn();
timestampColumn.setPrefWidth(110);
timestampColumn.setText("Time");
timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp"));
TableColumn validColumn = new TableColumn();
validColumn.setPrefWidth(50);
validColumn.setText("Valid");
validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
TableColumn timeslotColumn = new TableColumn();
timeslotColumn.setPrefWidth(35);
timeslotColumn.setText("TS");
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
TableColumn messageColumn = new TableColumn();
messageColumn.setPrefWidth(900);
messageColumn.setText("Message");
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
SimpleStringProperty property = new SimpleStringProperty();
if(param.getValue() instanceof MessagePackage messagePackage)
{
property.set(messagePackage.toString());
}
return property;
});
TableColumn decodeEventCountColumn = new TableColumn();
decodeEventCountColumn.setPrefWidth(50);
decodeEventCountColumn.setText("Events");
decodeEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decodeEventCount"));
TableColumn decoderStateEventCountColumn = new TableColumn();
decoderStateEventCountColumn.setPrefWidth(50);
decoderStateEventCountColumn.setText("States");
decoderStateEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decoderStateEventCount"));
TableColumn channelStartCountColumn = new TableColumn();
channelStartCountColumn.setPrefWidth(50);
channelStartCountColumn.setText("Starts");
channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount"));
mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn,
decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn);
}
return mMessagePackageTableView;
}
/**
* File selection button
* @return button
*/
private Button getSelectFileButton()
{
if(mSelectFileButton == null)
{
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select P25 Phase 2 .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
{
File file = new File(lastDirectory);
if(file.exists() && file.isDirectory())
{
fileChooser.setInitialDirectory(file);
}
}
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
if(selected != null)
{
mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
load(selected);
}
});
}
return mSelectFileButton;
}
/**
* Spinny loading icon to show over the message table view
*/
private ProgressIndicator getLoadingIndicator()
{
if(mLoadingIndicator == null)
{
mLoadingIndicator = new ProgressIndicator();
mLoadingIndicator.setProgress(-1);
mLoadingIndicator.setVisible(false);
}
return mLoadingIndicator;
}
/**
* Selected file path label.
*/
private Label getSelectedFileLabel()
{
if(mSelectedFileLabel == null)
{
mSelectedFileLabel = new Label(" ");
}
return mSelectedFileLabel;
}
/**
* Check box to apply filter to show/hide TS0 messages
* @return check box control
*/
private CheckBox getShowTS0()
{
if(mShowTS0 == null)
{
mShowTS0 = new CheckBox("TS0");
mShowTS0.setSelected(true);
mShowTS0.setOnAction(event -> updateFilters());
}
return mShowTS0;
}
/**
* Check box to apply filter to show/hide TS0 messages
* @return check box control
*/
private CheckBox getShowTS1()
{
if(mShowTS1 == null)
{
mShowTS1 = new CheckBox("TS1");
mShowTS1.setSelected(true);
mShowTS1.setOnAction(event -> updateFilters());
}
return mShowTS1;
}
/**
* Check box to apply filter to show/hide TS0 messages
* @return check box control
*/
private CheckBox getShowTS2()
{
if(mShowTS2 == null)
{
mShowTS2 = new CheckBox("TS2");
mShowTS2.setSelected(true);
mShowTS2.setOnAction(event -> updateFilters());
}
return mShowTS2;
}
/**
* Search text filter box
* @return text control for entering search text
*/
private TextField getSearchText()
{
if(mSearchText == null)
{
mSearchText = new TextField();
mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mSearchText;
}
/**
* Text box for find text
*/
private TextField getFindText()
{
if(mFindText == null)
{
mFindText = new TextField();
mFindText.setOnKeyPressed(event -> {
if(event.getCode().equals(KeyCode.ENTER))
{
getFindButton().fire();
}
});
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mFindText;
}
/**
* Find button to search for the text in the find text box.
* @return button
*/
private Button getFindButton()
{
if(mFindButton == null)
{
mFindButton = new Button("Find");
mFindButton.setOnAction(event -> find(getFindText().getText()));
}
return mFindButton;
}
/**
* Find next button to search for the text in the find text box.
* @return button
*/
private Button getFindNextButton()
{
if(mFindNextButton == null)
{
mFindNextButton = new Button("Next");
mFindNextButton.setOnAction(event -> findNext(getFindText().getText()));
}
return mFindNextButton;
}
}

View File

@@ -18,6 +18,8 @@
*/
package io.github.dsheirer.identifier;
import java.util.EnumSet;
/**
* Identifier form. Indicates the type of identifier.
*/
@@ -64,4 +66,21 @@ public enum Form
UNIQUE_ID,
WACN,
ANY;
Form()
{
}
/**
* Entity forms that are used to identify entities in P25 call events.
*/
public static EnumSet<Form> ENTITY_FORMS = EnumSet.of(Form.RADIO, Form.TALKGROUP, Form.PATCH_GROUP, Form.TELEPHONE_NUMBER);
/**
* Indicates if this form is a form that can be used to identify an entity
*/
public boolean isEntityForm()
{
return ENTITY_FORMS.contains(this);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -32,6 +32,12 @@ public enum IdentifierClass
//Communication network identifier
NETWORK,
//User network address
USER_NETWORK_ADDRESS,
//User network port
USER_NETWORK_PORT,
//Communication user identifier
USER;
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -20,14 +20,13 @@
package io.github.dsheirer.identifier;
import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* (Immutable) Collection of identifiers with convenient accessor methods
@@ -274,43 +273,90 @@ public class IdentifierCollection
}
/**
* Returns the first identifier in this collection that is assigned a FROM role
* Indicates if this collection has an identifier that matches the specified identiifer.
* @param toCheck identifier
* @return true if the identifier already exists in the collection.
*/
public boolean hasIdentifier(Identifier toCheck)
{
if(toCheck == null)
{
return false;
}
for(Identifier identifier : mIdentifiers)
{
if(identifier.equals(toCheck))
{
return true;
}
}
return false;
}
/**
* Returns the first entity identifier in this collection that is assigned a FROM role
*/
public Identifier getFromIdentifier()
{
Identifier from = getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
if(from == null)
if(from != null)
{
List<Identifier> fromIdentifiers = getIdentifiers(Role.FROM);
if(!fromIdentifiers.isEmpty())
{
from = fromIdentifiers.get(0);
}
return from;
}
return from;
List<Identifier> identifiers = getIdentifiers(Form.TELEPHONE_NUMBER);
if(!identifiers.isEmpty())
{
return identifiers.get(0);
}
List<Identifier> fromIdentifiers = getIdentifiers(Role.FROM);
if(!fromIdentifiers.isEmpty())
{
return fromIdentifiers.get(0);
}
return null;
}
/**
* Returns the first identifier in this collection that is assigned a TO role
* Returns the first entity identifier in this collection that is assigned a TO role
*/
public Identifier getToIdentifier()
{
Identifier to = getIdentifier(IdentifierClass.USER, Form.PATCH_GROUP, Role.TO);
if(to == null)
if(to != null)
{
to = getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
return to;
}
if(to == null)
to = getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
if(to != null)
{
List<Identifier> toIdentifiers = getIdentifiers(Role.TO);
if(!toIdentifiers.isEmpty())
return to;
}
to = getIdentifier(IdentifierClass.USER, Form.RADIO, Role.TO);
if(to != null)
{
return to;
}
List<Identifier> toIdentifiers = getIdentifiers(Role.TO);
for(Identifier identifier: toIdentifiers)
{
if(identifier.getForm() != Form.ENCRYPTION_KEY)
{
to = toIdentifiers.get(0);
return identifier;
}
}

View File

@@ -37,7 +37,6 @@ public abstract class IntegerIdentifier extends Identifier<Integer>
super(value, identifierClass, form, role);
}
@Override
public boolean equals(Object o)
{

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,7 +14,7 @@
*
* 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.identifier.ipv4;
@@ -29,6 +28,6 @@ public abstract class IPV4Identifier extends Identifier<IPV4Address>
{
public IPV4Identifier(IPV4Address value, Role role)
{
super(value, IdentifierClass.USER, Form.IPV4_ADDRESS, role);
super(value, IdentifierClass.USER_NETWORK_ADDRESS, Form.IPV4_ADDRESS, role);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -95,25 +95,44 @@ public class PatchGroup
/**
* Adds the talkgroup identifier to this patched group.
* @param patchedGroupIdentifier to add
* @return true if the patched group identifier is new and was added
*/
public void addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
public boolean addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
{
boolean added = false;
if(!mPatchedTalkgroupIdentifiers.contains(patchedGroupIdentifier))
{
mPatchedTalkgroupIdentifiers.add(patchedGroupIdentifier);
added |= mPatchedTalkgroupIdentifiers.add(patchedGroupIdentifier);
}
return added;
}
/**
* Removes the patched talkgroup from this patch group.
* @param talkgroup to remove
*/
public void removePatchedTalkgroup(TalkgroupIdentifier talkgroup)
{
mPatchedTalkgroupIdentifiers.remove(talkgroup);
}
/**
* Adds a list of talkgroup identifiers to this patched group.
* @param patchedGroupIdentifiers to add
*/
public void addPatchedTalkgroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
public boolean addPatchedTalkgroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
{
boolean added = false;
for(TalkgroupIdentifier identifier : patchedGroupIdentifiers)
{
addPatchedTalkgroup(identifier);
added |= addPatchedTalkgroup(identifier);
}
return added;
}
/**
@@ -129,24 +148,41 @@ public class PatchGroup
* Adds the radio identifier to this patched group.
* @param patchedRadioIdentifier to add
*/
public void addPatchedRadio(RadioIdentifier patchedRadioIdentifier)
public boolean addPatchedRadio(RadioIdentifier patchedRadioIdentifier)
{
if(!mPatchedRadioIdentifiers.contains(patchedRadioIdentifier))
{
mPatchedRadioIdentifiers.add(patchedRadioIdentifier);
return true;
}
return false;
}
/**
* Removes the patched radio from this patch group.
* @param radioIdentifier to remove
*/
public void removePatchedRadio(RadioIdentifier radioIdentifier)
{
mPatchedRadioIdentifiers.remove(radioIdentifier);
}
/**
* Adds a list of radio identifiers to this patched group.
* @param patchedRadioIdentifiers to add
* @return true if any new identifiers were added
*/
public void addPatchedRadios(List<RadioIdentifier> patchedRadioIdentifiers)
public boolean addPatchedRadios(List<RadioIdentifier> patchedRadioIdentifiers)
{
boolean added = false;
for(RadioIdentifier identifier : patchedRadioIdentifiers)
{
addPatchedRadio(identifier);
added |= addPatchedRadio(identifier);
}
return added;
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -22,11 +22,14 @@ package io.github.dsheirer.identifier.patch;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Manager for (temporary) patch groups aka super groups. This manager monitors patch group additions and deletions and
@@ -39,7 +42,8 @@ import java.util.Map;
*/
public class PatchGroupManager
{
private Map<Integer,PatchGroupIdentifier> mPatchGroupMap = new HashMap<>();
private static final long PATCH_GROUP_FRESHNESS_THRESHOLD_MS = Duration.ofSeconds(30).toMillis();
private Map<Integer,PatchGroupTracker> mPatchGroupTrackerMap = new HashMap<>();
/**
* Constructs an instance
@@ -48,12 +52,36 @@ public class PatchGroupManager
{
}
/**
* Summary listing of active patch groups
*/
public String getPatchGroupSummary()
{
StringBuilder sb = new StringBuilder();
sb.append("Active Patch Groups\n");
List<PatchGroupTracker> trackers = new ArrayList<>(mPatchGroupTrackerMap.values());
if(trackers.isEmpty())
{
sb.append("None");
}
else
{
for(PatchGroupTracker tracker : trackers)
{
sb.append(" ").append(tracker.mPatchGroupIdentifier).append("\n");
}
}
return sb.toString();
}
/**
* Clears any existing patch groups
*/
public void clear()
{
mPatchGroupMap.clear();
mPatchGroupTrackerMap.clear();
}
/**
@@ -63,46 +91,53 @@ public class PatchGroupManager
* existing patch group is discarded and replaced with the updated version of the patch group.
*
* @param patchGroupIdentifier to add or update
* @return true if the group was added or updated
*/
public synchronized void addPatchGroup(PatchGroupIdentifier patchGroupIdentifier)
public synchronized boolean addPatchGroup(PatchGroupIdentifier patchGroupIdentifier, long timestamp)
{
PatchGroup update = patchGroupIdentifier.getValue();
if(update.getPatchGroup().getValue() > 0)
{
if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue()))
{
PatchGroup existingPatchGroup = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
int patchGroup = update.getPatchGroup().getValue();
//If the patch group version number is the same, this is an update
if(existingPatchGroup.getVersion() == update.getVersion())
if(patchGroup > 0)
{
if(mPatchGroupTrackerMap.containsKey(patchGroup))
{
PatchGroupTracker tracker = mPatchGroupTrackerMap.get(patchGroup);
if(tracker.isStale(timestamp))
{
existingPatchGroup.addPatchedTalkgroups(update.getPatchedTalkgroupIdentifiers());
existingPatchGroup.addPatchedRadios(update.getPatchedRadioIdentifiers());
//Replace the existing patch group if it is stale.
mPatchGroupTrackerMap.put(patchGroup, new PatchGroupTracker(patchGroupIdentifier, timestamp));
return true;
}
//Otherwise, this is a replace operation.
else
{
mPatchGroupMap.put(update.getPatchGroup().getValue(), patchGroupIdentifier);
//Update the existing patch group.
return tracker.add(patchGroupIdentifier, timestamp);
}
}
else
{
mPatchGroupMap.put(update.getPatchGroup().getValue(), patchGroupIdentifier);
mPatchGroupTrackerMap.put(patchGroup, new PatchGroupTracker(patchGroupIdentifier, timestamp));
return true;
}
}
return false;
}
/**
* Adds any patch group identifiers contained in the list.
* @param referenceTimestamp as a reference for checking staleness of existing patch groups.
*/
public synchronized void addPatchGroups(List<Identifier> identifiers)
public synchronized void addPatchGroups(List<Identifier> identifiers, long referenceTimestamp)
{
for(Identifier identifier : identifiers)
{
if(identifier instanceof PatchGroupIdentifier)
{
addPatchGroup((PatchGroupIdentifier)identifier);
addPatchGroup((PatchGroupIdentifier)identifier, referenceTimestamp);
}
}
}
@@ -111,11 +146,12 @@ public class PatchGroupManager
* Removes the patch group from this manager if it is currently being managed.
*
* @param patchGroupIdentifier to remove
* @return true if the patch group was removed.
*/
public synchronized void removePatchGroup(PatchGroupIdentifier patchGroupIdentifier)
public synchronized boolean removePatchGroup(PatchGroupIdentifier patchGroupIdentifier)
{
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
mPatchGroupMap.remove(id);
return mPatchGroupTrackerMap.remove(id) != null;
}
/**
@@ -136,15 +172,16 @@ public class PatchGroupManager
* Updates the list of identifiers by replacing any talkgroups or patch groups with the current
* version of the patch group and a complete listing of the patched talkgroups.
* @param identifiers to update
* @param referenceTimestamp as a reference for checking staleness of existing patch groups.
* @return list of identifiers
*/
public List<Identifier> update(List<Identifier> identifiers)
public List<Identifier> update(List<Identifier> identifiers, long referenceTimestamp)
{
List<Identifier> updated = new ArrayList<>();
for(Identifier identifier: identifiers)
{
updated.add(update(identifier));
updated.add(update(identifier, referenceTimestamp));
}
return updated;
@@ -155,9 +192,10 @@ public class PatchGroupManager
* if the identifier matches a currently managed patch group.
*
* @param identifier for a talkgroup or a patch group.
* @param referenceTimestamp as a reference for checking staleness of existing patch groups.
* @return current patch group or the original identifier
*/
public Identifier update(Identifier identifier)
public Identifier update(Identifier identifier, long referenceTimestamp)
{
if(identifier != null && identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
{
@@ -166,22 +204,49 @@ public class PatchGroupManager
case TALKGROUP:
if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
Identifier mapValue = mPatchGroupMap.get(talkgroupIdentifier.getValue());
if (mapValue != null)
int id = talkgroupIdentifier.getValue();
PatchGroupTracker tracker = mPatchGroupTrackerMap.get(id);
if(tracker != null)
{
return mapValue;
if(tracker.isStale(referenceTimestamp))
{
mPatchGroupTrackerMap.remove(id);
}
else
{
//Perform substitution - return patch group instead of the original talkgroup
return tracker.getPatchGroupIdentifier(referenceTimestamp);
}
}
}
break;
case PATCH_GROUP:
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
int patchGroupId = patchGroupIdentifier.getValue().getPatchGroup().getValue();
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
Identifier mapValue = mPatchGroupMap.get(patchGroupId);
if (mapValue != null)
PatchGroupTracker tracker = mPatchGroupTrackerMap.get(id);
if(tracker != null)
{
return mapValue;
if(tracker.isStale(referenceTimestamp))
{
mPatchGroupTrackerMap.put(id, new PatchGroupTracker(patchGroupIdentifier, referenceTimestamp));
mPatchGroupTrackerMap.remove(id);
}
else if(tracker.getPatchGroupIdentifier(referenceTimestamp).getValue().getVersion() !=
patchGroupIdentifier.getValue().getVersion())
{
mPatchGroupTrackerMap.put(id, new PatchGroupTracker(patchGroupIdentifier, referenceTimestamp));
mPatchGroupTrackerMap.remove(id);
}
else
{
//Perform substitution - return patch group instead of the original talkgroup
return tracker.getPatchGroupIdentifier(referenceTimestamp);
}
}
}
break;
@@ -190,4 +255,107 @@ public class PatchGroupManager
return identifier;
}
/**
* Tracks the freshness of a patch group and any member talkgroup and radio identifiers to ensure that stale
* patch group identifiers are removed.
*/
public class PatchGroupTracker
{
private PatchGroupIdentifier mPatchGroupIdentifier;
private long mLastUpdateTimestamp;
private Map<TalkgroupIdentifier,Long> mTalkgroupTimestampMap = new HashMap<>();
private Map<RadioIdentifier,Long> mRadioTimestampMap = new HashMap<>();
/**
* Constructs an instance
* @param patchGroupIdentifier
*/
public PatchGroupTracker(PatchGroupIdentifier patchGroupIdentifier, long timestamp)
{
mPatchGroupIdentifier = patchGroupIdentifier;
add(patchGroupIdentifier, timestamp);
}
/**
* Patch group monitored by this tracker. This method will cleanup and remove any stale radio or talkgroup
* identifiers on access.
*
* Note: this tracker should be checked for freshness by first checking the isStale() method before accessing
* the tracked patch group by this method.
*
* @param referenceTimestamp for comparing stored radio and talkgroup values for staleness.
* @return current version of the talkgroup
*/
public PatchGroupIdentifier getPatchGroupIdentifier(long referenceTimestamp)
{
//Remove stale talkgroups
List<Map.Entry<TalkgroupIdentifier,Long>> toRemove = mTalkgroupTimestampMap.entrySet().stream()
.filter(entry -> isStale(entry.getValue(), referenceTimestamp)).collect(Collectors.toList());
for(Map.Entry<TalkgroupIdentifier,Long> entry: toRemove)
{
mTalkgroupTimestampMap.remove(entry.getKey());
mPatchGroupIdentifier.getValue().removePatchedTalkgroup(entry.getKey());
}
//Remove stale radios
List<Map.Entry<RadioIdentifier,Long>> toRemove2 = mRadioTimestampMap.entrySet().stream()
.filter(entry -> isStale(entry.getValue(), referenceTimestamp)).collect(Collectors.toList());
for(Map.Entry<RadioIdentifier,Long> entry: toRemove2)
{
mRadioTimestampMap.remove(entry.getKey());
mPatchGroupIdentifier.getValue().removePatchedRadio(entry.getKey());
}
return mPatchGroupIdentifier;
}
/**
* Indicates if the stored timestamp is stale relative to the reference timestamp.
* @param storedTimestamp to compare
* @param referenceTimestamp representing the current time.
* @return true of the stored timestamp is stale.
*/
private boolean isStale(long storedTimestamp, long referenceTimestamp)
{
return referenceTimestamp - storedTimestamp > PATCH_GROUP_FRESHNESS_THRESHOLD_MS;
}
/**
* Indicates if the tracked patch group is stale and hasn't received any add/updates within a period of time.
* @param referenceTimestamp representing now for comparison against the patch groups update timestamp.
* @return true if the tracked patch group is stale.
*/
public boolean isStale(long referenceTimestamp)
{
return isStale(mLastUpdateTimestamp, referenceTimestamp);
}
/**
* Add or update the patch group and update the freshness timestamp.
* @param patchGroupIdentifier containing an add value.
*/
public boolean add(PatchGroupIdentifier patchGroupIdentifier, long timestamp)
{
mLastUpdateTimestamp = timestamp;
boolean added = false;
for(TalkgroupIdentifier talkgroup: patchGroupIdentifier.getValue().getPatchedTalkgroupIdentifiers())
{
mTalkgroupTimestampMap.put(talkgroup, timestamp);
added |= mPatchGroupIdentifier.getValue().addPatchedTalkgroup(talkgroup);
}
for(RadioIdentifier radio: patchGroupIdentifier.getValue().getPatchedRadioIdentifiers())
{
mRadioTimestampMap.put(radio, timestamp);
added |= mPatchGroupIdentifier.getValue().addPatchedRadio(radio);
}
return added;
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -27,13 +27,25 @@ import io.github.dsheirer.identifier.IdentifierCollection;
*/
public class PatchGroupPreLoadDataContent extends PreloadDataContent<IdentifierCollection>
{
private long mTimestamp;
/**
* Constructs an instance
*
* @param data to preload
*/
public PatchGroupPreLoadDataContent(IdentifierCollection data)
public PatchGroupPreLoadDataContent(IdentifierCollection data, long timestamp)
{
super(data);
mTimestamp = timestamp;
}
/**
* Freshness timestamp for the preload data.
* @return timestamp milliseconds.
*/
public long getTimestamp()
{
return mTimestamp;
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.identifier.radio;
@@ -25,29 +22,74 @@ package io.github.dsheirer.identifier.radio;
import io.github.dsheirer.identifier.Role;
/**
* Fully qualified radio identifier
* Fully qualified radio identifier. This is used for roaming radios or any time there is a need to fully qualify a
* radio.
*/
public abstract class FullyQualifiedRadioIdentifier extends RadioIdentifier
{
private int mWacn;
private int mSystem;
private int mRadio;
/**
* Constructs an instance
* @param wacn
* @param system
* @param id
* @param localAddress radio identifier, aka alias. This can be the same as the radio ID when the fully qualified radio
* is not being aliased on a local radio system.
* @param wacn of the home network for the radio.
* @param system of the home network for the radio.
* @param id of the radio within the home network.
*/
public FullyQualifiedRadioIdentifier(int wacn, int system, int id, Role role)
public FullyQualifiedRadioIdentifier(int localAddress, int wacn, int system, int id, Role role)
{
super(id, role);
super(localAddress > 0 ? localAddress : id, role);
mWacn = wacn;
mSystem = system;
mRadio = id;
}
public int getWacn()
{
return mWacn;
}
public int getSystem()
{
return mSystem;
}
public int getRadio()
{
return mRadio;
}
/**
* Fully qualified radio identity.
* @return radio identity
*/
public String getFullyQualifiedRadioAddress()
{
return mWacn + "." + mSystem + "." + mRadio;
}
/**
* Indicates if the radio identify is aliased with a persona value that is different from the radio ID.
* @return true if aliased.
*/
public boolean isAliased()
{
return getValue() != mRadio;
}
@Override
public String toString()
{
return mWacn + "." + mSystem + "." + super.toString();
if(isAliased())
{
return super.toString() + "(" + getFullyQualifiedRadioAddress() + ")";
}
else
{
return getFullyQualifiedRadioAddress();
}
}
}

View File

@@ -26,7 +26,14 @@ import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
/**
* Radio identifier.
*
* Note: this class overrides the .equals method to ensure that all radios and subclasses can be compared using
* the radio value, regardless if it is a simple radio or a fully qualified radio identifier.
*/
public abstract class RadioIdentifier extends IntegerIdentifier
{
public RadioIdentifier(Integer value, Role role)
@@ -39,4 +46,28 @@ public abstract class RadioIdentifier extends IntegerIdentifier
{
return getValue() > 0;
}
/**
* Overrides to compare just the radio value, class, form, role and protocol. This allows a fully qualified
* radio identifier to be equivalent to a standard radio identifier for the traffic channel manager.
*/
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(o instanceof RadioIdentifier radio)
{
return (getValue().intValue() == radio.getValue().intValue()) &&
getIdentifierClass() == radio.getIdentifierClass() &&
getForm() == radio.getForm() &&
getRole() == radio.getRole() &&
getProtocol() == radio.getProtocol();
}
return false;
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.identifier.talkgroup;
@@ -31,23 +28,67 @@ public abstract class FullyQualifiedTalkgroupIdentifier extends TalkgroupIdentif
{
private int mWacn;
private int mSystem;
private int mTalkgroup;
/**
* Constructs an instance
* @param wacn
* @param system
* @param id
* @param localAddress used on the local system as an alias to the fully qualified talkgroup.
* @param wacn for the talkgroup home system.
* @param system for the talkgroup home system.
* @param id for the talkgroup within the home system.
* @param role played by the talkgroup.
*/
public FullyQualifiedTalkgroupIdentifier(int wacn, int system, int id, Role role)
public FullyQualifiedTalkgroupIdentifier(int localAddress, int wacn, int system, int id, Role role)
{
super(id, role);
super(localAddress > 0 ? localAddress : id, role);
mWacn = wacn;
mSystem = system;
mTalkgroup = id;
}
public int getWacn()
{
return mWacn;
}
public int getSystem()
{
return mSystem;
}
public int getTalkgroup()
{
return mTalkgroup;
}
/**
* Fully qualified talkgroup identity.
* @return talkgroup identity
*/
public String getFullyQualifiedTalkgroupAddress()
{
return mWacn + "." + mSystem + "." + mTalkgroup;
}
/**
* Indicates if the talkgroup identify is aliased with a persona value that is different from the talkgroup ID.
* @return true if aliased.
*/
public boolean isAliased()
{
return getValue() != mTalkgroup;
}
@Override
public String toString()
{
return mWacn + "." + mSystem + "." + super.toString();
if(isAliased())
{
return super.toString() + "(" + getFullyQualifiedTalkgroupAddress() + ")";
}
else
{
return getFullyQualifiedTalkgroupAddress();
}
}
}

View File

@@ -27,8 +27,19 @@ import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
/**
* Talkgroup identifier.
*
* Note: this class overrides the .equals method to ensure that all talkgroups and subclasses can be compared using
* the talkgroup value, regardless if it is a simple talkgroup or a fully qualified talkgroup identifier.
*/
public abstract class TalkgroupIdentifier extends IntegerIdentifier
{
/**
* Constructs an instance
* @param value for the talkgroup
* @param role for the talkgroup
*/
public TalkgroupIdentifier(Integer value, Role role)
{
super(value, IdentifierClass.USER, Form.TALKGROUP, role);
@@ -39,4 +50,28 @@ public abstract class TalkgroupIdentifier extends IntegerIdentifier
{
return getValue() > 0;
}
/**
* Overrides to compare just the talkgroup value, class, form, role and protocol. This allows a fully qualified
* talkgroup identifier to be equivalent to a standard talkgroup identifier for the traffic channel manager.
*/
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(o instanceof TalkgroupIdentifier tg)
{
return (getValue().intValue() == tg.getValue().intValue()) &&
getIdentifierClass() == tg.getIdentifierClass() &&
getForm() == tg.getForm() &&
getRole() == tg.getRole() &&
getProtocol() == tg.getProtocol();
}
return false;
}
}

View File

@@ -0,0 +1,191 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.message;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.FragmentedIntField;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.bits.LongField;
/**
* Base message class with general utility methods for accessing fields in BinaryMessages.
*/
public abstract class AbstractMessage
{
private CorrectedBinaryMessage mMessage;
private int mOffset = 0;
/**
* Constructs an instance
* @param message containing the message bits.
* @param offset to the start of the message within the corrected binary message.
*/
public AbstractMessage(CorrectedBinaryMessage message, int offset)
{
mMessage = message;
mOffset = offset;
}
/**
* Constructs an instance.
* @param message bits
*/
public AbstractMessage(CorrectedBinaryMessage message)
{
this(message, 0);
}
/**
* Indicates if this message is offset within the corrected binary message.
* @return true of offset is non-zero.
*/
private boolean hasOffset()
{
return mOffset > 0;
}
/**
* Offset to the start of the message (fragement) within the corrected binary message.
*/
public int getOffset()
{
return mOffset;
}
/**
* Binary (bits) message for this message.
* @return corrected binary message
*/
public CorrectedBinaryMessage getMessage()
{
return mMessage;
}
/**
* Indicates if the field has a non-zero value, meaning that any of the bits for the field are set to a one.
* @param field to inspect.
* @return true if the field has a non-zero value.
*/
public boolean hasInt(IntField field)
{
if(hasOffset())
{
return getMessage().hasInt(field, getOffset());
}
else
{
return getMessage().hasInt(field);
}
}
/**
* Gets the integer field value and formats it as hexadecimal number
* @param field to parse
* @param places to format (ie 2 for a byte, 4 for 2-bytes, etc.)
* @return formatted hex value.
*/
public String getIntAsHex(IntField field, int places)
{
int value = getInt(field);
String hex = Integer.toHexString(value).toUpperCase();
while(hex.length() < places)
{
hex = "0" + hex;
}
return hex;
}
/**
* Access the integer value for the specified field.
* @param field to access
* @return integer value for the field.
*/
public int getInt(IntField field)
{
if(hasOffset())
{
return getMessage().getInt(field, getOffset());
}
else
{
return getMessage().getInt(field);
}
}
/**
* Indicates if the field has a non-zero value, meaning that any of the bits for the field are set to a one.
* @param field to inspect.
* @return true if the field has a non-zero value.
*/
public boolean hasInt(FragmentedIntField field)
{
if(hasOffset())
{
return getMessage().hasInt(field, getOffset());
}
else
{
return getMessage().hasInt(field);
}
}
/**
* Access the integer value for the specified field.
* @param field to access
* @return integer value for the field.
*/
public int getInt(FragmentedIntField field)
{
if(hasOffset())
{
return getMessage().getInt(field, getOffset());
}
else
{
return getMessage().getInt(field);
}
}
/**
* Access the long value for the specified field.
* @param field to access
* @return value for the field.
*/
public long getLong(LongField field)
{
if(hasOffset())
{
return getMessage().getLong(field, getOffset());
}
else
{
return getMessage().getLong(field);
}
}
/**
* Utility method to format an octet as hex.
*/
public static String formatOctetAsHex(int value)
{
return String.format("%02X", value);
}
}

View File

@@ -0,0 +1,103 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.message;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* Timeslot message base class.
*/
public abstract class TimeslotMessage extends AbstractMessage
{
public static final int TIMESLOT_0 = 0;
public static final int TIMESLOT_1 = 1;
public static final int TIMESLOT_2 = 2;
private long mTimestamp;
private int mTimeslot;
private boolean mValid = true;
/**
* Constructs an instance
* @param message containing binary fields that may contain fields outside of the scope of this class and thus
* requires an offset value that points to the start of this message structure within the larger message context.
* @param offset in the binary message to the start of this chunk of the message.
* @param timeslot for this message
* @param timestamp for this message
*/
public TimeslotMessage(CorrectedBinaryMessage message, int offset, int timeslot, long timestamp)
{
super(message, offset);
mTimeslot = timeslot;
mTimestamp = timestamp;
}
/**
* Constructs an instance
* @param message containing binary fields.
* @param timeslot for this message
* @param timestamp for this message
*/
public TimeslotMessage(CorrectedBinaryMessage message, int timeslot, long timestamp)
{
super(message);
mTimeslot = timeslot;
mTimestamp = timestamp;
}
/**
* Timeslot for this message.
* @return timeslot, default of 0 for no timeslot.
*/
public int getTimeslot()
{
return mTimeslot;
}
/**
* Timestamp when the message was received or processed. This timestamp should be as close to
* accurate as possible.
*/
public long getTimestamp()
{
return mTimestamp;
}
/**
* Decoded textual representation of the message
*/
public abstract String toString();
/**
* Indicates if the message is valid and has passed crc/integrity checks
*/
public boolean isValid()
{
return mValid;
}
/**
* Sets the validity flag to indicate if the message is valid and passed any CRC or integrity checks.
* @param valid flag
*/
public void setValid(boolean valid)
{
mValid = valid;
}
}

View File

@@ -32,6 +32,7 @@ import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.filter.AllPassFilter;
import io.github.dsheirer.filter.FilterSet;
import io.github.dsheirer.filter.IFilter;
import io.github.dsheirer.identifier.patch.PatchGroupManager;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.MessageDirection;
import io.github.dsheirer.module.Module;
@@ -51,6 +52,7 @@ import io.github.dsheirer.module.decode.dmr.audio.DMRAudioModule;
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
import io.github.dsheirer.module.decode.dmr.message.filter.DmrMessageFilterSet;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2Decoder;
@@ -90,6 +92,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.filter.P25P1MessageFi
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
import io.github.dsheirer.module.decode.p25.phase2.P25P2DecoderHDQPSK;
import io.github.dsheirer.module.decode.p25.phase2.P25P2DecoderState;
import io.github.dsheirer.module.decode.p25.phase2.message.P25P2Message;
import io.github.dsheirer.module.decode.p25.phase2.message.filter.P25P2MessageFilterSet;
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
import io.github.dsheirer.module.decode.passport.PassportDecoder;
@@ -149,7 +152,7 @@ public class DecoderFactory
UserPreferences userPreferences, TrafficChannelManager trafficChannelManager,
IChannelDescriptor channelDescriptor)
{
List<Module> modules = new ArrayList<Module>();
List<Module> modules = new ArrayList<>();
AliasList aliasList = aliasModel.getAliasList(channel.getAliasListName());
modules.add(new AliasActionManager(aliasList));
@@ -178,7 +181,8 @@ public class DecoderFactory
processLTRNet(channel, modules, aliasList, (DecodeConfigLTRNet) decodeConfig);
break;
case MPT1327:
processMPT1327(channelMapModel, channel, modules, aliasList, channelType, (DecodeConfigMPT1327) decodeConfig);
processMPT1327(channelMapModel, channel, modules, aliasList, channelType,
(DecodeConfigMPT1327) decodeConfig, userPreferences);
break;
case PASSPORT:
processPassport(channel, modules, aliasList, decodeConfig);
@@ -187,7 +191,7 @@ public class DecoderFactory
processP25Phase1(channel, userPreferences, modules, aliasList, channelType, (DecodeConfigP25Phase1) decodeConfig);
break;
case P25_PHASE2:
processP25Phase2(channel, userPreferences, modules, aliasList);
processP25Phase2(channel, userPreferences, modules, aliasList, trafficChannelManager);
break;
default:
throw new IllegalArgumentException("Unknown decoder type [" + decodeConfig.getDecoderType().toString() + "]");
@@ -202,14 +206,53 @@ public class DecoderFactory
* @param userPreferences reference
* @param modules collection to add to
* @param aliasList for the channel
* @param trafficChannelManager from parent, if this is for a traffic channel, otherwise this can be null and it
* will be created automatically.
*/
private static void processP25Phase2(Channel channel, UserPreferences userPreferences, List<Module> modules, AliasList aliasList) {
private static void processP25Phase2(Channel channel, UserPreferences userPreferences, List<Module> modules,
AliasList aliasList, TrafficChannelManager trafficChannelManager)
{
modules.add(new P25P2DecoderHDQPSK((DecodeConfigP25Phase2)channel.getDecodeConfiguration()));
modules.add(new P25P2DecoderState(channel, 0));
modules.add(new P25P2DecoderState(channel, 1));
modules.add(new P25P2AudioModule(userPreferences, 0, aliasList));
modules.add(new P25P2AudioModule(userPreferences, 1, aliasList));
P25TrafficChannelManager p25TrafficChannelManager = null;
if(channel.getChannelType() == ChannelType.STANDARD)
{
p25TrafficChannelManager = new P25TrafficChannelManager(channel);
}
else if(trafficChannelManager instanceof P25TrafficChannelManager p25)
{
p25TrafficChannelManager = p25;
}
else
{
p25TrafficChannelManager = new P25TrafficChannelManager(channel);
}
//Only add traffic channel manager to the modules if this is the control channel
if(channel.isStandardChannel())
{
modules.add(p25TrafficChannelManager);
}
PatchGroupManager patchGroupManager = new PatchGroupManager();
modules.add(new P25P2DecoderState(channel, P25P2Message.TIMESLOT_1, p25TrafficChannelManager, patchGroupManager));
modules.add(new P25P2DecoderState(channel, P25P2Message.TIMESLOT_2, p25TrafficChannelManager, patchGroupManager));
modules.add(new P25P2AudioModule(userPreferences, P25P2Message.TIMESLOT_1, aliasList));
modules.add(new P25P2AudioModule(userPreferences, P25P2Message.TIMESLOT_2, aliasList));
//Add a channel rotation monitor when we have multiple control channel frequencies specified
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency &&
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies())
{
List<State> activeStates = new ArrayList<>();
activeStates.add(State.CONTROL);
modules.add(new ChannelRotationMonitor(activeStates,
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay(),
userPreferences));
}
}
/**
@@ -219,7 +262,8 @@ public class DecoderFactory
* @param modules collection to add to
* @param aliasList for the channel
*/
private static void processP25Phase1(Channel channel, UserPreferences userPreferences, List<Module> modules, AliasList aliasList, ChannelType channelType, DecodeConfigP25Phase1 decodeConfig) {
private static void processP25Phase1(Channel channel, UserPreferences userPreferences, List<Module> modules, AliasList aliasList, ChannelType channelType, DecodeConfigP25Phase1 decodeConfig)
{
DecodeConfigP25Phase1 p25Config = decodeConfig;
switch(p25Config.getModulation())
@@ -249,13 +293,12 @@ public class DecoderFactory
modules.add(new P25P1AudioModule(userPreferences, aliasList));
//Add a channel rotation monitor when we have multiple control channel frequencies specified
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency &&
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies())
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency sctmf &&
sctmf.hasMultipleFrequencies())
{
List<State> activeStates = new ArrayList<>();
activeStates.add(State.CONTROL);
modules.add(new ChannelRotationMonitor(activeStates,
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay()));
modules.add(new ChannelRotationMonitor(activeStates, sctmf.getFrequencyRotationDelay(), userPreferences));
}
}
@@ -285,7 +328,10 @@ public class DecoderFactory
* @param channelType for control or traffic
* @param decodeConfig configuration
*/
private static void processMPT1327(ChannelMapModel channelMapModel, Channel channel, List<Module> modules, AliasList aliasList, ChannelType channelType, DecodeConfigMPT1327 decodeConfig) {
private static void processMPT1327(ChannelMapModel channelMapModel, Channel channel, List<Module> modules,
AliasList aliasList, ChannelType channelType, DecodeConfigMPT1327 decodeConfig,
UserPreferences userPreferences)
{
DecodeConfigMPT1327 mptConfig = decodeConfig;
ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName());
Sync sync = mptConfig.getSync();
@@ -317,13 +363,12 @@ public class DecoderFactory
}
//Add a channel rotation monitor when we have multiple control channel frequencies specified
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency &&
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies())
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency sctmf &&
sctmf.hasMultipleFrequencies())
{
List<State> activeStates = new ArrayList<>();
activeStates.add(State.CONTROL);
modules.add(new ChannelRotationMonitor(activeStates,
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay()));
modules.add(new ChannelRotationMonitor(activeStates, sctmf.getFrequencyRotationDelay(), userPreferences));
}
}
@@ -440,8 +485,8 @@ public class DecoderFactory
modules.add(dmrTrafficChannelManager);
}
DMRDecoderState state1 = new DMRDecoderState(channel, 1, dmrTrafficChannelManager);
DMRDecoderState state2 = new DMRDecoderState(channel, 2, dmrTrafficChannelManager);
DMRDecoderState state1 = new DMRDecoderState(channel, DMRMessage.TIMESLOT_1, dmrTrafficChannelManager);
DMRDecoderState state2 = new DMRDecoderState(channel, DMRMessage.TIMESLOT_2, dmrTrafficChannelManager);
//Register the states with each other so that they can pass Cap+ site status messaging to resolve current channel
state1.setSisterDecoderState(state2);
@@ -457,7 +502,7 @@ public class DecoderFactory
lsn.setTimeslotFrequency(rest.getTimeslotFrequency());
}
if(lsn.getTimeslot() == 1)
if(lsn.getTimeslot() == DMRMessage.TIMESLOT_1)
{
state1.setCurrentChannel(lsn);
state2.setCurrentChannel(lsn.getSisterTimeslot());
@@ -473,7 +518,7 @@ public class DecoderFactory
{
DecodeEvent event = decodeConfig.getChannelGrantEvent();
if(decodeConfig.getChannelGrantEvent().getTimeslot() == 1)
if(decodeConfig.getChannelGrantEvent().getTimeslot() == DMRMessage.TIMESLOT_1)
{
state1.setCurrentCallEvent(event);
@@ -495,17 +540,16 @@ public class DecoderFactory
modules.add(state1);
modules.add(state2);
modules.add(new DMRAudioModule(userPreferences, aliasList, 1));
modules.add(new DMRAudioModule(userPreferences, aliasList, 2));
modules.add(new DMRAudioModule(userPreferences, aliasList, DMRMessage.TIMESLOT_1));
modules.add(new DMRAudioModule(userPreferences, aliasList, DMRMessage.TIMESLOT_2));
//Add a channel rotation monitor when we have multiple control channel frequencies specified
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency &&
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies())
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency sctmf &&
sctmf.hasMultipleFrequencies())
{
List<State> activeStates = new ArrayList<>();
activeStates.add(State.CONTROL);
modules.add(new ChannelRotationMonitor(activeStates,
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay()));
modules.add(new ChannelRotationMonitor(activeStates, sctmf.getFrequencyRotationDelay(), userPreferences));
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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;
@@ -34,8 +31,7 @@ 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.phase1.DecodeConfigP25Phase1;
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25;
import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
@@ -47,10 +43,9 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
@JsonSubTypes.Type(value = DecodeConfigLTRStandard.class, name = "decodeConfigLTRStandard"),
@JsonSubTypes.Type(value = DecodeConfigMPT1327.class, name = "decodeConfigMPT1327"),
@JsonSubTypes.Type(value = DecodeConfigNBFM.class, name = "decodeConfigNBFM"),
@JsonSubTypes.Type(value = DecodeConfigP25Phase1.class, name = "decodeConfigP25Phase1"),
@JsonSubTypes.Type(value = DecodeConfigP25Phase2.class, name = "decodeConfigP25Phase2"),
@JsonSubTypes.Type(value = DecodeConfigP25.class, name = "decodeConfigP25"),
@JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport"),
@JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR")
@JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR")
})
@JacksonXmlRootElement(localName = "decode_configuration")
public abstract class DecodeConfiguration extends Configuration
@@ -58,10 +53,7 @@ public abstract class DecodeConfiguration extends Configuration
public static final int DEFAULT_CALL_TIMEOUT_DELAY_SECONDS = 45;
public static final int CALL_TIMEOUT_MINIMUM = 1;
public static final int CALL_TIMEOUT_MAXIMUM = 180;
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 TRAFFIC_CHANNEL_LIMIT_DEFAULT = 20;
public DecodeConfiguration()
{

View File

@@ -124,7 +124,6 @@ public class DMRDecoderState extends TimeslotDecoderState
private DMRNetworkConfigurationMonitor mNetworkConfigurationMonitor;
private DMRTrafficChannelManager mTrafficChannelManager;
private DecodeEvent mCurrentCallEvent;
private long mCurrentFrequency;
private boolean mIgnoreCRCChecksums;
private DMRDecoderState mSisterDecoderState;
@@ -275,8 +274,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
super.reset();
resetState();
mCurrentFrequency = 0;
setCurrentFrequency(0);
}
/**
@@ -347,11 +345,11 @@ public class DMRDecoderState extends TimeslotDecoderState
private void updateRestChannel(DMRChannel restChannel)
{
//Only respond if this is a standard/control channel (not a traffic channel).
if(mChannel.isStandardChannel() && mCurrentFrequency > 0 &&
if(mChannel.isStandardChannel() && getCurrentFrequency() > 0 &&
restChannel.getDownlinkFrequency() > 0 &&
restChannel.getDownlinkFrequency() != mCurrentFrequency && mTrafficChannelManager != null)
restChannel.getDownlinkFrequency() != getCurrentFrequency() && mTrafficChannelManager != null)
{
mTrafficChannelManager.convertToTrafficChannel(mChannel, mCurrentFrequency, restChannel,
mTrafficChannelManager.convertToTrafficChannel(mChannel, getCurrentFrequency(), restChannel,
mNetworkConfigurationMonitor);
}
}
@@ -1400,9 +1398,9 @@ public class DMRDecoderState extends TimeslotDecoderState
Event event = (mCurrentCallEvent == null ? Event.START : Event.CONTINUATION);
//Create a repeater channel descriptor if we don't have one
if(mCurrentChannel == null && mCurrentFrequency > 0)
if(mCurrentChannel == null && getCurrentFrequency() > 0)
{
mCurrentChannel = new DMRAbsoluteChannel(getTimeslot(), getTimeslot(), mCurrentFrequency, 0);
mCurrentChannel = new DMRAbsoluteChannel(getTimeslot(), getTimeslot(), getCurrentFrequency(), 0);
}
if(mCurrentCallEvent == null)
@@ -1475,11 +1473,12 @@ public class DMRDecoderState extends TimeslotDecoderState
resetState();
break;
case NOTIFICATION_SOURCE_FREQUENCY:
long previous = mCurrentFrequency;
mCurrentFrequency = event.getFrequency();
if(hasTrafficChannelManager())
setCurrentFrequency(event.getFrequency());
//Only update the traffic channel manager if we're not a traffic channel.
if(hasTrafficChannelManager() && mChannel.isStandardChannel())
{
mTrafficChannelManager.setCurrentControlFrequency(previous, mCurrentFrequency, mChannel);
mTrafficChannelManager.setCurrentControlFrequency(getCurrentFrequency(), mChannel);
}
break;
default:

View File

@@ -132,13 +132,13 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
/**
* Sets the current parent control channel frequency so that channel grants for the current frequency do not
* produce an additional traffic channel allocation.
* @param previousControlFrequency for the current control channel (to remove from allocated channels)
* @param currentControlFrequency for current control channel (to add to allocated channels)
* @param previous for the current control channel (to remove from allocated channels)
* @param current for current control channel (to add to allocated channels)
* @param channel for the current control channel
*/
public void setCurrentControlFrequency(long previousControlFrequency, long currentControlFrequency, Channel channel)
public void processControlFrequencyUpdate(long previous, long current, Channel channel)
{
if(previousControlFrequency == currentControlFrequency)
if(previous == current)
{
return;
}
@@ -147,18 +147,18 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
try
{
Channel existing = mAllocatedChannelFrequencyMap.get(previousControlFrequency);
Channel existing = mAllocatedChannelFrequencyMap.get(previous);
//Only remove the channel if it is non-null and it matches the current control channel.
if(channel.equals(existing))
{
//Unlock the frequency in the channel rotation monitor
getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(previousControlFrequency));
mAllocatedChannelFrequencyMap.remove(previousControlFrequency);
getInterModuleEventBus().post(FrequencyLockChangeRequest.unlock(previous));
mAllocatedChannelFrequencyMap.remove(previous);
}
mAllocatedChannelFrequencyMap.put(currentControlFrequency, channel);
getInterModuleEventBus().post(FrequencyLockChangeRequest.lock(currentControlFrequency));
mAllocatedChannelFrequencyMap.put(current, channel);
getInterModuleEventBus().post(FrequencyLockChangeRequest.lock(current));
}
finally
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import java.util.ArrayList;
@@ -64,10 +65,7 @@ public class DecodeConfigDMR extends DecodeConfiguration
@Override
public int[] getTimeslots()
{
int[] timeslots = new int[2];
timeslots[0] = 1;
timeslots[1] = 2;
return timeslots;
return new int[]{DMRMessage.TIMESLOT_1, DMRMessage.TIMESLOT_2};
}
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -28,6 +28,9 @@ import io.github.dsheirer.protocol.Protocol;
*/
public abstract class DMRMessage extends Message
{
public static final int TIMESLOT_1 = 1;
public static final int TIMESLOT_2 = 2;
private CorrectedBinaryMessage mCorrectedBinaryMessage;
private boolean mValid = true;
private int mTimeslot;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -50,6 +50,28 @@ public class DecodeEvent implements IDecodeEvent
mTimeStart = start;
}
/**
* Sets or changes the decode event type.
* @param type to set.
*/
public void setDecodeEventType(DecodeEventType type)
{
if(type == null)
{
throw new IllegalArgumentException("Decode event type cannot be null");
}
mDecodeEventType = type;
}
/**
* Creates a snapshot of this decode event for offline analysis
*/
public DecodeEventSnapshot getSnapshot()
{
return new DecodeEventSnapshot(this);
}
/**
* Creates a new decode event builder with the specified start timestamp.
* @param decodeEventType for the event
@@ -238,7 +260,8 @@ public class DecodeEvent implements IDecodeEvent
sb.append(" IDS:").append(Joiner.on(",").join(mIdentifierCollection.getIdentifiers()));
}
sb.append(" DURATION:").append(getDuration());
sb.append(" CHANNEL:").append(mChannelDescriptor);
sb.append(" CHANNEL:").append(getChannelDescriptor());
sb.append(" TIMESLOT:").append(getTimeslot());
return sb.toString();
}

View File

@@ -0,0 +1,136 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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 java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Provides duplicate decode event detection support
*/
public class DecodeEventDuplicateDetector
{
private static final long EVENT_MAX_AGE_MILLISECONDS = Duration.ofMinutes(1).toMillis();
private Map<DecodeEventType,DecodeEventTracker> mTrackerMap = new HashMap<>();
/**
* Indicates if the event is a duplicate event.
* @param event to check
* @param timestamp of a current message to trigger time-based event age off.
* @return true if the event is a duplicate.
*/
public synchronized boolean isDuplicate(IDecodeEvent event, long timestamp)
{
//Null event types and voice call event types are not tracked by this detector
if(event.getEventType() == null || event.getEventType().isVoiceCallEvent())
{
return false;
}
if(!mTrackerMap.containsKey(event.getEventType()))
{
mTrackerMap.put(event.getEventType(), new DecodeEventTracker());
}
boolean duplicate = mTrackerMap.get(event.getEventType()).isDuplicate(event);
for(DecodeEventTracker tracker: mTrackerMap.values())
{
tracker.ageOff(timestamp);
}
return duplicate;
}
/**
* Tracks all decode events for a given decode event type
*/
private class DecodeEventTracker
{
private Map<String,IDecodeEvent> mDecodeEventMap = new HashMap<>();
/**
* Checks the event for duplicate
* @param event to check
* @return true if duplicate
*/
public boolean isDuplicate(IDecodeEvent event)
{
if(event.getIdentifierCollection().getToIdentifier() == null || event.getDetails() == null)
{
return false;
}
String key = getKey(event);
if(key == null || key.isEmpty())
{
return false;
}
if(mDecodeEventMap.containsKey(key))
{
return true;
}
else
{
mDecodeEventMap.put(key, event);
}
return false;
}
/**
* Generates a unique event key that is a combinatio of the TO identifier value and the details of the event.
* @param event for the key
* @return generated key.
*/
private static String getKey(IDecodeEvent event)
{
return event.getIdentifierCollection().getToIdentifier().toString() + event.getDetails();
}
/**
* Removes all decode events from the duplicate detection map that are too old.
* @param timestamp for the current messaging.
*/
public void ageOff(long timestamp)
{
long threshold = timestamp - EVENT_MAX_AGE_MILLISECONDS;
List<String> toRemove = new ArrayList<>();
for(Map.Entry<String,IDecodeEvent> entry: mDecodeEventMap.entrySet())
{
if(entry.getValue() == null || entry.getValue().getTimeStart() < threshold)
{
toRemove.add(entry.getKey());
}
}
for(String key: toRemove)
{
mDecodeEventMap.remove(key);
}
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -115,9 +115,9 @@ public class DecodeEventModel extends ClearableHistoryModel<IDecodeEvent> implem
if(channelDescriptor != null)
{
if(event.hasTimeslot())
if(event.hasTimeslot() && !event.toString().contains("TS"))
{
return channelDescriptor + " TS:" + event.getTimeslot();
return channelDescriptor + " TS" + event.getTimeslot();
}
else
{
@@ -126,9 +126,9 @@ public class DecodeEventModel extends ClearableHistoryModel<IDecodeEvent> implem
}
else
{
if(event.hasTimeslot())
if(event.hasTimeslot() && !event.toString().contains("TS"))
{
return "TS:" + event.getTimeslot();
return "TS" + event.getTimeslot();
}
else
{

View File

@@ -0,0 +1,50 @@
package io.github.dsheirer.module.decode.event;
import io.github.dsheirer.identifier.IdentifierCollection;
/**
* Snapshot of a decode event that also preserves the object instance hash code.
*/
public class DecodeEventSnapshot extends DecodeEvent
{
private int mOriginalHashCode;
/**
* Constructs an instance
*
* @param decodeEvent to snapshot
*/
public DecodeEventSnapshot(DecodeEvent decodeEvent)
{
super(decodeEvent.getEventType(), decodeEvent.getTimeStart());
mOriginalHashCode = decodeEvent.hashCode();
setDetails(decodeEvent.getDetails());
setDuration(decodeEvent.getDuration());
setProtocol(decodeEvent.getProtocol());
setTimeslot(decodeEvent.getTimeslot());
setChannelDescriptor(decodeEvent.getChannelDescriptor());
if(decodeEvent.getIdentifierCollection() != null)
{
setIdentifierCollection(new IdentifierCollection(decodeEvent.getIdentifierCollection().getIdentifiers()));
}
}
/**
* Hashcode of the original decode event for this snapshot (not this snapshot instance's hash code).
* @return
*/
public int getOriginalHashCode()
{
return mOriginalHashCode;
}
/**
* Frequency for the channel descriptor
* @return frequency or zero.
*/
public long getFrequency()
{
return getChannelDescriptor() != null ? getChannelDescriptor().getDownlinkFrequency() : 0;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -55,6 +55,7 @@ public enum DecodeEventType
DATA_CALL_ENCRYPTED("Encrypted Data Call"),
DATA_PACKET("Data Packet"),
DEREGISTER("Deregister"),
DYNAMIC_REGROUP("Dynamic Regroup"),
EMERGENCY("EMERGENCY"),
FUNCTION("Function"),
GPS("GPS"),
@@ -106,7 +107,7 @@ public enum DecodeEventType
public static final EnumSet<DecodeEventType> COMMANDS = EnumSet.of(DecodeEventType.ANNOUNCEMENT,
DecodeEventType.STATION_ID, DecodeEventType.ACKNOWLEDGE, DecodeEventType.PAGE, DecodeEventType.QUERY,
DecodeEventType.RADIO_CHECK, DecodeEventType.STATUS, DecodeEventType.COMMAND, DecodeEventType.EMERGENCY,
DecodeEventType.NOTIFICATION, DecodeEventType.FUNCTION);
DecodeEventType.NOTIFICATION, DecodeEventType.FUNCTION, DecodeEventType.DYNAMIC_REGROUP);
/**
* Data call event types for filtering
@@ -129,6 +130,13 @@ public enum DecodeEventType
public static final EnumSet<DecodeEventType> OTHERS = EnumSet.copyOf(Arrays.stream(DecodeEventType.values())
.filter(decodeEventType -> !decodeEventType.isGrouped()).toList());
/**
* Voice call event types.
*/
public static final EnumSet<DecodeEventType> VOICE_CALL_EVENTS = EnumSet.of(CALL, CALL_ENCRYPTED, CALL_GROUP,
CALL_GROUP_ENCRYPTED, CALL_PATCH_GROUP, CALL_PATCH_GROUP_ENCRYPTED, CALL_INTERCONNECT,
CALL_INTERCONNECT_ENCRYPTED, CALL_UNIT_TO_UNIT, CALL_UNIT_TO_UNIT_ENCRYPTED);
/**
* Constructor
* @param label for the element
@@ -149,12 +157,12 @@ public enum DecodeEventType
}
/**
* Indicates if this is a call event.
* @return true if this is a call event type.
* Indicates if this is a voice call event.
* @return true if this is a voice call event type.
*/
public boolean isCallEvent()
public boolean isVoiceCallEvent()
{
return VOICE_CALLS.contains(this) || VOICE_CALLS_ENCRYPTED.contains(this) || DATA_CALLS.contains(this);
return VOICE_CALL_EVENTS.contains(this);
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -37,7 +37,7 @@ public class UDPPort extends IntegerIdentifier
*/
public UDPPort(int value, Role role)
{
super(value, IdentifierClass.NETWORK, Form.UDP_PORT, role);
super(value, IdentifierClass.USER_NETWORK_PORT, Form.UDP_PORT, role);
}
@Override

View File

@@ -76,6 +76,32 @@ public class MPT1327TrafficChannelManager extends TrafficChannelManager implemen
mChannelMap = channelMap;
}
/**
* Notification that the control frequency is updated.
* @param previous frequency for the control channel (to remove from allocated channels)
* @param current frequency for the control channel (to add to allocated channels)
* @param channel for the current control channel
*/
@Override
protected void processControlFrequencyUpdate(long previous, long current, Channel channel)
{
MPT1327Channel toRemove = null;
for(MPT1327Channel mpt: mAllocatedTrafficChannelMap.keySet())
{
if(mpt.getDownlinkFrequency() == current)
{
toRemove = mpt;
break;
}
}
if(toRemove != null)
{
broadcast(new ChannelEvent(mAllocatedTrafficChannelMap.get(toRemove), ChannelEvent.Event.REQUEST_DISABLE));
}
}
/**
* Processes channel grants to allocate traffic channels and track overall channel usage. Generates
* decode events for each new channel that is allocated.

View File

@@ -0,0 +1,16 @@
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.module.decode.p25.reference.ServiceOptions;
/**
* APCO25 message that exposes a service options configuration
*/
public interface IServiceOptionsProvider
{
/**
* Service Options
* @return service options
*/
ServiceOptions getServiceOptions();
}

View File

@@ -117,7 +117,7 @@ public class P25ChannelGrantEvent extends P25DecodeEvent
* Sets the channel descriptor for this event
* @param channelDescriptor
*/
public P25ChannelGrantDecodeEventBuilder channel(IChannelDescriptor channelDescriptor)
public P25ChannelGrantDecodeEventBuilder channelDescriptor(IChannelDescriptor channelDescriptor)
{
mChannelDescriptor = channelDescriptor;
return this;

View File

@@ -0,0 +1,22 @@
package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.controller.channel.event.PreloadDataContent;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import java.util.Collection;
/**
* Frequency bands (aka Identifier Update) to preload into a traffic channel.
*/
public class P25FrequencyBandPreloadDataContent extends PreloadDataContent<Collection<IFrequencyBand>>
{
/**
* Constructs an instance
*
* @param data to preload
*/
public P25FrequencyBandPreloadDataContent(Collection<IFrequencyBand> data)
{
super(data);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -23,14 +23,14 @@ package io.github.dsheirer.module.decode.p25.audio;
import io.github.dsheirer.audio.codec.mbe.MBECallSequence;
import io.github.dsheirer.audio.codec.mbe.MBECallSequenceRecorder;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupVoiceChannelUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupVoiceChannelUpdateExplicit;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCTelephoneInterconnectVoiceChannelUser;
@@ -86,9 +86,9 @@ public class P25P1CallSequenceRecorder extends MBECallSequenceRecorder
@Override
public void receive(IMessage message)
{
if(message instanceof P25Message)
if(message instanceof P25P1Message)
{
P25Message p25 = (P25Message)message;
P25P1Message p25 = (P25P1Message)message;
if(p25.isValid())
{
@@ -113,7 +113,7 @@ public class P25P1CallSequenceRecorder extends MBECallSequenceRecorder
/**
* Processes any P25 Phase 1 message
*/
public void process(P25Message message)
public void process(P25P1Message message)
{
if(message instanceof LDUMessage)
{
@@ -200,15 +200,15 @@ public class P25P1CallSequenceRecorder extends MBECallSequenceRecorder
mCallSequence.setToIdentifier(tivcu.getAddress().toString());
mCallSequence.setCallType(CALL_TYPE_TELEPHONE_INTERCONNECT);
break;
case MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_USER:
LCMotorolaPatchGroupVoiceChannelUser mpgvcu = (LCMotorolaPatchGroupVoiceChannelUser)lcw;
case MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_USER:
LCMotorolaGroupRegroupVoiceChannelUser mpgvcu = (LCMotorolaGroupRegroupVoiceChannelUser)lcw;
mCallSequence.setFromIdentifier(mpgvcu.getSourceAddress().toString());
mCallSequence.setToIdentifier(mpgvcu.getGroupAddress().toString());
mCallSequence.setToIdentifier(mpgvcu.getSupergroupAddress().toString());
mCallSequence.setCallType(CALL_TYPE_GROUP);
break;
case MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE:
LCMotorolaPatchGroupVoiceChannelUpdate mpgvcup = (LCMotorolaPatchGroupVoiceChannelUpdate)lcw;
mCallSequence.setToIdentifier(mpgvcup.getPatchGroup().toString());
case MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_UPDATE:
LCMotorolaGroupRegroupVoiceChannelUpdate mpgvcup = (LCMotorolaGroupRegroupVoiceChannelUpdate)lcw;
mCallSequence.setToIdentifier(mpgvcup.getSupergroupAddress().toString());
mCallSequence.setCallType(CALL_TYPE_GROUP);
break;
case CALL_TERMINATION_OR_CANCELLATION:

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -36,6 +36,8 @@ import io.github.dsheirer.identifier.tone.ToneSequence;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageProvider;
import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructure;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.PushToTalk;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import io.github.dsheirer.preference.UserPreferences;
@@ -111,10 +113,8 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
{
if(message.getTimeslot() == getTimeslot())
{
if(message instanceof AbstractVoiceTimeslot)
if(message instanceof AbstractVoiceTimeslot abstractVoiceTimeslot)
{
AbstractVoiceTimeslot abstractVoiceTimeslot = (AbstractVoiceTimeslot)message;
if(mEncryptedCallStateEstablished)
{
if(!mEncryptedCall)
@@ -128,13 +128,18 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
mQueuedAudioTimeslots.offer(abstractVoiceTimeslot);
}
}
else if(message instanceof PushToTalk && message.isValid())
else if(message instanceof MacMessage macMessage && message.isValid())
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = ((PushToTalk)message).isEncrypted();
MacStructure macStructure = macMessage.getMacStructure();
//There should not be any pending voice timeslots to process since the PTT message is the first in
//the audio call sequence
if(macStructure instanceof PushToTalk pushToTalk)
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = pushToTalk.isEncrypted();
//There should not be any pending voice timeslots to process since the PTT message is the first in
//the audio call sequence.
clearPendingVoiceTimeslots();
}
}
else if(message instanceof EncryptionSynchronizationSequence && message.isValid())
{
@@ -159,6 +164,14 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
}
}
/**
* Clears/deletes any pending voice timeslots
*/
private void clearPendingVoiceTimeslots()
{
mQueuedAudioTimeslots.clear();
}
/**
* Process the audio voice frames
* @param voiceFrames to process

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.audio;
@@ -30,21 +27,20 @@ import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence;
import io.github.dsheirer.module.decode.p25.phase2.message.P25P2Message;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacStructure;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.EndPushToTalk;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.GroupVoiceChannelUserAbbreviated;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.GroupVoiceChannelUserExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructure;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.PushToTalk;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.TelephoneInterconnectVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserAbbreviated;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserExtended;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import io.github.dsheirer.preference.UserPreferences;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* P25 Phase 2 AMBE Frame recorder generates P25 call sequence recordings containing JSON representations of audio
* frames, optional encryption and call identifiers.
@@ -229,19 +225,19 @@ public class P25P2CallSequenceRecorder extends MBECallSequenceRecorder
case END_PUSH_TO_TALK:
processEndPTT(mac);
break;
case TDMA_1_GROUP_VOICE_CHANNEL_USER_ABBREVIATED:
case TDMA_01_GROUP_VOICE_CHANNEL_USER_ABBREVIATED:
processGVCUA(mac);
break;
case TDMA_2_UNIT_TO_UNIT_VOICE_CHANNEL_USER:
case TDMA_02_UNIT_TO_UNIT_VOICE_CHANNEL_USER_ABBREVIATED:
processUTUVCU(mac);
break;
case TDMA_3_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER:
case TDMA_03_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER:
processTIVCU(mac);
break;
case TDMA_33_GROUP_VOICE_CHANNEL_USER_EXTENDED:
case TDMA_21_GROUP_VOICE_CHANNEL_USER_EXTENDED:
processGVCUE(mac);
break;
case TDMA_34_UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED:
case TDMA_22_UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED:
processUTUVCUE(mac);
break;
}
@@ -258,7 +254,7 @@ public class P25P2CallSequenceRecorder extends MBECallSequenceRecorder
if(mac instanceof UnitToUnitVoiceChannelUserExtended)
{
UnitToUnitVoiceChannelUserExtended uuvcue = (UnitToUnitVoiceChannelUserExtended)mac;
mCallSequence.setFromIdentifier(uuvcue.getSourceAddress().toString());
mCallSequence.setFromIdentifier(uuvcue.getSource().toString());
mCallSequence.setToIdentifier(uuvcue.getTargetAddress().toString());
mCallSequence.setCallType(CALL_TYPE_INDIVIDUAL);
if(uuvcue.getServiceOptions().isEncrypted())
@@ -276,7 +272,7 @@ public class P25P2CallSequenceRecorder extends MBECallSequenceRecorder
if(mac instanceof GroupVoiceChannelUserExtended)
{
GroupVoiceChannelUserExtended gvcue = (GroupVoiceChannelUserExtended)mac;
mCallSequence.setFromIdentifier(gvcue.getSourceAddress().toString());
mCallSequence.setFromIdentifier(gvcue.getSource().toString());
mCallSequence.setToIdentifier(gvcue.getGroupAddress().toString());
mCallSequence.setCallType(CALL_TYPE_GROUP);
if(gvcue.getServiceOptions().isEncrypted())
@@ -294,7 +290,7 @@ public class P25P2CallSequenceRecorder extends MBECallSequenceRecorder
if(mac instanceof TelephoneInterconnectVoiceChannelUser)
{
TelephoneInterconnectVoiceChannelUser tivcu = (TelephoneInterconnectVoiceChannelUser)mac;
mCallSequence.setToIdentifier(tivcu.getToOrFromAddress().toString());
mCallSequence.setToIdentifier(tivcu.getTargetAddress().toString());
mCallSequence.setCallType(CALL_TYPE_TELEPHONE_INTERCONNECT);
if(tivcu.getServiceOptions().isEncrypted())
{

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,7 +14,7 @@
*
* 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.identifier;
@@ -46,4 +45,10 @@ public class APCO25Lra extends IntegerIdentifier
{
return new APCO25Lra(lra);
}
@Override
public String toString()
{
return getValue() + "/x" + String.format("%02X", getValue());
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,7 +14,7 @@
*
* 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.identifier;
@@ -52,4 +51,10 @@ public class APCO25Nac extends IntegerIdentifier
{
return new APCO25Nac(nac);
}
@Override
public String toString()
{
return getValue() + "/x" + String.format("%03X", getValue());
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,7 +14,7 @@
*
* 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.identifier;
@@ -46,4 +45,10 @@ public class APCO25Rfss extends IntegerIdentifier
{
return new APCO25Rfss(rfss);
}
@Override
public String toString()
{
return getValue() + "/x" + String.format("%02X", getValue());
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,12 +14,11 @@
*
* 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.identifier;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
@@ -42,8 +40,14 @@ public class APCO25Site extends IntegerIdentifier
/**
* Creates a new APCO-25 site identifier
*/
public static Identifier create(int site)
public static IntegerIdentifier create(int site)
{
return new APCO25Site(site);
}
@Override
public String toString()
{
return getValue() + "/x" + String.format("%02X", getValue());
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,7 +14,7 @@
*
* 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.identifier;
@@ -45,4 +44,11 @@ public class APCO25System extends IntegerIdentifier
{
return new APCO25System(system);
}
@Override
public String toString()
{
return getValue() + "/x" + String.format("%03X", getValue());
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,7 +14,7 @@
*
* 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.identifier;
@@ -46,4 +45,11 @@ public class APCO25Wacn extends IntegerIdentifier
{
return new APCO25Wacn(wacn);
}
@Override
public String toString()
{
return getValue() + "/x" + String.format("%05X", getValue());
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.identifier.channel;
@@ -25,7 +22,6 @@ package io.github.dsheirer.module.decode.p25.identifier.channel;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
import java.util.Objects;
public class P25Channel implements IChannelDescriptor
@@ -151,17 +147,24 @@ public class P25Channel implements IChannelDescriptor
}
/**
* Timeslot for a TDMA channel
* @return timeslot or 0 if the channel is not a TDMA channel
* Timeslot for a TDMA channel, 1 or 2
*
* Note: the ICD uses Logical Channel 0 (LCH 0) and Logical Channel 1 (LCH 1) but we refer to them as TS1 and TS2
* so that timeslot 0 can be used for non timeslot signalling like Sync Loss messages.
*
* @return timeslot or 1 if the channel is not a TDMA channel
*/
public int getTimeslot()
{
if(isTDMAChannel())
{
return getDownlinkChannelNumber() % getTimeslotCount();
if(getDownlinkChannelNumber() % getTimeslotCount() == 1)
{
return 2;
}
}
return 0;
return 1;
}
/**

View File

@@ -1,9 +1,27 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.identifier.channel;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol;
import java.text.DecimalFormat;
/**
@@ -11,7 +29,7 @@ import java.text.DecimalFormat;
*/
public class StandardChannel implements IChannelDescriptor
{
private static final DecimalFormat FREQUENCY_FORMATTER = new DecimalFormat("0.000");
private static final DecimalFormat FREQUENCY_FORMATTER = new DecimalFormat("0.0000");
private long mFrequency;
/**

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,12 +14,13 @@
*
* 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.identifier.patch;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.protocol.Protocol;
/**
@@ -43,4 +43,9 @@ public class APCO25PatchGroup extends PatchGroupIdentifier
{
return new APCO25PatchGroup(patchGroup);
}
public static APCO25PatchGroup create(int supergroup)
{
return new APCO25PatchGroup(new PatchGroup(APCO25Talkgroup.create(supergroup)));
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.identifier.radio;
@@ -31,9 +28,17 @@ import io.github.dsheirer.protocol.Protocol;
*/
public class APCO25FullyQualifiedRadioIdentifier extends FullyQualifiedRadioIdentifier
{
public APCO25FullyQualifiedRadioIdentifier(int wacn, int system, int id, Role role)
/**
* Constructs an instance
* @param localAddress radio identifier. This can be the same as the radio ID when the fully qualified radio
* is not being aliased on a local radio system.
* @param wacn of the home network for the radio.
* @param system of the home network for the radio.
* @param id of the radio within the home network.
*/
public APCO25FullyQualifiedRadioIdentifier(int localAddress, int wacn, int system, int id, Role role)
{
super(wacn, system, id, role);
super(localAddress, wacn, system, id, role);
}
@Override
@@ -42,13 +47,31 @@ public class APCO25FullyQualifiedRadioIdentifier extends FullyQualifiedRadioIden
return Protocol.APCO25;
}
public static APCO25FullyQualifiedRadioIdentifier createFrom(int wacn, int system, int id)
/**
* Creates a fully qualified radio and assigns the FROM role.
* @param localAddress radio identifier. This can be the same as the radio ID when the fully qualified radio
* is not being aliased on a local radio system.
* @param wacn of the home network for the radio.
* @param system of the home network for the radio.
* @param id of the radio within the home network.
* @return identifier
*/
public static APCO25FullyQualifiedRadioIdentifier createFrom(int localAddress, int wacn, int system, int id)
{
return new APCO25FullyQualifiedRadioIdentifier(wacn, system, id, Role.FROM);
return new APCO25FullyQualifiedRadioIdentifier(localAddress, wacn, system, id, Role.FROM);
}
public static APCO25FullyQualifiedRadioIdentifier createTo(int wacn, int system, int id)
/**
* Creates a fully qualified radio and assigns the TO role.
* @param localAddress radio identifier. This can be the same as the radio ID when the fully qualified radio
* is not being aliased on a local radio system.
* @param wacn of the home network for the radio.
* @param system of the home network for the radio.
* @param id of the radio within the home network.
* @return identifier
*/
public static APCO25FullyQualifiedRadioIdentifier createTo(int localAddress, int wacn, int system, int id)
{
return new APCO25FullyQualifiedRadioIdentifier(wacn, system, id, Role.TO);
return new APCO25FullyQualifiedRadioIdentifier(localAddress, wacn, system, id, Role.TO);
}
}

View File

@@ -1,29 +1,30 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.identifier.talkgroup;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25FullyQualifiedRadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.protocol.Protocol;
/**
@@ -31,9 +32,17 @@ import io.github.dsheirer.protocol.Protocol;
*/
public class APCO25FullyQualifiedTalkgroupIdentifier extends FullyQualifiedTalkgroupIdentifier
{
public APCO25FullyQualifiedTalkgroupIdentifier(int wacn, int system, int id, Role role)
/**
* Constructs an instance
* @param groupAddress used on the local system as an alias to the fully qualified talkgroup.
* @param wacn for the talkgroup home system.
* @param system for the talkgroup home system.
* @param id for the talkgroup within the home system.
* @param role played by the talkgroup.
*/
public APCO25FullyQualifiedTalkgroupIdentifier(int groupAddress, int wacn, int system, int id, Role role)
{
super(wacn, system, id, role);
super(groupAddress, wacn, system, id, role);
}
@Override
@@ -42,18 +51,39 @@ public class APCO25FullyQualifiedTalkgroupIdentifier extends FullyQualifiedTalkg
return Protocol.APCO25;
}
public static APCO25FullyQualifiedTalkgroupIdentifier createFrom(int wacn, int system, int id)
/**
* Creates an identifier for the fully qualified talkgroup ising the FROM role.
* @param groupAddress used on the local system as an alias to the fully qualified talkgroup.
* @param wacn for the talkgroup home system.
* @param system for the talkgroup home system.
* @param id for the talkgroup within the home system.
*/
public static APCO25FullyQualifiedTalkgroupIdentifier createFrom(int groupAddress, int wacn, int system, int id)
{
return new APCO25FullyQualifiedTalkgroupIdentifier(wacn, system, id, Role.FROM);
return new APCO25FullyQualifiedTalkgroupIdentifier(groupAddress, wacn, system, id, Role.FROM);
}
public static APCO25FullyQualifiedTalkgroupIdentifier createTo(int wacn, int system, int id)
/**
* Creates an identifier for the fully qualified talkgroup ising the TO role.
* @param groupAddress used on the local system as an alias to the fully qualified talkgroup.
* @param wacn for the talkgroup home system.
* @param system for the talkgroup home system.
* @param id for the talkgroup within the home system.
*/
public static APCO25FullyQualifiedTalkgroupIdentifier createTo(int groupAddress, int wacn, int system, int id)
{
return new APCO25FullyQualifiedTalkgroupIdentifier(wacn, system, id, Role.TO);
return new APCO25FullyQualifiedTalkgroupIdentifier(groupAddress, wacn, system, id, Role.TO);
}
public static APCO25FullyQualifiedTalkgroupIdentifier createAny(int wacn, int system, int id)
/**
* Creates an identifier for the fully qualified talkgroup ising the ANY role.
* @param groupAddress used on the local system as an alias to the fully qualified talkgroup.
* @param wacn for the talkgroup home system.
* @param system for the talkgroup home system.
* @param id for the talkgroup within the home system.
*/
public static APCO25FullyQualifiedTalkgroupIdentifier createAny(int groupAddress, int wacn, int system, int id)
{
return new APCO25FullyQualifiedTalkgroupIdentifier(wacn, system, id, Role.ANY);
return new APCO25FullyQualifiedTalkgroupIdentifier(groupAddress, wacn, system, id, Role.ANY);
}
}

View File

@@ -0,0 +1,72 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.phase1;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = DecodeConfigP25Phase1.class, name = "decodeConfigP25Phase1"),
@JsonSubTypes.Type(value = DecodeConfigP25Phase2.class, name = "decodeConfigP25Phase2"),
})
public abstract class DecodeConfigP25 extends DecodeConfiguration
{
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
private boolean mIgnoreDataCalls = false;
public DecodeConfigP25()
{
}
@JacksonXmlProperty(isAttribute = true, localName = "ignore_data_calls")
public boolean getIgnoreDataCalls()
{
return mIgnoreDataCalls;
}
public void setIgnoreDataCalls(boolean ignore)
{
mIgnoreDataCalls = ignore;
}
@JacksonXmlProperty(isAttribute = true, localName = "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;
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 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/>
* * *****************************************************************************
* 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.phase1;
@@ -25,10 +22,12 @@ package io.github.dsheirer.module.decode.p25.phase1;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
public class DecodeConfigP25Phase1 extends DecodeConfiguration
/**
* APCO25 Phase 1 decoder configuration
*/
public class DecodeConfigP25Phase1 extends DecodeConfigP25
{
public static final int CHANNEL_ROTATION_DELAY_MINIMUM_MS = 400;
public static final int CHANNEL_ROTATION_DELAY_DEFAULT_MS = 500;
@@ -36,9 +35,9 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration
private P25P1Decoder.Modulation mModulation = P25P1Decoder.Modulation.C4FM;
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
private boolean mIgnoreDataCalls = false;
/**
* Constructs an instance
*/
public DecodeConfigP25Phase1()
{
}
@@ -60,37 +59,6 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration
mModulation = modulation;
}
@JacksonXmlProperty(isAttribute = true, localName = "ignore_data_calls")
public boolean getIgnoreDataCalls()
{
return mIgnoreDataCalls;
}
public void setIgnoreDataCalls(boolean ignore)
{
mIgnoreDataCalls = ignore;
}
@JacksonXmlProperty(isAttribute = true, localName = "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;
}
/**
* Source channel specification for this decoder
*/

View File

@@ -18,11 +18,16 @@
*/
package io.github.dsheirer.module.decode.p25.phase1;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.dsp.squelch.PowerMonitor;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
@@ -52,6 +57,18 @@ public abstract class P25P1Decoder extends FeedbackDecoder implements ISourceEve
getDibitBroadcaster().addListener(mByteBufferAssembler);
}
/**
* Preloads P25 frequency bands when this is a traffic channel, since they are not transmitted on the traffic
* channel and the decoder state needs accurate frequency values to pass to the traffic channel manager for
* call event tracking.
* @param preLoadDataContent with known frequency bands.
*/
@Subscribe
public void process(P25FrequencyBandPreloadDataContent preLoadDataContent)
{
mMessageProcessor.preload(preLoadDataContent);
}
@Override
public void setSourceEventListener(Listener<SourceEvent> listener )
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -30,7 +30,6 @@ import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.Message;
import io.github.dsheirer.message.MessageProviderModule;
import io.github.dsheirer.message.StuffBitsMessage;
import io.github.dsheirer.message.SyncLossMessage;
@@ -40,8 +39,8 @@ import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket;
import io.github.dsheirer.module.decode.ip.udp.UDPPacket;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import io.github.dsheirer.module.decode.p25.phase1.message.P25MessageFactory;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUMessageFactory;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUSequence;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.PacketMessage;
@@ -73,7 +72,7 @@ public class P25P1MessageFramer implements Listener<Dibit>, IP25P1DataUnitDetect
private P25P1DataUnitDetector mDataUnitDetector;
private P25P1ChannelStatusProcessor mChannelStatusProcessor = new P25P1ChannelStatusProcessor();
private Listener<Message> mMessageListener;
private Listener<IMessage> mMessageListener;
private boolean mAssemblingMessage = false;
private CorrectedBinaryMessage mBinaryMessage;
private P25P1DataUnitID mDataUnitID;
@@ -153,7 +152,7 @@ public class P25P1MessageFramer implements Listener<Dibit>, IP25P1DataUnitDetect
*
* @param messageListener to receive framed and decoded messages
*/
public void setListener(Listener<Message> messageListener)
public void setListener(Listener<IMessage> messageListener)
{
mMessageListener = messageListener;
}
@@ -346,7 +345,7 @@ public class P25P1MessageFramer implements Listener<Dibit>, IP25P1DataUnitDetect
}
break;
default:
P25Message message = P25MessageFactory.create(mDataUnitID, mNAC, getTimestamp(), mBinaryMessage);
P25P1Message message = P25MessageFactory.create(mDataUnitID, mNAC, getTimestamp(), mBinaryMessage);
mMessageListener.receive(message);
reset(mDataUnitID.getMessageLength());
break;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -20,44 +20,81 @@ package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.ExtendedSourceLinkControlWord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSourceIDExtension;
import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU1Message;
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class P25P1MessageProcessor implements Listener<Message>
/**
* APCO25 Phase 1 Message Processor.
*
* Performs post-message creation processing before the message is sent downstream.
*/
public class P25P1MessageProcessor implements Listener<IMessage>
{
private final static Logger mLog = LoggerFactory.getLogger(P25P1MessageProcessor.class);
/**
* Downstream message listener
*/
private Listener<IMessage> 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 */
/**
* 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 Map<Integer,IFrequencyBand> mFrequencyBandMap = new TreeMap<Integer,IFrequencyBand>();
/**
* Temporary holding of an extended source link control message while it awaits the extension message to arrive.
*/
private ExtendedSourceLinkControlWord mExtendedSourceLinkControlWord;
public P25P1MessageProcessor()
{
}
@Override
public void receive(Message message)
/**
* Preloads frequency band information from the control channel.
* @param content to preload
*/
public void preload(P25FrequencyBandPreloadDataContent content)
{
if(content.hasData())
{
for(IFrequencyBand frequencyBand: content.getData())
{
mFrequencyBandMap.put(frequencyBand.getIdentifier(), frequencyBand);
}
}
}
@Override
public void receive(IMessage 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 frequency band identifier update messages into channel-type messages */
//Reassemble extended source link control messages.
if(message instanceof LDU1Message ldu)
{
reassembleLC(ldu.getLinkControlWord());
}
else if(message instanceof TDULinkControlMessage tdu)
{
reassembleLC(tdu.getLinkControlWord());
}
//Insert frequency band identifier update messages into channel-type messages */
if(message instanceof IFrequencyBandReceiver)
{
IFrequencyBandReceiver receiver = (IFrequencyBandReceiver)message;
@@ -78,8 +115,7 @@ public class P25P1MessageProcessor implements Listener<Message>
}
}
/* Store band identifiers so that they can be injected into channel
* type messages */
//Store band identifiers so that they can be injected into channel type messages
if(message instanceof IFrequencyBand)
{
IFrequencyBand bandIdentifier = (IFrequencyBand)message;
@@ -93,6 +129,30 @@ public class P25P1MessageProcessor implements Listener<Message>
}
}
/**
* Processes link control words to reassemble source ID extension messages.
* @param linkControlWord to process
*/
private void reassembleLC(LinkControlWord linkControlWord)
{
if(linkControlWord instanceof ExtendedSourceLinkControlWord eslcw)
{
mExtendedSourceLinkControlWord = eslcw;
}
else if(linkControlWord instanceof LCSourceIDExtension sie && mExtendedSourceLinkControlWord != null)
{
mExtendedSourceLinkControlWord.setSourceIDExtension(sie);
mExtendedSourceLinkControlWord = null;
}
else
{
//The source extension message should always immediately follow the message that is being extended, so if
//we get a message that is not an extended message or the extension, then we've missed the extension and we
//should nullify any extended message that's waiting for the extension.
mExtendedSourceLinkControlWord = null;
}
}
public void dispose()
{
mFrequencyBandMap.clear();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -45,16 +45,15 @@ import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.SND
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.SecondaryControlChannelBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.SecondaryControlChannelBroadcastExplicit;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.SystemServiceBroadcast;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tracks the network configuration details of a P25 Phase 1 network from the broadcast messages
@@ -236,7 +235,7 @@ public class P25P1NetworkConfigurationMonitor
}
break;
case CHANNEL_IDENTIFIER_UPDATE:
case CHANNEL_IDENTIFIER_UPDATE_EXPLICIT:
case CHANNEL_IDENTIFIER_UPDATE_VU:
if(lcw instanceof IFrequencyBand)
{
IFrequencyBand band = (IFrequencyBand)lcw;

View File

@@ -0,0 +1,95 @@
package io.github.dsheirer.module.decode.p25.phase1.message;
import org.apache.commons.math3.util.FastMath;
/**
* Utility class to declare a frequency band
*/
public class P25FrequencyBand implements IFrequencyBand
{
private int mIdentifier;
private long mBaseFrequency;
private long mTransmitOffset;
private long mChannelSpacing;
private int mBandwidth;
private int mTimeslotCount;
/**
* Construct an instance
* @param identifier for this frequency band
* @param baseFrequency in Hertz
* @param transmitOffset in Hertz
* @param channelSpacing in Hertz
* @param bandwidth in Hertz
* @param timeslotCount for number of timeslots, 1 or 2
*/
public P25FrequencyBand(int identifier, long baseFrequency, long transmitOffset, long channelSpacing,
int bandwidth, int timeslotCount)
{
mIdentifier = identifier;
mChannelSpacing = channelSpacing;
mBaseFrequency = baseFrequency;
mBandwidth = bandwidth;
mTransmitOffset = transmitOffset;
mTimeslotCount = timeslotCount;
}
@Override
public int getIdentifier()
{
return mIdentifier;
}
@Override
public long getChannelSpacing()
{
return mChannelSpacing;
}
@Override
public long getBaseFrequency()
{
return mBaseFrequency;
}
@Override
public int getBandwidth()
{
return mBandwidth;
}
@Override
public long getTransmitOffset()
{
return mTransmitOffset;
}
@Override
public long getDownlinkFrequency(int channelNumber)
{
if(isTDMA())
{
return getBaseFrequency() + (getChannelSpacing() * (int)(FastMath.floor(channelNumber / getTimeslotCount())));
}
return getBaseFrequency() + (getChannelSpacing() * channelNumber);
}
@Override
public long getUplinkFrequency(int channelNumber)
{
return getDownlinkFrequency(channelNumber) + getTransmitOffset();
}
@Override
public boolean isTDMA()
{
return getTimeslotCount() > 1;
}
@Override
public int getTimeslotCount()
{
return mTimeslotCount;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -49,7 +49,7 @@ public class P25MessageFactory
* correction
* @return constructed message parser
*/
public static P25Message create(P25P1DataUnitID dataUnitID, int nac, long timestamp, CorrectedBinaryMessage message)
public static P25P1Message create(P25P1DataUnitID dataUnitID, int nac, long timestamp, CorrectedBinaryMessage message)
{
switch(dataUnitID)
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -20,25 +20,32 @@ package io.github.dsheirer.module.decode.p25.phase1.message;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.message.Message;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.TimeslotMessage;
import io.github.dsheirer.module.decode.p25.identifier.APCO25Nac;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.protocol.Protocol;
public abstract class P25Message extends Message
/**
* Base APCO25 Phase 1 Message
*/
public abstract class P25P1Message extends TimeslotMessage implements IMessage
{
public enum DuplexMode
{
HALF, FULL
}
//P25 Phase 1 ICD defines octets starting at zero.
public static final int OCTET_0_BIT_0 = 0;
public static final int OCTET_1_BIT_8 = 8;
public static final int OCTET_2_BIT_16 = 16;
public static final int OCTET_3_BIT_24 = 24;
public static final int OCTET_4_BIT_32 = 32;
public static final int OCTET_5_BIT_40 = 40;
public static final int OCTET_6_BIT_48 = 48;
public static final int OCTET_7_BIT_56 = 56;
public static final int OCTET_8_BIT_64 = 64;
public static final int OCTET_9_BIT_72 = 72;
public static final int OCTET_10_BIT_80 = 80;
public static final int OCTET_11_BIT_88 = 88;
public static final int OCTET_12_BIT_96 = 96;
public enum SessionMode
{
PACKET, CIRCUIT
}
private CorrectedBinaryMessage mMessage;
private boolean mValid = true;
private Identifier mNAC;
/**
@@ -48,10 +55,9 @@ public abstract class P25Message extends Message
* @param nac Network Access Code (NAC) for the message
* @param timestamp when the message was transmitted
*/
public P25Message(CorrectedBinaryMessage message, int nac, long timestamp)
public P25P1Message(CorrectedBinaryMessage message, int nac, long timestamp)
{
super(timestamp);
mMessage = message;
super(message, 0, timestamp);
mNAC = APCO25Nac.create(nac);
}
@@ -61,7 +67,7 @@ public abstract class P25Message extends Message
* @param message containing the binary message and optional corrected bit count
* @param nac Network Access Code (NAC) for the message
*/
public P25Message(CorrectedBinaryMessage message, int nac)
public P25P1Message(CorrectedBinaryMessage message, int nac)
{
this(message, nac, System.currentTimeMillis());
}
@@ -71,28 +77,15 @@ public abstract class P25Message extends Message
* @param nac Network Access Code (NAC) for the message
* @param timestamp when the message was transmitted
*/
protected P25Message(int nac, long timestamp)
protected P25P1Message(int nac, long timestamp)
{
super(timestamp);
super(null, 0, timestamp);
mNAC = APCO25Nac.create(nac);
}
/**
* Establishes the message binary sequence.
* NAC code for this message
*/
protected void setMessage(CorrectedBinaryMessage message)
{
mMessage = message;
}
/**
* The transmitted binary message
*/
public CorrectedBinaryMessage getMessage()
{
return mMessage;
}
public Identifier getNAC()
{
return mNAC;
@@ -125,21 +118,4 @@ public abstract class P25Message extends Message
{
return Protocol.APCO25;
}
/**
* Indicates if this message is valid and has passed all error detection and correction routines.
*/
@Override
public boolean isValid()
{
return mValid;
}
/**
* Sets this message to the valid/invalid state. The default state is valid (true).
*/
protected void setValid(boolean valid)
{
mValid = valid;
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,7 +14,7 @@
*
* 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.phase1.message;
@@ -23,20 +22,19 @@ package io.github.dsheirer.module.decode.p25.phase1.message;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import java.util.Collections;
import java.util.List;
/**
* P25 Message with unknown or unrecognized data unit id
*/
public class UnknownP25Message extends P25Message
public class UnknownP25Message extends P25P1Message
{
private P25P1DataUnitID mDataUnitID;
public UnknownP25Message(CorrectedBinaryMessage message, int nac, long timestamp, P25P1DataUnitID dataUnitID)
{
super(message, nac);
super(message, nac, timestamp);
mDataUnitID = dataUnitID;
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -22,7 +22,7 @@ package io.github.dsheirer.module.decode.p25.phase1.message.filter;
import io.github.dsheirer.filter.Filter;
import io.github.dsheirer.filter.FilterElement;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
import java.util.function.Function;
/**
@@ -51,7 +51,7 @@ public class P25OtherMessageFilter extends Filter<IMessage,String>
@Override
public boolean canProcess(IMessage message)
{
return message instanceof P25Message && super.canProcess(message);
return message instanceof P25P1Message && super.canProcess(message);
}
/**
@@ -62,7 +62,7 @@ public class P25OtherMessageFilter extends Filter<IMessage,String>
@Override
public String apply(IMessage message)
{
if(message instanceof P25Message)
if(message instanceof P25P1Message)
{
return OTHER_KEY;
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -23,7 +23,7 @@ import io.github.dsheirer.filter.FilterSet;
import io.github.dsheirer.filter.SyncLossMessageFilter;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
/**
* Filter set for P25 messages
@@ -53,6 +53,6 @@ public class P25P1MessageFilterSet extends FilterSet<IMessage>
@Override
public boolean canProcess(IMessage message)
{
return message instanceof P25Message || message instanceof SyncLossMessage;
return message instanceof P25P1Message || message instanceof SyncLossMessage;
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 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/>
* * *****************************************************************************
* 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.phase1.message.hdu;
@@ -27,20 +24,19 @@ import io.github.dsheirer.edac.Golay18;
import io.github.dsheirer.edac.ReedSolomon_63_47_17_P25;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class HDUMessage extends P25Message
public class HDUMessage extends P25P1Message
{
private final static Logger mLog = LoggerFactory.getLogger(HDUMessage.class);
private static final int[] GOLAY_WORD_STARTS = {0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 252,
270, 288, 306, 324, 342, 360, 278, 396, 414, 432, 450, 468, 486, 504, 522, 540, 558, 576, 594, 612, 630};
private static final int[] CW_HEX_0 = {0, 1, 2, 3, 4, 5, 6};
private static final int[] CW_HEX_0 = {0, 1, 2, 3, 4, 5};
private static final int[] CW_HEX_1 = {18, 19, 20, 21, 22, 23};
private static final int[] CW_HEX_2 = {36, 37, 38, 39, 40, 41};
private static final int[] CW_HEX_3 = {54, 55, 56, 57, 58, 59};

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.phase1.message.lc;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.radio.FullyQualifiedRadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25FullyQualifiedRadioIdentifier;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSourceIDExtension;
/**
* Extended link control word. Base class for adding in the extension word for messages that require two link control
* fragments to fit the content.
*/
public abstract class ExtendedSourceLinkControlWord extends LinkControlWord
{
private static final IntField SOURCE_ADDRESS = IntField.length24(OCTET_6_BIT_48);
private LCSourceIDExtension mSourceIDExtension;
private FullyQualifiedRadioIdentifier mSourceAddress;
/**
* Constructs a Link Control Word from the binary message sequence.
*
* @param message
*/
public ExtendedSourceLinkControlWord(CorrectedBinaryMessage message)
{
super(message);
}
/**
* Optional source ID extension message for a fully qualified radio source.
*/
protected LCSourceIDExtension getSourceIDExtension()
{
return mSourceIDExtension;
}
/**
* Sets the extension message.
*/
public void setSourceIDExtension(LCSourceIDExtension extension)
{
mSourceIDExtension = extension;
}
/**
* Indicates if this message is carrying a source ID extension message.
*/
protected boolean hasSourceIDExtension()
{
return mSourceIDExtension != null;
}
/**
* Source address
*/
public FullyQualifiedRadioIdentifier getSourceAddress()
{
if(mSourceAddress == null && hasSourceIDExtension())
{
int radio = getInt(SOURCE_ADDRESS);
int wacn = getSourceIDExtension().getWACN();
int system = getSourceIDExtension().getSystem();
int id = getSourceIDExtension().getId();
mSourceAddress = APCO25FullyQualifiedRadioIdentifier.createFrom(radio, wacn, system, id);
}
return mSourceAddress;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -36,8 +36,8 @@ public enum LinkControlOpcode
TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER("TELEPHONE INTERCONNECT VOICE CHANNEL USER", 6),
TELEPHONE_INTERCONNECT_ANSWER_REQUEST("TELEPHONE INTERCONNECT ANSWER REQUEST", 7),
RESERVED_08("RESERVED-08", 8),
RESERVED_09("RESERVED-09", 9),
RESERVED_0A("RESERVED-0A", 10),
SOURCE_ID_EXTENSION("SOURCE ID EXTENSION", 9),
UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED("UNIT-TO-UNIT VOICE CHANNEL USER EXTENDED", 10),
RESERVED_0B("RESERVED-0B", 11),
RESERVED_0C("RESERVED-0C", 12),
RESERVED_0D("RESERVED-0D", 13),
@@ -52,10 +52,10 @@ public enum LinkControlOpcode
CALL_ALERT("CALL ALERT", 22),
EXTENDED_FUNCTION_COMMAND("EXTENDED FUNCTION COMMAND", 23),
CHANNEL_IDENTIFIER_UPDATE("CHANNEL IDENTIFIER UPDATE", 24),
CHANNEL_IDENTIFIER_UPDATE_EXPLICIT("CHANNEL IDENTIFIER UPDATE EXPLICIT", 25),
RESERVED_1A("RESERVED-1A", 26),
RESERVED_1B("RESERVED-1B", 27),
RESERVED_1C("RESERVED-1C", 28),
CHANNEL_IDENTIFIER_UPDATE_VU("CHANNEL IDENTIFIER UPDATE EXPLICIT", 25),
STATUS_UPDATE_EXTENDED("STATUS UPDATE-SOURCE ID REQUIRED", 26),
MESSAGE_UPDATE_EXTENDED("MESSAGE UPDATE-SOURCE ID REQUIRED", 27),
EXTENDED_FUNCTION_COMMAND_EXTENDED("EXTENDED FUNCTION COMMAND-SOURCE ID REQUIRED", 28),
RESERVED_1D("RESERVED-1D", 29),
RESERVED_1E("RESERVED-1E", 30),
RESERVED_1F("RESERVED-1F", 31),
@@ -69,7 +69,7 @@ public enum LinkControlOpcode
ADJACENT_SITE_STATUS_BROADCAST_EXPLICIT("ADJACENT SITE STATUS EXPLICIT", 39),
RFSS_STATUS_BROADCAST_EXPLICIT("RFSS STATUS BROADCAST EXPLICIT", 40),
NETWORK_STATUS_BROADCAST_EXPLICIT("NETWORK STATUS BROADCAST EXPLICIT", 41),
RESERVED_2A("RESERVED-2A", 42),
CONVENTIONAL_FALLBACK_INDICATION("CONVENTIONAL FALLBACK INDICATION", 42),
RESERVED_2B("RESERVED-2B", 43),
RESERVED_2C("RESERVED-2C", 44),
RESERVED_2D("RESERVED-2D", 45),
@@ -92,15 +92,18 @@ public enum LinkControlOpcode
RESERVED_3E("RESERVED-3E", 62),
RESERVED_3F("RESERVED-3F", 63),
MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_USER("PATCH GROUP VOICE CHANNEL USER", 0),
MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE("PATCH GROUP VOICE CHANNEL UPDATE", 1),
MOTOROLA_PATCH_GROUP_ADD("PATCH GROUP ADD", 3),
MOTOROLA_PATCH_GROUP_DELETE("PATCH GROUP DELETE", 4),
MOTOROLA_UNIT_GPS("UNIT GPS", 6),
MOTOROLA_TALK_COMPLETE("TALK_COMPLETE", 15),
MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_USER("MOTOROLA GROUP REGROUP VOICE CHANNEL USER", 0),
MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_UPDATE("MOTOROLA GROUP REGROUP VOICE CHANNEL UPDATE", 1),
MOTOROLA_GROUP_REGROUP_ADD("MOTOROLA GROUP REGROUP ADD", 3),
MOTOROLA_GROUP_REGROUP_DELETE("MOTOROLA GROUP REGROUP DELETE", 4),
MOTOROLA_UNIT_GPS("MOTOROLA UNIT GPS", 6),
MOTOROLA_EMERGENCY_ALARM_ACTIVATION("MOTOROLA EMERGENCY ALARM ACTIVATION", 10),
MOTOROLA_TALK_COMPLETE("MOTOROLA TALK_COMPLETE", 15),
MOTOROLA_RADIO_REPROGRAM_HEADER("MOTOROLA RADIO REPROGRAM HEADER", 21),
MOTOROLA_RADIO_REPROGRAM_RECORD("MOTOROLA RADIO REPROGRAM CONTINUATION", 23),
MOTOROLA_UNKNOWN("MOTOROLA UNKNOWN", -1),
L3HARRIS_UNKNOWN_0A("UNKNOWN OPCODE 10", 10),
L3HARRIS_RETURN_TO_CONTROL_CHANNEL("UNKNOWN OPCODE 10", 10),
L3HARRIS_UNKNOWN_2A("UNKNOWN OPCODE 42", 42),
L3HARRIS_UNKNOWN_2B("UNKNOWN OPCODE 43", 43),
L3HARRIS_UNKNOWN("L3HARRIS UNKNOWN", -1),
@@ -133,19 +136,19 @@ public enum LinkControlOpcode
* Motorola Opcodes
*/
public static final EnumSet<LinkControlOpcode> MOTOROLA_OPCODES =
EnumSet.range(MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_USER, MOTOROLA_UNKNOWN);
EnumSet.range(MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_USER, MOTOROLA_UNKNOWN);
/**
* L3Harris Opcodes
*/
public static final EnumSet<LinkControlOpcode> L3HARRIS_OPCODES = EnumSet.of(L3HARRIS_UNKNOWN_0A,
public static final EnumSet<LinkControlOpcode> L3HARRIS_OPCODES = EnumSet.of(L3HARRIS_RETURN_TO_CONTROL_CHANNEL,
L3HARRIS_UNKNOWN_2A, L3HARRIS_UNKNOWN_2B, L3HARRIS_UNKNOWN);
/**
* Network/channel related opcodes
*/
public static final EnumSet<LinkControlOpcode> NETWORK_OPCODES = EnumSet.of(CHANNEL_IDENTIFIER_UPDATE,
CHANNEL_IDENTIFIER_UPDATE_EXPLICIT, SYSTEM_SERVICE_BROADCAST, SECONDARY_CONTROL_CHANNEL_BROADCAST,
CHANNEL_IDENTIFIER_UPDATE_VU, SYSTEM_SERVICE_BROADCAST, SECONDARY_CONTROL_CHANNEL_BROADCAST,
SECONDARY_CONTROL_CHANNEL_BROADCAST_EXPLICIT, ADJACENT_SITE_STATUS_BROADCAST,
ADJACENT_SITE_STATUS_BROADCAST_EXPLICIT, RFSS_STATUS_BROADCAST, RFSS_STATUS_BROADCAST_EXPLICIT,
NETWORK_STATUS_BROADCAST, NETWORK_STATUS_BROADCAST_EXPLICIT, PROTECTION_PARAMETER_BROADCAST);
@@ -205,7 +208,7 @@ public enum LinkControlOpcode
switch(value)
{
case 10:
return L3HARRIS_UNKNOWN_0A;
return L3HARRIS_RETURN_TO_CONTROL_CHANNEL;
case 42:
return L3HARRIS_UNKNOWN_2A;
case 43:
@@ -217,17 +220,23 @@ public enum LinkControlOpcode
switch(value)
{
case 0:
return MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_USER;
return MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_USER;
case 1:
return MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE;
return MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_UPDATE;
case 3:
return MOTOROLA_PATCH_GROUP_ADD;
return MOTOROLA_GROUP_REGROUP_ADD;
case 4:
return MOTOROLA_PATCH_GROUP_DELETE;
return MOTOROLA_GROUP_REGROUP_DELETE;
case 6:
return MOTOROLA_UNIT_GPS;
case 10:
return MOTOROLA_EMERGENCY_ALARM_ACTIVATION;
case 15:
return MOTOROLA_TALK_COMPLETE;
case 21:
return MOTOROLA_RADIO_REPROGRAM_HEADER;
case 23:
return MOTOROLA_RADIO_REPROGRAM_RECORD;
default:
return MOTOROLA_UNKNOWN;
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,66 +14,73 @@
*
* 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.phase1.message.lc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.FragmentedIntField;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.message.AbstractMessage;
import io.github.dsheirer.module.decode.p25.reference.Vendor;
import java.util.List;
/**
* APCO 25 Link Control Word. This message word is contained in Logical Link Data Unit 1 and Terminator with
* Link Control messages.
*/
public abstract class LinkControlWord
public abstract class LinkControlWord extends AbstractMessage
{
public static final int OCTET_0_BIT_0 = 0;
public static final int OCTET_1_BIT_8 = 8;
public static final int OCTET_2_BIT_16 = 16;
public static final int OCTET_3_BIT_24 = 24;
public static final int OCTET_4_BIT_32 = 32;
public static final int OCTET_5_BIT_40 = 40;
public static final int OCTET_6_BIT_48 = 48;
public static final int OCTET_7_BIT_56 = 56;
public static final int OCTET_8_BIT_64 = 64;
public static final int OCTET_9_BIT_72 = 72;
public static final int OCTET_10_BIT_80 = 80;
public static final int OCTET_11_BIT_88 = 88;
public static final int OCTET_12_BIT_96 = 96;
private static final int ENCRYPTION_FLAG = 0;
private static final int STANDARD_VENDOR_ID_FLAG = 1;
private static final int[] OPCODE = {2, 3, 4, 5, 6, 7};
private static final int[] VENDOR = {8, 9, 10, 11, 12, 13, 14, 15};
private BinaryMessage mMessage;
private static final FragmentedIntField OPCODE = FragmentedIntField.of(2, 3, 4, 5, 6, 7);
private static final IntField VENDOR = IntField.length8(OCTET_1_BIT_8);
private LinkControlOpcode mLinkControlOpcode;
private boolean mValid = true;
private boolean mValid;
/**
* Constructs a Link Control Word from the binary message sequence.
*
* @param message
*/
public LinkControlWord(BinaryMessage message)
public LinkControlWord(CorrectedBinaryMessage message)
{
mMessage = message;
super(message);
}
/**
* Binary message sequence for this LCW
*/
protected BinaryMessage getMessage()
{
return mMessage;
}
/**
* Sets the valid flag for this message to mark a flag as invalid (false) or the default value is true.
*/
public void setValid(boolean valid)
{
mValid = valid;
}
/**
* Indicates if this link control word is valid and has passed all error detection and correction routines.
* Indicates if the message passes CRC checks.
*/
public boolean isValid()
{
return mValid;
}
/**
* Sets the valid flag for this message.
*/
public void setValid(boolean valid)
{
mValid = valid;
}
/**
* Indicates if this is an encrypted LCW
*/
@@ -110,7 +116,7 @@ public abstract class LinkControlWord
/**
* Lookup the Vendor format for the specified LCW
*/
public static Vendor getVendor(BinaryMessage binaryMessage)
public static Vendor getVendor(CorrectedBinaryMessage binaryMessage)
{
if(isStandardVendorFormat(binaryMessage))
{
@@ -140,13 +146,13 @@ public abstract class LinkControlWord
*/
public int getOpcodeNumber()
{
return getMessage().getInt(OPCODE);
return getInt(OPCODE);
}
/**
* Identifies the link control word opcode from the binary message.
*/
public static LinkControlOpcode getOpcode(BinaryMessage binaryMessage)
public static LinkControlOpcode getOpcode(CorrectedBinaryMessage binaryMessage)
{
return LinkControlOpcode.fromValue(binaryMessage.getInt(OPCODE), getVendor(binaryMessage));
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -19,14 +19,17 @@
package io.github.dsheirer.module.decode.p25.phase1.message.lc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode10;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisReturnToControlChannel;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode42;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode43;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupAdd;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupDelete;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaEmergencyAlarmActivation;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupGroupDelete;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupAdd;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupVoiceChannelUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaRadioReprogramHeader;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaRadioReprogramRecord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkComplete;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnknownOpcode;
@@ -34,14 +37,17 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacen
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcastExplicit;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCCallAlert;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCCallTermination;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCChannelIdentifierUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCChannelIdentifierUpdateVU;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCConventionalFallback;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCFrequencyBandUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCFrequencyBandUpdateExplicit;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommandExtended;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupAffiliationQuery;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupVoiceChannelUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupVoiceChannelUpdateExplicit;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCMessageUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCMessageUpdateExtended;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCNetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCNetworkStatusBroadcastExplicit;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCProtectionParameterBroadcast;
@@ -49,8 +55,10 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCRFSSSta
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCRFSSStatusBroadcastExplicit;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSecondaryControlChannelBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSecondaryControlChannelBroadcastExplicit;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSourceIDExtension;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCStatusQuery;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCStatusUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCStatusUpdateExtended;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSystemServiceBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCTelephoneInterconnectAnswerRequest;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCTelephoneInterconnectVoiceChannelUser;
@@ -58,6 +66,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCUnitAut
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCUnitRegistrationCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCUnitToUnitAnswerRequest;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCUnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCUnitToUnitVoiceChannelUserExtended;
/**
* Factory class for creating link control word (LCW) message parsers.
@@ -67,96 +76,114 @@ public class LinkControlWordFactory
/**
* Creates a link control word from the binary message sequence.
*
* @param binaryMessage containing the LCW binary message sequence.
* @param message containing the LCW binary message sequence.
*/
public static LinkControlWord create(BinaryMessage binaryMessage)
public static LinkControlWord create(CorrectedBinaryMessage message)
{
LinkControlOpcode opcode = LinkControlWord.getOpcode(binaryMessage);
LinkControlOpcode opcode = LinkControlWord.getOpcode(message);
switch(opcode)
{
case ADJACENT_SITE_STATUS_BROADCAST:
return new LCAdjacentSiteStatusBroadcast(binaryMessage);
return new LCAdjacentSiteStatusBroadcast(message);
case ADJACENT_SITE_STATUS_BROADCAST_EXPLICIT:
return new LCAdjacentSiteStatusBroadcastExplicit(binaryMessage);
return new LCAdjacentSiteStatusBroadcastExplicit(message);
case CALL_ALERT:
return new LCCallAlert(binaryMessage);
return new LCCallAlert(message);
case CALL_TERMINATION_OR_CANCELLATION:
return new LCCallTermination(binaryMessage);
return new LCCallTermination(message);
case CHANNEL_IDENTIFIER_UPDATE:
return new LCFrequencyBandUpdate(binaryMessage);
case CHANNEL_IDENTIFIER_UPDATE_EXPLICIT:
return new LCFrequencyBandUpdateExplicit(binaryMessage);
return new LCChannelIdentifierUpdate(message);
case CHANNEL_IDENTIFIER_UPDATE_VU:
return new LCChannelIdentifierUpdateVU(message);
case CONVENTIONAL_FALLBACK_INDICATION:
return new LCConventionalFallback(message);
case EXTENDED_FUNCTION_COMMAND:
return new LCExtendedFunctionCommand(binaryMessage);
return new LCExtendedFunctionCommand(message);
case EXTENDED_FUNCTION_COMMAND_EXTENDED:
return new LCExtendedFunctionCommandExtended(message);
case GROUP_AFFILIATION_QUERY:
return new LCGroupAffiliationQuery(binaryMessage);
return new LCGroupAffiliationQuery(message);
case GROUP_VOICE_CHANNEL_USER:
return new LCGroupVoiceChannelUser(binaryMessage);
return new LCGroupVoiceChannelUser(message);
case GROUP_VOICE_CHANNEL_UPDATE:
return new LCGroupVoiceChannelUpdate(binaryMessage);
return new LCGroupVoiceChannelUpdate(message);
case GROUP_VOICE_CHANNEL_UPDATE_EXPLICIT:
return new LCGroupVoiceChannelUpdateExplicit(binaryMessage);
return new LCGroupVoiceChannelUpdateExplicit(message);
case MESSAGE_UPDATE:
return new LCMessageUpdate(binaryMessage);
return new LCMessageUpdate(message);
case MESSAGE_UPDATE_EXTENDED:
return new LCMessageUpdateExtended(message);
case NETWORK_STATUS_BROADCAST:
return new LCNetworkStatusBroadcast(binaryMessage);
return new LCNetworkStatusBroadcast(message);
case NETWORK_STATUS_BROADCAST_EXPLICIT:
return new LCNetworkStatusBroadcastExplicit(binaryMessage);
return new LCNetworkStatusBroadcastExplicit(message);
case PROTECTION_PARAMETER_BROADCAST:
return new LCProtectionParameterBroadcast(binaryMessage);
return new LCProtectionParameterBroadcast(message);
case RFSS_STATUS_BROADCAST:
return new LCRFSSStatusBroadcast(binaryMessage);
return new LCRFSSStatusBroadcast(message);
case RFSS_STATUS_BROADCAST_EXPLICIT:
return new LCRFSSStatusBroadcastExplicit(binaryMessage);
return new LCRFSSStatusBroadcastExplicit(message);
case SECONDARY_CONTROL_CHANNEL_BROADCAST:
return new LCSecondaryControlChannelBroadcast(binaryMessage);
return new LCSecondaryControlChannelBroadcast(message);
case SECONDARY_CONTROL_CHANNEL_BROADCAST_EXPLICIT:
return new LCSecondaryControlChannelBroadcastExplicit(binaryMessage);
return new LCSecondaryControlChannelBroadcastExplicit(message);
case SOURCE_ID_EXTENSION:
return new LCSourceIDExtension(message);
case STATUS_QUERY:
return new LCStatusQuery(binaryMessage);
return new LCStatusQuery(message);
case STATUS_UPDATE:
return new LCStatusUpdate(binaryMessage);
return new LCStatusUpdate(message);
case STATUS_UPDATE_EXTENDED:
return new LCStatusUpdateExtended(message);
case SYSTEM_SERVICE_BROADCAST:
return new LCSystemServiceBroadcast(binaryMessage);
return new LCSystemServiceBroadcast(message);
case TELEPHONE_INTERCONNECT_ANSWER_REQUEST:
return new LCTelephoneInterconnectAnswerRequest(binaryMessage);
return new LCTelephoneInterconnectAnswerRequest(message);
case TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER:
return new LCTelephoneInterconnectVoiceChannelUser(binaryMessage);
return new LCTelephoneInterconnectVoiceChannelUser(message);
case UNIT_AUTHENTICATION_COMMAND:
return new LCUnitAuthenticationCommand(binaryMessage);
return new LCUnitAuthenticationCommand(message);
case UNIT_REGISTRATION_COMMAND:
return new LCUnitRegistrationCommand(binaryMessage);
return new LCUnitRegistrationCommand(message);
case UNIT_TO_UNIT_ANSWER_REQUEST:
return new LCUnitToUnitAnswerRequest(binaryMessage);
return new LCUnitToUnitAnswerRequest(message);
case UNIT_TO_UNIT_VOICE_CHANNEL_USER:
return new LCUnitToUnitVoiceChannelUser(binaryMessage);
return new LCUnitToUnitVoiceChannelUser(message);
case UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED:
return new LCUnitToUnitVoiceChannelUserExtended(message);
case L3HARRIS_UNKNOWN_0A:
return new LCHarrisUnknownOpcode10(binaryMessage);
case L3HARRIS_RETURN_TO_CONTROL_CHANNEL:
return new LCHarrisReturnToControlChannel(message);
case L3HARRIS_UNKNOWN_2A:
return new LCHarrisUnknownOpcode42(binaryMessage);
return new LCHarrisUnknownOpcode42(message);
case L3HARRIS_UNKNOWN_2B:
return new LCHarrisUnknownOpcode43(binaryMessage);
return new LCHarrisUnknownOpcode43(message);
case L3HARRIS_UNKNOWN:
return new UnknownLinkControlWord(binaryMessage);
return new UnknownLinkControlWord(message);
case MOTOROLA_PATCH_GROUP_ADD:
return new LCMotorolaPatchGroupAdd(binaryMessage);
case MOTOROLA_PATCH_GROUP_DELETE:
return new LCMotorolaPatchGroupDelete(binaryMessage);
case MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_USER:
return new LCMotorolaPatchGroupVoiceChannelUser(binaryMessage);
case MOTOROLA_GROUP_REGROUP_ADD:
return new LCMotorolaGroupRegroupAdd(message);
case MOTOROLA_GROUP_REGROUP_DELETE:
return new LCMotorolaGroupGroupDelete(message);
case MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_USER:
return new LCMotorolaGroupRegroupVoiceChannelUser(message);
case MOTOROLA_TALK_COMPLETE:
return new LCMotorolaTalkComplete(binaryMessage);
case MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE:
return new LCMotorolaPatchGroupVoiceChannelUpdate(binaryMessage);
return new LCMotorolaTalkComplete(message);
case MOTOROLA_GROUP_REGROUP_VOICE_CHANNEL_UPDATE:
return new LCMotorolaGroupRegroupVoiceChannelUpdate(message);
case MOTOROLA_UNIT_GPS:
return new LCMotorolaUnitGPS(binaryMessage);
return new LCMotorolaUnitGPS(message);
case MOTOROLA_RADIO_REPROGRAM_HEADER:
return new LCMotorolaRadioReprogramHeader(message);
case MOTOROLA_RADIO_REPROGRAM_RECORD:
return new LCMotorolaRadioReprogramRecord(message);
case MOTOROLA_EMERGENCY_ALARM_ACTIVATION:
return new LCMotorolaEmergencyAlarmActivation(message);
case MOTOROLA_UNKNOWN:
return new LCMotorolaUnknownOpcode(binaryMessage);
return new LCMotorolaUnknownOpcode(message);
default:
return new UnknownLinkControlWord(binaryMessage);
return new UnknownLinkControlWord(message);
}
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2024 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
@@ -15,14 +14,13 @@
*
* 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.phase1.message.lc;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import java.util.Collections;
import java.util.List;
@@ -36,7 +34,7 @@ public class UnknownLinkControlWord extends LinkControlWord
*
* @param message
*/
public UnknownLinkControlWord(BinaryMessage message)
public UnknownLinkControlWord(CorrectedBinaryMessage message)
{
super(message);
}

View File

@@ -0,0 +1,57 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.phase1.message.lc;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.module.decode.p25.IServiceOptionsProvider;
import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions;
/**
* Base voice call link control word
*/
public abstract class VoiceLinkControlMessage extends LinkControlWord implements IServiceOptionsProvider
{
private static final IntField SERVICE_OPTIONS = IntField.length8(OCTET_2_BIT_16);
private VoiceServiceOptions mVoiceServiceOptions;
/**
* Constructs a Link Control Word from the binary message sequence.
*
* @param message
*/
public VoiceLinkControlMessage(CorrectedBinaryMessage message)
{
super(message);
}
/**
* Service Options for this channel
*/
public VoiceServiceOptions getServiceOptions()
{
if(mVoiceServiceOptions == null)
{
mVoiceServiceOptions = new VoiceServiceOptions(getInt(SERVICE_OPTIONS));
}
return mVoiceServiceOptions;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -19,7 +19,8 @@
package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
@@ -28,20 +29,30 @@ import java.util.ArrayList;
import java.util.List;
/**
* L3Harris Unknown Opcode 10 (0x0A)
* L3Harris Opcode 10 (0x0A) - This seems to be a 'return to control channel' or 'private data call paging'.
* <p>
* Observed in a traffic channel carried by a TDU during an SNDCP packet data session. The controller send an 'All
* blocks received' PDU to the radio in the middle of a stream of these TDU/LC messages and the traffic channel ended
* with Call Termination TDULC messages. So, this may be some form of current data session user information update.
* Observed on a phase 1 traffic data channel carried by a TDU during an SNDCP packet data session. The controller sent
* an 'All blocks received' PDU to the radio and an 'Activate TDS context' in the middle of a stream of these TDU/LC
* messages, and then the radio terminated the traffic channel. So, this may be some form of return to control
* channel or maybe a private data call paging where the mobile returns to the control channel to receive the private
* data channel grant for another data call
*
* When the radio returned to the control channel, control sent the following two MAC messages on the Phase 2 control:
* LOCCH-U NAC:9/x009 SIGNAL CUSTOM/UNKNOWN VENDOR:HARRIS ID:A4 OPCODE:160 LENGTH:9 MSG:A0A409AC0312014871 (radio 0x014871 go to data channel 0x0312??)
* LOCCH-U NAC:9/x009 SIGNAL CUSTOM/UNKNOWN VENDOR:HARRIS ID:A4 OPCODE:172 LENGTH:12 MSG:ACA40C000312014871980418 (from 0x014871 to 0x980418 unit-2-unit data channel grant?)
*
* Both messages seem to refer to channel 0-786 (0x0312) so this may be a unit-2-unit private Phase 1 call or maybe
* a private data call. Radio addresses: 0x014871 and 0x980418
*
* In a second observation (Rockwall, TX), the control channel used the SNDCP data channel grant to send the
* mobile to the data channel and then the data channel transmitted a sequence of TDULCs containing only this message.
*
*/
public class LCHarrisUnknownOpcode10 extends LinkControlWord
public class LCHarrisReturnToControlChannel extends LinkControlWord
{
private static final int[] UNKNOWN_1 = {16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] TARGET_RADIO = {24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
42, 43, 44, 45, 46, 47};
private static final int[] SOURCE_RADIO = {48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71};
private static final IntField UNKNOWN_1 = IntField.length8(OCTET_2_BIT_16);
private static final IntField SOURCE_RADIO = IntField.length24(OCTET_3_BIT_24);
private static final IntField TARGET_RADIO = IntField.length24(OCTET_6_BIT_48);
private RadioIdentifier mSourceRadio;
private RadioIdentifier mTargetRadio;
private List<Identifier> mIdentifiers;
@@ -51,7 +62,7 @@ public class LCHarrisUnknownOpcode10 extends LinkControlWord
*
* @param message
*/
public LCHarrisUnknownOpcode10(BinaryMessage message)
public LCHarrisReturnToControlChannel(CorrectedBinaryMessage message)
{
super(message);
}
@@ -71,7 +82,7 @@ public class LCHarrisUnknownOpcode10 extends LinkControlWord
}
else
{
sb.append("L3HARRIS UNKNOWN OPCODE 10 ");
sb.append("L3HARRIS RETURN TO CONTROL CHANNEL OR PRIVATE DATA CALL PAGING");
sb.append(" FM:").append(getSourceRadio());
sb.append(" TO:").append(getTargetRadio());
sb.append(" UNK:").append(getUnknown());
@@ -82,7 +93,7 @@ public class LCHarrisUnknownOpcode10 extends LinkControlWord
public String getUnknown()
{
return getMessage().getHex(UNKNOWN_1, 2);
return getMessage().getHex(UNKNOWN_1);
}
/**
@@ -92,7 +103,7 @@ public class LCHarrisUnknownOpcode10 extends LinkControlWord
{
if(mSourceRadio == null)
{
mSourceRadio = APCO25RadioIdentifier.createFrom(getMessage().getInt(SOURCE_RADIO));
mSourceRadio = APCO25RadioIdentifier.createFrom(getInt(SOURCE_RADIO));
}
return mSourceRadio;
@@ -105,7 +116,7 @@ public class LCHarrisUnknownOpcode10 extends LinkControlWord
{
if(mTargetRadio == null)
{
mTargetRadio = APCO25RadioIdentifier.createTo(getMessage().getInt(TARGET_RADIO));
mTargetRadio = APCO25RadioIdentifier.createTo(getInt(TARGET_RADIO));
}
return mTargetRadio;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -19,7 +19,7 @@
package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import java.util.Collections;
@@ -38,7 +38,7 @@ public class LCHarrisUnknownOpcode42 extends LinkControlWord
*
* @param message
*/
public LCHarrisUnknownOpcode42(BinaryMessage message)
public LCHarrisUnknownOpcode42(CorrectedBinaryMessage message)
{
super(message);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 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
@@ -19,7 +19,7 @@
package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import java.util.Collections;
@@ -38,7 +38,7 @@ public class LCHarrisUnknownOpcode43 extends LinkControlWord
*
* @param message
*/
public LCHarrisUnknownOpcode43(BinaryMessage message)
public LCHarrisUnknownOpcode43(CorrectedBinaryMessage message)
{
super(message);
}

View File

@@ -0,0 +1,134 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.phase1.message.lc.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Link Control Opcode 10 appears to be an Emergency Alarm Activation message.
*
* This was observed in the following sequence:
* 1. Radio registers on network.
* 2. Radio assigned group and affiliation group
* 3. Network acknowledges radio's emergency alarm request
* 4. Network sends this message, interspersed with the ack message (3)
* 5. Network grants emergency group voice channel to radio and the same talkgroup references in this message, which
* is also the assigned talkgroup during initial registration.
* 6. Network grants second non-emerg group voice channel to radio and a second (unknown 0x39) talkgroup that may be
* a supervisor talkgroup.
*
* This link control message was also transmitted on active traffic channels.
*
* Note: the same opcode is used on both on control channel TSBK and traffic channel LCW messaging.
*/
public class LCMotorolaEmergencyAlarmActivation extends LinkControlWord
{
private static final IntField GROUP_ADDRESS = IntField.length16(OCTET_2_BIT_16);
//There seems to be room for another group address here, octets 4/5??
private static final IntField SOURCE_ADDRESS = IntField.length24(OCTET_6_BIT_48);
private TalkgroupIdentifier mGroupAddress;
private RadioIdentifier mSourceAddress;
private List<Identifier> mIdentifiers;
/**
* Constructs a Link Control Word from the binary message sequence.
*
* @param message
*/
public LCMotorolaEmergencyAlarmActivation(CorrectedBinaryMessage message)
{
super(message);
}
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("**CRC-FAILED** ");
}
if(isEncrypted())
{
sb.append(" ENCRYPTED");
}
else
{
sb.append("MOTOROLA EMERGENCY ALARM ACTIVATION RADIO:").append(getSourceAddress());
sb.append(" TALKGROUP:").append(getGroupAddress());
sb.append(" MSG:").append(getMessage().toHexString());
}
return sb.toString();
}
/**
* Source Address that activated the emergency alarm.
*/
public RadioIdentifier getSourceAddress()
{
if(mSourceAddress == null)
{
mSourceAddress = APCO25RadioIdentifier.createTo(getInt(SOURCE_ADDRESS));
}
return mSourceAddress;
}
/**
* Talkgroup to be activated for the emergency voice call
*/
public TalkgroupIdentifier getGroupAddress()
{
if(mGroupAddress == null)
{
mGroupAddress = APCO25Talkgroup.create(getInt(GROUP_ADDRESS));
}
return mGroupAddress;
}
/**
* List of identifiers contained in this message
*/
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getGroupAddress());
mIdentifiers.add(getSourceAddress());
}
return mIdentifiers;
}
}

Some files were not shown because too many files have changed in this diff Show More