Compare commits
458 Commits
Author | SHA1 | Date |
---|---|---|
Cort Buffington | a55f73d059 | |
Steve Zingman | 1c684486ca | |
Lorenzo Cipriani | 67fd6d62a5 | |
Lorenzo Cipriani | 785f44e7e6 | |
Cort Buffington | c023d4a565 | |
Cort Buffington | ad399792c9 | |
Cort Buffington | 198278c288 | |
Cort Buffington | 2520f394ed | |
Cort Buffington | bc59c75eb0 | |
Cort Buffington | 01a3fff754 | |
Cort Buffington | 2e1a4c3a58 | |
Cort Buffington | 59a59e4100 | |
Cort Buffington | 81c3467ec0 | |
Cort Buffington | 25d6bc08d0 | |
Cort Buffington | e8311b1f54 | |
Cort Buffington | cd11397416 | |
Cort Buffington | cdd65d8edf | |
Cort Buffington | 2b63b5c111 | |
Cort Buffington | 3fc0bdc63d | |
Cort Buffington | f5bc547d4d | |
Cort Buffington | 0acc6042e8 | |
Cort Buffington | b4ab2d31a3 | |
Cort Buffington | 5950240787 | |
Steve Zingman | 63611f2c6c | |
Steve Zingman | eb2ffe4ecb | |
Cort Buffington | bf699dcfbc | |
Steve N4IRS | c6a543527f | |
Steve Zingman | 3279aeb527 | |
Cort Buffington | f216300539 | |
Cort Buffington | 8e858e48a2 | |
Cort Buffington | c16d549e94 | |
Cort Buffington | ae9f71d715 | |
Cort Buffington | 4ac862b93c | |
Cort Buffington | 8fbd7ccf33 | |
Cort Buffington | b8d1449d2f | |
Cort Buffington | c8804d7231 | |
Cort Buffington | 7fde9d1ce8 | |
Cort Buffington | d1143ddb7c | |
Cort Buffington | 0079ad1baa | |
Cort Buffington | 547d6e23ed | |
Cort Buffington | 62fc209b4f | |
Cort Buffington | a3a296161a | |
M0NWI | 7edb7e26d5 | |
M0NWI | 018ecfebc7 | |
Steve N4IRS | 36a7092bfb | |
Steve N4IRS | 0fe54ad948 | |
Cort Buffington | 702159d20c | |
Cort Buffington | fa8e53bd81 | |
Cort Buffington | ff0801ba54 | |
Cort Buffington | e3070c5bb7 | |
Cort Buffington | 10cec77baa | |
Cort Buffington | d6e18737d0 | |
Cort Buffington | 8cfdab8614 | |
Cort Buffington | f55ac5e407 | |
Cort Buffington | 509b3b1173 | |
Cort Buffington | 0d87ffa2df | |
Cort Buffington | d5811b5228 | |
Cort Buffington | 353ce2be79 | |
Cort Buffington | 7a3157fcfa | |
Cort Buffington | 4c8a42bd81 | |
Cort Buffington | 202280e8d9 | |
Cort Buffington | 7a9ece180a | |
Cort Buffington | 00bbb77d8e | |
Cort Buffington | ae6f2fdeec | |
Cort Buffington | 55da4e3c6e | |
Cort Buffington | a7390de248 | |
Cort Buffington | 01916c3197 | |
Cort Buffington | 1f4fedb432 | |
Cort Buffington | 8f971496cd | |
Cort Buffington | 05c0e71773 | |
Cort Buffington | 04ce07dc24 | |
Cort Buffington | a8703e058c | |
Cort Buffington | d5ff0ff0bf | |
Cort Buffington | 86b2160330 | |
Cort Buffington | 65df5a2ab7 | |
Cort Buffington | 934d349532 | |
Cort Buffington | b833461a1c | |
Cort Buffington | 6caa10d083 | |
Cort Buffington | 87092c79a8 | |
Steve N4IRS | 5bebad3e1b | |
Cort Buffington | 42cb1fa4c7 | |
MrBungle42 | 95d3bb30c1 | |
MrBungle42 | 8d2365eb89 | |
MrBungle42 | 48ab7572ff | |
MrBungle42 | 11695ddb63 | |
Steve N4IRS | eda37bb7c8 | |
Steve N4IRS | 45463b73b8 | |
Steve N4IRS | 78994ef523 | |
Steve N4IRS | 5cb477fed1 | |
Steve N4IRS | f5c0fcdc60 | |
Steve N4IRS | aaaf85b063 | |
Steve N4IRS | 7071cc629f | |
Cort Buffington | 9a27b5185c | |
Cort Buffington | 56635d7e66 | |
Cort Buffington | 9d379bf3c4 | |
Cort Buffington | ed06b92ef7 | |
Cort Buffington | dd3eb5d96e | |
Cort Buffington | f45d9cd829 | |
Cort Buffington | 1fdd8d3697 | |
Cort Buffington | 49d7c1f484 | |
Cort Buffington | 1bd31f09f5 | |
Cort Buffington | 3063b35eb6 | |
Cort Buffington | a86ba63d2d | |
Cort Buffington | f29a01290e | |
Cort Buffington | da882f086d | |
Cort Buffington | 8ac1be5f35 | |
Cort Buffington | a693d70ca7 | |
Cort Buffington | 8c038689a8 | |
Cort Buffington | 4161c6c717 | |
Cort Buffington | 4f63e04a5b | |
Cort Buffington | b0313e2361 | |
Cort Buffington | b8abc44d20 | |
Steve N4IRS | ae8f24d4f0 | |
Steve N4IRS | 642f6fb27c | |
Steve N4IRS | 7f5f6c81f4 | |
Steve N4IRS | a08a155433 | |
Cort Buffington | 3918e3bc36 | |
N4IRS | 11706b6ea5 | |
N4IRS | dd56cc40d9 | |
Cort Buffington | ea606b0424 | |
Cort Buffington | cc2adcffae | |
Cort Buffington | c61f913514 | |
Cort Buffington | 614fdb030b | |
Cort Buffington | dbc7d923e1 | |
Cort Buffington | 80941c9608 | |
Cort Buffington | d7070dacf9 | |
Cort Buffington | f566cce1ca | |
Cort Buffington | 8f724d1f51 | |
Cort Buffington | fe7a3bf56d | |
Cort Buffington | 444bedb3eb | |
Cort Buffington | 9515790ab6 | |
Cort Buffington | 8dbba4b42d | |
Cort Buffington | 53c4ff5b23 | |
Cort Buffington | 9c6de3b902 | |
Cort Buffington | a8c868dc26 | |
Cort Buffington | 0622f8a9bf | |
Cort Buffington | 23354574d1 | |
Cort Buffington | c2bf16bd73 | |
Cort Buffington | d0963792e2 | |
Cort Buffington | 26baa3c26c | |
Cort Buffington | fbac51b3e9 | |
Cort Buffington | 477a00887a | |
Cort Buffington | 4b81a6b22d | |
Cort Buffington | 9f5ac3fad4 | |
Cort Buffington | 2704d32b69 | |
Cort Buffington | d4832fc0d0 | |
root | d62b6861ac | |
Cort Buffington | 8bb698a9f6 | |
Cort Buffington | 37ba34bd3b | |
Cort Buffington | a6058598e0 | |
Cort Buffington | c48495b1d2 | |
Cort Buffington | 25a82f743c | |
Cort Buffington | 0a5c53afd2 | |
Cort Buffington | da0eb0ad37 | |
Cort Buffington | 74eb8edb5f | |
Cort Buffington | c33ba91f4f | |
Cort Buffington | c26e5bb489 | |
Cort Buffington | 213756a161 | |
Cort Buffington | 2fed2912a8 | |
Cort Buffington | 15362a823b | |
Cort Buffington | 3209fce782 | |
Cort Buffington | 697b492d4e | |
Cort Buffington | e80fe33545 | |
Cort Buffington | ad1e9a2ca2 | |
Cort Buffington | d2279134f3 | |
Cort Buffington | d530be263c | |
Cort Buffington | 41e9351d7e | |
Cort Buffington | db3b3cfc0f | |
Cort Buffington | 22a71f0f52 | |
Cort Buffington | 76b43f027a | |
Cort Buffington | f9392ab032 | |
Cort Buffington | c349765cd5 | |
root | 66412ef7af | |
root | 9141e70867 | |
root | b9a96a594d | |
Cort Buffington | 78c23f93ec | |
Cort Buffington | 7b6d47fbb8 | |
Cort Buffington | 82993a03b7 | |
Cort Buffington | c75471d3d3 | |
Steve Zingman | f3535cf036 | |
Steve Zingman | b28a9d7534 | |
Steve Zingman | eaebb96e56 | |
Cort Buffington | 03e514ecff | |
Cort Buffington | d363e25cd5 | |
Cort Buffington | 21601f82f1 | |
root | f4c7620530 | |
root | d75291d391 | |
Cort Buffington | c1ff8d39cc | |
Cort Buffington | 4f5e542b82 | |
Cort Buffington | 6c70707d1a | |
Cort Buffington | 49a3e24a49 | |
Cort Buffington | 2e916c68ee | |
root | 2eee8de24a | |
Cort Buffington | 61b07f421b | |
Cort Buffington | 80f9224d8d | |
Cort Buffington | 9b72e99a60 | |
Cort Buffington | 43e2411f3e | |
Cort Buffington | 2e06ece1f3 | |
Cort Buffington | fffdd01dd2 | |
Cort Buffington | a8dd866881 | |
Cort Buffington | dba648bb89 | |
Cort Buffington | 70bf6d3dce | |
root | bb26a9cc77 | |
root | c71c088c9e | |
Cort Buffington | ce432f3d66 | |
root | 693336adcf | |
Mike Zingman | b47fd15155 | |
Cort Buffington | f367602237 | |
Cort Buffington | 2a2ececca9 | |
Cort Buffington | 4ae1d479cc | |
Cort Buffington | 27197d3716 | |
Cort Buffington | c6f4d47a5f | |
Cort Buffington | 6cc36ab910 | |
Cort Buffington | 01bbd77dbc | |
Cort Buffington | 37fdaed5bd | |
Cort Buffington | 52afb05517 | |
Cort Buffington | 1d51980281 | |
Cort Buffington | 1465468a96 | |
Cort Buffington | dce6339051 | |
Cort Buffington | 2d0bff17d1 | |
Cort Buffington | b068f3b090 | |
Cort Buffington | c5ad283393 | |
Cort Buffington | 200a7a45b2 | |
Cort Buffington | ecd9941588 | |
Cort Buffington | b4b3c6a0d5 | |
Cort Buffington | f293d408b2 | |
Cort Buffington | 475dc9d766 | |
Cort Buffington | 5f58ce04a0 | |
Cort Buffington | 251cc206ff | |
Cort Buffington | 5010d9a5b7 | |
Cort Buffington | 418757cfcd | |
Ryan Turner | e138cfc12f | |
Cort Buffington | bf33f3893e | |
Steve Zingman | 11bf48a03f | |
Steve Zingman | 72fd84dff6 | |
Steve Zingman N4IRS | 00c1d60843 | |
Steve Zingman N4IRS | a921de8087 | |
Steve Zingman N4IRS | 762988c7c6 | |
Steve Zingman N4IRS | e17fa904d1 | |
Steve Zingman N4IRS | 235e8a52d1 | |
Steve Zingman N4IRS | 9484b46b7d | |
Mike Zingman | 2092f2a1e5 | |
Steve Zingman N4IRS | 90da9014ea | |
Steve Zingman N4IRS | 0230251818 | |
Steve Zingman N4IRS | ddd5cc0606 | |
MrBungle42 | 7607a5309d | |
Mike Zingman | 4f7fc558cc | |
Mike Zingman | eb1614f814 | |
Mike Zingman | 108a9e14d3 | |
Mike Zingman | 628d0d3d0c | |
Steve Zingman N4IRS | 047963cd93 | |
Steve Zingman N4IRS | 9776784fb2 | |
Steve Zingman N4IRS | c1abb7e705 | |
Steve Zingman N4IRS | 818b2a3536 | |
Steve Zingman N4IRS | fc16193956 | |
Mike Zingman | 41391e4044 | |
Mike Zingman | 4ecf651cc5 | |
Mike Zingman | 2509528449 | |
Mike Zingman | 8b210f02c0 | |
Mike Zingman | f039cb7d10 | |
Steve Zingman N4IRS | fccc8b3367 | |
Steve Zingman N4IRS | 7113a56d2a | |
Steve Zingman N4IRS | 9e12fc9d5d | |
Steve Zingman N4IRS | 11786cb603 | |
Steve Zingman N4IRS | a6ee57bd26 | |
Steve Zingman N4IRS | e8789a6d96 | |
Steve Zingman N4IRS | c0c2dd7403 | |
Steve Zingman N4IRS | 3818747393 | |
Steve Zingman N4IRS | ff8b14afe7 | |
Mike Zingman | eb48513cf0 | |
Mike Zingman | 595d60a587 | |
Mike Zingman | 0c809a86c1 | |
Mike Zingman | 94ca9d4131 | |
Mike Zingman | d2b33e689b | |
Mike Zingman | 76c74fbec5 | |
Steve Zingman N4IRS | e6a397f479 | |
Mike Zingman | 17ea95b99f | |
Mike Zingman | a77061e21b | |
Cort Buffington | f5f9e14349 | |
Cort Buffington | 52946749d2 | |
Cort Buffington | 5c502e1667 | |
Cort Buffington | 86a42b27d8 | |
Cort Buffington | fec5f76ef7 | |
Cort Buffington | 8e24e4ceea | |
Cort Buffington | 2e331c6f10 | |
Cort Buffington | 566a8abf32 | |
Cort Buffington | 9100349fb2 | |
Cort Buffington | 82de012f2f | |
Cort Buffington | f1ed42d2eb | |
Cort Buffington | c460709c9c | |
Cort Buffington | a41fde756b | |
Cort Buffington | 47bbfad702 | |
Cort Buffington | a70b48a275 | |
Cort Buffington | 8a1e883ea4 | |
Cort Buffington | a3fa36e74b | |
Cort Buffington | 289edb310d | |
Cort Buffington | 1677247b75 | |
Cort Buffington | 9b077fc2f0 | |
Cort Buffington | a844c99daa | |
Cort Buffington | 342f5ac079 | |
Cort Buffington | db489526b1 | |
Cort Buffington | f4aec00fa4 | |
Cort Buffington | 22fdca6436 | |
Cort Buffington | 0076fd5f5a | |
Cort Buffington | 075dad4ee9 | |
Cort Buffington | 20f16091c8 | |
Cort Buffington | 9803a944b8 | |
Cort Buffington | a9ef6314cd | |
Cort Buffington | 174d74385e | |
Cort Buffington | 51be01c77d | |
Cort Buffington | 3705c08434 | |
Cort Buffington | 6eef23c9e2 | |
Cort Buffington | 5cccdd0bb7 | |
Cort Buffington | bc9ab4e11c | |
Cort Buffington | 725110e48e | |
Cort Buffington | dff474cbfb | |
Cort Buffington | c03cbe48b7 | |
Cort Buffington | 7a4e5fd07d | |
Cort Buffington | 03f80f900d | |
Cort Buffington | 7259f16ec0 | |
Cort Buffington | fc18a2f3c0 | |
Cort Buffington | 8d8f5a6eed | |
Cort Buffington | fb0b522e17 | |
Cort Buffington | 0885a3b565 | |
Cort Buffington | 95a03fc28a | |
Cort Buffington | 02d54288f3 | |
Cort Buffington | 08ea043b46 | |
Cort Buffington | 2524822e80 | |
Cort Buffington | 2635046aad | |
Cort Buffington | 7850ec8f85 | |
Cort Buffington | 52a2b26c0a | |
Cort Buffington | 231c37f744 | |
Cort Buffington | c73483a4fd | |
Cort Buffington | f0cc0e097b | |
Cort Buffington | e89f6e877b | |
Cort Buffington | e3051fec85 | |
Cort Buffington | 4e3cd140dd | |
Cort Buffington | 9204a90cfb | |
Cort Buffington | 5c2ef72028 | |
Cort Buffington | 474e73dd23 | |
Cort Buffington | 96d0f11615 | |
Cort Buffington | 9ac48cac3f | |
Cort Buffington | 113ebc8bf3 | |
Cort Buffington | 02439f6ec4 | |
Cort Buffington | b77e2a9809 | |
Cort Buffington | bd251461f4 | |
Cort Buffington | ae29844809 | |
Cort Buffington | 97fa6bfe92 | |
Cort Buffington | 354fa34e8a | |
Cort Buffington | 1285ddedff | |
Cort Buffington | d7f11d955c | |
Cort Buffington | 8e85b71e27 | |
Cort Buffington | ecd2648372 | |
Cort Buffington | 5b4520318f | |
Cort Buffington | 574a89e7f9 | |
Cort Buffington | 31223a846d | |
Cort Buffington | b8bd3da8db | |
Cort Buffington | 10a21ed118 | |
Cort Buffington | ec9c20d5ee | |
Cort Buffington | d2a73a044e | |
Cort Buffington | ea0188002a | |
Cort Buffington | 9265ebaa9b | |
Cort Buffington | 29789acfd9 | |
Cort Buffington | 707d9cc1f6 | |
Cort Buffington | bacb063121 | |
Cort Buffington | 38b6e955a7 | |
Cort Buffington | ea5141684b | |
Cort Buffington | e2f596f311 | |
Cort Buffington | 679038face | |
Cort Buffington | dd4dee21c4 | |
Cort Buffington | d4b51fd08f | |
Cort Buffington | b7f6b62993 | |
Cort Buffington | 6612ba70ef | |
Cort Buffington | e2c47ed6ca | |
Cort Buffington | 3cfc058468 | |
Cort Buffington | 66ae5fa873 | |
Cort Buffington | ffe5a61463 | |
Cort Buffington | 7937071930 | |
Cort Buffington | 4047e08a7e | |
Cort Buffington | 50e9e8ecba | |
Cort Buffington | 84b45cac26 | |
David Kierzkowski | d83fbba277 | |
Cort Buffington | 52793877e1 | |
Cort Buffington | fb72151c8f | |
Cort Buffington | b9b21ebebf | |
Cort Buffington | 5c27ce4d52 | |
Cort Buffington | 06c9ed48b4 | |
Cort Buffington | 16e7d2aaeb | |
Cort Buffington | 1b99f2bfa6 | |
Cort Buffington | acfb4700e5 | |
Cort Buffington | 5a5ecb322b | |
Cort Buffington | 626cc3674b | |
Cort Buffington | aed10e1d4e | |
Cort Buffington | 91a7e773b2 | |
Cort Buffington | e02c03a099 | |
Cort Buffington | 6e8292d5f1 | |
Cort Buffington | d10f33a325 | |
Cort Buffington | 648569eb91 | |
Cort Buffington | 324c16660c | |
Cort Buffington | f2148ab05f | |
Cort Buffington | 1457d34e21 | |
Cort Buffington | 140818bcb2 | |
Cort Buffington | 549b8d62bb | |
Cort Buffington | a4461d3de1 | |
Cort Buffington | fc271cd957 | |
Cort Buffington | 9a99fa585b | |
Cort Buffington | b13f90f971 | |
Cort Buffington | 08e5525ae9 | |
Cort Buffington | eb84cb2589 | |
Cort Buffington | 03fcae42de | |
Cort Buffington | ce29ce89c4 | |
Cort Buffington | bc25da467a | |
Cort Buffington | 82d93a759d | |
Cort Buffington | a6ca762314 | |
Cort Buffington | 6f08f1f2c4 | |
Cort Buffington | 4781321c2c | |
Cort Buffington | 7c99a1933a | |
Cort Buffington | f0d50c8211 | |
Cort Buffington | 1d461ca21a | |
Cort Buffington | 59224df788 | |
Cort Buffington | 5e5719eb2e | |
Cort Buffington | fc791295b2 | |
Cort Buffington | baf87b7bf8 | |
Cort Buffington | 92ff27e04a | |
Cort Buffington | f8ce89a17a | |
Cort Buffington | b78a52290e | |
Cort Buffington | 622f243ee8 | |
Cort Buffington | d160ec6d9e | |
Cort Buffington | dba7fa3e6b | |
Cort Buffington | b22d773e81 | |
Cort Buffington | a89a94dea8 | |
Cort Buffington | 6c41942aa2 | |
Cort Buffington | b2398a255a | |
Cort Buffington | 8c267955ff | |
Cort Buffington | 831949954a | |
Cort Buffington | 318ccfea78 | |
Cort Buffington | 94eb828006 | |
Cort Buffington | c84273272c | |
Cort Buffington | 30ce30749e | |
Cort Buffington | 40b929cadd | |
Cort Buffington | bd211001f7 | |
Cort Buffington | bfa060f0b1 | |
Cort Buffington | 211ebab6bd | |
Cort Buffington | c04bbc1b84 | |
Cort Buffington | 536dc73521 | |
Cort Buffington | d88f90d0b4 | |
Cort Buffington | 6e899a36c2 | |
Cort Buffington | 8270d983fd | |
Cort Buffington | 3fd92dd4da | |
Cort Buffington | 0ead8726f0 | |
Cort Buffington | 3a97e7160d | |
Cort Buffington | 12161c4959 | |
Cort Buffington | c73e733516 | |
Cort Buffington | 8a378fe2c1 | |
Cort Buffington | ce29b8de2d | |
Cort Buffington | 9699fc18ef | |
Cort Buffington | 132c1ece39 | |
Cort Buffington | 6fb3601724 |
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>.
|
|
@ -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
|
||||
|
|
|
@ -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!
|
|
@ -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
|
||||
|
||||
|
|
@ -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()
|
|
@ -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'
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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()
|
|
@ -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': [
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]
|
||||
|
||||
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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'
|
88
bridge.py
88
bridge.py
|
@ -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()
|
|
@ -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': [
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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
1478
dmrlink.py
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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***
|
|
@ -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
|
|
@ -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
|
|
@ -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,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))
|
|
@ -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'])
|
|
@ -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'
|
||||
}
|
|
@ -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.
|
||||
#
|
|
@ -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
90
log.py
|
@ -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()
|
|
@ -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
190
playback.py
190
playback.py
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
145
rcm.py
|
@ -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()
|
|
@ -1 +1,3 @@
|
|||
Twisted>=12.0.0
|
||||
dmr_utils>=0.1.2
|
||||
bitstring>=3.1.3
|
|
@ -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
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
|
Binary file not shown.
Loading…
Reference in New Issue