mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
* 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:
1
.idea/dictionaries/denny.xml
generated
1
.idea/dictionaries/denny.xml
generated
@@ -29,6 +29,7 @@
|
||||
<w>talkgroups</w>
|
||||
<w>tgid</w>
|
||||
<w>ultravox</w>
|
||||
<w>unsynchronized</w>
|
||||
<w>wacn</w>
|
||||
<w>xtea</w>
|
||||
</words>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
131
src/main/java/io/github/dsheirer/bits/IntField.java
Normal file
131
src/main/java/io/github/dsheirer/bits/IntField.java
Normal 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));
|
||||
}
|
||||
}
|
||||
122
src/main/java/io/github/dsheirer/bits/LongField.java
Normal file
122
src/main/java/io/github/dsheirer/bits/LongField.java
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 */
|
||||
|
||||
285
src/main/java/io/github/dsheirer/edac/Binary40_9_16.java
Normal file
285
src/main/java/io/github/dsheirer/edac/Binary40_9_16.java
Normal 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);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
159
src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
Normal file
159
src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
107
src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
Normal file
107
src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
738
src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
Normal file
738
src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ public abstract class IntegerIdentifier extends Identifier<Integer>
|
||||
super(value, identifierClass, form, role);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
191
src/main/java/io/github/dsheirer/message/AbstractMessage.java
Normal file
191
src/main/java/io/github/dsheirer/message/AbstractMessage.java
Normal 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);
|
||||
}
|
||||
}
|
||||
103
src/main/java/io/github/dsheirer/message/TimeslotMessage.java
Normal file
103
src/main/java/io/github/dsheirer/message/TimeslotMessage.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 )
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user