Compare commits

..

46 Commits

Author SHA1 Message Date
Denny Sheirer
02363c3113 Merge pull request #1690 from DSheirer/1689-harris-patch-group-prefixes
#1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris Patch Groups
2023-11-04 07:13:10 -04:00
Dennis Sheirer
cfaac0fcbc #1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris patch groups. 2023-11-04 07:12:29 -04:00
Denny Sheirer
bee0405e00 Merge pull request #1688 from DSheirer/1687-rtl2832-fc0013
Support For RTL2832 With Embedded FC0013 Tuner
2023-11-04 06:54:55 -04:00
Dennis Sheirer
50bdf0ee81 #1687 Adds support for RTL2832 with embedded FC0013 tuner. 2023-11-04 06:52:37 -04:00
Denny Sheirer
a27ddbe0d0 Merge pull request #1679 from DSheirer/1643-r828d-v4-tuner
#1643 RTL2832/R828D aka RTL-SDR V4 Dongle & Bias-T Support
2023-10-22 08:40:33 -04:00
Dennis Sheirer
0d652b9153 #1643 Adds support for RTL2832 with embedded R828D tuner and RTL-SDR.com V4 dongles. Introduces Bias-T support for all RTL-2832 dongles. 2023-10-22 08:38:16 -04:00
Denny Sheirer
9468fd189b Merge pull request #1677 from DSheirer/1607-harris-p25-patch-groups
#1607 P25 P1/P2 L3Harris Patch Group Support
2023-10-21 07:00:41 -04:00
sheirerd
215737ad9b #1607 Add support for L3Harris dynamic regroup/patch messages and events. Includes parsing for L3Harris unknown opcodes 10, 42 and 43. 2023-10-21 06:58:11 -04:00
Denny Sheirer
123c25b142 Merge pull request #1672 from DSheirer/1650-fm-decoder-7_5-bandwidth
#1650 Adds 7.5 kHz channel bandwidth option for FM channels.
2023-10-15 09:22:48 -04:00
Dennis Sheirer
af56198f0e #1650 Adds 7.5 kHz channel bandwidth option for FM channels. 2023-10-15 09:21:38 -04:00
Denny Sheirer
3c73fdfd6e Merge pull request #1671 from DSheirer/1670-dmr-cap-plus-missing-call-audio
#1670 Missing Audio Segments - DMR Cap+
2023-10-15 08:56:48 -04:00
Dennis Sheirer
b391ebbb5f #1670 Resolves missing audio segments for some DMR Cap+ call events. 2023-10-15 08:55:35 -04:00
Denny Sheirer
bad5a1f9f0 Merge pull request #1669 from DSheirer/1668-message-viewer-p25-support
#1668 Message Recording Viewer - P25P1 Support
2023-10-15 07:17:06 -04:00
Dennis Sheirer
a7470b463f #1668 Updates message recording (.bits) viewer to support P25 Phase 1. 2023-10-15 07:14:53 -04:00
Denny Sheirer
d6c706cb35 Merge pull request #1667 from DSheirer/1666-p25-moto-unit-gps
#1666 P25 Motorola Unit GPS Parsing & Plot to Map
2023-10-14 08:16:48 -04:00
Dennis Sheirer
a0d6950c75 #1666 P25 Motorola unit self-reported GPS location parsing and plotting to map. 2023-10-14 08:15:35 -04:00
Denny Sheirer
2495200b36 Merge pull request #1663 from DSheirer/1648-dmr-lrrp-not-plotting-map
#1648 DMR Embedded GPS & LRRP Plotting
2023-10-05 03:32:08 -04:00
Dennis Sheirer
bfe7ae0b1b #1648 Resolves issue with plotting DMR embedded GPS and LRRP positions. 2023-10-05 03:31:17 -04:00
Denny Sheirer
4912abbf07 Merge pull request #1662 from DSheirer/1661-dmr-gps-location-parsing-error
#1661 DMR Talker GPS Lat/Long Values Incorrect
2023-10-05 03:21:24 -04:00
Dennis Sheirer
2f2998d02e #1661 Resolves issue with parsing of DMR embedded GPS latitude and longitude values. 2023-10-05 03:20:17 -04:00
Dennis Sheirer
c96349fa1c Updates version to 0.6.0 Beta 2 2023-08-31 07:14:58 -04:00
Denny Sheirer
f640576cc3 Merge pull request #1646 from DSheirer/1637-dmr-cap-plus-and-max-trunking
#1637 DMR Cap+/CapMax Channel Frequency Map / Trunking Issue
2023-08-31 07:10:21 -04:00
Dennis Sheirer
77d725a488 #1637 Resolves issue with DMR Cap+/CapMax trunking channel to frequency map. Updates the DMR channel map to use LCN instead of LSN and aligns standard DMR with Tier-III DMR for channel identification.
Warning: breaking change.  Users will need to either recreate their DMR channel to frequency maps or simply delete and reload the channel from Radio Reference to correct the channel to frequency map.

Updates the DMR Decoder State to better track and update DMR events versus creating a new event and causing lots of scrolling in the event window of the Now Playing tab.
2023-08-31 07:06:50 -04:00
Denny Sheirer
2c0305f928 Merge pull request #1642 from DSheirer/1641-hytera-rrs-packets
#1641 DMR Hytera RRS packet support.
2023-08-27 07:47:14 -04:00
Dennis Sheirer
21fd61d507 #1641 DMR Hytera RRS packet support. 2023-08-27 07:44:43 -04:00
Denny Sheirer
8fb56a5342 Merge pull request #1640 from DSheirer/1638-hytera-encryption-parameters
#1638 DMR Decoder - Encryption Parameters & Hytera RRS
2023-08-26 07:36:31 -04:00
Dennis Sheirer
5d7b73f876 #1638 Enhances DMR decoder to include additional encryption details from two major vendor implementations. Adds Hytera short data Radio Registration Service (RRS) processing. 2023-08-26 07:32:46 -04:00
Dennis Sheirer
85b2a38058 Version 0.6.0 Beta 1 2023-08-19 05:16:20 -04:00
Denny Sheirer
c78517ee75 Merge pull request #1631 from DSheirer/1630-flc-arc4-encryption-parameters
#1630 DMR FLC ARC4/EP Encryption Parameters
2023-08-18 07:08:30 -04:00
Dennis Sheirer
ae116d3dfe #1630 Updates ARC4 FLC encryption parameters message to detect and parse both Hytera & Motorola message formats. 2023-08-18 07:06:26 -04:00
Denny Sheirer
93252f9a40 Merge pull request #1629 from DSheirer/1628-libusb-device-list-error
#1628 LibUsb Error -99 During Device Discovery
2023-08-18 03:57:50 -04:00
Dennis Sheirer
3c03a4db2e #1628 Handle libusb get device list error condition and retry with increased logging during error code -99 to determine what causes this error condition, primarily on some Linux systems. 2023-08-18 03:55:42 -04:00
Denny Sheirer
d0fcd617aa Merge pull request #1627 from DSheirer/1626-dmr-call-events-missing-timeslot
#1626 Updates DMR call events to include timeslot and adds DMR encryption info when available.
2023-08-15 04:01:52 -04:00
Dennis Sheirer
6ca0decbd6 #1626 Updates DMR call events to include timeslot and adds DMR encryption information when available. 2023-08-15 04:01:27 -04:00
Denny Sheirer
ddd9ca933e Merge pull request #1624 from DSheirer/1623-unknown-opcode-message-filter
#1623 P25 Message Filter Set - Unknown Vendor Opcode
2023-08-14 04:04:30 -04:00
Dennis Sheirer
b2cc29569e #1623 resolves issue with handling of unknown P25 trunking opcode in message filter set. 2023-08-14 04:03:06 -04:00
Denny Sheirer
411735772b Merge pull request #1622 from DSheirer/dmr-cap-plus-identifier-updates
#1618 DMR Decoder Message Enhancements
2023-08-13 08:38:12 -04:00
sheirerd
2ffb2963af #1618 Update Cap+ radio & group parsing, process TXI & voice frame encryption parameters, and many other changes to the DMR message processing. 2023-08-13 08:30:23 -04:00
Denny Sheirer
227c88c8a2 Merge pull request #1614 from DSheirer/1612-dcs-alias-identifier
#1612 Enhance aliasing to support DCS tones
2023-07-24 04:34:46 -04:00
Dennis Sheirer
cbe9c79498 #1612 Enhance aliasing to support DCS tones 2023-07-24 04:30:16 -04:00
Denny Sheirer
1b8c100b43 Merge pull request #1611 from DSheirer/1605-tuner-editor-fails-to-display
#1605 Blank Tuner Editor Panel on Certain Linux Configurations
2023-07-22 06:00:38 -04:00
Dennis Sheirer
30c5fd64f2 #1605 Resolves issue where certain Linux users were unable to display the tuner editor panel due to an issue with the frequency control editor's usage of custom cursor. 2023-07-22 05:55:43 -04:00
Denny Sheirer
3a6b486340 Merge pull request #1610 from DSheirer/1604-event-panel-row-layout
#1604 Event Panel UI Layout Creates Excess Space On Resizing
2023-07-22 05:25:11 -04:00
Dennis Sheirer
ded7335dbf #1604 Resolves issue with event panel UI layout creating excess space between history management panel and events table. 2023-07-22 05:21:26 -04:00
Denny Sheirer
005a4f8119 Merge pull request #1608 from DSheirer/1603-duplicate-events
#1603 De-Duplicate Call Events
2023-07-22 05:06:11 -04:00
Dennis Sheirer
1ac49bfa7f #1603 Resolves issue with de-duplicating events to allow call duration values to display correctly and minimize the quantity of duplicate events. 2023-07-22 04:57:26 -04:00
170 changed files with 8166 additions and 2205 deletions

2
.idea/misc.xml generated
View File

@@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="liberica-20" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </project>

View File

@@ -53,7 +53,8 @@ repositories {
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
} }
version = '0.6.0-alpha7' //Note: this should match the GitLab release tag.
version = '0.6.0-beta-3'
//Java 20 is required for this version of the Project Panama preview/incubator feature //Java 20 is required for this version of the Project Panama preview/incubator feature
java { java {

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -19,22 +19,15 @@
package io.github.dsheirer.alias; package io.github.dsheirer.alias;
import io.github.dsheirer.alias.action.AliasAction; import io.github.dsheirer.alias.action.AliasAction;
import io.github.dsheirer.alias.action.AliasActionType;
import io.github.dsheirer.alias.action.beep.BeepAction; import io.github.dsheirer.alias.action.beep.BeepAction;
import io.github.dsheirer.alias.action.clip.ClipAction; import io.github.dsheirer.alias.action.clip.ClipAction;
import io.github.dsheirer.alias.action.script.ScriptAction; import io.github.dsheirer.alias.action.script.ScriptAction;
import io.github.dsheirer.alias.id.AliasID; import io.github.dsheirer.alias.id.AliasID;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.alias.id.dcs.Dcs;
import io.github.dsheirer.alias.id.esn.Esn; import io.github.dsheirer.alias.id.esn.Esn;
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
import io.github.dsheirer.alias.id.legacy.mobileID.Min; import io.github.dsheirer.alias.id.legacy.mobileID.Min;
import io.github.dsheirer.alias.id.legacy.mpt1327.MPT1327ID;
import io.github.dsheirer.alias.id.legacy.nonrecordable.NonRecordable;
import io.github.dsheirer.alias.id.legacy.siteID.SiteID; import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID; import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
import io.github.dsheirer.alias.id.priority.Priority; import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.radio.Radio; import io.github.dsheirer.alias.id.radio.Radio;
@@ -45,11 +38,10 @@ import io.github.dsheirer.alias.id.status.UserStatusID;
import io.github.dsheirer.alias.id.talkgroup.Talkgroup; import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange; import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
import io.github.dsheirer.alias.id.tone.TonesID; import io.github.dsheirer.alias.id.tone.TonesID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AliasFactory public class AliasFactory
{ {
@@ -64,6 +56,11 @@ public class AliasFactory
BroadcastChannel copyBroadcast = new BroadcastChannel(); BroadcastChannel copyBroadcast = new BroadcastChannel();
copyBroadcast.setChannelName(originalBroadcast.getChannelName()); copyBroadcast.setChannelName(originalBroadcast.getChannelName());
return copyBroadcast; return copyBroadcast;
case DCS:
Dcs originalDcs = (Dcs)id;
Dcs copyDcs = new Dcs();
copyDcs.setDCSCode(originalDcs.getDCSCode());
return copyDcs;
case ESN: case ESN:
Esn originalESN = (Esn)id; Esn originalESN = (Esn)id;
Esn copyESN = new Esn(); Esn copyESN = new Esn();
@@ -234,65 +231,4 @@ public class AliasFactory
return copy; return copy;
} }
public static AliasID getAliasID(AliasIDType type)
{
switch(type)
{
case BROADCAST_CHANNEL:
return new BroadcastChannel();
case ESN:
return new Esn();
case FLEETSYNC:
return new FleetsyncID();
case LTR_NET_UID:
return new UniqueID();
case LOJACK:
return new LoJackFunctionAndID();
case MDC1200:
return new MDC1200ID();
case MIN:
return new Min();
case MPT1327:
return new MPT1327ID();
case NON_RECORDABLE:
return new NonRecordable();
case PRIORITY:
return new Priority();
case RADIO_ID:
return new Radio();
case RADIO_ID_RANGE:
return new RadioRange();
case RECORD:
return new Record();
case TALKGROUP:
return new Talkgroup();
case TALKGROUP_RANGE:
return new TalkgroupRange();
case SITE:
return new SiteID();
case STATUS:
return new UserStatusID();
case LEGACY_TALKGROUP:
return new LegacyTalkgroupID();
default:
throw new IllegalArgumentException("Unrecognized Alias ID type: " + type);
}
}
public static AliasAction getAliasAction(AliasActionType type)
{
switch(type)
{
case BEEP:
return new BeepAction();
case CLIP:
return new ClipAction();
case SCRIPT:
return new ScriptAction();
default:
throw new IllegalArgumentException("Unrecognized Alias Action type: " + type);
}
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -21,6 +21,7 @@ package io.github.dsheirer.alias;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.alias.id.AliasID; import io.github.dsheirer.alias.id.AliasID;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.alias.id.dcs.Dcs;
import io.github.dsheirer.alias.id.esn.Esn; import io.github.dsheirer.alias.id.esn.Esn;
import io.github.dsheirer.alias.id.priority.Priority; import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.radio.Radio; import io.github.dsheirer.alias.id.radio.Radio;
@@ -32,6 +33,7 @@ import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
import io.github.dsheirer.alias.id.tone.TonesID; import io.github.dsheirer.alias.id.tone.TonesID;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.dcs.DCSIdentifier;
import io.github.dsheirer.identifier.esn.ESNIdentifier; import io.github.dsheirer.identifier.esn.ESNIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup; import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
@@ -41,12 +43,8 @@ import io.github.dsheirer.identifier.status.UserStatusIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.identifier.tone.ToneIdentifier; import io.github.dsheirer.identifier.tone.ToneIdentifier;
import io.github.dsheirer.identifier.tone.ToneSequence; import io.github.dsheirer.identifier.tone.ToneSequence;
import io.github.dsheirer.module.decode.dcs.DCSCode;
import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.protocol.Protocol;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -57,6 +55,10 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* List of aliases that share the same alias list name and provides convenient methods for looking up alias * List of aliases that share the same alias list name and provides convenient methods for looking up alias
@@ -67,6 +69,7 @@ public class AliasList
private final static Logger mLog = LoggerFactory.getLogger(AliasList.class); private final static Logger mLog = LoggerFactory.getLogger(AliasList.class);
private Map<Protocol,TalkgroupAliasList> mTalkgroupProtocolMap = new EnumMap<>(Protocol.class); private Map<Protocol,TalkgroupAliasList> mTalkgroupProtocolMap = new EnumMap<>(Protocol.class);
private Map<Protocol,RadioAliasList> mRadioProtocolMap = new EnumMap<>(Protocol.class); private Map<Protocol,RadioAliasList> mRadioProtocolMap = new EnumMap<>(Protocol.class);
private Map<DCSCode,Alias> mDCSCodeAliasMap = new EnumMap<>(DCSCode.class);
private Map<String,Alias> mESNMap = new HashMap<>(); private Map<String,Alias> mESNMap = new HashMap<>();
private Map<Integer,Alias> mUnitStatusMap = new HashMap<>(); private Map<Integer,Alias> mUnitStatusMap = new HashMap<>();
private Map<Integer,Alias> mUserStatusMap = new HashMap<>(); private Map<Integer,Alias> mUserStatusMap = new HashMap<>();
@@ -178,6 +181,12 @@ public class AliasList
radioRangeAliasList.add(radioRange, alias); radioRangeAliasList.add(radioRange, alias);
break; break;
case DCS:
if(id instanceof Dcs dcs)
{
mDCSCodeAliasMap.put(dcs.getDCSCode(), alias);
}
break;
case ESN: case ESN:
String esn = ((Esn)id).getEsn(); String esn = ((Esn)id).getEsn();
@@ -379,6 +388,8 @@ public class AliasList
} }
break; break;
case PATCH_GROUP: case PATCH_GROUP:
List<Alias> aliases = new ArrayList<>();
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier; PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
PatchGroup patchGroup = patchGroupIdentifier.getValue(); PatchGroup patchGroup = patchGroupIdentifier.getValue();
@@ -386,8 +397,6 @@ public class AliasList
if(patchGroupAliasList != null) if(patchGroupAliasList != null)
{ {
List<Alias> aliases = new ArrayList<>();
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup()); Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
if(alias != null) if(alias != null)
@@ -395,19 +404,36 @@ public class AliasList
aliases.add(alias); aliases.add(alias);
} }
for(TalkgroupIdentifier patchedGroup: patchGroup.getPatchedGroupIdentifiers()) for(TalkgroupIdentifier patchedTalkgroup: patchGroup.getPatchedTalkgroupIdentifiers())
{ {
Alias patchedAlias = patchGroupAliasList.getAlias(patchedGroup); Alias patchedTalkgroupAlias = patchGroupAliasList.getAlias(patchedTalkgroup);
if(patchedAlias != null && !aliases.contains(patchedAlias)) if(patchedTalkgroupAlias != null && !aliases.contains(patchedTalkgroupAlias))
{ {
aliases.add(patchedAlias); aliases.add(patchedTalkgroupAlias);
} }
} }
return aliases;
} }
break;
if(patchGroup.hasPatchedRadios())
{
RadioAliasList radioAliasList = mRadioProtocolMap.get(patchGroupIdentifier.getProtocol());
if(radioAliasList != null)
{
for(RadioIdentifier patchedRadio: patchGroup.getPatchedRadioIdentifiers())
{
Alias patchedRadioAlias = radioAliasList.getAlias(patchedRadio);
if(patchedRadioAlias != null && !aliases.contains(patchedRadioAlias))
{
aliases.add(patchedRadioAlias);
}
}
}
}
return aliases;
case RADIO: case RADIO:
RadioIdentifier radio = (RadioIdentifier)identifier; RadioIdentifier radio = (RadioIdentifier)identifier;
@@ -418,7 +444,7 @@ public class AliasList
return toList(radioAliasList.getAlias(radio)); return toList(radioAliasList.getAlias(radio));
} }
break; break;
case ESN: case DCS:
if(identifier instanceof ESNIdentifier) if(identifier instanceof ESNIdentifier)
{ {
return toList(getESNAlias(((ESNIdentifier)identifier).getValue())); return toList(getESNAlias(((ESNIdentifier)identifier).getValue()));
@@ -439,9 +465,9 @@ public class AliasList
} }
break; break;
case TONE: case TONE:
if(identifier instanceof ToneIdentifier) if(identifier instanceof ToneIdentifier toneIdentifier)
{ {
ToneSequence toneSequence = ((ToneIdentifier)identifier).getValue(); ToneSequence toneSequence = toneIdentifier.getValue();
if(toneSequence != null && toneSequence.hasTones()) if(toneSequence != null && toneSequence.hasTones())
{ {
@@ -454,6 +480,15 @@ public class AliasList
} }
} }
} }
else if(identifier instanceof DCSIdentifier dcsIdentifier)
{
DCSCode dcsCode = dcsIdentifier.getValue();
if(dcsCode != null)
{
return toList(mDCSCodeAliasMap.get(dcsCode));
}
}
break; break;
} }
} }
@@ -465,9 +500,7 @@ public class AliasList
{ {
if(alias != null) if(alias != null)
{ {
List<Alias> aliases = new ArrayList<>(); return Collections.singletonList(alias);
aliases.add(alias);
return aliases;
} }
return Collections.emptyList(); return Collections.emptyList();

View File

@@ -1,23 +1,20 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2019 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * 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 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.alias.id; package io.github.dsheirer.alias.id;
@@ -26,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.alias.id.dcs.Dcs;
import io.github.dsheirer.alias.id.esn.Esn; import io.github.dsheirer.alias.id.esn.Esn;
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID; import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID; import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
@@ -54,6 +52,7 @@ import javafx.util.Callback;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = BroadcastChannel.class, name = "broadcastChannel"), @JsonSubTypes.Type(value = BroadcastChannel.class, name = "broadcastChannel"),
@JsonSubTypes.Type(value = Dcs.class, name = "dcs"),
@JsonSubTypes.Type(value = Esn.class, name = "esn"), @JsonSubTypes.Type(value = Esn.class, name = "esn"),
@JsonSubTypes.Type(value = FleetsyncID.class, name = "fleetsyncID"), @JsonSubTypes.Type(value = FleetsyncID.class, name = "fleetsyncID"),
@JsonSubTypes.Type(value = LegacyTalkgroupID.class, name = "talkgroupID"), @JsonSubTypes.Type(value = LegacyTalkgroupID.class, name = "talkgroupID"),

View File

@@ -1,23 +1,20 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2019 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * 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 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.alias.id; package io.github.dsheirer.alias.id;
@@ -26,6 +23,7 @@ import java.util.EnumSet;
public enum AliasIDType public enum AliasIDType
{ {
BROADCAST_CHANNEL("Audio Broadcast Channel"), BROADCAST_CHANNEL("Audio Broadcast Channel"),
DCS("Digital Coded Squelch (DCS)"),
ESN("ESN"), ESN("ESN"),
INVERT("Audio Inversion"), INVERT("Audio Inversion"),
LOJACK("LoJack"), LOJACK("LoJack"),

View File

@@ -0,0 +1,111 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.alias.id.dcs;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.alias.id.AliasID;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.module.decode.dcs.DCSCode;
/**
* Digital Coded Squelch (DCS) tone identifier
*/
public class Dcs extends AliasID implements Comparable<Dcs>
{
private DCSCode mDCSCode;
@Override
public AliasIDType getType()
{
return AliasIDType.DCS;
}
@Override
public boolean matches(AliasID id)
{
if(isValid() && id instanceof Dcs other)
{
return other.isValid() && getDCSCode().equals(other.getDCSCode());
}
return false;
}
/**
* DCS code
* @return DCS code or null.
*/
@JacksonXmlProperty(isAttribute = true, localName = "code")
public DCSCode getDCSCode()
{
return mDCSCode;
}
/**
* Sets the DCS code value.
* @param dcsCode to set
*/
public void setDCSCode(DCSCode dcsCode)
{
mDCSCode = dcsCode;
updateValueProperty();
}
@Override
public boolean isValid()
{
return mDCSCode != null;
}
@Override
public boolean isAudioIdentifier()
{
return false;
}
@Override
public String toString()
{
if(isValid())
{
return getDCSCode().toString();
}
return "DCS-Invalid - No Tone Selected";
}
@Override
public int compareTo(Dcs o)
{
if(isValid())
{
if(o.isValid())
{
return getDCSCode().compareTo(o.getDCSCode());
}
else
{
return 1;
}
}
return -1;
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -38,9 +38,6 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@@ -48,12 +45,13 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.time.Duration; import java.time.Duration;
import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Audio broadcaster to push completed audio recordings to the Broadcastify call push API. * Audio broadcaster to push completed audio recordings to the Broadcastify call push API.
@@ -475,7 +473,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(patchGroup.getPatchGroup().getValue().toString()); sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers()) for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{ {
sb.append(",").append(patched.getValue()); sb.append(",").append(patched.getValue());
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
package io.github.dsheirer.audio.broadcast.rdioscanner; package io.github.dsheirer.audio.broadcast.rdioscanner;
import com.google.common.net.HttpHeaders; import com.google.common.net.HttpHeaders;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias; import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.alias.AliasModel;
@@ -34,7 +33,6 @@ import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier; import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup; import io.github.dsheirer.identifier.patch.PatchGroup;
@@ -42,24 +40,12 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
@@ -67,6 +53,8 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
@@ -525,7 +513,12 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
sb.append(patchGroup.getPatchGroup().getValue().toString()); sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers()) for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{ {
sb.append(",").append(patched.getValue()); sb.append(",").append(patched.getValue());
} }

View File

@@ -844,13 +844,18 @@ public class BinaryMessage extends BitSet
*/ */
public int getTwosComplement(int start, int end) public int getTwosComplement(int start, int end)
{ {
BinaryMessage sub = getSubMessage(start, end); if(get(start))
boolean negative = sub.get(start); {
sub.flip(0, sub.size()); //Negative value - flip and add one
BinaryMessage fragment = getSubMessage(start, end);
int value = sub.getInt(0, sub.size()) + 1; fragment.flip(0, fragment.size());
value *= (negative ? -1 : 1); return fragment.getInt(1, fragment.size()) + 1;
return value; }
else
{
//Positive value - return the contents.
return getInt(start + 1, end);
}
} }

View File

@@ -23,6 +23,7 @@ import io.github.dsheirer.buffer.INativeBufferProvider;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest; import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.dsp.filter.design.FilterDesignException; import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples; import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
@@ -65,6 +66,7 @@ import org.slf4j.LoggerFactory;
public class PolyphaseChannelManager implements ISourceEventProcessor public class PolyphaseChannelManager implements ISourceEventProcessor
{ {
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000"); private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LoggerFactory.getLogger(PolyphaseChannelManager.class));
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class); private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0; private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
private static final double CHANNEL_OVERSAMPLING = 2.0; private static final double CHANNEL_OVERSAMPLING = 2.0;
@@ -203,7 +205,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
} }
catch(IllegalArgumentException iae) catch(IllegalArgumentException iae)
{ {
mLog.debug("Couldn't design final output low pass filter for polyphase channel source"); LOGGING_SUPPRESSOR.error(iae.getMessage(), 3, "Couldn't allocate channel. " + iae.getMessage());
channelSource = null;
} }
} }

View File

@@ -0,0 +1,125 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.CorrectedBinaryMessage;
/**
* Block Product Turbo Code 16/2 for decoding a DMR Voice Frame F payload from the 32-bit EMB field.
* <p>
* See ETSI TS 102 361-1 B.2.2.1 and B.2.2.2
*/
public class BPTC_16_2
{
private static final int[] DEINTERLEAVE = new int[]{0, 24, 1, 25, 2, 26, 3, 27, 4, 28, 5, 29, 6, 30, 7, 31, 8, 16,
9, 17, 10, 18, 11, 19, 12, 20, 13, 21, 14, 22, 15, 23};
/**
* Unscramble and perform FEC checks per paragraph B.2.2.1 for Non-Reverse Channel Single Burst
*
* @param message with 32 interleaved bits.
* @return descrambled and error checked message or null if the process fails or there are too many errors.
*/
public static CorrectedBinaryMessage decodeShortBurst(CorrectedBinaryMessage message)
{
CorrectedBinaryMessage deinterleaved = deinterleave(message);
int fec = Hamming16.checkAndCorrect(deinterleaved, 0);
if(fec == 2) //0 or 1 is good, 2 = uncorrectable errors
{
return null;
}
//Check for even parity. Bits 0-15 should be the same as bits 16-31.
for(int x = 0; x < 16; x++)
{
if(deinterleaved.get(x) ^ deinterleaved.get(x + 16))
{
return null;
}
}
return deinterleaved;
}
/**
* Unscramble and perform FEC checks per paragraph B.2.2.2 for Reverse Channel Single Burst
*
* @param binaryMessage with 32 interleaved bits.
* @return descrambled and error checked message or null if the process fails or there are too many errors.
*/
public static CorrectedBinaryMessage decodeReverseChannel(CorrectedBinaryMessage message)
{
CorrectedBinaryMessage deinterleaved = deinterleave(message);
System.out.println(" DEINTER: " + deinterleaved.toHexString());
int fec = Hamming16.checkAndCorrect(deinterleaved, 0);
System.out.println(" DECODED: " + deinterleaved.toHexString());
System.out.println("FEC:" + fec);
if(fec == 2) //0 or 1 is good, 2 = uncorrectable errors
{
return null;
}
//Check for odd parity. Bits 0-15 should be opposite of bits 16-31.
for(int x = 0; x < 16; x++)
{
if(deinterleaved.get(x) == deinterleaved.get(x + 16))
{
return null;
}
}
return deinterleaved;
}
/**
* Performs deinterleave of the interleaved message.
*
* @param original to deinterleave
* @return deinterleaved message
*/
public static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage original)
{
CorrectedBinaryMessage delinterleaved = new CorrectedBinaryMessage(32);
for(int x = 0; x < 32; x++)
{
if(original.get(x))
{
delinterleaved.set(DEINTERLEAVE[x]);
}
}
return delinterleaved;
}
public static void main(String[] args)
{
String[] msgs = new String[]{"05030A03", "35003A00", "1C6D2C9E"};
for(String msg : msgs)
{
CorrectedBinaryMessage original = new CorrectedBinaryMessage(BinaryMessage.loadHex(msg));
System.out.println("ORIGINAL: " + original.toHexString());
CorrectedBinaryMessage decoded = decodeReverseChannel(original);
System.out.println("-------------------------");
}
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -226,17 +226,19 @@ public class CRCUtil
{ {
mLog.debug("Starting"); mLog.debug("Starting");
//DMR message long poly = 0x13l;
// String raw = "100110010001000000001001000000001111011011010000011001010000000000000000000000001100001000001011"; long[] checksums = generate(32, 4, poly, 0, true);
String raw = "101010000000000000110110100000101101011011010000011001111100000000000011100101001001111010001110";
mLog.debug(raw);
raw = "101010000000000000110110100000101101011011010000011001111100000000000011100101000000000000000000";
BinaryMessage message = BinaryMessage.load(raw);
mLog.debug(message.toString());
long polynomial = 0x11021l; StringBuilder sb = new StringBuilder();
decode(message, 0, 80, polynomial, 16); sb.append("private static int[] CHECKSUMS = new int[]{");
mLog.debug(message.toString()); for(long checksum: checksums)
mLog.debug("Finished"); {
sb.append("0x").append(Long.toHexString(checksum).toUpperCase());
sb.append(",");
}
sb.append("};");
System.out.println("Checksums:\n" + sb);
} }
} }

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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; package io.github.dsheirer.edac;
import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.BinaryMessage;
@@ -5,27 +24,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* -----------------------------------------------------------------------
* Galois24 decoder based on Hank Wallace's tutorial/algorithm located at:
* http://www.aqdi.com/golay.htm
******************************************************************************/
/** /**
* Galois 24/12/7 decoder * Galois 24/12/7 decoder
*/ */
@@ -160,4 +158,17 @@ public class Golay24
return (checksum ^ calculated); return (checksum ^ calculated);
} }
public static void main(String[] args)
{
// 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 = Golay24.checkAndCorrect(bm, 0);
System.out.println("M:" + bm.toHexString());
System.out.println("A:" + a);
}
} }

