Compare commits

...

458 Commits
1.0 ... master

Author SHA1 Message Date
Cort Buffington a55f73d059 RadioID changes 2019-05-09 09:31:47 -05:00
Steve Zingman 1c684486ca
Merge pull request #27 from lorenzocipriani/master
Commented some cp commands on moved files
2019-04-15 09:42:59 -04:00
Lorenzo Cipriani 67fd6d62a5 Updated install script to work on RedHat and Debian derivates 2019-04-14 23:39:23 +00:00
Lorenzo Cipriani 785f44e7e6 Commented some cp commands on moved files 2019-04-14 18:35:58 +01:00
Cort Buffington c023d4a565 Change ID file format to JSON 2019-03-02 13:26:10 -06:00
Cort Buffington ad399792c9 Update dmrlink_config.py 2018-11-26 10:19:35 -06:00
Cort Buffington 198278c288 Update dmrlink_SAMPLE.cfg
because we can't just have one place to get config files that doens't change constantly
2018-10-12 22:58:06 -05:00
Cort Buffington 2520f394ed update 2018-10-08 22:12:56 -05:00
Cort Buffington bc59c75eb0 Added IPv6 Support
Someone made a comment… I added it.
2018-06-15 12:14:10 -05:00
Cort Buffington 01a3fff754
Fixing DMR-MARC database changes
....again... and again...
2018-04-16 09:09:47 -05:00
Cort Buffington 2e1a4c3a58 Revert "Accommodate DMR-MARC JSON only database changes"
This reverts commit 59a59e4100.
2018-04-16 08:46:42 -05:00
Cort Buffington 59a59e4100 Accommodate DMR-MARC JSON only database changes
Talkgroup files are samples – pick one format or the other if you want
those aliases. this changes the type of dump you do with DMR-MARC and
the way you save files locally. CSV still works, but DMR-MARC will stop
supporting it!!! DO NOT mix .csv filenames and .json downloads. The
extension must match the file data type.
2018-03-16 20:29:46 -05:00
Cort Buffington 81c3467ec0 Clean up formatting 2018-03-12 21:29:05 -05:00
Cort Buffington 25d6bc08d0 cleaned up superfluous spaces 2018-03-12 21:26:33 -05:00
Cort Buffington e8311b1f54 Last commit was MESSED UP! 2018-03-12 19:32:20 -05:00
Cort Buffington cd11397416 Improve IPSC/DMR Re-Write 2018-03-11 17:37:22 -05:00
Cort Buffington cdd65d8edf Added support for IPSC "Trunks"
A “TRUNK” is a variant on IPSC unique to this project. They are used to
pass traffic between DMRlink instances. They do no contention handling
regarding TS/TGID, they take any traffic sent to them.
2018-02-05 12:27:44 -06:00
Cort Buffington 2b63b5c111 Fix Bug where confbridge.py would not start if there was not an ACL defined. 2018-02-05 11:20:42 -06:00
Cort Buffington 3fc0bdc63d
Update playback_config_SAMPLE.py 2018-01-26 07:49:35 -06:00
Cort Buffington f5bc547d4d
Update DO_NOT_README.md 2018-01-08 13:29:34 -06:00
Cort Buffington 0acc6042e8
Update DO_NOT_README.md 2018-01-08 13:29:02 -06:00
Cort Buffington b4ab2d31a3
Update DO_NOT_README.md 2018-01-08 13:27:55 -06:00
Cort Buffington 5950240787
Update DO_NOT_README.md 2018-01-08 13:24:37 -06:00
Steve Zingman 63611f2c6c
Fix typo 2017-12-13 12:42:08 -05:00
Steve Zingman eb2ffe4ecb
Create README.MD 2017-12-09 07:58:53 -05:00
Cort Buffington bf699dcfbc Move retired file 2017-08-29 07:42:34 -05:00
Steve N4IRS c6a543527f Move retired applications to a seperate directory 2017-08-25 15:27:53 -04:00
Steve Zingman 3279aeb527 bitstring and bitarray moved to dmr_utils setup.py 2017-08-22 09:24:40 -04:00
Cort Buffington f216300539 Logic error where RESET TGID caused unintended action 2017-07-30 11:02:37 -05:00
Cort Buffington 8e858e48a2 Missed building hex values for RESET TGID Addition (see last) 2017-07-30 10:52:48 -05:00
Cort Buffington c16d549e94 Added additional way to reset running timers
Added a list of RESET TGIDs used, additionally, to reset running
timers. All previous actions still work the same way, this is only
ANOTHER way to reset timers.
2017-07-30 10:29:52 -05:00
Cort Buffington ae9f71d715 Merge branch 'socket-reporting' 2017-06-29 13:03:33 -05:00
Cort Buffington 4ac862b93c update ACL format and action to add "ranges" 2017-06-29 13:02:53 -05:00
Cort Buffington 8fbd7ccf33 more formatting.... UGH! 2017-06-28 16:35:51 -05:00
Cort Buffington b8d1449d2f format fix from last update. 2017-06-28 16:32:23 -05:00
Cort Buffington c8804d7231 Network logging format update
Make it a format that webtables.py can more easily re-format/manipulate.
2017-06-28 16:30:15 -05:00
Cort Buffington 7fde9d1ce8 Fixing syntax problems in network logging. Incremental 2017-06-28 16:22:38 -05:00
Cort Buffington d1143ddb7c bug fix - last update 2017-06-28 16:11:23 -05:00
Cort Buffington 0079ad1baa Change Network Logging Format 2017-06-28 16:07:29 -05:00
Cort Buffington 547d6e23ed Merge branch 'master' into socket-reporting 2017-06-28 16:03:42 -05:00
Cort Buffington 62fc209b4f fix TS parsing during trigger action 2017-06-23 16:04:45 -05:00
Cort Buffington a3a296161a Merge pull request #23 from M0NWI/master
Add files via upload
2017-06-13 08:26:17 -05:00
M0NWI 7edb7e26d5 Add files via upload
Corrected wrong copy too directory for the proxy, not RCM, probably a cut and paste issue.
2017-06-13 13:55:05 +01:00
M0NWI 018ecfebc7 Add files via upload
Fixed issue where ACL list and action were both being imported as ACTION.
2017-06-13 13:46:17 +01:00
Steve N4IRS 36a7092bfb Generalize the install source directory location 2017-05-29 07:56:32 -04:00
Steve N4IRS 0fe54ad948 Update install script and fix missing section 2017-05-29 07:25:09 -04:00
Cort Buffington 702159d20c Update copyright 2017-05-27 09:00:54 -05:00
Cort Buffington fa8e53bd81 Merge branch 'socket-reporting' 2017-05-27 08:59:21 -05:00
Cort Buffington ff0801ba54 Merge branch 'master' into socket-reporting 2017-05-27 08:58:56 -05:00
Cort Buffington e3070c5bb7 logging of report server connection attempts 2017-05-25 14:19:00 -05:00
Cort Buffington 10cec77baa Add report client wildcard 2017-05-25 14:08:52 -05:00
Cort Buffington d6e18737d0 ONLINE SUPPORT FORUM 2017-05-24 09:49:06 -05:00
Cort Buffington 8cfdab8614 ONLINE SUPPORT FORUM 2017-05-24 09:42:02 -05:00
Cort Buffington f55ac5e407 Update - I'm back! 2017-05-19 11:47:16 -05:00
Cort Buffington 509b3b1173 Remove debugging code 2017-05-17 20:56:12 -05:00
Cort Buffington 0d87ffa2df really fixed this time. 2017-05-17 15:46:18 -05:00
Cort Buffington d5811b5228 fix formatting of time 2017-05-17 15:45:26 -05:00
Cort Buffington 353ce2be79 time formatting fix 2017-05-17 15:37:40 -05:00
Cort Buffington 7a3157fcfa fix time report formatting 2017-05-17 15:35:37 -05:00
Cort Buffington 4c8a42bd81 bug fix - time formatting 2017-05-17 15:31:57 -05:00
Cort Buffington 202280e8d9 Fix Compatibility with Brandmeister
apparently they don’t increment the call sequence ID on each
transmission, you know, like the standard says to do.
2017-05-17 15:22:24 -05:00
Cort Buffington 7a9ece180a DEBUGGING - TEMP CHANGES 2017-05-17 12:54:58 -05:00
Cort Buffington 00bbb77d8e DEBUGGING: TEMP CHANGES 2017-05-17 12:46:54 -05:00
Cort Buffington ae6f2fdeec debugging version - TEMP ADDITIONS 2017-05-17 12:45:06 -05:00
Cort Buffington 55da4e3c6e added event reporting 2017-05-17 09:44:02 -05:00
Cort Buffington a7390de248 add event reporting 2017-05-17 09:32:48 -05:00
Cort Buffington 01916c3197 Dumb forwarder 2017-05-17 08:30:59 -05:00
Cort Buffington 1f4fedb432 fix bug in directory names 2017-05-16 16:55:08 -05:00
Cort Buffington 8f971496cd cleanup 2017-05-16 13:50:56 -05:00
Cort Buffington 05c0e71773 More cleanup... 2017-05-15 15:29:16 -05:00
Cort Buffington 04ce07dc24 Cleanup 2017-05-15 14:55:06 -05:00
Cort Buffington a8703e058c Updates.... 2017-05-15 14:31:34 -05:00
Cort Buffington d5ff0ff0bf Merge pull request #22 from n0mjs710/socket-reporting
Socket reporting
2017-05-01 16:46:56 -05:00
Cort Buffington 86b2160330 better example 2017-05-01 16:45:43 -05:00
Cort Buffington 65df5a2ab7 update sample for network reporting 2017-05-01 16:44:28 -05:00
Cort Buffington 934d349532 cleanup 2017-05-01 15:43:21 -05:00
Cort Buffington b833461a1c incremental progress -- not ready for prime time 2017-05-01 12:20:34 -05:00
Cort Buffington 6caa10d083 BROKEN CODE, but making process 2017-04-29 08:55:15 -05:00
Cort Buffington 87092c79a8 socket-based reporting branch 1st upload 2017-04-27 15:37:41 -05:00
Steve N4IRS 5bebad3e1b Add command line support for LOG_LEVEL and LOG_HANDLERS 2017-04-26 15:51:30 -04:00
Cort Buffington 42cb1fa4c7 fixed logging bug in call contention handler
No functionality was broken, just the logger line.
2017-04-14 07:23:18 -05:00
MrBungle42 95d3bb30c1 Update ambe_audio.py 2017-04-06 08:40:42 -04:00
MrBungle42 8d2365eb89 Update ambe_audio.py 2017-04-05 23:33:58 -04:00
MrBungle42 48ab7572ff Update ambe_audio.py 2017-04-05 17:18:50 -04:00
MrBungle42 11695ddb63 Update ambe_audio.py 2017-04-05 16:31:39 -04:00
Steve N4IRS eda37bb7c8 Fix self.playbackFromUDP per N4IRR 2017-04-05 09:45:28 -04:00
Steve N4IRS 45463b73b8 Fix formatting 2017-04-05 06:32:41 -04:00
Steve N4IRS 78994ef523 Fix formatting 2017-04-05 06:29:15 -04:00
Steve N4IRS 5cb477fed1 add a dummy parameter to the method launchUDP 2017-04-04 10:11:31 -04:00
Steve N4IRS f5c0fcdc60 Make script executable 2017-04-03 13:33:33 -04:00
Steve N4IRS aaaf85b063 Typo and minor expansion 2017-03-30 14:33:57 -04:00
Steve N4IRS 7071cc629f Fix typo 2017-03-29 09:08:54 -04:00
Cort Buffington 9a27b5185c Squashed unknown packet type callback bug 2017-03-28 13:36:04 -05:00
Cort Buffington 56635d7e66 Don't repeat timer cancel actions if unneeded. 2017-03-24 08:51:48 -05:00
Cort Buffington 9d379bf3c4 cancel timers when not in use 2017-03-24 08:44:00 -05:00
Cort Buffington ed06b92ef7 Start system with timers off 2017-03-24 08:38:34 -05:00
Cort Buffington dd3eb5d96e Squashed some timer bugs 2017-03-22 21:14:35 -05:00
Cort Buffington f45d9cd829 General Progress 2017-03-21 09:57:19 -05:00
Cort Buffington 1fdd8d3697 Prep for Web-based stats 2017-03-20 09:27:42 -05:00
Cort Buffington 49d7c1f484 Bug Fix - message_types to const
import of the constants naming convention didn’t get changed when the
global naming change occurred.
2017-03-20 07:51:39 -05:00
Cort Buffington 1bd31f09f5 fixed minor reference errors 2017-02-22 10:13:51 -06:00
Cort Buffington 3063b35eb6 formatting update 2017-02-17 09:25:38 -06:00
Cort Buffington a86ba63d2d formatting update 2017-02-16 22:27:34 -06:00
Cort Buffington f29a01290e use dmr_utils 2017-02-16 22:14:28 -06:00
Cort Buffington da882f086d Web-based Stats Display 2017-02-16 21:16:00 -06:00
Cort Buffington 8ac1be5f35 Update README.md 2017-02-01 09:10:38 -06:00
Cort Buffington a693d70ca7 Update DO_NOT_README.md 2017-02-01 09:07:35 -06:00
Cort Buffington 8c038689a8 Update README.md 2017-02-01 09:07:13 -06:00
Cort Buffington 4161c6c717 Create DO_NOT_README.md 2017-02-01 09:06:29 -06:00
Cort Buffington 4f63e04a5b Update README.md 2017-02-01 09:00:09 -06:00
Cort Buffington b0313e2361 Update README.md 2017-02-01 08:58:37 -06:00
Cort Buffington b8abc44d20 Update README.md 2017-02-01 08:53:28 -06:00
Steve N4IRS ae8f24d4f0 Fix doucments directory creation 2017-01-31 14:48:16 -05:00
Steve N4IRS 642f6fb27c Make script executable and fix typo 2017-01-31 13:20:12 -05:00
Steve N4IRS 7f5f6c81f4 Updated for dmr_utils 2017-01-30 15:24:36 -05:00
Steve N4IRS a08a155433 Updated for dmr_utils 2017-01-30 15:20:51 -05:00
Cort Buffington 3918e3bc36 Fixed Typo 2017-01-16 09:54:09 -06:00
N4IRS 11706b6ea5 Typo 2017-01-05 15:04:45 -05:00
N4IRS dd56cc40d9 Rename files and tweaks 2017-01-05 15:01:32 -05:00
Cort Buffington ea606b0424 Clean-up, re-arranging 2016-12-22 12:53:08 -06:00
Cort Buffington cc2adcffae Removed debugging from previous update 2016-12-21 21:09:33 -06:00
Cort Buffington c61f913514 Fixed contention handler GROUP_TIMEOUT checking
when the contention handler checked the GROUP_TIMEOUT value before
transmitting, it was using the SOURCE value, not the destination.
That’s now fixed.
2016-12-21 21:08:58 -06:00
Cort Buffington 614fdb030b Delete confbridge_rules.py 2016-12-21 21:01:22 -06:00
Cort Buffington dbc7d923e1 Fixed Activation/Deactivation Logic 2016-12-21 21:00:54 -06:00
Cort Buffington 80941c9608 bug fix 2016-12-21 19:59:16 -06:00
Cort Buffington d7070dacf9 Bug fix 2016-12-21 19:56:07 -06:00
Cort Buffington f566cce1ca Preparing for confbridge app. WARNING - CONFIG ADDITION!!! 2016-12-21 19:55:53 -06:00
Cort Buffington 8f724d1f51 fix logger for de-registration 2016-12-19 18:49:36 -06:00
Cort Buffington fe7a3bf56d fix action if ACL missing 2016-12-19 18:43:42 -06:00
Cort Buffington 444bedb3eb Update 2016-12-19 07:55:11 -06:00
Cort Buffington 9515790ab6 Update 2016-12-18 21:59:58 -06:00
Cort Buffington 8dbba4b42d Merge branch 'modularization' 2016-12-18 21:59:01 -06:00
Cort Buffington 53c4ff5b23 ALL ARE WORKING EXCEPT ambe_audio.py 2016-12-18 21:51:13 -06:00
Cort Buffington 9c6de3b902 update - incomplete 2016-12-16 08:10:33 -06:00
Cort Buffington a8c868dc26 NON-WORKING 2016-12-15 16:26:48 -06:00
Cort Buffington 0622f8a9bf NON-WORKING 2016-12-15 15:04:45 -06:00
Cort Buffington 23354574d1 Merge remote-tracking branch 'origin/modularization' into modularization
# Conflicts:
#	dmrlink.py
#	dmrlink_config.py
2016-12-15 13:08:51 -06:00
Cort Buffington c2bf16bd73 Update 2016-12-15 13:08:41 -06:00
Cort Buffington d0963792e2 update 2016-12-15 12:12:56 -06:00
Cort Buffington 26baa3c26c General progress - untested with live traffic 2016-11-23 16:55:39 -06:00
Cort Buffington fbac51b3e9 Progress - maybe works 2016-11-23 16:37:37 -06:00
Cort Buffington 477a00887a Removed File 2016-11-23 15:23:21 -06:00
Cort Buffington 4b81a6b22d Fix typos 2016-11-23 15:09:24 -06:00
Cort Buffington 9f5ac3fad4 Licence Change 2016-11-23 07:50:56 -06:00
Cort Buffington 2704d32b69 Better way to build hex strings 2016-11-20 10:16:12 -06:00
Cort Buffington d4832fc0d0 Contention Handler Updates 2016-11-19 10:12:00 -06:00
root d62b6861ac Add voice_packets.txt to doc directory 2016-10-13 14:17:22 -04:00
Cort Buffington 8bb698a9f6 clean up 2016-09-22 15:34:07 -05:00
Cort Buffington 37ba34bd3b Added debugging 2016-09-18 08:39:42 -05:00
Cort Buffington a6058598e0 Added timer logging information 2016-09-15 19:57:39 -05:00
Cort Buffington c48495b1d2 Remove superfluous timer reset. 2016-09-15 19:36:57 -05:00
Cort Buffington 25a82f743c Merge branch 'refactor-sending-packets' 2016-09-10 18:42:14 -05:00
Cort Buffington 0a5c53afd2 call logging additions 2016-09-10 16:57:48 -05:00
Cort Buffington da0eb0ad37 formatting 2016-09-10 16:37:07 -05:00
Cort Buffington 74eb8edb5f add call timer 2016-09-10 16:34:50 -05:00
Cort Buffington c33ba91f4f add call duration timer 2016-09-10 16:32:14 -05:00
Cort Buffington c26e5bb489 Maybe this time. 2016-09-10 16:26:44 -05:00
Cort Buffington 213756a161 how many stupid typos? I should slow down. 2016-09-10 10:52:29 -05:00
Cort Buffington 2fed2912a8 fix more stupid typos 2016-09-10 10:51:04 -05:00
Cort Buffington 15362a823b fix stupid typo 2016-09-10 10:43:40 -05:00
Cort Buffington 3209fce782 remove duplicate voice start header log messages 2016-09-10 10:41:42 -05:00
Cort Buffington 697b492d4e fix authentication checks on RX packets 2016-09-09 22:51:11 -05:00
Cort Buffington e80fe33545 updating hash/send routines 2016-09-09 22:44:40 -05:00
Cort Buffington ad1e9a2ca2 Don't reset timer that aren't used - waste of time 2016-09-09 22:31:08 -05:00
Cort Buffington d2279134f3 Don't reset unused timers.... waste of time 2016-09-09 22:30:10 -05:00
Cort Buffington d530be263c fixed typo 2016-09-09 22:13:44 -05:00
Cort Buffington 41e9351d7e refactoring auth code 2016-09-09 22:05:47 -05:00
Cort Buffington db3b3cfc0f enable authentication validation 2016-09-09 13:22:12 -05:00
Cort Buffington 22a71f0f52 Completed! 2016-09-07 20:03:47 -05:00
Cort Buffington 76b43f027a Completed 2016-09-07 20:02:06 -05:00
Cort Buffington f9392ab032 Working RS 12 9 Encoder. YAY! 2016-09-07 16:31:56 -05:00
Cort Buffington c349765cd5 progress 2016-09-07 16:31:56 -05:00
root 66412ef7af Add sub_acl to the bridge directory 2016-09-01 05:20:31 -04:00
root 9141e70867 fix script 2016-08-31 21:59:01 -04:00
root b9a96a594d Fix typos 2016-08-31 15:02:07 -04:00
Cort Buffington 78c23f93ec Formatting strings 2016-08-19 20:46:46 -05:00
Cort Buffington 7b6d47fbb8 more decoding 2016-08-19 15:32:04 -05:00
Cort Buffington 82993a03b7 voice packet decoding 2016-08-19 15:18:58 -05:00
Cort Buffington c75471d3d3 progress 2016-08-19 15:16:04 -05:00
Steve Zingman f3535cf036 Add template.py 2016-08-17 09:20:32 -04:00
Steve Zingman b28a9d7534 Don't re-install required files 2016-08-17 09:14:36 -04:00
Steve Zingman eaebb96e56 Don't overwrite existing config files 2016-08-17 08:32:06 -04:00
Cort Buffington 03e514ecff Work in progress 2016-08-16 08:28:52 -05:00
Cort Buffington d363e25cd5 LC FEC Calculations - in progress 2016-08-15 17:15:43 -05:00
Cort Buffington 21601f82f1 More Decoded 2016-08-10 10:31:05 -05:00
root f4c7620530 mv unit files to proper directory 2016-08-10 11:07:22 -04:00
root d75291d391 Update systemd unit files 2016-08-10 11:02:27 -04:00
Cort Buffington c1ff8d39cc UPDATE 2016-08-10 08:57:00 -05:00
Cort Buffington 4f5e542b82 update 2016-08-10 08:55:16 -05:00
Cort Buffington 6c70707d1a update 2016-08-10 08:53:29 -05:00
Cort Buffington 49a3e24a49 Voice Burst Decoding Work 2016-08-10 08:48:26 -05:00
Cort Buffington 2e916c68ee Work on voice packet decoding 2016-08-09 22:28:21 -05:00
root 2eee8de24a Start to add systemd support 2016-08-08 07:49:22 -04:00
Cort Buffington 61b07f421b Timer work - should be good now. 2016-08-06 20:51:05 -05:00
Cort Buffington 80f9224d8d Timers Should Work - Not Heavily Tested
NEEDS MORE TESTING email me with problems.
2016-08-06 19:02:28 -05:00
Cort Buffington 9b72e99a60 timer work... no TX-based reset yet. 2016-08-06 11:15:54 -05:00
Cort Buffington 43e2411f3e Update for timers 2016-08-06 11:10:37 -05:00
Cort Buffington 2e06ece1f3 Work on adding timers
transmission resets and reciprocal actions not yet completed, but
basics are here.
2016-08-06 10:58:53 -05:00
Cort Buffington fffdd01dd2 Fixed names 2016-08-06 09:12:14 -05:00
Cort Buffington a8dd866881 Prepare for timers 2016-08-05 09:22:53 -05:00
Cort Buffington dba648bb89 Prep for adding rule timers 2016-08-05 09:20:06 -05:00
Cort Buffington 70bf6d3dce Change Print to Logger 2016-08-05 09:19:50 -05:00
root bb26a9cc77 Update ambe_audio control port 2016-08-02 15:04:37 -04:00
root c71c088c9e Add sub_acl_SAMPLE to install 2016-08-01 16:06:34 -04:00
Cort Buffington ce432f3d66 Update README.md 2016-08-01 09:43:26 -05:00
root 693336adcf Change to bitstring-3.1.5.zip 2016-07-30 21:38:31 -04:00
Mike Zingman b47fd15155 Merge ambe_audio_2way into master 2016-07-10 09:53:36 -04:00
Cort Buffington f367602237 Update README.md 2016-05-19 08:26:01 -05:00
Cort Buffington 2a2ececca9 Update README.md 2016-05-19 08:25:07 -05:00
Cort Buffington 4ae1d479cc Update README.md 2016-05-19 08:23:50 -05:00
Cort Buffington 27197d3716 Update README.md 2016-05-19 08:23:21 -05:00
Cort Buffington c6f4d47a5f Update README.md 2016-05-18 15:48:17 -05:00
Cort Buffington 6cc36ab910 Update README.md 2016-05-18 15:47:52 -05:00
Cort Buffington 01bbd77dbc Update README.md 2016-05-18 10:19:58 -05:00
Cort Buffington 37fdaed5bd Update README.md 2016-05-18 10:16:30 -05:00
Cort Buffington 52afb05517 Access Control List (ACL) Added
Control permit or deny with a list of subscriber IDs. Thanks to Peter
Martin for the idea.
2016-05-03 20:15:55 -05:00
Cort Buffington 1d51980281 Adding Access Control List 2016-05-03 19:53:22 -05:00
Cort Buffington 1465468a96 Added null log handler
For those who REALLY like to be in the dark, now you can be completely
unaware.
2016-05-02 08:44:46 -05:00
Cort Buffington dce6339051 Fixed typo printing status code 2016-05-01 16:19:41 -05:00
Cort Buffington 2d0bff17d1 General Cleanup... Trying to improve it 2016-05-01 16:13:03 -05:00
Cort Buffington b068f3b090 Improve Documenation
With TGID-based in-band signaling added, the code needed cleaned up and
better documented for folks to follow/modify it.
2016-05-01 10:49:30 -05:00
Cort Buffington c5ad283393 Documentation for multiple triggers added 2016-05-01 07:55:08 -05:00
Cort Buffington 200a7a45b2 Multiple triggers allows per rule 2016-05-01 07:53:09 -05:00
Cort Buffington ecd9941588 Allow multiple triggers per group 2016-05-01 07:47:47 -05:00
Cort Buffington b4b3c6a0d5 Clean Up Comments 2016-04-28 15:30:42 -05:00
Cort Buffington f293d408b2 Update - Clearing Cruft 2016-04-28 15:25:59 -05:00
Cort Buffington 475dc9d766 Housekeeping Updates 2016-04-28 14:28:32 -05:00
Cort Buffington 5f58ce04a0 Update 2016-04-28 14:19:27 -05:00
Cort Buffington 251cc206ff TGID-Based Bridge Rule Triggering Added!
That’s PTT or UA to you c-Bridge dorks. It works now. Note you MUST
update your bridge rules file for this.
2016-04-27 16:27:23 -05:00
Cort Buffington 5010d9a5b7 User triggering of rules on/off being added
This would be PTT or UA for you c-Bridge dorks. It’s not done yet, but
it’s close. YOU MUST UPDATE YOUR BRIDGE RULES FILE IF YOU USE THIS
VERSION!!!
2016-04-26 18:56:45 -05:00
Cort Buffington 418757cfcd Merge pull request #18 from ryanturner/master
Updated dmrlink sample config to comment lines
2016-02-01 11:02:32 -06:00
Ryan Turner e138cfc12f Updated dmrlink sample config to comment lines 2016-02-01 10:46:23 -06:00
Cort Buffington bf33f3893e Alternate Subscriber ID for repeat transmission
Added a feature whereby a user can configure an alternate subscriber ID
in the config file that will be used for all group traffic repeated.
2016-01-16 18:28:39 -06:00
Steve Zingman 11bf48a03f Update README.md 2015-12-23 15:30:42 -05:00
Steve Zingman 72fd84dff6 Update FAQ.md 2015-12-23 15:27:15 -05:00
Steve Zingman N4IRS 00c1d60843 Add ambe_audio re-read 2015-12-23 15:21:33 -05:00
Steve Zingman N4IRS a921de8087 Human readable peers for debugging 2015-12-23 14:01:00 -05:00
Steve Zingman N4IRS 762988c7c6 Human readable Subscriber file for enhanced logging and debug 2015-12-23 13:59:48 -05:00
Steve Zingman N4IRS e17fa904d1 Talk Groups file for enhanced logging and debugging 2015-12-23 13:58:08 -05:00
Steve Zingman N4IRS 235e8a52d1 Merge https://github.com/n0mjs710/DMRlink 2015-12-23 11:43:50 -05:00
Steve Zingman N4IRS 9484b46b7d Added comments and re-order TS and TG for readability 2015-12-23 11:40:46 -05:00
Mike Zingman 2092f2a1e5 Add reread_subscribers remote control command 2015-12-16 15:20:01 -05:00
Steve Zingman N4IRS 90da9014ea Fixed missing playback config 2015-12-15 10:27:51 -05:00
Steve Zingman N4IRS 0230251818 Moved to DMRGateway 2015-12-12 09:12:24 -05:00
Steve Zingman N4IRS ddd5cc0606 Added bitstring 2015-12-12 08:26:34 -05:00
MrBungle42 7607a5309d Merge pull request #17 from N4IRS/master
Add Changes From N4IRS and N4IRR to original
2015-12-12 08:11:25 -05:00
Mike Zingman 4f7fc558cc Bridge auth to unuath (or visa versa) networks was not using the correct function. Force to use auth when needed. 2015-12-11 10:44:27 -05:00
Mike Zingman eb1614f814 Make the cwd for execution be the same directory as the .py file 2015-12-07 22:47:02 -05:00
Mike Zingman 108a9e14d3 Merge branch 'master' of https://github.com/N4IRS/DMRlink 2015-12-07 15:17:11 -05:00
Mike Zingman 628d0d3d0c Remove peer id from log output 2015-12-07 15:16:47 -05:00
Steve Zingman N4IRS 047963cd93 Expand examples 2015-12-06 22:16:39 -05:00
Steve Zingman N4IRS 9776784fb2 Remove old .csv 2015-12-06 21:38:30 -05:00
Steve Zingman N4IRS c1abb7e705 Fix DMRGateway install 2015-12-06 21:00:06 -05:00
Steve Zingman N4IRS 818b2a3536 remove overwrite of modules.conf 2015-12-06 17:45:32 -05:00
Steve Zingman N4IRS fc16193956 DMRGateway Install script 2015-12-06 17:27:28 -05:00
Mike Zingman 41391e4044 remove xxx file names 2015-12-06 16:57:24 -05:00
Mike Zingman 4ecf651cc5 Update to non-xxx file names and DMR-MARC format files 2015-12-06 16:55:31 -05:00
Mike Zingman 2509528449 Add transmit time to output logging 2015-12-06 15:50:50 -05:00
Mike Zingman 8b210f02c0 Merge branch 'master' of https://github.com/N4IRS/DMRlink 2015-12-05 17:38:03 -05:00
Mike Zingman f039cb7d10 Add timeout to capture in case we never see a EOT (which is used to release the TG "focus") 2015-12-05 17:37:16 -05:00
Steve Zingman N4IRS fccc8b3367 fix config 2015-12-04 21:41:16 -05:00
Steve Zingman N4IRS 7113a56d2a path needs a trailing / 2015-12-04 21:40:23 -05:00
Steve Zingman N4IRS 9e12fc9d5d Add unzip and twisted to script 2015-12-04 06:06:35 -05:00
Steve Zingman N4IRS 11786cb603 Simple install instructions 2015-12-03 09:38:58 -05:00
Steve Zingman N4IRS a6ee57bd26 Enable bitstring install 2015-12-03 09:35:25 -05:00
Steve Zingman N4IRS e8789a6d96 Create directory structure and copy files 2015-12-03 09:32:18 -05:00
Steve Zingman N4IRS c0c2dd7403 Init script for DMRlink script 2015-11-29 08:07:23 -05:00
Steve Zingman N4IRS 3818747393 Merge https://github.com/N4IRS/DMRlink 2015-11-28 06:08:15 -05:00
Steve Zingman N4IRS ff8b14afe7 Added bitstring 2015-11-28 06:07:10 -05:00
Mike Zingman eb48513cf0 Fix TS debug on ignore message (True/False -> 1/2) 2015-11-27 21:23:19 -05:00
Mike Zingman 595d60a587 Fix refactor bug 2015-11-27 17:51:44 -05:00
Mike Zingman 0c809a86c1 Class refactoring. No globals. 2015-11-27 14:57:13 -05:00
Mike Zingman 94ca9d4131 Merge branch 'master' of https://github.com/N4IRS/DMRlink 2015-11-27 10:27:14 -05:00
Mike Zingman d2b33e689b Remote control port define
Comments and formatting.
2015-11-27 10:27:03 -05:00
Mike Zingman 76c74fbec5 Add remote control port value 2015-11-27 10:26:33 -05:00
Steve Zingman N4IRS e6a397f479 Download the subscriber and repeater IDs 2015-11-27 08:02:30 -05:00
Mike Zingman 17ea95b99f Config additions 2015-11-26 19:26:00 -05:00
Mike Zingman a77061e21b Gateway changes 2015-11-26 19:25:23 -05:00
Cort Buffington f5f9e14349 simplified a few items. No functional change 2015-08-14 08:36:29 -05:00
Cort Buffington 52946749d2 Improved Logging Data 2015-08-13 22:17:06 -05:00
Cort Buffington 5c502e1667 debug, don't use 2015-08-13 21:27:56 -05:00
Cort Buffington 86a42b27d8 debug, don't use 2015-08-13 21:26:23 -05:00
Cort Buffington fec5f76ef7 Debug, don't use 2015-08-13 21:20:50 -05:00
Cort Buffington 8e24e4ceea Now Working: REALLY!!! 2015-08-12 11:12:49 -05:00
Cort Buffington 2e331c6f10 testing 2015-08-12 11:08:40 -05:00
Cort Buffington 566a8abf32 testing 2015-08-12 11:07:04 -05:00
Cort Buffington 9100349fb2 testing 2015-08-12 11:05:39 -05:00
Cort Buffington 82de012f2f testing 2015-08-12 10:58:02 -05:00
Cort Buffington f1ed42d2eb testing 2015-08-12 10:57:33 -05:00
Cort Buffington c460709c9c testing 2015-08-12 10:57:02 -05:00
Cort Buffington a41fde756b testing 2015-08-12 10:49:58 -05:00
Cort Buffington 47bbfad702 testing 2015-08-12 10:48:32 -05:00
Cort Buffington a70b48a275 testing 2015-08-12 10:47:19 -05:00
Cort Buffington 8a1e883ea4 testing 2015-08-12 10:43:55 -05:00
Cort Buffington a3fa36e74b test 2015-08-12 10:41:54 -05:00
Cort Buffington 289edb310d testing 2015-08-12 10:30:21 -05:00
Cort Buffington 1677247b75 testing 2015-08-12 10:29:25 -05:00
Cort Buffington 9b077fc2f0 testing 2015-08-12 10:27:06 -05:00
Cort Buffington a844c99daa testing 2015-08-12 10:25:48 -05:00
Cort Buffington 342f5ac079 testing 2015-08-12 10:23:31 -05:00
Cort Buffington db489526b1 testing 2015-08-12 10:14:48 -05:00
Cort Buffington f4aec00fa4 more testing 2015-08-12 10:07:52 -05:00
Cort Buffington 22fdca6436 Fixed bug 2015-08-12 10:07:01 -05:00
Cort Buffington 0076fd5f5a Working: TGID, TS and SRC translation working!
This application now meets it’s target functionality!
2015-08-12 10:06:30 -05:00
Cort Buffington 075dad4ee9 Might Work 2015-08-12 09:45:32 -05:00
Cort Buffington 20f16091c8 debugging, don't use 2015-08-12 09:43:25 -05:00
Cort Buffington 9803a944b8 debugging, don't use 2015-08-12 09:39:25 -05:00
Cort Buffington a9ef6314cd debugging, don't use 2015-08-12 09:37:48 -05:00
Cort Buffington 174d74385e debugging, don't use 2015-08-12 09:36:33 -05:00
Cort Buffington 51be01c77d debugging, don't use 2015-08-12 09:34:52 -05:00
Cort Buffington 3705c08434 debugging, don't use 2015-08-12 09:33:54 -05:00
Cort Buffington 6eef23c9e2 debugging, don't use 2015-08-12 09:30:48 -05:00
Cort Buffington 5cccdd0bb7 don't use 2015-08-12 09:27:13 -05:00
Cort Buffington bc9ab4e11c don't use 2015-08-12 09:26:32 -05:00
Cort Buffington 725110e48e Works w/o TS/TGID translation 2015-08-12 09:21:20 -05:00
Cort Buffington dff474cbfb debugging, don't use 2015-08-12 09:16:22 -05:00
Cort Buffington c03cbe48b7 debugging, don't use 2015-08-12 09:14:45 -05:00
Cort Buffington 7a4e5fd07d debugging don't use 2015-08-12 09:10:50 -05:00
Cort Buffington 03f80f900d debugging, don't use 2015-08-12 09:07:49 -05:00
Cort Buffington 7259f16ec0 debugging, don't use 2015-08-12 09:04:35 -05:00
Cort Buffington fc18a2f3c0 debugging, don't use 2015-08-12 09:02:49 -05:00
Cort Buffington 8d8f5a6eed debugging 2015-08-12 08:55:53 -05:00
Cort Buffington fb0b522e17 fix logging of trigger TGID 2015-08-12 08:50:56 -05:00
Cort Buffington 0885a3b565 TS/TGID Matching + re-Writes on TX
borrowed matching and re-write logic from bridge.py to make this match
on specific TS/TGID combinations and re-write values on playback.
2015-08-11 21:18:36 -05:00
Cort Buffington 95a03fc28a Bridge contention bug
Fixed a problem where I used return instead of continue in contention
handling. ONE rule problem threw the packet away for ALL subsequent
rules!!!
2015-08-06 22:51:34 -05:00
Cort Buffington 02d54288f3 Added Master/Peer IP Address 2015-08-05 10:20:42 -05:00
Cort Buffington 08ea043b46 Update RCM Message Types
I previously mistakenly put these contributed types (THANK YOU MARK!)
in the wrong place, this corrects that mistake.
2015-07-28 18:34:37 -05:00
Cort Buffington 2524822e80 added null reporting loop
this makes importing consistent for applications in case there is no
reporting configured.
2015-07-27 20:10:35 -05:00
Cort Buffington 2635046aad logging bug: Unknown IPSC Type
The packet type’s numeric value was reported wrong in log messages for
unknown types.
2015-07-27 17:13:41 -05:00
Cort Buffington 7850ec8f85 Additional IPSC Message Types
Thanks Mark M for the additional IPSC message types discovered!
2015-07-27 17:12:34 -05:00
Cort Buffington 52a2b26c0a Bug in Call Contention Error
saved the original packet destination TGID for checking on the target
IPSC, not the target (translated) TGID.
2015-07-27 16:34:46 -05:00
Cort Buffington 231c37f744 See Last Push 2015-07-27 16:29:23 -05:00
Cort Buffington c73483a4fd Additional Logging for Call Contention
more descriptive logging when a call is not bridged due to group
hangtime
2015-07-27 16:27:45 -05:00
Cort Buffington f0cc0e097b Call Contention Debugging Added
Added more information to info level log when a call is not bridged
because the destination is in Group Hangtime.
2015-07-27 16:26:02 -05:00
Cort Buffington e89f6e877b Add Trunk Type for IPSC 2015-07-22 15:02:36 -05:00
Cort Buffington e3051fec85 Update README.md 2015-07-19 07:55:19 -05:00
Cort Buffington 4e3cd140dd Update README.md 2015-07-06 10:22:49 -05:00
Cort Buffington 9204a90cfb Update README.md 2015-07-06 10:22:32 -05:00
Cort Buffington 5c2ef72028 Improve Display Format 2015-07-01 07:53:11 -05:00
Cort Buffington 474e73dd23 add timestamp 2015-06-30 20:59:13 -05:00
Cort Buffington 96d0f11615 add configurable frequency 2015-06-30 20:56:00 -05:00
Cort Buffington 9ac48cac3f formatting update 2015-06-30 20:54:15 -05:00
Cort Buffington 113ebc8bf3 formatting update 2015-06-30 20:53:28 -05:00
Cort Buffington 02439f6ec4 formatting update 2015-06-30 20:49:02 -05:00
Cort Buffington b77e2a9809 fix self-master reporting 2015-06-30 20:47:22 -05:00
Cort Buffington bd251461f4 debugging 2015-06-30 20:38:04 -05:00
Cort Buffington ae29844809 fix bridge.py reporting 2015-06-30 20:36:14 -05:00
Cort Buffington 97fa6bfe92 debug reporting 2015-06-30 19:32:40 -05:00
Cort Buffington 354fa34e8a add reporting loop to bridge.py 2015-06-30 19:29:59 -05:00
Cort Buffington 1285ddedff Application to Print IPSC Stats 2015-06-30 12:43:30 -05:00
Cort Buffington d7f11d955c Initial File Upload 2015-06-30 11:51:04 -05:00
Cort Buffington 8e85b71e27 Add Pickle Dump to Reporting 2015-06-30 11:50:51 -05:00
Cort Buffington ecd2648372 remove commented code 2015-06-29 14:58:39 -05:00
Cort Buffington 5b4520318f Fix Typo "_host, _port" was missing ", " 2015-06-29 14:48:58 -05:00
Cort Buffington 574a89e7f9 Remove JSON Reporting
It has a major problem with byte strings in the dictionaries… this may
never work
2015-06-20 19:57:07 -05:00
Cort Buffington 31223a846d Work on reporting options 2015-06-12 08:28:48 -05:00
Cort Buffington b8bd3da8db Add IPSC Trunk Flag
Trunk flag will (not yet) byass IPSC TS loading checks so that 2 or
more DMRlinks may join an IPSC that moves streams between them. It will
be possible to bridge an arbitrary number of streams since there’s no 2
TS restriction on a DMRlink to DMRlink IPSC
2015-06-07 21:06:30 -05:00
Cort Buffington 10a21ed118 Word on data reporting options
in the process of adding several ways to dump the data structure from
each IPSC for processing by some external program.
2015-06-07 20:54:21 -05:00
Cort Buffington ec9c20d5ee *NOT_BACKWARD_COMPATIBLE* changes
Changes to the REPORTS section of the config and dmrlink.py to pave the
way for more flexible reporting types.
2015-06-07 15:46:29 -05:00
Cort Buffington d2a73a044e log "ID, IP:PORT" instead of just "ID" 2015-06-07 13:19:32 -05:00
Cort Buffington ea0188002a Add IP:Port to logging for some messages 2015-06-07 11:47:07 -05:00
Cort Buffington 9265ebaa9b Update 2015-06-03 14:38:58 -05:00
Cort Buffington 29789acfd9 Internal Diagnostics (Timing)
Added some stuff for measuring timing. Must be uncommented to work.
2015-05-28 09:58:06 -05:00
Cort Buffington 707d9cc1f6 Internal Diagnostics
Added pieces for timing run times. No functional change unless you go
uncomment stuff to turn it on.
2015-05-28 09:42:58 -05:00
Cort Buffington bacb063121 Work on Data Packet Types 2015-05-28 09:42:14 -05:00
Cort Buffington 38b6e955a7 TXT Messages WORK! 2015-05-23 09:03:06 -05:00
Cort Buffington ea5141684b Work on TXT Messaging 2015-05-23 08:54:46 -05:00
Cort Buffington e2f596f311 DNS resolution
use gethostbyname when we parse the config, once, rather than leave it
to twisted to do when we send packets.
2015-05-21 09:45:24 -05:00
Cort Buffington 679038face Prepare for Data 2015-05-21 09:39:43 -05:00
Cort Buffington dd4dee21c4 Move gethostbyname
don’t rely on Twisted to do this on packet write, just translate once
when we read the config.
2015-05-21 09:39:42 -05:00
Cort Buffington d4b51fd08f Prepare for Dynamic Rule Changes
Change the name of the rules read from the file, then copy it to
“RULES” after processing. This will allow us to modify RULES on the
fly, while keeping the “original” - This is a hook to add dynamic rule
changes.
2015-05-17 11:19:19 -05:00
Cort Buffington b7f6b62993 bridge_rules.py and dmrlink.cfg checking
exit if the IPSCs in dmrlink.cfg (or specified file) don’t match the
IPSCs specified in bridge_rules.py
2015-05-17 08:14:29 -05:00
Cort Buffington 6612ba70ef Group hangtime added
new value, per IPSC, added for call contention handling.
2015-05-14 23:00:47 -05:00
Cort Buffington e2c47ed6ca Call contention handler reliable
MANY changes to make the call contention handler reliable. I’m ready to
say this works pretty well.
2015-05-14 23:00:19 -05:00
Cort Buffington 3cfc058468 CALL CONTENTION WORKING!
minor cleanup
2015-05-14 09:31:33 -05:00
Cort Buffington 66ae5fa873 Call Contention WORKING!
Not saying we won’t find a problem, but it looks pretty solid right
now!!!
2015-05-13 20:35:18 -05:00
Cort Buffington ffe5a61463 Call Contention Handling
NOT THOROUGHLY TESTED: Code added to avoid bridging when a valid rule
exists, but the target IPSC/TS appears to be busy, either in group
hang-time, or already in a call on the same TGID (not likely).
2015-05-13 12:33:47 -05:00
Cort Buffington 7937071930 Trivial Change 2015-05-13 10:01:02 -05:00
Cort Buffington 4047e08a7e Call Contention Feature
Working to add a call contention feature that does not bridge into an
IPSC with an active call
2015-05-13 09:40:57 -05:00
Cort Buffington 50e9e8ecba Slow progress...
Getting closer to sending data to an AMBE decoder.
2015-05-13 09:40:57 -05:00
Cort Buffington 84b45cac26 Merge pull request #15 from KD8EYF/master
fix exit program via CTRL-C
2015-04-10 07:47:56 -05:00
David Kierzkowski d83fbba277 fix exit program via CTRL-C 2015-04-10 01:41:44 -04:00
Cort Buffington 52793877e1 AMBE Audio Dumping 2015-03-24 14:22:03 -05:00
Cort Buffington fb72151c8f work on AMBE dump 2015-03-24 13:28:56 -05:00
Cort Buffington b9b21ebebf Begin AMBE Data Dump 2015-03-24 11:06:42 -05:00
Cort Buffington 5c27ce4d52 See last two changes (this is a clean-up commit) 2014-12-20 10:05:03 -06:00
Cort Buffington 06c9ed48b4 SEE LAST UPDATE (bug fix from it) 2014-12-20 09:56:15 -06:00
Cort Buffington 16e7d2aaeb send_to_ipsc moved into IPSC class
send_to_ipsc moved into the IPSC class, as well as a new class function
for the single line to write the socket. This allows for inserting a
debug logger line to dump EVERY packet transmitted on a per-IPSC basis.
2014-12-20 09:14:54 -06:00
Cort Buffington 1b99f2bfa6 Minor updates 2014-12-05 14:26:47 -06:00
Cort Buffington acfb4700e5 NEW APPLICATION
logs RCM status messages to a MySQL database
2014-12-05 14:25:29 -06:00
Cort Buffington 5a5ecb322b IP Interface may be specified now
This is useful when using multiple interfaces and bridge.py is employed
as an application gateway between multiple un-connected networks (like
VPNs to the real world).
2014-10-31 10:30:18 -05:00
Cort Buffington 626cc3674b Messing around... 2014-10-06 08:18:06 -05:00
Cort Buffington aed10e1d4e Messing around... 2014-10-06 08:17:48 -05:00
Cort Buffington 91a7e773b2 Don't import config for IPSC not enabled 2014-10-06 08:17:39 -05:00
Cort Buffington e02c03a099 publish data structure to file 2014-10-05 22:11:56 -05:00
Cort Buffington 6e8292d5f1 Use Warning Added 2014-09-27 10:40:50 -05:00
Cort Buffington d10f33a325 Update README.md 2014-09-18 20:13:26 -05:00
Cort Buffington 648569eb91 TIMESLOT TRANSLATION WORKS! 2014-09-18 20:08:54 -05:00
Cort Buffington 324c16660c Corrected MASTER_PEER Comment
Forgot to change this once DMRlink became capable of being a master.
2014-09-18 18:54:26 -05:00
Cort Buffington f2148ab05f Formatting & Documentation 2014-09-05 19:35:37 -05:00
Cort Buffington 1457d34e21 improve debugging 2014-09-05 16:02:30 -05:00
Cort Buffington 140818bcb2 add debugging 2014-09-05 15:59:55 -05:00
Cort Buffington 549b8d62bb improve unknown packet type debugging 2014-09-05 15:57:08 -05:00
Cort Buffington a4461d3de1 logger typo 2014-09-05 14:17:29 -05:00
Cort Buffington fc271cd957 variable typo 2014-09-05 14:15:05 -05:00
Cort Buffington 9a99fa585b Fix variable typo in master_reg_req 2014-09-05 14:13:18 -05:00
Cort Buffington b13f90f971 fix function name typeos 2014-09-05 14:10:51 -05:00
Cort Buffington 08e5525ae9 INT ID #s instead of HEX 2014-09-05 14:08:23 -05:00
Cort Buffington eb84cb2589 Move IPCS Maintenance to Functions 2014-09-05 13:57:47 -05:00
Cort Buffington 03fcae42de user integer ID in debugging instead of HEX 2014-09-05 10:16:06 -05:00
Cort Buffington ce29ce89c4 De-Registration Logs integer ID now 2014-09-05 09:02:59 -05:00
Cort Buffington bc25da467a Complete Polite Shutdown
DMRlink will now de-register from all peers on a SIGTERM, SIGINT or
SIGQUIT.
2014-09-05 08:56:51 -05:00
Cort Buffington 82d93a759d Added Logger name to config
Now you can configure the name shown in syslog on a per-instance basis.
Handy when you run more than one DMRlink instance on the same machine.
2014-09-05 07:50:49 -05:00
Cort Buffington a6ca762314 Update README.md 2014-09-04 12:24:49 -05:00
Cort Buffington 6f08f1f2c4 change logger name 2014-09-01 18:58:26 -05:00
Cort Buffington 4781321c2c Restore from a test 2014-08-31 18:33:45 -05:00
Cort Buffington 7c99a1933a Experimental: Don't Rewrite Peer ID 2014-08-31 18:17:34 -05:00
Cort Buffington f0d50c8211 Update Versions/Documentation 2014-08-31 13:18:35 -05:00
Cort Buffington 1d461ca21a v.27B Update - LOTS OF NEW! 2014-08-31 12:32:23 -05:00
Cort Buffington 59224df788 MANY CHANGES
Bridging works well. Backup and standard are consolidated to one
application, better documentation, bridging rules file greatly
simplified.
2014-08-31 11:27:00 -05:00
Cort Buffington 5e5719eb2e Quiet Superfluous Logging 2014-08-29 17:08:49 -05:00
Cort Buffington fc791295b2 Quiet superfluous logging 2014-08-29 17:06:46 -05:00
Cort Buffington baf87b7bf8 NEW TYPE FOUND 2014-08-29 17:01:12 -05:00
Cort Buffington 92ff27e04a Beta - Currently in Use! 2014-08-29 16:35:58 -05:00
Cort Buffington f8ce89a17a Fixed a typo 2014-08-29 16:20:44 -05:00
Cort Buffington b78a52290e General Improvements 2014-08-29 16:08:32 -05:00
Cort Buffington 622f243ee8 Complete, but needs tested 2014-08-29 15:12:45 -05:00
Cort Buffington d160ec6d9e Complete, but needs tested 2014-08-29 15:11:34 -05:00
Cort Buffington dba7fa3e6b Bridge Status Working! 2014-08-29 14:58:41 -05:00
Cort Buffington b22d773e81 NOT FULLY FUNCTIONAL: Backup Bridge
Bridge application that identifies known bridges based on RADIO ID and
does NOT bridge if they are actively connected to an IPSC and MODE byte
shows linking on a timeslot.
2014-08-29 09:05:19 -05:00
Cort Buffington a89a94dea8 reduce peer_list function logging 2014-08-27 19:43:42 -05:00
Cort Buffington 6c41942aa2 Allow Re-Registration
If a peer “re-registers” we were not updating it’s registration
information. For example, it may have changed it’s MODE byte, or worse,
it’s port number, etc. and we’d have missed it
2014-08-26 08:01:27 -05:00
Cort Buffington b2398a255a POC TEST ONLY 2014-08-24 21:03:08 -05:00
Cort Buffington 8c267955ff Stores useable files 2014-08-24 19:01:41 -05:00
Cort Buffington 831949954a NOT WORKING YET 2014-08-24 17:14:20 -05:00
Cort Buffington 318ccfea78 Improve Syslogging 2014-08-24 15:20:10 -05:00
Cort Buffington 94eb828006 Improve syslogging 2014-08-24 15:19:24 -05:00
Cort Buffington c84273272c Improve Syslogging 2014-08-24 15:16:30 -05:00
Cort Buffington 30ce30749e Update gitignore 2014-08-20 13:04:38 -04:00
Cort Buffington 40b929cadd delete unused files 2014-08-20 11:35:32 -05:00
Cort Buffington bd211001f7 Consolodate playback and playback_user 2014-08-17 12:45:37 -04:00
Cort Buffington bfa060f0b1 Update playback 2014-08-17 11:28:19 -04:00
Cort Buffington 211ebab6bd Consolidate playback and playback_user 2014-08-17 11:23:10 -04:00
Cort Buffington c04bbc1b84 Combine Playback Group and Private 2014-08-17 09:35:50 -05:00
Cort Buffington 536dc73521 Improved syslogging 2014-08-15 11:22:39 -05:00
Cort Buffington d88f90d0b4 Add cfg configurable report interval 2014-08-15 08:39:52 -05:00
Cort Buffington 6e899a36c2 Update FAQ.md 2014-08-14 09:28:46 -05:00
Cort Buffington 8270d983fd Update FAQ.md 2014-08-14 09:14:00 -05:00
Cort Buffington 3fd92dd4da update FAQ 2014-08-14 09:12:27 -05:00
Cort Buffington 0ead8726f0 NEW FAQ! 2014-08-14 09:11:52 -05:00
Cort Buffington 3a97e7160d Update README.md 2014-08-14 08:56:47 -05:00
Cort Buffington 12161c4959 Update README.md 2014-08-14 08:51:27 -05:00
Cort Buffington c73e733516 Update README.md 2014-08-14 08:48:50 -05:00
Cort Buffington 8a378fe2c1 More Decoding 2014-08-13 17:14:46 -05:00
Cort Buffington ce29b8de2d Small fixes 2014-08-11 19:16:14 -05:00
Cort Buffington 9699fc18ef Work on Private Playback
Still not working… packets look good, something is missing.
2014-08-04 21:43:58 -05:00
Cort Buffington 132c1ece39 Add Sample App
Playback, but uses DMRlink’s subscriber ID instead of a GID as the
target.
2014-07-29 08:51:31 -05:00
Cort Buffington 6fb3601724 New RCM Type Found
Type 0x84 was seen by a user running ARS & GPS. It is not yet known
exactly what the type is for.
2014-06-25 16:32:34 -05:00
50 changed files with 5501 additions and 1549 deletions

