mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 07:37:25 -05:00
Compare commits
410 Commits
v0.6.0-alp
...
jtransform
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
415a7017f9 | ||
|
|
0b3ce66cad | ||
|
|
14c77c4926 | ||
|
|
8aadbe4706 | ||
|
|
19d18b96b3 | ||
|
|
59e64a73ff | ||
|
|
8b338dec2d | ||
|
|
bab02222a4 | ||
|
|
9359f523c1 | ||
|
|
e385d2fdec | ||
|
|
49c3d3ff55 | ||
|
|
7b249af944 | ||
|
|
197cec04f3 | ||
|
|
c75baa48f4 | ||
|
|
866aadc8c3 | ||
|
|
276f4a316a | ||
|
|
e65ff5a8dc | ||
|
|
85bfd21257 | ||
|
|
9ebbf957a2 | ||
|
|
00c0418017 | ||
|
|
33a2471338 | ||
|
|
d86fba6945 | ||
|
|
248fe3e4f7 | ||
|
|
9a6dc2b0a0 | ||
|
|
6b9cd2d317 | ||
|
|
f2c98cdb40 | ||
|
|
5635341d12 | ||
|
|
df739f525f | ||
|
|
c8b1c8ed3a | ||
|
|
b0428a1d8b | ||
|
|
c0a96f94fb | ||
|
|
1945b83562 | ||
|
|
dc550f3121 | ||
|
|
0460f2b77b | ||
|
|
b37ed51d07 | ||
|
|
f1183e531d | ||
|
|
16a66112fc | ||
|
|
8aa6ae089a | ||
|
|
728a83a82d | ||
|
|
c05328cd1e | ||
|
|
a9120ed65e | ||
|
|
9305abc784 | ||
|
|
0c637ed80d | ||
|
|
7ee3c5d251 | ||
|
|
a33c99347b | ||
|
|
97cae50bdd | ||
|
|
80360029ef | ||
|
|
af5dbcfcdc | ||
|
|
9dcebb4943 | ||
|
|
e2d9c4cda3 | ||
|
|
3e21887d27 | ||
|
|
d60720aab7 | ||
|
|
2292af6c07 | ||
|
|
ff128f56d2 | ||
|
|
3c01c644c9 | ||
|
|
ade4dc47cd | ||
|
|
cbbbe3a131 | ||
|
|
b385a42b63 | ||
|
|
ccae1d05e7 | ||
|
|
788d994ad7 | ||
|
|
7cf6974b6b | ||
|
|
cf0737ed90 | ||
|
|
75fa495c99 | ||
|
|
7945f0ce63 | ||
|
|
a053315675 | ||
|
|
de0e722f0c | ||
|
|
a42592daa9 | ||
|
|
7f9dc86b23 | ||
|
|
2f5c165aec | ||
|
|
29164ac75d | ||
|
|
185df1735a | ||
|
|
c81e6cccfa | ||
|
|
a2509554db | ||
|
|
b83f77c7c6 | ||
|
|
d8e2e0ac57 | ||
|
|
b0a0ad4b43 | ||
|
|
17cdd0df27 | ||
|
|
6d23aaa06f | ||
|
|
933629e30d | ||
|
|
8a6bc03ffc | ||
|
|
0c616d4531 | ||
|
|
b472c68b80 | ||
|
|
f6f4b07623 | ||
|
|
de100b4662 | ||
|
|
bbde3033b7 | ||
|
|
f6866f5408 | ||
|
|
d09fad56db | ||
|
|
7c06075880 | ||
|
|
5b597e29eb | ||
|
|
feb66831d4 | ||
|
|
37d47f4219 | ||
|
|
0d886c272e | ||
|
|
48202da9e4 | ||
|
|
c4d9d58a5d | ||
|
|
fe5679fed7 | ||
|
|
b2be38b239 | ||
|
|
912925ee7d | ||
|
|
d86f8bf3d9 | ||
|
|
88d8633958 | ||
|
|
3bf3de82fc | ||
|
|
03a015f504 | ||
|
|
15b7923a4f | ||
|
|
189173e289 | ||
|
|
2dcd994eb2 | ||
|
|
afadb227db | ||
|
|
5dbcd5a8ad | ||
|
|
728a85c180 | ||
|
|
dd6ddd59e9 | ||
|
|
758794fdb4 | ||
|
|
49b95a9445 | ||
|
|
a4bcdfa769 | ||
|
|
872498badc | ||
|
|
4afb74a847 | ||
|
|
96f0b7dd55 | ||
|
|
b360950b80 | ||
|
|
5b0a1401a1 | ||
|
|
3fa114501b | ||
|
|
8935835921 | ||
|
|
dec255d444 | ||
|
|
1cb0490985 | ||
|
|
c8235ea4fc | ||
|
|
7d69d829b5 | ||
|
|
a46abf7da8 | ||
|
|
437264e272 | ||
|
|
8718d04ef5 | ||
|
|
4e367bb5fe | ||
|
|
71d2cb6fca | ||
|
|
e856de273d | ||
|
|
ea0a9da24c | ||
|
|
b3cd7d787a | ||
|
|
9a13a82ed1 | ||
|
|
9b71508d49 | ||
|
|
694ea83a80 | ||
|
|
70fb74b787 | ||
|
|
9ffa02e2e5 | ||
|
|
11abb87177 | ||
|
|
49cf92c7ab | ||
|
|
4cf51405ea | ||
|
|
db83d5121c | ||
|
|
d61d9c07b0 | ||
|
|
5c9c3fa503 | ||
|
|
552d3a6ce9 | ||
|
|
3bf425f94a | ||
|
|
660a5acd2f | ||
|
|
551277f399 | ||
|
|
a12040be5c | ||
|
|
e4be7dc5f4 | ||
|
|
b615c1e16a | ||
|
|
912af4e3e6 | ||
|
|
df0d0468c8 | ||
|
|
7509245751 | ||
|
|
02e236202c | ||
|
|
4b1d294211 | ||
|
|
40840dbd5c | ||
|
|
786ae9369b | ||
|
|
b7bbf10415 | ||
|
|
486ac01416 | ||
|
|
9e01cc48a5 | ||
|
|
37996d0477 | ||
|
|
a15ff9040b | ||
|
|
b3cd3f431f | ||
|
|
649d1f650a | ||
|
|
215e4064fa | ||
|
|
66a0b2543b | ||
|
|
378841b408 | ||
|
|
e28d726182 | ||
|
|
71bc27e3d9 | ||
|
|
bcfb2700cb | ||
|
|
2d9491ac93 | ||
|
|
4977c3e4fa | ||
|
|
54a0ca5247 | ||
|
|
024a624083 | ||
|
|
9a02416403 | ||
|
|
91e5bc8ba1 | ||
|
|
97afdfa8a9 | ||
|
|
67f91b80f2 | ||
|
|
1b3ce431e6 | ||
|
|
d24895575c | ||
|
|
c97e355349 | ||
|
|
3c8cd0386d | ||
|
|
600db3fb8a | ||
|
|
161595e7c8 | ||
|
|
0ecdf4dd2b | ||
|
|
32ce79b8af | ||
|
|
8070ae3c4b | ||
|
|
55b20c75c0 | ||
|
|
5d4dff7948 | ||
|
|
b1229b6971 | ||
|
|
9474d53a93 | ||
|
|
6f05db723f | ||
|
|
056bccb89a | ||
|
|
e00cd90aef | ||
|
|
8bb59d581f | ||
|
|
b9068da99c | ||
|
|
9f3f7e65a4 | ||
|
|
e6a804836b | ||
|
|
5d8d3e4a41 | ||
|
|
298d9d7016 | ||
|
|
d7fbbcefc5 | ||
|
|
36e198eb1b | ||
|
|
130e59f4c1 | ||
|
|
9223e7c056 | ||
|
|
c3487411fb | ||
|
|
142dd9d579 | ||
|
|
53d537bda8 | ||
|
|
24e11b8ed6 | ||
|
|
14a7b040f9 | ||
|
|
2af3e992a1 | ||
|
|
737c3faacc | ||
|
|
03792b0245 | ||
|
|
dddda052fa | ||
|
|
c50a914632 | ||
|
|
5f082973eb | ||
|
|
2f0419f796 | ||
|
|
cbe451a793 | ||
|
|
aa8d449140 | ||
|
|
7e709d9d6f | ||
|
|
f5938fa8e2 | ||
|
|
ca2509e9f9 | ||
|
|
1d2ba8c06c | ||
|
|
73275aa8ae | ||
|
|
2c32096ec9 | ||
|
|
6532e33296 | ||
|
|
7ea8cb63d2 | ||
|
|
1df29b62eb | ||
|
|
8a5b31f143 | ||
|
|
4d7a8e5980 | ||
|
|
aeeed71231 | ||
|
|
f6c4cc8a5f | ||
|
|
d019388034 | ||
|
|
e125d6a452 | ||
|
|
6b5a1fb827 | ||
|
|
0491b08818 | ||
|
|
7b9adc1da2 | ||
|
|
3acaea0019 | ||
|
|
be02393f65 | ||
|
|
614bf72f01 | ||
|
|
bed28b82ab | ||
|
|
c047d48d91 | ||
|
|
5222bffce4 | ||
|
|
42e7a49d37 | ||
|
|
686bddf7d5 | ||
|
|
1c420b8eee | ||
|
|
e2a0325d07 | ||
|
|
9d03c151af | ||
|
|
3c18bfdc81 | ||
|
|
d89ba65238 | ||
|
|
d2cf2ee7b5 | ||
|
|
7a03331955 | ||
|
|
7c5ef3803a | ||
|
|
6f3e7c86e5 | ||
|
|
fc51de2b4f | ||
|
|
e1e75710cb | ||
|
|
87ce08e9b9 | ||
|
|
1a5c7521a5 | ||
|
|
b54e94dfbe | ||
|
|
19977d5f4d | ||
|
|
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 | ||
|
|
16aed8741d | ||
|
|
e878ca0c7f | ||
|
|
ca61dd2543 | ||
|
|
6187253589 | ||
|
|
b51c80742f | ||
|
|
15352b9216 | ||
|
|
a848b21ae8 | ||
|
|
279aca7287 | ||
|
|
88879c956a | ||
|
|
729b7e27a8 | ||
|
|
feaed168ae | ||
|
|
50edb18db3 | ||
|
|
7302e2806d | ||
|
|
8845306b4a | ||
|
|
09e19e1486 | ||
|
|
0f5478925c | ||
|
|
09dce0b0d1 | ||
|
|
ad98b1cb5e | ||
|
|
8effd97749 | ||
|
|
9dbed72c32 | ||
|
|
b4dd3692b8 | ||
|
|
030cbb856c | ||
|
|
511101e9b7 | ||
|
|
60dd2b2df7 | ||
|
|
218dc94e02 | ||
|
|
2440f578b1 | ||
|
|
09c87237cb | ||
|
|
3d974ddd91 | ||
|
|
1489c970ba | ||
|
|
9e0afca3d1 | ||
|
|
99eda4013e | ||
|
|
a62f81f890 | ||
|
|
66f0731731 | ||
|
|
dc239801c6 | ||
|
|
d4ba0988bc | ||
|
|
ad0f953b2a | ||
|
|
e1bc9eabd6 | ||
|
|
d3dc1a8667 | ||
|
|
bf278120de | ||
|
|
2f52f6e009 | ||
|
|
02363c3113 | ||
|
|
cfaac0fcbc | ||
|
|
bee0405e00 | ||
|
|
50bdf0ee81 | ||
|
|
a27ddbe0d0 | ||
|
|
0d652b9153 | ||
|
|
9468fd189b | ||
|
|
215737ad9b | ||
|
|
123c25b142 | ||
|
|
af56198f0e | ||
|
|
3c73fdfd6e | ||
|
|
b391ebbb5f | ||
|
|
bad5a1f9f0 | ||
|
|
a7470b463f | ||
|
|
d6c706cb35 | ||
|
|
a0d6950c75 | ||
|
|
2495200b36 | ||
|
|
bfe7ae0b1b | ||
|
|
4912abbf07 | ||
|
|
2f2998d02e | ||
|
|
c96349fa1c | ||
|
|
f640576cc3 | ||
|
|
77d725a488 | ||
|
|
2c0305f928 | ||
|
|
21fd61d507 | ||
|
|
8fb56a5342 | ||
|
|
5d7b73f876 | ||
|
|
85b2a38058 | ||
|
|
c78517ee75 | ||
|
|
ae116d3dfe | ||
|
|
93252f9a40 | ||
|
|
3c03a4db2e | ||
|
|
d0fcd617aa | ||
|
|
6ca0decbd6 | ||
|
|
ddd9ca933e | ||
|
|
b2cc29569e | ||
|
|
411735772b | ||
|
|
2ffb2963af | ||
|
|
227c88c8a2 | ||
|
|
cbe9c79498 | ||
|
|
1b8c100b43 | ||
|
|
30c5fd64f2 | ||
|
|
3a6b486340 | ||
|
|
ded7335dbf | ||
|
|
005a4f8119 | ||
|
|
1ac49bfa7f | ||
|
|
c302c4d121 | ||
|
|
b0a524cf62 | ||
|
|
d246ffff30 | ||
|
|
adfc8b2d6f | ||
|
|
ad2ab08466 | ||
|
|
f3369410b2 | ||
|
|
bac38a5bd8 | ||
|
|
e5f5df74ad | ||
|
|
b3837f581a | ||
|
|
b2ab6f58e9 | ||
|
|
e04b59ba15 | ||
|
|
366eacd234 | ||
|
|
f792876c0a | ||
|
|
19f7a5171b | ||
|
|
5b6377cc11 | ||
|
|
9502ca3069 | ||
|
|
77629000b2 | ||
|
|
ca7916f992 | ||
|
|
9de83843a3 | ||
|
|
cbab7bb0c9 | ||
|
|
3df8d60eed | ||
|
|
b39dd1a19f | ||
|
|
0af5963d08 | ||
|
|
4f8b8e978b | ||
|
|
5654254cbb | ||
|
|
11f37e896a | ||
|
|
e8f563deeb | ||
|
|
f877354a11 | ||
|
|
df3d3b5ddc |
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support and Questions - How do I XXX? Does it support XXX?
|
||||
url: https://github.com/DSheirer/sdrtrunk/wiki/Support
|
||||
about: Please ask your user questions here -- DO NOT create an issue if you have a user question please!!
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: How-To Question or Does The Software Support xxx
|
||||
about: End user question or support discussion
|
||||
title: STOP - Don't Use Issue Reporting to Post End-User Questions!!!
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
STOP !!!!
|
||||
|
||||
GitHub issues are for reporting software bugs or requesting enhancements.
|
||||
|
||||
If you have a question about how to use the software, or if you want to know if sdrtrunk supports a protocol, tuner, or other aspect, please post your question in one of the Support forums.
|
||||
|
||||
Visit: https://github.com/DSheirer/sdrtrunk/wiki
|
||||
10
.github/workflows/gradle.yml
vendored
10
.github/workflows/gradle.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 19
|
||||
uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 27
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '19'
|
||||
java-version: '27'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@937999e9cc2425eddc7fd62d1053baf041147db7
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: clean build
|
||||
|
||||
49
.github/workflows/nightly.yml
vendored
Normal file
49
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# Create nightly release for Linux, Mac and Windows using the Linux runner.
|
||||
name: Nightly Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
# Don't run nightly build for pull requests - uncomment to test changes to this action within scope of the PR review
|
||||
# pull_request:
|
||||
# branches: [ master ]
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 27
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
# Must match the JDK version downloaded in build.gradle (jdk_download_base) - jlink requires an exact version match
|
||||
java-version: '27.0.0'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
|
||||
- name: Build Linux and Mac releases with Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: runtimeZipOthers -PprojectVersion=nightly
|
||||
|
||||
- name: Build Windows release with Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: runtimeZipWindows -PprojectVersion=nightly
|
||||
|
||||
- name: Create Build Info file
|
||||
run: |
|
||||
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt
|
||||
|
||||
- name: Add build artifacts to nightly release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: nightly
|
||||
prerelease: true
|
||||
files: |
|
||||
build/image/*.zip
|
||||
build/image/build_info.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
7
.idea/dictionaries/denny.xml
generated
7
.idea/dictionaries/denny.xml
generated
@@ -2,18 +2,24 @@
|
||||
<dictionary name="denny">
|
||||
<words>
|
||||
<w>bahroo</w>
|
||||
<w>baseband</w>
|
||||
<w>basebanded</w>
|
||||
<w>broadcastify</w>
|
||||
<w>cach</w>
|
||||
<w>correlator</w>
|
||||
<w>deinterleave</w>
|
||||
<w>descrambled</w>
|
||||
<w>dibit</w>
|
||||
<w>dinf</w>
|
||||
<w>dqpsk</w>
|
||||
<w>esnid</w>
|
||||
<w>fleetsync</w>
|
||||
<w>heterdyned</w>
|
||||
<w>heterodynes</w>
|
||||
<w>hytera</w>
|
||||
<w>icecast</w>
|
||||
<w>ident</w>
|
||||
<w>lcns</w>
|
||||
<w>nutall</w>
|
||||
<w>overflowable</w>
|
||||
<w>polyphase</w>
|
||||
@@ -29,6 +35,7 @@
|
||||
<w>talkgroups</w>
|
||||
<w>tgid</w>
|
||||
<w>ultravox</w>
|
||||
<w>unsynchronized</w>
|
||||
<w>wacn</w>
|
||||
<w>xtea</w>
|
||||
</words>
|
||||
|
||||
45
.idea/misc.xml
generated
45
.idea/misc.xml
generated
@@ -4,7 +4,50 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_19_PREVIEW" project-jdk-name="liberica-19.0.1.fx" project-jdk-type="JavaSDK">
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
|
||||
<option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" />
|
||||
<option name="myOrdered" value="true" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="13">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||
<item index="5" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
|
||||
<item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="7" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="8" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||
<item index="9" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
|
||||
<item index="10" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
|
||||
<item index="11" class="java.lang.String" itemvalue="org.springframework.lang.Nullable" />
|
||||
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="13">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||
<item index="5" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
|
||||
<item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="7" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="8" class="java.lang.String" itemvalue="lombok.NonNull" />
|
||||
<item index="9" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
|
||||
<item index="10" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
|
||||
<item index="11" class="java.lang.String" itemvalue="org.springframework.lang.NonNull" />
|
||||
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_27_PREVIEW" project-jdk-name="liberica-27" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
29
README.md
29
README.md
@@ -1,8 +1,28 @@
|
||||

|
||||

|
||||
|
||||
# MacOS Tahoe 26.1 Users - Attention:
|
||||
Changes to USB support in Tahoe version 26.x cause sdrtrunk to fail to launch. Do the following to install the latest libusb and create a symbolic link and then use the nightly build which includes an updated usb4java native library for Tahoe with ARM processor. There may still be issue(s) with MacOS accessing your USB SDR tuners.
|
||||
|
||||
```
|
||||
brew install libusb --HEAD
|
||||
cd /opt
|
||||
sudo mkdir local
|
||||
cd local
|
||||
sudo mkdir lib
|
||||
```
|
||||
Next, find where brew installed the libusb library, for example: ```/opt/homebrew/Cellar/libusb/HEAD-9ceaa52/lib/libusb-1.0.0.dylib``` Note: the folder "HEAD-9ceaa52" is the version stamp for HEAD when you installed from it.
|
||||
|
||||
Finally, create a symbolic link from the installed library to the place where usb4java is expecting to find libusb (/opt/local/lib/libusb-1.0.0.dylib)
|
||||
|
||||
```
|
||||
sudo ln -s /opt/homebrew/Cellar/libusb/HEAD-9ceaa52/lib/libusb-1.0.0.dylib /opt/local/lib/libusb-1.0.0.dylib
|
||||
```
|
||||
|
||||
# sdrtrunk
|
||||
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
|
||||
|
||||
* [Help/Wiki Home Page](https://github.com/DSheirer/sdrtrunk/wiki)
|
||||
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/Getting-Started)
|
||||
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/User-Manual)
|
||||
* [Download](https://github.com/DSheirer/sdrtrunk/releases)
|
||||
@@ -18,7 +38,14 @@ All release versions of sdrtrunk are available from the [releases](https://githu
|
||||
* **(beta)** These versions are currently being tested for bugs and functionality prior to final release.
|
||||
* **(final)** These versions have been tested and are the current release version.
|
||||
|
||||
## Download Nightly Software Build
|
||||
The [nightly](https://github.com/DSheirer/sdrtrunk/releases/tag/nightly) release contains current builds of the software
|
||||
for all supported operating systems. This version of the software may contain bugs and may not run correctly. However,
|
||||
it let's you preview the most recent changes and fixes before the next software release. **Always backup your
|
||||
playlist(s) before you use the nightly builds.** Note: the nightly release is updated each time code changes are
|
||||
committed to the code base, so it's not really 'nightly' as much as it is 'current'.
|
||||
|
||||
## Minimum System Requirements
|
||||
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
|
||||
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac (64-bit, 12.x or higher)
|
||||
* **CPU:** 4-core
|
||||
* **RAM:** 8GB or more (preferred). Depending on usage, 4GB may be sufficient.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
Auto-generating Java code from SDRPlay API using jextract.
|
||||
|
||||
Note: the api headers in the 3.11 folder are equivalent to the 3.08 headers. The only
|
||||
difference from the 3.07 headers is the addition of the boolean 'valid' flag to DeviceT structure.
|
||||
|
||||
1. Download JDK19 compatible build of jextract JDK: https://jdk.java.net/jextract/
|
||||
2. If generating on windows platform, modify the sdrplay_api.h to remove the windows
|
||||
pieces of the file. Specifically:
|
||||
2a. Comment out these lines but leave the *HANDLE typedef:
|
||||
|
||||
//#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
//#include "windows.h"
|
||||
//#elif defined (__GNUC__)
|
||||
typedef void *HANDLE;
|
||||
//#endif
|
||||
|
||||
//#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
//#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
//#define _SDRPLAY_DLL_QUALIFIER
|
||||
//#endif
|
||||
//#endif // _SDRPLAY_DLL_QUALIFIER
|
||||
|
||||
2b. Remove the '_SDRPLAY_DLL_QUALIFIER' from in front of the method declarations
|
||||
|
||||
2c. Change the 3.11 version string to 3.08 either in the header file, or in the generated java class
|
||||
|
||||
3. Open command prompt to the root source code where the generated files should be placed:
|
||||
..\SDRtrunk\sdrplay-api\src\main\java
|
||||
|
||||
4. Generate the code:
|
||||
c:\Users\Denny\Downloads\jdk-19-ea2-panama\bin\jextract "C:\Users\Denny\git\sdrtrunk\sdrplay-api\api\v3_11\sdrplay_api.h" -l libsdrplay_api --source
|
||||
|
||||
5. Do this for all versions of the API.
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
#include "sdrplay_api_rx_channel.h"
|
||||
#include "sdrplay_api_callback.h"
|
||||
|
||||
//#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
//#if defined(_M_X64) || defined(_M_IX86)
|
||||
//#include "windows.h"
|
||||
//#elif defined (__GNUC__)
|
||||
typedef void *HANDLE;
|
||||
//#endif
|
||||
|
||||
//
|
||||
//#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86))
|
||||
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
//#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
//#define _SDRPLAY_DLL_QUALIFIER
|
||||
@@ -21,7 +21,7 @@ typedef void *HANDLE;
|
||||
|
||||
// Application code should check that it is compiled against the same API version
|
||||
// sdrplay_api_ApiVersion() returns the API version
|
||||
#define SDRPLAY_API_VERSION (float)(3.11)
|
||||
#define SDRPLAY_API_VERSION (float)(3.08)
|
||||
|
||||
// API Constants
|
||||
#define SDRPLAY_MAX_DEVICES (16)
|
||||
@@ -198,30 +198,30 @@ extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
// Comman API function definitions
|
||||
sdrplay_api_ErrT sdrplay_api_Open(void);
|
||||
sdrplay_api_ErrT sdrplay_api_Close(void);
|
||||
sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
|
||||
sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
|
||||
sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
|
||||
sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
|
||||
sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
|
||||
const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
|
||||
sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
|
||||
sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
|
||||
|
||||
// Device API function definitions
|
||||
sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
|
||||
sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
|
||||
sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
// // Comman API function definitions
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Open(void);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Close(void);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
|
||||
// _SDRPLAY_DLL_QUALIFIER const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
|
||||
//
|
||||
// // Device API function definitions
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
// sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
// sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
78
artifacts/sdrplay-api-headers/v3_08/sdrplay_api_callback.h
Normal file
78
artifacts/sdrplay-api-headers/v3_08/sdrplay_api_callback.h
Normal file
@@ -0,0 +1,78 @@
|
||||
#ifndef SDRPLAY_API_CALLBACK_H
|
||||
#define SDRPLAY_API_CALLBACK_H
|
||||
|
||||
// Event callback enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Overload_Detected = 0,
|
||||
sdrplay_api_Overload_Corrected = 1,
|
||||
} sdrplay_api_PowerOverloadCbEventIdT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_MasterInitialised = 0,
|
||||
sdrplay_api_SlaveAttached = 1,
|
||||
sdrplay_api_SlaveDetached = 2,
|
||||
sdrplay_api_SlaveInitialised = 3,
|
||||
sdrplay_api_SlaveUninitialised = 4,
|
||||
sdrplay_api_MasterDllDisappeared = 5,
|
||||
sdrplay_api_SlaveDllDisappeared = 6,
|
||||
} sdrplay_api_RspDuoModeCbEventIdT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_GainChange = 0,
|
||||
sdrplay_api_PowerOverloadChange = 1,
|
||||
sdrplay_api_DeviceRemoved = 2,
|
||||
sdrplay_api_RspDuoModeChange = 3,
|
||||
} sdrplay_api_EventT;
|
||||
|
||||
// Event callback parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned int gRdB;
|
||||
unsigned int lnaGRdB;
|
||||
double currGain;
|
||||
} sdrplay_api_GainCbParamT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_PowerOverloadCbEventIdT powerOverloadChangeType;
|
||||
} sdrplay_api_PowerOverloadCbParamT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_RspDuoModeCbEventIdT modeChangeType;
|
||||
} sdrplay_api_RspDuoModeCbParamT;
|
||||
|
||||
// Event parameters overlay
|
||||
typedef union
|
||||
{
|
||||
sdrplay_api_GainCbParamT gainParams;
|
||||
sdrplay_api_PowerOverloadCbParamT powerOverloadParams;
|
||||
sdrplay_api_RspDuoModeCbParamT rspDuoModeParams;
|
||||
} sdrplay_api_EventParamsT;
|
||||
|
||||
// Stream callback parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned int firstSampleNum;
|
||||
int grChanged;
|
||||
int rfChanged;
|
||||
int fsChanged;
|
||||
unsigned int numSamples;
|
||||
} sdrplay_api_StreamCbParamsT;
|
||||
|
||||
// Callback function prototypes
|
||||
typedef void (*sdrplay_api_StreamCallback_t)(short *xi, short *xq, sdrplay_api_StreamCbParamsT *params, unsigned int numSamples, unsigned int reset, void *cbContext);
|
||||
typedef void (*sdrplay_api_EventCallback_t)(sdrplay_api_EventT eventId, sdrplay_api_TunerSelectT tuner, sdrplay_api_EventParamsT *params, void *cbContext);
|
||||
|
||||
// Callback function struct
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_StreamCallback_t StreamACbFn;
|
||||
sdrplay_api_StreamCallback_t StreamBCbFn;
|
||||
sdrplay_api_EventCallback_t EventCbFn;
|
||||
} sdrplay_api_CallbackFnsT;
|
||||
|
||||
#endif //SDRPLAY_API_CALLBACK_H
|
||||
45
artifacts/sdrplay-api-headers/v3_08/sdrplay_api_rspDx.h
Normal file
45
artifacts/sdrplay-api-headers/v3_08/sdrplay_api_rspDx.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef SDRPLAY_API_RSPDX_H
|
||||
#define SDRPLAY_API_RSPDX_H
|
||||
|
||||
#include "sdrplay_api_tuner.h"
|
||||
|
||||
#define RSPDX_NUM_LNA_STATES 28 // Number of LNA states in all bands (except where defined differently below)
|
||||
#define RSPDX_NUM_LNA_STATES_AMPORT2_0_12 19 // Number of LNA states when using AM Port 2 between 0 and 12MHz
|
||||
#define RSPDX_NUM_LNA_STATES_AMPORT2_12_60 20 // Number of LNA states when using AM Port 2 between 12 and 60MHz
|
||||
#define RSPDX_NUM_LNA_STATES_VHF_BAND3 27 // Number of LNA states in VHF and Band3
|
||||
#define RSPDX_NUM_LNA_STATES_420MHZ 21 // Number of LNA states in 420MHz band
|
||||
#define RSPDX_NUM_LNA_STATES_LBAND 19 // Number of LNA states in L-band
|
||||
#define RSPDX_NUM_LNA_STATES_DX 26 // Number of LNA states in DX path
|
||||
|
||||
// RSPdx parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDx_ANTENNA_A = 0,
|
||||
sdrplay_api_RspDx_ANTENNA_B = 1,
|
||||
sdrplay_api_RspDx_ANTENNA_C = 2,
|
||||
} sdrplay_api_RspDx_AntennaSelectT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDx_HDRMODE_BW_0_200 = 0,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_0_500 = 1,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_1_200 = 2,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_1_700 = 3,
|
||||
} sdrplay_api_RspDx_HdrModeBwT;
|
||||
|
||||
// RSPdx parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char hdrEnable; // default: 0
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_RspDx_AntennaSelectT antennaSel; // default: sdrplay_api_RspDx_ANTENNA_A
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
} sdrplay_api_RspDxParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_RspDx_HdrModeBwT hdrBw; // default: sdrplay_api_RspDx_HDRMODE_BW_1_700
|
||||
} sdrplay_api_RspDxTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSPDX_H
|
||||
235
artifacts/sdrplay-api-headers/v3_15/sdrplay_api.h
Normal file
235
artifacts/sdrplay-api-headers/v3_15/sdrplay_api.h
Normal file
@@ -0,0 +1,235 @@
|
||||
#ifndef SDRPLAY_API_H
|
||||
#define SDRPLAY_API_H
|
||||
|
||||
#include "sdrplay_api_dev.h"
|
||||
#include "sdrplay_api_rx_channel.h"
|
||||
#include "sdrplay_api_callback.h"
|
||||
|
||||
#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
#include "windows.h"
|
||||
#elif defined (__GNUC__)
|
||||
typedef void *HANDLE;
|
||||
#endif
|
||||
|
||||
#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
#define _SDRPLAY_DLL_QUALIFIER
|
||||
#endif
|
||||
#endif // _SDRPLAY_DLL_QUALIFIER
|
||||
|
||||
// Application code should check that it is compiled against the same API version
|
||||
// sdrplay_api_ApiVersion() returns the API version
|
||||
#define SDRPLAY_API_VERSION (float)(3.15)
|
||||
|
||||
// API Constants
|
||||
#define SDRPLAY_MAX_DEVICES (16)
|
||||
#define SDRPLAY_MAX_TUNERS_PER_DEVICE (2)
|
||||
|
||||
#define SDRPLAY_MAX_SER_NO_LEN (64)
|
||||
#define SDRPLAY_MAX_ROOT_NM_LEN (32)
|
||||
|
||||
#define SDRPLAY_RSP1_ID (1)
|
||||
#define SDRPLAY_RSP1A_ID (255)
|
||||
#define SDRPLAY_RSP2_ID (2)
|
||||
#define SDRPLAY_RSPduo_ID (3)
|
||||
#define SDRPLAY_RSPdx_ID (4)
|
||||
#define SDRPLAY_RSP1B_ID (6)
|
||||
#define SDRPLAY_RSPdxR2_ID (7)
|
||||
|
||||
// Enum types
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Success = 0,
|
||||
sdrplay_api_Fail = 1,
|
||||
sdrplay_api_InvalidParam = 2,
|
||||
sdrplay_api_OutOfRange = 3,
|
||||
sdrplay_api_GainUpdateError = 4,
|
||||
sdrplay_api_RfUpdateError = 5,
|
||||
sdrplay_api_FsUpdateError = 6,
|
||||
sdrplay_api_HwError = 7,
|
||||
sdrplay_api_AliasingError = 8,
|
||||
sdrplay_api_AlreadyInitialised = 9,
|
||||
sdrplay_api_NotInitialised = 10,
|
||||
sdrplay_api_NotEnabled = 11,
|
||||
sdrplay_api_HwVerError = 12,
|
||||
sdrplay_api_OutOfMemError = 13,
|
||||
sdrplay_api_ServiceNotResponding = 14,
|
||||
sdrplay_api_StartPending = 15,
|
||||
sdrplay_api_StopPending = 16,
|
||||
sdrplay_api_InvalidMode = 17,
|
||||
sdrplay_api_FailedVerification1 = 18,
|
||||
sdrplay_api_FailedVerification2 = 19,
|
||||
sdrplay_api_FailedVerification3 = 20,
|
||||
sdrplay_api_FailedVerification4 = 21,
|
||||
sdrplay_api_FailedVerification5 = 22,
|
||||
sdrplay_api_FailedVerification6 = 23,
|
||||
sdrplay_api_InvalidServiceVersion = 24
|
||||
} sdrplay_api_ErrT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Update_None = 0x00000000,
|
||||
|
||||
// Reasons for master only mode
|
||||
sdrplay_api_Update_Dev_Fs = 0x00000001,
|
||||
sdrplay_api_Update_Dev_Ppm = 0x00000002,
|
||||
sdrplay_api_Update_Dev_SyncUpdate = 0x00000004,
|
||||
sdrplay_api_Update_Dev_ResetFlags = 0x00000008,
|
||||
|
||||
sdrplay_api_Update_Rsp1a_BiasTControl = 0x00000010,
|
||||
sdrplay_api_Update_Rsp1a_RfNotchControl = 0x00000020,
|
||||
sdrplay_api_Update_Rsp1a_RfDabNotchControl = 0x00000040,
|
||||
|
||||
sdrplay_api_Update_Rsp2_BiasTControl = 0x00000080,
|
||||
sdrplay_api_Update_Rsp2_AmPortSelect = 0x00000100,
|
||||
sdrplay_api_Update_Rsp2_AntennaControl = 0x00000200,
|
||||
sdrplay_api_Update_Rsp2_RfNotchControl = 0x00000400,
|
||||
sdrplay_api_Update_Rsp2_ExtRefControl = 0x00000800,
|
||||
|
||||
sdrplay_api_Update_RspDuo_ExtRefControl = 0x00001000,
|
||||
|
||||
sdrplay_api_Update_Master_Spare_1 = 0x00002000,
|
||||
sdrplay_api_Update_Master_Spare_2 = 0x00004000,
|
||||
|
||||
// Reasons for master and slave mode
|
||||
// Note: sdrplay_api_Update_Tuner_Gr MUST be the first value defined in this section!
|
||||
sdrplay_api_Update_Tuner_Gr = 0x00008000,
|
||||
sdrplay_api_Update_Tuner_GrLimits = 0x00010000,
|
||||
sdrplay_api_Update_Tuner_Frf = 0x00020000,
|
||||
sdrplay_api_Update_Tuner_BwType = 0x00040000,
|
||||
sdrplay_api_Update_Tuner_IfType = 0x00080000,
|
||||
sdrplay_api_Update_Tuner_DcOffset = 0x00100000,
|
||||
sdrplay_api_Update_Tuner_LoMode = 0x00200000,
|
||||
|
||||
sdrplay_api_Update_Ctrl_DCoffsetIQimbalance = 0x00400000,
|
||||
sdrplay_api_Update_Ctrl_Decimation = 0x00800000,
|
||||
sdrplay_api_Update_Ctrl_Agc = 0x01000000,
|
||||
sdrplay_api_Update_Ctrl_AdsbMode = 0x02000000,
|
||||
sdrplay_api_Update_Ctrl_OverloadMsgAck = 0x04000000,
|
||||
|
||||
sdrplay_api_Update_RspDuo_BiasTControl = 0x08000000,
|
||||
sdrplay_api_Update_RspDuo_AmPortSelect = 0x10000000,
|
||||
sdrplay_api_Update_RspDuo_Tuner1AmNotchControl = 0x20000000,
|
||||
sdrplay_api_Update_RspDuo_RfNotchControl = 0x40000000,
|
||||
sdrplay_api_Update_RspDuo_RfDabNotchControl = 0x80000000,
|
||||
} sdrplay_api_ReasonForUpdateT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Update_Ext1_None = 0x00000000,
|
||||
|
||||
// Reasons for master only mode
|
||||
sdrplay_api_Update_RspDx_HdrEnable = 0x00000001,
|
||||
sdrplay_api_Update_RspDx_BiasTControl = 0x00000002,
|
||||
sdrplay_api_Update_RspDx_AntennaControl = 0x00000004,
|
||||
sdrplay_api_Update_RspDx_RfNotchControl = 0x00000008,
|
||||
sdrplay_api_Update_RspDx_RfDabNotchControl = 0x00000010,
|
||||
sdrplay_api_Update_RspDx_HdrBw = 0x00000020,
|
||||
sdrplay_api_Update_RspDuo_ResetSlaveFlags = 0x00000040,
|
||||
|
||||
// Reasons for master and slave mode
|
||||
} sdrplay_api_ReasonForUpdateExtension1T;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_DbgLvl_Disable = 0,
|
||||
sdrplay_api_DbgLvl_Verbose = 1,
|
||||
sdrplay_api_DbgLvl_Warning = 2,
|
||||
sdrplay_api_DbgLvl_Error = 3,
|
||||
sdrplay_api_DbgLvl_Message = 4,
|
||||
} sdrplay_api_DbgLvl_t;
|
||||
|
||||
// Device structure
|
||||
typedef struct
|
||||
{
|
||||
char SerNo[SDRPLAY_MAX_SER_NO_LEN];
|
||||
unsigned char hwVer;
|
||||
sdrplay_api_TunerSelectT tuner;
|
||||
sdrplay_api_RspDuoModeT rspDuoMode;
|
||||
unsigned char valid;
|
||||
double rspDuoSampleFreq;
|
||||
HANDLE dev;
|
||||
} sdrplay_api_DeviceT;
|
||||
|
||||
// Device parameter structure
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_DevParamsT *devParams;
|
||||
sdrplay_api_RxChannelParamsT *rxChannelA;
|
||||
sdrplay_api_RxChannelParamsT *rxChannelB;
|
||||
} sdrplay_api_DeviceParamsT;
|
||||
|
||||
// Extended error message structure
|
||||
typedef struct
|
||||
{
|
||||
char file[256];
|
||||
char function[256];
|
||||
int line;
|
||||
char message[1024];
|
||||
} sdrplay_api_ErrorInfoT;
|
||||
|
||||
// Comman API function types
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Open_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Close_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_ApiVersion_t)(float *apiVer);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_LockDeviceApi_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_UnlockDeviceApi_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_GetDevices_t)(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SelectDevice_t)(sdrplay_api_DeviceT *device);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_ReleaseDevice_t)(sdrplay_api_DeviceT *device);
|
||||
typedef const char* (*sdrplay_api_GetErrorString_t)(sdrplay_api_ErrT err);
|
||||
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastError_t)(sdrplay_api_DeviceT *device);
|
||||
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastErrorByType_t)(sdrplay_api_DeviceT *device, int type, unsigned long long *time);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_DisableHeartbeat_t)(void);
|
||||
|
||||
// Device API function types
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_DebugEnable_t)(HANDLE dev, sdrplay_api_DbgLvl_t dbgLvl);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_GetDeviceParams_t)(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Init_t)(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Uninit_t)(HANDLE dev);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Update_t)(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoActiveTuner_t)(HANDLE dev, sdrplay_api_TunerSelectT *tuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoDualTunerModeSampleRate_t)(double *currentSampleRate, double newSampleRate);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoMode_t)(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
|
||||
// API function definitions
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
// Comman API function definitions
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Open(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Close(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastErrorByType(sdrplay_api_DeviceT *device, int type, unsigned long long *time);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
|
||||
|
||||
// Device API function definitions
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //SDRPLAY_API_H
|
||||
55
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_control.h
Normal file
55
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_control.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef SDRPLAY_API_CONTROL_H
|
||||
#define SDRPLAY_API_CONTROL_H
|
||||
|
||||
// Control parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_AGC_DISABLE = 0,
|
||||
sdrplay_api_AGC_100HZ = 1,
|
||||
sdrplay_api_AGC_50HZ = 2,
|
||||
sdrplay_api_AGC_5HZ = 3,
|
||||
sdrplay_api_AGC_CTRL_EN = 4
|
||||
} sdrplay_api_AgcControlT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_ADSB_DECIMATION = 0,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_LOWPASS = 1,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_BANDPASS_2MHZ = 2,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_BANDPASS_3MHZ = 3
|
||||
} sdrplay_api_AdsbModeT;
|
||||
|
||||
// Control parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char DCenable; // default: 1
|
||||
unsigned char IQenable; // default: 1
|
||||
} sdrplay_api_DcOffsetT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char enable; // default: 0
|
||||
unsigned char decimationFactor; // default: 1
|
||||
unsigned char wideBandSignal; // default: 0
|
||||
} sdrplay_api_DecimationT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_AgcControlT enable; // default: sdrplay_api_AGC_50HZ
|
||||
int setPoint_dBfs; // default: -60
|
||||
unsigned short attack_ms; // default: 0
|
||||
unsigned short decay_ms; // default: 0
|
||||
unsigned short decay_delay_ms; // default: 0
|
||||
unsigned short decay_threshold_dB; // default: 0
|
||||
int syncUpdate; // default: 0
|
||||
} sdrplay_api_AgcT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_DcOffsetT dcOffset;
|
||||
sdrplay_api_DecimationT decimation;
|
||||
sdrplay_api_AgcT agc;
|
||||
sdrplay_api_AdsbModeT adsbMode; //default: sdrplay_api_ADSB_DECIMATION
|
||||
} sdrplay_api_ControlParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_CONTROL_H
|
||||
51
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_dev.h
Normal file
51
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_dev.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#ifndef SDRPLAY_API_DEV_H
|
||||
#define SDRPLAY_API_DEV_H
|
||||
|
||||
#include "sdrplay_api_rsp1a.h"
|
||||
#include "sdrplay_api_rsp2.h"
|
||||
#include "sdrplay_api_rspDuo.h"
|
||||
#include "sdrplay_api_rspDx.h"
|
||||
|
||||
// Dev parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_ISOCH = 0,
|
||||
sdrplay_api_BULK = 1
|
||||
} sdrplay_api_TransferModeT;
|
||||
|
||||
// Dev parameter structs
|
||||
typedef struct
|
||||
{
|
||||
double fsHz; // default: 2000000.0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
unsigned char reCal; // default: 0
|
||||
} sdrplay_api_FsFreqT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned int sampleNum; // default: 0
|
||||
unsigned int period; // default: 0
|
||||
} sdrplay_api_SyncUpdateT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char resetGainUpdate; // default: 0
|
||||
unsigned char resetRfUpdate; // default: 0
|
||||
unsigned char resetFsUpdate; // default: 0
|
||||
} sdrplay_api_ResetFlagsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
double ppm; // default: 0.0
|
||||
sdrplay_api_FsFreqT fsFreq;
|
||||
sdrplay_api_SyncUpdateT syncUpdate;
|
||||
sdrplay_api_ResetFlagsT resetFlags;
|
||||
sdrplay_api_TransferModeT mode; // default: sdrplay_api_ISOCH
|
||||
unsigned int samplesPerPkt; // default: 0 (output param)
|
||||
sdrplay_api_Rsp1aParamsT rsp1aParams;
|
||||
sdrplay_api_Rsp2ParamsT rsp2Params;
|
||||
sdrplay_api_RspDuoParamsT rspDuoParams;
|
||||
sdrplay_api_RspDxParamsT rspDxParams;
|
||||
} sdrplay_api_DevParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_DEV_H
|
||||
22
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rsp1a.h
Normal file
22
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rsp1a.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef SDRPLAY_API_RSP1A_H
|
||||
#define SDRPLAY_API_RSP1A_H
|
||||
|
||||
#define RSPIA_NUM_LNA_STATES 10
|
||||
#define RSPIA_NUM_LNA_STATES_AM 7
|
||||
#define RSPIA_NUM_LNA_STATES_LBAND 9
|
||||
|
||||
// RSP1A parameter enums
|
||||
|
||||
// RSP1A parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
} sdrplay_api_Rsp1aParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
} sdrplay_api_Rsp1aTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSP1A_H
|
||||
35
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rsp2.h
Normal file
35
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rsp2.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef SDRPLAY_API_RSP2_H
|
||||
#define SDRPLAY_API_RSP2_H
|
||||
|
||||
#define RSPII_NUM_LNA_STATES 9
|
||||
#define RSPII_NUM_LNA_STATES_AMPORT 5
|
||||
#define RSPII_NUM_LNA_STATES_420MHZ 6
|
||||
|
||||
// RSP2 parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Rsp2_ANTENNA_A = 5,
|
||||
sdrplay_api_Rsp2_ANTENNA_B = 6,
|
||||
} sdrplay_api_Rsp2_AntennaSelectT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Rsp2_AMPORT_1 = 1,
|
||||
sdrplay_api_Rsp2_AMPORT_2 = 0,
|
||||
} sdrplay_api_Rsp2_AmPortSelectT;
|
||||
|
||||
// RSP2 parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char extRefOutputEn; // default: 0
|
||||
} sdrplay_api_Rsp2ParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_Rsp2_AmPortSelectT amPortSel; // default: sdrplay_api_Rsp2_AMPORT_2
|
||||
sdrplay_api_Rsp2_AntennaSelectT antennaSel; // default: sdrplay_api_Rsp2_ANTENNA_A
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
} sdrplay_api_Rsp2TunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSP2_H
|
||||
47
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rspDuo.h
Normal file
47
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rspDuo.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifndef SDRPLAY_API_RSPduo_H
|
||||
#define SDRPLAY_API_RSPduo_H
|
||||
|
||||
#define RSPDUO_NUM_LNA_STATES 10
|
||||
#define RSPDUO_NUM_LNA_STATES_AMPORT 5
|
||||
#define RSPDUO_NUM_LNA_STATES_AM 7
|
||||
#define RSPDUO_NUM_LNA_STATES_LBAND 9
|
||||
|
||||
// RSPduo parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDuoMode_Unknown = 0,
|
||||
sdrplay_api_RspDuoMode_Single_Tuner = 1,
|
||||
sdrplay_api_RspDuoMode_Dual_Tuner = 2,
|
||||
sdrplay_api_RspDuoMode_Master = 4,
|
||||
sdrplay_api_RspDuoMode_Slave = 8,
|
||||
} sdrplay_api_RspDuoModeT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDuo_AMPORT_1 = 1,
|
||||
sdrplay_api_RspDuo_AMPORT_2 = 0,
|
||||
} sdrplay_api_RspDuo_AmPortSelectT;
|
||||
|
||||
// RSPduo parameter structs
|
||||
typedef struct
|
||||
{
|
||||
int extRefOutputEn; // default: 0
|
||||
} sdrplay_api_RspDuoParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char resetGainUpdate; // default: 0
|
||||
unsigned char resetRfUpdate; // default: 0
|
||||
} sdrplay_api_RspDuo_ResetSlaveFlagsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel; // default: sdrplay_api_RspDuo_AMPORT_2
|
||||
unsigned char tuner1AmNotchEnable; // default: 0
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
sdrplay_api_RspDuo_ResetSlaveFlagsT resetSlaveFlags;
|
||||
} sdrplay_api_RspDuoTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSPduo_H
|
||||
21
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rx_channel.h
Normal file
21
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rx_channel.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef SDRPLAY_API_RX_CHANNEL_H
|
||||
#define SDRPLAY_API_RX_CHANNEL_H
|
||||
|
||||
#include "sdrplay_api_tuner.h"
|
||||
#include "sdrplay_api_control.h"
|
||||
#include "sdrplay_api_rsp1a.h"
|
||||
#include "sdrplay_api_rsp2.h"
|
||||
#include "sdrplay_api_rspDuo.h"
|
||||
#include "sdrplay_api_rspDx.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_TunerParamsT tunerParams;
|
||||
sdrplay_api_ControlParamsT ctrlParams;
|
||||
sdrplay_api_Rsp1aTunerParamsT rsp1aTunerParams;
|
||||
sdrplay_api_Rsp2TunerParamsT rsp2TunerParams;
|
||||
sdrplay_api_RspDuoTunerParamsT rspDuoTunerParams;
|
||||
sdrplay_api_RspDxTunerParamsT rspDxTunerParams;
|
||||
} sdrplay_api_RxChannelParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RX_CHANNEL_H
|
||||
93
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_tuner.h
Normal file
93
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_tuner.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#ifndef SDRPLAY_API_TUNER_H
|
||||
#define SDRPLAY_API_TUNER_H
|
||||
|
||||
#define MAX_BB_GR (59)
|
||||
|
||||
// Tuner parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_BW_Undefined = 0,
|
||||
sdrplay_api_BW_0_200 = 200,
|
||||
sdrplay_api_BW_0_300 = 300,
|
||||
sdrplay_api_BW_0_600 = 600,
|
||||
sdrplay_api_BW_1_536 = 1536,
|
||||
sdrplay_api_BW_5_000 = 5000,
|
||||
sdrplay_api_BW_6_000 = 6000,
|
||||
sdrplay_api_BW_7_000 = 7000,
|
||||
sdrplay_api_BW_8_000 = 8000
|
||||
} sdrplay_api_Bw_MHzT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_IF_Undefined = -1,
|
||||
sdrplay_api_IF_Zero = 0,
|
||||
sdrplay_api_IF_0_450 = 450,
|
||||
sdrplay_api_IF_1_620 = 1620,
|
||||
sdrplay_api_IF_2_048 = 2048
|
||||
} sdrplay_api_If_kHzT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_LO_Undefined = 0,
|
||||
sdrplay_api_LO_Auto = 1,
|
||||
sdrplay_api_LO_120MHz = 2,
|
||||
sdrplay_api_LO_144MHz = 3,
|
||||
sdrplay_api_LO_168MHz = 4
|
||||
} sdrplay_api_LoModeT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_EXTENDED_MIN_GR = 0,
|
||||
sdrplay_api_NORMAL_MIN_GR = 20
|
||||
} sdrplay_api_MinGainReductionT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Tuner_Neither = 0,
|
||||
sdrplay_api_Tuner_A = 1,
|
||||
sdrplay_api_Tuner_B = 2,
|
||||
sdrplay_api_Tuner_Both = 3,
|
||||
} sdrplay_api_TunerSelectT;
|
||||
|
||||
// Tuner parameter structs
|
||||
typedef struct
|
||||
{
|
||||
float curr;
|
||||
float max;
|
||||
float min;
|
||||
} sdrplay_api_GainValuesT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int gRdB; // default: 50
|
||||
unsigned char LNAstate; // default: 0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
sdrplay_api_MinGainReductionT minGr; // default: sdrplay_api_NORMAL_MIN_GR
|
||||
sdrplay_api_GainValuesT gainVals; // output parameter
|
||||
} sdrplay_api_GainT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
double rfHz; // default: 200000000.0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
} sdrplay_api_RfFreqT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char dcCal; // default: 3 (Periodic mode)
|
||||
unsigned char speedUp; // default: 0 (No speedup)
|
||||
int trackTime; // default: 1 (=> time in uSec = (72 * 3 * trackTime) / 24e6 = 9uSec)
|
||||
int refreshRateTime; // default: 2048 (=> time in uSec = (72 * 3 * refreshRateTime) / 24e6 = 18432uSec)
|
||||
} sdrplay_api_DcOffsetTunerT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_Bw_MHzT bwType; // default: sdrplay_api_BW_0_200
|
||||
sdrplay_api_If_kHzT ifType; // default: sdrplay_api_IF_Zero
|
||||
sdrplay_api_LoModeT loMode; // default: sdrplay_api_LO_Auto
|
||||
sdrplay_api_GainT gain;
|
||||
sdrplay_api_RfFreqT rfFreq;
|
||||
sdrplay_api_DcOffsetTunerT dcOffsetTuner;
|
||||
} sdrplay_api_TunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_TUNER_H
|
||||
219
build.gradle
219
build.gradle
@@ -1,8 +1,6 @@
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,12 +17,18 @@ import java.text.SimpleDateFormat
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import org.beryx.runtime.RuntimeZipTask
|
||||
import org.gradle.internal.jvm.Jvm
|
||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
/**
|
||||
* Instructions for building/compiling the sdrtrunk application.
|
||||
*
|
||||
* Prerequisites:
|
||||
* Install and configure an OpenJDK version 19+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 7.6+
|
||||
* Install and configure an OpenJDK version 27+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 9.8+
|
||||
*
|
||||
* Scenario 1: run the application via gradle command line from the source code root directory:
|
||||
* command: ./gradlew run
|
||||
@@ -45,26 +49,43 @@ plugins {
|
||||
id 'application'
|
||||
id 'java'
|
||||
id 'idea'
|
||||
id 'org.beryx.runtime' version '1.12.7'
|
||||
id 'org.beryx.runtime' version '2.0.1'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
|
||||
version = project.findProperty('projectVersion')
|
||||
|
||||
version = '0.6.0-alpha5'
|
||||
|
||||
//Java 19 is required for this version of the Project Panama preview/incubator feature
|
||||
//Java 27 is required for this version of the Project Panama preview/incubator feature
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(19)
|
||||
languageVersion = JavaLanguageVersion.of(27)
|
||||
vendor = JvmVendorSpec.BELLSOFT
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs 'src/main'
|
||||
test.java.srcDirs 'src/test'
|
||||
/**
|
||||
* Verifies that the selected toolchain JDK includes the JavaFX modules (e.g. Bellsoft Liberica 'Full JDK').
|
||||
*
|
||||
* Gradle's toolchain spec can only select by language version, vendor and implementation, so it can't distinguish
|
||||
* between the Liberica JDK packages that do and don't bundle JavaFX. This task fails the build with a clear message
|
||||
* instead of allowing compilation to fail with missing javafx packages.
|
||||
*/
|
||||
tasks.register('verifyToolchainJavaFx') {
|
||||
def jdkHome = javaToolchains.launcherFor(java.toolchain).map { it.metadata.installationPath.asFile }
|
||||
|
||||
doLast {
|
||||
def release = new File(jdkHome.get(), 'release')
|
||||
|
||||
if(!release.exists() || !release.text.contains('javafx.base')) {
|
||||
throw new GradleException("The JDK at [${jdkHome.get()}] does not include the JavaFX modules. Install " +
|
||||
"the Bellsoft Liberica JDK 27 'Full' package (includes LibericaFX), or use " +
|
||||
"-Porg.gradle.java.installations.paths=<jdk path> to specify the JDK that Gradle should use.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('compileJava') {
|
||||
dependsOn 'verifyToolchainJavaFx'
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -74,61 +95,64 @@ test {
|
||||
dependencies {
|
||||
|
||||
// JUnit Tests
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.assertj:assertj-core:3.23.1'
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.9.0')
|
||||
testImplementation 'org.mockito:mockito-core:4.8.0'
|
||||
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
|
||||
testImplementation(platform("org.junit:junit-bom:6.0.1"))
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.1'
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1")
|
||||
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
|
||||
//Jitpack imports
|
||||
implementation 'com.github.dnault:libresample4j:master'
|
||||
implementation 'com.github.DSheirer.jmbe:jmbe-api:1.0.0'
|
||||
implementation 'com.github.DSheirer.jmbe:api:v2.1.0'
|
||||
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
|
||||
|
||||
//mavenCentral/jcenter imports
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.5'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.5'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
|
||||
implementation 'com.fazecast:jSerialComm:2.9.3'
|
||||
implementation 'ch.qos.logback:logback-classic:1.6.3'
|
||||
implementation 'ch.qos.logback:logback-core:1.5.38'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.22.4'
|
||||
implementation 'com.fazecast:jSerialComm:2.11.4'
|
||||
implementation 'com.formdev:flatlaf:3.7.2'
|
||||
implementation 'com.formdev:flatlaf-intellij-themes:3.7.2'
|
||||
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
|
||||
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||
implementation 'com.github.wendykierp:JTransforms:3.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.google.guava:guava:31.1-jre'
|
||||
implementation 'com.github.wendykierp:JTransforms:3.2'
|
||||
implementation 'com.google.code.gson:gson:2.14.0'
|
||||
implementation 'com.google.guava:guava:33.6.0-jre'
|
||||
implementation 'com.jidesoft:jide-oss:3.6.18'
|
||||
implementation 'com.miglayout:miglayout-swing:11.0'
|
||||
implementation 'com.miglayout:miglayout-swing:11.4.3'
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'commons-io:commons-io:2.22.0'
|
||||
implementation 'eu.hansolo:charts:1.0.5'
|
||||
implementation 'io.github.dsheirer:radio-reference-api:15.1.9'
|
||||
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
|
||||
implementation 'io.github.dsheirer:jhydrasdr:1.1.3' //HydraSDR tuners - GitHub release, see settings.gradle
|
||||
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
|
||||
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.3' //OSX & Windows aarch64 native libs
|
||||
implementation 'io.github.dsheirer:usb4java:1.3.5' //Fork with Loader support for OSX Multi-Version
|
||||
implementation 'javax.usb:usb-api:1.0.2'
|
||||
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
|
||||
implementation 'org.apache.commons:commons-compress:1.21'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
implementation 'org.apache.commons:commons-compress:1.28.0'
|
||||
implementation 'org.apache.commons:commons-lang3:3.20.0'
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'org.apache.commons:commons-csv:1.9.0'
|
||||
implementation 'org.apache.mina:mina-core:2.2.1'
|
||||
implementation 'org.apache.mina:mina-http:2.2.1'
|
||||
implementation 'org.controlsfx:controlsfx:11.1.2'
|
||||
implementation 'org.apache.commons:commons-csv:1.14.1'
|
||||
implementation 'org.apache.mina:mina-core:2.2.9'
|
||||
implementation 'org.apache.mina:mina-http:2.2.9'
|
||||
implementation 'org.controlsfx:controlsfx:11.2.3'
|
||||
implementation 'org.rauschig:jarchivelib:1.2.0'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.5'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||
implementation 'org.usb4java:libusb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java-javax:1.3.0'
|
||||
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
|
||||
implementation 'pl.edu.icm:JLargeArrays:1.6'
|
||||
}
|
||||
|
||||
def os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem
|
||||
def os = DefaultNativePlatform.currentOperatingSystem
|
||||
|
||||
/**
|
||||
* This is needed for the JDK19 panama and vector API ... until it moves out of incubation or preview
|
||||
* This is needed for the JDK23+ vector API ... until it moves out of incubation or preview
|
||||
*/
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs.add("--enable-preview") //Panama Foreign Function/Memory is preview in JDK 19
|
||||
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Needed while Panama Vector API remains in incubator
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs.add("--enable-preview") //Project Panama Vector API is still in incubator
|
||||
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Project Panama Vector API is still in incubator
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,23 +161,26 @@ tasks.withType(JavaCompile) {
|
||||
def jvmArgsWindows =
|
||||
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
|
||||
'--add-exports=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED', //windows users for JIDE
|
||||
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
|
||||
'--enable-native-access=ALL-UNNAMED',
|
||||
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //Allows Project Panama Foreign Function access to system library
|
||||
'--add-modules=jdk.incubator.vector', //Project Panama vector API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector API is still in incubator
|
||||
'--enable-native-access=ALL-UNNAMED', //Suppress unsafe warnings for FFM API Foreign Memory calls
|
||||
'--enable-native-access=javafx.graphics', //JDK24 - suppress restricted java.lang.System calls
|
||||
'--sun-misc-unsafe-memory-access=allow', //JDK24 - suppress sun.misc.Unsafe::allocateMemory warning
|
||||
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //SDRPlay API library path
|
||||
|
||||
/**
|
||||
* JVM Arguments for running the application on Linux.
|
||||
*/
|
||||
def jvmArgsLinux =
|
||||
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
|
||||
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
|
||||
'--add-modules=jdk.incubator.vector', //Project Panama vector API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
|
||||
'--enable-native-access=ALL-UNNAMED',
|
||||
'-Djava.library.path=/usr/local/lib'] //Allows Project Panama Foreign Function access to system library
|
||||
'--sun-misc-unsafe-memory-access=allow', //JDK24 - suppress sun.misc.Unsafe::allocateMemory warning
|
||||
'--enable-native-access=javafx.graphics', //JDK24 - suppress restricted java.lang.System calls
|
||||
'--enable-native-access=ALL-UNNAMED'] //SDRPlay API library path
|
||||
|
||||
application {
|
||||
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
|
||||
mainClass = "io.github.dsheirer.gui.SDRTrunk"
|
||||
|
||||
if(os.isWindows()) {
|
||||
applicationDefaultJvmArgs = jvmArgsWindows
|
||||
@@ -163,6 +190,15 @@ application {
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
if(os.isWindows()) {
|
||||
jvmArgs = jvmArgsWindows
|
||||
}
|
||||
else {
|
||||
jvmArgs = jvmArgsLinux
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes (
|
||||
@@ -183,6 +219,11 @@ idea {
|
||||
}
|
||||
}
|
||||
|
||||
javadoc {
|
||||
options.addStringOption('-add-modules', 'jdk.incubator.vector')
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime target OS and CPU image names. We append version here so that version details are at end of zip filename.
|
||||
*/
|
||||
@@ -191,28 +232,31 @@ String targetLinuxX86_64 = 'linux-x86_64-v' + version
|
||||
String targetOsxAarch64 = 'osx-aarch64-v' + version
|
||||
String targetOsxX86_64 = 'osx-x86_64-v' + version
|
||||
String targetWindowsX86_64 = 'windows-x86_64-v' + version
|
||||
String targetWindowsAarch64 = 'windows-aarch64-v' + version
|
||||
|
||||
/**
|
||||
* Optional pre-downloaded Java Development Kit (JDK) for each target OS and CPU. These locations are hard-coded to the
|
||||
* author's development environment, but can be changed to your environment if you want to use this build pattern.
|
||||
*/
|
||||
String jdk_base = '/home/denny/java_jdks/'
|
||||
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-19.0.1-full'
|
||||
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-19.0.1-full'
|
||||
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-19.0.1-full.jdk'
|
||||
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-19.0.1-full.jdk'
|
||||
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-19.0.1-full'
|
||||
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-25.0.1-full'
|
||||
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-25.0.1-full'
|
||||
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-25.0.1-full.jdk'
|
||||
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-25.0.1-full.jdk'
|
||||
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-25.0.1-full'
|
||||
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-25.0.1-full'
|
||||
|
||||
/**
|
||||
* Download URLs to download the JDK as part of the gradle build packaging process
|
||||
*/
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-"
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/27+36/bellsoft-jdk27+36-"
|
||||
def jdk_download_suffix = "-full.tar.gz"
|
||||
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_osx_x86_64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-amd64" + 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-aarch64" + jdk_download_suffix
|
||||
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
||||
def jdk_download_windows_aarch64 = jdk_download_base + "windows-aarch64-full.zip"
|
||||
|
||||
/**
|
||||
* Configures the runtime zip task with additional options/settings.
|
||||
@@ -220,15 +264,17 @@ def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
||||
* @param jvmArgs to use (windows or linux)
|
||||
* @return configured task.
|
||||
*/
|
||||
def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
def configure(RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
//Use the correct set of JVM arguments
|
||||
rt.extension.launcherData.get().jvmArgs = jvmArgs
|
||||
|
||||
//Additional modules to include with auto-detected modules.
|
||||
//jdk.charsets - NXDN BIG5 encoding support
|
||||
//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.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.charsets', 'jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
|
||||
|
||||
//Use auto-detected modules and 'add' any specified modules.
|
||||
rt.extension.additive.set(true)
|
||||
@@ -237,7 +283,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
rt.extension.addOptions('--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages')
|
||||
|
||||
//Runtime images have auto appended version in image zip name. Override to include version in single target image.
|
||||
rt.extension.imageZip.set(file("$buildDir/image/sdr-trunk.zip"))
|
||||
// rt.extension.imageDir=layout.buildDirectory.dir('image')
|
||||
rt.extension.imageZip.set(file(layout.buildDirectory.dir('image').get().file('sdr-trunk.zip')))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,30 +296,35 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
* Usage: gradlew runtimeZipCurrent (windows - using gradlew)
|
||||
* Usage: ./gradlew runtimeZipCurrent (linux - using gradlew)
|
||||
*/
|
||||
tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
tasks.register('runtimeZipCurrent', RuntimeZipTask) {rt ->
|
||||
def arch = System.properties.getProperty("os.arch")
|
||||
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
|
||||
def javaHome = Jvm.current().getJavaHome().toString()
|
||||
|
||||
if(os.isWindows()) {
|
||||
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
|
||||
if(arch == "amd64") {
|
||||
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
|
||||
}
|
||||
else if(arch == "aarch64") {
|
||||
rt.extension.targetPlatform(targetWindowsAarch64, javaHome)
|
||||
}
|
||||
configure(rt, jvmArgsWindows)
|
||||
}
|
||||
else if(os.isLinux())
|
||||
{
|
||||
if(arch.equals("amd64")) {
|
||||
if(arch == "amd64") {
|
||||
rt.extension.targetPlatform(targetLinuxX86_64, javaHome)
|
||||
}
|
||||
else if(arch.equals("aarch64")) {
|
||||
else if(arch == "aarch64") {
|
||||
rt.extension.targetPlatform(targetLinuxAarch64, javaHome)
|
||||
}
|
||||
configure(rt, jvmArgsLinux)
|
||||
}
|
||||
else if(os.isMacOsX())
|
||||
{
|
||||
if(arch.equals("amd64")) {
|
||||
if(arch == "amd64") {
|
||||
rt.extension.targetPlatform(targetOsxX86_64, javaHome)
|
||||
}
|
||||
else if(arch.equals("aarch64")) {
|
||||
else if(arch == "aarch64") {
|
||||
rt.extension.targetPlatform(targetOsxAarch64, javaHome)
|
||||
}
|
||||
configure(rt, jvmArgsLinux)
|
||||
@@ -289,7 +341,17 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
* Usage: gradlew runtimeZipWindows (windows - using gradlew)
|
||||
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
|
||||
*/
|
||||
tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
tasks.register('runtimeZipWindows', RuntimeZipTask) {rt ->
|
||||
//Windows aarch64
|
||||
if(file(jdk_windows_aarch64).exists()) {
|
||||
rt.extension.targetPlatform(targetWindowsAarch64, jdk_windows_aarch64, [])
|
||||
}
|
||||
else {
|
||||
rt.extension.targetPlatform(targetWindowsAarch64) {
|
||||
jdkHome = jdkDownload(jdk_download_windows_aarch64)
|
||||
}
|
||||
}
|
||||
//Windows x86-64
|
||||
if(file(jdk_windows_x86_64).exists()) {
|
||||
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, [])
|
||||
}
|
||||
@@ -311,7 +373,7 @@ tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
* Usage: gradlew runtimeZipOthers (windows - using gradlew)
|
||||
* Usage: ./gradlew runtimeZipOthers (linux - using gradlew)
|
||||
*/
|
||||
tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
tasks.register('runtimeZipOthers', RuntimeZipTask) {rt ->
|
||||
//Linux aarch64
|
||||
if(file(jdk_linux_aarch64).exists()) {
|
||||
rt.extension.targetPlatform(targetLinuxAarch64, jdk_linux_aarch64, [])
|
||||
@@ -339,7 +401,7 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
jdkHome = jdkDownload(jdk_download_osx_aarch64)
|
||||
}
|
||||
}
|
||||
//Linux x86-64
|
||||
//OSX x86-64
|
||||
if(file(jdk_osx_x86_64).exists()) {
|
||||
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
|
||||
}
|
||||
@@ -349,6 +411,5 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
}
|
||||
}
|
||||
|
||||
configure(rt, jvmArgsLinux);
|
||||
configure(rt, jvmArgsLinux)
|
||||
}
|
||||
|
||||
|
||||
31
gradle.properties
Normal file
31
gradle.properties
Normal file
@@ -0,0 +1,31 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2026 Dennis Sheirer
|
||||
#
|
||||
# This 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/>
|
||||
# *****************************************************************************
|
||||
#
|
||||
|
||||
# Version naming conventions:
|
||||
# Alpha: maj.min.patch-alpha-build
|
||||
# Beta: maj.min.patch-beta-build
|
||||
# Release: maj.min.patch
|
||||
#
|
||||
# Example: 0.6.0-beta-5
|
||||
#
|
||||
# Note: this is so that the file names for the resultant release build products match the GitLab release asset
|
||||
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
||||
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
||||
|
||||
projectVersion=0.6.2-beta-1
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
24
gradle/wrapper/gradle-wrapper.properties
vendored
24
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,25 +1,9 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# 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/>
|
||||
# *****************************************************************************
|
||||
#
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
65
gradlew
vendored
65
gradlew
vendored
@@ -1,27 +1,26 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2022 Dennis Sheirer
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# This 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.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# 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.
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
# *****************************************************************************
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
@@ -30,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
@@ -58,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -86,10 +85,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -117,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -136,10 +132,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@@ -147,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -155,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -200,16 +198,19 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
58
gradlew.bat
vendored
58
gradlew.bat
vendored
@@ -13,16 +13,18 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@@ -43,13 +45,13 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
@@ -1 +1,28 @@
|
||||
rootProject.name = 'sdr-trunk'
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
rootProject.name = 'sdr-trunk'
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
|
||||
//jHydraSDR is published as a GitHub release, with the native libraries for all platforms in the jar
|
||||
ivy {
|
||||
url = 'https://github.com/DSheirer/jHydraSDR/releases/download'
|
||||
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
|
||||
metadataSources { artifact() }
|
||||
content { includeModule 'io.github.dsheirer', 'jhydrasdr' }
|
||||
}
|
||||
|
||||
//jBladeRF is published as a GitHub release, with the native libraries for all platforms in the jar
|
||||
ivy {
|
||||
url = 'https://github.com/DSheirer/jBladeRF/releases/download'
|
||||
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
|
||||
metadataSources { artifact() }
|
||||
content { includeModule 'io.github.dsheirer', 'jbladerf' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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 com.sun.java.swing.plaf.windows;
|
||||
|
||||
import javax.swing.LookAndFeel;
|
||||
import javax.swing.UIDefaults;
|
||||
|
||||
/**
|
||||
* No-op stub for the JDK's Windows look-and-feel.
|
||||
*
|
||||
* <p>JIDE 3.6.18's {@code LookAndFeelFactory} contains compile-time
|
||||
* {@code instanceof com.sun.java.swing.plaf.windows.WindowsLookAndFeel} checks throughout its
|
||||
* dispatch logic. On Linux and macOS JDK builds that class is genuinely absent from
|
||||
* {@code java.desktop}, so the JVM throws {@link NoClassDefFoundError} the first time JIDE's
|
||||
* dispatch code executes the {@code instanceof} bytecode. That happens during
|
||||
* {@link com.jidesoft.swing.JideSplitPane#updateUI()} on every LAF change.
|
||||
*
|
||||
* <p>This stub lives in the classpath so the class reference resolves on non-Windows JDKs.
|
||||
* On Windows JDK builds the real {@code WindowsLookAndFeel} is loaded from the
|
||||
* {@code java.desktop} module via the boot class loader and shadows this class.
|
||||
*
|
||||
* <p>The stub reports itself as not native and not supported - JIDE never instantiates this
|
||||
* class, it only uses it for {@code instanceof} comparisons against the active LAF. Since
|
||||
* sdrtrunk never installs this LAF, the {@code instanceof} check always returns false and
|
||||
* JIDE proceeds to its next branch.
|
||||
*/
|
||||
public class WindowsLookAndFeel extends LookAndFeel
|
||||
{
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return "Windows (stub)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID()
|
||||
{
|
||||
return "WindowsStub";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription()
|
||||
{
|
||||
return "Compatibility stub for JIDE's instanceof checks on non-Windows JDKs.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNativeLookAndFeel()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupportedLookAndFeel()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UIDefaults getDefaults()
|
||||
{
|
||||
return new UIDefaults();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.alias;
|
||||
|
||||
@@ -30,13 +27,22 @@ import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup;
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -46,13 +52,6 @@ import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Alias provides an aliasing (e.g. name, color, etc) container that is linked to multiple alias identifiers and
|
||||
* provides a corresponding set of actions related to the alias.
|
||||
@@ -74,6 +73,7 @@ public class Alias
|
||||
private StringProperty mName = new SimpleStringProperty();
|
||||
private ObservableList<AliasID> mAliasIDs = FXCollections.observableArrayList();
|
||||
private ObservableList<AliasAction> mAliasActions = FXCollections.observableArrayList();
|
||||
private ObjectProperty<StreamAsTalkgroup> mStreamTalkgroupAlias = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance and sets the specified name.
|
||||
@@ -180,6 +180,12 @@ public class Alias
|
||||
return mColor;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public ObjectProperty streamTalkgroupAliasProperty()
|
||||
{
|
||||
return mStreamTalkgroupAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon name property
|
||||
*/
|
||||
@@ -229,6 +235,24 @@ public class Alias
|
||||
mName.set(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias name
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "stream_talkgroup_alias")
|
||||
public StreamAsTalkgroup getStreamTalkgroupAlias()
|
||||
{
|
||||
return mStreamTalkgroupAlias.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stream as talkgroup value that will be used in the TO field for streamed audio calls.
|
||||
* @param streamTalkgroupAlias with a talkgroup value.
|
||||
*/
|
||||
public void setStreamTalkgroupAlias(StreamAsTalkgroup streamTalkgroupAlias)
|
||||
{
|
||||
mStreamTalkgroupAlias.set(streamTalkgroupAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias list name for the alias list that this alias belongs to
|
||||
*/
|
||||
@@ -710,6 +734,7 @@ public class Alias
|
||||
{
|
||||
return (Alias a) -> new Observable[] {a.recordableProperty(), a.streamableProperty(), a.colorProperty(),
|
||||
a.aliasListNameProperty(), a.groupProperty(), a.iconNameProperty(), a.nameProperty(), a.aliasIds(),
|
||||
a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty()};
|
||||
a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty(), a.priorityProperty(),
|
||||
a.streamTalkgroupAliasProperty()};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,37 +19,35 @@
|
||||
package io.github.dsheirer.alias;
|
||||
|
||||
import io.github.dsheirer.alias.action.AliasAction;
|
||||
import io.github.dsheirer.alias.action.AliasActionType;
|
||||
import io.github.dsheirer.alias.action.beep.BeepAction;
|
||||
import io.github.dsheirer.alias.action.clip.ClipAction;
|
||||
import io.github.dsheirer.alias.action.script.ScriptAction;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
|
||||
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
|
||||
import io.github.dsheirer.alias.id.legacy.mobileID.Min;
|
||||
import io.github.dsheirer.alias.id.legacy.mpt1327.MPT1327ID;
|
||||
import io.github.dsheirer.alias.id.legacy.nonrecordable.NonRecordable;
|
||||
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
||||
import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
|
||||
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio;
|
||||
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AliasFactory
|
||||
{
|
||||
@@ -64,6 +62,11 @@ public class AliasFactory
|
||||
BroadcastChannel copyBroadcast = new BroadcastChannel();
|
||||
copyBroadcast.setChannelName(originalBroadcast.getChannelName());
|
||||
return copyBroadcast;
|
||||
case DCS:
|
||||
Dcs originalDcs = (Dcs)id;
|
||||
Dcs copyDcs = new Dcs();
|
||||
copyDcs.setDCSCode(originalDcs.getDCSCode());
|
||||
return copyDcs;
|
||||
case ESN:
|
||||
Esn originalESN = (Esn)id;
|
||||
Esn copyESN = new Esn();
|
||||
@@ -80,6 +83,43 @@ public class AliasFactory
|
||||
Min copyMin = new Min();
|
||||
copyMin.setMin(originalMin.getMin());
|
||||
return copyMin;
|
||||
case NXDN_TYPE_D_RADIO_ID:
|
||||
NXDNTypeDRadio originalTypeDRadio = (NXDNTypeDRadio)id;
|
||||
NXDNTypeDRadio copyTypeDRadio = new NXDNTypeDRadio(originalTypeDRadio.getHomeRepeater(),
|
||||
originalTypeDRadio.getIdentifier());
|
||||
copyTypeDRadio.setOverlap(originalTypeDRadio.overlapProperty().get());
|
||||
return copyTypeDRadio;
|
||||
case NXDN_TYPE_D_RADIO_ID_RANGE:
|
||||
NXDNTypeDRadioRange originalTypeDRadioRange = (NXDNTypeDRadioRange)id;
|
||||
NXDNTypeDRadioRange copyTypeDRadioRange = new NXDNTypeDRadioRange(originalTypeDRadioRange.getHomeRepeater(),
|
||||
originalTypeDRadioRange.getMinIdentifier(), originalTypeDRadioRange.getMaxIdentifier());
|
||||
copyTypeDRadioRange.setOverlap(originalTypeDRadioRange.overlapProperty().get());
|
||||
return copyTypeDRadioRange;
|
||||
case NXDN_TYPE_D_TALKGROUP:
|
||||
NXDNTypeDTalkgroup originalTypeDTalkgroup = (NXDNTypeDTalkgroup)id;
|
||||
NXDNTypeDTalkgroup copyTypeDTalkgroup = new NXDNTypeDTalkgroup(originalTypeDTalkgroup.getHomeRepeater(),
|
||||
originalTypeDTalkgroup.getIdentifier());
|
||||
copyTypeDTalkgroup.setOverlap(originalTypeDTalkgroup.overlapProperty().get());
|
||||
return copyTypeDTalkgroup;
|
||||
case NXDN_TYPE_D_TALKGROUP_RANGE:
|
||||
NXDNTypeDTalkgroupRange originalTypeDTalkgroupRange = (NXDNTypeDTalkgroupRange)id;
|
||||
NXDNTypeDTalkgroupRange copyTypeDTalkgroupRange = new NXDNTypeDTalkgroupRange(
|
||||
originalTypeDTalkgroupRange.getHomeRepeater(), originalTypeDTalkgroupRange.getMinIdentifier(),
|
||||
originalTypeDTalkgroupRange.getMaxIdentifier());
|
||||
copyTypeDTalkgroupRange.setOverlap(originalTypeDTalkgroupRange.overlapProperty().get());
|
||||
return copyTypeDTalkgroupRange;
|
||||
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:
|
||||
Priority originalPriority = (Priority)id;
|
||||
Priority copyPriority = new Priority();
|
||||
@@ -187,6 +227,7 @@ public class AliasFactory
|
||||
copy.setGroup(original.getGroup());
|
||||
copy.setColor(original.getColor());
|
||||
copy.setIconName(original.getIconName());
|
||||
copy.setRecordable(original.isRecordable());
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -234,65 +275,4 @@ public class AliasFactory
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
public static AliasID getAliasID(AliasIDType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case BROADCAST_CHANNEL:
|
||||
return new BroadcastChannel();
|
||||
case ESN:
|
||||
return new Esn();
|
||||
case FLEETSYNC:
|
||||
return new FleetsyncID();
|
||||
case LTR_NET_UID:
|
||||
return new UniqueID();
|
||||
case LOJACK:
|
||||
return new LoJackFunctionAndID();
|
||||
case MDC1200:
|
||||
return new MDC1200ID();
|
||||
case MIN:
|
||||
return new Min();
|
||||
case MPT1327:
|
||||
return new MPT1327ID();
|
||||
case NON_RECORDABLE:
|
||||
return new NonRecordable();
|
||||
case PRIORITY:
|
||||
return new Priority();
|
||||
case RADIO_ID:
|
||||
return new Radio();
|
||||
case RADIO_ID_RANGE:
|
||||
return new RadioRange();
|
||||
case RECORD:
|
||||
return new Record();
|
||||
case TALKGROUP:
|
||||
return new Talkgroup();
|
||||
case TALKGROUP_RANGE:
|
||||
return new TalkgroupRange();
|
||||
case SITE:
|
||||
return new SiteID();
|
||||
case STATUS:
|
||||
return new UserStatusID();
|
||||
case LEGACY_TALKGROUP:
|
||||
return new LegacyTalkgroupID();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized Alias ID type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public static AliasAction getAliasAction(AliasActionType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case BEEP:
|
||||
return new BeepAction();
|
||||
case CLIP:
|
||||
return new ClipAction();
|
||||
case SCRIPT:
|
||||
return new ScriptAction();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized Alias Action type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,32 +21,34 @@ package io.github.dsheirer.alias;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.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.RadioRange;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
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.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.dcs.DCSIdentifier;
|
||||
import io.github.dsheirer.identifier.esn.ESNIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
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.status.UnitStatusIdentifier;
|
||||
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.tone.ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -57,6 +59,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* List of aliases that share the same alias list name and provides convenient methods for looking up alias
|
||||
@@ -67,6 +73,7 @@ public class AliasList
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AliasList.class);
|
||||
private Map<Protocol,TalkgroupAliasList> mTalkgroupProtocolMap = new EnumMap<>(Protocol.class);
|
||||
private Map<Protocol,RadioAliasList> mRadioProtocolMap = new EnumMap<>(Protocol.class);
|
||||
private Map<DCSCode,Alias> mDCSCodeAliasMap = new EnumMap<>(DCSCode.class);
|
||||
private Map<String,Alias> mESNMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mUnitStatusMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mUserStatusMap = new HashMap<>();
|
||||
@@ -127,6 +134,7 @@ public class AliasList
|
||||
switch(id.getType())
|
||||
{
|
||||
case TALKGROUP:
|
||||
case NXDN_TYPE_D_TALKGROUP:
|
||||
Talkgroup talkgroup = (Talkgroup)id;
|
||||
|
||||
TalkgroupAliasList talkgroupAliasList = mTalkgroupProtocolMap.get(talkgroup.getProtocol());
|
||||
@@ -140,6 +148,7 @@ public class AliasList
|
||||
talkgroupAliasList.add(talkgroup, alias);
|
||||
break;
|
||||
case TALKGROUP_RANGE:
|
||||
case NXDN_TYPE_D_TALKGROUP_RANGE:
|
||||
TalkgroupRange talkgroupRange = (TalkgroupRange)id;
|
||||
|
||||
TalkgroupAliasList talkgroupRangeAliasList = mTalkgroupProtocolMap.get(talkgroupRange.getProtocol());
|
||||
@@ -152,7 +161,34 @@ public class AliasList
|
||||
|
||||
talkgroupRangeAliasList.add(talkgroupRange, alias);
|
||||
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 NXDN_TYPE_D_RADIO_ID:
|
||||
Radio radio = (Radio)id;
|
||||
|
||||
RadioAliasList radioAliasList = mRadioProtocolMap.get(radio.getProtocol());
|
||||
@@ -166,6 +202,7 @@ public class AliasList
|
||||
radioAliasList.add(radio, alias);
|
||||
break;
|
||||
case RADIO_ID_RANGE:
|
||||
case NXDN_TYPE_D_RADIO_ID_RANGE:
|
||||
RadioRange radioRange = (RadioRange)id;
|
||||
|
||||
RadioAliasList radioRangeAliasList = mRadioProtocolMap.get(radioRange.getProtocol());
|
||||
@@ -178,6 +215,12 @@ public class AliasList
|
||||
|
||||
radioRangeAliasList.add(radioRange, alias);
|
||||
break;
|
||||
case DCS:
|
||||
if(id instanceof Dcs dcs)
|
||||
{
|
||||
mDCSCodeAliasMap.put(dcs.getDCSCode(), alias);
|
||||
}
|
||||
break;
|
||||
case ESN:
|
||||
String esn = ((Esn)id).getEsn();
|
||||
|
||||
@@ -379,6 +422,8 @@ public class AliasList
|
||||
}
|
||||
break;
|
||||
case PATCH_GROUP:
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
@@ -386,8 +431,6 @@ public class AliasList
|
||||
|
||||
if(patchGroupAliasList != null)
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
|
||||
|
||||
if(alias != null)
|
||||
@@ -395,19 +438,36 @@ public class AliasList
|
||||
aliases.add(alias);
|
||||
}
|
||||
|
||||
for(TalkgroupIdentifier patchedGroup: patchGroup.getPatchedGroupIdentifiers())
|
||||
for(TalkgroupIdentifier patchedTalkgroup: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||
{
|
||||
Alias patchedAlias = patchGroupAliasList.getAlias(patchedGroup);
|
||||
Alias patchedTalkgroupAlias = patchGroupAliasList.getAlias(patchedTalkgroup);
|
||||
|
||||
if(patchedAlias != null && !aliases.contains(patchedAlias))
|
||||
if(patchedTalkgroupAlias != null && !aliases.contains(patchedTalkgroupAlias))
|
||||
{
|
||||
aliases.add(patchedAlias);
|
||||
aliases.add(patchedTalkgroupAlias);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
break;
|
||||
|
||||
if(patchGroup.hasPatchedRadios())
|
||||
{
|
||||
RadioAliasList radioAliasList = mRadioProtocolMap.get(patchGroupIdentifier.getProtocol());
|
||||
|
||||
if(radioAliasList != null)
|
||||
{
|
||||
for(RadioIdentifier patchedRadio: patchGroup.getPatchedRadioIdentifiers())
|
||||
{
|
||||
Alias patchedRadioAlias = radioAliasList.getAlias(patchedRadio);
|
||||
|
||||
if(patchedRadioAlias != null && !aliases.contains(patchedRadioAlias))
|
||||
{
|
||||
aliases.add(patchedRadioAlias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
case RADIO:
|
||||
RadioIdentifier radio = (RadioIdentifier)identifier;
|
||||
|
||||
@@ -439,9 +499,9 @@ public class AliasList
|
||||
}
|
||||
break;
|
||||
case TONE:
|
||||
if(identifier instanceof ToneIdentifier)
|
||||
if(identifier instanceof ToneIdentifier toneIdentifier)
|
||||
{
|
||||
ToneSequence toneSequence = ((ToneIdentifier)identifier).getValue();
|
||||
ToneSequence toneSequence = toneIdentifier.getValue();
|
||||
|
||||
if(toneSequence != null && toneSequence.hasTones())
|
||||
{
|
||||
@@ -454,6 +514,15 @@ public class AliasList
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(identifier instanceof DCSIdentifier dcsIdentifier)
|
||||
{
|
||||
DCSCode dcsCode = dcsIdentifier.getValue();
|
||||
|
||||
if(dcsCode != null)
|
||||
{
|
||||
return toList(mDCSCodeAliasMap.get(dcsCode));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -465,9 +534,7 @@ public class AliasList
|
||||
{
|
||||
if(alias != null)
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
aliases.add(alias);
|
||||
return aliases;
|
||||
return Collections.singletonList(alias);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
@@ -590,6 +657,7 @@ public class AliasList
|
||||
*/
|
||||
public class TalkgroupAliasList
|
||||
{
|
||||
private Map<String,Alias> mFullyQualifiedTalkgroupAliasMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
|
||||
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
|
||||
|
||||
@@ -599,6 +667,13 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(TalkgroupIdentifier identifier)
|
||||
{
|
||||
//Attempt to do a fully qualified identifier match only
|
||||
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
|
||||
{
|
||||
return mFullyQualifiedTalkgroupAliasMap.get(fqti.getFullyQualifiedTalkgroupAddress());
|
||||
}
|
||||
|
||||
//Attempt to match the talkgroup value
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mTalkgroupAliasMap.get(value);
|
||||
@@ -607,6 +682,7 @@ public class AliasList
|
||||
return mapValue;
|
||||
}
|
||||
|
||||
//Alternatively, match the talkgroup to any talkgroup ranges
|
||||
for(Map.Entry<TalkgroupRange, Alias> entry : mTalkgroupRangeAliasMap.entrySet())
|
||||
{
|
||||
if(entry.getKey().contains(value))
|
||||
@@ -620,26 +696,55 @@ public class AliasList
|
||||
|
||||
public void add(Talkgroup talkgroup, Alias alias)
|
||||
{
|
||||
//Detect talkgroup collisions and set overlap flag for both
|
||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||
if(talkgroup instanceof P25FullyQualifiedTalkgroup fqt)
|
||||
{
|
||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||
|
||||
if(!existing.equals(alias))
|
||||
//Detect collisions
|
||||
if(mFullyQualifiedTalkgroupAliasMap.containsKey(fqt.getHashKey()))
|
||||
{
|
||||
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)
|
||||
@@ -672,6 +777,7 @@ public class AliasList
|
||||
*/
|
||||
public class RadioAliasList
|
||||
{
|
||||
private Map<String,Alias> mFullyQualifiedRadioAliasMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
|
||||
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
|
||||
|
||||
@@ -681,6 +787,13 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(RadioIdentifier identifier)
|
||||
{
|
||||
//Match fully qualified identifier only.
|
||||
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
|
||||
{
|
||||
return mFullyQualifiedRadioAliasMap.get(fqri.getFullyQualifiedRadioAddress());
|
||||
}
|
||||
|
||||
//Attempt to match against the radio identifier
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mRadioAliasMap.get(value);
|
||||
@@ -689,6 +802,7 @@ public class AliasList
|
||||
return mapValue;
|
||||
}
|
||||
|
||||
//Alternatively, attempt to match the radio address against any radio ranges.
|
||||
for(Map.Entry<RadioRange, Alias> entry : mRadioRangeAliasMap.entrySet())
|
||||
{
|
||||
if(entry.getKey().contains(value))
|
||||
@@ -702,26 +816,55 @@ public class AliasList
|
||||
|
||||
public void add(Radio radio, Alias alias)
|
||||
{
|
||||
//Detect collisions
|
||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||
if(radio instanceof P25FullyQualifiedRadio fqr)
|
||||
{
|
||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||
|
||||
if(!existing.equals(alias))
|
||||
//Detect collisions
|
||||
if(mFullyQualifiedRadioAliasMap.containsKey(fqr.getHashKey()))
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,17 +23,16 @@ import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Alias Model contains all aliases and is responsible for creation and management of alias lists. Alias lists are a
|
||||
@@ -64,6 +63,69 @@ public class AliasModel
|
||||
return mAliasListNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears and reloads the list of alias list names from the current set of aliases.
|
||||
*/
|
||||
public void refreshAliasListNames()
|
||||
{
|
||||
mAliasListNames.clear();
|
||||
|
||||
for(Alias alias : mAliases)
|
||||
{
|
||||
String aliasListName = alias.getAliasListName();
|
||||
|
||||
if(aliasListName != null && !aliasListName.isEmpty() && !mAliasListNames.contains(aliasListName))
|
||||
{
|
||||
mAliasListNames.add(aliasListName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the alias list across the set of aliases.
|
||||
* @param oldName currently used by the alias
|
||||
* @param newName to apply to the alias
|
||||
*/
|
||||
public void renameAliasList(String oldName, String newName)
|
||||
{
|
||||
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mAliases.stream().filter(alias -> alias.getAliasListName().equals(oldName)).forEach(alias -> alias.setAliasListName(newName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes any aliases that have the alias list name
|
||||
* @param aliasListName to delete
|
||||
*/
|
||||
public void deleteAliasList(String aliasListName)
|
||||
{
|
||||
if(aliasListName == null || aliasListName.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mAliases.removeIf(alias -> alias.getAliasListName().equals(aliasListName));
|
||||
mAliasListMap.remove(aliasListName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the list of alias list names to the currently managed set of alias list names.
|
||||
* @param aliasListNames to add
|
||||
*/
|
||||
public void addAliasListNames(List<String> aliasListNames)
|
||||
{
|
||||
for(String aliasListName : aliasListNames)
|
||||
{
|
||||
if(!mAliasListNames.contains(aliasListName))
|
||||
{
|
||||
mAliasListNames.add(aliasListName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmodifiable list of all aliases currently in the model
|
||||
*/
|
||||
@@ -184,10 +246,8 @@ public class AliasModel
|
||||
*/
|
||||
public void addAliases(List<Alias> aliases)
|
||||
{
|
||||
for(Alias alias : aliases)
|
||||
{
|
||||
addAlias(alias);
|
||||
}
|
||||
removeAliases(aliases);
|
||||
mAliases.addAll(aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,6 +301,26 @@ public class AliasModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the list of aliases from this model and any alias lists that might contain each alias.
|
||||
* @param aliases
|
||||
*/
|
||||
public void removeAliases(List<Alias> aliases)
|
||||
{
|
||||
if(aliases != null && !aliases.isEmpty())
|
||||
{
|
||||
mAliases.removeAll(aliases);
|
||||
|
||||
for(Alias alias: aliases)
|
||||
{
|
||||
if(hasAliasList(alias.getAliasListName()))
|
||||
{
|
||||
getAliasList(alias.getAliasListName()).removeAlias(alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all aliases that match the alias list and have at least one alias ID of the specified type
|
||||
* @param aliasListName to search
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.alias.id;
|
||||
|
||||
@@ -26,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
|
||||
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
|
||||
@@ -37,11 +35,18 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
|
||||
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio;
|
||||
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -54,6 +59,7 @@ import javafx.util.Callback;
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = BroadcastChannel.class, name = "broadcastChannel"),
|
||||
@JsonSubTypes.Type(value = Dcs.class, name = "dcs"),
|
||||
@JsonSubTypes.Type(value = Esn.class, name = "esn"),
|
||||
@JsonSubTypes.Type(value = FleetsyncID.class, name = "fleetsyncID"),
|
||||
@JsonSubTypes.Type(value = LegacyTalkgroupID.class, name = "talkgroupID"),
|
||||
@@ -62,11 +68,18 @@ import javafx.util.Callback;
|
||||
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
||||
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
||||
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
|
||||
@JsonSubTypes.Type(value = NXDNTypeDRadio.class, name = "nxdnTypeDRadio"),
|
||||
@JsonSubTypes.Type(value = NXDNTypeDRadioRange.class, name = "nxdnTypeDRadioRange"),
|
||||
@JsonSubTypes.Type(value = NXDNTypeDTalkgroup.class, name = "nxdnTypeDTalkgroup"),
|
||||
@JsonSubTypes.Type(value = NXDNTypeDTalkgroupRange.class, name = "nxdnTypeDTalkgroupRange"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"),
|
||||
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
|
||||
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
|
||||
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
||||
@JsonSubTypes.Type(value = Record.class, name = "record"),
|
||||
@JsonSubTypes.Type(value = SiteID.class, name = "siteID"),
|
||||
@JsonSubTypes.Type(value = StreamAsTalkgroup.class, name = "streamAsTalkgroup"),
|
||||
@JsonSubTypes.Type(value = Talkgroup.class, name = "talkgroup"),
|
||||
@JsonSubTypes.Type(value = TalkgroupRange.class, name = "talkgroupRange"),
|
||||
@JsonSubTypes.Type(value = TonesID.class, name = "tones"),
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.alias.id;
|
||||
|
||||
@@ -26,11 +23,18 @@ import java.util.EnumSet;
|
||||
public enum AliasIDType
|
||||
{
|
||||
BROADCAST_CHANNEL("Audio Broadcast Channel"),
|
||||
DCS("Digital Coded Squelch (DCS)"),
|
||||
ESN("ESN"),
|
||||
INVERT("Audio Inversion"),
|
||||
LOJACK("LoJack"),
|
||||
LTR_NET_UID("LTR-Net UID"),
|
||||
MIN("Passport MIN"),
|
||||
NXDN_TYPE_D_RADIO_ID("NXDN Type-D Radio ID"),
|
||||
NXDN_TYPE_D_RADIO_ID_RANGE("NXDN Type-D Radio ID Range"),
|
||||
NXDN_TYPE_D_TALKGROUP("NXDN Type-D Talkgroup"),
|
||||
NXDN_TYPE_D_TALKGROUP_RANGE("NXDN Type-D Talkgroup Range"),
|
||||
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
|
||||
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
|
||||
PRIORITY("Audio Priority"),
|
||||
RADIO_ID("Radio ID"),
|
||||
RADIO_ID_RANGE("Radio ID Range"),
|
||||
@@ -58,7 +62,8 @@ public enum AliasIDType
|
||||
|
||||
//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,
|
||||
UNIT_STATUS, STATUS, TONES);
|
||||
NXDN_TYPE_D_RADIO_ID, NXDN_TYPE_D_RADIO_ID_RANGE, NXDN_TYPE_D_TALKGROUP, NXDN_TYPE_D_TALKGROUP_RANGE,
|
||||
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
|
||||
|
||||
public String toString()
|
||||
{
|
||||
|
||||
46
src/main/java/io/github/dsheirer/alias/id/INXDNTypeD.java
Normal file
46
src/main/java/io/github/dsheirer/alias/id/INXDNTypeD.java
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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;
|
||||
|
||||
/**
|
||||
* NXDN Type-D alias identifier composed of a home repeater and a radio or talkgroup identifier value.
|
||||
*/
|
||||
public interface INXDNTypeD
|
||||
{
|
||||
/**
|
||||
* Home repeater for the identifier
|
||||
*/
|
||||
int getHomeRepeater();
|
||||
|
||||
/**
|
||||
* Sets the home repeater for the identifier
|
||||
*/
|
||||
void setHomeRepeater(int homeRepeater);
|
||||
|
||||
/**
|
||||
* Radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
int getIdentifier();
|
||||
|
||||
/**
|
||||
* Sets the radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
void setIdentifier(int identifier);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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;
|
||||
|
||||
/**
|
||||
* NXDN Type-D alias identifier range of radio or talkgroup values that share a common home repeater.
|
||||
*/
|
||||
public interface INXDNTypeDRange
|
||||
{
|
||||
/**
|
||||
* Home repeater for the range
|
||||
*/
|
||||
int getHomeRepeater();
|
||||
|
||||
/**
|
||||
* Sets the home repeater for the range
|
||||
*/
|
||||
void setHomeRepeater(int homeRepeater);
|
||||
|
||||
/**
|
||||
* Minimum radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
int getMinIdentifier();
|
||||
|
||||
/**
|
||||
* Sets the minimum radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
void setMinIdentifier(int identifier);
|
||||
|
||||
/**
|
||||
* Maximum radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
int getMaxIdentifier();
|
||||
|
||||
/**
|
||||
* Sets the maximum radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
void setMaxIdentifier(int identifier);
|
||||
}
|
||||
111
src/main/java/io/github/dsheirer/alias/id/dcs/Dcs.java
Normal file
111
src/main/java/io/github/dsheirer/alias/id/dcs/Dcs.java
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.dcs;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
|
||||
/**
|
||||
* Digital Coded Squelch (DCS) tone identifier
|
||||
*/
|
||||
public class Dcs extends AliasID implements Comparable<Dcs>
|
||||
{
|
||||
private DCSCode mDCSCode;
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.DCS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(AliasID id)
|
||||
{
|
||||
if(isValid() && id instanceof Dcs other)
|
||||
{
|
||||
return other.isValid() && getDCSCode().equals(other.getDCSCode());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* DCS code
|
||||
* @return DCS code or null.
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "code")
|
||||
public DCSCode getDCSCode()
|
||||
{
|
||||
return mDCSCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the DCS code value.
|
||||
* @param dcsCode to set
|
||||
*/
|
||||
public void setDCSCode(DCSCode dcsCode)
|
||||
{
|
||||
mDCSCode = dcsCode;
|
||||
updateValueProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return mDCSCode != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAudioIdentifier()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
if(isValid())
|
||||
{
|
||||
return getDCSCode().toString();
|
||||
}
|
||||
|
||||
return "DCS-Invalid - No Tone Selected";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Dcs o)
|
||||
{
|
||||
if(isValid())
|
||||
{
|
||||
if(o.isValid())
|
||||
{
|
||||
return getDCSCode().compareTo(o.getDCSCode());
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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 io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.INXDNTypeD;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* NXDN Type-D radio. The value is the over-the-air 16-bit identifier that combines the home repeater and radio so that
|
||||
* it matches the value of an NXDN Type-D radio identifier.
|
||||
*/
|
||||
public class NXDNTypeDRadio extends Radio implements INXDNTypeD
|
||||
{
|
||||
public NXDNTypeDRadio()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
setProtocol(Protocol.NXDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param homeRepeater for the radio
|
||||
* @param radio value, without the home repeater
|
||||
*/
|
||||
public NXDNTypeDRadio(int homeRepeater, int radio)
|
||||
{
|
||||
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, radio));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.NXDN_TYPE_D_RADIO_ID;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHomeRepeater(int homeRepeater)
|
||||
{
|
||||
setValue(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, getIdentifier()));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIdentifier(int identifier)
|
||||
{
|
||||
setValue(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Type-D Radio ID:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getIdentifier(), true));
|
||||
sb.append(" Protocol:").append(getProtocol());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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 io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.INXDNTypeDRange;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* NXDN Type-D radio range for a single home repeater. The min and max values are the over-the-air 16-bit identifiers
|
||||
* that combine the home repeater and radio.
|
||||
*/
|
||||
public class NXDNTypeDRadioRange extends RadioRange implements INXDNTypeDRange
|
||||
{
|
||||
public NXDNTypeDRadioRange()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
setProtocol(Protocol.NXDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param homeRepeater for the range
|
||||
* @param minRadio value, without the home repeater
|
||||
* @param maxRadio value, without the home repeater
|
||||
*/
|
||||
public NXDNTypeDRadioRange(int homeRepeater, int minRadio, int maxRadio)
|
||||
{
|
||||
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, minRadio),
|
||||
NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, maxRadio));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.NXDN_TYPE_D_RADIO_ID_RANGE;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMinRadio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHomeRepeater(int homeRepeater)
|
||||
{
|
||||
int min = getMinIdentifier();
|
||||
int max = getMaxIdentifier();
|
||||
setMinRadio(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, min));
|
||||
setMaxRadio(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, max));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getMinIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMinRadio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinIdentifier(int identifier)
|
||||
{
|
||||
setMinRadio(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getMaxIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMaxRadio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxIdentifier(int identifier)
|
||||
{
|
||||
setMaxRadio(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid when the base range is valid and both ends of the range use the same home repeater.
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return super.isValid() && getHomeRepeater() == NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMaxRadio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Type-D Radio ID Range:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMinIdentifier(), true));
|
||||
sb.append(" to ").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMaxIdentifier(), true));
|
||||
sb.append(" Protocol:").append(getProtocol());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.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();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.radio;
|
||||
@@ -38,10 +35,11 @@ public enum RadioFormat
|
||||
"<html>APCO25 valid range is 0 to 16,777,215"),
|
||||
DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
|
||||
"<html>DMR unit id valid range is 0 to 16,777,215"),
|
||||
NXDN("*****", 1, 0xFFFF, "1 to 65,535", "NXDN valid range is 1 to 65,535"),
|
||||
PASSPORT("********", 0, 0x7FFFFF, "0 to 8,388,607",
|
||||
"<html>PASSPORT valid range is 0 to 8,388,607"),
|
||||
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||
"Unknown protocol valid value range is 1-16,777,215");
|
||||
"<html>Unknown protocol valid value range is 1-16,777,215");
|
||||
|
||||
private String mMask;
|
||||
private int mMinimumValue;
|
||||
@@ -116,6 +114,8 @@ public enum RadioFormat
|
||||
return APCO25;
|
||||
case DMR:
|
||||
return DMR;
|
||||
case NXDN:
|
||||
return NXDN;
|
||||
case PASSPORT:
|
||||
return PASSPORT;
|
||||
default:
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.radio;
|
||||
@@ -26,14 +23,14 @@ import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Protocol-specific formatting of radio id values and parsing of formatted radio id values.
|
||||
@@ -47,6 +44,7 @@ public class RadioFormatter
|
||||
{
|
||||
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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 io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.INXDNTypeD;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* NXDN Type-D talkgroup. The value is the over-the-air 16-bit identifier that combines the home repeater and talkgroup
|
||||
* so that it matches the value of an NXDN Type-D talkgroup identifier.
|
||||
*/
|
||||
public class NXDNTypeDTalkgroup extends Talkgroup implements INXDNTypeD
|
||||
{
|
||||
public NXDNTypeDTalkgroup()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
setProtocol(Protocol.NXDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param homeRepeater for the talkgroup
|
||||
* @param talkgroup value, without the home repeater
|
||||
*/
|
||||
public NXDNTypeDTalkgroup(int homeRepeater, int talkgroup)
|
||||
{
|
||||
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, talkgroup));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.NXDN_TYPE_D_TALKGROUP;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHomeRepeater(int homeRepeater)
|
||||
{
|
||||
setValue(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, getIdentifier()));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIdentifier(int identifier)
|
||||
{
|
||||
setValue(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Type-D Talkgroup:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getIdentifier(), true));
|
||||
sb.append(" Protocol:").append(getProtocol());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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 io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.INXDNTypeDRange;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* NXDN Type-D talkgroup range for a single home repeater. The min and max values are the over-the-air 16-bit
|
||||
* identifiers that combine the home repeater and talkgroup.
|
||||
*/
|
||||
public class NXDNTypeDTalkgroupRange extends TalkgroupRange implements INXDNTypeDRange
|
||||
{
|
||||
public NXDNTypeDTalkgroupRange()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
setProtocol(Protocol.NXDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param homeRepeater for the range
|
||||
* @param minTalkgroup value, without the home repeater
|
||||
* @param maxTalkgroup value, without the home repeater
|
||||
*/
|
||||
public NXDNTypeDTalkgroupRange(int homeRepeater, int minTalkgroup, int maxTalkgroup)
|
||||
{
|
||||
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, minTalkgroup),
|
||||
NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, maxTalkgroup));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.NXDN_TYPE_D_TALKGROUP_RANGE;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMinTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHomeRepeater(int homeRepeater)
|
||||
{
|
||||
int min = getMinIdentifier();
|
||||
int max = getMaxIdentifier();
|
||||
setMinTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, min));
|
||||
setMaxTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, max));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getMinIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMinTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinIdentifier(int identifier)
|
||||
{
|
||||
setMinTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getMaxIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMaxTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxIdentifier(int identifier)
|
||||
{
|
||||
setMaxTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid when the base range is valid and both ends of the range use the same home repeater.
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return super.isValid() &&
|
||||
getHomeRepeater() == NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMaxTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Type-D Talkgroup Range:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMinIdentifier(), true));
|
||||
sb.append(" to ").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMaxIdentifier(), true));
|
||||
sb.append(" Protocol:").append(getProtocol());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This 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 io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Streaming talkgroup alias. For streamed audio metadata, this value is used to replace the decoded talkgroup or TO
|
||||
* value with an alias value when there are talkgroup collisions resulting from streaming multiple channels to the
|
||||
* same stream. This is common in P25 conventional repeater setups where each radio channel is using talkgroup 1.
|
||||
*/
|
||||
public class StreamAsTalkgroup extends Talkgroup
|
||||
{
|
||||
public StreamAsTalkgroup(int talkgroup)
|
||||
{
|
||||
super(Protocol.UNKNOWN, talkgroup);
|
||||
}
|
||||
|
||||
public StreamAsTalkgroup()
|
||||
{
|
||||
//No arg JAXB constructor
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.talkgroup;
|
||||
@@ -34,6 +31,8 @@ import io.github.dsheirer.protocol.Protocol;
|
||||
*/
|
||||
public enum TalkgroupFormat
|
||||
{
|
||||
AM("*****", 1, 0xFFFF, "1 to 65,535",
|
||||
"AM valid value range is 1-65,535"),
|
||||
APCO25("********", 0, 0xFFFF, "0 to 65,535",
|
||||
"<html>APCO25 talkgroup valid range is 0 to 65,535"),
|
||||
DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
|
||||
@@ -48,6 +47,8 @@ public enum TalkgroupFormat
|
||||
"<html>MPT-1327 valid ranges are 0-127(prefix)<br>and 1-8192(ident) (ie. 000-0001 to 127-8192)"),
|
||||
NBFM("*****", 1, 0xFFFF, "1 to 65,535",
|
||||
"NBFM valid value range is 1-65,535"),
|
||||
NXDN("*****", 0, 0xFFFF, "0 to 65,535",
|
||||
"NXDN talkgroup valid range is 0 to 65,535"),
|
||||
PASSPORT("*****", 1, 0xFFFF, "1 to 65,535",
|
||||
"Passport valid value range is 1-65,535"),
|
||||
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||
@@ -122,6 +123,8 @@ public enum TalkgroupFormat
|
||||
|
||||
switch(protocol)
|
||||
{
|
||||
case AM:
|
||||
return AM;
|
||||
case APCO25:
|
||||
return APCO25;
|
||||
case FLEETSYNC:
|
||||
@@ -135,6 +138,8 @@ public enum TalkgroupFormat
|
||||
return MPT1327;
|
||||
case NBFM:
|
||||
return NBFM;
|
||||
case NXDN:
|
||||
return NXDN;
|
||||
case PASSPORT:
|
||||
return PASSPORT;
|
||||
default:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +14,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.talkgroup;
|
||||
@@ -23,21 +22,21 @@ package io.github.dsheirer.alias.id.talkgroup;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.AnalogTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NBFMTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Protocol-specific formatting of talkgroup values and parsing of formatted talkgroup values.
|
||||
@@ -49,6 +48,10 @@ public class TalkgroupFormatter
|
||||
|
||||
static
|
||||
{
|
||||
AnalogTalkgroupFormatter analogTalkgroupFormatter = new AnalogTalkgroupFormatter();
|
||||
mFormatterMap.put(Protocol.AM, analogTalkgroupFormatter);
|
||||
mFormatterMap.put(Protocol.NBFM, analogTalkgroupFormatter);
|
||||
|
||||
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.FLEETSYNC, new FleetsyncTalkgroupFormatter());
|
||||
@@ -57,7 +60,7 @@ public class TalkgroupFormatter
|
||||
mFormatterMap.put(Protocol.LTR_NET, ltr);
|
||||
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.NBFM, new NBFMTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
/**
|
||||
* Used for defining an array list of filter methods to be implemented in AudioModule
|
||||
* The filters will be applied in the order given in the list.
|
||||
* Each audio filter mush implement the filter method.
|
||||
*/
|
||||
|
||||
public abstract class AbstractAudioFilter
|
||||
{
|
||||
/**
|
||||
* To be overridden by each audio filter
|
||||
* @param audio a buffer of 8 KHz audio samples
|
||||
* @return float[] filtered buffer of 8 KHz audio samples
|
||||
*/
|
||||
public abstract float[] filter(float[] audio);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory;
|
||||
public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class);
|
||||
public static final long DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH = 60 * 8000; // 1 minute @ 8kHz
|
||||
public static final long DEFAULT_MAX_SEGMENT_LENGTH_MILLIS = 60_000; // 1 minute
|
||||
public static final int DEFAULT_TIMESLOT = 0;
|
||||
private final int mMaxSegmentAudioSampleLength;
|
||||
private Listener<AudioSegment> mAudioSegmentListener;
|
||||
@@ -51,12 +51,12 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
|
||||
* Constructs an abstract audio module
|
||||
*
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param maxSegmentAudioSampleLength in milliseconds
|
||||
* @param maxSegmentLengthMillis maximum audio segment length in milliseconds
|
||||
*/
|
||||
public AbstractAudioModule(AliasList aliasList, int timeslot, long maxSegmentAudioSampleLength)
|
||||
public AbstractAudioModule(AliasList aliasList, int timeslot, long maxSegmentLengthMillis)
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
mMaxSegmentAudioSampleLength = (int)(maxSegmentAudioSampleLength * 8); //Convert milliseconds to samples
|
||||
mMaxSegmentAudioSampleLength = (int)(maxSegmentLengthMillis * 8); //Convert milliseconds to samples
|
||||
mTimeslot = timeslot;
|
||||
mIdentifierCollection = new MutableIdentifierCollection(getTimeslot());
|
||||
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
|
||||
@@ -67,7 +67,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
|
||||
*/
|
||||
public AbstractAudioModule(AliasList aliasList)
|
||||
{
|
||||
this(aliasList, DEFAULT_TIMESLOT, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
|
||||
this(aliasList, DEFAULT_TIMESLOT, DEFAULT_MAX_SEGMENT_LENGTH_MILLIS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
44
src/main/java/io/github/dsheirer/audio/AudioGainFilter.java
Normal file
44
src/main/java/io/github/dsheirer/audio/AudioGainFilter.java
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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;
|
||||
|
||||
import io.github.dsheirer.dsp.gain.AudioGainAndDcFilter;
|
||||
|
||||
/**
|
||||
* This is a wrapper for the AudioGainAndDcFilter class so it can be used with the AudioModule filter list.
|
||||
* Usually instantiated in the DecoderFactory
|
||||
*/
|
||||
public class AudioGainFilter extends AbstractAudioFilter
|
||||
{
|
||||
AudioGainAndDcFilter mAudioGain;
|
||||
|
||||
public AudioGainFilter(float minGain, float maxGain, float objectiveAmplitude)
|
||||
{
|
||||
mAudioGain = new AudioGainAndDcFilter(minGain, maxGain, objectiveAmplitude);
|
||||
mAudioGain.setDecayRate(2); // to remove pumping effect
|
||||
mAudioGain.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] filter(float[] audio)
|
||||
{
|
||||
return mAudioGain.process(audio);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -32,6 +32,8 @@ import io.github.dsheirer.sample.real.IRealBufferListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides packaging of demodulated audio sample buffers into audio segments for broadcast to registered listeners.
|
||||
* Includes audio packet metadata in constructed audio segments.
|
||||
@@ -42,55 +44,33 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
Listener<float[]>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
|
||||
static
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopBandCutoff(200)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.025)
|
||||
.passBandStart(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.build();
|
||||
try
|
||||
{
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
sHighPassFilterCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
}
|
||||
|
||||
private IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
|
||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private List<AbstractAudioFilter>mAudioFilters;
|
||||
private final SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
||||
|
||||
/**
|
||||
* Creates an Audio Module.
|
||||
*
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param timeslot for this audio module
|
||||
* @param maxAudioSegmentLength in milliseconds
|
||||
* @param audioFilters a list of audio filters to apply to the incoming audio samples
|
||||
*/
|
||||
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength)
|
||||
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, List<AbstractAudioFilter> audioFilters)
|
||||
{
|
||||
super(aliasList, timeslot, maxAudioSegmentLength);
|
||||
mAudioFilters = audioFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Audio Module.
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param audioFilters a list of audio filters to apply to the incoming audio samples
|
||||
*/
|
||||
public AudioModule(AliasList aliasList)
|
||||
public AudioModule(AliasList aliasList, List<AbstractAudioFilter> audioFilters)
|
||||
{
|
||||
super(aliasList);
|
||||
mAudioFilters = audioFilters;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,7 +101,15 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
{
|
||||
if(mSquelchState == SquelchState.UNSQUELCH)
|
||||
{
|
||||
audioBuffer = mHighPassFilter.filter(audioBuffer);
|
||||
// apply audio filters if any (i.e. deemphasis, high pass, etc.)
|
||||
if(!mAudioFilters.isEmpty())
|
||||
{
|
||||
for(AbstractAudioFilter filter : mAudioFilters)
|
||||
{
|
||||
audioBuffer = filter.filter(audioBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
addAudio(audioBuffer);
|
||||
}
|
||||
}
|
||||
@@ -141,11 +129,16 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
@Override
|
||||
public void receive(SquelchStateEvent event)
|
||||
{
|
||||
mSquelchState = event.getSquelchState();
|
||||
SquelchState squelchState = event.getSquelchState();
|
||||
|
||||
if(mSquelchState == SquelchState.SQUELCH)
|
||||
if(mSquelchState != squelchState)
|
||||
{
|
||||
closeAudioSegment();
|
||||
mSquelchState = squelchState;
|
||||
|
||||
if(mSquelchState == SquelchState.SQUELCH)
|
||||
{
|
||||
closeAudioSegment();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.Collection;
|
||||
@@ -60,7 +61,7 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* Producers will add audio buffers and update identifiers throughout the life-cycle of an audio segment. The producer
|
||||
* will signal the completion of an audio segment by setting the complete property to true. This allows consumers the
|
||||
* option to process the audio buffers throughout the life-cycle of the segment, or to process all of the buffers once
|
||||
* option to process the audio buffers throughout the life-cycle of the segment, or to process all the buffers once
|
||||
* the segment is complete.
|
||||
*/
|
||||
public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
@@ -68,6 +69,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
|
||||
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mDuplicate = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mEncrypted = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
|
||||
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
|
||||
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
|
||||
@@ -129,6 +131,25 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
return (mSampleCount / 8); //8 kHz audio generates 8 samples per millisecond
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio segment contains encrypted audio.
|
||||
*
|
||||
* @return encrypted property
|
||||
*/
|
||||
public BooleanProperty encryptedProperty()
|
||||
{
|
||||
return mEncrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio segment is encrypted
|
||||
* @return true if encrypted.
|
||||
*/
|
||||
public boolean isEncrypted()
|
||||
{
|
||||
return mEncrypted.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete property is used by the audio segment producer to signal that the segment is complete and no
|
||||
* additional audio or identifiers will be added to the segment.
|
||||
@@ -386,6 +407,11 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
throw new IllegalStateException("Can't add audio to an audio segment that is being disposed");
|
||||
}
|
||||
|
||||
if(mAudioBuffers.isEmpty())
|
||||
{
|
||||
mStartTimestamp = System.currentTimeMillis() - 20;
|
||||
}
|
||||
|
||||
mAudioBuffers.add(audioBuffer);
|
||||
mSampleCount += audioBuffer.length;
|
||||
}
|
||||
@@ -415,6 +441,14 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
mIdentifierCollection.update(identifier);
|
||||
|
||||
/**
|
||||
* If we have a late-add encryption key, set the encrypted flag to true.
|
||||
*/
|
||||
if(identifier instanceof EncryptionKeyIdentifier eki)
|
||||
{
|
||||
mEncrypted.set(eki.isEncrypted());
|
||||
}
|
||||
|
||||
List<Alias> aliases = mAliasList.getAliases(identifier);
|
||||
|
||||
for(Alias alias: aliases)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -28,12 +28,9 @@ 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.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
|
||||
import io.github.dsheirer.preference.duplicate.ICallManagementProvider;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -41,7 +38,10 @@ import java.util.Map;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
|
||||
@@ -52,18 +52,41 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
|
||||
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
|
||||
private ICallManagementProvider mCallManagementProvider;
|
||||
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
|
||||
protected Listener<AudioSegment> mDuplicateCallDetectionListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param userPreferences to access the duplicate call detection preferences.
|
||||
*/
|
||||
public DuplicateCallDetector(UserPreferences userPreferences)
|
||||
{
|
||||
mDuplicateCallDetectionPreference = userPreferences.getDuplicateCallDetectionPreference();
|
||||
this(userPreferences.getCallManagementPreference());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param callManagementProvider to provide call management preferences.
|
||||
*/
|
||||
public DuplicateCallDetector(ICallManagementProvider callManagementProvider)
|
||||
{
|
||||
mCallManagementProvider = callManagementProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional listener to be notified each time an audio segment is flagged as duplicate.
|
||||
* @param listener to register
|
||||
*/
|
||||
public void setDuplicateCallDetectionListener(Listener<AudioSegment> listener)
|
||||
{
|
||||
mDuplicateCallDetectionListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionEnabled())
|
||||
{
|
||||
Identifier identifier = audioSegment.getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
|
||||
@@ -78,7 +101,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
|
||||
if(detector == null)
|
||||
{
|
||||
detector = new SystemDuplicateCallDetector();
|
||||
detector = new SystemDuplicateCallDetector(mCallManagementProvider, system);
|
||||
mDetectorMap.put(system, detector);
|
||||
}
|
||||
|
||||
@@ -88,45 +111,99 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* System level duplicate call detector. Uses a scheduled executor to run every 25 ms to compare all ongoing call
|
||||
* audio segments to detect duplicates.
|
||||
*
|
||||
* All audio segments remain in the queue until they are flagged as complete. While in the queue, each call is
|
||||
* compared against the others to detect duplicates. Once all calls are either flagged as complete or flagged as
|
||||
* duplicate and removed, the queue is empty and the monitoring is shutdown until a new audio segment arrives and
|
||||
* then the monitoring starts again.
|
||||
*/
|
||||
public class SystemDuplicateCallDetector
|
||||
{
|
||||
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private AtomicBoolean mMonitoring = new AtomicBoolean();
|
||||
private final LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private final List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private ScheduledFuture<?> mProcessorFuture;
|
||||
private Lock mLock = new ReentrantLock();
|
||||
private boolean mMonitoring = false;
|
||||
private final ICallManagementProvider mCallManagementProvider;
|
||||
private String mSystem;
|
||||
|
||||
public SystemDuplicateCallDetector()
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param callManagementProvider to check for duplicate monitoring preferences
|
||||
*/
|
||||
public SystemDuplicateCallDetector(ICallManagementProvider callManagementProvider, String system)
|
||||
{
|
||||
mCallManagementProvider = callManagementProvider;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the audio segment to the monitoring queue.
|
||||
* @param audioSegment to add
|
||||
*/
|
||||
public void add(AudioSegment audioSegment)
|
||||
{
|
||||
//Block on audio segment queue so that we don't interfere with monitoring shutdown
|
||||
synchronized(mAudioSegmentQueue)
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
mAudioSegmentQueue.add(audioSegment);
|
||||
startMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
private void startMonitoring()
|
||||
{
|
||||
if(mMonitoring.compareAndSet(false, true))
|
||||
finally
|
||||
{
|
||||
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> process(),
|
||||
0, 25, TimeUnit.MILLISECONDS);
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the call monitoring thread if it's not already running.
|
||||
*
|
||||
* Note: this method should only be called from a thread with the lock acquired.
|
||||
*/
|
||||
private void startMonitoring()
|
||||
{
|
||||
if(!mMonitoring)
|
||||
{
|
||||
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::process,
|
||||
0, 25, TimeUnit.MILLISECONDS);
|
||||
mMonitoring = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the call monitoring thread if the audio segements queue is empty.
|
||||
*
|
||||
* Note: this should only be called from a separate thread and not from within the scheduled monitoring thread
|
||||
* because cancelling the scheduled timer from within the process() method would kill the thread without
|
||||
* releasing the lock, causing a deadlock.
|
||||
*/
|
||||
private void stopMonitoring()
|
||||
{
|
||||
if(mMonitoring.compareAndSet(true, false))
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(mProcessorFuture != null)
|
||||
//Recheck the audio segments queue to make sure we didn't slip in another audio segment before we can
|
||||
//shut down the scheduled monitoring thread.
|
||||
if(mMonitoring && mAudioSegments.isEmpty())
|
||||
{
|
||||
mProcessorFuture.cancel(true);
|
||||
if(mProcessorFuture != null)
|
||||
{
|
||||
mProcessorFuture.cancel(true);
|
||||
mProcessorFuture = null;
|
||||
}
|
||||
|
||||
mMonitoring = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +214,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
*/
|
||||
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
|
||||
{
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByTalkgroupEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionByTalkgroupEnabled())
|
||||
{
|
||||
//Step 1 check for duplicate TO values
|
||||
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
|
||||
@@ -149,7 +226,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionByRadioEnabled())
|
||||
{
|
||||
//Step 2 check for duplicate FROM values
|
||||
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
|
||||
@@ -169,53 +246,51 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
* @param identifiers2
|
||||
* @return
|
||||
*/
|
||||
private boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
|
||||
public static boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
|
||||
{
|
||||
for(Identifier identifier1: identifiers1)
|
||||
{
|
||||
if(identifier1 instanceof TalkgroupIdentifier)
|
||||
if(identifier1 instanceof TalkgroupIdentifier tgId1)
|
||||
{
|
||||
int talkgroup1 = ((TalkgroupIdentifier)identifier1).getValue();
|
||||
int tg1 = tgId1.getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof TalkgroupIdentifier &&
|
||||
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
|
||||
if(identifier2 instanceof TalkgroupIdentifier tgId2 && tgId2.getValue() == tg1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(identifier2 instanceof PatchGroupIdentifier &&
|
||||
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
else if(identifier2 instanceof PatchGroupIdentifier pgId2 &&
|
||||
pgId2.getValue().getPatchGroup().getValue() == tg1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(identifier1 instanceof PatchGroupIdentifier)
|
||||
else if(identifier1 instanceof PatchGroupIdentifier pgId1)
|
||||
{
|
||||
int talkgroup1 = ((PatchGroupIdentifier)identifier1).getValue().getPatchGroup().getValue();
|
||||
int talkgroup1 = pgId1.getValue().getPatchGroup().getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof TalkgroupIdentifier &&
|
||||
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
|
||||
if(identifier2 instanceof TalkgroupIdentifier tgId2 && tgId2.getValue() == talkgroup1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(identifier2 instanceof PatchGroupIdentifier &&
|
||||
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
else if(identifier2 instanceof PatchGroupIdentifier pgId2 &&
|
||||
pgId2.getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(identifier1 instanceof RadioIdentifier)
|
||||
else if(identifier1 instanceof RadioIdentifier raId1)
|
||||
{
|
||||
int radio1 = ((RadioIdentifier)identifier1).getValue();
|
||||
int radio1 = raId1.getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof RadioIdentifier && ((RadioIdentifier)identifier2).getValue() == radio1)
|
||||
if(identifier2 instanceof RadioIdentifier raId2 && raId2.getValue() == radio1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -231,6 +306,8 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
*/
|
||||
private void process()
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
//Transfer in newly arrived audio segments
|
||||
@@ -248,6 +325,19 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
return complete;
|
||||
});
|
||||
|
||||
//Remove any encrypted audio segments.
|
||||
mAudioSegments.removeIf(audioSegment -> {
|
||||
boolean encrypted = audioSegment.isEncrypted();
|
||||
|
||||
if(encrypted)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
|
||||
|
||||
return encrypted;
|
||||
});
|
||||
|
||||
//Only check for duplicates if there is more than one call
|
||||
if(mAudioSegments.size() > 1)
|
||||
{
|
||||
@@ -258,7 +348,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
{
|
||||
AudioSegment current = mAudioSegments.get(currentIndex);
|
||||
|
||||
if(!current.isDuplicate())
|
||||
if(current.hasAudio() && !current.isDuplicate())
|
||||
{
|
||||
int checkIndex = currentIndex + 1;
|
||||
|
||||
@@ -273,6 +363,12 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
toCheck.setDuplicate(true);
|
||||
toCheck.decrementConsumerCount();
|
||||
duplicates.add(toCheck);
|
||||
|
||||
//Notify optional listener that we flagged the call as duplicate.
|
||||
if(mDuplicateCallDetectionListener != null)
|
||||
{
|
||||
mDuplicateCallDetectionListener.receive(toCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,26 +382,12 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
mAudioSegments.removeAll(duplicates);
|
||||
}
|
||||
|
||||
//Finally, if the audio segment queue is empty, shutdown montitoring until a new segment arrives
|
||||
//Finally, if the audio segment queue is now empty, shutdown monitoring until a new segment arrives.
|
||||
//The monitor shutdown method has to be called on a separate thread so that we don't kill our current
|
||||
// thread and fail to release the lock.
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
//Block on the audio segment queue so that we can shutdown before any new segments are added, and
|
||||
//allow the add(segment) to restart monitoring as soon as needed.
|
||||
synchronized(mAudioSegmentQueue)
|
||||
{
|
||||
if(mAudioSegmentQueue.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
stopMonitoring();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unexpected error during duplicate audio segment monitoring shutdown", e);
|
||||
//Do nothing, we got interrupted
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadPool.CACHED.submit(this::stopMonitoring);
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
@@ -313,6 +395,10 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
mLog.error("Unknown error while processing audio segments for duplicate call detection. Please report " +
|
||||
"this to the developer.", t);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class HighPassAudioFilter extends AbstractAudioFilter
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(HighPassAudioFilter.class);
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
|
||||
static
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopBandCutoff(200)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.025)
|
||||
.passBandStart(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.build();
|
||||
try
|
||||
{
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
sHighPassFilterCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
}
|
||||
|
||||
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
|
||||
|
||||
@Override
|
||||
public float[] filter(float[] audio)
|
||||
{
|
||||
audio = mHighPassFilter.filter(audio);
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,27 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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;
|
||||
|
||||
import io.github.dsheirer.audio.playback.AudioOutput;
|
||||
import io.github.dsheirer.audio.playback.AudioChannel;
|
||||
import io.github.dsheirer.audio.playback.AudioPlaybackDeviceDescriptor;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -12,13 +30,13 @@ import java.util.List;
|
||||
public interface IAudioController
|
||||
{
|
||||
/* Current Mixer and MixerChannel configuration */
|
||||
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException;
|
||||
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException;
|
||||
public void setAudioPlaybackDevice(AudioPlaybackDeviceDescriptor device ) throws AudioException;
|
||||
public AudioPlaybackDeviceDescriptor getAudioPlaybackDevice() throws AudioException;
|
||||
|
||||
/* Audio Output(s) */
|
||||
public List<AudioOutput> getAudioOutputs();
|
||||
/* Audio Channel(s) */
|
||||
public List<AudioChannel> getAudioChannels();
|
||||
|
||||
/* Controller Audio Event Listener */
|
||||
public void addControllerListener( Listener<AudioEvent> listener );
|
||||
public void removeControllerListener( Listener<AudioEvent> listener );
|
||||
public void addAudioEventListener(Listener<AudioEvent> listener );
|
||||
public void removeAudioEventListener(Listener<AudioEvent> listener );
|
||||
}
|
||||
|
||||
@@ -34,7 +34,9 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
private Listener<BroadcastEvent> mBroadcastEventListener;
|
||||
private T mBroadcastConfiguration;
|
||||
protected ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY);
|
||||
protected ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
|
||||
protected int mStreamedAudioCount = 0;
|
||||
protected int mDuplicateRejectedAudioCount = 0;
|
||||
protected int mErrorAudioCount = 0;
|
||||
protected int mAgedOffAudioCount = 0;
|
||||
|
||||
@@ -55,6 +57,14 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable last bad broadcast state property
|
||||
*/
|
||||
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
|
||||
{
|
||||
return mLastBadBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current state of the broadcastAudio connection
|
||||
*/
|
||||
@@ -68,10 +78,26 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
*/
|
||||
public void setBroadcastState(BroadcastState broadcastState)
|
||||
{
|
||||
if(broadcastState == BroadcastState.CONNECTED)
|
||||
{
|
||||
mLastBadBroadcastState.setValue(null);
|
||||
}
|
||||
else if(broadcastState.isErrorState() || broadcastState.isWarningState())
|
||||
{
|
||||
mLastBadBroadcastState.setValue(broadcastState);
|
||||
}
|
||||
mBroadcastState.setValue(broadcastState);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Last bad state of the broadcastAudio connection
|
||||
*/
|
||||
public BroadcastState getLastBadBroadcastState()
|
||||
{
|
||||
return mLastBadBroadcastState.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the broadcaster
|
||||
*/
|
||||
@@ -138,6 +164,22 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
mStreamedAudioCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of audio recordings rejected by the remote service as duplicates.
|
||||
*/
|
||||
public int getDuplicateRejectedAudioCount()
|
||||
{
|
||||
return mDuplicateRejectedAudioCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the duplicate-rejected audio count by one.
|
||||
*/
|
||||
public void incrementDuplicateRejectedAudioCount()
|
||||
{
|
||||
mDuplicateRejectedAudioCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of audio recordings that were removed for exceeding age limit
|
||||
*/
|
||||
|
||||
@@ -211,9 +211,17 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
|
||||
super.setBroadcastState(state);
|
||||
|
||||
if(mBroadcastState.get() != null && mBroadcastState.get().isErrorState())
|
||||
if(mBroadcastState.get() != null)
|
||||
{
|
||||
stop();
|
||||
if(mBroadcastState.get().isErrorState())
|
||||
{
|
||||
mLog.error("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
|
||||
stop();
|
||||
}
|
||||
else if(mBroadcastState.get().isWarningState())
|
||||
{
|
||||
mLog.warn("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
|
||||
}
|
||||
}
|
||||
|
||||
if(!connected())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,25 +19,34 @@
|
||||
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.AudioSegmentRecorder;
|
||||
import io.github.dsheirer.record.RecordFormat;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on
|
||||
@@ -127,7 +136,7 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled())
|
||||
if(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateStreamingSuppressionEnabled())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
@@ -138,29 +147,60 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
|
||||
if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels())
|
||||
{
|
||||
Path path = getTemporaryRecordingPath();
|
||||
long length = 0;
|
||||
|
||||
for(float[] audioBuffer: audioSegment.getAudioBuffers())
|
||||
{
|
||||
length += audioBuffer.length;
|
||||
}
|
||||
|
||||
length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3, mUserPreferences);
|
||||
IdentifierCollection identifierCollectionCopy =
|
||||
IdentifierCollection identifiers =
|
||||
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
|
||||
|
||||
AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(),
|
||||
identifierCollectionCopy, audioSegment.getStartTimestamp(), length);
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
if(identifiers.getToIdentifier() instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
mLog.error("Error recording temporary stream MP3");
|
||||
if(mUserPreferences.getCallManagementPreference()
|
||||
.getPatchGroupStreamingOption() == PatchGroupStreamingOption.TALKGROUPS)
|
||||
{
|
||||
//Decompose the patch group into the individual (patched) talkgroups and process the audio
|
||||
//segment for each patched talkgroup.
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
List<Identifier> ids = new ArrayList<>();
|
||||
ids.addAll(patchGroup.getPatchedTalkgroupIdentifiers());
|
||||
ids.addAll(patchGroup.getPatchedRadioIdentifiers());
|
||||
|
||||
//If there are no patched radios/talkgroups, override user preference and stream as a patch group
|
||||
if(ids.isEmpty() || audioSegment.getAliasList() == null)
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
else
|
||||
{
|
||||
AliasList aliasList = audioSegment.getAliasList();
|
||||
|
||||
for(Identifier identifier: ids)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
Set<BroadcastChannel> broadcastChannels = new HashSet<>();
|
||||
for(Alias alias: aliases)
|
||||
{
|
||||
broadcastChannels.addAll(alias.getBroadcastChannels());
|
||||
}
|
||||
|
||||
if(!broadcastChannels.isEmpty())
|
||||
{
|
||||
MutableIdentifierCollection decomposedIdentifiers =
|
||||
new MutableIdentifierCollection(identifiers.getIdentifiers());
|
||||
//Remove patch group TO identifier & replace with the patched talkgroup/radio
|
||||
decomposedIdentifiers.remove(Role.TO);
|
||||
decomposedIdentifiers.update(identifier);
|
||||
processAudioSegment(audioSegment, decomposedIdentifiers, broadcastChannels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +209,40 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an audio segment for streaming by creating a temporary MP3 recording and submitting the recording
|
||||
* to the specific broadcast channel(s).
|
||||
* @param audioSegment to process for streaming
|
||||
* @param identifierCollection to use for the streamed audio recording.
|
||||
* @param broadcastChannels to receive the audio recording
|
||||
*/
|
||||
private void processAudioSegment(AudioSegment audioSegment, IdentifierCollection identifierCollection,
|
||||
Set<BroadcastChannel> broadcastChannels)
|
||||
{
|
||||
Path path = getTemporaryRecordingPath();
|
||||
long length = 0;
|
||||
|
||||
for(float[] audioBuffer: audioSegment.getAudioBuffers())
|
||||
{
|
||||
length += audioBuffer.length;
|
||||
}
|
||||
|
||||
length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3, mUserPreferences, identifierCollection);
|
||||
|
||||
AudioRecording audioRecording = new AudioRecording(path, broadcastChannels, identifierCollection,
|
||||
audioSegment.getStartTimestamp(), length);
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error recording temporary stream MP3");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary streaming recording file path
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||
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.shoutcast.v1.ShoutcastV1Configuration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||
@@ -46,6 +48,8 @@ import java.net.SocketAddress;
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
||||
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
||||
@JsonSubTypes.Type(value = OpenMHzConfiguration.class, name="OpenMHzConfiguration"),
|
||||
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
||||
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
||||
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
||||
|
||||
@@ -82,6 +82,7 @@ public class BroadcastEvent
|
||||
BROADCASTER_QUEUE_CHANGE,
|
||||
BROADCASTER_STATE_CHANGE,
|
||||
BROADCASTER_STREAMED_COUNT_CHANGE,
|
||||
BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE,
|
||||
BROADCASTER_AGED_OFF_COUNT_CHANGE,
|
||||
BROADCASTER_ERROR_COUNT_CHANGE,
|
||||
BROADCASTER_DELETE,
|
||||
|
||||
@@ -22,6 +22,12 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
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.IcecastHTTPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||
@@ -61,6 +67,12 @@ public class BroadcastFactory
|
||||
case BROADCASTIFY_CALL:
|
||||
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case RDIOSCANNER_CALL:
|
||||
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case BROADCASTIFY:
|
||||
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
@@ -99,8 +111,12 @@ public class BroadcastFactory
|
||||
{
|
||||
case BROADCASTIFY_CALL:
|
||||
return new BroadcastifyCallConfiguration(format);
|
||||
case RDIOSCANNER_CALL:
|
||||
return new RdioScannerConfiguration(format);
|
||||
case BROADCASTIFY:
|
||||
return new BroadcastifyFeedConfiguration(format);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzConfiguration(format);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPConfiguration(format);
|
||||
case ICECAST_TCP:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -62,11 +62,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
public static final int COLUMN_BROADCASTER_STATUS = 2;
|
||||
public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
|
||||
public static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
|
||||
public static final int COLUMN_BROADCASTER_AGED_OFF_COUNT = 5;
|
||||
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 6;
|
||||
public static final int COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT = 5;
|
||||
public static final int COLUMN_BROADCASTER_AGED_OFF_COUNT = 6;
|
||||
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 7;
|
||||
|
||||
public static final String[] COLUMN_NAMES = new String[]
|
||||
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
|
||||
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Duplicate Rejected", "Aged Off", "Upload Error"};
|
||||
|
||||
private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
|
||||
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
|
||||
@@ -344,14 +345,13 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
deleteBroadcaster(configuredBroadcast);
|
||||
}
|
||||
|
||||
AbstractAudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration,
|
||||
final AbstractAudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration,
|
||||
mAliasModel, mUserPreferences);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
configuredBroadcast.setAudioBroadcaster(audioBroadcaster);
|
||||
audioBroadcaster.setListener(mBroadcastEventListener);
|
||||
audioBroadcaster.start();
|
||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getId(), audioBroadcaster);
|
||||
|
||||
int index = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
@@ -362,6 +362,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
|
||||
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
|
||||
ThreadPool.CACHED.submit(() -> audioBroadcaster.start());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,6 +532,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
|
||||
}
|
||||
break;
|
||||
case BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT);
|
||||
}
|
||||
break;
|
||||
case BROADCASTER_AGED_OFF_COUNT_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
@@ -607,6 +614,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
return configuredBroadcast.getAudioBroadcaster().getStreamedAudioCount();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT:
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
return configuredBroadcast.getAudioBroadcaster().getDuplicateRejectedAudioCount();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
@@ -643,6 +656,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
|
||||
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||
case COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT:
|
||||
case COLUMN_BROADCASTER_ERROR_COUNT:
|
||||
return Integer.class;
|
||||
case COLUMN_BROADCAST_SERVER_TYPE:
|
||||
|
||||
@@ -31,6 +31,8 @@ public enum BroadcastServerType
|
||||
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
|
||||
|
||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
||||
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
||||
OPENMHZ("OpenMHz", "images/openmhz.png"),
|
||||
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||
|
||||
@@ -23,90 +23,92 @@ public enum BroadcastState
|
||||
/**
|
||||
* General error in configuration that causes the remote server to reject the connection
|
||||
*/
|
||||
CONFIGURATION_ERROR("Configuration Error", true),
|
||||
CONFIGURATION_ERROR("Configuration Error", true, false),
|
||||
|
||||
/**
|
||||
* Connected to the broadcastAudio server and capable of streaming
|
||||
*/
|
||||
CONNECTED("Connected", false),
|
||||
CONNECTED("Connected", false, false),
|
||||
|
||||
/**
|
||||
* Connection interrupted, attempting to reconnect.
|
||||
*/
|
||||
CONNECTING("Connecting", false),
|
||||
CONNECTING("Connecting", false, false),
|
||||
|
||||
/**
|
||||
* Indicates the configuration is disabled
|
||||
*/
|
||||
DISABLED("Disabled", true),
|
||||
DISABLED("Disabled", true, false),
|
||||
|
||||
/**
|
||||
* Disconnected from the broadcastAudio server
|
||||
*/
|
||||
DISCONNECTED("Disconnected", false),
|
||||
DISCONNECTED("Disconnected", false, false),
|
||||
|
||||
/**
|
||||
* Invalid credentials - user name or password
|
||||
*/
|
||||
INVALID_CREDENTIALS("Invalid User Name/Password", true),
|
||||
INVALID_CREDENTIALS("Invalid User Name/Password", true, false),
|
||||
|
||||
/**
|
||||
* Invalid mount point
|
||||
*/
|
||||
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true),
|
||||
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true, false),
|
||||
|
||||
/**
|
||||
* Invalid configuration settings
|
||||
*/
|
||||
INVALID_SETTINGS("Invalid Settings", true),
|
||||
INVALID_SETTINGS("Invalid Settings", true, false),
|
||||
|
||||
/**
|
||||
* Remote server max sources has been exceeded
|
||||
*/
|
||||
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true),
|
||||
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", false, true),
|
||||
|
||||
/**
|
||||
* Specified mount point is already in use
|
||||
*/
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false),
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false, true),
|
||||
|
||||
/**
|
||||
* Network is unavailable or the server address cannot be resolved
|
||||
*/
|
||||
NETWORK_UNAVAILABLE("Network Unavailable", false),
|
||||
NETWORK_UNAVAILABLE("Network Unavailable", false, false),
|
||||
|
||||
/**
|
||||
* Server is not known or reachable
|
||||
*/
|
||||
NO_SERVER("No Server", false),
|
||||
NO_SERVER("No Server", false, false),
|
||||
|
||||
/**
|
||||
* Initial state with no connection attempted.
|
||||
*/
|
||||
READY("Ready", false),
|
||||
READY("Ready", false, false),
|
||||
|
||||
/**
|
||||
* Error while broadcasting stream data. Temporary error state to allow connection to be reset.
|
||||
*/
|
||||
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false),
|
||||
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false, false),
|
||||
|
||||
/**
|
||||
* Unsupported audio format
|
||||
*/
|
||||
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true),
|
||||
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true, false),
|
||||
|
||||
/**
|
||||
* Unspecified error
|
||||
*/
|
||||
ERROR("Error", true);
|
||||
ERROR("Error", true, false);
|
||||
|
||||
private String mLabel;
|
||||
private boolean mErrorState;
|
||||
private boolean mWarningState;
|
||||
|
||||
BroadcastState(String label, boolean errorState)
|
||||
BroadcastState(String label, boolean errorState, boolean warningState)
|
||||
{
|
||||
mLabel = label;
|
||||
mErrorState = errorState;
|
||||
mWarningState = warningState;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
@@ -118,4 +120,9 @@ public enum BroadcastState
|
||||
{
|
||||
return mErrorState;
|
||||
}
|
||||
|
||||
public boolean isWarningState()
|
||||
{
|
||||
return mWarningState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,10 @@ public class BroadcastStatusPanel extends JPanel
|
||||
else if(state == BroadcastState.DISABLED)
|
||||
{
|
||||
setBackground(table.getBackground());
|
||||
setForeground(Color.LIGHT_GRAY);
|
||||
//Use the LAF's disabled-foreground so the cell reads correctly on any
|
||||
//theme - LIGHT_GRAY is invisible on a light background.
|
||||
Color disabled = javax.swing.UIManager.getColor("Label.disabledForeground");
|
||||
setForeground(disabled != null ? disabled : table.getForeground());
|
||||
}
|
||||
else if(state == BroadcastState.INVALID_SETTINGS ||
|
||||
state == BroadcastState.NETWORK_UNAVAILABLE)
|
||||
|
||||
@@ -37,6 +37,7 @@ public class ConfiguredBroadcast
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
private AbstractAudioBroadcaster mAudioBroadcaster;
|
||||
private ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>();
|
||||
private ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -89,6 +90,14 @@ public class ConfiguredBroadcast
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last bad broadcast state of the configured audio broadcaster (optional)
|
||||
*/
|
||||
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
|
||||
{
|
||||
return mLastBadBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio broadcaster
|
||||
* @param audioBroadcaster to use for this configuration
|
||||
@@ -96,11 +105,13 @@ public class ConfiguredBroadcast
|
||||
public void setAudioBroadcaster(AbstractAudioBroadcaster audioBroadcaster)
|
||||
{
|
||||
mBroadcastState.unbind();
|
||||
mLastBadBroadcastState.unbind();
|
||||
mAudioBroadcaster = audioBroadcaster;
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
mBroadcastState.bind(mAudioBroadcaster.broadcastStateProperty());
|
||||
mLastBadBroadcastState.bind(mAudioBroadcaster.lastBadBroadcastStateProperty());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -120,6 +131,7 @@ public class ConfiguredBroadcast
|
||||
{
|
||||
mBroadcastState.setValue(BroadcastState.CONFIGURATION_ERROR);
|
||||
}
|
||||
mLastBadBroadcastState.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.audio.broadcast;
|
||||
|
||||
/**
|
||||
* Options for streaming management of patch groups. These options are used when streaming an audio call that is
|
||||
* tagged to a patch group. The audio will either be streamed once and identified as the patch group, or it will
|
||||
* be streamed multiple times, once for each individual patched talkgroup.
|
||||
*/
|
||||
public enum PatchGroupStreamingOption
|
||||
{
|
||||
PATCH_GROUP("Patch Group"),
|
||||
TALKGROUPS("Individual Talkgroups");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param label to display
|
||||
*/
|
||||
PatchGroupStreamingOption(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,6 +20,8 @@
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
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;
|
||||
@@ -32,15 +34,14 @@ import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
|
||||
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.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@@ -54,6 +55,8 @@ import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Audio broadcaster to push completed audio recordings to the Broadcastify call push API.
|
||||
@@ -78,6 +81,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
.build();
|
||||
private long mLastConnectionAttempt;
|
||||
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||
final private AliasModel mAliasModel;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
@@ -88,6 +92,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
{
|
||||
super(config);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,6 +254,8 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
String radioId = getFrom(audioRecording);
|
||||
float frequency = getFrequency(audioRecording);
|
||||
|
||||
String radioIdAlias = getFromAlias(audioRecording);
|
||||
|
||||
BroadcastifyCallBuilder bodyBuilder = new BroadcastifyCallBuilder();
|
||||
bodyBuilder.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
|
||||
.addPart(FormField.SYSTEM_ID, getBroadcastConfiguration().getSystemID())
|
||||
@@ -259,6 +266,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
.addPart(FormField.FREQUENCY, frequency)
|
||||
.addPart(FormField.ENCODING, ENCODING_TYPE_MP3);
|
||||
|
||||
if(radioIdAlias != null && !radioIdAlias.isEmpty())
|
||||
{
|
||||
bodyBuilder.addPart(FormField.RADIO_ID_ALIAS, radioIdAlias);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
@@ -358,6 +370,9 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
else if(urlResponse.startsWith("1 SKIPPED"))
|
||||
{
|
||||
//Broadcastify is telling us to skip audio upload - someone already uploaded it
|
||||
incrementDuplicateRejectedAudioCount();
|
||||
broadcast(new BroadcastEvent(BroadcastifyCallBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
else
|
||||
@@ -442,13 +457,58 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the over-the-air talker alias for the FROM (source radio) identifier, acquired in real-time from RF
|
||||
* (e.g. P25 Motorola Talker Alias or DMR Talker Alias). Returns null if no OTA alias was received for this call.
|
||||
* This is sent as the srcId_alias field in Broadcastify uploads.
|
||||
*/
|
||||
private static String getFromAlias(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof TalkerAliasIdentifier talkerAliasIdentifier)
|
||||
{
|
||||
if(talkerAliasIdentifier.isValid())
|
||||
{
|
||||
return talkerAliasIdentifier.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*/
|
||||
private static String getTo(AudioRecording audioRecording)
|
||||
private String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
//Alias the TO value when the user specifies a 'Stream As Talkgroup'
|
||||
if(identifier != null)
|
||||
{
|
||||
AliasListConfigurationIdentifier config = audioRecording.getIdentifierCollection().getAliasListConfiguration();
|
||||
|
||||
if(config != null)
|
||||
{
|
||||
AliasList aliasList = mAliasModel.getAliasList(config.getValue());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
|
||||
for(Alias a: aliases)
|
||||
{
|
||||
if(a.getStreamTalkgroupAlias() != null)
|
||||
{
|
||||
return String.valueOf(a.getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
return format(patchGroupIdentifier);
|
||||
@@ -475,7 +535,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
@@ -35,6 +32,7 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyFeedConfiguration.class);
|
||||
|
||||
private int mFeedID;
|
||||
private boolean mVerboseLogging = false;
|
||||
|
||||
public BroadcastifyFeedConfiguration()
|
||||
{
|
||||
@@ -123,4 +121,15 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
mFeedID = feedID;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "verbose_logging")
|
||||
public boolean isVerboseLogging()
|
||||
{
|
||||
return mVerboseLogging;
|
||||
}
|
||||
|
||||
public void setVerboseLogging(boolean verboseLogging)
|
||||
{
|
||||
mVerboseLogging = verboseLogging;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ public enum FormField
|
||||
TIMESTAMP("ts"),
|
||||
TALKGROUP_ID("tg"),
|
||||
RADIO_ID("src"),
|
||||
RADIO_ID_ALIAS("srcId_alias"),
|
||||
FREQUENCY("freq"),
|
||||
ENCODING("enc"),
|
||||
TEST("test");
|
||||
|
||||
@@ -52,8 +52,10 @@ import org.slf4j.LoggerFactory;
|
||||
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
|
||||
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; // 5 seconds
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
private static final String HTTP_1_0_OK_HEX_DUMP = "48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B";
|
||||
private static final String HTTP_1_1_CONTINUE_HEX_DUMP = "48 54 54 50 2F 31 2E 31 20 31 30 30";
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
@@ -61,6 +63,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
private AtomicBoolean mConnectionFailureLogged = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* Creates an Icecast 2.4.x compatible broadcaster using HTTP 1.1 protocol. This broadcaster is
|
||||
@@ -141,7 +144,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(10000);
|
||||
mSocketConnector.setConnectTimeoutMillis(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(1000);
|
||||
|
||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastHTTPAudioBroadcaster.class);
|
||||
// loggingFilter.setMessageReceivedLogLevel(LogLevel.DEBUG);
|
||||
@@ -160,37 +164,58 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
ConnectFuture future = mSocketConnector
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
future.awaitUninterruptibly();
|
||||
mStreamingSession = future.getSession();
|
||||
}
|
||||
catch(RuntimeIoException rie)
|
||||
{
|
||||
|
||||
boolean connected;
|
||||
try {
|
||||
connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (mConnectionFailureLogged.compareAndSet(false, true)) {
|
||||
mLog.debug("HTTP connect attempt interrupted");
|
||||
}
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
future.cancel();
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (connected && future.isConnected()) {
|
||||
mStreamingSession = future.getSession();
|
||||
mConnecting.set(false);
|
||||
mConnectionFailureLogged.set(false);
|
||||
return;
|
||||
} else {
|
||||
if (mConnectionFailureLogged.compareAndSet(false, true)) {
|
||||
mLog.debug("HTTP connect attempt timed out ({} ms) or not connected",
|
||||
CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
|
||||
}
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
future.cancel();
|
||||
disconnect();
|
||||
}
|
||||
} catch (RuntimeIoException rie) {
|
||||
Throwable throwableCause = rie.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
}
|
||||
else if(throwableCause != null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLog.debug("Failed to connect", rie);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLog.debug("Failed to connect - no exception is available");
|
||||
}
|
||||
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
if (mConnectionFailureLogged.compareAndSet(false, true)) {
|
||||
if (throwableCause != null) {
|
||||
mLog.debug("Failed to connect", rie);
|
||||
} else {
|
||||
mLog.debug("Failed to connect - no exception is available");
|
||||
}
|
||||
}
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
disconnect();
|
||||
} finally {
|
||||
if (!connected()) {
|
||||
mConnecting.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,6 +235,11 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
if(!getBroadcastState().isErrorState()) {
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,6 +263,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
mHTTPHeaders = new HashMap<>();
|
||||
mHTTPHeaders.put(IcecastHeader.ACCEPT.getValue(), "*/*");
|
||||
mHTTPHeaders.put(IcecastHeader.EXPECT.getValue(), "100-continue");
|
||||
mHTTPHeaders.put(IcecastHeader.CONTENT_TYPE.getValue(), getConfiguration().getBroadcastFormat().getValue());
|
||||
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
||||
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getBase64EncodedCredentials());
|
||||
@@ -288,6 +319,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
@Override
|
||||
public void sessionClosed(IoSession session) throws Exception
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
@@ -320,9 +352,10 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
String hexDump = ((ProtocolDecoderException)throwable).getHexdump();
|
||||
|
||||
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP))
|
||||
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP) || hexDump.startsWith(HTTP_1_1_CONTINUE_HEX_DUMP))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mConnectionFailureLogged.set(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -341,15 +374,29 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP 403 protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
break;
|
||||
case 401: //Unauthorized
|
||||
if(message.toString().contains("Authentication Required"))
|
||||
{
|
||||
mLog.error("Stream [" + getStreamName() + "] - unable to connect - invalid credentials");
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP 401 protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -387,10 +434,15 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
switch(response.getStatus())
|
||||
{
|
||||
case INFORMATIONAL_CONTINUE:
|
||||
//Icecast 2.5+ signals that we can start sending audio with 100-continue
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mConnecting.set(false);
|
||||
mConnectionFailureLogged.set(false);
|
||||
break;
|
||||
case SUCCESS_OK:
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mConnecting.set(false);
|
||||
mConnectionFailureLogged.set(false);
|
||||
break;
|
||||
case CLIENT_ERROR_UNAUTHORIZED:
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
@@ -401,9 +453,16 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
disconnect();
|
||||
break;
|
||||
default:
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
/**
|
||||
* Only allow a generic error to update state if we've not already experienced a more
|
||||
* specific error. Otherwise, trailing messages will clear the more meaningful error state.
|
||||
*/
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
|
||||
disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -447,7 +506,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
|
||||
mMessage = new String(bytes);
|
||||
|
||||
Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");
|
||||
//Icecast 2.4+ responds with HTTP/1.0 and Icecast 2.5+ responds with HTTP/1.1
|
||||
Pattern pattern = Pattern.compile("HTTP/1\\.[01] (\\d{3})");
|
||||
Matcher m = pattern.matcher(mMessage);
|
||||
|
||||
if(m.find())
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
@@ -25,21 +22,16 @@ 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.icecast.IcecastConfiguration;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
|
||||
public class IcecastMetadata
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(IcecastMetadata.class);
|
||||
@@ -73,13 +65,23 @@ public class IcecastMetadata
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
|
||||
List<Alias> aliases = aliasList.getAliases(to);
|
||||
|
||||
if(aliases != null && !aliases.isEmpty())
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.codec.IcecastCodecFactory;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
||||
@@ -39,6 +40,8 @@ import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
||||
import org.apache.mina.filter.logging.LogLevel;
|
||||
import org.apache.mina.filter.logging.LoggingFilter;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -56,6 +59,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
private IoSession mStreamingSession = null;
|
||||
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private boolean mVerboseLogging = false;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
@@ -73,6 +77,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
{
|
||||
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
|
||||
|
||||
if(configuration instanceof BroadcastifyFeedConfiguration broadcastify)
|
||||
{
|
||||
mVerboseLogging = broadcastify.isVerboseLogging();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +94,6 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
if(mInlineActive)
|
||||
{
|
||||
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
|
||||
if (mInlineRemaining == -1)
|
||||
{
|
||||
mInlineRemaining = mInlineInterval;
|
||||
@@ -96,16 +104,34 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
byte[] chunk = Arrays.copyOfRange(audio, audioOffset, Math.min(audioOffset + mInlineRemaining, audio.length));
|
||||
mInlineRemaining -= chunk.length;
|
||||
audioOffset += chunk.length;
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Broadcasting audio data - sending [" + chunk.length + "/" + audio.length + "] bytes");
|
||||
}
|
||||
|
||||
mStreamingSession.write(chunk);
|
||||
if(mInlineRemaining == 0)
|
||||
{
|
||||
mInlineRemaining = mInlineInterval;
|
||||
|
||||
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Sending inline metadata: " + new String(metadata));
|
||||
}
|
||||
|
||||
mStreamingSession.write(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Broadcasting audio data [" + audio.length + "] bytes");
|
||||
}
|
||||
mStreamingSession.write(audio);
|
||||
}
|
||||
}
|
||||
@@ -132,9 +158,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
|
||||
|
||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||
loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||
mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
||||
}
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("codec",
|
||||
new ProtocolCodecFilter(new IcecastCodecFactory()));
|
||||
@@ -148,6 +177,10 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Attempting connection ...");
|
||||
}
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
try
|
||||
@@ -156,19 +189,42 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Socket created - asynchronous connect requested - entering wait period");
|
||||
}
|
||||
|
||||
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
||||
|
||||
if(connected)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Connected.");
|
||||
}
|
||||
|
||||
mStreamingSession = future.getSession();
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Not Connected. Connection attempt timeout [" + CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS + "ms] exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(RuntimeIoException rioe)
|
||||
{
|
||||
if(rioe.getCause() instanceof SocketException)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Socket error. This usually indicates sdrtrunk can't reach the server " +
|
||||
"address over the current network connection. Setting state to " +
|
||||
"NETWORK UNAVAILABLE", rioe);
|
||||
}
|
||||
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
@@ -176,12 +232,22 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
catch(UnresolvedAddressException uae)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Unresolved Address error. This means the domain name services can't resolve " +
|
||||
"the server URL to an IP address. Setting state to NETWORK UNAVAILABLE", uae);
|
||||
}
|
||||
|
||||
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Unknown error. An error occurred while attempting to connect to the server.", e);
|
||||
}
|
||||
mLog.error("Error", e);
|
||||
//Disregard ... we'll disconnect and try again
|
||||
}
|
||||
@@ -190,6 +256,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mLog.error("Throwable error caught", t);
|
||||
}
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Starting disconnect sequence since an error occurred while trying to connect.");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
mConnecting.set(false);
|
||||
}
|
||||
@@ -209,6 +280,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
if(connected() && mStreamingSession != null)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Routine disconnect requested from a connected state with a non-null streaming session");
|
||||
}
|
||||
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
else
|
||||
@@ -217,6 +293,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
//want to preserve the error state that got us here, so the user can see it.
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Disconnect requested - previous non-error state was [" + getBroadcastState() +
|
||||
"] - changing state to DISCONNECTED");
|
||||
}
|
||||
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
@@ -282,12 +364,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mInlineActive = false;
|
||||
}
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Session opened. Sending: " + sb);
|
||||
}
|
||||
|
||||
|
||||
session.write(sb.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionClosed(IoSession session) throws Exception
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Session closed. Setting connecting flag to false.");
|
||||
}
|
||||
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||
@@ -311,15 +404,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
||||
}
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Session error caught.", cause);
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object object) throws Exception
|
||||
{
|
||||
if(object instanceof String)
|
||||
if(object instanceof String message)
|
||||
{
|
||||
String message = (String) object;
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Message Received [" + message + "]");
|
||||
}
|
||||
|
||||
if(message != null && !message.trim().isEmpty())
|
||||
{
|
||||
@@ -347,7 +448,15 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
else
|
||||
{
|
||||
mLog.error("Unrecognized server response:" + message);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
|
||||
/**
|
||||
* Only allow a generic error to update state if we've not already experienced a more
|
||||
* specific error. Otherwise, trailing messages will clear the more meaningful error state.
|
||||
*/
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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"),
|
||||
PATCH_LIST("patch_list"),
|
||||
TALKER_ALIAS("talker_alias");
|
||||
|
||||
private String mHeader;
|
||||
|
||||
FormField(String header)
|
||||
{
|
||||
mHeader = header;
|
||||
}
|
||||
|
||||
public String getHeader()
|
||||
{
|
||||
return mHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This 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 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.Role;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
|
||||
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 java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
if(response == "OK")
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if (response == "No Response")
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
mLog.error("Error connecting to OpenMHz server [Server not found or not reachable]");
|
||||
}
|
||||
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.
|
||||
*/
|
||||
private boolean connected()
|
||||
{
|
||||
if(getBroadcastState() != BroadcastState.CONNECTED &&
|
||||
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(response == "OK")
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if (response == "No Response")
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
mLog.error("Error reconnecting to OpenMHz server [Server not found or not reachable]");
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.error("Error reconnecting to OpenMHz server [" + response + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
String talkerAlias = getTalkerAlias(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())
|
||||
.addPart(FormField.PATCH_LIST, patches)
|
||||
.addPart(FormField.TALKER_ALIAS, talkerAlias)
|
||||
.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 String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier != null)
|
||||
{
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
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 "[]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with talkerAlias identifier
|
||||
*/
|
||||
private static String getTalkerAlias(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof TalkerAliasIdentifier)
|
||||
{
|
||||
TalkerAliasIdentifier talkerID = (TalkerAliasIdentifier)identifier;
|
||||
|
||||
if(talkerID.isValid())
|
||||
{
|
||||
return talkerID.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
return "No Response";
|
||||
}
|
||||
|
||||
mLog.error("Exception connecting to OpenMHz server [" + e.toString() + "]");
|
||||
return "Uknown Exception";
|
||||
}
|
||||
}
|
||||
|
||||
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,234 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 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.gui.playlist.streaming.AbstractBroadcastEditor;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -17,28 +17,41 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.edac;
|
||||
package io.github.dsheirer.audio.broadcast.rdioscanner;
|
||||
|
||||
/**
|
||||
* Berlekemp Massey decoder for GF(8) using DMR Generator Polynomial
|
||||
* HTTP headers used for posting to Rdio Scanner API
|
||||
*/
|
||||
public class ReedSolomon_255_DMR extends BerlekempMassey
|
||||
public enum FormField
|
||||
{
|
||||
/**
|
||||
* DMR Reed-Solomon code is generated from a Galois Field(2^8) by the polynomial: a8 + a4 + a3 + a2 + a0.
|
||||
* In binary, this is expressed as: 100011101 which is reversed to big-endian format for this algorithm
|
||||
* See: TS 102-361-1 paragraph B.3.6 Reed-Solomon(12,9)
|
||||
*/
|
||||
public static final int[] DMR_GENERATOR_POLYNOMIAL = {1, 0, 1, 1, 1, 0, 0, 0, 1};
|
||||
AUDIO("audio"),
|
||||
AUDIO_NAME("audioName"),
|
||||
AUDIO_TYPE("audioType"),
|
||||
DATE_TIME("dateTime"),
|
||||
FREQUENCIES("frequencies"),
|
||||
FREQUENCY("frequency"),
|
||||
KEY("key"),
|
||||
PATCHES("patches"),
|
||||
SOURCE("source"),
|
||||
SOURCES("sources"),
|
||||
SYSTEM("system"),
|
||||
SYSTEM_LABEL("systemLabel"),
|
||||
TALKER_ALIAS("talkerAlias"),
|
||||
TALKGROUP_ID("talkgroup"),
|
||||
TALKGROUP_GROUP("talkgroupGroup"),
|
||||
TALKGROUP_LABEL("talkgroupLabel"),
|
||||
TALKGROUP_TAG("talkgroupTag"),
|
||||
TEST("test");
|
||||
|
||||
private String mHeader;
|
||||
|
||||
/**
|
||||
* Constructs an instance of RS(total symbols, message symbols)
|
||||
*
|
||||
* @param n total symbols
|
||||
* @param k message symbols
|
||||
*/
|
||||
public ReedSolomon_255_DMR(int n, int k)
|
||||
FormField(String header)
|
||||
{
|
||||
super(8, n, k, DMR_GENERATOR_POLYNOMIAL);
|
||||
mHeader = header;
|
||||
}
|
||||
|
||||
public String getHeader()
|
||||
{
|
||||
return mHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This 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.rdioscanner;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
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.Role;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
|
||||
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 java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Audio broadcaster to push completed audio recordings to the Rdio Scanner call upload API.
|
||||
*
|
||||
*/
|
||||
public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScannerConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RdioScannerBroadcaster.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 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 RdioScannerBroadcaster(RdioScannerConfiguration 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();
|
||||
|
||||
/**
|
||||
* Rdio Scanner API does not currently expose a test method.
|
||||
*/
|
||||
if(response != null && response.toLowerCase().startsWith("incomplete call data: no talkgroup"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Error connecting to Rdio Scanner 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.
|
||||
*
|
||||
* Rdio Scanner 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.toLowerCase().startsWith("incomplete call data: no talkgroup"))
|
||||
{
|
||||
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)
|
||||
{
|
||||
float durationSeconds = (float)(audioRecording.getRecordingLength() / 1E3f);
|
||||
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
|
||||
String talkgroup = getTo(audioRecording);
|
||||
String radioId = getFrom(audioRecording);
|
||||
String talkerAlias = getTalkerAlias(audioRecording);
|
||||
Long frequency = getFrequency(audioRecording);
|
||||
String patches = getPatches(audioRecording);
|
||||
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||
String talkgroupGroup = getTalkgroupGroup(audioRecording);
|
||||
String systemLabel = getSystemLabel(audioRecording);
|
||||
String path = audioRecording.getPath().toString();
|
||||
// Remove TEMPORARY_STREAM_FILE_SUFFIX
|
||||
String audioName = path.substring(path.substring(0, path.lastIndexOf("_")).lastIndexOf("_") + 1);
|
||||
|
||||
try
|
||||
{
|
||||
byte[] audioBytes = null;
|
||||
|
||||
try
|
||||
{
|
||||
audioBytes = Files.readAllBytes(audioRecording.getPath());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Rdio Scanner API - audio recording file not found - ignoring upload");
|
||||
}
|
||||
|
||||
if(audioBytes != null)
|
||||
{
|
||||
|
||||
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
|
||||
bodyBuilder.addPart(FormField.KEY, getBroadcastConfiguration().getApiKey())
|
||||
.addPart(FormField.SYSTEM, getBroadcastConfiguration().getSystemID())
|
||||
.addAudioName(audioName)
|
||||
.addFile(audioBytes)
|
||||
.addPart(FormField.DATE_TIME, timestampSeconds)
|
||||
.addPart(FormField.TALKGROUP_ID, talkgroup)
|
||||
.addPart(FormField.SOURCE, radioId)
|
||||
.addPart(FormField.FREQUENCY, frequency)
|
||||
.addPart(FormField.TALKER_ALIAS, talkerAlias)
|
||||
.addPart(FormField.TALKGROUP_LABEL, talkgroupLabel)
|
||||
.addPart(FormField.TALKGROUP_GROUP, talkgroupGroup)
|
||||
.addPart(FormField.SYSTEM_LABEL, systemLabel)
|
||||
.addPart(FormField.PATCHES, patches);
|
||||
|
||||
HttpRequest fileRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
||||
.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("Rdio Scanner API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("Rdio Scanner API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
}
|
||||
else
|
||||
{
|
||||
String fileResponseString = fileResponse.body();
|
||||
|
||||
if(fileResponseString.contains("Call imported successfully."))
|
||||
{
|
||||
incrementStreamedAudioCount();
|
||||
broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
else if(fileResponseString.contains("duplicate call rejected"))
|
||||
{
|
||||
//Rdio Scanner is telling us to skip audio upload - someone already uploaded it
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("Rdio Scanner API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
//Register an error for the file not found exception
|
||||
mLog.error("Rdio Scanner API - upload file not found [" +
|
||||
audioRecording.getPath().toString() + "]");
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(RdioScannerBroadcaster.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";
|
||||
}
|
||||
|
||||
private static String getTalkerAlias(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof TalkerAliasIdentifier)
|
||||
{
|
||||
TalkerAliasIdentifier talkerID = (TalkerAliasIdentifier)identifier;
|
||||
|
||||
if(talkerID.isValid())
|
||||
{
|
||||
return talkerID.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*
|
||||
*/
|
||||
private String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier != null)
|
||||
{
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
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 RdioScanner Call API service
|
||||
* @param configuration containing API key and system id
|
||||
* @return error string or null if test is successful
|
||||
*/
|
||||
public static String testConnection(RdioScannerConfiguration configuration)
|
||||
{
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_1_1)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
|
||||
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
|
||||
bodyBuilder.addPart(FormField.KEY, configuration.getApiKey())
|
||||
.addPart(FormField.SYSTEM, configuration.getSystemID())
|
||||
.addPart(FormField.TEST, 1);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(configuration.getHost()))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.header(HttpHeaders.ACCEPT, "*/*")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponse<String> response = httpClient.send(request, responseHandler);
|
||||
String responseBody = response.body();
|
||||
return (responseBody != null ? responseBody : "(no response)") + " Status Code:" + response.statusCode();
|
||||
}
|
||||
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 ...");
|
||||
|
||||
RdioScannerConfiguration config = new RdioScannerConfiguration();
|
||||
config.setHost("https://api.RdioScanner.com/call-upload-dev");
|
||||
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
|
||||
config.setSystemID(11);
|
||||
|
||||
String response = testConnection(config);
|
||||
|
||||
if(response == null)
|
||||
{
|
||||
mLog.debug("Test Successful!");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(response.contains("1 Invalid-API-Key"))
|
||||
{
|
||||
mLog.error("Invalid API Key");
|
||||
}
|
||||
else if(response.contains("1 API-Key-Access-Denied"))
|
||||
{
|
||||
mLog.error("System ID not valid for API Key");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.debug("Response: " + response);
|
||||
}
|
||||
}
|
||||
|
||||
mLog.debug("Finished!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.rdioscanner;
|
||||
|
||||
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 RdioScanner call event
|
||||
*/
|
||||
public class RdioScannerBuilder
|
||||
{
|
||||
private static final String DASH_DASH = "--";
|
||||
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
||||
private List<Part> mParts = new ArrayList<>();
|
||||
private byte[] audioBytes = null;
|
||||
private String audioName = null;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public RdioScannerBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the static multi-part boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
{
|
||||
return BOUNDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Audio file name part to the call
|
||||
*/
|
||||
public RdioScannerBuilder addAudioName(String value)
|
||||
{
|
||||
audioName = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Audio bytes part to the call
|
||||
*/
|
||||
public RdioScannerBuilder addFile(byte[] value)
|
||||
{
|
||||
audioBytes = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a string part to the call
|
||||
*/
|
||||
public RdioScannerBuilder 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 RdioScannerBuilder 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; filename=\"").append(audioName);
|
||||
sb.append("\"; name=\"").append("audio").append("\"\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,135 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.audio.broadcast.rdioscanner;
|
||||
|
||||
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.IntegerProperty;
|
||||
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 RdioScannerConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
private IntegerProperty mSystemID = new SimpleIntegerProperty();
|
||||
private StringProperty mApiKey = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Constructor for faster jackson
|
||||
*/
|
||||
public RdioScannerConfiguration()
|
||||
{
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
* @param format to use for audio recording (MP3)
|
||||
*/
|
||||
public RdioScannerConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
//The parent class binds this property, so we unbind it and rebind it here
|
||||
mValid.unbind();
|
||||
mValid.bind(Bindings.and(Bindings.and(Bindings.greaterThan(mSystemID, 0), Bindings.isNotNull(mApiKey)),
|
||||
Bindings.isNotNull(mHost)));
|
||||
|
||||
if(mHost.getValue() == null || mHost.getValue().isEmpty())
|
||||
{
|
||||
mHost.set("http://localhost");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* System ID as a property
|
||||
*/
|
||||
public IntegerProperty systemIDProperty()
|
||||
{
|
||||
return mSystemID;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ID as provided by RdioScanner.com
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system_id")
|
||||
public int getSystemID()
|
||||
{
|
||||
return mSystemID.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system ID provided by RdioScanner.com
|
||||
*/
|
||||
public void setSystemID(int systemID)
|
||||
{
|
||||
mSystemID.set(systemID);
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.RDIOSCANNER_CALL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
RdioScannerConfiguration copy = new RdioScannerConfiguration();
|
||||
copy.setSystemID(getSystemID());
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -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.rdioscanner;
|
||||
|
||||
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 RdioScannerFeedConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RdioScannerFeedConfiguration.class);
|
||||
|
||||
private int mFeedID;
|
||||
|
||||
public RdioScannerFeedConfiguration()
|
||||
{
|
||||
//No-arg constructor for JAXB
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* RdioScanner configuration for Icecast 2.3.2 compatible servers
|
||||
*
|
||||
* @param format of output audio (MP3)
|
||||
*/
|
||||
public RdioScannerFeedConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
setBitRate(16);
|
||||
setChannels(1);
|
||||
setSampleRate(8000);
|
||||
setInline(true);
|
||||
}
|
||||
|
||||
public static RdioScannerFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
|
||||
{
|
||||
RdioScannerFeedConfiguration config = new RdioScannerFeedConfiguration(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()
|
||||
{
|
||||
RdioScannerFeedConfiguration copy = new RdioScannerFeedConfiguration(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());
|
||||
|
||||
//RdioScanner 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.RDIOSCANNER_CALL;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "feed_id")
|
||||
public int getFeedID()
|
||||
{
|
||||
return mFeedID;
|
||||
}
|
||||
|
||||
public void setFeedID(int feedID)
|
||||
{
|
||||
mFeedID = feedID;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -29,6 +29,17 @@ import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
@@ -41,17 +52,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
|
||||
@@ -212,10 +212,20 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
|
||||
List<Alias> aliases = aliasList.getAliases(to);
|
||||
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
}
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -49,6 +49,15 @@ import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
@@ -59,15 +68,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcaster implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioStreamingBroadcaster.class);
|
||||
@@ -303,13 +303,23 @@ public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcas
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sbTitle2.append("TO:").append(to);
|
||||
|
||||
List<Alias> aliases = aliasList.getAliases(to);
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
sbTitle2.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
sbTitle2.append("TO:").append(to);
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -35,16 +35,22 @@ import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import jmbe.iface.IAudioCodec;
|
||||
import jmbe.iface.IAudioCodecLibrary;
|
||||
import jmbe.iface.IAudioCodecLibraryV2;
|
||||
import jmbe.iface.IAudioCodecV2;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base audio module for protocols that use the JMBE audio codec.
|
||||
*/
|
||||
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
|
||||
ISquelchStateListener
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JmbeAudioModule.class);
|
||||
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
|
||||
private static final List<String> mLibraryLoadStatusLogged = new ArrayList<>();
|
||||
private IAudioCodec mAudioCodec;
|
||||
@@ -53,18 +59,60 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
|
||||
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
|
||||
{
|
||||
super(aliasList, timeslot, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
|
||||
super(aliasList, timeslot, DEFAULT_MAX_SEGMENT_LENGTH_MILLIS);
|
||||
mUserPreferences = userPreferences;
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
loadConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of features supported by the JMBE audio library. Note: features are only supported in Version 2.0+
|
||||
* @return list of features or an empty list if the current library version doesn't support features.
|
||||
*/
|
||||
public List<String> getFeatures()
|
||||
{
|
||||
if(mAudioCodec instanceof IAudioCodecV2 v2)
|
||||
{
|
||||
return v2.getFeatures();
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JMBE feature
|
||||
* @param name of the feature
|
||||
* @param value for the feature
|
||||
*/
|
||||
public void setFeature(String name, Object value)
|
||||
{
|
||||
if(mAudioCodec instanceof IAudioCodecV2 v2)
|
||||
{
|
||||
v2.setFeature(name, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Feature [" + name + "] is not supported by the current JMBE audio codec library version");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeAudioSegment()
|
||||
{
|
||||
super.closeAudioSegment();
|
||||
|
||||
//Reset the audio codec to clear any leftover frame data from the previous call.
|
||||
if(mAudioCodec != null)
|
||||
{
|
||||
mAudioCodec.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
mAudioCodec = null;
|
||||
}
|
||||
|
||||
public IAudioCodec getAudioCodec()
|
||||
@@ -121,7 +169,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.info("Loading JMBE library from [" + path + "]");
|
||||
LOG.info("Loading JMBE library from [" + path + "]");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -129,13 +177,32 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()},
|
||||
this.getClass().getClassLoader());
|
||||
|
||||
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
|
||||
try
|
||||
{
|
||||
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibraryV2", true, childClassLoader);
|
||||
}
|
||||
catch(ClassNotFoundException legacyLibrary)
|
||||
{
|
||||
try
|
||||
{
|
||||
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
|
||||
}
|
||||
catch(ClassNotFoundException missingLibrary)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
LOG.error("Couldn't load JMBE audio conversion library from [" + path +
|
||||
"] - neither V2 nor V1 library class was found");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY + getCodecName()))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
|
||||
LOG.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY + getCodecName());
|
||||
}
|
||||
}
|
||||
@@ -143,18 +210,11 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
catch(ClassNotFoundException e1)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - class not found");
|
||||
LOG.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,11 +229,23 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
if((library.getMajorVersion() == 1 && library.getMinorVersion() >= 0 &&
|
||||
library.getBuildVersion() >= 0) || library.getMajorVersion() >= 1)
|
||||
{
|
||||
audioConverter = library.getAudioConverter(getCodecName());
|
||||
audioConverter = library instanceof IAudioCodecLibraryV2 libraryV2 ?
|
||||
libraryV2.getAudioConverterV2(getCodecName()) :
|
||||
library.getAudioConverter(getCodecName());
|
||||
|
||||
String codecStatusKey = JMBE_AUDIO_LIBRARY + ":codec:" + getCodecName();
|
||||
if(!mLibraryLoadStatusLogged.contains(codecStatusKey))
|
||||
{
|
||||
LOG.info("JMBE codec [" + getCodecName() + "] class [" +
|
||||
audioConverter.getClass().getName() + "] features [" +
|
||||
(audioConverter instanceof IAudioCodecV2 v2 ?
|
||||
v2.getFeatures() : Collections.emptyList()) + "]");
|
||||
mLibraryLoadStatusLogged.add(codecStatusKey);
|
||||
}
|
||||
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.info("JMBE audio conversion library loaded: " + library.getVersion());
|
||||
LOG.info("JMBE audio conversion library loaded: " + library.getVersion());
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -181,7 +253,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.warn("JMBE library version 1.0.0 or higher is required - found: " + library.getVersion());
|
||||
LOG.warn("JMBE library version 1.0.0 or higher is required - found: " + library.getVersion());
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -190,7 +262,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.info("JMBE audio conversion library NOT FOUND");
|
||||
LOG.info("JMBE audio conversion library NOT FOUND");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -199,7 +271,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - invocation target exception", ite);
|
||||
LOG.error("Couldn't load JMBE audio conversion library - invocation target exception", ite);
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -207,7 +279,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - instantiation exception", e1);
|
||||
LOG.error("Couldn't load JMBE audio conversion library - instantiation exception", e1);
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -215,7 +287,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - security restrictions");
|
||||
LOG.error("Couldn't load JMBE audio conversion library - security restrictions");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -223,7 +295,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - no such method exception");
|
||||
LOG.error("Couldn't load JMBE audio conversion library - no such method exception");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -232,7 +304,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.warn("JMBE audio library path is NOT SET in your User Preferences.");
|
||||
LOG.warn("JMBE audio library path is NOT SET in your User Preferences.");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -26,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.annotation.JsonRootName;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.encryption.EncryptionKey;
|
||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -36,7 +35,7 @@ import java.util.List;
|
||||
* radio identifiers.
|
||||
*/
|
||||
@JsonRootName("mbe_call")
|
||||
@JsonPropertyOrder({"protocol", "call_type", "from", "to", "encrypted", "frames"})
|
||||
@JsonPropertyOrder({"protocol", "version", "call_type", "from", "to", "encrypted", "system", "site", "frames"})
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class MBECallSequence
|
||||
{
|
||||
@@ -77,6 +76,15 @@ public class MBECallSequence
|
||||
mProtocol = protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version number for the mbe file structure.
|
||||
* Should be incremented only on breaking structural changes, not minor additions.
|
||||
*/
|
||||
@JsonProperty("version")
|
||||
public Integer getVersion()
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this sequences contains any audio frames
|
||||
@@ -279,10 +287,18 @@ public class MBECallSequence
|
||||
}
|
||||
else
|
||||
{
|
||||
mVoiceFrames.add(new VoiceFrame(timestamp, frame));
|
||||
add(new VoiceFrame(timestamp, frame));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the voice frame to the sequence
|
||||
*/
|
||||
public void add(VoiceFrame voiceFrame)
|
||||
{
|
||||
mVoiceFrames.add(voiceFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an encrypted audio frame to this call sequence
|
||||
*
|
||||
@@ -308,6 +324,6 @@ public class MBECallSequence
|
||||
*/
|
||||
public void addEncryptedVoiceFrame(long timestamp, String frame, int algorithm, int keyid, String messageIndicator)
|
||||
{
|
||||
mVoiceFrames.add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
|
||||
add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -25,7 +25,6 @@ import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder;
|
||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
@@ -125,7 +124,7 @@ public class MBECallSequenceConverter
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
|
||||
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath, audioSegment.getIdentifierCollection());
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
@@ -25,7 +22,6 @@ package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.p25.audio;
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
@@ -27,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonRootName;
|
||||
*/
|
||||
@JsonRootName(value = "frame")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonPropertyOrder({"encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"})
|
||||
@JsonPropertyOrder({"tag", "encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"})
|
||||
public class VoiceFrame
|
||||
{
|
||||
private long mTimestamp;
|
||||
@@ -35,6 +38,7 @@ public class VoiceFrame
|
||||
private Integer mAlgorithm;
|
||||
private Integer mKeyId;
|
||||
private String mMessageIndicator;
|
||||
private String mTag;
|
||||
|
||||
public VoiceFrame()
|
||||
{
|
||||
@@ -70,6 +74,24 @@ public class VoiceFrame
|
||||
mMessageIndicator = messageIndicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag value for the voice frame
|
||||
*/
|
||||
@JsonProperty("tag")
|
||||
public String getTag()
|
||||
{
|
||||
return mTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional tag value for the frame
|
||||
* @param tag to set
|
||||
*/
|
||||
public void setTag(String tag)
|
||||
{
|
||||
mTag = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp the frame was transmitted
|
||||
*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user