View File

@@ -24,8 +24,6 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.controller.channel.map.ChannelMap; import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.controller.channel.map.ChannelRange; import io.github.dsheirer.controller.channel.map.ChannelRange;
import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.dmr.DMRRecordingViewer;
import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest;
import io.github.dsheirer.gui.icon.IconManager; import io.github.dsheirer.gui.icon.IconManager;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest; import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.PlaylistEditor; import io.github.dsheirer.gui.playlist.PlaylistEditor;
@@ -37,6 +35,8 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.UserPreferencesEditor; import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest; import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog; import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.RecordingViewer;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor; import io.github.dsheirer.jmbe.JmbeEditor;
import io.github.dsheirer.jmbe.JmbeEditorRequest; import io.github.dsheirer.jmbe.JmbeEditorRequest;
@@ -67,7 +67,7 @@ public class JavaFxWindowManager extends Application
public static final String USER_PREFERENCES_EDITOR = "preferences"; public static final String USER_PREFERENCES_EDITOR = "preferences";
public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog"; public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog";
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map"; public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
public static final String STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER = "dmr.message.viewer"; public static final String STAGE_MONITOR_KEY_RECORDING_VIEWER = "recording.viewer";
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager"; public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor"; public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist"; public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
@@ -82,14 +82,14 @@ public class JavaFxWindowManager extends Application
private TunerManager mTunerManager; private TunerManager mTunerManager;
private UserPreferences mUserPreferences; private UserPreferences mUserPreferences;
private UserPreferencesEditor mUserPreferencesEditor; private UserPreferencesEditor mUserPreferencesEditor;
private DMRRecordingViewer mDmrRecordingViewer; private RecordingViewer mRecordingViewer;
private Stage mChannelMapStage; private Stage mChannelMapStage;
private Stage mIconManagerStage; private Stage mIconManagerStage;
private Stage mJmbeEditorStage; private Stage mJmbeEditorStage;
private Stage mPlaylistStage; private Stage mPlaylistStage;
private Stage mUserPreferencesStage; private Stage mUserPreferencesStage;
private Stage mDmrRecordingViewerStage; private Stage mRecordingViewerStage;
/** /**
* Constructs an instance. Note: this constructor is used for Swing applications. * Constructs an instance. Note: this constructor is used for Swing applications.
@@ -195,31 +195,31 @@ public class JavaFxWindowManager extends Application
} }
/** /**
* Stage for the DMR Message Viewer * Stage for the recording viewer
*/ */
public Stage getDmrRecordingViewerStage() public Stage getRecordingViewerStage()
{ {
if(mDmrRecordingViewerStage == null) if(mRecordingViewerStage == null)
{ {
createJFXPanel(); createJFXPanel();
Scene scene = new Scene(getDmrRecordingViewer(), 1100, 800); Scene scene = new Scene(getRecordingViewer(), 1100, 800);
mDmrRecordingViewerStage = new Stage(); mRecordingViewerStage = new Stage();
mDmrRecordingViewerStage.setTitle("sdrtrunk - DMR Recording Viewer"); mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
mDmrRecordingViewerStage.setScene(scene); mRecordingViewerStage.setScene(scene);
mUserPreferences.getJavaFxPreferences().monitor(mDmrRecordingViewerStage, STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER); mUserPreferences.getJavaFxPreferences().monitor(mRecordingViewerStage, STAGE_MONITOR_KEY_RECORDING_VIEWER);
} }
return mDmrRecordingViewerStage; return mRecordingViewerStage;
} }
public DMRRecordingViewer getDmrRecordingViewer() public RecordingViewer getRecordingViewer()
{ {
if(mDmrRecordingViewer == null) if(mRecordingViewer == null)
{ {
mDmrRecordingViewer = new DMRRecordingViewer(); mRecordingViewer = new RecordingViewer();
} }
return mDmrRecordingViewer; return mRecordingViewer;
} }
public Stage getIconManagerStage() public Stage getIconManagerStage()
@@ -453,12 +453,12 @@ public class JavaFxWindowManager extends Application
* Process a channel map editor request * Process a channel map editor request
*/ */
@Subscribe @Subscribe
public void process(final ViewDmrRecordingViewerRequest request) public void process(final ViewRecordingViewerRequest request)
{ {
execute(() -> { execute(() -> {
getDmrRecordingViewerStage().show(); getRecordingViewerStage().show();
getDmrRecordingViewerStage().requestFocus(); getRecordingViewerStage().requestFocus();
getDmrRecordingViewerStage().toFront(); getRecordingViewerStage().toFront();
}); });
} }

View File

@@ -32,13 +32,13 @@ import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
import io.github.dsheirer.controller.channel.ChannelException; import io.github.dsheirer.controller.channel.ChannelException;
import io.github.dsheirer.controller.channel.ChannelSelectionManager; import io.github.dsheirer.controller.channel.ChannelSelectionManager;
import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest; import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest; import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
import io.github.dsheirer.gui.preference.CalibrateRequest; import io.github.dsheirer.gui.preference.CalibrateRequest;
import io.github.dsheirer.gui.preference.PreferenceEditorType; import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest; import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog; import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.log.ApplicationLog; import io.github.dsheirer.log.ApplicationLog;
import io.github.dsheirer.map.MapService; import io.github.dsheirer.map.MapService;
@@ -420,9 +420,9 @@ public class SDRTrunk implements Listener<TunerEvent>
viewMenu.add(new JSeparator()); viewMenu.add(new JSeparator());
JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer"); JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest())); recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
viewMenu.add(dmrMessageViewerMenu); viewMenu.add(recordingViewerMenu);
JMenuItem settingsMenu = new JMenuItem("Icon Manager"); JMenuItem settingsMenu = new JMenuItem("Icon Manager");
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest())); settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,22 +22,9 @@ import io.github.dsheirer.source.ISourceEventProcessor;
import io.github.dsheirer.source.InvalidFrequencyException; import io.github.dsheirer.source.InvalidFrequencyException;
import io.github.dsheirer.source.SourceEvent; import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.SourceException;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import java.awt.Color; import java.awt.Color;
import java.awt.Cursor;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.awt.Font; import java.awt.Font;
import java.awt.Image;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
@@ -49,27 +36,39 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
/**
* Custom frequency controller for displaying or editing frequency values in MHz.
*/
public class JFrequencyControl extends JPanel implements ISourceEventProcessor public class JFrequencyControl extends JPanel implements ISourceEventProcessor
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class); private final static Logger mLog = LoggerFactory.getLogger(JFrequencyControl.class);
private List<ISourceEventProcessor> mProcessors = new ArrayList<>(); private List<ISourceEventProcessor> mProcessors = new ArrayList<>();
private Color mHighlightColor = Color.YELLOW; private Color mHighlightColor = Color.YELLOW;
private long mFrequency; private long mFrequency;
private Cursor mBlankCursor;
private HashMap<Integer,Digit> mDigits = new HashMap<Integer,Digit>(); private HashMap<Integer,Digit> mDigits = new HashMap<Integer,Digit>();
/**
* Constructor
*/
public JFrequencyControl() public JFrequencyControl()
{ {
init(); init();
} }
/**
* Initializes the control/layout.
*/
private void init() private void init()
{ {
setLayout(new MigLayout("insets 0 0 0 0", "[]0[]", "")); setLayout(new MigLayout("insets 0 0 0 0", "[]0[]", ""));
@@ -104,19 +103,13 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
} }
} }
/**
* Create a blank cursor to use when the mouse is over the digits
*/
//Create an empty byte array
byte[] imageByte = new byte[0];
//Create image for cursor using empty array
Image cursorImage = Toolkit.getDefaultToolkit().createImage(imageByte);
mBlankCursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImage, new Point(0, 0), "cursor");
revalidate(); revalidate();
} }
/**
* Enables or disables the control.
* @param enabled true if this component should be enabled, false otherwise
*/
@Override @Override
public void setEnabled(boolean enabled) public void setEnabled(boolean enabled)
{ {
@@ -157,11 +150,18 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
} }
} }
/**
* Current frequency value for the control.
* @return frequency
*/
public long getFrequency() public long getFrequency()
{ {
return mFrequency; return mFrequency;
} }
/**
* Updates the frequency value according to the manually changed digit value.
*/
private void updateFrequency() private void updateFrequency()
{ {
long frequency = 0; long frequency = 0;
@@ -174,14 +174,15 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
mFrequency = frequency; mFrequency = frequency;
} }
/**
* Fires a source changed event
* @throws SourceException of there's an error
*/
private void fireSourceChanged() throws SourceException private void fireSourceChanged() throws SourceException
{ {
updateFrequency(); updateFrequency();
Iterator<ISourceEventProcessor> it = mProcessors.iterator(); Iterator<ISourceEventProcessor> it = mProcessors.iterator();
SourceEvent event = SourceEvent.frequencyRequest(mFrequency); SourceEvent event = SourceEvent.frequencyRequest(mFrequency);
while(it.hasNext()) while(it.hasNext())
{ {
it.next().process(event); it.next().process(event);
@@ -217,12 +218,21 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
mProcessors.remove(processor); mProcessors.remove(processor);
} }
/**
* Custom single digit value editor.
*/
public class Digit extends JTextField public class Digit extends JTextField
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private int mPower = 0; private int mPower = 0;
private long mValue = 0; private long mValue = 0;
/**
* Constructor
* @param position of the digit where a position of 1 is the ones unit of MHz and a position of -1 is the tenths
* unit of MHz.
* @throws ParseException if there is an error
*/
private Digit(int position) throws ParseException private Digit(int position) throws ParseException
{ {
super("0"); super("0");
@@ -236,6 +246,11 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
this.addMouseWheelListener(listener); this.addMouseWheelListener(listener);
} }
/**
* Generates a custom tooltip for the current hover-over digit.
* @param power of the digit in units of 10
* @return custom tooptip.
*/
private static String getTooltip(int power) private static String getTooltip(int power)
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@@ -288,23 +303,32 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
{ {
//Strip the digits higher than this one //Strip the digits higher than this one
long lower = frequency % (long)(FastMath.pow(10, mPower + 1)); long lower = frequency % (long)(FastMath.pow(10, mPower + 1));
//Set the value to int value of dividing by 10 to this power //Set the value to int value of dividing by 10 to this power
long value = (long)(lower / (long)(FastMath.pow(10, mPower))); long value = (long)(lower / (long)(FastMath.pow(10, mPower)));
set(value, fireChangeEvent); set(value, fireChangeEvent);
} }
/**
* Frequency value.
* @return frequency value.
*/
public long getFrequency() public long getFrequency()
{ {
return mValue * (long)FastMath.pow(10, mPower); return mValue * (long)FastMath.pow(10, mPower);
} }
/**
* Increments the unit value and fires a change event
*/
public void increment() public void increment()
{ {
increment(true); increment(true);
} }
/**
* Increments the unit value and fires a change event
* @param fireChangeEvent true to fire an event
*/
public void increment(boolean fireChangeEvent) public void increment(boolean fireChangeEvent)
{ {
if(isEnabled()) if(isEnabled())
@@ -313,11 +337,18 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
} }
} }
/**
* Decrements the unit value and fires a change event.
*/
public void decrement() public void decrement()
{ {
decrement(true); decrement(true);
} }
/**
* Decrements the unit value and optionally fires a change event.
* @param fireChangeEvent set to true to fire an event
*/
public void decrement(boolean fireChangeEvent) public void decrement(boolean fireChangeEvent)
{ {
if(isEnabled()) if(isEnabled())
@@ -349,7 +380,6 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
while(mValue < 0) while(mValue < 0)
{ {
mValue += 10; mValue += 10;
Digit nextHigherDigit = mDigits.get(mPower + 1); Digit nextHigherDigit = mDigits.get(mPower + 1);
if(nextHigherDigit != null) if(nextHigherDigit != null)
@@ -363,7 +393,6 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
while(mValue > 9) while(mValue > 9)
{ {
mValue -= 10; mValue -= 10;
Digit nextHigherDigit = mDigits.get(mPower + 1); Digit nextHigherDigit = mDigits.get(mPower + 1);
if(nextHigherDigit != null) if(nextHigherDigit != null)
@@ -403,10 +432,8 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
} }
} }
if(se instanceof InvalidFrequencyException) if(se instanceof InvalidFrequencyException ife)
{ {
InvalidFrequencyException ife = (InvalidFrequencyException)se;
JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() + JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() +
"] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner."); "] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner.");
} }
@@ -415,13 +442,13 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
setText(String.valueOf(mValue)); setText(String.valueOf(mValue));
repaint(); repaint();
} }
private class Listener implements KeyListener, /**
MouseListener, * Multiple Listener interface implementation
MouseWheelListener */
private class Listener implements KeyListener, MouseListener, MouseWheelListener
{ {
@Override @Override
public void keyReleased(KeyEvent e) public void keyReleased(KeyEvent e)
@@ -503,14 +530,9 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
} }
@Override @Override
public void keyPressed(KeyEvent e) public void keyPressed(KeyEvent e) {}
{
}
@Override @Override
public void keyTyped(KeyEvent e) public void keyTyped(KeyEvent e) {}
{
}
@Override @Override
public void mouseClicked(MouseEvent e) public void mouseClicked(MouseEvent e)
@@ -528,21 +550,13 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
} }
} }
public void mousePressed(MouseEvent e) public void mousePressed(MouseEvent e) {}
{ public void mouseReleased(MouseEvent e) {}
}
public void mouseReleased(MouseEvent e)
{
}
@Override @Override
public void mouseEntered(MouseEvent e) public void mouseEntered(MouseEvent e)
{ {
Digit.this.setBackground(mHighlightColor); Digit.this.setBackground(mHighlightColor);
setCursor(mBlankCursor);
repaint(); repaint();
} }
@@ -550,9 +564,6 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
public void mouseExited(MouseEvent e) public void mouseExited(MouseEvent e)
{ {
Digit.this.setBackground(Color.WHITE); Digit.this.setBackground(Color.WHITE);
setCursor(Cursor.getDefaultCursor());
repaint(); repaint();
} }

View File

@@ -1,23 +1,20 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2020 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * 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 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.playlist.alias; package io.github.dsheirer.gui.playlist.alias;
@@ -37,6 +34,7 @@ import io.github.dsheirer.alias.action.script.ScriptAction;
import io.github.dsheirer.alias.id.AliasID; import io.github.dsheirer.alias.id.AliasID;
import io.github.dsheirer.alias.id.AliasIDType; import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel; import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.alias.id.dcs.Dcs;
import io.github.dsheirer.alias.id.esn.Esn; import io.github.dsheirer.alias.id.esn.Esn;
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID; import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
import io.github.dsheirer.alias.id.radio.Radio; import io.github.dsheirer.alias.id.radio.Radio;
@@ -63,6 +61,11 @@ import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat; import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.protocol.Protocol;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
@@ -106,12 +109,6 @@ import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/** /**
* Editor for configuring individual aliases * Editor for configuring individual aliases
*/ */
@@ -717,6 +714,7 @@ public class AliasItemEditor extends Editor<Alias>
Menu nbfmMenu = new ProtocolMenu(Protocol.NBFM); Menu nbfmMenu = new ProtocolMenu(Protocol.NBFM);
nbfmMenu.getItems().add(new AddTalkgroupItem(Protocol.NBFM)); nbfmMenu.getItems().add(new AddTalkgroupItem(Protocol.NBFM));
nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM)); nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM));
nbfmMenu.getItems().add(new AddDcsItem());
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT); Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT)); passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
@@ -1281,6 +1279,24 @@ public class AliasItemEditor extends Editor<Alias>
} }
} }
/**
* Add Digital Coded Squelch (DCS) alias identifier menu item
*/
public class AddDcsItem extends MenuItem
{
public AddDcsItem()
{
super("Digital Coded Squelch (DCS)");
setOnAction(event -> {
Dcs dcs = new Dcs();
getIdentifiersList().getItems().add(dcs);
getIdentifiersList().getSelectionModel().select(dcs);
getIdentifiersList().scrollTo(dcs);
modifiedProperty().set(true);
});
}
}
/** /**
* Menu Item for adding a new protocol-specific Radio ID alias identifier * Menu Item for adding a new protocol-specific Radio ID alias identifier
*/ */

View File

