Compare commits

...

124 Commits

Author SHA1 Message Date
Denny Sheirer
0492f19a95 Merge pull request #1770 from DSheirer/1769-release-0.6.0
#1769 Release version 0.6.0
2023-12-16 06:48:10 -05:00
Dennis Sheirer
098b039154 #1769 Release version 0.6.0 2023-12-16 06:44:15 -05:00
Denny Sheirer
5c6e6059f8 Merge pull request #1754 from tadscottsmith/openmhz-broadcasting
Add OpenMHz Broadcasting Support
2023-12-16 06:14:14 -05:00
Denny Sheirer
72d45543df Merge pull request #1768 from szechyjs/master
Fix osx nightly architectures
2023-12-16 05:21:06 -05:00
Jared Szechy
e96fc46315 Fix osx nightly architectures
Resolves #1766
2023-12-15 13:59:35 -05:00
Denny Sheirer
f0cfbb80dd Merge pull request #1764 from DSheirer/1760-dmr-vendor-ids
#1760 Update DMR vendor enumeration.
2023-12-14 07:50:12 -05:00
Dennis Sheirer
a199f03a60 #1760 Updates DMR vendor enumeration. 2023-12-14 07:47:45 -05:00
Denny Sheirer
526c9609ef Merge pull request #1762 from DSheirer/1761-hytera-dmr-talker-alias-and-status
#1761 DMR Hytera Tier 3 UTF-16 Talker Alias & Traffic Channel Talker Status CSBK
2023-12-14 07:13:36 -05:00
Dennis Sheirer
ec9ceeefb3 #1761 DMR decoder supports Hytera Tier 3 UTF-16 Talker Alias & new Traffic Channel Talker Status CSBKO 47. Implements a tracking feature to detect when vendors are playing games using non-standard CSBK CRC mask values by tracking when a non-standard mask value is consistently detected and applying that mask when the initial CRC check fails. 2023-12-14 07:10:30 -05:00
Denny Sheirer
b6c6505824 Merge pull request #1759 from DSheirer/1758-l3h-talker-alias-gps
#1758 P25 Phase 2 L3Harris Talker Alias and GPS Report.
2023-12-10 09:52:48 -05:00
Dennis Sheirer
93c61c48f9 #1758 Adds P25 Phase 2 L3Harris talker alias and GPS report. 2023-12-10 09:45:49 -05:00
Denny Sheirer
9b89e80bb5 Merge pull request #1756 from DSheirer/1753-sdrplay-remove-unsupported-ifagc-modes
#1753 SDRPlay - Remove Deprecated AGC Modes
2023-12-01 03:43:16 -05:00
Dennis Sheirer
e0ecaa6e08 #1753 Changes tuner editors to use toggle button for IF AGC mode and updates enum lookup to always return either ENABLE or DISABLE. Deprecates 3x enum values as no longer supported. 2023-12-01 03:39:57 -05:00
Denny Sheirer
a7c89b5d1a Merge pull request #1755 from DSheirer/1746-dmr-simplex-repeater
#1746 DMR Decoder - Simplex Repeater and DCDM Calls
2023-12-01 03:02:13 -05:00
Dennis Sheirer
834846d339 #1746 DMR Decoder now supports simplex repeater and DMDC modes of operation. 2023-12-01 02:58:27 -05:00
tadscottsmith
45c8bb73fa Authorization features. 2023-11-29 14:40:45 -06:00
Denny Sheirer
d2580f8a99 Merge pull request #1751 from DSheirer/1188-fully-qualified-radio-identiifer-aliasing
#1188 APCO25 (ISSI) Fully Qualified Radio & Fully Qualified Talkgroup Aliasing
2023-11-29 02:34:43 -05:00
Dennis Sheirer
804c2e6359 #1188 Add support for P25 fully qualified radio and talkgroup identifiers to aliases and alias lists. Resolve issue with ESN alias lookups. 2023-11-28 04:32:11 -05:00
Denny Sheirer
3f2ec612d5 Merge pull request #1745 from DSheirer/1743-cpu-load-info-not-available
#1743 CPU Status Indicator & Show/Hide Status Panel & Now Playing Channel Detail Tabs
2023-11-18 03:29:39 -05:00
Dennis Sheirer
8bead998f5 #1743 Resolves CPU status indicator strobing and adds control to show/hide status panel and now playing channel details tabs. 2023-11-18 03:26:21 -05:00
tadscottsmith
2ecdaba183 Initial commit. (#4) 2023-11-15 17:23:09 -06:00
Denny Sheirer
82cb10a03d Merge pull request #1738 from hans109h/patch-1
Update JavaFxWindowManager.java for #1619
2023-11-15 05:01:16 -05:00
Denny Sheirer
8717ce38eb Merge pull request #1741 from DSheirer/1735-radio-ref-api-invalid-county-id-temp-site
#1735 Radio Reference API - Temporary Site With County ID 99999 - Unknown Location
2023-11-15 04:55:02 -05:00
sheirerd
5858a85a50 #1735 Adds support for county ID with sentinel value of 99999 to indicate county unknown for sites of a system. 2023-11-15 04:51:07 -05:00
hans109h
65d16da997 Update JavaFxWindowManager.java for #1619
Fixes #1619
2023-11-14 11:10:16 -06:00
Denny Sheirer
b1b5ae5fd2 Merge pull request #1734 from DSheirer/1733-management-factory-module-missing
#1733 Java Module Missing (java.management)
2023-11-14 06:07:11 -05:00
sheirerd
b5ec6d8877 #1733 Updates gradle build script to explicitly include the java.management module. 2023-11-14 06:04:36 -05:00
Denny Sheirer
8599aebed9 Merge pull request #1730 from CrimeIsDown/rdio-scanner-http-fix
Fix Rdio Scanner API call to comply with HTTP standards
2023-11-12 07:36:57 -05:00
Denny Sheirer
2b9e753033 Merge pull request #1732 from DSheirer/1731-application-status-bar
#1731 Application Status Bar (cpu/memory/disk space).
2023-11-12 07:21:43 -05:00
Dennis Sheirer
dba50744a9 #1731 Adds application status bar with progress indicators for CPU, memory and disk space (event logs / recordings) usage. Updates user preferences to include threshold settings in the directories editor for max size for event logs and recordings. 2023-11-12 07:19:10 -05:00
Eric Tendian
490569e023 Fix Rdio Scanner API call to comply with HTTP standards 2023-11-12 00:43:48 -06:00
Denny Sheirer
e79f2af759 Merge pull request #1728 from DSheirer/1619-javafx-windows-reappear-iconified
#1619 JavaFX Windows Restore to Previous Size/Location On View Request
2023-11-11 06:47:27 -05:00
Dennis Sheirer
ec7ee1ce91 #1619 JavaFX windows restore to previous size on view. 2023-11-11 06:45:23 -05:00
Denny Sheirer
672207c27c Merge pull request #1727 from DSheirer/1726-application-root-dir-on-startup
#1726 Don't Auto-Create App Root In User Home Directory Once Changed
2023-11-11 06:28:40 -05:00
Dennis Sheirer
a1a6b3e988 #1726 On startup, application no longer auto-creates SDRTrunk folder and system properties in user's home directory when user has changed the application root in the user preferences. 2023-11-11 06:17:04 -05:00
Denny Sheirer
2347135614 Merge pull request #1725 from DSheirer/1554-view-logs-file-explorer
#1554 App View Menu Cleanup & File Explorer Location
2023-11-11 05:33:13 -05:00
Dennis Sheirer
93438ddd97 #1554 Updates application view menu to show user-selected file locations. Enhances menu items with icons. Changes icon for playlist editor tab. 2023-11-11 05:28:36 -05:00
Eric Tendian
ae31c41643 #1635 Add a special county agency to show all county freqs without an agency (#1724)
#1635 Playlist Editor Add a special county agency to show all county freqs without an agency.
Merge pull request #1724
2023-11-11 04:39:17 -05:00
Denny Sheirer
16aed8741d Merge pull request #1721 from DSheirer/1720-version-0.6.0-beta-5
#1720 Updates version to 0.6.0 Beta 5
2023-11-10 05:43:31 -05:00
Dennis Sheirer
e878ca0c7f #1720 Updates version to 0.6.0 Beta 5 2023-11-10 05:42:41 -05:00
Denny Sheirer
ca61dd2543 Merge pull request #1719 from DSheirer/1619-java-fx-windows-open-minimized
#1619 JavaFX Windows Show Minimized
2023-11-10 05:34:09 -05:00
Dennis Sheirer
6187253589 #1619 Resolves issue where JavaFX window size and placement might show minimized after application is started and an initial JavaFX window is opened and then closed, where it impacted subsequent JavaFX window openings. Corrects issue where JavaFX stage monitors were not being registered correctly, so that they could be deregistered prior to application shutdown. 2023-11-10 05:31:05 -05:00
Denny Sheirer
b51c80742f Merge pull request #1717 from DSheirer/1681-audio-recordings-incorrect-system-and-site
#1681 Audio Recorder Mixing Up System and Site When Decoding Multiple Systems
2023-11-10 04:51:54 -05:00
Dennis Sheirer
15352b9216 #1681 Resolves issue where audio recordings have incorrect system and site labels in metadata. Adds synchronized to channel processing manager's start channel request method to ensure that each channel is configured and started sequentially to avoid chance that simultaneous traffic channel grants occurring across two separate systems get inter-mingled on configuration and startup. 2023-11-10 04:34:45 -05:00
Denny Sheirer
a848b21ae8 Merge pull request #1716 from DSheirer/1715-rtl-tuner-v3-v4-mixed-tuning-issue
#1715 RTL2832 Tuner Issue When Using V4 Dongle With Other R820T Tuners
2023-11-10 03:49:53 -05:00
Dennis Sheirer
279aca7287 #1715 Resolves issue of using both legacy and new V4 RTL2832 dongles at same time causes errant tuning behavior on the first tuner that gets initialized. Removed static qualifier from VCO reference value so that each tuner is allowed to initialize correctly. 2023-11-10 03:47:01 -05:00
Denny Sheirer
88879c956a Merge pull request #1714 from DSheirer/732-p25p2-missing-from-identifier-call-event
#732 P25 Phase 2 - FROM ID Updated For Call Event Logging
2023-11-09 04:51:01 -05:00
Dennis Sheirer
729b7e27a8 #732 Resolves P25P2 call events sometimes missing FROM radio ID even though the ID was decoded. 2023-11-09 04:48:42 -05:00
Denny Sheirer
feaed168ae Merge pull request #1713 from DSheirer/967-bulk-alias-editor-set-recordable
#967 Multi-Alias Editor Supports Changing Record Audio Status
2023-11-09 04:34:28 -05:00
Dennis Sheirer
50edb18db3 967 Updates bulk alias editor to support editing alias recordable status. 2023-11-09 04:30:25 -05:00
Denny Sheirer
7302e2806d Merge pull request #1712 from DSheirer/968-alias-editor-clear-overlap-state
#968 Playlist Alias Editor - Clear Overlap Error Warning Once Resolved
2023-11-09 04:15:17 -05:00
Dennis Sheirer
8845306b4a #968 Resolves issue with clearing alias ID overlap state when the user edits the alias to correct the overlap. 2023-11-09 04:12:46 -05:00
Denny Sheirer
09e19e1486 Merge pull request #1708 from tadscottsmith/rdio-scanner-filename
Explicitly send filename to Rdio-Scanner.
2023-11-07 04:12:14 -05:00
Denny Sheirer
0f5478925c Update README.md 2023-11-07 04:01:40 -05:00
Denny Sheirer
09dce0b0d1 Merge pull request #1710 from DSheirer/1709-nightly-build-timestamp-info
#1709 Nightly Release Build Timestamp Info
2023-11-07 03:32:42 -05:00
Dennis Sheirer
ad98b1cb5e #1709 Adds build timestamp info to nightly releases. 2023-11-07 03:31:31 -05:00
tadscottsmith
8effd97749 Explicitly send filename to Rdio-Scanner. 2023-11-06 10:54:41 -06:00
Denny Sheirer
9dbed72c32 Merge pull request #1707 from DSheirer/1706-nightly-release
#1706 Github Action For Nightly Releases
2023-11-06 05:06:19 -05:00
Dennis Sheirer
b4dd3692b8 #1706 Github action to create nightly application releases. 2023-11-06 05:05:40 -05:00
Denny Sheirer
030cbb856c Update README.md 2023-11-06 04:36:59 -05:00
Denny Sheirer
511101e9b7 Merge pull request #1705 from DSheirer/1704-harris-patch-group-formatting
#1704 Patch Group Formatting
2023-11-06 02:52:49 -05:00
Dennis Sheirer
60dd2b2df7 #1704 Updates patch group format for compatibility. 2023-11-06 02:50:46 -05:00
Denny Sheirer
218dc94e02 Merge pull request #1701 from DSheirer/1644-sdrplay-api-3-12
#1644 SDRPlay API Version 3.12 For Mac OSX
2023-11-05 07:22:04 -05:00
Dennis Sheirer
2440f578b1 #1644 Update SDRPlay API for version 3.12 compatability 2023-11-05 07:20:29 -05:00
Denny Sheirer
09c87237cb Merge pull request #1700 from DSheirer/1651-gradle-build-version-naming
#1651 Version 0.6.0 Beta 4 and Version Naming Conventions
2023-11-05 07:10:00 -05:00
Dennis Sheirer
3d974ddd91 #1651 Increment version to 0.6.0 Beta 4 and add version naming conventions comments. 2023-11-05 07:08:18 -05:00
Denny Sheirer
1489c970ba Merge pull request #1699 from DSheirer/1653-channel-edit-with-filtering
#1653 Playlist Channel Edits With Applied Filter Produces Error
2023-11-05 06:58:36 -05:00
Dennis Sheirer
9e0afca3d1 #1653 Playlist channel edits with applied filter doesn't produce error 2023-11-05 06:56:49 -05:00
Denny Sheirer
99eda4013e Merge pull request #1698 from DSheirer/1654-playlist-editor-channel-frequency-sort
#1654 Playlist Channel Editor Frequency Column Sort
2023-11-05 06:32:23 -05:00
Dennis Sheirer
a62f81f890 #1654 Playlist channel editor sorts by frequency numerically (vs lexicographically) 2023-11-05 06:29:23 -05:00
Denny Sheirer
66f0731731 Merge pull request #1697 from DSheirer/1665-refresh-broadcastify-streams
#1665 Refresh Broadcastify Streams In Playlist Editor
2023-11-05 06:00:48 -05:00
Dennis Sheirer
dc239801c6 #1665 Add option to refresh available streams from broadcastify in playlist editor's streaming tab. 2023-11-05 05:58:32 -05:00
Denny Sheirer
d4ba0988bc Merge pull request #1696 from DSheirer/1673-playlist-editor-broadcastify-system-id
#1673 Broadcastify Call Streaming Config Editor's default value of 0 …
2023-11-05 05:24:03 -05:00
Dennis Sheirer
ad0f953b2a #1673 Broadcastify Call Streaming Config Editor's default value of 0 causes field to be uneditable. 2023-11-05 05:20:15 -05:00
Denny Sheirer
e1bc9eabd6 Merge pull request #1695 from DSheirer/1694-sdrplay-rsp-tuner-memory-leaks
#1694 SDRPlay RSP Memory Leak & Audio Playback Delays
2023-11-05 04:26:48 -05:00
Dennis Sheirer
d3dc1a8667 #1694 Resolves memory leak in SDRPlay RSP tuner implementation. Sample stream and event callback functions were using a global arena for memory allocation where memory was not being de-allocated. Changed to on-demand arena creation and deterministic memory de-allocation. Also, when using a reduced sample rate from the RSP tuner, the dispatcher was not keeping up with the inbound sample buffer rate, resulting in a buildup of RSP native sample buffers over time and causing growing audio delays with broken/stilted audio during playback. 2023-11-05 04:24:19 -05:00
Denny Sheirer
bf278120de Merge pull request #1692 from DSheirer/DSheirer-patch-1
Update README.md
2023-11-04 07:41:29 -04:00
Denny Sheirer
2f52f6e009 Update README.md 2023-11-04 07:41:12 -04:00
Denny Sheirer
02363c3113 Merge pull request #1690 from DSheirer/1689-harris-patch-group-prefixes
#1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris Patch Groups
2023-11-04 07:13:10 -04:00
Dennis Sheirer
cfaac0fcbc #1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris patch groups. 2023-11-04 07:12:29 -04:00
Denny Sheirer
bee0405e00 Merge pull request #1688 from DSheirer/1687-rtl2832-fc0013
Support For RTL2832 With Embedded FC0013 Tuner
2023-11-04 06:54:55 -04:00
Dennis Sheirer
50bdf0ee81 #1687 Adds support for RTL2832 with embedded FC0013 tuner. 2023-11-04 06:52:37 -04:00
Denny Sheirer
a27ddbe0d0 Merge pull request #1679 from DSheirer/1643-r828d-v4-tuner
#1643 RTL2832/R828D aka RTL-SDR V4 Dongle & Bias-T Support
2023-10-22 08:40:33 -04:00
Dennis Sheirer
0d652b9153 #1643 Adds support for RTL2832 with embedded R828D tuner and RTL-SDR.com V4 dongles. Introduces Bias-T support for all RTL-2832 dongles. 2023-10-22 08:38:16 -04:00
Denny Sheirer
9468fd189b Merge pull request #1677 from DSheirer/1607-harris-p25-patch-groups
#1607 P25 P1/P2 L3Harris Patch Group Support
2023-10-21 07:00:41 -04:00
sheirerd
215737ad9b #1607 Add support for L3Harris dynamic regroup/patch messages and events. Includes parsing for L3Harris unknown opcodes 10, 42 and 43. 2023-10-21 06:58:11 -04:00
Denny Sheirer
123c25b142 Merge pull request #1672 from DSheirer/1650-fm-decoder-7_5-bandwidth
#1650 Adds 7.5 kHz channel bandwidth option for FM channels.
2023-10-15 09:22:48 -04:00
Dennis Sheirer
af56198f0e #1650 Adds 7.5 kHz channel bandwidth option for FM channels. 2023-10-15 09:21:38 -04:00
Denny Sheirer
3c73fdfd6e Merge pull request #1671 from DSheirer/1670-dmr-cap-plus-missing-call-audio
#1670 Missing Audio Segments - DMR Cap+
2023-10-15 08:56:48 -04:00
Dennis Sheirer
b391ebbb5f #1670 Resolves missing audio segments for some DMR Cap+ call events. 2023-10-15 08:55:35 -04:00
Denny Sheirer
bad5a1f9f0 Merge pull request #1669 from DSheirer/1668-message-viewer-p25-support
#1668 Message Recording Viewer - P25P1 Support
2023-10-15 07:17:06 -04:00
Dennis Sheirer
a7470b463f #1668 Updates message recording (.bits) viewer to support P25 Phase 1. 2023-10-15 07:14:53 -04:00
Denny Sheirer
d6c706cb35 Merge pull request #1667 from DSheirer/1666-p25-moto-unit-gps
#1666 P25 Motorola Unit GPS Parsing & Plot to Map
2023-10-14 08:16:48 -04:00
Dennis Sheirer
a0d6950c75 #1666 P25 Motorola unit self-reported GPS location parsing and plotting to map. 2023-10-14 08:15:35 -04:00
Denny Sheirer
2495200b36 Merge pull request #1663 from DSheirer/1648-dmr-lrrp-not-plotting-map
#1648 DMR Embedded GPS & LRRP Plotting
2023-10-05 03:32:08 -04:00
Dennis Sheirer
bfe7ae0b1b #1648 Resolves issue with plotting DMR embedded GPS and LRRP positions. 2023-10-05 03:31:17 -04:00
Denny Sheirer
4912abbf07 Merge pull request #1662 from DSheirer/1661-dmr-gps-location-parsing-error
#1661 DMR Talker GPS Lat/Long Values Incorrect
2023-10-05 03:21:24 -04:00
Dennis Sheirer
2f2998d02e #1661 Resolves issue with parsing of DMR embedded GPS latitude and longitude values. 2023-10-05 03:20:17 -04:00
Dennis Sheirer
c96349fa1c Updates version to 0.6.0 Beta 2 2023-08-31 07:14:58 -04:00
Denny Sheirer
f640576cc3 Merge pull request #1646 from DSheirer/1637-dmr-cap-plus-and-max-trunking
#1637 DMR Cap+/CapMax Channel Frequency Map / Trunking Issue
2023-08-31 07:10:21 -04:00
Dennis Sheirer
77d725a488 #1637 Resolves issue with DMR Cap+/CapMax trunking channel to frequency map. Updates the DMR channel map to use LCN instead of LSN and aligns standard DMR with Tier-III DMR for channel identification.
Warning: breaking change.  Users will need to either recreate their DMR channel to frequency maps or simply delete and reload the channel from Radio Reference to correct the channel to frequency map.

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

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up JDK 20
uses: actions/setup-java@v3
with:

47
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
# 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 20
uses: actions/setup-java@v3
with:
java-version: '20'
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: pyTooling/Actions/releaser@main
with:
tag: nightly
rm: false
token: ${{ secrets.GITHUB_TOKEN }}
files: |
build/image/*.zip
build/image/build_info.txt

2
.idea/misc.xml generated
View File

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

View File

@@ -1,8 +1,10 @@
![Gradle Build](https://github.com/dsheirer/sdrtrunk/actions/workflows/gradle.yml/badge.svg)
![Nightly Release](https://github.com/dsheirer/sdrtrunk/actions/workflows/nightly.yml/badge.svg)
# 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,6 +20,13 @@ 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)
* **CPU:** 4-core

View File

@@ -53,7 +53,8 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.6.0-alpha7'
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
version = project.findProperty('projectVersion')
//Java 20 is required for this version of the Project Panama preview/incubator feature
java {
@@ -210,8 +211,8 @@ def jdk_download_base = "https://download.bell-sw.com/java/20.0.1+10/bellsoft-jd
def jdk_download_suffix = "-full.tar.gz"
def jdk_download_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"
/**
@@ -228,7 +229,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
//jdk.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.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
//Use auto-detected modules and 'add' any specified modules.
rt.extension.additive.set(true)
@@ -339,7 +341,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, [])
}
@@ -351,4 +353,3 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
configure(rt, jvmArgsLinux);
}

31
gradle.properties Normal file
View File

@@ -0,0 +1,31 @@
#
# ******************************************************************************
# 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/>
# *****************************************************************************
#
# 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.0

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,37 +19,31 @@
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.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.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 +58,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 +79,18 @@ public class AliasFactory
Min copyMin = new Min();
copyMin.setMin(originalMin.getMin());
return copyMin;
case P25_FULLY_QUALIFIED_RADIO_ID:
P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id;
P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(),
originalP25.getSystem(), originalP25.getValue());
copyP25.setOverlap(originalP25.overlapProperty().get());
return copyP25;
case P25_FULLY_QUALIFIED_TALKGROUP:
P25FullyQualifiedTalkgroup originalFqt = (P25FullyQualifiedTalkgroup) id;
P25FullyQualifiedTalkgroup copyFqt = new P25FullyQualifiedTalkgroup(originalFqt.getWacn(),
originalFqt.getSystem(), originalFqt.getValue());
copyFqt.setOverlap(originalFqt.overlapProperty().get());
return copyFqt;
case PRIORITY:
Priority originalPriority = (Priority)id;
Priority copyPriority = new Priority();
@@ -187,6 +198,7 @@ public class AliasFactory
copy.setGroup(original.getGroup());
copy.setColor(original.getColor());
copy.setIconName(original.getIconName());
copy.setRecordable(original.isRecordable());
return copy;
}
@@ -234,65 +246,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);
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* 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.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<>();
@@ -152,6 +159,32 @@ 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:
Radio radio = (Radio)id;
@@ -178,6 +211,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 +418,8 @@ public class AliasList
}
break;
case PATCH_GROUP:
List<Alias> aliases = new ArrayList<>();
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
PatchGroup patchGroup = patchGroupIdentifier.getValue();
@@ -386,8 +427,6 @@ public class AliasList
if(patchGroupAliasList != null)
{
List<Alias> aliases = new ArrayList<>();
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
if(alias != null)
@@ -395,19 +434,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 +495,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 +510,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 +530,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 +653,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 +663,11 @@ public class AliasList
public Alias getAlias(TalkgroupIdentifier identifier)
{
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
{
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
}
int value = identifier.getValue();
Alias mapValue = mTalkgroupAliasMap.get(value);
@@ -620,26 +689,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 +770,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 +780,11 @@ public class AliasList
public Alias getAlias(RadioIdentifier identifier)
{
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
{
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
}
int value = identifier.getValue();
Alias mapValue = mRadioAliasMap.get(value);
@@ -702,26 +806,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)

View File

@@ -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,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,13 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
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.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.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;
@@ -54,6 +54,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,6 +63,8 @@ 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 = 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"),

View File

@@ -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,14 @@ 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"),
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 +58,7 @@ 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);
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
public String toString()
{

View File

@@ -0,0 +1,111 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.dcs;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.alias.id.AliasID;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.module.decode.dcs.DCSCode;
/**
* Digital Coded Squelch (DCS) tone identifier
*/
public class Dcs extends AliasID implements Comparable<Dcs>
{
private DCSCode mDCSCode;
@Override
public AliasIDType getType()
{
return AliasIDType.DCS;
}
@Override
public boolean matches(AliasID id)
{
if(isValid() && id instanceof Dcs other)
{
return other.isValid() && getDCSCode().equals(other.getDCSCode());
}
return false;
}
/**
* DCS code
* @return DCS code or null.
*/
@JacksonXmlProperty(isAttribute = true, localName = "code")
public DCSCode getDCSCode()
{
return mDCSCode;
}
/**
* Sets the DCS code value.
* @param dcsCode to set
*/
public void setDCSCode(DCSCode dcsCode)
{
mDCSCode = dcsCode;
updateValueProperty();
}
@Override
public boolean isValid()
{
return mDCSCode != null;
}
@Override
public boolean isAudioIdentifier()
{
return false;
}
@Override
public String toString()
{
if(isValid())
{
return getDCSCode().toString();
}
return "DCS-Invalid - No Tone Selected";
}
@Override
public int compareTo(Dcs o)
{
if(isValid())
{
if(o.isValid())
{
return getDCSCode().compareTo(o.getDCSCode());
}
else
{
return 1;
}
}
return -1;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -25,6 +25,7 @@ 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;
@@ -48,6 +49,7 @@ import java.net.SocketAddress;
@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"),

View File

@@ -25,6 +25,9 @@ import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfigura
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
import io.github.dsheirer.audio.broadcast.rdioscanner.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;
@@ -67,6 +70,9 @@ public class BroadcastFactory
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);
@@ -109,6 +115,8 @@ public class BroadcastFactory
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:

View File

@@ -32,6 +32,7 @@ public enum BroadcastServerType
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"),

View File

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

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.openmhz;
/**
* HTTP headers used for posting to Rdio Scanner API
*/
public enum FormField
{
CALL("call"),
FREQ("freq"),
START_TIME("start_time"),
STOP_TIME("stop_time"),
CALL_LENGTH("call_length"),
TALKGROUP_NUM("talkgroup_num"),
EMERGENCY("emergency"),
API_KEY("api_key"),
SOURCE_LIST("source_list"),
FREQ_LIST("freq_list"),
PATCHES("patches");
private String mHeader;
FormField(String header)
{
mHeader = header;
}
public String getHeader()
{
return mHeader;
}
}

View File

@@ -0,0 +1,634 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.openmhz;
import com.google.common.net.HttpHeaders;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Audio broadcaster to push completed audio recordings to the OpenMHz call upload API.
*
*/
public class OpenMHzBroadcaster extends AbstractAudioBroadcaster<OpenMHzConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzBroadcaster.class);
private static final String ENCODING_TYPE_MP3 = "mp3";
private static final String MULTIPART_TYPE = "multipart";
private static final String DEFAULT_SUBTYPE = "form-data";
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
private static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded";
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
private HttpClient mHttpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
private long mLastConnectionAttempt;
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
* @param config to use
* @param aliasModel for access to aliases
*/
public OpenMHzBroadcaster(OpenMHzConfiguration config, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(config);
mAliasModel = aliasModel;
}
/**
* Starts the audio recording processor thread
*/
@Override
public void start()
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
/**
* OpenMHz API does not currently expose a test method.
* TODO: FIX THIS
*/
if(response == "OK")// && response.toLowerCase().startsWith("<head><title>502 bad gateway</title></head>"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
mLog.error("Error connecting to OpenMHz server on startup [" + response + "]");
setBroadcastState(BroadcastState.ERROR);
}
if(mAudioRecordingProcessorFuture == null)
{
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
0, 500, TimeUnit.MILLISECONDS);
}
}
/**
* Stops the audio recording processor thread
*/
@Override
public void stop()
{
if(mAudioRecordingProcessorFuture != null)
{
mAudioRecordingProcessorFuture.cancel(true);
mAudioRecordingProcessorFuture = null;
dispose();
setBroadcastState(BroadcastState.DISCONNECTED);
}
}
/**
* Prepares for disposal
*/
@Override
public void dispose()
{
AudioRecording audioRecording = mAudioRecordingQueue.poll();
while(audioRecording != null)
{
audioRecording.removePendingReplay();
audioRecording = mAudioRecordingQueue.poll();
}
}
/**
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
* the audio processor thread will persistently invoke this method to attempt a reconnect.
*
* OpenMHz does not have a test API endpoint, so we look for the incomplete call response.
*/
private boolean connected()
{
if(getBroadcastState() != BroadcastState.CONNECTED &&
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
if(response != null && response == "200")
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
setBroadcastState(BroadcastState.ERROR);
}
}
return getBroadcastState() == BroadcastState.CONNECTED;
}
@Override
public int getAudioQueueSize()
{
return mAudioRecordingQueue.size();
}
@Override
public void receive(AudioRecording audioRecording)
{
mAudioRecordingQueue.offer(audioRecording);
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
/**
* Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
* exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
* deleted to ensure that the in-memory queue size doesn't blow up.
* @param audioRecording to test
* @return true if the recording is valid
*/
private boolean isValid(AudioRecording audioRecording)
{
return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
getBroadcastConfiguration().getMaximumRecordingAge();
}
/**
* Processes any enqueued audio recordings. This method employs asynchronous
* interaction with the server, so multiple audio recording uploads can occur simultaneously.
*/
private void processRecordingQueue()
{
while(connected() && !mAudioRecordingQueue.isEmpty())
{
final AudioRecording audioRecording = mAudioRecordingQueue.poll();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
{
int durationSeconds = (int)(audioRecording.getRecordingLength() / 1E3f);
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
String talkgroup = getTo(audioRecording);
String radioId = getFrom(audioRecording);
Long frequency = getFrequency(audioRecording);
String patches = getPatches(audioRecording);
String talkgroupLabel = getTalkgroupLabel(audioRecording);
String talkgroupGroup = getTalkgroupGroup(audioRecording);
String systemLabel = getSystemLabel(audioRecording);
try
{
byte[] audioBytes = null;
try
{
audioBytes = Files.readAllBytes(audioRecording.getPath());
}
catch(IOException e)
{
mLog.error("OpenMHz - audio recording file not found - ignoring upload");
}
if(audioBytes != null)
{
String uri = getBroadcastConfiguration().getHost() + "/" + getBroadcastConfiguration().getSystemName() + "/upload";
OpenMHzBuilder bodyBuilder = new OpenMHzBuilder();
bodyBuilder
.addFile(audioBytes)
.addPart(FormField.FREQ, frequency)
.addPart(FormField.START_TIME, timestampSeconds)
.addPart(FormField.STOP_TIME, timestampSeconds)
.addPart(FormField.CALL_LENGTH, durationSeconds)
.addPart(FormField.TALKGROUP_NUM, talkgroup)
.addPart(FormField.EMERGENCY, 0)
.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
// For future use if OpenMHz supports patch information
//.addPart(FormField.PATCHES, patches)
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
HttpRequest fileRequest = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.POST(bodyBuilder.build())
.build();
mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
.whenComplete((fileResponse, throwable1) -> {
if(throwable1 != null || fileResponse.statusCode() != 200)
{
if(throwable1 instanceof IOException || throwable1 instanceof CompletionException)
{
//We get socket reset exceptions occasionally when the remote server doesn't
//fully read our request and immediately responds.
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("OpenMHz API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
else
{
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("OpenMHz API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
incrementErrorAudioCount();
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
}
else
{
if(fileResponse.statusCode() == 200)
{
incrementStreamedAudioCount();
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
else
{
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("OpenMHz API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
}
});
}
else
{
//Register an error for the file not found exception
mLog.error("OpenMHz API - upload file not found [" +
audioRecording.getPath().toString() + "]");
incrementErrorAudioCount();
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
}
catch(Exception e)
{
mLog.error("Unknown Error", e);
setBroadcastState(BroadcastState.ERROR);
incrementErrorAudioCount();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
}
}
//If we're not connected and there are recordings in the queue, check the recording at the head of the queue
// and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
AudioRecording audioRecording = mAudioRecordingQueue.peek();
while(audioRecording != null)
{
if(isValid(audioRecording))
{
return;
}
else
{
//Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
mAudioRecordingQueue.poll();
audioRecording.removePendingReplay();
incrementAgedOffAudioCount();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
audioRecording = mAudioRecordingQueue.peek();
}
}
}
/**
* Creates a frequency value from the audio recording identifier collection.
*/
private static Long getFrequency(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
Form.CHANNEL_FREQUENCY, Role.ANY);
if(identifier instanceof ConfigurationLongIdentifier)
{
Long value = ((ConfigurationLongIdentifier)identifier).getValue();
if(value != null)
{
return value;
}
}
return Long.valueOf(0);
}
/**
* Creates a formatted string with the FROM identifier or uses a default of zero(0)
*/
private static String getFrom(AudioRecording audioRecording)
{
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
{
if(identifier instanceof RadioIdentifier)
{
return ((RadioIdentifier)identifier).getValue().toString();
}
}
return "0";
}
/**
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*
*/
private static String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
}
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
}
return "0";
}
/**
* Creates a formatted string with the Talkgroup Label from the Audio Recording Alias
* If this is a PatchGroup we return only the first label as the primary talkgroup label.
*
*/
private String getTalkgroupLabel(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
StringBuilder sb = new StringBuilder();
if(identifier != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
if(!aliases.isEmpty())
{
sb.append(aliases.get(0));
}
}
return sb.toString();
}
/**
* Creates a formatted string with the Talkgroup Group from the Audio Recording Alias
* If this is a PatchGroup we return only the first group as the primary talkgroup group.
*
*/
private String getTalkgroupGroup(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
StringBuilder sb = new StringBuilder();
if(identifier != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
if(!aliases.isEmpty())
{
sb.append(aliases.get(0).getGroup());
}
}
return sb.toString();
}
/**
* Creates a formatted string with the System Label from the Audio Recording Alias
* If this is a PatchGroup we return only the first sytem as the primary talkgroup system.
*
*/
private String getSystemLabel(AudioRecording audioRecording)
{
List<Identifier> systems = audioRecording.getIdentifierCollection().getIdentifiers(Form.SYSTEM);
StringBuilder sb = new StringBuilder();
if(!systems.isEmpty())
{
sb.append(systems.get(0));
}
return sb.toString();
}
/**
* Creates a formatted string with the patched talkgroups
*/
public static String getPatches(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
PatchGroup patchGroup = patchGroupIdentifier.getValue();
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{
sb.append(",").append(patched.getValue());
}
sb.append("]");
return sb.toString();
}
return "[]";
}
/**
* Tests both the connection and configuration against the OpenMHz Call API service
* @param configuration containing API key and system id
* @return error string or null if test is successful
*/
public static String testConnection(OpenMHzConfiguration configuration)
{
String uri = configuration.getHost() + "/" + configuration.getSystemName() + "/authorize";
HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.POST(HttpRequest.BodyPublishers.ofString("api_key="+configuration.getApiKey()))
.build();
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
try
{
HttpResponse<String> response = httpClient.send(request, responseHandler);
String responseBody = response.body();
if (response.statusCode() == 200)
{
return "OK";
}
else if(response.statusCode() == 403)
{
return "Invalid API Key";
}
else if(response.statusCode() == 500)
{
return "Invalid System Name";
}
return "No Response";
}
catch(Exception e)
{
return e.getLocalizedMessage();
}
}
public class AudioRecordingProcessor implements Runnable
{
@Override
public void run()
{
processRecordingQueue();
}
}
public static void main(String[] args)
{
mLog.debug("Starting ...");
OpenMHzConfiguration config = new OpenMHzConfiguration();
config.setHost("https://api.OpenMHz.com/call-upload-dev");
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
config.setSystemName("systemx");
String response = testConnection(config);
mLog.error("Response: " + response);
if(response == "OK")
{
mLog.debug("Test Successful!");
}
else
{
if(response.contains("Invalid API Key"))
{
mLog.error("Invalid API Key");
}
else if(response.contains("Invalid System Name"))
{
mLog.error("Invalid System Name");
}
else
{
mLog.debug("Response: " + response);
}
}
mLog.debug("Finished!");
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,233 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.streaming;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.playlist.PlaylistManager;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.controlsfx.control.ToggleSwitch;
/**
* OpenMHz calls API configuration editor
*/
public class OpenMHzEditor extends AbstractBroadcastEditor<OpenMHzConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzEditor.class);
private TextField mSystemNameTextField;
private IntegerTextField mMaxAgeTextField;
private TextField mApiKeyTextField;
private TextField mHostTextField;
private GridPane mEditorPane;
/**
* Constructs an instance
* @param playlistManager for accessing the broadcast model
*/
public OpenMHzEditor(PlaylistManager playlistManager)
{
super(playlistManager);
}
@Override
public void setItem(OpenMHzConfiguration item)
{
super.setItem(item);
getSystemNameTextField().setDisable(item == null);
getApiKeyTextField().setDisable(item == null);
getHostTextField().setDisable(item == null);
getMaxAgeTextField().setDisable(item == null);
if(item != null)
{
getSystemNameTextField().setText(item.getSystemName());
getApiKeyTextField().setText(item.getApiKey());
getHostTextField().setText(item.getHost());
getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000));
}
else
{
getSystemNameTextField().setText(null);
getApiKeyTextField().setText(null);
getHostTextField().setText(null);
getMaxAgeTextField().set(0);
}
modifiedProperty().set(false);
}
@Override
public void dispose()
{
}
@Override
public void save()
{
if(getItem() != null)
{
getItem().setSystemName(getSystemNameTextField().getText());
getItem().setHost(getHostTextField().getText());
getItem().setApiKey(getApiKeyTextField().getText());
getItem().setMaximumRecordingAge(getMaxAgeTextField().get() * 1000);
}
super.save();
}
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.OPENMHZ;
}
protected GridPane getEditorPane()
{
if(mEditorPane == null)
{
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 5, 10,10));
mEditorPane.setVgap(10);
mEditorPane.setHgap(5);
int row = 0;
Label formatLabel = new Label("Format");
GridPane.setHalignment(formatLabel, HPos.RIGHT);
GridPane.setConstraints(formatLabel, 0, row);
mEditorPane.getChildren().add(formatLabel);
GridPane.setConstraints(getFormatField(), 1, row);
mEditorPane.getChildren().add(getFormatField());
Label enabledLabel = new Label("Enabled");
GridPane.setHalignment(enabledLabel, HPos.RIGHT);
GridPane.setConstraints(enabledLabel, 2, row);
mEditorPane.getChildren().add(enabledLabel);
GridPane.setConstraints(getEnabledSwitch(), 3, row);
mEditorPane.getChildren().add(getEnabledSwitch());
Label systemLabel = new Label("Name");
GridPane.setHalignment(systemLabel, HPos.RIGHT);
GridPane.setConstraints(systemLabel, 0, ++row);
mEditorPane.getChildren().add(systemLabel);
GridPane.setConstraints(getNameTextField(), 1, row);
mEditorPane.getChildren().add(getNameTextField());
Label apiKeyLabel = new Label("API Key");
GridPane.setHalignment(apiKeyLabel, HPos.RIGHT);
GridPane.setConstraints(apiKeyLabel, 0, ++row);
mEditorPane.getChildren().add(apiKeyLabel);
GridPane.setConstraints(getApiKeyTextField(), 1, row);
mEditorPane.getChildren().add(getApiKeyTextField());
Label systemNameLabel = new Label("System Short Name");
GridPane.setHalignment(systemNameLabel, HPos.RIGHT);
GridPane.setConstraints(systemNameLabel, 0, ++row);
mEditorPane.getChildren().add(systemNameLabel);
GridPane.setConstraints(getSystemNameTextField(), 1, row);
mEditorPane.getChildren().add(getSystemNameTextField());
Label hostLabel = new Label("OpenMHz Upload Server (API)");
GridPane.setHalignment(hostLabel, HPos.RIGHT);
GridPane.setConstraints(hostLabel, 0, ++row);
mEditorPane.getChildren().add(hostLabel);
GridPane.setConstraints(getHostTextField(), 1, row);
mEditorPane.getChildren().add(getHostTextField());
Label maxAgeLabel = new Label("Max Recording Age (seconds)");
GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
GridPane.setConstraints(maxAgeLabel, 0, ++row);
mEditorPane.getChildren().add(maxAgeLabel);
GridPane.setConstraints(getMaxAgeTextField(), 1, row);
mEditorPane.getChildren().add(getMaxAgeTextField());
}
return mEditorPane;
}
private IntegerTextField getMaxAgeTextField()
{
if(mMaxAgeTextField == null)
{
mMaxAgeTextField = new IntegerTextField();
mMaxAgeTextField.setDisable(true);
mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
}
return mMaxAgeTextField;
}
private TextField getHostTextField()
{
if(mHostTextField == null)
{
mHostTextField = new TextField();
mHostTextField.setDisable(true);
mHostTextField.textProperty().addListener(mEditorModificationListener);
}
return mHostTextField;
}
private TextField getApiKeyTextField()
{
if(mApiKeyTextField == null)
{
mApiKeyTextField = new TextField();
mApiKeyTextField.setDisable(true);
mApiKeyTextField.textProperty().addListener(mEditorModificationListener);
}
return mApiKeyTextField;
}
private TextField getSystemNameTextField()
{
if(mSystemNameTextField == null)
{
mSystemNameTextField = new TextField();
mSystemNameTextField.setDisable(true);
mSystemNameTextField.textProperty().addListener(mEditorModificationListener);
}
return mSystemNameTextField;
}
}

