mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 17:17:29 -05:00
Compare commits
38 Commits
v0.6.0-bet
...
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0492f19a95 | ||
|
|
098b039154 | ||
|
|
5c6e6059f8 | ||
|
|
72d45543df | ||
|
|
e96fc46315 | ||
|
|
f0cfbb80dd | ||
|
|
a199f03a60 | ||
|
|
526c9609ef | ||
|
|
ec9ceeefb3 | ||
|
|
b6c6505824 | ||
|
|
93c61c48f9 | ||
|
|
9b89e80bb5 | ||
|
|
e0ecaa6e08 | ||
|
|
a7c89b5d1a | ||
|
|
834846d339 | ||
|
|
45c8bb73fa | ||
|
|
d2580f8a99 | ||
|
|
804c2e6359 | ||
|
|
3f2ec612d5 | ||
|
|
8bead998f5 | ||
|
|
2ecdaba183 | ||
|
|
82cb10a03d | ||
|
|
8717ce38eb | ||
|
|
5858a85a50 | ||
|
|
65d16da997 | ||
|
|
b1b5ae5fd2 | ||
|
|
b5ec6d8877 | ||
|
|
8599aebed9 | ||
|
|
2b9e753033 | ||
|
|
dba50744a9 | ||
|
|
490569e023 | ||
|
|
e79f2af759 | ||
|
|
ec7ee1ce91 | ||
|
|
672207c27c | ||
|
|
a1a6b3e988 | ||
|
|
2347135614 | ||
|
|
93438ddd97 | ||
|
|
ae31c41643 |
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -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_20" default="true" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_20_PREVIEW" 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>
|
||||||
10
build.gradle
10
build.gradle
@@ -211,8 +211,8 @@ def jdk_download_base = "https://download.bell-sw.com/java/20.0.1+10/bellsoft-jd
|
|||||||
def jdk_download_suffix = "-full.tar.gz"
|
def jdk_download_suffix = "-full.tar.gz"
|
||||||
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
||||||
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
||||||
def jdk_download_osx_x86_64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
def jdk_download_osx_x86_64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
def jdk_download_osx_aarch64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||||
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,7 +229,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
|||||||
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
|
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
|
||||||
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
||||||
//jdk.accessibility is used with assistive technologies like screen readers
|
//jdk.accessibility is used with assistive technologies like screen readers
|
||||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility')
|
//java.management for JVM resource monitoring
|
||||||
|
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
|
||||||
|
|
||||||
//Use auto-detected modules and 'add' any specified modules.
|
//Use auto-detected modules and 'add' any specified modules.
|
||||||
rt.extension.additive.set(true)
|
rt.extension.additive.set(true)
|
||||||
@@ -340,7 +341,7 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
|||||||
jdkHome = jdkDownload(jdk_download_osx_aarch64)
|
jdkHome = jdkDownload(jdk_download_osx_aarch64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Linux x86-64
|
//OSX x86-64
|
||||||
if(file(jdk_osx_x86_64).exists()) {
|
if(file(jdk_osx_x86_64).exists()) {
|
||||||
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
|
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
|
||||||
}
|
}
|
||||||
@@ -352,4 +353,3 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
|||||||
|
|
||||||
configure(rt, jvmArgsLinux);
|
configure(rt, jvmArgsLinux);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,4 @@
|
|||||||
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
||||||
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
||||||
|
|
||||||
projectVersion=0.6.0-beta-5
|
projectVersion=0.6.0
|
||||||
@@ -30,11 +30,13 @@ import io.github.dsheirer.alias.id.legacy.mobileID.Min;
|
|||||||
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
||||||
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.P25FullyQualifiedRadio;
|
||||||
import io.github.dsheirer.alias.id.radio.Radio;
|
import io.github.dsheirer.alias.id.radio.Radio;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||||
import io.github.dsheirer.alias.id.record.Record;
|
import io.github.dsheirer.alias.id.record.Record;
|
||||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||||
|
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||||
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;
|
||||||
@@ -77,6 +79,18 @@ public class AliasFactory
|
|||||||
Min copyMin = new Min();
|
Min copyMin = new Min();
|
||||||
copyMin.setMin(originalMin.getMin());
|
copyMin.setMin(originalMin.getMin());
|
||||||
return copyMin;
|
return copyMin;
|
||||||
|
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||||
|
P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id;
|
||||||
|
P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(),
|
||||||
|
originalP25.getSystem(), originalP25.getValue());
|
||||||
|
copyP25.setOverlap(originalP25.overlapProperty().get());
|
||||||
|
return copyP25;
|
||||||
|
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||||
|
P25FullyQualifiedTalkgroup originalFqt = (P25FullyQualifiedTalkgroup) id;
|
||||||
|
P25FullyQualifiedTalkgroup copyFqt = new P25FullyQualifiedTalkgroup(originalFqt.getWacn(),
|
||||||
|
originalFqt.getSystem(), originalFqt.getValue());
|
||||||
|
copyFqt.setOverlap(originalFqt.overlapProperty().get());
|
||||||
|
return copyFqt;
|
||||||
case PRIORITY:
|
case PRIORITY:
|
||||||
Priority originalPriority = (Priority)id;
|
Priority originalPriority = (Priority)id;
|
||||||
Priority copyPriority = new Priority();
|
Priority copyPriority = new Priority();
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
|||||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
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.P25FullyQualifiedRadio;
|
||||||
import io.github.dsheirer.alias.id.radio.Radio;
|
import io.github.dsheirer.alias.id.radio.Radio;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||||
|
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||||
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;
|
||||||
@@ -37,9 +39,11 @@ 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;
|
||||||
|
import io.github.dsheirer.identifier.radio.FullyQualifiedRadioIdentifier;
|
||||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||||
import io.github.dsheirer.identifier.status.UnitStatusIdentifier;
|
import io.github.dsheirer.identifier.status.UnitStatusIdentifier;
|
||||||
import io.github.dsheirer.identifier.status.UserStatusIdentifier;
|
import io.github.dsheirer.identifier.status.UserStatusIdentifier;
|
||||||
|
import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier;
|
||||||
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;
|
||||||
@@ -155,6 +159,32 @@ public class AliasList
|
|||||||
|
|
||||||
talkgroupRangeAliasList.add(talkgroupRange, alias);
|
talkgroupRangeAliasList.add(talkgroupRange, alias);
|
||||||
break;
|
break;
|
||||||
|
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||||
|
P25FullyQualifiedRadio qualifiedRadio = (P25FullyQualifiedRadio) id;
|
||||||
|
|
||||||
|
RadioAliasList p25RadioAliasList = mRadioProtocolMap.get(qualifiedRadio.getProtocol());
|
||||||
|
|
||||||
|
if(p25RadioAliasList == null)
|
||||||
|
{
|
||||||
|
p25RadioAliasList = new RadioAliasList();
|
||||||
|
mRadioProtocolMap.put(qualifiedRadio.getProtocol(), p25RadioAliasList);
|
||||||
|
}
|
||||||
|
|
||||||
|
p25RadioAliasList.add(qualifiedRadio, alias);
|
||||||
|
break;
|
||||||
|
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||||
|
P25FullyQualifiedTalkgroup qualifiedTalkgroup = (P25FullyQualifiedTalkgroup) id;
|
||||||
|
|
||||||
|
TalkgroupAliasList p25TalkgroupAliasList = mTalkgroupProtocolMap.get(qualifiedTalkgroup.getProtocol());
|
||||||
|
|
||||||
|
if(p25TalkgroupAliasList == null)
|
||||||
|
{
|
||||||
|
p25TalkgroupAliasList = new TalkgroupAliasList();
|
||||||
|
mTalkgroupProtocolMap.put(qualifiedTalkgroup.getProtocol(), p25TalkgroupAliasList);
|
||||||
|
}
|
||||||
|
|
||||||
|
p25TalkgroupAliasList.add(qualifiedTalkgroup, alias);
|
||||||
|
break;
|
||||||
case RADIO_ID:
|
case RADIO_ID:
|
||||||
Radio radio = (Radio)id;
|
Radio radio = (Radio)id;
|
||||||
|
|
||||||
@@ -444,7 +474,7 @@ public class AliasList
|
|||||||
return toList(radioAliasList.getAlias(radio));
|
return toList(radioAliasList.getAlias(radio));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DCS:
|
case ESN:
|
||||||
if(identifier instanceof ESNIdentifier)
|
if(identifier instanceof ESNIdentifier)
|
||||||
{
|
{
|
||||||
return toList(getESNAlias(((ESNIdentifier)identifier).getValue()));
|
return toList(getESNAlias(((ESNIdentifier)identifier).getValue()));
|
||||||
@@ -623,6 +653,7 @@ public class AliasList
|
|||||||
*/
|
*/
|
||||||
public class TalkgroupAliasList
|
public class TalkgroupAliasList
|
||||||
{
|
{
|
||||||
|
private Map<String,Alias> mFullyQualifiedTalkgroupAliasMap = new HashMap<>();
|
||||||
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
|
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
|
||||||
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
|
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
|
||||||
|
|
||||||
@@ -632,6 +663,11 @@ public class AliasList
|
|||||||
|
|
||||||
public Alias getAlias(TalkgroupIdentifier identifier)
|
public Alias getAlias(TalkgroupIdentifier identifier)
|
||||||
{
|
{
|
||||||
|
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
|
||||||
|
{
|
||||||
|
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
|
||||||
|
}
|
||||||
|
|
||||||
int value = identifier.getValue();
|
int value = identifier.getValue();
|
||||||
|
|
||||||
Alias mapValue = mTalkgroupAliasMap.get(value);
|
Alias mapValue = mTalkgroupAliasMap.get(value);
|
||||||
@@ -653,26 +689,55 @@ public class AliasList
|
|||||||
|
|
||||||
public void add(Talkgroup talkgroup, Alias alias)
|
public void add(Talkgroup talkgroup, Alias alias)
|
||||||
{
|
{
|
||||||
//Detect talkgroup collisions and set overlap flag for both
|
if(talkgroup instanceof P25FullyQualifiedTalkgroup fqt)
|
||||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
|
||||||
{
|
{
|
||||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
//Detect collisions
|
||||||
|
if(mFullyQualifiedTalkgroupAliasMap.containsKey(fqt.getHashKey()))
|
||||||
if(!existing.equals(alias))
|
|
||||||
{
|
{
|
||||||
talkgroup.setOverlap(true);
|
Alias existing = mFullyQualifiedTalkgroupAliasMap.get(fqt.getHashKey());
|
||||||
|
|
||||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
if(!existing.equals(alias))
|
||||||
{
|
{
|
||||||
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
|
fqt.setOverlap(true);
|
||||||
|
|
||||||
|
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||||
{
|
{
|
||||||
aliasID.setOverlap(true);
|
if(aliasID instanceof P25FullyQualifiedTalkgroup existingFqt &&
|
||||||
|
existingFqt.getHashKey().contentEquals(fqt.getHashKey()))
|
||||||
|
{
|
||||||
|
aliasID.setOverlap(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mFullyQualifiedTalkgroupAliasMap.put(fqt.getHashKey(), alias);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Detect talkgroup collisions and set overlap flag for both
|
||||||
|
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||||
|
{
|
||||||
|
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||||
|
|
||||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
if(!existing.equals(alias))
|
||||||
|
{
|
||||||
|
talkgroup.setOverlap(true);
|
||||||
|
|
||||||
|
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||||
|
{
|
||||||
|
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
|
||||||
|
{
|
||||||
|
aliasID.setOverlap(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(TalkgroupRange talkgroupRange, Alias alias)
|
public void add(TalkgroupRange talkgroupRange, Alias alias)
|
||||||
@@ -705,6 +770,7 @@ public class AliasList
|
|||||||
*/
|
*/
|
||||||
public class RadioAliasList
|
public class RadioAliasList
|
||||||
{
|
{
|
||||||
|
private Map<String,Alias> mFullyQualifiedRadioAliasMap = new HashMap<>();
|
||||||
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
|
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
|
||||||
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
|
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
|
||||||
|
|
||||||
@@ -714,6 +780,11 @@ public class AliasList
|
|||||||
|
|
||||||
public Alias getAlias(RadioIdentifier identifier)
|
public Alias getAlias(RadioIdentifier identifier)
|
||||||
{
|
{
|
||||||
|
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
|
||||||
|
{
|
||||||
|
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
int value = identifier.getValue();
|
int value = identifier.getValue();
|
||||||
|
|
||||||
Alias mapValue = mRadioAliasMap.get(value);
|
Alias mapValue = mRadioAliasMap.get(value);
|
||||||
@@ -735,26 +806,55 @@ public class AliasList
|
|||||||
|
|
||||||
public void add(Radio radio, Alias alias)
|
public void add(Radio radio, Alias alias)
|
||||||
{
|
{
|
||||||
//Detect collisions
|
if(radio instanceof P25FullyQualifiedRadio fqr)
|
||||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
|
||||||
{
|
{
|
||||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
//Detect collisions
|
||||||
|
if(mFullyQualifiedRadioAliasMap.containsKey(fqr.getHashKey()))
|
||||||
if(!existing.equals(alias))
|
|
||||||
{
|
{
|
||||||
radio.setOverlap(true);
|
Alias existing = mFullyQualifiedRadioAliasMap.get(fqr.getHashKey());
|
||||||
|
|
||||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
if(!existing.equals(alias))
|
||||||
{
|
{
|
||||||
if(aliasID instanceof Radio && ((Radio)aliasID).getValue() == radio.getValue())
|
fqr.setOverlap(true);
|
||||||
|
|
||||||
|
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||||
{
|
{
|
||||||
aliasID.setOverlap(true);
|
if(aliasID instanceof P25FullyQualifiedRadio existingFqr &&
|
||||||
|
existingFqr.getHashKey().contentEquals(fqr.getHashKey()))
|
||||||
|
{
|
||||||
|
aliasID.setOverlap(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mFullyQualifiedRadioAliasMap.put(fqr.getHashKey(), alias);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Detect collisions
|
||||||
|
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||||
|
{
|
||||||
|
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||||
|
|
||||||
mRadioAliasMap.put(radio.getValue(), alias);
|
if(!existing.equals(alias))
|
||||||
|
{
|
||||||
|
radio.setOverlap(true);
|
||||||
|
|
||||||
|
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||||
|
{
|
||||||
|
if(aliasID instanceof Radio existingRadio && (existingRadio.getValue() == radio.getValue()))
|
||||||
|
{
|
||||||
|
aliasID.setOverlap(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mRadioAliasMap.put(radio.getValue(), alias);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(RadioRange radioRange, Alias alias)
|
public void add(RadioRange radioRange, Alias alias)
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
|
|||||||
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
|
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.P25FullyQualifiedRadio;
|
||||||
import io.github.dsheirer.alias.id.radio.Radio;
|
import io.github.dsheirer.alias.id.radio.Radio;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||||
import io.github.dsheirer.alias.id.record.Record;
|
import io.github.dsheirer.alias.id.record.Record;
|
||||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||||
|
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||||
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;
|
||||||
@@ -61,6 +63,8 @@ import javafx.util.Callback;
|
|||||||
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
||||||
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
||||||
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
|
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
|
||||||
|
@JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"),
|
||||||
|
@JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"),
|
||||||
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
|
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
|
||||||
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
|
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
|
||||||
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ public enum AliasIDType
|
|||||||
LOJACK("LoJack"),
|
LOJACK("LoJack"),
|
||||||
LTR_NET_UID("LTR-Net UID"),
|
LTR_NET_UID("LTR-Net UID"),
|
||||||
MIN("Passport MIN"),
|
MIN("Passport MIN"),
|
||||||
|
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
|
||||||
|
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
|
||||||
PRIORITY("Audio Priority"),
|
PRIORITY("Audio Priority"),
|
||||||
RADIO_ID("Radio ID"),
|
RADIO_ID("Radio ID"),
|
||||||
RADIO_ID_RANGE("Radio ID Range"),
|
RADIO_ID_RANGE("Radio ID Range"),
|
||||||
@@ -56,7 +58,7 @@ public enum AliasIDType
|
|||||||
|
|
||||||
//Values used by the View-By alias editor
|
//Values used by the View-By alias editor
|
||||||
public static EnumSet<AliasIDType> VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE,
|
public static EnumSet<AliasIDType> VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE,
|
||||||
UNIT_STATUS, STATUS, TONES);
|
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
|
||||||
|
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.radio;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||||
|
import io.github.dsheirer.alias.id.AliasIDType;
|
||||||
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully qualified radio. Note: this is only for P25.
|
||||||
|
*/
|
||||||
|
public class P25FullyQualifiedRadio extends Radio
|
||||||
|
{
|
||||||
|
private int mWacn;
|
||||||
|
private int mSystem;
|
||||||
|
|
||||||
|
public P25FullyQualifiedRadio()
|
||||||
|
{
|
||||||
|
//No-arg JAXB constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param wacn for the radio
|
||||||
|
* @param system for the radio
|
||||||
|
* @param id for the radio
|
||||||
|
*/
|
||||||
|
public P25FullyQualifiedRadio(int wacn, int system, int id)
|
||||||
|
{
|
||||||
|
super(Protocol.APCO25, id);
|
||||||
|
mWacn = wacn;
|
||||||
|
mSystem = system;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AliasIDType getType()
|
||||||
|
{
|
||||||
|
return AliasIDType.P25_FULLY_QUALIFIED_RADIO_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WACN for this radio
|
||||||
|
* @return wacn
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
|
||||||
|
public int getWacn()
|
||||||
|
{
|
||||||
|
return mWacn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the WACN for this radio
|
||||||
|
* @param wacn for this radio
|
||||||
|
*/
|
||||||
|
public void setWacn(int wacn)
|
||||||
|
{
|
||||||
|
mWacn = wacn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System for this radio
|
||||||
|
* @return system
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "system")
|
||||||
|
public int getSystem()
|
||||||
|
{
|
||||||
|
return mSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the system for this radio
|
||||||
|
* @param system for this radio
|
||||||
|
*/
|
||||||
|
public void setSystem(int system)
|
||||||
|
{
|
||||||
|
mSystem = system;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.append("Fully Qualified Radio ID:").append(getWacn());
|
||||||
|
sb.append(".").append(getSystem());
|
||||||
|
sb.append(".").append(getValue());
|
||||||
|
sb.append(" Protocol:").append((getProtocol()));
|
||||||
|
|
||||||
|
if(!isValid())
|
||||||
|
{
|
||||||
|
sb.append(" **NOT VALID**");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hashable string key for use in a lookup map
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
public String getHashKey()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(getWacn());
|
||||||
|
sb.append(".").append(getSystem());
|
||||||
|
sb.append(".").append(getValue());
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.talkgroup;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||||
|
import io.github.dsheirer.alias.id.AliasIDType;
|
||||||
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully qualified talkgroup. Note: this is only for P25.
|
||||||
|
*/
|
||||||
|
public class P25FullyQualifiedTalkgroup extends Talkgroup
|
||||||
|
{
|
||||||
|
private int mWacn;
|
||||||
|
private int mSystem;
|
||||||
|
|
||||||
|
public P25FullyQualifiedTalkgroup()
|
||||||
|
{
|
||||||
|
//No-arg JAXB constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param wacn for the radio
|
||||||
|
* @param system for the radio
|
||||||
|
* @param talkgroup value
|
||||||
|
*/
|
||||||
|
public P25FullyQualifiedTalkgroup(int wacn, int system, int talkgroup)
|
||||||
|
{
|
||||||
|
super(Protocol.APCO25, talkgroup);
|
||||||
|
mWacn = wacn;
|
||||||
|
mSystem = system;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AliasIDType getType()
|
||||||
|
{
|
||||||
|
return AliasIDType.P25_FULLY_QUALIFIED_TALKGROUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WACN for this radio
|
||||||
|
* @return wacn
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
|
||||||
|
public int getWacn()
|
||||||
|
{
|
||||||
|
return mWacn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the WACN for this radio
|
||||||
|
* @param wacn for this radio
|
||||||
|
*/
|
||||||
|
public void setWacn(int wacn)
|
||||||
|
{
|
||||||
|
mWacn = wacn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System for this radio
|
||||||
|
* @return system
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "system")
|
||||||
|
public int getSystem()
|
||||||
|
{
|
||||||
|
return mSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the system for this radio
|
||||||
|
* @param system for this radio
|
||||||
|
*/
|
||||||
|
public void setSystem(int system)
|
||||||
|
{
|
||||||
|
mSystem = system;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.append("Fully Qualified Radio ID:").append(getWacn());
|
||||||
|
sb.append(".").append(getSystem());
|
||||||
|
sb.append(".").append(getValue());
|
||||||
|
sb.append(" Protocol:").append((getProtocol()));
|
||||||
|
|
||||||
|
if(!isValid())
|
||||||
|
{
|
||||||
|
sb.append(" **NOT VALID**");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hashable string key for use in a lookup map
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@JsonIgnore
|
||||||
|
public String getHashKey()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(getWacn());
|
||||||
|
sb.append(".").append(getSystem());
|
||||||
|
sb.append(".").append(getValue());
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
|||||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||||
|
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||||
@@ -48,6 +49,7 @@ import java.net.SocketAddress;
|
|||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
||||||
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
||||||
|
@JsonSubTypes.Type(value = OpenMHzConfiguration.class, name="OpenMHzConfiguration"),
|
||||||
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
||||||
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
||||||
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfigura
|
|||||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
|
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
|
||||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
|
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
|
||||||
|
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||||
|
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||||
|
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzFeedConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||||
@@ -67,6 +70,9 @@ public class BroadcastFactory
|
|||||||
case RDIOSCANNER_CALL:
|
case RDIOSCANNER_CALL:
|
||||||
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
||||||
inputAudioFormat, mp3Setting, aliasModel);
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
|
case OPENMHZ:
|
||||||
|
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
|
||||||
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
case BROADCASTIFY:
|
case BROADCASTIFY:
|
||||||
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
||||||
inputAudioFormat, mp3Setting, aliasModel);
|
inputAudioFormat, mp3Setting, aliasModel);
|
||||||
@@ -109,6 +115,8 @@ public class BroadcastFactory
|
|||||||
return new RdioScannerConfiguration(format);
|
return new RdioScannerConfiguration(format);
|
||||||
case BROADCASTIFY:
|
case BROADCASTIFY:
|
||||||
return new BroadcastifyFeedConfiguration(format);
|
return new BroadcastifyFeedConfiguration(format);
|
||||||
|
case OPENMHZ:
|
||||||
|
return new OpenMHzConfiguration(format);
|
||||||
case ICECAST_HTTP:
|
case ICECAST_HTTP:
|
||||||
return new IcecastHTTPConfiguration(format);
|
return new IcecastHTTPConfiguration(format);
|
||||||
case ICECAST_TCP:
|
case ICECAST_TCP:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public enum BroadcastServerType
|
|||||||
|
|
||||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
||||||
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
||||||
|
OPENMHZ("OpenMHz", "images/openmhz.png"),
|
||||||
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP headers used for posting to Rdio Scanner API
|
||||||
|
*/
|
||||||
|
public enum FormField
|
||||||
|
{
|
||||||
|
CALL("call"),
|
||||||
|
FREQ("freq"),
|
||||||
|
START_TIME("start_time"),
|
||||||
|
STOP_TIME("stop_time"),
|
||||||
|
CALL_LENGTH("call_length"),
|
||||||
|
TALKGROUP_NUM("talkgroup_num"),
|
||||||
|
EMERGENCY("emergency"),
|
||||||
|
API_KEY("api_key"),
|
||||||
|
SOURCE_LIST("source_list"),
|
||||||
|
FREQ_LIST("freq_list"),
|
||||||
|
PATCHES("patches");
|
||||||
|
|
||||||
|
private String mHeader;
|
||||||
|
|
||||||
|
FormField(String header)
|
||||||
|
{
|
||||||
|
mHeader = header;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHeader()
|
||||||
|
{
|
||||||
|
return mHeader;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,634 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2022 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.audio.broadcast.openmhz;
|
||||||
|
|
||||||
|
import com.google.common.net.HttpHeaders;
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
import io.github.dsheirer.alias.Alias;
|
||||||
|
import io.github.dsheirer.alias.AliasList;
|
||||||
|
import io.github.dsheirer.alias.AliasModel;
|
||||||
|
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||||
|
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||||
|
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||||
|
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||||
|
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||||
|
import io.github.dsheirer.identifier.Form;
|
||||||
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
|
import io.github.dsheirer.identifier.IdentifierClass;
|
||||||
|
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||||
|
import io.github.dsheirer.identifier.Role;
|
||||||
|
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||||
|
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||||
|
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||||
|
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||||
|
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||||
|
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.PrintWriter;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.LinkedTransferQueue;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio broadcaster to push completed audio recordings to the OpenMHz call upload API.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class OpenMHzBroadcaster extends AbstractAudioBroadcaster<OpenMHzConfiguration>
|
||||||
|
{
|
||||||
|
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzBroadcaster.class);
|
||||||
|
|
||||||
|
private static final String ENCODING_TYPE_MP3 = "mp3";
|
||||||
|
private static final String MULTIPART_TYPE = "multipart";
|
||||||
|
private static final String DEFAULT_SUBTYPE = "form-data";
|
||||||
|
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
|
||||||
|
private static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded";
|
||||||
|
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
|
||||||
|
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
|
||||||
|
private HttpClient mHttpClient = HttpClient.newBuilder()
|
||||||
|
.version(HttpClient.Version.HTTP_2)
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
|
.connectTimeout(Duration.ofSeconds(20))
|
||||||
|
.build();
|
||||||
|
private long mLastConnectionAttempt;
|
||||||
|
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||||
|
private AliasModel mAliasModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance of the broadcaster
|
||||||
|
* @param config to use
|
||||||
|
* @param aliasModel for access to aliases
|
||||||
|
*/
|
||||||
|
public OpenMHzBroadcaster(OpenMHzConfiguration config, InputAudioFormat inputAudioFormat,
|
||||||
|
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
mAliasModel = aliasModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the audio recording processor thread
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void start()
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.CONNECTING);
|
||||||
|
String response = testConnection(getBroadcastConfiguration());
|
||||||
|
mLastConnectionAttempt = System.currentTimeMillis();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenMHz API does not currently expose a test method.
|
||||||
|
* TODO: FIX THIS
|
||||||
|
*/
|
||||||
|
if(response == "OK")// && response.toLowerCase().startsWith("<head><title>502 bad gateway</title></head>"))
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.CONNECTED);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mLog.error("Error connecting to OpenMHz server on startup [" + response + "]");
|
||||||
|
setBroadcastState(BroadcastState.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mAudioRecordingProcessorFuture == null)
|
||||||
|
{
|
||||||
|
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
|
||||||
|
0, 500, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the audio recording processor thread
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void stop()
|
||||||
|
{
|
||||||
|
if(mAudioRecordingProcessorFuture != null)
|
||||||
|
{
|
||||||
|
mAudioRecordingProcessorFuture.cancel(true);
|
||||||
|
mAudioRecordingProcessorFuture = null;
|
||||||
|
dispose();
|
||||||
|
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares for disposal
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void dispose()
|
||||||
|
{
|
||||||
|
AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||||
|
|
||||||
|
while(audioRecording != null)
|
||||||
|
{
|
||||||
|
audioRecording.removePendingReplay();
|
||||||
|
audioRecording = mAudioRecordingQueue.poll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
|
||||||
|
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
|
||||||
|
* the audio processor thread will persistently invoke this method to attempt a reconnect.
|
||||||
|
*
|
||||||
|
* OpenMHz does not have a test API endpoint, so we look for the incomplete call response.
|
||||||
|
*/
|
||||||
|
private boolean connected()
|
||||||
|
{
|
||||||
|
if(getBroadcastState() != BroadcastState.CONNECTED &&
|
||||||
|
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.CONNECTING);
|
||||||
|
|
||||||
|
String response = testConnection(getBroadcastConfiguration());
|
||||||
|
mLastConnectionAttempt = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if(response != null && response == "200")
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.CONNECTED);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBroadcastState() == BroadcastState.CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAudioQueueSize()
|
||||||
|
{
|
||||||
|
return mAudioRecordingQueue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receive(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
mAudioRecordingQueue.offer(audioRecording);
|
||||||
|
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
|
||||||
|
* exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
|
||||||
|
* deleted to ensure that the in-memory queue size doesn't blow up.
|
||||||
|
* @param audioRecording to test
|
||||||
|
* @return true if the recording is valid
|
||||||
|
*/
|
||||||
|
private boolean isValid(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
|
||||||
|
getBroadcastConfiguration().getMaximumRecordingAge();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes any enqueued audio recordings. This method employs asynchronous
|
||||||
|
* interaction with the server, so multiple audio recording uploads can occur simultaneously.
|
||||||
|
*/
|
||||||
|
private void processRecordingQueue()
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
while(connected() && !mAudioRecordingQueue.isEmpty())
|
||||||
|
{
|
||||||
|
final AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||||
|
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||||
|
|
||||||
|
if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
|
||||||
|
{
|
||||||
|
int durationSeconds = (int)(audioRecording.getRecordingLength() / 1E3f);
|
||||||
|
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
|
||||||
|
String talkgroup = getTo(audioRecording);
|
||||||
|
String radioId = getFrom(audioRecording);
|
||||||
|
Long frequency = getFrequency(audioRecording);
|
||||||
|
String patches = getPatches(audioRecording);
|
||||||
|
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||||
|
String talkgroupGroup = getTalkgroupGroup(audioRecording);
|
||||||
|
String systemLabel = getSystemLabel(audioRecording);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] audioBytes = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
audioBytes = Files.readAllBytes(audioRecording.getPath());
|
||||||
|
}
|
||||||
|
catch(IOException e)
|
||||||
|
{
|
||||||
|
mLog.error("OpenMHz - audio recording file not found - ignoring upload");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(audioBytes != null)
|
||||||
|
{
|
||||||
|
String uri = getBroadcastConfiguration().getHost() + "/" + getBroadcastConfiguration().getSystemName() + "/upload";
|
||||||
|
|
||||||
|
OpenMHzBuilder bodyBuilder = new OpenMHzBuilder();
|
||||||
|
bodyBuilder
|
||||||
|
.addFile(audioBytes)
|
||||||
|
.addPart(FormField.FREQ, frequency)
|
||||||
|
.addPart(FormField.START_TIME, timestampSeconds)
|
||||||
|
.addPart(FormField.STOP_TIME, timestampSeconds)
|
||||||
|
.addPart(FormField.CALL_LENGTH, durationSeconds)
|
||||||
|
.addPart(FormField.TALKGROUP_NUM, talkgroup)
|
||||||
|
.addPart(FormField.EMERGENCY, 0)
|
||||||
|
.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
|
||||||
|
// For future use if OpenMHz supports patch information
|
||||||
|
//.addPart(FormField.PATCHES, patches)
|
||||||
|
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
|
||||||
|
|
||||||
|
HttpRequest fileRequest = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(uri))
|
||||||
|
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||||
|
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||||
|
.POST(bodyBuilder.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
|
||||||
|
.whenComplete((fileResponse, throwable1) -> {
|
||||||
|
if(throwable1 != null || fileResponse.statusCode() != 200)
|
||||||
|
{
|
||||||
|
if(throwable1 instanceof IOException || throwable1 instanceof CompletionException)
|
||||||
|
{
|
||||||
|
//We get socket reset exceptions occasionally when the remote server doesn't
|
||||||
|
//fully read our request and immediately responds.
|
||||||
|
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||||
|
mLog.error("OpenMHz API file upload fail [" +
|
||||||
|
fileResponse.statusCode() + "] response [" +
|
||||||
|
fileResponse.body() + "]");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||||
|
mLog.error("OpenMHz API file upload fail [" +
|
||||||
|
fileResponse.statusCode() + "] response [" +
|
||||||
|
fileResponse.body() + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementErrorAudioCount();
|
||||||
|
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||||
|
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
if(fileResponse.statusCode() == 200)
|
||||||
|
{
|
||||||
|
incrementStreamedAudioCount();
|
||||||
|
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||||
|
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
|
||||||
|
audioRecording.removePendingReplay();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||||
|
mLog.error("OpenMHz API file upload fail [" +
|
||||||
|
fileResponse.statusCode() + "] response [" +
|
||||||
|
fileResponse.body() + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Register an error for the file not found exception
|
||||||
|
mLog.error("OpenMHz API - upload file not found [" +
|
||||||
|
audioRecording.getPath().toString() + "]");
|
||||||
|
incrementErrorAudioCount();
|
||||||
|
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||||
|
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||||
|
audioRecording.removePendingReplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Unknown Error", e);
|
||||||
|
setBroadcastState(BroadcastState.ERROR);
|
||||||
|
incrementErrorAudioCount();
|
||||||
|
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||||
|
audioRecording.removePendingReplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//If we're not connected and there are recordings in the queue, check the recording at the head of the queue
|
||||||
|
// and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
|
||||||
|
AudioRecording audioRecording = mAudioRecordingQueue.peek();
|
||||||
|
|
||||||
|
while(audioRecording != null)
|
||||||
|
{
|
||||||
|
if(isValid(audioRecording))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
|
||||||
|
mAudioRecordingQueue.poll();
|
||||||
|
audioRecording.removePendingReplay();
|
||||||
|
incrementAgedOffAudioCount();
|
||||||
|
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
|
||||||
|
audioRecording = mAudioRecordingQueue.peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a frequency value from the audio recording identifier collection.
|
||||||
|
*/
|
||||||
|
private static Long getFrequency(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
|
||||||
|
Form.CHANNEL_FREQUENCY, Role.ANY);
|
||||||
|
|
||||||
|
if(identifier instanceof ConfigurationLongIdentifier)
|
||||||
|
{
|
||||||
|
Long value = ((ConfigurationLongIdentifier)identifier).getValue();
|
||||||
|
|
||||||
|
if(value != null)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Long.valueOf(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a formatted string with the FROM identifier or uses a default of zero(0)
|
||||||
|
*/
|
||||||
|
private static String getFrom(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||||
|
{
|
||||||
|
if(identifier instanceof RadioIdentifier)
|
||||||
|
{
|
||||||
|
return ((RadioIdentifier)identifier).getValue().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static String getTo(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||||
|
|
||||||
|
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||||
|
{
|
||||||
|
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
|
||||||
|
}
|
||||||
|
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
|
||||||
|
{
|
||||||
|
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
|
||||||
|
talkgroupIdentifier.getProtocol()));
|
||||||
|
}
|
||||||
|
else if(identifier instanceof RadioIdentifier radioIdentifier)
|
||||||
|
{
|
||||||
|
return radioIdentifier.getValue().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a formatted string with the Talkgroup Label from the Audio Recording Alias
|
||||||
|
* If this is a PatchGroup we return only the first label as the primary talkgroup label.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private String getTalkgroupLabel(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
|
||||||
|
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||||
|
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if(identifier != null)
|
||||||
|
{
|
||||||
|
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||||
|
if(!aliases.isEmpty())
|
||||||
|
{
|
||||||
|
sb.append(aliases.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a formatted string with the Talkgroup Group from the Audio Recording Alias
|
||||||
|
* If this is a PatchGroup we return only the first group as the primary talkgroup group.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private String getTalkgroupGroup(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
|
||||||
|
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||||
|
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if(identifier != null)
|
||||||
|
{
|
||||||
|
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||||
|
if(!aliases.isEmpty())
|
||||||
|
{
|
||||||
|
sb.append(aliases.get(0).getGroup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a formatted string with the System Label from the Audio Recording Alias
|
||||||
|
* If this is a PatchGroup we return only the first sytem as the primary talkgroup system.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private String getSystemLabel(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
List<Identifier> systems = audioRecording.getIdentifierCollection().getIdentifiers(Form.SYSTEM);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if(!systems.isEmpty())
|
||||||
|
{
|
||||||
|
sb.append(systems.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a formatted string with the patched talkgroups
|
||||||
|
*/
|
||||||
|
public static String getPatches(AudioRecording audioRecording)
|
||||||
|
{
|
||||||
|
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||||
|
|
||||||
|
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||||
|
{
|
||||||
|
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("[");
|
||||||
|
|
||||||
|
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
||||||
|
|
||||||
|
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||||
|
{
|
||||||
|
sb.append(",").append(patched.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
|
||||||
|
{
|
||||||
|
sb.append(",").append(patched.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("]");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests both the connection and configuration against the OpenMHz Call API service
|
||||||
|
* @param configuration containing API key and system id
|
||||||
|
* @return error string or null if test is successful
|
||||||
|
*/
|
||||||
|
public static String testConnection(OpenMHzConfiguration configuration)
|
||||||
|
{
|
||||||
|
String uri = configuration.getHost() + "/" + configuration.getSystemName() + "/authorize";
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClient.newBuilder()
|
||||||
|
.version(HttpClient.Version.HTTP_1_1)
|
||||||
|
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||||
|
.connectTimeout(Duration.ofSeconds(20))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(uri))
|
||||||
|
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_FORM_URLENCODED)
|
||||||
|
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString("api_key="+configuration.getApiKey()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = httpClient.send(request, responseHandler);
|
||||||
|
String responseBody = response.body();
|
||||||
|
if (response.statusCode() == 200)
|
||||||
|
{
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
else if(response.statusCode() == 403)
|
||||||
|
{
|
||||||
|
return "Invalid API Key";
|
||||||
|
}
|
||||||
|
else if(response.statusCode() == 500)
|
||||||
|
{
|
||||||
|
return "Invalid System Name";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "No Response";
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
return e.getLocalizedMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AudioRecordingProcessor implements Runnable
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
processRecordingQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
mLog.debug("Starting ...");
|
||||||
|
|
||||||
|
OpenMHzConfiguration config = new OpenMHzConfiguration();
|
||||||
|
config.setHost("https://api.OpenMHz.com/call-upload-dev");
|
||||||
|
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
|
||||||
|
config.setSystemName("systemx");
|
||||||
|
|
||||||
|
String response = testConnection(config);
|
||||||
|
mLog.error("Response: " + response);
|
||||||
|
|
||||||
|
if(response == "OK")
|
||||||
|
{
|
||||||
|
mLog.debug("Test Successful!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(response.contains("Invalid API Key"))
|
||||||
|
{
|
||||||
|
mLog.error("Invalid API Key");
|
||||||
|
}
|
||||||
|
else if(response.contains("Invalid System Name"))
|
||||||
|
{
|
||||||
|
mLog.error("Invalid System Name");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mLog.debug("Response: " + response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mLog.debug("Finished!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||||
|
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.lang.*;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder for an HTTP body publisher to produce a OpenMHz call event
|
||||||
|
*/
|
||||||
|
public class OpenMHzBuilder
|
||||||
|
{
|
||||||
|
private static final String DASH_DASH = "--";
|
||||||
|
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
||||||
|
private List<Part> mParts = new ArrayList<>();
|
||||||
|
private byte[] audioBytes = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*/
|
||||||
|
public OpenMHzBuilder()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access the static multi-part boundary string
|
||||||
|
*/
|
||||||
|
public String getBoundary()
|
||||||
|
{
|
||||||
|
return BOUNDARY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a Audio bytes part to the call
|
||||||
|
*/
|
||||||
|
public OpenMHzBuilder addFile(byte[] value)
|
||||||
|
{
|
||||||
|
audioBytes = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a string part to the call
|
||||||
|
*/
|
||||||
|
public OpenMHzBuilder addPart(FormField key, String value)
|
||||||
|
{
|
||||||
|
if(key != null && value != null)
|
||||||
|
{
|
||||||
|
mParts.add(new Part(key.getHeader(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a number part to the call
|
||||||
|
*/
|
||||||
|
public OpenMHzBuilder addPart(FormField key, Number value)
|
||||||
|
{
|
||||||
|
if(key != null && value != null)
|
||||||
|
{
|
||||||
|
mParts.add(new Part(key.getHeader(), value.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a form-data item
|
||||||
|
*/
|
||||||
|
private static String formatPart(Part part, String boundary)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||||
|
sb.append("Content-Disposition: form-data; name=\"").append(part.mKey).append("\"\r\n\r\n");
|
||||||
|
sb.append(part.getValue()).append("\r\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the audio file item
|
||||||
|
*/
|
||||||
|
private String formatFilePart(String boundary)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(audioBytes == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
StringBuilder sb= new StringBuilder();
|
||||||
|
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||||
|
sb.append("Content-Disposition: form-data; name=\"").append("call").append("\"; filename=\"audio.mp3\"\r\n\r\n");
|
||||||
|
return sb.toString();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the boundary closing item
|
||||||
|
*/
|
||||||
|
private static String getClosingBoundary(String boundary)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(DASH_DASH).append(boundary).append(DASH_DASH).append("\r\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BodyPublisher for accessing the call form data
|
||||||
|
*/
|
||||||
|
public HttpRequest.BodyPublisher build()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
for(Part part: mParts)
|
||||||
|
{
|
||||||
|
sb.append(formatPart(part, BOUNDARY));
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(formatFilePart(BOUNDARY));
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to create a ByteArray consisting of the Sting "parts" and the audio file bytes
|
||||||
|
*/
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
outputStream.write(sb.toString().getBytes());
|
||||||
|
|
||||||
|
if(audioBytes != null)
|
||||||
|
{
|
||||||
|
outputStream.write(audioBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb = new StringBuilder();
|
||||||
|
sb.append("\r\n");
|
||||||
|
sb.append(getClosingBoundary(BOUNDARY));
|
||||||
|
|
||||||
|
outputStream.write(sb.toString().getBytes());
|
||||||
|
}
|
||||||
|
catch(IOException e)
|
||||||
|
{
|
||||||
|
//mLog.error("Rdio Scanner API - unable to create POST reqeust.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpRequest.BodyPublishers.ofByteArray(outputStream.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
for(Part part: mParts)
|
||||||
|
{
|
||||||
|
sb.append(formatPart(part, BOUNDARY));
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(getClosingBoundary(BOUNDARY));
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key:Value pair holder
|
||||||
|
*/
|
||||||
|
public class Part
|
||||||
|
{
|
||||||
|
private String mKey;
|
||||||
|
private String mValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new part
|
||||||
|
* @param key value
|
||||||
|
* @param value item
|
||||||
|
*/
|
||||||
|
public Part(String key, String value)
|
||||||
|
{
|
||||||
|
mKey = key;
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey()
|
||||||
|
{
|
||||||
|
return mKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue()
|
||||||
|
{
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streaming configuration for Rdio Scanner API.
|
||||||
|
*
|
||||||
|
* Note: this API is not a streaming audio service, rather a completed call push service. However, it fits nicely
|
||||||
|
* with the structure of the audio streaming subsystem in sdrtrunk
|
||||||
|
*/
|
||||||
|
public class OpenMHzConfiguration extends BroadcastConfiguration
|
||||||
|
{
|
||||||
|
public static final String PRODUCTION_ENDPOINT = "https://api.openmhz.com";
|
||||||
|
|
||||||
|
private StringProperty mSystemName = new SimpleStringProperty();
|
||||||
|
private StringProperty mApiKey = new SimpleStringProperty();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for faster jackson
|
||||||
|
*/
|
||||||
|
public OpenMHzConfiguration()
|
||||||
|
{
|
||||||
|
this(BroadcastFormat.MP3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public constructor.
|
||||||
|
* @param format to use for audio recording (MP3)
|
||||||
|
*/
|
||||||
|
public OpenMHzConfiguration(BroadcastFormat format)
|
||||||
|
{
|
||||||
|
super(format);
|
||||||
|
|
||||||
|
if(getHost() == null)
|
||||||
|
{
|
||||||
|
setHost(PRODUCTION_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
//The parent class binds this property, so we unbind it and rebind it here
|
||||||
|
mValid.unbind();
|
||||||
|
mValid.bind(Bindings.and(Bindings.and(Bindings.isNotNull(mSystemName), Bindings.isNotNull(mApiKey)),
|
||||||
|
Bindings.isNotNull(mHost)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System ID as a property
|
||||||
|
*/
|
||||||
|
public StringProperty systemIDProperty()
|
||||||
|
{
|
||||||
|
return mSystemName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API key as a property
|
||||||
|
*/
|
||||||
|
public StringProperty apiKeyProperty()
|
||||||
|
{
|
||||||
|
return mApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "api_key")
|
||||||
|
public String getApiKey()
|
||||||
|
{
|
||||||
|
return mApiKey.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the api key
|
||||||
|
* @param apiKey
|
||||||
|
*/
|
||||||
|
public void setApiKey(String apiKey)
|
||||||
|
{
|
||||||
|
mApiKey.setValue(apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System name as provided by OpenMHz.com
|
||||||
|
*/
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "system_name")
|
||||||
|
public String getSystemName()
|
||||||
|
{
|
||||||
|
return mSystemName.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the system name provided by OpenMHz.com
|
||||||
|
*/
|
||||||
|
public void setSystemName(String systemName)
|
||||||
|
{
|
||||||
|
mSystemName.set(systemName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||||
|
@Override
|
||||||
|
public BroadcastServerType getBroadcastServerType()
|
||||||
|
{
|
||||||
|
return BroadcastServerType.OPENMHZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BroadcastConfiguration copyOf()
|
||||||
|
{
|
||||||
|
OpenMHzConfiguration copy = new OpenMHzConfiguration();
|
||||||
|
copy.setSystemName(getSystemName());
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.gui.playlist.streaming;
|
||||||
|
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||||
|
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||||
|
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||||
|
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||||
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
|
import javafx.geometry.HPos;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.controlsfx.control.ToggleSwitch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenMHz calls API configuration editor
|
||||||
|
*/
|
||||||
|
public class OpenMHzEditor extends AbstractBroadcastEditor<OpenMHzConfiguration>
|
||||||
|
{
|
||||||
|
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzEditor.class);
|
||||||
|
private TextField mSystemNameTextField;
|
||||||
|
private IntegerTextField mMaxAgeTextField;
|
||||||
|
private TextField mApiKeyTextField;
|
||||||
|
private TextField mHostTextField;
|
||||||
|
private GridPane mEditorPane;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param playlistManager for accessing the broadcast model
|
||||||
|
*/
|
||||||
|
public OpenMHzEditor(PlaylistManager playlistManager)
|
||||||
|
{
|
||||||
|
super(playlistManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setItem(OpenMHzConfiguration item)
|
||||||
|
{
|
||||||
|
super.setItem(item);
|
||||||
|
|
||||||
|
getSystemNameTextField().setDisable(item == null);
|
||||||
|
getApiKeyTextField().setDisable(item == null);
|
||||||
|
getHostTextField().setDisable(item == null);
|
||||||
|
getMaxAgeTextField().setDisable(item == null);
|
||||||
|
|
||||||
|
if(item != null)
|
||||||
|
{
|
||||||
|
getSystemNameTextField().setText(item.getSystemName());
|
||||||
|
getApiKeyTextField().setText(item.getApiKey());
|
||||||
|
getHostTextField().setText(item.getHost());
|
||||||
|
getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
getSystemNameTextField().setText(null);
|
||||||
|
getApiKeyTextField().setText(null);
|
||||||
|
getHostTextField().setText(null);
|
||||||
|
getMaxAgeTextField().set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiedProperty().set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save()
|
||||||
|
{
|
||||||
|
if(getItem() != null)
|
||||||
|
{
|
||||||
|
getItem().setSystemName(getSystemNameTextField().getText());
|
||||||
|
getItem().setHost(getHostTextField().getText());
|
||||||
|
getItem().setApiKey(getApiKeyTextField().getText());
|
||||||
|
getItem().setMaximumRecordingAge(getMaxAgeTextField().get() * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BroadcastServerType getBroadcastServerType()
|
||||||
|
{
|
||||||
|
return BroadcastServerType.OPENMHZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GridPane getEditorPane()
|
||||||
|
{
|
||||||
|
if(mEditorPane == null)
|
||||||
|
{
|
||||||
|
mEditorPane = new GridPane();
|
||||||
|
mEditorPane.setPadding(new Insets(10, 5, 10,10));
|
||||||
|
mEditorPane.setVgap(10);
|
||||||
|
mEditorPane.setHgap(5);
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
|
||||||
|
Label formatLabel = new Label("Format");
|
||||||
|
GridPane.setHalignment(formatLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(formatLabel, 0, row);
|
||||||
|
mEditorPane.getChildren().add(formatLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getFormatField(), 1, row);
|
||||||
|
mEditorPane.getChildren().add(getFormatField());
|
||||||
|
|
||||||
|
Label enabledLabel = new Label("Enabled");
|
||||||
|
GridPane.setHalignment(enabledLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(enabledLabel, 2, row);
|
||||||
|
mEditorPane.getChildren().add(enabledLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getEnabledSwitch(), 3, row);
|
||||||
|
mEditorPane.getChildren().add(getEnabledSwitch());
|
||||||
|
|
||||||
|
Label systemLabel = new Label("Name");
|
||||||
|
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(systemLabel, 0, ++row);
|
||||||
|
mEditorPane.getChildren().add(systemLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getNameTextField(), 1, row);
|
||||||
|
mEditorPane.getChildren().add(getNameTextField());
|
||||||
|
|
||||||
|
Label apiKeyLabel = new Label("API Key");
|
||||||
|
GridPane.setHalignment(apiKeyLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(apiKeyLabel, 0, ++row);
|
||||||
|
mEditorPane.getChildren().add(apiKeyLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getApiKeyTextField(), 1, row);
|
||||||
|
mEditorPane.getChildren().add(getApiKeyTextField());
|
||||||
|
|
||||||
|
Label systemNameLabel = new Label("System Short Name");
|
||||||
|
GridPane.setHalignment(systemNameLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(systemNameLabel, 0, ++row);
|
||||||
|
mEditorPane.getChildren().add(systemNameLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getSystemNameTextField(), 1, row);
|
||||||
|
mEditorPane.getChildren().add(getSystemNameTextField());
|
||||||
|
|
||||||
|
Label hostLabel = new Label("OpenMHz Upload Server (API)");
|
||||||
|
GridPane.setHalignment(hostLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(hostLabel, 0, ++row);
|
||||||
|
mEditorPane.getChildren().add(hostLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getHostTextField(), 1, row);
|
||||||
|
mEditorPane.getChildren().add(getHostTextField());
|
||||||
|
|
||||||
|
Label maxAgeLabel = new Label("Max Recording Age (seconds)");
|
||||||
|
GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(maxAgeLabel, 0, ++row);
|
||||||
|
mEditorPane.getChildren().add(maxAgeLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getMaxAgeTextField(), 1, row);
|
||||||
|
mEditorPane.getChildren().add(getMaxAgeTextField());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return mEditorPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntegerTextField getMaxAgeTextField()
|
||||||
|
{
|
||||||
|
if(mMaxAgeTextField == null)
|
||||||
|
{
|
||||||
|
mMaxAgeTextField = new IntegerTextField();
|
||||||
|
mMaxAgeTextField.setDisable(true);
|
||||||
|
mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mMaxAgeTextField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getHostTextField()
|
||||||
|
{
|
||||||
|
if(mHostTextField == null)
|
||||||
|
{
|
||||||
|
mHostTextField = new TextField();
|
||||||
|
mHostTextField.setDisable(true);
|
||||||
|
mHostTextField.textProperty().addListener(mEditorModificationListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mHostTextField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getApiKeyTextField()
|
||||||
|
{
|
||||||
|
if(mApiKeyTextField == null)
|
||||||
|
{
|
||||||
|
mApiKeyTextField = new TextField();
|
||||||
|
mApiKeyTextField.setDisable(true);
|
||||||
|
mApiKeyTextField.textProperty().addListener(mEditorModificationListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mApiKeyTextField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getSystemNameTextField()
|
||||||
|
{
|
||||||
|
if(mSystemNameTextField == null)
|
||||||
|
{
|
||||||
|
mSystemNameTextField = new TextField();
|
||||||
|
mSystemNameTextField.setDisable(true);
|
||||||
|
mSystemNameTextField.textProperty().addListener(mEditorModificationListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mSystemNameTextField;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* * ******************************************************************************
|
||||||
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * You should have received a copy of the GNU General Public License
|
||||||
|
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* * *****************************************************************************
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||||
|
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||||
|
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||||
|
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class OpenMHzFeedConfiguration extends IcecastTCPConfiguration
|
||||||
|
{
|
||||||
|
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzFeedConfiguration.class);
|
||||||
|
|
||||||
|
private int mFeedID;
|
||||||
|
|
||||||
|
public OpenMHzFeedConfiguration()
|
||||||
|
{
|
||||||
|
//No-arg constructor for JAXB
|
||||||
|
this(BroadcastFormat.MP3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenMHz configuration for Icecast 2.3.2 compatible servers
|
||||||
|
*
|
||||||
|
* @param format of output audio (MP3)
|
||||||
|
*/
|
||||||
|
public OpenMHzFeedConfiguration(BroadcastFormat format)
|
||||||
|
{
|
||||||
|
super(format);
|
||||||
|
|
||||||
|
setBitRate(16);
|
||||||
|
setChannels(1);
|
||||||
|
setSampleRate(8000);
|
||||||
|
setInline(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OpenMHzFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
|
||||||
|
{
|
||||||
|
OpenMHzFeedConfiguration config = new OpenMHzFeedConfiguration(BroadcastFormat.MP3);
|
||||||
|
config.setName(userFeedBroadcast.getDescription());
|
||||||
|
config.setHost(userFeedBroadcast.getHostname());
|
||||||
|
config.setMountPoint(userFeedBroadcast.getMount());
|
||||||
|
config.setFeedID(userFeedBroadcast.getFeedId());
|
||||||
|
config.setPassword(userFeedBroadcast.getPassword());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
config.setPort(Integer.parseInt(userFeedBroadcast.getPort()));
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Error creating Rdio Scanner configuration from radio reference user feed instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BroadcastConfiguration copyOf()
|
||||||
|
{
|
||||||
|
OpenMHzFeedConfiguration copy = new OpenMHzFeedConfiguration(getBroadcastFormat());
|
||||||
|
|
||||||
|
//Broadcast Configuration Parameters
|
||||||
|
copy.setName(getName());
|
||||||
|
copy.setHost(getHost());
|
||||||
|
copy.setPort(getPort());
|
||||||
|
copy.setInline(getInline());
|
||||||
|
copy.setPassword(getPassword());
|
||||||
|
copy.setDelay(getDelay());
|
||||||
|
copy.setEnabled(false);
|
||||||
|
|
||||||
|
//Icecast Configuration Parameters
|
||||||
|
copy.setUserName(getUserName());
|
||||||
|
copy.setMountPoint(getMountPoint());
|
||||||
|
copy.setDescription(getDescription());
|
||||||
|
copy.setGenre(getGenre());
|
||||||
|
copy.setPublic(isPublic());
|
||||||
|
copy.setURL(getURL());
|
||||||
|
|
||||||
|
//OpenMHz Configuration Parameters
|
||||||
|
copy.setFeedID(getFeedID());
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||||
|
@Override
|
||||||
|
public BroadcastServerType getBroadcastServerType()
|
||||||
|
{
|
||||||
|
return BroadcastServerType.OPENMHZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "feed_id")
|
||||||
|
public int getFeedID()
|
||||||
|
{
|
||||||
|
return mFeedID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeedID(int feedID)
|
||||||
|
{
|
||||||
|
mFeedID = feedID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -270,7 +270,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
|||||||
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
||||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||||
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
|
|
||||||
.POST(bodyBuilder.build())
|
.POST(bodyBuilder.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ public class RdioScannerBuilder
|
|||||||
}
|
}
|
||||||
StringBuilder sb= new StringBuilder();
|
StringBuilder sb= new StringBuilder();
|
||||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||||
sb.append("Content-Disposition: form-data; filename =\"").append(audioName);
|
sb.append("Content-Disposition: form-data; filename=\"").append(audioName);
|
||||||
sb.append("\"; name=\"").append("audio").append("\"\r\n\r\n");
|
sb.append("\"; name=\"").append("audio").append("\"\r\n\r\n");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.bits;
|
|||||||
|
|
||||||
import io.github.dsheirer.edac.CRC;
|
import io.github.dsheirer.edac.CRC;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.apache.commons.math3.util.FastMath;
|
import org.apache.commons.math3.util.FastMath;
|
||||||
@@ -1278,7 +1279,7 @@ public class BinaryMessage extends BitSet
|
|||||||
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
|
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new String(bytes);
|
return new String(bytes, Charset.forName("UTF-16"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ import net.miginfocom.swing.MigLayout;
|
|||||||
|
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swing panel for Now Playing channels table and channel details tab set.
|
||||||
|
*/
|
||||||
public class NowPlayingPanel extends JPanel
|
public class NowPlayingPanel extends JPanel
|
||||||
{
|
{
|
||||||
private final ChannelMetadataPanel mChannelMetadataPanel;
|
private final ChannelMetadataPanel mChannelMetadataPanel;
|
||||||
@@ -40,45 +43,95 @@ public class NowPlayingPanel extends JPanel
|
|||||||
private final DecodeEventPanel mDecodeEventPanel;
|
private final DecodeEventPanel mDecodeEventPanel;
|
||||||
private final MessageActivityPanel mMessageActivityPanel;
|
private final MessageActivityPanel mMessageActivityPanel;
|
||||||
private final ChannelPowerPanel mChannelPowerPanel;
|
private final ChannelPowerPanel mChannelPowerPanel;
|
||||||
|
private JideTabbedPane mTabbedPane;
|
||||||
|
private JideSplitPane mSplitPane;
|
||||||
|
private boolean mDetailTabsVisible;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
||||||
* messages, events, and spectral view.
|
* messages, events, and spectral view.
|
||||||
*/
|
*/
|
||||||
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
|
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
|
||||||
SettingsManager settingsManager)
|
SettingsManager settingsManager, boolean detailTabsVisible)
|
||||||
{
|
{
|
||||||
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
|
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
|
||||||
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
||||||
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
||||||
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
||||||
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
||||||
|
mDetailTabsVisible = detailTabsVisible;
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the visibility of the channel details tabs panel.
|
||||||
|
* @param visible true to show or false to hide.
|
||||||
|
*/
|
||||||
|
public void setDetailTabsVisible(boolean visible)
|
||||||
|
{
|
||||||
|
//Only adjust if there is a change in state
|
||||||
|
if(visible ^ mDetailTabsVisible)
|
||||||
|
{
|
||||||
|
mDetailTabsVisible = visible;
|
||||||
|
|
||||||
|
if(mDetailTabsVisible)
|
||||||
|
{
|
||||||
|
getSplitPane().add(getTabbedPane());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
getSplitPane().remove(getTabbedPane());
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JideTabbedPane getTabbedPane()
|
||||||
|
{
|
||||||
|
if(mTabbedPane == null)
|
||||||
|
{
|
||||||
|
mTabbedPane = new JideTabbedPane();
|
||||||
|
mTabbedPane.addTab("Details", mChannelDetailPanel);
|
||||||
|
mTabbedPane.addTab("Events", mDecodeEventPanel);
|
||||||
|
mTabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||||
|
mTabbedPane.addTab("Channel", mChannelPowerPanel);
|
||||||
|
mTabbedPane.setFont(this.getFont());
|
||||||
|
mTabbedPane.setForeground(Color.BLACK);
|
||||||
|
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||||
|
mTabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
|
||||||
|
.indexOfComponent(mChannelPowerPanel)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mTabbedPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split pane for channels table and channel details tabs.
|
||||||
|
*/
|
||||||
|
private JideSplitPane getSplitPane()
|
||||||
|
{
|
||||||
|
if(mSplitPane == null)
|
||||||
|
{
|
||||||
|
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||||
|
mSplitPane.setShowGripper(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mSplitPane;
|
||||||
|
}
|
||||||
|
|
||||||
private void init()
|
private void init()
|
||||||
{
|
{
|
||||||
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
||||||
|
getSplitPane().add(mChannelMetadataPanel);
|
||||||
|
|
||||||
JideTabbedPane tabbedPane = new JideTabbedPane();
|
if(mDetailTabsVisible)
|
||||||
tabbedPane.addTab("Details", mChannelDetailPanel);
|
{
|
||||||
tabbedPane.addTab("Events", mDecodeEventPanel);
|
getSplitPane().add(getTabbedPane());
|
||||||
tabbedPane.addTab("Messages", mMessageActivityPanel);
|
}
|
||||||
tabbedPane.addTab("Channel", mChannelPowerPanel);
|
|
||||||
tabbedPane.setFont(this.getFont());
|
|
||||||
tabbedPane.setForeground(Color.BLACK);
|
|
||||||
|
|
||||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
|
||||||
tabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(tabbedPane.getSelectedIndex() == tabbedPane
|
|
||||||
.indexOfComponent(mChannelPowerPanel)));
|
|
||||||
|
|
||||||
JideSplitPane splitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
|
||||||
splitPane.setShowGripper(true);
|
|
||||||
splitPane.add(mChannelMetadataPanel);
|
|
||||||
splitPane.add(tabbedPane);
|
|
||||||
add(splitPane);
|
|
||||||
|
|
||||||
|
add(getSplitPane());
|
||||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
||||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
||||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
||||||
|
|||||||
@@ -59,17 +59,25 @@ public class ControllerPanel extends JPanel
|
|||||||
|
|
||||||
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
|
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
|
||||||
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
|
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
|
||||||
TunerManager tunerManager, UserPreferences userPreferences)
|
TunerManager tunerManager, UserPreferences userPreferences, boolean detailTabsVisible)
|
||||||
{
|
{
|
||||||
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
|
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
|
||||||
playlistManager.getAliasModel());
|
playlistManager.getAliasModel());
|
||||||
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager);
|
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager, detailTabsVisible);
|
||||||
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
|
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
|
||||||
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
|
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now playing panel.
|
||||||
|
*/
|
||||||
|
public NowPlayingPanel getNowPlayingPanel()
|
||||||
|
{
|
||||||
|
return mNowPlayingPanel;
|
||||||
|
}
|
||||||
|
|
||||||
private void init()
|
private void init()
|
||||||
{
|
{
|
||||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
|
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
|
||||||
@@ -97,8 +105,8 @@ public class ControllerPanel extends JPanel
|
|||||||
mTabbedPane.addTab("Map", mMapPanel);
|
mTabbedPane.addTab("Map", mMapPanel);
|
||||||
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
||||||
|
|
||||||
Icon gearIcon = IconFontSwing.buildIcon(FontAwesome.COG, 20, Color.DARK_GRAY);
|
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
|
||||||
mTabbedPane.addTab("Playlist Editor", gearIcon, new JLabel("Show Playlist Manager"));
|
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
|
||||||
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
||||||
|
|
||||||
//Set preferred size to influence the split between these panels
|
//Set preferred size to influence the split between these panels
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
/*******************************************************************************
|
/*
|
||||||
* sdr-trunk
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
* 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
* it under the terms of the GNU General Public License as published by
|
||||||
* later version.
|
* 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
|
* This program is distributed in the hope that it will be useful,
|
||||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
* 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.
|
* You should have received a copy of the GNU General Public License
|
||||||
* If not, see <http://www.gnu.org/licenses/>
|
* 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;
|
||||||
@@ -177,6 +180,28 @@ public class CRCDMR
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the residual CRC value using a mask of zero which should identify the mask value when an alternate
|
||||||
|
* mask value is employed.
|
||||||
|
* @param message to be checked
|
||||||
|
* @param messageStart location
|
||||||
|
* @param crcStart location
|
||||||
|
* @return residual CRC check value.
|
||||||
|
*/
|
||||||
|
public static int calculateResidual(CorrectedBinaryMessage message, int messageStart, int crcStart)
|
||||||
|
{
|
||||||
|
int calculated = 0; //Starting value
|
||||||
|
|
||||||
|
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||||
|
for(int i = message.nextSetBit(messageStart); i >= messageStart && i < crcStart; i = message.nextSetBit(i + 1))
|
||||||
|
{
|
||||||
|
calculated ^= CCITT_80_CHECKSUMS[i - messageStart];
|
||||||
|
}
|
||||||
|
|
||||||
|
int checksum = getIntChecksum(message, crcStart, 16);
|
||||||
|
return calculated ^ checksum;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error detection for CRC-9 protected Confirmed Packet Data blocks. These
|
* Error detection for CRC-9 protected Confirmed Packet Data blocks. These
|
||||||
* data blocks have a slightly complicated structure because the checksum
|
* data blocks have a slightly complicated structure because the checksum
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ 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;
|
||||||
import io.github.dsheirer.module.log.EventLogManager;
|
import io.github.dsheirer.module.log.EventLogManager;
|
||||||
|
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||||
|
import io.github.dsheirer.monitor.StatusBox;
|
||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
@@ -90,6 +92,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
private Stage mPlaylistStage;
|
private Stage mPlaylistStage;
|
||||||
private Stage mUserPreferencesStage;
|
private Stage mUserPreferencesStage;
|
||||||
private Stage mRecordingViewerStage;
|
private Stage mRecordingViewerStage;
|
||||||
|
private JFXPanel mStatusPanel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance. Note: this constructor is used for Swing applications.
|
* Constructs an instance. Note: this constructor is used for Swing applications.
|
||||||
@@ -118,6 +121,27 @@ public class JavaFxWindowManager extends Application
|
|||||||
setup();
|
setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or accesses the JavaFX status panel, used by the main application GUI.
|
||||||
|
* @param resourceMonitor for statistics
|
||||||
|
* @return JFXPanel accessible on Swing thread that delegates JavaFX scene creation to the FX event thread.
|
||||||
|
*/
|
||||||
|
public JFXPanel getStatusPanel(ResourceMonitor resourceMonitor)
|
||||||
|
{
|
||||||
|
if(mStatusPanel == null)
|
||||||
|
{
|
||||||
|
mStatusPanel = new JFXPanel();
|
||||||
|
|
||||||
|
//JFXPanel has to be populated on the FX event thread
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Scene scene = new Scene(new StatusBox(resourceMonitor));
|
||||||
|
mStatusPanel.setScene(scene);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mStatusPanel;
|
||||||
|
}
|
||||||
|
|
||||||
private void setup()
|
private void setup()
|
||||||
{
|
{
|
||||||
//Register this class to receive events via each method annotated with @Subscribe
|
//Register this class to receive events via each method annotated with @Subscribe
|
||||||
@@ -244,9 +268,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> {
|
||||||
getJmbeEditorStage().show();
|
restoreStage(getJmbeEditorStage());
|
||||||
getJmbeEditorStage().requestFocus();
|
|
||||||
getJmbeEditorStage().toFront();
|
|
||||||
getJmbeEditor().process(request);
|
getJmbeEditor().process(request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -317,9 +339,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
execute(() -> {
|
execute(() -> {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
getPlaylistStage().show();
|
restoreStage(getPlaylistStage());
|
||||||
getPlaylistStage().requestFocus();
|
|
||||||
getPlaylistStage().toFront();
|
|
||||||
getPlaylistEditor().process(request);
|
getPlaylistEditor().process(request);
|
||||||
}
|
}
|
||||||
catch(Throwable t)
|
catch(Throwable t)
|
||||||
@@ -367,9 +387,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
public void process(final ViewUserPreferenceEditorRequest request)
|
public void process(final ViewUserPreferenceEditorRequest request)
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> {
|
||||||
getUserPreferencesStage().show();
|
restoreStage(getUserPreferencesStage());
|
||||||
getUserPreferencesStage().requestFocus();
|
|
||||||
getUserPreferencesStage().toFront();
|
|
||||||
getUserPreferencesEditor().process(request);
|
getUserPreferencesEditor().process(request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -408,11 +426,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
@Subscribe
|
@Subscribe
|
||||||
public void process(final ViewIconManagerRequest request)
|
public void process(final ViewIconManagerRequest request)
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> restoreStage(getIconManagerStage()));
|
||||||
getIconManagerStage().show();
|
|
||||||
getIconManagerStage().requestFocus();
|
|
||||||
getIconManagerStage().toFront();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -422,9 +436,7 @@ public class JavaFxWindowManager extends Application
|
|||||||
public void process(final ViewChannelMapEditorRequest request)
|
public void process(final ViewChannelMapEditorRequest request)
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> {
|
||||||
getChannelMapStage().show();
|
restoreStage(getChannelMapStage());
|
||||||
getChannelMapStage().requestFocus();
|
|
||||||
getChannelMapStage().toFront();
|
|
||||||
getChannelMapEditor().process(request);
|
getChannelMapEditor().process(request);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -435,11 +447,22 @@ public class JavaFxWindowManager extends Application
|
|||||||
@Subscribe
|
@Subscribe
|
||||||
public void process(final ViewRecordingViewerRequest request)
|
public void process(final ViewRecordingViewerRequest request)
|
||||||
{
|
{
|
||||||
execute(() -> {
|
execute(() -> restoreStage(getRecordingViewerStage()));
|
||||||
getRecordingViewerStage().show();
|
}
|
||||||
getRecordingViewerStage().requestFocus();
|
|
||||||
getRecordingViewerStage().toFront();
|
/**
|
||||||
});
|
* Restores the stage to previous size and location.
|
||||||
|
* @param stage to restore.
|
||||||
|
*/
|
||||||
|
private void restoreStage(Stage stage)
|
||||||
|
{
|
||||||
|
stage.setIconified(false);
|
||||||
|
stage.show();
|
||||||
|
stage.requestFocus();
|
||||||
|
stage.toFront();
|
||||||
|
stage.setMinHeight(stage.getHeight());
|
||||||
|
stage.setMinWidth(stage.getWidth());
|
||||||
|
// System.out.println("Stage [" + stage.getTitle() + "] restored - width[" + stage.getWidth() + "] height [" + stage.getHeight() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ 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;
|
||||||
import io.github.dsheirer.module.log.EventLogManager;
|
import io.github.dsheirer.module.log.EventLogManager;
|
||||||
|
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.properties.SystemProperties;
|
import io.github.dsheirer.properties.SystemProperties;
|
||||||
@@ -54,7 +55,7 @@ import io.github.dsheirer.source.tuner.TunerEvent;
|
|||||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||||
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
|
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
|
||||||
import io.github.dsheirer.spectrum.ClearTunerMenuItem;
|
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
|
||||||
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
||||||
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
@@ -68,11 +69,11 @@ import java.awt.GraphicsEnvironment;
|
|||||||
import java.awt.Point;
|
import java.awt.Point;
|
||||||
import java.awt.Robot;
|
import java.awt.Robot;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.ActionListener;
|
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.awt.event.WindowAdapter;
|
import java.awt.event.WindowAdapter;
|
||||||
import java.awt.event.WindowEvent;
|
import java.awt.event.WindowEvent;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -80,7 +81,9 @@ import java.nio.file.Path;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.prefs.Preferences;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.embed.swing.JFXPanel;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
import jiconfont.icons.font_awesome.FontAwesome;
|
import jiconfont.icons.font_awesome.FontAwesome;
|
||||||
import jiconfont.swing.IconFontSwing;
|
import jiconfont.swing.IconFontSwing;
|
||||||
@@ -105,14 +108,19 @@ import javax.swing.plaf.metal.MetalLookAndFeel;
|
|||||||
public class SDRTrunk implements Listener<TunerEvent>
|
public class SDRTrunk implements Listener<TunerEvent>
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
|
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
|
||||||
|
private Preferences mPreferences = Preferences.userNodeForPackage(SDRTrunk.class);
|
||||||
|
|
||||||
private static final String PROPERTY_BROADCAST_STATUS_VISIBLE = "main.broadcast.status.visible";
|
private static final String PREFERENCE_BROADCAST_STATUS_VISIBLE = "sdrtrunk.broadcast.status.visible";
|
||||||
|
private static final String PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE = "sdrtrunk.now.playing.details.visible";
|
||||||
|
private static final String PREFERENCE_RESOURCE_STATUS_VISIBLE = "sdrtrunk.resource.status.visible";
|
||||||
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
|
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
|
||||||
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
|
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
|
||||||
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
|
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
|
||||||
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
||||||
|
|
||||||
private boolean mBroadcastStatusVisible;
|
private boolean mBroadcastStatusVisible;
|
||||||
|
private boolean mResourceStatusVisible;
|
||||||
|
private boolean mNowPlayingDetailsVisible;
|
||||||
private AudioRecordingManager mAudioRecordingManager;
|
private AudioRecordingManager mAudioRecordingManager;
|
||||||
private AudioStreamingManager mAudioStreamingManager;
|
private AudioStreamingManager mAudioStreamingManager;
|
||||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||||
@@ -127,6 +135,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
private UserPreferences mUserPreferences = new UserPreferences();
|
private UserPreferences mUserPreferences = new UserPreferences();
|
||||||
private TunerManager mTunerManager;
|
private TunerManager mTunerManager;
|
||||||
private ApplicationLog mApplicationLog;
|
private ApplicationLog mApplicationLog;
|
||||||
|
private ResourceMonitor mResourceMonitor;
|
||||||
|
private JFXPanel mResourceStatusPanel;
|
||||||
|
|
||||||
private String mTitle;
|
private String mTitle;
|
||||||
|
|
||||||
@@ -140,6 +150,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mApplicationLog = new ApplicationLog(mUserPreferences);
|
mApplicationLog = new ApplicationLog(mUserPreferences);
|
||||||
mApplicationLog.start();
|
mApplicationLog.start();
|
||||||
|
|
||||||
|
mResourceMonitor = new ResourceMonitor(mUserPreferences);
|
||||||
|
|
||||||
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
||||||
|
|
||||||
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
|
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
|
||||||
@@ -155,18 +167,10 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Setup the application home directory
|
|
||||||
Path home = getHomePath();
|
|
||||||
|
|
||||||
ThreadPool.logSettings();
|
ThreadPool.logSettings();
|
||||||
|
|
||||||
mLog.info("Home path: " + home.toString());
|
|
||||||
|
|
||||||
//Load properties file
|
//Load properties file
|
||||||
if(home != null)
|
loadProperties();
|
||||||
{
|
|
||||||
loadProperties(home);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Log current properties setting
|
//Log current properties setting
|
||||||
SystemProperties.getInstance().logCurrentSettings();
|
SystemProperties.getInstance().logCurrentSettings();
|
||||||
@@ -213,10 +217,12 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
MapService mapService = new MapService(mIconModel);
|
MapService mapService = new MapService(mIconModel);
|
||||||
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
|
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
|
||||||
|
|
||||||
|
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
|
||||||
|
|
||||||
if(!GraphicsEnvironment.isHeadless())
|
if(!GraphicsEnvironment.isHeadless())
|
||||||
{
|
{
|
||||||
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
|
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
|
||||||
mSettingsManager, mTunerManager, mUserPreferences);
|
mSettingsManager, mTunerManager, mUserPreferences, mNowPlayingDetailsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
|
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
|
||||||
@@ -323,7 +329,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
*/
|
*/
|
||||||
private void initGUI()
|
private void initGUI()
|
||||||
{
|
{
|
||||||
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
|
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]0[shrink 0]"));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup main JFrame window
|
* Setup main JFrame window
|
||||||
@@ -379,7 +385,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mSplitPane.add(mSpectralPanel);
|
mSplitPane.add(mSpectralPanel);
|
||||||
mSplitPane.add(mControllerPanel);
|
mSplitPane.add(mControllerPanel);
|
||||||
|
|
||||||
mBroadcastStatusVisible = SystemProperties.getInstance().get(PROPERTY_BROADCAST_STATUS_VISIBLE, false);
|
mBroadcastStatusVisible = mPreferences.getBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, false);
|
||||||
|
|
||||||
//Show broadcast status panel when user requests - disabled by default
|
//Show broadcast status panel when user requests - disabled by default
|
||||||
if(mBroadcastStatusVisible)
|
if(mBroadcastStatusVisible)
|
||||||
@@ -389,6 +395,13 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
|
|
||||||
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
|
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
|
||||||
|
|
||||||
|
mResourceMonitor.start();
|
||||||
|
mResourceStatusVisible = mPreferences.getBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, true);
|
||||||
|
if(mResourceStatusVisible)
|
||||||
|
{
|
||||||
|
mMainGui.add(getResourceStatusPanel(), "span,growx");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu items
|
* Menu items
|
||||||
*/
|
*/
|
||||||
@@ -399,14 +412,9 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
menuBar.add(fileMenu);
|
menuBar.add(fileMenu);
|
||||||
|
|
||||||
JMenuItem exitMenu = new JMenuItem("Exit");
|
JMenuItem exitMenu = new JMenuItem("Exit");
|
||||||
exitMenu.addActionListener(
|
exitMenu.addActionListener(event -> {
|
||||||
new ActionListener()
|
processShutdown();
|
||||||
{
|
System.exit(0);
|
||||||
public void actionPerformed(ActionEvent event)
|
|
||||||
{
|
|
||||||
processShutdown();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -415,56 +423,121 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
JMenu viewMenu = new JMenu("View");
|
JMenu viewMenu = new JMenu("View");
|
||||||
|
|
||||||
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
|
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
|
||||||
|
viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12));
|
||||||
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
|
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
|
||||||
viewMenu.add(viewPlaylistItem);
|
viewMenu.add(viewPlaylistItem);
|
||||||
|
|
||||||
viewMenu.add(new JSeparator());
|
viewMenu.add(new JSeparator());
|
||||||
|
|
||||||
|
JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files");
|
||||||
|
viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||||
|
viewApplicationLogsMenu.addActionListener(arg0 -> {
|
||||||
|
File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Desktop.getDesktop().open(logsDirectory);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't open file explorer");
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(mMainGui,
|
||||||
|
"Can't launch file explorer - files are located at: " + logsDirectory,
|
||||||
|
"Can't launch file explorer",
|
||||||
|
JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
viewMenu.add(viewApplicationLogsMenu);
|
||||||
|
|
||||||
|
JMenuItem viewRecordingsMenuItem = new JMenuItem("Audio Recordings");
|
||||||
|
viewRecordingsMenuItem.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||||
|
viewRecordingsMenuItem.addActionListener(arg0 -> {
|
||||||
|
File recordingsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording().toFile();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Desktop.getDesktop().open(recordingsDirectory);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't open file explorer");
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(mMainGui,
|
||||||
|
"Can't launch file explorer - files are located at: " +
|
||||||
|
recordingsDirectory,
|
||||||
|
"Can't launch file explorer",
|
||||||
|
JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
viewMenu.add(viewRecordingsMenuItem);
|
||||||
|
|
||||||
|
JMenuItem viewEventLogsMenu = new JMenuItem("Channel Event Log Files");
|
||||||
|
viewEventLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||||
|
viewEventLogsMenu.addActionListener(arg0 -> {
|
||||||
|
File eventLogsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryEventLog().toFile();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Desktop.getDesktop().open(eventLogsDirectory);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't open file explorer");
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(mMainGui,
|
||||||
|
"Can't launch file explorer - files are located at: " + eventLogsDirectory,
|
||||||
|
"Can't launch file explorer",
|
||||||
|
JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
viewMenu.add(viewEventLogsMenu);
|
||||||
|
|
||||||
|
JMenuItem iconManagerMenu = new JMenuItem("Icon Manager");
|
||||||
|
iconManagerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.PICTURE_O, 12));
|
||||||
|
iconManagerMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
||||||
|
viewMenu.add(iconManagerMenu);
|
||||||
|
|
||||||
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
|
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
|
||||||
|
recordingViewerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.BRAILLE, 12));
|
||||||
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
|
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
|
||||||
viewMenu.add(recordingViewerMenu);
|
viewMenu.add(recordingViewerMenu);
|
||||||
|
|
||||||
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
|
JMenuItem viewScreenCapturesMenu = new JMenuItem("Screen Captures");
|
||||||
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
viewScreenCapturesMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||||
viewMenu.add(settingsMenu);
|
viewScreenCapturesMenu.addActionListener(arg0 -> {
|
||||||
|
File screenCapturesDirectory = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().toFile();
|
||||||
JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings");
|
try
|
||||||
logFilesMenu.addActionListener(new ActionListener()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent arg0)
|
|
||||||
{
|
{
|
||||||
try
|
Desktop.getDesktop().open(screenCapturesDirectory);
|
||||||
{
|
}
|
||||||
Desktop.getDesktop().open(getHomePath().toFile());
|
catch(Exception e)
|
||||||
}
|
{
|
||||||
catch(Exception e)
|
mLog.error("Couldn't open file explorer");
|
||||||
{
|
|
||||||
mLog.error("Couldn't open file explorer");
|
|
||||||
|
|
||||||
JOptionPane.showMessageDialog(mMainGui,
|
JOptionPane.showMessageDialog(mMainGui,
|
||||||
"Can't launch file explorer - files are located at: " +
|
"Can't launch file explorer - files are located at: " + screenCapturesDirectory,
|
||||||
getHomePath().toString(),
|
"Can't launch file explorer",
|
||||||
"Can't launch file explorer",
|
JOptionPane.ERROR_MESSAGE);
|
||||||
JOptionPane.ERROR_MESSAGE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
viewMenu.add(logFilesMenu);
|
viewMenu.add(viewScreenCapturesMenu);
|
||||||
|
|
||||||
JMenuItem preferencesItem = new JMenuItem("User Preferences");
|
JMenuItem preferencesItem = new JMenuItem("User Preferences");
|
||||||
|
preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12));
|
||||||
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
|
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
|
||||||
viewMenu.add(preferencesItem);
|
viewMenu.add(preferencesItem);
|
||||||
|
|
||||||
viewMenu.add(new JSeparator());
|
viewMenu.add(new JSeparator());
|
||||||
viewMenu.add(new TunersMenu());
|
viewMenu.add(new TunersMenu());
|
||||||
viewMenu.add(new JSeparator());
|
viewMenu.add(new JSeparator());
|
||||||
viewMenu.add(new ClearTunerMenuItem(mSpectralPanel));
|
viewMenu.add(new DisableSpectrumWaterfallMenuItem(mSpectralPanel));
|
||||||
viewMenu.add(new BroadcastStatusVisibleMenuItem(mControllerPanel));
|
viewMenu.add(new NowPlayingChannelDetailsVisibleMenuItem());
|
||||||
|
viewMenu.add(new BroadcastStatusVisibleMenuItem());
|
||||||
|
viewMenu.add(new ResourceStatusVisibleMenuItem());
|
||||||
|
|
||||||
menuBar.add(viewMenu);
|
menuBar.add(viewMenu);
|
||||||
|
|
||||||
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
|
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
|
||||||
|
screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12));
|
||||||
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
|
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
|
||||||
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
|
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
|
||||||
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
|
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
|
||||||
@@ -515,6 +588,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mLog.info("Stopping channels ...");
|
mLog.info("Stopping channels ...");
|
||||||
mPlaylistManager.getChannelProcessingManager().shutdown();
|
mPlaylistManager.getChannelProcessingManager().shutdown();
|
||||||
mAudioRecordingManager.stop();
|
mAudioRecordingManager.stop();
|
||||||
|
mResourceMonitor.stop();
|
||||||
|
|
||||||
mLog.info("Stopping spectral display ...");
|
mLog.info("Stopping spectral display ...");
|
||||||
mSpectralPanel.clearTuner();
|
mSpectralPanel.clearTuner();
|
||||||
@@ -560,7 +634,54 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mMainGui.revalidate();
|
mMainGui.revalidate();
|
||||||
});
|
});
|
||||||
|
|
||||||
SystemProperties.getInstance().set(PROPERTY_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
|
mPreferences.putBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy constructor for resource status panel
|
||||||
|
*/
|
||||||
|
private JFXPanel getResourceStatusPanel()
|
||||||
|
{
|
||||||
|
|
||||||
|
if(mResourceStatusPanel == null)
|
||||||
|
{
|
||||||
|
mResourceStatusPanel = mJavaFxWindowManager.getStatusPanel(mResourceMonitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mResourceStatusPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles visibility of the resource status panel at the bottom of the main UI window
|
||||||
|
*/
|
||||||
|
private void toggleResourceStatusPanelVisibility()
|
||||||
|
{
|
||||||
|
mResourceStatusVisible = !mResourceStatusVisible;
|
||||||
|
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
if(mResourceStatusVisible)
|
||||||
|
{
|
||||||
|
mMainGui.add(getResourceStatusPanel(), "span,growx");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mMainGui.remove(getResourceStatusPanel());
|
||||||
|
}
|
||||||
|
|
||||||
|
mMainGui.revalidate();
|
||||||
|
});
|
||||||
|
|
||||||
|
mPreferences.putBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, mResourceStatusVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles visibility of the Now Playing channel details panel
|
||||||
|
*/
|
||||||
|
private void toggleNowPlayingDetailsPanelVisibility()
|
||||||
|
{
|
||||||
|
mNowPlayingDetailsVisible = !mNowPlayingDetailsVisible;
|
||||||
|
mControllerPanel.getNowPlayingPanel().setDetailTabsVisible(mNowPlayingDetailsVisible);
|
||||||
|
mPreferences.putBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, mNowPlayingDetailsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -568,34 +689,30 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
* Loads the application properties file from the user's home directory,
|
* Loads the application properties file from the user's home directory,
|
||||||
* creating the properties file for the first-time, if necessary
|
* creating the properties file for the first-time, if necessary
|
||||||
*/
|
*/
|
||||||
private void loadProperties(Path homePath)
|
private void loadProperties()
|
||||||
{
|
{
|
||||||
Path propsPath = homePath.resolve("SDRTrunk.properties");
|
Path propertiesPath = mUserPreferences.getDirectoryPreference().getDirectoryApplicationRoot().resolve("SDRTrunk.properties");
|
||||||
|
|
||||||
if(!Files.exists(propsPath))
|
if(!Files.exists(propertiesPath))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
mLog.info("SDRTrunk - creating application properties file [" +
|
mLog.info("SDRTrunk - creating application properties file [" + propertiesPath.toAbsolutePath() + "]");
|
||||||
propsPath.toAbsolutePath() + "]");
|
Files.createFile(propertiesPath);
|
||||||
|
|
||||||
Files.createFile(propsPath);
|
|
||||||
}
|
}
|
||||||
catch(IOException e)
|
catch(IOException e)
|
||||||
{
|
{
|
||||||
mLog.error("SDRTrunk - couldn't create application properties "
|
mLog.error("SDRTrunk - couldn't create application properties file [" + propertiesPath.toAbsolutePath(), e);
|
||||||
+ "file [" + propsPath.toAbsolutePath(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Files.exists(propsPath))
|
if(Files.exists(propertiesPath))
|
||||||
{
|
{
|
||||||
SystemProperties.getInstance().load(propsPath);
|
SystemProperties.getInstance().load(propertiesPath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk " +
|
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk application properties file");
|
||||||
"application properties file");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,26 +795,50 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast status panel visible toggle menu item
|
||||||
|
*/
|
||||||
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
|
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||||
{
|
{
|
||||||
private ControllerPanel mControllerPanel;
|
public BroadcastStatusVisibleMenuItem()
|
||||||
|
|
||||||
public BroadcastStatusVisibleMenuItem(ControllerPanel controllerPanel)
|
|
||||||
{
|
{
|
||||||
super("Show Streaming Status");
|
super("Show Streaming Status");
|
||||||
|
setSelected(mBroadcastStatusVisible);
|
||||||
|
addActionListener(e -> {
|
||||||
|
toggleBroadcastStatusPanelVisibility();
|
||||||
|
setSelected(mBroadcastStatusVisible);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mControllerPanel = controllerPanel;
|
/**
|
||||||
|
* Resource status panel visible toggle menu item
|
||||||
|
*/
|
||||||
|
public class ResourceStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||||
|
{
|
||||||
|
public ResourceStatusVisibleMenuItem()
|
||||||
|
{
|
||||||
|
super("Show Resource Status");
|
||||||
|
setSelected(mResourceStatusVisible);
|
||||||
|
addActionListener(e -> {
|
||||||
|
toggleResourceStatusPanelVisibility();
|
||||||
|
setSelected(mResourceStatusVisible);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setSelected(mBroadcastStatusPanel != null);
|
/**
|
||||||
|
* Now Playing channel details visible toggle menu item
|
||||||
addActionListener(new ActionListener()
|
*/
|
||||||
{
|
public class NowPlayingChannelDetailsVisibleMenuItem extends JCheckBoxMenuItem
|
||||||
@Override
|
{
|
||||||
public void actionPerformed(ActionEvent e)
|
public NowPlayingChannelDetailsVisibleMenuItem()
|
||||||
{
|
{
|
||||||
toggleBroadcastStatusPanelVisibility();
|
super("Show Now Playing Channel Details");
|
||||||
setSelected(mBroadcastStatusVisible);
|
setSelected(mNowPlayingDetailsVisible);
|
||||||
}
|
addActionListener(e -> {
|
||||||
|
toggleNowPlayingDetailsPanelVisibility();
|
||||||
|
setSelected(mNowPlayingDetailsVisible);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,13 @@ import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
|||||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
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.P25FullyQualifiedRadio;
|
||||||
import io.github.dsheirer.alias.id.radio.Radio;
|
import io.github.dsheirer.alias.id.radio.Radio;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioFormatter;
|
import io.github.dsheirer.alias.id.radio.RadioFormatter;
|
||||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||||
|
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter;
|
import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter;
|
||||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||||
@@ -684,8 +686,10 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
Menu p25Menu = new ProtocolMenu(Protocol.APCO25);
|
Menu p25Menu = new ProtocolMenu(Protocol.APCO25);
|
||||||
p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25));
|
p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25));
|
||||||
p25Menu.getItems().add(new AddTalkgroupRangeItem(Protocol.APCO25));
|
p25Menu.getItems().add(new AddTalkgroupRangeItem(Protocol.APCO25));
|
||||||
|
p25Menu.getItems().add(new AddP25FullyQualifiedTalkgroupItem());
|
||||||
p25Menu.getItems().add(new AddRadioIdItem(Protocol.APCO25));
|
p25Menu.getItems().add(new AddRadioIdItem(Protocol.APCO25));
|
||||||
p25Menu.getItems().add(new AddRadioIdRangeItem(Protocol.APCO25));
|
p25Menu.getItems().add(new AddRadioIdRangeItem(Protocol.APCO25));
|
||||||
|
p25Menu.getItems().add(new AddP25FullyQualifiedRadioIdItem());
|
||||||
p25Menu.getItems().add(new SeparatorMenuItem());
|
p25Menu.getItems().add(new SeparatorMenuItem());
|
||||||
p25Menu.getItems().add(new AddUserStatusItem());
|
p25Menu.getItems().add(new AddUserStatusItem());
|
||||||
p25Menu.getItems().add(new AddUnitStatusItem());
|
p25Menu.getItems().add(new AddUnitStatusItem());
|
||||||
@@ -1322,6 +1326,44 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu Item for adding a new protocol-specific Fully Qualified Radio ID alias identifier
|
||||||
|
*/
|
||||||
|
public class AddP25FullyQualifiedRadioIdItem extends MenuItem
|
||||||
|
{
|
||||||
|
public AddP25FullyQualifiedRadioIdItem()
|
||||||
|
{
|
||||||
|
super("Fully Qualified Radio ID");
|
||||||
|
setOnAction(event -> {
|
||||||
|
P25FullyQualifiedRadio radioId = new P25FullyQualifiedRadio();
|
||||||
|
radioId.setProtocol(Protocol.APCO25);
|
||||||
|
getIdentifiersList().getItems().add(radioId);
|
||||||
|
getIdentifiersList().getSelectionModel().select(radioId);
|
||||||
|
getIdentifiersList().scrollTo(radioId);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu Item for adding a new protocol-specific Fully Qualified Talkgroup alias identifier
|
||||||
|
*/
|
||||||
|
public class AddP25FullyQualifiedTalkgroupItem extends MenuItem
|
||||||
|
{
|
||||||
|
public AddP25FullyQualifiedTalkgroupItem()
|
||||||
|
{
|
||||||
|
super("Fully Qualified Talkgroup");
|
||||||
|
setOnAction(event -> {
|
||||||
|
P25FullyQualifiedTalkgroup talkgroup = new P25FullyQualifiedTalkgroup();
|
||||||
|
talkgroup.setProtocol(Protocol.APCO25);
|
||||||
|
getIdentifiersList().getItems().add(talkgroup);
|
||||||
|
getIdentifiersList().getSelectionModel().select(talkgroup);
|
||||||
|
getIdentifiersList().scrollTo(talkgroup);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu Item for adding a new protocol-specific Radio ID range alias identifier
|
* Menu Item for adding a new protocol-specific Radio ID range alias identifier
|
||||||
*/
|
*/
|
||||||
@@ -1505,7 +1547,25 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
|
|
||||||
if(item != null)
|
if(item != null)
|
||||||
{
|
{
|
||||||
if(item instanceof Talkgroup)
|
if(item instanceof P25FullyQualifiedTalkgroup fqt)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("APCO-25 Fully Qualified Talkgroup:").append(fqt.getWacn());
|
||||||
|
sb.append(".").append(fqt.getSystem());
|
||||||
|
sb.append(".").append(fqt.getValue());
|
||||||
|
|
||||||
|
if(fqt.overlapProperty().get())
|
||||||
|
{
|
||||||
|
sb.append(" - Error: Overlap");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!fqt.isValid())
|
||||||
|
{
|
||||||
|
sb.append(" **NOT VALID**");
|
||||||
|
}
|
||||||
|
setText(sb.toString());
|
||||||
|
}
|
||||||
|
else if(item instanceof Talkgroup)
|
||||||
{
|
{
|
||||||
Talkgroup talkgroup = (Talkgroup)item;
|
Talkgroup talkgroup = (Talkgroup)item;
|
||||||
Protocol protocol = talkgroup.getProtocol();
|
Protocol protocol = talkgroup.getProtocol();
|
||||||
@@ -1552,6 +1612,24 @@ public class AliasItemEditor extends Editor<Alias>
|
|||||||
}
|
}
|
||||||
setText(sb.toString());
|
setText(sb.toString());
|
||||||
}
|
}
|
||||||
|
else if(item instanceof P25FullyQualifiedRadio fqr)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("APCO-25 Fully Qualified Radio ID:").append(fqr.getWacn());
|
||||||
|
sb.append(".").append(fqr.getSystem());
|
||||||
|
sb.append(".").append(fqr.getValue());
|
||||||
|
|
||||||
|
if(fqr.overlapProperty().get())
|
||||||
|
{
|
||||||
|
sb.append(" - Error: Overlap");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!fqr.isValid())
|
||||||
|
{
|
||||||
|
sb.append(" **NOT VALID**");
|
||||||
|
}
|
||||||
|
setText(sb.toString());
|
||||||
|
}
|
||||||
else if(item instanceof Radio)
|
else if(item instanceof Radio)
|
||||||
{
|
{
|
||||||
Radio radio = (Radio)item;
|
Radio radio = (Radio)item;
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ public class IdentifierEditorFactory
|
|||||||
return new EsnEditor();
|
return new EsnEditor();
|
||||||
case LOJACK:
|
case LOJACK:
|
||||||
return new LojackEditor();
|
return new LojackEditor();
|
||||||
|
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||||
|
return new P25FullyQualifiedRadioIdEditor(userPreferences);
|
||||||
|
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||||
|
return new P25FullyQualifiedTalkgroupEditor(userPreferences);
|
||||||
case RADIO_ID:
|
case RADIO_ID:
|
||||||
return new RadioIdEditor(userPreferences);
|
return new RadioIdEditor(userPreferences);
|
||||||
case RADIO_ID_RANGE:
|
case RADIO_ID_RANGE:
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.radio.P25FullyQualifiedRadio;
|
||||||
|
import io.github.dsheirer.gui.control.HexFormatter;
|
||||||
|
import io.github.dsheirer.gui.control.IntegerFormatter;
|
||||||
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.geometry.HPos;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.TextFormatter;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor for P25 fully qualified radio alias identifiers
|
||||||
|
*/
|
||||||
|
public class P25FullyQualifiedRadioIdEditor extends IdentifierEditor<P25FullyQualifiedRadio>
|
||||||
|
{
|
||||||
|
private static final Logger mLog = LoggerFactory.getLogger(P25FullyQualifiedRadioIdEditor.class);
|
||||||
|
|
||||||
|
private UserPreferences mUserPreferences;
|
||||||
|
private Label mProtocolLabel;
|
||||||
|
private TextField mWacnField;
|
||||||
|
private TextField mSystemField;
|
||||||
|
private TextField mRadioField;
|
||||||
|
private TextFormatter<Integer> mWacnTextFormatter;
|
||||||
|
private TextFormatter<Integer> mSystemTextFormatter;
|
||||||
|
private TextFormatter<Integer> mRadioTextFormatter;
|
||||||
|
private WacnValueChangeListener mWacnValueChangeListener = new WacnValueChangeListener();
|
||||||
|
private SystemValueChangeListener mSystemValueChangeListener = new SystemValueChangeListener();
|
||||||
|
private RadioValueChangeListener mRadioValueChangeListener = new RadioValueChangeListener();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param userPreferences for determining user preferred talkgroup formats
|
||||||
|
*/
|
||||||
|
public P25FullyQualifiedRadioIdEditor(UserPreferences userPreferences)
|
||||||
|
{
|
||||||
|
mUserPreferences = userPreferences;
|
||||||
|
|
||||||
|
GridPane gridPane = new GridPane();
|
||||||
|
gridPane.setHgap(5);
|
||||||
|
gridPane.setVgap(3);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getProtocolLabel(), 0, 0);
|
||||||
|
gridPane.getChildren().add(getProtocolLabel());
|
||||||
|
|
||||||
|
Label valueLabel = new Label("WACN");
|
||||||
|
GridPane.setHalignment(valueLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(valueLabel, 1, 0);
|
||||||
|
gridPane.getChildren().add(valueLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getWacnField(), 2, 0);
|
||||||
|
gridPane.getChildren().add(getWacnField());
|
||||||
|
|
||||||
|
Label systemLabel = new Label("System");
|
||||||
|
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(systemLabel, 3, 0);
|
||||||
|
gridPane.getChildren().add(systemLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getSystemField(), 4, 0);
|
||||||
|
gridPane.getChildren().add(getSystemField());
|
||||||
|
|
||||||
|
Label radioLabel = new Label("Radio ID");
|
||||||
|
GridPane.setHalignment(radioLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(radioLabel, 5, 0);
|
||||||
|
gridPane.getChildren().add(radioLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getRadioField(), 6, 0);
|
||||||
|
gridPane.getChildren().add(getRadioField());
|
||||||
|
|
||||||
|
getChildren().add(gridPane);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setItem(P25FullyQualifiedRadio item)
|
||||||
|
{
|
||||||
|
super.setItem(item);
|
||||||
|
|
||||||
|
P25FullyQualifiedRadio fqr = getItem();
|
||||||
|
|
||||||
|
getProtocolLabel().setDisable(fqr == null);
|
||||||
|
getWacnField().setDisable(fqr == null);
|
||||||
|
getSystemField().setDisable(fqr == null);
|
||||||
|
getRadioField().setDisable(fqr == null);
|
||||||
|
|
||||||
|
if(fqr != null)
|
||||||
|
{
|
||||||
|
getProtocolLabel().setText(fqr.getProtocol().toString());
|
||||||
|
updateTextFormatter();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
getWacnField().setText(null);
|
||||||
|
getSystemField().setText(null);
|
||||||
|
getRadioField().setText(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiedProperty().set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTextFormatter()
|
||||||
|
{
|
||||||
|
if(mWacnTextFormatter != null)
|
||||||
|
{
|
||||||
|
mWacnTextFormatter.valueProperty().removeListener(mWacnValueChangeListener);
|
||||||
|
}
|
||||||
|
if(mSystemTextFormatter != null)
|
||||||
|
{
|
||||||
|
mSystemTextFormatter.valueProperty().removeListener(mSystemValueChangeListener);
|
||||||
|
}
|
||||||
|
if(mRadioTextFormatter != null)
|
||||||
|
{
|
||||||
|
mRadioTextFormatter.valueProperty().removeListener(mRadioValueChangeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(getItem().getProtocol());
|
||||||
|
|
||||||
|
if(format == IntegerFormat.DECIMAL && (mRadioTextFormatter == null || !(mRadioTextFormatter instanceof IntegerFormatter)))
|
||||||
|
{
|
||||||
|
mWacnTextFormatter = new IntegerFormatter(0,0xFFFFF);
|
||||||
|
mSystemTextFormatter = new IntegerFormatter(0,0xFFF);
|
||||||
|
mRadioTextFormatter = new IntegerFormatter(0,0xFFFFFF);
|
||||||
|
|
||||||
|
mWacnField.setTooltip(new Tooltip("Format: 0 - 1048575"));
|
||||||
|
mSystemField.setTooltip(new Tooltip("Format: 0 - 4095"));
|
||||||
|
mRadioField.setTooltip(new Tooltip("Format: 0 - 16777215"));
|
||||||
|
}
|
||||||
|
else if(format == IntegerFormat.HEXADECIMAL && (mRadioTextFormatter == null || !(mRadioTextFormatter instanceof HexFormatter)))
|
||||||
|
{
|
||||||
|
mWacnTextFormatter = new HexFormatter(0,0xFFFFF);
|
||||||
|
mSystemTextFormatter = new HexFormatter(0,0xFFF);
|
||||||
|
mRadioTextFormatter = new HexFormatter(0,0xFFFFFF);
|
||||||
|
|
||||||
|
mWacnField.setTooltip(new Tooltip("Format: 0 - FFFFF"));
|
||||||
|
mSystemField.setTooltip(new Tooltip("Format: 0 - FFF"));
|
||||||
|
mRadioField.setTooltip(new Tooltip("Format: 0 - FFFFFF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||||
|
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||||
|
mRadioField.setTextFormatter(mRadioTextFormatter);
|
||||||
|
|
||||||
|
mWacnTextFormatter.setValue(getItem() != null ? getItem().getWacn() : null);
|
||||||
|
mSystemTextFormatter.setValue(getItem() != null ? getItem().getSystem() : null);
|
||||||
|
mRadioTextFormatter.setValue(getItem() != null ? getItem().getValue() : null);
|
||||||
|
|
||||||
|
mWacnTextFormatter.valueProperty().addListener(mWacnValueChangeListener);
|
||||||
|
mSystemTextFormatter.valueProperty().addListener(mSystemValueChangeListener);
|
||||||
|
mRadioTextFormatter.valueProperty().addListener(mRadioValueChangeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
private Label getProtocolLabel()
|
||||||
|
{
|
||||||
|
if(mProtocolLabel == null)
|
||||||
|
{
|
||||||
|
mProtocolLabel = new Label();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mProtocolLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getWacnField()
|
||||||
|
{
|
||||||
|
if(mWacnField == null)
|
||||||
|
{
|
||||||
|
mWacnField = new TextField();
|
||||||
|
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mWacnField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getSystemField()
|
||||||
|
{
|
||||||
|
if(mSystemField == null)
|
||||||
|
{
|
||||||
|
mSystemField = new TextField();
|
||||||
|
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mSystemField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getRadioField()
|
||||||
|
{
|
||||||
|
if(mRadioField == null)
|
||||||
|
{
|
||||||
|
mRadioField = new TextField();
|
||||||
|
mRadioField.setTextFormatter(mRadioTextFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadioField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WacnValueChangeListener implements ChangeListener<Integer>
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||||
|
{
|
||||||
|
if(getItem() != null)
|
||||||
|
{
|
||||||
|
getItem().setWacn(newValue != null ? newValue : 0);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SystemValueChangeListener implements ChangeListener<Integer>
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||||
|
{
|
||||||
|
if(getItem() != null)
|
||||||
|
{
|
||||||
|
getItem().setSystem(newValue != null ? newValue : 0);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RadioValueChangeListener implements ChangeListener<Integer>
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||||
|
{
|
||||||
|
if(getItem() != null)
|
||||||
|
{
|
||||||
|
getItem().setValue(newValue != null ? newValue : 0);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.talkgroup.P25FullyQualifiedTalkgroup;
|
||||||
|
import io.github.dsheirer.gui.control.HexFormatter;
|
||||||
|
import io.github.dsheirer.gui.control.IntegerFormatter;
|
||||||
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.geometry.HPos;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.TextFormatter;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor for P25 fully qualified talkgroup alias identifiers
|
||||||
|
*/
|
||||||
|
public class P25FullyQualifiedTalkgroupEditor extends IdentifierEditor<P25FullyQualifiedTalkgroup>
|
||||||
|
{
|
||||||
|
private static final Logger mLog = LoggerFactory.getLogger(P25FullyQualifiedTalkgroupEditor.class);
|
||||||
|
|
||||||
|
private UserPreferences mUserPreferences;
|
||||||
|
private Label mProtocolLabel;
|
||||||
|
private TextField mWacnField;
|
||||||
|
private TextField mSystemField;
|
||||||
|
private TextField mTalkgroupField;
|
||||||
|
private TextFormatter<Integer> mWacnTextFormatter;
|
||||||
|
private TextFormatter<Integer> mSystemTextFormatter;
|
||||||
|
private TextFormatter<Integer> mTalkgroupTextFormatter;
|
||||||
|
private WacnValueChangeListener mWacnValueChangeListener = new WacnValueChangeListener();
|
||||||
|
private SystemValueChangeListener mSystemValueChangeListener = new SystemValueChangeListener();
|
||||||
|
private TalkgroupValueChangeListener mTalkgroupValueChangeListener = new TalkgroupValueChangeListener();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param userPreferences for determining user preferred talkgroup formats
|
||||||
|
*/
|
||||||
|
public P25FullyQualifiedTalkgroupEditor(UserPreferences userPreferences)
|
||||||
|
{
|
||||||
|
mUserPreferences = userPreferences;
|
||||||
|
|
||||||
|
GridPane gridPane = new GridPane();
|
||||||
|
gridPane.setHgap(5);
|
||||||
|
gridPane.setVgap(3);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getProtocolLabel(), 0, 0);
|
||||||
|
gridPane.getChildren().add(getProtocolLabel());
|
||||||
|
|
||||||
|
Label valueLabel = new Label("WACN");
|
||||||
|
GridPane.setHalignment(valueLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(valueLabel, 1, 0);
|
||||||
|
gridPane.getChildren().add(valueLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getWacnField(), 2, 0);
|
||||||
|
gridPane.getChildren().add(getWacnField());
|
||||||
|
|
||||||
|
Label systemLabel = new Label("System");
|
||||||
|
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(systemLabel, 3, 0);
|
||||||
|
gridPane.getChildren().add(systemLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getSystemField(), 4, 0);
|
||||||
|
gridPane.getChildren().add(getSystemField());
|
||||||
|
|
||||||
|
Label radioLabel = new Label("Talkgroup");
|
||||||
|
GridPane.setHalignment(radioLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(radioLabel, 5, 0);
|
||||||
|
gridPane.getChildren().add(radioLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getTalkgroupField(), 6, 0);
|
||||||
|
gridPane.getChildren().add(getTalkgroupField());
|
||||||
|
|
||||||
|
getChildren().add(gridPane);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setItem(P25FullyQualifiedTalkgroup item)
|
||||||
|
{
|
||||||
|
super.setItem(item);
|
||||||
|
|
||||||
|
P25FullyQualifiedTalkgroup fqt = getItem();
|
||||||
|
|
||||||
|
getProtocolLabel().setDisable(fqt == null);
|
||||||
|
getWacnField().setDisable(fqt == null);
|
||||||
|
getSystemField().setDisable(fqt == null);
|
||||||
|
getTalkgroupField().setDisable(fqt == null);
|
||||||
|
|
||||||
|
if(fqt != null)
|
||||||
|
{
|
||||||
|
getProtocolLabel().setText(fqt.getProtocol().toString());
|
||||||
|
updateTextFormatter();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
getWacnField().setText(null);
|
||||||
|
getSystemField().setText(null);
|
||||||
|
getTalkgroupField().setText(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiedProperty().set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTextFormatter()
|
||||||
|
{
|
||||||
|
if(mWacnTextFormatter != null)
|
||||||
|
{
|
||||||
|
mWacnTextFormatter.valueProperty().removeListener(mWacnValueChangeListener);
|
||||||
|
}
|
||||||
|
if(mSystemTextFormatter != null)
|
||||||
|
{
|
||||||
|
mSystemTextFormatter.valueProperty().removeListener(mSystemValueChangeListener);
|
||||||
|
}
|
||||||
|
if(mTalkgroupTextFormatter != null)
|
||||||
|
{
|
||||||
|
mTalkgroupTextFormatter.valueProperty().removeListener(mTalkgroupValueChangeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(getItem().getProtocol());
|
||||||
|
|
||||||
|
if(format == IntegerFormat.DECIMAL && (mTalkgroupTextFormatter == null || !(mTalkgroupTextFormatter instanceof IntegerFormatter)))
|
||||||
|
{
|
||||||
|
mWacnTextFormatter = new IntegerFormatter(0,0xFFFFF);
|
||||||
|
mSystemTextFormatter = new IntegerFormatter(0,0xFFF);
|
||||||
|
mTalkgroupTextFormatter = new IntegerFormatter(0,0xFFFF);
|
||||||
|
|
||||||
|
mWacnField.setTooltip(new Tooltip("Format: 0 - 1048575"));
|
||||||
|
mSystemField.setTooltip(new Tooltip("Format: 0 - 4095"));
|
||||||
|
mTalkgroupField.setTooltip(new Tooltip("Format: 0 - 65535"));
|
||||||
|
}
|
||||||
|
else if(format == IntegerFormat.HEXADECIMAL && (mTalkgroupTextFormatter == null || !(mTalkgroupTextFormatter instanceof HexFormatter)))
|
||||||
|
{
|
||||||
|
mWacnTextFormatter = new HexFormatter(0,0xFFFFF);
|
||||||
|
mSystemTextFormatter = new HexFormatter(0,0xFFF);
|
||||||
|
mTalkgroupTextFormatter = new HexFormatter(0,0xFFFFFF);
|
||||||
|
|
||||||
|
mWacnField.setTooltip(new Tooltip("Format: 0 - FFFFF"));
|
||||||
|
mSystemField.setTooltip(new Tooltip("Format: 0 - FFF"));
|
||||||
|
mTalkgroupField.setTooltip(new Tooltip("Format: 0 - FFFF"));
|
||||||
|
}
|
||||||
|
|
||||||
|
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||||
|
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||||
|
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
|
||||||
|
|
||||||
|
mWacnTextFormatter.setValue(getItem() != null ? getItem().getWacn() : null);
|
||||||
|
mSystemTextFormatter.setValue(getItem() != null ? getItem().getSystem() : null);
|
||||||
|
mTalkgroupTextFormatter.setValue(getItem() != null ? getItem().getValue() : null);
|
||||||
|
|
||||||
|
mWacnTextFormatter.valueProperty().addListener(mWacnValueChangeListener);
|
||||||
|
mSystemTextFormatter.valueProperty().addListener(mSystemValueChangeListener);
|
||||||
|
mTalkgroupTextFormatter.valueProperty().addListener(mTalkgroupValueChangeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose()
|
||||||
|
{
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
private Label getProtocolLabel()
|
||||||
|
{
|
||||||
|
if(mProtocolLabel == null)
|
||||||
|
{
|
||||||
|
mProtocolLabel = new Label();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mProtocolLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getWacnField()
|
||||||
|
{
|
||||||
|
if(mWacnField == null)
|
||||||
|
{
|
||||||
|
mWacnField = new TextField();
|
||||||
|
mWacnField.setTextFormatter(mWacnTextFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mWacnField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getSystemField()
|
||||||
|
{
|
||||||
|
if(mSystemField == null)
|
||||||
|
{
|
||||||
|
mSystemField = new TextField();
|
||||||
|
mSystemField.setTextFormatter(mSystemTextFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mSystemField;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextField getTalkgroupField()
|
||||||
|
{
|
||||||
|
if(mTalkgroupField == null)
|
||||||
|
{
|
||||||
|
mTalkgroupField = new TextField();
|
||||||
|
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mTalkgroupField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WacnValueChangeListener implements ChangeListener<Integer>
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||||
|
{
|
||||||
|
if(getItem() != null)
|
||||||
|
{
|
||||||
|
getItem().setWacn(newValue != null ? newValue : 0);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SystemValueChangeListener implements ChangeListener<Integer>
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||||
|
{
|
||||||
|
if(getItem() != null)
|
||||||
|
{
|
||||||
|
getItem().setSystem(newValue != null ? newValue : 0);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TalkgroupValueChangeListener implements ChangeListener<Integer>
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
|
||||||
|
{
|
||||||
|
if(getItem() != null)
|
||||||
|
{
|
||||||
|
getItem().setValue(newValue != null ? newValue : 0);
|
||||||
|
modifiedProperty().set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.playlist.PlaylistManager;
|
|||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.rrapi.type.Agency;
|
import io.github.dsheirer.rrapi.type.Agency;
|
||||||
import io.github.dsheirer.rrapi.type.AgencyInfo;
|
import io.github.dsheirer.rrapi.type.AgencyInfo;
|
||||||
|
import io.github.dsheirer.rrapi.type.CountyInfo;
|
||||||
import io.github.dsheirer.service.radioreference.RadioReference;
|
import io.github.dsheirer.service.radioreference.RadioReference;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
@@ -158,8 +159,13 @@ public class AgencyEditor extends VBox
|
|||||||
ThreadPool.CACHED.submit(() -> {
|
ThreadPool.CACHED.submit(() -> {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
|
if (mLevel == Level.COUNTY && agency instanceof CountyAgency) {
|
||||||
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
|
final CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(-agency.getAgencyId());
|
||||||
|
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(countyInfo.getCategories()));
|
||||||
|
} else {
|
||||||
|
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
|
||||||
|
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(Throwable t)
|
catch(Throwable t)
|
||||||
{
|
{
|
||||||
@@ -196,11 +202,11 @@ public class AgencyEditor extends VBox
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
else if(o1.getName() == null)
|
else if(o1.getName() == null || o2 instanceof CountyAgency)
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
else if(o2.getName() == null)
|
else if(o2.getName() == null || o1 instanceof CountyAgency)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,9 +158,12 @@ public class AgencyFrequencyEditor extends GridPane
|
|||||||
|
|
||||||
if(categories != null && !categories.isEmpty())
|
if(categories != null && !categories.isEmpty())
|
||||||
{
|
{
|
||||||
getCategoryComboBox().getItems().add(ALL_CATEGORIES);
|
// Only allow the combined list of all categories if there aren't that many
|
||||||
|
if (categories.size() < 10) {
|
||||||
|
categories.add(0, ALL_CATEGORIES);
|
||||||
|
}
|
||||||
getCategoryComboBox().getItems().addAll(categories);
|
getCategoryComboBox().getItems().addAll(categories);
|
||||||
getCategoryComboBox().getSelectionModel().select(ALL_CATEGORIES);
|
getCategoryComboBox().getSelectionModel().select(categories.get(0));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2022 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.radioreference;
|
||||||
|
|
||||||
|
import io.github.dsheirer.rrapi.type.Agency;
|
||||||
|
import io.github.dsheirer.rrapi.type.CountyInfo;
|
||||||
|
|
||||||
|
public class CountyAgency extends Agency
|
||||||
|
{
|
||||||
|
public CountyAgency(CountyInfo countyInfo)
|
||||||
|
{
|
||||||
|
this.setAgencyId(-countyInfo.getCountyId());
|
||||||
|
this.setName(countyInfo.getName() + " County (All)");
|
||||||
|
this.setType(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.gui.playlist.radioreference;
|
|||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.rrapi.RadioReferenceException;
|
import io.github.dsheirer.rrapi.RadioReferenceException;
|
||||||
|
import io.github.dsheirer.rrapi.type.Agency;
|
||||||
import io.github.dsheirer.rrapi.type.AuthorizationInformation;
|
import io.github.dsheirer.rrapi.type.AuthorizationInformation;
|
||||||
import io.github.dsheirer.rrapi.type.Country;
|
import io.github.dsheirer.rrapi.type.Country;
|
||||||
import io.github.dsheirer.rrapi.type.CountryInfo;
|
import io.github.dsheirer.rrapi.type.CountryInfo;
|
||||||
@@ -55,6 +56,7 @@ import jiconfont.javafx.IconNode;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -689,7 +691,12 @@ public class RadioReferenceEditor extends BorderPane implements Consumer<Authori
|
|||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
getCountySystemEditor().setSystems(countyInfo.getSystems());
|
getCountySystemEditor().setSystems(countyInfo.getSystems());
|
||||||
getCountyAgencyEditor().setAgencies(countyInfo.getAgencies());
|
|
||||||
|
// Make a new list so we don't add our CountyAgency to the original list of agencies
|
||||||
|
List<Agency> countyAgencies = new ArrayList<Agency>();
|
||||||
|
countyAgencies.add(new CountyAgency(countyInfo));
|
||||||
|
countyAgencies.addAll(countyInfo.getAgencies());
|
||||||
|
getCountyAgencyEditor().setAgencies(countyAgencies);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch(RadioReferenceException rre)
|
catch(RadioReferenceException rre)
|
||||||
|
|||||||
@@ -294,7 +294,11 @@ public class SystemEditor extends VBox
|
|||||||
{
|
{
|
||||||
CountyInfo countyInfo = null;
|
CountyInfo countyInfo = null;
|
||||||
|
|
||||||
if(site.getCountyId() > 0)
|
int countyId = site.getCountyId();
|
||||||
|
|
||||||
|
//Temporary sites that have been added to a system where the county/location is unknown, can
|
||||||
|
//have a county ID of 99,999 as observed from API (undocumented).
|
||||||
|
if(countyId > 0 && countyId != 99999)
|
||||||
{
|
{
|
||||||
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
|
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public class StreamEditorFactory
|
|||||||
return new BroadcastifyStreamEditor(playlistManager);
|
return new BroadcastifyStreamEditor(playlistManager);
|
||||||
case RDIOSCANNER_CALL:
|
case RDIOSCANNER_CALL:
|
||||||
return new RdioScannerEditor(playlistManager);
|
return new RdioScannerEditor(playlistManager);
|
||||||
|
case OPENMHZ:
|
||||||
|
return new OpenMHzEditor(playlistManager);
|
||||||
case BROADCASTIFY_CALL:
|
case BROADCASTIFY_CALL:
|
||||||
return new BroadcastifyCallEditor(playlistManager);
|
return new BroadcastifyCallEditor(playlistManager);
|
||||||
case ICECAST_HTTP:
|
case ICECAST_HTTP:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.eventbus.MyEventBus;
|
|||||||
import io.github.dsheirer.preference.PreferenceType;
|
import io.github.dsheirer.preference.PreferenceType;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.preference.directory.DirectoryPreference;
|
import io.github.dsheirer.preference.directory.DirectoryPreference;
|
||||||
|
import java.io.File;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
@@ -31,6 +32,7 @@ import javafx.geometry.Orientation;
|
|||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Separator;
|
import javafx.scene.control.Separator;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
@@ -39,8 +41,6 @@ import javafx.stage.Stage;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference settings for channel event view
|
* Preference settings for channel event view
|
||||||
@@ -97,6 +97,9 @@ public class DirectoryPreferenceEditor extends HBox
|
|||||||
private Button mResetStreamingButton;
|
private Button mResetStreamingButton;
|
||||||
private Label mStreamingPathLabel;
|
private Label mStreamingPathLabel;
|
||||||
|
|
||||||
|
private Spinner<Integer> mRecordingSpinner;
|
||||||
|
private Spinner<Integer> mEventLogSpinner;
|
||||||
|
|
||||||
public DirectoryPreferenceEditor(UserPreferences userPreferences)
|
public DirectoryPreferenceEditor(UserPreferences userPreferences)
|
||||||
{
|
{
|
||||||
mDirectoryPreference = userPreferences.getDirectoryPreference();
|
mDirectoryPreference = userPreferences.getDirectoryPreference();
|
||||||
@@ -247,11 +250,57 @@ public class DirectoryPreferenceEditor extends HBox
|
|||||||
|
|
||||||
GridPane.setMargin(getResetStreamingButton(), new Insets(2, 0, 2, 0));
|
GridPane.setMargin(getResetStreamingButton(), new Insets(2, 0, 2, 0));
|
||||||
mEditorPane.add(getResetStreamingButton(), 3, row++);
|
mEditorPane.add(getResetStreamingButton(), 3, row++);
|
||||||
|
|
||||||
|
Label monitorLabel = new Label("File storage usage monitoring - maximum size thresholds (MB)");
|
||||||
|
GridPane.setMargin(monitorLabel, new Insets(15, 0, 2, 0));
|
||||||
|
mEditorPane.add(monitorLabel, 0, row++, 4, 1);
|
||||||
|
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, row++, 4, 1);
|
||||||
|
|
||||||
|
mEditorPane.add(new Label("Event Logs"), 0, row);
|
||||||
|
GridPane.setMargin(getEventLogSpinner(), new Insets(2, 2, 2, 0));
|
||||||
|
mEditorPane.add(getEventLogSpinner(), 1, row++);
|
||||||
|
|
||||||
|
mEditorPane.add(new Label("Recordings"), 0, row);
|
||||||
|
GridPane.setMargin(getRecordingSpinner(), new Insets(2, 2, 2, 0));
|
||||||
|
mEditorPane.add(getRecordingSpinner(), 1, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mEditorPane;
|
return mEditorPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recording directory maximum size threshold spinner
|
||||||
|
* @return spinner
|
||||||
|
*/
|
||||||
|
private Spinner<Integer> getRecordingSpinner()
|
||||||
|
{
|
||||||
|
if(mRecordingSpinner == null)
|
||||||
|
{
|
||||||
|
mRecordingSpinner = new Spinner<>(100, Integer.MAX_VALUE, mDirectoryPreference.getDirectoryMaxUsageRecordings(), 100);
|
||||||
|
mRecordingSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mDirectoryPreference
|
||||||
|
.setDirectoryMaxUsageRecordings(newValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRecordingSpinner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event log directory maximum size threshold spinner
|
||||||
|
* @return spinner
|
||||||
|
*/
|
||||||
|
private Spinner<Integer> getEventLogSpinner()
|
||||||
|
{
|
||||||
|
if(mEventLogSpinner == null)
|
||||||
|
{
|
||||||
|
mEventLogSpinner = new Spinner<>(100, Integer.MAX_VALUE, mDirectoryPreference.getDirectoryMaxUsageEventLogs(), 100);
|
||||||
|
mEventLogSpinner.setEditable(true);
|
||||||
|
mEventLogSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mDirectoryPreference
|
||||||
|
.setDirectoryMaxUsageEventLogs(newValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mEventLogSpinner;
|
||||||
|
}
|
||||||
|
|
||||||
private Label getApplicationRootLabel()
|
private Label getApplicationRootLabel()
|
||||||
{
|
{
|
||||||
if(mApplicationRootLabel == null)
|
if(mApplicationRootLabel == null)
|
||||||
|
|||||||
@@ -69,9 +69,17 @@ public class RecordingViewer extends VBox
|
|||||||
|
|
||||||
Menu createNewViewerMenu = new Menu("New Viewer ...");
|
Menu createNewViewerMenu = new Menu("New Viewer ...");
|
||||||
MenuItem dmrMenuItem = new MenuItem("DMR");
|
MenuItem dmrMenuItem = new MenuItem("DMR");
|
||||||
dmrMenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer())));
|
dmrMenuItem.onActionProperty().set(event -> {
|
||||||
|
Tab tab = new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer());
|
||||||
|
getTabPane().getTabs().add(tab);
|
||||||
|
getTabPane().getSelectionModel().select(tab);
|
||||||
|
});
|
||||||
MenuItem p25p1MenuItem = new MenuItem("P25P1");
|
MenuItem p25p1MenuItem = new MenuItem("P25P1");
|
||||||
p25p1MenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer())));
|
p25p1MenuItem.onActionProperty().set(event -> {
|
||||||
|
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer());
|
||||||
|
getTabPane().getTabs().add(tab);
|
||||||
|
getTabPane().getSelectionModel().select(tab);
|
||||||
|
});
|
||||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
|
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
|
||||||
|
|
||||||
MenuItem exitMenu = new MenuItem("Exit");
|
MenuItem exitMenu = new MenuItem("Exit");
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ public enum Form
|
|||||||
DTMF,
|
DTMF,
|
||||||
ENCRYPTION_KEY,
|
ENCRYPTION_KEY,
|
||||||
ESN,
|
ESN,
|
||||||
FULLY_QUALIFIED_IDENTIFIER,
|
|
||||||
IPV4_ADDRESS,
|
IPV4_ADDRESS,
|
||||||
KNOX_TONE,
|
KNOX_TONE,
|
||||||
LOCATION,
|
LOCATION,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.identifier.alias;
|
||||||
|
|
||||||
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Talker alias value provided by the network for the current talker (ie FROM).
|
||||||
|
*/
|
||||||
|
public class P25TalkerAliasIdentifier extends TalkerAliasIdentifier
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructs an instance.
|
||||||
|
* @param value of the talker alias
|
||||||
|
*/
|
||||||
|
public P25TalkerAliasIdentifier(String value)
|
||||||
|
{
|
||||||
|
super(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid()
|
||||||
|
{
|
||||||
|
return getValue() != null && !getValue().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Protocol getProtocol()
|
||||||
|
{
|
||||||
|
return Protocol.APCO25;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static P25TalkerAliasIdentifier create(String value)
|
||||||
|
{
|
||||||
|
return new P25TalkerAliasIdentifier(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -284,11 +284,27 @@ public class DMRBurstFramer implements Listener<Dibit>
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//When we don't have sufficient timeslot tracking data, dump the messages to timeslot 0 so that they
|
//When we don't have sufficient timeslot tracking data, we either have misaligned timeslots from a
|
||||||
//don't corrupt the decoder states for either timeslot, until we positively regain timeslot tracking
|
//repeater or we have some form of mobile burst. If mobile, process to the correct timeslto, otherwise
|
||||||
|
//dump the messages to timeslot 0 so that the burst doesn't corrupt the decoder states for either
|
||||||
|
// timeslot, until we positively regain timeslot tracking
|
||||||
if(burst1 != null)
|
if(burst1 != null)
|
||||||
{
|
{
|
||||||
mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 0);
|
DMRSyncPattern sync1 = mSyncTrackerTimeslot1.getSyncPattern();
|
||||||
|
|
||||||
|
if(sync1.isMobileStationSyncPattern() || sync1.isDirectModeTS1())
|
||||||
|
{
|
||||||
|
mBurstDetectListener.burstDetected(burst1, sync1, 1);
|
||||||
|
}
|
||||||
|
else if(sync1.isDirectModeTS2())
|
||||||
|
{
|
||||||
|
mBurstDetectListener.burstDetected(burst1, sync1, 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mBurstDetectListener.burstDetected(burst1, sync1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -298,7 +314,21 @@ public class DMRBurstFramer implements Listener<Dibit>
|
|||||||
|
|
||||||
if(burst2 != null)
|
if(burst2 != null)
|
||||||
{
|
{
|
||||||
mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 0);
|
DMRSyncPattern sync2 = mSyncTrackerTimeslot2.getSyncPattern();
|
||||||
|
|
||||||
|
if(sync2.isMobileStationSyncPattern() || sync2.isDirectModeTS1())
|
||||||
|
{
|
||||||
|
mBurstDetectListener.burstDetected(burst2, sync2, 1);
|
||||||
|
}
|
||||||
|
else if(sync2.isDirectModeTS2())
|
||||||
|
{
|
||||||
|
mBurstDetectListener.burstDetected(burst2, sync2, 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mBurstDetectListener.burstDetected(burst2, sync2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
mDibitCounter -= BURST_DIBIT_LENGTH;
|
mDibitCounter -= BURST_DIBIT_LENGTH;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
|||||||
import io.github.dsheirer.source.ISourceEventListener;
|
import io.github.dsheirer.source.ISourceEventListener;
|
||||||
import io.github.dsheirer.source.ISourceEventProvider;
|
import io.github.dsheirer.source.ISourceEventProvider;
|
||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
|
import io.github.dsheirer.source.wave.ComplexWaveSource;
|
||||||
|
import java.io.File;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -332,4 +335,38 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
|||||||
{
|
{
|
||||||
return DMRDecoder.this;
|
return DMRDecoder.this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
File file = new File("/home/denny/Downloads/TIII,HYT414.585000,.wav");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ComplexWaveSource source = new ComplexWaveSource(file);
|
||||||
|
source.open();
|
||||||
|
System.out.println("Source Sample Rate: " + source.getSampleRate());
|
||||||
|
DMRDecoder decoder = new DMRDecoder(new DecodeConfigDMR());
|
||||||
|
decoder.setMessageListener(message -> System.out.println("TS:" + message.getTimeslot() + " " + message));
|
||||||
|
source.setListener(iNativeBuffer -> {
|
||||||
|
Iterator<ComplexSamples> it = iNativeBuffer.iterator();
|
||||||
|
while(it.hasNext())
|
||||||
|
{
|
||||||
|
ComplexSamples samples = it.next();
|
||||||
|
decoder.receive(samples);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
decoder.setSampleRate(source.getSampleRate());
|
||||||
|
decoder.start();
|
||||||
|
source.start();
|
||||||
|
|
||||||
|
while(true)
|
||||||
|
{
|
||||||
|
source.next(65535);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
|||||||
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
|
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
|
import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTrafficChannelTalkerStatus;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus;
|
||||||
@@ -445,11 +446,18 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
*/
|
*/
|
||||||
private void processVoice(VoiceMessage message)
|
private void processVoice(VoiceMessage message)
|
||||||
{
|
{
|
||||||
if(message.getSyncPattern().isDirectMode())
|
if(message.getSyncPattern().isMobileSyncPattern())
|
||||||
{
|
{
|
||||||
updateCurrentCall(DecodeEventType.CALL, "DIRECT MODE", message.getTimestamp());
|
if(message.getSyncPattern().isDirectMode())
|
||||||
|
{
|
||||||
|
updateCurrentCall(DecodeEventType.CALL, "DIRECT MODE", message.getTimestamp());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
updateCurrentCall(DecodeEventType.CALL, "REPEATER", message.getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
//Use the timeslot as the talkgroup identifier since DCDM mode doesn't use talkgroups
|
//Use the timeslot as the talkgroup identifier since DCDM & simple repeater modes don't use talkgroups
|
||||||
getIdentifierCollection().update(DMRTalkgroup.create(getTimeslot()));
|
getIdentifierCollection().update(DMRTalkgroup.create(getTimeslot()));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -701,7 +709,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
|||||||
case HYTERA_08_ANNOUNCEMENT:
|
case HYTERA_08_ANNOUNCEMENT:
|
||||||
case HYTERA_68_ANNOUNCEMENT:
|
case HYTERA_68_ANNOUNCEMENT:
|
||||||
case HYTERA_68_XPT_SITE_STATE:
|
case HYTERA_68_XPT_SITE_STATE:
|
||||||
|
break;
|
||||||
|
case HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS:
|
||||||
|
if(csbk instanceof HyteraTrafficChannelTalkerStatus status)
|
||||||
|
{
|
||||||
|
if(status.isChannelActive())
|
||||||
|
{
|
||||||
|
getIdentifierCollection().update(status.getIdentifiers());
|
||||||
|
updateCurrentCall(DecodeEventType.CALL_GROUP, "HYTERA TIER 3 CALL", status.getTimestamp());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
getIdentifierCollection().remove(Role.FROM);
|
||||||
|
getIdentifierCollection().update(status.getDestinationRadio());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case MOTOROLA_CAPPLUS_NEIGHBOR_REPORT:
|
case MOTOROLA_CAPPLUS_NEIGHBOR_REPORT:
|
||||||
if(csbk instanceof CapacityPlusNeighbors)
|
if(csbk instanceof CapacityPlusNeighbors)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.module.decode.dmr.message.CACH;
|
|||||||
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
|
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
|
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
|
||||||
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.csbk.CSBKMessage;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement;
|
||||||
@@ -71,6 +72,7 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
|||||||
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
|
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
|
||||||
private Listener<IMessage> mMessageListener;
|
private Listener<IMessage> mMessageListener;
|
||||||
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
|
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
|
||||||
|
private DmrCrcMaskManager mCrcMaskManager = new DmrCrcMaskManager();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an instance
|
* Constructs an instance
|
||||||
@@ -93,6 +95,17 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
|||||||
@Override
|
@Override
|
||||||
public void receive(IMessage message)
|
public void receive(IMessage message)
|
||||||
{
|
{
|
||||||
|
if(message == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Detect and correct messages employing an alternate CRC mask pattern.
|
||||||
|
if(!message.isValid() && message instanceof CSBKMessage csbk)
|
||||||
|
{
|
||||||
|
mCrcMaskManager.check(csbk);
|
||||||
|
}
|
||||||
|
|
||||||
if(message instanceof FullLCMessage flc)
|
if(message instanceof FullLCMessage flc)
|
||||||
{
|
{
|
||||||
if(flc.getTimeslot() == 1)
|
if(flc.getTimeslot() == 1)
|
||||||
@@ -153,10 +166,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
|||||||
dispatch(message);
|
dispatch(message);
|
||||||
|
|
||||||
//Extract the Full Link Control message fragment from the Voice with embedded signalling message
|
//Extract the Full Link Control message fragment from the Voice with embedded signalling message
|
||||||
if(message instanceof VoiceEMBMessage)
|
if(message instanceof VoiceEMBMessage voice)
|
||||||
{
|
{
|
||||||
VoiceEMBMessage voice = (VoiceEMBMessage)message;
|
|
||||||
|
|
||||||
if(message.getTimeslot() == 1)
|
if(message.getTimeslot() == 1)
|
||||||
{
|
{
|
||||||
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
|
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -91,9 +91,17 @@ public enum DMRSyncPattern
|
|||||||
private static final EnumSet<DMRSyncPattern> CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
|
private static final EnumSet<DMRSyncPattern> CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
|
||||||
BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F);
|
BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F);
|
||||||
|
|
||||||
//Direct Mode Sync Patterns
|
//Direct Mode TS 1 Sync Patterns
|
||||||
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_PATTERNS = EnumSet.of(DIRECT_MODE_DATA_TIMESLOT_1,
|
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS1_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_1,
|
||||||
DIRECT_MODE_DATA_TIMESLOT_2, DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2);
|
DIRECT_MODE_DATA_TIMESLOT_1);
|
||||||
|
|
||||||
|
//Direct Mode TS 2 Sync Patterns
|
||||||
|
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS2_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_2,
|
||||||
|
DIRECT_MODE_DATA_TIMESLOT_2);
|
||||||
|
|
||||||
|
//Mobile Station Sync Patterns
|
||||||
|
private static final EnumSet<DMRSyncPattern> MOBILE_STATION_PATTERNS = EnumSet.of(MOBILE_STATION_DATA,
|
||||||
|
MOBILE_STATION_VOICE, MS_VOICE_FRAME_B, MS_VOICE_FRAME_C, MS_VOICE_FRAME_D, MS_VOICE_FRAME_E, MS_VOICE_FRAME_F);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pattern that represents the enum entry
|
* Pattern that represents the enum entry
|
||||||
@@ -140,7 +148,43 @@ public enum DMRSyncPattern
|
|||||||
*/
|
*/
|
||||||
public boolean isDirectMode()
|
public boolean isDirectMode()
|
||||||
{
|
{
|
||||||
return DIRECT_MODE_PATTERNS.contains(this);
|
return isDirectModeTS1() || isDirectModeTS2();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this is Direct Mode sync pattern for timestot 1
|
||||||
|
* @return true if DMTS1
|
||||||
|
*/
|
||||||
|
public boolean isDirectModeTS1()
|
||||||
|
{
|
||||||
|
return DIRECT_MODE_TS1_PATTERNS.contains(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this is Direct Mode sync pattern for timestot 2
|
||||||
|
* @return true if DMTS2
|
||||||
|
*/
|
||||||
|
public boolean isDirectModeTS2()
|
||||||
|
{
|
||||||
|
return DIRECT_MODE_TS2_PATTERNS.contains(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this is a mobile station sync pattern
|
||||||
|
* @return true if a mobile station sync pattern.
|
||||||
|
*/
|
||||||
|
public boolean isMobileStationSyncPattern()
|
||||||
|
{
|
||||||
|
return MOBILE_STATION_PATTERNS.contains(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this is a mobile sync pattern, either mobile station or direct mode.
|
||||||
|
* @return true if a mobile sync pattern.
|
||||||
|
*/
|
||||||
|
public boolean isMobileSyncPattern()
|
||||||
|
{
|
||||||
|
return isMobileStationSyncPattern() || isDirectMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import io.github.dsheirer.edac.CRCDMR;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages alternate CRC masks that may be employed in a system.
|
||||||
|
*/
|
||||||
|
public class DmrCrcMaskManager
|
||||||
|
{
|
||||||
|
private Logger LOGGER = LoggerFactory.getLogger(DmrCrcMaskManager.class);
|
||||||
|
private Map<Integer, ResidualCrcMaskTracker> mCsbkResidualTrackerMap = new TreeMap<>();
|
||||||
|
private int mDominantMask = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*/
|
||||||
|
public DmrCrcMaskManager()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a deep copy (ie clone) of all tracked values
|
||||||
|
* @return list of tracker clones.
|
||||||
|
*/
|
||||||
|
public List<ResidualCrcMaskTracker> cloneTrackers()
|
||||||
|
{
|
||||||
|
List<ResidualCrcMaskTracker>trackers = new ArrayList<>();
|
||||||
|
for(ResidualCrcMaskTracker tracker: mCsbkResidualTrackerMap.values())
|
||||||
|
{
|
||||||
|
trackers.add(tracker.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the current tracked CRC values and counts.
|
||||||
|
*/
|
||||||
|
public void log()
|
||||||
|
{
|
||||||
|
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
|
||||||
|
Collections.sort(trackers);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("DMR CRC Mask Manager - Current Tracked Values\n");
|
||||||
|
for(ResidualCrcMaskTracker tracker: trackers)
|
||||||
|
{
|
||||||
|
sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue()));
|
||||||
|
sb.append(" Count:").append(tracker.getCount());
|
||||||
|
sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the CSBK message that has a failed CRC checksum to detect if an alternate CRC mask value is employed and
|
||||||
|
* if so, attempts to correct the message by using the alternate mask value once the alternate mask value is
|
||||||
|
* detected to be used consistently enough.
|
||||||
|
* @param csbk to re-check.
|
||||||
|
*/
|
||||||
|
public void check(CSBKMessage csbk)
|
||||||
|
{
|
||||||
|
if(!csbk.isValid())
|
||||||
|
{
|
||||||
|
int residual = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80);
|
||||||
|
|
||||||
|
if(mCsbkResidualTrackerMap.containsKey(residual))
|
||||||
|
{
|
||||||
|
mCsbkResidualTrackerMap.get(residual).increment();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mCsbkResidualTrackerMap.put(residual, new ResidualCrcMaskTracker(residual));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
|
||||||
|
Collections.sort(trackers);
|
||||||
|
|
||||||
|
if(trackers.size() > 5)
|
||||||
|
{
|
||||||
|
mCsbkResidualTrackerMap.remove(trackers.get(0).getTrackedValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(trackers.size() > 0)
|
||||||
|
{
|
||||||
|
ResidualCrcMaskTracker dominant = trackers.get(trackers.size() - 1);
|
||||||
|
|
||||||
|
if(dominant.isValid())
|
||||||
|
{
|
||||||
|
mDominantMask = dominant.getTrackedValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mDominantMask != 0)
|
||||||
|
{
|
||||||
|
csbk.checkCRC(mDominantMask);
|
||||||
|
//
|
||||||
|
// if(csbk.isValid())
|
||||||
|
// {
|
||||||
|
// LOGGER.info("CSBK fixed using alternate mask: " + Integer.toHexString(mDominantMask).toUpperCase());
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Residual CRC mask value tracker. Tracks the number of observances of a residual CRC calculated value to detect
|
||||||
|
* when an alternate CRC mask pattern is employed and automatically correct CRC values for messages.
|
||||||
|
*/
|
||||||
|
public class ResidualCrcMaskTracker implements Comparable<ResidualCrcMaskTracker>
|
||||||
|
{
|
||||||
|
private int mTrackedValue;
|
||||||
|
private int mCount;
|
||||||
|
private long mLastUpdated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance for the specified residual, sets the count to one and sets the last update to now.
|
||||||
|
* @param residual value to track.
|
||||||
|
*/
|
||||||
|
public ResidualCrcMaskTracker(int residual)
|
||||||
|
{
|
||||||
|
mTrackedValue = residual;
|
||||||
|
mCount = 1;
|
||||||
|
mLastUpdated = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a deep copy of this instance.
|
||||||
|
* @return cloned instance.
|
||||||
|
*/
|
||||||
|
public ResidualCrcMaskTracker clone()
|
||||||
|
{
|
||||||
|
ResidualCrcMaskTracker clone = new ResidualCrcMaskTracker(getTrackedValue());
|
||||||
|
clone.mCount = mCount;
|
||||||
|
clone.mLastUpdated = mLastUpdated;
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count of number of times this residual has been observed.
|
||||||
|
* @return count
|
||||||
|
*/
|
||||||
|
public int getCount()
|
||||||
|
{
|
||||||
|
return mCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the residual value has been observed at least 3x times indicating that it is possibly valid.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isValid()
|
||||||
|
{
|
||||||
|
return mCount >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when this value was last observed.
|
||||||
|
*/
|
||||||
|
public long getLastUpdated()
|
||||||
|
{
|
||||||
|
return mLastUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Residual tracked value.
|
||||||
|
* @return residual
|
||||||
|
*/
|
||||||
|
public int getTrackedValue()
|
||||||
|
{
|
||||||
|
return mTrackedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void increment()
|
||||||
|
{
|
||||||
|
mCount++;
|
||||||
|
//Prevent rollover by keeping value at max integer value.
|
||||||
|
if(mCount < 0)
|
||||||
|
{
|
||||||
|
mCount = Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
mLastUpdated = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom sort order by count and then by last updated timestamp.
|
||||||
|
* @param o the object to be compared.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compareTo(ResidualCrcMaskTracker o)
|
||||||
|
{
|
||||||
|
int comparison = Integer.compare(getCount(), o.getCount());
|
||||||
|
|
||||||
|
if(comparison == 0)
|
||||||
|
{
|
||||||
|
comparison = Long.compare(getLastUpdated(), o.getLastUpdated());
|
||||||
|
}
|
||||||
|
|
||||||
|
return comparison;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.identifier;
|
||||||
|
|
||||||
|
import io.github.dsheirer.identifier.Role;
|
||||||
|
import io.github.dsheirer.identifier.location.LocationIdentifier;
|
||||||
|
import io.github.dsheirer.identifier.location.Point;
|
||||||
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P25 location identifier for user radio reported GPS positions.
|
||||||
|
*/
|
||||||
|
public class P25Location extends LocationIdentifier
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*
|
||||||
|
* @param value location
|
||||||
|
* @param role to or from
|
||||||
|
*/
|
||||||
|
public P25Location(Point value, Role role)
|
||||||
|
{
|
||||||
|
super(value, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Protocol getProtocol()
|
||||||
|
{
|
||||||
|
return Protocol.APCO25;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to create a new DMR location
|
||||||
|
* @param latitude of the position
|
||||||
|
* @param longitude of the position
|
||||||
|
* @return constructed DMR location identifier
|
||||||
|
*/
|
||||||
|
public static P25Location createFrom(double latitude, double longitude)
|
||||||
|
{
|
||||||
|
Point point = new Point(latitude, longitude);
|
||||||
|
return new P25Location(point, Role.FROM);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao
|
|
||||||
*
|
*
|
||||||
* 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
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
package io.github.dsheirer.module.decode.dmr.message.data.csbk;
|
package io.github.dsheirer.module.decode.dmr.message.data.csbk;
|
||||||
|
|
||||||
@@ -72,6 +71,17 @@ public abstract class CSBKMessage extends DataMessage
|
|||||||
setValid(correctedBitCount < 2);
|
setValid(correctedBitCount < 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks CRC using an alternate mask value and sets the message valid flag according to the results.
|
||||||
|
*/
|
||||||
|
public void checkCRC(int mask)
|
||||||
|
{
|
||||||
|
int correctedBitCount = CRCDMR.correctCCITT80(getMessage(), 0, 80, mask);
|
||||||
|
|
||||||
|
//Set message valid flag according to the corrected bit count for the CRC protected message
|
||||||
|
setValid(correctedBitCount < 2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks crc for multi-block CSBK where the continuation block is an absolute channel parameters block
|
* Checks crc for multi-block CSBK where the continuation block is an absolute channel parameters block
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAdjac
|
|||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAloha;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAloha;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAnnouncement;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAnnouncement;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko44;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko44;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko47;
|
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraSmsAvailableNotification;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraSmsAvailableNotification;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraTrafficChannelTalkerStatus;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTPreamble;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTPreamble;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTSiteState;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTSiteState;
|
||||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
|
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
|
||||||
@@ -250,8 +250,8 @@ public class CSBKMessageFactory
|
|||||||
case HYTERA_08_CSBKO_44:
|
case HYTERA_08_CSBKO_44:
|
||||||
csbk = new HyteraCsbko44(pattern, message, cach, slotType, timestamp, timeslot);
|
csbk = new HyteraCsbko44(pattern, message, cach, slotType, timestamp, timeslot);
|
||||||
break;
|
break;
|
||||||
case HYTERA_08_CSBKO_47:
|
case HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS:
|
||||||
csbk = new HyteraCsbko47(pattern, message, cach, slotType, timestamp, timeslot);
|
csbk = new HyteraTrafficChannelTalkerStatus(pattern, message, cach, slotType, timestamp, timeslot);
|
||||||
break;
|
break;
|
||||||
case HYTERA_68_ACKNOWLEDGE:
|
case HYTERA_68_ACKNOWLEDGE:
|
||||||
csbk = new Hytera68Acknowledge(pattern, message, cach, slotType, timestamp, timeslot);
|
csbk = new Hytera68Acknowledge(pattern, message, cach, slotType, timestamp, timeslot);
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public enum Opcode
|
|||||||
//CSBKO 44 & 47 observed on Tier3 interleaved in voice group call terminator sequence. 44 was continuously transmitted
|
//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.
|
//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_TRAFFIC_CHANNEL_TALKER_STATUS(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"),
|
||||||
|
|
||||||
HYTERA_68_XPT_SITE_STATE(Vendor.HYTERA_68, 10, "HYTERA 68 XPT SITE STATE"),
|
HYTERA_68_XPT_SITE_STATE(Vendor.HYTERA_68, 10, "HYTERA 68 XPT SITE STATE"),
|
||||||
HYTERA_68_ALOHA(Vendor.HYTERA_68, 25, "HYTERA 68 ALOHA"),
|
HYTERA_68_ALOHA(Vendor.HYTERA_68, 25, "HYTERA 68 ALOHA"),
|
||||||
@@ -201,7 +201,7 @@ public enum Opcode
|
|||||||
* Hytera opcodes
|
* Hytera opcodes
|
||||||
*/
|
*/
|
||||||
public static final EnumSet<Opcode> HYTERA = EnumSet.of(HYTERA_08_ACKNOWLEDGE, HYTERA_08_ANNOUNCEMENT,
|
public static final EnumSet<Opcode> HYTERA = EnumSet.of(HYTERA_08_ACKNOWLEDGE, HYTERA_08_ANNOUNCEMENT,
|
||||||
HYTERA_08_CSBKO_44, HYTERA_08_CSBKO_47, HYTERA_68_XPT_SITE_STATE, HYTERA_68_ALOHA,
|
HYTERA_08_CSBKO_44, HYTERA_08_TRAFFIC_CHANNEL_TALKER_STATUS, HYTERA_68_XPT_SITE_STATE, HYTERA_68_ALOHA,
|
||||||
HYTERA_68_ACKNOWLEDGE, HYTERA_68_ANNOUNCEMENT, HYTERA_68_XPT_PREAMBLE, HYTERA_68_CSBKO_62);
|
HYTERA_68_ACKNOWLEDGE, HYTERA_68_ANNOUNCEMENT, HYTERA_68_XPT_PREAMBLE, HYTERA_68_CSBKO_62);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ import java.util.List;
|
|||||||
* Analysis: ... this was transmitted on the Tier III traffic channel during call termination. This CSBK was
|
* Analysis: ... this was transmitted on the Tier III traffic channel during call termination. This CSBK was
|
||||||
* interleaved with every other Terminator message, starting after the first 2x terminator messages. This continued
|
* interleaved with every other Terminator message, starting after the first 2x terminator messages. This continued
|
||||||
* through the final teardown which was 3x Clear messages.
|
* through the final teardown which was 3x Clear messages.
|
||||||
|
*
|
||||||
|
* On a second example, there were 8 distinct configurations of this message that were repeated in-order, multiple times
|
||||||
|
* during the traffic channel shutdown ... a total of 51x of these messages were transmitted.
|
||||||
*/
|
*/
|
||||||
public class HyteraCsbko44 extends CSBKMessage
|
public class HyteraCsbko44 extends CSBKMessage
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* Hytera DMR Tier III - CSBKO 47 -
|
* Hytera DMR Tier III - CSBKO 47 -
|
||||||
*
|
*
|
||||||
* Analysis: ... this was transmitted on the traffic channel while the mobile was at the end of a call, in the terminator
|
* Analysis: this message is transmitted three times at the beginning or end of a voice transmission and appears to
|
||||||
* sequence. Since this comes from the SMS gateway 16776906, I wonder if this is a traffic channel message waiting notification?
|
* indicate that the traffic channel continues active and signals the start/end of an active speaker.
|
||||||
*/
|
*/
|
||||||
public class HyteraCsbko47 extends CSBKMessage
|
public class HyteraTrafficChannelTalkerStatus extends CSBKMessage
|
||||||
{
|
{
|
||||||
private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
|
private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
|
||||||
private static final int[] UNKNOWN_2 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
|
private static final int[] UNKNOWN_2 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
|
||||||
|
private static final int TALKER_INACTIVE = 30;
|
||||||
private static final int[] DESTINATION_RADIO = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
|
private static final int[] DESTINATION_RADIO = 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};
|
49, 50, 51, 52, 53, 54, 55};
|
||||||
private static final int[] SOURCE_RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
|
private static final int[] SOURCE_RADIO = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
|
||||||
@@ -59,7 +60,7 @@ public class HyteraCsbko47 extends CSBKMessage
|
|||||||
* @param timestamp
|
* @param timestamp
|
||||||
* @param timeslot
|
* @param timeslot
|
||||||
*/
|
*/
|
||||||
public HyteraCsbko47(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
public HyteraTrafficChannelTalkerStatus(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);
|
||||||
}
|
}
|
||||||
@@ -79,17 +80,39 @@ public class HyteraCsbko47 extends CSBKMessage
|
|||||||
{
|
{
|
||||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||||
}
|
}
|
||||||
sb.append(" HYTERA CSBKO=47 ######### UNKNOWN");
|
sb.append(" HYTERA TRAFFIC CHANNEL");
|
||||||
|
if(isChannelInactive())
|
||||||
|
{
|
||||||
|
sb.append(" NOW AVAILABLE - PREVIOUS TALKER:").append(getDestinationRadio());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append(" IN USE - CURRENT TALKER:").append(getDestinationRadio());
|
||||||
|
}
|
||||||
sb.append(" FM:").append(getSourceRadio());
|
sb.append(" FM:").append(getSourceRadio());
|
||||||
sb.append(" TO:").append(getDestinationRadio());
|
|
||||||
sb.append(" UNK1:").append(getUnknown1());
|
sb.append(" UNK1:").append(getUnknown1());
|
||||||
sb.append(" UNK2:").append(getUnknown2());
|
sb.append(" UNK2:").append(getUnknown2());
|
||||||
|
|
||||||
sb.append(" MSG:").append(getMessage().toHexString());
|
sb.append(" MSG:").append(getMessage().toHexString());
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the channel is inactive and available for use.
|
||||||
|
*/
|
||||||
|
public boolean isChannelInactive()
|
||||||
|
{
|
||||||
|
return getMessage().get(TALKER_INACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the channel is active and in-use by the radio identifiers
|
||||||
|
*/
|
||||||
|
public boolean isChannelActive()
|
||||||
|
{
|
||||||
|
return !isChannelInactive();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Addressed (ie TO) radio identifier
|
* Addressed (ie TO) radio identifier
|
||||||
* @return radio identifier
|
* @return radio identifier
|
||||||
@@ -108,7 +131,6 @@ public class HyteraCsbko47 extends CSBKMessage
|
|||||||
{
|
{
|
||||||
return getMessage().getHex(UNKNOWN_1, 2);
|
return getMessage().getHex(UNKNOWN_1, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUnknown2()
|
public String getUnknown2()
|
||||||
{
|
{
|
||||||
return getMessage().getHex(UNKNOWN_2, 2);
|
return getMessage().getHex(UNKNOWN_2, 2);
|
||||||
@@ -118,7 +118,7 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
|
|||||||
sb.append(" ENCRYPTED");
|
sb.append(" ENCRYPTED");
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.append(" CLEAR - RETURN TO ").append(getMoveToChannel());
|
sb.append(" CLEAR - RETURN TO").append(getMoveToChannel());
|
||||||
sb.append(" FM:").append(getSourceRadio());
|
sb.append(" FM:").append(getSourceRadio());
|
||||||
sb.append(" TO:").append(getDestinationId());
|
sb.append(" TO:").append(getDestinationId());
|
||||||
sb.append(" MSG:").append(getMessage().toHexString());
|
sb.append(" MSG:").append(getMessage().toHexString());
|
||||||
|
|||||||
@@ -109,6 +109,15 @@ public class LCMessageFactory
|
|||||||
|
|
||||||
LCOpcode opcode = FullLCMessage.getOpcode(message);
|
LCOpcode opcode = FullLCMessage.getOpcode(message);
|
||||||
|
|
||||||
|
//Some Hytera Tier-3 systems use a zero-valued mask when the FLC is carried in the voice header or terminator
|
||||||
|
//and in the same transmission it uses the standard mask when carried across the voice frames. It doesn't make
|
||||||
|
//sense, but maybe it's a bug in their software implementation?
|
||||||
|
if(!valid && opcode == LCOpcode.FULL_STANDARD_GROUP_VOICE_CHANNEL_USER)
|
||||||
|
{
|
||||||
|
//Retry the RS(12,9,4) with a mask value of zero
|
||||||
|
valid = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message, 0);
|
||||||
|
}
|
||||||
|
|
||||||
FullLCMessage flc = null;
|
FullLCMessage flc = null;
|
||||||
|
|
||||||
switch(opcode)
|
switch(opcode)
|
||||||
|
|||||||
@@ -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,35 +33,62 @@ public enum Vendor
|
|||||||
MOTOROLA_CONNECT_PLUS(6, "MOTOROLA CON+"),
|
MOTOROLA_CONNECT_PLUS(6, "MOTOROLA CON+"),
|
||||||
RADIO_DATA_GMBH(7, "RADIO DATA GMBH"),
|
RADIO_DATA_GMBH(7, "RADIO DATA GMBH"),
|
||||||
HYTERA_8(8, "HYTERA"),
|
HYTERA_8(8, "HYTERA"),
|
||||||
|
ASELSAN(9, "ASELSAN"),
|
||||||
|
KIRISUN(10, "KIRISUN"),
|
||||||
|
DMR_ASSOCIATION(11, "DMR ASSOCIATION"),
|
||||||
|
SEPURA(12, "SEPURA"),
|
||||||
|
ITALIA_RED_CROSS(13, "ITALIA RED CROSS"),
|
||||||
MOTOROLA_CAPACITY_PLUS(16, "MOTOROLA CAP+"),
|
MOTOROLA_CAPACITY_PLUS(16, "MOTOROLA CAP+"),
|
||||||
EMC_SPA_19(19, "EMC SPA"),
|
MINISTERO_DELL_INTERNO(19, "ITALY MIN INTERIOR"),
|
||||||
EMC_SPA_28(28, "EMC SPA"),
|
EMC_COMM_SRL_28(28, "EMC COMM SRL 28"),
|
||||||
RADIO_ACTIVITY_SRL_51(51, "RADIO ACTIVITY SRL"),
|
EMC_COMM_SRL_32(32, "EMC COMM SRL 32"),
|
||||||
RADIO_ACTIVITY_SRL_60(60, "RADIO ACTIVITY SRL"),
|
JVC_KENWOOD(51, "JVC-KENWOOD"),
|
||||||
|
RADIO_ACTIVITY_SRL(60, "RADIO ACTIVITY SRL"),
|
||||||
|
ZTE_TRUNKING(84, "ZTE TRUNKING"),
|
||||||
TAIT(88, "TAIT"),
|
TAIT(88, "TAIT"),
|
||||||
HYTERA_68(104, "HYTERA"),
|
HYTERA_68(104, "HYTERA"),
|
||||||
VERTEX_STANDARD(119, "VERTEX STANDARD"),
|
VERTEX_STANDARD(119, "VERTEX STANDARD"),
|
||||||
|
SIMOCO(120, "SIMOCO"),
|
||||||
|
TEST(121, "TEST"),
|
||||||
|
HYTERA_88(136, "HYTERA"),
|
||||||
UNKNOWN(-1, "UNKNOWN");
|
UNKNOWN(-1, "UNKNOWN");
|
||||||
|
|
||||||
private int mValue;
|
private int mValue;
|
||||||
private String mLabel;
|
private String mLabel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param value decimal for the vendor
|
||||||
|
* @param label for pretty print
|
||||||
|
*/
|
||||||
Vendor(int value, String label)
|
Vendor(int value, String label)
|
||||||
{
|
{
|
||||||
mValue = value;
|
mValue = value;
|
||||||
mLabel = label;
|
mLabel = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor ID value
|
||||||
|
* @return value
|
||||||
|
*/
|
||||||
public int getValue()
|
public int getValue()
|
||||||
{
|
{
|
||||||
return mValue;
|
return mValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty print label
|
||||||
|
* @return label
|
||||||
|
*/
|
||||||
public String getLabel()
|
public String getLabel()
|
||||||
{
|
{
|
||||||
return mLabel;
|
return mLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides default to use the label value instead.
|
||||||
|
* @return string representation.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
@@ -69,7 +96,7 @@ public enum Vendor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of integer values to corresponding vendor entries
|
* Quick lookup map of integer values to corresponding vendor entries
|
||||||
*/
|
*/
|
||||||
private static final Map<Integer,Vendor> LOOKUP_MAP = new TreeMap<>();
|
private static final Map<Integer,Vendor> LOOKUP_MAP = new TreeMap<>();
|
||||||
|
|
||||||
|
|||||||
@@ -48,15 +48,13 @@ public class VoiceAMessage extends VoiceMessage
|
|||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
if(getSyncPattern().isDirectMode())
|
if(!getSyncPattern().isMobileSyncPattern())
|
||||||
{
|
{
|
||||||
sb.append(getSyncPattern());
|
sb.append("CC:- ");
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.append("CC:- ").append(getSyncPattern());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sb.append(getSyncPattern());
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class VoiceEMBMessage extends VoiceMessage
|
|||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
if(getEMB().isValid())
|
if(!getSyncPattern().isMobileSyncPattern() && getEMB().isValid())
|
||||||
{
|
{
|
||||||
sb.append("CC:").append(getEMB().getColorCode()).append(" ");
|
sb.append("CC:").append(getEMB().getColorCode()).append(" ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import io.github.dsheirer.message.IMessage;
|
|||||||
import io.github.dsheirer.module.decode.DecoderType;
|
import io.github.dsheirer.module.decode.DecoderType;
|
||||||
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.p25.P25DecodeEvent;
|
import io.github.dsheirer.module.decode.p25.P25DecodeEvent;
|
||||||
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
|
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
|
||||||
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
|
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
|
||||||
@@ -87,8 +88,12 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToU
|
|||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelGrantUpdateExtended;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelGrantUpdateExtended;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserAbbreviated;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserAbbreviated;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserExtended;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserExtended;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisGpsLocation;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisRegroupCommand;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisRegroupCommand;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisTalkerAlias;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
|
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
|
||||||
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
|
import java.util.Date;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -392,6 +397,15 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
|||||||
case PHASE1_109_UNIT_REGISTRATION_COMMAND_ABBREVIATED:
|
case PHASE1_109_UNIT_REGISTRATION_COMMAND_ABBREVIATED:
|
||||||
broadcast(message, mac, getCurrentChannel(), DecodeEventType.COMMAND, "UNIT REGISTRATION");
|
broadcast(message, mac, getCurrentChannel(), DecodeEventType.COMMAND, "UNIT REGISTRATION");
|
||||||
break;
|
break;
|
||||||
|
case PHASE1_128_L3HARRIS_GPS_LOCATION:
|
||||||
|
processL3HarrisGps(message, mac);
|
||||||
|
break;
|
||||||
|
case PHASE1_168_L3HARRIS_TALKER_ALIAS:
|
||||||
|
if(mac instanceof L3HarrisTalkerAlias talkerAlias)
|
||||||
|
{
|
||||||
|
getIdentifierCollection().update(talkerAlias.getAlias());
|
||||||
|
}
|
||||||
|
break;
|
||||||
case PHASE1_176_L3HARRIS_GROUP_REGROUP:
|
case PHASE1_176_L3HARRIS_GROUP_REGROUP:
|
||||||
if(mac instanceof L3HarrisRegroupCommand regroup)
|
if(mac instanceof L3HarrisRegroupCommand regroup)
|
||||||
{
|
{
|
||||||
@@ -587,6 +601,25 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts the L3Harris gps position report as a mappable/plottable event.
|
||||||
|
*/
|
||||||
|
private void processL3HarrisGps(MacMessage message, MacStructure structure)
|
||||||
|
{
|
||||||
|
if(structure instanceof L3HarrisGpsLocation gps)
|
||||||
|
{
|
||||||
|
MutableIdentifierCollection collection = getUpdatedMutableIdentifierCollection(gps);
|
||||||
|
|
||||||
|
broadcast(PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, message.getTimestamp())
|
||||||
|
.protocol(Protocol.APCO25)
|
||||||
|
.location(gps.getGeoPosition())
|
||||||
|
.channel(getCurrentChannel())
|
||||||
|
.details(gps.getLocation().toString() + " " + new Date(gps.getTimestampMs()))
|
||||||
|
.identifiers(collection)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void broadcast(MacMessage message, MacStructure mac, IChannelDescriptor currentChannel, DecodeEventType eventType, String details) {
|
private void broadcast(MacMessage message, MacStructure mac, IChannelDescriptor currentChannel, DecodeEventType eventType, String details) {
|
||||||
MutableIdentifierCollection collection = getUpdatedMutableIdentifierCollection(mac);
|
MutableIdentifierCollection collection = getUpdatedMutableIdentifierCollection(mac);
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToU
|
|||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownStructure;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownStructure;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownVendorMessage;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownVendorMessage;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisRegroupCommand;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisRegroupCommand;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisTalkerAlias;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -290,6 +291,8 @@ public class MacMessageFactory
|
|||||||
return new AdjacentStatusBroadcastAbbreviated(message, offset);
|
return new AdjacentStatusBroadcastAbbreviated(message, offset);
|
||||||
case PHASE1_125_IDENTIFIER_UPDATE:
|
case PHASE1_125_IDENTIFIER_UPDATE:
|
||||||
return new FrequencyBandUpdate(message, offset);
|
return new FrequencyBandUpdate(message, offset);
|
||||||
|
case PHASE1_168_L3HARRIS_TALKER_ALIAS:
|
||||||
|
return new L3HarrisTalkerAlias(message, offset);
|
||||||
case PHASE1_176_L3HARRIS_GROUP_REGROUP:
|
case PHASE1_176_L3HARRIS_GROUP_REGROUP:
|
||||||
return new L3HarrisRegroupCommand(message, offset);
|
return new L3HarrisRegroupCommand(message, offset);
|
||||||
case PHASE1_192_GROUP_VOICE_CHANNEL_GRANT_EXTENDED:
|
case PHASE1_192_GROUP_VOICE_CHANNEL_GRANT_EXTENDED:
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ public enum MacOpcode
|
|||||||
PHASE1_123_NETWORK_STATUS_BROADCAST_ABBREVIATED(123, "NETWORK STATUS BROADCAST ABBREVIATED", 11),
|
PHASE1_123_NETWORK_STATUS_BROADCAST_ABBREVIATED(123, "NETWORK STATUS BROADCAST ABBREVIATED", 11),
|
||||||
PHASE1_124_ADJACENT_STATUS_BROADCAST_ABBREVIATED(124, "ADJACENT STATUS BROADCAST ABBREVIATED", 9),
|
PHASE1_124_ADJACENT_STATUS_BROADCAST_ABBREVIATED(124, "ADJACENT STATUS BROADCAST ABBREVIATED", 9),
|
||||||
PHASE1_125_IDENTIFIER_UPDATE(125, "IDENTIFIER UPDATE", 9),
|
PHASE1_125_IDENTIFIER_UPDATE(125, "IDENTIFIER UPDATE", 9),
|
||||||
|
PHASE1_128_L3HARRIS_GPS_LOCATION(128, "GPS LOCATION", -1),
|
||||||
|
PHASE1_168_L3HARRIS_TALKER_ALIAS(168, "TALKER ALIAS", -1),
|
||||||
PHASE1_176_L3HARRIS_GROUP_REGROUP(176, "REGROUP COMMAND", 17),
|
PHASE1_176_L3HARRIS_GROUP_REGROUP(176, "REGROUP COMMAND", 17),
|
||||||
PHASE1_PARTITION_1_UNKNOWN_OPCODE(-1, "UNKNOWN PHASE 1 OPCODE", -1),
|
PHASE1_PARTITION_1_UNKNOWN_OPCODE(-1, "UNKNOWN PHASE 1 OPCODE", -1),
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.p25.phase2.message.mac.structure.l3harris;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
|
import io.github.dsheirer.module.decode.dmr.identifier.P25Location;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacStructure;
|
||||||
|
import io.github.dsheirer.module.decode.p25.reference.Vendor;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import org.jdesktop.swingx.mapviewer.GeoPosition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L3Harris GPS Location.
|
||||||
|
*
|
||||||
|
* Bit field definitions are best-guess from observed samples.
|
||||||
|
*/
|
||||||
|
public class L3HarrisGpsLocation extends MacStructure
|
||||||
|
{
|
||||||
|
public static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm:ss");
|
||||||
|
static {
|
||||||
|
SDF.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
}
|
||||||
|
private static final DecimalFormat GPS_FORMAT = new DecimalFormat("0.000000");
|
||||||
|
private static final DecimalFormat FIXED = new DecimalFormat("000");
|
||||||
|
private static final int[] OPCODE = {0, 1, 2, 3, 4, 5, 6, 7};
|
||||||
|
private static final int[] UNKNOWN = {8, 9, 10, 11, 12, 13, 14, 15};
|
||||||
|
private static final int[] VENDOR = {16, 17, 18, 19, 20, 21, 22, 23};
|
||||||
|
private static final int[] LENGTH = {24, 25, 26, 27, 28, 29, 30, 31}; //Length is 17.5 bytes ... observed 17 here
|
||||||
|
|
||||||
|
//Bits 32 & 33 not set in sample data - seems unused for a 1/5000th of a minute number system
|
||||||
|
private static final int[] LATITUDE_MINUTES_FRACTIONAL = {32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46};
|
||||||
|
private static final int LATITUDE_HEMISPHERE = 48;
|
||||||
|
//Bit 47 & 49 not set in sample data
|
||||||
|
private static final int[] LATITUDE_MINUTES = {50, 51, 52, 53, 54, 55};
|
||||||
|
private static final int[] LATITUDE_DEGREES = {56, 57, 58, 59, 60, 61, 62, 63};
|
||||||
|
|
||||||
|
//Bits 64 & 65 not set in sample data - seems unused for a 1/5000th of a minute number system
|
||||||
|
private static final int[] LONGITUDE_MINUTES_FRACTIONAL = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78};
|
||||||
|
private static final int LONGITUDE_HEMISPHERE = 80;
|
||||||
|
//Bit 79 & 81 not set in sample data
|
||||||
|
private static final int[] LONGITUDE_MINUTES = {82, 83, 84, 85, 86, 87};
|
||||||
|
private static final int[] LONGITUDE_DEGREES = {88, 89, 90, 91, 92, 93, 94, 95};
|
||||||
|
|
||||||
|
//There's a leading bit missing from GPS Time to get from (2^16) to (2^17) needed address space (86,400 total seconds)
|
||||||
|
private static final int[] GPS_TIME = {96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111};
|
||||||
|
|
||||||
|
private static final int[] U2 = {112, 113, 114, 115, 116, 117, 118, 119};
|
||||||
|
private static final int[] U3 = {120, 121, 122, 123, 124, 125, 126, 127};
|
||||||
|
private static final int[] U4 = {128, 129, 130, 131, 132, 133, 134, 135};
|
||||||
|
private static final int[] U5 = {136, 137, 138, 139, 140, 141, 142, 143};
|
||||||
|
private static final int[] U6 = {144, 145, 146, 147, 148, 149, 150, 151};
|
||||||
|
private static final int[] U7 = {152, 153, 154, 155, 156, 157, 158, 159};
|
||||||
|
private static final int[] U8 = {160, 161, 162, 163, 164, 165, 166, 167};
|
||||||
|
|
||||||
|
private P25Location mLocation;
|
||||||
|
private List<Identifier> mIdentifiers;
|
||||||
|
private GeoPosition mGeoPosition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the message
|
||||||
|
*
|
||||||
|
* @param message containing the message bits
|
||||||
|
* @param offset into the message for this structure
|
||||||
|
*/
|
||||||
|
public L3HarrisGpsLocation(CorrectedBinaryMessage message, int offset)
|
||||||
|
{
|
||||||
|
super(message, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPS Position time in milliseconds.
|
||||||
|
* @return time in ms UTC
|
||||||
|
*/
|
||||||
|
public long getTimestampMs()
|
||||||
|
{
|
||||||
|
return getMessage().getInt(GPS_TIME, getOffset()) * 1000; //Convert seconds to milliseconds.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPS Location
|
||||||
|
* @return location in decimal degrees
|
||||||
|
*/
|
||||||
|
public P25Location getLocation()
|
||||||
|
{
|
||||||
|
if(mLocation == null)
|
||||||
|
{
|
||||||
|
mLocation = P25Location.createFrom(getLatitude(), getLongitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
return mLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geo position
|
||||||
|
* @return position
|
||||||
|
*/
|
||||||
|
public GeoPosition getGeoPosition()
|
||||||
|
{
|
||||||
|
if(mGeoPosition == null)
|
||||||
|
{
|
||||||
|
mGeoPosition = new GeoPosition(getLatitude(), getLongitude());
|
||||||
|
}
|
||||||
|
|
||||||
|
return mGeoPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPS location - latitude
|
||||||
|
* @return latitude degrees decimal
|
||||||
|
*/
|
||||||
|
public double getLatitude()
|
||||||
|
{
|
||||||
|
return (getLatitudeDegrees() + (getLatitudeMinutes() / 60.0)) * (getMessage().get(LATITUDE_HEMISPHERE + getOffset()) ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Latitude degrees
|
||||||
|
*/
|
||||||
|
private double getLatitudeDegrees()
|
||||||
|
{
|
||||||
|
return getMessage().getInt(LATITUDE_DEGREES, getOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Latitude minutes
|
||||||
|
*/
|
||||||
|
private double getLatitudeMinutes()
|
||||||
|
{
|
||||||
|
return getMessage().getInt(LATITUDE_MINUTES, getOffset()) +
|
||||||
|
getMessage().getInt(LATITUDE_MINUTES_FRACTIONAL, getOffset()) / 5000d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPS location - longitude
|
||||||
|
* @return longitude degrees decimal
|
||||||
|
*/
|
||||||
|
public double getLongitude()
|
||||||
|
{
|
||||||
|
return (getLongitudeDegrees() + (getLongitudeMinutes() / 60.0)) * (getMessage().get(LONGITUDE_HEMISPHERE + getOffset()) ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Longitude degrees
|
||||||
|
*/
|
||||||
|
private double getLongitudeDegrees()
|
||||||
|
{
|
||||||
|
return getMessage().getInt(LONGITUDE_DEGREES, getOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Longitude minutes
|
||||||
|
*/
|
||||||
|
private double getLongitudeMinutes()
|
||||||
|
{
|
||||||
|
return getMessage().getInt(LONGITUDE_MINUTES, getOffset()) +
|
||||||
|
getMessage().getInt(LONGITUDE_MINUTES_FRACTIONAL, getOffset()) / 5000d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Textual representation of this message
|
||||||
|
*/
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
if(getVendor() == Vendor.HARRIS)
|
||||||
|
{
|
||||||
|
sb.append("L3H TALKER GPS ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append("VENDOR:").append(getVendor()).append(" TALKER GPS ");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(GPS_FORMAT.format(getLatitude())).append(" ").append(GPS_FORMAT.format(getLongitude()));
|
||||||
|
sb.append(" TIME:").append(SDF.format(getTimestampMs()));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor ID. This should be L3Harris unless another vendor is also using this Opcode.
|
||||||
|
*/
|
||||||
|
public Vendor getVendor()
|
||||||
|
{
|
||||||
|
return Vendor.fromValue(getMessage().getInt(VENDOR, getOffset()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message length.
|
||||||
|
*
|
||||||
|
* @return length in bytes, including the opcode.
|
||||||
|
*/
|
||||||
|
public int getLength()
|
||||||
|
{
|
||||||
|
return getMessage().getInt(LENGTH, getOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Identifier> getIdentifiers()
|
||||||
|
{
|
||||||
|
if(mIdentifiers == null)
|
||||||
|
{
|
||||||
|
mIdentifiers = new ArrayList<>();
|
||||||
|
mIdentifiers.add(getLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
return mIdentifiers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
String[] examples = new String[]{
|
||||||
|
"8080AAA41106CC031E150E8A53C0E69200061C05E00D9000",
|
||||||
|
"8080AAA41106CC031E150E8A53C0E89300061C2A700D9000",
|
||||||
|
"8080AAA41106CC031E150E8A53C0E99300061C78400D9000",
|
||||||
|
"8080AAA41106D6031E150E8A53C0F29300061CDB200D9000",
|
||||||
|
"8080AAA41106D6031E150E8A53C0F39300061C89100D9000",
|
||||||
|
"8C80AAA41106CC031E150E8A53C11D9800061CE68088C000",
|
||||||
|
"8C80AAA41106CC031E150E8A53C11E9800061C10D088C000",
|
||||||
|
"8880AAA41106CC031E150E8A53C0D39100061C6AF0732000",
|
||||||
|
"8080AAA4111F7C231D0852B152BBB99600001C75700D9000",
|
||||||
|
"8080AAA4111F7C231D0852B152BBBA9700001CC3700D9000",
|
||||||
|
"8480AAA4111F86231D0852B152BBE08D00001C8BD0F67000",
|
||||||
|
"8480AAA4111F86231D0852B152BBE18D00001CD9E0F67000",
|
||||||
|
"8480AAA4111F7C231D0852B152BBEA8C00001C6560F67000",
|
||||||
|
"8480AAA4111F7C231D0852B152BBEB8D00001C7700F67000",
|
||||||
|
"8480AAA4111F7C231D0852B152BBEE8D00001CE480F67000",
|
||||||
|
"8480AAA4111F7C231D0852B152BBF88A00001CDDB0F67000",
|
||||||
|
"8480AAA4111F7C231D0852B152BBF98A00001C8F80F67000",
|
||||||
|
"8480AAA4111F7C231D0852B152BBF88D00001C9570F67000",
|
||||||
|
"8880AAA4111748212525C6B84DBD9C0F080517E600732000", //19 04642010
|
||||||
|
"8880AAA4111748212525C6B84DBDA69F0A08172480732000",
|
||||||
|
"8880AAA4111748212525C6B84DBDA89F0A08174B40732000",
|
||||||
|
"8880AAA4111748212525C6B84DBDAAB4010816B9E0732000",
|
||||||
|
"8880AAA4111748212525C6B84DBDABB401081762A0732000",
|
||||||
|
"8880AAA4111748212525C6B84DBDAC9C0008170650732000",
|
||||||
|
"8C80AAA4111734212525DAB84DBE454200071748A088C000",
|
||||||
|
"9080AAA4111734212525DAB84DBE4E42000717B4E0F0F000",
|
||||||
|
"9080AAA4111734212525DAB84DBE4F42000717E6D0F0F000",
|
||||||
|
"8480AAA4111748212525D0B84DBE868E180217A0A0F67000",
|
||||||
|
"8480AAA4111748212525C6B84DBE87641303170E80F67000",
|
||||||
|
"8480AAA411173E212525BCB84DBE89641303172190F67000",
|
||||||
|
"8480AAA41116A821252468B84DC0C3CF3A0915ECF0F67000",
|
||||||
|
"8480AAA41116A82125247CB84DC0C443130916FBA0F67000",
|
||||||
|
"8480AAA41116A821252486B84DC0C7272005133230F67000",
|
||||||
|
"9080AAA41116BC2325178EB24DBE2855050D15E920F0F000", //04642051
|
||||||
|
"8C80AAA41116BC2325178EB24DBE2E71060D142C1088C000",
|
||||||
|
"8C80AAA41116BC2325178EB24DBE2F73030D141C1088C000",
|
||||||
|
"8480AAA411173E232517D4B24DBF03C1020D14B910F67000",
|
||||||
|
"9080AAA411260C21251A90804EBF8A9CAD1A166980F0F000",//04642016 << only sample set that is moving, heading south then north
|
||||||
|
"9080AAA411261621251B1C804EBF8B8C921A16FAA0F0F000",
|
||||||
|
"8480AAA411263421250F6E814EBFD3982F13168E80F67000",
|
||||||
|
"8480AAA411263421250EEC814EBFD4982F1316FC70F67000",
|
||||||
|
"8480AAA411263421250E60814EBFD5982F131645A0F67000",
|
||||||
|
"8880AAA411087A21251F0E804EC07CB39108143C40732000",
|
||||||
|
"8880AAA411087A21251F54804EC07DB39108140EE0732000",
|
||||||
|
"8480AAA4110820212520EE804EC0AA4ED00116E970F67000",
|
||||||
|
"8480AAA411083E21252094804EC0AB26F702164140F67000",
|
||||||
|
"8480AAA411086621252058804EC0AC26F702169920F67000",
|
||||||
|
"8480AAA411088E2125201C804EC0AD26F702167A00F67000",
|
||||||
|
"8480AAA41108B621251FD6804EC0AEEFE50116DDB0F67000",
|
||||||
|
"8C80AAA4110AD21F25134C804EBFCDBD040A1801F088C000", //50 04642025
|
||||||
|
"8C80AAA4110AD21F25134C804EBFCEBD090A1832C088C000",
|
||||||
|
"8080AAA4110AFA1F25136A804EBFE9A70B0A18A4F00D9000",
|
||||||
|
"9080AAA4111BE41E250DD4AE4DC09B5F010314D660F0F000", //53 04642031
|
||||||
|
"9080AAA4111BE41E250DD4AE4DC09D5F0903149440F0F000",
|
||||||
|
"9080AAA4111BE41E250DD4AE4DC09E5F0903146210F0F000",
|
||||||
|
"8080AAA4111BBC1E250DF2AE4DC0A3022F0714FA900D9000",
|
||||||
|
"8080AAA4111BBC1E250DE8AE4DC0A4022F071484F00D9000",
|
||||||
|
"8080AAA4111BD01E250DE8AE4DC0A5022F07146DB00D9000",
|
||||||
|
};
|
||||||
|
|
||||||
|
for(String example: examples)
|
||||||
|
{
|
||||||
|
CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(CorrectedBinaryMessage.loadHex(example));
|
||||||
|
L3HarrisGpsLocation gps = new L3HarrisGpsLocation(cbm, 8);
|
||||||
|
System.out.println(gps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.p25.phase2.message.mac.structure.l3harris;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
|
import io.github.dsheirer.identifier.alias.P25TalkerAliasIdentifier;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacStructure;
|
||||||
|
import io.github.dsheirer.module.decode.p25.reference.Vendor;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L3Harris Talker Alias.
|
||||||
|
*/
|
||||||
|
public class L3HarrisTalkerAlias extends MacStructure
|
||||||
|
{
|
||||||
|
private static final int[] VENDOR = {8, 9, 10, 11, 12, 13, 14, 15};
|
||||||
|
private static final int[] LENGTH = {16, 17, 18, 19, 20, 21, 22, 23};
|
||||||
|
private static final int ALIAS_START = 24;
|
||||||
|
|
||||||
|
private List<Identifier> mIdentifiers;
|
||||||
|
private P25TalkerAliasIdentifier mAliasIdentifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the message
|
||||||
|
*
|
||||||
|
* @param message containing the message bits
|
||||||
|
* @param offset into the message for this structure
|
||||||
|
*/
|
||||||
|
public L3HarrisTalkerAlias(CorrectedBinaryMessage message, int offset)
|
||||||
|
{
|
||||||
|
super(message, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Textual representation of this message
|
||||||
|
*/
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
if(getVendor() == Vendor.HARRIS)
|
||||||
|
{
|
||||||
|
sb.append("L3HARRIS ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append("WARNING: UNKNOWN VENDOR:").append(getVendor());
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(" TALKER ALIAS: ");
|
||||||
|
sb.append(getAlias());
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias identifier
|
||||||
|
*/
|
||||||
|
public P25TalkerAliasIdentifier getAlias()
|
||||||
|
{
|
||||||
|
if(mAliasIdentifier == null)
|
||||||
|
{
|
||||||
|
int length = getLength() * 8;
|
||||||
|
String alias = new String(getMessage().getSubMessage(ALIAS_START + getOffset(), length + getOffset()).getBytes()).trim();
|
||||||
|
mAliasIdentifier = P25TalkerAliasIdentifier.create(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mAliasIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor ID. This should be L3Harris unless another vendor is also using this Opcode.
|
||||||
|
*/
|
||||||
|
public Vendor getVendor()
|
||||||
|
{
|
||||||
|
return Vendor.fromValue(getMessage().getInt(VENDOR, getOffset()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message length.
|
||||||
|
*
|
||||||
|
* @return length in bytes, including the opcode.
|
||||||
|
*/
|
||||||
|
public int getLength()
|
||||||
|
{
|
||||||
|
return getMessage().getInt(LENGTH, getOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Identifier> getIdentifiers()
|
||||||
|
{
|
||||||
|
if(mIdentifiers == null)
|
||||||
|
{
|
||||||
|
mIdentifiers = new ArrayList<>();
|
||||||
|
mIdentifiers.add(getAlias());
|
||||||
|
}
|
||||||
|
|
||||||
|
return mIdentifiers;
|
||||||
|
}
|
||||||
|
}
|
||||||
285
src/main/java/io/github/dsheirer/monitor/ResourceMonitor.java
Normal file
285
src/main/java/io/github/dsheirer/monitor/ResourceMonitor.java
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.monitor;
|
||||||
|
|
||||||
|
import io.github.dsheirer.log.LoggingSuppressor;
|
||||||
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.OperatingSystemMXBean;
|
||||||
|
import java.nio.file.FileStore;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides real-time monitoring of application resource usage.
|
||||||
|
*
|
||||||
|
* Monitors:
|
||||||
|
* - CPU usage
|
||||||
|
* - RAM usage
|
||||||
|
* - Drive Space - Event Logs
|
||||||
|
* - Drive Space - Recordings
|
||||||
|
*/
|
||||||
|
public class ResourceMonitor
|
||||||
|
{
|
||||||
|
private static final Logger sLog = LoggerFactory.getLogger(ResourceMonitor.class);
|
||||||
|
private static final LoggingSuppressor sLogSuppressor = new LoggingSuppressor(sLog);
|
||||||
|
private static final int SCALOR_MEGABYTE = 1024 * 1024;
|
||||||
|
private UserPreferences mUserPreferences;
|
||||||
|
private ScheduledFuture<?> mMemoryCpuMonitorFuture;
|
||||||
|
private ScheduledFuture<?> mStorageMonitorFuture;
|
||||||
|
private LongProperty mMemoryTotal = new SimpleLongProperty();
|
||||||
|
private LongProperty mMemoryAllocated = new SimpleLongProperty();
|
||||||
|
private LongProperty mMemoryUsed = new SimpleLongProperty();
|
||||||
|
private DoubleProperty mJavaMemoryUsedPercentage = new SimpleDoubleProperty();
|
||||||
|
private DoubleProperty mSystemMemoryUsedPercentage = new SimpleDoubleProperty();
|
||||||
|
private DoubleProperty mCpuPercentage = new SimpleDoubleProperty();
|
||||||
|
private BooleanProperty mCpuAvailable = new SimpleBooleanProperty();
|
||||||
|
private DoubleProperty mDirectoryUsePercentEventLogs = new SimpleDoubleProperty();
|
||||||
|
private DoubleProperty mDirectoryUsePercentRecordings = new SimpleDoubleProperty();
|
||||||
|
private StringProperty mFileSizeEventLogs = new SimpleStringProperty();
|
||||||
|
private StringProperty mFileSizeRecordings = new SimpleStringProperty();
|
||||||
|
private OperatingSystemMXBean mOperatingSystemMXBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
*/
|
||||||
|
public ResourceMonitor(UserPreferences userPreferences)
|
||||||
|
{
|
||||||
|
mUserPreferences = userPreferences;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mOperatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
sLog.error("Error accessing operating system MX bean to monitor CPU usage", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
mMemoryTotal.set(Runtime.getRuntime().maxMemory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts resource monitoring.
|
||||||
|
*/
|
||||||
|
public void start()
|
||||||
|
{
|
||||||
|
if(mMemoryCpuMonitorFuture == null)
|
||||||
|
{
|
||||||
|
mMemoryCpuMonitorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> updateCpuMemory(), 1, 1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mStorageMonitorFuture == null)
|
||||||
|
{
|
||||||
|
mStorageMonitorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> updateDirectoryUsage(), 1,30, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops resource monitoring.
|
||||||
|
*/
|
||||||
|
public void stop()
|
||||||
|
{
|
||||||
|
if(mMemoryCpuMonitorFuture != null)
|
||||||
|
{
|
||||||
|
mMemoryCpuMonitorFuture.cancel(true);
|
||||||
|
mMemoryCpuMonitorFuture = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mStorageMonitorFuture != null)
|
||||||
|
{
|
||||||
|
mStorageMonitorFuture.cancel(true);
|
||||||
|
mStorageMonitorFuture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timer-driven method to update CPU and memory usage statistics.
|
||||||
|
*/
|
||||||
|
private void updateCpuMemory()
|
||||||
|
{
|
||||||
|
double cpuLoadScaled = 0.0;
|
||||||
|
|
||||||
|
if(mOperatingSystemMXBean != null)
|
||||||
|
{
|
||||||
|
double load = mOperatingSystemMXBean.getSystemLoadAverage();
|
||||||
|
cpuLoadScaled = load / mOperatingSystemMXBean.getAvailableProcessors();
|
||||||
|
}
|
||||||
|
|
||||||
|
final double loadFinal = cpuLoadScaled;
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
mMemoryAllocated.set(Runtime.getRuntime().totalMemory());
|
||||||
|
mMemoryUsed.set(mMemoryAllocated.getValue() - Runtime.getRuntime().freeMemory());
|
||||||
|
mJavaMemoryUsedPercentage.set((double)mMemoryUsed.get() / (double)mMemoryAllocated.get());
|
||||||
|
mSystemMemoryUsedPercentage.set((double)mMemoryAllocated.get() / (double)mMemoryTotal.get());
|
||||||
|
mCpuPercentage.set(loadFinal > 0 ? loadFinal : 0);
|
||||||
|
mCpuAvailable.set(loadFinal >= 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timer-driven method to update directory space usage statistics
|
||||||
|
*/
|
||||||
|
private void updateDirectoryUsage()
|
||||||
|
{
|
||||||
|
long thresholdEventLog = mUserPreferences.getDirectoryPreference().getDirectoryMaxUsageEventLogs() * SCALOR_MEGABYTE;
|
||||||
|
long thresholdRecording = mUserPreferences.getDirectoryPreference().getDirectoryMaxUsageRecordings() * SCALOR_MEGABYTE;
|
||||||
|
|
||||||
|
Path recordingPath = mUserPreferences.getDirectoryPreference().getDirectoryRecording();
|
||||||
|
Path eventLogsPath = mUserPreferences.getDirectoryPreference().getDirectoryEventLog();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FileStore recordingFileStore = Files.getFileStore(recordingPath);
|
||||||
|
FileStore eventLogsFileStore = Files.getFileStore(eventLogsPath);
|
||||||
|
|
||||||
|
long recordingAvailable = recordingFileStore.getUsableSpace();
|
||||||
|
long eventLogsAvailable = eventLogsFileStore.getUsableSpace();
|
||||||
|
|
||||||
|
long eventLogUsed = FileUtils.sizeOfDirectory(eventLogsPath.toFile());
|
||||||
|
long recordingUsed = FileUtils.sizeOfDirectory(recordingPath.toFile());
|
||||||
|
|
||||||
|
long eventLogMax = Math.min(thresholdEventLog, eventLogUsed + eventLogsAvailable);
|
||||||
|
long recordingMax = Math.min(thresholdRecording, recordingUsed + recordingAvailable);
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
mDirectoryUsePercentEventLogs.set((double)eventLogUsed / (double)eventLogMax);
|
||||||
|
mDirectoryUsePercentRecordings.set((double)recordingUsed / (double)recordingMax);
|
||||||
|
mFileSizeEventLogs.set(FileUtils.byteCountToDisplaySize(eventLogUsed));
|
||||||
|
mFileSizeRecordings.set(FileUtils.byteCountToDisplaySize(recordingUsed));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(IOException ioe)
|
||||||
|
{
|
||||||
|
sLogSuppressor.error("Log Once", 1, "Unable to monitor file system - " + ioe.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory use percentage for event logs directory.
|
||||||
|
* @return usage in range 0.0 - 1.0 (or larger if it exceeds the threshold)
|
||||||
|
*/
|
||||||
|
public DoubleProperty directoryUsePercentEventLogsProperty()
|
||||||
|
{
|
||||||
|
return mDirectoryUsePercentEventLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory use percentage for recordings directory.
|
||||||
|
* @return usage in range 0.0 - 1.0 (or larger if it exceeds the threshold)
|
||||||
|
*/
|
||||||
|
public DoubleProperty directoryUsePercentRecordingsProperty()
|
||||||
|
{
|
||||||
|
return mDirectoryUsePercentRecordings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatted value property for file size in event logs directory.
|
||||||
|
* @return print friendly value property
|
||||||
|
*/
|
||||||
|
public StringProperty fileSizeEventLogsProperty()
|
||||||
|
{
|
||||||
|
return mFileSizeEventLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatted value property for file size in recordings directory.
|
||||||
|
* @return print friendly value property
|
||||||
|
*/
|
||||||
|
public StringProperty fileSizeRecordingsProperty()
|
||||||
|
{
|
||||||
|
return mFileSizeRecordings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CPU usage percentage.
|
||||||
|
* @return usage in range 0.0 - 1.0
|
||||||
|
*/
|
||||||
|
public DoubleProperty cpuPercentageProperty()
|
||||||
|
{
|
||||||
|
return mCpuPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the CPU usage percentage value is available on this operating system.
|
||||||
|
* @return false when the CPU load value is a negative value, indicating that the JVM for this OS doesn't support it.
|
||||||
|
*/
|
||||||
|
public BooleanProperty cpuAvailableProperty()
|
||||||
|
{
|
||||||
|
return mCpuAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property for total system memory
|
||||||
|
*/
|
||||||
|
public LongProperty memoryTotalProperty()
|
||||||
|
{
|
||||||
|
return mMemoryTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property for memory currently allocated to the JVM
|
||||||
|
*/
|
||||||
|
public LongProperty memoryAllocatedProperty()
|
||||||
|
{
|
||||||
|
return mMemoryAllocated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property for memory used by the JVM (out of what has been allocated)
|
||||||
|
*/
|
||||||
|
public LongProperty memoryUsedProperty()
|
||||||
|
{
|
||||||
|
return mMemoryUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property for memory used vs memory allocated to the JVM.
|
||||||
|
*/
|
||||||
|
public DoubleProperty javaMemoryUsedPercentageProperty()
|
||||||
|
{
|
||||||
|
return mJavaMemoryUsedPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property for JVM allocated vs total system memory.
|
||||||
|
*/
|
||||||
|
public DoubleProperty systemMemoryUsedPercentageProperty()
|
||||||
|
{
|
||||||
|
return mSystemMemoryUsedPercentage;
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/main/java/io/github/dsheirer/monitor/StatusBox.java
Normal file
103
src/main/java/io/github/dsheirer/monitor/StatusBox.java
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.monitor;
|
||||||
|
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ProgressBar;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaFX status panel box.
|
||||||
|
*/
|
||||||
|
public class StatusBox extends HBox
|
||||||
|
{
|
||||||
|
private ResourceMonitor mResourceMonitor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance.
|
||||||
|
* @param resourceMonitor for accessing resource usage statistics.
|
||||||
|
*/
|
||||||
|
public StatusBox(ResourceMonitor resourceMonitor)
|
||||||
|
{
|
||||||
|
mResourceMonitor = resourceMonitor;
|
||||||
|
setPadding(new Insets(1, 0, 1, 0));
|
||||||
|
setSpacing(6);
|
||||||
|
Label cpuLabel = new Label("CPU:");
|
||||||
|
cpuLabel.setPadding(new Insets(0, 0, 0, 10));
|
||||||
|
cpuLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getChildren().add(cpuLabel);
|
||||||
|
|
||||||
|
ProgressBar cpuIndicator = new ProgressBar();
|
||||||
|
cpuIndicator.progressProperty().bind(mResourceMonitor.cpuPercentageProperty());
|
||||||
|
cpuIndicator.disableProperty().bind(mResourceMonitor.cpuAvailableProperty().not());
|
||||||
|
cpuIndicator.setTooltip(new Tooltip("Java process CPU usage. Disabled if the CPU loading is not available from the OS"));
|
||||||
|
getChildren().add(cpuIndicator);
|
||||||
|
|
||||||
|
Label memoryLabel = new Label("Allocated Memory:");
|
||||||
|
memoryLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getChildren().add(memoryLabel);
|
||||||
|
|
||||||
|
ProgressBar memoryBar = new ProgressBar();
|
||||||
|
memoryBar.progressProperty().bind(mResourceMonitor.systemMemoryUsedPercentageProperty());
|
||||||
|
memoryBar.setTooltip(new Tooltip("Percentage of total system memory that Java has reserved from the Operating System."));
|
||||||
|
getChildren().add(memoryBar);
|
||||||
|
|
||||||
|
Label javaMemoryLabel = new Label("Used Memory:");
|
||||||
|
javaMemoryLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getChildren().add(javaMemoryLabel);
|
||||||
|
|
||||||
|
ProgressBar javaMemoryBar = new ProgressBar();
|
||||||
|
javaMemoryBar.progressProperty().bind(mResourceMonitor.javaMemoryUsedPercentageProperty());
|
||||||
|
javaMemoryBar.setTooltip(new Tooltip("Percentage of allocated memory that Java/sdrtrunk is currently using. This value fluctuates as Java manages memory and garbage collection"));
|
||||||
|
getChildren().add(javaMemoryBar);
|
||||||
|
|
||||||
|
Label eventLogsLabel = new Label("Event Logs:");
|
||||||
|
eventLogsLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getChildren().add(eventLogsLabel);
|
||||||
|
|
||||||
|
ProgressBar eventLogsBar = new ProgressBar();
|
||||||
|
eventLogsBar.progressProperty().bind(mResourceMonitor.directoryUsePercentEventLogsProperty());
|
||||||
|
eventLogsBar.setTooltip(new Tooltip("Percentage of drive space used for event logs based on user-specified max threshold in user preferences"));
|
||||||
|
getChildren().add(eventLogsBar);
|
||||||
|
|
||||||
|
Label eventLogsSizeLabel = new Label();
|
||||||
|
eventLogsSizeLabel.textProperty().bind(mResourceMonitor.fileSizeEventLogsProperty());
|
||||||
|
eventLogsSizeLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getChildren().add(eventLogsSizeLabel);
|
||||||
|
|
||||||
|
Label recordingsLabel = new Label("Recordings:");
|
||||||
|
recordingsLabel.setPadding(new Insets(0, 0, 0, 10));
|
||||||
|
recordingsLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getChildren().add(recordingsLabel);
|
||||||
|
|
||||||
|
ProgressBar recordingsBar = new ProgressBar();
|
||||||
|
recordingsBar.progressProperty().bind(mResourceMonitor.directoryUsePercentRecordingsProperty());
|
||||||
|
recordingsBar.setTooltip(new Tooltip("Percentage of drive space used for recordings based on user-specified max threshold in user preferences"));
|
||||||
|
getChildren().add(recordingsBar);
|
||||||
|
|
||||||
|
Label recordingsSizeLabel = new Label();
|
||||||
|
recordingsSizeLabel.textProperty().bind(mResourceMonitor.fileSizeRecordingsProperty());
|
||||||
|
recordingsSizeLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
getChildren().add(recordingsSizeLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,13 +22,12 @@ package io.github.dsheirer.preference.directory;
|
|||||||
import io.github.dsheirer.preference.Preference;
|
import io.github.dsheirer.preference.Preference;
|
||||||
import io.github.dsheirer.preference.PreferenceType;
|
import io.github.dsheirer.preference.PreferenceType;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User preferences for directories
|
* User preferences for directories
|
||||||
@@ -38,6 +37,9 @@ public class DirectoryPreference extends Preference
|
|||||||
private final static Logger mLog = LoggerFactory.getLogger(DirectoryPreference.class);
|
private final static Logger mLog = LoggerFactory.getLogger(DirectoryPreference.class);
|
||||||
private Preferences mPreferences = Preferences.userNodeForPackage(DirectoryPreference.class);
|
private Preferences mPreferences = Preferences.userNodeForPackage(DirectoryPreference.class);
|
||||||
|
|
||||||
|
private static final int DEFAULT_USAGE_THRESHOLD_RECORDINGS_MB = 2000;
|
||||||
|
private static final int DEFAULT_USAGE_THRESHOLD_EVENT_LOGS_MB = 200;
|
||||||
|
|
||||||
private static final String DIRECTORY_APPLICATION_ROOT = "SDRTrunk";
|
private static final String DIRECTORY_APPLICATION_ROOT = "SDRTrunk";
|
||||||
private static final String DIRECTORY_APPLICATION_LOG = "logs";
|
private static final String DIRECTORY_APPLICATION_LOG = "logs";
|
||||||
private static final String DIRECTORY_CONFIGURATION = "configuration";
|
private static final String DIRECTORY_CONFIGURATION = "configuration";
|
||||||
@@ -57,6 +59,8 @@ public class DirectoryPreference extends Preference
|
|||||||
private static final String PREFERENCE_KEY_DIRECTORY_RECORDING = "directory.recording";
|
private static final String PREFERENCE_KEY_DIRECTORY_RECORDING = "directory.recording";
|
||||||
private static final String PREFERENCE_KEY_DIRECTORY_SCREEN_CAPTURE = "directory.screen.capture";
|
private static final String PREFERENCE_KEY_DIRECTORY_SCREEN_CAPTURE = "directory.screen.capture";
|
||||||
private static final String PREFERENCE_KEY_DIRECTORY_STREAMING = "directory.streaming";
|
private static final String PREFERENCE_KEY_DIRECTORY_STREAMING = "directory.streaming";
|
||||||
|
private static final String PREFERENCE_KEY_DIRECTORY_MAX_USAGE_RECORDINGS = "directory.max.usage.recordings";
|
||||||
|
private static final String PREFERENCE_KEY_DIRECTORY_MAX_USAGE_EVENT_LOGS = "directory.max.usage.event.logs";
|
||||||
|
|
||||||
private Path mDirectoryApplicationRoot;
|
private Path mDirectoryApplicationRoot;
|
||||||
private Path mDirectoryApplicationLogs;
|
private Path mDirectoryApplicationLogs;
|
||||||
@@ -67,6 +71,8 @@ public class DirectoryPreference extends Preference
|
|||||||
private Path mDirectoryRecording;
|
private Path mDirectoryRecording;
|
||||||
private Path mDirectoryScreenCapture;
|
private Path mDirectoryScreenCapture;
|
||||||
private Path mDirectoryStreaming;
|
private Path mDirectoryStreaming;
|
||||||
|
private Integer mDirectoryMaxUsageRecordings;
|
||||||
|
private Integer mDirectoryMaxUsageEventLogs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs this preference with an update listener
|
* Constructs this preference with an update listener
|
||||||
@@ -127,6 +133,58 @@ public class DirectoryPreference extends Preference
|
|||||||
mDirectoryStreaming = null;
|
mDirectoryStreaming = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-specified maximum drive space usage for the recording directory.
|
||||||
|
* @return max usage threshold in MB.
|
||||||
|
*/
|
||||||
|
public int getDirectoryMaxUsageRecordings()
|
||||||
|
{
|
||||||
|
if(mDirectoryMaxUsageRecordings == null)
|
||||||
|
{
|
||||||
|
mDirectoryMaxUsageRecordings = mPreferences.getInt(PREFERENCE_KEY_DIRECTORY_MAX_USAGE_RECORDINGS,
|
||||||
|
DEFAULT_USAGE_THRESHOLD_RECORDINGS_MB);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mDirectoryMaxUsageRecordings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the threshold for maximum drive space usage for the recordings directory.
|
||||||
|
* @param thresholdMB threshold in MB
|
||||||
|
*/
|
||||||
|
public void setDirectoryMaxUsageRecordings(int thresholdMB)
|
||||||
|
{
|
||||||
|
mPreferences.putInt(PREFERENCE_KEY_DIRECTORY_MAX_USAGE_RECORDINGS, thresholdMB);
|
||||||
|
mDirectoryMaxUsageRecordings = thresholdMB;
|
||||||
|
notifyPreferenceUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-specified maximum drive space usage for the event logs directory.
|
||||||
|
* @return max usage threshold in MB.
|
||||||
|
*/
|
||||||
|
public int getDirectoryMaxUsageEventLogs()
|
||||||
|
{
|
||||||
|
if(mDirectoryMaxUsageEventLogs == null)
|
||||||
|
{
|
||||||
|
mDirectoryMaxUsageEventLogs = mPreferences.getInt(PREFERENCE_KEY_DIRECTORY_MAX_USAGE_EVENT_LOGS,
|
||||||
|
DEFAULT_USAGE_THRESHOLD_EVENT_LOGS_MB);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mDirectoryMaxUsageEventLogs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the threshold for maximum drive space usage for the event logs directory.
|
||||||
|
* @param thresholdMB threshold in MB
|
||||||
|
*/
|
||||||
|
public void setDirectoryMaxUsageEventLogs(int thresholdMB)
|
||||||
|
{
|
||||||
|
mPreferences.putInt(PREFERENCE_KEY_DIRECTORY_MAX_USAGE_EVENT_LOGS, thresholdMB);
|
||||||
|
mDirectoryMaxUsageEventLogs = thresholdMB;
|
||||||
|
notifyPreferenceUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a stored recording directory preference so that the default path can be used again
|
* Removes a stored recording directory preference so that the default path can be used again
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -161,10 +161,6 @@ public class SystemProperties
|
|||||||
{
|
{
|
||||||
mLog.info("SystemProperties - no properties file loaded - using defaults");
|
mLog.info("SystemProperties - no properties file loaded - using defaults");
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
mLog.info("SystemProperties - application properties loaded [" + mPropertiesPath.toString() + "]");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -35,9 +35,6 @@ import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier;
|
|||||||
import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
|
import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
|
||||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||||
import io.github.dsheirer.properties.SystemProperties;
|
import io.github.dsheirer.properties.SystemProperties;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
@@ -47,6 +44,8 @@ import java.util.Date;
|
|||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class AudioMetadataUtils
|
public class AudioMetadataUtils
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComboBox;
|
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JSlider;
|
import javax.swing.JSlider;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract RSP tuner editor
|
* Abstract RSP tuner editor
|
||||||
@@ -48,7 +48,7 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
|
|||||||
private Logger mLog = LoggerFactory.getLogger(RspTunerEditor.class);
|
private Logger mLog = LoggerFactory.getLogger(RspTunerEditor.class);
|
||||||
private JSlider mGainSlider;
|
private JSlider mGainSlider;
|
||||||
private JLabel mGainValueLabel;
|
private JLabel mGainValueLabel;
|
||||||
private JComboBox<AgcMode> mAgcModeCombo;
|
private JToggleButton mAgcButton;
|
||||||
private JButton mGainOverloadButton;
|
private JButton mGainOverloadButton;
|
||||||
private AtomicBoolean mGainOverloadAlert = new AtomicBoolean();
|
private AtomicBoolean mGainOverloadAlert = new AtomicBoolean();
|
||||||
|
|
||||||
@@ -120,22 +120,20 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IF AGC mode combobox control
|
* IF AGC mode (enable/disable) toggle button
|
||||||
*/
|
*/
|
||||||
protected JComboBox<AgcMode> getAgcModeCombo()
|
protected JToggleButton getAgcButton()
|
||||||
{
|
{
|
||||||
if(mAgcModeCombo == null)
|
if(mAgcButton == null)
|
||||||
{
|
{
|
||||||
mAgcModeCombo = new JComboBox<>(AgcMode.values());
|
mAgcButton = new JToggleButton("IF AGC");
|
||||||
mAgcModeCombo.setEnabled(false);
|
mAgcButton.setEnabled(false);
|
||||||
mAgcModeCombo.addActionListener(e -> {
|
mAgcButton.addActionListener(e -> {
|
||||||
if(hasTuner() && !isLoading())
|
if(hasTuner() && !isLoading())
|
||||||
{
|
{
|
||||||
AgcMode selected = (AgcMode)mAgcModeCombo.getSelectedItem();
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
getTunerController().getControlRsp().setAgcMode(selected);
|
getTunerController().getControlRsp().setAgcMode(mAgcButton.isSelected() ? AgcMode.ENABLE : AgcMode.DISABLE);
|
||||||
save();
|
|
||||||
}
|
}
|
||||||
catch(SDRPlayException se)
|
catch(SDRPlayException se)
|
||||||
{
|
{
|
||||||
@@ -146,7 +144,7 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return mAgcModeCombo;
|
return mAgcButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,16 +20,19 @@
|
|||||||
package io.github.dsheirer.source.tuner.sdrplay.api.parameter.control;
|
package io.github.dsheirer.source.tuner.sdrplay.api.parameter.control;
|
||||||
|
|
||||||
import io.github.dsheirer.source.tuner.sdrplay.api.v3_07.sdrplay_api_h;
|
import io.github.dsheirer.source.tuner.sdrplay.api.v3_07.sdrplay_api_h;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatic Gain Control (AGC) Control (mode)
|
* Automatic Gain Control (AGC) Control (mode)
|
||||||
*/
|
*/
|
||||||
public enum AgcMode
|
public enum AgcMode
|
||||||
{
|
{
|
||||||
|
//Deprecated modes ... only use the ENABLE or DISABLE, in conjunction with the
|
||||||
|
AGC_100_HZ(sdrplay_api_h.sdrplay_api_AGC_100HZ(), "Deprecated - 100 Hz"),
|
||||||
|
AGC_50_HZ(sdrplay_api_h.sdrplay_api_AGC_50HZ(), "Deprecated 50 Hz"),
|
||||||
|
AGC_5_HZ(sdrplay_api_h.sdrplay_api_AGC_5HZ(), "Deprecated 5 Hz"),
|
||||||
|
|
||||||
DISABLE(sdrplay_api_h.sdrplay_api_AGC_DISABLE(), "DISABLE"),
|
DISABLE(sdrplay_api_h.sdrplay_api_AGC_DISABLE(), "DISABLE"),
|
||||||
AGC_100_HZ(sdrplay_api_h.sdrplay_api_AGC_100HZ(), "100 Hz"),
|
|
||||||
AGC_50_HZ(sdrplay_api_h.sdrplay_api_AGC_50HZ(), "50 Hz"),
|
|
||||||
AGC_5_HZ(sdrplay_api_h.sdrplay_api_AGC_5HZ(), "5 Hz"),
|
|
||||||
ENABLE(sdrplay_api_h.sdrplay_api_AGC_CTRL_EN(), "ENABLE");
|
ENABLE(sdrplay_api_h.sdrplay_api_AGC_CTRL_EN(), "ENABLE");
|
||||||
|
|
||||||
private int mValue;
|
private int mValue;
|
||||||
@@ -41,6 +44,8 @@ public enum AgcMode
|
|||||||
mDescription = description;
|
mDescription = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static EnumSet<AgcMode> SUPPORTED_MODES = EnumSet.of(ENABLE, DISABLE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Numeric value
|
* Numeric value
|
||||||
*/
|
*/
|
||||||
@@ -49,6 +54,15 @@ public enum AgcMode
|
|||||||
return mValue;
|
return mValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the mode is a supported mode.
|
||||||
|
* @return true if it's a supported mode.
|
||||||
|
*/
|
||||||
|
public boolean isSupported()
|
||||||
|
{
|
||||||
|
return SUPPORTED_MODES.contains(this);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lookup the entry from a return code
|
* Lookup the entry from a return code
|
||||||
* @param value to lookup
|
* @param value to lookup
|
||||||
@@ -60,7 +74,16 @@ public enum AgcMode
|
|||||||
{
|
{
|
||||||
if(status.getValue() == value)
|
if(status.getValue() == value)
|
||||||
{
|
{
|
||||||
return status;
|
//legacy support in case some users had the deprecated values stored in their tuner configs. In
|
||||||
|
//this situation we return ENABLED instead of one of these deprecated values.
|
||||||
|
if(status.isSupported())
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ENABLE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
|
|||||||
add(new JLabel("IF AGC Mode:"));
|
add(new JLabel("IF AGC Mode:"));
|
||||||
JPanel gainPanel = new JPanel();
|
JPanel gainPanel = new JPanel();
|
||||||
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
||||||
gainPanel.add(getAgcModeCombo());
|
gainPanel.add(getAgcButton());
|
||||||
gainPanel.add(getGainOverloadButton());
|
gainPanel.add(getGainOverloadButton());
|
||||||
add(gainPanel, "wrap");
|
add(gainPanel, "wrap");
|
||||||
|
|
||||||
@@ -134,10 +134,11 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
|
|||||||
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
||||||
updateSampleRateToolTip();
|
updateSampleRateToolTip();
|
||||||
|
|
||||||
getAgcModeCombo().setEnabled(hasTuner());
|
getAgcButton().setEnabled(hasTuner());
|
||||||
if(hasTuner())
|
if(hasTuner())
|
||||||
{
|
{
|
||||||
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
|
AgcMode current = getTunerController().getControlRsp().getAgcMode();
|
||||||
|
getAgcButton().setSelected(current == null || current.equals(AgcMode.ENABLE));
|
||||||
//Register to receive gain overload notifications
|
//Register to receive gain overload notifications
|
||||||
getTunerController().getControlRsp().setGainOverloadListener(this);
|
getTunerController().getControlRsp().setGainOverloadListener(this);
|
||||||
}
|
}
|
||||||
@@ -160,7 +161,7 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
|
|||||||
getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
|
getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
|
||||||
getConfiguration().setSampleRate((RspSampleRate)getSampleRateCombo().getSelectedItem());
|
getConfiguration().setSampleRate((RspSampleRate)getSampleRateCombo().getSelectedItem());
|
||||||
getConfiguration().setGain(getGainSlider().getValue());
|
getConfiguration().setGain(getGainSlider().getValue());
|
||||||
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
|
getConfiguration().setAgcMode(getAgcButton().isSelected() ? AgcMode.ENABLE : AgcMode.DISABLE);
|
||||||
|
|
||||||
saveConfiguration();
|
saveConfiguration();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
|
|||||||
add(new JLabel("IF AGC Mode:"));
|
add(new JLabel("IF AGC Mode:"));
|
||||||
JPanel gainPanel = new JPanel();
|
JPanel gainPanel = new JPanel();
|
||||||
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
||||||
gainPanel.add(getAgcModeCombo());
|
gainPanel.add(getAgcButton());
|
||||||
gainPanel.add(getGainOverloadButton());
|
gainPanel.add(getGainOverloadButton());
|
||||||
add(gainPanel, "wrap");
|
add(gainPanel, "wrap");
|
||||||
|
|
||||||
@@ -145,10 +145,11 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
|
|||||||
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
||||||
updateSampleRateToolTip();
|
updateSampleRateToolTip();
|
||||||
|
|
||||||
getAgcModeCombo().setEnabled(hasTuner());
|
getAgcButton().setEnabled(hasTuner());
|
||||||
if(hasTuner())
|
if(hasTuner())
|
||||||
{
|
{
|
||||||
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
|
AgcMode current = getTunerController().getControlRsp().getAgcMode();
|
||||||
|
getAgcButton().setSelected(current == null || current.equals(AgcMode.ENABLE));
|
||||||
//Register to receive gain overload notifications
|
//Register to receive gain overload notifications
|
||||||
getTunerController().getControlRsp().setGainOverloadListener(this);
|
getTunerController().getControlRsp().setGainOverloadListener(this);
|
||||||
}
|
}
|
||||||
@@ -204,7 +205,7 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
|
|||||||
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
||||||
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
|
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
|
||||||
getConfiguration().setGain(getGainSlider().getValue());
|
getConfiguration().setGain(getGainSlider().getValue());
|
||||||
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
|
getConfiguration().setAgcMode(getAgcButton().isSelected() ? AgcMode.ENABLE : AgcMode.DISABLE);
|
||||||
|
|
||||||
saveConfiguration();
|
saveConfiguration();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
|
|||||||
add(new JLabel("IF AGC Mode:"));
|
add(new JLabel("IF AGC Mode:"));
|
||||||
JPanel gainPanel = new JPanel();
|
JPanel gainPanel = new JPanel();
|
||||||
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
||||||
gainPanel.add(getAgcModeCombo());
|
gainPanel.add(getAgcButton());
|
||||||
gainPanel.add(getGainOverloadButton());
|
gainPanel.add(getGainOverloadButton());
|
||||||
add(gainPanel, "wrap");
|
add(gainPanel, "wrap");
|
||||||
|
|
||||||
@@ -149,10 +149,11 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
|
|||||||
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
||||||
updateSampleRateToolTip();
|
updateSampleRateToolTip();
|
||||||
|
|
||||||
getAgcModeCombo().setEnabled(hasTuner());
|
getAgcButton().setEnabled(hasTuner());
|
||||||
if(hasTuner())
|
if(hasTuner())
|
||||||
{
|
{
|
||||||
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
|
AgcMode current = getTunerController().getControlRsp().getAgcMode();
|
||||||
|
getAgcButton().setSelected(current == null || current.equals(AgcMode.ENABLE));
|
||||||
//Register to receive gain overload notifications
|
//Register to receive gain overload notifications
|
||||||
getTunerController().getControlRsp().setGainOverloadListener(this);
|
getTunerController().getControlRsp().setGainOverloadListener(this);
|
||||||
}
|
}
|
||||||
@@ -218,7 +219,7 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
|
|||||||
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
||||||
getConfiguration().setAntennaSelection((Rsp2AntennaSelection)getAntennaSelectionCombo().getSelectedItem());
|
getConfiguration().setAntennaSelection((Rsp2AntennaSelection)getAntennaSelectionCombo().getSelectedItem());
|
||||||
getConfiguration().setGain(getGainSlider().getValue());
|
getConfiguration().setGain(getGainSlider().getValue());
|
||||||
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
|
getConfiguration().setAgcMode(getAgcButton().isSelected() ? AgcMode.ENABLE : AgcMode.DISABLE);
|
||||||
|
|
||||||
saveConfiguration();
|
saveConfiguration();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
|
|||||||
add(new JLabel("IF AGC Mode:"));
|
add(new JLabel("IF AGC Mode:"));
|
||||||
JPanel gainPanel = new JPanel();
|
JPanel gainPanel = new JPanel();
|
||||||
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
||||||
gainPanel.add(getAgcModeCombo());
|
gainPanel.add(getAgcButton());
|
||||||
gainPanel.add(getGainOverloadButton());
|
gainPanel.add(getGainOverloadButton());
|
||||||
add(gainPanel, "wrap");
|
add(gainPanel, "wrap");
|
||||||
|
|
||||||
@@ -154,8 +154,6 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
|
|||||||
}
|
}
|
||||||
getTunerStatusLabel().setText(status);
|
getTunerStatusLabel().setText(status);
|
||||||
getButtonPanel().updateControls();
|
getButtonPanel().updateControls();
|
||||||
mLog.info("Update tuner status - has tuner: " + hasTuner() + " is locked:" + getTuner().getTunerController().isLockedSampleRate());
|
|
||||||
|
|
||||||
getFrequencyPanel().updateControls();
|
getFrequencyPanel().updateControls();
|
||||||
|
|
||||||
clearSampleRates();
|
clearSampleRates();
|
||||||
@@ -167,10 +165,11 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
|
|||||||
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
||||||
updateSampleRateToolTip();
|
updateSampleRateToolTip();
|
||||||
|
|
||||||
getAgcModeCombo().setEnabled(hasTuner());
|
getAgcButton().setEnabled(hasTuner());
|
||||||
if(hasTuner())
|
if(hasTuner())
|
||||||
{
|
{
|
||||||
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
|
AgcMode current = getTunerController().getControlRsp().getAgcMode();
|
||||||
|
getAgcButton().setSelected(current == null || current.equals(AgcMode.ENABLE));
|
||||||
//Register to receive gain overload notifications
|
//Register to receive gain overload notifications
|
||||||
getTunerController().getControlRsp().setGainOverloadListener(this);
|
getTunerController().getControlRsp().setGainOverloadListener(this);
|
||||||
}
|
}
|
||||||
@@ -249,7 +248,7 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
|
|||||||
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
|
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
|
||||||
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
||||||
getConfiguration().setGain(getGainSlider().getValue());
|
getConfiguration().setGain(getGainSlider().getValue());
|
||||||
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
|
getConfiguration().setAgcMode(getAgcButton().isSelected() ? AgcMode.ENABLE : AgcMode.DISABLE);
|
||||||
|
|
||||||
saveConfiguration();
|
saveConfiguration();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
|
|||||||
add(new JLabel("IF AGC Mode:"));
|
add(new JLabel("IF AGC Mode:"));
|
||||||
JPanel gainPanel = new JPanel();
|
JPanel gainPanel = new JPanel();
|
||||||
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
||||||
gainPanel.add(getAgcModeCombo());
|
gainPanel.add(getAgcButton());
|
||||||
gainPanel.add(getGainOverloadButton());
|
gainPanel.add(getGainOverloadButton());
|
||||||
add(gainPanel, "wrap");
|
add(gainPanel, "wrap");
|
||||||
|
|
||||||
@@ -182,10 +182,11 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
|
|||||||
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
||||||
updateSampleRateToolTip();
|
updateSampleRateToolTip();
|
||||||
|
|
||||||
getAgcModeCombo().setEnabled(hasTuner());
|
getAgcButton().setEnabled(hasTuner());
|
||||||
if(hasTuner())
|
if(hasTuner())
|
||||||
{
|
{
|
||||||
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
|
AgcMode current = getTunerController().getControlRsp().getAgcMode();
|
||||||
|
getAgcButton().setSelected(current == null || current.equals(AgcMode.ENABLE));
|
||||||
//Register to receive gain overload notifications
|
//Register to receive gain overload notifications
|
||||||
getTunerController().getControlRsp().setGainOverloadListener(this);
|
getTunerController().getControlRsp().setGainOverloadListener(this);
|
||||||
}
|
}
|
||||||
@@ -259,7 +260,7 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
|
|||||||
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
|
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
|
||||||
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
||||||
getConfiguration().setGain(getGainSlider().getValue());
|
getConfiguration().setGain(getGainSlider().getValue());
|
||||||
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
|
getConfiguration().setAgcMode(getAgcButton().isSelected() ? AgcMode.ENABLE : AgcMode.DISABLE);
|
||||||
|
|
||||||
saveConfiguration();
|
saveConfiguration();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public class RspDxTunerEditor extends RspTunerEditor<RspDxTunerConfiguration>
|
|||||||
add(new JLabel("IF AGC Mode:"));
|
add(new JLabel("IF AGC Mode:"));
|
||||||
JPanel gainPanel = new JPanel();
|
JPanel gainPanel = new JPanel();
|
||||||
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
|
||||||
gainPanel.add(getAgcModeCombo());
|
gainPanel.add(getAgcButton());
|
||||||
gainPanel.add(getGainOverloadButton());
|
gainPanel.add(getGainOverloadButton());
|
||||||
add(gainPanel, "wrap");
|
add(gainPanel, "wrap");
|
||||||
|
|
||||||
@@ -158,10 +158,11 @@ public class RspDxTunerEditor extends RspTunerEditor<RspDxTunerConfiguration>
|
|||||||
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
|
||||||
updateSampleRateToolTip();
|
updateSampleRateToolTip();
|
||||||
|
|
||||||
getAgcModeCombo().setEnabled(hasTuner());
|
getAgcButton().setEnabled(hasTuner());
|
||||||
if(hasTuner())
|
if(hasTuner())
|
||||||
{
|
{
|
||||||
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
|
AgcMode current = getTunerController().getControlRsp().getAgcMode();
|
||||||
|
getAgcButton().setSelected(current == null || current.equals(AgcMode.ENABLE));
|
||||||
//Register to receive gain overload notifications
|
//Register to receive gain overload notifications
|
||||||
getTunerController().getControlRsp().setGainOverloadListener(this);
|
getTunerController().getControlRsp().setGainOverloadListener(this);
|
||||||
}
|
}
|
||||||
@@ -239,7 +240,7 @@ public class RspDxTunerEditor extends RspTunerEditor<RspDxTunerConfiguration>
|
|||||||
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
|
||||||
getConfiguration().setAntenna((RspDxAntenna) getAntennaCombo().getSelectedItem());
|
getConfiguration().setAntenna((RspDxAntenna) getAntennaCombo().getSelectedItem());
|
||||||
getConfiguration().setGain(getGainSlider().getValue());
|
getConfiguration().setGain(getGainSlider().getValue());
|
||||||
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
|
getConfiguration().setAgcMode(getAgcButton().isSelected() ? AgcMode.ENABLE : AgcMode.DISABLE);
|
||||||
|
|
||||||
saveConfiguration();
|
saveConfiguration();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
/*******************************************************************************
|
|
||||||
* sdr-trunk
|
|
||||||
* Copyright (C) 2014-2018 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.spectrum;
|
|
||||||
|
|
||||||
import io.github.dsheirer.properties.SystemProperties;
|
|
||||||
|
|
||||||
import javax.swing.JMenuItem;
|
|
||||||
import java.awt.event.ActionEvent;
|
|
||||||
import java.awt.event.ActionListener;
|
|
||||||
|
|
||||||
public class ClearTunerMenuItem extends JMenuItem
|
|
||||||
{
|
|
||||||
private SpectralDisplayPanel mSpectralDisplayPanel;
|
|
||||||
|
|
||||||
public ClearTunerMenuItem(SpectralDisplayPanel spectralDisplayPanel)
|
|
||||||
{
|
|
||||||
super("Disable Spectrum & Waterfall");
|
|
||||||
|
|
||||||
mSpectralDisplayPanel = spectralDisplayPanel;
|
|
||||||
|
|
||||||
addActionListener(new ActionListener()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e)
|
|
||||||
{
|
|
||||||
SystemProperties properties = SystemProperties.getInstance();
|
|
||||||
properties.set(SpectralDisplayPanel.SPECTRAL_DISPLAY_ENABLED, false);
|
|
||||||
mSpectralDisplayPanel.clearTuner();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* 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.spectrum;
|
||||||
|
|
||||||
|
import io.github.dsheirer.properties.SystemProperties;
|
||||||
|
import jiconfont.icons.font_awesome.FontAwesome;
|
||||||
|
import jiconfont.swing.IconFontSwing;
|
||||||
|
|
||||||
|
import javax.swing.JMenuItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu item to disable the spectrum and waterfall and no longer monitor a tuner.
|
||||||
|
*/
|
||||||
|
public class DisableSpectrumWaterfallMenuItem extends JMenuItem
|
||||||
|
{
|
||||||
|
private SpectralDisplayPanel mSpectralDisplayPanel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an instance
|
||||||
|
* @param spectralDisplayPanel to disable.
|
||||||
|
*/
|
||||||
|
public DisableSpectrumWaterfallMenuItem(SpectralDisplayPanel spectralDisplayPanel)
|
||||||
|
{
|
||||||
|
super("Disable Spectrum & Waterfall");
|
||||||
|
setIcon(IconFontSwing.buildIcon(FontAwesome.EYE_SLASH, 12));
|
||||||
|
|
||||||
|
mSpectralDisplayPanel = spectralDisplayPanel;
|
||||||
|
|
||||||
|
addActionListener(e -> {
|
||||||
|
SystemProperties properties = SystemProperties.getInstance();
|
||||||
|
properties.set(SpectralDisplayPanel.SPECTRAL_DISPLAY_ENABLED, false);
|
||||||
|
mSpectralDisplayPanel.clearTuner();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -763,7 +763,7 @@ public class SpectralDisplayPanel extends JPanel
|
|||||||
if(mTuner != null)
|
if(mTuner != null)
|
||||||
{
|
{
|
||||||
contextMenu.add(new JSeparator());
|
contextMenu.add(new JSeparator());
|
||||||
contextMenu.add(new ClearTunerMenuItem(SpectralDisplayPanel.this));
|
contextMenu.add(new DisableSpectrumWaterfallMenuItem(SpectralDisplayPanel.this));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean separatorAdded = false;
|
boolean separatorAdded = false;
|
||||||
|
|||||||
BIN
src/main/resources/images/openmhz.png
Normal file
BIN
src/main/resources/images/openmhz.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user