10
.gitignore vendored Normal file → Executable file
View File

@ -3,9 +3,19 @@
*.out
Icon
dmrlink.cfg
rcm.cfg
stats.py
pub*
bridge_rules.py
confbridge_rules.py
playback_config.py
known_bridges.py
sub_acl.py
*.pyc
*.bak
*.lcl
*.conf
*.config
*.json
*.pickle
*.csv

214
DO_NOT_README.md Executable file
View File

@ -0,0 +1,214 @@
#STUFF NOBODY READS ANYWAY, BUT I DON'T WANT TO LOSE TRACK OF
###CONNECTION ESTABLISHMENT AND MAINTENANCE
**CORE CONCEPTS:**
The IPSC system contains, essentially, two types of nodes: Master and Peer. Each IPSC network has exactly one master device and zero or more peers, recommended not to exceed 15. IPSC nodes may be a number of types of systems, such as repeaters, dispatch consoles, application software, etc. For example, the Motorola RDAC application acts as a peer in the IPSC network, though it doesn't operate as a repeater. The IPSC protocol supports many possible node types, and only a few have been identified. This document currently only explores repeaters - both Master and Peer, and their roles in the IPSC network.
All IPSC communication is via UDP, and only the master needs a static IP address. Masters will operate behind NATs. A single UDP port, specified in programming the IPSC master device must be mapped through any NAT/stateful firewalls for the master, while peers require no special treatment.
All nodes in an IPSC network maintain communication with each other at all times. The role of the master is merely to coordinate the joining of new nodes to the IPSC network. A functional IPSC network will continue without its master, as long as no new nodes need to join (or existing nodes need to re-join after a communications outage, etc.) This is one of the most important core concepts in IPSC, as it is central to the NAT traversal AND tracking of active peers.
Each peer will send keep-alives to each other peer in the IPSC network at an interval specified in the devices "firewall open timer". The elegantly simple, yet effective approach of IPSC, uses this keep-alive to both open, and keep open stateful firewall and NAT translations between peers. Since each device handles all communications from a single UDP port, when a device sends a keep-alive or a registration request to another device, the source-destination address/port tuple for that communication is opened through stateful devices. The only requirement to maintain communication is that this timer be shorter than the UDP session timeout of network control elements (firewalls, packet shapers, NATs, etc.) Moreover, it does NOT appear that all devices in the IPSC network require the same setting for this. Each device would appear to maintain its own set timing without interference from different interval settings on other nodes in the IPSC.
**KNOWN IPSC PACKET TYPES:**
The following sections of this document will include various packet types. This is a list of currently known types and their meanings. Note: The names are arbitrarily chosen with the intention of being descriptive, and each is defined by what they've been "observed" to do in the wild.
CALL_CONFIRMATION = 0x05 Confirmation FROM the recipient of a confirmed call.
CALL_MON_ORIGIN = 0x61 Sent to Repeater Call Monitor Peers from repeater originating a call
CALL_MON_RPT = 0x62 Sent to Repeater Call Monitor Peers from all repeaters repeating a call
CALL_MON_NACK = 0x63 Sent to Repeater Call Monitor Peers from repeaters that cannot transmit a call (ie. ID in progress)
XCMP_XNL = 0x70 Control protocol messages
GROUP_VOICE = 0x80 This is a group voice call
PVT_VOICE = 0x81 This is a private voice call
GROUP_DATA = 0x83 This is a group data call
PVT_DATA = 0x84 This is a private data call
RPT_WAKE_UP = 0x85 Wakes up all repeaters on the IPSC
MASTER_REG_REQ = 0x90 Request registration with master (from peer, to master)
MASTER_REG_REPLY = 0x91 Master registration request reply (from master, to peer)
PEER_LIST_REQ = 0x92 Request peer list from master
PEER_LIST_REPLY = 0x93 Master peer list reply
PEER_REG_REQ = 0x94 Peer registration request
PEER_REG_REPLY = 0x95 Peer registration response
MASTER_ALIVE_REQ = 0x96 Master keep alive request (to master)
MASTER_ALIVE_REPLY = 0x97 Master keep alive reply (from master)
PEER_ALIVE_REQ = 0x98 Peer keep alive request (to peer)
PEER_ALIVE_REPLY = 0x99 Peer keep alive reply (from peer)
DE_REG_REQ = 0x9a De-registraiton request (to master or all?)
DE_REG_REPLY = 0x9b De-registration reply (from master or all?)
**AUTHENTICATION:**
Most IPSC networks will be operated as "authenticated". This means that a key is used to create a digest of the packets exchanged in order to authenticate them. Each node in the IPSC network must have the authentication key programmed in order for the mechanism to work. The process is based on the SHA-1 digest protocol, where the "key" is a 20 byte hexadecimal *string* (if a shorter key is programmed, leading zeros are used to create a 20 byte key). The IPSC payload and the key are used to create the digest, of which only the most significant 10 bytes are used (the last 10 are truncated). This digest is appended to the end of the IPSC payload before transmission. An example is illustrated below:
IPSC Registration Packet Digest
90000000016a000080dc04030400 b0ec45f4c3f8fb0c0b1d
**CONNECTION CREATION AND MAINTENANCE:**
The IPSC network truly "forms" when the first peer registers with the master. All peers register with the master in the same way, with a slight variation from the first peer. Below is a descirption of the process and states in creating a connection, as a peer, and maitaining it.
There are various states, timers and counters associated with each. When peers or the master send us requests, we should answer them immediatley. Our own communcation with them is timed, and may share the same timer. Counter values should be the same for every master and peer in an IPSC. They don't have to be, but that is what mother M does, and it saves a lot of resources.
*COMMUNICATION WITH MASTER:*
The following illustrates the communication that a peer (us, for example) has with the master. The peer must register, then send keep-alives at an arbitrary interval (usually 5 - 30 seconds). If more than some arbitrary number of keep-alives are missed, we should return to the beginning and attempt to register again -- but do NOT elimiate the peers list, as peers may still be active. The only additional communcation with the master is if the master sends an unsolicited peer list. In this case, we should update our peer list as appropriate and continue.
+-----------------+
|Send Registration|
+---------------------------->|Request To Master|<-------------+
| +--------+--------+ |
| | |
| v |
| +--------------+ +-----+------+
| |Did The Master| NO |Wait FW Open|
| | Respond ? +-------->| Timer |
| +----+-----+---+ +------------+
| | |
| | YES |
| +-------------+ v |
| |Add 1 To Keep| +----------------+ | +-------------+
| | Alive Missed| |Send Master Keep| | |Is Peer Count|
| | Counter +---->| Alive Request | +------------>| > 1 ? |
| +-------------+ +-------+--------+ +------+------+
| ^ | ^ | YES
YES| | NO v | v
+---+---------+--+ +------------+ | +-----------------+
| Is The Missed | |Wait FW Open| | |Request Peer List|
| Keep-Alive | | Timer | | | From Master |<-----+
|Count Exceeded ?| +-----+------+ | +-------+---------+ |
+----------------+ | | | |
^ v | v |
| +--------------+ ++-------------+ +---------+ |
| NO |Did The Master| YES |Set Keep Alive| |Peer List| NO |
+---------+ Respond ? +---->| Counter To 0 | |Received?+----------+
+--------------+ +--------------+ +---------+
*COMMUNICATION WITH PEERS:*
Once we have registered with the master, it will send a peer list update to any existing peers. Those peers will **immediately** respond by sending peer registrations to us, and then keep alives once we answer. We should send responses to any such requests as long as we have the peer in our own peer list -- which means we may miss one while waiting for receipt of our own peer list from the master. Even though we receive registration requests and keep-alives from the peers, we should send the same to them, even though this is redundant, it is how we ensure that firewall UDP sessions remain open. A bit wonky, but elegant. For example, a peer may not have a firewall, so it only sends keep-alives every 30 seconds, but we may need to every 5; which we achieve by sending our own keep-alives based on our own timer. The diagram only shows the action for the *initial* peer list reply from the master. Unsolicited peer lists from the master should update the list, and take appropriate action: De-register peers not in the new list, or begin registration for new peers.
+-----------------+ +-------------+
|Recieve Peer List| |Received Peer|
| From Master | |Leave Notice?|
+------+----------+ +------+------+
| |
v FOR EACH PEER |
+----------------------+ v
|Send Peer Registration| +-----------+
+------------------->| Request |<-----------+ |Remove Peer|
| +----------+-----------+ | | From List |
| | | +-----------+
| v |
| +---------------------+ +------+------+
| +---------+ |Registration Response| NO |Wait Firewall|
| |+1 Missed| | Received ? +---->| Open Timer |
| | Counter | +---------+-----------+ +-------------+
| +-------+-+ |
| ^ | v YES
| | | +----------+
| | +--------------->|Send Peer |
| | +-------->|Keep Alive|
| | | +----+-----+
|YES |NO | |
+---+---------+--+ +-----+------+ |
| Keep Alive | | Set Missed | |
| Count Exceeded?| |Counter to 0| |
+----------------+ +------------+ |
NO ^ ^ YES |
| | v
+---+------+----+ +-------------+
| Peer Keep | |Wait Firewall|
|Alive Received?|<------+ Open Timer |
+---------------+ +-------------+
**PACKET FORMATS:**
REGISTRATION REQUESTS, KEEP-ALIVE REQUESTS AND RESPONSES:
The fields 'LINKING', 'FLAGS' and 'VERSION' are described in detail in the next section.
TYPE(1 Byte) + SRC_ID (4 Bytes) + LINKING (1 Byte) + FLAGS (4 Bytes) + VERSION (4 Bytes) [+ AUTHENTICATION (10 Bytes)]
90 0004C2C0 6A 000080DC 04030400 [AUTHENTICATION (10 Bytes)]
PEER LIST REQUEST:
TYPE(1 Byte) + SRC_ID (4 Bytes) [+ AUTHENTICATION (10 Bytes)]
92 0004C2C0 [AUTHENTICATION (10 Bytes)]
PEER LIST RESPONSE:
TYPE(1 Byte) + SRC_ID (4 Bytes) + PEER_LIST_LENGTH* (2 Bytes) + {PEER_ID, PEER_IP, PEER_PORT, PEER_LINKING}... [+ AUTHENTICATION (10 Bytes)]
93 0004c2c0 002c
00000001 6ccf7505 c351 6a
0004c2c3 d17271e9 c35a 6a
0004c2c5 446716bb c35c 6a
00c83265 a471c50c c351 6a
d66a94568d29357205c2
*Number of peers can be derived from PEER_LIST_LENGTH, as each peer entry is 11 bytes (Thanks Hans!)
Number of peers can be derived from PEER_LIST_LENGTH, as each peer entry is 11 bytes
**CAPABILITIES: Bytes 6-14 (6-16 for master reg. reply):**
(Displayed in most to least significant bytes)
***LINKING STATUS: Byte 6***
Byte 1 - BIT FLAGS:
xx.. .... = Peer Operational (01 only known valid value)
..xx .... = Peer MODE: 00 - No Radio, 01 - Analog, 10 - Digital
.... xx.. = IPSC Slot 1: 10 on, 01 off
.... ..xx = IPSC Slot 2: 10 on, 01 off
***SERVICE FLAGS: Bytes 7-10 (or 7-12)***
Byte 1 - 0x00 = Unknown
Byte 2 - 0x00 = Unknown
Byte 3 - BIT FLAGS:
x... .... = CSBK Message
.x.. .... = Repeater Call Monitoring
..x. .... = 3rd Party "Console" Application
...x xxxx = Unknown - default to 0
Byte 4 = BIT FLAGS:
x... .... = XNL Connected (1=true)
.x.. .... = XNL Master Device
..x. .... = XNL Slave Device
...x .... = Set if packets are authenticated
.... x... = Set if data calls are supported
.... .x.. = Set if voice calls are supported
.... ..x. = Unknown - default to 0
.... ...x = Set if master
(the following only used in registration response from master)
NUMBER of PEERS: 2 Bytes
Byte 5 - 0x00 = Unknown
Byte 6 - Number of Peers (not including us - ODDLY FORMATTED!!!)
***PROTOCOL VERSION: Bytes 11-14 (or 12-16)***
(These are pure guesses based on repeater and c-Bridge code revisions)
Bytes 1-2 - 0x04, 0x03 = Current version? (numbering scheme unknown)
Bytes 3-4 = 0x04, 0x00 = Oldest supported version? (same as above)
**SAMPLE CODE:**
*Sample Python3 code to generate the authentication digest:*
import binascii
import hmac
import hashlib
def add_authentication (_payload, _key):
_digest = binascii.unhexlify((hmac.new(_key,_payload,hashlib.sha1)).hexdigest()[:20])
_full_payload = _payload + _digest
return _full_payload
PAYLOAD = binascii.unhexlify('90000000016a000080dc04030400') # Registration packet
KEY = binascii.unhexlify('0000000000000000000000000000000000012345') # Key '12345'
FULL_PAYLOAD = add_authentication(PAYLOAD, KEY)
print(binascii.b2a_hex(FULL_PAYLOAD))
Copyright (C) 2013-2017 Cortney T. Buffington, N0MJS <n0mjs@me.com>