View File

@@ -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;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
package io.github.dsheirer.audio.broadcast.rdioscanner;
import com.google.common.net.HttpHeaders;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
@@ -34,7 +33,6 @@ import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
@@ -42,24 +40,12 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
@@ -67,6 +53,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;
/**
@@ -244,6 +232,9 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
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
{
@@ -264,6 +255,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
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)
@@ -278,7 +270,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
.uri(URI.create(getBroadcastConfiguration().getHost()))
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
.POST(bodyBuilder.build())
.build();
@@ -525,7 +516,12 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
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());
}

View File

@@ -34,7 +34,8 @@ 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 byte[] audioBytes = null;
private String audioName = null;
/**
* Constructs an instance
@@ -51,6 +52,15 @@ public class RdioScannerBuilder
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
*/
@@ -110,7 +120,8 @@ public class RdioScannerBuilder
}
StringBuilder sb= new StringBuilder();
sb.append(DASH_DASH).append(boundary).append("\r\n");
sb.append("Content-Disposition: form-data; name=\"").append("audio").append("\"\r\n\r\n");
sb.append("Content-Disposition: form-data; filename=\"").append(audioName);
sb.append("\"; name=\"").append("audio").append("\"\r\n\r\n");
return sb.toString();
}

View File

@@ -20,6 +20,7 @@ package io.github.dsheirer.bits;
import io.github.dsheirer.edac.CRC;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.BitSet;
import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.util.FastMath;
@@ -844,13 +845,18 @@ public class BinaryMessage extends BitSet
*/
public int getTwosComplement(int start, int end)
{
BinaryMessage sub = getSubMessage(start, end);
boolean negative = sub.get(start);
sub.flip(0, sub.size());
int value = sub.getInt(0, sub.size()) + 1;
value *= (negative ? -1 : 1);
return value;
if(get(start))
{
//Negative value - flip and add one
BinaryMessage fragment = getSubMessage(start, end);
fragment.flip(0, fragment.size());
return fragment.getInt(1, fragment.size()) + 1;
}
else
{
//Positive value - return the contents.
return getInt(start + 1, end);
}
}
@@ -1273,7 +1279,7 @@ public class BinaryMessage extends BitSet
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes);
return new String(bytes, Charset.forName("UTF-16"));
}
/**

View File

@@ -33,6 +33,9 @@ import net.miginfocom.swing.MigLayout;
import javax.swing.JPanel;
/**
* Swing panel for Now Playing channels table and channel details tab set.
*/
public class NowPlayingPanel extends JPanel
{
private final ChannelMetadataPanel mChannelMetadataPanel;
@@ -40,45 +43,95 @@ public class NowPlayingPanel extends JPanel
private final DecodeEventPanel mDecodeEventPanel;
private final MessageActivityPanel mMessageActivityPanel;
private final ChannelPowerPanel mChannelPowerPanel;
private JideTabbedPane mTabbedPane;
private JideSplitPane mSplitPane;
private boolean mDetailTabsVisible;
/**
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
* messages, events, and spectral view.
*/
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
SettingsManager settingsManager)
SettingsManager settingsManager, boolean detailTabsVisible)
{
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
mDetailTabsVisible = detailTabsVisible;
init();
}
/**
* Change the visibility of the channel details tabs panel.
* @param visible true to show or false to hide.
*/
public void setDetailTabsVisible(boolean visible)
{
//Only adjust if there is a change in state
if(visible ^ mDetailTabsVisible)
{
mDetailTabsVisible = visible;
if(mDetailTabsVisible)
{
getSplitPane().add(getTabbedPane());
}
else
{
getSplitPane().remove(getTabbedPane());
}
revalidate();
}
}
private JideTabbedPane getTabbedPane()
{
if(mTabbedPane == null)
{
mTabbedPane = new JideTabbedPane();
mTabbedPane.addTab("Details", mChannelDetailPanel);
mTabbedPane.addTab("Events", mDecodeEventPanel);
mTabbedPane.addTab("Messages", mMessageActivityPanel);
mTabbedPane.addTab("Channel", mChannelPowerPanel);
mTabbedPane.setFont(this.getFont());
mTabbedPane.setForeground(Color.BLACK);
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
mTabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
.indexOfComponent(mChannelPowerPanel)));
}
return mTabbedPane;
}
/**
* Split pane for channels table and channel details tabs.
*/
private JideSplitPane getSplitPane()
{
if(mSplitPane == null)
{
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
mSplitPane.setShowGripper(true);
}
return mSplitPane;
}
private void init()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
getSplitPane().add(mChannelMetadataPanel);
JideTabbedPane tabbedPane = new JideTabbedPane();
tabbedPane.addTab("Details", mChannelDetailPanel);
tabbedPane.addTab("Events", mDecodeEventPanel);
tabbedPane.addTab("Messages", mMessageActivityPanel);
tabbedPane.addTab("Channel", mChannelPowerPanel);
tabbedPane.setFont(this.getFont());
tabbedPane.setForeground(Color.BLACK);
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
tabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(tabbedPane.getSelectedIndex() == tabbedPane
.indexOfComponent(mChannelPowerPanel)));
JideSplitPane splitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
splitPane.setShowGripper(true);
splitPane.add(mChannelMetadataPanel);
splitPane.add(tabbedPane);
add(splitPane);
if(mDetailTabsVisible)
{
getSplitPane().add(getTabbedPane());
}
add(getSplitPane());
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);