@@ -0,0 +1,98 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.playlist.alias.identifier;
import io.github.dsheirer.alias.id.dcs.Dcs;
import io.github.dsheirer.module.decode.dcs.DCSCode;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Editor for Digital Coded Squelch (DCS) alias identifiers
*/
public class DcsEditor extends IdentifierEditor<Dcs>
{
private static final Logger mLog = LoggerFactory.getLogger(DcsEditor.class);
private ComboBox<DCSCode> mDCSCodeComboBox;
/**
* Constructs an instance
*/
public DcsEditor()
{
GridPane gridPane = new GridPane();
gridPane.setHgap(5);
Label typeLabel = new Label("DCS Code");
GridPane.setConstraints(typeLabel, 0, 0);
gridPane.getChildren().add(typeLabel);
GridPane.setConstraints(getDCSCodeComboBox(), 1, 0);
gridPane.getChildren().add(getDCSCodeComboBox());
getChildren().add(gridPane);
}
@Override
public void setItem(Dcs item)
{
super.setItem(item);
if(item.isValid())
{
getDCSCodeComboBox().getSelectionModel().select(item.getDCSCode());
}
modifiedProperty().set(false);
}
@Override
public void save()
{
//no-op
}
@Override
public void dispose()
{
//no-op
}
/**
* Combo-box loaded with DCS codes
* @return
*/
private ComboBox<DCSCode> getDCSCodeComboBox()
{
if(mDCSCodeComboBox == null)
{
mDCSCodeComboBox = new ComboBox<>();
mDCSCodeComboBox.getItems().addAll(DCSCode.STANDARD_CODES);
mDCSCodeComboBox.getItems().addAll(DCSCode.INVERTED_CODES);
mDCSCodeComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
getItem().setDCSCode(getDCSCodeComboBox().getSelectionModel().getSelectedItem());
modifiedProperty().set(true);
});
}
return mDCSCodeComboBox;
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -35,6 +35,8 @@ public class IdentifierEditorFactory
{ {
switch(type) switch(type)
{ {
case DCS:
return new DcsEditor();
case ESN: case ESN:
return new EsnEditor(); return new EsnEditor();
case LOJACK: case LOJACK:

View File

@@ -85,7 +85,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
private ToggleSwitch mUseCompressedTalkgroupsToggle; private ToggleSwitch mUseCompressedTalkgroupsToggle;
private Spinner<Integer> mTrafficChannelPoolSizeSpinner; private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
private TableView<TimeslotFrequency> mTimeslotFrequencyTable; private TableView<TimeslotFrequency> mTimeslotFrequencyTable;
private IntegerTextField mLogicalSlotNumberField; private IntegerTextField mLogicalChannelNumberField;
private FrequencyField mDownlinkFrequencyField; private FrequencyField mDownlinkFrequencyField;
// private FrequencyField mUplinkFrequencyField; // private FrequencyField mUplinkFrequencyField;
private Button mAddTimeslotFrequencyButton; private Button mAddTimeslotFrequencyButton;
@@ -172,7 +172,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
GridPane.setConstraints(useCompressedTalkgroupsLabel, 7, row); GridPane.setConstraints(useCompressedTalkgroupsLabel, 7, row);
gridPane.getChildren().add(useCompressedTalkgroupsLabel); gridPane.getChildren().add(useCompressedTalkgroupsLabel);
Label timeslotTableLabel = new Label("Logical Slot Number (LSN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies"); Label timeslotTableLabel = new Label("Logical Channel Number (LCN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies. LSN = Logical Slot Number");
GridPane.setHalignment(timeslotTableLabel, HPos.LEFT); GridPane.setHalignment(timeslotTableLabel, HPos.LEFT);
GridPane.setConstraints(timeslotTableLabel, 0, ++row, 6, 1); GridPane.setConstraints(timeslotTableLabel, 0, ++row, 6, 1);
gridPane.getChildren().add(timeslotTableLabel); gridPane.getChildren().add(timeslotTableLabel);
@@ -194,17 +194,13 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
editorBox.setAlignment(Pos.CENTER_LEFT); editorBox.setAlignment(Pos.CENTER_LEFT);
editorBox.setSpacing(5); editorBox.setSpacing(5);
Label lsnLabel = new Label("LSN"); Label lcnLabel = new Label("LCN");
editorBox.getChildren().addAll(lsnLabel,getLogicalSlotNumberField()); editorBox.getChildren().addAll(lcnLabel, getLogicalChannelNumberField());
Label downlinkLabel = new Label("Frequency (MHz)"); Label downlinkLabel = new Label("Frequency (MHz)");
downlinkLabel.setPadding(new Insets(0,0,0,5)); downlinkLabel.setPadding(new Insets(0,0,0,5));
editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField()); editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField());
// Label uplinkLabel = new Label("Uplink");
// uplinkLabel.setPadding(new Insets(0,0,0,5));
// editorBox.getChildren().addAll(uplinkLabel,getUplinkFrequencyField());
GridPane.setConstraints(editorBox, 0, row, 4, 1); GridPane.setConstraints(editorBox, 0, row, 4, 1);
gridPane.getChildren().add(editorBox); gridPane.getChildren().add(editorBox);
@@ -287,20 +283,22 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor())); mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor()));
mTimeslotFrequencyTable.setPrefHeight(100.0); mTimeslotFrequencyTable.setPrefHeight(100.0);
TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LSN"); TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LCN");
numberColumn.setPrefWidth(70); numberColumn.setPrefWidth(75);
numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty()); numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty());
mTimeslotFrequencyTable.getColumns().addAll(numberColumn); mTimeslotFrequencyTable.getColumns().addAll(numberColumn);
mTimeslotFrequencyTable.getSortOrder().add(numberColumn); mTimeslotFrequencyTable.getSortOrder().add(numberColumn);
TableColumn<TimeslotFrequency,Number> downlinkColumn = new TableColumn("Frequency (MHz)"); TableColumn<TimeslotFrequency,Number> downlinkColumn = new TableColumn("Frequency (MHz)");
downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz()); downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz());
downlinkColumn.setPrefWidth(175); downlinkColumn.setPrefWidth(150);
mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn); mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn);
// TableColumn<TimeslotFrequency,Number> uplinkColumn = new TableColumn("Uplink (MHz)"); TableColumn<TimeslotFrequency,String> lsnColumn = new TableColumn("IDs (TS1/TS2)");
// uplinkColumn.setCellValueFactory(cellData -> cellData.getValue().getUplinkMHz()); lsnColumn.setPrefWidth(225);
// mTimeslotFrequencyTable.getColumns().addAll(uplinkColumn); lsnColumn.setCellValueFactory(new PropertyValueFactory<>("logicalSlotNumbers"));
mTimeslotFrequencyTable.getColumns().addAll(lsnColumn);
mTimeslotFrequencyTable.getSelectionModel().selectedItemProperty() mTimeslotFrequencyTable.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> setTimeslot(newValue)); .addListener((observable, oldValue, newValue) -> setTimeslot(newValue));
@@ -317,20 +315,20 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
//Preserve the current modified flag state since setting values in the editor will change it. //Preserve the current modified flag state since setting values in the editor will change it.
boolean modified = modifiedProperty().get(); boolean modified = modifiedProperty().get();
getLogicalSlotNumberField().setDisable(timeslot == null); getLogicalChannelNumberField().setDisable(timeslot == null);
getDownlinkFrequencyField().setDisable(timeslot == null); getDownlinkFrequencyField().setDisable(timeslot == null);
// getUplinkFrequencyField().setDisable(timeslot == null); // getUplinkFrequencyField().setDisable(timeslot == null);
getDeleteTimeslotFrequencyButton().setDisable(timeslot == null); getDeleteTimeslotFrequencyButton().setDisable(timeslot == null);
if(timeslot != null) if(timeslot != null)
{ {
getLogicalSlotNumberField().set(timeslot.getNumber()); getLogicalChannelNumberField().set(timeslot.getNumber());
getDownlinkFrequencyField().set(timeslot.getDownlinkFrequency()); getDownlinkFrequencyField().set(timeslot.getDownlinkFrequency());
// getUplinkFrequencyField().set(timeslot.getUplinkFrequency()); // getUplinkFrequencyField().set(timeslot.getUplinkFrequency());
} }
else else
{ {
getLogicalSlotNumberField().set(0); getLogicalChannelNumberField().set(0);
getDownlinkFrequencyField().set(0); getDownlinkFrequencyField().set(0);
// getUplinkFrequencyField().set(0); // getUplinkFrequencyField().set(0);
} }
@@ -412,19 +410,19 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
return mDeleteTimeslotFrequencyButton; return mDeleteTimeslotFrequencyButton;
} }
private IntegerTextField getLogicalSlotNumberField() private IntegerTextField getLogicalChannelNumberField()
{ {
if(mLogicalSlotNumberField == null) if(mLogicalChannelNumberField == null)
{ {
mLogicalSlotNumberField = new IntegerTextField(); mLogicalChannelNumberField = new IntegerTextField();
mLogicalSlotNumberField.setDisable(true); mLogicalChannelNumberField.setDisable(true);
mLogicalSlotNumberField.setPrefWidth(65); mLogicalChannelNumberField.setPrefWidth(65);
mLogicalSlotNumberField.textProperty().addListener((observable, oldValue, newValue) -> { mLogicalChannelNumberField.textProperty().addListener((observable, oldValue, newValue) -> {
TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem(); TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
if(selected != null) if(selected != null)
{ {
Integer value = mLogicalSlotNumberField.get(); Integer value = mLogicalChannelNumberField.get();
if(value != null) if(value != null)
{ {
@@ -436,7 +434,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
}); });
} }
return mLogicalSlotNumberField; return mLogicalChannelNumberField;
} }
private FrequencyField getDownlinkFrequencyField() private FrequencyField getDownlinkFrequencyField()
@@ -610,8 +608,8 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
getTimeslotTable().setDisable(config == null); getTimeslotTable().setDisable(config == null);
getAddTimeslotFrequencyButton().setDisable(config == null); getAddTimeslotFrequencyButton().setDisable(config == null);
getDeleteTimeslotFrequencyButton().setDisable(true); getDeleteTimeslotFrequencyButton().setDisable(true);
getLogicalSlotNumberField().set(0); getLogicalChannelNumberField().set(0);
getLogicalSlotNumberField().setDisable(true); getLogicalChannelNumberField().setDisable(true);
getDownlinkFrequencyField().set(0); getDownlinkFrequencyField().set(0);
getDownlinkFrequencyField().setDisable(true); getDownlinkFrequencyField().setDisable(true);
// getUplinkFrequencyField().set(0); // getUplinkFrequencyField().set(0);

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -41,13 +41,12 @@ import io.github.dsheirer.rrapi.type.Tag;
import io.github.dsheirer.rrapi.type.Talkgroup; import io.github.dsheirer.rrapi.type.Talkgroup;
import io.github.dsheirer.rrapi.type.Type; import io.github.dsheirer.rrapi.type.Type;
import io.github.dsheirer.rrapi.type.Voice; import io.github.dsheirer.rrapi.type.Voice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Utility class for decoding type, flavor and voice for systems and formatting talkgroups according to user preferences. * Utility class for decoding type, flavor and voice for systems and formatting talkgroups according to user preferences.
@@ -374,16 +373,10 @@ public class RadioReferenceDecoder
} }
} }
int lsn = (lcn - 1) * 2 + 1; TimeslotFrequency timeslotFrequency = new TimeslotFrequency();
TimeslotFrequency timeslot1Frequency = new TimeslotFrequency(); timeslotFrequency.setNumber(lcn);
timeslot1Frequency.setNumber(lsn); timeslotFrequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
timeslot1Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6)); frequencies.add(timeslotFrequency);
frequencies.add(timeslot1Frequency);
TimeslotFrequency timeslot2Frequency = new TimeslotFrequency();
timeslot2Frequency.setNumber(lsn + 1);
timeslot2Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
frequencies.add(timeslot2Frequency);
} }
return frequencies; return frequencies;

View File

@@ -17,20 +17,22 @@
* **************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.gui.dmr; package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.DMRMessageFramer; import io.github.dsheirer.module.decode.dmr.DMRMessageFramer;
import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor; import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor;
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR; import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
import io.github.dsheirer.record.binary.BinaryReader; import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.util.ThreadPool;
import java.io.File; import java.io.File;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import javafx.application.Application;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@@ -40,10 +42,10 @@ import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition; import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
@@ -58,21 +60,19 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Callback; import javafx.util.Callback;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* Utility application to load and view a DMR .bits recording file with the messages fully parsed. * DMR Viewer panel
*/ */
public class DMRRecordingViewer extends VBox public class DmrViewer extends VBox
{ {
private static final Logger mLog = LoggerFactory.getLogger(DMRRecordingViewer.class); private static final Logger mLog = LoggerFactory.getLogger(DmrViewer.class);
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY); private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory"; private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.dmr";
private Preferences mPreferences = Preferences.userNodeForPackage(DMRRecordingViewer.class); private Preferences mPreferences = Preferences.userNodeForPackage(DmrViewer.class);
private Scene mScene;
private Button mSelectFileButton; private Button mSelectFileButton;
private Label mSelectedFileLabel; private Label mSelectedFileLabel;
private TableView<IMessage> mMessageTableView; private TableView<IMessage> mMessageTableView;
@@ -86,8 +86,9 @@ public class DMRRecordingViewer extends VBox
private TextField mFindText; private TextField mFindText;
private Button mFindButton; private Button mFindButton;
private Button mFindNextButton; private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
public DMRRecordingViewer() public DmrViewer()
{ {
setPadding(new Insets(5)); setPadding(new Insets(5));
setSpacing(5); setSpacing(5);
@@ -132,6 +133,21 @@ public class DMRRecordingViewer extends VBox
getChildren().addAll(fileBox, filterBox, getMessageTableView()); getChildren().addAll(fileBox, filterBox, getMessageTableView());
} }
/**
* 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;
}
/** /**
* Processes the recording file and loads the content into the viewer * Processes the recording file and loads the content into the viewer
* @param file containing a .bits recording of decoded DMR data. * @param file containing a .bits recording of decoded DMR data.
@@ -140,33 +156,45 @@ public class DMRRecordingViewer extends VBox
{ {
if(file != null && file.exists()) if(file != null && file.exists())
{ {
getSelectedFileLabel().setText(file.getName());
mMessages.clear(); mMessages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
final boolean useCompressed = getUseCompressedTalkgroups().isSelected();
DMRMessageFramer messageFramer = new DMRMessageFramer(null); ThreadPool.CACHED.submit(new Runnable()
DecodeConfigDMR config = new DecodeConfigDMR();
if(getUseCompressedTalkgroups().isSelected())
{ {
config.setUseCompressedTalkgroups(true); @Override
} public void run()
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
messageFramer.setListener(messageProcessor);
messageProcessor.setMessageListener(message -> mMessages.add(message));
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{ {
ByteBuffer buffer = reader.next(); List<IMessage> messages = new ArrayList<>();
messageFramer.receive(buffer); DMRMessageFramer messageFramer = new DMRMessageFramer(null);
} DecodeConfigDMR config = new DecodeConfigDMR();
} config.setUseCompressedTalkgroups(useCompressed);
catch(Exception ioe) DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
{ messageFramer.setListener(messageProcessor);
ioe.printStackTrace(); messageProcessor.setMessageListener(message -> messages.add(message));
}
getMessageTableView().scrollTo(0); try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessages.addAll(messages);
getMessageTableView().scrollTo(0);
});
}
});
} }
} }
@@ -176,9 +204,9 @@ public class DMRRecordingViewer extends VBox
private void updateFilters() private void updateFilters()
{ {
Predicate<IMessage> timeslotPredicate = message -> Predicate<IMessage> timeslotPredicate = message ->
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) || (getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) || (getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
(getShowTS2().isSelected() && (message.getTimeslot() == 2)); (getShowTS2().isSelected() && (message.getTimeslot() == 2));
String filterText = getSearchText().getText(); String filterText = getSearchText().getText();
@@ -256,6 +284,7 @@ public class DMRRecordingViewer extends VBox
if(mMessageTableView == null) if(mMessageTableView == null)
{ {
mMessageTableView = new TableView<>(); mMessageTableView = new TableView<>();
mMessageTableView.setPlaceholder(getLoadingIndicator());
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages); SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty()); sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
mMessageTableView.setItems(sortedList); mMessageTableView.setItems(sortedList);
@@ -459,6 +488,12 @@ public class DMRRecordingViewer extends VBox
if(mFindText == null) if(mFindText == null)
{ {
mFindText = new TextField(); mFindText = new TextField();
mFindText.setOnKeyPressed(event -> {
if(event.getCode().equals(KeyCode.ENTER))
{
getFindButton().fire();
}
});
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters()); mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
} }
@@ -503,37 +538,9 @@ public class DMRRecordingViewer extends VBox
{ {
if(mUseCompressedTalkgroups == null) if(mUseCompressedTalkgroups == null)
{ {
mUseCompressedTalkgroups = new CheckBox("Use Compressed Talkgroups"); mUseCompressedTalkgroups = new CheckBox("Use Hytera Tier III Compressed Talkgroups");
} }
return mUseCompressedTalkgroups; return mUseCompressedTalkgroups;
} }
public static void main(String[] args)
{
Application viewer = new Application()
{
@Override
public void start(Stage primaryStage) throws Exception
{
Scene scene = new Scene(new DMRRecordingViewer(), 1100, 800);
primaryStage.setTitle("DMR Recording Viewer");
primaryStage.setScene(scene);
primaryStage.show();
}
};
Runnable r = () -> {
try
{
viewer.start(new Stage());
}
catch(Exception e)
{
mLog.error("Error starting DMR recording viewer application", e);
}
};
Platform.startup(r);
}
} }

View File

@@ -0,0 +1,462 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.message.IMessage;
import io.github.dsheirer.message.StuffBitsMessage;
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;
import io.github.dsheirer.util.ThreadPool;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
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.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 1 viewer panel
*/
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 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 TextField mSearchText;
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
public P25P1Viewer()
{
setPadding(new Insets(5));
setSpacing(5);
HBox fileBox = new HBox();
fileBox.setMaxWidth(Double.MAX_VALUE);
fileBox.setAlignment(Pos.CENTER_LEFT);
fileBox.setSpacing(5);
HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
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);
filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
getSearchText());
VBox.setVgrow(fileBox, Priority.NEVER);
VBox.setVgrow(filterBox, Priority.NEVER);
VBox.setVgrow(getMessageTableView(), Priority.ALWAYS);
getChildren().addAll(fileBox, filterBox, getMessageTableView());
}
/**
* 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 DMR data.
*/
private void load(File file)
{
if(file != null && file.exists())
{
mMessages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
ThreadPool.CACHED.submit(() -> {
List<IMessage> messages = new ArrayList<>();
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
messageFramer.setListener(messageProcessor);
messageProcessor.setMessageListener(message -> {
if(!(message instanceof StuffBitsMessage))
{
messages.add(message);
}
});
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessages.addAll(messages);
getMessageTableView().scrollTo(0);
});
});
}
}
/**
* Updates the filter(s) applies to the list of messages
*/
private void updateFilters()
{
String filterText = getSearchText().getText();
if(filterText != null && !filterText.isEmpty())
{
Predicate<IMessage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
mFilteredMessages.setPredicate(textPredicate);
}
else
{
mFilteredMessages.setPredicate(null);
}
}
/**
* 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(IMessage message: mFilteredMessages)
{
if(message.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
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())
{
IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem();
if(selected == null)
{
find(text);
return;
}
int row = mFilteredMessages.indexOf(selected);
for(int x = row + 1; x < mFilteredMessages.size(); x++)
{
if(x < mFilteredMessages.size())
{
IMessage message = mFilteredMessages.get(x);
if(message.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
return;
}
}
}
}
}
/**
* List view control with DMR messages
*/
private TableView<IMessage> getMessageTableView()
{
if(mMessageTableView == null)
{
mMessageTableView = new TableView<>();
mMessageTableView.setPlaceholder(getLoadingIndicator());
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
mMessageTableView.setItems(sortedList);
mMessageTableView.setOnKeyPressed(event ->
{
if(KEY_CODE_COPY.match(event))
{
final Set<Integer> rows = new TreeSet<>();
for (final TablePosition tablePosition : mMessageTableView.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 : mMessageTableView.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 messageColumn = new TableColumn();
messageColumn.setPrefWidth(1000);
messageColumn.setText("Message");
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
SimpleStringProperty property = new SimpleStringProperty();
if(param.getValue() instanceof IMessage message)
{
property.set(message.toString());
}
return property;
});
mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn);
}
return mMessageTableView;
}
/**
* 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 DMR .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;
}
/**
* Search text filter box
* @return text control for entering search text
*/
private TextField getSearchText()
{
if(mSearchText == null)
{
mSearchText = new TextField();
mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mSearchText;
}
/**
* Text box for find text
*/
private TextField getFindText()
{
if(mFindText == null)
{
mFindText = new TextField();
mFindText.setOnKeyPressed(event -> {
if(event.getCode().equals(KeyCode.ENTER))
{
getFindButton().fire();
}
});
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mFindText;
}
/**
* Find button to search for the text in the find text box.
* @return button
*/
private Button getFindButton()
{
if(mFindButton == null)
{
mFindButton = new Button("Find");
mFindButton.setOnAction(event -> find(getFindText().getText()));
}
return mFindButton;
}
/**
* Find next button to search for the text in the find text box.
* @return button
*/
private Button getFindNextButton()
{
if(mFindNextButton == null)
{
mFindNextButton = new Button("Next");
mFindNextButton.setOnAction(event -> findNext(getFindText().getText()));
}
return mFindNextButton;
}
}

View File

@@ -0,0 +1,180 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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 javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
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.
*/
public class RecordingViewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class);
private MenuBar mMenuBar;
private TabPane mTabPane;
private int mTabCounterDmr = 1;
private int mTabCounterP25P1 = 1;
/**
* Constructs an instance
*/
public RecordingViewer()
{
VBox.setVgrow(getTabPane(), Priority.ALWAYS);
getChildren().addAll(getMenuBar(), getTabPane());
}
public MenuBar getMenuBar()
{
if(mMenuBar == null)
{
mMenuBar = new MenuBar();
Menu fileMenu = new Menu("File");
Menu createNewViewerMenu = new Menu("New Viewer ...");
MenuItem dmrMenuItem = new MenuItem("DMR");
dmrMenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer())));
MenuItem p25p1MenuItem = new MenuItem("P25P1");
p25p1MenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer())));
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
MenuItem exitMenu = new MenuItem("Exit");
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
fileMenu.getItems().addAll(createNewViewerMenu, new SeparatorMenuItem(), exitMenu);
mMenuBar.getMenus().add(fileMenu);
}
return mMenuBar;
}
/**
* Tab pane for each viewer instance
*/
public TabPane getTabPane()
{
if(mTabPane == null)
{
mTabPane = new TabPane();
mTabPane.setMaxHeight(Double.MAX_VALUE);
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()));
}
return mTabPane;
}
/**
* Decorates the tab with label renaming feature using double-click or right-click to change text
*/
public class LabeledTab extends Tab
{
private Label mLabel = new Label();
private TextField mTextField = new TextField();
/**
* Constructs an instance
* @param label to use initially
* @param node for content
*/
public LabeledTab(String label, Node node)
{
super(null, node);
mLabel.setText(label);
setGraphic(mLabel);
mLabel.setOnMouseClicked(event -> {
if((event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() > 1) ||
event.getButton().equals(MouseButton.SECONDARY))
{
setGraphic(mTextField);
mTextField.setText(mLabel.getText());
mTextField.selectAll();
mTextField.requestFocus();
}
});
mTextField.setOnAction(event -> {
mLabel.setText(mTextField.getText());
LabeledTab.this.setGraphic(mLabel);
});
mTextField.focusedProperty().addListener((observable, oldValue, newValue) -> {
if(!newValue)
{
mLabel.setText(mTextField.getText());
LabeledTab.this.setGraphic(mLabel);
}
});
}
public LabeledTab(String label)
{
this(label, null);
}
}
public static void main(String[] args)
{
Application viewer = new Application()
{
@Override
public void start(Stage primaryStage) throws Exception
{
Scene scene = new Scene(new RecordingViewer(), 1100, 800);
primaryStage.setTitle("Message Recording Viewer (.bits)");
primaryStage.setScene(scene);
primaryStage.show();
}
};
Runnable r = () -> {
try
{
viewer.start(new Stage());
}
catch(Exception e)
{
mLog.error("Error starting message recording viewer application", e);
}
};
Platform.startup(r);
}
}

View File

@@ -17,17 +17,17 @@
* **************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.gui.dmr; package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.gui.JavaFxWindowRequest; import io.github.dsheirer.gui.JavaFxWindowRequest;
/** /**
* Request to show the DMR recording viewer * Request to show the recording viewer
*/ */
public class ViewDmrRecordingViewerRequest extends JavaFxWindowRequest public class ViewRecordingViewerRequest extends JavaFxWindowRequest
{ {
/** /**
* Constructs an instance * Constructs an instance
*/ */
public ViewDmrRecordingViewerRequest() {} public ViewRecordingViewerRequest() {}
} }

View File

@@ -32,6 +32,7 @@ public enum Form
CHANNEL_DESCRIPTOR, CHANNEL_DESCRIPTOR,
CHANNEL_NAME, CHANNEL_NAME,
CHANNEL_FREQUENCY, CHANNEL_FREQUENCY,
DCS,
DECODER_TYPE, DECODER_TYPE,
DTMF, DTMF,
ENCRYPTION_KEY, ENCRYPTION_KEY,

View File