246
LICENSE.txt Normal file → Executable file
View File

@ -1,7 +1,241 @@
Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
GNU GENERAL PUBLIC LICENSE
This work is licensed under the Creative Commons Attribution-ShareAlike
3.0 Unported License.To view a copy of this license, visit
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
Creative Commons, 444 Castro Street, Suite 900, Mountain View,
California, 94041, USA.
Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and other kinds of works.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
“This License” refers to version 3 of the GNU General Public License.
“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
A “covered work” means either the unmodified Program or a work based on the Program.
To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This 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/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.

272
README.md Normal file → Executable file
View File

@ -1,4 +1,24 @@
***OFFICIAL VERSION V0.2 RELEASE***
---
### FOR SUPPORT, DISCUSSION, GETTING INVOLVED ###
Please join the DVSwitch group at groups.io for online forum support, discussion, and to become part of the development team.
DVSwitch@groups.io
---
**FOR THE IMPATENT PLEASE AT LEAST READ THIS**
There are two ways to "install" DMRlink:
1) Use the mk-drmlink
2) Don't
The mk-dmrlink script will create a directory tree and copy all of the pertient files, making duplicates of some things so that each of the applications have a full set of all of the files they need, standalone.
Not using the mk-dmrlink leaves the structure as it existis in the repo, which is fully functional. It trades the autonomy of the file tree and duplicates for a simpler installation, and the ability to sync to new versions more easily.
The one you use is up to you -- just please don't blindly go download it and type "./mk-dmrlink" becuase that's just what you always do. Please think about it.
##PROJECT: Open Source IPSC Client.
@ -20,241 +40,47 @@ When communications exchanges are described, the symbols "->" and "<-" are used
**HOW TO USE THIS SOFTWARE:**
The primary objective is the IPSC "stack" itself, and is represended in dmrlink.py. It gets the majority of work, and the applicaitons are examples to show how dmrlink.py can be used. As such, dmrlink.py, dmrlink.cfg and the ipsc directory are pre-requisites for eveyrthing here. dmrlink.py does very little on it's own, but you should ALWAYS make sure it runs directly, as nothing else will work if it does not. Turn on the logging options, set the logger to DEBUG and watch to make sure everything works right **BEFORE** working with the application files.
dmrlink, optionally, uses three additional files to map dmr identifiers to understandable names. These are the .csv files for subscribers, peers (other repeaters, 3rd party console applications, etc.) and one for common talkgroups. These files are really only meaningful for logging and reporting.
dmrlink, optionally, uses three additional files to map DMR identifiers to understandable names. These are the .csv files for subscribers, peers (other repeaters, 3rd party console applications, etc.) and one for common talkgroups. These files are really only meaningful for logging and reporting.
The remaining files are sample applicaitons that sub-class dmrlink. Since dmrlink takes a default action on each packet type, overriding the class methods for particular packet types are how the examples are presented. For example, bridge.py only needs access to group voice packets, so the group_voice class method is overridden to perform the bridging function. In this particlar example, several other class methods are also overridden, but only to set them to do nothing.
**FILES:**
+ ***dmrlink.py, dmrlink.cfg, ipsc (directory):*** Core files for dmrlink to work
+ ***talkgroup_ids.csv, subscriber_ids.csv, peer_ids.csv:*** DMR numeric ID to name mapping files (optional)
+ ***bridge.py, log.py, rcm.py, playback.py:*** Sample applications to demonstrate dmrlink's abilities
+ ***bridge.py, log.py, rcm.py, playback.py, playback.py, play_group.py, record.py, confbridge.py:*** Sample applications to demonstrate dmrlink's abilities
+ ***files with SAMPLE in the name:*** Configuration files for certain apps - remove "_SAMPLE" and customize to your needs to use. for example, "dmrlink_SAMPLE.cfg" becomes "dmrlink.cfg"
**SAMPLE APPLICATIONS:**
***bridge.py:*** This applicaiton allows DMRlink to function as an IPSC bridge. Bridging means connecting multiple IPSC networks together, and allowing only certain timeslot/group IDs, etc. to pass between them. Sometimes re-writing the talkgroup ID, etc. bridge.py does not have nearly the wealth of features that commercial IPSC bridges do, but works very, very well. One unique feature here is that bridge.py can be told the radio ID of other bridges and can operate in multiple bridge active/standby/standby... configurations -- which is to say, you may have TWO bridges configured in the same IPSC, set to bridge the same talkgroups, but only one will be active at a time, offering multiple-bridge redundancy to minimize service outages. When two or more instances of DMRlink are connected to each other, and they are the ONLY devices in the IPSC, you may trunk many more than 2 packet streams at once between them.
***confbridge.py*** This application This applicaiton allows DMRlink to function as an IPSC bridge. Bridging means connecting multiple IPSC networks together, and allowing only certain timeslot/group IDs, etc. to pass between them. Sometimes re-writing the talkgroup ID, etc.confbridge.py is very similar to bridge.py, except it implements a simpler rule file that works around the concept of a "conference bridge" not unlike you'd use on a telephone system. Or maybe like a "reflector" for those of you who are d$tar users. It's also a lot like a "bridge group" with integrated "super group" characteristics of a c-Bridge.
***log.py:*** This is a logging application based on gathering data from the actual call packet stream. It doesn't do a whole awful lot, but it is an example of the IPSC side of building a back-end data gathering applicaiton without screen-scraping or tcpdumping a c-Bridge, etc. As a sample app, it just logs to the screen, or a file, etc. but it would be trivial to have it log to a database for web presentation, a central syslog server, etc.
***rcm.py:*** Very similar to log.py, but this one uses a feature called "Repeater Call Monitor" to get nearly identical data, but puts a MUCH smaller load on the IPSC network, since user call datastreams aren't forwarded to DMRlink running as (and properly configured as) an RCM. All of the logging, almost none of the overhead. Again, beats the pants off of screen-scraping, tcpdumping, etc.
***playback.py:*** As of this writing, a large multi-national ham radio group has deployed this on TGID 9998 and dubbed it the "parrot". This application can listen to group and/or private voice transmissions and if you trasmit to the group and/or private IDs it's programmed to use, it will record the digital packet stream and then re-play it back. Handy for listening to your audio to see what you actually sound like on the air.
***play_group.py:*** NOT YET STABLE. This applicaiton is for playing back pre-recorded audio messages based on particlar events. Events could be IPSC-based (like a keyup on a particular TS/TGID combination, time of day, etc.). It works, but requires quite a bit of under-the-hood mucking about as of now.
***record.py:*** Companion applicaiton to play_group.py. This will never be "fancy", since it's intended as a utility for network operators to use to capture voice call packet streams to be played back later. It will be improved beyond where it is at now, but not a high priority.
**CONFIGURATION:**
The configuration file for dmrlink is in ".ini" format, and is self-documented. A warning not in the self-documentation: Don't enable features you do not undertand, it can break dmrlink or the target IPSC (nothing turning off dmrlink shouldn't fix). There are options avaialble because the IPSC protocol appears to make them available, but dmrlink doesn't yet understand them. For exmaple, dmrlink does not process XNL/XCMP. If you enable it, and other peers expect interaction with it, the results may be unpredictable. Chances are, you'll confuse applications like RDAC that require it.
###CONNECTION ESTABLISHMENT AND MAINTENANCE
**CORE CONCEPTS:**
The IPSC system contains, essentially, two types of nodes: Master and Peer. Each IPSC network has exactly one master device and zero or more peers, recommended not to exceed 15. IPSC nodes may be a number of types of systems, such as repeaters, dispatch consoles, application software, etc. For example, the Motorola RDAC application acts as a peer in the IPSC network, though it doesn't operate as a repeater. The IPSC protocol supports many possible node types, and only a few have been identified. This document currently only explores repeaters - both Master and Peer, and their roles in the IPSC network.
All IPSC communication is via UDP, and only the master needs a static IP address. Masters will operate behind NATs. A single UDP port, specified in programming the IPSC master device must be mapped through any NAT/stateful firewalls for the master, while peers require no special treatment.
All nodes in an IPSC network maintain communication with each other at all times. The role of the master is merely to coordinate the joining of new nodes to the IPSC network. A functional IPSC network will continue without its master, as long as no new nodes need to join (or existing nodes need to re-join after a communications outage, etc.) This is one of the most important core concepts in IPSC, as it is central to the NAT traversal AND tracking of active peers.
Each peer will send keep-alives to each other peer in the IPSC network at an interval specified in the devices "firewall open timer". The elegantly simple, yet effective approach of IPSC, uses this keep-alive to both open, and keep open stateful firewall and NAT translations between peers. Since each device handles all communications from a single UDP port, when a device sends a keep-alive or a registration request to another device, the source-destination address/port tuple for that communication is opened through stateful devices. The only requirement to maintain communication is that this timer be shorter than the UDP session timeout of network control elements (firewalls, packet shapers, NATs, etc.) Moreover, it does NOT appear that all devices in the IPSC network require the same setting for this. Each device would appear to maintain its own set timing without interference from different interval settings on other nodes in the IPSC.
**KNOWN IPSC PACKET TYPES:**
The following sections of this document will include various packet types. This is a list of currently known types and their meanings. Note: The names are arbitrarily chosen with the intention of being descriptive, and each is defined by what they've been "observed" to do in the wild.
CALL_CONFIRMATION = 0x05 Confirmation FROM the recipient of a confirmed call.
CALL_MON_ORIGIN = 0x61 Sent to Repeater Call Monitor Peers from repeater originating a call
CALL_MON_RPT = 0x62 Sent to Repeater Call Monitor Peers from all repeaters repeating a call
CALL_MON_NACK = 0x63 Sent to Repeater Call Monitor Peers from repeaters that cannot transmit a call (ie. ID in progress)
XCMP_XNL = 0x70 Control protocol messages
GROUP_VOICE = 0x80 This is a group voice call
PVT_VOICE = 0x81 This is a private voice call
GROUP_DATA = 0x83 This is a group data call
PVT_DATA = 0x84 This is a private data call
RPT_WAKE_UP = 0x85 Wakes up all repeaters on the IPSC
MASTER_REG_REQ = 0x90 Request registration with master (from peer, to master)
MASTER_REG_REPLY = 0x91 Master registration request reply (from master, to peer)
PEER_LIST_REQ = 0x92 Request peer list from master
PEER_LIST_REPLY = 0x93 Master peer list reply
PEER_REG_REQ = 0x94 Peer registration request
PEER_REG_REPLY = 0x95 Peer registration response
MASTER_ALIVE_REQ = 0x96 Master keep alive request (to master)
MASTER_ALIVE_REPLY = 0x97 Master keep alive reply (from master)
PEER_ALIVE_REQ = 0x98 Peer keep alive request (to peer)
PEER_ALIVE_REPLY = 0x99 Peer keep alive reply (from peer)
DE_REG_REQ = 0x9a De-registraiton request (to master or all?)
DE_REG_REPLY = 0x9b De-registration reply (from master or all?)
**AUTHENTICATION:**
Most IPSC networks will be operated as "authenticated". This means that a key is used to create a digest of the packets exchanged in order to authenticate them. Each node in the IPSC network must have the authentication key programmed in order for the mechanism to work. The process is based on the SHA-1 digest protocol, where the "key" is a 20 byte hexadecimal *string* (if a shorter key is programmed, leading zeros are used to create a 20 byte key). The IPSC payload and the key are used to create the digest, of which only the most significant 10 bytes are used (the last 10 are truncated). This digest is appended to the end of the IPSC payload before transmission. An example is illustrated below:
IPSC Registration Packet Digest
90000000016a000080dc04030400 b0ec45f4c3f8fb0c0b1d
**CONNECTION CREATION AND MAINTENANCE:**
The IPSC network truly "forms" when the first peer registers with the master. All peers register with the master in the same way, with a slight variation from the first peer. Below is a descirption of the process and states in creating a connection, as a peer, and maitaining it.
There are various states, timers and counters associated with each. When peers or the master send us requests, we should answer them immediatley. Our own communcation with them is timed, and may share the same timer. Counter values should be the same for every master and peer in an IPSC. They don't have to be, but that is what mother M does, and it saves a lot of resources.
*COMMUNICATION WITH MASTER:*
The following illustrates the communication that a peer (us, for example) has with the master. The peer must register, then send keep-alives at an arbitrary interval (usually 5 - 30 seconds). If more than some arbitrary number of keep-alives are missed, we should return to the beginning and attempt to register again -- but do NOT elimiate the peers list, as peers may still be active. The only additional communcation with the master is if the master sends an unsolicited peer list. In this case, we should update our peer list as appropriate and continue.
+-----------------+
|Send Registration|
+---------------------------->|Request To Master|<-------------+
| +--------+--------+ |
| | |
| v |
| +--------------+ +-----+------+
| |Did The Master| NO |Wait FW Open|
| | Respond ? +-------->| Timer |
| +----+-----+---+ +------------+
| | |
| | YES |
| +-------------+ v |
| |Add 1 To Keep| +----------------+ | +-------------+
| | Alive Missed| |Send Master Keep| | |Is Peer Count|
| | Counter +---->| Alive Request | +------------>| > 1 ? |
| +-------------+ +-------+--------+ +------+------+
| ^ | ^ | YES
YES| | NO v | v
+---+---------+--+ +------------+ | +-----------------+
| Is The Missed | |Wait FW Open| | |Request Peer List|
| Keep-Alive | | Timer | | | From Master |<-----+
|Count Exceeded ?| +-----+------+ | +-------+---------+ |
+----------------+ | | | |
^ v | v |
| +--------------+ ++-------------+ +---------+ |
| NO |Did The Master| YES |Set Keep Alive| |Peer List| NO |
+-------------+ Respond ? +---->| Counter To 0 | |Received?+----------+
+--------------+ +--------------+ +---------+
*COMMUNICATION WITH PEERS:*
Once we have registered with the master, it will send a peer list update to any existing peers. Those peers will **immediately** respond by sending peer registrations to us, and then keep alives once we answer. We should send responses to any such requests as long as we have the peer in our own peer list -- which means we may miss one while waiting for receipt of our own peer list from the master. Even though we receive registration requests and keep-alives from the peers, we should send the same to them, even though this is redundant, it is how we ensure that firewall UDP sessions remain open. A bit wonky, but elegant. For example, a peer may not have a firewall, so it only sends keep-alives every 30 seconds, but we may need to every 5; which we achieve by sending our own keep-alives based on our own timer. The diagram only shows the action for the *initial* peer list reply from the master. Unsolicited peer lists from the master should update the list, and take appropriate action: De-register peers not in the new list, or begin registration for new peers.
+-----------------+ +-------------+
|Recieve Peer List| |Received Peer|
| From Master | |Leave Notice?|
+------+----------+ +------+------+
| |
v FOR EACH PEER |
+----------------------+ v
|Send Peer Registration| +-----------+
+------------------->| Request |<-----------+ |Remove Peer|
| +----------+-----------+ | | From List |
| | | +-----------+
| v |
| +---------------------+ +------+------+
| +---------+ |Registration Response| NO |Wait Firewall|
| |+1 Missed| | Recieved ? +---->| Open Timer |
| | Counter | +---------+-----------+ +-------------+
| +-------+-+ |
| ^ | v YES
| | | +----------+
| | +--------------->|Send Peer |
| | +-------->|Keep Alive|
| | | +----+-----+
|YES |NO | |
+---+---------+--+ +-----+------+ |
| Keep Alive | | Set Missed | |
| Count Exceeded?| |Counter to 0| |
+----------------+ +------------+ |
NO ^ ^ YES |
| | v
+---+------+----+ +-------------+
| Peer Keep | |Wait Firewall|
|Alive Received?|<------+ Open Timer |
+---------------+ +-------------+
**PACKET FORMATS:**
REGISTRATION REQUESTS, KEEP-ALIVE REQUESTS AND RESPONSES:
The fields 'LINKING', 'FLAGS' and 'VERSION' are described in detail in the next section.
TYPE(1 Byte) + SRC_ID (4 Bytes) + LINKING (1 Byte) + FLAGS (4 Bytes) + VERSION (4 Bytes) [+ AUTHENTICATION (10 Bytes)]
90 0004C2C0 6A 000080DC 04030400 [AUTHENTICATION (10 Bytes)]
PEER LIST REQUEST:
TYPE(1 Byte) + SRC_ID (4 Bytes) [+ AUTHENTICATION (10 Bytes)]
92 0004C2C0 [AUTHENTICATION (10 Bytes)]
PEER LIST RESPONSE:
TYPE(1 Byte) + SRC_ID (4 Bytes) + PEER_LIST_LENGTH* (2 Bytes) + {PEER_ID, PEER_IP, PEER_PORT, PEER_LINKING}... [+ AUTHENTICATION (10 Bytes)]
93 0004c2c0 002c
00000001 6ccf7505 c351 6a
0004c2c3 d17271e9 c35a 6a
0004c2c5 446716bb c35c 6a
00c83265 a471c50c c351 6a
d66a94568d29357205c2
*Number of peers can be derived from PEER_LIST_LENGTH, as each peer entry is 11 bytes (Thanks Hans!)
Number of peers can be derived from PEER_LIST_LENGTH, as each peer entry is 11 bytes
**CAPABILITIES: Bytes 6-14 (6-16 for master reg. reply):**
(Displayed in most to least significant bytes)
***LINKING STATUS: Byte 6***
Byte 1 - BIT FLAGS:
xx.. .... = Peer Operational (01 only known valid value)
..xx .... = Peer MODE: 00 - No Radio, 01 - Analog, 10 - Digital
.... xx.. = IPSC Slot 1: 10 on, 01 off
.... ..xx = IPSC Slot 2: 10 on, 01 off
***SERVICE FLAGS: Bytes 7-10 (or 7-12)***
Byte 1 - 0x00 = Unknown
Byte 2 - 0x00 = Unknown
Byte 3 - BIT FLAGS:
x... .... = CSBK Message
.x.. .... = Repeater Call Monitoring
..x. .... = 3rd Party "Console" Application
...x xxxx = Unknown - default to 0
Byte 4 = BIT FLAGS:
x... .... = XNL Connected (1=true)
.x.. .... = XNL Master Device
..x. .... = XNL Slave Device
...x .... = Set if packets are authenticated
.... x... = Set if data calls are supported
.... .x.. = Set if voice calls are supported
.... ..x. = Unknown - default to 0
.... ...x = Set if master
(the following only used in registration response from master)
NUMBER of PEERS: 2 Bytes
Byte 5 - 0x00 = Unknown
Byte 6 - Number of Peers (not including us - ODDLY FORMATTED!!!)
***PROTOCOL VERSION: Bytes 11-14 (or 12-16)***
(These are pure guesses based on repeater and c-Bridge code revisions)
Bytes 1-2 - 0x04, 0x03 = Current version? (numbering scheme unknown)
Bytes 3-4 = 0x04, 0x00 = Oldest supported version? (same as above)
**SAMPLE CODE:**
*Sample Python3 code to generate the authentication digest:*
import binascii
import hmac
import hashlib
def add_authentication (_payload, _key):
_digest = binascii.unhexlify((hmac.new(_key,_payload,hashlib.sha1)).hexdigest()[:20])
_full_payload = _payload + _digest
return _full_payload
PAYLOAD = binascii.unhexlify('90000000016a000080dc04030400') # Registration packet
KEY = binascii.unhexlify('0000000000000000000000000000000000012345') # Key '12345'
FULL_PAYLOAD = add_authentication(PAYLOAD, KEY)
print(binascii.b2a_hex(FULL_PAYLOAD))
The configuration file for dmrlink is in ".ini" format, and is self-documented. A warning not in the self-documentation: Don't enable features you do not undertand, it can break dmrlink or the target IPSC (nothing turning off dmrlink shouldn't fix). There are options avaialble because the IPSC protocol appears to make them available, but dmrlink doesn't yet understand them. For exmaple, dmrlink does not process XNL/XCMP. If you enable it, and other peers expect interaction with it, the results may be unpredictable. Chances are, you'll confuse applications like RDAC that require it. The advantage to dmrlink not processing XNL/XCMP is that it also cannot "brick" a repeater or subscriber, since all of these dangerous features use XNL/XCMP.
**NOTE:**
This is important: If you e-mail me asking about dmrlink and don't use the phrase "here I am, rock you like a hurricane" (at least initially), I will probably delete your e-mail without reading it. I need to be very clear. This software is NOT intended to be an out-of-box replacement for c-Bridge, SmartPTT, GenWatch, RDAC, etc. Please do not contact me with the express intent of wanting to know how to configure it to do the same thing as any of these fine products, because it doesn't do what they do, and will likely never be something you can "just run" and peform those functions with. This is free software, shared with the world so that others can learn from or do useful things with it. The price of open source is that I didn't sell you a product, and there is no support or warranty, or even responsiblity on my part for your use of it. If you want something that is a c-Bridge or SmartPTT, then please go buy one of those products -- they work great, I own both. Using dmrlink will require you to get your hands dirty. Using dmrlink requires basic understanding of Python. If you have read this README.md, have looked for comments or other direction within the files themsleves, and understand I owe you nothing, then please e-mail me, and I'll try to help if I can.
This is important: If you e-mail me asking about dmrlink and don't use the phrase "here I am, rock you like a hurricane" (at least initially), I will probably delete your e-mail without reading it. I need to be very clear. This software is NOT intended to be an out-of-box replacement for c-Bridge, SmartPTT, GenWatch, RDAC, etc. Please do not contact me with the express intent of wanting to know how to configure it to do the same thing as any of these fine products in a production environment, because it doesn't do all of the things that they do, and will likely never be something you can "just run" and peform those functions without more knowledge and patience. This is free software, shared with the world so that others can learn from it or do useful things with it. If you want something that is a c-Bridge or SmartPTT, then please go buy one of those products -- they work great, K0USY Group owns and uses both. If you're a tinkerer, or don't need a commercial grade solution, and want to get your "hands dirty", then DMRlink might be right for you. Using dmrlink requires only a very basic understanding of Python. If you have read this README.md, have looked for comments or other direction within the files themsleves, and still can't figure something out, then please e-mail me, and I'll try to help if I can.
***73 DE N0MJS***
***0x49 DE N0MJS***
Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
Copyright (C) 2013-2017 Cortney T. Buffington, N0MJS <n0mjs@me.com>
This work is licensed under the Creative Commons Attribution-ShareAlike
3.0 Unported License.To view a copy of this license, visit
http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
Creative Commons, 444 Castro Street, Suite 900, Mountain View,
California, 94041, USA.
This 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

5
Retired/README.MD Executable file
View File

@ -0,0 +1,5 @@
**Retired files:**
The files in this directory are being kept for reference ONLY. They contain routines that may have been of use to someone.
Do not try to use these programs as is. They will not work!

54
Retired/ambe_audio.cfg Executable file
View File

@ -0,0 +1,54 @@
################################################
# ambe_audio configuration file.
################################################
# DEFAULTS - General settings. These values are
# inherited in each subsequent section (defined by section value).
[DEFAULTS]
debug = False # Debug output for each VOICE frame
outToFile = False # Write each AMBE frame to a file called ambe.bin
outToUDP = True # Send each AMBE frame to the _sock object (turn on/off DMRGateway operation)
gateway = 127.0.0.1 # IP address of DMRGateway app
toGatewayPort = 31000 # Port DMRGateway is listening on for AMBE frames to decode
remoteControlPort = 31002 # Port that ambe_audio is listening on for remote control commands
fromGatewayPort = 31003 # Port to listen on for AMBE frames to transmit to all peers
gatewayDmrId = 0 # id to use when transmitting from the gateway
tgFilter = 9 # A list of TG IDs to monitor. All TGs will be passed to DMRGateway
txTg = 9 # TG to use for all frames received from DMRGateway -> IPSC
txTs = 2 # Slot to use for frames received from DMRGateway -> IPSC
#
# The section setting defines the current section to use. By default, the ENABLED section in dmrlink.cfg is used.
# Any values in the named section override the values from the DEFAULTS section. For example, if the BM section
# has a value for gatewayDmrId it would override the value above. Only one section should be set here. Think
# of this as an easy way to switch between several different configurations with a single line.
#
# section = BM # Use BM section values
# section = Sandbox # Use SANDBOX section values
[BM] # BrandMeister
tgFilter = 3100,31094 # A list of TG IDs to monitor. All TGs will be passed to DMRGateway
txTg = 3100 # TG to use for all frames received from DMRGateway -> IPSC
txTs = 2 # Slot to use for frames received from DMRGateway -> IPSC
[BM2] # Alternate BM configuration
tgFilter = 31094
txTg = 31094
txTs = 2
[Sandbox] # DMR MARC sandbox network
tgFilter = 3120
txTg = 3120
txTs = 2
[Sandbox2] # DMR MARC sandbox network
tgFilter = 1
txTg = 1
txTs = 1
[N4IRS] # N4IRS/INAD network
tgFilter = 1,2,3,13,3174,3777215,3100,9,9998,3112,3136,310,311,312,9997
txTg = 9998
txTs = 2

678
Retired/ambe_audio.py Executable file
View File