View File

@@ -59,17 +59,25 @@ public class ControllerPanel extends JPanel
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
TunerManager tunerManager, UserPreferences userPreferences)
TunerManager tunerManager, UserPreferences userPreferences, boolean detailTabsVisible)
{
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
playlistManager.getAliasModel());
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager);
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager, detailTabsVisible);
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
init();
}
/**
* Now playing panel.
*/
public NowPlayingPanel getNowPlayingPanel()
{
return mNowPlayingPanel;
}
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
@@ -97,8 +105,8 @@ public class ControllerPanel extends JPanel
mTabbedPane.addTab("Map", mMapPanel);
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
Icon gearIcon = IconFontSwing.buildIcon(FontAwesome.COG, 20, Color.DARK_GRAY);
mTabbedPane.addTab("Playlist Editor", gearIcon, new JLabel("Show Playlist Manager"));
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
//Set preferred size to influence the split between these panels

View File

@@ -365,7 +365,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
* @param request containing channel and other details
* @throws ChannelException if a source is not available for the channel
*/
private void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
private synchronized void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
{
Channel channel = request.getChannel();

View File

@@ -68,8 +68,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024;
//Sized to process 1/20th batch of 152 buffers a second, at 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(152 / 20, 25);
//Sized to process 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(25);
private FloatFFT_1D mFFT;
private float[] mInlineSamples;
private float[] mInlineFilter;
@@ -406,9 +406,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>>
{
public IFFTProcessorDispatcher(int batchSize, long interval)
public IFFTProcessorDispatcher(long interval)
{
super("sdrtrunk polyphase ifft processor", batchSize, interval);
super("sdrtrunk polyphase ifft processor", interval);
//We create a listener interface to receive the batched channel results arrays from the scheduled thread pool
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each

View File

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

View File

@@ -50,8 +50,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
mInputChannelCount = inputChannelCount;
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
mHeartbeatManager = heartbeatManager;
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel", (int)(sampleRate / 10),
50, mHeartbeatManager);
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel",50, mHeartbeatManager);
mChannelResultsDispatcher.setListener(floats -> {
try
{

View File

@@ -0,0 +1,125 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* Block Product Turbo Code 16/2 for decoding a DMR Voice Frame F payload from the 32-bit EMB field.
* <p>
* See ETSI TS 102 361-1 B.2.2.1 and B.2.2.2
*/
public class BPTC_16_2
{
private static final int[] DEINTERLEAVE = new int[]{0, 24, 1, 25, 2, 26, 3, 27, 4, 28, 5, 29, 6, 30, 7, 31, 8, 16,
9, 17, 10, 18, 11, 19, 12, 20, 13, 21, 14, 22, 15, 23};
/**
* Unscramble and perform FEC checks per paragraph B.2.2.1 for Non-Reverse Channel Single Burst
*
* @param message with 32 interleaved bits.
* @return descrambled and error checked message or null if the process fails or there are too many errors.
*/
public static CorrectedBinaryMessage decodeShortBurst(CorrectedBinaryMessage message)
{
CorrectedBinaryMessage deinterleaved = deinterleave(message);
int fec = Hamming16.checkAndCorrect(deinterleaved, 0);
if(fec == 2) //0 or 1 is good, 2 = uncorrectable errors
{
return null;
}
//Check for even parity. Bits 0-15 should be the same as bits 16-31.
for(int x = 0; x < 16; x++)
{
if(deinterleaved.get(x) ^ deinterleaved.get(x + 16))
{
return null;
}
}
return deinterleaved;
}
/**
* Unscramble and perform FEC checks per paragraph B.2.2.2 for Reverse Channel Single Burst
*
* @param binaryMessage with 32 interleaved bits.
* @return descrambled and error checked message or null if the process fails or there are too many errors.
*/
public static CorrectedBinaryMessage decodeReverseChannel(CorrectedBinaryMessage message)
{
CorrectedBinaryMessage deinterleaved = deinterleave(message);
System.out.println(" DEINTER: " + deinterleaved.toHexString());
int fec = Hamming16.checkAndCorrect(deinterleaved, 0);
System.out.println(" DECODED: " + deinterleaved.toHexString());
System.out.println("FEC:" + fec);
if(fec == 2) //0 or 1 is good, 2 = uncorrectable errors
{
return null;
}
//Check for odd parity. Bits 0-15 should be opposite of bits 16-31.
for(int x = 0; x < 16; x++)
{
if(deinterleaved.get(x) == deinterleaved.get(x + 16))
{
return null;
}
}
return deinterleaved;
}
/**
* Performs deinterleave of the interleaved message.
*
* @param original to deinterleave
* @return deinterleaved message
*/
public static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage original)
{
CorrectedBinaryMessage delinterleaved = new CorrectedBinaryMessage(32);
for(int x = 0; x < 32; x++)
{
if(original.get(x))
{
delinterleaved.set(DEINTERLEAVE[x]);
}
}
return delinterleaved;
}
public static void main(String[] args)
{
String[] msgs = new String[]{"05030A03", "35003A00", "1C6D2C9E"};
for(String msg : msgs)
{
CorrectedBinaryMessage original = new CorrectedBinaryMessage(BinaryMessage.loadHex(msg));
System.out.println("ORIGINAL: " + original.toHexString());
CorrectedBinaryMessage decoded = decodeReverseChannel(original);
System.out.println("-------------------------");
}
}
}

View File

@@ -1,18 +1,21 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.BinaryMessage;
@@ -177,6 +180,28 @@ public class CRCDMR
return 2;
}
/**
* Calculates the residual CRC value using a mask of zero which should identify the mask value when an alternate
* mask value is employed.
* @param message to be checked
* @param messageStart location
* @param crcStart location
* @return residual CRC check value.
*/
public static int calculateResidual(CorrectedBinaryMessage message, int messageStart, int crcStart)
{
int calculated = 0; //Starting value
/* Iterate the set bits and XOR running checksum with lookup value */
for(int i = message.nextSetBit(messageStart); i >= messageStart && i < crcStart; i = message.nextSetBit(i + 1))
{
calculated ^= CCITT_80_CHECKSUMS[i - messageStart];
}
int checksum = getIntChecksum(message, crcStart, 16);
return calculated ^ checksum;
}
/**
* Error detection for CRC-9 protected Confirmed Packet Data blocks. These
* data blocks have a slightly complicated structure because the checksum

View File

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

View File

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

View File

@@ -24,8 +24,6 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.controller.channel.map.ChannelRange;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.dmr.DMRRecordingViewer;
import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest;
import io.github.dsheirer.gui.icon.IconManager;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.PlaylistEditor;
@@ -37,10 +35,14 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.RecordingViewer;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor;
import io.github.dsheirer.jmbe.JmbeEditorRequest;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.monitor.ResourceMonitor;
import io.github.dsheirer.monitor.StatusBox;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.TunerManager;
@@ -67,7 +69,7 @@ public class JavaFxWindowManager extends Application
public static final String USER_PREFERENCES_EDITOR = "preferences";
public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog";
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
public static final String STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER = "dmr.message.viewer";
public static final String STAGE_MONITOR_KEY_RECORDING_VIEWER = "recording.viewer";
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
@@ -82,14 +84,15 @@ public class JavaFxWindowManager extends Application
private TunerManager mTunerManager;
private UserPreferences mUserPreferences;
private UserPreferencesEditor mUserPreferencesEditor;
private DMRRecordingViewer mDmrRecordingViewer;
private RecordingViewer mRecordingViewer;
private Stage mChannelMapStage;
private Stage mIconManagerStage;
private Stage mJmbeEditorStage;
private Stage mPlaylistStage;
private Stage mUserPreferencesStage;
private Stage mDmrRecordingViewerStage;
private Stage mRecordingViewerStage;
private JFXPanel mStatusPanel;
/**
* Constructs an instance. Note: this constructor is used for Swing applications.
@@ -118,6 +121,27 @@ public class JavaFxWindowManager extends Application
setup();
}
/**
* Creates or accesses the JavaFX status panel, used by the main application GUI.
* @param resourceMonitor for statistics
* @return JFXPanel accessible on Swing thread that delegates JavaFX scene creation to the FX event thread.
*/
public JFXPanel getStatusPanel(ResourceMonitor resourceMonitor)
{
if(mStatusPanel == null)
{
mStatusPanel = new JFXPanel();
//JFXPanel has to be populated on the FX event thread
Platform.runLater(() -> {
Scene scene = new Scene(new StatusBox(resourceMonitor));
mStatusPanel.setScene(scene);
});
}
return mStatusPanel;
}
private void setup()
{
//Register this class to receive events via each method annotated with @Subscribe
@@ -164,27 +188,7 @@ public class JavaFxWindowManager extends Application
public void shutdown()
{
MyEventBus.getGlobalEventBus().unregister(this);
if(mChannelMapStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mChannelMapStage);
}
if(mIconManagerStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mIconManagerStage);
}
if(mPlaylistStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mPlaylistStage);
}
if(mUserPreferencesStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mUserPreferencesStage);
}
mUserPreferences.getJavaFxPreferences().clearStageMonitors();
Platform.exit();
}
@@ -195,31 +199,31 @@ public class JavaFxWindowManager extends Application
}
/**
* Stage for the DMR Message Viewer
* Stage for the recording viewer
*/
public Stage getDmrRecordingViewerStage()
public Stage getRecordingViewerStage()
{
if(mDmrRecordingViewerStage == null)
if(mRecordingViewerStage == null)
{
createJFXPanel();
Scene scene = new Scene(getDmrRecordingViewer(), 1100, 800);
mDmrRecordingViewerStage = new Stage();
mDmrRecordingViewerStage.setTitle("sdrtrunk - DMR Recording Viewer");
mDmrRecordingViewerStage.setScene(scene);
mUserPreferences.getJavaFxPreferences().monitor(mDmrRecordingViewerStage, STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER);
Scene scene = new Scene(getRecordingViewer(), 1100, 800);
mRecordingViewerStage = new Stage();
mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
mRecordingViewerStage.setScene(scene);
mUserPreferences.getJavaFxPreferences().monitor(mRecordingViewerStage, STAGE_MONITOR_KEY_RECORDING_VIEWER);
}
return mDmrRecordingViewerStage;
return mRecordingViewerStage;
}
public DMRRecordingViewer getDmrRecordingViewer()
public RecordingViewer getRecordingViewer()
{
if(mDmrRecordingViewer == null)
if(mRecordingViewer == null)
{
mDmrRecordingViewer = new DMRRecordingViewer();
mRecordingViewer = new RecordingViewer();
}
return mDmrRecordingViewer;
return mRecordingViewer;
}
public Stage getIconManagerStage()
@@ -264,9 +268,7 @@ public class JavaFxWindowManager extends Application
else
{
execute(() -> {
getJmbeEditorStage().show();
getJmbeEditorStage().requestFocus();
getJmbeEditorStage().toFront();
restoreStage(getJmbeEditorStage());
getJmbeEditor().process(request);
});
}
@@ -337,9 +339,7 @@ public class JavaFxWindowManager extends Application
execute(() -> {
try
{
getPlaylistStage().show();
getPlaylistStage().requestFocus();
getPlaylistStage().toFront();
restoreStage(getPlaylistStage());
getPlaylistEditor().process(request);
}
catch(Throwable t)
@@ -387,9 +387,7 @@ public class JavaFxWindowManager extends Application
public void process(final ViewUserPreferenceEditorRequest request)
{
execute(() -> {
getUserPreferencesStage().show();
getUserPreferencesStage().requestFocus();
getUserPreferencesStage().toFront();
restoreStage(getUserPreferencesStage());
getUserPreferencesEditor().process(request);
});
}
@@ -428,11 +426,7 @@ public class JavaFxWindowManager extends Application
@Subscribe
public void process(final ViewIconManagerRequest request)
{
execute(() -> {
getIconManagerStage().show();
getIconManagerStage().requestFocus();
getIconManagerStage().toFront();
});
execute(() -> restoreStage(getIconManagerStage()));
}
/**
@@ -442,9 +436,7 @@ public class JavaFxWindowManager extends Application
public void process(final ViewChannelMapEditorRequest request)
{
execute(() -> {
getChannelMapStage().show();
getChannelMapStage().requestFocus();
getChannelMapStage().toFront();
restoreStage(getChannelMapStage());
getChannelMapEditor().process(request);
});
}
@@ -453,13 +445,24 @@ public class JavaFxWindowManager extends Application
* Process a channel map editor request
*/
@Subscribe
public void process(final ViewDmrRecordingViewerRequest request)
public void process(final ViewRecordingViewerRequest request)
{
execute(() -> {
getDmrRecordingViewerStage().show();
getDmrRecordingViewerStage().requestFocus();
getDmrRecordingViewerStage().toFront();
});
execute(() -> restoreStage(getRecordingViewerStage()));
}
/**
* Restores the stage to previous size and location.
* @param stage to restore.
*/
private void restoreStage(Stage stage)
{
stage.setIconified(false);
stage.show();
stage.requestFocus();
stage.toFront();
stage.setMinHeight(stage.getHeight());
stage.setMinWidth(stage.getWidth());
// System.out.println("Stage [" + stage.getTitle() + "] restored - width[" + stage.getWidth() + "] height [" + stage.getHeight() + "]");
}
@Override

View File

@@ -32,17 +32,18 @@ import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
import io.github.dsheirer.controller.channel.ChannelException;
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
import io.github.dsheirer.gui.preference.CalibrateRequest;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.log.ApplicationLog;
import io.github.dsheirer.map.MapService;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.monitor.ResourceMonitor;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.properties.SystemProperties;
@@ -54,7 +55,7 @@ import io.github.dsheirer.source.tuner.TunerEvent;
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
import io.github.dsheirer.spectrum.ClearTunerMenuItem;
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.ThreadPool;
@@ -68,11 +69,11 @@ import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@@ -80,7 +81,9 @@ import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.control.ButtonType;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
@@ -105,14 +108,19 @@ import javax.swing.plaf.metal.MetalLookAndFeel;
public class SDRTrunk implements Listener<TunerEvent>
{
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
private Preferences mPreferences = Preferences.userNodeForPackage(SDRTrunk.class);
private static final String PROPERTY_BROADCAST_STATUS_VISIBLE = "main.broadcast.status.visible";
private static final String PREFERENCE_BROADCAST_STATUS_VISIBLE = "sdrtrunk.broadcast.status.visible";
private static final String PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE = "sdrtrunk.now.playing.details.visible";
private static final String PREFERENCE_RESOURCE_STATUS_VISIBLE = "sdrtrunk.resource.status.visible";
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
private boolean mBroadcastStatusVisible;
private boolean mResourceStatusVisible;
private boolean mNowPlayingDetailsVisible;
private AudioRecordingManager mAudioRecordingManager;
private AudioStreamingManager mAudioStreamingManager;
private BroadcastStatusPanel mBroadcastStatusPanel;
@@ -127,6 +135,8 @@ public class SDRTrunk implements Listener<TunerEvent>
private UserPreferences mUserPreferences = new UserPreferences();
private TunerManager mTunerManager;
private ApplicationLog mApplicationLog;
private ResourceMonitor mResourceMonitor;
private JFXPanel mResourceStatusPanel;
private String mTitle;
@@ -140,6 +150,8 @@ public class SDRTrunk implements Listener<TunerEvent>
mApplicationLog = new ApplicationLog(mUserPreferences);
mApplicationLog.start();
mResourceMonitor = new ResourceMonitor(mUserPreferences);
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
@@ -155,18 +167,10 @@ public class SDRTrunk implements Listener<TunerEvent>
}
}
//Setup the application home directory
Path home = getHomePath();
ThreadPool.logSettings();
mLog.info("Home path: " + home.toString());
//Load properties file
if(home != null)
{
loadProperties(home);
}
loadProperties();
//Log current properties setting
SystemProperties.getInstance().logCurrentSettings();
@@ -213,10 +217,12 @@ public class SDRTrunk implements Listener<TunerEvent>
MapService mapService = new MapService(mIconModel);
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
if(!GraphicsEnvironment.isHeadless())
{
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
mSettingsManager, mTunerManager, mUserPreferences);
mSettingsManager, mTunerManager, mUserPreferences, mNowPlayingDetailsVisible);
}
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
@@ -323,7 +329,7 @@ public class SDRTrunk implements Listener<TunerEvent>
*/
private void initGUI()
{
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]0[shrink 0]"));
/**
* Setup main JFrame window
@@ -379,7 +385,7 @@ public class SDRTrunk implements Listener<TunerEvent>
mSplitPane.add(mSpectralPanel);
mSplitPane.add(mControllerPanel);
mBroadcastStatusVisible = SystemProperties.getInstance().get(PROPERTY_BROADCAST_STATUS_VISIBLE, false);
mBroadcastStatusVisible = mPreferences.getBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, false);
//Show broadcast status panel when user requests - disabled by default
if(mBroadcastStatusVisible)
@@ -389,6 +395,13 @@ public class SDRTrunk implements Listener<TunerEvent>
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
mResourceMonitor.start();
mResourceStatusVisible = mPreferences.getBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, true);
if(mResourceStatusVisible)
{
mMainGui.add(getResourceStatusPanel(), "span,growx");
}
/**
* Menu items
*/
@@ -399,14 +412,9 @@ public class SDRTrunk implements Listener<TunerEvent>
menuBar.add(fileMenu);
JMenuItem exitMenu = new JMenuItem("Exit");
exitMenu.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
processShutdown();
System.exit(0);
}
exitMenu.addActionListener(event -> {
processShutdown();
System.exit(0);
}
);
@@ -415,56 +423,121 @@ public class SDRTrunk implements Listener<TunerEvent>
JMenu viewMenu = new JMenu("View");
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12));
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
viewMenu.add(viewPlaylistItem);
viewMenu.add(new JSeparator());
JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer");
dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest()));
viewMenu.add(dmrMessageViewerMenu);
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
viewMenu.add(settingsMenu);
JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings");
logFilesMenu.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent arg0)
JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files");
viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
viewApplicationLogsMenu.addActionListener(arg0 -> {
File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile();
try
{
try
{
Desktop.getDesktop().open(getHomePath().toFile());
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
Desktop.getDesktop().open(logsDirectory);
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(mMainGui,
"Can't launch file explorer - files are located at: " +
getHomePath().toString(),
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
JOptionPane.showMessageDialog(mMainGui,
"Can't launch file explorer - files are located at: " + logsDirectory,
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
});
viewMenu.add(logFilesMenu);
viewMenu.add(viewApplicationLogsMenu);
JMenuItem viewRecordingsMenuItem = new JMenuItem("Audio Recordings");
viewRecordingsMenuItem.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
viewRecordingsMenuItem.addActionListener(arg0 -> {
File recordingsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording().toFile();
try
{
Desktop.getDesktop().open(recordingsDirectory);
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(mMainGui,
"Can't launch file explorer - files are located at: " +
recordingsDirectory,
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
});
viewMenu.add(viewRecordingsMenuItem);
JMenuItem viewEventLogsMenu = new JMenuItem("Channel Event Log Files");
viewEventLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
viewEventLogsMenu.addActionListener(arg0 -> {
File eventLogsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryEventLog().toFile();
try
{
Desktop.getDesktop().open(eventLogsDirectory);
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(mMainGui,
"Can't launch file explorer - files are located at: " + eventLogsDirectory,
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
});
viewMenu.add(viewEventLogsMenu);
JMenuItem iconManagerMenu = new JMenuItem("Icon Manager");
iconManagerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.PICTURE_O, 12));
iconManagerMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
viewMenu.add(iconManagerMenu);
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
recordingViewerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.BRAILLE, 12));
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
viewMenu.add(recordingViewerMenu);
JMenuItem viewScreenCapturesMenu = new JMenuItem("Screen Captures");
viewScreenCapturesMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
viewScreenCapturesMenu.addActionListener(arg0 -> {
File screenCapturesDirectory = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().toFile();
try
{
Desktop.getDesktop().open(screenCapturesDirectory);
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(mMainGui,
"Can't launch file explorer - files are located at: " + screenCapturesDirectory,
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
});
viewMenu.add(viewScreenCapturesMenu);
JMenuItem preferencesItem = new JMenuItem("User Preferences");
preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12));
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
viewMenu.add(preferencesItem);
viewMenu.add(new JSeparator());
viewMenu.add(new TunersMenu());
viewMenu.add(new JSeparator());
viewMenu.add(new ClearTunerMenuItem(mSpectralPanel));
viewMenu.add(new BroadcastStatusVisibleMenuItem(mControllerPanel));
viewMenu.add(new DisableSpectrumWaterfallMenuItem(mSpectralPanel));
viewMenu.add(new NowPlayingChannelDetailsVisibleMenuItem());
viewMenu.add(new BroadcastStatusVisibleMenuItem());
viewMenu.add(new ResourceStatusVisibleMenuItem());
menuBar.add(viewMenu);
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12));
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
@@ -515,6 +588,7 @@ public class SDRTrunk implements Listener<TunerEvent>
mLog.info("Stopping channels ...");
mPlaylistManager.getChannelProcessingManager().shutdown();
mAudioRecordingManager.stop();
mResourceMonitor.stop();
mLog.info("Stopping spectral display ...");
mSpectralPanel.clearTuner();
@@ -560,7 +634,54 @@ public class SDRTrunk implements Listener<TunerEvent>
mMainGui.revalidate();
});
SystemProperties.getInstance().set(PROPERTY_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
mPreferences.putBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
}
/**
* Lazy constructor for resource status panel
*/
private JFXPanel getResourceStatusPanel()
{
if(mResourceStatusPanel == null)
{
mResourceStatusPanel = mJavaFxWindowManager.getStatusPanel(mResourceMonitor);
}
return mResourceStatusPanel;
}
/**
* Toggles visibility of the resource status panel at the bottom of the main UI window
*/
private void toggleResourceStatusPanelVisibility()
{
mResourceStatusVisible = !mResourceStatusVisible;
EventQueue.invokeLater(() -> {
if(mResourceStatusVisible)
{
mMainGui.add(getResourceStatusPanel(), "span,growx");
}
else
{
mMainGui.remove(getResourceStatusPanel());
}
mMainGui.revalidate();
});
mPreferences.putBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, mResourceStatusVisible);
}
/**
* Toggles visibility of the Now Playing channel details panel
*/
private void toggleNowPlayingDetailsPanelVisibility()
{
mNowPlayingDetailsVisible = !mNowPlayingDetailsVisible;
mControllerPanel.getNowPlayingPanel().setDetailTabsVisible(mNowPlayingDetailsVisible);
mPreferences.putBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, mNowPlayingDetailsVisible);
}
@@ -568,34 +689,30 @@ public class SDRTrunk implements Listener<TunerEvent>
* Loads the application properties file from the user's home directory,
* creating the properties file for the first-time, if necessary
*/
private void loadProperties(Path homePath)
private void loadProperties()
{
Path propsPath = homePath.resolve("SDRTrunk.properties");
Path propertiesPath = mUserPreferences.getDirectoryPreference().getDirectoryApplicationRoot().resolve("SDRTrunk.properties");
if(!Files.exists(propsPath))
if(!Files.exists(propertiesPath))
{
try
{
mLog.info("SDRTrunk - creating application properties file [" +
propsPath.toAbsolutePath() + "]");
Files.createFile(propsPath);
mLog.info("SDRTrunk - creating application properties file [" + propertiesPath.toAbsolutePath() + "]");
Files.createFile(propertiesPath);
}
catch(IOException e)
{
mLog.error("SDRTrunk - couldn't create application properties "
+ "file [" + propsPath.toAbsolutePath(), e);
mLog.error("SDRTrunk - couldn't create application properties file [" + propertiesPath.toAbsolutePath(), e);
}
}
if(Files.exists(propsPath))
if(Files.exists(propertiesPath))
{
SystemProperties.getInstance().load(propsPath);
SystemProperties.getInstance().load(propertiesPath);
}
else
{
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk " +
"application properties file");
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk application properties file");
}
}
@@ -678,26 +795,50 @@ public class SDRTrunk implements Listener<TunerEvent>
}
}
/**
* Broadcast status panel visible toggle menu item
*/
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
{
private ControllerPanel mControllerPanel;
public BroadcastStatusVisibleMenuItem(ControllerPanel controllerPanel)
public BroadcastStatusVisibleMenuItem()
{
super("Show Streaming Status");
setSelected(mBroadcastStatusVisible);
addActionListener(e -> {
toggleBroadcastStatusPanelVisibility();
setSelected(mBroadcastStatusVisible);
});
}
}
mControllerPanel = controllerPanel;
/**
* Resource status panel visible toggle menu item
*/
public class ResourceStatusVisibleMenuItem extends JCheckBoxMenuItem
{
public ResourceStatusVisibleMenuItem()
{
super("Show Resource Status");
setSelected(mResourceStatusVisible);
addActionListener(e -> {
toggleResourceStatusPanelVisibility();
setSelected(mResourceStatusVisible);
});
}
}
setSelected(mBroadcastStatusPanel != null);
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
toggleBroadcastStatusPanelVisibility();
setSelected(mBroadcastStatusVisible);
}
/**
* Now Playing channel details visible toggle menu item
*/
public class NowPlayingChannelDetailsVisibleMenuItem extends JCheckBoxMenuItem
{
public NowPlayingChannelDetailsVisibleMenuItem()
{
super("Show Now Playing Channel Details");
setSelected(mNowPlayingDetailsVisible);
addActionListener(e -> {
toggleNowPlayingDetailsPanelVisibility();
setSelected(mNowPlayingDetailsVisible);
});
}
}