@@ -1,69 +1,161 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2019 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * 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 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.patch; package io.github.dsheirer.identifier.patch;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
/**
* Temporary grouping or regrouping of talkgroups or individual radios.
*/
public class PatchGroup public class PatchGroup
{ {
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroup.class);
private TalkgroupIdentifier mPatchGroupIdentifier; private TalkgroupIdentifier mPatchGroupIdentifier;
private List<TalkgroupIdentifier> mPatchedGroupIdentifiers = new ArrayList<>(); private List<TalkgroupIdentifier> mPatchedTalkgroupIdentifiers = new ArrayList<>();
private List<RadioIdentifier> mPatchedRadioIdentifiers = new ArrayList<>();
private int mVersion;
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier) /**
* Constructs an instance
* @param patchGroupIdentifier for the patch group
* @param version for this patch group identifier. Note: if the patch group identifier is frequently reused, a
* new version number will indicate a new instance of the patch group.
*/
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier, int version)
{ {
mPatchGroupIdentifier = patchGroupIdentifier; mPatchGroupIdentifier = patchGroupIdentifier;
mVersion = version;
} }
/**
* Constructs an instance
* @param patchGroupIdentifier that is the overall identifier for this temporary grouping.
*/
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier)
{
this(patchGroupIdentifier, 0);
}
/**
* Version number for this patch group used to differentiate frequently reused patch group identifiers.
* @return version number (aka super group sequence number).
*/
public int getVersion()
{
return mVersion;
}
/**
* Patch group identifier.
* @return identifier
*/
public TalkgroupIdentifier getPatchGroup() public TalkgroupIdentifier getPatchGroup()
{ {
return mPatchGroupIdentifier; return mPatchGroupIdentifier;
} }
public void addPatchedGroup(TalkgroupIdentifier patchedGroupIdentifier) /**
* Indicates if this patch group contains any patched talkgroups.
* @return true if there are patched talkgroups.
*/
public boolean hasPatchedTalkgroups()
{ {
if(!mPatchedGroupIdentifiers.contains(patchedGroupIdentifier)) return !getPatchedTalkgroupIdentifiers().isEmpty();
}
/**
* Indicates if this patch group contains any patched radio identifiers.
* @return true if there are patched radios.
*/
public boolean hasPatchedRadios()
{
return !getPatchedRadioIdentifiers().isEmpty();
}
/**
* Adds the talkgroup identifier to this patched group.
* @param patchedGroupIdentifier to add
*/
public void addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
{
if(!mPatchedTalkgroupIdentifiers.contains(patchedGroupIdentifier))
{ {
mPatchedGroupIdentifiers.add(patchedGroupIdentifier); mPatchedTalkgroupIdentifiers.add(patchedGroupIdentifier);
} }
} }
public void addPatchedGroups(List<TalkgroupIdentifier> patchedGroupIdentifiers) /**
* Adds a list of talkgroup identifiers to this patched group.
* @param patchedGroupIdentifiers to add
*/
public void addPatchedTalkgroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
{ {
for(TalkgroupIdentifier identifier : patchedGroupIdentifiers) for(TalkgroupIdentifier identifier : patchedGroupIdentifiers)
{ {
addPatchedGroup(identifier); addPatchedTalkgroup(identifier);
} }
} }
public List<TalkgroupIdentifier> getPatchedGroupIdentifiers() /**
* List of patched group identifiers that are part of this patch group.
* @return patched talkgroups.
*/
public List<TalkgroupIdentifier> getPatchedTalkgroupIdentifiers()
{ {
return mPatchedGroupIdentifiers; return mPatchedTalkgroupIdentifiers;
}
/**
* Adds the radio identifier to this patched group.
* @param patchedRadioIdentifier to add
*/
public void addPatchedRadio(RadioIdentifier patchedRadioIdentifier)
{
if(!mPatchedRadioIdentifiers.contains(patchedRadioIdentifier))
{
mPatchedRadioIdentifiers.add(patchedRadioIdentifier);
}
}
/**
* Adds a list of radio identifiers to this patched group.
* @param patchedRadioIdentifiers to add
*/
public void addPatchedRadios(List<RadioIdentifier> patchedRadioIdentifiers)
{
for(RadioIdentifier identifier : patchedRadioIdentifiers)
{
addPatchedRadio(identifier);
}
}
/**
* List of patched radio identifiers that are part of this patch group.
* @return patched radios.
*/
public List<RadioIdentifier> getPatchedRadioIdentifiers()
{
return mPatchedRadioIdentifiers;
} }
@Override @Override
@@ -72,9 +164,14 @@ public class PatchGroup
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("P:").append(getPatchGroup()); sb.append("P:").append(getPatchGroup());
if(!getPatchedGroupIdentifiers().isEmpty()) if(hasPatchedTalkgroups())
{ {
sb.append(" ").append(getPatchedGroupIdentifiers()); sb.append(getPatchedTalkgroupIdentifiers());
}
if(hasPatchedRadios())
{
sb.append(getPatchedRadioIdentifiers());
} }
return sb.toString(); return sb.toString();

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,27 +22,23 @@ package io.github.dsheirer.identifier.patch;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Manager for (temporary) patch groups and related identifiers. * Manager for (temporary) patch groups aka super groups. This manager monitors patch group additions and deletions and
* maintains a map of the current state of each patch group active in the system.
* *
* Patch groups are primarily used on Motorola P25 systems. This manager monitors patch group additions and deletions * Traffic channel calls will reference a patch group by simply using the patch group identifier. This
* and maintains a map of the current patch groups. * manager will replace that reference with the current state of the full patch group so that the call event has the
* * full patch group including all patched talkgroups or individual radios, which may not have been included in the
* Traffic channel calls will sometimes reference a patch group by simply using the patch group identifier. This * patch group reference on the control or traffic channel.
* manager will replace that reference with the full patch group so that the call event has the full patch group
* including all of the patched groups, which may not have been included in the patch group reference on the
* traffic channel.
*/ */
public class PatchGroupManager public class PatchGroupManager
{ {
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroupManager.class);
private Map<Integer,Long> mPatchGroupLastUpdatedMap = new HashMap<>();
private Map<Integer,PatchGroupIdentifier> mPatchGroupMap = new HashMap<>(); private Map<Integer,PatchGroupIdentifier> mPatchGroupMap = new HashMap<>();
/** /**
@@ -58,13 +54,13 @@ public class PatchGroupManager
public void clear() public void clear()
{ {
mPatchGroupMap.clear(); mPatchGroupMap.clear();
mPatchGroupLastUpdatedMap.clear();
} }
/** /**
* Adds the patch group identifier to the set of managed patch groups. If the patch group is already being * Adds the patch group identifier to the set of managed patch groups. If the patch group is already being
* managed, the new identifier is checked for additional patched talkgroups and they are added to the existing * managed, the new patch group argument is checked for additional patched talkgroups and they are added to the
* managed patch group. * existing managed patch group. Where the patch group version (super group sequence number) is different, the
* existing patch group is discarded and replaced with the updated version of the patch group.
* *
* @param patchGroupIdentifier to add or update * @param patchGroupIdentifier to add or update
*/ */
@@ -76,8 +72,19 @@ public class PatchGroupManager
{ {
if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue())) if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue()))
{ {
PatchGroup current = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue(); PatchGroup existingPatchGroup = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
current.addPatchedGroups(update.getPatchedGroupIdentifiers());
//If the patch group version number is the same, this is an update
if(existingPatchGroup.getVersion() == update.getVersion())
{
existingPatchGroup.addPatchedTalkgroups(update.getPatchedTalkgroupIdentifiers());
existingPatchGroup.addPatchedRadios(update.getPatchedRadioIdentifiers());
}
//Otherwise, this is a replace operation.
else
{
mPatchGroupMap.put(update.getPatchGroup().getValue(), patchGroupIdentifier);
}
} }
else else
{ {
@@ -109,7 +116,6 @@ public class PatchGroupManager
{ {
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue(); int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
mPatchGroupMap.remove(id); mPatchGroupMap.remove(id);
mPatchGroupLastUpdatedMap.remove(id);
} }
/** /**
@@ -153,23 +159,28 @@ public class PatchGroupManager
*/ */
public Identifier update(Identifier identifier) public Identifier update(Identifier identifier)
{ {
if(identifier == null) if(identifier != null && identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
{
return identifier;
}
if(identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
{ {
switch(identifier.getForm()) switch(identifier.getForm())
{ {
case PATCH_GROUP: case TALKGROUP:
if(identifier instanceof PatchGroupIdentifier) if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
Identifier mapValue = mPatchGroupMap.get(talkgroupIdentifier.getValue());
if (mapValue != null)
{
return mapValue;
}
}
break;
case PATCH_GROUP:
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{ {
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
int patchGroupId = patchGroupIdentifier.getValue().getPatchGroup().getValue(); int patchGroupId = patchGroupIdentifier.getValue().getPatchGroup().getValue();
Identifier mapValue = mPatchGroupMap.get(patchGroupId); Identifier mapValue = mPatchGroupMap.get(patchGroupId);
if (mapValue != null) { if (mapValue != null)
{
return mapValue; return mapValue;
} }
} }

View File

@@ -145,6 +145,7 @@ public abstract class DecodeConfigAnalog extends DecodeConfiguration implements
{ {
BW_3_0("3.0 kHz", 3000.0), BW_3_0("3.0 kHz", 3000.0),
BW_5_0("5.0 kHz", 5000.0), BW_5_0("5.0 kHz", 5000.0),
BW_7_5("7.5 kHz", 7500.0),
BW_8_33("8.33 kHz", 8333.0), BW_8_33("8.33 kHz", 8333.0),
BW_12_5("12.5 kHz", 12500.0), BW_12_5("12.5 kHz", 12500.0),
BW_15_0("15.0 kHz", 15000.0), BW_15_0("15.0 kHz", 15000.0),
@@ -168,7 +169,7 @@ public abstract class DecodeConfigAnalog extends DecodeConfiguration implements
public static EnumSet<Bandwidth> AM_BANDWIDTHS = EnumSet.of(BW_3_0, BW_5_0, BW_8_33, BW_15_0, BW_25_0); public static EnumSet<Bandwidth> AM_BANDWIDTHS = EnumSet.of(BW_3_0, BW_5_0, BW_8_33, BW_15_0, BW_25_0);
//FM demodulator channel bandwidth options //FM demodulator channel bandwidth options
public static EnumSet<Bandwidth> FM_BANDWIDTHS = EnumSet.of(BW_12_5, BW_25_0); public static EnumSet<Bandwidth> FM_BANDWIDTHS = EnumSet.of(BW_7_5, BW_12_5, BW_25_0);
/** /**
* Indicates if this entry is valid for the AM decoder. * Indicates if this entry is valid for the AM decoder.

View File

@@ -213,7 +213,12 @@ public enum DCSCode
/** /**
* Inverted DCS codes * Inverted DCS codes
*/ */
private static final EnumSet<DCSCode> INVERTED_CODES = EnumSet.range(I023, I754); public static final EnumSet<DCSCode> INVERTED_CODES = EnumSet.range(I023, I754);
/**
* Standard DCS codes
*/
public static final EnumSet<DCSCode> STANDARD_CODES = EnumSet.range(N023,N754);
/** /**
* Lookup map for quickly finding a DCS code from the transmitted value. * Lookup map for quickly finding a DCS code from the transmitted value.

View File

@@ -47,7 +47,7 @@ public class DCSMessage extends Message
@Override @Override
public String toString() public String toString()
{ {
return "Digital Coded Squelch (DCS) Detected: " + mDCSCode.name(); return "Digital Coded Squelch (DCS) Detected: " + mDCSCode.toString();
} }
/** /**

View File

@@ -57,6 +57,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcem
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice; import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant; import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage; import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage; import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
@@ -65,25 +66,33 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoice
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel; import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
import io.github.dsheirer.module.decode.dmr.message.data.packet.DMRPacketMessage; import io.github.dsheirer.module.decode.dmr.message.data.packet.DMRPacketMessage;
import io.github.dsheirer.module.decode.dmr.message.data.packet.UDTShortMessageService; import io.github.dsheirer.module.decode.dmr.message.data.packet.UDTShortMessageService;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions; import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
import io.github.dsheirer.module.decode.event.DecodeEvent; import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.event.DecodeEventType; import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent; import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket; import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket; import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.ars.ARSPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket;
import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest; import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
import io.github.dsheirer.util.PacketUtil; import io.github.dsheirer.util.PacketUtil;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import org.jdesktop.swingx.mapviewer.GeoPosition; import org.jdesktop.swingx.mapviewer.GeoPosition;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -102,7 +111,7 @@ public class DMRDecoderState extends TimeslotDecoderState
private DecodeEvent mCurrentCallEvent; private DecodeEvent mCurrentCallEvent;
private long mCurrentFrequency; private long mCurrentFrequency;
private boolean mIgnoreCRCChecksums; private boolean mIgnoreCRCChecksums;
private Map<Integer,DecodeEvent> mDetectedCallEventsMap = new TreeMap<>(); private Map<DMRChannel,DecodeEvent> mDetectedCallEventsMap = new HashMap<>();
private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST = private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST =
new AddChannelRotationActiveStateRequest(State.ACTIVE); new AddChannelRotationActiveStateRequest(State.ACTIVE);
@@ -289,7 +298,7 @@ public class DMRDecoderState extends TimeslotDecoderState
DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, sms.getTimestamp()) DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, sms.getTimestamp())
.details("MESSAGE: " + sms.getSMS()) .details("MESSAGE: " + sms.getSMS())
.identifiers(new IdentifierCollection(sms.getIdentifiers())) .identifiers(new IdentifierCollection(sms.getIdentifiers()))
.timeslot(sms.getTimeslot()) .timeslot(getTimeslot())
.build(); .build();
broadcast(smsEvent); broadcast(smsEvent);
} }
@@ -312,11 +321,51 @@ public class DMRDecoderState extends TimeslotDecoderState
DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, packet.getTimestamp()) DecodeEvent smsEvent = DMRDecodeEvent.builder(DecodeEventType.SMS, packet.getTimestamp())
.identifiers(mic) .identifiers(mic)
.timeslot(packet.getTimeslot()) .timeslot(getTimeslot())
.details("SMS:" + hyteraSmsPacket.getSMS()) .details("SMS:" + hyteraSmsPacket.getSMS())
.build(); .build();
broadcast(smsEvent); broadcast(smsEvent);
} }
//Hytera Radio Registration Service (RRS)
else if(packet.getPacket() instanceof HyteraRrsPacket rrs)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
StringBuilder sb = new StringBuilder();
sb.append("HYTERA RRS REGISTER RADIO:");
sb.append(rrs.getDestination());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(sb.toString())
.build();
broadcast(shortDataEvent);
}
//Hytera Short Data
else if(packet.getPacket() instanceof HyteraShortDataPacket hsdp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
StringBuilder sb = new StringBuilder();
sb.append("HYTERA");
if(hsdp.getPacketSequence().isEncrypted())
{
HyteraDataEncryptionHeader hdeh = (HyteraDataEncryptionHeader)hsdp.getPacketSequence().getProprietaryDataHeader();
sb.append(" ENCRYPTED ALGORITHM:").append(hdeh.getAlgorithm());
sb.append(" KEY:").append(hdeh.getKeyId());
sb.append(" IV:").append(hdeh.getIV());
}
sb.append(" SHORT DATA:").append(hsdp.getMessage().toHexString());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(sb.toString())
.build();
broadcast(shortDataEvent);
}
//Unknown Hytera Long Data Service Token Message //Unknown Hytera Long Data Service Token Message
else if(packet.getPacket() instanceof HyteraUnknownPacket hyteraUnknownPacket) else if(packet.getPacket() instanceof HyteraUnknownPacket hyteraUnknownPacket)
{ {
@@ -324,33 +373,70 @@ public class DMRDecoderState extends TimeslotDecoderState
DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp()) DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp())
.identifiers(mic) .identifiers(mic)
.timeslot(packet.getTimeslot()) .timeslot(getTimeslot())
.details("HYTERA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString()) .details("HYTERA LONG DATA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
.build(); .build();
broadcast(unknownTokenEvent); broadcast(unknownTokenEvent);
} }
//Motorola ARS
else if(packet.getPacket() instanceof ARSPacket ars)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(ars.toString())
.build();
broadcast(shortDataEvent);
}
//Motorola LRRP
else if(packet.getPacket() instanceof LRRPPacket lrrp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.LRRP, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(lrrp.toString())
.build();
broadcast(shortDataEvent);
}
//Motorola XCMP
else if(packet.getPacket() instanceof XCMPPacket xcmp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.XCMP, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(xcmp.toString())
.build();
broadcast(shortDataEvent);
}
else else
{ {
DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp()) DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp())
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers())) .identifiers(new IdentifierCollection(packet.getIdentifiers()))
.timeslot(packet.getTimeslot()) .timeslot(getTimeslot())
.details(packet.toString()) .details(packet.toString())
.build(); .build();
broadcast(packetEvent); broadcast(packetEvent);
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket()); }
if (geoPosition != null) { GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
.channel(getCurrentChannel())
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
.protocol(Protocol.LRRP)
.location(geoPosition)
.build();
broadcast(plottableDecodeEvent); if (geoPosition != null) {
} PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
.channel(getCurrentChannel())
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
.protocol(Protocol.LRRP)
.location(geoPosition)
.build();
broadcast(plottableDecodeEvent);
} }
} }
@@ -370,6 +456,19 @@ public class DMRDecoderState extends TimeslotDecoderState
{ {
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot())); broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
} }
if(message.getSyncPattern() == DMRSyncPattern.BS_VOICE_FRAME_F && message instanceof VoiceEMBMessage voiceEmb)
{
if(voiceEmb.hasEmbeddedParameters())
{
EmbeddedParameters embedded = voiceEmb.getEmbeddedParameters();
if(embedded.getShortBurst() instanceof EncryptionParameters arc4)
{
updateEncryptedCall(arc4, true, voiceEmb.getTimestamp());
}
}
}
} }
/** /**
@@ -637,21 +736,22 @@ public class DMRDecoderState extends TimeslotDecoderState
ChannelGrant dataGrant = (ChannelGrant)csbk; ChannelGrant dataGrant = (ChannelGrant)csbk;
DMRChannel channel = dataGrant.getChannel(); DMRChannel channel = dataGrant.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager()) if(hasTrafficChannelManager())
{ {
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
csbk.getTimestamp(), csbk.isEncrypted()); csbk.getTimestamp(), csbk.isEncrypted());
} }
else else
{ {
DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); DecodeEvent event = mDetectedCallEventsMap.get(channel);
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
{ {
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, mergedIdentifiers); event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, event);
} }
else else
{ {
@@ -670,21 +770,22 @@ public class DMRDecoderState extends TimeslotDecoderState
ChannelGrant tgGrant = (ChannelGrant)csbk; ChannelGrant tgGrant = (ChannelGrant)csbk;
DMRChannel channel = tgGrant.getChannel(); DMRChannel channel = tgGrant.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager()) if(hasTrafficChannelManager())
{ {
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
} }
else else
{ {
DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); DecodeEvent event = mDetectedCallEventsMap.get(channel);
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
{ {
event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, mergedIdentifiers); event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, event);
} }
else else
{ {
@@ -702,21 +803,23 @@ public class DMRDecoderState extends TimeslotDecoderState
{ {
ChannelGrant channelGrant = (ChannelGrant)csbk; ChannelGrant channelGrant = (ChannelGrant)csbk;
DMRChannel channel = channelGrant.getChannel(); DMRChannel channel = channelGrant.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager()) if(hasTrafficChannelManager())
{ {
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted()); csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
} }
else else
{ {
DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); DecodeEvent event = mDetectedCallEventsMap.get(channel);
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
{ {
event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel, mergedIdentifiers); event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel,
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, event);
} }
else else
{ {
@@ -747,21 +850,22 @@ public class DMRDecoderState extends TimeslotDecoderState
ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk; ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk;
DMRChannel channel = cpdcg.getChannel(); DMRChannel channel = cpdcg.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager()) if(hasTrafficChannelManager())
{ {
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
csbk.getTimestamp(), csbk.isEncrypted()); csbk.getTimestamp(), csbk.isEncrypted());
} }
else else
{ {
DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); DecodeEvent event = mDetectedCallEventsMap.get(channel);
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers())) if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
{ {
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, mergedIdentifiers); event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event); new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, event);
} }
else else
{ {
@@ -777,8 +881,8 @@ public class DMRDecoderState extends TimeslotDecoderState
case MOTOROLA_CONPLUS_REGISTRATION_REQUEST: case MOTOROLA_CONPLUS_REGISTRATION_REQUEST:
DecodeEvent event = DMRDecodeEvent.builder(DecodeEventType.REQUEST, csbk.getTimestamp()) DecodeEvent event = DMRDecodeEvent.builder(DecodeEventType.REQUEST, csbk.getTimestamp())
.details("Registration Request") .details("Registration Request")
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) .identifiers(new IdentifierCollection(csbk.getIdentifiers()))
.timeslot(csbk.getTimeslot()) .timeslot(getTimeslot())
.build(); .build();
broadcast(event); broadcast(event);
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
@@ -786,8 +890,8 @@ public class DMRDecoderState extends TimeslotDecoderState
case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE: case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE:
DecodeEvent regRespEvent = DMRDecodeEvent.builder(DecodeEventType.RESPONSE, csbk.getTimestamp()) DecodeEvent regRespEvent = DMRDecodeEvent.builder(DecodeEventType.RESPONSE, csbk.getTimestamp())
.details("Registration Response") .details("Registration Response")
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) .identifiers(new IdentifierCollection(csbk.getIdentifiers()))
.timeslot(csbk.getTimeslot()) .timeslot(getTimeslot())
.build(); .build();
broadcast(regRespEvent); broadcast(regRespEvent);
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
@@ -798,21 +902,22 @@ public class DMRDecoderState extends TimeslotDecoderState
ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk; ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk;
DMRChannel channel = cpvcu.getChannel(); DMRChannel channel = cpvcu.getChannel();
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
if(hasTrafficChannelManager()) if(hasTrafficChannelManager())
{ {
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(), mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
csbk.getTimestamp(), csbk.isEncrypted()); csbk.getTimestamp(), csbk.isEncrypted());
} }
else else
{ {
DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber()); DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel);
if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers())) if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers()))
{ {
detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, mergedIdentifiers); detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), detectedEvent); new IdentifierCollection(csbk.getIdentifiers()));
mDetectedCallEventsMap.put(channel, detectedEvent);
} }
else else
{ {
@@ -828,8 +933,8 @@ public class DMRDecoderState extends TimeslotDecoderState
case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION: case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION:
DecodeEvent affiliateEvent = DMRDecodeEvent.builder(DecodeEventType.AFFILIATE, csbk.getTimestamp()) DecodeEvent affiliateEvent = DMRDecodeEvent.builder(DecodeEventType.AFFILIATE, csbk.getTimestamp())
.details("TALKGROUP AFFILIATION") .details("TALKGROUP AFFILIATION")
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) .identifiers(new IdentifierCollection(csbk.getIdentifiers()))
.timeslot(csbk.getTimeslot()) .timeslot(getTimeslot())
.build(); .build();
broadcast(affiliateEvent); broadcast(affiliateEvent);
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot())); broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
@@ -840,19 +945,20 @@ public class DMRDecoderState extends TimeslotDecoderState
} }
} }
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, DMRChannel channel, IdentifierCollection mergedIdentifiers) { private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, DMRChannel channel,
IdentifierCollection identifierCollection) {
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp()) return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
.channel(channel) .channel(channel)
.details(csbk.getOpcode().getLabel()) .details(csbk.getOpcode().getLabel())
.identifiers(mergedIdentifiers) .identifiers(identifierCollection)
.timeslot(channel.getTimeslot()) .timeslot(getTimeslot())
.build(); .build();
} }
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, String details) { private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, String details) {
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp()) return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers())) .identifiers(new IdentifierCollection(csbk.getIdentifiers()))
.timeslot(csbk.getTimeslot()) .timeslot(getTimeslot())
.details(details) .details(details)
.build(); .build();
} }
@@ -955,6 +1061,15 @@ public class DMRDecoderState extends TimeslotDecoderState
{ {
switch(message.getOpcode()) switch(message.getOpcode())
{ {
case FULL_ENCRYPTION_PARAMETERS:
if(message instanceof io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters ep)
{
if(mCurrentCallEvent != null)
{
mCurrentCallEvent.setDetails(ep.getDetails());
}
}
break;
case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION: case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION:
if(message instanceof CapacityPlusRestChannel) if(message instanceof CapacityPlusRestChannel)
{ {
@@ -978,11 +1093,9 @@ public class DMRDecoderState extends TimeslotDecoderState
} }
} }
break; break;
case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER: case FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER:
if(message instanceof CapacityPlusGroupVoiceChannelUser cpgvcu) if(message instanceof MotorolaGroupVoiceChannelUser cpgvcu)
{ {
updateRestChannel(cpgvcu.getRestChannel());
if(isTerminator) if(isTerminator)
{ {
getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().remove(Role.FROM);
@@ -1097,19 +1210,17 @@ public class DMRDecoderState extends TimeslotDecoderState
} }
break; break;
case FULL_STANDARD_GPS_INFO: case FULL_STANDARD_GPS_INFO:
if(message instanceof GPSInformation) if(message instanceof GPSInformation gpsInformation)
{ {
GPSInformation gpsInformation = (GPSInformation)message; PlottableDecodeEvent plottableGPS = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, message.getTimestamp())
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); .channel(getCurrentChannel())
ic.update(gpsInformation.getGPSLocation()); .details("LOCATION:" + gpsInformation.getGPSLocation())
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
.protocol(Protocol.DMR)
.location(gpsInformation.getPosition())
.build();
DecodeEvent gpsEvent = DMRDecodeEvent.builder(DecodeEventType.GPS, message.getTimestamp()) broadcast(plottableGPS);
.identifiers(ic)
.timeslot(message.getTimeslot())
.details("LOCATION:" + gpsInformation.getGPSLocation())
.build();
broadcast(gpsEvent);
} }
break; break;
case FULL_STANDARD_TALKER_ALIAS_COMPLETE: case FULL_STANDARD_TALKER_ALIAS_COMPLETE:
@@ -1121,6 +1232,41 @@ public class DMRDecoderState extends TimeslotDecoderState
} }
} }
/**
* Updates the current call with encryption information.
* @param encryptionParameters decoded from the Voice Frame F
* @param isGroup true for group or false for individual call.
*/
private void updateEncryptedCall(EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
{
if(mCurrentCallEvent != null)
{
String details = mCurrentCallEvent.getDetails();;
if(details == null)
{
details = encryptionParameters.toString();
}
else if(!details.contains(encryptionParameters.toString()) && !details.contains("ENCRYPTION"))
{
details += " " + encryptionParameters;
}
mCurrentCallEvent.setDetails(details);
}
else
{
mCurrentCallEvent = DMRDecodeEvent.builder(isGroup ? DecodeEventType.CALL_GROUP_ENCRYPTED :
DecodeEventType.CALL_ENCRYPTED, timestamp)
.channel(getCurrentChannel())
.details(encryptionParameters.toString())
.identifiers(getIdentifierCollection().copyOf())
.timeslot(getTimeslot())
.build();
broadcast(mCurrentCallEvent);
}
}
/** /**
* Updates or creates a current call event. * Updates or creates a current call event.
* *
@@ -1138,13 +1284,14 @@ public class DMRDecoderState extends TimeslotDecoderState
.channel(getCurrentChannel()) .channel(getCurrentChannel())
.details(details) .details(details)
.identifiers(getIdentifierCollection().copyOf()) .identifiers(getIdentifierCollection().copyOf())
.timeslot(getTimeslot())
.build(); .build();
broadcast(mCurrentCallEvent); broadcast(mCurrentCallEvent);
} }
else else
{ {
if(type != DecodeEventType.CALL) if(mCurrentCallEvent.getDetails() == null)
{ {
mCurrentCallEvent.setDetails(details); mCurrentCallEvent.setDetails(details);
} }

View File

@@ -44,6 +44,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBloc
import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequenceAssembler; import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequenceAssembler;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceSuperFrameProcessor;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -59,6 +61,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
{ {
private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class); private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class);
private DecodeConfigDMR mConfigDMR; private DecodeConfigDMR mConfigDMR;
private VoiceSuperFrameProcessor mSuperFrameProcessor1 = new VoiceSuperFrameProcessor();
private VoiceSuperFrameProcessor mSuperFrameProcessor2 = new VoiceSuperFrameProcessor();
private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1); private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1);
private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2); private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2);
private MBCAssembler mMBCAssembler = new MBCAssembler(); private MBCAssembler mMBCAssembler = new MBCAssembler();
@@ -89,19 +93,53 @@ public class DMRMessageProcessor implements Listener<IMessage>
@Override @Override
public void receive(IMessage message) public void receive(IMessage message)
{ {
//Enrich messages that carry DMR Logical Slot Number channels with LCN to frequency mappings if(message instanceof FullLCMessage flc)
{
if(flc.getTimeslot() == 1)
{
mSuperFrameProcessor1.process(flc);
}
else
{
mSuperFrameProcessor2.process(flc);
}
}
else if(message instanceof VoiceMessage voiceMessage)
{
if(voiceMessage.getTimeslot() == 1)
{
mSuperFrameProcessor1.process(voiceMessage);
}
else
{
mSuperFrameProcessor2.process(voiceMessage);
}
}
else if(message instanceof DMRBurst dmrBurst && dmrBurst.isValid())
{
if(dmrBurst.getTimeslot() == 1)
{
mSuperFrameProcessor1.reset();
}
else
{
mSuperFrameProcessor2.reset();
}
}
//Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings
if(message instanceof ITimeslotFrequencyReceiver) if(message instanceof ITimeslotFrequencyReceiver)
{ {
ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message; ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
int[] lsns = receiver.getLogicalTimeslotNumbers(); int[] lcns = receiver.getLogicalChannelNumbers();
List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>(); List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>();
for(int lsn: lsns) for(int lcn: lcns)
{ {
if(mTimeslotFrequencyMap.containsKey(lsn)) if(mTimeslotFrequencyMap.containsKey(lcn))
{ {
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lsn)); timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn));
} }
} }

View File

@@ -1,23 +1,20 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2020 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * 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 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.dmr; package io.github.dsheirer.module.decode.dmr;
@@ -41,14 +38,13 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusCo
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel; import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel;
import io.github.dsheirer.module.decode.dmr.message.type.Model; import io.github.dsheirer.module.decode.dmr.message.type.Model;
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Tracks the network configuration details of a DMR network from the broadcast messages * Tracks the network configuration details of a DMR network from the broadcast messages
@@ -250,7 +246,7 @@ public class DMRNetworkConfigurationMonitor
ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk; ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk;
mNeighborSites.addAll(cpnr.getNeighbors()); mNeighborSites.addAll(cpnr.getNeighbors());
} }
if(mBrand == null) if(mBrand == null || mBrand != BRAND_MOTOROLA_CONNECT_PLUS)
{ {
mBrand = BRAND_MOTOROLA_CONNECT_PLUS; mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
} }
@@ -279,7 +275,7 @@ public class DMRNetworkConfigurationMonitor
*/ */
private void addDmrChannel(DMRChannel dmrChannel) private void addDmrChannel(DMRChannel dmrChannel)
{ {
mObservedChannelMap.put(dmrChannel.getLogicalSlotNumber(), dmrChannel); mObservedChannelMap.put(dmrChannel.getValue(), dmrChannel);
} }
public void reset() public void reset()

View File

@@ -98,7 +98,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
private List<Channel> mManagedTrafficChannels; private List<Channel> mManagedTrafficChannels;
private Map<Long,Channel> mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>(); private Map<Long,Channel> mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>();
private Map<Integer,DMRChannelGrantEvent> mLSNGrantEventMap = new ConcurrentHashMap<>(); private Map<DMRChannel,DMRChannelGrantEvent> mChannelGrantEventMap = new ConcurrentHashMap<>();
private Listener<ChannelEvent> mChannelEventListener; private Listener<ChannelEvent> mChannelEventListener;
private Listener<IDecodeEvent> mDecodeEventListener; private Listener<IDecodeEvent> mDecodeEventListener;
@@ -322,9 +322,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection, public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection,
Opcode opcode, long timestamp, boolean encrypted) Opcode opcode, long timestamp, boolean encrypted)
{ {
int lsn = channel.getLogicalSlotNumber(); DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn);
DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted); DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted);
if(isStale(event, timestamp, identifierCollection)) //Create new event if(isStale(event, timestamp, identifierCollection)) //Create new event
@@ -335,7 +333,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
.identifiers(identifierCollection) .identifiers(identifierCollection)
.build(); .build();
mLSNGrantEventMap.put(lsn, event); mChannelGrantEventMap.put(channel, event);
} }
else //Update current event else //Update current event
@@ -355,7 +353,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
.identifiers(identifierCollection) .identifiers(identifierCollection)
.build(); .build();
mLSNGrantEventMap.put(lsn, event); mChannelGrantEventMap.put(channel, event);
broadcast(event); broadcast(event);
} }
} }
@@ -643,37 +641,37 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
*/ */
private void removeCallEvents(long frequency) private void removeCallEvents(long frequency)
{ {
List<Integer> lsnsToRemove = new ArrayList<>(); List<DMRChannel> channelsToRemove = new ArrayList<>();
for(Map.Entry<Integer,DMRChannelGrantEvent> entry: mLSNGrantEventMap.entrySet()) for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
{ {
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency) if(entry.getKey().getDownlinkFrequency() == frequency)
{ {
lsnsToRemove.add(entry.getKey()); channelsToRemove.add(entry.getKey());
} }
} }
for(Integer lsn: lsnsToRemove) for(DMRChannel channel: channelsToRemove)
{ {
mLSNGrantEventMap.remove(lsn); mChannelGrantEventMap.remove(channel);
} }
} }
private void updateCallEventDetails(long frequency, String detailFragment) private void updateCallEventDetails(long frequency, String detailFragment)
{ {
List<Integer> lsnsToUpdate = new ArrayList<>(); List<DMRChannel> channelsToUpdate = new ArrayList<>();
for(Map.Entry<Integer,DMRChannelGrantEvent> entry: mLSNGrantEventMap.entrySet()) for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
{ {
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency) if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
{ {
lsnsToUpdate.add(entry.getKey()); channelsToUpdate.add(entry.getKey());
} }
} }
for(Integer lsn: lsnsToUpdate) for(DMRChannel channel: channelsToUpdate)
{ {
DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn); DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
if(event != null) if(event != null)
{ {

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -38,7 +38,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.UserPreferences;
@@ -105,63 +104,54 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
{ {
if(hasAudioCodec() && message.getTimeslot() == getTimeslot()) if(hasAudioCodec() && message.getTimeslot() == getTimeslot())
{ {
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted. //Attempt to set the audio encryption state from certain types of messages
if(!mEncryptedCallStateEstablished && message instanceof VoiceMessage vm && if(!mEncryptedCallStateEstablished)
vm.getSyncPattern().isDirectMode())
{ {
mEncryptedCallStateEstablished = true; //DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
mEncryptedCall = false; if(message instanceof VoiceMessage vm && vm.getSyncPattern().isDirectMode())
List<byte[]> frames = vm.getAMBEFrames();
for(byte[] frame: frames)
{ {
processAudio(frame, message.getTimestamp()); mEncryptedCallStateEstablished = true;
mEncryptedCall = false;
} }
} //Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames //in the EMB field.
//in the EMB field. else if(message instanceof VoiceEMBMessage voiceMessage && voiceMessage.getEMB().isValid())
else if(!mEncryptedCallStateEstablished && message instanceof VoiceEMBMessage voiceMessage)
{
if(voiceMessage.getEMB().isValid())
{ {
mEncryptedCallStateEstablished = true; mEncryptedCallStateEstablished = true;
mEncryptedCall = voiceMessage.getEMB().isEncrypted(); mEncryptedCall = voiceMessage.getEMB().isEncrypted();
} }
else if(message instanceof VoiceHeader voiceHeader)
{
LCMessage lc = voiceHeader.getLCMessage();
List<byte[]> frames = voiceMessage.getAMBEFrames(); if(lc instanceof FullLCMessage flc) //We know it is, but this null checks as well
for(byte[] frame: frames)
{
processAudio(frame, message.getTimestamp());
}
}
else if(message instanceof VoiceMessage voiceMessage)
{
List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames)
{
processAudio(frame, message.getTimestamp());
}
}
else if(!mEncryptedCallStateEstablished && message instanceof VoiceHeader voiceHeader)
{
LCMessage lc = voiceHeader.getLCMessage();
if(lc instanceof FullLCMessage flc) //We know it is, but this null checks as well
{
if(flc.isValid())
{ {
mEncryptedCallStateEstablished = true; if(flc.isValid())
mEncryptedCall = flc.isEncrypted(); {
mEncryptedCallStateEstablished = true;
mEncryptedCall = flc.isEncrypted();
}
} }
} }
//Note: the DMRMessageProcessor extracts Full Link Control messages from Voice Frames B-C and sends them
// independent of any DMR Burst messaging. When encountered, it can be assumed that they are part of
// an ongoing call and can be used to establish encryption state when the FLC is a voice channel user.
else if(message instanceof AbstractVoiceChannelUser avcu)
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = avcu.getServiceOptions().isEncrypted();
}
} }
//Note: the DMRMessageProcess extracts Full Link Control messages from Voice Frames B-C and sends them
// independent of any DMR Burst messaging. When encountered, it can be assumed that they are part of //Queue or process audio frames. Note: audio frames are held/queued until encrypted state is established
// an ongoing call and can be used to establish encryption state when the FLC is a voice channel user. //before any audio is generated for the audio segment.
else if(!mEncryptedCallStateEstablished && message instanceof AbstractVoiceChannelUser avcu) if(message instanceof VoiceMessage voiceMessage)
{ {
ServiceOptions serviceOptions = avcu.getServiceOptions(); List<byte[]> frames = voiceMessage.getAMBEFrames();
mEncryptedCallStateEstablished = true; for(byte[] frame: frames)
mEncryptedCall = serviceOptions.isEncrypted(); {
processAudio(frame, message.getTimestamp());
}
} }
else if(message instanceof Terminator) else if(message instanceof Terminator)
{ {

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2021 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -32,17 +32,16 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChann
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.List;
/** /**
* DMR AMBE Frame recorder generates call sequence recordings containing JSON representations of audio * DMR AMBE Frame recorder generates call sequence recordings containing JSON representations of audio
* frames, optional encryption and call identifiers. * frames, optional encryption and call identifiers.
@@ -206,8 +205,8 @@ public class DMRCallSequenceRecorder extends MBECallSequenceRecorder
{ {
switch(message.getOpcode()) switch(message.getOpcode())
{ {
case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER: case FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER:
if(message instanceof CapacityPlusGroupVoiceChannelUser cpvcu) if(message instanceof MotorolaGroupVoiceChannelUser cpvcu)
{ {
if(mCallSequence == null) if(mCallSequence == null)
{ {

View File

@@ -1,23 +1,20 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2019 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * 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 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.dmr.channel; package io.github.dsheirer.module.decode.dmr.channel;

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -26,6 +26,8 @@ import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier; import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.protocol.Protocol; import io.github.dsheirer.protocol.Protocol;
import java.util.List;
import java.util.Objects;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
/** /**
@@ -33,8 +35,9 @@ import org.apache.commons.lang3.Validate;
* *
* Note: timeslots are tracked as 1 and 2 * Note: timeslots are tracked as 1 and 2
*/ */
public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor, ITimeslotFrequencyReceiver
{ {
private TimeslotFrequency mTimeslotFrequency;
private int mTimeslot; private int mTimeslot;
/** /**
@@ -56,13 +59,23 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
} }
/** /**
* Repeater number for the channel. * Repeater number aka Logical Channel Number (LCN) for the channel.
*/ */
public int getRepeater() public int getChannelNumber()
{ {
return getValue(); return getValue();
} }
/**
* Convenience method to format the single channel number as an array for TimeslotFrequency matching.
* @return channel number as an integer array.
*/
public int[] getLogicalChannelNumbers()
{
return new int[]{getChannelNumber()};
}
/** /**
* Timeslot for the channel. * Timeslot for the channel.
* @return timeslot as zero-based index with values in range: 0 or 1. * @return timeslot as zero-based index with values in range: 0 or 1.
@@ -72,36 +85,6 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
return mTimeslot; return mTimeslot;
} }
/**
* Logical slot number for this channel. LSN is a 1-based index value where repeater one, timeslot 1 is
* LSN 1, timeslot 2 is LSN 2, etc.
*
* Formula: LSN = ((channel - 1) * 2) + timeslot
*
* @return logical slot number, a 1-based index value
*/
public int getLogicalSlotNumber()
{
int repeater = getRepeater();
if(repeater > 0)
{
return ((repeater - 1) * 2) + getTimeslot();
}
return 0;
}
/**
* Returns an array of length 1 containing this channel's logical slot number
*/
public int[] getLSNArray()
{
int[] logicalSlotNumbers = new int[1];
logicalSlotNumbers[0] = getLogicalSlotNumber();
return logicalSlotNumbers;
}
/** /**
* Number of timeslots for the DMR channel. * Number of timeslots for the DMR channel.
* @return 2 always. * @return 2 always.
@@ -121,6 +104,13 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
return true; return true;
} }
@Override
public String toString()
{
return "CHAN:" + getChannelNumber() + ":" + getTimeslot();
}
/** /**
* Not implemented * Not implemented
*/ */
@@ -138,4 +128,72 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
{ {
throw new IllegalArgumentException("This method is not supported"); throw new IllegalArgumentException("This method is not supported");
} }
@Override
public long getDownlinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getDownlinkFrequency();
}
return 0;
}
@Override
public long getUplinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getUplinkFrequency();
}
return 0;
}
/**
* Sets the lsn to frequency mapper value.
* @param timeslotFrequency to set
*/
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
{
mTimeslotFrequency = timeslotFrequency;
}
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(timeslotFrequency.getChannelNumber() == getChannelNumber())
{
setTimeslotFrequency(timeslotFrequency);
return;
}
}
}
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(!(o instanceof DMRChannel that))
{
return false;
}
if(!super.equals(o))
{
return false;
}
return getTimeslot() == that.getTimeslot();
}
@Override
public int hashCode()
{
return Objects.hash(super.hashCode(), getTimeslot());
}
} }

View File

@@ -1,90 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.channel;
/**
* DMR logical channel. This channel uses a logical channel number and a timeslot.
*/
public class DMRLogicalChannel extends DMRChannel
{
private TimeslotFrequency mTimeslotFrequency;
/**
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
* calculated logical slot value for visual compatibility for users.
*
* @param channel number or repeater number
* @param logicalSlotNumber - zero based index.
*/
public DMRLogicalChannel(int channel, int timeslot)
{
super(channel, timeslot);
}
/**
* Downlink frequency
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
*/
@Override
public long getDownlinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getDownlinkFrequency();
}
return 0;
}
/**
* Uplink frequency
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
*/
@Override
public long getUplinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getUplinkFrequency();
}
return 0;
}
/**
* Sets the timeslot frequency mapping
* @param timeslotFrequency
*/
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
{
mTimeslotFrequency = timeslotFrequency;
}
/**
* Formatted channel number
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("LSN:").append(getLogicalSlotNumber());
// sb.append(" LCN:").append(getValue());
return sb.toString();
}
}

View File

@@ -0,0 +1,72 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.channel;
/**
* DMR Logical Slot Number (LSN) channel.
*
* Note: LSN 1-16 are represented in memory using the following channel and timeslot values:
*
* LSN: CHANNEL/TIMESLOT
* 1: 1/1
* 2: 1/2
* 3: 2/1
* 4: 2/2
* 5: 3/1
* 6: 3/2
* 7: 4/1
* 8: 4/2
* 9: 5/1
* 10: 5/2
* 11: 6/1
* 12: 6/2
* 13: 7/1
* 14: 7/2
* 15: 8/1
* 16: 8/2
*/
public class DMRLsn extends DMRChannel
{
/**
* Constructs an instance
*
* @param lsn in range 1 - 16
*/
public DMRLsn(int lsn)
{
super(((lsn - 1) / 2) + 1, ((lsn - 1) % 2) + 1);
}
@Override
public String toString()
{
return "LSN:" + getLsn() + " LCN:" + getChannelNumber();
}
/**
* Logical Slot Number
* @return lsn
*/
public int getLsn()
{
return ((getChannelNumber() - 1) * 2) + getTimeslot();
}
}