@ -0,0 +1,678 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample applicaiton that dumps all raw AMBE+2 voice frame data
# It is useful for things like, decoding the audio stream with a DVSI dongle, etc.
from __future__ import print_function
from twisted.internet import reactor
from binascii import b2a_hex as h
from bitstring import BitArray
import sys, socket, ConfigParser, thread, traceback
import cPickle as pickle
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, build_aliases, config_reports
from dmr_utils.utils import int_id, hex_str_3, hex_str_4, get_alias, get_info
from time import time, sleep, clock, localtime, strftime
import csv
import struct
from random import randint
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski, KD8EYF; Robert Garcia, N5QM; Steve Zingman, N4IRS; Mike Zingman, N4IRR'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
try:
from ipsc.ipsc_const import *
except ImportError:
sys.exit('IPSC constants file not found or invalid')
try:
from ipsc.ipsc_mask import *
except ImportError:
sys.exit('IPSC mask values file not found or invalid')
#
# ambeIPSC class,
#
class ambeIPSC(IPSC):
_configFile='ambe_audio.cfg' # Name of the config file to over-ride these default values
_debug = False # Debug output for each VOICE frame
_outToFile = False # Write each AMBE frame to a file called ambe.bin
_outToUDP = True # Send each AMBE frame to the _sock object (turn on/off DMRGateway operation)
#_gateway = "192.168.1.184"
_gateway = "127.0.0.1" # IP address of DMRGateway app
_gateway_port = 31000 # Port DMRGateway is listening on for AMBE frames to decode
_remote_control_port = 31002 # Port that ambe_audio is listening on for remote control commands
_ambeRxPort = 31003 # Port to listen on for AMBE frames to transmit to all peers
_gateway_dmr_id = 0 # id to use when transmitting from the gateway
_tg_filter = [2,3,13,3174,3777215,3100,9,9998,3112] #set this to the tg to monitor
_no_tg = -99 # Flag (const) that defines a value for "no tg is currently active"
_busy_slots = [0,0,0] # Keep track of activity on each slot. Make sure app is polite
_sock = -1; # Socket object to send AMBE to DMRGateway
lastPacketTimeout = 0 # Time of last packet. Used to trigger an artifical TERM if one was not seen
_transmitStartTime = 0 # Used for info on transmission duration
_start_seq = 0 # Used to maintain error statistics for a transmission
_packet_count = 0 # Used to maintain error statistics for a transmission
_seq = 0 # Transmit frame sequence number (auto-increments for each frame)
_f = None # File handle for debug AMBE binary output
_tx_tg = hex_str_3(9998) # Hard code the destination TG. This ensures traffic will not show up on DMR-MARC
_tx_ts = 2 # Time Slot 2
_currentNetwork = ""
_dmrgui = ''
###### DEBUGDEBUGDEBUG
#_d = None
###### DEBUGDEBUGDEBUG
def __init__(self, _name, _config, _logger, _report):
IPSC.__init__(self, _name, _config, _logger, _report)
self.CALL_DATA = []
#
# Define default values for operation. These will be overridden by the .cfg file if found
#
self._currentTG = self._no_tg
self._currentNetwork = str(_name)
self.readConfigFile(self._configFile, None, self._currentNetwork)
logger.info('DMRLink ambe server')
if self._gateway_dmr_id == 0:
sys.exit( "Error: gatewayDmrId must be set (greater than zero)" )
#
# Open output sincs
#
if self._outToFile == True:
self._f = open('ambe.bin', 'wb')
logger.info('Opening output file: ambe.bin')
if self._outToUDP == True:
self._sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
logger.info('Send UDP frames to DMR gateway {}:{}'.format(self._gateway, self._gateway_port))
###### DEBUGDEBUGDEBUG
#self._d = open('recordData.bin', 'wb')
###### DEBUGDEBUGDEBUG
try:
thread.start_new_thread( self.remote_control, (self._remote_control_port, ) ) # Listen for remote control commands
thread.start_new_thread( self.launchUDP, (_name, ) ) # Package AMBE into IPSC frames and send to all peers
except:
traceback.print_exc()
logger.error( "Error: unable to start thread" )
# Utility function to convert bytes to string of hex values (for debug)
def ByteToHex( self, byteStr ):
return ''.join( [ "%02X " % ord(x) for x in byteStr ] ).strip()
#
# Now read the configuration file and parse out the values we need
#
def defaultOption( self, config, sec, opt, defaultValue ):
try:
_value = config.get(sec, opt).split(None)[0] # Get the value from the named section
except ConfigParser.NoOptionError as e:
try:
_value = config.get('DEFAULTS', opt).split(None)[0] # Try the global DEFAULTS section
except ConfigParser.NoOptionError as e:
_value = defaultValue # Not found anywhere, use the default value
logger.info(opt + ' = ' + str(_value))
return _value
def readConfigFile(self, configFileName, sec, networkName='DEFAULTS'):
config = ConfigParser.ConfigParser()
try:
config.read(configFileName)
if sec == None:
sec = self.defaultOption(config, 'DEFAULTS', 'section', networkName)
if config.has_section(sec) == False:
logger.error('Section ' + sec + ' was not found, using DEFAULTS')
sec = 'DEFAULTS'
self._debug = bool(self.defaultOption(config, sec,'debug', self._debug) == 'True')
self._outToFile = bool(self.defaultOption(config, sec,'outToFile', self._outToFile) == 'True')
self._outToUDP = bool(self.defaultOption(config, sec,'outToUDP', self._outToUDP) == 'True')
self._gateway = self.defaultOption(config, sec,'gateway', self._gateway)
self._gateway_port = int(self.defaultOption(config, sec,'toGatewayPort', self._gateway_port))
self._remote_control_port = int(self.defaultOption(config, sec,'remoteControlPort', self._remote_control_port))
self._ambeRxPort = int(self.defaultOption(config, sec,'fromGatewayPort', self._ambeRxPort))
self._gateway_dmr_id = int(self.defaultOption(config, sec, 'gatewayDmrId', self._gateway_dmr_id))
_tgs = self.defaultOption(config, sec,'tgFilter', str(self._tg_filter).strip('[]'))
self._tg_filter = map(int, _tgs.split(','))
self._tx_tg = hex_str_3(int(self.defaultOption(config, sec, 'txTg', int_id(self._tx_tg))))
self._tx_ts = int(self.defaultOption(config, sec, 'txTs', self._tx_ts))
except ConfigParser.NoOptionError as e:
print('Using a default value:', e)
except:
traceback.print_exc()
sys.exit('Configuration file \''+configFileName+'\' is not a valid configuration file! Exiting...')
def rewriteFrame( self, _frame, _newSlot, _newGroup, _newSouceID, _newPeerID ):
_peerid = _frame[1:5] # int32 peer who is sending us a packet
_src_sub = _frame[6:9] # int32 Id of source
_burst_data_type = _frame[30]
########################################################################
# re-Write the peer radio ID to that of this program
_frame = _frame.replace(_peerid, _newPeerID)
# re-Write the source subscriber ID to that of this program
_frame = _frame.replace(_src_sub, _newSouceID)
# Re-Write the destination Group ID
_frame = _frame.replace(_frame[9:12], _newGroup)
# Re-Write IPSC timeslot value
_call_info = int_id(_frame[17:18])
if _newSlot == 1:
_call_info &= ~(1 << 5)
elif _newSlot == 2:
_call_info |= 1 << 5
_call_info = chr(_call_info)
_frame = _frame[:17] + _call_info + _frame[18:]
_x = struct.pack("i", self._seq)
_frame = _frame[:20] + _x[1] + _x[0] + _frame[22:]
self._seq = self._seq + 1
# Re-Write DMR timeslot value
# Determine if the slot is present, so we can translate if need be
if _burst_data_type == BURST_DATA_TYPE['SLOT1_VOICE'] or _burst_data_type == BURST_DATA_TYPE['SLOT2_VOICE']:
# Re-Write timeslot if necessary...
if _newSlot == 1:
_burst_data_type = BURST_DATA_TYPE['SLOT1_VOICE']
elif _newSlot == 2:
_burst_data_type = BURST_DATA_TYPE['SLOT2_VOICE']
_frame = _frame[:30] + _burst_data_type + _frame[31:]
if (time() - self._busy_slots[_newSlot]) >= 0.10 : # slot is not busy so it is safe to transmit
# Send the packet to all peers in the target IPSC
self.send_to_ipsc(_frame)
else:
logger.info('Slot {} is busy, will not transmit packet from gateway'.format(_newSlot))
########################################################################
# Read a record from the captured IPSC file looking for a payload type that matches the filter
def readRecord(self, _file, _match_type):
_notEOF = True
# _file.seek(0)
while (_notEOF):
_data = ""
_bLen = _file.read(4)
if _bLen:
_len, = struct.unpack("i", _bLen)
if _len > 0:
_data = _file.read(_len)
_payload_type = _data[30]
if _payload_type == _match_type:
return _data
else:
_notEOF = False
else:
_notEOF = False
return _data
# Read bytes from the socket with "timeout" I hate this code.
def readSock( self, _sock, len ):
counter = 0
while(counter < 3):
_ambe = _sock.recv(len)
if _ambe: break
sleep(0.1)
counter = counter + 1
return _ambe
# Concatenate 3 frames from the stream into a bit array and return the bytes
def readAmbeFrameFromUDP( self, _sock ):
_ambeAll = BitArray() # Start with an empty array
for i in range(0, 3):
_ambe = self.readSock(_sock,7) # Read AMBE from the socket
if _ambe:
_ambe1 = BitArray('0x'+h(_ambe[0:49]))
_ambeAll += _ambe1[0:50] # Append the 49 bits to the string
else:
break
return _ambeAll.tobytes() # Return the 49 * 3 as an array of bytes
# Set up the socket and run the method to gather the AMBE. Sending it to all peers
def launchUDP(self, _name):
s = socket.socket() # Create a socket object
s.bind(('', self._ambeRxPort)) # Bind to the port
while (1): # Forever!
s.listen(5) # Now wait for client connection.
_sock, addr = s.accept() # Establish connection with client.
if int_id(self._tx_tg) > 0: # Test if we are allowed to transmit
self.playbackFromUDP(_sock) # SSZ was here.
else:
self.transmitDisabled(_sock, self._system) #tg is zero, so just eat the network trafic
_sock.close()
# This represents a full transmission (HEAD, VOICE and TERM)
def playbackFromUDP(self, _sock):
_delay = 0.055 # Yes, I know it should be 0.06, but there seems to be some latency, so this is a hack
_src_sub = hex_str_3(self._gateway_dmr_id) # DMR ID to sign this transmission with
_src_peer = self._config['LOCAL']['RADIO_ID'] # Use this peers ID as the source repeater
logger.info('Transmit from gateway to TG {}:'.format(int_id(self._tx_tg)) )
try:
try:
_t = open('template.bin', 'rb') # Open the template file. This was recorded OTA
_tempHead = [0] * 3 # It appears that there 3 frames of HEAD (mostly the same)
for i in range(0, 3):
_tempHead[i] = self.readRecord(_t, BURST_DATA_TYPE['VOICE_HEAD'])
_tempVoice = [0] * 6
for i in range(0, 6): # Then there are 6 frames of AMBE. We will just use them in order
_tempVoice[i] = self.readRecord(_t, BURST_DATA_TYPE['SLOT2_VOICE'])
_tempTerm = self.readRecord(_t, BURST_DATA_TYPE['VOICE_TERM'])
_t.close()
except IOError:
logger.error('Can not open template.bin file')
return
logger.debug('IPSC templates loaded')
_eof = False
self._seq = randint(0,32767) # A transmission uses a random number to begin its sequence (16 bit)
for i in range(0, 3): # Output the 3 HEAD frames to our peers
self.rewriteFrame(_tempHead[i], self._tx_ts, self._tx_tg, _src_sub, _src_peer)
#self.group_voice(self._system, _src_sub, self._tx_tg, True, '', hex_str_3(0), _tempHead[i])
sleep(_delay)
i = 0 # Initialize the VOICE template index
while(_eof == False):
_ambe = self.readAmbeFrameFromUDP(_sock) # Read the 49*3 bit sample from the stream
if _ambe:
i = (i + 1) % 6 # Round robbin with the 6 VOICE templates
_frame = _tempVoice[i][:33] + _ambe + _tempVoice[i][52:] # Insert the 3 49 bit AMBE frames
self.rewriteFrame(_frame, self._tx_ts, self._tx_tg, _src_sub, _src_peer)
#self.group_voice(self._system, _src_sub, self._tx_tg, True, '', hex_str_3(0), _frame)
sleep(_delay) # Since this comes from a file we have to add delay between IPSC frames
else:
_eof = True # There are no more AMBE frames, so terminate the loop
self.rewriteFrame(_tempTerm, self._tx_ts, self._tx_tg, _src_sub, _src_peer)
#self.group_voice(self._system, _src_sub, self._tx_tg, True, '', hex_str_3(0), _tempTerm)
except IOError:
logger.error('Can not transmit to peers')
logger.info('Transmit complete')
def transmitDisabled(self, _sock):
_eof = False
logger.debug('Transmit disabled begin')
while(_eof == False):
if self.readAmbeFrameFromUDP(_sock):
pass
else:
_eof = True # There are no more AMBE frames, so terminate the loop
logger.debug('Transmit disabled end')
# Debug method used to test the AMBE code.
def playbackFromFile(self, _fileName):
_r = open(_fileName, 'rb')
_eof = False
host = socket.gethostbyname(socket.gethostname()) # Get local machine name
_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
_sock.connect((host, self._ambeRxPort))
while(_eof == False):
for i in range(0, 3):
_ambe = _r.read(7)
if _ambe:
_sock.send(_ambe)
else:
_eof = True
sleep(0.055)
logger.info('File playback complete')
def dumpTemplate(self, _fileName):
_file = open(_fileName, 'rb')
_eof = False
while(_eof == False):
_data = ""
_bLen = _file.read(4)
if _bLen:
_len, = struct.unpack("i", _bLen)
if _len > 0:
_data = _file.read(_len)
self.dumpIPSCFrame(_data)
else:
_eof = True
logger.info('File dump complete')
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
#self.dumpIPSCFrame(_data)
# THIS FUNCTION IS NOT COMPLETE!!!!
_payload_type = _data[30:31]
# _ambe_frames = _data[33:52]
_ambe_frames = BitArray('0x'+h(_data[33:52]))
_ambe_frame1 = _ambe_frames[0:49]
_ambe_frame2 = _ambe_frames[50:99]
_ambe_frame3 = _ambe_frames[100:149]
_tg_id = int_id(_dst_sub)
self._busy_slots[_ts] = time()
###### DEBUGDEBUGDEBUG
# if _tg_id == 2:
# __iLen = len(_data)
# self._d.write(struct.pack("i", __iLen))
# self._d.write(_data)
# else:
# self.rewriteFrame(_data, 1, 9)
###### DEBUGDEBUGDEBUG
if _tg_id in self._tg_filter: #All TGs
_dst_sub = get_alias(_dst_sub, talkgroup_ids)
if _payload_type == BURST_DATA_TYPE['VOICE_HEAD']:
if self._currentTG == self._no_tg:
_src_sub = get_subscriber_info(_src_sub)
logger.info('Voice Transmission Start on TS {} and TG {} ({}) from {}'.format(_ts, _dst_sub, _tg_id, _src_sub))
self._sock.sendto('reply log2 {} {}'.format(_src_sub, _tg_id), (self._dmrgui, 34003))
self._currentTG = _tg_id
self._transmitStartTime = time()
self._start_seq = int_id(_data[20:22])
self._packet_count = 0
else:
if self._currentTG != _tg_id:
if time() > self.lastPacketTimeout:
self._currentTG = self._no_tg #looks like we never saw an EOT from the last stream
logger.warning('EOT timeout')
else:
logger.warning('Transmission in progress, will not decode stream on TG {}'.format(_tg_id))
if self._currentTG == _tg_id:
if _payload_type == BURST_DATA_TYPE['VOICE_TERM']:
_source_packets = ( int_id(_data[20:22]) - self._start_seq ) - 3 # the 3 is because the start and end are not part of the voice but counted in the RTP
if self._packet_count > _source_packets:
self._packet_count = _source_packets
if _source_packets > 0:
_lost_percentage = 100.0 - ((self._packet_count / float(_source_packets)) * 100.0)
else:
_lost_percentage = 0.0
_duration = (time() - self._transmitStartTime)
logger.info('Voice Transmission End {:.2f} seconds loss rate: {:.2f}% ({}/{})'.format(_duration, _lost_percentage, _source_packets - self._packet_count, _source_packets))
self._sock.sendto("reply log" +
strftime(" %m/%d/%y %H:%M:%S", localtime(self._transmitStartTime)) +
' {} {} "{}"'.format(get_subscriber_info(_src_sub), _ts, _dst_sub) +
' {:.2f}%'.format(_lost_percentage) +
' {:.2f}s'.format(_duration), (self._dmrgui, 34003))
self._currentTG = self._no_tg
if _payload_type == BURST_DATA_TYPE['SLOT1_VOICE']:
self.outputFrames(_ambe_frames, _ambe_frame1, _ambe_frame2, _ambe_frame3)
self._packet_count += 1
if _payload_type == BURST_DATA_TYPE['SLOT2_VOICE']:
self.outputFrames(_ambe_frames, _ambe_frame1, _ambe_frame2, _ambe_frame3)
self._packet_count += 1
self.lastPacketTimeout = time() + 10
else:
if _payload_type == BURST_DATA_TYPE['VOICE_HEAD']:
_dst_sub = get_alias(_dst_sub, talkgroup_ids)
logger.warning('Ignored Voice Transmission Start on TS {} and TG {}'.format(_ts, _dst_sub))
def outputFrames(self, _ambe_frames, _ambe_frame1, _ambe_frame2, _ambe_frame3):
if self._debug == True:
logger.debug(_ambe_frames)
logger.debug('Frame 1:', self.ByteToHex(_ambe_frame1.tobytes()))
logger.debug('Frame 2:', self.ByteToHex(_ambe_frame2.tobytes()))
logger.debug('Frame 3:', self.ByteToHex(_ambe_frame3.tobytes()))
if self._outToFile == True:
self._f.write( _ambe_frame1.tobytes() )
self._f.write( _ambe_frame2.tobytes() )
self._f.write( _ambe_frame3.tobytes() )
if self._outToUDP == True:
self._sock.sendto(_ambe_frame1.tobytes(), (self._gateway, self._gateway_port))
self._sock.sendto(_ambe_frame2.tobytes(), (self._gateway, self._gateway_port))
self._sock.sendto(_ambe_frame3.tobytes(), (self._gateway, self._gateway_port))
def private_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
print('private voice')
# __iLen = len(_data)
# self._d.write(struct.pack("i", __iLen))
# self._d.write(_data)
#
# Remote control thread
# Use netcat to dynamically change ambe_audio without a restart
# echo -n "tgs=x,y,z" | nc 127.0.0.1 31002
# echo -n "reread_subscribers" | nc 127.0.0.1 31002
# echo -n "reread_config" | nc 127.0.0.1 31002
# echo -n "txTg=##" | nc 127.0.0.1 31002
# echo -n "txTs=#" | nc 127.0.0.1 31002
# echo -n "section=XX" | nc 127.0.0.1 31002
#
def remote_control(self, port):
s = socket.socket() # Create a socket object
s.bind(('', port)) # Bind to the port
s.listen(5) # Now wait for client connection.
logger.info('Remote control is listening on {}:{}'.format(socket.getfqdn(), port))
while True:
c, addr = s.accept() # Establish connection with client.
logger.info( 'Got connection from {}'.format(addr) )
self._dmrgui = addr[0]
_tmp = c.recv(1024)
_tmp = _tmp.split(None)[0] #first get rid of whitespace
_cmd = _tmp.split('=')[0]
logger.info('Command:"{}"'.format(_cmd))
if _cmd:
if _cmd == 'reread_subscribers':
reread_subscribers()
elif _cmd == 'reread_config':
self.readConfigFile(self._configFile, None, self._currentNetwork)
elif _cmd == 'txTg':
self._tx_tg = hex_str_3(int(_tmp.split('=')[1]))
print('New txTg = ' + str(int_id(self._tx_tg)))
elif _cmd == 'txTs':
self._tx_ts = int(_tmp.split('=')[1])
print('New txTs = ' + str(self._tx_ts))
elif _cmd == 'section':
self.readConfigFile(self._configFile, _tmp.split('=')[1])
elif _cmd == 'gateway_dmr_id':
self._gateway_dmr_id = int(_tmp.split('=')[1])
print('New gateway_dmr_id = ' + str(self._gateway_dmr_id))
elif _cmd == 'gateway_peer_id':
peerID = int(_tmp.split('=')[1])
self._config['LOCAL']['RADIO_ID'] = hex_str_3(peerID)
print('New peer_id = ' + str(peerID))
elif _cmd == 'restart':
reactor.callFromThread(reactor.stop)
elif _cmd == 'playbackFromFile':
self.playbackFromFile('ambe.bin')
elif _cmd == 'tgs':
_args = _tmp.split('=')[1]
self._tg_filter = map(int, _args.split(','))
logger.info( 'New TGs={}'.format(self._tg_filter) )
elif _cmd == 'dump_template':
self.dumpTemplate('PrivateVoice.bin')
elif _cmd == 'get_alias':
self._sock.sendto('reply dmr_info {} {} {} {}'.format(self._currentNetwork,
int_id(self._CONFIG[self._currentNetwork]['LOCAL']['RADIO_ID']),
self._gateway_dmr_id,
get_subscriber_info(hex_str_3(self._gateway_dmr_id))), (self._dmrgui, 34003))
elif _cmd == 'eval':
_sz = len(_tmp)-5
_evalExpression = _tmp[-_sz:]
_evalResult = eval(_evalExpression)
print("eval of {} is {}".format(_evalExpression, _evalResult))
self._sock.sendto('reply eval {}'.format(_evalResult), (self._dmrgui, 34003))
elif _cmd == 'exec':
_sz = len(_tmp)-5
_evalExpression = _tmp[-_sz:]
exec(_evalExpression)
print("exec of {}".format(_evalExpression))
else:
logger.error('Unknown command')
c.close() # Close the connection
#************************************************
# Debug: print IPSC frame on console
#************************************************
def dumpIPSCFrame( self, _frame ):
_packettype = int_id(_frame[0:1]) # int8 GROUP_VOICE, PVT_VOICE, GROUP_DATA, PVT_DATA, CALL_MON_STATUS, CALL_MON_RPT, CALL_MON_NACK, XCMP_XNL, RPT_WAKE_UP, DE_REG_REQ
_peerid = int_id(_frame[1:5]) # int32 peer who is sending us a packet
_ipsc_seq = int_id(_frame[5:6]) # int8 looks like a sequence number for a packet
_src_sub = int_id(_frame[6:9]) # int32 Id of source
_dst_sub = int_id(_frame[9:12]) # int32 Id of destination
_call_type = int_id(_frame[12:13]) # int8 Priority Voice/Data
_call_ctrl_info = int_id(_frame[13:17]) # int32
_call_info = int_id(_frame[17:18]) # int8 Bits 6 and 7 defined as TS and END
# parse out the RTP values
_rtp_byte_1 = int_id(_frame[18:19]) # Call Ctrl Src
_rtp_byte_2 = int_id(_frame[19:20]) # Type
_rtp_seq = int_id(_frame[20:22]) # Call Seq No
_rtp_tmstmp = int_id(_frame[22:26]) # Timestamp
_rtp_ssid = int_id(_frame[26:30]) # Sync Src Id
_payload_type = _frame[30] # int8 VOICE_HEAD, VOICE_TERM, SLOT1_VOICE, SLOT2_VOICE
_ts = bool(_call_info & TS_CALL_MSK)
_end = bool(_call_info & END_MSK)
if _payload_type == BURST_DATA_TYPE['VOICE_HEAD']:
print('HEAD:', h(_frame))
if _payload_type == BURST_DATA_TYPE['VOICE_TERM']:
_ipsc_rssi_threshold_and_parity = int_id(_frame[31])
_ipsc_length_to_follow = int_id(_frame[32:34])
_ipsc_rssi_status = int_id(_frame[34])
_ipsc_slot_type_sync = int_id(_frame[35])
_ipsc_data_size = int_id(_frame[36:38])
_ipsc_data = _frame[38:38+(_ipsc_length_to_follow * 2)-4]
_ipsc_full_lc_byte1 = int_id(_frame[38])
_ipsc_full_lc_fid = int_id(_frame[39])
_ipsc_voice_pdu_service_options = int_id(_frame[40])
_ipsc_voice_pdu_dst = int_id(_frame[41:44])
_ipsc_voice_pdu_src = int_id(_frame[44:47])
print('{} {} {} {} {} {} {} {} {} {} {}'.format(_ipsc_rssi_threshold_and_parity,_ipsc_length_to_follow,_ipsc_rssi_status,_ipsc_slot_type_sync,_ipsc_data_size,h(_ipsc_data),_ipsc_full_lc_byte1,_ipsc_full_lc_fid,_ipsc_voice_pdu_service_options,_ipsc_voice_pdu_dst,_ipsc_voice_pdu_src))
print('TERM:', h(_frame))
if _payload_type == BURST_DATA_TYPE['SLOT1_VOICE']:
_rtp_len = _frame[31:32]
_ambe = _frame[33:52]
print('SLOT1:', h(_frame))
if _payload_type == BURST_DATA_TYPE['SLOT2_VOICE']:
_rtp_len = _frame[31:32]
_ambe = _frame[33:52]
print('SLOT2:', h(_frame))
print("pt={:02X} pid={} seq={:02X} src={} dst={} ct={:02X} uk={} ci={} rsq={}".format(_packettype, _peerid,_ipsc_seq, _src_sub,_dst_sub,_call_type,_call_ctrl_info,_call_info,_rtp_seq))
def get_subscriber_info(_src_sub):
return get_info(int_id(_src_sub), subscriber_ids)
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, ambeIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

31
Retired/ambe_audio_commands.txt Executable file
View File

@ -0,0 +1,31 @@
AllStar DTMF command examples:
82=cmd,/bin/bash -c 'do something here'
82=cmd,/bin/bash -c 'echo -n "section=Shutup" | nc 127.0.0.1 31002'
Shell command examples:
# Use netcat to dynamically change ambe_audio without a restart
# echo -n "tgs=x,y,z" | nc 127.0.0.1 31002
# echo -n "reread_subscribers" | nc 127.0.0.1 31002
# echo -n "reread_config" | nc 127.0.0.1 31002
# echo -n "txTg=##" | nc 127.0.0.1 31002
# echo -n "txTs=#" | nc 127.0.0.1 31002
# echo -n "section=XX" | nc 127.0.0.1 31002
Remote control commands:
'reread_subscribers'
'reread_config'
'txTg'
'txTs'
'section'
'gateway_dmr_id'
'gateway_peer_id'
'restart'
'playbackFromFile'
'tgs'
'dump_template'
'get_info'

530
Retired/bridge.py Executable file
View File