View File

@@ -1,33 +1,29 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.gui.control;
import java.util.function.UnaryOperator;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.util.converter.IntegerStringConverter;
import java.util.function.UnaryOperator;
/**
* Text field control for integer values.
*/
@@ -52,7 +48,7 @@ public class IntegerTextField extends TextField
return null;
};
setTextFormatter(new TextFormatter<Integer>(new IntegerStringConverter(), 0, filter));
setTextFormatter(new TextFormatter<Integer>(new IntegerStringConverter(), null, filter));
}
/**
@@ -71,7 +67,7 @@ public class IntegerTextField extends TextField
//Do nothing ... we couldn't parse the value
}
return null;
return 0;
}
/**

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2021 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,16 +19,12 @@
package io.github.dsheirer.gui.playlist.alias;
import java.util.List;
import org.controlsfx.control.ToggleSwitch;
import com.google.common.collect.Ordering;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.playlist.PlaylistManager;
import java.util.List;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -49,6 +45,7 @@ import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.util.Callback;
import org.controlsfx.control.ToggleSwitch;
/**
* Editor for multiple selected aliases providing limited options for changing attributes of multiple aliases
@@ -65,6 +62,8 @@ public class AliasBulkEditor extends Editor<List<Alias>>
private ToggleSwitch mMonitorAudioToggleSwitch;
private ComboBox<Integer> mMonitorPriorityComboBox;
private Button mApplyMonitorButton;
private ToggleSwitch mRecordToggleSwitch;
private Button mApplyRecordButton;
private BooleanProperty mChangeInProgressProperty;
private ReadOnlyBooleanProperty mChangeInProgressROProperty;
@@ -144,6 +143,17 @@ public class AliasBulkEditor extends Editor<List<Alias>>
GridPane.setConstraints(getApplyMonitorButton(), 4, row);
gridPane.getChildren().add(getApplyMonitorButton());
Label recordLabel = new Label("Record");
GridPane.setHalignment(recordLabel, HPos.RIGHT);
GridPane.setConstraints(recordLabel, 0, ++row);
gridPane.getChildren().add(recordLabel);
GridPane.setConstraints(getRecordToggleSwitch(), 1, row);
gridPane.getChildren().add(getRecordToggleSwitch());
GridPane.setConstraints(getApplyRecordButton(), 4, row);
gridPane.getChildren().add(getApplyRecordButton());
getChildren().add(gridPane);
}
@@ -326,6 +336,19 @@ public class AliasBulkEditor extends Editor<List<Alias>>
return mMonitorAudioToggleSwitch;
}
private ToggleSwitch getRecordToggleSwitch()
{
if(mRecordToggleSwitch == null)
{
mRecordToggleSwitch = new ToggleSwitch();
mRecordToggleSwitch.setSelected(false);
mRecordToggleSwitch.selectedProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mRecordToggleSwitch;
}
private ComboBox<Integer> getMonitorPriorityComboBox()
{
if(mMonitorPriorityComboBox == null)
@@ -346,8 +369,6 @@ public class AliasBulkEditor extends Editor<List<Alias>>
return mMonitorPriorityComboBox;
}
private static int setCount = 0;
private Button getApplyMonitorButton()
{
if(mApplyMonitorButton == null)
@@ -378,7 +399,6 @@ public class AliasBulkEditor extends Editor<List<Alias>>
for(Alias alias : getItem())
{
alias.setCallPriority(pri);
//System.out.println(++setCount);
}
endChange();
@@ -389,6 +409,25 @@ public class AliasBulkEditor extends Editor<List<Alias>>
return mApplyMonitorButton;
}
private Button getApplyRecordButton()
{
if(mApplyRecordButton == null)
{
mApplyRecordButton = new Button("Apply");
mApplyRecordButton.setOnAction(event -> {
startChange();
boolean record = getRecordToggleSwitch().isSelected();
for(Alias alias : getItem())
{
alias.setRecordable(record);
}
endChange();
});
}
return mApplyRecordButton;
}
/**
* Cell factory for combo box for dislaying icon name and graphic
*/