View File

@@ -1,16 +1,35 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.channel; package io.github.dsheirer.module.decode.dmr.channel;
/** /**
* DMR Tier III Trunking Channel * DMR Tier III Trunking channel. This channel uses a logical channel number and a timeslot.
*/ */
public class DMRTier3Channel extends DMRLogicalChannel public class DMRTier3Channel extends DMRChannel
{ {
/** /**
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the * Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
* calculated logical slot value for visual compatibility for users. * calculated logical slot value for visual compatibility for users.
* *
* @param channel number or repeater number * @param channel number or repeater number
* @param timeslot * @param timeslot either 1 or 2.
*/ */
public DMRTier3Channel(int channel, int timeslot) public DMRTier3Channel(int channel, int timeslot)
{ {
@@ -18,23 +37,22 @@ public class DMRTier3Channel extends DMRLogicalChannel
} }
/** /**
* Logical slot number for this channel. * Tier III Channel ID
* * @return channel ID
* Formula: LSN = (channel * 2) + timeslot
*
* @return logical slot number, a 1-based index value
*/ */
@Override public int getChannelId()
public int getLogicalSlotNumber()
{ {
int repeater = getRepeater(); return getChannelNumber() * 2 + getTimeslot();
if(repeater > 0)
{
return ((repeater) * 2) + getTimeslot();
}
return 0;
} }
/**
* Formatted channel number
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(" LCN:").append(getChannelNumber());
sb.append(" CHANID:").append(getChannelId());
return sb.toString();
}
} }

View File

@@ -0,0 +1,42 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.channel;
/**
* DMR Rest Channel logical slot number (LSN).
*/
public class DmrRestLsn extends DMRLsn
{
/**
* Constructs an instance
*
* @param lsn in range 1 - 16
*/
public DmrRestLsn(int lsn)
{
super(lsn);
}
@Override
public String toString()
{
return "REST:" + getLsn();
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -24,9 +24,9 @@ import java.util.List;
public interface ITimeslotFrequencyReceiver public interface ITimeslotFrequencyReceiver
{ {
/** /**
* Provides the logical slot number(s) that require a matching timeslot frequency mapping * Provides the logical channel number(s) that require a matching timeslot frequency mapping
*/ */
public int[] getLogicalTimeslotNumbers(); public int[] getLogicalChannelNumbers();
/** /**
* Applies the list of timeslot frequency mappings to the implementer * Applies the list of timeslot frequency mappings to the implementer

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -31,7 +31,7 @@ import javafx.beans.property.SimpleLongProperty;
import javafx.util.Callback; import javafx.util.Callback;
/** /**
* Maps a timeslot number to a pair of channel frequency values * Maps a logical channel number (LCN) to a pair of channel frequency values
*/ */
public class TimeslotFrequency public class TimeslotFrequency
{ {
@@ -73,7 +73,7 @@ public class TimeslotFrequency
* Downlink Frequency property * Downlink Frequency property
*/ */
@JsonIgnore @JsonIgnore
public LongProperty downlinkFrequencyPropertyProperty() public LongProperty downlinkFrequencyProperty()
{ {
return mDownlinkFrequencyProperty; return mDownlinkFrequencyProperty;
} }
@@ -100,7 +100,10 @@ public class TimeslotFrequency
} }
/** /**
* Logical slot number (LSN) as a 1-index based counter * Logical channel number (LCN) as a 1-index based counter
*
* Note: lsn (logical slot number) is a legacy name for this field. The field currently holds the LCN or logical
* channel number. To avoid breaking serialization, we leave this field labeled as lsn.
*/ */
@JacksonXmlProperty(isAttribute = true, localName = "lsn") @JacksonXmlProperty(isAttribute = true, localName = "lsn")
public int getNumber() public int getNumber()
@@ -109,12 +112,52 @@ public class TimeslotFrequency
} }
/** /**
* Sets the logical slot number (LSN) as a 1-index start * Logical channel number which is the same value as getNumber.
* @param lsn where LSN 1 is the first slot, 2 the second, etc * @return
*/ */
public void setNumber(int lsn) @JsonIgnore
public int getChannelNumber()
{ {
mNumberProperty.set(lsn); return getNumber();
}
@JsonIgnore
public String getLogicalSlotNumbers()
{
StringBuilder sb = new StringBuilder();
int channel = getChannelNumber();
if(channel > 0)
{
if(channel < 32) //Connect Plus uses 5-bits for LSN for a maximum value of 31
{
//Cap+ & Con+ Logical Slot Numbers
int lsn1 = (channel - 1) * 2 + 1;
int lsn2 = (channel - 1) * 2 + 2;
sb.append("LSN:").append(lsn1).append("/").append(lsn2).append(" or ");
}
//Tier III channel IDs
int chanId1 = channel * 2 + 1;
int chanId2 = channel * 2 + 2;
sb.append("Tier3 Chan:").append(chanId1).append("/").append(chanId2);
}
else
{
sb.append("(empty)");
}
return sb.toString();
}
/**
* Sets the logical channel number (LCN) as a 1-index start
* @param lcn where LCN 1 is logical slot numbers (LSN) 1 and 2, LCN2=LSN3/4, etc.
*/
public void setNumber(int lcn)
{
mNumberProperty.set(lcn);
} }
/** /**
@@ -160,7 +203,7 @@ public class TimeslotFrequency
@Override @Override
public String toString() public String toString()
{ {
return "TIMESLOT LSN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency(); return "LCN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency();
} }
/** /**
@@ -168,7 +211,7 @@ public class TimeslotFrequency
*/ */
public static Callback<TimeslotFrequency,Observable[]> extractor() public static Callback<TimeslotFrequency,Observable[]> extractor()
{ {
return (TimeslotFrequency tf) -> new Observable[] {tf.getNumberProperty(), tf.downlinkFrequencyPropertyProperty(), return (TimeslotFrequency tf) -> new Observable[] {tf.getNumberProperty(), tf.downlinkFrequencyProperty(),
tf.uplinkFrequencyProperty(), tf.getDownlinkMHz(), tf.getUplinkMHz()}; tf.uplinkFrequencyProperty(), tf.getDownlinkMHz(), tf.getUplinkMHz()};
} }
} }

View File

@@ -1,5 +1,25 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.message; package io.github.dsheirer.module.decode.dmr.message;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
/** /**
@@ -53,4 +73,13 @@ public abstract class DMRBurst extends DMRMessage
{ {
return mSyncPattern; return mSyncPattern;
} }
/**
* Extracts the sync payload from between the two payload fragments.
* @return binary message containing just the sync payload bits (64).
*/
public BinaryMessage getSyncPayload()
{
return getMessage().get(SYNC_START, PAYLOAD_2_START);
}
} }