@ -0,0 +1,530 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application to bridge traffic between IPSC systems. it uses
# one required (bridge_rules.py) and one optional (known_bridges.py) additional
# configuration files. Both files have their own documentation for use.
#
# "bridge_rules" contains the IPSC network, Timeslot and TGID matching rules to
# determine which voice calls are bridged between IPSC systems and which are
# not.
#
# "known_bridges" contains DMR radio ID numbers of known bridges. This file is
# used when you want bridge.py to be "polite" or serve as a backup bridge. If
# a known bridge exists in either a source OR target IPSC network, then no
# bridging between those IPSC systems will take place. This behavior is
# dynamic and updates each keep-alive interval (main configuration file).
# For faster failover, configure a short keep-alive time and a low number of
# missed keep-alives before timout. I recommend 5 sec keep-alive and 3 missed.
# That gives a worst-case scenario of 15 seconds to fail over. Recovery will
# typically happen with a single "blip" in the transmission up to about 5
# seconds.
#
# While this file is listed as Beta status, K0USY Group depends on this code
# for the bridigng of it's many repeaters. We consider it reliable, but you
# get what you pay for... as usual, no guarantees.
#
# Use to make test strings: #print('PKT:', "\\x".join("{:02x}".format(ord(c)) for c in _data))
from __future__ import print_function
from twisted.internet import reactor
from twisted.internet import task
from binascii import b2a_hex as ahex
from time import time
from importlib import import_module
import sys
from dmr_utils.utils import hex_str_3, hex_str_4, int_id
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, REPORT_OPCODES, build_aliases, config_reports
from ipsc.ipsc_const import BURST_DATA_TYPE
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski, KD8EYF; Steve Zingman, N4IRS; Mike Zingman, N4IRR'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
# Minimum time between different subscribers transmitting on the same TGID
#
TS_CLEAR_TIME = .2
# Import Bridging rules
# Note: A stanza *must* exist for any IPSC configured in the main
# configuration file and listed as "active". It can be empty,
# but it has to exist.
#
def build_rules(_bridge_rules):
try:
rule_file = import_module(_bridge_rules)
logger.info('Bridge rules file found and rules imported')
except ImportError:
sys.exit('Bridging rules file not found or invalid')
# Convert integer GROUP ID numbers from the config into hex strings
# we need to send in the actual data packets.
#
for _ipsc in rule_file.RULES:
for _rule in rule_file.RULES[_ipsc]['GROUP_VOICE']:
_rule['SRC_GROUP'] = hex_str_3(_rule['SRC_GROUP'])
_rule['DST_GROUP'] = hex_str_3(_rule['DST_GROUP'])
_rule['SRC_TS'] = _rule['SRC_TS']
_rule['DST_TS'] = _rule['DST_TS']
for i, e in enumerate(_rule['ON']):
_rule['ON'][i] = hex_str_3(_rule['ON'][i])
for i, e in enumerate(_rule['OFF']):
_rule['OFF'][i] = hex_str_3(_rule['OFF'][i])
_rule['TIMEOUT']= _rule['TIMEOUT']*60
_rule['TIMER'] = time() + _rule['TIMEOUT']
if _ipsc not in CONFIG['SYSTEMS']:
sys.exit('ERROR: Bridge rules found for an IPSC network not configured in main configuration')
for _ipsc in CONFIG['SYSTEMS']:
if _ipsc not in rule_file.RULES:
sys.exit('ERROR: Bridge rules not found for all IPSC network configured')
return rule_file.RULES
# Import List of Bridges
# This is how we identify known bridges. If one of these is present
# and it's mode byte is set to bridge, we don't
#
def build_bridges(_known_bridges):
try:
bridges_file = import_module(_known_bridges)
logger.info('Known bridges file found and bridge ID list imported ')
return bridges_file.BRIDGES
except ImportError:
logger.critical('\'known_bridges.py\' not found - backup bridge service will not be enabled')
return []
# Import subscriber ACL
# ACL may be a single list of subscriber IDs
# Global action is to allow or deny them. Multiple lists with different actions and ranges
# are not yet implemented.
def build_acl(_sub_acl):
try:
logger.info('ACL file found, importing entries. This will take about 1.5 seconds per 1 million IDs')
acl_file = import_module(_sub_acl)
sections = acl_file.ACL.split(':')
ACL_ACTION = sections[0]
entries_str = sections[1]
ACL = set()
for entry in entries_str.split(','):
if '-' in entry:
start,end = entry.split('-')
start,end = int(start), int(end)
for id in range(start, end+1):
ACL.add(hex_str_3(id))
else:
id = int(entry)
ACL.add(hex_str_3(id))
logger.info('ACL loaded: action "{}" for {:,} radio IDs'.format(ACL_ACTION, len(ACL)))
except ImportError:
logger.info('ACL file not found or invalid - all subscriber IDs are valid')
ACL_ACTION = 'NONE'
# Depending on which type of ACL is used (PERMIT, DENY... or there isn't one)
# define a differnet function to be used to check the ACL
global allow_sub
if ACL_ACTION == 'PERMIT':
def allow_sub(_sub):
if _sub in ACL:
return True
else:
return False
elif ACL_ACTION == 'DENY':
def allow_sub(_sub):
if _sub not in ACL:
return True
else:
return False
else:
def allow_sub(_sub):
return True
return ACL
# Run this every minute for rule timer updates
def rule_timer_loop():
logger.debug('(ALL IPSC) Rule timer loop started')
_now = time()
for _network in RULES:
for _rule in RULES[_network]['GROUP_VOICE']:
if _rule['TO_TYPE'] == 'ON':
if _rule['ACTIVE'] == True:
if _rule['TIMER'] < _now:
_rule['ACTIVE'] = False
logger.info('(%s) Rule timout DEACTIVATE: Rule name: %s, Target IPSC: %s, TS: %s, TGID: %s', _network, _rule['NAME'], _rule['DST_NET'], _rule['DST_TS'], int_id(_rule['DST_GROUP']))
else:
timeout_in = _rule['TIMER'] - _now
logger.info('(%s) Rule ACTIVE with ON timer running: Timeout eligible in: %ds, Rule name: %s, Target IPSC: %s, TS: %s, TGID: %s', _network, timeout_in, _rule['NAME'], _rule['DST_NET'], _rule['DST_TS'], int_id(_rule['DST_GROUP']))
elif _rule['TO_TYPE'] == 'OFF':
if _rule['ACTIVE'] == False:
if _rule['TIMER'] < _now:
_rule['ACTIVE'] = True
logger.info('(%s) Rule timout ACTIVATE: Rule name: %s, Target IPSC: %s, TS: %s, TGID: %s', _network, _rule['NAME'], _rule['DST_NET'], _rule['DST_TS'], int_id(_rule['DST_GROUP']))
else:
timeout_in = _rule['TIMER'] - _now
logger.info('(%s) Rule DEACTIVE with OFF timer running: Timeout eligible in: %ds, Rule name: %s, Target IPSC: %s, TS: %s, TGID: %s', _network, timeout_in, _rule['NAME'], _rule['DST_NET'], _rule['DST_TS'], int_id(_rule['DST_GROUP']))
else:
logger.debug('Rule timer loop made no rule changes')
class bridgeIPSC(IPSC):
def __init__(self, _name, _config, _logger, report):
IPSC.__init__(self, _name, _config, _logger, report)
self.BRIDGES = BRIDGES
if self.BRIDGES:
self._logger.info('(%s) Initializing backup/polite bridging', self._system)
self.BRIDGE = False
else:
self.BRIDGE = True
self._logger.info('Initializing standard bridging')
self.IPSC_STATUS = {
1: {'RX_GROUP':'\x00', 'TX_GROUP':'\x00', 'RX_TIME':0, 'TX_TIME':0, 'RX_SRC_SUB':'\x00', 'TX_SRC_SUB':'\x00'},
2: {'RX_GROUP':'\x00', 'TX_GROUP':'\x00', 'RX_TIME':0, 'TX_TIME':0, 'RX_SRC_SUB':'\x00', 'TX_SRC_SUB':'\x00'}
}
self.last_seq_id = '\x00'
self.call_start = 0
# Setup the backup/polite bridging maintenance loop (based on keep-alive timer)
def startProtocol(self):
IPSC.startProtocol(self)
if self.BRIDGES:
self._bridge_presence = task.LoopingCall(self.bridge_presence_loop)
self._bridge_presence_loop = self._bridge_presence.start(self._local['ALIVE_TIMER'])
# This is the backup/polite bridge maintenance loop
def bridge_presence_loop(self):
self._logger.debug('(%s) Bridge presence loop initiated', self._system)
_temp_bridge = True
for peer in self.BRIDGES:
_peer = hex_str_4(peer)
if _peer in self._peers.keys() and (self._peers[_peer]['MODE_DECODE']['TS_1'] or self._peers[_peer]['MODE_DECODE']['TS_2']):
_temp_bridge = False
self._logger.debug('(%s) Peer %s is an active bridge', self._system, int_id(_peer))
if _peer == self._master['RADIO_ID'] \
and self._master['STATUS']['CONNECTED'] \
and (self._master['MODE_DECODE']['TS_1'] or self._master['MODE_DECODE']['TS_2']):
_temp_bridge = False
self._logger.debug('(%s) Master %s is an active bridge',self._system, int_id(_peer))
if self.BRIDGE != _temp_bridge:
self._logger.info('(%s) Changing bridge status to: %s', self._system, _temp_bridge )
self.BRIDGE = _temp_bridge
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _src_sub, _dst_group, _ts, _end, _peerid, _data):
# Check for ACL match, and return if the subscriber is not allowed
if allow_sub(_src_sub) == False:
self._logger.warning('(%s) Group Voice Packet ***REJECTED BY ACL*** From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_group))
return
# Process the packet
self._logger.debug('(%s) Group Voice Packet Received From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_group))
_burst_data_type = _data[30] # Determine the type of voice packet this is (see top of file for possible types)
_seq_id = _data[5]
now = time() # Mark packet arrival time -- we'll need this for call contention handling
for rule in RULES[self._system]['GROUP_VOICE']:
_target = rule['DST_NET'] # Shorthand to reduce length and make it easier to read
_status = systems[_target].IPSC_STATUS # Shorthand to reduce length and make it easier to read
# This is the primary rule match to determine if the call will be routed.
if (rule['SRC_GROUP'] == _dst_group and rule['SRC_TS'] == _ts and rule['ACTIVE'] == True) and (self.BRIDGE == True or systems[_target].BRIDGE == True):
#
# BEGIN CONTENTION HANDLING
#
# If this is an inter-DMRlink trunk, this isn't necessary
if RULES[self._system]['TRUNK'] == False:
# The rules for each of the 4 "ifs" below are listed here for readability. The Frame To Send is:
# From a different group than last RX from this IPSC, but it has been less than Group Hangtime
# From a different group than last TX to this IPSC, but it has been less than Group Hangtime
# From the same group as the last RX from this IPSC, but from a different subscriber, and it has been less than TS Clear Time
# From the same group as the last TX to this IPSC, but from a different subscriber, and it has been less than TS Clear Time
# The "continue" at the end of each means the next iteration of the for loop that tests for matching rules
#
if ((rule['DST_GROUP'] != _status[rule['DST_TS']]['RX_GROUP']) and ((now - _status[rule['DST_TS']]['RX_TIME']) < RULES[_target]['GROUP_HANGTIME'])):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged to TGID%s, target active or in group hangtime: IPSC: %s, TS: %s, TGID: %s', self._system, int_id(rule['DST_GROUP']), _target, rule['DST_TS'], int_id(_status[rule['DST_TS']]['RX_GROUP']))
continue
if ((rule['DST_GROUP'] != _status[rule['DST_TS']]['TX_GROUP']) and ((now - _status[rule['DST_TS']]['TX_TIME']) < RULES[_target]['GROUP_HANGTIME'])):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged to TGID%s, target in group hangtime: IPSC: %s, TS: %s, TGID: %s', self._system, int_id(rule['DST_GROUP']), _target, rule['DST_TS'], int_id(_status[rule['DST_TS']]['TX_GROUP']))
continue
if (rule['DST_GROUP'] == _status[rule['DST_TS']]['RX_GROUP']) and ((now - _status[rule['DST_TS']]['RX_TIME']) < TS_CLEAR_TIME):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged to TGID%s, matching call already active on target: IPSC: %s, TS: %s, TGID: %s', self._system, int_id(rule['DST_GROUP']), _target, rule['DST_TS'], int_id(_status[rule['DST_TS']]['RX_GROUP']))
continue
if (rule['DST_GROUP'] == _status[rule['DST_TS']]['TX_GROUP']) and (_src_sub != _status[rule['DST_TS']]['TX_SRC_SUB']) and ((now - _status[rule['DST_TS']]['TX_TIME']) < TS_CLEAR_TIME):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged for subscriber %s, call bridge in progress on target: IPSC: %s, TS: %s, TGID: %s SUB: %s', self._system, int_id(_src_sub), _target, rule['DST_TS'], int_id(_status[rule['DST_TS']]['TX_GROUP']), int_id(_status[rule['DST_TS']]['TX_SRC_SUB']))
continue
#
# END CONTENTION HANDLING
#
#
# BEGIN FRAME FORWARDING
#
# Make a copy of the payload
_tmp_data = _data
# Re-Write the IPSC SRC to match the target network's ID
_tmp_data = _tmp_data.replace(_peerid, self._CONFIG['SYSTEMS'][_target]['LOCAL']['RADIO_ID'])
# Re-Write the destination Group ID
_tmp_data = _tmp_data.replace(_dst_group, rule['DST_GROUP'])
# Re-Write IPSC timeslot value
_call_info = int_id(_data[17:18])
if rule['DST_TS'] == 1:
_call_info &= ~(1 << 5)
elif rule['DST_TS'] == 2:
_call_info |= 1 << 5
_call_info = chr(_call_info)
_tmp_data = _tmp_data[:17] + _call_info + _tmp_data[18:]
# Re-Write DMR timeslot value
# Determine if the slot is present, so we can translate if need be
if _burst_data_type == BURST_DATA_TYPE['SLOT1_VOICE'] or _burst_data_type == BURST_DATA_TYPE['SLOT2_VOICE']:
_slot_valid = True
else:
_slot_valid = False
# Re-Write timeslot if necessary...
if _slot_valid:
if rule['DST_TS'] == 1:
_burst_data_type = BURST_DATA_TYPE['SLOT1_VOICE']
elif rule['DST_TS'] == 1:
_burst_data_type = BURST_DATA_TYPE['SLOT2_VOICE']
_tmp_data = _tmp_data[:30] + _burst_data_type + _tmp_data[31:]
# Send the packet to all peers in the target IPSC
systems[_target].send_to_ipsc(_tmp_data)
#
# END FRAME FORWARDING
#
# Set values for the contention handler to test next time there is a frame to forward
_status[_ts]['TX_GROUP'] = rule['DST_GROUP']
_status[_ts]['TX_TIME'] = now
_status[_ts]['TX_SRC_SUB'] = _src_sub
# Mark the group and time that a packet was recieved for the contention handler to use later
self.IPSC_STATUS[_ts]['RX_GROUP'] = _dst_group
self.IPSC_STATUS[_ts]['RX_TIME'] = now
#
# BEGIN IN-BAND SIGNALING BASED ON TGID & VOICE TERMINATOR FRAME
#
# Activate/Deactivate rules based on group voice activity -- PTT or UA for you c-Bridge dorks.
# This will ONLY work for symmetrical rules!!!
# Action happens on key up
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
if self.last_seq_id != _seq_id:
self.last_seq_id = _seq_id
self.call_start = time()
self._logger.info('(%s) GROUP VOICE START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group))
# Action happens on un-key
if _burst_data_type == BURST_DATA_TYPE['VOICE_TERM']:
if self.last_seq_id == _seq_id:
self.call_duration = time() - self.call_start
self._logger.info('(%s) GROUP VOICE END: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s Duration: %.2fs', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration)
else:
self._logger.warning('(%s) GROUP VOICE END WITHOUT MATCHING START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group),)
# Iterate the rules dictionary
for rule in RULES[self._system]['GROUP_VOICE']:
_target = rule['DST_NET']
# TGID matches a rule source, reset its timer
if _ts == rule['SRC_TS'] and _dst_group == rule['SRC_GROUP'] and ((rule['TO_TYPE'] == 'ON' and (rule['ACTIVE'] == True)) or (rule['TO_TYPE'] == 'OFF' and rule['ACTIVE'] == False)):
rule['TIMER'] = now + rule['TIMEOUT']
self._logger.info('(%s) Source group transmission match for rule \"%s\". Reset timeout to %s', self._system, rule['NAME'], rule['TIMER'])
# Scan for reciprocal rules and reset their timers as well.
for target_rule in RULES[_target]['GROUP_VOICE']:
if target_rule['NAME'] == rule['NAME']:
target_rule['TIMER'] = now + target_rule['TIMEOUT']
self._logger.info('(%s) Reciprocal group transmission match for rule \"%s\" on IPSC \"%s\". Reset timeout to %s', self._system, target_rule['NAME'], _target, rule['TIMER'])
# TGID matches an ACTIVATION trigger
if _dst_group in rule['ON']:
# Set the matching rule as ACTIVE
rule['ACTIVE'] = True
rule['TIMER'] = now + rule['TIMEOUT']
self._logger.info('(%s) Primary Bridge Rule \"%s\" changed to state: %s', self._system, rule['NAME'], rule['ACTIVE'])
# Set reciprocal rules for other IPSCs as ACTIVE
for target_rule in RULES[_target]['GROUP_VOICE']:
if target_rule['NAME'] == rule['NAME']:
target_rule['ACTIVE'] = True
target_rule['TIMER'] = now + target_rule['TIMEOUT']
self._logger.info('(%s) Reciprocal Bridge Rule \"%s\" in IPSC \"%s\" changed to state: %s', self._system, target_rule['NAME'], _target, rule['ACTIVE'])
# TGID matches an DE-ACTIVATION trigger
if _dst_group in rule['OFF']:
# Set the matching rule as ACTIVE
rule['ACTIVE'] = False
self._logger.info('(%s) Bridge Rule \"%s\" changed to state: %s', self._system, rule['NAME'], rule['ACTIVE'])
# Set reciprocal rules for other IPSCs as ACTIVE
_target = rule['DST_NET']
for target_rule in RULES[_target]['GROUP_VOICE']:
if target_rule['NAME'] == rule['NAME']:
target_rule['ACTIVE'] = False
self._logger.info('(%s) Reciprocal Bridge Rule \"%s\" in IPSC \"%s\" changed to state: %s', self._system, target_rule['NAME'], _target, rule['ACTIVE'])
#
# END IN-BAND SIGNALLING
#
def group_data(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
self._logger.debug('(%s) Group Data Packet Received From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_sub))
for target in RULES[self._system]['GROUP_DATA']:
if self.BRIDGE == True or systems[target].BRIDGE == True:
_tmp_data = _data
# Re-Write the IPSC SRC to match the target network's ID
_tmp_data = _tmp_data.replace(_peerid, self._CONFIG[target]['LOCAL']['RADIO_ID'])
# Send the packet to all peers in the target IPSC
systems[target].send_to_ipsc(_tmp_data)
def private_data(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
self._logger.debug('(%s) Private Data Packet Received From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_sub))
for target in RULES[self._system]['PRIVATE_DATA']:
if self.BRIDGE == True or systems[target].BRIDGE == True:
_tmp_data = _data
# Re-Write the IPSC SRC to match the target network's ID
_tmp_data = _tmp_data.replace(_peerid, self._CONFIG[target]['LOCAL']['RADIO_ID'])
# Send the packet to all peers in the target IPSC
systems[target].send_to_ipsc(_tmp_data)
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# BRIDGE.PY SPECIFIC ITEMS GO HERE:
# Build the routing rules file
RULES = build_rules('bridge_rules')
# Build list of known bridge IDs
BRIDGES = build_bridges('known_bridges')
# Build the Access Control List
ACL = build_acl('sub_acl')
# INITIALIZE THE REPORTING LOOP IF CONFIGURED
rule_timer = task.LoopingCall(rule_timer_loop)
rule_timer.start(60)
# MAIN INITIALIZATION ITEMS HERE
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGURED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, bridgeIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

69
Retired/bridge_rules_SAMPLE.py Executable file
View File

@ -0,0 +1,69 @@
'''
The following is an example for your bridge_rules file. Note, all bridging is ONE-WAY!
Rules for an IPSC network indicate destination IPSC network for the Group ID specified
(allowing transcoding of the Group ID to a different value). Group IDs used to be
hex strings, then a function was added to convert them, now that function has been
moved into the bridge.py (program file) to make this file as simple and easy as
possible
The IPSC name must match an IPSC name from dmrlink.cfg, and any IPSC network defined
as "active" in the dmrlink.cfg *MUST* have an entry here. It may be an empty entry,
but there must be one so that the data structure can be parsed.
The example below cross-patches TS 1/TGID 1 on an IPSC network named "IPSC_FOO" with
TS 2/TGID 2 on an IPSC network named "IPSC_BAR". Note, one entry must be made on EACH
IPSC network (IPSC_FOO and IPSC_BAR in this example) for bridging to occur in both
directions.
THIS EXAMPLE WILL NOT WORK AS IT IS - YOU MUST SPECIFY NAMES AND GROUP IDS!!!
NOTES:
* PRIVATE_VOICE is not yet implemented
* GROUP_HANGTIME should be set to the same value as the repeaters in the IPSC network
* TRUNK is a boolean set to True only for DMRlink to DMRlink IPSCs that need to move
multiple packet streams that may match the same TS - this essentially makes the
source,timeslot,talkgroup ID a tuple to indentify an arbitrary number of streams
* NAME is any name you want, and is used to match reciprocal rules for user-activateion
* ACTIVE should be set to True if you want the rule active by default, False to be inactive
* ON and OFF are LISTS of Talkgroup IDs used to trigger this rule off and on. Even if you
only want one (as shown in the ON example), it has to be in list format. None can be
handled with an empty list, such as " 'ON': [] ".
* TO_TYPE is timeout type. If you want to use timers, ON means when it's turned on, it will
turn off afer the timout period and OFF means it will turn back on after the timout
period. If you don't want to use timers, set it to anything else, but 'NONE' might be
a good value for documentation!
* TIMOUT is a value in minutes for the timout timer. No, I won't make it 'seconds', so don't
ask. Timers are performance "expense".
DO YOU THINK THIS FILE IS TOO COMPLICATED?
Because you guys all want more and more features, this file is getting complicated. I have
dabbled with using a parser to make it easier to build. I'm torn. There is a HUGE benefit
to having it like it is. This is a python file. Simply running it
(i.e. "python bridge_rules.py) will tell you if there's a syntax error and where. Think
about that for a few minutes :)
'''
RULES = {
'IPSC_FOO': {
'TRUNK': False,
'GROUP_HANGTIME': 5,
'GROUP_VOICE': [
{'NAME': 'STATEWIDE', 'ACTIVE': False, 'TO_TYPE': 'ON', 'TIMEOUT': 2, 'ON': [8,], 'OFF': [9,10], 'SRC_TS': 1, 'SRC_GROUP': 1, 'DST_NET': 'IPSC_BAR', 'DST_TS': 2, 'DST_GROUP': 2},
# Send the IPSC_FOO network Time Slice 1, Talk Group 1 to the IPSC_BAR network on Time Slice 2 Talk Group 2
# Repeat the above line for as many rules for this IPSC network as you want.
],
'PRIVATE_VOICE': [
]
},
'IPSC_BAR': {
'TRUNK': False,
'GROUP_HANGTIME': 5,
'GROUP_VOICE': [
{'NAME': 'STATEWIDE', 'ACTIVE': False, 'TO_TYPE': 'ON', 'TIMEOUT': 2, 'ON': [8,], 'OFF': [9,10], 'SRC_TS': 2, 'SRC_GROUP': 2, 'DST_NET': 'IPSC_FOO', 'DST_TS': 1, 'DST_GROUP': 1},
# Send the IPSC_BAR network Time Slice 2, Talk Group 2 to the IPSC_FOO network on Time Slice 1 Talk Group 1
# Repeat the above line for as many rules for this IPSC network as you want.
],
'PRIVATE_VOICE': [
]
}
}

29
Retired/known_bridges_SAMPLE.py Executable file
View File

@ -0,0 +1,29 @@
'''
WARNING - IF YOU USE THIS FILE, BRIDGE.PY WILL ASSUME IT IS TO
OPERATE IN BACKUP BRIDGE MODE. THIS MAY REALLY RUIN YOUR DAY!
The following is an example for your "known_bridges" file. This is a
simple list (in python syntax) of integer DMR radios IDs of bridges
that we expect to encounter.
You should only add bridges that will be encountered - adding a bunch
of bridges just because you can will really slow things down, so don't
do it. Please note each line but the last must end in a comma. This is
about the only thing you can mess up... but I manage to bork that one
every 3rd time or so I make updates, so watch out.
A bridge that is "encountered" means another bridge that might be in
the same IPSC network we're going to try to bridge for. This is useful
only in the case where we want to provide backup bridging service.
There are cases when you do NOT want to use this feature -- say for
example if one IPSC has two bridges but they're bridging different
talkgroups.
'''
BRIDGES = [
123456,
234567,
345678
]

147
Retired/log.py Executable file
View File

@ -0,0 +1,147 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application that snoops voice traffic to log calls
from __future__ import print_function
from twisted.internet import reactor
from binascii import b2a_hex as h
import time
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, build_aliases, config_reports
from dmr_utils.utils import hex_str_3, hex_str_4, int_id, get_alias
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK, Dave Kierzkowski, KD8EYF'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
class logIPSC(IPSC):
def __init__(self, _name, _config, _logger, _report):
IPSC.__init__(self, _name, _config, _logger, _report)
self.ACTIVE_CALLS = []
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
def group_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
if (_ts not in self.ACTIVE_CALLS) or _end:
_time = time.strftime('%m/%d/%y %H:%M:%S')
_dst_sub = get_alias(_dst_sub, talkgroup_ids)
_peerid = get_alias(_peerid, peer_ids)
_src_sub = get_alias(_src_sub, subscriber_ids)
if not _end: self.ACTIVE_CALLS.append(_ts)
if _end: self.ACTIVE_CALLS.remove(_ts)
if _end: _end = 'END'
else: _end = 'START'
print('{} ({}) Call {} Group Voice: \n\tIPSC Source:\t{}\n\tSubscriber:\t{}\n\tDestination:\t{}\n\tTimeslot\t{}' .format(_time, self._system, _end, _peerid, _src_sub, _dst_sub, _ts))
def private_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
if (_ts not in self.ACTIVE_CALLS) or _end:
_time = time.strftime('%m/%d/%y %H:%M:%S')
_dst_sub = get_alias(_dst_sub, subscriber_ids)
_peerid = get_alias(_peerid, peer_ids)
_src_sub = get_alias(_src_sub, subscriber_ids)
if not _end: self.ACTIVE_CALLS.append(_ts)
if _end: self.ACTIVE_CALLS.remove(_ts)
if _ts: _ts = 2
else: _ts = 1
if _end: _end = 'END'
else: _end = 'START'
print('{} ({}) Call {} Private Voice: \n\tIPSC Source:\t{}\n\tSubscriber:\t{}\n\tDestination:\t{}\n\tTimeslot\t{}' .format(_time, self._system, _end, _peerid, _src_sub, _dst_sub, _ts))
def group_data(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
_dst_sub = get_alias(_dst_sub, talkgroup_ids)
_peerid = get_alias(_peerid, peer_ids)
_src_sub = get_alias(_src_sub, subscriber_ids)
print('({}) Group Data Packet Received From: {}' .format(self._system, _src_sub))
def private_data(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
_dst_sub = get_alias(_dst_sub, subscriber_ids)
_peerid = get_alias(_peerid, peer_ids)
_src_sub = get_alias(_src_sub, subscriber_ids)
print('({}) Private Data Packet Received From: {} To: {}' .format(self._system, _src_sub, _dst_sub))
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, logIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

190
Retired/play_group.py Executable file
View File

@ -0,0 +1,190 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application that "plays" a voice tranmission from a file
# that was created with record.py. The file is just a pickle of an entire
# transmission.
#
# This program consults a list of "trigger groups" for each timeslot that
# will initiate playback. When playback occurs, several items are re-written:
# Source Subscriber: this DMRlink's local subscriber ID
# Source Peer: this DMRlink's local subscriber ID
# Timeslot: timeslot of the tranmission that triggered
# TGID: TGID of the message that triggered it
from __future__ import print_function
from twisted.internet import reactor
import sys, time
import cPickle as pickle
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, build_aliases, config_reports
from dmr_utils.utils import int_id, hex_str_3
from ipsc.ipsc_const import BURST_DATA_TYPE
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2014 - 2015 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski KD8EYF'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
# path+filename for the transmission to play back
filename = '../test.pickle'
# trigger logic - True, trigger on these IDs, False trigger on any but these IDs
trigger = True
# groups that we want to trigger playback of this file (ts1 and ts2)
# Note this is a python list type, even if there's just one value
trigger_groups_1 = ['\x00\x00\x01', '\x00\x00\x0D', '\x00\x00\x64']
trigger_groups_2 = ['\x00\x0C\x30',]
class playIPSC(IPSC):
def __init__(self, _name, _config, _logger,_report):
IPSC.__init__(self, _name, _config, _logger, _report)
self.CALL_DATA = []
self.event_id = 1
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _src_sub, _dst_group, _ts, _end, _peerid, _data):
if _end:
_self_peer = self._config['LOCAL']['RADIO_ID']
_self_src = _self_peer[1:]
if (_peerid == _self_peer) or (_src_sub == _self_src):
self._logger.error('(%s) Just received a packet that appears to have been originated by us. PeerID: %s Subscriber: %s TS: %s, TGID: %s', self._system, int_id(_peerid), int_id(_src_sub), int(_ts), int_id(_dst_group))
return
if trigger == False:
if (_ts == 1 and _dst_group not in trigger_groups_1) or (_ts == 2 and _dst_group not in trigger_groups_2):
return
else:
if (_ts == 1 and _dst_group not in trigger_groups_1) or (_ts == 2 and _dst_group not in trigger_groups_2):
return
self._logger.info('(%s) Event ID: %s - Playback triggered from SourceID: %s, TS: %s, TGID: %s, PeerID: %s', self._system, self.event_id, int_id(_src_sub), _ts, int_id(_dst_group), int_id(_peerid))
# Determine the type of voice packet this is (see top of file for possible types)
_burst_data_type = _data[30]
time.sleep(2)
self.CALL_DATA = pickle.load(open(filename, 'rb'))
self._logger.info('(%s) Event ID: %s - Playing back file: %s', self._system, self.event_id, filename)
for i in self.CALL_DATA:
_tmp_data = i
# re-Write the peer radio ID to that of this program
_tmp_data = _tmp_data.replace(_peerid, _self_peer)
# re-Write the source subscriber ID to that of this program
_tmp_data = _tmp_data.replace(_src_sub, _self_src)
# Re-Write the destination Group ID
_tmp_data = _tmp_data.replace(_tmp_data[9:12], _dst_group)
# Re-Write IPSC timeslot value
_call_info = int_id(_tmp_data[17:18])
if _ts == 1:
_call_info &= ~(1 << 5)
elif _ts == 2:
_call_info |= 1 << 5
_call_info = chr(_call_info)
_tmp_data = _tmp_data[:17] + _call_info + _tmp_data[18:]
# Re-Write DMR timeslot value
# Determine if the slot is present, so we can translate if need be
if _burst_data_type == BURST_DATA_TYPE['SLOT1_VOICE'] or _burst_data_type == BURST_DATA_TYPE['SLOT2_VOICE']:
# Re-Write timeslot if necessary...
if _ts == 1:
_burst_data_type = BURST_DATA_TYPE['SLOT1_VOICE']
elif _ts == 2:
_burst_data_type = BURST_DATA_TYPE['SLOT2_VOICE']
_tmp_data = _tmp_data[:30] + _burst_data_type + _tmp_data[31:]
# Send the packet to all peers in the target IPSC
self.send_to_ipsc(_tmp_data)
time.sleep(0.06)
self.CALL_DATA = []
self._logger.info('(%s) Event ID: %s - Playback Completed', self._system, self.event_id)
self.event_id = self.event_id + 1
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, playIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

201
Retired/rcm.py Executable file
View File

@ -0,0 +1,201 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application that uses the Repeater Call Monitor packets to display events in the IPSC
# NOTE: dmrlink.py MUST BE CONFIGURED TO CONNECT AS A "REPEATER CALL MONITOR" PEER!!!
# ALSO NOTE, I'M NOT DONE MAKING THIS WORK, SO UNTIL THIS MESSAGE IS GONE, DON'T EXPECT GREAT THINGS.
from __future__ import print_function
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from twisted.internet import task
from binascii import b2a_hex as ahex
import datetime
import binascii
import dmrlink
import sys
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, build_aliases, config_reports
from dmr_utils.utils import get_alias, int_id
from ipsc.ipsc_const import *
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski KD8EYF'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
status = True
rpt = True
nack = True
class rcmIPSC(IPSC):
def __init__(self, _name, _config, _logger, _report):
IPSC.__init__(self, _name, _config, _logger, _report)
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def call_mon_status(self, _data):
if not status:
return
_source = _data[1:5]
_ipsc_src = _data[5:9]
_seq_num = _data[9:13]
_ts = _data[13]
_status = _data[15] # suspect [14:16] but nothing in leading byte?
_rf_src = _data[16:19]
_rf_tgt = _data[19:22]
_type = _data[22]
_prio = _data[23]
_sec = _data[24]
_source = str(int_id(_source)) + ', ' + str(get_alias(_source, peer_ids))
_ipsc_src = str(int_id(_ipsc_src)) + ', ' + str(get_alias(_ipsc_src, peer_ids))
_rf_src = str(int_id(_rf_src)) + ', ' + str(get_alias(_rf_src, subscriber_ids))
if _type == '\x4F' or '\x51':
_rf_tgt = 'TGID: ' + str(int_id(_rf_tgt)) + ', ' + str(get_alias(_rf_tgt, talkgroup_ids))
else:
_rf_tgt = 'SID: ' + str(int_id(_rf_tgt)) + ', ' + str(get_alias(_rf_tgt, subscriber_ids))
print('Call Monitor - Call Status')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
print('IPSC: ', self._system)
print('IPSC Source: ', _ipsc_src)
print('Timeslot: ', TS[_ts])
try:
print('Status: ', STATUS[_status])
except KeyError:
print('Status (unknown): ', ahex(_status))
try:
print('Type: ', TYPE[_type])
except KeyError:
print('Type (unknown): ', ahex(_type))
print('Source Sub: ', _rf_src)
print('Target Sub: ', _rf_tgt)
print()
def call_mon_rpt(self, _data):
if not rpt:
return
_source = _data[1:5]
_ts1_state = _data[5]
_ts2_state = _data[6]
_source = str(int_id(_source)) + ', ' + str(get_alias(_source, peer_ids))
print('Call Monitor - Repeater State')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
try:
print('TS1 State: ', REPEAT[_ts1_state])
except KeyError:
print('TS1 State (unknown): ', ahex(_ts1_state))
try:
print('TS2 State: ', REPEAT[_ts2_state])
except KeyError:
print('TS2 State (unknown): ', ahex(_ts2_state))
print()
def call_mon_nack(self, _data):
if not nack:
return
_source = _data[1:5]
_nack = _data[5]
_source = get_alias(_source, peer_ids)
print('Call Monitor - Transmission NACK')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
try:
print('NACK Cause: ', NACK[_nack])
except KeyError:
print('NACK Cause (unknown): ', ahex(_nack))
print()
def repeater_wake_up(self, _data):
_source = _data[1:5]
_source_name = get_alias(_source, peer_ids)
print('({}) Repeater Wake-Up Packet Received: {} ({})' .format(self._system, _source_name, int_id(_source)))
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, rcmIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

165
Retired/rcm_db_log.py Executable file
View File

@ -0,0 +1,165 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application that uses the Repeater Call Monitor packets to display events in the IPSC
# NOTE: dmrlink.py MUST BE CONFIGURED TO CONNECT AS A "REPEATER CALL MONITOR" PEER!!!
#************************************
# WHAT THIS PROGRAM WILL DO
#************************************
'''
This program will log RCM 'status' messages to a MySQL database, based on
the DB configuration information supplied in the section labelled
"USER DEFINED ITEMS GO HERE". Columns logged are as follows:
data_source (INT) - The DMR radio ID of the source of this information
ipsc (INT) - The IPSC peer that was the origin of the event that triggered this message
timeslot (INT) - IPSC timeslot, 0 if not applicable
type (VARCHAR) - The type of radio call, if applicable
subscriber (INT) - the RF source, if applicable, that caused the message
talkgroup (INT) - the TGID, if applicable
status (VARCHAR) - the RCM message time for 'status' messages
'''
from __future__ import print_function
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from twisted.internet import task
import pymysql
import dmrlink
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, build_aliases, config_reports
from ipsc.ipsc_const import *
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski KD8EYF and he who wishes not to be named'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
#************************************
# USER DEFINED ITEMS GO HERE
#************************************
#
db_host = '127.0.0.1'
db_port = 1234
db_user = 'dmrlink'
db_pwd = 'dmrlink'
db_name = 'dmrlink'
#
# To change the table name, look for the line with:
# cur.execute("insert INTO rcm_status(da...
# and change "rcm_status" to the name of your table
#
#************************************
class rcmIPSC(IPSC):
def __init__(self, *args, **kwargs):
IPSC.__init__(self, *args, **kwargs)
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def call_mon_status(self, _network, _data):
_source = int_id(_data[1:5])
_ipsc_src = int_id(_data[5:9])
_ts = TS[_data[13]]
_status = _data[15] # suspect [14:16] but nothing in leading byte?
_rf_src = int_id(_data[16:19])
_rf_tgt = int_id(_data[19:22])
_type = _data[22]
try:
_status = STATUS[_status]
except KeyError:
pass
try:
_type = TYPE[_type]
except KeyError:
pass
con = pymysql.connect(host = db_host, port = db_port, user = db_user, passwd = db_pwd, db = db_name)
cur = con.cursor()
cur.execute("insert INTO rcm_status(data_source, ipsc, timeslot, type, subscriber, talkgroup, status) VALUES(%s, %s, %s, %s, %s, %s, %s)", (_source, _ipsc_src, _ts, _type, _rf_src, _rf_tgt, _status))
con.commit()
con.close()
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, rcmIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

162
Retired/record.py Executable file
View File

@ -0,0 +1,162 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application that "records" voice transmissions to
# a datafile... presumably to be played back later.
from __future__ import print_function
from twisted.internet import reactor
from binascii import b2a_hex as h
import sys
import cPickle as pickle
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, build_aliases, config_reports
from dmr_utils.utils import hex_str_3, int_id
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski KD8EYF'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
print('This program will record the first matching voice call and exit.\n')
while True:
tx_type = raw_input('Group (g) or Private voice (p)? ')
if tx_type == 'g' or tx_type == 'p':
break
print('...input must be either \'g\' or \'p\'')
while True:
ts = raw_input('Which timeslot (1, 2 or \'both\')? ')
if ts == '1' or ts == '2' or ts =='both':
if ts == '1':
ts = (1,)
if ts == '2':
ts = (2,)
if ts == 'both':
ts = (1,2)
break
print('...input must be \'1\', \'2\' or \'both\'')
id = raw_input('Which Group or Subscriber ID to record? ')
id = int(id)
id = hex_str_3(id)
filename = raw_input('Filename to use for this recording? ')
class recordIPSC(IPSC):
def __init__(self, _name, _config, _logger, _report):
IPSC.__init__(self, _name, _config, _logger, _report)
self.CALL_DATA = []
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
if tx_type == 'g':
print('Initializing to record GROUP VOICE transmission')
def group_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
if id == _dst_sub and _ts in ts:
if not _end:
if not self.CALL_DATA:
print('({}) Recording transmission from subscriber: {}' .format(self._system, int_id(_src_sub)))
self.CALL_DATA.append(_data)
if _end:
self.CALL_DATA.append(_data)
print('({}) Transmission ended, writing to disk: {}' .format(self._system, filename))
pickle.dump(self.CALL_DATA, open(filename, 'wb'))
reactor.stop()
print('Recording created, program terminating')
if tx_type == 'p':
print('Initializing ro record PRIVATE VOICE transmission')
def private_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
if id == _dst_sub and _ts in ts:
if not _end:
if not self.CALL_DATA:
print('({}) Recording transmission from subscriber: {}' .format(self._system, int_id(_src_sub)))
self.CALL_DATA.append(_data)
if _end:
self.CALL_DATA.append(_data)
print('({}) Transmission ended, writing to disk: {}' .format(self._system, filename))
pickle.dump(self.CALL_DATA, open(filename, 'wb'))
reactor.stop()
print('Recording created, program terminating')
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, recordIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

10
Retired/template.py Executable file
View File

@ -0,0 +1,10 @@
v_hed_1 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\xdd\x3b\x01\x3b\xeb\x3c\xe0\x00\x00\x00\x00\x01\x80\x00\x0a\x80\x0a\x00\x60\x00\x00\x00\x00\x00\x02\x2f\x7c\xca\x92\xaf\x70\x00\x11\x35\x32'
v_hed_2 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\x5d\x3b\x02\x3b\xeb\x3e\xc0\x00\x00\x00\x00\x01\x80\x00\x0a\x80\x0a\x00\x60\x00\x00\x00\x00\x00\x02\x2f\x7c\xca\x92\xaf\x70\x00\x11\x35\x30'
v_hed_3 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\x5d\x3b\x03\x3b\xeb\x40\xa0\x00\x00\x00\x00\x01\x80\x00\x0a\x80\x0a\x00\x60\x00\x00\x00\x00\x00\x02\x2f\x7c\xca\x92\xaf\x70\x00\x11\x35\x31'
voice_1 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\x5d\x3b\x04\x3b\xeb\x42\x80\x00\x00\x00\x00\x8a\x14\x40\xf8\x01\xa9\x9f\x8c\xe0\xbe\x00\x6a\x67\xe3\x38\x2f\x80\x1a\x99\xf8\xce\x08'
voice_2 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\x5d\x3b\x05\x3b\xeb\x44\x60\x00\x00\x00\x00\x8a\x19\x06\xf8\x01\xa9\x9f\x8c\xe0\xbe\x00\x6a\x67\xe3\x38\x2f\x80\x1a\x99\xf8\xce\x08\x05\x05\x06\x06\x12'
voice_3 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\x5d\x3b\x06\x3b\xeb\x46\x40\x00\x00\x00\x00\x8a\x19\x06\xf8\x01\xa9\x9f\x8c\xe0\xbe\x00\x6a\x67\xe3\x38\x2f\x80\x1a\x99\xf8\xce\x08\x09\x05\x06\x05\x16'
voice_4 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\x5d\x3b\x07\x3b\xeb\x48\x20\x00\x00\x00\x00\x8a\x19\x06\x98\x02\xb9\x4f\xa4\xd3\xbb\xb7\x96\xc7\x83\xd8\xee\x81\x19\x41\xe4\x4a\x68\x0f\x05\x06\x0f\x16'
voice_5 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\x5d\x3b\x08\x3b\xeb\x4a\x00\x00\x00\x00\x00\x8a\x22\x16\xe8\x1a\x62\xd6\x8c\x6b\xba\x06\x3d\x0d\xeb\x04\xe9\x81\xdd\xf1\x04\x86\xc8\x00\x0a\x0a\x0c\x00\x00\x00\x00\x00\x02\x2f\x7c\xca\x14'
voice_6 = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x20\x80\x5d\x3b\x09\x3b\xeb\x4b\xe0\x00\x00\x00\x00\x8a\x19\x06\x98\x22\xd3\xd9\x00\xb4\xa6\x05\x6d\x29\xa2\x17\xa8\x82\x75\x14\xf8\x10\x08\x00\x00\x00\x00\x10'
voice_t = '\x80\x00\x04\xbf\xfd\x08\x2f\x7c\xca\x00\x00\x02\x02\x00\x00\x30\xac\x60\x80\x5e\x3e\x76\x3b\xf1\xb8\x40\x00\x00\x00\x00\x02\x80\x00\x0a\x80\x0a\x00\x60\x00\x00\x00\x00\x00\x02\x2f\x7c\xca\x9d\xa0\x7f\x00\x12\x35\x35'

View File

@ -1,88 +0,0 @@
#!/usr/bin/env python
#
# This work is licensed under the Creative Commons Attribution-ShareAlike
# 3.0 Unported License.To view a copy of this license, visit
# http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
# Creative Commons, 444 Castro Street, Suite 900, Mountain View,
# California, 94041, USA.
# This is a sample application to bridge traffic between IPSC networks
from __future__ import print_function
from twisted.internet import reactor
from binascii import b2a_hex as h
import sys
from dmrlink import IPSC, NETWORK, networks, send_to_ipsc, dmr_nat, logger
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK, Dave K, and he who wishes not to be named'
__license__ = 'Creative Commons Attribution-ShareAlike 3.0 Unported'
__version__ = '0.2a'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'Production'
NAT = 0
#NAT = '\x2f\x9b\x80'
# Notes and pieces of next steps...
# RPT_WAKE_UP = b'\x85' + NETWORK[_network]['LOCAL']['RADIO_ID] + b'\x00\x00\x00\x01' + b'\x01' + b'\x01'
# TS1 = 0, TS2 = 1
# Import Bridging rules
#
try:
from bridge_rules import RULES
except ImportError:
sys.exit('Bridging rules file not found or invalid')
class bridgeIPSC(IPSC):
def __init__(self, *args, **kwargs):
IPSC.__init__(self, *args, **kwargs)
self.ACTIVE_CALLS = []
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _network, _src_sub, _dst_group, _ts, _end, _peerid, _data):
if _ts not in self.ACTIVE_CALLS:
self.ACTIVE_CALLS.append(_ts)
# send repeater wake up, but send them when a repeater is likely not TXing check time since end (see below)
if _end:
self.ACTIVE_CALLS.remove(_ts)
# flag the time here so we can test to see if the last call ended long enough ago to send a wake-up
# timer = time()
for rule in RULES[_network]['GROUP_VOICE']:
# Matching for rules is against the Destination Group in the SOURCE packet (SRC_GROUP)
if rule['SRC_GROUP'] == _dst_group and rule['SRC_TS'] == _ts:
_tmp_data = _data
_target = rule['DST_NET']
# Re-Write the IPSC SRC to match the target network's ID
_tmp_data = _tmp_data.replace(_peerid, NETWORK[_target]['LOCAL']['RADIO_ID'])
# Re-Write the destination Group ID
_tmp_data = _tmp_data.replace(_dst_group, rule['DST_GROUP'])
# NAT doesn't work well... use at your own risk!
if NAT:
_tmp_data = dmr_nat(_tmp_data, _src_sub, NAT)
# Calculate and append the authentication hash for the target network... if necessary
if NETWORK[_target]['LOCAL']['AUTH_ENABLED']:
_tmp_data = self.hashed_packet(NETWORK[_target]['LOCAL']['AUTH_KEY'], _tmp_data)
# Send the packet to all peers in the target IPSC
send_to_ipsc(_target, _tmp_data)
if __name__ == '__main__':
logger.info('DMRlink \'bridge.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
for ipsc_network in NETWORK:
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
networks[ipsc_network] = bridgeIPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()

View File

@ -1,46 +0,0 @@
'''
The following is an example for your bridge_rules file. Note, all bridging is ONE-WAY!
Rules for an IPSC network indicate destination IPSC network for the Group ID specified
(allowing transcoding of the Group ID to a different value). Group IDs are specified
as hex strings.
The IPSC name must match an IPSC name from dmrlink.cfg.
The example below cross-patches TGID 1 on an IPSC network named "IPSC_FOO" with TGID 2
on an IPSC network named "IPSC_BAR".
THIS EXAMPLE WILL NOT WORK AS IT IS - YOU MUST SPECIFY NAMES AND GROUP IDS!!!
NOTE: Timeslot transcoding does not yet work (SRC_TS) and (DST_TS) are ignored
'''
def id(_id):
# Create a 3 byte TGID or UID from an integer
return hex(_id)[2:].rjust(6,'0').decode('hex')
RULES = {
'IPSC_FOO': {
'GROUP_VOICE': [
{'SRC_GROUP': id(1), 'SRC_TS': 1, 'DST_NET': 'IPSC_BAR', 'DST_GROUP': id(2), 'DST_TS': 1},
# Repeat the above line for as many rules for this IPSC network as you want.
],
'PRIVATE_VOICE': [
],
'GROUP_DATA': [
],
'PRIVATE_DATA': [
]
},
'IPSC_BAR:' {
'GROUP_VOICE': [
{'SRC_GROUP': id(2), 'SRC_TS': 1, 'DST_NET': 'IPSC_FOO', 'DST_GROUP': id(1), 'DST_TS': 1},
# Repeat the above line for as many rules for this IPSC network as you want.
],
'PRIVATE_VOICE': [
],
'GROUP_DATA': [
],
'PRIVATE_DATA': [
]
}
}

512
confbridge.py Executable file
View File

@ -0,0 +1,512 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application to bridge traffic between IPSC systems. it uses
# one required (bridge_rules.py) and one optional (known_bridges.py) additional
# configuration files. Both files have their own documentation for use.
#
# "bridge_rules" contains the IPSC network, Timeslot and TGID matching rules to
# determine which voice calls are bridged between IPSC systems and which are
# not.
#
# "known_bridges" contains DMR radio ID numbers of known bridges. This file is
# used when you want bridge.py to be "polite" or serve as a backup bridge. If
# a known bridge exists in either a source OR target IPSC network, then no
# bridging between those IPSC systems will take place. This behavior is
# dynamic and updates each keep-alive interval (main configuration file).
# For faster failover, configure a short keep-alive time and a low number of
# missed keep-alives before timout. I recommend 5 sec keep-alive and 3 missed.
# That gives a worst-case scenario of 15 seconds to fail over. Recovery will
# typically happen with a single "blip" in the transmission up to about 5
# seconds.
#
# While this file is listed as Beta status, K0USY Group depends on this code
# for the bridigng of it's many repeaters. We consider it reliable, but you
# get what you pay for... as usual, no guarantees.
#
# Use to make test strings: #print('PKT:', "\\x".join("{:02x}".format(ord(c)) for c in _data))
from __future__ import print_function
from twisted.internet.protocol import Factory, Protocol
from twisted.protocols.basic import NetstringReceiver
from twisted.internet import reactor
from twisted.internet import task
from binascii import b2a_hex as ahex
from time import time
from importlib import import_module
import cPickle as pickle
from dmr_utils.utils import hex_str_3, hex_str_4, int_id
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, REPORT_OPCODES, build_aliases
from ipsc.ipsc_const import BURST_DATA_TYPE
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013 - 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski, KD8EYF; Steve Zingman, N4IRS; Mike Zingman, N4IRR'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
# Minimum time between different subscribers transmitting on the same TGID
#
TS_CLEAR_TIME = .2
# Declare this here so that we can define functions around it
#
BRIDGES = {}
# Timed loop used for reporting IPSC status
#
# REPORT BASED ON THE TYPE SELECTED IN THE MAIN CONFIG FILE
def config_reports(_config, _logger, _factory):
if _config['REPORTS']['REPORT_NETWORKS'] == 'PRINT':
def reporting_loop(_logger):
_logger.debug('Periodic Reporting Loop Started (PRINT)')
for system in _config['SYSTEMS']:
print_master(_config, system)
print_peer_list(_config, system)
reporting = task.LoopingCall(reporting_loop, _logger)
reporting.start(_config['REPORTS']['REPORT_INTERVAL'])
report_server = False
elif _config['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
def reporting_loop(_logger, _server):
_logger.debug('Periodic Reporting Loop Started (NETWORK)')
_server.send_config()
_server.send_bridge()
_logger.info('DMRlink TCP reporting server starting')
report_server = _factory(_config, _logger)
report_server.clients = []
reactor.listenTCP(_config['REPORTS']['REPORT_PORT'], report_server)
reporting = task.LoopingCall(reporting_loop, _logger, report_server)
reporting.start(_config['REPORTS']['REPORT_INTERVAL'])
else:
def reporting_loop(_logger):
_logger.debug('Periodic Reporting Loop Started (NULL)')
report_server = False
return report_server
# Build the conference bridging structure from the bridge file.
#
def make_bridge_config(_confbridge_rules):
try:
bridge_file = import_module(_confbridge_rules)
logger.info('Bridge configuration file found and imported')
except ImportError:
sys.exit('Bridge configuration file not found or invalid')
# Convert integer GROUP ID numbers from the config into hex strings
# we need to send in the actual data packets.
#
for _bridge in bridge_file.BRIDGES:
for _system in bridge_file.BRIDGES[_bridge]:
if _system['SYSTEM'] not in CONFIG['SYSTEMS']:
sys.exit('ERROR: Conference bridges found for system not configured main configuration')
_system['TGID'] = hex_str_3(_system['TGID'])
for i, e in enumerate(_system['ON']):
_system['ON'][i] = hex_str_3(_system['ON'][i])
for i, e in enumerate(_system['OFF']):
_system['OFF'][i] = hex_str_3(_system['OFF'][i])
for i, e in enumerate(_system['RESET']):
_system['RESET'][i] = hex_str_3(_system['RESET'][i])
_system['TIMEOUT'] = _system['TIMEOUT']*60
_system['TIMER'] = time()
return {'BRIDGE_CONF': bridge_file.BRIDGE_CONF, 'BRIDGES': bridge_file.BRIDGES, 'TRUNKS': bridge_file.TRUNKS}
# Import subscriber ACL
# ACL may be a single list of subscriber IDs
# Global action is to allow or deny them. Multiple lists with different actions and ranges
# are not yet implemented.
def build_acl(_sub_acl):
ACL = set()
try:
logger.info('ACL file found, importing entries. This will take about 1.5 seconds per 1 million IDs')
acl_file = import_module(_sub_acl)
sections = acl_file.ACL.split(':')
ACL_ACTION = sections[0]
entries_str = sections[1]
for entry in entries_str.split(','):
if '-' in entry:
start,end = entry.split('-')
start,end = int(start), int(end)
for id in range(start, end+1):
ACL.add(hex_str_3(id))
else:
id = int(entry)
ACL.add(hex_str_3(id))
logger.info('ACL loaded: action "{}" for {:,} radio IDs'.format(ACL_ACTION, len(ACL)))
except ImportError:
logger.info('ACL file not found or invalid - all subscriber IDs are valid')
ACL_ACTION = 'NONE'
# Depending on which type of ACL is used (PERMIT, DENY... or there isn't one)
# define a differnet function to be used to check the ACL
global allow_sub
if ACL_ACTION == 'PERMIT':
def allow_sub(_sub):
if _sub in ACL:
return True
else:
return False
elif ACL_ACTION == 'DENY':
def allow_sub(_sub):
if _sub not in ACL:
return True
else:
return False
else:
def allow_sub(_sub):
return True
return ACL
# Run this every minute for rule timer updates
def rule_timer_loop():
logger.info('(ALL IPSC SYSTEMS) Rule timer loop started')
_now = time()
for _bridge in BRIDGES:
for _system in BRIDGES[_bridge]:
if _system['TO_TYPE'] == 'ON':
if _system['ACTIVE'] == True:
if _system['TIMER'] < _now:
_system['ACTIVE'] = False
logger.info('Conference Bridge TIMEOUT: DEACTIVATE System: %s, Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
else:
timeout_in = _system['TIMER'] - _now
logger.info('Conference Bridge ACTIVE (ON timer running): System: %s Bridge: %s, TS: %s, TGID: %s, Timeout in: %ss,', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']), timeout_in)
elif _system['ACTIVE'] == False:
logger.debug('Conference Bridge INACTIVE (no change): System: %s Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
elif _system['TO_TYPE'] == 'OFF':
if _system['ACTIVE'] == False:
if _system['TIMER'] < _now:
_system['ACTIVE'] = True
logger.info('Conference Bridge TIMEOUT: ACTIVATE System: %s, Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
else:
timeout_in = _system['TIMER'] - _now
logger.info('Conference Bridge INACTIVE (OFF timer running): System: %s Bridge: %s, TS: %s, TGID: %s, Timeout in: %ss,', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']), timeout_in)
elif _system['ACTIVE'] == True:
logger.debug('Conference Bridge ACTIVE (no change): System: %s Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
else:
logger.debug('Conference Bridge NO ACTION: System: %s, Bridge: %s, TS: %s, TGID: %s', _system['SYSTEM'], _bridge, _system['TS'], int_id(_system['TGID']))
if BRIDGE_CONF['REPORT'] == 'network':
report_server.send_clients('bridge updated')
class confbridgeIPSC(IPSC):
def __init__(self, _name, _config, _logger, _report):
IPSC.__init__(self, _name, _config, _logger, _report)
self.STATUS = {
1: {'RX_TGID':'\x00', 'TX_TGID':'\x00', 'RX_TIME':0, 'TX_TIME':0, 'RX_SRC_SUB':'\x00', 'TX_SRC_SUB':'\x00'},
2: {'RX_TGID':'\x00', 'TX_TGID':'\x00', 'RX_TIME':0, 'TX_TIME':0, 'RX_SRC_SUB':'\x00', 'TX_SRC_SUB':'\x00'}
}
self.last_seq_id = '\x00'
self.call_start = 0
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _src_sub, _dst_group, _ts, _end, _peerid, _data):
# Check for ACL match, and return if the subscriber is not allowed
if allow_sub(_src_sub) == False:
self._logger.warning('(%s) Group Voice Packet ***REJECTED BY ACL*** From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_group))
return
# Process the packet
#self._logger.debug('(%s) Group Voice Packet Received From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_group))
_burst_data_type = _data[30] # Determine the type of voice packet this is (see top of file for possible types)
_seq_id = _data[5]
now = time() # Mark packet arrival time -- we'll need this for call contention handling
for _bridge in BRIDGES:
for _system in BRIDGES[_bridge]:
if (_system['SYSTEM'] == self._system and _system['TGID'] == _dst_group and _system['TS'] == _ts and _system['ACTIVE'] == True):
for _target in BRIDGES[_bridge]:
if _target['SYSTEM'] != self._system:
if _target['ACTIVE']:
_target_status = systems[_target['SYSTEM']].STATUS
_target_system = self._CONFIG['SYSTEMS'][_target['SYSTEM']]
# BEGIN CONTENTION HANDLING
#
# If the system is listed as a "TRUNK", there will be no contention handling. All traffic is forwarded to it
#
# The rules for each of the 4 "ifs" below are listed here for readability. The Frame To Send is:
# From a different group than last RX from this IPSC, but it has been less than Group Hangtime
# From a different group than last TX to this IPSC, but it has been less than Group Hangtime
# From the same group as the last RX from this IPSC, but from a different subscriber, and it has been less than TS Clear Time
# From the same group as the last TX to this IPSC, but from a different subscriber, and it has been less than TS Clear Time
# The "continue" at the end of each means the next iteration of the for loop that tests for matching rules
#
if _target not in TRUNKS:
if ((_target['TGID'] != _target_status[_target['TS']]['RX_TGID']) and ((now - _target_status[_target['TS']]['RX_TIME']) < _target_system['LOCAL']['GROUP_HANGTIME'])):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged to TGID%s, target active or in group hangtime: IPSC: %s, TS: %s, TGID: %s', self._system, int_id(_target['TGID']), _target['SYSTEM'], _target['TS'], int_id(_target_status[_target['TS']]['RX_TGID']))
continue
if ((_target['TGID'] != _target_status[_target['TS']]['TX_TGID']) and ((now - _target_status[_target['TS']]['TX_TIME']) < _target_system['LOCAL']['GROUP_HANGTIME'])):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged to TGID%s, target in group hangtime: IPSC: %s, TS: %s, TGID: %s', self._system, int_id(_target['TGID']), _target['SYSTEM'], _target['TS'], int_id(_target_status[_target['TS']]['TX_TGID']))
continue
if (_target['TGID'] == _target_status[_target['TS']]['RX_TGID']) and ((now - _target_status[_target['TS']]['RX_TIME']) < TS_CLEAR_TIME):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged to TGID%s, matching call already active on target: IPSC: %s, TS: %s, TGID: %s', self._system, int_id(_target['TGID']), _target['SYSTEM'], _target['TS'], int_id(_target_status[_target['TS']]['RX_TGID']))
continue
if (_target['TGID'] == _target_status[_target['TS']]['TX_TGID']) and (_src_sub != _target_status[_target['TS']]['TX_SRC_SUB']) and ((now - _target_status[_target['TS']]['TX_TIME']) < TS_CLEAR_TIME):
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
self._logger.info('(%s) Call not bridged for subscriber %s, call bridge in progress on target: IPSC: %s, TS: %s, TGID: %s SUB: %s', self._system, int_id(_src_sub), _target['SYSTEM'], _target['TGID'], int_id(_target_status[_target['TS']]['TX_TGID']), int_id(_target_status[_target['TS']]['TX_SRC_SUB']))
continue
#
# END CONTENTION HANDLING
#
#
# BEGIN FRAME FORWARDING
#
# Make a copy of the payload
_tmp_data = _data
# Re-Write the PEER ID in the IPSC Header:
_tmp_data = _tmp_data.replace(_peerid, _target_system['LOCAL']['RADIO_ID'], 1)
# Re-Write the IPSC SRC + DST GROUP in IPSC Headers:
_tmp_data = _tmp_data.replace(_src_sub + _dst_group, _src_sub + _target['TGID'], 1)
# Re-Write the DST GROUP + IPSC SRC in DMR LC (Header, Terminator and Voice Burst E):
_tmp_data = _tmp_data.replace(_dst_group + _src_sub, _target['TGID'] + _src_sub, 1)
# Re-Write IPSC timeslot value
_call_info = int_id(_data[17:18])
if _target['TS'] == 1:
_call_info &= ~(1 << 5)
elif _target['TS'] == 2:
_call_info |= 1 << 5
_call_info = chr(_call_info)
_tmp_data = _tmp_data[:17] + _call_info + _tmp_data[18:]
# Re-Write DMR timeslot value
# Determine if the slot is present, so we can translate if need be
if _burst_data_type == BURST_DATA_TYPE['SLOT1_VOICE'] or _burst_data_type == BURST_DATA_TYPE['SLOT2_VOICE']:
_slot_valid = True
else:
_slot_valid = False
# Re-Write timeslot if necessary...
if _slot_valid:
if _target['TS'] == 1:
_burst_data_type = BURST_DATA_TYPE['SLOT1_VOICE']
elif _target['TS'] == 1:
_burst_data_type = BURST_DATA_TYPE['SLOT2_VOICE']
_tmp_data = _tmp_data[:30] + _burst_data_type + _tmp_data[31:]
# Send the packet to all peers in the target IPSC
systems[_target['SYSTEM']].send_to_ipsc(_tmp_data)
#
# END FRAME FORWARDING
#
# Set values for the contention handler to test next time there is a frame to forward
_target_status[_target['TS']]['TX_TGID'] = _target['TGID']
_target_status[_target['TS']]['TX_TIME'] = now
_target_status[_target['TS']]['TX_SRC_SUB'] = _src_sub
# Mark the group and time that a packet was recieved for the contention handler to use later
self.STATUS[_ts]['RX_TGID'] = _dst_group
self.STATUS[_ts]['RX_TIME'] = now
#
# BEGIN IN-BAND SIGNALING BASED ON TGID & VOICE TERMINATOR FRAME
#
# Activate/Deactivate rules based on group voice activity -- PTT or UA for you c-Bridge dorks.
# This will ONLY work for symmetrical rules!!!
# Action happens on key up
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
if self.last_seq_id != _seq_id or (self.call_start + TS_CLEAR_TIME) < now:
self.last_seq_id = _seq_id
self.call_start = now
self._logger.info('(%s) GROUP VOICE START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group))
if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
self._report.send_bridgeEvent('GROUP VOICE,START,{},{},{},{},{},{}'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group)))
# Action happens on un-key
if _burst_data_type == BURST_DATA_TYPE['VOICE_TERM']:
if self.last_seq_id == _seq_id:
self.call_duration = now - self.call_start
self._logger.info('(%s) GROUP VOICE END: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s Duration: %.2fs', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration)
if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
self._report.send_bridgeEvent('GROUP VOICE,END,{},{},{},{},{},{},{:.2f}'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration))
else:
self._logger.warning('(%s) GROUP VOICE END WITHOUT MATCHING START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group))
if self._CONFIG['REPORTS']['REPORT_NETWORKS'] == 'NETWORK':
self._report.send_bridgeEvent('GROUP VOICE,UNMATCHED END,{},{},{},{},{},{}'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group)))
# Iterate the rules dictionary
for _bridge in BRIDGES:
for _system in BRIDGES[_bridge]:
if _system['SYSTEM'] == self._system:
# TGID matches an ACTIVATION trigger
if (_dst_group in _system['ON'] or _dst_group in _system['RESET']) and _ts == _system['TS']:
# Set the matching rule as ACTIVE
if _dst_group in _system['ON']:
if _system['ACTIVE'] == False:
_system['ACTIVE'] = True
self._logger.info('(%s) Bridge: %s, connection changed to state: %s', self._system, _bridge, _system['ACTIVE'])
# Cancel the timer if we've enabled an "OFF" type timeout
if _system['TO_TYPE'] == 'OFF':
_system['TIMER'] = now
self._logger.info('(%s) Bridge: %s set to "OFF" with an on timer rule: timeout timer cancelled', self._system, _bridge)
# Reset the timer for the rule
if _system['ACTIVE'] == True and _system['TO_TYPE'] == 'ON':
_system['TIMER'] = now + _system['TIMEOUT']
self._logger.info('(%s) Bridge: %s, timeout timer reset to: %s', self._system, _bridge, _system['TIMER'] - now)
# TGID matches an DE-ACTIVATION trigger
if (_dst_group in _system['OFF'] or _dst_group in _system['RESET']) and _ts == _system['TS']:
# Set the matching rule as ACTIVE
if _dst_group in _system['OFF']:
if _system['ACTIVE'] == True:
_system['ACTIVE'] = False
self._logger.info('(%s) Bridge: %s, connection changed to state: %s', self._system, _bridge, _system['ACTIVE'])
# Cancel the timer if we've enabled an "ON" type timeout
if _system['TO_TYPE'] == 'ON':
_system['TIMER'] = now
self._logger.info('(%s) Bridge: %s set to ON with and "OFF" timer rule: timeout timer cancelled', self._system, _bridge)
# Reset the timer for the rule
if _system['ACTIVE'] == False and _system['TO_TYPE'] == 'OFF':
_system['TIMER'] = now + _system['TIMEOUT']
self._logger.info('(%s) Bridge: %s, timeout timer reset to: %s', self._system, _bridge, _system['TIMER'] - now)
# Cancel the timer if we've enabled an "ON" type timeout
if _system['ACTIVE'] == True and _system['TO_TYPE'] == 'ON' and _dst_group in _system['OFF']:
_system['TIMER'] = now
self._logger.info('(%s) Bridge: %s set to ON with and "OFF" timer rule: timeout timer cancelled', self._system, _bridge)
#
# END IN-BAND SIGNALLING
#
class confbridgeReportFactory(reportFactory):
def send_bridge(self):
serialized = pickle.dumps(BRIDGES, protocol=pickle.HIGHEST_PROTOCOL)
self.send_clients(REPORT_OPCODES['BRIDGE_SND']+serialized)
def send_bridgeEvent(self, _data):
self.send_clients(REPORT_OPCODES['BRDG_EVENT']+_data)
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, confbridgeReportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGURED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, confbridgeIPSC, report_server)
# CONFBRIDGE.PY SPECIFIC ITEMS GO HERE:
# Build the routing rules and other configuration
CONFIG_DICT = make_bridge_config('confbridge_rules')
BRIDGE_CONF = CONFIG_DICT['BRIDGE_CONF']
TRUNKS = CONFIG_DICT['TRUNKS']
BRIDGES = CONFIG_DICT['BRIDGES']
# Build the Access Control List
ACL = build_acl('sub_acl')
# Initialize the rule timer loop
rule_timer = task.LoopingCall(rule_timer_loop)
rule_timer.start(60)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

68
confbridge_rules_SAMPLE.py Executable file
View File

@ -0,0 +1,68 @@
'''
THIS EXAMPLE WILL NOT WORK AS IT IS - YOU MUST SPECIFY YOUR OWN VALUES!!!
This file is organized around the "Conference Bridges" that you wish to use. If you're a c-Bridge
person, think of these as "bridge groups". You might also liken them to a "reflector". If a particular
system is "ACTIVE" on a particular conference bridge, any traffic from that system will be sent
to any other system that is active on the bridge as well. This is not an "end to end" method, because
each system must independently be activated on the bridge.
The first level (e.g. "WORLDWIDE" or "STATEWIDE" in the examples) is the name of the conference
bridge. This is any arbitrary ASCII text string you want to use. Under each conference bridge
definition are the following items -- one line for each System as defined in the main DMRlink
configuration file.
* SYSTEM - The name of the sytem as listed in the main dmrlink configuration file (e.g.dmrlink.cfg)
This MUST be the exact same name and case as in the main config file!!!
* TS - Timeslot used for matching traffic to this confernce bridge
* TGID - Talkgroup ID used for matching traffic to this conference bridge
* ON and OFF are LISTS of Talkgroup IDs used to trigger this system off and on. Even if you
only want one (as shown in the ON example), it has to be in list format. None can be
handled with an empty list, such as " 'ON': [] ".
* RESET is a list of Talkgroup IDs that, in addition to the ON and OFF lists will cause a running
timer to be reset. This is useful if you are using different TGIDs for voice traffic than
triggering. If you are not, there is NO NEED to use this feature.
* TO_TYPE is timeout type. If you want to use timers, ON means when it's turned on, it will
turn off afer the timout period and OFF means it will turn back on after the timout
period. If you don't want to use timers, set it to anything else, but 'NONE' might be
a good value for documentation!
* TIMOUT is a value in minutes for the timout timer. It MUST have a value. No,
I won't make it 'seconds', so don't ask. Timers are performance "expense".
'''
# CONFIGURATION ITEMS SPECIFICALLY FOR confbridge.py
#
# REPORT:
# True or False. True if you want to write a pickle file of the current rule file
# state. This is useful (and necessary) for reporting features to be active.
# The path follows the reporting path in the main dmrlink.cfg file.
BRIDGE_CONF = {
'REPORT': True,
}
# TRUNK IPSC Systems -- trunk bypasses the contention handler and always transmits traffic
#
# This is a python LIST data type. It needs to be here, but just leave it empty if not used.
# The contents are a quoted, comma separated list of IPSC systems that are traffic trunks.
# Example: TRUNKS = ['MASTER-1', 'CLIENT-2']
TRUNKS = []
BRIDGES = {
'WORLDWIDE': [
{'SYSTEM': 'MASTER-1', 'TS': 1, 'TGID': 1, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'ON', 'ON': [2,], 'OFF': [9,10], 'RESET': []},
{'SYSTEM': 'CLIENT-1', 'TS': 1, 'TGID': 3100, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'ON', 'ON': [2,], 'OFF': [9,10], 'RESET': []}
],
'ENGLISH': [
{'SYSTEM': 'MASTER-1', 'TS': 1, 'TGID': 13, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [3,], 'OFF': [8,10], 'RESET': []},
{'SYSTEM': 'CLIENT-2', 'TS': 1, 'TGID': 13, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [3,], 'OFF': [8,10], 'RESET': []}
],
'STATEWIDE': [
{'SYSTEM': 'MASTER-1', 'TS': 2, 'TGID': 3129, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [4,], 'OFF': [7,10], 'RESET': []},
{'SYSTEM': 'CLIENT-2', 'TS': 2, 'TGID': 3129, 'ACTIVE': True, 'TIMEOUT': 2, 'TO_TYPE': 'NONE', 'ON': [4,], 'OFF': [7,10], 'RESET': []}
]
}
if __name__ == '__main__':
from pprint import pprint
pprint(BRIDGES)

1478
dmrlink.py

File diff suppressed because it is too large Load Diff

206
dmrlink_SAMPLE.cfg Normal file → Executable file
View File

@ -1,30 +1,65 @@
# DMRLink SAMPLE CONFIGURATION FILE
#
# Rename to dmrlink.cfg and add your information
# # GLOBAL CONFIGURATION ITEMS
# There are no global options at this time
#
# minor tweaks to match install for use by DMRGateway
# N4IRS
#
#
# GLOBAL CONFIGURATION ITEMS
#
[GLOBAL]
PATH: /absolute/path/to/DMRlink
PATH: /opt/dmrlink/
# STDOUT REPORTING CONFIG
# Enabling "REPORT_PEERS" will cause a print-out of the peers in each
# IPSC each time the periodic reporting loop runs. Likewise, the
# additional features listed will cause that list to either include
# or not include MODE and/or SERVICE FLAG details.
# NETWORK REPORTING CONFIGURATION
# Enabling "REPORT_NETWORKS" will cause a reporting action for
# IPSC each time the periodic reporting loop runs, that period is
# specified by "REPORT_INTERVAL" in seconds. Possible values
# for "REPORT_NETWORKS" are:
#
# PRINT - a pretty print (STDOUT) of the data structure
# "PRINT_PEERS_INC_MODE" - Boolean to include mode bits
# "PRINT_PEERS_INC_FLAGS" - Boolean to include flag bits
#
# NETWORK - This is the right way to do it. Opens a TCP socket
# listener. The protocol is still in its infancy, but the
# idea is that dmrlink will talk to another application
# to send event and status updates. Of course, the big
# goal here is a web dashboard that doesn't live on the
# dmrlink machine itself.
#
# PRINT should only be used for debugging; it sends prettily formatted
# stuff to STDOUT. The others send the internal data structure of the
# IPSC instance and let some program on the other end sort it out.
#
# REPORT_RCM - If True, and REPORT_NETWORKS = 'NETWORK', will send RCM
# Packets to connected reporting clients. This also requires
# individual IPSC systems to have RCM and CON_APP both set 'True'
#
# REPORT_INTERVAL - Seconds between reports
# REPORT_PORT - TCP port to listen on if "REPORT_NETWORKS" = NETWORK
# REPORT_CLIENTS - comma separated list of IPs you will allow clients
# to connect on.
#
[REPORTS]
REPORT_PEERS: 0
PEER_REPORT_INC_MODE: 0
PEER_REPORT_INC_FLAGS: 0
REPORT_NETWORKS:
REPORT_RCM:
REPORT_INTERVAL: 60
REPORT_PORT: 4321
REPORT_CLIENTS: 127.0.0.1, 192.168.1.1
PRINT_PEERS_INC_MODE: 0
PRINT_PEERS_INC_FLAGS: 0
# SYSTEM LOGGER CONFIGURAITON
# This allows the logger to be configured without chaning the individual
# python logger stuff in dmrlink.py. LOG_FILE should be a complete
# path/filename for *your* system. LOG_HANDERLS may be any of the
# following, please, no spaces in the list if you use several:
# path/filename for *your* system -- use /dev/null for non-file handlers.
# LOG_HANDERLS may be any of the following, please, no spaces in the
# list if you use several:
# null
# console
# console-timed
# file
@ -35,68 +70,123 @@ PEER_REPORT_INC_FLAGS: 0
# used.
#
[LOGGER]
LOG_FILE: /tmp/dmrlink.log
LOG_HANDLERS: console
LOG_LEVEL: CRITICAL
LOG_FILE: /var/log/dmrlink/dmrlink.log
LOG_HANDLERS: file
LOG_LEVEL: INFO
LOG_NAME: DMRlink
# DOWNLOAD AND IMPORT SUBSCRIBER, PEER and TGID ALIASES
# Ok, not the TGID, there's no master list I know of to download
# This is intended as a facility for other applcations built on top of
# DMRlink to use, and will NOT be used in DMRlink directly.
# STALE_DAYS is the number of days since the last download before we
# download again. Don't be an ass and change this to less than a few days.
[ALIASES]
TRY_DOWNLOAD: True
LOCAL_FILE: False
PATH: ./
PEER_FILE: peer_ids.json
SUBSCRIBER_FILE: subscriber_ids.json
TGID_FILE: talkgroup_ids.json
PEER_URL: https://www.radioid.net/static/rptrs.json
SUBSCRIBER_URL: https://www.radioid.net/static/users.json
STALE_DAYS: 7
# CONFIGURATION FOR IPSC NETWORKS
# Please read these closely - catastrophic results could result by setting
# certain flags for things DMRlink cannot do.
#
# [NAME] The name you want to use to identify the IPSC instance (use
# something better than "IPSC1"...)
# ENABLED: Should we communiate with this network? Handy if you need to
# shut one down but don't want to lose the config
# RADIO_ID: This is the radio ID that DMRLink should use to communicate
# PORT: This is the UDP source port for DMRLink to use for this
# IPSC network, must be unique!!!
# ALIVE_TIMER: Seconds between keep-alive transmissions
# MAX_MISSED: How many missed keep-alives before we remove a peer
# PEER_OPER: This signals the master and peers whether or not we are
# operational. True is the only thing that makes sense.
# IPSC_MODE: May be 'DIGITAL', 'ANALOG', or 'NONE'. Digital is really the
# only thing that makes sense.
# TSx_LINK: Is this time slot linked?
# CSBK_CALL: Should be False, we cannot process these, but may be useful
# for debugging.
# RCM: Repeater Call Monitoring - don't unable unless you plan to
# actually use it, this craetes extra network traffic.
# CON_APP: Third Party Console App - exactly what DMRlink is, should
# be set to True.
# XNL_CALL: Can cause problems if not set to False, DMRlink does not
# process XCMP/XNL calls.
# XNL_MASTER: Obviously, should also be False, see XNL_CALL.
# DATA_CALL: Process data calls. True if you want to process data calls
# VOICE_CALL: Process voice calls. True if you want to process voice calls
# MASTER_PEER: Must be False, we cannot yet act as a master peer.
# AUTH_ENABLED: Do we use authenticated IPSC?
# AUTH_KEY: The Authentication key (up to 40 hex characters)
# MASTER_IP: IP address of the IPSC master (ignored if DMRlink is the master)
# MASTER_PORT: UDP port of the IPSC master (ignored if DMRlinkn is the master)
# [NAME] The name you want to use to identify the IPSC instance (use
# something better than "IPSC1"...)
# ENABLED: Should we communicate with this network? Handy if you need to
# shut one down but don't want to lose the config
# RADIO_ID: This is the radio ID that DMRLink should use to communicate
# IP: This is the local IPv4 address to listen on. It may be left
# blank if you do not need or wish to specify. It is mostly
# useful when DMRlink uses multiple interfaces to serve as an
# application gateway/proxy from private and/or VPN networks
# to the real world.
# PORT: This is the UDP source port for DMRLink to use for this
# PSC network, must be unique!!!
# ALIVE_TIMER: Seconds between keep-alive transmissions
# MAX_MISSED: How many missed keep-alives before we remove a peer
# PEER_OPER: This signals the master and peers whether or not we are
# operational. True is the only thing that makes sense.
# IPSC_MODE: May be 'DIGITAL', 'ANALOG', or 'NONE'. Digital is really the
# only thing that makes sense.
# TSx_LINK: Is this time slot linked?
# CSBK_CALL: Should be False, we cannot process these, but may be useful
# for debugging.
# RCM: Repeater Call Monitoring - don't unable unless you plan to
# actually use it, this creates extra network traffic.
# CON_APP: Third Party Console App - exactly what DMRlink is, should
# be set to True, and must be if you intend to process RCM
# packets (like with network-based reporting)
# XNL_CALL: Can cause problems if not set to False, DMRlink does not
# process XCMP/XNL calls.
# XNL_MASTER: Obviously, should also be False, see XNL_CALL.
# DATA_CALL: Process data calls. True if you want to process data calls
# VOICE_CALL: Process voice calls. True if you want to process voice calls
# MASTER_PEER: True if DMRlink will be the master, False if we're a peer
# AUTH_ENABLED: Do we use authenticated IPSC?
# AUTH_KEY: The Authentication key (up to 40 hex characters)
# MASTER_IP: IP address of the IPSC master (ignored if DMRlink is the master)
# MASTER_PORT: UDP port of the IPSC master (ignored if DMRlink is the master)
# GROUP_HANGTIME: Group hangtime, per DMR configuration
#
# ...Repeat the block for each IPSC network to join.
#
[IPSC1]
[SAMPLE_PEER]
ENABLED: True
RADIO_ID: 12345
IP:
PORT: 50000
ALIVE_TIMER: 5
MAX_MISSED: 20
PEER_OPER = True
IPSC_MODE = DIGITAL
PEER_OPER: True
IPSC_MODE: DIGITAL
TS1_LINK: True
TS2_LINK: True
CSBK_CALL = False
RCM = True
CON_APP = True
XNL_CALL = False
XNL_MASTER = False
DATA_CALL = True
VOICE_CALL = True
MASTER_PEER = False
AUTH_ENABLED = True
CSBK_CALL: False
RCM: True
CON_APP: True
XNL_CALL: False
XNL_MASTER: False
DATA_CALL: True
VOICE_CALL: True
MASTER_PEER: False
AUTH_ENABLED: True
AUTH_KEY: 1A2B3C
MASTER_IP: 1.2.3.4
MASTER_PORT: 50000
GROUP_HANGTIME: 5
[SAMPLE_MASTER]
ENABLED: False
RADIO_ID: 54321
IP: 192.168.1.1
PORT: 50000
ALIVE_TIMER: 5
MAX_MISSED: 20
PEER_OPER: True
IPSC_MODE: DIGITAL
TS1_LINK: True
TS2_LINK: True
CSBK_CALL: False
RCM: True
CON_APP: True
XNL_CALL: False
XNL_MASTER: False
DATA_CALL: True
VOICE_CALL: True
MASTER_PEER: True
AUTH_ENABLED: True
AUTH_KEY: 1A2B3C
# Below not used for a Master
# MASTER_IP: 1.2.3.4
# MASTER_PORT: 50000
GROUP_HANGTIME: 5

17
documents/FAQ.md Executable file
View File

@ -0,0 +1,17 @@
##DMRlink FAQ
**PURPOSE:** Since DMRlink was published, a number of similar questions have come in regarding it's use. This FAQ will attempt to address common questions or concerns.
**Can DMRlink bridge networks like a c-Bridge?** Yes, bridge.py can bridge IPSC networks, but no, not not quite like a c-Bridge. It does not have automatic scheduling or "trigggering" of bridge events. Currently bridge rules must all be static.
**Someone said DMRlink "bricked" their repeater, is it safe to use?** DMRlink has no abilty to speak the XNL/XCMP protocol (which involves encrypted keys) that Motorla uses to control radios and repeaters. DMRlink simply cannot even remotely speak the language necessary to do this.
**DMRlink is OpenSource, but IPSC is proprietary, will I get in trouble for using it?** DMRlink is an original interpretation of the IPSC protocol. It's probably not quite 100% correct, and it certainly doesn't implement every last feature in IPSC. It is not being sold, and it is not presented as a replacement for exising commercial software. We have received no complaints from Motorola regarding this project. We do not believe using it will be a problem -- if there's a problem, it will be with those of us who wrote it, and to date, we have recieved no complaints.
**Was DMRlink created by hacking the c-Bridge and/or SmartPTT?** Absolutely not! DMRlink was created using wireshark to capture packets between IPSC speaking endpoints on an IPSC network and pattern-matching. For example, when we know the transmisison was from radio ID 12345, we assume that when we find 12345 in the data stream, that's the source radio ID... we then further match patterns to validate what we find. This is why DMRlink will likely never include all features in IPSC, like XNL/XCMP for example, which uses encryption that makes pattern matching virutally impossible.
**Why can't DMRlink talk to my c-Bridge over a CC-CC connection??** The c-Bridge CC-CC connection is a proprietary system written by Ravennet Systems. It is not part of IPSC, and is used only between c-Bridge, TL-NET and other Ravennet-based RoIP systems. The DMRlink project only deals with IPSC. As such it doesn't communicate with SmartPTT radioserver-to-radioserver links either.
**Will you help me get it working?** DMRlink is not commercial software, and nobody is getting paid to write it. The work here represents HUNDREDS of hours of volunteer effort. We will help as we can, but you must be familiar with IP data networking, very basic programming (preferably python) and IPSC or you will likely have a very hard time getting it to work to your satisfaction.
***73 DE N0MJS***

View File

View File

@ -0,0 +1,166 @@
PARTS OF THE PACKET THAT ARE NOT KNOWN ARE WRAPPED WITH STARS -- LIKE THIS **00.00.AC**, MEANS WE DONT KNOW WHAT THAT IS
VOICE HEADER 1: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.dd 3b.01 3b.eb.3c.e0 00.00.00.00 | 01 80 00.0a 80 0a 00.60 00 00 00 00.00.02 2f.7c.ca 92.af.70 **00113532**
VOICE HEADER 2: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.5d 3b.02 3b.eb.3e.c0 00.00.00.00 | 01 80 00.0a 80 0a 00.60 00 00 00 00.00.02 2f.7c.ca 92.af.70 **00113530**
VOICE HEADER 3: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.5d 3b.03 3b.eb.40.a0 00.00.00.00 | 01 80 00.0a 80 0a 00.60 00 00 00 00.00.02 2f.7c.ca 92.af.70 **00113531**
VOICE BURST A: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.5d 3b.04 3b.eb.42.80 00.00.00.00 | 8a 14 **40** f8.01.a9.9f.8c.e0.be.00.6a.67.e3.38.2f.80.1a.99.f8.ce.08
VOICE BURST B: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.5d 3b.05 3b.eb.44.60 00.00.00.00 | 8a 19 **06** f8.01.a9.9f.8c.e0.be.00.6a.67.e3.38.2f.80.1a.99.f8.ce.08 **0505060612**
VOICE BURST C: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.5d 3b.06 3b.eb.46.40 00.00.00.00 | 8a 19 **06** f8.01.a9.9f.8c.e0.be.00.6a.67.e3.38.2f.80.1a.99.f8.ce.08 **0905060516**
VOICE BURST D: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.5d 3b.07 3b.eb.48.20 00.00.00.00 | 8a 19 **06** 98.02.b9.4f.a4.d3.bb.b7.96.c7.83.d8.ee.81.19.41.e4.4a.68 **0f05060f16**
VOICE BURST E: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.5d 3b.08 3b.eb.4a.00 00.00.00.00 | 8a 22 **16** e8.1a.62.d6.8c.6b.ba.06.3d.0d.eb.04.e9.81.dd.f1.04.86.c8 **000a0a0c000000** 00.00.02 2f.7c.ca **14**
VOICE BURST F: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 20 | 80.5d 3b.09 3b.eb.4b.e0 00.00.00.00 | 8a 19 **06** 98.22.d3.d9.00.b4.a6.05.6d.29.a2.17.a8.82.75.14.f8.10.08 **0000000010**
VOICE TERMINATOR: 80 00.04.bf.fd 08 2f.7c.ca 00.00.02 02 **00.00.30.ac** 60 | 80.5e 3e.76 3b.f1.b8.40 00.00.00.00 | 02 80 00.0a 80 0a 00.60 00 00 00 00.00.02 2f.7c.ca 9d.a0.7f **00123535**
VOICE HEADER: 54 Bytes (0-53) (sent 3 times, see notes):
IPSC:
PACKET_TYPE[0]
PEER_ID[1-4]
IPSC_SEQ[5]
SRC_SUB[6-9]
DST_SUB[9-11]
CALL_TYPE[12]
CALL_CONTROL[13-16] (use a random number)
CALL_INFO[17]
RTP:
RTP_HEAD[18-19]
RTP_SEQ[20-21]
RTP_TIMESTMP[22-25]
RTP_SSID[26-29]
RTP PAYLOAD:
BURST_TYPE[30]
RSSI_THRESH_PARITY[31]
LENGTH_TO_FOLLOW[32-33] (in words)
RSSI_STATUS[34]
SLOT_TYPE_SYNC[35]
DATA_SIZE[36-37] Burst data length in bits; 96/8 = 12.. last 4 bytes ont part of Burst??
FULL_LC_BYTE1[38] (PF, R, FLCO)
FULL_LC_FID[39]
VOICE_PDU_SVC_OPT[40]
VOICE_PDU_DST[41-43]
VOICE_PDU_SRC[44-46]
BURST_CRC[47-49]
VOICE_PDU_DST[41-43]
VOICE_PDU_SRC[44-46]
BURST_CRC[47-49] (Reed-Solomon(12,9) if the same as DMR burst, though sample data doesn't come out right)
???[50-53]
VOICE BURST A: 52 Bytes (0-51):
IPSC:
PACKET_TYPE[0]
PEER_ID[1-4]
IPSC_SEQ[5]
SRC_SUB[6-9]
DST_SUB[9-11]
CALL_TYPE[12]
CALL_CONTROL[13-16] (use a random number)
CALL_INFO[17]
RTP:
RTP_HEAD[18-19]
RTP_SEQ[20-21]
RTP_TIMESTMP[22-25]
RTP_SSID[26-29]
RTP PAYLOAD:
BURST_TYPE[30]
LENGTH[31] (bytes left after this one)
???[32]
AMBE_DATA[33-51]
VOICE BURST B-D and maybe F: 57 Bytes (0-56):
IPSC:
PACKET_TYPE[0]
PEER_ID[1-4]
IPSC_SEQ[5]
SRC_SUB[6-9]
DST_SUB[9-11]
CALL_TYPE[12]
CALL_CONTROL[13-16] (use a random number)
CALL_INFO[17]
RTP:
RTP_HEAD[18-19]
RTP_SEQ[20-21]
RTP_TIMESTMP[22-25]
RTP_SSID[26-29]
RTP PAYLOAD:
BURST_TYPE[30]
LENGTH[31] (bytes left after this one)
???[32]
AMBE_DATA[33-51]
???[52-56]
VOICE BURST E: 66 Bytes (0-65):
IPSC:
PACKET_TYPE[0]
PEER_ID[1-4]
IPSC_SEQ[5]
SRC_SUB[6-9]
DST_SUB[9-11]
CALL_TYPE[12]
CALL_CONTROL[13-16] (use a random number)
CALL_INFO[17]
RTP:
RTP_HEAD[18-19]
RTP_SEQ[20-21]
RTP_TIMESTMP[22-25]
RTP_SSID[26-29]
RTP PAYLOAD:
BURST_TYPE[30]
LENGTH[31] (bytes left after this one)
???[32]
AMBE_DATA[33-51]
???[52-64]
VOICE_PDU_DST[59-61]
VOICE_PDU_SRC[62-64]
???[65]
VOICE BURST F, Same as B-D???: 57 Bytes (0-56):
VOICE TERMINATOR: 54 Bytes (0-53)
IPSC:
PACKET_TYPE[0]
PEER_ID[1-4]
IPSC_SEQ[5]
SRC_SUB[6-9]
DST_SUB[9-11]
CALL_TYPE[12]
CALL_CONTROL[13-16] (use a random number)
CALL_INFO[17]
RTP:
RTP_HEAD[18-19]
RTP_SEQ[20-21]
RTP_TIMESTMP[22-25]
RTP_SSID[26-29]
RTP PAYLOAD:
BURST_TYPE[30]
RSSI_THRESH_PARITY[31]
LENGTH_TO_FOLLOW[32-33] (in words)
RSSI_STATUS[34]
SLOT_TYPE_SYNC[35]
DATA_SIZE[36-37] Burst data length in bits; 96/8 = 12.. last 4 bytes ont part of Burst??
IPSC_DATA { [38] to (LENGTH_TO_FOLLOW *2)-4 }
FULL_LC_BYTE1[38] (PF, R, FLCO)
FULL_LC_FID[39]
VOICE_PDU_SVC_OPT[40]
VOICE_PDU_DST[41-43]
VOICE_PDU_SRC[44-46]
BURST_CRC[47-49] (Reed-Solomon(12,9) if the same as DMR burst, though sample data doesn't come out right)
???[50-53]
A is a sync burst
B,C,D are the ame
E has extra data -- EMB?
F is the same length as B,C,D, but has a lot of zeros near the end.
Send a wakeup before starting a call (type 0x85
IPSC Sequence Number - incremented with each call made
RTP Header - Marker set for 1st voide header - 0x80DD, not for anything else 0x805D except terminator 0x80DE, which is the payload type and use is proprietary.
I've decoded the bits in the RTP header, it's going to be ok to use this recipe. Nothing else is going on.
RTP Sequence number -- increment with each packet
RTP timestamp - assumed currently to be 32 bit fixed point number 16bit.16bit seconds. Which would place these packets at 4.8ms apart... seems wrong.
LENGTH_TO_FOLLOW is in 16bit words

14
documents/voice_packets.txt Executable file
View File

@ -0,0 +1,14 @@
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.dd a9.97 1c.4d.ab.76 00.00.00.00 | 01 80 00.0a 80 0a 00.60 00 10 20 00.0c.30 2f.9b.e5 da.d4.5a 00 11 71 18
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.5d a9.98 1c.4d.ad.56 00.00.00.00 | 01 80 00.0a 80 0a 00.60 00 10 20 00.0c.30 2f.9b.e5 da.d4.5a 00 11 71 2b
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.5d a9.99 1c.4d.af.36 00.00.00.00 | 01 80 00.0a 80 0a 00.60 00 10 20 00.0c.30 2f.9b.e5 da.d4.5a 00 11 71 8a
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.5d a9.9a 1c.4d.b1.16 00.00.00.00 | 8a 14 40 f8.01.a9.9f.8c.e0.be.00.6a.67.e3.38.2f.80.1a.99.f8.ce.08
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.5d a9.9b 1c.4d.b2.f6 00.00.00.00 | 8a 19 06 f8.01.a9.9f.8c.e0.be.00.6a.67.e3.38.2f.80.1a.99.f8.ce.08 4e 0f 06 06 12
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.5d a9.9c 1c.4d.b4.d6 00.00.00.00 | 8a 19 06 f8.01.a9.9f.8c.e0.be.00.6a.67.e3.38.2f.80.1a.99.f8.ce.08 17 11 00 47 16
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.5d a9.9d 1c.4d.b6.b6 00.00.00.00 | 8a 19 06 f8.01.a9.9f.8c.e0.a6.00.ae.53.e9.34.ea.c0.92.e8.b4.fb.88 0c 03 18 1b 16
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.5d a9.9e 1c.4d.b8.96 00.00.00.00 | 8a 22 16 48.03.42.64.a4.e9.1a.02.d2.a1.63.1a.a9.80.9a.32.78.1b.a8 17 5a 0f 4e 00 10 20 00.0c.30 2f.9b.e5 14
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 20 | 80.5d a9.9f 1c.4d.ba.76 00.00.00.00 | 8a 19 06 98.02.24.99.01.0b.3a.02.6a.b3.59.36.4a.80.27.90.ec.68.58 00 00 00 00 10
80 00.04.c2.c0 cb 2f.9b.e5 00.0c.30 02 00.00.48.c2 60 | 80.5e a9.ca 1c.4e.0b.16 00.00.00.00 | 02 80 00.0a 80 0a 00.60 00 10 20 00.0c.30 2f.9b.e5 d5.db.55 00 12 74 5c

64
init.d/bridge Executable file
View File

@ -0,0 +1,64 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: Mototrbo_IPSC_bridging
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: DMRlink Bridge
# Description: Open Source IPSC bridging
### END INIT INFO
# Where is the directory containing DMRlink
DIR=/opt/dmrlink/bridge
# Filename of the python script
DAEMON=$DIR/bridge.py
# Daemon name
DAEMON_NAME=bridge
# Add any command line options for your daemon where
DAEMON_OPTS=""
# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=root
# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid
. /lib/lsb/init-functions
do_start () {
log_daemon_msg "Starting system $DAEMON_NAME daemon"
start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS
log_end_msg $?
}
do_stop () {
log_daemon_msg "Stopping system $DAEMON_NAME daemon"
start-stop-daemon --stop --pidfile $PIDFILE --remove-pidfile --retry 10
log_end_msg $?
}
case "$1" in
start|stop)
do_${1}
;;
restart|reload|force-reload)
do_stop
do_start
;;
status)
status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
;;
*)
echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
exit 1
;;
esac
exit 0

0
ipsc/.gitignore vendored Normal file → Executable file
View File

0
ipsc/__init__.py Normal file → Executable file
View File

241
ipsc/dmrlink_config.py Executable file
View File

@ -0,0 +1,241 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016-2018 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
import ConfigParser
import sys
from socket import getaddrinfo, IPPROTO_UDP
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2016-2018 Cortney T. Buffington, N0MJS and the K0USY Group'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
def get_address(_config):
ipv4 = ''
ipv6 = ''
socket_info = getaddrinfo(_config, None, 0, 0, IPPROTO_UDP)
for item in socket_info:
if item[0] == 2:
ipv4 = item[4][0]
elif item[0] == 30:
ipv6 = item[4][0]
if ipv4:
return ipv4
if ipv6:
return ipv6
return 'invalid address'
def build_config(_config_file):
config = ConfigParser.ConfigParser()
if not config.read(_config_file):
sys.exit('Configuration file \''+_config_file+'\' is not a valid configuration file! Exiting...')
CONFIG = {}
CONFIG['GLOBAL'] = {}
CONFIG['REPORTS'] = {}
CONFIG['LOGGER'] = {}
CONFIG['ALIASES'] = {}
CONFIG['SYSTEMS'] = {}
try:
for section in config.sections():
if section == 'GLOBAL':
CONFIG['GLOBAL'].update({
'PATH': config.get(section, 'PATH')
})
elif section == 'REPORTS':
CONFIG['REPORTS'].update({
'REPORT_NETWORKS': config.get(section, 'REPORT_NETWORKS'),
'REPORT_RCM': config.get(section, 'REPORT_RCM'),
'REPORT_INTERVAL': config.getint(section, 'REPORT_INTERVAL'),
'REPORT_PORT': config.get(section, 'REPORT_PORT'),
'REPORT_CLIENTS': config.get(section, 'REPORT_CLIENTS').split(','),
'PRINT_PEERS_INC_MODE': config.getboolean(section, 'PRINT_PEERS_INC_MODE'),
'PRINT_PEERS_INC_FLAGS': config.getboolean(section, 'PRINT_PEERS_INC_FLAGS')
})
if CONFIG['REPORTS']['REPORT_PORT']:
CONFIG['REPORTS']['REPORT_PORT'] = int(CONFIG['REPORTS']['REPORT_PORT'])
if CONFIG['REPORTS']['REPORT_RCM']:
CONFIG['REPORTS']['REPORT_RCM'] = bool(CONFIG['REPORTS']['REPORT_RCM'])
elif section == 'LOGGER':
CONFIG['LOGGER'].update({
'LOG_FILE': config.get(section, 'LOG_FILE'),
'LOG_HANDLERS': config.get(section, 'LOG_HANDLERS'),
'LOG_LEVEL': config.get(section, 'LOG_LEVEL'),
'LOG_NAME': config.get(section, 'LOG_NAME')
})
elif section == 'ALIASES':
CONFIG['ALIASES'].update({
'TRY_DOWNLOAD': config.getboolean(section, 'TRY_DOWNLOAD'),
'PATH': config.get(section, 'PATH'),
'PEER_FILE': config.get(section, 'PEER_FILE'),
'SUBSCRIBER_FILE': config.get(section, 'SUBSCRIBER_FILE'),
'TGID_FILE': config.get(section, 'TGID_FILE'),
'LOCAL_FILE': config.get(section, 'LOCAL_FILE'),
'PEER_URL': config.get(section, 'PEER_URL'),
'SUBSCRIBER_URL': config.get(section, 'SUBSCRIBER_URL'),
'STALE_TIME': config.getint(section, 'STALE_DAYS') * 86400,
})
elif config.getboolean(section, 'ENABLED'):
CONFIG['SYSTEMS'].update({section: {'LOCAL': {}, 'MASTER': {}, 'PEERS': {}}})
CONFIG['SYSTEMS'][section]['LOCAL'].update({
# In case we want to keep config, but not actually connect to the network
'ENABLED': config.getboolean(section, 'ENABLED'),
# These items are used to create the MODE byte
'PEER_OPER': config.getboolean(section, 'PEER_OPER'),
'IPSC_MODE': config.get(section, 'IPSC_MODE'),
'TS1_LINK': config.getboolean(section, 'TS1_LINK'),
'TS2_LINK': config.getboolean(section, 'TS2_LINK'),
'MODE': '',
# These items are used to create the multi-byte FLAGS field
'AUTH_ENABLED': config.getboolean(section, 'AUTH_ENABLED'),
'CSBK_CALL': config.getboolean(section, 'CSBK_CALL'),
'RCM': config.getboolean(section, 'RCM'),
'CON_APP': config.getboolean(section, 'CON_APP'),
'XNL_CALL': config.getboolean(section, 'XNL_CALL'),
'XNL_MASTER': config.getboolean(section, 'XNL_MASTER'),
'DATA_CALL': config.getboolean(section, 'DATA_CALL'),
'VOICE_CALL': config.getboolean(section, 'VOICE_CALL'),
'MASTER_PEER': config.getboolean(section, 'MASTER_PEER'),
'FLAGS': '',
# Things we need to know to connect and be a peer in this IPSC
'RADIO_ID': hex(int(config.get(section, 'RADIO_ID')))[2:].rjust(8,'0').decode('hex'),
'IP': config.get(section, 'IP'),
'PORT': config.getint(section, 'PORT'),
'ALIVE_TIMER': config.getint(section, 'ALIVE_TIMER'),
'MAX_MISSED': config.getint(section, 'MAX_MISSED'),
'AUTH_KEY': (config.get(section, 'AUTH_KEY').rjust(40,'0')).decode('hex'),
'GROUP_HANGTIME': config.getint(section, 'GROUP_HANGTIME'),
'NUM_PEERS': 0,
})
# Master means things we need to know about the master peer of the network
CONFIG['SYSTEMS'][section]['MASTER'].update({
'RADIO_ID': '\x00\x00\x00\x00',
'MODE': '\x00',
'MODE_DECODE': '',
'FLAGS': '\x00\x00\x00\x00',
'FLAGS_DECODE': '',
'STATUS': {
'CONNECTED': False,
'PEER_LIST': False,
'KEEP_ALIVES_SENT': 0,
'KEEP_ALIVES_MISSED': 0,
'KEEP_ALIVES_OUTSTANDING': 0,
'KEEP_ALIVES_RECEIVED': 0,
'KEEP_ALIVE_RX_TIME': 0
},
'IP': '',
'PORT': ''
})
if not CONFIG['SYSTEMS'][section]['LOCAL']['MASTER_PEER']:
CONFIG['SYSTEMS'][section]['MASTER'].update({
'IP': get_address(config.get(section, 'MASTER_IP')),
'PORT': config.getint(section, 'MASTER_PORT')
})
# Temporary locations for building MODE and FLAG data
MODE_BYTE = 0
FLAG_1 = 0
FLAG_2 = 0
# Construct and store the MODE field
if CONFIG['SYSTEMS'][section]['LOCAL']['PEER_OPER']:
MODE_BYTE |= 1 << 6
if CONFIG['SYSTEMS'][section]['LOCAL']['IPSC_MODE'] == 'ANALOG':
MODE_BYTE |= 1 << 4
elif CONFIG['SYSTEMS'][section]['LOCAL']['IPSC_MODE'] == 'DIGITAL':
MODE_BYTE |= 1 << 5
if CONFIG['SYSTEMS'][section]['LOCAL']['TS1_LINK']:
MODE_BYTE |= 1 << 3
else:
MODE_BYTE |= 1 << 2
if CONFIG['SYSTEMS'][section]['LOCAL']['TS2_LINK']:
MODE_BYTE |= 1 << 1
else:
MODE_BYTE |= 1 << 0
CONFIG['SYSTEMS'][section]['LOCAL']['MODE'] = chr(MODE_BYTE)
# Construct and store the FLAGS field
if CONFIG['SYSTEMS'][section]['LOCAL']['CSBK_CALL']:
FLAG_1 |= 1 << 7
if CONFIG['SYSTEMS'][section]['LOCAL']['RCM']:
FLAG_1 |= 1 << 6
if CONFIG['SYSTEMS'][section]['LOCAL']['CON_APP']:
FLAG_1 |= 1 << 5
if CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL']:
FLAG_2 |= 1 << 7
if CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL'] and CONFIG['SYSTEMS'][section]['LOCAL']['XNL_MASTER']:
FLAG_2 |= 1 << 6
elif CONFIG['SYSTEMS'][section]['LOCAL']['XNL_CALL'] and not CONFIG['SYSTEMS'][section]['LOCAL']['XNL_MASTER']:
FLAG_2 |= 1 << 5
if CONFIG['SYSTEMS'][section]['LOCAL']['AUTH_ENABLED']:
FLAG_2 |= 1 << 4
if CONFIG['SYSTEMS'][section]['LOCAL']['DATA_CALL']:
FLAG_2 |= 1 << 3
if CONFIG['SYSTEMS'][section]['LOCAL']['VOICE_CALL']:
FLAG_2 |= 1 << 2
if CONFIG['SYSTEMS'][section]['LOCAL']['MASTER_PEER']:
FLAG_2 |= 1 << 0
CONFIG['SYSTEMS'][section]['LOCAL']['FLAGS'] = '\x00\x00'+chr(FLAG_1)+chr(FLAG_2)
except ConfigParser.Error, err:
print(err)
sys.exit('Could not parse configuration file, exiting...')
return CONFIG
# Used to run this file direclty and print the config,
# which might be useful for debugging
if __name__ == '__main__':
import sys
import os
import argparse
from pprint import pprint
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CONFIG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
cli_args = parser.parse_args()
# Ensure we have a path for the config file, if one wasn't specified, then use the execution directory
if not cli_args.CONFIG_FILE:
cli_args.CONFIG_FILE = os.path.dirname(os.path.abspath(__file__))+'/../dmrlink.cfg'
pprint(build_config(cli_args.CONFIG_FILE))

87
ipsc/dmrlink_log.py Executable file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
import logging
from logging.config import dictConfig
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2016 Cortney T. Buffington, N0MJS and the K0USY Group'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
def config_logging(_logger):
dictConfig({
'version': 1,
'disable_existing_loggers': False,
'filters': {
},
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
'timed': {
'format': '%(levelname)s %(asctime)s %(message)s'
},
'simple': {
'format': '%(levelname)s %(message)s'
},
'syslog': {
'format': '%(name)s (%(process)d): %(levelname)s %(message)s'
}
},
'handlers': {
'null': {
'class': 'logging.NullHandler'
},
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'console-timed': {
'class': 'logging.StreamHandler',
'formatter': 'timed'
},
'file': {
'class': 'logging.FileHandler',
'formatter': 'simple',
'filename': _logger['LOG_FILE'],
},
'file-timed': {
'class': 'logging.FileHandler',
'formatter': 'timed',
'filename': _logger['LOG_FILE'],
},
'syslog': {
'class': 'logging.handlers.SysLogHandler',
'formatter': 'syslog',
}
},
'loggers': {
_logger['LOG_NAME']: {
'handlers': _logger['LOG_HANDLERS'].split(','),
'level': _logger['LOG_LEVEL'],
'propagate': True,
}
}
})
return logging.getLogger(_logger['LOG_NAME'])

69
ipsc/ipsc_message_types.py → ipsc/ipsc_const.py Normal file → Executable file
View File

@ -1,4 +1,4 @@
# Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
# Copyright (c) 2013 - 2015 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
#
# This work is licensed under the Creative Commons Attribution-ShareAlike
# 3.0 Unported License.To view a copy of this license, visit
@ -8,6 +8,7 @@
# Known IPSC Message Types
CALL_CONFIRMATION = '\x05' # Confirmation FROM the recipient of a confirmed call.
TXT_MESSAGE_ACK = '\x54' # Doesn't seem to mean success, though. This code is sent success or failure
CALL_MON_STATUS = '\x61' # |
CALL_MON_RPT = '\x62' # | Exact meaning unknown
CALL_MON_NACK = '\x63' # |
@ -17,6 +18,7 @@ PVT_VOICE = '\x81'
GROUP_DATA = '\x83'
PVT_DATA = '\x84'
RPT_WAKE_UP = '\x85' # Similar to OTA DMR "wake up"
UNKNOWN_COLLISION = '\x86' # Seen when two dmrlinks try to transmit at once
MASTER_REG_REQ = '\x90' # FROM peer TO master
MASTER_REG_REPLY = '\x91' # FROM master TO peer
PEER_LIST_REQ = '\x92' # From peer TO master
@ -43,6 +45,14 @@ IPSC_VER_22 = '\x04'
# Link Type Values - assumed that cap+, etc. are different, this is all I can confirm
LINK_TYPE_IPSC = '\x04'
# Burst Data Types
BURST_DATA_TYPE = {
'VOICE_HEAD': '\x01',
'VOICE_TERM': '\x02',
'SLOT1_VOICE': '\x0A',
'SLOT2_VOICE': '\x8A'
}
# IPSC Version and Link Type are Used for a 4-byte version field in registration packets
IPSC_VER = LINK_TYPE_IPSC + IPSC_VER_17 + LINK_TYPE_IPSC + IPSC_VER_16
@ -74,14 +84,23 @@ TYPE = {
'\x30': 'Private Data Set-Up',
'\x31': 'Group Data Set-Up',
'\x32': 'Private CSBK Set-Up',
'\x47': 'Radio Check Request',
'\x45': 'Call Alert',
'\x47': 'Radio Check Request',
'\x48': 'Radio Check Success',
'\x49': 'Radio Disable Request',
'\x4A': 'Radio Disable Received',
'\x4B': 'Radio Enable Request',
'\x4C': 'Radio Enable Received',
'\x4D': 'Remote Monitor Request',
'\x4E': 'Remote Monitor Request Received', #(doesn't mean it was successful)
'\x4D': 'Remote Monitor Request',
'\x4F': 'Group Voice',
'\x50': 'Private Voice',
'\x51': 'Group Data',
'\x52': 'Private Data',
'\x53': 'All Call'
'\x53': 'All Call',
'\x54': 'Message ACK/Failure', #text message acknowledgement, but doesn't mean it was successful - it gives the same code if it worked or failed...
'\x84': 'ARS/GPS?' # Not yet clear, seen by a user running ARS & GPS
}
SEC = {
@ -109,41 +128,11 @@ REPEAT = {
}
# Conditions for accepting certain types of messages... the cornerstone of a secure IPSC system :)
'''
REQ_VALID_PEER = [
PEER_REG_REQ,
PEER_REG_REPLY
]
# DMR IPSC Contants (in the RTP Payload)
REQ_VALID_MASTER = [
MASTER_REG_REQ,
MASTER_REG_REPLY
]
REQ_MASTER_CONNECTED = [
CALL_MON_STATUS,
CALL_MON_RPT,
CALL_MON_NACK,
XCMP_XNL,
GROUP_VOICE,
PVT_VOICE,
GROUP_DATA,
GROUP_VOICE,
PVT_DATA,
RPT_WAKE_UP,
MASTER_ALIVE_REQ,
MASTER_ALIVE_REPLY,
DE_REG_REQ,
DE_REG_REPLY
]
REQ_PEER_CONNECTED = [
PEER_ALIVE_REQ,
PEER_ALIVE_REPLY
]
REQ_VALID_MASTER_OR_PEER = [
REQ_VALID_PEER, REQ_VALID_MASTER
]
'''
BURST_DATA_TYPE = {
'VOICE_HEAD': '\x01',
'VOICE_TERM': '\x02',
'SLOT1_VOICE': '\x0A',
'SLOT2_VOICE': '\x8A'
}

31
ipsc/ipsc_mask.py Normal file → Executable file
View File

@ -6,6 +6,11 @@
# Creative Commons, 444 Castro Street, Suite 900, Mountain View,
# California, 94041, USA.
# MASKS FOR IPSC, RTP AND THE RTP PAYLOAD (DMR FRAME + FRIENDS) ARE LOCATED
# IN THIS FILE IN THIS ORDER: IPSC, RTP, PAYLOAD
# IPSC MASK VALUES
#
# LINKING STATUS:
# Byte 1 - BIT FLAGS:
# xx.. .... = Peer Operational (01 only known valid value)
@ -53,11 +58,33 @@ VOICE_CALL_MSK = 0b00000100
MSTR_PEER_MSK = 0b00000001
# TIMESLOT CALL & STATUS BYTE
# Byte 17 of Group and Private Voice/Data Packets
# ..x.. ....TS Value (0=TS1, 1=TS2)
# .x... ....TS In Progress/End (0=In Progress, 1=End)
# Possible values: 0x00=TS1, 0x20=TS2, 0x40=TS1 End, 0x60=TS2 End
# MASK VALUE:
END_MSK = 0b01000000
TS_CALL_MSK = 0b00100000
TS_CALL_MSK = 0b00100000
# RTP MASK VALUES
# Bytes 1 and 2 of the RTP header are bit-fields, the rest
# are at least one byte long, and do not need masked
# Byte 1
RTP_VER_MSK = 0b11000000
RTP_PAD_MSK = 0b00100000
RTP_EXT_MSK = 0b00010000
RTP_CSIC_MSK = 0b00001111
# Byte 2
RTP_MRKR_MSK = 0b10000000
RTP_PAY_TYPE_MSK = 0b01111111
# RTP PAYLOAD (DMR FRAME + FRIENDS) MASK VALUES
# This one is tricky. The DMR Frame contents are here
# and re-ordered from their position in the original DMR
# frame format. There are also some other friends in here
# that Motorla added.
#

31
ipsc/reporting_const.py Executable file
View File

@ -0,0 +1,31 @@
###############################################################################
# Copyright (C) 201t Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# Opcodes for the network-based reporting protocol
REPORT_OPCODES = {
'CONFIG_REQ': '\x00',
'CONFIG_SND': '\x01',
'BRIDGE_REQ': '\x02',
'BRIDGE_SND': '\x03',
'CONFIG_UPD': '\x04',
'BRIDGE_UPD': '\x05',
'LINK_EVENT': '\x06',
'BRDG_EVENT': '\x07',
'RCM_SND': '\x08'
}

90
log.py
View File

@ -1,90 +0,0 @@
#!/usr/bin/env python
#
# This work is licensed under the Creative Commons Attribution-ShareAlike
# 3.0 Unported License.To view a copy of this license, visit
# http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
# Creative Commons, 444 Castro Street, Suite 900, Mountain View,
# California, 94041, USA.
# This is a sample application that snoops voice traffic to log calls
from __future__ import print_function
from twisted.internet import reactor
from binascii import b2a_hex as h
import time
from dmrlink import IPSC, NETWORK, networks, get_info, int_id, subscriber_ids, peer_ids, talkgroup_ids, logger
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK, Dave K, and he who wishes not to be named'
__license__ = 'Creative Commons Attribution-ShareAlike 3.0 Unported'
__version__ = '0.2a'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'Production'
class logIPSC(IPSC):
def __init__(self, *args, **kwargs):
IPSC.__init__(self, *args, **kwargs)
self.ACTIVE_CALLS = []
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
def group_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
# _log = logger.debug
if (_ts not in self.ACTIVE_CALLS) or _end:
_time = time.strftime('%m/%d/%y %H:%M:%S')
_dst_sub = get_info(int_id(_dst_sub), talkgroup_ids)
_peerid = get_info(int_id(_peerid), peer_ids)
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
if not _end: self.ACTIVE_CALLS.append(_ts)
if _end: self.ACTIVE_CALLS.remove(_ts)
if _ts: _ts = 2
else: _ts = 1
if _end: _end = 'END'
else: _end = 'START'
print('{} ({}) Call {} Group Voice: \n\tIPSC Source:\t{}\n\tSubscriber:\t{}\n\tDestination:\t{}\n\tTimeslot\t{}' .format(_time, _network, _end, _peerid, _src_sub, _dst_sub, _ts))
def private_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
# _log = logger.debug
if (_ts not in self.ACTIVE_CALLS) or _end:
_time = time.strftime('%m/%d/%y %H:%M:%S')
_dst_sub = get_info(int_id(_dst_sub), subscriber_ids)
_peerid = get_info(int_id(_peerid), peer_ids)
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
if not _end: self.ACTIVE_CALLS.append(_ts)
if _end: self.ACTIVE_CALLS.remove(_ts)
if _ts: _ts = 2
else: _ts = 1
if _end: _end = 'END'
else: _end = 'START'
print('{} ({}) Call {} Private Voice: \n\tIPSC Source:\t{}\n\tSubscriber:\t{}\n\tDestination:\t{}\n\tTimeslot\t{}' .format(_time, _network, _end, _peerid, _src_sub, _dst_sub, _ts))
def group_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
_dst_sub = get_info(int_id(_dst_sub), talkgroup_ids)
_peerid = get_info(int_id(_peerid), peer_ids)
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
print('({}) Group Data Packet Received From: {}' .format(_network, _src_sub))
def private_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
_dst_sub = get_info(int_id(_dst_sub), subscriber_ids)
_peerid = get_info(int_id(_peerid), peer_ids)
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
print('({}) Private Data Packet Received From: {} To: {}' .format(_network, _src_sub, _dst_sub))
if __name__ == '__main__':
logger.info('DMRlink \'log.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
for ipsc_network in NETWORK:
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
networks[ipsc_network] = logIPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()

163
mk-dmrlink Executable file
View File

@ -0,0 +1,163 @@
#! /bin/bash
PREFIX=/opt/dmrlink
echo "DMRlink will be installed in: $PREFIX"
currentdir=`pwd`
echo "Current working directory is: $currentdir"
echo ""
#################################################
# #
# Install DMRlink in seperate directories by #
# Application #
#################################################
# Install the required support programs
distro=$(lsb_release -i | awk -F":" '{ gsub(/^[ \t]+/, "", $2); print $2 }')
release=$(lsb_release -r | awk -F":" '{ gsub(/^[ \t]+/, "", $2); print $2 }')
echo "Current Linux distribution is: $distro $release"
if [[ "$distro" =~ ^(CentOS|Fedora|openSUSE|)$ ]]; then
echo "$distro uses yum"
yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(echo $release | awk -F"." '{print $1}').noarch.rpm
yum install -y gcc gcc-c++ glibc-devel make
yum install -y unzip
yum install -y python-devel
yum install -y python-pip
yum install -y python-twisted
# pip install bitstring
# pip install bitarray
else
echo "$distro uses apt"
apt-get install -y build-essential
apt-get install -y unzip
apt-get install -y python-dev
apt-get install -y python-pip
apt-get install -y python-twisted
# pip install bitstring
# pip install bitarray
fi
# Install dmr_utils with pip install
pip install dmr_utils
###############################################################################
# Following lines should be removed due to the pip install method for dmr_utils
#cd /opt
#if [ ! -d /opt/dmr_utils ]; then
# git clone https://github.com/n0mjs710/dmr_utils.git
#fi
#cd dmr_utils/
#git pull
#pip install .
###############################################################################
echo "Required programs installed, continuing"
# To allow multiple instances of DMRlink to run
# You need multiple ipsc directories, dmrlink.py and dmrlink.cfg
# The needed files are copied to /opt/dmrlink
# Make needed directories
mkdir -p $PREFIX/confbridge/
mkdir -p $PREFIX/playback/
mkdir -p $PREFIX/proxy/
mkdir -p $PREFIX/samples
mkdir -p /var/log/dmrlink
cd $PREFIX
# Put common files in /opt/dmrlink
# cp $currentdir/peer_ids.csv /opt/dmrlink
# cp $currentdir/subscriber_ids.csv /opt/dmrlink
# cp $currentdir/talkgroup_ids.csv /opt/dmrlink
# Copy ipsc directory into each app directory
cp -rf $currentdir/ipsc/ $PREFIX/confbridge/
cp -rf $currentdir/ipsc/ $PREFIX/playback/
cp -rf $currentdir/ipsc/ $PREFIX/proxy/
# Put a copy of the samples together for easy reference
#cp $currentdir/bridge_rules_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/confbridge_rules_SAMPLE.py $PREFIX/samples
cp $currentdir/dmrlink_SAMPLE.cfg $PREFIX/samples
#cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/samples
cp $currentdir/playback_config_SAMPLE.py $PREFIX/samples
#cp $currentdir/ambe_audio.cfg /opt/dmrlink/samples
cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/samples
# Put the doc together for easy reference
cp -rf $currentdir/documents $PREFIX
cp $currentdir/LICENSE.txt $PREFIX/documents
cp $currentdir/requirements.txt $PREFIX/documents
#cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/documents
# ambe_audio
#cp $currentdir/dmrlink.py /opt/dmrlink/ambe_audio/
#cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/ambe_audio/
#
#cp $currentdir/ambe_audio.cfg /opt/dmrlink/ambe_audio/
#cp $currentdir/ambe_audio.py /opt/dmrlink/ambe_audio/
#cp $currentdir/ambe_audio_commands.txt /opt/dmrlink/ambe_audio/
#cp $currentdir/template.bin /opt/dmrlink/ambe_audio/
# Bridge app
#cp $currentdir/dmrlink.py /opt/dmrlink/bridge/
#cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/bridge/
#
#cp $currentdir/bridge.py /opt/dmrlink/bridge/
#cp $currentdir/bridge_rules_SAMPLE.py /opt/dmrlink/bridge/
#cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/bridge/
#cp $currentdir/sub_acl_SAMPLE.py /opt/dmrlink/bridge/
# ConfBridge app
cp $currentdir/dmrlink.py $PREFIX/confbridge/
cp $currentdir/dmrlink_SAMPLE.cfg $PREFIX/confbridge/
#
cp $currentdir/confbridge.py $PREFIX/confbridge/
cp $currentdir/confbridge_rules_SAMPLE.py $PREFIX/confbridge/
#cp $currentdir/known_bridges_SAMPLE.py /opt/dmrlink/confbridge/
cp $currentdir/sub_acl_SAMPLE.py $PREFIX/confbridge/
# Log app
#cp $currentdir/dmrlink.py /opt/dmrlink/log/
#cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/log/
#
#cp $currentdir/log.py /opt/dmrlink/log/
# Playback (Parrot)
cp $currentdir/dmrlink.py $PREFIX/playback/
cp $currentdir/dmrlink_SAMPLE.cfg $PREFIX/playback/
#
cp $currentdir/playback.py $PREFIX/playback/
cp $currentdir/playback_config_SAMPLE.py $PREFIX/playback/
# Play Group app
#cp $currentdir/dmrlink.py /opt/dmrlink/play_group/
#cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/play_group/
#
#cp $currentdir/play_group.py /opt/dmrlink/play_group/
# proxy app
cp $currentdir/dmrlink.py $PREFIX/proxy/
cp $currentdir/dmrlink_SAMPLE.cfg $PREFIX/proxy/
#
cp $currentdir/proxy.py $PREFIX/proxy/
#cp $currentdir/known_bridges_SAMPLE.py $PREFIX/proxy/
cp $currentdir/sub_acl_SAMPLE.py $PREFIX/proxy/
# rcm app
#cp $currentdir/dmrlink.py /opt/dmrlink/rcm/
#cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/rcm/
#
#cp $currentdir/rcm_db_log.py /opt/dmrlink/rcm/
#cp $currentdir/rcm.py /opt/dmrlink/rcm/
# record app
#cp $currentdir/dmrlink.py /opt/dmrlink/record/
#cp $currentdir/dmrlink_SAMPLE.cfg /opt/dmrlink/record/
#
#cp $currentdir/record.py /opt/dmrlink/record/

File diff suppressed because one or more lines are too long

View File

@ -1,28 +1,39 @@
#!/usr/bin/env python
#
# This work is licensed under the Creative Commons Attribution-ShareAlike
# 3.0 Unported License.To view a copy of this license, visit
# http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
# Creative Commons, 444 Castro Street, Suite 900, Mountain View,
# California, 94041, USA.
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application that "records" and replays transmissions for testing.
from __future__ import print_function
from twisted.internet import reactor
from binascii import b2a_hex as h
from binascii import b2a_hex as ahex
import sys, time
from dmrlink import IPSC, NETWORK, networks, logger, dmr_nat, int_id, send_to_ipsc, hex_str_3
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, build_aliases, config_reports
from dmr_utils.utils import int_id, hex_str_3
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave K; and he who wishes not to be named'
__license__ = 'Creative Commons Attribution-ShareAlike 3.0 Unported'
__version__ = '0.1b'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'pre-alpha'
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski, KD8EYF'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
try:
@ -30,44 +41,131 @@ try:
except ImportError:
sys.exit('Configuration file not found or invalid')
HEX_TGID = hex_str_3(TGID)
HEX_TGID = hex_str_3(TGID)
HEX_SUB = hex_str_3(SUB)
BOGUS_SUB = '\xFF\xFF\xFF'
class playbackIPSC(IPSC):
def __init__(self, *args, **kwargs):
IPSC.__init__(self, *args, **kwargs)
def __init__(self, _name, _config, _logger, _report):
IPSC.__init__(self, _name, _config, _logger, _report)
self.CALL_DATA = []
if GROUP_SRC_SUB:
self._logger.info('Playback: USING SUBSCRIBER ID: %s FOR GROUP REPEAT', GROUP_SRC_SUB)
self.GROUP_SRC_SUB = hex_str_3(GROUP_SRC_SUB)
if GROUP_REPEAT:
self._logger.info('Playback: GROUP REPEAT ENABLED')
if PRIVATE_REPEAT:
self._logger.info('Playback: PRIVATE REPEAT ENABLED')
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
if HEX_TGID == _dst_sub and TS == _ts:
if not _end:
if not self.CALL_DATA:
logger.info('(%s) Receiving transmission to be played back from subscriber: %s', _network, int_id(_src_sub))
_tmp_data = _data
#_tmp_data = dmr_nat(_data, _src_sub, NETWORK[_network]['LOCAL']['RADIO_ID'])
self.CALL_DATA.append(_tmp_data)
if _end:
self.CALL_DATA.append(_data)
time.sleep(2)
logger.info('(%s) Playing back transmission from subscriber: %s', _network, int_id(_src_sub))
for i in self.CALL_DATA:
_tmp_data = i
_tmp_data = _tmp_data.replace(_peerid, NETWORK[_network]['LOCAL']['RADIO_ID'])
_tmp_data = self.hashed_packet(NETWORK[_network]['LOCAL']['AUTH_KEY'], _tmp_data)
# Send the packet to all peers in the target IPSC
send_to_ipsc(_network, _tmp_data)
time.sleep(0.06)
self.CALL_DATA = []
if GROUP_REPEAT:
def group_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
if HEX_TGID == _dst_sub and _ts in GROUP_TS:
if not _end:
if not self.CALL_DATA:
self._logger.info('(%s) Receiving transmission to be played back from subscriber: %s', self._system, int_id(_src_sub))
_tmp_data = _data
#_tmp_data = dmr_nat(_data, _src_sub, self._config['LOCAL']['RADIO_ID'])
self.CALL_DATA.append(_tmp_data)
if _end:
self.CALL_DATA.append(_data)
time.sleep(2)
self._logger.info('(%s) Playing back transmission from subscriber: %s', self._system, int_id(_src_sub))
for i in self.CALL_DATA:
_tmp_data = i
_tmp_data = _tmp_data.replace(_peerid, self._config['LOCAL']['RADIO_ID'])
if GROUP_SRC_SUB:
_tmp_data = _tmp_data.replace(_src_sub, self.GROUP_SRC_SUB)
# Send the packet to all peers in the target IPSC
self.send_to_ipsc(_tmp_data)
time.sleep(0.06)
self.CALL_DATA = []
if PRIVATE_REPEAT:
def private_voice(self, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
if HEX_SUB == _dst_sub and _ts in PRIVATE_TS:
if not _end:
if not self.CALL_DATA:
self._logger.info('(%s) Receiving transmission to be played back from subscriber: %s, to subscriber: %s', self._system, int_id(_src_sub), int_id(_dst_sub))
_tmp_data = _data
self.CALL_DATA.append(_tmp_data)
if _end:
self.CALL_DATA.append(_data)
time.sleep(1)
self._logger.info('(%s) Playing back transmission from subscriber: %s, to subscriber %s', self._system, int_id(_src_sub), int_id(_dst_sub))
_orig_src = _src_sub
_orig_dst = _dst_sub
for i in self.CALL_DATA:
_tmp_data = i
_tmp_data = _tmp_data.replace(_peerid, self._config['LOCAL']['RADIO_ID'])
_tmp_data = _tmp_data.replace(_dst_sub, BOGUS_SUB)
_tmp_data = _tmp_data.replace(_src_sub, _orig_dst)
_tmp_data = _tmp_data.replace(BOGUS_SUB, _orig_src)
# Send the packet to all peers in the target IPSC
self.send_to_ipsc(_tmp_data)
time.sleep(0.06)
self.CALL_DATA = []
if __name__ == '__main__':
logger.info('DMRlink \'playback.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
for ipsc_network in NETWORK:
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
networks[ipsc_network] = playbackIPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, reportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGRUED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, playbackIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

View File

@ -1,7 +1,35 @@
#!/usr/bin/env python
#
# THESE ARE THE THINGS THAT YOU NEED TO CONFIGURE TO USE playback.py
# ENABLE GROUP VOICE PLAYBACK?
# Values may be True or False
GROUP_REPEAT = True
# TGID TO LISTEN FOR AND REPEAT ON
TGID = 10
# TIMESLOT TO LISTEN FOR AND REPEAT ON
TS = 0
# Integer for the Talkgroup ID
TGID = 12345
# TIMESLOT TO LISTEN FOR GROUP VOICE AND REPEAT
# This is a tuple of timeslots to listen to. Note, if there's only
# one, you still have to use the parenthesis and comma. Just
# deal with it, or make it better. TS1 = 1, TS2 = 2.
GROUP_TS = (2,)
# ALTERNATE SOURCE SUBSCRIBER ID FOR REPEATED TRANSMISSION
# Some folks have radios that don't respond to their own subscriber
# IDs. Some just don't want to have the playback come from the same
# subscriber ID. If this variable is set to something, it will
# be used as the source subscriber for playback.
# SET TO 0 TO NOT USE THIS FEATURE!!!
GROUP_SRC_SUB = 0
# ENABLE PRIVATE VOICE PLAYBACK?
# Values may be True or False
PRIVATE_REPEAT = True
# SUBSCRIBER ID TO LISTEN FOR AND REPEAT ON
# Integer for the Subscriber (Radio) ID
SUB = 12345
# TIMESLOT TO LISTEN FOR PRIVATE VOICE AND REPEAT
# This is a tuple of timeslots to listen to. Note, if there's only
# one, you still have to use the parenthesis and comma. Just
# deal with it, or make it better. TS1 = 1, TS2 = 2.
PRIVATE_TS = (1,2)

252
proxy.py Executable file
View File

@ -0,0 +1,252 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2016 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
# This is a sample application to bridge traffic between IPSC systems. it uses
# one required (bridge_rules.py) and one optional (known_bridges.py) additional
# configuration files. Both files have their own documentation for use.
#
# "bridge_rules" contains the IPSC network, Timeslot and TGID matching rules to
# determine which voice calls are bridged between IPSC systems and which are
# not.
#
# "known_bridges" contains DMR radio ID numbers of known bridges. This file is
# used when you want bridge.py to be "polite" or serve as a backup bridge. If
# a known bridge exists in either a source OR target IPSC network, then no
# bridging between those IPSC systems will take place. This behavior is
# dynamic and updates each keep-alive interval (main configuration file).
# For faster failover, configure a short keep-alive time and a low number of
# missed keep-alives before timout. I recommend 5 sec keep-alive and 3 missed.
# That gives a worst-case scenario of 15 seconds to fail over. Recovery will
# typically happen with a single "blip" in the transmission up to about 5
# seconds.
#
# While this file is listed as Beta status, K0USY Group depends on this code
# for the bridigng of it's many repeaters. We consider it reliable, but you
# get what you pay for... as usual, no guarantees.
#
# Use to make test strings: #print('PKT:', "\\x".join("{:02x}".format(ord(c)) for c in _data))
from __future__ import print_function
from twisted.internet import reactor
from twisted.internet import task
from binascii import b2a_hex as ahex
from time import time
from importlib import import_module
import sys
from dmr_utils.utils import hex_str_3, hex_str_4, int_id
from dmrlink import IPSC, mk_ipsc_systems, systems, reportFactory, REPORT_OPCODES, build_aliases, config_reports
from ipsc.ipsc_const import BURST_DATA_TYPE
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2017 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK; Dave Kierzkowski, KD8EYF; Steve Zingman, N4IRS; Mike Zingman, N4IRR'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
# Import subscriber ACL
# ACL may be a single list of subscriber IDs
# Global action is to allow or deny them. Multiple lists with different actions and ranges
# are not yet implemented.
def build_acl(_sub_acl):
try:
logger.info('ACL file found, importing entries. This will take about 1.5 seconds per 1 million IDs')
acl_file = import_module(_sub_acl)
sections = acl_file.ACL.split(':')
ACL_ACTION = sections[0]
entries_str = sections[1]
ACL = set()
for entry in entries_str.split(','):
if '-' in entry:
start,end = entry.split('-')
start,end = int(start), int(end)
for id in range(start, end+1):
ACL.add(hex_str_3(id))
else:
id = int(entry)
ACL.add(hex_str_3(id))
logger.info('ACL loaded: action "{}" for {:,} radio IDs'.format(ACL_ACTION, len(ACL)))
except ImportError:
logger.info('ACL file not found or invalid - all subscriber IDs are valid')
ACL_ACTION = 'NONE'
# Depending on which type of ACL is used (PERMIT, DENY... or there isn't one)
# define a differnet function to be used to check the ACL
global allow_sub
if ACL_ACTION == 'PERMIT':
def allow_sub(_sub):
if _sub in ACL:
return True
else:
return False
elif ACL_ACTION == 'DENY':
def allow_sub(_sub):
if _sub not in ACL:
return True
else:
return False
else:
def allow_sub(_sub):
return True
return ACL
class proxyIPSC(IPSC):
def __init__(self, _name, _config, _logger, report):
IPSC.__init__(self, _name, _config, _logger, report)
self.last_seq_id = '\x00'
self.call_start = 0
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _src_sub, _dst_group, _ts, _end, _peerid, _data):
# Check for ACL match, and return if the subscriber is not allowed
if allow_sub(_src_sub) == False:
self._logger.warning('(%s) Group Voice Packet ***REJECTED BY ACL*** From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_group))
return
# Process the packet
self._logger.debug('(%s) Group Voice Packet Received From: %s, IPSC Peer %s, Destination %s', self._system, int_id(_src_sub), int_id(_peerid), int_id(_dst_group))
_burst_data_type = _data[30] # Determine the type of voice packet this is (see top of file for possible types)
_seq_id = _data[5]
for system in systems:
if system != self._system:
#
# BEGIN FRAME FORWARDING
#
# Make a copy of the payload
_tmp_data = _data
# Re-Write the IPSC SRC to match the target network's ID
_tmp_data = _tmp_data.replace(_peerid, self._CONFIG['SYSTEMS'][system]['LOCAL']['RADIO_ID'])
# Send the packet to all peers in the target IPSC
systems[system].send_to_ipsc(_tmp_data)
#
# END FRAME FORWARDING
#
#
# BEGIN IN-BAND SIGNALING BASED ON TGID & VOICE TERMINATOR FRAME
#
# Action happens on key up
if _burst_data_type == BURST_DATA_TYPE['VOICE_HEAD']:
if self.last_seq_id != _seq_id:
self.last_seq_id = _seq_id
self.call_start = time()
self._logger.info('(%s) GROUP VOICE START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group))
self._report.send_proxyEvent('({}) GROUP VOICE START: CallID: {} PEER: {}, SUB: {}, TS: {}, TGID: {}'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group)))
# Action happens on un-key
if _burst_data_type == BURST_DATA_TYPE['VOICE_TERM']:
if self.last_seq_id == _seq_id:
self.call_duration = time() - self.call_start
self._logger.info('(%s) GROUP VOICE END: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s Duration: %.2fs', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration)
self._report.send_proxyEvent('({}) GROUP VOICE END: CallID: {} PEER: {}, SUB: {}, TS: {}, TGID: {} Duration: {:.2f}s'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group), self.call_duration))
else:
self._logger.warning('(%s) GROUP VOICE END WITHOUT MATCHING START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s', self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group),)
self._report.send_proxyEvent('(%s) GROUP VOICE END WITHOUT MATCHING START: CallID: %s PEER: %s, SUB: %s, TS: %s, TGID: %s'.format(self._system, int_id(_seq_id), int_id(_peerid), int_id(_src_sub), _ts, int_id(_dst_group)))
class proxyReportFactory(reportFactory):
def send_proxyEvent(self, _data):
self.send_clients(REPORT_OPCODES['BRDG_EVENT']+_data)
if __name__ == '__main__':
import argparse
import sys
import os
import signal
from ipsc.dmrlink_config import build_config
from ipsc.dmrlink_log import config_logging
# Change the current directory to the location of the application
os.chdir(os.path.dirname(os.path.realpath(sys.argv[0])))
# CLI argument parser - handles picking up the config file from the command line, and sending a "help" message
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
parser.add_argument('-ll', '--log_level', action='store', dest='LOG_LEVEL', help='Override config file logging level.')
parser.add_argument('-lh', '--log_handle', action='store', dest='LOG_HANDLERS', help='Override config file logging handler.')
cli_args = parser.parse_args()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
# Call the external routine to build the configuration dictionary
CONFIG = build_config(cli_args.CFG_FILE)
# Call the external routing to start the system logger
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
if cli_args.LOG_HANDLERS:
CONFIG['LOGGER']['LOG_HANDLERS'] = cli_args.LOG_HANDLERS
logger = config_logging(CONFIG['LOGGER'])
logger.info('DMRlink \'dmrlink.py\' (c) 2013 - 2015 N0MJS & the K0USY Group - SYSTEM STARTING...')
# Set signal handers so that we can gracefully exit if need be
def sig_handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for system in systems:
systems[system].de_register_self()
reactor.stop()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, sig_handler)
# PROXY.PY SPECIFIC ITEMS GO HERE:
# Build the Access Control List
ACL = build_acl('sub_acl')
# MAIN INITIALIZATION ITEMS HERE
# INITIALIZE THE REPORTING LOOP
report_server = config_reports(CONFIG, logger, proxyReportFactory)
# Build ID Aliases
peer_ids, subscriber_ids, talkgroup_ids, local_ids = build_aliases(CONFIG, logger)
# INITIALIZE AN IPSC OBJECT (SELF SUSTAINING) FOR EACH CONFIGURED IPSC
systems = mk_ipsc_systems(CONFIG, logger, systems, proxyIPSC, report_server)
# INITIALIZATION COMPLETE -- START THE REACTOR
reactor.run()

145
rcm.py
View File

@ -1,145 +0,0 @@
#!/usr/bin/env python
#
# This work is licensed under the Creative Commons Attribution-ShareAlike
# 3.0 Unported License.To view a copy of this license, visit
# http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
# Creative Commons, 444 Castro Street, Suite 900, Mountain View,
# California, 94041, USA.
# This is a sample application that uses the Repeater Call Monitor packets to display events in the IPSC
# NOTE: dmrlink.py MUST BE CONFIGURED TO CONNECT AS A "REPEATER CALL MONITOR" PEER!!!
# ALSO NOTE, I'M NOT DONE MAKING THIS WORK, SO UNTIL THIS MESSAGE IS GONE, DON'T EXPECT GREAT THINGS.
from __future__ import print_function
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from twisted.internet import task
from binascii import b2a_hex as h
import datetime
import binascii
import dmrlink
from dmrlink import IPSC, NETWORK, networks, get_info, int_id, subscriber_ids, peer_ids, talkgroup_ids, logger
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK, Dave K, and he who wishes not to be named'
__license__ = 'Creative Commons Attribution-ShareAlike 3.0 Unported'
__version__ = '0.2a'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'Production'
try:
from ipsc.ipsc_message_types import *
except ImportError:
sys.exit('IPSC message types file not found or invalid')
status = True
rpt = False
nack = False
class rcmIPSC(IPSC):
def __init__(self, *args, **kwargs):
IPSC.__init__(self, *args, **kwargs)
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def call_mon_status(self, _network, _data):
if not status:
return
_source = _data[1:5]
_ipsc_src = _data[5:9]
_seq_num = _data[9:13]
_ts = _data[13]
_status = _data[15] # suspect [14:16] but nothing in leading byte?
_rf_src = _data[16:19]
_rf_tgt = _data[19:22]
_type = _data[22]
_prio = _data[23]
_sec = _data[24]
_source = get_info(int_id(_source), peer_ids)
_ipsc_src = get_info(int_id(_ipsc_src), peer_ids)
_rf_src = get_info(int_id(_rf_src), subscriber_ids)
if _type == '\x4F' or '\x51':
_rf_tgt = get_info(int_id(_rf_tgt), talkgroup_ids)
else:
_rf_tgt = get_info(int_id(_rf_tgt), subscriber_ids)
print('Call Monitor - Call Status')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
print('IPSC: ', _network)
print('IPSC Source: ', _ipsc_src)
print('Timeslot: ', TS[_ts])
try:
print('Status: ', STATUS[_status])
except KeyError:
print('Status (unknown): ', h(status))
try:
print('Type: ', TYPE[_type])
except KeyError:
print('Type (unknown): ', h(_type))
print('Source Sub: ', _rf_src)
print('Target Sub: ', _rf_tgt)
print()
def call_mon_rpt(self, _network, _data):
if not rpt:
return
_source = _data[1:5]
_ts1_state = _data[5]
_ts2_state = _data[6]
_source = get_info(int_id(_source), peer_ids)
print('Call Monitor - Repeater State')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
try:
print('TS1 State: ', REPEAT[_ts1_state])
except KeyError:
print('TS1 State (unknown): ', h(_ts1_state))
try:
print('TS2 State: ', REPEAT[_ts2_state])
except KeyError:
print('TS2 State (unknown): ', h(_ts2_state))
print()
def call_mon_nack(self, _network, _data):
if not nack:
return
_source = _data[1:5]
_nack = _data[5]
_source = get_info(int_id(_source), peer_ids)
print('Call Monitor - Transmission NACK')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
try:
print('NACK Cause: ', NACK[_nack])
except KeyError:
print('NACK Cause (unknown): ', h(_nack))
print()
def repeater_wake_up(self, _network, _data):
_source = _data[1:5]
_source_dec = int_id(_source)
_source_name = get_info(_source_dec, peer_ids)
#print('({}) Repeater Wake-Up Packet Received: {} ({})' .format(_network, _source_name, _source_dec))
if __name__ == '__main__':
logger.info('DMRlink \'rcm.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
for ipsc_network in NETWORK:
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
networks[ipsc_network] = rcmIPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()

2
requirements.txt Normal file → Executable file
View File

@ -1 +1,3 @@
Twisted>=12.0.0
dmr_utils>=0.1.2
bitstring>=3.1.3

6
sub_acl_SAMPLE.py Executable file
View File

@ -0,0 +1,6 @@
# The 'action' May be PERMIT|DENY
# Each entry may be a single radio id, or a hypenated range (e.g. 1-2999)
# Format:
# ACL = 'action:id|start-end|,id|start-end,....'
ACL = 'DENY:1-2999,16777215'

File diff suppressed because one or more lines are too long

17
systemd/ambe_audio.service Executable file
View File

@ -0,0 +1,17 @@
[Unit]
Description=DMRlink ambe audio Service
# Description=Place this file in /lib/systemd/system
[Service]
Type=simple
StandardOutput=null
WorkingDirectory=/opt/dmrlink/ambe_audio
Restart=always
RestartSec=3
ExecStart=/usr/bin/python /opt/dmrlink/ambe_audio/ambe_audio.py
ExecReload=/bin/kill -2 $MAINPID
KillMode=process
[Install]
WantedBy=network-online.target

17
systemd/bridge.service Executable file
View File

@ -0,0 +1,17 @@
[Unit]
Description=DMRlink bridge Service
# Description=Place this file in /lib/systemd/system
[Service]
Type=simple
StandardOutput=null
WorkingDirectory=/opt/dmrlink/bridge
Restart=always
RestartSec=3
ExecStart=/usr/bin/python /opt/dmrlink/bridge/bridge.py
ExecReload=/bin/kill -2 $MAINPID
KillMode=process
[Install]
WantedBy=network-online.target

0
systemd/playback.service Executable file
View File

View File

@ -1 +0,0 @@
Worldwide,1 Local,2 North America,3 T6-DCI Bridge,3100 Kansas Statewide,3120 Missouri,3129 Massachussetts,3125 Midwest,3169 Northeast,3172
1 Worldwide 1 Local 2 North America 3 T6-DCI Bridge 3100 Kansas Statewide 3120 Missouri 3129 Massachussetts 3125 Midwest 3169 Northeast 3172

BIN
template.bin Executable file

Binary file not shown.