View File

@@ -387,7 +387,7 @@ public class AliasConfigurationEditor extends SplitPane
idsColumn.setCellValueFactory(new IdentifierCountCell());
TableColumn<Alias, Boolean> errorsColumn = new TableColumn<>("Error");
errorsColumn.setPrefWidth(80);
errorsColumn.setPrefWidth(120);
errorsColumn.setCellValueFactory(new PropertyValueFactory<>("overlap"));
errorsColumn.setCellFactory(param ->
{

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.gui.playlist.alias;
@@ -37,13 +34,16 @@ 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.lojack.LoJackFunctionAndID;
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
import io.github.dsheirer.alias.id.radio.Radio;
import io.github.dsheirer.alias.id.radio.RadioFormatter;
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.TalkgroupFormatter;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
@@ -63,6 +63,11 @@ import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.protocol.Protocol;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
@@ -106,12 +111,6 @@ import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* Editor for configuring individual aliases
*/
@@ -365,7 +364,10 @@ public class AliasItemEditor extends Editor<Alias>
for(AliasID aliasID: getIdentifiersList().getItems())
{
//Create a copy of the identifier so that the alias and the editor don't have the same instance
alias.addAliasID(AliasFactory.copyOf(aliasID));
//and reset the overlap flag (if set) so that the alias list can reevaluate the overlap state.
AliasID copy = AliasFactory.copyOf(aliasID);
copy.setOverlap(false);
alias.addAliasID(copy);
}
//Remove and replace alias actions
@@ -684,8 +686,10 @@ public class AliasItemEditor extends Editor<Alias>
Menu p25Menu = new ProtocolMenu(Protocol.APCO25);
p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25));
p25Menu.getItems().add(new AddTalkgroupRangeItem(Protocol.APCO25));
p25Menu.getItems().add(new AddP25FullyQualifiedTalkgroupItem());
p25Menu.getItems().add(new AddRadioIdItem(Protocol.APCO25));
p25Menu.getItems().add(new AddRadioIdRangeItem(Protocol.APCO25));
p25Menu.getItems().add(new AddP25FullyQualifiedRadioIdItem());
p25Menu.getItems().add(new SeparatorMenuItem());
p25Menu.getItems().add(new AddUserStatusItem());
p25Menu.getItems().add(new AddUnitStatusItem());
@@ -717,6 +721,7 @@ public class AliasItemEditor extends Editor<Alias>
Menu nbfmMenu = new ProtocolMenu(Protocol.NBFM);
nbfmMenu.getItems().add(new AddTalkgroupItem(Protocol.NBFM));
nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM));
nbfmMenu.getItems().add(new AddDcsItem());
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
@@ -1281,6 +1286,24 @@ public class AliasItemEditor extends Editor<Alias>
}
}
/**
* Add Digital Coded Squelch (DCS) alias identifier menu item
*/
public class AddDcsItem extends MenuItem
{
public AddDcsItem()
{
super("Digital Coded Squelch (DCS)");
setOnAction(event -> {
Dcs dcs = new Dcs();
getIdentifiersList().getItems().add(dcs);
getIdentifiersList().getSelectionModel().select(dcs);
getIdentifiersList().scrollTo(dcs);
modifiedProperty().set(true);
});
}
}
/**
* Menu Item for adding a new protocol-specific Radio ID alias identifier
*/
@@ -1303,6 +1326,44 @@ public class AliasItemEditor extends Editor<Alias>
}
}
/**
* Menu Item for adding a new protocol-specific Fully Qualified Radio ID alias identifier
*/
public class AddP25FullyQualifiedRadioIdItem extends MenuItem
{
public AddP25FullyQualifiedRadioIdItem()
{
super("Fully Qualified Radio ID");
setOnAction(event -> {
P25FullyQualifiedRadio radioId = new P25FullyQualifiedRadio();
radioId.setProtocol(Protocol.APCO25);
getIdentifiersList().getItems().add(radioId);
getIdentifiersList().getSelectionModel().select(radioId);
getIdentifiersList().scrollTo(radioId);
modifiedProperty().set(true);
});
}
}
/**
* Menu Item for adding a new protocol-specific Fully Qualified Talkgroup alias identifier
*/
public class AddP25FullyQualifiedTalkgroupItem extends MenuItem
{
public AddP25FullyQualifiedTalkgroupItem()
{
super("Fully Qualified Talkgroup");
setOnAction(event -> {
P25FullyQualifiedTalkgroup talkgroup = new P25FullyQualifiedTalkgroup();
talkgroup.setProtocol(Protocol.APCO25);
getIdentifiersList().getItems().add(talkgroup);
getIdentifiersList().getSelectionModel().select(talkgroup);
getIdentifiersList().scrollTo(talkgroup);
modifiedProperty().set(true);
});
}
}
/**
* Menu Item for adding a new protocol-specific Radio ID range alias identifier
*/
@@ -1486,7 +1547,25 @@ public class AliasItemEditor extends Editor<Alias>
if(item != null)
{
if(item instanceof Talkgroup)
if(item instanceof P25FullyQualifiedTalkgroup fqt)
{
StringBuilder sb = new StringBuilder();
sb.append("APCO-25 Fully Qualified Talkgroup:").append(fqt.getWacn());
sb.append(".").append(fqt.getSystem());
sb.append(".").append(fqt.getValue());
if(fqt.overlapProperty().get())
{
sb.append(" - Error: Overlap");
}
if(!fqt.isValid())
{
sb.append(" **NOT VALID**");
}
setText(sb.toString());
}
else if(item instanceof Talkgroup)
{
Talkgroup talkgroup = (Talkgroup)item;
Protocol protocol = talkgroup.getProtocol();
@@ -1533,6 +1612,24 @@ public class AliasItemEditor extends Editor<Alias>
}
setText(sb.toString());
}
else if(item instanceof P25FullyQualifiedRadio fqr)
{
StringBuilder sb = new StringBuilder();
sb.append("APCO-25 Fully Qualified Radio ID:").append(fqr.getWacn());
sb.append(".").append(fqr.getSystem());
sb.append(".").append(fqr.getValue());
if(fqr.overlapProperty().get())
{
sb.append(" - Error: Overlap");
}
if(!fqr.isValid())
{
sb.append(" **NOT VALID**");
}
setText(sb.toString());
}
else if(item instanceof Radio)
{
Radio radio = (Radio)item;

View File

@@ -0,0 +1,98 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.alias.identifier;
import io.github.dsheirer.alias.id.dcs.Dcs;
import io.github.dsheirer.module.decode.dcs.DCSCode;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Editor for Digital Coded Squelch (DCS) alias identifiers
*/
public class DcsEditor extends IdentifierEditor<Dcs>
{
private static final Logger mLog = LoggerFactory.getLogger(DcsEditor.class);
private ComboBox<DCSCode> mDCSCodeComboBox;
/**
* Constructs an instance
*/
public DcsEditor()
{
GridPane gridPane = new GridPane();
gridPane.setHgap(5);
Label typeLabel = new Label("DCS Code");
GridPane.setConstraints(typeLabel, 0, 0);
gridPane.getChildren().add(typeLabel);
GridPane.setConstraints(getDCSCodeComboBox(), 1, 0);
gridPane.getChildren().add(getDCSCodeComboBox());
getChildren().add(gridPane);
}
@Override
public void setItem(Dcs item)
{
super.setItem(item);
if(item.isValid())
{
getDCSCodeComboBox().getSelectionModel().select(item.getDCSCode());
}
modifiedProperty().set(false);
}
@Override
public void save()
{
//no-op
}
@Override
public void dispose()
{
//no-op
}
/**
* Combo-box loaded with DCS codes
* @return
*/
private ComboBox<DCSCode> getDCSCodeComboBox()
{
if(mDCSCodeComboBox == null)
{
mDCSCodeComboBox = new ComboBox<>();
mDCSCodeComboBox.getItems().addAll(DCSCode.STANDARD_CODES);
mDCSCodeComboBox.getItems().addAll(DCSCode.INVERTED_CODES);
mDCSCodeComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
getItem().setDCSCode(getDCSCodeComboBox().getSelectionModel().getSelectedItem());
modifiedProperty().set(true);
});
}
return mDCSCodeComboBox;
}
}

View File

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

View File

@@ -0,0 +1,270 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.alias.identifier;
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
import io.github.dsheirer.gui.control.HexFormatter;
import io.github.dsheirer.gui.control.IntegerFormatter;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Editor for P25 fully qualified radio alias identifiers
*/
public class P25FullyQualifiedRadioIdEditor extends IdentifierEditor<P25FullyQualifiedRadio>
{
private static final Logger mLog = LoggerFactory.getLogger(P25FullyQualifiedRadioIdEditor.class);
private UserPreferences mUserPreferences;
private Label mProtocolLabel;
private TextField mWacnField;
private TextField mSystemField;
private TextField mRadioField;
private TextFormatter<Integer> mWacnTextFormatter;
private TextFormatter<Integer> mSystemTextFormatter;
private TextFormatter<Integer> mRadioTextFormatter;
private WacnValueChangeListener mWacnValueChangeListener = new WacnValueChangeListener();
private SystemValueChangeListener mSystemValueChangeListener = new SystemValueChangeListener();
private RadioValueChangeListener mRadioValueChangeListener = new RadioValueChangeListener();
/**
* Constructs an instance
* @param userPreferences for determining user preferred talkgroup formats
*/
public P25FullyQualifiedRadioIdEditor(UserPreferences userPreferences)
{
mUserPreferences = userPreferences;
GridPane gridPane = new GridPane();
gridPane.setHgap(5);
gridPane.setVgap(3);
GridPane.setConstraints(getProtocolLabel(), 0, 0);
gridPane.getChildren().add(getProtocolLabel());
Label valueLabel = new Label("WACN");
GridPane.setHalignment(valueLabel, HPos.RIGHT);
GridPane.setConstraints(valueLabel, 1, 0);
gridPane.getChildren().add(valueLabel);
GridPane.setConstraints(getWacnField(), 2, 0);
gridPane.getChildren().add(getWacnField());
Label systemLabel = new Label("System");
GridPane.setHalignment(systemLabel, HPos.RIGHT);
GridPane.setConstraints(systemLabel, 3, 0);
gridPane.getChildren().add(systemLabel);
GridPane.setConstraints(getSystemField(), 4, 0);
gridPane.getChildren().add(getSystemField());
Label radioLabel = new Label("Radio ID");
GridPane.setHalignment(radioLabel, HPos.RIGHT);
GridPane.setConstraints(radioLabel, 5, 0);
gridPane.getChildren().add(radioLabel);
GridPane.setConstraints(getRadioField(), 6, 0);
gridPane.getChildren().add(getRadioField());
getChildren().add(gridPane);
}
@Override
public void setItem(P25FullyQualifiedRadio item)
{
super.setItem(item);
P25FullyQualifiedRadio fqr = getItem();
getProtocolLabel().setDisable(fqr == null);
getWacnField().setDisable(fqr == null);
getSystemField().setDisable(fqr == null);
getRadioField().setDisable(fqr == null);
if(fqr != null)
{
getProtocolLabel().setText(fqr.getProtocol().toString());
updateTextFormatter();
}
else
{
getWacnField().setText(null);
getSystemField().setText(null);
getRadioField().setText(null);
}
modifiedProperty().set(false);
}
private void updateTextFormatter()
{
if(mWacnTextFormatter != null)
{
mWacnTextFormatter.valueProperty().removeListener(mWacnValueChangeListener);
}
if(mSystemTextFormatter != null)
{
mSystemTextFormatter.valueProperty().removeListener(mSystemValueChangeListener);
}
if(mRadioTextFormatter != null)
{
mRadioTextFormatter.valueProperty().removeListener(mRadioValueChangeListener);
}
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(getItem().getProtocol());
if(format == IntegerFormat.DECIMAL && (mRadioTextFormatter == null || !(mRadioTextFormatter instanceof IntegerFormatter)))
{
mWacnTextFormatter = new IntegerFormatter(0,0xFFFFF);
mSystemTextFormatter = new IntegerFormatter(0,0xFFF);
mRadioTextFormatter = new IntegerFormatter(0,0xFFFFFF);
mWacnField.setTooltip(new Tooltip("Format: 0 - 1048575"));
mSystemField.setTooltip(new Tooltip("Format: 0 - 4095"));
mRadioField.setTooltip(new Tooltip("Format: 0 - 16777215"));
}
else if(format == IntegerFormat.HEXADECIMAL && (mRadioTextFormatter == null || !(mRadioTextFormatter instanceof HexFormatter)))
{
mWacnTextFormatter = new HexFormatter(0,0xFFFFF);
mSystemTextFormatter = new HexFormatter(0,0xFFF);
mRadioTextFormatter = new HexFormatter(0,0xFFFFFF);
mWacnField.setTooltip(new Tooltip("Format: 0 - FFFFF"));
mSystemField.setTooltip(new Tooltip("Format: 0 - FFF"));
mRadioField.setTooltip(new Tooltip("Format: 0 - FFFFFF"));
}
mWacnField.setTextFormatter(mWacnTextFormatter);
mSystemField.setTextFormatter(mSystemTextFormatter);
mRadioField.setTextFormatter(mRadioTextFormatter);
mWacnTextFormatter.setValue(getItem() != null ? getItem().getWacn() : null);
mSystemTextFormatter.setValue(getItem() != null ? getItem().getSystem() : null);
mRadioTextFormatter.setValue(getItem() != null ? getItem().getValue() : null);
mWacnTextFormatter.valueProperty().addListener(mWacnValueChangeListener);
mSystemTextFormatter.valueProperty().addListener(mSystemValueChangeListener);
mRadioTextFormatter.valueProperty().addListener(mRadioValueChangeListener);
}
@Override
public void save()
{
//no-op
}
@Override
public void dispose()
{
//no-op
}
private Label getProtocolLabel()
{
if(mProtocolLabel == null)
{
mProtocolLabel = new Label();
}
return mProtocolLabel;
}
private TextField getWacnField()
{
if(mWacnField == null)
{
mWacnField = new TextField();
mWacnField.setTextFormatter(mWacnTextFormatter);
}
return mWacnField;
}
private TextField getSystemField()
{
if(mSystemField == null)
{
mSystemField = new TextField();
mSystemField.setTextFormatter(mSystemTextFormatter);
}
return mSystemField;
}
private TextField getRadioField()
{
if(mRadioField == null)
{
mRadioField = new TextField();
mRadioField.setTextFormatter(mRadioTextFormatter);
}
return mRadioField;
}
public class WacnValueChangeListener implements ChangeListener<Integer>
{
@Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
{
if(getItem() != null)
{
getItem().setWacn(newValue != null ? newValue : 0);
modifiedProperty().set(true);
}
}
}
public class SystemValueChangeListener implements ChangeListener<Integer>
{
@Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
{
if(getItem() != null)
{
getItem().setSystem(newValue != null ? newValue : 0);
modifiedProperty().set(true);
}
}
}
public class RadioValueChangeListener implements ChangeListener<Integer>
{
@Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
{
if(getItem() != null)
{
getItem().setValue(newValue != null ? newValue : 0);
modifiedProperty().set(true);
}
}
}
}

View File