View File

@@ -0,0 +1,34 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.message;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
/**
* DMR message that exposes a service options configuration
*/
public interface IServiceOptionsProvider
{
/**
* Service Options
* @return service options
*/
ServiceOptions getServiceOptions();
}

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -33,8 +33,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessageFactory
import io.github.dsheirer.module.decode.dmr.message.data.header.ConfirmedDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.ConfirmedDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.DataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.DataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.DefinedShortDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.DefinedShortDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.PiHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.RawShortDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.RawShortDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ResponseDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.ResponseDataHeader;
@@ -43,9 +43,9 @@ import io.github.dsheirer.module.decode.dmr.message.data.header.StatusDataHeader
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UnconfirmedDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.UnconfirmedDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.data.usb.USBData; import io.github.dsheirer.module.decode.dmr.message.data.usb.USBData;
@@ -98,7 +98,7 @@ public class DMRDataMessageFactory
return new MBCHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot); return new MBCHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case CHANNEL_CONTROL_ENC_HEADER: case CHANNEL_CONTROL_ENC_HEADER:
case PI_HEADER: case PI_HEADER:
return new HeaderMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot); return new PiHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case VOICE_HEADER: case VOICE_HEADER:
return new VoiceHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot); return new VoiceHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case DATA_ENC_HEADER: case DATA_ENC_HEADER:
@@ -132,16 +132,16 @@ public class DMRDataMessageFactory
} }
else else
{ {
MotorolaProprietaryDataHeader mprdh = new MotorolaProprietaryDataHeader(pattern, MotorolaDataEncryptionHeader mprdh = new MotorolaDataEncryptionHeader(pattern,
payload, cach, slotType, timestamp, timeslot); payload, cach, slotType, timestamp, timeslot);
mprdh.setValid(valid); mprdh.setValid(valid);
return mprdh; return mprdh;
} }
case HYTERA_68: case HYTERA_68:
HyteraProprietaryDataHeader hpdh = new HyteraProprietaryDataHeader(pattern, payload, HyteraDataEncryptionHeader hsdh = new HyteraDataEncryptionHeader(pattern, payload,
cach, slotType, timestamp, timeslot); cach, slotType, timestamp, timeslot);
hpdh.setValid(valid); hsdh.setValid(valid);
return hpdh; return hsdh;
default: default:
ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach, ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach,
slotType, timestamp, timeslot); slotType, timestamp, timeslot);

View File

@@ -34,7 +34,7 @@ import java.util.List;
*/ */
public abstract class DataMessageWithLinkControl extends DataMessage public abstract class DataMessageWithLinkControl extends DataMessage
{ {
private LCMessage mLCMessage; protected LCMessage mLCMessage;
/** /**
* Constructs an instance. * Constructs an instance.

View File

@@ -56,6 +56,10 @@ public class DataBlock1Rate extends DataBlock
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode()); sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid()) if(!isValid())
{ {
sb.append(" [CRC ERROR]"); sb.append(" [CRC ERROR]");

View File

@@ -56,6 +56,10 @@ public class DataBlock1_2Rate extends DataBlock
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode()); sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid()) if(!isValid())
{ {
sb.append(" [CRC ERROR]"); sb.append(" [CRC ERROR]");

View File

@@ -57,6 +57,10 @@ public class DataBlock3_4Rate extends DataBlock
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode()); sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid()) if(!isValid())
{ {
sb.append(" [CRC ERROR]"); sb.append(" [CRC ERROR]");

View File

@@ -27,7 +27,6 @@ import java.util.TreeMap;
/** /**
* Control Signalling Block (CSBK) Opcode enumeration * Control Signalling Block (CSBK) Opcode enumeration
*
* ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B * ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B
* ETSI TS 102 361-4 DMR Trunking Protocol, Annex B * ETSI TS 102 361-4 DMR Trunking Protocol, Annex B
*/ */
@@ -91,6 +90,8 @@ public enum Opcode
HYTERA_08_ACKNOWLEDGE(Vendor.HYTERA_8, 32, "HYTERA 08 ACKNOWLEDGE"), HYTERA_08_ACKNOWLEDGE(Vendor.HYTERA_8, 32, "HYTERA 08 ACKNOWLEDGE"),
HYTERA_08_ANNOUNCEMENT(Vendor.HYTERA_8, 40, "HYTERA 08 ANNOUNCEMENT"), HYTERA_08_ANNOUNCEMENT(Vendor.HYTERA_8, 40, "HYTERA 08 ANNOUNCEMENT"),
//CSBKO 44 & 47 observed on Tier3 interleaved in voice group call terminator sequence. 44 was continuously transmitted
//and 47 was only transmitted 3x times in succession in the middle of the terminators and CSBKO 44 messages.
HYTERA_08_CSBKO_44(Vendor.HYTERA_8, 44, "HYTERA 08 CSBKO 44"), HYTERA_08_CSBKO_44(Vendor.HYTERA_8, 44, "HYTERA 08 CSBKO 44"),
HYTERA_08_CSBKO_47(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"), HYTERA_08_CSBKO_47(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"),
@@ -104,9 +105,9 @@ public enum Opcode
UNKNOWN(Vendor.UNKNOWN, -1, "UNKNOWN"); UNKNOWN(Vendor.UNKNOWN, -1, "UNKNOWN");
private Vendor mVendor; private final Vendor mVendor;
private int mValue; private final int mValue;
private String mLabel; private final String mLabel;
/** /**
* Constructor * Constructor

View File

@@ -22,7 +22,7 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRSite; import io.github.dsheirer.module.decode.dmr.identifier.DMRSite;
@@ -39,7 +39,6 @@ import java.util.List;
public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequencyReceiver public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequencyReceiver
{ {
private static final int[] LC_START_STOP = new int[]{16, 17}; private static final int[] LC_START_STOP = new int[]{16, 17};
private static final int TIMESLOT = 18;
private static final int[] REST_LSN = new int[]{19, 20, 21, 22, 23}; private static final int[] REST_LSN = new int[]{19, 20, 21, 22, 23};
private static final int ASYNC = 24; private static final int ASYNC = 24;
private static final int[] SITE = new int[]{25, 26, 27, 28}; private static final int[] SITE = new int[]{25, 26, 27, 28};
@@ -58,7 +57,7 @@ public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequ
private static final int[] NEIGHBOR_6_REST = new int[]{76, 77, 78, 79}; private static final int[] NEIGHBOR_6_REST = new int[]{76, 77, 78, 79};
private DMRLogicalChannel mRestChannel; private DmrRestLsn mRestChannel;
private DMRSite mSite; private DMRSite mSite;
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;
@@ -286,37 +285,23 @@ public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequ
return getMessage().getInt(REST_LSN); return getMessage().getInt(REST_LSN);
} }
/**
* Rest repeater
*/
public int getRestRepeater() { return (int) Math.ceil(getRestLSN() / 2.0); }
/**
* Rest timeslot
* @return timeslot 1 or 2
*/
public int getRestTimeslot()
{
return (getRestLSN() % 2 == 0) ? 2 : 1;
}
/** /**
* DMR Channel * DMR Channel
*/ */
public DMRLogicalChannel getRestChannel() public DmrRestLsn getRestChannel()
{ {
if(mRestChannel == null) if(mRestChannel == null)
{ {
mRestChannel = new DMRLogicalChannel(getRestRepeater(), getRestTimeslot()); mRestChannel = new DmrRestLsn(getRestLSN());
} }
return mRestChannel; return mRestChannel;
} }
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
return getRestChannel().getLSNArray(); return getRestChannel().getLogicalChannelNumbers();
} }
/** /**
@@ -327,13 +312,7 @@ public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequ
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies) getRestChannel().apply(timeslotFrequencies);
{
if(timeslotFrequency.getNumber() == getRestChannel().getLogicalSlotNumber())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
} }
@Override @Override

View File

@@ -22,7 +22,7 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.message.CACH; import io.github.dsheirer.module.decode.dmr.message.CACH;
@@ -39,13 +39,11 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
private static final int[] BYTE = new int[]{0, 1, 2, 3, 4, 5, 6, 7}; private static final int[] BYTE = new int[]{0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] TWO_BYTES = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; private static final int[] TWO_BYTES = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] SEGMENT_INDICATOR = new int[]{16, 17}; private static final int[] SEGMENT_INDICATOR = new int[]{16, 17};
private static final int TIMESLOT = 18; private static final int[] REST_LSN = new int[]{19, 20, 21, 22, 23};
private static final int RESERVED = 19;
private static final int[] REST_LSN = new int[]{20, 21, 22, 23};
private static final int[] LSN_VOICE_BITMAP = new int[]{24, 25, 26, 27, 28, 29, 30, 31}; private static final int[] LSN_VOICE_BITMAP = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int LSN_1_8_BITMAP_START = 24; private static final int LSN_1_8_BITMAP_START = 24;
private DMRLogicalChannel mRestChannel; private DmrRestLsn mRestChannel;
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;
/** /**
@@ -80,7 +78,7 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
sb.append(" RAS:").append(getBPTCReservedBits()); sb.append(" RAS:").append(getBPTCReservedBits());
} }
sb.append(" CSBK CAP+ SITE STATUS REST ").append(getRestChannel()); sb.append(" CSBK CAP+ SITE STATUS ").append(getRestChannel());
sb.append(" ").append(getSegmentIndicator()); sb.append(" ").append(getSegmentIndicator());
sb.append(" "); sb.append(" ");
@@ -277,11 +275,11 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
/** /**
* Current rest channel for this site. * Current rest channel for this site.
*/ */
public DMRLogicalChannel getRestChannel() public DmrRestLsn getRestChannel()
{ {
if(mRestChannel == null) if(mRestChannel == null)
{ {
mRestChannel = new DMRLogicalChannel(getRestRepeater(), getRestTimeslot()); mRestChannel = new DmrRestLsn(getRestLSN());
} }
return mRestChannel; return mRestChannel;
@@ -297,31 +295,13 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
return getMessage().getInt(REST_LSN); return getMessage().getInt(REST_LSN);
} }
/**
* Rest Channel Repeater
*/
public int getRestRepeater()
{
return (int) Math.ceil(getRestLSN() / 2.0);
}
/**
* Rest Channel Timeslot
*
* @return 1 or 2
*/
public int getRestTimeslot()
{
return (getRestLSN() % 2 == 0) ? 2 : 1;
}
/** /**
* Logical slot numbers that require slot to frequency mappings. * Logical slot numbers that require slot to frequency mappings.
*/ */
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
return getRestChannel().getLSNArray(); return getRestChannel().getLogicalChannelNumbers();
} }
/** /**
@@ -332,13 +312,7 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies) getRestChannel().apply(timeslotFrequencies);
{
if(getRestChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
} }
@Override @Override

View File

@@ -23,7 +23,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
@@ -40,14 +40,13 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
{ {
private static final int[] TARGET_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, private static final int[] TARGET_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39}; 32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] REPEATER = new int[]{40, 41, 42, 43}; private static final int[] LOGICAL_SLOT_NUMBER = new int[]{40, 41, 42, 43, 44};
private static final int[] CHANNEL_GRANT_TIMESLOT = new int[]{44};
//Analysis: this field correlates to UNKNOWN_FIELD_1(bits: 40-48) in ConnectPlusTerminateChannelGrant. //Analysis: this field correlates to UNKNOWN_FIELD_1(bits: 40-48) in ConnectPlusTerminateChannelGrant.
private static final int[] UNKNOWN_FIELD = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; private static final int[] UNKNOWN_FIELD = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private RadioIdentifier mTargetRadio; private RadioIdentifier mTargetRadio;
private DMRLogicalChannel mDMRLogicalChannel; private DMRLsn mDmrLsn;
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;
/** /**
@@ -110,39 +109,30 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
} }
/** /**
* Channel grant repeater number * Channel grant logical slot number
*/ */
public int getRepeater() public int getLsn()
{ {
return getMessage().getInt(REPEATER); return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Don't change this ... always subtract 1 to get the repeater LSN
}
/**
* Channel grant timeslot
* @return 1 or 2
*/
public int getChannelGrantTimeslot()
{
return getMessage().getInt(CHANNEL_GRANT_TIMESLOT) + 1;
} }
/** /**
* DMR Channel * DMR Channel
*/ */
public DMRLogicalChannel getChannel() public DMRLsn getChannel()
{ {
if(mDMRLogicalChannel == null) if(mDmrLsn == null)
{ {
mDMRLogicalChannel = new DMRLogicalChannel(getRepeater(), getChannelGrantTimeslot()); mDmrLsn = new DMRLsn(getLsn());
} }
return mDMRLogicalChannel; return mDmrLsn;
} }
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
return getChannel().getLSNArray(); return getChannel().getLogicalChannelNumbers();
} }
/** /**
@@ -152,13 +142,7 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) getChannel().apply(timeslotFrequencies);
{
if(timeslotFrequency.getNumber() == getChannel().getLogicalSlotNumber())
{
getChannel().setTimeslotFrequency(timeslotFrequency);
}
}
} }
@Override @Override

View File

@@ -22,7 +22,7 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.message.CACH; import io.github.dsheirer.module.decode.dmr.message.CACH;
@@ -44,11 +44,10 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
private static final int[] MESSAGE_TYPE = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; private static final int[] MESSAGE_TYPE = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] VERSION = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39}; private static final int[] VERSION = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] UNKNOWN = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, private static final int[] UNKNOWN = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, 63}; 57, 58, 59, 60, 61, 62};
private static final int[] DATA_REPEATER = new int[]{64, 65, 66, 67}; private static final int[] LOGICAL_SLOT_NUMBER = new int[]{63, 64, 65, 66, 67};
private static final int[] DATA_TIMESLOT = new int[]{68};
private DMRLogicalChannel mDataChannel; private DMRLsn mDataChannel;
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;
/** /**
@@ -100,39 +99,30 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
} }
/** /**
* Data repeater number * Data logical slot numberrepeater number
*/ */
public int getDataRepeater() public int getDataLsn()
{ {
return getMessage().getInt(DATA_REPEATER); return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Leave this as minus 1
}
/**
* Data Timeslot
* @return 1 or 2
*/
public int getDataTimeslot()
{
return getMessage().getInt(DATA_TIMESLOT) + 1;
} }
/** /**
* DMR Channel where the data is available * DMR Channel where the data is available
*/ */
public DMRLogicalChannel getDataChannel() public DMRLsn getDataChannel()
{ {
if(mDataChannel == null) if(mDataChannel == null)
{ {
mDataChannel = new DMRLogicalChannel(getDataRepeater(), getDataTimeslot()); mDataChannel = new DMRLsn(getDataLsn());
} }
return mDataChannel; return mDataChannel;
} }
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
return getDataChannel().getLSNArray(); return getDataChannel().getLogicalChannelNumbers();
} }
/** /**
@@ -143,13 +133,7 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies) getDataChannel().apply(timeslotFrequencies);
{
if(timeslotFrequency.getNumber() == getDataChannel().getLogicalSlotNumber())
{
getDataChannel().setTimeslotFrequency(timeslotFrequency);
}
}
} }
@Override @Override

View File

@@ -24,7 +24,7 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
@@ -44,13 +44,12 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
32, 33, 34, 35, 36, 37, 38, 39}; 32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63}; 56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] TRAFFIC_CHANNEL_REPEATER = new int[]{64, 65, 66, 67}; private static final int[] LOGICAL_SLOT_NUMBER = new int[]{64, 65, 66, 67, 68};
private static final int[] TRAFFIC_CHANNEL_TIMESLOT = new int[]{68};
private static final int[] UNKNOWN_FIELD = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; private static final int[] UNKNOWN_FIELD = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private RadioIdentifier mRadio; private RadioIdentifier mRadio;
private TalkgroupIdentifier mTalkgroup; private TalkgroupIdentifier mTalkgroup;
private DMRLogicalChannel mDMRLogicalChannel; private DMRLsn mDmrLsn;
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;
/** /**
@@ -127,37 +126,28 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
/** /**
* Traffic channel repeater * Traffic channel repeater
*/ */
public int getTrafficChannelRepeater() public int getTrafficLsn()
{ {
return getMessage().getInt(TRAFFIC_CHANNEL_REPEATER); return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Always leave as minus 1
}
/**
* Traffic channel timeslot
* @return 1 or 2
*/
public int getTrafficChannelTimeslot()
{
return getMessage().getInt(TRAFFIC_CHANNEL_TIMESLOT) + 1;
} }
/** /**
* DMR Channel * DMR Channel
*/ */
public DMRLogicalChannel getChannel() public DMRLsn getChannel()
{ {
if(mDMRLogicalChannel == null) if(mDmrLsn == null)
{ {
mDMRLogicalChannel = new DMRLogicalChannel(getTrafficChannelRepeater(), getTrafficChannelTimeslot()); mDmrLsn = new DMRLsn(getTrafficLsn());
} }
return mDMRLogicalChannel; return mDmrLsn;
} }
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
return getChannel().getLSNArray(); return getChannel().getLogicalChannelNumbers();
} }
/** /**
@@ -167,13 +157,7 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) getChannel().apply(timeslotFrequencies);
{
if(timeslotFrequency.getNumber() == getChannel().getLogicalSlotNumber())
{
getChannel().setTimeslotFrequency(timeslotFrequency);
}
}
} }
@Override @Override

View File

@@ -25,7 +25,6 @@ import io.github.dsheirer.identifier.integer.IntegerIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel; import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
@@ -224,11 +223,11 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
* Logical Slot Number(s) for channels contained in this message * Logical Slot Number(s) for channels contained in this message
*/ */
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
if(getMoveToChannel() instanceof DMRLogicalChannel) if(getMoveToChannel() != null)
{ {
return ((DMRLogicalChannel)getMoveToChannel()).getLSNArray(); return getMoveToChannel().getLogicalChannelNumbers();
} }
return new int[0]; return new int[0];
@@ -241,17 +240,9 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
if(getMoveToChannel() instanceof DMRLogicalChannel) if(getMoveToChannel() != null)
{ {
DMRLogicalChannel channel = (DMRLogicalChannel)getMoveToChannel(); getMoveToChannel().apply(timeslotFrequencies);
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(channel.getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
channel.setTimeslotFrequency(timeslotFrequency);
}
}
} }
} }

View File

@@ -88,7 +88,8 @@ public class MoveTSCC extends CSBKMessage
if(multiBlock != null) if(multiBlock != null)
{ {
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); //Timeslot hard-coded to 1 since this is a control channel.
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -23,24 +23,20 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel; import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.message.CACH; import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType; import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters;
import io.github.dsheirer.module.decode.dmr.message.type.DataType; import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* DMR Tier III - Announcement Message - Adjacent/Neighbor Site Information * DMR Tier III - Announcement Message - Adjacent/Neighbor Site Information
*/ */
public class AdjacentSiteInformation extends Announcement implements ITimeslotFrequencyReceiver public class AdjacentSiteInformation extends Announcement
{ {
private static final int NEIGHBOR_SYSTEM_IDENTITY_CODE_OFFSET = 21; private static final int NEIGHBOR_SYSTEM_IDENTITY_CODE_OFFSET = 21;
private static final int NETWORK_CONNECTION_STATUS_AVAILABLE_FLAG = 56; private static final int NETWORK_CONNECTION_STATUS_AVAILABLE_FLAG = 56;
@@ -88,8 +84,8 @@ public class AdjacentSiteInformation extends Announcement implements ITimeslotFr
if(multiBlock != null) if(multiBlock != null)
{ {
//Timeslot hard-coded to zero for control channel //Timeslot hard-coded to one for control channel
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
} }
} }
@@ -235,39 +231,4 @@ public class AdjacentSiteInformation extends Announcement implements ITimeslotFr
return mIdentifiers; return mIdentifiers;
} }
/**
* Logical Slot Number(s) for channels contained in this message
*/
@Override
public int[] getLogicalTimeslotNumbers()
{
if(getNeighborChannel() instanceof DMRLogicalChannel)
{
return ((DMRLogicalChannel)getNeighborChannel()).getLSNArray();
}
return new int[0];
}
/**
* Applies the timeslot frequency lookup information to channels contained in this message
* @param timeslotFrequencies that match the logical timeslots
*/
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
if(getNeighborChannel() instanceof DMRLogicalChannel)
{
DMRLogicalChannel channel = (DMRLogicalChannel)getNeighborChannel();
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(channel.getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
channel.setTimeslotFrequency(timeslotFrequency);
}
}
}
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -27,7 +27,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters;
import io.github.dsheirer.module.decode.dmr.message.type.DataType; import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -56,7 +55,8 @@ public class AnnounceChannelFrequency extends Announcement
if(multiBlock != null) if(multiBlock != null)
{ {
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); //Timeslot hard-coded to one
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -29,7 +29,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters;
import io.github.dsheirer.module.decode.dmr.message.type.DataType; import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -87,8 +86,8 @@ public class AnnounceWithdrawTSCC extends Announcement
if(multiBlock != null) if(multiBlock != null)
{ {
//Control channel timeslot hard-coded to 0 //Control channel timeslot hard-coded to 1
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel; import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
@@ -33,7 +32,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBloc
import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters;
import io.github.dsheirer.module.decode.dmr.message.type.DataType; import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode; import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -91,7 +89,8 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
if(multiBlock != null) if(multiBlock != null)
{ {
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0); //Timeslot hard-coded to one for control channel
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
} }
} }
@@ -240,11 +239,11 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
* Logical Slot Number(s) for channels contained in this message * Logical Slot Number(s) for channels contained in this message
*/ */
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
if(getChannel() instanceof DMRLogicalChannel) if(getChannel() != null)
{ {
return ((DMRLogicalChannel)getChannel()).getLSNArray(); return getChannel().getLogicalChannelNumbers();
} }
return new int[0]; return new int[0];
@@ -257,17 +256,9 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
if(getChannel() instanceof DMRLogicalChannel) if(getChannel() != null)
{ {
DMRLogicalChannel channel = (DMRLogicalChannel)getChannel(); getChannel().apply(timeslotFrequencies);
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(channel.getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
channel.setTimeslotFrequency(timeslotFrequency);
}
}
} }
} }
} }

View File

@@ -104,7 +104,7 @@ public class BroadcastTalkgroupVoiceChannelGrant extends ChannelGrant
sb.append(" BROADCAST TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio()); sb.append(" BROADCAST TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationTalkgroup()); sb.append(" TO:").append(getDestinationTalkgroup());
sb.append(" CHAN:").append(getChannel()); sb.append(" ").append(getChannel());
return sb.toString(); return sb.toString();
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel; import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel;
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel; import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
@@ -32,7 +31,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock; import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters; import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters;
import io.github.dsheirer.module.decode.dmr.message.type.DataType; import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.List; import java.util.List;
/** /**
@@ -159,11 +157,11 @@ public abstract class ChannelGrant extends CSBKMessage implements ITimeslotFrequ
* Logical Slot Number(s) for channels contained in this message * Logical Slot Number(s) for channels contained in this message
*/ */
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
if(getChannel() instanceof DMRLogicalChannel) if(getChannel() != null)
{ {
return ((DMRLogicalChannel)getChannel()).getLSNArray(); return getChannel().getLogicalChannelNumbers();
} }
return new int[0]; return new int[0];
@@ -176,17 +174,9 @@ public abstract class ChannelGrant extends CSBKMessage implements ITimeslotFrequ
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
if(getChannel() instanceof DMRLogicalChannel) if(getChannel() != null)
{ {
DMRLogicalChannel channel = (DMRLogicalChannel)getChannel(); getChannel().apply(timeslotFrequencies);
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
{
if(channel.getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
channel.setTimeslotFrequency(timeslotFrequency);
}
}
} }
} }
} }

View File

@@ -103,7 +103,7 @@ public class DuplexPrivateDataChannelGrant extends ChannelGrant
sb.append(" DUPLEX PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio()); sb.append(" DUPLEX PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio()); sb.append(" TO:").append(getDestinationRadio());
sb.append(" CHAN:").append(getChannel()); sb.append(" ").append(getChannel());
return sb.toString(); return sb.toString();
} }

View File