@@ -0,0 +1,270 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.alias.identifier;
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
import io.github.dsheirer.gui.control.HexFormatter;
import io.github.dsheirer.gui.control.IntegerFormatter;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Editor for P25 fully qualified talkgroup alias identifiers
*/
public class P25FullyQualifiedTalkgroupEditor extends IdentifierEditor<P25FullyQualifiedTalkgroup>
{
private static final Logger mLog = LoggerFactory.getLogger(P25FullyQualifiedTalkgroupEditor.class);
private UserPreferences mUserPreferences;
private Label mProtocolLabel;
private TextField mWacnField;
private TextField mSystemField;
private TextField mTalkgroupField;
private TextFormatter<Integer> mWacnTextFormatter;
private TextFormatter<Integer> mSystemTextFormatter;
private TextFormatter<Integer> mTalkgroupTextFormatter;
private WacnValueChangeListener mWacnValueChangeListener = new WacnValueChangeListener();
private SystemValueChangeListener mSystemValueChangeListener = new SystemValueChangeListener();
private TalkgroupValueChangeListener mTalkgroupValueChangeListener = new TalkgroupValueChangeListener();
/**
* Constructs an instance
* @param userPreferences for determining user preferred talkgroup formats
*/
public P25FullyQualifiedTalkgroupEditor(UserPreferences userPreferences)
{
mUserPreferences = userPreferences;
GridPane gridPane = new GridPane();
gridPane.setHgap(5);
gridPane.setVgap(3);
GridPane.setConstraints(getProtocolLabel(), 0, 0);
gridPane.getChildren().add(getProtocolLabel());
Label valueLabel = new Label("WACN");
GridPane.setHalignment(valueLabel, HPos.RIGHT);
GridPane.setConstraints(valueLabel, 1, 0);
gridPane.getChildren().add(valueLabel);
GridPane.setConstraints(getWacnField(), 2, 0);
gridPane.getChildren().add(getWacnField());
Label systemLabel = new Label("System");
GridPane.setHalignment(systemLabel, HPos.RIGHT);
GridPane.setConstraints(systemLabel, 3, 0);
gridPane.getChildren().add(systemLabel);
GridPane.setConstraints(getSystemField(), 4, 0);
gridPane.getChildren().add(getSystemField());
Label radioLabel = new Label("Talkgroup");
GridPane.setHalignment(radioLabel, HPos.RIGHT);
GridPane.setConstraints(radioLabel, 5, 0);
gridPane.getChildren().add(radioLabel);
GridPane.setConstraints(getTalkgroupField(), 6, 0);
gridPane.getChildren().add(getTalkgroupField());
getChildren().add(gridPane);
}
@Override
public void setItem(P25FullyQualifiedTalkgroup item)
{
super.setItem(item);
P25FullyQualifiedTalkgroup fqt = getItem();
getProtocolLabel().setDisable(fqt == null);
getWacnField().setDisable(fqt == null);
getSystemField().setDisable(fqt == null);
getTalkgroupField().setDisable(fqt == null);
if(fqt != null)
{
getProtocolLabel().setText(fqt.getProtocol().toString());
updateTextFormatter();
}
else
{
getWacnField().setText(null);
getSystemField().setText(null);
getTalkgroupField().setText(null);
}
modifiedProperty().set(false);
}
private void updateTextFormatter()
{
if(mWacnTextFormatter != null)
{
mWacnTextFormatter.valueProperty().removeListener(mWacnValueChangeListener);
}
if(mSystemTextFormatter != null)
{
mSystemTextFormatter.valueProperty().removeListener(mSystemValueChangeListener);
}
if(mTalkgroupTextFormatter != null)
{
mTalkgroupTextFormatter.valueProperty().removeListener(mTalkgroupValueChangeListener);
}
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(getItem().getProtocol());
if(format == IntegerFormat.DECIMAL && (mTalkgroupTextFormatter == null || !(mTalkgroupTextFormatter instanceof IntegerFormatter)))
{
mWacnTextFormatter = new IntegerFormatter(0,0xFFFFF);
mSystemTextFormatter = new IntegerFormatter(0,0xFFF);
mTalkgroupTextFormatter = new IntegerFormatter(0,0xFFFF);
mWacnField.setTooltip(new Tooltip("Format: 0 - 1048575"));
mSystemField.setTooltip(new Tooltip("Format: 0 - 4095"));
mTalkgroupField.setTooltip(new Tooltip("Format: 0 - 65535"));
}
else if(format == IntegerFormat.HEXADECIMAL && (mTalkgroupTextFormatter == null || !(mTalkgroupTextFormatter instanceof HexFormatter)))
{
mWacnTextFormatter = new HexFormatter(0,0xFFFFF);
mSystemTextFormatter = new HexFormatter(0,0xFFF);
mTalkgroupTextFormatter = new HexFormatter(0,0xFFFFFF);
mWacnField.setTooltip(new Tooltip("Format: 0 - FFFFF"));
mSystemField.setTooltip(new Tooltip("Format: 0 - FFF"));
mTalkgroupField.setTooltip(new Tooltip("Format: 0 - FFFF"));
}
mWacnField.setTextFormatter(mWacnTextFormatter);
mSystemField.setTextFormatter(mSystemTextFormatter);
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
mWacnTextFormatter.setValue(getItem() != null ? getItem().getWacn() : null);
mSystemTextFormatter.setValue(getItem() != null ? getItem().getSystem() : null);
mTalkgroupTextFormatter.setValue(getItem() != null ? getItem().getValue() : null);
mWacnTextFormatter.valueProperty().addListener(mWacnValueChangeListener);
mSystemTextFormatter.valueProperty().addListener(mSystemValueChangeListener);
mTalkgroupTextFormatter.valueProperty().addListener(mTalkgroupValueChangeListener);
}
@Override
public void save()
{
//no-op
}
@Override
public void dispose()
{
//no-op
}
private Label getProtocolLabel()
{
if(mProtocolLabel == null)
{
mProtocolLabel = new Label();
}
return mProtocolLabel;
}
private TextField getWacnField()
{
if(mWacnField == null)
{
mWacnField = new TextField();
mWacnField.setTextFormatter(mWacnTextFormatter);
}
return mWacnField;
}
private TextField getSystemField()
{
if(mSystemField == null)
{
mSystemField = new TextField();
mSystemField.setTextFormatter(mSystemTextFormatter);
}
return mSystemField;
}
private TextField getTalkgroupField()
{
if(mTalkgroupField == null)
{
mTalkgroupField = new TextField();
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
}
return mTalkgroupField;
}
public class WacnValueChangeListener implements ChangeListener<Integer>
{
@Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
{
if(getItem() != null)
{
getItem().setWacn(newValue != null ? newValue : 0);
modifiedProperty().set(true);
}
}
}
public class SystemValueChangeListener implements ChangeListener<Integer>
{
@Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
{
if(getItem() != null)
{
getItem().setSystem(newValue != null ? newValue : 0);
modifiedProperty().set(true);
}
}
}
public class TalkgroupValueChangeListener implements ChangeListener<Integer>
{
@Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
{
if(getItem() != null)
{
getItem().setValue(newValue != null ? newValue : 0);
modifiedProperty().set(true);
}
}
}
}

View File

@@ -92,9 +92,9 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public AMConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,6 +38,8 @@ import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.util.ThreadPool;
import java.util.Optional;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -69,9 +71,6 @@ import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import java.util.function.Predicate;
/**
* Channel configuration editor
*/
@@ -100,6 +99,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
private IconNode mPlayGraphicNode;
private IconNode mStopGraphicNode;
private ChannelProcessingMonitor mChannelProcessingMonitor = new ChannelProcessingMonitor();
private IFilterProcessor mFilterProcessor;
/**
* Constructs an instance
@@ -108,11 +108,12 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
* @param userPreferences for preferences
*/
public ChannelConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
mPlaylistManager = playlistManager;
mTunerManager = tunerManager;
mUserPreferences = userPreferences;
mFilterProcessor = filterProcessor;
setMaxWidth(Double.MAX_VALUE);
@@ -689,7 +690,16 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
mSaveButton.setMaxWidth(Double.MAX_VALUE);
mSaveButton.disableProperty().bind(modifiedProperty().not());
mSaveButton.setOnAction(event -> {
save();
if(mFilterProcessor != null)
{
mFilterProcessor.clearFilter();
save();
mFilterProcessor.restoreFilter();
}
else
{
save();
}
if(getItem().isProcessing())
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,11 +23,10 @@ import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides access to channel configuration editors for various decoder types.
@@ -41,31 +40,35 @@ public class ChannelConfigurationEditorFactory
* Constructs an editor for the specified decoder type
* @param decoderType to create
* @param playlistManager for the editor
* @param tunerManager for tuners
* @param userPreferences for preferences
* @param filterProcessor to be notified to clear and restore any applied filters.
* @return constructed editor
*/
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager,
TunerManager tunerManager, UserPreferences userPreferences)
TunerManager tunerManager, UserPreferences userPreferences,
IFilterProcessor filterProcessor)
{
switch(decoderType)
{
case AM:
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case DMR:
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case NBFM:
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case LTR_NET:
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case LTR:
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case MPT1327:
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case PASSPORT:
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case P25_PHASE1:
return new P25P1ConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new P25P1ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
case P25_PHASE2:
return new P25P2ConfigurationEditor(playlistManager, tunerManager, userPreferences);
return new P25P2ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
default:
if(decoderType != null && !mLoggedUnrecognizedTypes.contains(decoderType))
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,6 +26,12 @@ import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
@@ -63,17 +69,10 @@ import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
/**
* JavaFX editor for managing channel configurations.
*/
public class ChannelEditor extends SplitPane
public class ChannelEditor extends SplitPane implements IFilterProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
private PlaylistManager mPlaylistManager;
@@ -106,7 +105,8 @@ public class ChannelEditor extends SplitPane
mPlaylistManager = playlistManager;
mTunerManager = tunerManager;
mUserPreferences = userPreferences;
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager, userPreferences);
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager,
userPreferences, this);
HBox channelsBox = new HBox();
channelsBox.setSpacing(10.0);
@@ -203,7 +203,7 @@ public class ChannelEditor extends SplitPane
if(editor == null)
{
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager,
mTunerManager, mUserPreferences);
mTunerManager, mUserPreferences, this);
if(editor != null)
{
@@ -374,6 +374,24 @@ public class ChannelEditor extends SplitPane
mChannelFilteredList.setPredicate(mChannelListFilter);
}
/**
* Temporarily clear the filter when the channel configuration editor is applying changes.
*/
@Override
public void clearFilter()
{
mChannelFilteredList.setPredicate(null);
}
/**
* Restore the filter once the channel configuration editor has completed applying changes.
*/
@Override
public void restoreFilter()
{
mChannelFilteredList.setPredicate(mChannelListFilter);
}
private TableView<Channel> getChannelTableView()
{
if(mChannelTableView == null)
@@ -449,6 +467,25 @@ public class ChannelEditor extends SplitPane
nameColumn.setPrefWidth(200);
TableColumn frequencyColumn = new TableColumn("Frequency");
frequencyColumn.setComparator((o1, o2) -> {
if(o1 instanceof Channel c1 && o2 instanceof Channel c2)
{
if(!c1.getFrequencyList().isEmpty() && !c2.getFrequencyList().isEmpty())
{
return Long.compare(c1.getFrequencyList().get(0), c2.getFrequencyList().get(0));
}
else if(!c1.getFrequencyList().isEmpty())
{
return Long.compare(c1.getFrequencyList().get(0), 0l);
}
else if(!c2.getFrequencyList().isEmpty())
{
return Long.compare(0l, c2.getFrequencyList().get(0));
}
}
return Integer.compare(o1.hashCode(), o2.hashCode());
});
frequencyColumn.setCellValueFactory(new FrequencyCellValueFactory());
frequencyColumn.setPrefWidth(100);

View File

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

View File

@@ -0,0 +1,30 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.channel;
/**
* Interface for a processor that implements a filtering mechanism and can be commanded by an external controller to
* temporarily clear a filter and then restore the filter.
*/
public interface IFilterProcessor
{
void clearFilter();
void restoreFilter();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
@@ -50,9 +52,6 @@ import org.controlsfx.control.SegmentedButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* LTR (standard) channel configuration editor
*/
@@ -77,9 +76,9 @@ public class LTRConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public LTRConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
@@ -50,9 +52,6 @@ import org.controlsfx.control.SegmentedButton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* LTR-Net channel configuration editor
*/
@@ -76,9 +75,10 @@ public class LTRNetConfigurationEditor extends ChannelConfigurationEditor
* @param tunerManager for tuners
* @param userPreferences for preferences
*/
public LTRNetConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager, UserPreferences userPreferences)
public LTRNetConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,6 +38,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
@@ -56,9 +58,6 @@ import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MAXIMUM;
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MINIMUM;
@@ -87,9 +86,9 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public MPT1327ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -27,7 +27,6 @@ import io.github.dsheirer.gui.playlist.eventlog.EventLogConfigurationEditor;
import io.github.dsheirer.gui.playlist.source.FrequencyEditor;
import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog;
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
@@ -41,6 +40,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
@@ -58,9 +59,6 @@ import javafx.scene.text.TextAlignment;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.control.ToggleSwitch;
import java.util.ArrayList;
import java.util.List;
/**
* Narrow-Band FM channel configuration editor
*/
@@ -95,9 +93,9 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public NBFMConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,6 +36,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
@@ -54,9 +56,6 @@ import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* P25 Phase 1 channel configuration editor
*/
@@ -83,9 +82,9 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences for preferences
*/
public P25P1ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,6 +37,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
@@ -46,9 +48,6 @@ import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* P25 Phase 2 channel configuration editor
*/
@@ -72,9 +71,9 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences
*/
public P25P2ConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,6 +36,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TitledPane;
@@ -43,9 +45,6 @@ import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* Passport channel configuration editor
*/
@@ -68,9 +67,9 @@ public class PassportConfigurationEditor extends ChannelConfigurationEditor
* @param userPreferences
*/
public PassportConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -45,9 +45,9 @@ public class UnknownConfigurationEditor extends ChannelConfigurationEditor
* @param aliasModel
*/
public UnknownConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
UserPreferences userPreferences)
UserPreferences userPreferences, IFilterProcessor filterProcessor)
{
super(playlistManager, tunerManager, userPreferences);
super(playlistManager, tunerManager, userPreferences, filterProcessor);
getTitledPanesBox().getChildren().add(getDecoderPane());
}

View File