@@ -103,7 +103,7 @@ public class DuplexPrivateVoiceChannelGrant extends ChannelGrant
sb.append(" DUPLEX PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio()); sb.append(" DUPLEX PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio()); sb.append(" TO:").append(getDestinationRadio());
sb.append(" CHAN:").append(getChannel()); sb.append(" ").append(getChannel());
return sb.toString(); return sb.toString();
} }

View File

@@ -108,7 +108,7 @@ public class PrivateDataChannelGrant extends ChannelGrant
sb.append(" PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio()); sb.append(" PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio()); sb.append(" TO:").append(getDestinationRadio());
sb.append(" CHAN:").append(getChannel()); sb.append(" ").append(getChannel());
return sb.toString(); return sb.toString();
} }

View File

@@ -102,7 +102,7 @@ public class PrivateVoiceChannelGrant extends ChannelGrant
sb.append(" PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio()); sb.append(" PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio()); sb.append(" TO:").append(getDestinationRadio());
sb.append(" CHAN:").append(getChannel()); sb.append(" ").append(getChannel());
return sb.toString(); return sb.toString();
} }

View File

@@ -110,7 +110,7 @@ public class TalkgroupDataChannelGrant extends ChannelGrant
sb.append(" TALKGROUP DATA CHANNEL GRANT FM:").append(getSourceRadio()); sb.append(" TALKGROUP DATA CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationTalkgroup()); sb.append(" TO:").append(getDestinationTalkgroup());
sb.append(" CHAN:").append(getChannel()); sb.append(" ").append(getChannel());
return sb.toString(); return sb.toString();
} }

View File

@@ -104,7 +104,7 @@ public class TalkgroupVoiceChannelGrant extends ChannelGrant
sb.append(" TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio()); sb.append(" TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationTalkgroup()); sb.append(" TO:").append(getDestinationTalkgroup());
sb.append(" CHAN:").append(getChannel()); sb.append(" ").append(getChannel());
return sb.toString(); return sb.toString();
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -25,7 +25,6 @@ import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH; import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType; import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.type.DefinedDataFormat; import io.github.dsheirer.module.decode.dmr.message.type.DefinedDataFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -39,6 +38,7 @@ public class DefinedShortDataHeader extends PacketSequenceHeader
private static final int RESYNCHRONIZE_FLAG = 70; private static final int RESYNCHRONIZE_FLAG = 70;
private static final int FULL_MESSAGE_FLAG = 71; private static final int FULL_MESSAGE_FLAG = 71;
private static final int[] BIT_PADDING = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; private static final int[] BIT_PADDING = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private static final int[] HEADER_CRC = new int[]{80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95};
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;

View File

@@ -17,18 +17,19 @@
* **************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.module.decode.dmr.message.data.header.hytera; package io.github.dsheirer.module.decode.dmr.message.data.header;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern; import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH; import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType; import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory;
/** /**
* Hytera Proprietary Data Header * PI/Encryption Header with Link Control
*/ */
public class HyteraProprietaryDataHeader extends ProprietaryDataHeader public class PiHeader extends HeaderMessage
{ {
/** /**
* Constructs an instance. * Constructs an instance.
@@ -40,23 +41,42 @@ public class HyteraProprietaryDataHeader extends ProprietaryDataHeader
* @param timestamp message was received * @param timestamp message was received
* @param timeslot for the DMR burst * @param timeslot for the DMR burst
*/ */
public HyteraProprietaryDataHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) public PiHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
{ {
super(syncPattern, message, cach, slotType, timestamp, timeslot); super(syncPattern, message, cach, slotType, timestamp, timeslot);
} }
/**
* Access the embedded link control message
*/
public LCMessage getLCMessage()
{
if(mLCMessage == null)
{
//Overrides default to ignore opcode and construct an encryption parameters message.
mLCMessage = LCMessageFactory.createFullEncryption(getMessage(), getTimestamp(), getTimeslot());
}
return mLCMessage;
}
@Override @Override
public String toString() public String toString()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(!isValid()) if(!isValid())
{ {
sb.append(" [CRC ERROR]"); sb.append(" [CRC ERROR]");
} }
sb.append(" HYTERA PROPRIETARY DATA HEADER");
sb.append(" SAP:").append(getServiceAccessPoint()); sb.append(getSlotType());
sb.append(" MSG:").append(getMessage().toHexString());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits()).append(" ");
}
sb.append(" ").append(getLCMessage());
return sb.toString(); return sb.toString();
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -34,6 +34,11 @@ public class ProprietaryDataHeader extends DataHeader
{ {
private static final int[] SERVICE_ACCESS_POINT = new int[]{0, 1, 2, 3}; private static final int[] SERVICE_ACCESS_POINT = new int[]{0, 1, 2, 3};
private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] VENDOR_DATA = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
private static final int[] HEADER_CRC = new int[]{80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95};
/** /**
* Constructs an instance. * Constructs an instance.
@@ -62,10 +67,20 @@ public class ProprietaryDataHeader extends DataHeader
sb.append(" PROPRIETARY DATA HEADER"); sb.append(" PROPRIETARY DATA HEADER");
sb.append(" VENDOR:").append(getVendor()); sb.append(" VENDOR:").append(getVendor());
sb.append(" ").append(getServiceAccessPoint()); sb.append(" ").append(getServiceAccessPoint());
sb.append(" VENDOR DATA:").append(getVendorData());
sb.append(" MSG:").append(getMessage().toHexString()); sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString(); return sb.toString();
} }
/**
* Vendor defined data payload from this proprietary header.
* @return hex values.
*/
public String getVendorData()
{
return getMessage().getHex(VENDOR_DATA, 16);
}
/** /**
* Optional packet prefix. * Optional packet prefix.
* @return packet prefix fragment or null. * @return packet prefix fragment or null.

View File

@@ -0,0 +1,126 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.message.data.header.hytera;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
/**
* Hytera Proprietary Data Header for SAP Short Data
*/
public class HyteraDataEncryptionHeader extends ProprietaryDataHeader
{
private static final int[] ALGORITHM = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] KEY = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] INITIALIZATION_VECTOR = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] UNKNOWN = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
/**
* Constructs an instance.
*
* @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA
* @param message containing extracted 196-bit payload.
* @param cach for the DMR burst
* @param slotType for this data message
* @param timestamp message was received
* @param timeslot for the DMR burst
*/
public HyteraDataEncryptionHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
{
super(syncPattern, message, cach, slotType, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(!isValid())
{
sb.append(" [CRC ERROR]");
}
sb.append(" HYTERA DATA ENCRYPTION HEADER - SAP:").append(getServiceAccessPoint());
switch(getAlgorithm())
{
case NO_ENCRYPTION:
sb.append(" ALGORITHM:UNENCRYPTED");
break;
case UNKNOWN:
sb.append(" ALGORITHM:").append(getMessage().getInt(ALGORITHM));
break;
default:
sb.append(" ALGORITHM:").append(getAlgorithm());
break;
}
if(getAlgorithm() != EncryptionAlgorithm.NO_ENCRYPTION)
{
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getIV());
sb.append(" UNK:").append(getUnknown());
}
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Encryption algorithm.
* @return algorithm or UNKNOWN if the value is not known.
*/
public EncryptionAlgorithm getAlgorithm()
{
return EncryptionAlgorithm.fromValue(getMessage().getInt(ALGORITHM));
}
/**
* Encryption key ID (1-255).
* @return key ID
*/
public int getKeyId()
{
return getMessage().getInt(KEY);
}
/**
* Hex string representation of the initialization vector
* @return IV in hex string.
*/
public String getIV()
{
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
}
/**
* Unknown field.
* @return hex string from the field contents.
*/
public String getUnknown()
{
return getMessage().getHex(UNKNOWN, 4);
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -66,7 +66,8 @@ public class MNISProprietaryDataHeader extends ProprietaryDataHeader
public String toString() public String toString()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("MOTOROLA MNIS HEADER"); sb.append("CC:").append(getSlotType().getColorCode());
sb.append(" MOTOROLA MNIS HEADER");
if(getApplicationType() == ApplicationType.UNKNOWN) if(getApplicationType() == ApplicationType.UNKNOWN)
{ {
sb.append(" APPLICATION TYPE:0x").append(Integer.toHexString(getApplicationTypeNumber()).toUpperCase()); sb.append(" APPLICATION TYPE:0x").append(Integer.toHexString(getApplicationTypeNumber()).toUpperCase());

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -31,10 +31,15 @@ import io.github.dsheirer.module.decode.dmr.message.type.Vendor;
/** /**
* Motorola Proprietary Data Header * Motorola Proprietary Data Header
*/ */
public class MotorolaProprietaryDataHeader extends ProprietaryDataHeader public class MotorolaDataEncryptionHeader extends ProprietaryDataHeader
{ {
private static final int[] SERVICE_ACCESS_POINT = new int[]{0, 1, 2, 3}; private static final int[] SERVICE_ACCESS_POINT = new int[]{0, 1, 2, 3};
private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] ALGORITHM = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] KEY_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] UNKNOWN = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] INITIALIZATION_VECTOR = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
/** /**
* Constructs an instance. * Constructs an instance.
@@ -46,7 +51,7 @@ public class MotorolaProprietaryDataHeader extends ProprietaryDataHeader
* @param timestamp message was received * @param timestamp message was received
* @param timeslot for the DMR burst * @param timeslot for the DMR burst
*/ */
public MotorolaProprietaryDataHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot) public MotorolaDataEncryptionHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
{ {
super(syncPattern, message, cach, slotType, timestamp, timeslot); super(syncPattern, message, cach, slotType, timestamp, timeslot);
} }
@@ -55,14 +60,57 @@ public class MotorolaProprietaryDataHeader extends ProprietaryDataHeader
public String toString() public String toString()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("MOTOROLA PROPRIETARY DATA HEADER"); sb.append("CC:").append(getSlotType().getColorCode());
sb.append(" ").append(getServiceAccessPoint()); sb.append(" MOTOROLA DATA ENCRYPTION HEADER");
sb.append(" SAP:").append(getServiceAccessPoint());
sb.append(" ALGORITHM?:").append(getAlgorithm());
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" UNK:").append(getUnknown());
sb.append(" MSG:").append(getMessage().toHexString()); sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString(); return sb.toString();
} }
/**
* Unknown message field(s).
* @return hex value.
*/
public String getUnknown()
{
return getMessage().getHex(UNKNOWN, 4);
}
/**
* Encryption key ID
* @return key ID
*/
public int getKeyId()
{
return getMessage().getInt(KEY_ID);
}
/**
* Encryption Algorithm
* @return algorithm ID
*/
public int getAlgorithm()
{
return getMessage().getInt(ALGORITHM);
}
/**
* Encryption initialization vector
*
* @return vector in hex
*/
public String getInitializationVector()
{
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
}
/** /**
* Utility method to lookup the vendor from a CSBK message * Utility method to lookup the vendor from a CSBK message
*
* @param message containing CSBK bits * @param message containing CSBK bits
* @return vendor * @return vendor
*/ */

View File

@@ -23,6 +23,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCDMR; import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.edac.Checksum_5_DMR; import io.github.dsheirer.edac.Checksum_5_DMR;
import io.github.dsheirer.edac.ReedSolomon_12_9_4_DMR; import io.github.dsheirer.edac.ReedSolomon_12_9_4_DMR;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
@@ -37,8 +38,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGr
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraTerminator; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraTerminator;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ActivityUpdateMessage; import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ActivityUpdateMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel; import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusControlChannel; import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusControlChannel;
@@ -59,8 +60,20 @@ public class LCMessageFactory
{ {
private final static Logger mLog = LoggerFactory.getLogger(LCMessageFactory.class); private final static Logger mLog = LoggerFactory.getLogger(LCMessageFactory.class);
private static final ReedSolomon_12_9_4_DMR REED_SOLOMON_12_9_4_DMR = new ReedSolomon_12_9_4_DMR(); private static final ReedSolomon_12_9_4_DMR REED_SOLOMON_12_9_4_DMR = new ReedSolomon_12_9_4_DMR();
private static int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99; private static final int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99;
private static int VOICE_LINK_CONTROL_CRC_MASK = 0x96; private static final int VOICE_LINK_CONTROL_CRC_MASK = 0x96;
/**
* Creates a full link control message specifically for the PI_HEADER message type.
* @param message bits
* @param timestamp of the original message
* @param timeslot of the original message
* @return encryption parameters link control.
*/
public static FullLCMessage createFullEncryption(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
return new EncryptionParameters(message, timestamp, timeslot);
}
/** /**
* Creates a full link control message * Creates a full link control message
@@ -112,8 +125,8 @@ public class LCMessageFactory
case FULL_STANDARD_TERMINATOR_DATA: case FULL_STANDARD_TERMINATOR_DATA:
flc = new TerminatorData(message, timestamp, timeslot); flc = new TerminatorData(message, timestamp, timeslot);
break; break;
case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER: case FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER:
flc = new CapacityPlusGroupVoiceChannelUser(message, timestamp, timeslot); flc = new MotorolaGroupVoiceChannelUser(message, timestamp, timeslot);
break; break;
case FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER: case FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER:
flc = new CapacityPlusEncryptedVoiceChannelUser(message, timestamp, timeslot); flc = new CapacityPlusEncryptedVoiceChannelUser(message, timestamp, timeslot);
@@ -121,7 +134,10 @@ public class LCMessageFactory
case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER: case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER:
flc = new CapacityPlusWideAreaVoiceChannelUser(message, timestamp, timeslot); flc = new CapacityPlusWideAreaVoiceChannelUser(message, timestamp, timeslot);
break; break;
case FULL_ENCRYPTION_PARAMETERS:
flc = new EncryptionParameters(message, timestamp, timeslot);
valid = flc.isValid();
break;
case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER: case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER:
flc = new HyteraGroupVoiceChannelUser(message, timestamp, timeslot); flc = new HyteraGroupVoiceChannelUser(message, timestamp, timeslot);
break; break;

View File

@@ -27,7 +27,6 @@ import java.util.TreeMap;
/** /**
* Full and Short Link Control Opcode enumeration * Full and Short Link Control Opcode enumeration
*
* ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B.1 & B.2 * ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B.1 & B.2
* ETSI TS 102 361-4 DMR Trunking Protocol, Annex B.1 & B.2 * ETSI TS 102 361-4 DMR Trunking Protocol, Annex B.1 & B.2
*/ */
@@ -44,10 +43,12 @@ public enum LCOpcode
FULL_STANDARD_TERMINATOR_DATA(Vendor.STANDARD, true, 48, "TERMINATOR DATA"), FULL_STANDARD_TERMINATOR_DATA(Vendor.STANDARD, true, 48, "TERMINATOR DATA"),
FULL_STANDARD_UNKNOWN(Vendor.STANDARD,true, -1, "FULL UNKNOWN"), FULL_STANDARD_UNKNOWN(Vendor.STANDARD,true, -1, "FULL UNKNOWN"),
FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 0, "GROUP VOICE CHANNEL USER"), FULL_MOTOROLA_GROUP_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 0, "GROUP VOICE CHANNEL USER"),
FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 4, "WAN GROUP VOICE CHANNEL USER"), FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 4, "WAN GROUP VOICE CHANNEL USER"),
//Observed on Cap+ Multi-Site System during an encrypted voice call //Observed on Cap+ Multi-Site System during an encrypted voice call
FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 32, "ENCRYPTED VOICE CHANNEL USER"), FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 32, "ENCRYPTED VOICE CHANNEL USER"),
//Observed on Cap+ Multi-Site System during an encrypted voice call
FULL_ENCRYPTION_PARAMETERS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 33, "ENCRYPTION PARAMETERS"),
//Cap+ opcodes from https://forums.radioreference.com/threads/understanding-capacity-plus-trunking-some-more.452566/ //Cap+ opcodes from https://forums.radioreference.com/threads/understanding-capacity-plus-trunking-some-more.452566/
//FLCO 0: Group Call Maintenance //FLCO 0: Group Call Maintenance
//FLCO 3: Private Call Maintenance (TermLC) //FLCO 3: Private Call Maintenance (TermLC)
@@ -79,10 +80,10 @@ public enum LCOpcode
private static final EnumSet<LCOpcode> TALKER_ALIAS_OPCODES = EnumSet.of(FULL_STANDARD_TALKER_ALIAS_HEADER, private static final EnumSet<LCOpcode> TALKER_ALIAS_OPCODES = EnumSet.of(FULL_STANDARD_TALKER_ALIAS_HEADER,
FULL_STANDARD_TALKER_ALIAS_BLOCK_1, FULL_STANDARD_TALKER_ALIAS_BLOCK_2, FULL_STANDARD_TALKER_ALIAS_BLOCK_3); FULL_STANDARD_TALKER_ALIAS_BLOCK_1, FULL_STANDARD_TALKER_ALIAS_BLOCK_2, FULL_STANDARD_TALKER_ALIAS_BLOCK_3);
private Vendor mVendor; private final Vendor mVendor;
private boolean mFull; private final boolean mFull;
private int mValue; private final int mValue;
private String mLabel; private final String mLabel;
/** /**
* Constructs an instance * Constructs an instance

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@
package io.github.dsheirer.module.decode.dmr.message.data.lc.full; package io.github.dsheirer.module.decode.dmr.message.data.lc.full;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.message.IServiceOptionsProvider;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions; import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
/** /**
@@ -27,7 +28,7 @@ import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
* *
* ETSI TS 102 361-2 7.1.1.2 * ETSI TS 102 361-2 7.1.1.2
*/ */
public abstract class AbstractVoiceChannelUser extends FullLCMessage public abstract class AbstractVoiceChannelUser extends FullLCMessage implements IServiceOptionsProvider
{ {
private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};

View File

@@ -0,0 +1,165 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.message.data.lc.full;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
import java.util.ArrayList;
import java.util.List;
/**
* Encryption Parameters, carried by a PI_HEADER.
*
* Note: this message repurposes the OPCODE field to carry the encryption algorithm.
*/
public class EncryptionParameters extends FullLCMessage
{
private static final int[] KEY_ID = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] INITIALIZATION_VECTOR = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] DESTINATION_GROUP = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 75, 76, 77, 78, 79};
//CRC-CCITT with 0x9696 initial fill, bits: 80-95
private DMRTalkgroup mTalkgroup;
private List<Identifier> mIdentifiers;
/**
* Constructor
* @param message bits
* @param timestamp for the message
* @param timeslot of the message
*/
public EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
int bitErrors = CRCDMR.correctCCITT80(message, 0, 80, 0x9696);
setValid(bitErrors < 2);
}
/**
* Overrides the default method for opcode to return a static value, because the opcode field is repurposed to
* be the algorithm field in both Motorola and Hytera variants of this message, both carried by the Encryption
* Header message.
* @return full encryption parameters opcode.
*/
@Override
public LCOpcode getOpcode()
{
return LCOpcode.FULL_ENCRYPTION_PARAMETERS;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" *ENCRYPTED*");
}
if(isReservedBitSet())
{
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC ENCRYPTION PARAMETERS");
sb.append(" VENDOR:").append(getVendor());
if(getAlgorithm() == EncryptionAlgorithm.UNKNOWN)
{
//List the algorithm (aka opcod) value for unknown algorithms
sb.append(" ALGORITHM:").append(getOpcodeValue());
}
else
{
sb.append(" ALGORITHM:").append(getAlgorithm());
}
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" TALKGROUP:").append(getTalkgroup());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Encryption parameter details
* @return details
*/
public String getDetails()
{
StringBuilder sb = new StringBuilder();
sb.append("ENCRYPTION ALGORITHM:").append(getAlgorithm());
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" VENDOR:").append(getVendor());
return sb.toString();
}
/**
* Encryption algorithm. Note: this message repurposes the OPCODE field to carry the algorithm ID.
* @return algorithm or UNKNOWN.
*/
public EncryptionAlgorithm getAlgorithm()
{
return EncryptionAlgorithm.fromValue(getOpcodeValue());
}
public DMRTalkgroup getTalkgroup()
{
if(mTalkgroup == null)
{
mTalkgroup = new DMRTalkgroup(getMessage().getInt(DESTINATION_GROUP));
}
return mTalkgroup;
}
public int getKeyId()
{
return getMessage().getInt(KEY_ID);
}
public String getInitializationVector()
{
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkgroup());
}
return mIdentifiers;
}
}

View File

@@ -30,7 +30,7 @@ public abstract class FullLCMessage extends LCMessage
{ {
private static final int ENCRYPTION_FLAG = 0; private static final int ENCRYPTION_FLAG = 0;
private static final int RESERVED = 1; private static final int RESERVED = 1;
private static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7}; protected static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7};
private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15}; private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
/** /**

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -24,10 +24,10 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.location.LocationIdentifier; import io.github.dsheirer.identifier.location.LocationIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRLocation; import io.github.dsheirer.module.decode.dmr.identifier.DMRLocation;
import io.github.dsheirer.module.decode.dmr.message.type.PositionError; import io.github.dsheirer.module.decode.dmr.message.type.PositionError;
import org.apache.commons.math3.util.FastMath;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.commons.math3.util.FastMath;
import org.jdesktop.swingx.mapviewer.GeoPosition;
/** /**
* GPS Information * GPS Information
@@ -81,16 +81,30 @@ public class GPSInformation extends FullLCMessage
return PositionError.fromValue(getMessage().getInt(POSITION_ERROR)); return PositionError.fromValue(getMessage().getInt(POSITION_ERROR));
} }
/**
* Latitude in decimal degrees
*/
public double getLatitude() public double getLatitude()
{ {
return getMessage().getTwosComplement(LATITUDE_START, LATITUDE_END) * LATITUDE_UNITS; return getMessage().getTwosComplement(LATITUDE_START, LATITUDE_END) * LATITUDE_UNITS;
} }
/**
* Longitude in decimal degrees
*/
public double getLongitude() public double getLongitude()
{ {
return getMessage().getTwosComplement(LONGITUDE_START, LONGITUDE_END) * LONGITUDE_UNITS; return getMessage().getTwosComplement(LONGITUDE_START, LONGITUDE_END) * LONGITUDE_UNITS;
} }
/**
* Geo-position for the lat/lon
*/
public GeoPosition getPosition()
{
return new GeoPosition(getLatitude(), getLongitude());
}
/** /**
* GPS location as an identifier * GPS location as an identifier
*/ */

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,13 +22,14 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.message.IServiceOptionsProvider;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions; import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
/** /**
* Hytera Full Link Control * Hytera Full Link Control
*/ */
public abstract class HyteraFullLC extends FullLCMessage public abstract class HyteraFullLC extends FullLCMessage implements IServiceOptionsProvider
{ {
private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] FREE_REPEATER = new int[]{24, 25, 26, 27}; private static final int[] FREE_REPEATER = new int[]{24, 25, 26, 27};
@@ -36,11 +37,12 @@ public abstract class HyteraFullLC extends FullLCMessage
protected static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; protected static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] PRIORITY_CALL_HASHED_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; private static final int[] PRIORITY_CALL_HASHED_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71}; private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] UNKNOWN_1 = new int[]{72, 73, 74, 75}; //FEC: 72-96
private static final int[] UNKNOWN_2 = new int[]{76, 77, 78, 79}; //TODO: Target and Source address fields may be full 24-bit values. How do we distinguish between Hytera repeater
//TODO: and a Hytera XPT repeater? Do the service options identify when it's XPT?
private ServiceOptions mServiceOptions; private ServiceOptions mServiceOptions;
private RadioIdentifier mSourceRadio; protected RadioIdentifier mSourceRadio;
/** /**
* Constructs an instance. * Constructs an instance.
@@ -109,24 +111,6 @@ public abstract class HyteraFullLC extends FullLCMessage
return String.format("%02X", getMessage().getInt(PRIORITY_CALL_HASHED_ADDRESS)).toUpperCase(); return String.format("%02X", getMessage().getInt(PRIORITY_CALL_HASHED_ADDRESS)).toUpperCase();
} }
/**
* Unknown nibble one. This value remains consistent for the FLC between the Voice Header and the Terminator
* for calls. It does not seem to have correlation to the channel/repeater number
*/
public int getUnknownField1()
{
return getMessage().getInt(UNKNOWN_1);
}
/**
* Unknown nibble two. For each talkgroup, this field shows one value for the Voice Header and the same value but
* bit-inverted in the Terminator.
*/
public int getUnknownField2()
{
return getMessage().getInt(UNKNOWN_2);
}
/** /**
* Source radio address * Source radio address
*/ */

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -21,9 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup; import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -32,6 +33,11 @@ import java.util.List;
*/ */
public class HyteraGroupVoiceChannelUser extends HyteraFullLC public class HyteraGroupVoiceChannelUser extends HyteraFullLC
{ {
protected static final int[] TALKGROUP = new int[]{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 = new int[]{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 TalkgroupIdentifier mTalkgroup; private TalkgroupIdentifier mTalkgroup;
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;
@@ -55,26 +61,11 @@ public class HyteraGroupVoiceChannelUser extends HyteraFullLC
sb.append("[CRC-ERROR] "); sb.append("[CRC-ERROR] ");
} }
sb.append("FLC HYTERA XPT GROUP VOICE CHANNEL USER FM:"); sb.append("FLC HYTERA GROUP VOICE CHANNEL USER FM:");
sb.append(getSourceRadio()); sb.append(getSourceRadio());
sb.append(" TO:").append(getTalkgroup()); sb.append(" TO:").append(getTalkgroup());
if(isAllChannelsBusy())
{
sb.append(" ALL REPEATERS BUSY");
}
else
{
sb.append(" FREE REPEATER:").append(getFreeRepeater());
}
if(hasPriorityCall())
{
sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress());
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
}
sb.append(" ").append(getServiceOptions()); sb.append(" ").append(getServiceOptions());
sb.append(" UNK1:").append(getUnknownField1()); sb.append(" MSG:").append(getMessage().toHexString());
sb.append(" UNK2:").append(getUnknownField2());
return sb.toString(); return sb.toString();
} }
@@ -85,12 +76,25 @@ public class HyteraGroupVoiceChannelUser extends HyteraFullLC
{ {
if(mTalkgroup == null) if(mTalkgroup == null)
{ {
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS)); mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TALKGROUP));
} }
return mTalkgroup; return mTalkgroup;
} }
/**
* Source radio address
*/
public RadioIdentifier getSourceRadio()
{
if(mSourceRadio == null)
{
mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO));
}
return mSourceRadio;
}
@Override @Override
public List<Identifier> getIdentifiers() public List<Identifier> getIdentifiers()
{ {

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup; import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -73,8 +72,6 @@ public class HyteraTerminator extends HyteraFullLC
sb.append(" ON REPEATER:").append(getPriorityCallRepeater()); sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
} }
sb.append(" ").append(getServiceOptions()); sb.append(" ").append(getServiceOptions());
sb.append(" UNK1:").append(getUnknownField1());
sb.append(" UNK2:").append(getUnknownField2());
return sb.toString(); return sb.toString();
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -72,8 +71,6 @@ public class HyteraUnitToUnitVoiceChannelUser extends HyteraFullLC
sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress()); sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress());
sb.append(" ON REPEATER:").append(getPriorityCallRepeater()); sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
} }
sb.append(" UNK1:").append(getUnknownField1());
sb.append(" UNK2:").append(getUnknownField2());
sb.append(" ").append(getServiceOptions()); sb.append(" ").append(getServiceOptions());
return sb.toString(); return sb.toString();
} }

View File

@@ -37,7 +37,7 @@ public class CapacityPlusEncryptedVoiceChannelUser extends CapacityPlusVoiceChan
private static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; private static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] UNKNOWN_2 = new int[]{48, 49, 50, 51, 52, 53, 54, 55}; private static final int[] UNKNOWN_2 = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71}; private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] UNKNOWN_3 = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; //Reed Solomon FEC: 72-95
private RadioIdentifier mRadio; private RadioIdentifier mRadio;
private TalkgroupIdentifier mTalkgroup; private TalkgroupIdentifier mTalkgroup;
@@ -94,10 +94,6 @@ public class CapacityPlusEncryptedVoiceChannelUser extends CapacityPlusVoiceChan
{ {
return getMessage().getHex(UNKNOWN_2, 2); return getMessage().getHex(UNKNOWN_2, 2);
} }
public String getUnknown3()
{
return getMessage().getHex(UNKNOWN_3, 2);
}
/** /**
* Source radio address * Source radio address

View File

@@ -1,236 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
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.dmr.channel.DMRLogicalChannel;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Capacity Plus - Group Voice Channel User
*/
public class CapacityPlusGroupVoiceChannelUser extends CapacityPlusVoiceChannelUser implements ITimeslotFrequencyReceiver
{
private static final int[] CAPACITY_PLUS_GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] CONVENTIONAL_GROUP_ADDRESS = new int[]{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[] REST_CHANNEL = new int[]{51, 52, 53, 54};
private static final int[] REST_CHANNEL_TIMESLOT = new int[]{55};
private static final int[] CAPACITY_PLUS_SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] CONVENTIONAL_SOURCE_ADDRESS = new int[]{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 int[] UNKNOWN = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private RadioIdentifier mRadio;
private TalkgroupIdentifier mTalkgroup;
private DMRLogicalChannel mRestChannel;
private List<Identifier> mIdentifiers;
/**
* Constructs an instance.
*
* @param message for the link control payload
*/
public CapacityPlusGroupVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" ENCRYPTED");
}
if(isReservedBitSet())
{
sb.append(" RESERVED-BIT");
}
if(getServiceOptions().isCapacityPlus())
{
sb.append("FLC MOTOROLA CAP+ GROUP VOICE CHANNEL USER");
}
else
{
sb.append("FLC MOTOROLA CONV/IP SITE GROUP VOICE CHANNEL USER");
}
sb.append(" FM:").append(getRadio());
sb.append(" TO:").append(getTalkgroup());
if(hasRestChannel())
{
sb.append(" REST:");
sb.append(getRestChannel());
}
sb.append(" ").append(getServiceOptions());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Unknown 8-bit field
*/
public String getUnknown()
{
return getMessage().getHex(UNKNOWN, 2);
}
/**
* Logical channel number (ie repeater number).
*/
public DMRLogicalChannel getRestChannel()
{
if(mRestChannel == null)
{
mRestChannel = new DMRLogicalChannel(getRestChannelRepeater(), getRestChannelTimeslot());
}
return mRestChannel;
}
/**
* Rest repeater number
*/
public int getRestChannelRepeater()
{
return getMessage().getInt(REST_CHANNEL) + 1;
}
/**
* Rest timeslot
*
* @return
*/
public int getRestChannelTimeslot()
{
return getMessage().getInt(REST_CHANNEL_TIMESLOT) + 1;
}
/**
* Indicates if this message has a rest channel indicated for the call
*/
public boolean hasRestChannel()
{
return getServiceOptions().isCapacityPlus() && getRestChannelRepeater() != 0;
}
/**
* Source radio address
*/
public RadioIdentifier getRadio()
{
if(mRadio == null)
{
if(getServiceOptions().isCapacityPlus())
{
mRadio = DMRRadio.createFrom(getMessage().getInt(CAPACITY_PLUS_SOURCE_ADDRESS));
}
else
{
mRadio = DMRRadio.createFrom(getMessage().getInt(CONVENTIONAL_SOURCE_ADDRESS));
}
}
return mRadio;
}
/**
* Talkgroup address
*/
public TalkgroupIdentifier getTalkgroup()
{
if(mTalkgroup == null)
{
if(getServiceOptions().isCapacityPlus())
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(CAPACITY_PLUS_GROUP_ADDRESS));
}
else
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(CONVENTIONAL_GROUP_ADDRESS));
}
}
return mTalkgroup;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkgroup());
mIdentifiers.add(getRadio());
if(hasRestChannel())
{
mIdentifiers.add(getRestChannel());
}
}
return mIdentifiers;
}
/**
* Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message.
*/
@Override
public int[] getLogicalTimeslotNumbers()
{
return getRestChannel().getLSNArray();
}
/**
* Applies the LSN to frequency map to the rest channel.
*
* @param timeslotFrequencies that match the logical timeslots
*/
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
{
if(getRestChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
}
}

View File

@@ -20,15 +20,17 @@
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola; package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; import io.github.dsheirer.module.decode.dmr.message.IServiceOptionsProvider;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.type.CapacityPlusServiceOptions; import io.github.dsheirer.module.decode.dmr.message.type.CapacityPlusServiceOptions;
/** /**
* Any Capacity Plus Voice Channel User link control message that contains vendor-specific service options. * Any Capacity Plus Voice Channel User link control message that contains vendor-specific service options.
*/ */
public abstract class CapacityPlusVoiceChannelUser extends FullLCMessage public abstract class CapacityPlusVoiceChannelUser extends AbstractVoiceChannelUser implements IServiceOptionsProvider
{ {
private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
//Reed Solomon FEC: 72-95
private CapacityPlusServiceOptions mServiceOptions; private CapacityPlusServiceOptions mServiceOptions;

View File

@@ -23,7 +23,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio; import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
@@ -32,21 +32,23 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
* Motorola Capacity Plus - Wide Area (Multi-Site) Voice Channel User * Motorola Capacity Plus - Wide Area (Linked or Multi-Site) Voice Channel User
*
* Note: in Linked Capacity Plus talkgroups range 1-255 and radio IDs range 1-65535
*/ */
public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChannelUser implements ITimeslotFrequencyReceiver public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChannelUser implements ITimeslotFrequencyReceiver
{ {
private static final int[] UNKNOWN_1 = new int[]{24, 25, 26, 27, 28, 29, 30, 31}; //Bits 16-23: Service Options
private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47}; private static final int[] UNKNOWN_1 = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
//private static final int[] REST_REPEATER = new int[]{51, 52, 53, 54}; private static final int[] GROUP_ID = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
//private static final int[] REST_TIMESLOT = new int[]{55}; private static final int[] UNUSED = new int[]{48, 49, 50};
private static final int[] REST_LSN = new int[]{52, 53, 54, 55}; private static final int[] REST_LSN = new int[]{51, 52, 53, 54, 55};
private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71}; private static final int[] RADIO_ID = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] UNKNOWN_3 = new int[]{72, 73, 74, 75, 76, 77, 78, 79}; //Reed Solomon FEC: 72-95
private RadioIdentifier mRadio; private RadioIdentifier mRadio;
private TalkgroupIdentifier mTalkgroup; private TalkgroupIdentifier mTalkgroup;
private DMRLogicalChannel mRestChannel; private DmrRestLsn mRestChannel;
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;
/** /**
@@ -82,18 +84,12 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
sb.append("FLC MOTOROLA CAP+ WIDE-AREA VOICE CHANNEL USER FM:"); sb.append("FLC MOTOROLA CAP+ WIDE-AREA VOICE CHANNEL USER FM:");
sb.append(getRadio()); sb.append(getRadio());
sb.append(" TO:").append(getTalkgroup()); sb.append(" TO:").append(getTalkgroup());
sb.append(" REST:");
if(hasRestChannel()) if(hasRestChannel())
{ {
sb.append(getRestChannel()); sb.append(" ").append(getRestChannel());
} }
else
{
sb.append("--");
}
sb.append(" UNK1:").append(getUnknown1());
sb.append(" UNK2:").append(getUnknown2());
sb.append(" ").append(getServiceOptions()); sb.append(" ").append(getServiceOptions());
sb.append(" UNK1:").append(getUnknown1());
sb.append(" MSG:").append(getMessage().toHexString()); sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString(); return sb.toString();
} }
@@ -101,27 +97,19 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
/** /**
* Unknown1 8-bit field * Unknown1 8-bit field
*/ */
public int getUnknown1() public String getUnknown1()
{ {
return getMessage().getInt(UNKNOWN_1); return getMessage().getHex(UNKNOWN_1, 4);
}
/**
* Unknown2 8-bit field
*/
public int getUnknown2()
{
return getMessage().getInt(UNKNOWN_3);
} }
/** /**
* Logical channel number (ie repeater number). * Logical channel number (ie repeater number).
*/ */
public DMRLogicalChannel getRestChannel() public DmrRestLsn getRestChannel()
{ {
if(mRestChannel == null) if(mRestChannel == null)
{ {
mRestChannel = new DMRLogicalChannel(getRestRepeater(), getRestTimeslot()); mRestChannel = new DmrRestLsn(getRestLSN());
} }
return mRestChannel; return mRestChannel;
@@ -136,25 +124,12 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
return getMessage().getInt(REST_LSN); return getMessage().getInt(REST_LSN);
} }
/**
* Rest channel timeslot
*/
public int getRestTimeslot()
{
return (getRestLSN() % 2 == 0) ? 2 : 1;
}
/**
* Rest channel repeater number
*/
public int getRestRepeater() { return (int) Math.ceil(getRestLSN() / 2.0); }
/** /**
* Indicates if this message has a reset channel defined. * Indicates if this message has a reset channel defined.
*/ */
public boolean hasRestChannel() public boolean hasRestChannel()
{ {
return getRestRepeater() != 0; return getRestLSN() != 0;
} }
/** /**
@@ -164,7 +139,7 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
{ {
if(mRadio == null) if(mRadio == null)
{ {
mRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS)); mRadio = DMRRadio.createFrom(getMessage().getInt(RADIO_ID));
} }
return mRadio; return mRadio;
@@ -177,7 +152,7 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
{ {
if(mTalkgroup == null) if(mTalkgroup == null)
{ {
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ADDRESS)); mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ID));
} }
return mTalkgroup; return mTalkgroup;
@@ -201,9 +176,9 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
* Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message. * Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message.
*/ */
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
return getRestChannel().getLSNArray(); return getRestChannel().getLogicalChannelNumbers();
} }
/** /**
@@ -214,12 +189,6 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies) getRestChannel().apply(timeslotFrequencies);
{
if(getRestChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
} }
} }

View File

@@ -0,0 +1,122 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
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.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Capacity Plus - Group Voice Channel User
*/
public class MotorolaGroupVoiceChannelUser extends CapacityPlusVoiceChannelUser
{
private static final int[] GROUP_ADDRESS = new int[]{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_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
//Reed Solomon FEC: 72-95
private RadioIdentifier mRadio;
private TalkgroupIdentifier mTalkgroup;
private List<Identifier> mIdentifiers;
/**
* Constructs an instance.
*
* @param message for the link control payload
*/
public MotorolaGroupVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" ENCRYPTED");
}
if(isReservedBitSet())
{
sb.append(" RESERVED-BIT");
}
sb.append("FLC MOTOROLA GROUP VOICE CHANNEL USER");
sb.append(" FM:").append(getRadio());
sb.append(" TO:").append(getTalkgroup());
sb.append(" ").append(getServiceOptions());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Source radio address
*/
public RadioIdentifier getRadio()
{
if(mRadio == null)
{
mRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS));
}
return mRadio;
}
/**
* Talkgroup address
*/
public TalkgroupIdentifier getTalkgroup()
{
if(mTalkgroup == null)
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ADDRESS));
}
return mTalkgroup;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkgroup());
mIdentifiers.add(getRadio());
}
return mIdentifiers;
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.message.type.Activity; import io.github.dsheirer.module.decode.dmr.message.type.Activity;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -55,13 +54,18 @@ public class ActivityUpdateMessage extends ShortLCMessage
{ {
sb.append("[CRC ERROR] "); sb.append("[CRC ERROR] ");
} }
sb.append("SLC TS1:");
sb.append("SLC ACTIVITY UPDATE TS1 [");
sb.append(getHashAddressTS1()).append("] ");
sb.append(getActivityTS1()); sb.append(getActivityTS1());
sb.append(" / TS2 ["); if(getActivityTS1() != Activity.IDLE)
sb.append(getHashAddressTS2()).append("] "); {
sb.append(" [").append(getHashAddressTS1()).append("]");
}
sb.append(" TS2:");
sb.append(getActivityTS2()); sb.append(getActivityTS2());
if(getActivityTS2() != Activity.IDLE)
{
sb.append(" [").append(getHashAddressTS2()).append("]");
}
sb.append(" MSG:").append(getMessage().toHexString()); sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString(); return sb.toString();
} }

View File

@@ -21,7 +21,7 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.channel.DMRLogicalChannel; import io.github.dsheirer.module.decode.dmr.channel.DmrRestLsn;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver; import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency; import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRSite; import io.github.dsheirer.module.decode.dmr.identifier.DMRSite;
@@ -34,13 +34,11 @@ import java.util.List;
public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslotFrequencyReceiver public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslotFrequencyReceiver
{ {
private static final int[] UNKNOWN = new int[]{12, 13, 14}; private static final int[] UNKNOWN = new int[]{12, 13, 14};
//private static final int[] REST_REPEATER = new int[]{15, 16, 17, 18};
private static final int[] REST_LSN = new int[]{15, 16, 17, 18, 19}; private static final int[] REST_LSN = new int[]{15, 16, 17, 18, 19};
//private static final int[] REST_TIMESLOT = new int[]{19};
private static final int[] SITE = new int[]{20, 21, 22, 23, 24}; private static final int[] SITE = new int[]{20, 21, 22, 23, 24};
private static final int[] UNKNOWN_2 = new int[]{25, 26, 27}; private static final int[] UNKNOWN_2 = new int[]{25, 26, 27};
private DMRLogicalChannel mRestChannel; private DmrRestLsn mRestChannel;
private DMRSite mSite; private DMRSite mSite;
private List<Identifier> mIdentifiers; private List<Identifier> mIdentifiers;
@@ -63,7 +61,7 @@ public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslot
sb.append("[CRC ERROR] "); sb.append("[CRC ERROR] ");
} }
sb.append("SLC MOTOROLA CAP+ SITE:").append(getSite()); sb.append("SLC MOTOROLA CAP+ SITE:").append(getSite());
sb.append(" REST:").append(getRestChannel()); sb.append(" ").append(getRestChannel());
sb.append(" MSG:").append(getMessage().toHexString()); sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString(); return sb.toString();
} }
@@ -84,11 +82,11 @@ public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslot
/** /**
* Rest Channel Number * Rest Channel Number
*/ */
public DMRLogicalChannel getRestChannel() public DmrRestLsn getRestChannel()
{ {
if(mRestChannel == null) if(mRestChannel == null)
{ {
mRestChannel = new DMRLogicalChannel(getRestRepeater(), getRestTimeslot()); mRestChannel = new DmrRestLsn(getRestLSN());
} }
return mRestChannel; return mRestChannel;
@@ -103,26 +101,13 @@ public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslot
return getMessage().getInt(REST_LSN); return getMessage().getInt(REST_LSN);
} }
/**
* Rest repeater
*/
public int getRestRepeater() { return (int) Math.ceil(getRestLSN() / 2.0); }
/**
* Rest timeslot
*/
public int getRestTimeslot()
{
return (getRestLSN() % 2 == 0) ? 2 : 1;
}
/** /**
* Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message. * Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message.
*/ */
@Override @Override
public int[] getLogicalTimeslotNumbers() public int[] getLogicalChannelNumbers()
{ {
return getRestChannel().getLSNArray(); return getRestChannel().getLogicalChannelNumbers();
} }
/** /**
@@ -132,13 +117,7 @@ public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslot
@Override @Override
public void apply(List<TimeslotFrequency> timeslotFrequencies) public void apply(List<TimeslotFrequency> timeslotFrequencies)
{ {
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies) getRestChannel().apply(timeslotFrequencies);
{
if(getRestChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
getRestChannel().setTimeslotFrequency(timeslotFrequency);
}
}
} }
@Override @Override

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -21,7 +21,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.shorty;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -48,8 +47,7 @@ public class NullMessage extends ShortLCMessage
{ {
sb.append("[CRC ERROR] "); sb.append("[CRC ERROR] ");
} }
sb.append("SLC IDLE/NULL MESSAGE"); sb.append("SLC TS1:IDLE TS2:IDLE");
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString(); return sb.toString();
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.message.DMRMessage; import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
import io.github.dsheirer.module.decode.ip.IPacket; import io.github.dsheirer.module.decode.ip.IPacket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -75,6 +74,10 @@ public class DMRPacketMessage extends DMRMessage
sb.append("CC:").append(getPacketSequence().getPacketSequenceHeader().getSlotType().getColorCode()); sb.append("CC:").append(getPacketSequence().getPacketSequenceHeader().getSlotType().getColorCode());
sb.append(" FM:").append(getPacketSequence().getPacketSequenceHeader().getSourceLLID()); sb.append(" FM:").append(getPacketSequence().getPacketSequenceHeader().getSourceLLID());
sb.append(" TO:").append(getPacketSequence().getPacketSequenceHeader().getDestinationLLID()); sb.append(" TO:").append(getPacketSequence().getPacketSequenceHeader().getDestinationLLID());
if(getPacketSequence().isEncrypted())
{
sb.append(" ENCRYPTED");
}
sb.append(" ").append(getPacket().toString()); sb.append(" ").append(getPacket().toString());
return sb.toString(); return sb.toString();
} }

View File

@@ -24,6 +24,9 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -32,12 +35,12 @@ import java.util.List;
*/ */
public class PacketSequence public class PacketSequence
{ {
private int mTimeslot; private final int mTimeslot;
private PacketSequenceHeader mPacketSequenceHeader; private PacketSequenceHeader mPacketSequenceHeader;
private UDTHeader mUDTHeader; private UDTHeader mUDTHeader;
private ProprietaryDataHeader mProprietaryDataHeader; private ProprietaryDataHeader mProprietaryDataHeader;
private List<Preamble> mPreambles = new ArrayList<>(); private final List<Preamble> mPreambles = new ArrayList<>();
private List<DataBlock> mDataBlocks = new ArrayList<>(); private final List<DataBlock> mDataBlocks = new ArrayList<>();
public PacketSequence(int timeslot) public PacketSequence(int timeslot)
{ {
@@ -89,6 +92,27 @@ public class PacketSequence
return false; return false;
} }
/**
* Indicates if this sequence contains a Motorola data encryption header.
* @return true if so
*/
public boolean isEncrypted()
{
if(hasProprietaryDataHeader())
{
if(getProprietaryDataHeader() instanceof MotorolaDataEncryptionHeader)
{
return true;
}
else if(getProprietaryDataHeader() instanceof HyteraDataEncryptionHeader hdeh)
{
return hdeh.getAlgorithm() != EncryptionAlgorithm.NO_ENCRYPTION;
}
}
return false;
}
public int getTimeslot() public int getTimeslot()
{ {
return mTimeslot; return mTimeslot;

View File

@@ -25,14 +25,19 @@ import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock; import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage; import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader; import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType; import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType;
import io.github.dsheirer.module.decode.dmr.message.type.DataPacketFormat;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint;
import io.github.dsheirer.module.decode.ip.DefinedShortDataPacket;
import io.github.dsheirer.module.decode.ip.UnknownPacket; import io.github.dsheirer.module.decode.ip.UnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader; import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket; import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket; import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header; import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet; import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
@@ -164,24 +169,46 @@ public class PacketSequenceMessageFactory
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
} }
} }
else if(secondaryHeader instanceof HyteraProprietaryDataHeader) else if(secondaryHeader instanceof HyteraDataEncryptionHeader hdeh)
{ {
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet); ServiceAccessPoint sap = hdeh.getServiceAccessPoint();
if(hyteraTokenHeader.isSMSMessage()) switch(sap)
{ {
return new DMRPacketMessage(packetSequence, new HyteraSmsPacket(hyteraTokenHeader), packet, case SHORT_DATA:
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); return new DMRPacketMessage(packetSequence, new HyteraShortDataPacket(packetSequence, packet), packet,
} packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
else case PROPRIETARY_DATA:
{ HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet, if(hyteraTokenHeader.isSMSMessage())
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); {
return new DMRPacketMessage(packetSequence, new HyteraSmsPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
else
{
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
default:
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
} }
} }
else if(secondaryHeader instanceof MotorolaDataEncryptionHeader &&
packetSequence.getPacketSequenceHeader().getDataPacketFormat() == DataPacketFormat.DEFINED_SHORT_DATA)
{
return createDefinedShortData(packetSequence, packet);
}
else else
{ {
mLog.info("Unknown Proprietary Packet Header Type - creating unknown packet."); if(packetSequence.getProprietaryDataHeader() != null)
{
mLog.info("Unknown Proprietary Packet Header Type - creating unknown packet. Data Packet Format: " +
packetSequence.getPacketSequenceHeader().getDataPacketFormat() + " Proprietary Header: " +
packetSequence.getProprietaryDataHeader().getClass());
}
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet, return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp()); packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
} }
@@ -217,9 +244,19 @@ public class PacketSequenceMessageFactory
*/ */
public static IMessage createDefinedShortData(PacketSequence packetSequence, CorrectedBinaryMessage packet) public static IMessage createDefinedShortData(PacketSequence packetSequence, CorrectedBinaryMessage packet)
{ {
mLog.info("Unknown Short Data Packet Header Type - creating unknown packet."); //Test to see if this is Hytera Radio Registration Service (RRS)
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet, HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
if(hyteraTokenHeader.isRRSMessage())
{
return new DMRPacketMessage(packetSequence, new HyteraRrsPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
else
{
return new DMRPacketMessage(packetSequence, new DefinedShortDataPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
} }
/** /**
@@ -235,7 +272,7 @@ public class PacketSequenceMessageFactory
if(sequence.hasProprietaryDataHeader()) if(sequence.hasProprietaryDataHeader())
{ {
CorrectedBinaryMessage prefix = ((ProprietaryDataHeader)sequence.getProprietaryDataHeader()).getPacketPrefix(); CorrectedBinaryMessage prefix = sequence.getProprietaryDataHeader().getPacketPrefix();
if(prefix != null) if(prefix != null)
{ {

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