@@ -23,6 +23,7 @@ import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.rrapi.type.Agency;
import io.github.dsheirer.rrapi.type.AgencyInfo;
import io.github.dsheirer.rrapi.type.CountyInfo;
import io.github.dsheirer.service.radioreference.RadioReference;
import io.github.dsheirer.util.ThreadPool;
import javafx.application.Platform;
@@ -158,8 +159,13 @@ public class AgencyEditor extends VBox
ThreadPool.CACHED.submit(() -> {
try
{
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
if (mLevel == Level.COUNTY && agency instanceof CountyAgency) {
final CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(-agency.getAgencyId());
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(countyInfo.getCategories()));
} else {
final AgencyInfo agencyInfo = mRadioReference.getService().getAgencyInfo(agency);
Platform.runLater(() -> getAgencyFrequencyEditor().setCategories(agencyInfo.getCategories()));
}
}
catch(Throwable t)
{
@@ -196,11 +202,11 @@ public class AgencyEditor extends VBox
{
return 0;
}
else if(o1.getName() == null)
else if(o1.getName() == null || o2 instanceof CountyAgency)
{
return 1;
}
else if(o2.getName() == null)
else if(o2.getName() == null || o1 instanceof CountyAgency)
{
return -1;
}

View File

@@ -158,9 +158,12 @@ public class AgencyFrequencyEditor extends GridPane
if(categories != null && !categories.isEmpty())
{
getCategoryComboBox().getItems().add(ALL_CATEGORIES);
// Only allow the combined list of all categories if there aren't that many
if (categories.size() < 10) {
categories.add(0, ALL_CATEGORIES);
}
getCategoryComboBox().getItems().addAll(categories);
getCategoryComboBox().getSelectionModel().select(ALL_CATEGORIES);
getCategoryComboBox().getSelectionModel().select(categories.get(0));
}
else
{

View File

@@ -0,0 +1,33 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.radioreference;
import io.github.dsheirer.rrapi.type.Agency;
import io.github.dsheirer.rrapi.type.CountyInfo;
public class CountyAgency extends Agency
{
public CountyAgency(CountyInfo countyInfo)
{
this.setAgencyId(-countyInfo.getCountyId());
this.setName(countyInfo.getName() + " County (All)");
this.setType(-1);
}
}

View File

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

View File

@@ -22,6 +22,7 @@ package io.github.dsheirer.gui.playlist.radioreference;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.rrapi.RadioReferenceException;
import io.github.dsheirer.rrapi.type.Agency;
import io.github.dsheirer.rrapi.type.AuthorizationInformation;
import io.github.dsheirer.rrapi.type.Country;
import io.github.dsheirer.rrapi.type.CountryInfo;
@@ -55,6 +56,7 @@ import jiconfont.javafx.IconNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@@ -689,7 +691,12 @@ public class RadioReferenceEditor extends BorderPane implements Consumer<Authori
Platform.runLater(() -> {
getCountySystemEditor().setSystems(countyInfo.getSystems());
getCountyAgencyEditor().setAgencies(countyInfo.getAgencies());
// Make a new list so we don't add our CountyAgency to the original list of agencies
List<Agency> countyAgencies = new ArrayList<Agency>();
countyAgencies.add(new CountyAgency(countyInfo));
countyAgencies.addAll(countyInfo.getAgencies());
getCountyAgencyEditor().setAgencies(countyAgencies);
});
}
catch(RadioReferenceException rre)

View File

@@ -294,7 +294,11 @@ public class SystemEditor extends VBox
{
CountyInfo countyInfo = null;
if(site.getCountyId() > 0)
int countyId = site.getCountyId();
//Temporary sites that have been added to a system where the county/location is unknown, can
//have a county ID of 99,999 as observed from API (undocumented).
if(countyId > 0 && countyId != 99999)
{
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
}

View File

@@ -40,6 +40,8 @@ public class StreamEditorFactory
return new BroadcastifyStreamEditor(playlistManager);
case RDIOSCANNER_CALL:
return new RdioScannerEditor(playlistManager);
case OPENMHZ:
return new OpenMHzEditor(playlistManager);
case BROADCASTIFY_CALL:
return new BroadcastifyCallEditor(playlistManager);
case ICECAST_HTTP:

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,6 +28,11 @@ import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfigura
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
import io.github.dsheirer.util.ThreadPool;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@@ -48,6 +53,7 @@ import javafx.scene.control.TabPane;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@@ -58,12 +64,6 @@ import jiconfont.javafx.IconNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Editor for broadcast audio stream configurations
*/
@@ -75,6 +75,7 @@ public class StreamingEditor extends SplitPane
private TableView<ConfiguredBroadcast> mConfiguredBroadcastTableView;
private MenuButton mNewButton;
private Button mDeleteButton;
private Button mRefreshButton;
private TabPane mTabPane;
private Tab mConfigurationTab;
private Tab mAliasTab;
@@ -101,7 +102,7 @@ public class StreamingEditor extends SplitPane
refreshBroadcastifyStreams();
VBox buttonsBox = new VBox();
buttonsBox.getChildren().addAll(getNewButton(), getDeleteButton());
buttonsBox.getChildren().addAll(getNewButton(), getDeleteButton(), getRefreshButton());
buttonsBox.setPadding(new Insets(0, 0, 0, 10));
buttonsBox.setSpacing(10);
@@ -231,21 +232,16 @@ public class StreamingEditor extends SplitPane
{
if(mPlaylistManager.getRadioReference().availableProperty().get())
{
ThreadPool.CACHED.submit(new Runnable()
{
@Override
public void run()
ThreadPool.CACHED.submit(() -> {
try
{
try
{
List<UserFeedBroadcast> feeds = mPlaylistManager.getRadioReference().getService().getUserFeeds();
mBroadcastifyFeeds.clear();
mBroadcastifyFeeds.addAll(feeds);
}
catch(Throwable t)
{
mLog.error("Unable to refresh broadcastify stream configuration(s)");
}
List<UserFeedBroadcast> feeds = mPlaylistManager.getRadioReference().getService().getUserFeeds();
mBroadcastifyFeeds.clear();
mBroadcastifyFeeds.addAll(feeds);
}
catch(Throwable t)
{
mLog.error("Unable to refresh broadcastify stream configuration(s)");
}
});
}
@@ -348,6 +344,22 @@ public class StreamingEditor extends SplitPane
return mNewButton;
}
/**
* Refresh broadcastify feeds.
* @return button to refresh.
*/
private Button getRefreshButton()
{
if(mRefreshButton == null)
{
mRefreshButton = new Button("Refresh");
mRefreshButton.setTooltip(new Tooltip("Refresh streams available from Broadcastify"));
mRefreshButton.setOnAction(event -> refreshBroadcastifyStreams());
}
return mRefreshButton;
}
private Button getDeleteButton()
{
if(mDeleteButton == null)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,6 +24,7 @@ import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.directory.DirectoryPreference;
import java.io.File;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
@@ -31,6 +32,7 @@ import javafx.geometry.Orientation;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.Spinner;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@@ -39,8 +41,6 @@ import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
/**
* Preference settings for channel event view
@@ -97,6 +97,9 @@ public class DirectoryPreferenceEditor extends HBox
private Button mResetStreamingButton;
private Label mStreamingPathLabel;
private Spinner<Integer> mRecordingSpinner;
private Spinner<Integer> mEventLogSpinner;
public DirectoryPreferenceEditor(UserPreferences userPreferences)
{
mDirectoryPreference = userPreferences.getDirectoryPreference();
@@ -247,11 +250,57 @@ public class DirectoryPreferenceEditor extends HBox
GridPane.setMargin(getResetStreamingButton(), new Insets(2, 0, 2, 0));
mEditorPane.add(getResetStreamingButton(), 3, row++);
Label monitorLabel = new Label("File storage usage monitoring - maximum size thresholds (MB)");
GridPane.setMargin(monitorLabel, new Insets(15, 0, 2, 0));
mEditorPane.add(monitorLabel, 0, row++, 4, 1);
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, row++, 4, 1);
mEditorPane.add(new Label("Event Logs"), 0, row);
GridPane.setMargin(getEventLogSpinner(), new Insets(2, 2, 2, 0));
mEditorPane.add(getEventLogSpinner(), 1, row++);
mEditorPane.add(new Label("Recordings"), 0, row);
GridPane.setMargin(getRecordingSpinner(), new Insets(2, 2, 2, 0));
mEditorPane.add(getRecordingSpinner(), 1, row);
}
return mEditorPane;
}
/**
* Recording directory maximum size threshold spinner
* @return spinner
*/
private Spinner<Integer> getRecordingSpinner()
{
if(mRecordingSpinner == null)
{
mRecordingSpinner = new Spinner<>(100, Integer.MAX_VALUE, mDirectoryPreference.getDirectoryMaxUsageRecordings(), 100);
mRecordingSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mDirectoryPreference
.setDirectoryMaxUsageRecordings(newValue));
}
return mRecordingSpinner;
}
/**
* Event log directory maximum size threshold spinner
* @return spinner
*/
private Spinner<Integer> getEventLogSpinner()
{
if(mEventLogSpinner == null)
{
mEventLogSpinner = new Spinner<>(100, Integer.MAX_VALUE, mDirectoryPreference.getDirectoryMaxUsageEventLogs(), 100);
mEventLogSpinner.setEditable(true);
mEventLogSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mDirectoryPreference
.setDirectoryMaxUsageEventLogs(newValue));
}
return mEventLogSpinner;
}
private Label getApplicationRootLabel()
{
if(mApplicationRootLabel == null)

View File

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

View File

@@ -0,0 +1,462 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.StuffBitsMessage;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor;
import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.util.ThreadPool;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* APCO25 Phase 1 viewer panel
*/
public class P25P1Viewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(P25P1Viewer.class);
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.p25p1";
private Preferences mPreferences = Preferences.userNodeForPackage(P25P1Viewer.class);
private Button mSelectFileButton;
private Label mSelectedFileLabel;
private TableView<IMessage> mMessageTableView;
private ObservableList<IMessage> mMessages = FXCollections.observableArrayList();
private FilteredList<IMessage> mFilteredMessages = new FilteredList<>(mMessages);
private TextField mSearchText;
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
public P25P1Viewer()
{
setPadding(new Insets(5));
setSpacing(5);
HBox fileBox = new HBox();
fileBox.setMaxWidth(Double.MAX_VALUE);
fileBox.setAlignment(Pos.CENTER_LEFT);
fileBox.setSpacing(5);
HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
HBox filterBox = new HBox();
filterBox.setMaxWidth(Double.MAX_VALUE);
filterBox.setAlignment(Pos.BASELINE_CENTER);
filterBox.setSpacing(5);
Label searchLabel = new Label("Message Filter:");
HBox.setMargin(searchLabel, new Insets(0,0,0,15));
Label findLabel = new Label("Find:");
HBox.setHgrow(getFindText(), Priority.ALWAYS);
HBox.setHgrow(getSearchText(), Priority.ALWAYS);
filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
getSearchText());
VBox.setVgrow(fileBox, Priority.NEVER);
VBox.setVgrow(filterBox, Priority.NEVER);
VBox.setVgrow(getMessageTableView(), Priority.ALWAYS);
getChildren().addAll(fileBox, filterBox, getMessageTableView());
}
/**
* Processes the recording file and loads the content into the viewer
*
* Note: invoke this method off of the UI thread in a thread pool executor and the results will be loaded into the
* message table back on the JavaFX UI thread.
*
* @param file containing a .bits recording of decoded DMR data.
*/
private void load(File file)
{
if(file != null && file.exists())
{
mMessages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
ThreadPool.CACHED.submit(() -> {
List<IMessage> messages = new ArrayList<>();
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
messageFramer.setListener(messageProcessor);
messageProcessor.setMessageListener(message -> {
if(!(message instanceof StuffBitsMessage))
{
messages.add(message);
}
});
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessages.addAll(messages);
getMessageTableView().scrollTo(0);
});
});
}
}
/**
* Updates the filter(s) applies to the list of messages
*/
private void updateFilters()
{
String filterText = getSearchText().getText();
if(filterText != null && !filterText.isEmpty())
{
Predicate<IMessage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
mFilteredMessages.setPredicate(textPredicate);
}
else
{
mFilteredMessages.setPredicate(null);
}
}
/**
* Finds and selects the first row containing the text argument.
* @param text to search for.
*/
private void find(String text)
{
if(text != null && !text.isEmpty())
{
for(IMessage message: mFilteredMessages)
{
if(message.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
return;
}
}
}
}
/**
* Finds and selects the first row containing the text argument, after the currently selected row.
* @param text to search for.
*/
private void findNext(String text)
{
if(text != null && !text.isEmpty())
{
IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem();
if(selected == null)
{
find(text);
return;
}
int row = mFilteredMessages.indexOf(selected);
for(int x = row + 1; x < mFilteredMessages.size(); x++)
{
if(x < mFilteredMessages.size())
{
IMessage message = mFilteredMessages.get(x);
if(message.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
return;
}
}
}
}
}
/**
* List view control with DMR messages
*/
private TableView<IMessage> getMessageTableView()
{
if(mMessageTableView == null)
{
mMessageTableView = new TableView<>();
mMessageTableView.setPlaceholder(getLoadingIndicator());
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
mMessageTableView.setItems(sortedList);
mMessageTableView.setOnKeyPressed(event ->
{
if(KEY_CODE_COPY.match(event))
{
final Set<Integer> rows = new TreeSet<>();
for (final TablePosition tablePosition : mMessageTableView.getSelectionModel().getSelectedCells())
{
rows.add(tablePosition.getRow());
}
final StringBuilder sb = new StringBuilder();
boolean firstRow = true;
for (final Integer row : rows)
{
if(firstRow)
{
firstRow = false;
}
else
{
sb.append('\n');
}
boolean firstCol = true;
for (final TableColumn<?, ?> column : mMessageTableView.getColumns())
{
if(firstCol)
{
firstCol = false;
}
else
{
sb.append('\t');
}
final Object cellData = column.getCellData(row);
sb.append(cellData == null ? "" : cellData.toString());
}
}
final ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(sb.toString());
Clipboard.getSystemClipboard().setContent(clipboardContent);
}
});
TableColumn timestampColumn = new TableColumn();
timestampColumn.setPrefWidth(110);
timestampColumn.setText("Time");
timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp"));
TableColumn validColumn = new TableColumn();
validColumn.setPrefWidth(50);
validColumn.setText("Valid");
validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
TableColumn messageColumn = new TableColumn();
messageColumn.setPrefWidth(1000);
messageColumn.setText("Message");
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
SimpleStringProperty property = new SimpleStringProperty();
if(param.getValue() instanceof IMessage message)
{
property.set(message.toString());
}
return property;
});
mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn);
}
return mMessageTableView;
}
/**
* File selection button
* @return button
*/
private Button getSelectFileButton()
{
if(mSelectFileButton == null)
{
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select DMR .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
{
File file = new File(lastDirectory);
if(file.exists() && file.isDirectory())
{
fileChooser.setInitialDirectory(file);
}
}
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
if(selected != null)
{
mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
load(selected);
}
});
}
return mSelectFileButton;
}
/**
* Spinny loading icon to show over the message table view
*/
private ProgressIndicator getLoadingIndicator()
{
if(mLoadingIndicator == null)
{
mLoadingIndicator = new ProgressIndicator();
mLoadingIndicator.setProgress(-1);
mLoadingIndicator.setVisible(false);
}
return mLoadingIndicator;
}
/**
* Selected file path label.
*/
private Label getSelectedFileLabel()
{
if(mSelectedFileLabel == null)
{
mSelectedFileLabel = new Label(" ");
}
return mSelectedFileLabel;
}
/**
* Search text filter box
* @return text control for entering search text
*/
private TextField getSearchText()
{
if(mSearchText == null)
{
mSearchText = new TextField();
mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mSearchText;
}
/**
* Text box for find text
*/
private TextField getFindText()
{
if(mFindText == null)
{
mFindText = new TextField();
mFindText.setOnKeyPressed(event -> {
if(event.getCode().equals(KeyCode.ENTER))
{
getFindButton().fire();
}
});
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mFindText;
}
/**
* Find button to search for the text in the find text box.
* @return button
*/
private Button getFindButton()
{
if(mFindButton == null)
{
mFindButton = new Button("Find");
mFindButton.setOnAction(event -> find(getFindText().getText()));
}
return mFindButton;
}
/**
* Find next button to search for the text in the find text box.
* @return button
*/
private Button getFindNextButton()
{
if(mFindNextButton == null)
{
mFindNextButton = new Button("Next");
mFindNextButton.setOnAction(event -> findNext(getFindText().getText()));
}
return mFindNextButton;
}
}

View File

@@ -0,0 +1,188 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.viewer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility application to load and view .bits recording file with the messages fully parsed.
*
* Supported Protocols: DMR and APCO25 Phase 1.
*/
public class RecordingViewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class);
private MenuBar mMenuBar;
private TabPane mTabPane;
private int mTabCounterDmr = 1;
private int mTabCounterP25P1 = 1;
/**
* Constructs an instance
*/
public RecordingViewer()
{
VBox.setVgrow(getTabPane(), Priority.ALWAYS);
getChildren().addAll(getMenuBar(), getTabPane());
}
public MenuBar getMenuBar()
{
if(mMenuBar == null)
{
mMenuBar = new MenuBar();
Menu fileMenu = new Menu("File");
Menu createNewViewerMenu = new Menu("New Viewer ...");
MenuItem dmrMenuItem = new MenuItem("DMR");
dmrMenuItem.onActionProperty().set(event -> {
Tab tab = new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer());
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
MenuItem p25p1MenuItem = new MenuItem("P25P1");
p25p1MenuItem.onActionProperty().set(event -> {
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer());
getTabPane().getTabs().add(tab);
getTabPane().getSelectionModel().select(tab);
});
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
MenuItem exitMenu = new MenuItem("Exit");
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
fileMenu.getItems().addAll(createNewViewerMenu, new SeparatorMenuItem(), exitMenu);
mMenuBar.getMenus().add(fileMenu);
}
return mMenuBar;
}
/**
* Tab pane for each viewer instance
*/
public TabPane getTabPane()
{
if(mTabPane == null)
{
mTabPane = new TabPane();
mTabPane.setMaxHeight(Double.MAX_VALUE);
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()));
}
return mTabPane;
}
/**
* Decorates the tab with label renaming feature using double-click or right-click to change text
*/
public class LabeledTab extends Tab
{
private Label mLabel = new Label();
private TextField mTextField = new TextField();
/**
* Constructs an instance
* @param label to use initially
* @param node for content
*/
public LabeledTab(String label, Node node)
{
super(null, node);
mLabel.setText(label);
setGraphic(mLabel);
mLabel.setOnMouseClicked(event -> {
if((event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() > 1) ||
event.getButton().equals(MouseButton.SECONDARY))
{
setGraphic(mTextField);
mTextField.setText(mLabel.getText());
mTextField.selectAll();
mTextField.requestFocus();
}
});
mTextField.setOnAction(event -> {
mLabel.setText(mTextField.getText());
LabeledTab.this.setGraphic(mLabel);
});
mTextField.focusedProperty().addListener((observable, oldValue, newValue) -> {
if(!newValue)
{
mLabel.setText(mTextField.getText());
LabeledTab.this.setGraphic(mLabel);
}
});
}
public LabeledTab(String label)
{
this(label, null);
}
}
public static void main(String[] args)
{
Application viewer = new Application()
{
@Override
public void start(Stage primaryStage) throws Exception
{
Scene scene = new Scene(new RecordingViewer(), 1100, 800);
primaryStage.setTitle("Message Recording Viewer (.bits)");
primaryStage.setScene(scene);
primaryStage.show();
}
};
Runnable r = () -> {
try
{
viewer.start(new Stage());
}
catch(Exception e)
{
mLog.error("Error starting message recording viewer application", e);
}
};
Platform.startup(r);
}
}

View File

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

View File

@@ -32,11 +32,11 @@ public enum Form
CHANNEL_DESCRIPTOR,
CHANNEL_NAME,
CHANNEL_FREQUENCY,
DCS,
DECODER_TYPE,
DTMF,
ENCRYPTION_KEY,
ESN,
FULLY_QUALIFIED_IDENTIFIER,
IPV4_ADDRESS,
KNOX_TONE,
LOCATION,

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.identifier.alias;
import io.github.dsheirer.protocol.Protocol;
/**
* Talker alias value provided by the network for the current talker (ie FROM).
*/
public class P25TalkerAliasIdentifier extends TalkerAliasIdentifier
{
/**
* Constructs an instance.
* @param value of the talker alias
*/
public P25TalkerAliasIdentifier(String value)
{
super(value);
}
@Override
public boolean isValid()
{
return getValue() != null && !getValue().isEmpty();
}
@Override
public Protocol getProtocol()
{
return Protocol.APCO25;
}
public static P25TalkerAliasIdentifier create(String value)
{
return new P25TalkerAliasIdentifier(value);
}
}

View File

@@ -1,69 +1,161 @@
/*
* *****************************************************************************
* 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.identifier.patch;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Temporary grouping or regrouping of talkgroups or individual radios.
*/
public class PatchGroup
{
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroup.class);
private TalkgroupIdentifier mPatchGroupIdentifier;
private List<TalkgroupIdentifier> mPatchedGroupIdentifiers = new ArrayList<>();
private List<TalkgroupIdentifier> mPatchedTalkgroupIdentifiers = new ArrayList<>();
private List<RadioIdentifier> mPatchedRadioIdentifiers = new ArrayList<>();
private int mVersion;
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier)
/**
* Constructs an instance
* @param patchGroupIdentifier for the patch group
* @param version for this patch group identifier. Note: if the patch group identifier is frequently reused, a
* new version number will indicate a new instance of the patch group.
*/
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier, int version)
{
mPatchGroupIdentifier = patchGroupIdentifier;
mVersion = version;
}
/**
* Constructs an instance
* @param patchGroupIdentifier that is the overall identifier for this temporary grouping.
*/
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier)
{
this(patchGroupIdentifier, 0);
}
/**
* Version number for this patch group used to differentiate frequently reused patch group identifiers.
* @return version number (aka super group sequence number).
*/
public int getVersion()
{
return mVersion;
}
/**
* Patch group identifier.
* @return identifier
*/
public TalkgroupIdentifier getPatchGroup()
{
return mPatchGroupIdentifier;
}
public void addPatchedGroup(TalkgroupIdentifier patchedGroupIdentifier)
/**
* Indicates if this patch group contains any patched talkgroups.
* @return true if there are patched talkgroups.
*/
public boolean hasPatchedTalkgroups()
{
if(!mPatchedGroupIdentifiers.contains(patchedGroupIdentifier))
return !getPatchedTalkgroupIdentifiers().isEmpty();
}
/**
* Indicates if this patch group contains any patched radio identifiers.
* @return true if there are patched radios.
*/
public boolean hasPatchedRadios()
{
return !getPatchedRadioIdentifiers().isEmpty();
}
/**
* Adds the talkgroup identifier to this patched group.
* @param patchedGroupIdentifier to add
*/
public void addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
{
if(!mPatchedTalkgroupIdentifiers.contains(patchedGroupIdentifier))
{
mPatchedGroupIdentifiers.add(patchedGroupIdentifier);
mPatchedTalkgroupIdentifiers.add(patchedGroupIdentifier);
}
}
public void addPatchedGroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
/**
* Adds a list of talkgroup identifiers to this patched group.
* @param patchedGroupIdentifiers to add
*/
public void addPatchedTalkgroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
{
for(TalkgroupIdentifier identifier : patchedGroupIdentifiers)
{
addPatchedGroup(identifier);
addPatchedTalkgroup(identifier);
}
}
public List<TalkgroupIdentifier> getPatchedGroupIdentifiers()
/**
* List of patched group identifiers that are part of this patch group.
* @return patched talkgroups.
*/
public List<TalkgroupIdentifier> getPatchedTalkgroupIdentifiers()
{
return mPatchedGroupIdentifiers;
return mPatchedTalkgroupIdentifiers;
}
/**
* Adds the radio identifier to this patched group.
* @param patchedRadioIdentifier to add
*/
public void addPatchedRadio(RadioIdentifier patchedRadioIdentifier)
{
if(!mPatchedRadioIdentifiers.contains(patchedRadioIdentifier))
{
mPatchedRadioIdentifiers.add(patchedRadioIdentifier);
}
}
/**
* Adds a list of radio identifiers to this patched group.
* @param patchedRadioIdentifiers to add
*/
public void addPatchedRadios(List<RadioIdentifier> patchedRadioIdentifiers)
{
for(RadioIdentifier identifier : patchedRadioIdentifiers)
{
addPatchedRadio(identifier);
}
}
/**
* List of patched radio identifiers that are part of this patch group.
* @return patched radios.
*/
public List<RadioIdentifier> getPatchedRadioIdentifiers()
{
return mPatchedRadioIdentifiers;
}
@Override
@@ -72,9 +164,14 @@ public class PatchGroup
StringBuilder sb = new StringBuilder();
sb.append("P:").append(getPatchGroup());
if(!getPatchedGroupIdentifiers().isEmpty())
if(hasPatchedTalkgroups())
{
sb.append(" ").append(getPatchedGroupIdentifiers());
sb.append(" ").append(getPatchedTalkgroupIdentifiers());
}
if(hasPatchedRadios())
{
sb.append(" ").append(getPatchedRadioIdentifiers());
}
return sb.toString();

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -284,11 +284,27 @@ public class DMRBurstFramer implements Listener<Dibit>
}
else
{
//When we don't have sufficient timeslot tracking data, dump the messages to timeslot 0 so that they
//don't corrupt the decoder states for either timeslot, until we positively regain timeslot tracking
//When we don't have sufficient timeslot tracking data, we either have misaligned timeslots from a
//repeater or we have some form of mobile burst. If mobile, process to the correct timeslto, otherwise
//dump the messages to timeslot 0 so that the burst doesn't corrupt the decoder states for either
// timeslot, until we positively regain timeslot tracking
if(burst1 != null)
{
mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 0);
DMRSyncPattern sync1 = mSyncTrackerTimeslot1.getSyncPattern();
if(sync1.isMobileStationSyncPattern() || sync1.isDirectModeTS1())
{
mBurstDetectListener.burstDetected(burst1, sync1, 1);
}
else if(sync1.isDirectModeTS2())
{
mBurstDetectListener.burstDetected(burst1, sync1, 2);
}
else
{
mBurstDetectListener.burstDetected(burst1, sync1, 0);
}
mDibitCounter -= BURST_DIBIT_LENGTH;
}
else
@@ -298,7 +314,21 @@ public class DMRBurstFramer implements Listener<Dibit>
if(burst2 != null)
{
mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 0);
DMRSyncPattern sync2 = mSyncTrackerTimeslot2.getSyncPattern();
if(sync2.isMobileStationSyncPattern() || sync2.isDirectModeTS1())
{
mBurstDetectListener.burstDetected(burst2, sync2, 1);
}
else if(sync2.isDirectModeTS2())
{
mBurstDetectListener.burstDetected(burst2, sync2, 2);
}
else
{
mBurstDetectListener.burstDetected(burst2, sync2, 0);
}
mDibitCounter -= BURST_DIBIT_LENGTH;
}
else

View File

@@ -42,8 +42,11 @@ import io.github.dsheirer.sample.complex.IComplexSamplesListener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.wave.ComplexWaveSource;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -332,4 +335,38 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
{
return DMRDecoder.this;
}
public static void main(String[] args)
{
File file = new File("/home/denny/Downloads/TIII,HYT414.585000,.wav");
try
{
ComplexWaveSource source = new ComplexWaveSource(file);
source.open();
System.out.println("Source Sample Rate: " + source.getSampleRate());
DMRDecoder decoder = new DMRDecoder(new DecodeConfigDMR());
decoder.setMessageListener(message -> System.out.println("TS:" + message.getTimeslot() + " " + message));
source.setListener(iNativeBuffer -> {
Iterator<ComplexSamples> it = iNativeBuffer.iterator();
while(it.hasNext())
{
ComplexSamples samples = it.next();
decoder.receive(samples);
}
});
decoder.setSampleRate(source.getSampleRate());
decoder.start();
source.start();
while(true)
{
source.next(65535);
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}

View File

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

View File

@@ -27,6 +27,7 @@ import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement;
@@ -44,6 +45,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBloc
import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequenceAssembler;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceSuperFrameProcessor;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.List;
@@ -59,6 +62,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
{
private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class);
private DecodeConfigDMR mConfigDMR;
private VoiceSuperFrameProcessor mSuperFrameProcessor1 = new VoiceSuperFrameProcessor();
private VoiceSuperFrameProcessor mSuperFrameProcessor2 = new VoiceSuperFrameProcessor();
private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1);
private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2);
private MBCAssembler mMBCAssembler = new MBCAssembler();
@@ -67,6 +72,7 @@ public class DMRMessageProcessor implements Listener<IMessage>
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
private Listener<IMessage> mMessageListener;
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
private DmrCrcMaskManager mCrcMaskManager = new DmrCrcMaskManager();
/**
* Constructs an instance
@@ -89,19 +95,64 @@ public class DMRMessageProcessor implements Listener<IMessage>
@Override
public void receive(IMessage message)
{
//Enrich messages that carry DMR Logical Slot Number channels with LCN to frequency mappings
if(message == null)
{
return;
}
//Detect and correct messages employing an alternate CRC mask pattern.
if(!message.isValid() && message instanceof CSBKMessage csbk)
{
mCrcMaskManager.check(csbk);
}
if(message instanceof FullLCMessage flc)
{
if(flc.getTimeslot() == 1)
{
mSuperFrameProcessor1.process(flc);
}
else
{
mSuperFrameProcessor2.process(flc);
}
}
else if(message instanceof VoiceMessage voiceMessage)
{
if(voiceMessage.getTimeslot() == 1)
{
mSuperFrameProcessor1.process(voiceMessage);
}
else
{
mSuperFrameProcessor2.process(voiceMessage);
}
}
else if(message instanceof DMRBurst dmrBurst && dmrBurst.isValid())
{
if(dmrBurst.getTimeslot() == 1)
{
mSuperFrameProcessor1.reset();
}
else
{
mSuperFrameProcessor2.reset();
}
}
//Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings
if(message instanceof ITimeslotFrequencyReceiver)
{
ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
int[] lsns = receiver.getLogicalTimeslotNumbers();
int[] lcns = receiver.getLogicalChannelNumbers();
List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>();
for(int lsn: lsns)
for(int lcn: lcns)
{
if(mTimeslotFrequencyMap.containsKey(lsn))
if(mTimeslotFrequencyMap.containsKey(lcn))
{
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lsn));
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn));
}
}
@@ -115,10 +166,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
dispatch(message);
//Extract the Full Link Control message fragment from the Voice with embedded signalling message
if(message instanceof VoiceEMBMessage)
if(message instanceof VoiceEMBMessage voice)
{
VoiceEMBMessage voice = (VoiceEMBMessage)message;
if(message.getTimeslot() == 1)
{
FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr;
@@ -41,14 +38,13 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusCo
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel;
import io.github.dsheirer.module.decode.dmr.message.type.Model;
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tracks the network configuration details of a DMR network from the broadcast messages
@@ -250,7 +246,7 @@ public class DMRNetworkConfigurationMonitor
ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk;
mNeighborSites.addAll(cpnr.getNeighbors());
}
if(mBrand == null)
if(mBrand == null || mBrand != BRAND_MOTOROLA_CONNECT_PLUS)
{
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
}
@@ -279,7 +275,7 @@ public class DMRNetworkConfigurationMonitor
*/
private void addDmrChannel(DMRChannel dmrChannel)
{
mObservedChannelMap.put(dmrChannel.getLogicalSlotNumber(), dmrChannel);
mObservedChannelMap.put(dmrChannel.getValue(), dmrChannel);
}
public void reset()

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -91,9 +91,17 @@ public enum DMRSyncPattern
private static final EnumSet<DMRSyncPattern> CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F);
//Direct Mode Sync Patterns
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_PATTERNS = EnumSet.of(DIRECT_MODE_DATA_TIMESLOT_1,
DIRECT_MODE_DATA_TIMESLOT_2, DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2);
//Direct Mode TS 1 Sync Patterns
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS1_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_1,
DIRECT_MODE_DATA_TIMESLOT_1);
//Direct Mode TS 2 Sync Patterns
private static final EnumSet<DMRSyncPattern> DIRECT_MODE_TS2_PATTERNS = EnumSet.of(DIRECT_MODE_VOICE_TIMESLOT_2,
DIRECT_MODE_DATA_TIMESLOT_2);
//Mobile Station Sync Patterns
private static final EnumSet<DMRSyncPattern> MOBILE_STATION_PATTERNS = EnumSet.of(MOBILE_STATION_DATA,
MOBILE_STATION_VOICE, MS_VOICE_FRAME_B, MS_VOICE_FRAME_C, MS_VOICE_FRAME_D, MS_VOICE_FRAME_E, MS_VOICE_FRAME_F);
/**
* Pattern that represents the enum entry
@@ -140,7 +148,43 @@ public enum DMRSyncPattern
*/
public boolean isDirectMode()
{
return DIRECT_MODE_PATTERNS.contains(this);
return isDirectModeTS1() || isDirectModeTS2();
}
/**
* Indicates if this is Direct Mode sync pattern for timestot 1
* @return true if DMTS1
*/
public boolean isDirectModeTS1()
{
return DIRECT_MODE_TS1_PATTERNS.contains(this);
}
/**
* Indicates if this is Direct Mode sync pattern for timestot 2
* @return true if DMTS2
*/
public boolean isDirectModeTS2()
{
return DIRECT_MODE_TS2_PATTERNS.contains(this);
}
/**
* Indicates if this is a mobile station sync pattern
* @return true if a mobile station sync pattern.
*/
public boolean isMobileStationSyncPattern()
{
return MOBILE_STATION_PATTERNS.contains(this);
}
/**
* Indicates if this is a mobile sync pattern, either mobile station or direct mode.
* @return true if a mobile sync pattern.
*/
public boolean isMobileSyncPattern()
{
return isMobileStationSyncPattern() || isDirectMode();
}
/**

View File

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

View File

@@ -0,0 +1,232 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages alternate CRC masks that may be employed in a system.
*/
public class DmrCrcMaskManager
{
private Logger LOGGER = LoggerFactory.getLogger(DmrCrcMaskManager.class);
private Map<Integer, ResidualCrcMaskTracker> mCsbkResidualTrackerMap = new TreeMap<>();
private int mDominantMask = 0;
/**
* Constructs an instance
*/
public DmrCrcMaskManager()
{
}
/**
* Creates a deep copy (ie clone) of all tracked values
* @return list of tracker clones.
*/
public List<ResidualCrcMaskTracker> cloneTrackers()
{
List<ResidualCrcMaskTracker>trackers = new ArrayList<>();
for(ResidualCrcMaskTracker tracker: mCsbkResidualTrackerMap.values())
{
trackers.add(tracker.clone());
}
return trackers;
}
/**
* Logs the current tracked CRC values and counts.
*/
public void log()
{
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
Collections.sort(trackers);
StringBuilder sb = new StringBuilder();
sb.append("DMR CRC Mask Manager - Current Tracked Values\n");
for(ResidualCrcMaskTracker tracker: trackers)
{
sb.append("\tTracked Value: ").append(String.format("0x%04X", tracker.getTrackedValue()));
sb.append(" Count:").append(tracker.getCount());
sb.append(" Last Observed: " + new Date(tracker.getLastUpdated())).append("\n");
}
LOGGER.info(sb.toString());
}
/**
* Checks the CSBK message that has a failed CRC checksum to detect if an alternate CRC mask value is employed and
* if so, attempts to correct the message by using the alternate mask value once the alternate mask value is
* detected to be used consistently enough.
* @param csbk to re-check.
*/
public void check(CSBKMessage csbk)
{
if(!csbk.isValid())
{
int residual = CRCDMR.calculateResidual(csbk.getMessage(), 0, 80);
if(mCsbkResidualTrackerMap.containsKey(residual))
{
mCsbkResidualTrackerMap.get(residual).increment();
}
else
{
mCsbkResidualTrackerMap.put(residual, new ResidualCrcMaskTracker(residual));
}
List<ResidualCrcMaskTracker> trackers = new ArrayList<>(mCsbkResidualTrackerMap.values());
Collections.sort(trackers);
if(trackers.size() > 5)
{
mCsbkResidualTrackerMap.remove(trackers.get(0).getTrackedValue());
}
if(trackers.size() > 0)
{
ResidualCrcMaskTracker dominant = trackers.get(trackers.size() - 1);
if(dominant.isValid())
{
mDominantMask = dominant.getTrackedValue();
}
}
if(mDominantMask != 0)
{
csbk.checkCRC(mDominantMask);
//
// if(csbk.isValid())
// {
// LOGGER.info("CSBK fixed using alternate mask: " + Integer.toHexString(mDominantMask).toUpperCase());
// }
}
}
}
/**
* Residual CRC mask value tracker. Tracks the number of observances of a residual CRC calculated value to detect
* when an alternate CRC mask pattern is employed and automatically correct CRC values for messages.
*/
public class ResidualCrcMaskTracker implements Comparable<ResidualCrcMaskTracker>
{
private int mTrackedValue;
private int mCount;
private long mLastUpdated;
/**
* Constructs an instance for the specified residual, sets the count to one and sets the last update to now.
* @param residual value to track.
*/
public ResidualCrcMaskTracker(int residual)
{
mTrackedValue = residual;
mCount = 1;
mLastUpdated = System.currentTimeMillis();
}
/**
* Create a deep copy of this instance.
* @return cloned instance.
*/
public ResidualCrcMaskTracker clone()
{
ResidualCrcMaskTracker clone = new ResidualCrcMaskTracker(getTrackedValue());
clone.mCount = mCount;
clone.mLastUpdated = mLastUpdated;
return clone;
}
/**
* Count of number of times this residual has been observed.
* @return count
*/
public int getCount()
{
return mCount;
}
/**
* Indicates that the residual value has been observed at least 3x times indicating that it is possibly valid.
* @return
*/
public boolean isValid()
{
return mCount >= 3;
}
/**
* Timestamp when this value was last observed.
*/
public long getLastUpdated()
{
return mLastUpdated;
}
/**
* Residual tracked value.
* @return residual
*/
public int getTrackedValue()
{
return mTrackedValue;
}
public void increment()
{
mCount++;
//Prevent rollover by keeping value at max integer value.
if(mCount < 0)
{
mCount = Integer.MAX_VALUE;
}
mLastUpdated = System.currentTimeMillis();
}
/**
* Custom sort order by count and then by last updated timestamp.
* @param o the object to be compared.
* @return
*/
@Override
public int compareTo(ResidualCrcMaskTracker o)
{
int comparison = Integer.compare(getCount(), o.getCount());
if(comparison == 0)
{
comparison = Long.compare(getLastUpdated(), o.getLastUpdated());
}
return comparison;
}
}
}

View File

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

View File

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

View File

@@ -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.module.decode.dmr.channel;

View File

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

View File

@@ -1,90 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.channel;
/**
* DMR logical channel. This channel uses a logical channel number and a timeslot.
*/
public class DMRLogicalChannel extends DMRChannel
{
private TimeslotFrequency mTimeslotFrequency;
/**
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
* calculated logical slot value for visual compatibility for users.
*
* @param channel number or repeater number
* @param logicalSlotNumber - zero based index.
*/
public DMRLogicalChannel(int channel, int timeslot)
{
super(channel, timeslot);
}
/**
* Downlink frequency
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
*/
@Override
public long getDownlinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getDownlinkFrequency();
}
return 0;
}
/**
* Uplink frequency
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
*/
@Override
public long getUplinkFrequency()
{
if(mTimeslotFrequency != null)
{
return mTimeslotFrequency.getUplinkFrequency();
}
return 0;
}
/**
* Sets the timeslot frequency mapping
* @param timeslotFrequency
*/
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
{
mTimeslotFrequency = timeslotFrequency;
}
/**
* Formatted channel number
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("LSN:").append(getLogicalSlotNumber());
// sb.append(" LCN:").append(getValue());
return sb.toString();
}
}

View File

@@ -0,0 +1,72 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.channel;
/**
* DMR Logical Slot Number (LSN) channel.
*
* Note: LSN 1-16 are represented in memory using the following channel and timeslot values:
*
* LSN: CHANNEL/TIMESLOT
* 1: 1/1
* 2: 1/2
* 3: 2/1
* 4: 2/2
* 5: 3/1
* 6: 3/2
* 7: 4/1
* 8: 4/2
* 9: 5/1
* 10: 5/2
* 11: 6/1
* 12: 6/2
* 13: 7/1
* 14: 7/2
* 15: 8/1
* 16: 8/2
*/
public class DMRLsn extends DMRChannel
{
/**
* Constructs an instance
*
* @param lsn in range 1 - 16
*/
public DMRLsn(int lsn)
{
super(((lsn - 1) / 2) + 1, ((lsn - 1) % 2) + 1);
}
@Override
public String toString()
{
return "LSN:" + getLsn() + " LCN:" + getChannelNumber();
}
/**
* Logical Slot Number
* @return lsn
*/
public int getLsn()
{
return ((getChannelNumber() - 1) * 2) + getTimeslot();
}
}

View File

@@ -1,16 +1,35 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.channel;
/**
* DMR Tier III Trunking Channel
* DMR Tier III Trunking channel. This channel uses a logical channel number and a timeslot.
*/
public class DMRTier3Channel extends DMRLogicalChannel
public class DMRTier3Channel extends DMRChannel
{
/**
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
* calculated logical slot value for visual compatibility for users.
*
* @param channel number or repeater number
* @param timeslot
* @param timeslot either 1 or 2.
*/
public DMRTier3Channel(int channel, int timeslot)
{
@@ -18,23 +37,22 @@ public class DMRTier3Channel extends DMRLogicalChannel
}
/**
* Logical slot number for this channel.
*
* Formula: LSN = (channel * 2) + timeslot
*
* @return logical slot number, a 1-based index value
* Tier III Channel ID
* @return channel ID
*/
@Override
public int getLogicalSlotNumber()
public int getChannelId()
{
int repeater = getRepeater();
if(repeater > 0)
{
return ((repeater) * 2) + getTimeslot();
}
return 0;
return getChannelNumber() * 2 + getTimeslot();
}
/**
* Formatted channel number
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(" LCN:").append(getChannelNumber());
sb.append(" CHANID:").append(getChannelId());
return sb.toString();
}
}

View File

@@ -0,0 +1,42 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.channel;
/**
* DMR Rest Channel logical slot number (LSN).
*/
public class DmrRestLsn extends DMRLsn
{
/**
* Constructs an instance
*
* @param lsn in range 1 - 16
*/
public DmrRestLsn(int lsn)
{
super(lsn);
}
@Override
public String toString()
{
return "REST:" + getLsn();
}
}

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.identifier;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.location.LocationIdentifier;
import io.github.dsheirer.identifier.location.Point;
import io.github.dsheirer.protocol.Protocol;
/**
* P25 location identifier for user radio reported GPS positions.
*/
public class P25Location extends LocationIdentifier
{
/**
* Constructs an instance
*
* @param value location
* @param role to or from
*/
public P25Location(Point value, Role role)
{
super(value, role);
}
@Override
public Protocol getProtocol()
{
return Protocol.APCO25;
}
/**
* Utility method to create a new DMR location
* @param latitude of the position
* @param longitude of the position
* @return constructed DMR location identifier
*/
public static P25Location createFrom(double latitude, double longitude)
{
Point point = new Point(latitude, longitude);
return new P25Location(point, Role.FROM);
}
}

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