Compare commits

...

336 Commits

Author SHA1 Message Date
Max 6f1b25d2d1 oplog bug fixes thx as always Billb 2024-01-19 15:27:30 -05:00
Max 1d56794504 un-comment importnow, thx Trip 2023-11-30 11:05:54 -05:00
Max a47bb712d2 fix grp_aff_resp code thx billb 2023-11-02 13:36:18 -04:00
Max 2feb62e8cf patch to enable http console over nginx reverse proxy 2023-10-12 17:50:13 -04:00
Max 310ad30a21 pin pkg versions, thx billb 2023-06-23 13:07:49 -04:00
Max eef44be741 multi_rx touchups 2022-12-21 22:11:25 -05:00
Max b05a4bcc4a allow auto-tuning to trunked uplink frequencies 2022-12-20 22:44:53 -05:00
Max c10dde6065 trunked call uplink frequency tracking 2022-12-20 21:48:57 -05:00
Max fbe4b881bb test 2022-12-18 11:07:21 -05:00
Max e63c106ce2 test 2022-12-18 11:05:05 -05:00
Max 8a3bbae9bc new cpmd plot 2022-12-07 16:35:18 -05:00
Max dc6856d26c experimental h-cpm demod 2022-12-05 14:23:52 -05:00
Max ac9cfa58b7 cpm mode plots 2022-11-18 17:59:51 -05:00
Max cbb6462106 possible fix for crash in multi_rx 2022-11-17 13:28:33 -05:00
Max 7b31ebd34e ui update Thx Trip 2022-10-30 21:21:32 -04:00
Max 7bc0fb2c1b fixes fdma/tdma, ext fnct cmd 2022-10-23 19:23:33 -04:00
Max 874b72042c add TSBKs 29 and 35 2022-10-15 13:11:54 -04:00
Max ebc980c205 fix startup crash in flask app 2022-10-09 09:37:28 -04:00
Max 7dc6d61f75 poss. fix race condition in sql_dbi init 2022-10-07 21:06:14 -04:00
Max d90ecd7c89 gr3.8 build 2022-09-25 18:53:44 -04:00
Max de370340cf udev and blacklist rules for airspy and rtl 2022-09-23 18:21:15 -04:00
Max 245ff05489 fix compile warning 2022-09-22 14:05:39 -04:00
Max 1cd344a838 bug fixes thx Bill B 2022-09-22 13:26:20 -04:00
Max 16e8b53fbf python 2022-09-20 15:40:41 -04:00
Max 4028ec6a4e imports 2022-09-20 15:29:48 -04:00
Max e7fc1c6c45 scripts 2022-09-20 15:16:00 -04:00
Max f010f3f5dc message, msg_queue, msg_handler 2022-09-20 15:00:31 -04:00
Matt Ames dc90a85c02 AlgID updates, finally discovered Motorola's GOST implementation in the wild! 2022-09-14 11:30:37 +10:00
Max dd830a1463 poss. fix for tailing audio artifact p25p2 2022-07-03 16:13:44 -04:00
Max 1e7b8b43a2 chg include file handling 2022-06-17 15:31:35 -04:00
Max f51999c49f tdma crc message: increase log threshold 2022-06-12 15:14:35 -04:00
Max 72ccc09ee0 remove file 2022-06-07 16:24:54 -04:00
Max 86a609d743 gr3.9 build process updates 2022-06-07 16:23:31 -04:00
Max b2ec8b376e fix compile problem 2022-06-05 16:03:41 -04:00
Max bc0ba19e9b define M_PI et al 2022-06-05 15:23:31 -04:00
Max c6b3799b96 update gr3.9 install script 2022-06-05 14:57:43 -04:00
Max 3af1dbe0db class variable naming conflict 2022-06-05 14:56:27 -04:00
Max 387cf57662 update gr3.9 install deps 2022-06-04 21:02:50 -04:00
Max ff96eb549b update messaging 2022-06-01 13:14:19 -04:00
Max 6966b5fbe3 README-gr3.9 2022-05-31 17:18:29 -04:00
Max 48b0de1986 build scripts for gr3.9 and gr3.10 2022-05-31 17:18:01 -04:00
Max e965205121 build scripts for gr3.9 and gr3.10 2022-05-31 17:17:15 -04:00
Max 713e632e6c fix some compiler warnings 2022-05-24 21:04:21 -04:00
Max f8a9b6128a fix includes 2022-05-24 19:18:33 -04:00
Max 23cd0111b7 python2/3 compat int/float problem 2022-05-20 20:25:32 -04:00
Max cca08a1694 correct virtual function defs 2022-05-19 22:38:48 -04:00
Max 702370af8c fix c++ include paths 2022-05-19 21:38:07 -04:00
Max d1ee843822 python: drop gru from import 2022-05-19 20:21:21 -04:00
Max 3b687098d0 src address UI updates thx Trip 2022-05-08 09:53:10 -04:00
Max ecc0c6f6b1 error tracking updates, fixes 2022-04-06 14:04:55 -04:00
Max 84042249fc discrete passband tuning based on step size 2022-04-06 14:02:07 -04:00
Max 61144fa477 allow input from file in disctap mode 2022-02-01 13:33:51 -05:00
Max b9752d54bf New RX Two Pane Mode thx Trip 2022-01-31 22:43:34 -05:00
Max 53490d0c8c workaround spurious algid 2022-01-26 14:16:53 -05:00
Max f31302bff7 main.js fix font size, auth_dmd, Thx Trip 2022-01-14 13:56:57 -05:00
Max 5e87fdc889 new ui, added cfg options, add AUTH_DMD 2022-01-11 20:53:15 -05:00
Max e540ade02b fix mbt00, add new AUTH_DMD mbt op 2022-01-06 17:40:43 -05:00
Max eb9a8be83d fix debug messages 2022-01-06 15:15:22 -05:00
Max a148cf1fe5 bugfixes for audio-if mode 2022-01-06 14:41:18 -05:00
Max 3492bd70b1 new algo to eliminate +- 2400 Hz tuning ambiguity 2022-01-04 19:06:28 -05:00
Max fbae3bcfde p25p1 link control msg python handler 2022-01-04 17:17:29 -05:00
Max 2ee08e9c24 for xor area cleared at incorrect time 2022-01-04 15:35:48 -05:00
Max 5818d58dba p25p2 pdu updates, new 'sync' plot mode 2022-01-04 15:29:01 -05:00
Max 8a948a3bee p25craft.py python3 updates 2021-12-25 14:45:00 -05:00
Max 8b77120005 udp configs: fix discrepancy 2021-12-10 19:08:14 -05:00
Max d9794b5123 terminal.py KeyError crash / fix 2021-12-07 20:40:27 -05:00
Max e40f2c53da xormask area uninitialized, crash in rs decode 2021-12-07 20:38:02 -05:00
Max ddd70bdd07 audio-if updates 2021-12-07 15:34:07 -05:00
Max 13a3bccb9e gr3.8 updates for debian 11 bullseye build 2021-11-13 18:46:41 -05:00
Max 7f37fada79 proper defs for virtual functions 2021-11-13 17:01:09 -05:00
Max c06a318222 store_i fix return type 2021-11-13 16:58:19 -05:00
Max f14bf6deb8 CMakeLists.txt, remove cmake Gr* modules 2021-11-13 16:33:41 -05:00
Max 676409131d rx.py reload_tags 2021-10-10 14:03:43 -04:00
Max 5ef6bd7862 multi_rx fix crash for non-p25-trunked 2021-10-04 19:31:51 -04:00
Max 0ecddbb1bf cfg-trunk and cfg-trunk2 2021-10-01 14:19:03 -04:00
Matt Ames c2cebde6d5 More updates to AlgID values 2021-09-23 16:01:22 +10:00
Max 56daa99b94 http_server bugfixes 2021-09-21 17:31:48 -04:00
Max f93460a0bd fix spurious algid 2021-09-07 19:32:11 -04:00
Max e080f32fc6 crash in http_server on malformed json 2021-09-05 13:47:31 -04:00
Max 65b5c318b0 update index.html and main.js thx Trip 2021-09-03 20:49:11 -04:00
Max a48ba912ea oplog update 2021-08-29 19:13:33 -04:00
Max a4c7d3852f fix ess_keyid storage size 2021-08-26 17:24:10 -04:00
Max 233f810d3b rx.py error tracking message level 2021-08-12 18:26:52 -04:00
Max 855b590f2d rx.py stalls if http term not connected 2021-08-11 15:42:27 -04:00
Max 3e2aece2e6 add channel id to timeout msgs 2021-08-11 15:41:49 -04:00
Max a8d8dbc882 main.js update/bugfix thx Trip 2021-08-09 13:33:31 -04:00
Max 3a3509fbf0 p25p2_tdma.cc tdma cc 2021-08-08 14:23:03 -04:00
Max ecc8db384f rx.py error tracking debugging 2021-08-08 11:05:23 -04:00
Max 5c53f3f643 rx.py fix freq error tracking missing from web gui 2021-08-06 13:50:02 -04:00
Max 70143fa2f7 main.js keyboard fix thx Trip 2021-08-05 10:31:15 -04:00
Max ced242a072 README-July-2021 updates 2021-08-01 17:10:40 -04:00
Max b1e7d81f5e multi_rx.py make executable 2021-07-31 21:21:27 -04:00
Max a169f37c3a http_server filename regex 2021-07-28 20:06:15 -04:00
Max d964703e83 python3 strikes again 2021-07-28 15:15:18 -04:00
Max b57db0d599 install-sql.sh possible bugfix 2021-07-27 17:13:22 -04:00
Max 6fb7a6b1ca bypass error msg install-sql.sh 2021-07-26 20:52:06 -04:00
Max 7373d10e1c voice chan timeout poss. bugfix 2021-07-26 20:49:41 -04:00
Max 7290f13486 rx.py add sql logging 2021-07-26 20:49:13 -04:00
Max 47052abc1a oplog error.html update 2021-07-26 20:48:46 -04:00
Max db9e7db946 fix secondary cc overflow thx Trip 2021-07-26 20:48:11 -04:00
Max f212d1b6ff fix oplog __init__ - thx Trip 2021-07-25 21:37:14 -04:00
Max 806d7c44ca oplog rc2-final 2021-07-25 20:47:32 -04:00
Max 51042858e8 core rc2-final 2021-07-25 20:46:14 -04:00
Max 180a4dec97 www rc2-final 2021-07-25 20:43:04 -04:00
Max aec5a8012c favicon.ico thx triptolemus 2021-04-01 15:11:12 -04:00
Max ccc0001872 update copyright 2021-03-22 20:50:34 -04:00
Max dced944480 css updates thx Triptolemus 2021-03-22 20:14:13 -04:00
Max 91dead3b5b add hostname info to README-hls 2021-02-14 18:29:59 -05:00
Max 3aefbf332b increase plot color intensity 2021-02-14 12:01:56 -05:00
Max c3010f3265 set default plot dark colors 2021-02-14 11:59:03 -05:00
Max 18c1cc0a54 possible fix for unsafe access in make_status_png 2021-02-14 11:56:45 -05:00
Max ed3334ca6c add hls initialization error checking 2021-02-13 20:57:01 -05:00
Max 2dcbbbcdc2 fix file mode bits 2021-02-12 18:36:58 -05:00
Max e971078c55 dark style, thx triptolemus 2021-02-12 18:25:01 -05:00
Max 59a631b700 plot color cfg 2021-02-05 23:38:41 -05:00
Max ccf9a9f20b additional README-hls updates 2020-12-25 22:30:39 -05:00
Max 76ca14e5e8 fix typo README-hls 2020-12-25 21:58:34 -05:00
Max e540ac9c54 hls hacks 2020-12-25 15:34:25 -05:00
Max 5c5bd11a0c udp rx/tx linkage docs/examples 2020-12-25 15:27:48 -05:00
Max 27ea95ee38 hls.js 2020-12-21 22:54:06 -05:00
Matt Ames d8c01956ba Make .gitignore a bit clearer 2020-12-09 06:39:00 +11:00
Matt Ames bdd6707dc5 Add in sensible .gitignore that excludes build artifacts and python bytecode files 2020-12-07 17:00:09 +11:00
Max 3612767021 talkgroup goto: thx Triptolemus 2020-12-06 18:48:10 -05:00
Matt Ames 679336d55d Adding more AlgIDs as they are discovered (HAYSTACK and AES-128-OFB) 2020-11-25 12:38:54 +11:00
Max 19b720715c possible fix for lfsr generator bug 2020-11-22 19:55:22 -05:00
Max 856ed97419 add log_ts.py 2020-08-06 20:43:00 -04:00
Max a6bd25163a liquidsoap thx boatbod 2020-08-05 21:47:49 -04:00
Max 28ef6960cc add pulseaudio thx boatbod 2020-08-05 21:31:11 -04:00
Max 1180f9d8b6 python 2/3 agnosticism sockaudio.py 2020-08-05 16:33:07 -04:00
Max a11ef18339 gr3.8.patch updates for ubuntu 20.04 2020-08-04 13:32:32 -04:00
Max 478d2cba70 fakecc updates 2020-08-02 15:49:05 -04:00
Max 3a0901e646 udp transport option for tx/rx 2020-08-02 15:29:47 -04:00
Max 62e2ef92f0 add additional L3 message types 2020-07-22 18:10:31 -04:00
Max a5df0ec044 fix disctap if rate 2020-07-18 19:34:01 -04:00
Max 94dc7536e6 fix dmr correlation 2020-07-18 19:33:11 -04:00
Max 9d16effff3 correlation.json 2020-07-13 21:54:17 -04:00
Max 0421a55968 add correlation plot 2020-07-13 20:29:00 -04:00
Max 602cb0659f html/css/js updates+bugfixes 2020-07-12 21:09:57 -04:00
Max 1da0714da5 python 2/3 agnosticism http_server.py 2020-07-12 21:04:35 -04:00
Max 5e5566f0f3 nxdn update for rx 2020-07-11 18:22:11 -04:00
Max ffb2035c67 add commentary in nxdn-cfg.dat 2020-07-11 13:19:01 -04:00
Max 82364672ed fix syntax error 2020-06-18 21:50:44 -04:00
Max 921a1d8ea0 nxdn tx 2020-06-18 21:30:19 -04:00
Max 5e01df4883 nxdn tx 2020-06-18 21:28:58 -04:00
Max 3b69f5e335 fix terminal verbiage 2020-06-18 21:09:14 -04:00
Max 0686f2de14 dmr group whitelist/blacklist 2020-04-02 18:55:17 -04:00
Max 733e891d22 multi_rx option to log recovered symbols to file 2020-03-05 22:15:12 -05:00
Max 57a682d5f0 bugfix + touchup 2020-03-01 21:12:54 -05:00
Max c26b51e0c1 README-gr3.8.patch 2020-02-26 15:40:36 -05:00
Max 18042e1368 gr3.8.patch 2020-02-26 15:40:24 -05:00
Max 8b00b8459a additional python 2/3 agnosticism 2020-02-23 21:08:04 -05:00
Max 92ba1df2c0 define nxdn tx impulse response 2020-02-23 20:25:49 -05:00
Max 6f02fc63cc rename http.py to http_server.py 2020-02-23 20:04:19 -05:00
Max b91039ecd0 initial hacks for python 2/3 agnosticism 2020-02-23 20:03:43 -05:00
Max 3de2d3e440 nxdn96 demodulation 2020-02-07 19:30:57 -05:00
Max e25e7be488 add frequency error tracking option 2019-11-14 22:40:19 -05:00
Max b6b0eeb7b8 merge branch max into master 2019-11-14 22:10:34 -05:00
Max 140a76ac76 nxdn48 initial checkin for rx 2019-05-11 16:41:49 -04:00
Max 538ae6015b debug: dump raw IMBE frame data 2019-01-25 20:40:21 -05:00
Max 111b311d88 trim mixer plot CPU usage and add plot command logfile 2019-01-16 21:08:01 -05:00
Max f974a32a19 eliminate warning: large integer implicitly truncated to unsigned type 2019-01-12 14:27:58 -05:00
Max 6064229027 filter out some of the more bogus HDU/LLDU FEC failures in p25p1_fdma 2019-01-12 13:18:38 -05:00
Max 66d0ee86fd fix display of src addr and ess 2019-01-04 22:05:42 -05:00
Max 4a7cf6b79c Merge branch 'max' of git.osmocom.org:op25 into max 2018-12-25 21:31:34 -05:00
Max c9fa2f6a31 Merge remote-tracking branch 'balint256/master' into max 2018-12-25 21:25:00 -05:00
Max c4fe5610ba December 25 2018 merge 2018-12-25 21:10:06 -05:00
Max 582ba4395a value_string symlinks 2018-12-25 21:03:46 -05:00
Max 0f2f040307 ezpwd 2018-12-25 21:02:49 -05:00
Matt Ames 652b29b7a2 Clarify some of the algorithms codes and key sizes 2018-12-13 12:45:13 +11:00
Matt Ames 7afb19a5b4 Clean up test commit 2018-11-02 11:07:02 +11:00
Matt Ames a07ba04296 Test commit 2018-11-02 11:06:16 +11:00
Max 74db47bd06 fix uninitialized var 2018-09-08 17:04:10 -04:00
Max 971a7502aa add new demod method 2018-09-08 16:01:23 -04:00
Matt Ames 0ece965f78 Update PADSTONE AlgID entry to reflect that the TIA.102 AlgID document has this AlgID listed as "MSI (Motorola Solutions International) Assigned" 2018-08-25 14:40:33 +10:00
Matt Ames 933cb72121 Correct AES-GCM to indicate it is not 100% confirmed as correct 2018-08-25 14:39:06 +10:00
Matt Ames 37a9dbdce1 Include PADSTONE in AlgID list. 2018-08-25 09:46:36 +10:00
Matt Ames 302115e19e More AlgID updates 2018-08-24 22:11:29 +10:00
Matt Ames 9a5820d614 Added in 0xAF as probable AES-GCM algo ID 2018-08-24 19:42:37 +10:00
Matt Ames ff09558c41 Add in AlgID for Motorola CFX-256 2018-08-24 19:37:34 +10:00
Max 0ae2b9624f gmsk 2018-08-19 18:10:12 -04:00
Matt Ames d408c7fee2 Update AlgIDs with references from TIA TR-8 PDF 2018-06-29 10:24:13 +10:00
Max 9c01b98542 configuration updates 2018-06-06 21:46:05 -04:00
Max 94a34dc4df configuration updates 2018-05-17 21:55:02 -04:00
Max 384c8c28d0 ui additions thx Trip 2018-04-27 10:02:33 -04:00
Max 3c50b3c54b test pattern file utility 2018-04-13 09:09:34 -04:00
Matt Ames a9fc0c206a Update AlgIDs to latest known values 2018-04-12 11:34:06 +10:00
Matt Ames 8a8ab6e90b Add in P25 compliant test patterns to tx 2018-04-12 11:13:37 +10:00
Max 332a5968b9 layout updates thx trip 2018-04-03 19:40:14 -04:00
Max 31e11e999d configuration updates 2018-04-03 15:25:46 -04:00
Max c9bfc96ed0 http server updates 2018-04-03 15:19:57 -04:00
Max e0eabf10f0 http server updates 2018-04-03 15:18:58 -04:00
Max 7bc0a5ef96 http server updates 2018-04-03 15:15:44 -04:00
Max c0bbdc9c70 http server updates 2018-04-03 14:40:11 -04:00
Max 85d09681fe js update 2018-03-21 13:58:55 -04:00
Max bd95950d8c main.js edits 2018-03-16 16:56:15 -04:00
Max e572cfe802 configuration additions 2018-03-16 16:38:26 -04:00
Max 9d344f283b add config directory 2018-03-16 16:29:54 -04:00
Max 411d385125 temp.png 2018-03-16 16:28:37 -04:00
Max abf9b4f06c configuration additions 2018-03-16 16:26:42 -04:00
Max 5ea5186c5d byteify 2018-03-16 16:14:43 -04:00
Max 4630e58564 json trunking additions 2018-03-15 14:43:57 -04:00
Max c17443d9ac scan control bugfix, js debug 2018-03-03 16:51:49 -05:00
Max 69f2091f02 rx.py config updates 2018-03-03 16:49:40 -05:00
Max 993c52686c bugfix two .h files missing from CMakeLists.txt thx Adrian YO8RZZ 2018-03-01 20:50:13 -05:00
Max 2ff4ae5a38 css updates many thx triptolemus 2018-03-01 20:10:19 -05:00
Max a77c18d745 sync install.sh with max branch 2018-02-19 20:17:04 -05:00
Max 6b45a95754 http bugfix thx wa8wg for the report 2018-02-19 20:03:13 -05:00
Max 7bdfe78aa0 build fixes thx MattSR via CS 2018-02-19 11:58:40 -05:00
Max 4cde5ac5d8 bugfix in -P thx Scott 2018-01-26 14:55:14 -05:00
Max 43bd7e7ae1 update README 2018-01-25 19:36:11 -05:00
Max d5ee60abf8 http.py 2018-01-25 19:13:32 -05:00
Max 34d08d9255 terminal.py http additions 2018-01-25 19:02:15 -05:00
Max 6ecebfffcc rx.py http and remote plot additions 2018-01-25 19:00:53 -05:00
Max 9d0747b1ca allow plot output to files 2018-01-25 18:51:16 -05:00
Max d16702fb9e html/css/js/png file additions 2018-01-25 18:50:43 -05:00
Max b28a976768 additional trunking data collection 2018-01-23 22:23:45 -05:00
Max dc5b77ff9b sockaudio.py thx boatbod 2018-01-15 13:42:50 -05:00
Max ee399ed962 multi_tx: 2 bugfixes 2018-01-14 11:09:19 -05:00
Max 25ae933b47 possible fixes for two bugs in trunked WAV file logger 2018-01-08 11:30:15 -05:00
Max 38b2cb60be partial workaround for excess cpu usage 2018-01-08 11:09:10 -05:00
Max d25d93cf9e p25 p2/tdma bugfix, code cleanups 2017-12-28 21:16:07 -05:00
Max 9b93b5699d multi rx 2017-12-25 20:15:10 -05:00
Max cd16c15c54 readme update II 2017-12-25 20:13:43 -05:00
Max 18e96a2307 update readme 2017-12-25 19:36:35 -05:00
Max 9f8b9af3a4 dv_tx resamp hack 2017-12-23 18:54:48 -05:00
Max 682bd4abcb crc16.h 2017-12-22 20:15:47 -05:00
Max e9a6e3806f rx_sync swig interface 2017-12-22 20:09:37 -05:00
Max e79d7eaabc rx_sync additions 2017-12-22 20:08:10 -05:00
Max 5601e5290e op25_audio 2017-12-22 19:11:29 -05:00
Max 3b26dd2fd2 vocoder_impl 2017-12-22 18:29:43 -05:00
Max 5f264c68bf bit utils 2017-12-22 18:24:05 -05:00
Max b055a0b6d0 dstar CRC 2017-12-22 18:23:49 -05:00
Max db1bd03e25 dstar parameters 2017-12-22 18:22:46 -05:00
Max 1b1c8842c1 add get_freq_error() 2017-12-22 18:16:18 -05:00
Max 90fbe46518 dstar cfg file 2017-12-22 18:12:37 -05:00
Max dd9bc87c86 dv_tx - add check for dstar cfg file 2017-12-22 18:11:56 -05:00
Max 1fc2df5ac1 dstar cfg 2017-12-22 18:10:50 -05:00
Max 04692d23e2 terminal.py 2017-12-22 18:08:46 -05:00
Max 0fcef6a5be rx.py 2017-12-22 18:05:29 -05:00
Max 35da7d958a demodulator updates 2017-12-22 18:02:18 -05:00
Max 5edfc1c463 install.sh 2017-12-22 16:52:30 -05:00
Max ce129911bc use 480K multi_tx IF rate 2017-12-05 20:11:46 -05:00
Max d6615c84fe dv_tx QRO 2017-12-05 20:11:02 -05:00
Max 5b280ae2ef updated dv_tx 2017-11-29 13:16:50 -05:00
Max 0927bc11a9 vocoder: fix excess cpu usage 2017-11-29 00:16:18 -05:00
Max 796c81f219 cleanup unused vars 2017-11-27 13:08:42 -05:00
Max a3f822dcc1 cleanup unused vars 2017-11-27 13:07:55 -05:00
Max 98e43839b9 ysf frame decode 2017-11-22 19:04:25 -05:00
Max 84f0a32ee1 p25_frame.h: fix include guard 2017-11-22 19:02:20 -05:00
Max ef586696a1 dmr hamming 7_4 decode 2017-11-22 19:00:34 -05:00
Max d6f3be0604 bugfix: uninitialized struct in d2460.cc 2017-11-22 18:57:41 -05:00
Max 2d52d95a0a bugfix: static in check_frame_sync 2017-11-22 18:56:52 -05:00
Max d3719bd11c update TX rate conversion logic 2017-11-11 19:59:07 -05:00
Max 53dbfb4347 dv_tx.py crash bugfix 2017-11-10 22:08:07 -05:00
Max 43a70b67db rx.py AF modes bugfix 2017-11-09 21:12:46 -05:00
Max 8fc1995181 p25 tx touch up 2017-11-09 21:12:03 -05:00
Max a3cab238b5 d2460 2017-11-07 10:41:03 -05:00
Max e2cfbe9d1e dstar gain 2017-11-02 20:39:23 -04:00
Max 9858e0cecc dstar alt interleave 2017-11-02 20:31:01 -04:00
Max 9f6c73e280 build type debug 2017-11-02 20:28:34 -04:00
Max 7ae554682a install.sh update 2017-10-18 20:03:39 -04:00
Max aa7a4be349 doc.html updates 2017-10-18 20:02:58 -04:00
Max 87f546a947 scope.py message update 2017-10-18 19:11:53 -04:00
Max e7d67538ae fix excessive cpu usage in datascope plot 2017-10-18 17:45:20 -04:00
Max 77b21b5e32 revert gr_gnuplot commit 2017-10-18 17:34:52 -04:00
Max e31207cf88 plot module enhancements thx Graham 2017-10-17 22:32:04 -04:00
Max 6ddc299edf update doc.html 2017-10-17 22:17:44 -04:00
Max c412a8fd5b update doc.html 2017-10-17 22:15:09 -04:00
Max bbe9506b46 dummy scope.py 2017-10-17 21:43:42 -04:00
Max bb38456f9c one tx to rule them all: multi_tx.py 2017-10-17 21:35:12 -04:00
Max c8cf7a7da8 dv_tx.py updates 2017-10-17 21:34:50 -04:00
Max a9522ee636 set_gain_adjust patch 2017-10-17 16:58:56 -04:00
Max 15f0acfa17 patch to fix compile errors thx Graham 2017-10-11 16:28:32 -04:00
Max 37f2c3bbfc modulator filter updates 2017-10-11 14:59:24 -04:00
Max 68476ab6c1 dstar update 2017-10-05 19:15:17 -04:00
Max 20743ddc60 dstar update 2017-10-05 19:13:47 -04:00
Max a3f4963dea forgot to add this 2017-09-06 08:20:15 -04:00
Max 04b16056df possible fix for compiler errors 2017-09-05 16:47:45 -04:00
Max 93b19531ee big patch from Graham - many thx 2017-09-05 16:42:55 -04:00
Max 187f5d180c possible race condition 2017-07-05 16:51:25 -04:00
Max 2f3eb8908a update readme 2017-07-05 16:51:07 -04:00
Max 99926acda5 install.sh 2017-06-30 10:25:45 -04:00
Max 8f79322244 add fft and catch exceptions 2017-05-10 09:45:34 -04:00
Max 8c98a8be52 patch to clear tgid at end of call thx Graham (boadbod) 2017-05-04 21:55:47 -04:00
Max b5ec34562b patch for p25p2 audio thx Graham (boatbod) 2017-05-03 20:53:50 -04:00
Max d0eacef338 readme 2017-04-30 11:13:47 -04:00
Max e9911c5df8 rx.py replaces scope.py 2017-04-29 15:57:35 -04:00
Max 45794418ae update copyright 2017-04-29 15:12:10 -04:00
Max e6deba2cd5 frame assembler sink-only mode 2017-04-29 12:32:47 -04:00
Max 59a78e3671 add hold command 2017-04-29 12:30:33 -04:00
Max 10aaadf1cd trunking.py updates 2017-04-25 19:59:35 -04:00
Max bcef5dfc29 update help text 2017-04-02 09:32:21 -04:00
Max 4ece3269e3 add spectrum zoom 2017-04-02 09:31:40 -04:00
Max 596fe4a733 doc update for osmosdr device support 2017-04-01 17:28:17 -04:00
Max c348afa99e compile error fix 2017-04-01 17:21:39 -04:00
Max e383a7a719 modulator test input files 2017-04-01 17:21:02 -04:00
Max 11cd46b8e7 add osmosdr device support 2017-04-01 17:20:39 -04:00
Max ff3a65028e fix AF gain in disc tap mode 2017-03-20 10:55:43 -04:00
Max 82cf2c9217 doc.html update 2017-03-20 10:52:00 -04:00
Max 5972402ff5 doc.html update 2017-03-19 17:29:24 -04:00
Max 8b96413093 audio input attenuator hack 2017-03-19 17:09:05 -04:00
Max f9d7b65cc3 doc.html 2017-03-14 23:42:01 -04:00
Max 299d5eb9bf dv_tx.py audio cleanup 2017-03-12 18:03:21 -04:00
Max b6390b9d17 bugfix 2017-03-12 01:18:14 -05:00
Max f6693ab481 dv_tx.py 2017-03-12 01:17:44 -05:00
Max 356ce4b7ac dstar cleanup 2017-03-10 10:44:17 -05:00
Max a964f6ac52 dstar gmsk modulation 2017-03-10 10:43:49 -05:00
Max f454b775ae ysf-cfg.dat 2017-03-09 23:20:19 -05:00
Max d403abb882 dstar cleanup 2017-03-08 10:08:38 -05:00
Max 89ef1f3297 initial dstar checkin 2017-03-06 09:17:50 -05:00
Max eb522cf5e5 initial dstar checkin 2017-03-06 09:17:16 -05:00
Max 8242b8e0fc fullrate patch 2017-02-27 15:40:27 -05:00
Max d0fd94fc2f bugfix 2017-02-26 21:57:19 -05:00
Max fa76f07e56 ysf tx initial checkin 2017-02-26 17:18:22 -05:00
Max cb410c1f95 ysf patch 2017-02-26 17:17:26 -05:00
Max 847621dd39 cleanup 2017-02-26 17:15:56 -05:00
Max 560f0c5461 dmr updates 2017-02-09 09:05:34 -05:00
Max b81ad173b1 get rid of offsets in tsv file, should be set to zero 2017-02-05 12:18:36 -05:00
Max 72090fddbb per channel enable in cfg 2017-02-02 10:43:43 -05:00
Max 089a8f9284 dmr cleanup 2017-02-01 11:59:44 -05:00
Max 02e0ad5814 add pop count algo 2017-01-30 16:07:46 -05:00
Max 668ff60e34 dmr bs tx initial checkin 2017-01-25 19:33:02 -05:00
Max aaee53c998 dmr bs tx patch 2017-01-25 19:24:10 -05:00
Max a101714375 typo correction 2017-01-25 19:21:31 -05:00
Max a83fbd8c86 ambe_encoder_sb block 2017-01-20 13:47:25 -05:00
Max 7214bc2614 ambe encoder 2016-12-18 10:21:10 -05:00
Max 0bfa16971f ambe encoder patch 2016-12-18 10:20:08 -05:00
Max 3a558b03b2 bugfix 2016-12-18 10:16:22 -05:00
Max a74d80c67c bugfix 2016-12-18 10:14:45 -05:00
Max e0a0636ee3 bugfix- uninitialized var 2016-11-03 16:28:19 -04:00
Max dc552b5d51 bugfix 2016-08-26 10:03:11 -04:00
Max d8ebe71374 experimental additions for explicit format voice grant 2016-08-25 17:31:02 -04:00
Max 158463961b initial fedora/wx3.0 patch 2016-02-16 14:16:31 -05:00
Balint Seeber e2cf7087fc Fixes for compilation under Linux 2015-11-24 23:18:12 -08:00
Max d34fbe52ed reduce skip time 2015-10-07 16:08:29 -04:00
Balint Seeber aea5479646 Big enhancements:
Decryption support (DES-OFB)
Fixes to GRC files
Optional logging to console
Optional disabling of output silence during idle time
More error correction
Decoding of LSDW
BCH error correction fix (after IT++ BCH breaking change)
Fix for GR 3.7 build process
2015-09-19 17:31:41 -07:00
328 changed files with 117222 additions and 5280 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore build artifacts
build/
# Ignore .pyc compiled python bytecode files
*.pyc

View File

@ -1,6 +1,27 @@
cmake_minimum_required(VERSION 2.6)
project(gr-op25 CXX C)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_CXX_FLAGS "-std=c++11")
execute_process(COMMAND python3 -c "
import os
import sys
from distutils import sysconfig
pfx = '/usr/local'
m1 = os.path.join('lib', 'python' + sys.version[:3], 'dist-packages')
m2 = sysconfig.get_python_lib(plat_specific=True, prefix='')
f1 = os.path.join(pfx, m1)
f2 = os.path.join(pfx, m2)
ok2 = f2 in sys.path
if ok2:
print(m2)
else:
print(m1)
" OUTPUT_VARIABLE OP25_PYTHON_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
)
MESSAGE(STATUS "OP25_PYTHON_DIR has been set to \"${OP25_PYTHON_DIR}\".")
add_subdirectory(op25/gr-op25)
add_subdirectory(op25/gr-op25_repeater)

23
README Normal file
View File

@ -0,0 +1,23 @@
As of this writing (Sept. 2022) OP25 builds for python3 and GNU Radio 3.8.
The full list of supported versions is as follows:
PYTHON 2 AND GNU RADIO 3.7
==========================
It should still be possible to use the file gr3.8.patch (in reverse) to
downgrade the source tree to build against Python 2 and GNU Radio 3.7,
although this has not been tested.
$ cat gr3.8.patch | patch -p1 -R
Once this has been done, proceed by running the install.sh script.
PYTHON 3 AND GNU RADIO 3.8
==========================
It is no longer necessary to apply the gr3.8 patch to the op25 source tree,
as Python3/GNU Radio 3.8 is now the default. You can proceed directly to
running the install.sh script.
PYTHON 3 AND GNU RADIO 3.9 / 3.10
=================================
It is no longer necessary to apply the gr3.8 patch to the op25 source tree,
See the file README-gr3.9 for procedures for both GNU Radio 3.9 and 3.10.

10
README-gr3.8.patch Normal file
View File

@ -0,0 +1,10 @@
Updated Sept. 2022
By default, OP25 now builds for Python3 and GNU Radio3.8.
Accordingly, it is no longer necessary to apply the op25 patch
for gr3.8.
It should be possible to use the gr3.8.patch in reverse to downgrade
the source tree to build for Python 2 and GNU Radio 3.7. See the
file README in this directory for details.

32
README-gr3.9 Normal file
View File

@ -0,0 +1,32 @@
Running OP25 in Gnuradio 3.9 and 3.10 Date: May 31, 2022
==============================================================
Installation
------------
0. First remove any existing OP25 installation
cd ...path-to-existing/op25/build
sudo make uninstall
1. Step 1 is removed.
2. Run the command
./install-gr3.9.sh
(Note, do not use the standard install.sh script for gr3.9 and/or gr3.10).
Bugs
----
1. The location for WIN_HANN, WIN_HAMMING, and so forth has moved to
fft.window (formerly filter.firdes). The python has not yet been
updated to reflect.
2. A strange hang condition has been observed in rx.py - the bug is not
fully understood. The strace output shows many futex calls ending
with a "timeout" - not clear if some missing event might be causing this
stall condition... User feedback is requested...
3. Currently the cmake process is not centralized; 'make uninstall' must
therefore be run once from each of the 'build' and 'build_repeater'
directories under 'src/'.

231
add_gr3.9.py Executable file
View File

@ -0,0 +1,231 @@
#! /usr/bin/python3
# Copyright 2022, Max H. Parke KA1RBI
#
# This file is part of GNU Radio and part of OP25
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
import sys
import os
import glob
import shutil
from gnuradio.modtool.core.newmod import ModToolNewModule
from gnuradio.modtool.core.add import ModToolAdd
from gnuradio.modtool.core.bind import ModToolGenBindings
msg = """
This 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, or (at your option)
any later version.
"""
print('\n%s Copyright 2022, Max H. Parke KA1RBI\nhttps://osmocom.org/projects/op25\n%s' % (sys.argv[0], msg))
TLD = 'op25'
MODS={
'op25': 'decoder_bf decoder_ff fsk4_demod_ff fsk4_slicer_fb pcap_source_b message msg_queue msg_handler'.split(),
'op25_repeater': 'ambe_encoder_sb dmr_bs_tx_bb dstar_tx_sb frame_assembler fsk4_slicer_fb gardner_costas_cc nxdn_tx_sb p25_frame_assembler vocoder ysf_tx_sb'.split()
}
SKIP_CC = 'd2460.cc qa_op25.cc test_op25.cc qa_op25_repeater.cc test_op25_repeater.cc message.cc msg_queue.cc msg_handler.cc'.split()
SRC_DIR = sys.argv[1]
DEST_DIR = sys.argv[2]
if '..' in SRC_DIR or not SRC_DIR.startswith('/'):
sys.stderr.write('error, %s must be an absolute path\n' % SRC_DIR)
sys.exit(1)
if not os.access(SRC_DIR, os.R_OK):
sys.stderr.write('error, unable to access %s\n' % SRC_DIR)
sys.exit(1)
if not os.path.isdir(SRC_DIR + '/op25/gr-op25_repeater'):
sys.stderr.write('error, op25 package not found in %s\n' % SRC_DIR)
sys.exit(3)
if os.access(DEST_DIR, os.F_OK) or os.path.isdir(DEST_DIR):
sys.stderr.write('error, destination path %s must not exist\n' % DEST_DIR)
sys.exit(4)
os.mkdir(DEST_DIR)
op25_dir = DEST_DIR + '/op25'
os.mkdir(op25_dir)
os.chdir(op25_dir)
SCRIPTS = SRC_DIR + '/scripts'
TXT = """add_library(op25-message SHARED message.cc msg_queue.cc msg_handler.cc)
install(TARGETS op25-message EXPORT op25-message-export DESTINATION lib)
install(EXPORT op25-message-export DESTINATION ${GR_CMAKE_DIR})
target_include_directories(op25-message
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
PUBLIC $<INSTALL_INTERFACE:include>
)
"""
def edit_cmake(filename, mod, srcfiles):
lines = open(filename).read().rstrip().split('\n')
srcdefs = []
state = 0
end_mark = 0
add_library = 0
tll = 0 # target_link_library
srcfiles = [s.split('/')[-1] for s in srcfiles if s.endswith('.cc') or s.endswith('.c') or s.endswith('.cpp')]
lines = [l for l in lines if l.strip() not in SKIP_CC]
for i in range(len(lines)):
if 'add_library' in lines[i] and 'gnuradio-op25' in lines[i]:
add_library = i
if lines[i].startswith('list(APPEND op25_') and ('_sources' in lines[i] or '_python_files' in lines[i]):
state = 1
continue
elif ')' in lines[i] and state:
state = 0
end_mark = i
continue
elif lines[i].startswith('target_link_libraries(gnuradio-op25'):
assert lines[i].endswith(')')
tll = i
continue
if state:
srcdefs.append(lines[i].strip())
srcfiles = [" %s" % s for s in srcfiles if s not in srcdefs and s not in SKIP_CC]
tlls = {
'op25': 'target_link_libraries(gnuradio-op25 gnuradio::gnuradio-runtime Boost::system Boost::program_options Boost::filesystem Boost::thread itpp pcap op25-message)',
'op25_repeater': 'target_link_libraries(gnuradio-op25_repeater PUBLIC gnuradio::gnuradio-runtime gnuradio::gnuradio-filter op25-message PRIVATE imbe_vocoder)'
}
assert tll # fail if target_link_libraries line not found
lines[tll] = tlls[mod]
if mod == 'op25_repeater':
lines = lines[:tll] + ['\n' + 'add_subdirectory(imbe_vocoder)\n'] + lines[tll:]
elif mod == 'op25':
assert add_library > 0
lines = lines[:add_library] + [s for s in TXT.split('\n')] + lines[add_library:]
new_lines = lines[:end_mark] + srcfiles + lines[end_mark:]
s = '\n'.join(new_lines)
s += '\n'
with open(filename, 'w') as fp:
fp.write(s)
def get_args_from_h(mod):
lines = open(mod).read().rstrip().split('\n')
lines = [line for line in lines if 'make' in line]
answer = []
for s in lines:
s = s.rstrip()
if s[-1] != ';':
continue
s = s[:-1]
s = s.rstrip()
if s[-1] != ')':
continue
s = s[:-1]
lp = s.find('(')
if lp > 0:
s =s[lp+1:]
else:
continue
for arg in s.split(','):
eq = arg.find('=')
if eq > 0:
arg = arg[:eq]
answer.append(arg)
return ','.join(answer)
return ''
for mod in sorted(MODS.keys()):
m = ModToolNewModule(module_name=mod, srcdir=None)
m.run()
print('gr_modtool newmod %s getcwd now %s' % (mod, os.getcwd()))
pfx = '%s/op25/gr-%s' % (SRC_DIR, mod)
lib = '%s/lib' % pfx
s_py = '%s/python/op25/bindings' % pfx
incl = '%s/include/%s' % (pfx, mod)
d_pfx = '%s/op25/gr-%s' % (DEST_DIR, mod)
d_lib = '%s/lib' % d_pfx
d_py = '%s/python/op25/bindings' % d_pfx
d_incl_alt1 = '%s/include/%s' % (d_pfx, mod)
d_incl_alt2 = '%s/include/gnuradio/%s' % (d_pfx, mod)
if os.path.isdir(d_incl_alt1):
d_incl = d_incl_alt1
elif os.path.isdir(d_incl_alt2):
d_incl = d_incl_alt2
sl = 'gnuradio/%s' % mod
os.symlink(sl, '%s/include/%s' % (d_pfx, mod))
if mod == 'op25_repeater':
p_pfx = '%s/op25/gr-%s' % (DEST_DIR, 'op25')
p_incl = '%s/include/%s' % (p_pfx, 'op25')
d = '/'.join(d_incl.split('/')[:-1])
os.symlink(p_incl, '%s/include/%s' % (d_pfx, 'op25'))
else:
sys.stderr.write('neither %s nor %s found, aborting\n' % (d_incl_alt1, d_incl_alt2))
sys.exit(1)
for block in MODS[mod]:
include = '%s/%s.h' % (incl, block)
args = get_args_from_h(include)
t = 'sync' if block == 'fsk4_slicer_fb' or block == 'pcap_source_b' else 'general'
if block == 'message' or block == 'msg_queue' or block == 'msg_handler':
t = 'noblock'
print ('add %s %s type %s directory %s args %s' % (mod, block, t, os.getcwd(), args))
m = ModToolAdd(blockname=block,block_type=t,lang='cpp',copyright='Steve Glass, OP25 Group', argument_list=args)
m.run()
srcfiles = []
srcfiles += glob.glob('%s/lib/*.cc' % pfx)
srcfiles += glob.glob('%s/lib/*.cpp' % pfx)
srcfiles += glob.glob('%s/lib/*.c' % pfx)
srcfiles += glob.glob('%s/lib/*.h' % pfx)
hfiles = glob.glob('%s/*.h' % incl)
assert os.path.isdir(d_lib)
assert os.path.isdir(d_incl)
for f in srcfiles:
shutil.copy(f, d_lib)
for f in hfiles:
shutil.copy(f, d_incl)
os.system('/bin/bash %s/%s %s' % (SCRIPTS, 'do_sedm.sh', d_incl))
if mod == 'op25_repeater':
for d in 'imbe_vocoder ezpwd'.split():
os.mkdir('%s/%s' % (d_lib, d))
imbefiles = []
imbefiles += glob.glob('%s/%s/*' % (lib, d))
dest = '%s/%s' % (d_lib, d)
for f in imbefiles:
shutil.copy(f, dest)
edit_cmake('%s/CMakeLists.txt' % d_lib, mod, srcfiles)
os.system('/bin/bash %s/do_sed.sh' % (SCRIPTS))
f = '%s/CMakeLists.txt' % (d_pfx)
if mod == 'op25':
exe = '%s/do_sedb.sh %s' % (SCRIPTS, f)
elif mod == 'op25_repeater':
exe = '%s/do_sedc.sh %s' % (SCRIPTS, f)
os.system('/bin/bash %s %s' % (exe, f))
os.system('/bin/bash %s/do_sedp.sh %s' % (SCRIPTS, f))
os.system('/bin/bash %s/do_sedp2.sh %s' % (SCRIPTS, d_pfx))
for block in MODS[mod]:
print ('bind %s %s' % (mod, block))
m = ModToolGenBindings(block, addl_includes='', define_symbols='', update_hash_only=False)
m.run()
if mod == 'op25':
py_cc_srcfiles = 'message_python.cc msg_handler_python.cc msg_queue_python.cc'.split()
for f in py_cc_srcfiles:
shutil.copy('%s/%s' % (s_py, f), d_py)
os.chdir(op25_dir)

View File

@ -1,210 +0,0 @@
# Copyright 2010-2011 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio 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, or (at your option)
# any later version.
#
# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
if(DEFINED __INCLUDED_GR_MISC_UTILS_CMAKE)
return()
endif()
set(__INCLUDED_GR_MISC_UTILS_CMAKE TRUE)
########################################################################
# Set global variable macro.
# Used for subdirectories to export settings.
# Example: include and library paths.
########################################################################
function(GR_SET_GLOBAL var)
set(${var} ${ARGN} CACHE INTERNAL "" FORCE)
endfunction(GR_SET_GLOBAL)
########################################################################
# Set the pre-processor definition if the condition is true.
# - def the pre-processor definition to set and condition name
########################################################################
function(GR_ADD_COND_DEF def)
if(${def})
add_definitions(-D${def})
endif(${def})
endfunction(GR_ADD_COND_DEF)
########################################################################
# Check for a header and conditionally set a compile define.
# - hdr the relative path to the header file
# - def the pre-processor definition to set
########################################################################
function(GR_CHECK_HDR_N_DEF hdr def)
include(CheckIncludeFileCXX)
CHECK_INCLUDE_FILE_CXX(${hdr} ${def})
GR_ADD_COND_DEF(${def})
endfunction(GR_CHECK_HDR_N_DEF)
########################################################################
# Include subdirectory macro.
# Sets the CMake directory variables,
# includes the subdirectory CMakeLists.txt,
# resets the CMake directory variables.
#
# This macro includes subdirectories rather than adding them
# so that the subdirectory can affect variables in the level above.
# This provides a work-around for the lack of convenience libraries.
# This way a subdirectory can append to the list of library sources.
########################################################################
macro(GR_INCLUDE_SUBDIRECTORY subdir)
#insert the current directories on the front of the list
list(INSERT _cmake_source_dirs 0 ${CMAKE_CURRENT_SOURCE_DIR})
list(INSERT _cmake_binary_dirs 0 ${CMAKE_CURRENT_BINARY_DIR})
#set the current directories to the names of the subdirs
set(CMAKE_CURRENT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${subdir})
set(CMAKE_CURRENT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${subdir})
#include the subdirectory CMakeLists to run it
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
include(${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt)
#reset the value of the current directories
list(GET _cmake_source_dirs 0 CMAKE_CURRENT_SOURCE_DIR)
list(GET _cmake_binary_dirs 0 CMAKE_CURRENT_BINARY_DIR)
#pop the subdir names of the front of the list
list(REMOVE_AT _cmake_source_dirs 0)
list(REMOVE_AT _cmake_binary_dirs 0)
endmacro(GR_INCLUDE_SUBDIRECTORY)
########################################################################
# Check if a compiler flag works and conditionally set a compile define.
# - flag the compiler flag to check for
# - have the variable to set with result
########################################################################
macro(GR_ADD_CXX_COMPILER_FLAG_IF_AVAILABLE flag have)
include(CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG(${flag} ${have})
if(${have})
add_definitions(${flag})
endif(${have})
endmacro(GR_ADD_CXX_COMPILER_FLAG_IF_AVAILABLE)
########################################################################
# Generates the .la libtool file
# This appears to generate libtool files that cannot be used by auto*.
# Usage GR_LIBTOOL(TARGET [target] DESTINATION [dest])
# Notice: there is not COMPONENT option, these will not get distributed.
########################################################################
function(GR_LIBTOOL)
if(NOT DEFINED GENERATE_LIBTOOL)
set(GENERATE_LIBTOOL OFF) #disabled by default
endif()
if(GENERATE_LIBTOOL)
include(CMakeParseArgumentsCopy)
CMAKE_PARSE_ARGUMENTS(GR_LIBTOOL "" "TARGET;DESTINATION" "" ${ARGN})
find_program(LIBTOOL libtool)
if(LIBTOOL)
include(CMakeMacroLibtoolFile)
CREATE_LIBTOOL_FILE(${GR_LIBTOOL_TARGET} /${GR_LIBTOOL_DESTINATION})
endif(LIBTOOL)
endif(GENERATE_LIBTOOL)
endfunction(GR_LIBTOOL)
########################################################################
# Do standard things to the library target
# - set target properties
# - make install rules
# Also handle gnuradio custom naming conventions w/ extras mode.
########################################################################
function(GR_LIBRARY_FOO target)
#parse the arguments for component names
include(CMakeParseArgumentsCopy)
CMAKE_PARSE_ARGUMENTS(GR_LIBRARY "" "RUNTIME_COMPONENT;DEVEL_COMPONENT" "" ${ARGN})
#set additional target properties
set_target_properties(${target} PROPERTIES SOVERSION ${LIBVER})
#install the generated files like so...
install(TARGETS ${target}
LIBRARY DESTINATION ${GR_LIBRARY_DIR} COMPONENT ${GR_LIBRARY_RUNTIME_COMPONENT} # .so/.dylib file
ARCHIVE DESTINATION ${GR_LIBRARY_DIR} COMPONENT ${GR_LIBRARY_DEVEL_COMPONENT} # .lib file
RUNTIME DESTINATION ${GR_RUNTIME_DIR} COMPONENT ${GR_LIBRARY_RUNTIME_COMPONENT} # .dll file
)
#extras mode enabled automatically on linux
if(NOT DEFINED LIBRARY_EXTRAS)
set(LIBRARY_EXTRAS ${LINUX})
endif()
#special extras mode to enable alternative naming conventions
if(LIBRARY_EXTRAS)
#create .la file before changing props
GR_LIBTOOL(TARGET ${target} DESTINATION ${GR_LIBRARY_DIR})
#give the library a special name with ultra-zero soversion
set_target_properties(${target} PROPERTIES LIBRARY_OUTPUT_NAME ${target}-${LIBVER} SOVERSION "0.0.0")
set(target_name lib${target}-${LIBVER}.so.0.0.0)
#custom command to generate symlinks
add_custom_command(
TARGET ${target}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink ${target_name} ${CMAKE_CURRENT_BINARY_DIR}/lib${target}.so
COMMAND ${CMAKE_COMMAND} -E create_symlink ${target_name} ${CMAKE_CURRENT_BINARY_DIR}/lib${target}-${LIBVER}.so.0
COMMAND ${CMAKE_COMMAND} -E touch ${target_name} #so the symlinks point to something valid so cmake 2.6 will install
)
#and install the extra symlinks
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/lib${target}.so
${CMAKE_CURRENT_BINARY_DIR}/lib${target}-${LIBVER}.so.0
DESTINATION ${GR_LIBRARY_DIR} COMPONENT ${GR_LIBRARY_RUNTIME_COMPONENT}
)
endif(LIBRARY_EXTRAS)
endfunction(GR_LIBRARY_FOO)
########################################################################
# Create a dummy custom command that depends on other targets.
# Usage:
# GR_GEN_TARGET_DEPS(unique_name target_deps <target1> <target2> ...)
# ADD_CUSTOM_COMMAND(<the usual args> ${target_deps})
#
# Custom command cant depend on targets, but can depend on executables,
# and executables can depend on targets. So this is the process:
########################################################################
function(GR_GEN_TARGET_DEPS name var)
file(
WRITE ${CMAKE_CURRENT_BINARY_DIR}/${name}.cpp.in
"int main(void){return 0;}\n"
)
execute_process(
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_BINARY_DIR}/${name}.cpp.in
${CMAKE_CURRENT_BINARY_DIR}/${name}.cpp
)
add_executable(${name} ${CMAKE_CURRENT_BINARY_DIR}/${name}.cpp)
if(ARGN)
add_dependencies(${name} ${ARGN})
endif(ARGN)
if(CMAKE_CROSSCOMPILING)
set(${var} "DEPENDS;${name}" PARENT_SCOPE) #cant call command when cross
else()
set(${var} "DEPENDS;${name};COMMAND;${name}" PARENT_SCOPE)
endif()
endfunction(GR_GEN_TARGET_DEPS)

View File

@ -1,46 +0,0 @@
# Copyright 2011 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio 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, or (at your option)
# any later version.
#
# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
if(DEFINED __INCLUDED_GR_PLATFORM_CMAKE)
return()
endif()
set(__INCLUDED_GR_PLATFORM_CMAKE TRUE)
########################################################################
# Setup additional defines for OS types
########################################################################
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LINUX TRUE)
endif()
if(LINUX AND EXISTS "/etc/debian_version")
set(DEBIAN TRUE)
endif()
if(LINUX AND EXISTS "/etc/redhat-release")
set(REDHAT TRUE)
endif()
########################################################################
# when the library suffix should be 64 (applies to redhat linux family)
########################################################################
if(NOT DEFINED LIB_SUFFIX AND REDHAT AND CMAKE_SYSTEM_PROCESSOR MATCHES "64$")
set(LIB_SUFFIX 64)
endif()
set(LIB_SUFFIX ${LIB_SUFFIX} CACHE STRING "lib directory suffix")

View File

@ -1,227 +0,0 @@
# Copyright 2010-2011 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio 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, or (at your option)
# any later version.
#
# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
if(DEFINED __INCLUDED_GR_PYTHON_CMAKE)
return()
endif()
set(__INCLUDED_GR_PYTHON_CMAKE TRUE)
########################################################################
# Setup the python interpreter:
# This allows the user to specify a specific interpreter,
# or finds the interpreter via the built-in cmake module.
########################################################################
#this allows the user to override PYTHON_EXECUTABLE
if(PYTHON_EXECUTABLE)
set(PYTHONINTERP_FOUND TRUE)
#otherwise if not set, try to automatically find it
else(PYTHON_EXECUTABLE)
#use the built-in find script
find_package(PythonInterp 2)
#and if that fails use the find program routine
if(NOT PYTHONINTERP_FOUND)
find_program(PYTHON_EXECUTABLE NAMES python python2 python2.7 python2.6 python2.5)
if(PYTHON_EXECUTABLE)
set(PYTHONINTERP_FOUND TRUE)
endif(PYTHON_EXECUTABLE)
endif(NOT PYTHONINTERP_FOUND)
endif(PYTHON_EXECUTABLE)
#make the path to the executable appear in the cmake gui
set(PYTHON_EXECUTABLE ${PYTHON_EXECUTABLE} CACHE FILEPATH "python interpreter")
#make sure we can use -B with python (introduced in 2.6)
if(PYTHON_EXECUTABLE)
execute_process(
COMMAND ${PYTHON_EXECUTABLE} -B -c ""
OUTPUT_QUIET ERROR_QUIET
RESULT_VARIABLE PYTHON_HAS_DASH_B_RESULT
)
if(PYTHON_HAS_DASH_B_RESULT EQUAL 0)
set(PYTHON_DASH_B "-B")
endif()
endif(PYTHON_EXECUTABLE)
########################################################################
# Check for the existence of a python module:
# - desc a string description of the check
# - mod the name of the module to import
# - cmd an additional command to run
# - have the result variable to set
########################################################################
macro(GR_PYTHON_CHECK_MODULE desc mod cmd have)
message(STATUS "")
message(STATUS "Python checking for ${desc}")
execute_process(
COMMAND ${PYTHON_EXECUTABLE} -c "
#########################################
try: import ${mod}
except: exit(-1)
try: assert ${cmd}
except: exit(-1)
#########################################"
RESULT_VARIABLE ${have}
)
if(${have} EQUAL 0)
message(STATUS "Python checking for ${desc} - found")
set(${have} TRUE)
else(${have} EQUAL 0)
message(STATUS "Python checking for ${desc} - not found")
set(${have} FALSE)
endif(${have} EQUAL 0)
endmacro(GR_PYTHON_CHECK_MODULE)
########################################################################
# Sets the python installation directory GR_PYTHON_DIR
########################################################################
execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "
from distutils import sysconfig
print sysconfig.get_python_lib(plat_specific=True, prefix='')
" OUTPUT_VARIABLE GR_PYTHON_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
)
file(TO_CMAKE_PATH ${GR_PYTHON_DIR} GR_PYTHON_DIR)
########################################################################
# Create an always-built target with a unique name
# Usage: GR_UNIQUE_TARGET(<description> <dependencies list>)
########################################################################
function(GR_UNIQUE_TARGET desc)
file(RELATIVE_PATH reldir ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR})
execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "import re, hashlib
unique = hashlib.md5('${reldir}${ARGN}').hexdigest()[:5]
print(re.sub('\\W', '_', '${desc} ${reldir} ' + unique))"
OUTPUT_VARIABLE _target OUTPUT_STRIP_TRAILING_WHITESPACE)
add_custom_target(${_target} ALL DEPENDS ${ARGN})
endfunction(GR_UNIQUE_TARGET)
########################################################################
# Install python sources (also builds and installs byte-compiled python)
########################################################################
function(GR_PYTHON_INSTALL)
include(CMakeParseArgumentsCopy)
CMAKE_PARSE_ARGUMENTS(GR_PYTHON_INSTALL "" "DESTINATION;COMPONENT" "FILES;PROGRAMS" ${ARGN})
####################################################################
if(GR_PYTHON_INSTALL_FILES)
####################################################################
install(${ARGN}) #installs regular python files
#create a list of all generated files
unset(pysrcfiles)
unset(pycfiles)
unset(pyofiles)
foreach(pyfile ${GR_PYTHON_INSTALL_FILES})
get_filename_component(pyfile ${pyfile} ABSOLUTE)
list(APPEND pysrcfiles ${pyfile})
#determine if this file is in the source or binary directory
file(RELATIVE_PATH source_rel_path ${CMAKE_CURRENT_SOURCE_DIR} ${pyfile})
string(LENGTH "${source_rel_path}" source_rel_path_len)
file(RELATIVE_PATH binary_rel_path ${CMAKE_CURRENT_BINARY_DIR} ${pyfile})
string(LENGTH "${binary_rel_path}" binary_rel_path_len)
#and set the generated path appropriately
if(${source_rel_path_len} GREATER ${binary_rel_path_len})
set(pygenfile ${CMAKE_CURRENT_BINARY_DIR}/${binary_rel_path})
else()
set(pygenfile ${CMAKE_CURRENT_BINARY_DIR}/${source_rel_path})
endif()
list(APPEND pycfiles ${pygenfile}c)
list(APPEND pyofiles ${pygenfile}o)
#ensure generation path exists
get_filename_component(pygen_path ${pygenfile} PATH)
file(MAKE_DIRECTORY ${pygen_path})
endforeach(pyfile)
#the command to generate the pyc files
add_custom_command(
DEPENDS ${pysrcfiles} OUTPUT ${pycfiles}
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_BINARY_DIR}/python_compile_helper.py ${pysrcfiles} ${pycfiles}
)
#the command to generate the pyo files
add_custom_command(
DEPENDS ${pysrcfiles} OUTPUT ${pyofiles}
COMMAND ${PYTHON_EXECUTABLE} -O ${CMAKE_BINARY_DIR}/python_compile_helper.py ${pysrcfiles} ${pyofiles}
)
#create install rule and add generated files to target list
set(python_install_gen_targets ${pycfiles} ${pyofiles})
install(FILES ${python_install_gen_targets}
DESTINATION ${GR_PYTHON_INSTALL_DESTINATION}
COMPONENT ${GR_PYTHON_INSTALL_COMPONENT}
)
####################################################################
elseif(GR_PYTHON_INSTALL_PROGRAMS)
####################################################################
file(TO_NATIVE_PATH ${PYTHON_EXECUTABLE} pyexe_native)
foreach(pyfile ${GR_PYTHON_INSTALL_PROGRAMS})
get_filename_component(pyfile_name ${pyfile} NAME)
get_filename_component(pyfile ${pyfile} ABSOLUTE)
string(REPLACE "${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}" pyexefile "${pyfile}.exe")
list(APPEND python_install_gen_targets ${pyexefile})
get_filename_component(pyexefile_path ${pyexefile} PATH)
file(MAKE_DIRECTORY ${pyexefile_path})
add_custom_command(
OUTPUT ${pyexefile} DEPENDS ${pyfile}
COMMAND ${PYTHON_EXECUTABLE} -c
\"open('${pyexefile}', 'w').write('\#!${pyexe_native}\\n'+open('${pyfile}').read())\"
COMMENT "Shebangin ${pyfile_name}"
)
#on windows, python files need an extension to execute
get_filename_component(pyfile_ext ${pyfile} EXT)
if(WIN32 AND NOT pyfile_ext)
set(pyfile_name "${pyfile_name}.py")
endif()
install(PROGRAMS ${pyexefile} RENAME ${pyfile_name}
DESTINATION ${GR_PYTHON_INSTALL_DESTINATION}
COMPONENT ${GR_PYTHON_INSTALL_COMPONENT}
)
endforeach(pyfile)
endif()
GR_UNIQUE_TARGET("pygen" ${python_install_gen_targets})
endfunction(GR_PYTHON_INSTALL)
########################################################################
# Write the python helper script that generates byte code files
########################################################################
file(WRITE ${CMAKE_BINARY_DIR}/python_compile_helper.py "
import sys, py_compile
files = sys.argv[1:]
srcs, gens = files[:len(files)/2], files[len(files)/2:]
for src, gen in zip(srcs, gens):
py_compile.compile(file=src, cfile=gen, doraise=True)
")

View File

@ -1,229 +0,0 @@
# Copyright 2010-2011 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio 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, or (at your option)
# any later version.
#
# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
if(DEFINED __INCLUDED_GR_SWIG_CMAKE)
return()
endif()
set(__INCLUDED_GR_SWIG_CMAKE TRUE)
include(GrPython)
########################################################################
# Builds a swig documentation file to be generated into python docstrings
# Usage: GR_SWIG_MAKE_DOCS(output_file input_path input_path....)
#
# Set the following variable to specify extra dependent targets:
# - GR_SWIG_DOCS_SOURCE_DEPS
# - GR_SWIG_DOCS_TARGET_DEPS
########################################################################
function(GR_SWIG_MAKE_DOCS output_file)
find_package(Doxygen)
if(DOXYGEN_FOUND)
#setup the input files variable list, quote formated
set(input_files)
unset(INPUT_PATHS)
foreach(input_path ${ARGN})
if (IS_DIRECTORY ${input_path}) #when input path is a directory
file(GLOB input_path_h_files ${input_path}/*.h)
else() #otherwise its just a file, no glob
set(input_path_h_files ${input_path})
endif()
list(APPEND input_files ${input_path_h_files})
set(INPUT_PATHS "${INPUT_PATHS} \"${input_path}\"")
endforeach(input_path)
#determine the output directory
get_filename_component(name ${output_file} NAME_WE)
get_filename_component(OUTPUT_DIRECTORY ${output_file} PATH)
set(OUTPUT_DIRECTORY ${OUTPUT_DIRECTORY}/${name}_swig_docs)
make_directory(${OUTPUT_DIRECTORY})
#generate the Doxyfile used by doxygen
configure_file(
${CMAKE_SOURCE_DIR}/docs/doxygen/Doxyfile.swig_doc.in
${OUTPUT_DIRECTORY}/Doxyfile
@ONLY)
#Create a dummy custom command that depends on other targets
include(GrMiscUtils)
GR_GEN_TARGET_DEPS(_${name}_tag tag_deps ${GR_SWIG_DOCS_TARGET_DEPS})
#call doxygen on the Doxyfile + input headers
add_custom_command(
OUTPUT ${OUTPUT_DIRECTORY}/xml/index.xml
DEPENDS ${input_files} ${GR_SWIG_DOCS_SOURCE_DEPS} ${tag_deps}
COMMAND ${DOXYGEN_EXECUTABLE} ${OUTPUT_DIRECTORY}/Doxyfile
COMMENT "Generating doxygen xml for ${name} docs"
)
#call the swig_doc script on the xml files
add_custom_command(
OUTPUT ${output_file}
DEPENDS ${input_files} ${OUTPUT_DIRECTORY}/xml/index.xml
COMMAND ${PYTHON_EXECUTABLE} ${PYTHON_DASH_B}
${CMAKE_SOURCE_DIR}/docs/doxygen/swig_doc.py
${OUTPUT_DIRECTORY}/xml
${output_file}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/docs/doxygen
)
else(DOXYGEN_FOUND)
file(WRITE ${output_file} "\n") #no doxygen -> empty file
endif(DOXYGEN_FOUND)
endfunction(GR_SWIG_MAKE_DOCS)
########################################################################
# Build a swig target for the common gnuradio use case. Usage:
# GR_SWIG_MAKE(target ifile ifile ifile...)
#
# Set the following variables before calling:
# - GR_SWIG_FLAGS
# - GR_SWIG_INCLUDE_DIRS
# - GR_SWIG_LIBRARIES
# - GR_SWIG_SOURCE_DEPS
# - GR_SWIG_TARGET_DEPS
# - GR_SWIG_DOC_FILE
# - GR_SWIG_DOC_DIRS
########################################################################
macro(GR_SWIG_MAKE name)
set(ifiles ${ARGN})
#do swig doc generation if specified
if (GR_SWIG_DOC_FILE)
set(GR_SWIG_DOCS_SOURCE_DEPS ${GR_SWIG_SOURCE_DEPS})
set(GR_SWIG_DOCS_TAREGT_DEPS ${GR_SWIG_TARGET_DEPS})
GR_SWIG_MAKE_DOCS(${GR_SWIG_DOC_FILE} ${GR_SWIG_DOC_DIRS})
list(APPEND GR_SWIG_SOURCE_DEPS ${GR_SWIG_DOC_FILE})
endif()
#append additional include directories
find_package(PythonLibs 2)
list(APPEND GR_SWIG_INCLUDE_DIRS ${PYTHON_INCLUDE_PATH}) #deprecated name (now dirs)
list(APPEND GR_SWIG_INCLUDE_DIRS ${PYTHON_INCLUDE_DIRS})
list(APPEND GR_SWIG_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR})
list(APPEND GR_SWIG_INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR})
#determine include dependencies for swig file
execute_process(
COMMAND ${PYTHON_EXECUTABLE}
${CMAKE_BINARY_DIR}/get_swig_deps.py
"${ifiles}" "${GR_SWIG_INCLUDE_DIRS}"
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE SWIG_MODULE_${name}_EXTRA_DEPS
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
#Create a dummy custom command that depends on other targets
include(GrMiscUtils)
GR_GEN_TARGET_DEPS(_${name}_swig_tag tag_deps ${GR_SWIG_TARGET_DEPS})
set(tag_file ${CMAKE_CURRENT_BINARY_DIR}/${name}.tag)
add_custom_command(
OUTPUT ${tag_file}
DEPENDS ${GR_SWIG_SOURCE_DEPS} ${tag_deps}
COMMAND ${CMAKE_COMMAND} -E touch ${tag_file}
)
#append the specified include directories
include_directories(${GR_SWIG_INCLUDE_DIRS})
list(APPEND SWIG_MODULE_${name}_EXTRA_DEPS ${tag_file})
#setup the swig flags with flags and include directories
set(CMAKE_SWIG_FLAGS -fvirtual -modern -keyword -w511 -module ${name} ${GR_SWIG_FLAGS})
foreach(dir ${GR_SWIG_INCLUDE_DIRS})
list(APPEND CMAKE_SWIG_FLAGS "-I${dir}")
endforeach(dir)
#set the C++ property on the swig .i file so it builds
set_source_files_properties(${ifiles} PROPERTIES CPLUSPLUS ON)
#setup the actual swig library target to be built
include(UseSWIG)
SWIG_ADD_MODULE(${name} python ${ifiles})
SWIG_LINK_LIBRARIES(${name} ${PYTHON_LIBRARIES} ${GR_SWIG_LIBRARIES})
endmacro(GR_SWIG_MAKE)
########################################################################
# Install swig targets generated by GR_SWIG_MAKE. Usage:
# GR_SWIG_INSTALL(
# TARGETS target target target...
# [DESTINATION destination]
# [COMPONENT component]
# )
########################################################################
macro(GR_SWIG_INSTALL)
include(CMakeParseArgumentsCopy)
CMAKE_PARSE_ARGUMENTS(GR_SWIG_INSTALL "" "DESTINATION;COMPONENT" "TARGETS" ${ARGN})
foreach(name ${GR_SWIG_INSTALL_TARGETS})
install(TARGETS ${SWIG_MODULE_${name}_REAL_NAME}
DESTINATION ${GR_SWIG_INSTALL_DESTINATION}
COMPONENT ${GR_SWIG_INSTALL_COMPONENT}
)
include(GrPython)
GR_PYTHON_INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/${name}.py
DESTINATION ${GR_SWIG_INSTALL_DESTINATION}
COMPONENT ${GR_SWIG_INSTALL_COMPONENT}
)
GR_LIBTOOL(
TARGET ${SWIG_MODULE_${name}_REAL_NAME}
DESTINATION ${GR_SWIG_INSTALL_DESTINATION}
)
endforeach(name)
endmacro(GR_SWIG_INSTALL)
########################################################################
# Generate a python file that can determine swig dependencies.
# Used by the make macro above to determine extra dependencies.
# When you build C++, CMake figures out the header dependencies.
# This code essentially performs that logic for swig includes.
########################################################################
file(WRITE ${CMAKE_BINARY_DIR}/get_swig_deps.py "
import os, sys, re
include_matcher = re.compile('[#|%]include\\s*[<|\"](.*)[>|\"]')
include_dirs = sys.argv[2].split(';')
def get_swig_incs(file_path):
file_contents = open(file_path, 'r').read()
return include_matcher.findall(file_contents, re.MULTILINE)
def get_swig_deps(file_path, level):
deps = [file_path]
if level == 0: return deps
for inc_file in get_swig_incs(file_path):
for inc_dir in include_dirs:
inc_path = os.path.join(inc_dir, inc_file)
if not os.path.exists(inc_path): continue
deps.extend(get_swig_deps(inc_path, level-1))
return deps
if __name__ == '__main__':
ifiles = sys.argv[1].split(';')
deps = sum([get_swig_deps(ifile, 3) for ifile in ifiles], [])
#sys.stderr.write(';'.join(set(deps)) + '\\n\\n')
print(';'.join(set(deps)))
")

View File

@ -1,133 +0,0 @@
# Copyright 2010-2011 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio 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, or (at your option)
# any later version.
#
# GNU Radio 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 GNU Radio; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
if(DEFINED __INCLUDED_GR_TEST_CMAKE)
return()
endif()
set(__INCLUDED_GR_TEST_CMAKE TRUE)
########################################################################
# Add a unit test and setup the environment for a unit test.
# Takes the same arguments as the ADD_TEST function.
#
# Before calling set the following variables:
# GR_TEST_TARGET_DEPS - built targets for the library path
# GR_TEST_LIBRARY_DIRS - directories for the library path
# GR_TEST_PYTHON_DIRS - directories for the python path
########################################################################
function(GR_ADD_TEST test_name)
if(WIN32)
#Ensure that the build exe also appears in the PATH.
list(APPEND GR_TEST_TARGET_DEPS ${ARGN})
#In the land of windows, all libraries must be in the PATH.
#Since the dependent libraries are not yet installed,
#we must manually set them in the PATH to run tests.
#The following appends the path of a target dependency.
foreach(target ${GR_TEST_TARGET_DEPS})
get_target_property(location ${target} LOCATION)
if(location)
get_filename_component(path ${location} PATH)
string(REGEX REPLACE "\\$\\(.*\\)" ${CMAKE_BUILD_TYPE} path ${path})
list(APPEND GR_TEST_LIBRARY_DIRS ${path})
endif(location)
endforeach(target)
#SWIG generates the python library files into a subdirectory.
#Therefore, we must append this subdirectory into PYTHONPATH.
#Only do this for the python directories matching the following:
foreach(pydir ${GR_TEST_PYTHON_DIRS})
get_filename_component(name ${pydir} NAME)
if(name MATCHES "^(swig|lib|src)$")
list(APPEND GR_TEST_PYTHON_DIRS ${pydir}/${CMAKE_BUILD_TYPE})
endif()
endforeach(pydir)
endif(WIN32)
file(TO_NATIVE_PATH ${CMAKE_CURRENT_SOURCE_DIR} srcdir)
file(TO_NATIVE_PATH "${GR_TEST_LIBRARY_DIRS}" libpath) #ok to use on dir list?
file(TO_NATIVE_PATH "${GR_TEST_PYTHON_DIRS}" pypath) #ok to use on dir list?
set(environs "GR_DONT_LOAD_PREFS=1" "srcdir=${srcdir}")
#http://www.cmake.org/pipermail/cmake/2009-May/029464.html
#Replaced this add test + set environs code with the shell script generation.
#Its nicer to be able to manually run the shell script to diagnose problems.
#ADD_TEST(${ARGV})
#SET_TESTS_PROPERTIES(${test_name} PROPERTIES ENVIRONMENT "${environs}")
if(UNIX)
set(binpath "${CMAKE_CURRENT_BINARY_DIR}:$PATH")
#set both LD and DYLD paths to cover multiple UNIX OS library paths
list(APPEND libpath "$LD_LIBRARY_PATH" "$DYLD_LIBRARY_PATH")
list(APPEND pypath "$PYTHONPATH")
#replace list separator with the path separator
string(REPLACE ";" ":" libpath "${libpath}")
string(REPLACE ";" ":" pypath "${pypath}")
list(APPEND environs "PATH=${binpath}" "LD_LIBRARY_PATH=${libpath}" "DYLD_LIBRARY_PATH=${libpath}" "PYTHONPATH=${pypath}")
#generate a bat file that sets the environment and runs the test
find_program(SHELL sh)
set(sh_file ${CMAKE_CURRENT_BINARY_DIR}/${test_name}_test.sh)
file(WRITE ${sh_file} "#!${SHELL}\n")
#each line sets an environment variable
foreach(environ ${environs})
file(APPEND ${sh_file} "export ${environ}\n")
endforeach(environ)
#load the command to run with its arguments
foreach(arg ${ARGN})
file(APPEND ${sh_file} "${arg} ")
endforeach(arg)
file(APPEND ${sh_file} "\n")
#make the shell file executable
execute_process(COMMAND chmod +x ${sh_file})
add_test(${test_name} ${SHELL} ${sh_file})
endif(UNIX)
if(WIN32)
list(APPEND libpath ${DLL_PATHS} "%PATH%")
list(APPEND pypath "%PYTHONPATH%")
#replace list separator with the path separator (escaped)
string(REPLACE ";" "\\;" libpath "${libpath}")
string(REPLACE ";" "\\;" pypath "${pypath}")
list(APPEND environs "PATH=${libpath}" "PYTHONPATH=${pypath}")
#generate a bat file that sets the environment and runs the test
set(bat_file ${CMAKE_CURRENT_BINARY_DIR}/${test_name}_test.bat)
file(WRITE ${bat_file} "@echo off\n")
#each line sets an environment variable
foreach(environ ${environs})
file(APPEND ${bat_file} "SET ${environ}\n")
endforeach(environ)
#load the command to run with its arguments
foreach(arg ${ARGN})
file(APPEND ${bat_file} "${arg} ")
endforeach(arg)
file(APPEND ${bat_file} "\n")
add_test(${test_name} ${bat_file})
endif(WIN32)
endfunction(GR_ADD_TEST)

367
gr3.8.patch Normal file
View File

@ -0,0 +1,367 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 56f95f4..c43483a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,6 +4,24 @@ project(gr-op25 CXX C)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_CXX_FLAGS "-std=c++11")
+execute_process(COMMAND python3 -c "
+import os
+import sys
+from distutils import sysconfig
+pfx = '/usr/local'
+m1 = os.path.join('lib', 'python' + sys.version[:3], 'dist-packages')
+m2 = sysconfig.get_python_lib(plat_specific=True, prefix='')
+f1 = os.path.join(pfx, m1)
+f2 = os.path.join(pfx, m2)
+ok2 = f2 in sys.path
+if ok2:
+ print(m2)
+else:
+ print(m1)
+" OUTPUT_VARIABLE OP25_PYTHON_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
+)
+MESSAGE(STATUS "OP25_PYTHON_DIR has been set to \"${OP25_PYTHON_DIR}\".")
+
add_subdirectory(op25/gr-op25)
add_subdirectory(op25/gr-op25_repeater)
diff --git a/install.sh b/install.sh
index 10fbeb5..4246447 100755
--- a/install.sh
+++ b/install.sh
@@ -10,9 +10,12 @@ if [ ! -d op25/gr-op25 ]; then
exit
fi
+sudo sed -i -- 's/^# *deb-src/deb-src/' /etc/apt/sources.list
+
sudo apt-get update
sudo apt-get build-dep gnuradio
-sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python-numpy python-waitress python-requests
+sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python3-numpy python3-waitress python3-requests
+sudo apt-get install liborc-dev
mkdir build
cd build
diff --git a/op25/gr-op25/CMakeLists.txt b/op25/gr-op25/CMakeLists.txt
index 6c05df5..1c6fa23 100644
--- a/op25/gr-op25/CMakeLists.txt
+++ b/op25/gr-op25/CMakeLists.txt
@@ -68,6 +68,9 @@ endif()
########################################################################
find_package(CppUnit)
+set(ENABLE_PYTHON "TRUE" CACHE BOOL "enable python")
+cmake_policy(SET CMP0012 NEW)
+
# To run a more advanced search for GNU Radio and it's components and
# versions, use the following. Add any components required to the list
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
@@ -76,11 +79,12 @@ find_package(CppUnit)
# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...)
# find_package(Gnuradio "version")
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
-find_package(Gnuradio)
-
-if(NOT GNURADIO_RUNTIME_FOUND)
- message(FATAL_ERROR "GnuRadio Runtime required to compile op25")
+SET(MIN_GR_VERSION "3.8.0")
+find_package(Gnuradio REQUIRED)
+if("${Gnuradio_VERSION}" VERSION_LESS MIN_GR_VERSION)
+ MESSAGE(FATAL_ERROR "GnuRadio version required: >=\"" ${MIN_GR_VERSION} "\" found: \"" ${Gnuradio_VERSION} "\"")
endif()
+
if(NOT CPPUNIT_FOUND)
message(FATAL_ERROR "CppUnit required to compile op25")
endif()
diff --git a/op25/gr-op25/lib/CMakeLists.txt b/op25/gr-op25/lib/CMakeLists.txt
index 1befdd9..609fa84 100644
--- a/op25/gr-op25/lib/CMakeLists.txt
+++ b/op25/gr-op25/lib/CMakeLists.txt
@@ -63,7 +63,7 @@ list(APPEND op25_sources
)
add_library(gnuradio-op25 SHARED ${op25_sources})
-target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} itpp pcap)
+target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} gnuradio::gnuradio-runtime itpp pcap)
set_target_properties(gnuradio-op25 PROPERTIES DEFINE_SYMBOL "gnuradio_op25_EXPORTS")
########################################################################
diff --git a/op25/gr-op25/python/CMakeLists.txt b/op25/gr-op25/python/CMakeLists.txt
index 03361a2..e497faa 100644
--- a/op25/gr-op25/python/CMakeLists.txt
+++ b/op25/gr-op25/python/CMakeLists.txt
@@ -31,7 +31,7 @@ endif()
GR_PYTHON_INSTALL(
FILES
__init__.py
- DESTINATION ${GR_PYTHON_DIR}/op25
+ DESTINATION ${OP25_PYTHON_DIR}/op25
)
########################################################################
diff --git a/op25/gr-op25/swig/CMakeLists.txt b/op25/gr-op25/swig/CMakeLists.txt
index e99226f..fd7bd85 100644
--- a/op25/gr-op25/swig/CMakeLists.txt
+++ b/op25/gr-op25/swig/CMakeLists.txt
@@ -21,7 +21,7 @@
# Include swig generation macros
########################################################################
find_package(SWIG)
-find_package(PythonLibs 2)
+find_package(PythonLibs 3)
if(NOT SWIG_FOUND OR NOT PYTHONLIBS_FOUND)
return()
endif()
@@ -31,9 +31,7 @@ include(GrPython)
########################################################################
# Setup swig generation
########################################################################
-foreach(incdir ${GNURADIO_RUNTIME_INCLUDE_DIRS})
- list(APPEND GR_SWIG_INCLUDE_DIRS ${incdir}/gnuradio/swig)
-endforeach(incdir)
+set(GR_SWIG_INCLUDE_DIRS $<TARGET_PROPERTY:gnuradio::runtime_swig,INTERFACE_INCLUDE_DIRECTORIES>)
set(GR_SWIG_LIBRARIES gnuradio-op25)
set(GR_SWIG_DOC_FILE ${CMAKE_CURRENT_BINARY_DIR}/op25_swig_doc.i)
@@ -44,7 +42,7 @@ GR_SWIG_MAKE(op25_swig op25_swig.i)
########################################################################
# Install the build swig module
########################################################################
-GR_SWIG_INSTALL(TARGETS op25_swig DESTINATION ${GR_PYTHON_DIR}/op25)
+GR_SWIG_INSTALL(TARGETS op25_swig DESTINATION ${OP25_PYTHON_DIR}/op25)
########################################################################
# Install swig .i files for development
diff --git a/op25/gr-op25_repeater/CMakeLists.txt b/op25/gr-op25_repeater/CMakeLists.txt
index fa29b9e..dc1d8e7 100644
--- a/op25/gr-op25_repeater/CMakeLists.txt
+++ b/op25/gr-op25_repeater/CMakeLists.txt
@@ -68,6 +68,9 @@ endif()
########################################################################
find_package(CppUnit)
+set(ENABLE_PYTHON "TRUE" CACHE BOOL "enable python")
+cmake_policy(SET CMP0012 NEW)
+
# To run a more advanced search for GNU Radio and it's components and
# versions, use the following. Add any components required to the list
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
@@ -75,11 +78,12 @@ find_package(CppUnit)
#
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
# find_package(Gnuradio "version")
-find_package(Gnuradio)
-
-if(NOT GNURADIO_RUNTIME_FOUND)
- message(FATAL_ERROR "GnuRadio Runtime required to compile op25_repeater")
+set(MIN_GR_VERSION "3.8.0")
+find_package(Gnuradio REQUIRED)
+if("${Gnuradio_VERSION}" VERSION_LESS MIN_GR_VERSION)
+ MESSAGE(FATAL_ERROR "GnuRadio version required: >=\"" ${MIN_GR_VERSION} "\" found: \"" ${Gnuradio_VERSION} "\"")
endif()
+
if(NOT CPPUNIT_FOUND)
message(FATAL_ERROR "CppUnit required to compile op25_repeater")
endif()
diff --git a/op25/gr-op25_repeater/apps/audio.py b/op25/gr-op25_repeater/apps/audio.py
index 26cbe4f..255812f 100755
--- a/op25/gr-op25_repeater/apps/audio.py
+++ b/op25/gr-op25_repeater/apps/audio.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# Copyright 2017, 2018 Graham Norbury
#
diff --git a/op25/gr-op25_repeater/apps/http_server.py b/op25/gr-op25_repeater/apps/http_server.py
index f402353..f4b047f 100755
--- a/op25/gr-op25_repeater/apps/http_server.py
+++ b/op25/gr-op25_repeater/apps/http_server.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
#
diff --git a/op25/gr-op25_repeater/apps/multi_rx.py b/op25/gr-op25_repeater/apps/multi_rx.py
index e4c71ca..42625f5 100755
--- a/op25/gr-op25_repeater/apps/multi_rx.py
+++ b/op25/gr-op25_repeater/apps/multi_rx.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
#
diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py
index c671120..b226f8a 100755
--- a/op25/gr-op25_repeater/apps/rx.py
+++ b/op25/gr-op25_repeater/apps/rx.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# Copyright 2008-2011 Steve Glass
#
diff --git a/op25/gr-op25_repeater/apps/sockaudio.py b/op25/gr-op25_repeater/apps/sockaudio.py
index 76282ac..68c6adb 100755
--- a/op25/gr-op25_repeater/apps/sockaudio.py
+++ b/op25/gr-op25_repeater/apps/sockaudio.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# Copyright 2017, 2018 Graham Norbury
#
diff --git a/op25/gr-op25_repeater/apps/terminal.py b/op25/gr-op25_repeater/apps/terminal.py
index c732a3a..fa73af4 100755
--- a/op25/gr-op25_repeater/apps/terminal.py
+++ b/op25/gr-op25_repeater/apps/terminal.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
# Copyright 2008-2011 Steve Glass
#
diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py
index 342ba3f..fba399f 100755
--- a/op25/gr-op25_repeater/apps/tx/dv_tx.py
+++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
#################################################################################
#
diff --git a/op25/gr-op25_repeater/apps/tx/generate-tsbks.py b/op25/gr-op25_repeater/apps/tx/generate-tsbks.py
index f4b06d9..de3eb28 100755
--- a/op25/gr-op25_repeater/apps/tx/generate-tsbks.py
+++ b/op25/gr-op25_repeater/apps/tx/generate-tsbks.py
@@ -1,4 +1,4 @@
-#! /usr/bin/python
+#! /usr/bin/python3
from p25craft import make_fakecc_tsdu
diff --git a/op25/gr-op25_repeater/apps/tx/multi_tx.py b/op25/gr-op25_repeater/apps/tx/multi_tx.py
index a54bc01..1707502 100755
--- a/op25/gr-op25_repeater/apps/tx/multi_tx.py
+++ b/op25/gr-op25_repeater/apps/tx/multi_tx.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
#################################################################################
#
diff --git a/op25/gr-op25_repeater/apps/tx/op25_tx.py b/op25/gr-op25_repeater/apps/tx/op25_tx.py
index 3e7afe4..7baa6ae 100755
--- a/op25/gr-op25_repeater/apps/tx/op25_tx.py
+++ b/op25/gr-op25_repeater/apps/tx/op25_tx.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
#
# Copyright 2005,2006,2007 Free Software Foundation, Inc.
#
diff --git a/op25/gr-op25_repeater/apps/tx/p25craft.py b/op25/gr-op25_repeater/apps/tx/p25craft.py
index 7fae739..b6e3999 100755
--- a/op25/gr-op25_repeater/apps/tx/p25craft.py
+++ b/op25/gr-op25_repeater/apps/tx/p25craft.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
#
# p25craft.py - utility for crafting APCO P25 packets
#
diff --git a/op25/gr-op25_repeater/apps/tx/unpack.py b/op25/gr-op25_repeater/apps/tx/unpack.py
index 7cbf05b..73a861a 100755
--- a/op25/gr-op25_repeater/apps/tx/unpack.py
+++ b/op25/gr-op25_repeater/apps/tx/unpack.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
from gnuradio import gr, audio, eng_notation, blocks
from gnuradio.eng_option import eng_option
from optparse import OptionParser
diff --git a/op25/gr-op25_repeater/apps/util/arb-resample.py b/op25/gr-op25_repeater/apps/util/arb-resample.py
index 56a762f..2bf75af 100755
--- a/op25/gr-op25_repeater/apps/util/arb-resample.py
+++ b/op25/gr-op25_repeater/apps/util/arb-resample.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import sys
import math
diff --git a/op25/gr-op25_repeater/apps/util/cqpsk-demod-file.py b/op25/gr-op25_repeater/apps/util/cqpsk-demod-file.py
index 051ddf0..171878d 100755
--- a/op25/gr-op25_repeater/apps/util/cqpsk-demod-file.py
+++ b/op25/gr-op25_repeater/apps/util/cqpsk-demod-file.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
#
# (C) Copyright 2010, 2014 Max H. Parke, KA1RBI
diff --git a/op25/gr-op25_repeater/lib/CMakeLists.txt b/op25/gr-op25_repeater/lib/CMakeLists.txt
index da84e14..1464230 100644
--- a/op25/gr-op25_repeater/lib/CMakeLists.txt
+++ b/op25/gr-op25_repeater/lib/CMakeLists.txt
@@ -68,7 +68,7 @@ else(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
find_library(GR_FILTER_LIBRARY libgnuradio-filter.so )
endif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(GR_FILTER_LIBRARIES ${GR_FILTER_LIBRARY})
-target_link_libraries(gnuradio-op25_repeater ${Boost_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} ${GR_FILTER_LIBRARIES} imbe_vocoder)
+target_link_libraries(gnuradio-op25_repeater ${Boost_LIBRARIES} gnuradio::gnuradio-runtime ${GR_FILTER_LIBRARIES} imbe_vocoder)
set_target_properties(gnuradio-op25_repeater PROPERTIES DEFINE_SYMBOL "gnuradio_op25_repeater_EXPORTS")
########################################################################
diff --git a/op25/gr-op25_repeater/python/CMakeLists.txt b/op25/gr-op25_repeater/python/CMakeLists.txt
index 9958577..bb117a0 100644
--- a/op25/gr-op25_repeater/python/CMakeLists.txt
+++ b/op25/gr-op25_repeater/python/CMakeLists.txt
@@ -31,7 +31,7 @@ endif()
GR_PYTHON_INSTALL(
FILES
__init__.py
- DESTINATION ${GR_PYTHON_DIR}/op25_repeater
+ DESTINATION ${OP25_PYTHON_DIR}/op25_repeater
)
########################################################################
diff --git a/op25/gr-op25_repeater/swig/CMakeLists.txt b/op25/gr-op25_repeater/swig/CMakeLists.txt
index 1d88bbd..50819d7 100644
--- a/op25/gr-op25_repeater/swig/CMakeLists.txt
+++ b/op25/gr-op25_repeater/swig/CMakeLists.txt
@@ -21,7 +21,7 @@
# Include swig generation macros
########################################################################
find_package(SWIG)
-find_package(PythonLibs 2)
+find_package(PythonLibs 3)
if(NOT SWIG_FOUND OR NOT PYTHONLIBS_FOUND)
return()
endif()
@@ -31,9 +31,7 @@ include(GrPython)
########################################################################
# Setup swig generation
########################################################################
-foreach(incdir ${GNURADIO_RUNTIME_INCLUDE_DIRS})
- list(APPEND GR_SWIG_INCLUDE_DIRS ${incdir}/gnuradio/swig)
-endforeach(incdir)
+set(GR_SWIG_INCLUDE_DIRS $<TARGET_PROPERTY:gnuradio::runtime_swig,INTERFACE_INCLUDE_DIRECTORIES>)
set(GR_SWIG_LIBRARIES gnuradio-op25_repeater)
set(GR_SWIG_DOC_FILE ${CMAKE_CURRENT_BINARY_DIR}/op25_repeater_swig_doc.i)
@@ -44,7 +42,7 @@ GR_SWIG_MAKE(op25_repeater_swig op25_repeater_swig.i)
########################################################################
# Install the build swig module
########################################################################
-GR_SWIG_INSTALL(TARGETS op25_repeater_swig DESTINATION ${GR_PYTHON_DIR}/op25_repeater)
+GR_SWIG_INSTALL(TARGETS op25_repeater_swig DESTINATION ${OP25_PYTHON_DIR}/op25_repeater)
########################################################################
# Install swig .i files for development

72
install-gr3.9.sh Executable file
View File

@ -0,0 +1,72 @@
#! /bin/bash
# op25 install script for debian based systems
# including ubuntu 14/16 and raspbian
#
# *** this script for gnuradio 3.9 and 3.10 ***
TREE_DIR="$PWD/src" # directory in which our gr3.9 tree will be built
if [ ! -d op25/gr-op25 ]; then
echo ====== error, op25 top level directories not found
echo ====== you must change to the op25 top level directory
echo ====== before running this script
exit
fi
TOP_DIR=$PWD
sudo apt-get update
sudo apt-get build-dep gnuradio
sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python3-numpy python3-waitress python3-requests python3-pip pybind11-dev clang-format libsndfile1-dev
# setup and populate gr3.9 src tree
echo
echo " = = = = = = = generating source tree for gr3.9, this could take a while = = = = = = ="
echo
python3 add_gr3.9.py $PWD $TREE_DIR
if [ ! -d $TREE_DIR ]; then
echo ==== Error, directory $TREE_DIR creation failed, exiting
exit 1
fi
cd $TREE_DIR
mkdir build
cd build
cmake ../op25/gr-op25
make
sudo make install
sudo ldconfig
cd ../
mkdir build_repeater
cd build_repeater
cmake ../op25/gr-op25_repeater
make
sudo make install
sudo ldconfig
cd ../
cd $TOP_DIR/op25
sh ../scripts/do_sedpy.sh
echo ======
echo ====== NOTICE
echo ======
echo ====== The gnuplot package is not installed by default here,
echo ====== as its installation requires numerous prerequisite packages
echo ====== that you may not want to install.
echo ======
echo ====== In order to do plotting in rx.py using the \-P option
echo ====== you must install gnuplot, e.g., manually as follows:
echo ======
echo ====== sudo apt-get install gnuplot-x11
echo ======
echo ======
echo ====== Separately, we suggest you set device and driver permissions:
echo ====== \$ cd scripts
echo ====== \$ ./udev_rules.sh
echo ====== It is only necessary to do this once. Currently this script
echo ====== handles the rtl-sdr and airspy only.
echo ======

45
install.sh Executable file
View File

@ -0,0 +1,45 @@
#! /bin/sh
# op25 install script for debian based systems
# including ubuntu 14/16 and raspbian
if [ ! -d op25/gr-op25 ]; then
echo ====== error, op25 top level directories not found
echo ====== you must change to the op25 top level directory
echo ====== before running this script
exit
fi
sudo sed -i -- 's/^# *deb-src/deb-src/' /etc/apt/sources.list
sudo apt-get update
sudo apt-get build-dep gnuradio
sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python3-numpy python3-waitress python3-requests
sudo apt-get install liborc-dev
mkdir build
cd build
cmake ../
make
sudo make install
sudo ldconfig
echo ======
echo ====== NOTICE
echo ======
echo ====== The gnuplot package is not installed by default here,
echo ====== as its installation requires numerous prerequisite packages
echo ====== that you may not want to install.
echo ======
echo ====== In order to do plotting in rx.py using the \-P option
echo ====== you must install gnuplot, e.g., manually as follows:
echo ======
echo ====== sudo apt-get install gnuplot-x11
echo ======
echo ======
echo ====== Separately, we suggest you set device and driver permissions:
echo ====== \$ cd scripts
echo ====== \$ ./udev_rules.sh
echo ====== It is only necessary to do this once. Currently this script
echo ====== handles the rtl-sdr and airspy only.
echo ======

View File

@ -63,6 +63,32 @@ if(NOT Boost_FOUND)
message(FATAL_ERROR "Boost required to compile op25")
endif()
########################################################################
# Find gnuradio build dependencies
########################################################################
find_package(CppUnit)
set(ENABLE_PYTHON "TRUE" CACHE BOOL "enable python")
cmake_policy(SET CMP0012 NEW)
# To run a more advanced search for GNU Radio and it's components and
# versions, use the following. Add any components required to the list
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
# minimum API compatible version required.
#
# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...)
# find_package(Gnuradio "version")
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
SET(MIN_GR_VERSION "3.8.0")
find_package(Gnuradio REQUIRED)
if("${Gnuradio_VERSION}" VERSION_LESS MIN_GR_VERSION)
MESSAGE(FATAL_ERROR "GnuRadio version required: >=\"" ${MIN_GR_VERSION} "\" found: \"" ${Gnuradio_VERSION} "\"")
endif()
if(NOT CPPUNIT_FOUND)
message(FATAL_ERROR "CppUnit required to compile op25")
endif()
########################################################################
# Install directories
########################################################################
@ -80,31 +106,6 @@ set(GR_LIBEXEC_DIR libexec)
set(GR_PKG_LIBEXEC_DIR ${GR_LIBEXEC_DIR}/${CMAKE_PROJECT_NAME})
set(GRC_BLOCKS_DIR ${GR_PKG_DATA_DIR}/grc/blocks)
########################################################################
# Find gnuradio build dependencies
########################################################################
find_package(GnuradioRuntime)
find_package(CppUnit)
# To run a more advanced search for GNU Radio and it's components and
# versions, use the following. Add any components required to the list
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
# minimum API compatible version required.
#
# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...)
# find_package(Gnuradio "version")
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
find_package(Gnuradio)
endif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
if(NOT GNURADIO_RUNTIME_FOUND)
message(FATAL_ERROR "GnuRadio Runtime required to compile op25")
endif()
if(NOT CPPUNIT_FOUND)
message(FATAL_ERROR "CppUnit required to compile op25")
endif()
########################################################################
# Setup the include and linker paths
########################################################################

View File

@ -4,17 +4,17 @@
<key>op25_decoder_ff</key>
<category>op25</category>
<import>import op25</import>
<make>op25.decoder_ff($)</make>
<make>op25.decoder_ff()</make>
<!-- Make one 'param' node for every Parameter you want settable from the GUI.
Sub-nodes:
* name
* key (makes the value accessible as $keyname, e.g. in the make node)
* type -->
<param>
<!--<param>
<name>...</name>
<key>...</key>
<type>...</type>
</param>
</param>-->
<!-- Make one 'sink' node per input. Sub-nodes:
* name (an identifier for the GUI)
@ -23,7 +23,7 @@
* optional (set to 1 for optional inputs) -->
<sink>
<name>in</name>
<type><!-- e.g. int, float, complex, byte, short, xxx_vector, ...--></type>
<type>float</type>
</sink>
<!-- Make one 'source' node per output. Sub-nodes:
@ -32,7 +32,7 @@
* vlen
* optional (set to 1 for optional inputs) -->
<source>
<name>out</name>
<type><!-- e.g. int, float, complex, byte, short, xxx_vector, ...--></type>
<name>audio</name>
<type>float</type>
</source>
</block>

View File

@ -4,7 +4,7 @@
<key>op25_fsk4_demod_ff</key>
<category>op25</category>
<import>import op25</import>
<make>op25.fsk4_demod_ff(self.auto_tune_msgq, $sample_rate, $symbol_rate)</make>
<make>op25.fsk4_demod_ff($(id)_msgq_out, $sample_rate, $symbol_rate)</make>
<param>
<name>Sample Rate</name>
@ -20,7 +20,8 @@
<type>real</type>
</param>
<param>
<!-- Must connect tune message queue as the block expects a queue as first argument! -->
<!--<param>
<name>Output Auto Tune</name>
<key>tune_out</key>
<value>True</value>
@ -34,7 +35,7 @@
<name>No</name>
<key>False</key>
</option>
</param>
</param>-->
<sink>
<name>in</name>
@ -45,4 +46,10 @@
<name>dibits</name>
<type>float</type>
</source>
<source>
<name>tune</name>
<type>msg</type>
<!--<optional>1</optional>-->
</source>
</block>

View File

@ -51,7 +51,7 @@ namespace gr {
* class. op25::decoder_bf::make is the public interface for
* creating new instances.
*/
static sptr make();
static sptr make(bool idle_silence = true, bool verbose = false);
/**
* Return a pointer to a string identifying the destination of
@ -78,6 +78,17 @@ namespace gr {
* message queue.
*/
virtual void set_msgq(gr::msg_queue::sptr msgq) = 0;
virtual void set_idle_silence(bool idle_silence = true) = 0;
virtual void set_logging(bool verbose = true) = 0;
typedef std::vector<uint8_t> key_type;
typedef std::map<uint16_t, key_type> key_map_type;
virtual void set_key(const key_type& key) = 0;
virtual void set_key_map(const key_map_type& keys) = 0;
};
} // namespace op25

View File

@ -0,0 +1,82 @@
/* -*- c++ -*- */
/*
* Copyright 2005,2013 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifndef INCLUDED_OP25_MESSAGE_H
#define INCLUDED_OP25_MESSAGE_H
#include <op25/api.h>
#include <gnuradio/types.h>
#include <string>
namespace gr {
namespace op25 {
/*!
* \brief Message class.
*
* \ingroup misc
* The ideas and method names for adjustable message length were
* lifted from the click modular router "Packet" class.
*/
class OP25_API message
{
public:
typedef std::shared_ptr<message> sptr;
private:
sptr d_next; // link field for msg queue
long d_type; // type of the message
double d_arg1; // optional arg1
double d_arg2; // optional arg2
std::vector<unsigned char> d_buf;
unsigned char* d_msg_start; // where the msg starts
unsigned char* d_msg_end; // one beyond end of msg
message(long type, double arg1, double arg2, size_t length);
friend class msg_queue;
unsigned char* buf_data() { return d_buf.data(); }
size_t buf_len() const { return d_buf.size(); }
public:
/*!
* \brief public constructor for message
*/
static sptr make(long type = 0, double arg1 = 0, double arg2 = 0, size_t length = 0);
static sptr make_from_string(const std::string s,
long type = 0,
double arg1 = 0,
double arg2 = 0);
~message();
long type() const { return d_type; }
double arg1() const { return d_arg1; }
double arg2() const { return d_arg2; }
void set_type(long type) { d_type = type; }
void set_arg1(double arg1) { d_arg1 = arg1; }
void set_arg2(double arg2) { d_arg2 = arg2; }
unsigned char* msg() const { return d_msg_start; }
size_t length() const { return d_msg_end - d_msg_start; }
std::string to_string() const;
};
OP25_API long message_ncurrently_allocated();
} /* namespace op25 */
} /* namespace gr */
#endif /* INCLUDED_OP25_MESSAGE_H */

View File

@ -0,0 +1,39 @@
/* -*- c++ -*- */
/*
* Copyright 2005,2013 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifndef INCLUDED_OP25_MSG_HANDLER_H
#define INCLUDED_OP25_MSG_HANDLER_H
#include <op25/api.h>
#include <op25/message.h>
namespace gr {
namespace op25 {
class msg_handler;
typedef std::shared_ptr<msg_handler> msg_handler_sptr;
/*!
* \brief abstract class of message handlers
* \ingroup base
*/
class OP25_API msg_handler
{
public:
virtual ~msg_handler();
//! handle \p msg
virtual void handle(message::sptr msg) = 0;
};
} /* namespace op25 */
} /* namespace gr */
#endif /* INCLUDED_OP25_MSG_HANDLER_H */

View File

@ -0,0 +1,85 @@
/* -*- c++ -*- */
/*
* Copyright 2005,2009 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifndef INCLUDED_OP25_MSG_QUEUE_H
#define INCLUDED_OP25_MSG_QUEUE_H
#include <op25/api.h>
#include <op25/msg_handler.h>
#include <gnuradio/thread/thread.h>
namespace gr {
namespace op25 {
/*!
* \brief thread-safe message queue
* \ingroup misc
*/
class OP25_API msg_queue : public msg_handler
{
gr::thread::mutex d_mutex;
gr::thread::condition_variable d_not_empty;
gr::thread::condition_variable d_not_full;
message::sptr d_head;
message::sptr d_tail;
unsigned int d_count; // # of messages in queue.
unsigned int d_limit; // max # of messages in queue. 0 -> unbounded
public:
typedef std::shared_ptr<msg_queue> sptr;
static sptr make(unsigned int limit = 0);
msg_queue(unsigned int limit);
~msg_queue() override;
//! Generic msg_handler method: insert the message.
void handle(message::sptr msg) override { insert_tail(msg); }
/*!
* \brief Insert message at tail of queue.
* \param msg message
*
* Block if queue if full.
*/
void insert_tail(message::sptr msg);
/*!
* \brief Delete message from head of queue and return it.
* Block if no message is available.
*/
message::sptr delete_head();
/*!
* \brief If there's a message in the q, delete it and return it.
* If no message is available, return 0.
*/
message::sptr delete_head_nowait();
//! Delete all messages from the queue
void flush();
//! is the queue empty?
bool empty_p() const { return d_count == 0; }
//! is the queue full?
bool full_p() const { return d_limit != 0 && d_count >= d_limit; }
//! return number of messages in queue
unsigned int count() const { return d_count; }
//! return limit on number of message in queue. 0 -> unbounded
unsigned int limit() const { return d_limit; }
};
} /* namespace op25 */
} /* namespace gr */
#endif /* INCLUDED_OP25_MSG_QUEUE_H */

View File

@ -53,10 +53,17 @@ list(APPEND op25_sources
value_string.cc
pickle.cc
pcap_source_b_impl.cc
bch.cc
ldu.cc
crypto.cc
crypto_module_du_handler.cc
deskey.c
desport.c
dessp.c
)
add_library(gnuradio-op25 SHARED ${op25_sources})
target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} itpp pcap)
target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} gnuradio::gnuradio-runtime itpp pcap)
set_target_properties(gnuradio-op25 PROPERTIES DEFINE_SYMBOL "gnuradio_op25_EXPORTS")
########################################################################

View File

@ -55,10 +55,10 @@ abstract_data_unit::correct_errors()
}
void
abstract_data_unit::decode_audio(imbe_decoder& imbe)
abstract_data_unit::decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod)
{
if(is_complete()) {
do_decode_audio(d_frame_body, imbe);
do_decode_audio(d_frame_body, imbe, crypto_mod);
} else {
ostringstream msg;
msg << "cannot decode audio - frame is not complete" << endl;
@ -153,7 +153,8 @@ abstract_data_unit::dump(ostream& os) const
}
abstract_data_unit::abstract_data_unit(const_bit_queue& frame_body) :
d_frame_body(frame_body.size())
d_frame_body(frame_body.size()),
d_logging_enabled(false)
{
copy(frame_body.begin(), frame_body.end(), d_frame_body.begin());
}
@ -164,7 +165,7 @@ abstract_data_unit::do_correct_errors(bit_vector& frame_body)
}
void
abstract_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe)
abstract_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod)
{
}
@ -179,3 +180,15 @@ abstract_data_unit::frame_size() const
{
return d_frame_body.size();
}
void
abstract_data_unit::set_logging(bool on)
{
d_logging_enabled = on;
}
bool
abstract_data_unit::logging_enabled() const
{
return d_logging_enabled;
}

View File

@ -26,6 +26,7 @@
#include "data_unit.h"
#include "op25_yank.h"
#include "crypto.h"
#include <string>
#include <vector>
@ -62,7 +63,7 @@ public:
* \precondition is_complete() == true.
* \param imbe The imbe_decoder to use to generate the audio.
*/
virtual void decode_audio(imbe_decoder& imbe);
virtual void decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod);
/**
* Decode the frame into an octet vector.
@ -117,6 +118,15 @@ public:
*/
virtual std::string snapshot() const;
/**
* Returns a string describing the Data Unit ID (DUID).
*
* \return A string identifying the DUID.
*/
virtual std::string duid_str() const = 0;
virtual void set_logging(bool on);
protected:
/**
@ -140,7 +150,7 @@ protected:
* \param frame_body The const_bit_vector to decode.
* \param imbe The imbe_decoder to use.
*/
virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe);
virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod);
/**
* Decode frame_body and write the decoded frame contents to msg.
@ -152,13 +162,6 @@ protected:
*/
virtual size_t decode_frame(const_bit_vector& frame_body, size_t msg_sz, uint8_t *msg);
/**
* Returns a string describing the Data Unit ID (DUID).
*
* \return A string identifying the DUID.
*/
virtual std::string duid_str() const = 0;
/**
* Return a reference to the frame body.
*/
@ -180,6 +183,8 @@ protected:
*/
virtual uint16_t frame_size() const;
virtual bool logging_enabled() const;
private:
/**
@ -187,6 +192,7 @@ private:
*/
bit_vector d_frame_body;
bool d_logging_enabled;
};
#endif /* INCLUDED_ABSTRACT_DATA_UNIT_H */

162
op25/gr-op25/lib/bch.cc Normal file
View File

@ -0,0 +1,162 @@
#include <stdio.h>
#include <vector>
#include "bch.h"
/*
* Copyright 2010, KA1RBI
*/
static const int bchGFexp[64] = {
1, 2, 4, 8, 16, 32, 3, 6, 12, 24, 48, 35, 5, 10, 20, 40,
19, 38, 15, 30, 60, 59, 53, 41, 17, 34, 7, 14, 28, 56, 51, 37,
9, 18, 36, 11, 22, 44, 27, 54, 47, 29, 58, 55, 45, 25, 50, 39,
13, 26, 52, 43, 21, 42, 23, 46, 31, 62, 63, 61, 57, 49, 33, 0
};
static const int bchGFlog[64] = {
-1, 0, 1, 6, 2, 12, 7, 26, 3, 32, 13, 35, 8, 48, 27, 18,
4, 24, 33, 16, 14, 52, 36, 54, 9, 45, 49, 38, 28, 41, 19, 56,
5, 62, 25, 11, 34, 31, 17, 47, 15, 23, 53, 51, 37, 44, 55, 40,
10, 61, 46, 30, 50, 22, 39, 43, 29, 60, 42, 21, 20, 59, 57, 58
};
static const int bchG[48] = {
1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0,
1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0,
1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1
};
int bchDec(bit_vector& Codeword)
{
int elp[24][ 22], S[23];
int D[23], L[24], uLu[24];
int root[11], locn[11], reg[12];
int i,j,U,q,count;
int SynError, CantDecode;
SynError = 0; CantDecode = 0;
for(i = 1; i <= 22; i++) {
S[i] = 0;
// FOR j = 0 TO 62
for(j = 0; j <= 62; j++) {
if( Codeword[j]) { S[i] = S[i] ^ bchGFexp[(i * j) % 63]; }
}
if( S[i]) { SynError = 1; }
S[i] = bchGFlog[S[i]];
// printf("S[%d] %d\n", i, S[i]);
}
if( SynError) { //if there are errors, try to correct them
L[0] = 0; uLu[0] = -1; D[0] = 0; elp[0][ 0] = 0;
L[1] = 0; uLu[1] = 0; D[1] = S[1]; elp[1][ 0] = 1;
//FOR i = 1 TO 21
for(i = 1; i <= 21; i++) {
elp[0][ i] = -1; elp[1][ i] = 0;
}
U = 0;
do {
U = U + 1;
if( D[U] == -1) {
L[U + 1] = L[U];
// FOR i = 0 TO L[U]
for(i = 0; i <= L[U]; i++) {
elp[U + 1][ i] = elp[U][ i]; elp[U][ i] = bchGFlog[elp[U][ i]];
}
} else {
//search for words with greatest uLu(q) for which d(q)!=0
q = U - 1;
while((D[q] == -1) &&(q > 0)) { q = q - 1; }
//have found first non-zero d(q)
if( q > 0) {
j = q;
do { j = j - 1; if((D[j] != -1) &&(uLu[q] < uLu[j])) { q = j; }
} while( j > 0) ;
}
//store degree of new elp polynomial
if( L[U] > L[q] + U - q) {
L[U + 1] = L[U] ;
} else {
L[U + 1] = L[q] + U - q;
}
///* form new elp(x) */
// FOR i = 0 TO 21
for(i = 0; i <= 21; i++) {
elp[U + 1][ i] = 0;
}
// FOR i = 0 TO L(q)
for(i = 0; i <= L[q]; i++) {
if( elp[q][ i] != -1) {
elp[U + 1][ i + U - q] = bchGFexp[(D[U] + 63 - D[q] + elp[q][ i]) % 63];
}
}
// FOR i = 0 TO L(U)
for(i = 0; i <= L[U]; i++) {
elp[U + 1][ i] = elp[U + 1][ i] ^ elp[U][ i];
elp[U][ i] = bchGFlog[elp[U][ i]];
}
}
uLu[U + 1] = U - L[U + 1];
//form(u+1)th discrepancy
if( U < 22) {
//no discrepancy computed on last iteration
if( S[U + 1] != -1) { D[U + 1] = bchGFexp[S[U + 1]]; } else { D[U + 1] = 0; }
// FOR i = 1 TO L(U + 1)
for(i = 1; i <= L[U + 1]; i++) {
if((S[U + 1 - i] != -1) &&(elp[U + 1][ i] != 0)) {
D[U + 1] = D[U + 1] ^ bchGFexp[(S[U + 1 - i] + bchGFlog[elp[U + 1][ i]]) % 63];
}
}
//put d(u+1) into index form */
D[U + 1] = bchGFlog[D[U + 1]];
}
} while((U < 22) &&(L[U + 1] <= 11));
U = U + 1;
if( L[U] <= 11) { // /* Can correct errors */
//put elp into index form
// FOR i = 0 TO L[U]
for(i = 0; i <= L[U]; i++) {
elp[U][ i] = bchGFlog[elp[U][ i]];
}
//Chien search: find roots of the error location polynomial
// FOR i = 1 TO L(U)
for(i = 1; i <= L[U]; i++) {
reg[i] = elp[U][ i];
}
count = 0;
// FOR i = 1 TO 63
for(i = 1; i <= 63; i++) {
q = 1;
//FOR j = 1 TO L(U)
for(j = 1; j <= L[U]; j++) {
if( reg[j] != -1) {
reg[j] =(reg[j] + j) % 63; q = q ^ bchGFexp[reg[j]];
}
}
if( q == 0) { //store root and error location number indices
root[count] = i; locn[count] = 63 - i; count = count + 1;
}
}
if( count == L[U]) {
//no. roots = degree of elp hence <= t errors
//FOR i = 0 TO L[U] - 1
for(i = 0; i <= L[U]-1; i++) {
Codeword[locn[i]] = Codeword[locn[i]] ^ 1;
}
CantDecode = count;
} else { //elp has degree >t hence cannot solve
CantDecode = -1;
}
} else {
CantDecode = -2;
}
}
return CantDecode;
}

4
op25/gr-op25/lib/bch.h Normal file
View File

@ -0,0 +1,4 @@
#include <vector>
typedef std::vector<bool> bit_vector;
int bchDec(bit_vector& Codeword);

286
op25/gr-op25/lib/crypto.cc Normal file
View File

@ -0,0 +1,286 @@
#include "crypto.h"
#include <sstream>
#include <boost/format.hpp>
#include <iostream>
#include <stdio.h>
#include <memory.h>
extern "C" {
#include "des.h"
}
static unsigned long long swap_bytes(uint64_t l)
{
unsigned long long r;
unsigned char* pL = (unsigned char*)&l;
unsigned char* pR = (unsigned char*)&r;
for (int i = 0; i < sizeof(l); ++i)
pR[i] = pL[(sizeof(l) - 1) - i];
return r;
}
///////////////////////////////////////////////////////////////////////////////
/*
class null_algorithm : public crypto_algorithm // This is an algorithm skeleton (can be used for no encryption as pass-through)
{
private:
size_t m_generated_bits;
public:
null_algorithm()
: m_generated_bits(0)
{
}
const type_id id() const
{
return crypto_algorithm::NONE;
}
bool update(const struct CryptoState& state)
{
fprintf(stderr, "NULL:\t%d bits generated\n", m_generated_bits);
m_generated_bits = 0;
return true;
}
bool set_key(const crypto_algorithm::key_type& key)
{
return true;
}
uint64_t generate(size_t n)
{
m_generated_bits += n;
return 0;
}
};
*/
///////////////////////////////////////////////////////////////////////////////
class des_ofb : public crypto_algorithm
{
public:
unsigned long long m_key_des, m_next_iv, m_ks;
int m_ks_idx;
DES_KS m_ksDES;
int m_iterations;
uint16_t m_current_kid;
key_type m_default_key;
key_map_type m_key_map;
bool m_verbose;
public:
des_ofb()
: m_current_kid(-1)
{
memset(&m_ksDES, 0, sizeof(m_ksDES));
m_key_des = 0;
m_next_iv = 0;
m_ks_idx = 0;
m_ks = 0;
m_iterations = 0;
}
void set_logging(bool on)
{
m_verbose = on;
}
const type_id id() const
{
return crypto_algorithm::DES_OFB;
}
bool update(const struct CryptoState& state)
{
if (m_current_kid != state.kid)
{
if (m_key_map.empty())
{
// Nothing to do
}
else
{
key_map_type::iterator it = m_key_map.find(state.kid);
if (it != m_key_map.end())
{
set_key(it->second);
}
else if (!m_default_key.empty())
{
/*if (m_verbose) */fprintf(stderr, "Key 0x%04x not found in key map - using default key\n", state.kid);
set_key(m_default_key);
}
else
{
/*if (m_verbose) */fprintf(stderr, "Key 0x%04x not found in key map and no default key\n", state.kid);
}
}
m_current_kid = state.kid;
}
uint64_t iv = 0;
size_t n = std::min(sizeof(iv), state.mi.size());
memcpy(&iv, &state.mi[0], n);
set_iv(iv);
return (n == 8);
}
void set_key_map(const key_map_type& key_map)
{
m_key_map = key_map;
m_current_kid = -1; // To refresh on next update if it has changed
}
bool set_key(const crypto_algorithm::key_type& key)
{
const size_t valid_key_length = 8;
if (key.size() != valid_key_length)
{
if (m_verbose) fprintf(stderr, "DES:\tIncorrect key length of %lu (should be %lu)\n", key.size(), valid_key_length);
return false;
}
m_default_key = key;
memcpy(&m_key_des, &key[0], std::min(key.size(), sizeof(m_key_des)));
if (m_verbose)
{
std::stringstream ss;
for (int i = 0; i < valid_key_length; ++i)
ss << boost::format("%02X") % (int)key[i];
std::cerr << "DES:\tKey: " << ss.str() << std::endl;
}
deskey(m_ksDES, (unsigned char*)&m_key_des, 0); // 0: encrypt (for OFB mode)
return true;
}
void set_iv(uint64_t iv)
{
if (m_iterations > 0)
{
if (m_verbose) fprintf(stderr, "DES:\t%i bits used from %i iterations\n", m_ks_idx, m_iterations);
}
m_next_iv = iv;
m_ks_idx = 0;
m_iterations = 0;
m_ks = m_next_iv;
des(m_ksDES, (unsigned char*)&m_ks); // First initialisation
++m_iterations;
des(m_ksDES, (unsigned char*)&m_ks); // Throw out first iteration & prepare for second
++m_iterations;
generate(64); // Reserved 3 + first 5 of LC (3 left)
generate(3 * 8); // Use remaining 3 bytes for LC
}
uint64_t generate(size_t count) // 1..64
{
unsigned long long ullCurrent = swap_bytes(m_ks);
const int max_len = 64;
int pos = m_ks_idx % max_len;
m_ks_idx += count;
if ((pos + count) <= max_len) // Up to 64
{
if ((m_ks_idx % max_len) == 0)
{
des(m_ksDES, (unsigned char*)&m_ks); // Prepare for next iteration
++m_iterations;
}
unsigned long long result = (ullCurrent >> (((max_len - 1) - pos) - (count-1))) & ((count == max_len) ? (unsigned long long)-1 : ((1ULL << count) - 1));
return result;
}
// Over-flow 64-bit boundary (so all of rest of current will be used)
des(m_ksDES, (unsigned char*)&m_ks); // Compute second part
++m_iterations;
unsigned long long first = ullCurrent << pos; // RHS will be zeros
ullCurrent = swap_bytes(m_ks);
int remainder = count - (max_len - pos);
first >>= (((max_len - 1) - remainder) - ((max_len - 1) - pos));
unsigned long long next = (ullCurrent >> (((max_len - 1) - 0) - (remainder-1))) & ((1ULL << remainder) - 1);
return (first | next);
}
};
///////////////////////////////////////////////////////////////////////////////
crypto_module::crypto_module(bool verbose/* = true*/)
: d_verbose(verbose)
{
}
crypto_algorithm::sptr crypto_module::algorithm(crypto_algorithm::type_id algid)
{
if ((!d_current_algorithm && (algid == crypto_algorithm::NONE)) || // This line should be commented out if 'null_algorithm' is to be tested
(d_current_algorithm && (algid == d_current_algorithm->id())))
return d_current_algorithm;
switch (algid)
{
case crypto_algorithm::DES_OFB:
d_current_algorithm = crypto_algorithm::sptr(new des_ofb());
break;
//case crypto_algorithm::NONE:
// d_current_algorithm = crypto_algorithm::sptr(new null_algorithm());
// break;
default:
d_current_algorithm = crypto_algorithm::sptr();
};
if (d_current_algorithm)
{
d_current_algorithm->set_logging(logging_enabled());
if (!d_persistent_key_map.empty())
d_current_algorithm->set_key_map(d_persistent_key_map);
if (!d_persistent_key.empty())
d_current_algorithm->set_key(d_persistent_key);
}
return d_current_algorithm;
}
void crypto_module::set_key(const crypto_algorithm::key_type& key)
{
d_persistent_key = key;
if (d_current_algorithm)
d_current_algorithm->set_key(d_persistent_key);
}
void crypto_module::set_key_map(const crypto_algorithm::key_map_type& keys)
{
d_persistent_key_map = keys;
if (d_current_algorithm)
d_current_algorithm->set_key_map(d_persistent_key_map);
}
void crypto_module::set_logging(bool on/* = true*/)
{
d_verbose = on;
if (d_current_algorithm)
d_current_algorithm->set_logging(on);
}

73
op25/gr-op25/lib/crypto.h Normal file
View File

@ -0,0 +1,73 @@
#ifndef INCLUDED_CRYPTO_H
#define INCLUDED_CRYPTO_H
#include <stdint.h>
#include <vector>
#include <map>
#include <boost/shared_ptr.hpp>
static const int MESSAGE_INDICATOR_LENGTH = 9;
class CryptoState
{
public:
CryptoState() :
mi(MESSAGE_INDICATOR_LENGTH), kid(0), algid(0)
{ }
public:
std::vector<uint8_t> mi;
uint16_t kid;
uint8_t algid;
};
class crypto_state_provider
{
public:
virtual struct CryptoState crypto_state() const=0;
};
class crypto_algorithm
{
public:
typedef boost::shared_ptr<class crypto_algorithm> sptr;
typedef std::vector<uint8_t> key_type;
typedef std::map<uint16_t, key_type > key_map_type;
typedef uint8_t type_id;
enum
{
NONE = 0x80,
DES_OFB = 0x81,
};
public:
virtual const type_id id() const=0;
virtual bool set_key(const key_type& key)=0;
virtual void set_key_map(const key_map_type& key_map)=0;
virtual bool update(const struct CryptoState& state)=0;
virtual uint64_t generate(size_t n_bits)=0; // Can request up to 64 bits of key stream at one time
virtual void set_logging(bool on)=0;
};
class crypto_module
{
public:
typedef boost::shared_ptr<class crypto_module> sptr;
public:
crypto_module(bool verbose = false);
public:
virtual crypto_algorithm::sptr algorithm(crypto_algorithm::type_id algid);
virtual void set_key(const crypto_algorithm::key_type& key);
virtual void set_key_map(const crypto_algorithm::key_map_type& keys);
virtual void set_logging(bool on = true);
protected:
crypto_algorithm::sptr d_current_algorithm;
crypto_algorithm::key_type d_persistent_key;
crypto_algorithm::key_map_type d_persistent_key_map;
bool d_verbose;
public:
virtual crypto_algorithm::sptr current_algorithm() const
{ return d_current_algorithm; }
virtual bool logging_enabled() const
{ return d_verbose; }
};
#endif // INCLUDED_CRYPTO_H

View File

@ -0,0 +1,64 @@
#include "crypto_module_du_handler.h"
#include "abstract_data_unit.h"
#include <boost/format.hpp>
#include <sstream>
#include <stdio.h>
crypto_module_du_handler::crypto_module_du_handler(data_unit_handler_sptr next, crypto_module::sptr crypto_mod)
: data_unit_handler(next)
, d_crypto_mod(crypto_mod)
{
}
void
crypto_module_du_handler::handle(data_unit_sptr du)
{
if (!d_crypto_mod)
{
data_unit_handler::handle(du);
return;
}
crypto_state_provider* p = dynamic_cast<crypto_state_provider*>(du.get());
if (p == NULL)
{
data_unit_handler::handle(du);
return;
}
CryptoState state = p->crypto_state();
///////////////////////////////////
if (d_crypto_mod->logging_enabled())
{
std::string duid_str("?");
abstract_data_unit* adu = dynamic_cast<abstract_data_unit*>(du.get());
if (adu)
duid_str = adu->duid_str();
std::stringstream ss;
for (size_t n = 0; n < state.mi.size(); ++n)
ss << (boost::format("%02x") % (int)state.mi[n]);
fprintf(stderr, "%s:\tAlgID: 0x%02x, KID: 0x%04x, MI: %s\n", duid_str.c_str(), state.algid, state.kid, ss.str().c_str());
}
///////////////////////////////////
crypto_algorithm::sptr algorithm = d_crypto_mod->algorithm(state.algid);
if (!algorithm)
{
data_unit_handler::handle(du);
return;
}
// TODO: Could do key management & selection here with 'state.kid'
// Assuming we're only using one key (ignoring 'kid')
algorithm->update(state);
data_unit_handler::handle(du);
}

View File

@ -0,0 +1,21 @@
#ifndef INCLUDED_CRYPTO_MODULE_DU_HANDLER_H
#define INCLUDED_CRYPTO_MODULE_DU_HANDLER_H
#include <boost/shared_ptr.hpp>
#include "data_unit_handler.h"
#include "crypto.h"
class crypto_module_du_handler : public data_unit_handler
{
public:
crypto_module_du_handler(data_unit_handler_sptr next, crypto_module::sptr crypto_mod);
public:
typedef boost::shared_ptr<class crypto_module_du_handler> sptr;
public:
virtual void handle(data_unit_sptr du);
private:
crypto_module::sptr d_crypto_mod;
};
#endif //INCLUDED_CRYPTO_MODULE_HANDLER_H

View File

@ -32,6 +32,8 @@
#include <iosfwd>
#include <stdint.h>
#include "crypto.h"
typedef std::deque<bool> bit_queue;
typedef const std::deque<bool> const_bit_queue;
@ -76,7 +78,7 @@ public:
* \precondition is_complete() == true.
* \param imbe The imbe_decoder to use to generate the audio.
*/
virtual void decode_audio(imbe_decoder& imbe) = 0;
virtual void decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod) = 0;
/**
* Decode the frame into an octet vector.
@ -132,6 +134,8 @@ public:
*/
virtual std::string snapshot() const = 0;
virtual void set_logging(bool on) = 0;
protected:
/**

View File

@ -34,6 +34,7 @@
#include "offline_imbe_decoder.h"
#include "voice_du_handler.h"
#include "op25_yank.h"
#include "bch.h"
using namespace std;
@ -41,13 +42,13 @@ namespace gr {
namespace op25 {
decoder_bf::sptr
decoder_bf::make()
decoder_bf::make(bool idle_silence /*= true*/, bool verbose /*= false*/)
{
return gnuradio::get_initial_sptr
(new decoder_bf_impl());
(new decoder_bf_impl(idle_silence, verbose));
}
decoder_bf_impl::decoder_bf_impl() :
decoder_bf_impl::decoder_bf_impl(bool idle_silence /*= true*/, bool verbose /*= false*/) :
gr::block("decoder_bf",
gr::io_signature::make(1, 1, sizeof(uint8_t)),
gr::io_signature::make(0, 1, sizeof(float))),
@ -56,14 +57,25 @@ namespace gr {
d_frame_hdr(),
d_imbe(imbe_decoder::make()),
d_state(SYNCHRONIZING),
d_p25cai_du_handler(NULL)
d_p25cai_du_handler(NULL),
d_idle_silence(idle_silence),
d_verbose(false)
{
set_logging(verbose);
d_p25cai_du_handler = new p25cai_du_handler(d_data_unit_handler,
"224.0.0.1", 23456);
d_data_unit_handler = data_unit_handler_sptr(d_p25cai_du_handler);
d_snapshot_du_handler = new snapshot_du_handler(d_data_unit_handler);
d_data_unit_handler = data_unit_handler_sptr(d_snapshot_du_handler);
d_data_unit_handler = data_unit_handler_sptr(new voice_du_handler(d_data_unit_handler, d_imbe));
d_crypto_module = crypto_module::sptr(new crypto_module(verbose));
d_crypto_module_du_handler = crypto_module_du_handler::sptr(new crypto_module_du_handler(d_data_unit_handler, d_crypto_module));
d_data_unit_handler = data_unit_handler_sptr(d_crypto_module_du_handler);
d_data_unit_handler = data_unit_handler_sptr(new voice_du_handler(d_data_unit_handler, d_imbe, d_crypto_module));
}
decoder_bf_impl::~decoder_bf_impl()
@ -104,34 +116,35 @@ namespace gr {
gr_vector_void_star &output_items)
{
try {
gr::thread::scoped_lock lock(d_mutex);
// process input
const uint8_t *in = reinterpret_cast<const uint8_t*>(input_items[0]);
for(int i = 0; i < ninput_items[0]; ++i) {
dibit d = in[i] & 0x3;
receive_symbol(d);
}
consume_each(ninput_items[0]);
// process input
const uint8_t *in = reinterpret_cast<const uint8_t*>(input_items[0]);
for(int i = 0; i < ninput_items[0]; ++i) {
dibit d = in[i] & 0x3;
receive_symbol(d);
}
consume_each(ninput_items[0]);
// produce audio
audio_samples *samples = d_imbe->audio();
float *out = reinterpret_cast<float*>(output_items[0]);
const int n = min(static_cast<int>(samples->size()), noutput_items);
if(0 < n) {
copy(samples->begin(), samples->begin() + n, out);
samples->erase(samples->begin(), samples->begin() + n);
}
if(n < noutput_items) {
fill(out + n, out + noutput_items, 0.0);
}
return noutput_items;
// produce audio
audio_samples *samples = d_imbe->audio();
float *out = reinterpret_cast<float*>(output_items[0]);
const int n = min(static_cast<int>(samples->size()), noutput_items);
if(0 < n) {
copy(samples->begin(), samples->begin() + n, out);
samples->erase(samples->begin(), samples->begin() + n);
}
if((d_idle_silence) && (n < noutput_items)) {
fill(out + n, out + noutput_items, 0.0);
}
return (d_idle_silence ? noutput_items : n);
} catch(const std::exception& x) {
cerr << x.what() << endl;
exit(1);
cerr << x.what() << endl;
exit(1);
} catch(...) {
cerr << "unhandled exception" << endl;
exit(2); }
cerr << "unhandled exception" << endl;
exit(2); }
}
const char*
@ -177,14 +190,12 @@ namespace gr {
};
size_t NID_SZ = sizeof(NID) / sizeof(NID[0]);
itpp::bvec b(63), zeroes(16);
itpp::BCH bch(63, 16, 11, "6 3 3 1 1 4 1 3 6 7 2 3 5 4 5 3", true);
bit_vector b(NID_SZ);
yank(d_frame_hdr, NID, NID_SZ, b, 0);
b = bch.decode(b);
if(b != zeroes) {
b = bch.encode(b);
if(bchDec(b) >= 0) {
yank_back(b, 0, d_frame_hdr, NID, NID_SZ);
d_data_unit = data_unit::make_data_unit(d_frame_hdr);
d_data_unit->set_logging(d_verbose);
} else {
data_unit_sptr null;
d_data_unit = null;
@ -229,5 +240,36 @@ namespace gr {
break;
}
}
void
decoder_bf_impl::set_idle_silence(bool idle_silence/* = true*/)
{
gr::thread::scoped_lock lock(d_mutex);
d_idle_silence = idle_silence;
}
void
decoder_bf_impl::set_logging(bool verbose/* = true*/)
{
if (verbose) fprintf(stderr, "[%s<%lu>] verbose logging enabled\n", name().c_str(), unique_id());
d_verbose = verbose;
if (d_crypto_module)
d_crypto_module->set_logging(verbose);
}
void
decoder_bf_impl::set_key(const crypto_algorithm::key_type& key)
{
d_crypto_module->set_key(key);
}
void
decoder_bf_impl::set_key_map(const crypto_algorithm::key_map_type& keys)
{
d_crypto_module->set_key_map(keys);
}
} /* namespace op25 */
} /* namespace gr */

View File

@ -24,11 +24,14 @@
#define INCLUDED_OP25_DECODER_BF_IMPL_H
#include <op25/decoder_bf.h>
#include <gnuradio/thread/thread.h>
#include "data_unit.h"
#include "data_unit_handler.h"
#include "imbe_decoder.h"
#include "p25cai_du_handler.h"
#include "snapshot_du_handler.h"
#include "crypto.h"
#include "crypto_module_du_handler.h"
namespace gr {
namespace op25 {
@ -102,8 +105,21 @@ namespace gr {
*/
class snapshot_du_handler *d_snapshot_du_handler;
/*
* Whether or not to output silence when no audio is synthesised.
*/
bool d_idle_silence;
bool d_verbose;
crypto_module::sptr d_crypto_module;
crypto_module_du_handler::sptr d_crypto_module_du_handler;
gr::thread::mutex d_mutex;
public:
decoder_bf_impl();
decoder_bf_impl(bool idle_silence = true, bool verbose = false);
~decoder_bf_impl();
// Where all the action really happens
@ -139,6 +155,14 @@ namespace gr {
* message queue.
*/
void set_msgq(gr::msg_queue::sptr msgq);
void set_idle_silence(bool idle_silence = true);
void set_logging(bool verbose = true);
void set_key(const crypto_algorithm::key_type& key);
void set_key_map(const crypto_algorithm::key_map_type& keys);
};
} // namespace op25
} // namespace gr

View File

@ -34,6 +34,7 @@
#include "offline_imbe_decoder.h"
#include "voice_du_handler.h"
#include "op25_yank.h"
#include "bch.h"
using namespace std;
@ -185,12 +186,9 @@ namespace gr {
};
size_t NID_SZ = sizeof(NID) / sizeof(NID[0]);
itpp::bvec b(63), zeroes(16);
itpp::BCH bch(63, 16, 11, "6 3 3 1 1 4 1 3 6 7 2 3 5 4 5 3", true);
bit_vector b(NID_SZ);
yank(d_frame_hdr, NID, NID_SZ, b, 0);
b = bch.decode(b);
if(b != zeroes) {
b = bch.encode(b);
if(bchDec(b) >= 0) {
yank_back(b, 0, d_frame_hdr, NID, NID_SZ);
d_data_unit = data_unit::make_data_unit(d_frame_hdr);
} else {

15
op25/gr-op25/lib/des.h Normal file
View File

@ -0,0 +1,15 @@
typedef unsigned long DES_KS[16][2]; /* Single-key DES key schedule */
typedef unsigned long DES3_KS[48][2]; /* Triple-DES key schedule */
/* In deskey.c: */
void deskey(DES_KS,unsigned char *,int);
void des3key(DES3_KS,unsigned char *,int);
/* In desport.c, desborl.cas or desgnu.s: */
void des(DES_KS,unsigned char *);
/* In des3port.c, des3borl.cas or des3gnu.s: */
void des3(DES3_KS,unsigned char *);
extern int Asmversion; /* 1 if we're linked with an asm version, 0 if C */

124
op25/gr-op25/lib/deskey.c Normal file
View File

@ -0,0 +1,124 @@
/* Portable C code to create DES key schedules from user-provided keys
* This doesn't have to be fast unless you're cracking keys or UNIX
* passwords
*/
#include <string.h>
#include "des.h"
/* Key schedule-related tables from FIPS-46 */
/* permuted choice table (key) */
static unsigned char pc1[] = {
57, 49, 41, 33, 25, 17, 9,
1, 58, 50, 42, 34, 26, 18,
10, 2, 59, 51, 43, 35, 27,
19, 11, 3, 60, 52, 44, 36,
63, 55, 47, 39, 31, 23, 15,
7, 62, 54, 46, 38, 30, 22,
14, 6, 61, 53, 45, 37, 29,
21, 13, 5, 28, 20, 12, 4
};
/* number left rotations of pc1 */
static unsigned char totrot[] = {
1,2,4,6,8,10,12,14,15,17,19,21,23,25,27,28
};
/* permuted choice key (table) */
static unsigned char pc2[] = {
14, 17, 11, 24, 1, 5,
3, 28, 15, 6, 21, 10,
23, 19, 12, 4, 26, 8,
16, 7, 27, 20, 13, 2,
41, 52, 31, 37, 47, 55,
30, 40, 51, 45, 33, 48,
44, 49, 39, 56, 34, 53,
46, 42, 50, 36, 29, 32
};
/* End of DES-defined tables */
/* bit 0 is left-most in byte */
static int bytebit[] = {
0200,0100,040,020,010,04,02,01
};
/* Generate key schedule for encryption or decryption
* depending on the value of "decrypt"
*/
void
deskey(DES_KS k,unsigned char *key,int decrypt)
/* Key schedule array */
/* 64 bits (will use only 56) */
/* 0 = encrypt, 1 = decrypt */
{
unsigned char pc1m[56]; /* place to modify pc1 into */
unsigned char pcr[56]; /* place to rotate pc1 into */
register int i,j,l;
int m;
unsigned char ks[8];
for (j=0; j<56; j++) { /* convert pc1 to bits of key */
l=pc1[j]-1; /* integer bit location */
m = l & 07; /* find bit */
pc1m[j]=(key[l>>3] & /* find which key byte l is in */
bytebit[m]) /* and which bit of that byte */
? 1 : 0; /* and store 1-bit result */
}
for (i=0; i<16; i++) { /* key chunk for each iteration */
memset(ks,0,sizeof(ks)); /* Clear key schedule */
for (j=0; j<56; j++) /* rotate pc1 the right amount */
pcr[j] = pc1m[(l=j+totrot[decrypt? 15-i : i])<(j<28? 28 : 56) ? l: l-28];
/* rotate left and right halves independently */
for (j=0; j<48; j++){ /* select bits individually */
/* check bit that goes to ks[j] */
if (pcr[pc2[j]-1]){
/* mask it in if it's there */
l= j % 6;
ks[j/6] |= bytebit[l] >> 2;
}
}
/* Now convert to packed odd/even interleaved form */
k[i][0] = ((long)ks[0] << 24)
| ((long)ks[2] << 16)
| ((long)ks[4] << 8)
| ((long)ks[6]);
k[i][1] = ((long)ks[1] << 24)
| ((long)ks[3] << 16)
| ((long)ks[5] << 8)
| ((long)ks[7]);
if(Asmversion){
/* The assembler versions pre-shift each subkey 2 bits
* so the Spbox indexes are already computed
*/
k[i][0] <<= 2;
k[i][1] <<= 2;
}
}
}
/* Generate key schedule for triple DES in E-D-E (or D-E-D) mode.
*
* The key argument is taken to be 24 bytes. The first 8 bytes are K1
* for the first stage, the second 8 bytes are K2 for the middle stage
* and the third 8 bytes are K3 for the last stage
*/
void
des3key(DES3_KS k,unsigned char *key,int decrypt)
/* 192 bits (will use only 168) */
/* 0 = encrypt, 1 = decrypt */
{
if(!decrypt){
deskey(&k[0],&key[0],0);
deskey(&k[16],&key[8],1);
deskey(&k[32],&key[16],0);
} else {
deskey(&k[32],&key[0],1);
deskey(&k[16],&key[8],0);
deskey(&k[0],&key[16],1);
}
}

236
op25/gr-op25/lib/desport.c Normal file
View File

@ -0,0 +1,236 @@
/* Portable C version of des() function */
#include <stdint.h>
#include "des.h"
/* Tables defined in the Data Encryption Standard documents
* Three of these tables, the initial permutation, the final
* permutation and the expansion operator, are regular enough that
* for speed, we hard-code them. They're here for reference only.
* Also, the S and P boxes are used by a separate program, gensp.c,
* to build the combined SP box, Spbox[]. They're also here just
* for reference.
*/
#ifdef notdef
/* initial permutation IP */
static unsigned char ip[] = {
58, 50, 42, 34, 26, 18, 10, 2,
60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6,
64, 56, 48, 40, 32, 24, 16, 8,
57, 49, 41, 33, 25, 17, 9, 1,
59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5,
63, 55, 47, 39, 31, 23, 15, 7
};
/* final permutation IP^-1 */
static unsigned char fp[] = {
40, 8, 48, 16, 56, 24, 64, 32,
39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30,
37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28,
35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25
};
/* expansion operation matrix */
static unsigned char ei[] = {
32, 1, 2, 3, 4, 5,
4, 5, 6, 7, 8, 9,
8, 9, 10, 11, 12, 13,
12, 13, 14, 15, 16, 17,
16, 17, 18, 19, 20, 21,
20, 21, 22, 23, 24, 25,
24, 25, 26, 27, 28, 29,
28, 29, 30, 31, 32, 1
};
/* The (in)famous S-boxes */
static unsigned char sbox[8][64] = {
/* S1 */
14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13,
/* S2 */
15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9,
/* S3 */
10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12,
/* S4 */
7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14,
/* S5 */
2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3,
/* S6 */
12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13,
/* S7 */
4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12,
/* S8 */
13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11
};
/* 32-bit permutation function P used on the output of the S-boxes */
static unsigned char p32i[] = {
16, 7, 20, 21,
29, 12, 28, 17,
1, 15, 23, 26,
5, 18, 31, 10,
2, 8, 24, 14,
32, 27, 3, 9,
19, 13, 30, 6,
22, 11, 4, 25
};
#endif
int Asmversion = 0;
/* Combined SP lookup table, linked in
* For best results, ensure that this is aligned on a 32-bit boundary;
* Borland C++ 3.1 doesn't guarantee this!
*/
extern uint64_t Spbox[8][64]; /* Combined S and P boxes */
/* Primitive function F.
* Input is r, subkey array in keys, output is XORed into l.
* Each round consumes eight 6-bit subkeys, one for
* each of the 8 S-boxes, 2 longs for each round.
* Each long contains four 6-bit subkeys, each taking up a byte.
* The first long contains, from high to low end, the subkeys for
* S-boxes 1, 3, 5 & 7; the second contains the subkeys for S-boxes
* 2, 4, 6 & 8 (using the origin-1 S-box numbering in the standard,
* not the origin-0 numbering used elsewhere in this code)
* See comments elsewhere about the pre-rotated values of r and Spbox.
*/
#define F(l,r,key){\
work = ((r >> 4) | (r << 28)) ^ key[0];\
l ^= Spbox[6][work & 0x3f];\
l ^= Spbox[4][(work >> 8) & 0x3f];\
l ^= Spbox[2][(work >> 16) & 0x3f];\
l ^= Spbox[0][(work >> 24) & 0x3f];\
work = r ^ key[1];\
l ^= Spbox[7][work & 0x3f];\
l ^= Spbox[5][(work >> 8) & 0x3f];\
l ^= Spbox[3][(work >> 16) & 0x3f];\
l ^= Spbox[1][(work >> 24) & 0x3f];\
}
/* Encrypt or decrypt a block of data in ECB mode */
void
des(unsigned long ks[16][2],unsigned char block[8])
/* Key schedule */
/* Data block */
{
unsigned long left,right,work;
/* Read input block and place in left/right in big-endian order */
left = ((unsigned long)block[0] << 24)
| ((unsigned long)block[1] << 16)
| ((unsigned long)block[2] << 8)
| (unsigned long)block[3];
right = ((unsigned long)block[4] << 24)
| ((unsigned long)block[5] << 16)
| ((unsigned long)block[6] << 8)
| (unsigned long)block[7];
/* Hoey's clever initial permutation algorithm, from Outerbridge
* (see Schneier p 478)
*
* The convention here is the same as Outerbridge: rotate each
* register left by 1 bit, i.e., so that "left" contains permuted
* input bits 2, 3, 4, ... 1 and "right" contains 33, 34, 35, ... 32
* (using origin-1 numbering as in the FIPS). This allows us to avoid
* one of the two rotates that would otherwise be required in each of
* the 16 rounds.
*/
work = ((left >> 4) ^ right) & 0x0f0f0f0f;
right ^= work;
left ^= work << 4;
work = ((left >> 16) ^ right) & 0xffff;
right ^= work;
left ^= work << 16;
work = ((right >> 2) ^ left) & 0x33333333;
left ^= work;
right ^= (work << 2);
work = ((right >> 8) ^ left) & 0xff00ff;
left ^= work;
right ^= (work << 8);
right = (right << 1) | (right >> 31);
work = (left ^ right) & 0xaaaaaaaa;
left ^= work;
right ^= work;
left = (left << 1) | (left >> 31);
/* Now do the 16 rounds */
F(left,right,ks[0]);
F(right,left,ks[1]);
F(left,right,ks[2]);
F(right,left,ks[3]);
F(left,right,ks[4]);
F(right,left,ks[5]);
F(left,right,ks[6]);
F(right,left,ks[7]);
F(left,right,ks[8]);
F(right,left,ks[9]);
F(left,right,ks[10]);
F(right,left,ks[11]);
F(left,right,ks[12]);
F(right,left,ks[13]);
F(left,right,ks[14]);
F(right,left,ks[15]);
/* Inverse permutation, also from Hoey via Outerbridge and Schneier */
right = (right << 31) | (right >> 1);
work = (left ^ right) & 0xaaaaaaaa;
left ^= work;
right ^= work;
left = (left >> 1) | (left << 31);
work = ((left >> 8) ^ right) & 0xff00ff;
right ^= work;
left ^= work << 8;
work = ((left >> 2) ^ right) & 0x33333333;
right ^= work;
left ^= work << 2;
work = ((right >> 16) ^ left) & 0xffff;
left ^= work;
right ^= work << 16;
work = ((right >> 4) ^ left) & 0x0f0f0f0f;
left ^= work;
right ^= work << 4;
/* Put the block back into the user's buffer with final swap */
block[0] = right >> 24;
block[1] = right >> 16;
block[2] = right >> 8;
block[3] = right;
block[4] = left >> 24;
block[5] = left >> 16;
block[6] = left >> 8;
block[7] = left;
}

131
op25/gr-op25/lib/dessp.c Normal file
View File

@ -0,0 +1,131 @@
#include <stdint.h>
uint64_t Spbox[8][64] = {
0x01010400,0x00000000,0x00010000,0x01010404,
0x01010004,0x00010404,0x00000004,0x00010000,
0x00000400,0x01010400,0x01010404,0x00000400,
0x01000404,0x01010004,0x01000000,0x00000004,
0x00000404,0x01000400,0x01000400,0x00010400,
0x00010400,0x01010000,0x01010000,0x01000404,
0x00010004,0x01000004,0x01000004,0x00010004,
0x00000000,0x00000404,0x00010404,0x01000000,
0x00010000,0x01010404,0x00000004,0x01010000,
0x01010400,0x01000000,0x01000000,0x00000400,
0x01010004,0x00010000,0x00010400,0x01000004,
0x00000400,0x00000004,0x01000404,0x00010404,
0x01010404,0x00010004,0x01010000,0x01000404,
0x01000004,0x00000404,0x00010404,0x01010400,
0x00000404,0x01000400,0x01000400,0x00000000,
0x00010004,0x00010400,0x00000000,0x01010004,
0x80108020,0x80008000,0x00008000,0x00108020,
0x00100000,0x00000020,0x80100020,0x80008020,
0x80000020,0x80108020,0x80108000,0x80000000,
0x80008000,0x00100000,0x00000020,0x80100020,
0x00108000,0x00100020,0x80008020,0x00000000,
0x80000000,0x00008000,0x00108020,0x80100000,
0x00100020,0x80000020,0x00000000,0x00108000,
0x00008020,0x80108000,0x80100000,0x00008020,
0x00000000,0x00108020,0x80100020,0x00100000,
0x80008020,0x80100000,0x80108000,0x00008000,
0x80100000,0x80008000,0x00000020,0x80108020,
0x00108020,0x00000020,0x00008000,0x80000000,
0x00008020,0x80108000,0x00100000,0x80000020,
0x00100020,0x80008020,0x80000020,0x00100020,
0x00108000,0x00000000,0x80008000,0x00008020,
0x80000000,0x80100020,0x80108020,0x00108000,
0x00000208,0x08020200,0x00000000,0x08020008,
0x08000200,0x00000000,0x00020208,0x08000200,
0x00020008,0x08000008,0x08000008,0x00020000,
0x08020208,0x00020008,0x08020000,0x00000208,
0x08000000,0x00000008,0x08020200,0x00000200,
0x00020200,0x08020000,0x08020008,0x00020208,
0x08000208,0x00020200,0x00020000,0x08000208,
0x00000008,0x08020208,0x00000200,0x08000000,
0x08020200,0x08000000,0x00020008,0x00000208,
0x00020000,0x08020200,0x08000200,0x00000000,
0x00000200,0x00020008,0x08020208,0x08000200,
0x08000008,0x00000200,0x00000000,0x08020008,
0x08000208,0x00020000,0x08000000,0x08020208,
0x00000008,0x00020208,0x00020200,0x08000008,
0x08020000,0x08000208,0x00000208,0x08020000,
0x00020208,0x00000008,0x08020008,0x00020200,
0x100802001,0x100002081,0x100002081,0x00000080,
0x00802080,0x100800081,0x100800001,0x100002001,
0x00000000,0x00802000,0x00802000,0x100802081,
0x100000081,0x00000000,0x00800080,0x100800001,
0x100000001,0x00002000,0x00800000,0x100802001,
0x00000080,0x00800000,0x100002001,0x00002080,
0x100800081,0x100000001,0x00002080,0x00800080,
0x00002000,0x00802080,0x100802081,0x100000081,
0x00800080,0x100800001,0x00802000,0x100802081,
0x100000081,0x00000000,0x00000000,0x00802000,
0x00002080,0x00800080,0x100800081,0x100000001,
0x100802001,0x100002081,0x100002081,0x00000080,
0x100802081,0x100000081,0x100000001,0x00002000,
0x100800001,0x100002001,0x00802080,0x100800081,
0x100002001,0x00002080,0x00800000,0x100802001,
0x00000080,0x00800000,0x00002000,0x00802080,
0x00000100,0x02080100,0x02080000,0x42000100,
0x00080000,0x00000100,0x40000000,0x02080000,
0x40080100,0x00080000,0x02000100,0x40080100,
0x42000100,0x42080000,0x00080100,0x40000000,
0x02000000,0x40080000,0x40080000,0x00000000,
0x40000100,0x42080100,0x42080100,0x02000100,
0x42080000,0x40000100,0x00000000,0x42000000,
0x02080100,0x02000000,0x42000000,0x00080100,
0x00080000,0x42000100,0x00000100,0x02000000,
0x40000000,0x02080000,0x42000100,0x40080100,
0x02000100,0x40000000,0x42080000,0x02080100,
0x40080100,0x00000100,0x02000000,0x42080000,
0x42080100,0x00080100,0x42000000,0x42080100,
0x02080000,0x00000000,0x40080000,0x42000000,
0x00080100,0x02000100,0x40000100,0x00080000,
0x00000000,0x40080000,0x02080100,0x40000100,
0x20000010,0x20400000,0x00004000,0x20404010,
0x20400000,0x00000010,0x20404010,0x00400000,
0x20004000,0x00404010,0x00400000,0x20000010,
0x00400010,0x20004000,0x20000000,0x00004010,
0x00000000,0x00400010,0x20004010,0x00004000,
0x00404000,0x20004010,0x00000010,0x20400010,
0x20400010,0x00000000,0x00404010,0x20404000,
0x00004010,0x00404000,0x20404000,0x20000000,
0x20004000,0x00000010,0x20400010,0x00404000,
0x20404010,0x00400000,0x00004010,0x20000010,
0x00400000,0x20004000,0x20000000,0x00004010,
0x20000010,0x20404010,0x00404000,0x20400000,
0x00404010,0x20404000,0x00000000,0x20400010,
0x00000010,0x00004000,0x20400000,0x00404010,
0x00004000,0x00400010,0x20004010,0x00000000,
0x20404000,0x20000000,0x00400010,0x20004010,
0x00200000,0x04200002,0x04000802,0x00000000,
0x00000800,0x04000802,0x00200802,0x04200800,
0x04200802,0x00200000,0x00000000,0x04000002,
0x00000002,0x04000000,0x04200002,0x00000802,
0x04000800,0x00200802,0x00200002,0x04000800,
0x04000002,0x04200000,0x04200800,0x00200002,
0x04200000,0x00000800,0x00000802,0x04200802,
0x00200800,0x00000002,0x04000000,0x00200800,
0x04000000,0x00200800,0x00200000,0x04000802,
0x04000802,0x04200002,0x04200002,0x00000002,
0x00200002,0x04000000,0x04000800,0x00200000,
0x04200800,0x00000802,0x00200802,0x04200800,
0x00000802,0x04000002,0x04200802,0x04200000,
0x00200800,0x00000000,0x00000002,0x04200802,
0x00000000,0x00200802,0x04200000,0x00000800,
0x04000002,0x04000800,0x00000800,0x00200002,
0x10001040,0x00001000,0x00040000,0x10041040,
0x10000000,0x10001040,0x00000040,0x10000000,
0x00040040,0x10040000,0x10041040,0x00041000,
0x10041000,0x00041040,0x00001000,0x00000040,
0x10040000,0x10000040,0x10001000,0x00001040,
0x00041000,0x00040040,0x10040040,0x10041000,
0x00001040,0x00000000,0x00000000,0x10040040,
0x10000040,0x10001000,0x00041040,0x00040000,
0x00041040,0x00040000,0x10041000,0x00001000,
0x00000040,0x10040040,0x00001000,0x00041040,
0x10001000,0x00000040,0x10000040,0x10040000,
0x10040040,0x10000000,0x00040000,0x10001040,
0x00000000,0x10041040,0x00040040,0x10000040,
0x10040000,0x10001000,0x10001040,0x00000000,
0x10041040,0x00041000,0x00041000,0x00001040,
0x00001040,0x00040040,0x10000000,0x10041000,
};

View File

@ -27,6 +27,7 @@
#include <stdio.h>
#include <gnuradio/io_signature.h>
#include <boost/scoped_array.hpp>
#include "fsk4_demod_ff_impl.h"
/*
@ -186,7 +187,7 @@ namespace gr {
gr::io_signature::make(1, 1, sizeof(float)),
gr::io_signature::make(1, 1, sizeof(float))),
d_block_rate(sample_rate_Hz / symbol_rate_Hz),
d_history(new float[NTAPS]),
my_d_history(new float[NTAPS]),
d_history_last(0),
d_queue(queue),
d_symbol_clock(0.0),
@ -196,7 +197,7 @@ namespace gr {
fine_frequency_correction = 0.0;
coarse_frequency_correction = 0.0;
std::fill(&d_history[0], &d_history[NTAPS], 0.0);
std::fill(&my_d_history[0], &my_d_history[NTAPS], 0.0);
}
/*
@ -269,7 +270,7 @@ namespace gr {
{
d_symbol_clock += d_symbol_time;
d_history[d_history_last++] = input;
my_d_history[d_history_last++] = input;
d_history_last %= NTAPS;
if(d_symbol_clock > 1.0) {
@ -296,8 +297,8 @@ namespace gr {
double interp = 0.0;
double interp_p1 = 0.0;
for(size_t i = 0, j = d_history_last; i < NTAPS; ++i) {
interp += TAPS[imu][i] * d_history[j];
interp_p1 += TAPS[imu_p1][i] * d_history[j];
interp += TAPS[imu][i] * my_d_history[j];
interp_p1 += TAPS[imu_p1][i] * my_d_history[j];
j = (j + 1) % NTAPS;
}
#else
@ -306,8 +307,8 @@ namespace gr {
double interp_p1 = 0.0;
for(int i=0; i<NTAPS; i++)
{
interp += TAPS[imu ][i] * d_history[j];
interp_p1 += TAPS[imu_p1][i] * d_history[j];
interp += TAPS[imu ][i] * my_d_history[j];
interp_p1 += TAPS[imu_p1][i] * my_d_history[j];
j = (j+1) % NTAPS;
}
#endif

View File

@ -33,7 +33,7 @@ namespace gr {
{
private:
const float d_block_rate;
boost::scoped_array<float> d_history;
boost::scoped_array<float> my_d_history;
size_t d_history_last;
gr::msg_queue::sptr d_queue;
double d_symbol_clock;

View File

@ -28,6 +28,8 @@
#include <iomanip>
#include <sstream>
#include <boost/format.hpp>
#include <stdio.h>
using namespace std;
@ -65,6 +67,8 @@ hdu::do_correct_errors(bit_vector& frame)
{
apply_golay_correction(frame);
apply_rs_correction(frame);
if (logging_enabled()) fprintf(stderr, "\n");
}
void
@ -136,33 +140,58 @@ hdu::frame_size_max() const
return 792;
}
string
hdu::algid_str() const
uint8_t
hdu::algid() const
{
const size_t ALGID_BITS[] = {
356, 357, 360, 361, 374, 375, 376, 377
};
const size_t ALGID_BITS_SZ = sizeof(ALGID_BITS) / sizeof(ALGID_BITS[0]);
uint8_t algid = extract(frame_body(), ALGID_BITS, ALGID_BITS_SZ);
return lookup(algid, ALGIDS, ALGIDS_SZ);
return extract(frame_body(), ALGID_BITS, ALGID_BITS_SZ);
}
string
hdu::kid_str() const
hdu::algid_str() const
{
uint8_t _algid = algid();
return lookup(_algid, ALGIDS, ALGIDS_SZ);
}
uint16_t
hdu::kid() const
{
const size_t KID_BITS[] = {
378, 379, 392, 393, 394, 395, 396, 397,
410, 411, 412, 413, 414, 415, 428, 429
};
const size_t KID_BITS_SZ = sizeof(KID_BITS) / sizeof(KID_BITS[0]);
uint16_t kid = extract(frame_body(), KID_BITS, KID_BITS_SZ);
return extract(frame_body(), KID_BITS, KID_BITS_SZ);
}
string
hdu::kid_str() const
{
uint16_t _kid = kid();
ostringstream os;
os << hex << showbase << setfill('0') << setw(4) << kid;
os << hex << showbase << setfill('0') << setw(4) << _kid;
return os.str();
}
std::string
hdu::mi_str() const
{
std::vector<uint8_t> _mi(mi());
ostringstream os;
os << "0x";
for(size_t i = 0; i < _mi.size(); ++i) {
uint16_t octet = _mi[i];
os << hex << setfill('0') << setw(2) << octet;
}
return os.str();
}
std::vector<uint8_t>
hdu::mi() const
{
const size_t MI_BITS[] = {
114, 115, 116, 117, 118, 119, 132, 133,
@ -177,15 +206,9 @@ hdu::mi_str() const
};
const size_t MI_BITS_SZ = sizeof(MI_BITS) / sizeof(MI_BITS[0]);
uint8_t mi[9];
extract(frame_body(), MI_BITS, MI_BITS_SZ, mi);
ostringstream os;
os << "0x";
for(size_t i = 0; i < (sizeof(mi) / sizeof(mi[0])); ++i) {
uint16_t octet = mi[i];
os << hex << setfill('0') << setw(2) << octet;
}
return os.str();
std::vector<uint8_t> _mi(((MI_BITS_SZ + 7) / 8));
extract(frame_body(), MI_BITS, MI_BITS_SZ, &_mi[0]);
return _mi;
}
string
@ -219,7 +242,21 @@ hdu::tgid_str() const
};
const size_t TGID_BITS_SZ = sizeof(TGID_BITS) / sizeof(TGID_BITS[0]);
const uint16_t tgid = extract(frame_body(), TGID_BITS, TGID_BITS_SZ);
ostringstream os;
os << hex << showbase << setfill('0') << setw(4) << tgid;
return os.str();
// Zero fill isn't working properly in original implementation
//ostringstream os;
//os << hex << showbase << setfill('0') << setw(4) << tgid;
//return os.str();
return (boost::format("0x%04x") % tgid).str();
}
struct CryptoState
hdu::crypto_state() const
{
struct CryptoState state;
state.mi = mi();
state.kid = kid();
state.algid = algid();
return state;
}

View File

@ -25,11 +25,12 @@
#define INCLUDED_HDU_H
#include "abstract_data_unit.h"
#include "crypto.h"
/**
* P25 header data unit (HDU).
*/
class hdu : public abstract_data_unit
class hdu : public abstract_data_unit, public crypto_state_provider
{
public:
@ -96,7 +97,9 @@ protected:
*/
virtual uint16_t frame_size_max() const;
private:
public:
uint8_t algid() const;
/**
* Return a string describing the encryption algorithm ID (ALGID).
@ -105,6 +108,8 @@ private:
*/
std::string algid_str() const;
virtual uint16_t kid() const;
/**
* Returns a string describing the key id (KID).
*
@ -119,6 +124,8 @@ private:
*/
virtual std::string mfid_str() const;
virtual std::vector<uint8_t> mi() const;
/**
* Returns a string describing the message indicator (MI).
*
@ -139,6 +146,10 @@ private:
* \return A string identifying the TGID.
*/
virtual std::string tgid_str() const;
public:
struct CryptoState crypto_state() const;
};
#endif /* INCLUDED_HDU_H */

102
op25/gr-op25/lib/ldu.cc Normal file
View File

@ -0,0 +1,102 @@
#include "ldu.h"
#include <stdio.h>
#include <itpp/base/vec.h>
#include <itpp/base/mat.h>
#include <itpp/base/binary.h>
#include <itpp/base/converters.h>
const static itpp::Mat<int> ham_10_6_3_6("1 1 1 0 0 1 1 0 0 0; 1 1 0 1 0 1 0 1 0 0; 1 0 1 1 1 0 0 0 1 0; 0 1 1 1 1 0 0 0 0 1");
typedef std::vector<itpp::Vec<int> > VecArray;
ldu::ldu(const_bit_queue& frame_body) :
voice_data_unit(frame_body),
m_hamming_error_count(0)
{
}
void
ldu::do_correct_errors(bit_vector& frame_body)
{
voice_data_unit::do_correct_errors(frame_body);
}
bool
ldu::process_meta_data(bit_vector& frame_body)
{
m_hamming_error_count = 0;
//std::vector<uint8_t> lc(30);
//std::vector<uint16_t> ham(24);
int lc_bit_idx = 0;
VecArray arrayVec;
itpp::Vec<int> vecRaw(10); // First 6 bits contain data
for (int i = 400; i < 1360; i += 184)
{
for (int j = 0; j < 40; j++)
{
int x = (i + j) + (((i + j) / 70) * 2); // Adjust bit index for status
unsigned char ch = frame_body[x];
//lc[lc_bit_idx / 8] |= (ch << (7 - (lc_bit_idx % 8)));
//ham[lc_bit_idx / 10] = ((ham[lc_bit_idx / 10]) << 1) | ch;
vecRaw(lc_bit_idx % 10) = ch;
++lc_bit_idx;
if ((lc_bit_idx % 10) == 0)
arrayVec.push_back(vecRaw);
}
}
if (lc_bit_idx != 240) // Not enough bits
{
return false;
}
if (arrayVec.size() != 24) // Not enough vectors
{
return false;
}
itpp::Vec<int> vecZero(4);
vecZero.zeros();
m_raw_meta_data.clear();
for (int i = 0; i < arrayVec.size(); ++i)
{
itpp::Vec<int>& vec = arrayVec[i];
itpp::bvec vB(itpp::to_bvec(vec));
itpp::Vec<int> vS = ham_10_6_3_6 * vec;
for (int i = 0; i < vS.length(); ++i)
vS[i] = vS[i] % 2;
itpp::bvec vb(to_bvec(vS));
if (itpp::bin2dec(vb) != 0)
{
++m_hamming_error_count;
}
m_raw_meta_data = concat(m_raw_meta_data, vB.mid(0, 6)); // Includes RS for last 72 bits
}
if (logging_enabled()) fprintf(stderr, "%s: %lu hamming errors, %s\n", duid_str().c_str(), m_hamming_error_count, (meta_data_valid() ? "valid" : "invalid"));
return meta_data_valid();
}
const itpp::bvec&
ldu::raw_meta_data() const
{
return m_raw_meta_data;
}
bool
ldu::meta_data_valid() const
{
return (m_raw_meta_data.length() == 144); // Not enough bits after Hamming(10,6,3)
}

28
op25/gr-op25/lib/ldu.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef INCLUDED_LDU_H
#define INCLUDED_LDU_H
#include "voice_data_unit.h"
class ldu : public voice_data_unit
{
private:
size_t m_hamming_error_count;
itpp::bvec m_raw_meta_data;
protected:
virtual void do_correct_errors(bit_vector& frame_body);
virtual bool process_meta_data(bit_vector& frame_body);
virtual const itpp::bvec& raw_meta_data() const;
public:
ldu(const_bit_queue& frame_body);
virtual bool meta_data_valid() const;
};
#endif // INCLUDED_LDU_H

View File

@ -23,10 +23,19 @@
#include "ldu1.h"
#include <itpp/base/vec.h>
#include <itpp/base/converters.h>
#include <boost/format.hpp>
#include <iostream>
#include "pickle.h"
#include "value_string.h"
using std::string;
ldu1::ldu1(const_bit_queue& frame_body) :
voice_data_unit(frame_body)
ldu(frame_body)
{
}
@ -34,6 +43,64 @@ ldu1::~ldu1()
{
}
void ldu1::do_correct_errors(bit_vector& frame_body)
{
ldu::do_correct_errors(frame_body);
if (!process_meta_data(frame_body))
return;
const itpp::bvec& data = raw_meta_data();
std::stringstream ss;
m_meta_data.m.lcf = bin2dec(data.mid(0, 8));
m_meta_data.m.mfid = bin2dec(data.mid(8, 8));
ss << (boost::format("%s: LCF: 0x%02x, MFID: 0x%02x") % duid_str() % m_meta_data.m.lcf % m_meta_data.m.mfid);
if (m_meta_data.m.lcf == 0x00)
{
m_meta_data.m0.emergency = data[16];
m_meta_data.m0.reserved = bin2dec(data.mid(17, 15));
m_meta_data.m0.tgid = bin2dec(data.mid(32, 16));
m_meta_data.m0.source = bin2dec(data.mid(48, 24));
ss << (boost::format(", Emergency: 0x%02x, Reserved: 0x%04x, TGID: 0x%04x, Source: 0x%06x") % m_meta_data.m0.emergency % m_meta_data.m0.reserved % m_meta_data.m0.tgid % m_meta_data.m0.source);
}
else if (m_meta_data.m.lcf == 0x03)
{
m_meta_data.m3.reserved = bin2dec(data.mid(16, 8));
m_meta_data.m3.destination = bin2dec(data.mid(24, 24));
m_meta_data.m3.source = bin2dec(data.mid(48, 24));
ss << (boost::format(", Reserved: 0x%02x, Destination: 0x%06x, Source: 0x%06x") % m_meta_data.m3.reserved % m_meta_data.m3.destination % m_meta_data.m3.source);
}
else
{
ss << " (unknown LCF)";
}
if (logging_enabled()) std::cerr << ss.str() << std::endl;
}
std::string
ldu1::snapshot() const
{
pickle p;
p.add("duid", duid_str());
p.add("mfid", lookup(m_meta_data.m.mfid, MFIDS, MFIDS_SZ));
if ((m_meta_data.m.lcf == 0x00) || (m_meta_data.m.lcf == 0x03))
p.add("source", (boost::format("0x%06x") % m_meta_data.m0.source).str());
if (m_meta_data.m.lcf == 0x00)
p.add("tgid", (boost::format("0x%04x") % m_meta_data.m0.tgid).str());
if (m_meta_data.m.lcf == 0x03)
p.add("dest", (boost::format("0x%06x") % m_meta_data.m3.destination).str());
return p.to_string();
}
ldu1::combined_meta_data
ldu1::meta_data() const
{
return m_meta_data;
}
string
ldu1::duid_str() const
{

View File

@ -24,13 +24,45 @@
#ifndef INCLUDED_LDU1_H
#define INCLUDED_LDU1_H
#include "voice_data_unit.h"
#include "ldu.h"
/**
* P25 Logical Data Unit 1.
*/
class ldu1 : public voice_data_unit
class ldu1 : public ldu
{
protected:
void do_correct_errors(bit_vector& frame_body);
public:
struct base_meta_data
{
unsigned char lcf;
unsigned char mfid;
unsigned int source;
unsigned short reserved;
};
struct meta_data_0 : public base_meta_data
{
bool emergency;
unsigned short tgid;
};
struct meta_data_3 : public base_meta_data
{
unsigned int destination;
};
union combined_meta_data
{
base_meta_data m;
meta_data_0 m0;
meta_data_3 m3;
} m_meta_data;
public:
/**
@ -50,6 +82,10 @@ public:
*/
std::string duid_str() const;
virtual std::string snapshot() const;
combined_meta_data meta_data() const;
};
#endif /* INCLUDED_LDU1_H */

View File

@ -23,10 +23,18 @@
#include "ldu2.h"
#include <itpp/base/vec.h>
#include <itpp/base/converters.h>
#include <boost/format.hpp>
#include "pickle.h"
#include "value_string.h"
using std::string;
ldu2::ldu2(const_bit_queue& frame_body) :
voice_data_unit(frame_body)
ldu(frame_body)
{
}
@ -39,3 +47,43 @@ ldu2::duid_str() const
{
return string("LDU2");
}
std::string
ldu2::snapshot() const
{
pickle p;
p.add("duid", duid_str());
std::stringstream ss;
ss << "0x";
for (size_t n = 0; n < m_crypto_state.mi.size(); ++n)
ss << (boost::format("%02x") % (int)m_crypto_state.mi[n]);
p.add("mi", ss.str());
p.add("algid", lookup(m_crypto_state.algid, ALGIDS, ALGIDS_SZ));
p.add("kid", (boost::format("0x%04x") % m_crypto_state.kid).str());
return p.to_string();
}
void
ldu2::do_correct_errors(bit_vector& frame_body)
{
ldu::do_correct_errors(frame_body);
if (!process_meta_data(frame_body))
return;
const itpp::bvec& data = raw_meta_data();
for (int i = 0; i < 72; i += 8)
{
m_crypto_state.mi[i/8] = bin2dec(data.mid(i, 8));
}
m_crypto_state.algid = bin2dec(data.mid(72, 8));
m_crypto_state.kid = bin2dec(data.mid(80, 16));
}
struct CryptoState
ldu2::crypto_state() const
{
return m_crypto_state;
}

View File

@ -24,13 +24,22 @@
#ifndef INCLUDED_LDU2_H
#define INCLUDED_LDU2_H
#include "voice_data_unit.h"
#include "ldu.h"
#include "crypto.h"
/**
* P25 Logical Data Unit 2.
*/
class ldu2 : public voice_data_unit
class ldu2 : public ldu, public crypto_state_provider
{
private:
struct CryptoState m_crypto_state;
protected:
void do_correct_errors(bit_vector& frame_body);
public:
/**
@ -49,6 +58,10 @@ public:
* Returns a string describing the Data Unit ID (DUID).
*/
std::string duid_str() const;
virtual std::string snapshot() const;
struct CryptoState crypto_state() const;
};
#endif /* INCLUDED_LDU2_H */

View File

@ -0,0 +1,63 @@
/* -*- c++ -*- */
/*
* Copyright 2005 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gnuradio/message.h>
#include <cassert>
#include <cstring>
namespace gr {
namespace op25 {
static long s_ncurrently_allocated = 0;
message::sptr message::make(long type, double arg1, double arg2, size_t length)
{
return message::sptr(new message(type, arg1, arg2, length));
}
message::sptr
message::make_from_string(const std::string s, long type, double arg1, double arg2)
{
message::sptr m = message::make(type, arg1, arg2, s.size());
memcpy(m->msg(), s.data(), s.size());
return m;
}
message::message(long type, double arg1, double arg2, size_t length)
: d_type(type), d_arg1(arg1), d_arg2(arg2), d_buf(length)
{
if (length == 0)
d_msg_start = d_msg_end = nullptr;
else {
d_msg_start = d_buf.data();
d_msg_end = d_msg_start + length;
}
s_ncurrently_allocated++;
}
message::~message()
{
assert(d_next == 0);
s_ncurrently_allocated--;
}
std::string message::to_string() const
{
return std::string((char*)d_msg_start, length());
}
long message_ncurrently_allocated() { return s_ncurrently_allocated; }
} /* namespace op25 */
} /* namespace gr */

View File

@ -0,0 +1,23 @@
/* -*- c++ -*- */
/*
* Copyright 2005,2013 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <op25/msg_handler.h>
namespace gr {
namespace op25 {
msg_handler::~msg_handler() {}
} /* namespace op25 */
} /* namespace gr */

View File

@ -0,0 +1,113 @@
/* -*- c++ -*- */
/*
* Copyright 2005,2009,2013 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gnuradio/msg_queue.h>
#include <stdexcept>
namespace gr {
namespace op25 {
msg_queue::sptr msg_queue::make(unsigned int limit)
{
return msg_queue::sptr(new msg_queue(limit));
}
msg_queue::msg_queue(unsigned int limit)
: d_not_empty(),
d_not_full(),
/*d_head(0), d_tail(0),*/ d_count(0),
d_limit(limit)
{
}
msg_queue::~msg_queue() { flush(); }
void msg_queue::insert_tail(message::sptr msg)
{
if (msg->d_next)
throw std::invalid_argument("gr::msg_queue::insert_tail: msg already in queue");
gr::thread::scoped_lock guard(d_mutex);
while (full_p())
d_not_full.wait(guard);
if (d_tail == 0) {
d_tail = d_head = msg;
// msg->d_next = 0;
msg->d_next.reset();
} else {
d_tail->d_next = msg;
d_tail = msg;
// msg->d_next = 0;
msg->d_next.reset();
}
d_count++;
d_not_empty.notify_one();
}
message::sptr msg_queue::delete_head()
{
gr::thread::scoped_lock guard(d_mutex);
message::sptr m;
while ((m = d_head) == 0)
d_not_empty.wait(guard);
d_head = m->d_next;
if (d_head == 0) {
// d_tail = 0;
d_tail.reset();
}
d_count--;
// m->d_next = 0;
m->d_next.reset();
d_not_full.notify_one();
return m;
}
message::sptr msg_queue::delete_head_nowait()
{
gr::thread::scoped_lock guard(d_mutex);
message::sptr m;
if ((m = d_head) == 0) {
// return 0;
return message::sptr();
}
d_head = m->d_next;
if (d_head == 0) {
// d_tail = 0;
d_tail.reset();
}
d_count--;
// m->d_next = 0;
m->d_next.reset();
d_not_full.notify_one();
return m;
}
void msg_queue::flush()
{
message::sptr m;
while ((m = delete_head_nowait()) != 0)
;
}
} /* namespace op25 */
} /* namespace gr */

View File

@ -1,113 +0,0 @@
/* -*- C++ -*- */
%feature("autodoc", "1");
%{
#include <stddef.h>
%}
%include "exception.i"
%import "gnuradio.i"
%{
#include "gnuradio/swig/gnuradio_swig_bug_workaround.h"
#include "op25_fsk4_demod_ff.h"
#include "op25_fsk4_slicer_fb.h"
#include "op25_decoder_bf.h"
#include "op25_pcap_source_b.h"
%}
// ----------------------------------------------------------------
/*
* This does some behind-the-scenes magic so we can
* access fsk4_square_ff from python as fsk4.square_ff
*/
GR_SWIG_BLOCK_MAGIC(op25, fsk4_demod_ff);
/*
* Publicly-accesible default constuctor function for op25_fsk4_demod_bf.
*/
op25_fsk4_demod_ff_sptr op25_make_fsk4_demod_ff(gr::msg_queue::sptr queue, float sample_rate, float symbol_rate);
class op25_fsk4_demod_ff : public gr_block
{
private:
op25_fsk4_demod_ff(gr::msg_queue::sptr queue, float sample_rate, float symbol_rate);
};
// ----------------------------------------------------------------
/*
* This does some behind-the-scenes magic so we can invoke
* op25_make_slicer_fb from python as op25.slicer_fbf.
*/
GR_SWIG_BLOCK_MAGIC(op25, fsk4_slicer_fb);
/*
* Publicly-accesible default constuctor function for op25_decoder_bf.
*/
op25_fsk4_slicer_fb_sptr op25_make_fsk4_slicer_fb(const std::vector<float> &slice_levels);
/*
* The op25_fsk4_slicer block. Takes a series of float samples and
* partitions them into dibit symbols according to the slices_levels
* provided to the constructor.
*/
class op25_fsk4_slicer_fb : public gr_sync_block
{
private:
op25_fsk4_slicer_fb (const std::vector<float> &slice_levels);
};
// ----------------------------------------------------------------
/*
* This does some behind-the-scenes magic so we can invoke
* op25_make_decoder_bsf from python as op25.decoder_bf.
*/
GR_SWIG_BLOCK_MAGIC(op25, decoder_bf);
/*
* Publicly-accesible default constuctor function for op25_decoder_bf.
*/
op25_decoder_bf_sptr op25_make_decoder_bf();
/**
* The op25_decoder_bf block. Accepts a stream of dibit symbols and
* produces an 8KS/s audio stream.
*/
class op25_decoder_bf : public gr_block
{
private:
op25_decoder_bf();
public:
const char *destination() const;
gr::msg_queue::sptr get_msgq() const;
void set_msgq(gr::msg_queue::sptr msgq);
};
// ----------------------------------------------------------------
/*
* This does some behind-the-scenes magic so we can invoke
* op25_make_pcap_source_b from python as op25.pcap_source_b.
*/
GR_SWIG_BLOCK_MAGIC(op25, pcap_source_b);
/*
* Publicly-accesible constuctor function for op25_pcap_source.
*/
op25_pcap_source_b_sptr op25_make_pcap_source_b(const char *path, float delay);
/*
* The op25_pcap_source block. Reads symbols from a tcpdump-formatted
* file and produces a stream of symbols of the appropriate size.
*/
class op25_pcap_source_b : public gr_sync_block
{
private:
op25_pcap_source_b(const char *path, float delay);
};
// ----------------------------------------------------------------

View File

@ -3,6 +3,7 @@
#include <cstddef>
#include <stdint.h>
#include <assert.h>
/*
* APCO Hamming(15,11,3) ecoder.
@ -183,4 +184,18 @@ hamming_15_decode(uint16_t& cw)
return errs;
}
static const uint32_t hmg1063EncTbl[64] = {
0, 12, 3, 15, 7, 11, 4, 8, 11, 7, 8, 4, 12, 0, 15, 3,
13, 1, 14, 2, 10, 6, 9, 5, 6, 10, 5, 9, 1, 13, 2, 14,
14, 2, 13, 1, 9, 5, 10, 6, 5, 9, 6, 10, 2, 14, 1, 13,
3, 15, 0, 12, 4, 8, 7, 11, 8, 4, 11, 7, 15, 3, 12, 0 };
static const uint32_t hmg1063DecTbl[16] = {
0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 8, 1, 16, 32, 0 };
static inline int hmg1063Dec (uint32_t Dat, uint32_t Par) {
assert ((Dat < 64) && (Par < 16));
return Dat ^ hmg1063DecTbl[hmg1063EncTbl[Dat] ^ Par];
}
#endif /* INCLUDED_OP25_HAMMING_H */

View File

@ -10,9 +10,54 @@
#include <vector>
typedef std::vector<bool> voice_codeword;
typedef std::vector<uint8_t> packed_codeword;
typedef const std::vector<bool> const_bit_vector;
typedef std::vector<bool> bit_vector;
static const uint16_t hdu_codeword_bits[658] = { // 329 symbols = 324 + 5 pad
114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 144, 145, 146, 147,
148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163,
164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
212, 213, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245,
246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261,
262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277,
278, 279, 280, 281, 282, 283, 284, 285, 288, 289, 290, 291, 292, 293, 294, 295,
296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311,
312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327,
328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343,
344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 360, 361,
362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377,
378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393,
394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409,
410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425,
426, 427, 428, 429, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443,
444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459,
460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475,
476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491,
492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 504, 505, 506, 507, 508, 509,
510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525,
526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541,
542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557,
558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573,
576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591,
592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607,
608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623,
624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639,
640, 641, 642, 643, 644, 645, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657,
658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673,
674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689,
690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705,
706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 720, 721, 722, 723,
724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739,
740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755,
756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771,
772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787,
788, 789 };
static const size_t nof_voice_codewords = 9, voice_codeword_sz = 144;
static const uint16_t imbe_ldu_NID_bits[] = {
@ -251,8 +296,8 @@ pngen23(uint32_t& Pr)
* \param u0-u7 Result output vectors
*/
static inline void
imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_t& u2, uint32_t& u3, uint32_t& u4, uint32_t& u5, uint32_t& u6, uint32_t& u7, uint32_t& E0, uint32_t& ET)
static inline size_t
imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_t& u2, uint32_t& u3, uint32_t& u4, uint32_t& u5, uint32_t& u6, uint32_t& u7, uint32_t& E0, uint32_t& ET, bool bot_shift = true)
{
ET = 0;
@ -294,7 +339,30 @@ imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_
u6 = v6;
u7 = extract(cw, 137, 144);
u7 <<= 1; /* so that bit0 is free (see note about BOT bit */
if (bot_shift)
u7 <<= 1; /* so that bit0 is free (see note about BOT bit */
return errs;
}
/*
* Pack 88 bit IMBE parameters into uint8_t vector
*/
static inline void
imbe_pack(packed_codeword& cw, uint32_t u0, uint32_t u1, uint32_t u2, uint32_t u3, uint32_t u4, uint32_t u5, uint32_t u6, uint32_t u7)
{
cw.empty();
cw.push_back(u0 >> 4);
cw.push_back(((u0 & 0xf) << 4) + (u1 >> 8));
cw.push_back(u1 & 0xff);
cw.push_back(u2 >> 4);
cw.push_back(((u2 & 0xf) << 4) + (u3 >> 8));
cw.push_back(u3 & 0xff);
cw.push_back(u4 >> 3);
cw.push_back(((u4 & 0x7) << 5) + (u5 >> 6));
cw.push_back(((u5 & 0x3f) << 2) + (u6 >> 9));
cw.push_back(u6 >> 1);
cw.push_back(((u6 & 0x1) << 7) + (u7 >> 1));
}
/* APCO IMBE header encoder.

View File

@ -2,6 +2,7 @@
#define INCLUDED_SWAB_H
#include <stdint.h>
#include <algorithm>
/**
* Yank in[bits[0]..bits[bits_sz]) to out[where,where+bits_sz).

View File

@ -2,6 +2,7 @@
/*
* Copyright 2008 Steve Glass
* Copyright 2022 Matt Ames
*
* This file is part of OP25.
*
@ -53,18 +54,40 @@ const value_string ALGIDS[] = {
{ 0x02, "FIREFLY Type 1" },
{ 0x03, "MAYFLY Type 1" },
{ 0x04, "SAVILLE" },
{ 0x05, "Motorola Assigned - PADSTONE" },
{ 0x41, "BATON (Auto Odd)" },
/* Type III */
{ 0x80, "Plain" },
{ 0x81, "DES-OFB" },
{ 0x82, "2 key Triple DES" },
{ 0x83, "3 key Triple DES" },
{ 0x84, "AES-256" },
/* Motorola proprietary */
{ 0x9F, "Motorola DES-XL" },
{ 0x80, "Unencrypted" },
{ 0x81, "DES-OFB, 56 bit key" },
{ 0x83, "3 key Triple DES, 168 bit key" },
{ 0x84, "AES-256-OFB" },
{ 0x85, "AES-128-ECB"},
{ 0x88, "AES-CBC"},
{ 0x89, "AES-128-OFB"},
/* Motorola proprietary - some of these have been observed over the air,
some have been taken from firmware dumps on various devices, others
have come from the TIA's FTP website while it was still public,
from document "ALGID Guide 2015-04-15.pdf", and others have been
have been worked out with a little bit of "guesswork" ;) */
{ 0x9F, "Motorola DES-XL 56-bit key" },
{ 0xA0, "Motorola DVI-XL" },
{ 0xA1, "Motorola DVP-XL" },
{ 0xAA, "Motorola ADP" },
{ 0xA2, "Motorola DVI-XL-SPFL"},
{ 0xA3, "Motorola HAYSTACK" },
{ 0xA4, "Motorola Assigned - Unknown" },
{ 0xA5, "Motorola Assigned - Unknown" },
{ 0xA6, "Motorola Assigned - Unknown" },
{ 0xA7, "Motorola Assigned - Unknown" },
{ 0xA8, "Motorola Assigned - Unknown" },
{ 0xA9, "Motorola Assigned - Unknown" },
{ 0xAA, "Motorola ADP (40 bit RC4)" },
{ 0xAB, "Motorola CFX-256" },
{ 0xAC, "Motorola GOST 28147-89 (RFC 5830)" },
{ 0xAD, "Motorola Assigned - LOCALIZED" },
{ 0xAE, "Motorola Assigned - Unknown" },
{ 0xAF, "Motorola AES+" },
{ 0xB0, "Motorola DVP"},
{ 0xD0, "Motorola LOCAL_BR"}
};
const size_t ALGIDS_SZ = sizeof(ALGIDS) / sizeof(ALGIDS[0]);

View File

@ -24,32 +24,325 @@
#include "voice_data_unit.h"
#include "op25_imbe_frame.h"
#include <sstream>
#include <map>
#include <iostream>
#include <boost/format.hpp>
#include <stdio.h>
#include <itpp/base/vec.h>
#include <itpp/base/mat.h>
#include <itpp/base/binary.h>
#include <itpp/base/converters.h>
using namespace std;
voice_data_unit::~voice_data_unit()
static void vec_mod(itpp::ivec& vec, int modulus = 2)
{
for (int i = 0; i < vec.length(); ++i)
vec[i] = vec[i] % modulus;
}
class cyclic_16_8_5_syndromes
{
public:
typedef map<unsigned char,unsigned short> SyndromeTableMap;
const static itpp::imat cyclic_16_8_5;
private:
SyndromeTableMap m_syndrome_table;
public:
inline const SyndromeTableMap table() const
{
return m_syndrome_table;
}
cyclic_16_8_5_syndromes(bool generate_now = false)
{
if (generate_now)
generate();
}
int generate()
{
if (m_syndrome_table.empty() == false)
return -1;
// n=16, k=8
// E1
itpp::ivec v("1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0");
itpp::ivec r(cyclic_16_8_5 * v);
vec_mod(r);
itpp::bvec b(to_bvec(r));
unsigned char ch = (unsigned char)bin2dec(b);
itpp::bvec bV(to_bvec(v));
unsigned short us = (unsigned short)bin2dec(bV);
m_syndrome_table.insert(make_pair(ch, us));
// E2
for (int i = 0; i <= (16 - 2); ++i)
{
itpp::ivec v2(v);
v2[15-i] = 1;
r = cyclic_16_8_5 * v2;
bV = itpp::to_bvec(v2);
vec_mod(r);
b = itpp::to_bvec(r);
unsigned char ch = (unsigned char)itpp::bin2dec(b);
unsigned short us = (unsigned short)itpp::bin2dec(bV);
m_syndrome_table.insert(make_pair(ch, us));
}
// E3 - disabled: min.d = 5, t=floor(5/2)=2
/*for (int i = 0; i <= (16 - 2); ++i)
{
for (int j = 0; j < i; ++j)
{
ivec v3(v);
v3[15-i] = 1;
v3[15-j] = 1;
r = cyclic_16_8_5 * v3;
bV = to_bvec(v3);
vec_mod(r);
b = to_bvec(r);
unsigned char ch = (unsigned char)bin2dec(b);
unsigned short us = (unsigned short)bin2dec(bV);
m_syndrome_table.insert(make_pair(ch, us));
}
}*/
return m_syndrome_table.size();
}
};
const itpp::imat cyclic_16_8_5_syndromes::cyclic_16_8_5(
"0 0 1 1 1 1 0 0 1 0 0 0 0 0 0 0;"
"1 0 0 1 1 1 1 0 0 1 0 0 0 0 0 0;"
"0 1 0 0 1 1 1 1 0 0 1 0 0 0 0 0;"
"0 0 0 1 1 0 1 1 0 0 0 1 0 0 0 0;"
"1 0 1 1 0 0 0 1 0 0 0 0 1 0 0 0;"
"1 1 1 0 0 1 0 0 0 0 0 0 0 1 0 0;"
"1 1 1 1 0 0 1 0 0 0 0 0 0 0 1 0;"
"0 1 1 1 1 0 0 1 0 0 0 0 0 0 0 1"
);
static cyclic_16_8_5_syndromes g_cyclic_16_8_5_syndromes(true);
static int decode_cyclic_16_8_5(const itpp::ivec& vec, itpp::ivec& out)
{
itpp::ivec vc(cyclic_16_8_5_syndromes::cyclic_16_8_5 * vec);
vec_mod(vc);
itpp::bvec vb(to_bvec(vc));
unsigned char ch = (unsigned char)itpp::bin2dec(vb);
if (ch == 0x00)
return 0;
const cyclic_16_8_5_syndromes::SyndromeTableMap& syndrome_table = g_cyclic_16_8_5_syndromes.table();
cyclic_16_8_5_syndromes::SyndromeTableMap::const_iterator it = syndrome_table.find(ch);
int j = 0;
while (it == syndrome_table.end())
{
++j;
vc = itpp::concat(itpp::ivec("0 0 0 0 0 0 0 0"), vc); // Restore to 16 bits
vc.shift_left(vc[0]); // Rotate (s * x)
vc = cyclic_16_8_5_syndromes::cyclic_16_8_5 * vc;
vec_mod(vc);
vb = itpp::to_bvec(vc);
ch = (unsigned char)itpp::bin2dec(vb);
it = syndrome_table.find(ch);
if (j >= 15)
break;
}
if (it == syndrome_table.end())
{
return -1;
}
unsigned short us = it->second;
itpp::bvec es(itpp::dec2bin(16, us));
if (j > 0)
es.shift_right(es.mid(16-j, j)); // e
vb = itpp::to_bvec(vec);
vb -= es;
out = itpp::to_ivec(vb);
vc = cyclic_16_8_5_syndromes::cyclic_16_8_5 * out;
vec_mod(vc);
vb = itpp::to_bvec(vc);
if (itpp::bin2dec(vb) != 0x00)
{
return -1;
}
return 1;
}
static int decode_cyclic_16_8_5(itpp::ivec& vec)
{
return decode_cyclic_16_8_5(vec, vec);
}
////////////////////////////////////////////////////////////////////////////////////
voice_data_unit::voice_data_unit(const_bit_queue& frame_body) :
abstract_data_unit(frame_body)
abstract_data_unit(frame_body),
d_lsdw(0),
d_lsdw_valid(false)
{
memset(d_lsd_byte_valid, 0x00, sizeof(d_lsd_byte_valid));
}
voice_data_unit::~voice_data_unit()
{
}
void
voice_data_unit::do_correct_errors(bit_vector& frame_body)
{
if (logging_enabled()) fprintf(stderr, "\n");
d_lsd_byte_valid[0] = d_lsd_byte_valid[1] = false;
d_lsdw_valid = false;
itpp::ivec lsd1(16), lsd2(16);
for (int i = 0; i < 32; ++i)
{
int x = 1504 + i;
x = x + ((x / 70) * 2); // Adjust bit index for status
if (i < 16)
lsd1[i] = frame_body[x];
else
lsd2[i-16] = frame_body[x];
}
int iDecode1 = decode_cyclic_16_8_5(lsd1);
if (iDecode1 >= 0)
{
d_lsd_byte_valid[0] = true;
}
else if (iDecode1 == -1)
{
// Error
}
int iDecode2 = decode_cyclic_16_8_5(lsd2);
if (iDecode2 >= 0)
{
d_lsd_byte_valid[1] = true;
}
else
{
// Error
}
d_lsdw = 0;
for (int i = 0; i < 8; ++i)
d_lsdw = d_lsdw | (lsd1[i] << (7 - i)); // Little-endian byte swap
for (int i = 0; i < 8; ++i)
d_lsdw = d_lsdw | (lsd2[i] << (15 - i)); // Little-endian byte swap
if (d_lsd_byte_valid[0] && d_lsd_byte_valid[1])
d_lsdw_valid = true;
}
uint16_t
voice_data_unit::lsdw() const
{
return d_lsdw;
}
bool
voice_data_unit::lsdw_valid() const
{
return d_lsdw_valid;
}
static void extract(unsigned int u, size_t n, std::vector<bool>& out)
{
for (size_t i = 0; i < n; ++i)
out.push_back(((u & (1 << (n-1-i))) != 0));
}
void
voice_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe)
voice_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod)
{
voice_codeword cw(voice_codeword_sz);
for(size_t i = 0; i < nof_voice_codewords; ++i) {
imbe_deinterleave(frame_body, cw, i);
unsigned int u0 = 0;
unsigned int u1,u2,u3,u4,u5,u6,u7;
unsigned int E0 = 0;
unsigned int ET = 0;
// PN/Hamming/Golay - etc.
size_t errs = imbe_header_decode(cw, u0, u1, u2, u3, u4, u5, u6, u7, E0, ET, false); // E0 & ET are not used, and are always returned as 0
crypto_algorithm::sptr algorithm;
if (crypto_mod)
algorithm = crypto_mod->current_algorithm();
if (algorithm)
{
if (i == 8)
{
d_lsdw ^= algorithm->generate(16); // LSDW
}
u0 ^= (int)algorithm->generate(12);
u1 ^= (int)algorithm->generate(12);
u2 ^= (int)algorithm->generate(12);
u3 ^= (int)algorithm->generate(12);
u4 ^= (int)algorithm->generate(11);
u5 ^= (int)algorithm->generate(11);
u6 ^= (int)algorithm->generate(11);
u7 ^= (int)algorithm->generate(7);
imbe_header_encode(cw, u0, u1, u2, u3, u4, u5, u6, (u7 << 1));
}
std::vector<bool> cw_raw;
extract(u0, 12, cw_raw);
extract(u1, 12, cw_raw);
extract(u2, 12, cw_raw);
extract(u3, 12, cw_raw);
extract(u4, 11, cw_raw);
extract(u5, 11, cw_raw);
extract(u6, 11, cw_raw);
extract(u7, 7, cw_raw);
const int cw_octets = 11;
std::vector<uint8_t> cw_vector(cw_octets);
extract(cw_raw, 0, (cw_octets * 8), &cw_vector[0]);
if (logging_enabled())
{
std::stringstream ss;
for (size_t n = 0; n < cw_vector.size(); ++n)
{
ss << (boost::format("%02x") % (int)cw_vector[n]);
if (n < (cw_vector.size() - 1))
ss << " ";
}
if (errs > 0)
ss << (boost::format(" (%llu errors)") % errs);
std:cerr << (boost::format("%s:\t%s") % duid_str() % ss.str()) << std::endl;
}
imbe.decode(cw);
}
if (logging_enabled()) fprintf(stderr, "%s: LSDW: 0x%04x, %s\n", duid_str().c_str(), d_lsdw, (d_lsdw_valid ? "valid" : "invalid"));
}
uint16_t

View File

@ -38,6 +38,17 @@ public:
*/
virtual ~voice_data_unit();
const static int LSD_BYTE_COUNT=2;
private:
union {
uint8_t d_lsd_byte[LSD_BYTE_COUNT];
uint16_t d_lsdw;
};
bool d_lsdw_valid;
bool d_lsd_byte_valid[LSD_BYTE_COUNT];
protected:
/**
@ -63,7 +74,7 @@ protected:
* \param frame_body The const_bit_vector to decode.
* \param imbe The imbe_decoder to use to generate the audio.
*/
virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe);
virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod);
/**
* Returns the expected size (in bits) of this data_unit. For
@ -74,6 +85,10 @@ protected:
*/
virtual uint16_t frame_size_max() const;
virtual uint16_t lsdw() const;
virtual bool lsdw_valid() const;
};
#endif /* INCLUDED_VOICE_DATA_UNIT_H */

View File

@ -28,9 +28,10 @@
using namespace std;
voice_du_handler::voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder) :
voice_du_handler::voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder, crypto_module::sptr crypto_mod) :
data_unit_handler(next),
d_decoder(decoder)
d_decoder(decoder),
d_crypto_mod(crypto_mod)
{
}
@ -42,6 +43,6 @@ voice_du_handler::~voice_du_handler()
void
voice_du_handler::handle(data_unit_sptr du)
{
du->decode_audio(*d_decoder);
du->decode_audio(*d_decoder, d_crypto_mod);
data_unit_handler::handle(du);
}

View File

@ -26,6 +26,7 @@
#include "data_unit_handler.h"
#include "imbe_decoder.h"
#include "crypto.h"
#include <boost/noncopyable.hpp>
@ -43,7 +44,7 @@ public:
* \param next The next data_unit_handler in the chain.
* \param decoder An imbe_decoder_sptr to the IMBE decoder to use.
*/
voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder);
voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder, crypto_module::sptr crypto_mod = crypto_module::sptr()); // TODO: Add capability to decoder_ff (remove default argument)
/**
* voice_du_handler virtual destructor.
@ -64,6 +65,8 @@ private:
*/
imbe_decoder_sptr d_decoder;
crypto_module::sptr d_crypto_mod;
};
#endif /* INCLUDED_VOICE_DU_HANDLER_H */

View File

@ -31,7 +31,7 @@ endif()
GR_PYTHON_INSTALL(
FILES
__init__.py
DESTINATION ${GR_PYTHON_DIR}/op25
DESTINATION ${OP25_PYTHON_DIR}/op25
)
########################################################################

View File

@ -31,9 +31,9 @@ try:
from dl import RTLD_GLOBAL as _RTLD_GLOBAL
except ImportError:
try:
from DLFCN import RTLD_GLOBAL as _RTLD_GLOBAL
from DLFCN import RTLD_GLOBAL as _RTLD_GLOBAL
except ImportError:
pass
pass
if _RTLD_GLOBAL != 0:
_dlopenflags = sys.getdlopenflags()
@ -42,7 +42,7 @@ if _RTLD_GLOBAL != 0:
# import swig generated symbols into the op25 namespace
from op25_swig import *
from .op25_swig import *
# import any pure python here
#

View File

@ -0,0 +1,134 @@
/*
* Copyright 2022 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
/***********************************************************************************/
/* This file is automatically generated using bindtool and can be manually edited */
/* The following lines can be configured to regenerate this file during cmake */
/* If manual edits are made, the following tags should be modified accordingly. */
/* BINDTOOL_GEN_AUTOMATIC(0) */
/* BINDTOOL_USE_PYGCCXML(0) */
/* BINDTOOL_HEADER_FILE(message.h) */
/* BINDTOOL_HEADER_FILE_HASH(e324acfee988515a91a4759680dbabbf) */
/***********************************************************************************/
#include <pybind11/complex.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
#include <gnuradio/op25/message.h>
// pydoc.h is automatically generated in the build directory
#include <message_pydoc.h>
void bind_message(py::module& m)
{
using message = ::gr::op25::message;
py::class_<message,
std::shared_ptr<message>>(m, "message", D(message))
.def(py::init(&message::make),
py::arg("type") = 0,
py::arg("arg1") = 0,
py::arg("arg2") = 0,
py::arg("length") = 0,
D(message,make)
)
.def_static("make_from_string",&message::make_from_string,
py::arg("s"),
py::arg("type") = 0,
py::arg("arg1") = 0,
py::arg("arg2") = 0,
D(message,make_from_string)
)
.def("type",&message::type,
D(message,type)
)
.def("arg1",&message::arg1,
D(message,arg1)
)
.def("arg2",&message::arg2,
D(message,arg2)
)
.def("set_type",&message::set_type,
py::arg("type"),
D(message,set_type)
)
.def("set_arg1",&message::set_arg1,
py::arg("arg1"),
D(message,set_arg1)
)
.def("set_arg2",&message::set_arg2,
py::arg("arg2"),
D(message,set_arg2)
)
.def("msg",&message::msg,
D(message,msg)
)
//.def("to_string",&message::to_string,
// D(message,to_string)
//)
.def("to_string",
[](std::shared_ptr<message> msg) {
std::string s = msg->to_string();
return py::bytes(s); // Return the data without transcoding
})
;
m.def("message_ncurrently_allocated",&::gr::op25::message_ncurrently_allocated,
D(message_ncurrently_allocated)
);
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2022 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
/***********************************************************************************/
/* This file is automatically generated using bindtool and can be manually edited */
/* The following lines can be configured to regenerate this file during cmake */
/* If manual edits are made, the following tags should be modified accordingly. */
/* BINDTOOL_GEN_AUTOMATIC(0) */
/* BINDTOOL_USE_PYGCCXML(0) */
/* BINDTOOL_HEADER_FILE(msg_handler.h) */
/* BINDTOOL_HEADER_FILE_HASH(668ae41ad9b9d463886fcf60a87e9ede) */
/***********************************************************************************/
#include <pybind11/complex.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
#include <gnuradio/op25/msg_handler.h>
// pydoc.h is automatically generated in the build directory
#include <msg_handler_pydoc.h>
void bind_msg_handler(py::module& m)
{
using msg_handler = ::gr::op25::msg_handler;
py::class_<msg_handler,
std::shared_ptr<msg_handler>>(m, "msg_handler", D(msg_handler))
// .def(py::init<>(),D(msg_handler,msg_handler,0))
// .def(py::init<gr::op25::msg_handler const &>(), py::arg("arg0"),
// D(msg_handler,msg_handler,1)
// )
.def("handle",&msg_handler::handle,
py::arg("msg"),
D(msg_handler,handle)
)
;
}

View File

@ -0,0 +1,116 @@
/*
* Copyright 2022 Free Software Foundation, Inc.
*
* This file is part of GNU Radio
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
/***********************************************************************************/
/* This file is automatically generated using bindtool and can be manually edited */
/* The following lines can be configured to regenerate this file during cmake */
/* If manual edits are made, the following tags should be modified accordingly. */
/* BINDTOOL_GEN_AUTOMATIC(0) */
/* BINDTOOL_USE_PYGCCXML(0) */
/* BINDTOOL_HEADER_FILE(msg_queue.h) */
/* BINDTOOL_HEADER_FILE_HASH(3f70adbde5e636fca8dc78e0505b06fd) */
/***********************************************************************************/
#include <pybind11/complex.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
#include <gnuradio/op25/msg_queue.h>
// pydoc.h is automatically generated in the build directory
#include <msg_queue_pydoc.h>
void bind_msg_queue(py::module& m)
{
using msg_queue = ::gr::op25::msg_queue;
py::class_<msg_queue,
std::shared_ptr<msg_queue>>(m, "msg_queue", D(msg_queue))
.def(py::init(&msg_queue::make),
py::arg("limit") = 0,
D(msg_queue,make)
)
.def("handle",&msg_queue::handle,
py::arg("msg"),
D(msg_queue,handle)
)
.def("insert_tail",&msg_queue::insert_tail,
py::arg("msg"),
D(msg_queue,insert_tail)
)
.def("delete_head",&msg_queue::delete_head, py::call_guard<py::gil_scoped_release>(),
D(msg_queue,delete_head)
)
.def("delete_head_nowait",&msg_queue::delete_head_nowait,
D(msg_queue,delete_head_nowait)
)
.def("flush",&msg_queue::flush,
D(msg_queue,flush)
)
.def("empty_p",&msg_queue::empty_p,
D(msg_queue,empty_p)
)
.def("full_p",&msg_queue::full_p,
D(msg_queue,full_p)
)
.def("count",&msg_queue::count,
D(msg_queue,count)
)
.def("limit",&msg_queue::limit,
D(msg_queue,limit)
)
;
}

View File

@ -21,7 +21,7 @@
# Include swig generation macros
########################################################################
find_package(SWIG)
find_package(PythonLibs 2)
find_package(PythonLibs 3)
if(NOT SWIG_FOUND OR NOT PYTHONLIBS_FOUND)
return()
endif()
@ -31,9 +31,7 @@ include(GrPython)
########################################################################
# Setup swig generation
########################################################################
foreach(incdir ${GNURADIO_RUNTIME_INCLUDE_DIRS})
list(APPEND GR_SWIG_INCLUDE_DIRS ${incdir}/gnuradio/swig)
endforeach(incdir)
set(GR_SWIG_INCLUDE_DIRS $<TARGET_PROPERTY:gnuradio::runtime_swig,INTERFACE_INCLUDE_DIRECTORIES>)
set(GR_SWIG_LIBRARIES gnuradio-op25)
set(GR_SWIG_DOC_FILE ${CMAKE_CURRENT_BINARY_DIR}/op25_swig_doc.i)
@ -44,7 +42,7 @@ GR_SWIG_MAKE(op25_swig op25_swig.i)
########################################################################
# Install the build swig module
########################################################################
GR_SWIG_INSTALL(TARGETS op25_swig DESTINATION ${GR_PYTHON_DIR}/op25)
GR_SWIG_INSTALL(TARGETS op25_swig DESTINATION ${OP25_PYTHON_DIR}/op25)
########################################################################
# Install swig .i files for development

View File

@ -1,5 +1,7 @@
/* -*- c++ -*- */
%include "pycontainer.swg"
#define OP25_API
%include "gnuradio.i" // the common stuff
@ -15,6 +17,11 @@
#include "op25/pcap_source_b.h"
%}
%template(key_type) std::vector<unsigned char>;
// This causes SWIG to segfault
//%template(key_map_type) std::map<uint16_t,key_type >;
%template(key_map_type) std::map<uint16_t,std::vector<unsigned char> >;
%include "op25/fsk4_demod_ff.h"
GR_SWIG_BLOCK_MAGIC2(op25, fsk4_demod_ff);
%include "op25/fsk4_slicer_fb.h"

View File

@ -63,6 +63,31 @@ if(NOT Boost_FOUND)
message(FATAL_ERROR "Boost required to compile op25_repeater")
endif()
########################################################################
# Find gnuradio build dependencies
########################################################################
find_package(CppUnit)
set(ENABLE_PYTHON "TRUE" CACHE BOOL "enable python")
cmake_policy(SET CMP0012 NEW)
# To run a more advanced search for GNU Radio and it's components and
# versions, use the following. Add any components required to the list
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
# minimum API compatible version required.
#
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
# find_package(Gnuradio "version")
set(MIN_GR_VERSION "3.8.0")
find_package(Gnuradio REQUIRED)
if("${Gnuradio_VERSION}" VERSION_LESS MIN_GR_VERSION)
MESSAGE(FATAL_ERROR "GnuRadio version required: >=\"" ${MIN_GR_VERSION} "\" found: \"" ${Gnuradio_VERSION} "\"")
endif()
if(NOT CPPUNIT_FOUND)
message(FATAL_ERROR "CppUnit required to compile op25_repeater")
endif()
########################################################################
# Install directories
########################################################################
@ -80,31 +105,6 @@ set(GR_LIBEXEC_DIR libexec)
set(GR_PKG_LIBEXEC_DIR ${GR_LIBEXEC_DIR}/${CMAKE_PROJECT_NAME})
set(GRC_BLOCKS_DIR ${GR_PKG_DATA_DIR}/grc/blocks)
########################################################################
# Find gnuradio build dependencies
########################################################################
find_package(GnuradioRuntime)
find_package(CppUnit)
# To run a more advanced search for GNU Radio and it's components and
# versions, use the following. Add any components required to the list
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
# minimum API compatible version required.
#
# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...)
# find_package(Gnuradio "version")
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
find_package(Gnuradio)
endif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
if(NOT GNURADIO_RUNTIME_FOUND)
message(FATAL_ERROR "GnuRadio Runtime required to compile op25_repeater")
endif()
if(NOT CPPUNIT_FOUND)
message(FATAL_ERROR "CppUnit required to compile op25_repeater")
endif()
########################################################################
# Setup the include and linker paths
########################################################################

View File

@ -0,0 +1,2 @@
"Sysname" "Control Channel List" "Offset" "NAC" "Modulation" "TGID Tags File" "Whitelist" "Blacklist" "Center Frequency"
"Fake" "924.975" "0" "0x293" "FSK4" "" "" "" "924.95"
1 Sysname Control Channel List Offset NAC Modulation TGID Tags File Whitelist Blacklist Center Frequency
2 Fake 924.975 0 0x293 FSK4 924.95

View File

@ -0,0 +1,243 @@
April, 2017
===========
This file contains notes on the new version OP25 receiver (rx.py) which
replaces the prior version scope.py. The primary differences are:
* The dependency on WX is completely removed. By default OP25 runs in
a console window in text-only mode.
* An optional real-time plot can be selected when launching rx.py:
contellation, datascope (eye pattern), or symbol trace.
* cqpsk versus fsk4 mode is now selected as a command line parameter.
* most rx.py command-line parameters are compatible with scope.py.
* reduced CPU consumption, as the frame assembler block now runs as a
sink-only GR block.
* dependency on 14.04 is completely removed. Should now run in later
ubuntu and fedora versions with only minor changes (not yet tested).
ADDITIONAL REQUIRED PACKAGES
============================
sudo apt-get install gnuplot-x11
EXAMPLE COMMAND LINE
====================
./rx.py --args 'rtl' --gains 'lna:49' -f 456.7e6 -T tsys.tsv -q -1 -S 1000000 -P symbol -o 50000 -w 2> stderr.2
Running stderr to a file (e.g., "2> stderr.2") is recommended to avoid
screen misprinting.
NOTE: For phase1 voice the "-V" option is not used. Instead the
"-w" option is used (see AUDIO SERVER section, below). For P25 phase 2/TDMA,
the "-2" option is required in addition to the "-w" option.
TERMINAL OPERATION
==================
After starting rx.py if plotting is in use a separate gnuplot window
should open. You must click on the terminal window to restore it to
focus, otherwise all keystrokes are consumed by gnuplot. Once in the
terminal window there are several keyboard commands:
h - hold
H - hold/goto the specified tgid
l - lockout
s - skip
q - quit program
There are also two experimental commands (should not be used in -T mode)
f - manually change frequencies
t - if currently tuned to a CC, autostart scanning talkgroups
The "t" command allows trunk tracking without setting up a trunking TSV
file. However running with the -T <filename> command line option is
preferred as that allows use of white/black lists and talkgroup tags files.
If the terminal window freezes there may have been a crash. Press Ctrl-Z
to suspend the program and examine stderr.2 for error messages. If there
is a traceback please report the full traceback (and the command line) to
the mail list.
REMOTE TERMINAL
===============
Adding (for example) "-l 56111" to the rx.py command starts rx.py but does
not attach a curses terminal. Instead the program runs as normal in the
foreground (hit CTRL-C to end rx.py as desired). To connect to a running
instance of rx.py, (in this example)
./terminal.py 127.0.0.1 56111
NOTE: rx.py and terminal.py need not run on the same machine. The machine
where terminal.py is running need not have an SDR device directly attached;
but GNU Radio (and OP25) must be available.
WARNING: there is no security or encryption on the UDP port.
EXTERNAL UDP AUDIO SERVER
=========================
Starting rx.py with the "-w -W host" options directs udp audio data to
be sent over the network to the specified remote host. It can then be
received and played back with either of the following methods:
1. Execute ./audio.sh on a remote machine equipped with python2.7,
libasound.so.2 and the sockaudio.py file.
-or-
2. Execute the command:
nc -kluvw 1 127.0.0.1 23456 | aplay -c1 -f S16_LE -r 8000
NOTE: audio underruns are to be expected when using nc | aplay as the
pcm stream is interrupted every time a radio transmission ends. The
sockaudio player is designed to handle this more gracefully, and generally
only underruns due to high cpu utilization or reception/decoding errors.
INTERNAL AUDIO SERVER
=====================
Starting rx.py with the "-U" command line option enables an internal udp
audio server which will play received audio through the default ALSA
device. Optionally you may specify which ALSA device to use by setting
the "-O audio_out" option along with "-U".
As of this writing (Aug 2017) it is still necessary to specify the "-w"
(wireshark) option if using either the internal or external audio server.
PLOT MODES
==========
Three types of plotting are currently implemented, via the -P parameter:
* constellation
* datascope
* symbol
The symbol mode is allowed both in fsk4 and cqpsk modes. The datascope
mode works only with fsk4 demod mode (-D fsk4). The constellation mode
only works when the cqpsk demod mode is selected (or defaulted).
A couple of notes specific to plot mode:
1. At program startup time the gnuplot window is given the focus after
it opens. Before you can enter terminal commands you need to click on
the terminal window once to make it the active window.
2. In some cases the gnuplot window is displayed on top of the terminal
window used by OP25. If so it may be necessary to move one or the other
of the two windows.
COMMAND LINE OPTIONS
====================
Usage: rx.py [options]
Options:
-h, --help show this help message and exit
--args=ARGS device args
--antenna=ANTENNA select antenna
-a, --audio use direct audio input
-A, --audio-if soundcard IF mode (use --calibration to set IF freq)
-I AUDIO_INPUT, --audio-input=AUDIO_INPUT
pcm input device name. E.g., hw:0,0 or /dev/dsp
-i INPUT, --input=INPUT
input file name
-b Hz, --excess-bw=Hz
for RRC filter
-c Hz, --calibration=Hz
USRP offset or audio IF frequency
-C Hz, --costas-alpha=Hz
value of alpha for Costas loop
-D DEMOD_TYPE, --demod-type=DEMOD_TYPE
cqpsk | fsk4
-P PLOT_MODE, --plot-mode=PLOT_MODE
constellation | symbol | datascope
-f Hz, --frequency=Hz
USRP center frequency
-F IFILE, --ifile=IFILE
read input from complex capture file
-H HAMLIB_MODEL, --hamlib-model=HAMLIB_MODEL
specify model for hamlib
-s SEEK, --seek=SEEK ifile seek in K
-l TERMINAL_TYPE, --terminal-type=TERMINAL_TYPE
'curses' or udp port or 'http:host:port'
-L LOGFILE_WORKERS, --logfile-workers=LOGFILE_WORKERS
number of demodulators to instantiate
-S SAMPLE_RATE, --sample-rate=SAMPLE_RATE
source samp rate
-t, --tone-detect use experimental tone detect algorithm
-T TRUNK_CONF_FILE, --trunk-conf-file=TRUNK_CONF_FILE
trunking config file name
-v VERBOSITY, --verbosity=VERBOSITY
message debug level
-V, --vocoder voice codec
-o Hz, --offset=Hz tuning offset frequency [to circumvent DC offset]
-p, --pause block on startup
-w, --wireshark output data to Wireshark
-W WIRESHARK_HOST, --wireshark-host=WIRESHARK_HOST
Wireshark host
-r RAW_SYMBOLS, --raw-symbols=RAW_SYMBOLS
dump decoded symbols to file
-R RX_SUBDEV_SPEC, --rx-subdev-spec=RX_SUBDEV_SPEC
select USRP Rx side A or B (default=A)
-g GAIN, --gain=GAIN set USRP gain in dB (default is midpoint) or set audio
gain
-G GAIN_MU, --gain-mu=GAIN_MU
gardner gain
-N GAINS, --gains=GAINS
gain settings
-O AUDIO_OUTPUT, --audio-output=AUDIO_OUTPUT
audio output device name
-q FREQ_CORR, --freq-corr=FREQ_CORR
frequency correction
-2, --phase2-tdma enable phase2 tdma decode
-Z DECIM_AMT, --decim-amt=DECIM_AMT
spectrum decimation
HTTP CONSOLE
============
New as of Jan. 2018, the OP25 dashboard is accessible to any Web browser over
HTTP. Include the option "-l http:<host>:<port>" when starting the rx.py app,
where <host> is either "127.0.0.1" to limit access from only this host, or
"0.0.0.0" if HTTP access from anywhere is to be allowed*. After rx.py has
started it begins listening on the specified port for incoming connections.
Once connected the status page should automatically update to show trunking
system status, frequency list, adjacent sites, and other data.
Example: you have started rx.py with the option "-l http:127.0.0.1:8080".
To connect, set your web browser URL to "http://127.0.0.1:8080".
If one or more plot modes has been selected using the "-P" option you may
view them by clicking the "PLOT" button. The plots are updated approx.
every five seconds. Click "STATUS" to return to the main status page.
*WARNING*: there is no security or encryption. Be careful when using "0.0.0.0"
as the listening address since anyone with access to the network can connect.
NOTE: the python-pyramid package is required when using this option. It can
be installed by running
sudo apt-get install python-pyramid
MULTI-RECEIVER
==============
The multi_rx.py app allows an arbitrary number of SDR devices and channels
to be defined. Each channel may have one or more plot windows attached.
Configuration is achieved via a json file (see cfg.json for an example).
In this version, channels are automatically assigned to the first device
found whose frequency span includes the selected frequency.
As of this writing (winter, 2017), neither trunking nor P25 P2/TDMA are
supported in multi_rx.py. The rx.py app should be used for P25 trunking,
for either P1/FDMA or P2/TDMA.
Below is a summary of the major config file keys:
demod_type: 'cqpsk' for qpsk p25 only; 'fsk4' for ysf/dstar/dmr/fsk4 p25
filter_type: 'rc' for p25; 'rrc' for dmr and ysf; 'gmsk' for d-star
plot: 'fft', 'constellation', 'datascope', 'symbol'
[if more than one plot desired, provide a comma-separated list]
destination: 'udp://host:port' or 'file://<filename>'
name: arbitrary string used to identify channels and devices
bug: 'fft' and 'constellation' currently mutually exclusive
bug: 'gmsk' needs work
Note: DMR audio for the second time slot is sent on the specified port number
plus two. In the example 'udp://127.0.0.1:56122', audio for the first slot
would use 56122; and 56124 for the second.
The command line options for multi_rx:
Usage: multi_rx.py [options]
Options:
-h, --help show this help message and exit
-c CONFIG_FILE, --config-file=CONFIG_FILE
specify config file name
-v VERBOSITY, --verbosity=VERBOSITY
message debug level
-p, --pause block on startup

View File

@ -0,0 +1,291 @@
New features in this release (June, 2021)
=========================================
1. With thanks to OP25 user Triptolemus, the web client is enhanced to
include comprehensive logs of recent control channel signalling and
call activity. Many other features are also added:
* unit ID (subscriber ID) tagging - similar to the existing TGID
tags setup.
* tag color coding (for both TGID and SUID tags).
* tag ranges and wildcarding - for both the TGID and SUID tag maps,
a single definition line may be used to create tags for a range of
IDs.
* real time system frequency status table
* smart colors
* user settings (colors, preferences) may be edited and saved via a
convenient set of web forms and applications
2. The multi_rx app adds extensions to include trunked P25 call following
concurrent with full-time tracking of one or more P25 control channels.
If necessary, additional SDR devices may be configured to allow full
coverage of all control channels without loss of CC data even during voice
call reception. Several new command line options to multi_rx have been
added - -T (trunking TSV file) -l (terminal type) as well as -X and -U,
all having the same meaning as in rx.py.
3. Control channel logging to SQL database is added. For details see the
section on the Flask Datatables App, below.
4. Experimental TDMA Control Channel support
Installation
============
First locate and change to your current OP25 install build/ directory and
run the command
sude make uninstall
Since this version includes library C++ code updates it requires a full
source rebuild via the standard install script (install.sh).
The installation will include one or more SDR receivers, depending
on the the amount of spectrum utilized by the target trunking system, how
many control channels are to be monitored concurrently, and whether voice
call following is desired.
* When SQL logging is used, it is most desirable to keep the control channel
tuned in 100% of the time. With a single SDR this is not possible when the
range of control channel and voice channel frequencies exceed the tuning band
of the SDR.
* When voice call following is to be used, a separate voice channel must be
defined for each device over which voice reception is desired. It is
redundant to have more than one voice channel assigned to a given device.
* A separate SDR can be dedicated to voice call following if needed. If there
is already a frequency-locked ("tunable"=false) device whose tuning band
includes all desired voice frequencies, a separate voice SDR is not needed.
* This version of OP25 follows the same voice call system as in rx.py.
That is, a single call at a time is monitored and a 3-second (nominal)
time delay is applied at the end of each call to catch possible replies.
* A single device may be shared by multiple channels. When more than one channel
is assigned to a device, the device should be tuned to a fixed frequency and
"tunable" should be set to "false".
Simplified example: Of all frequencies (control and voice) in the system,
the lowest frequency is 464.05 and the highest is 464.6. An RTL-SDR having
a maximum sample rate of 2.56 MHz is to be used. Since the band required is
0.55 MHz, a single SDR configuration can be used. The sample rate for
this example, 2.56 MHz, could be reduced to 1.0 MHz to conserve CPU.
NOTE: Proper logging of CC activity requires two things:
1) Device and/or channel resources must be allocated so that there
is 100% time coverage of the control channel. Voice channel
operation on the same SDR can only occur when the entire system
fits within the SDR tuning band.
2) Control channel reception and demodulation must be 100% error-free.
Occasional errors are potentially corrected by the FEC but a better
course is to increase the receive SNR and/or decrease the system BER.
Notes on JSON Configuration/Parameters
======================================
Example json config files are included in the apps/ directory. You
should choose one of these files (as described above) and make edits
to a working copy of the file. The name of the resulting JSON config
file must be passed to multi_rx.py via the "-c" parameter.
cfg-trunk.json - When all system frequencies (CC and VC) will fit
within the SDR tuning band (without retuning the SDR),
or voice decode is not needed.
cfg-trunk2.json - When two SDRs are needed to cover both CC and all VCs.
cfg-trunkx.json - Large system example with voice following and several CCs.
There are several key values to note:
"tunable" In the single-SDR configuration where all system frequencies
(primary/secondary CCs and VCs) are within the SDR band,
you should set this to "false". In this case the SDR is
fixed-tuned and remains on a single frequency, the center
frequency. You must set the center frequency to a value
halfway between the lowest and highest frequencies in the
system, via the device "frequency" setting.
"frequency" See above. When "tunable" is set to "true" this value must
be filled in. Otherwise the value is used to set the device
frequency at startup time (must be a valid frequency for the
device). The device will most likely be retuned one or more
times during execution.
"decode" Assists multi_rx in assigning channels to the proper device(s).
If the value of "decode" starts with the string "p25_decoder",
multi_rx uses the p25 decoder instead of its standard decoder.
Note that "tunable" is a device-specific parameter, and that "decode" is a
channel-specific parameter. Also, while both the device and channel define
the "frequency" parameter, the description above is for device entries. A
channel entry may also define a frequency, but the channel "frequency" parameter
is ignored (in this version).
When the p25_decoder is used, there is a parameter string consisting of a
colon-separated list of parameters with each parameter in the form "key=value",
with the parameter string defined as the value of the "decode" parameter.
Here are two examples:
"decode": "p25_decoder:role=cc:dev=rtl11:nac=0x4e1", [control]
"decode": "p25_decoder:role=vc:dev=rtl12_vc", [voice]
The valid parameter keywords are:
"p25_decoder" Required for trunked P25. This keyword introduces the
parameter list. There is no value.
"role" Must be set to "vc" or "cc".
"dev" Must be set to the name of the device. Each channel is
assigned to exactly one device.
"nac" Comma-separated list of NACs for the channel. Only trunked
systems having a NAC in the list can be assigned to this
channel.
"sysid" Comma-separated list of SYSIDs for the channel. Only trunked
systems having a SYSID in the list can be assigned to this
channel.
The "nac" and "sysid" options are only checked for control channels ("role=cc").
Values starting with "0x" are hexadecimal; otherwise decimal values are assumed .
A blank/default value for "sysid" and/or "nac" indicates that parameter is not
checked.
The following startup messages in the stderr log are typical in a 2-SDR config:
assigning channel "p25 control channel" (channel id 1) to device "rtl11_cc"
assigning channel "p25 voice channel" (channel id 2) to device "rtl12_vc"
Note that the channel ID displayed in the "tuning error +/-1200" messages can be
linked to the specific device(s) encountering the error using this ID.
Experimental TDMA Control Channel Support
=========================================
The following specifics detail the JSON configuration file channel parameters
needed to define a TDMA control channel:
"demod_type": "cqpsk",
"if_rate": 24000,
"symbol_rate": 6000,
"decode": "p25_decoder:role=cc:dev=<device-name>:nac=0x4e1",
The NAC should be changed to match that of the system being received, and
<device-name> should refer to the assigned device.
Colors and Tags for Talkgroup and Radio IDs
===========================================
Tags and colors are defined in two TSV files, one for TGIDs and one for SUIDs.
The TSV file format, compatible with earlier versions of OP25 has the TAB
separated columns defined as:
column one: decimal TG or SU ID. May contain wildcards (see below)
column two: tag text (string)
column three(optional): encoded priority/color value, decimal (see below)
The color code is directly mapped by client JS into style sheet (CSS) colors.
If only two columns are present the third column is defaulted to zero.
The file names of the two files are specified (comma-separated) in the
trunking TSV "TGID Tags File" column (the trunking TSV in turn is the
file referred to by the "-T" command option of rx.py or multi_rx.py).
The talkgroup tags file name is specified first, followed by a comma,
then the SUID tags file. The SUID tags file can't be specified alone.
Wildcard IDs (column one) may be (for example)
* 123-678 [all IDs in range, inclusive, are set to same tag/color]
* 444.... [all IDs from 4440000 to 4449999]
* 456* [all IDs starting with 456]
* 54321 [defines that one ID]
Column three contains a color value from 0-99 (decimal).
In the TGID file (only), the column value also contains a talkgroup
priority, encoded as follows:
- the low-order two decimal digits (tens and units digits) are the
color code
- the remaining upper-order decimal digits (hundreds digit and above) are
the priority value for talkgroup pre-emption purposes.
Setup SQL Log Database (Optional)
=================================
This addition provides a permanent server-side log of control channel
activity via logging to an SQL database. See the next section for details
on installing and using the log viewer.
1. Make sure that sqlite3 is installed in python
WARNING: OP25 MUST NOT BE RUNNING DURING THIS STEP
2. Initialize DB (any existing DB data will be destroyed)
op25/.../apps$ python sql_dbi.py reset_db
WARNING: OP25 MUST NOT BE RUNNING DURING THIS STEP
3. Import talkgroups tags file
op25/.../apps$ python sql_dbi.py import_tgid tags.tsv <sysid>
also, import the radio ID tags file (optional)
op25/.../apps$ python sql_dbi.py import_unit radio-tags.tsv <sysid>
import the System ID tags file (see below)
op25/.../apps$ python sql_dbi.py import_sysid sysid-tags.tsv 0
The sysid tags must be a TSV file containing two columns
column 1 is the P25 trunked sysid (int, decimal)
colunn 2 is the System Name (text)
(Note: there is no header row line in this TSV file).
NOTE: in the various import commands above, the sysid (decimal) must follow
as the next argument after the TSV file name. For the sysid tags file, the
sysid should be set to zero.
4. Run op25 as usual. Logfile data should be inserted into DB in real time
and you should be able to view activity via the OP25 http console (once
the flask/datatables app has been set up; see next section).
Setup Flask Datatables App
==========================
0. The DB must first be established (see previous section)
1. Install the necessary libs. If you are running the install in Ubuntu
16.04 there are two lines in the script that must be un-commented prior
to running; then, in any case do:
op25/.../apps$ sh install-sql.sh
Note: you may need to 'sudo apt install git' prior to running this script.
2. Update your .bashrc file as instructed, then re-login to pick up the
update to PATH. Verify that the updated PATH is correct. You can run
the command "echo $PATH" to display its current value. Here is an example
response: /home/op25/.local/bin:/usr/local/sbin:/usr/local/bin.....
You should confirm that the file "flask" exists and is executable (see
warning below).
$ ls -l ~/.local/bin/flask
-rwxr-xr-x 1 op25 op25 212 Apr 29 21:43 /home/op25/.local/bin/flask
3. First change to the "..../apps/oplog" directory, then run the following
commands to start the flask http process (listens on port 5000)
op25/.../apps/oplog$ export FLASK_APP=op25
op25/.../apps/oplog$ FLASK_DEBUG=1 flask run
WARNING: if you receive the following messages when attempting the "flask run"
command
-------------------------------------------------------------
Command 'flask' not found, but can be installed with:
sudo apt install python3-flask
-------------------------------------------------------------
most likely this indicates the PATH is not properly set up (see step 2).
In this case we do NOT recommend that you attempt to install the apt version
of flask. The install-sql.sh script (step 1, above) should have installed a
version of flask in a directory such as:
$ ls -l ~/.local/bin/flask
-rwxr-xr-x 1 op25 op25 212 Apr 29 21:43 /home/op25/.local/bin/flask
If install of the apt version of flask is attempted, it may result in an
obsolete and/or incompatible flask version being installed.
NOTE: The following command example can be used when starting the oplog
process as a system service:
/home/<user>/op25/op25/gr-op25_repeater/apps/oplog/oplog.sh
* Change <user> to match the username
* Make appropriate corrections if the git repo is cloned into a different
directory than in the command shown
* Verify the resulting path and filename is correct.
============ oplog.sh ============
#! /bin/sh
export FLASK_APP=op25
FLASK_DEBUG=1 flask run --host=0.0.0.0
==================================
In lieu of setting the flask PATH (as per step 2, above) you could also
specify it explicitly. In that case, replace the last line of oplog.sh as
follows:
FLASK_DEBUG=1 /home/<user>/.local/bin/flask run --host=0.0.0.0
* Change <user> to match the username
* Confirm the "flask" file is present in ..../bin/ and is executable
* Set "host" to "127.0.0.1" to restrict HTTP connections to the local
machine.
* WARNING The web server is not security-hardened. It is not
designed for exposure to public web-facing applications.

View File

@ -0,0 +1,38 @@
Linking OP25 TX and RX December 2020
===================================================================
The OP25 TX application can be configured to transmit one or more
channels directly to one of the RX apps (rx.py or multi_rx.py) in
each case by specifying a device args string of the form
udp:host:port
where host and port are the IP address and UDP port number,
respectively. Typically, the TX is started first followed by
the RX, but they can be started in any order. UDP port 25252 is
used in the examples.
The sample RX script is in runudp.sh, and the sample TX json config
is in cfg-900.json .
To start the TX
cd .../op25/op25-gr_repeater/apps/tx
./multi_tx.py -c ../cfg-900.json
The RX is started with
cd .../op25/op25-gr_repeater/apps
./runudp.sh
There is a 120KHz sized block of spectrum transmitted (SR=120000)
which is treated as complex baseband with each side pretending that
the spectrum is converted to/from the 900 MHz band. On the RX side
any frequency-change requests are ignored; instead, a "center
frequency" is defined. There are two channels (P25 trunk control
channel and P25 voice channel) spaced at 50 KHz separation.
The TX reads two data files that must be created beforehand. To
create the trunk control channel data symbol file, refer to
tx/README-fakecc.txt
An audio file (PCM/ rate 8000 / mono / signed int16) must also be
created - this file will be real-time encoded with the voice codec
and sent on the P25 voice channel. These two files must be defined
in the cfg-900.json file (channel "source" keyword).

View File

@ -0,0 +1,268 @@
OP25 HTTP live streaming December 2020
=====================================================================
These hacks ("OP25-hls hacks") add a new option for audio reception
and playback in OP25; namely, via an HTTP live stream to any remote
client using a standard Web browser*. The web server software used
(nginx) is industrial-strength and immediately scalable to dozens or
hundreds of simultaneous remote users with zero added effort. More
than one upstream source (in parallel) can be served simultaneously.
OP25's liquidsoap script is hacked to pipe output PCM audio data
to ffmpeg, which also reads the www/images/status.png image file
that makes up the video portion of the encoded live stream. The
image png file is kept updated by rx.py.
The selection of ffmpeg codecs ("libx264" for video and "aac" for
audio) allows us directly to send the encoded data stream from
ffmpeg to the web server (nginx) utilizing RTMP. Individual
MPEG-TS segments are stored as files in nginx web server URL-space,
and served to web clients via standard HTTP GET requests. The
hls.js package is used at the client.
The entire effort mostly involved assembling existing off-the-shelf
building blocks. The ffmpeg package was built manually from source
to enable the "libx264" codec, and a modified nginx config was
used.
*the web browser must support the "MediaSource Extensions" API.
All recent broswer versions should qualify.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. nginx installation
The libnginx-mod-rtmp package must be installed (in addition to
nginx itself). You can copy the sample nginx configuration at the
end of this README file to /etc/nginx/nginx.conf, followed by
restarting the web server
sudo systemctl stop nginx
sudo systemctl start nginx
With this configuration the web server should listen on HTTP port
8081 and RTMP port 1935.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2. ffmpeg installation
git clone https://code.videolan.org/videolan/x264.git
git clone https://git.ffmpeg.org/ffmpeg.git
cd x264
./configure
make
sudo make install
cd ../ffmpeg
./configure --enable-shared --enable-libx264 --enable-gpl
make
sudo make install
To confirm the installation run the "ffmpeg" command and
verify the presence of "--enable-shared" and "--enable-libx264"
in the "configuration:" section of the output.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3. liquidsoap installation
Both packages "liquidsoap" and "liquidsoap-plugin-all" were
installed, but not tested whether the plugins are required for
this application.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4. nginx setup
with the custom config installed as per step 1, copy the files
from op25/gr_op25_repeater/www to /var/www/html as follows:
live.html
live.m3u8
hls.js
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5. liquidsoap setup
in the op25/gr_op25_repeater/apps directory, note the ffmpeg.liq
script. Overall the filtering and buffering options should be
similar to those in op25.liq. The default version of ffmpeg.liq
should be OK for most uses.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6. operation
With OP25 rx.py started using the options -V -w (and -2 if using
TDMA) and with ffmpeg.liq started (both from the apps directory),
you should be able to connect to http://hostip:8081/live.html
and click the Play button to begin. NOTE: See note E for more
details about how to specify the value for 'hostip' in the above
link.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7. troubleshooting
A. with the ffmpeg.liq script running ffmpeg should start sending
rtmp data over port 1935 to nginx. You should see files
start being populated in /var/www/html/hls/ .
B. If /var/www/html/hls is empty, check ffmpeg message output
for possible errors, and also check the nginx access and
error logs. Note that the /var/www/html/hls directory should
start receiving files a few seconds after ffmpeg.liq is started
(regardless of whether OP25 is actively receiving a station,
or is not receiving).
C. js debug can be enabled for hls.js by editing that file as
follows; locate the lines of code and change the "debug"
setting to "true"
var hlsDefaultConfig = _objectSpread(_objectSpread({
autoStartLoad: true,
// used by stream-controller
startPosition: -1,
// used by stream-controller
defaultAudioCodec: void 0,
// used by stream-controller
debug: true, ///// <<<=== change this line from
///// "false" to "true"
D. after reloading the page and with the web browser js console
opened (and with all message types enabled), debug messages
should now start appearing in the console. As before another
place to look for messages is in the nginx access and error
logs.
E. if you are doing heavy client-side debugging it may be helpful
to obtain a copy of the hls.js distribution and to populate
the hls.js.map file (with updated hls.js) in /var/www/html.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8. notes
A. due to the propagation delay inherent in the streaming
process, there is a latency of several seconds from when
the transmissions are receieved before they are played in
the remote web browser. OP25 attempts to keep the video
and audio synchronized but the usual user controls (hold,
lockout, etc). are not available (in this release) because
the several-second delay could cause the commands to operate
on stale talkgroup data (without additional work).
B. in keeping with the current OP25 liquidsoap setup, the audio
stream is converted to mono prior to streaming. It might be
possible to retain the stereo data (in cases where the L and
R channels contain separate information), but this has not
been tested. The ffmpeg.liq script would need to be changed to
use "output" instead of "mean(output)" and the ffmpeg script
would need to change "-ac 1" to "-ac 2". In addition the
options stereo=true and channels=2 would need to be set in the
%wav specification parameters.
C. multiple independent streams can be served simultaneously by
invoking a separate ffmpeg.sh script for each stream and by
changing the last component of the rtmp URL to a unique
value; for example:
rtmp://localhost/live/stream2
A unified (parameterized) version of ffmpeg.sh could also be
used.
Also, new versions of live.html and live.m3u8 in /var/www/html
(reflecting the above modification) would need to be added.
D. note that pausing and seeking etc. in the media feed isn't
possible when doing live streaming.
E. when connecting from the remote client to the nginx server as
detailed in step 6 (above) you should specify the value for
'hostip' as follows (omitting the quotes):
'localhost' (default) - use this when the client is on the same
machine as the server
'host.ip.address' - specify the IP address of the server when
the client and server machines are different, and the server
does not have a DNS hostname.
'hostname' - if the server has a DNS hostname, this name should
be used in place of 'hostip'.
Note that in the second and third cases above you must also
change the hostname from 'localhost' to the IP address or DNS
hostname, respectively, in the following files
live.html
live.m3u8
in /var/www/html (total three occurrences).
Similarly, if an IP port other than 8081 is to be used, the same
updates as above must be made, and also the nginx conf file must
be updated to reflect the changed port number.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
########################################################################
####### tested on ubuntu 18.04 #######
####### sample nginx conf file - copy everything below this line #######
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
# RTMP configuration
rtmp {
server {
listen 1935; # Listen on standard RTMP port
chunk_size 4000;
application live {
live on;
# Turn on HLS
hls on;
hls_path /var/www/html/hls/;
hls_fragment 3;
hls_playlist_length 60;
# disable consuming the stream from nginx as rtmp
deny play all;
}
}
}
http {
sendfile off;
tcp_nopush on;
#aio on;
directio 512;
default_type application/octet-stream;
server {
listen 8081;
location / {
# Disable cache
add_header 'Cache-Control' 'no-cache';
# CORS setup
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length';
# allow CORS preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
# include /etc/nginx/mime.types;
types {
text/html html;
text/css css;
application/javascript js;
application/dash+xml mpd;
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
root /var/www/html;
}
}
}

View File

@ -0,0 +1,7 @@
{
"label_color": "#000000",
"tic_color": "#000000",
"border_color": "#000000",
"plot_color": "#c000ff",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python3
# Copyright 2017, 2018 Graham Norbury
#
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI
#
# This file is part of OP25 and part of GNU Radio
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
import signal
import sys
import time
from optparse import OptionParser
from sockaudio import socket_audio
def signal_handler(signal, frame):
sys.stderr.write("audio.py shutting down\n")
audio_handler.stop()
sys.exit(0)
parser = OptionParser()
parser.add_option("-O", "--audio-output", type="string", default="default", help="audio output device name")
parser.add_option("-H", "--host-ip", type="string", default="0.0.0.0", help="IP address to bind to")
parser.add_option("-u", "--wireshark-port", type="int", default=23456, help="Wireshark port")
parser.add_option("-2", "--two-channel", action="store_true", default=False, help="single or two channel audio")
parser.add_option("-x", "--audio-gain", type="float", default="1.0", help="audio gain (default = 1.0)")
parser.add_option("-s", "--stdout", action="store_true", default=False, help="write to stdout instead of audio device")
parser.add_option("-S", "--silence", action="store_true", default=False, help="suppress output of zeros after timeout")
(options, args) = parser.parse_args()
if len(args) != 0:
parser.print_help()
sys.exit(1)
audio_handler = socket_audio(options.host_ip, options.wireshark_port, options.audio_output, options.two_channel, options.audio_gain, options.stdout, silent_flag=options.silence)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
audio_handler.run()

View File

@ -0,0 +1,40 @@
{
"channels": [
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56120",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 924975000,
"if_rate": 24000,
"name": "p25 control channel",
"plot": "datascope",
"source": "symbols:sym-cc925.dat",
"symbol_rate": 4800
},
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56124",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 924925000,
"if_rate": 24000,
"name": "p25 voice channel",
"plot": "datascope",
"source": "/home/mhp/rand4.raw",
"symbol_rate": 4800
}
],
"devices": [
{
"args": "udp:127.0.0.1:25252",
"frequency": 924950000,
"gains": "",
"name": "udp",
"offset": 0,
"ppm": 0,
"rate": 120000,
"tunable": false
}
]
}

View File

@ -0,0 +1,40 @@
{
"channels": [
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:56124",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "p25 control channel",
"plot": "symbol",
"decode": "p25_decoder:cc",
"symbol_rate": 4800
},
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:23456",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "p25 voice channel",
"plot": "symbol",
"decode": "p25_decoder:vc",
"symbol_rate": 4800
}
],
"devices": [
{
"args": "rtl=1",
"frequency": 454300000,
"gains": "lna:49",
"name": "rtl0",
"offset": 0,
"ppm": 54,
"rate": 1000000,
"tunable": false
}
]
}

View File

@ -0,0 +1,50 @@
{
"channels": [
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:56124",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "p25 control channel",
"plot": "symbol",
"decode": "p25_decoder:cc",
"symbol_rate": 4800
},
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:23456",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "p25 voice channel",
"plot": "symbol",
"decode": "p25_decoder:vc",
"symbol_rate": 4800
}
],
"devices": [
{
"args": "rtl=00000011",
"frequency": 460300000,
"gains": "lna:49",
"name": "rtl11_cc",
"offset": 0,
"ppm": 54,
"rate": 1000000,
"tunable": false
},
{
"args": "rtl=00000012",
"frequency": 453000000,
"gains": "lna:49",
"name": "rtl12_vc",
"offset": 0,
"ppm": 54,
"rate": 1000000,
"tunable": false
}
]
}

View File

@ -0,0 +1,98 @@
{
"channels": [
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:23456",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "Oswego CC",
"plot": "symbol",
"decode": "p25_decoder:role=cc:dev=rtl12:nac=0x2a4",
"symbol_rate": 4800
},
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:23456",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "Cayuga CC",
"plot": "symbol",
"decode": "p25_decoder:role=cc:dev=rtl12:nac=0x2a8",
"symbol_rate": 4800
},
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:23456",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "460 MHz VC",
"plot": "symbol",
"decode": "p25_decoder:role=vc:dev=rtl12",
"symbol_rate": 4800
},
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:23456",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "453-454 MHz VC",
"plot": "symbol",
"decode": "p25_decoder:role=vc:dev=rtl11",
"symbol_rate": 4800
},
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:56124",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "Onondaga CC",
"plot": "symbol",
"decode": "p25_decoder:role=cc:dev=rtl12:nac=0x2a0",
"symbol_rate": 4800
},
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:56124",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 0,
"if_rate": 24000,
"name": "Cortland CC",
"plot": "constellation",
"decode": "p25_decoder:role=cc:dev=rtl11:nac=0x4e1",
"symbol_rate": 4800
}
],
"devices": [
{
"args": "rtl=00000012",
"frequency": 460500000,
"gains": "lna:49",
"name": "rtl12",
"offset": 0,
"ppm": 54,
"rate": 1000000,
"tunable": false
},
{
"args": "rtl=00000011",
"frequency": 453850000,
"gains": "lna:49",
"name": "rtl11",
"offset": 0,
"ppm": 55,
"rate": 2048000,
"tunable": false
}
]
}

View File

@ -0,0 +1,76 @@
{
"channels": [
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56124",
"excess_bw": 0.2,
"filter_type": "nxdn",
"frequency": 442112500,
"if_rate": 24000,
"name": "nxdn48",
"plot": "datascope",
"source": "/home/mhp/rand0.raw",
"symbol_rate": 2400
},
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56128",
"excess_bw": 0.2,
"filter_type": "rrc",
"frequency": 442187500,
"if_rate": 24000,
"name": "dmr",
"plot": "datascope",
"source": "/home/mhp/rand1.raw",
"symbol_rate": 4800
},
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56132",
"excess_bw": 0.2,
"filter_type": "gmsk",
"frequency": 442262500,
"if_rate": 24000,
"name": "dstar",
"plot": "datascope",
"source": "/home/mhp/rand2.raw",
"symbol_rate": 4800
},
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56136",
"excess_bw": 0.2,
"filter_type": "rrc",
"frequency": 442337500,
"if_rate": 24000,
"name": "ysf",
"plot": "datascope",
"source": "/home/mhp/rand3.raw",
"symbol_rate": 4800
},
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56120",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 442412500,
"if_rate": 24000,
"name": "p25",
"plot": "datascope",
"source": "/home/mhp/rand4.raw",
"symbol_rate": 4800
}
],
"devices": [
{
"args": "udp:127.0.0.1:25252",
"frequency": 442262500,
"gains": "",
"name": "udp",
"offset": 0,
"ppm": 0,
"rate": 480000,
"tunable": false
}
]
}

View File

@ -0,0 +1,49 @@
{
"channels": [
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:56120",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 460412500,
"if_rate": 24000,
"name": "p25",
"plot": "symbol",
"symbol_rate": 4800
},
{
"demod_type": "fsk4",
"destination": "file:///tmp/out1.raw",
"excess_bw": 0.2,
"filter_type": "rrc",
"frequency": 460500000,
"if_rate": 24000,
"name": "ysf",
"plot": "datascope",
"symbol_rate": 4800
},
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56122",
"excess_bw": 0.2,
"filter_type": "rrc",
"frequency": 460050000,
"if_rate": 24000,
"name": "dmr",
"plot": "symbol",
"symbol_rate": 4800
}
],
"devices": [
{
"args": "rtl=0",
"frequency": 460100000,
"gains": "lna:49",
"name": "rtl0",
"offset": 0,
"ppm": 38,
"rate": 1000000,
"tunable": false
}
]
}

View File

@ -0,0 +1,602 @@
[
[
500,
"placeholder",
"do-not-use",
false
],
[
1,
"#0066ff",
"",
false
],
[
2,
"#ff0000",
"",
false
],
[
3,
"#ff9900",
"",
false
],
[
4,
"#eeeeee",
"",
false
],
[
5,
"#9966ff",
"",
false
],
[
6,
"#00ff00",
"",
false
],
[
7,
"#009933",
"",
false
],
[
8,
"#ffff00",
"",
false
],
[
9,
"#eee",
"",
false
],
[
10,
"#ff6666",
"",
false
],
[
11,
"#0080C0",
"",
false
],
[
12,
"#666666",
"",
false
],
[
13,
"#666666",
"",
false
],
[
14,
"#666666",
"",
false
],
[
15,
"#666666",
"",
false
],
[
16,
"#666666",
"",
false
],
[
17,
"#666666",
"",
false
],
[
18,
"#666666",
"",
false
],
[
19,
"#666666",
"",
false
],
[
20,
"#666666",
"",
false
],
[
21,
"#666666",
"",
false
],
[
22,
"#ff0000",
"",
true
],
[
23,
"#666666",
"",
false
],
[
24,
"#666666",
"",
false
],
[
25,
"#666666",
"",
false
],
[
26,
"#666666",
"",
false
],
[
27,
"#666666",
"",
false
],
[
28,
"#666666",
"",
false
],
[
29,
"#666666",
"",
false
],
[
30,
"#666666",
"",
false
],
[
31,
"#666666",
"",
false
],
[
32,
"#666666",
"",
false
],
[
33,
"#666666",
"",
false
],
[
34,
"#666666",
"",
false
],
[
35,
"#666666",
"",
false
],
[
36,
"#666666",
"",
false
],
[
37,
"#666666",
"",
false
],
[
38,
"#666666",
"",
false
],
[
39,
"#666666",
"",
false
],
[
40,
"#666666",
"",
false
],
[
41,
"#666666",
"",
false
],
[
42,
"#666666",
"",
false
],
[
43,
"#666666",
"",
false
],
[
44,
"#666666",
"",
false
],
[
45,
"#666666",
"",
false
],
[
46,
"#666666",
"",
false
],
[
47,
"#666666",
"",
false
],
[
48,
"#666666",
"",
false
],
[
49,
"#666666",
"",
false
],
[
50,
"#666666",
"",
false
],
[
51,
"#666666",
"",
false
],
[
52,
"#666666",
"",
false
],
[
53,
"#666666",
"",
false
],
[
54,
"#666666",
"",
false
],
[
55,
"#666666",
"",
false
],
[
56,
"#666666",
"",
false
],
[
57,
"#666666",
"",
false
],
[
58,
"#666666",
"",
false
],
[
59,
"#666666",
"",
false
],
[
60,
"#666666",
"",
false
],
[
61,
"#666666",
"",
false
],
[
62,
"#666666",
"",
false
],
[
63,
"#666666",
"",
false
],
[
64,
"#666666",
"",
false
],
[
65,
"#666666",
"",
false
],
[
66,
"#666666",
"",
false
],
[
67,
"#666666",
"",
false
],
[
68,
"#666666",
"",
false
],
[
69,
"#666666",
"",
false
],
[
70,
"#666666",
"",
false
],
[
71,
"#666666",
"",
false
],
[
72,
"#666666",
"",
false
],
[
73,
"#666666",
"",
false
],
[
74,
"#666666",
"",
false
],
[
75,
"#666666",
"",
false
],
[
76,
"#666666",
"",
false
],
[
77,
"#666666",
"",
false
],
[
78,
"#666666",
"",
false
],
[
79,
"#666666",
"",
false
],
[
80,
"#666666",
"",
false
],
[
81,
"#666666",
"",
false
],
[
82,
"#666666",
"",
false
],
[
83,
"#666666",
"",
false
],
[
84,
"#666666",
"",
false
],
[
85,
"#666666",
"",
false
],
[
86,
"#666666",
"",
false
],
[
87,
"#666666",
"",
false
],
[
88,
"#666666",
"",
false
],
[
89,
"#666666",
"",
false
],
[
90,
"#666666",
"",
false
],
[
91,
"#666666",
"",
false
],
[
92,
"#666666",
"",
false
],
[
93,
"#666666",
"",
false
],
[
94,
"#666666",
"",
false
],
[
95,
"#666666",
"",
false
],
[
96,
"#666666",
"",
false
],
[
97,
"#666666",
"",
false
],
[
98,
"#666666",
"",
false
],
[
99,
"#00ff00",
"#000000",
false
]
]

View File

@ -0,0 +1,27 @@
[
{
"fs":[ 1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1 ],
"length":864,
"name":"P25"
},
{
"fs":[ -3, -3, 1, 1, -3, -3, 3, 3, -3, -3, -3, -3, 3, 3, 3, 3, -1, -1, 3, 3 ],
"length":384,
"name":"NXDN48"
},
{
"fs":[ -3, 1, -3, 3, -3, -3, 3, 3, -1, 3 ],
"length":192,
"name":"NXDN96"
},
{
"fs":[ 1, -1, 1, 1, 1, 1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, -1, 1, -1 ],
"length":576,
"name":"DMR"
},
{
"fs":[ -3, 3, 3, 1, 3, -3, 1, 3, -3, 1, -1, 3, 3, -1, 1, -3, 3, 1, -3, 3 ],
"length":480,
"name":"YSF"
}
]

View File

@ -0,0 +1,57 @@
#!/usr/bin/env python
#
# (c) Copyright 2020, OP25
#
# This file is part of OP25 and part of GNU Radio
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
""" generate named image file consisting of multi-line text """
from PIL import Image, ImageDraw, ImageFont
import os
_TTF_FILE = '/usr/share/fonts/truetype/freefont/FreeSerif.ttf'
def create_image(textlist=["Blank"], imgfile="test.png", bgcolor='red', fgcolor='black', windowsize=(400,300)):
global _TTF_FILE
width=windowsize[0]
height=windowsize[1]
margin = 4
if not os.access(_TTF_FILE, os.R_OK):
font = ImageFont.load_default()
else:
font = ImageFont.truetype(_TTF_FILE, 16)
img = Image.new('RGB', (width, height), bgcolor)
draw = ImageDraw.Draw(img)
cursor = 0
for line in textlist:
w,h = draw.textsize(line, font)
# TODO: overwidth check needed?
if cursor+h >= height:
break
draw.text((margin, cursor), line,'black',font)
cursor += h + margin // 2
img.save(imgfile)
if __name__ == '__main__':
s = []
s.append('Starting...')
create_image(textlist=s, bgcolor='#c0c0c0')

View File

@ -0,0 +1,383 @@
#sql_dbi events map
events_map = {
"grp_v_ch_grant_mbt": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'options'],
['frequency', 'frequency'],
['tgid', 'group'],
['suid', 'srcaddr'],
],
"grg_exenc_cmd": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'mfrid'],
['tgid', 'sg'],
['p', 'keyid'],
],
"grp_v_ch_grant": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'mfrid'],
['p', 'options'],
['frequency', 'frequency'],
['tgid', 'group'],
['suid', 'srcaddr'],
],
"mot_grg_cn_grant": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'mfrid'],
['frequency', 'frequency'],
['tgid', 'sg'],
['suid', 'sa'],
],
"grp_v_ch_grant_updt": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'mfrid'],
['frequency', 'frequency1'],
['tgid', 'group1'],
],
"grp_v_ch_grant_updt_exp": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'mfrid'],
['p', 'options'],
['frequency', 'frequency'],
['tgid', 'group'],
],
"ack_resp_fne": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'aiv'],
['p2', 'ex'],
['p3', 'addl'],
['wacn', 'wacn'],
['suid', 'source'],
['suid2', 'target'],
],
"deny_resp": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'aiv'],
['p2', 'reason'],
['p3', 'additional'],
['suid', 'target'],
],
"grp_aff_resp": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'affiliation'],
['p2', 'group_aff_value'],
['tgid', 'announce_group'],
['tgid2', 'group'],
['suid', 'target'],
],
"grp_aff_q": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['suid', 'source'],
['suid2', 'target'],
],
"loc_reg_resp": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'rv'],
['p2', 'rfss'],
['p3', 'siteid'],
['tgid', 'group'],
['suid', 'target'],
],
"u_reg_resp": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'rv'],
['suid', 'source'],
['suid2', 'target'],
],
"u_reg_cmd": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['suid', 'source'],
['suid2', 'target'],
],
"u_de_reg_ack": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['wacn', 'wacn'],
['suid', 'source'],
],
"ext_fnct_cmd": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'mfrid'],
['p', 'efclass'],
['p2', 'efoperand'],
['suid', 'efargs'],
],
"end_call": [
['time', 'time'],
['sysid', 'sysid'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'code'],
['suid', 'srcaddr'],
['tgid', 'tgid'],
['p2', 'duration'],
['p3', 'count'],
],
}
# cc_event to numerical id (oplog and sql_dbi)
cc_events = {
"ack_resp_fne": 1,
"deny_resp": 2,
"end_call": 3,
"ext_fnct_cmd": 4,
"grg_exenc_cmd": 5,
"grp_aff_q": 6,
"grp_aff_resp": 7,
"grp_v_ch_grant": 8,
"grp_v_ch_grant_mbt": 9,
"grp_v_ch_grant_updt": 10,
"grp_v_ch_grant_updt_exp": 11,
"loc_reg_resp": 12,
"u_de_reg_ack": 13,
"u_reg_cmd": 14,
"u_reg_resp": 15,
"mot_grg_cn_grant": 16,
}
# sql column names to DataTables (Oplog)
oplog_map = {
"grp_v_ch_grant_mbt": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'MFRID'],
['p', 'Options'],
['frequency', 'Frequency'],
['tgid', 'Talkgroup ID'],
['tgid', 'Talkgroup'],
['suid', 'Source ID'],
['suid', 'Source'],
],
"grg_exenc_cmd": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'MFRID'],
['tgid', 'SG (tgid)'],
['p', 'Key ID'],
],
"grp_v_ch_grant": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'MFRID'],
['p', 'Options'],
['frequency', 'Frequency'],
['tgid', 'Talkgroup ID'],
['tgid', 'Talkgroup'],
['suid', 'Source ID'],
['suid', 'Source'],
],
"mot_grg_cn_grant": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'MFRID'],
['frequency', 'Frequency'],
['tgid', 'Talkgroup ID'],
['tgid', 'Talkgroup'],
['suid', 'Source ID'],
['suid', 'Source'],
],
"grp_v_ch_grant_updt": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'MFRID'],
['frequency', 'Frequency'],
['tgid', 'Talkgroup ID'],
['tgid', 'Talkgroup'],
],
"grp_v_ch_grant_updt_exp": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'mfrid'],
['p', 'Options'],
['frequency', 'Frequency'],
['tgid', 'Talkgroup ID'],
['tgid', 'Talkgroup'],
],
"ack_resp_fne": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'aiv'],
['p2', 'ex'],
['p3', 'Additional'],
['wacn', 'wacn'],
['suid', 'System Source'],
['suid2', 'Target ID'],
['suid2', 'Target'],
],
"deny_resp": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'aiv'],
['p2', 'Reason'],
['p3', 'Additional'],
['suid', 'Target ID'],
['suid', 'Target'],
],
"grp_aff_resp": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'Affiliation'],
['p2', 'Group Aff Value'],
['tgid', 'Announce Group'],
['tgid2', 'Talkgroup ID'],
['tgid2', 'Talkgroup'],
['suid', 'Target ID'],
['suid', 'Target'],
],
"grp_aff_q": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['suid', 'System Source'],
['suid2', 'Target ID'],
['suid2', 'Target'],
],
"loc_reg_resp": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'rv'],
['p2', 'RFSS'],
['p3', 'Site'],
['tgid', 'Talkgroup ID'],
['tgid', 'Talkgroup'],
['suid', 'Target ID'],
['suid', 'Target'],
],
"u_reg_resp": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'rv'],
['suid', 'Source ID'],
['suid', 'Source'],
['suid2', 'Target'],
],
"u_reg_cmd": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['suid', 'System Source'],
['suid2', 'Target ID'],
['suid2', 'Target'],
],
"u_de_reg_ack": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['wacn', 'WACN'],
['suid', 'Source ID'],
['suid', 'Source'],
],
"ext_fnct_cmd": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['mfrid', 'MFRID'],
['p', 'Class'],
['p2', 'Operand'],
['suid', 'System Source'],
],
"end_call": [
['time', 'Time'],
['sysid', 'System'],
['opcode', 'opcode'],
['cc_event', 'cc_event'],
['p', 'Code'],
['suid', 'Source ID'],
['suid', 'Source'],
['tgid', 'Talkgroup ID'],
['tgid', 'Talkgroup'],
['p2', 'Duration (ms)'],
['p3', 'Count (p3)'],
],
}
# friendly long description strings, used in Oplog
cc_desc = {
"ack_resp_fne": "Acknowledge Response FNE - 0x20",
"deny_resp": "Deny Response - 0x27",
"end_call": "End Call (not a naitve control channel event)",
"ext_fnct_cmd": "Extended Function Command - 0x24",
"grg_exenc_cmd": "Harris Group Regroup Explicit Encryption Command - 0x30",
"grp_aff_q": "Group Affiliation Query - 0x2A",
"grp_aff_resp": "Group Affiliation Response - 0x28",
"grp_v_ch_grant": "Group Voice Channel Grant - 0x00",
"grp_v_ch_grant_mbt": "Group Voice Channel Grant, Multiple Block Trunking",
"grp_v_ch_grant_updt": "Group Voice Channel Grant Update - 0x02",
"grp_v_ch_grant_updt_exp": "Group Voice Channel Grant Update, Explicit - 0x03",
"loc_reg_resp": "Location Registration Response 0x2B",
"mot_grg_cn_grant": "Motorola Patch Channel Grant - 0x02",
"u_de_reg_ack": "De-Registration Acknowledge (Logout) - 0x2F",
"u_reg_cmd": "Unit Registration Command (Force Unit Registration) - 0x2D",
"u_reg_resp": "Unit Registration Response - 0x2C"
}

View File

@ -0,0 +1,73 @@
#!/usr/bin/liquidsoap
# Example liquidsoap streaming from op25 to icecast
# (c) 2019, 2020 gnorbury@bondcar.com, wllmbecks@gmail.com
#
set("log.stdout", true)
set("log.file.path", "/home/pi/op25/op25/gr-op25_repeater/apps/liquidsoap.log")
set("log.file", true)
set("log.level", 3)
# Make the native sample rate compatible with op25
set("frame.audio.samplerate", 8000)
# SOURCE INPUT BLOCK OPTIONS
# Mono and stereo input sources are mutually exclusive. Choose type.
# Mono input source
input = mksafe(input.external(buffer=0.25, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -x 2 -s"))
# Consider increasing the buffer value on slow systems such as RPi3. e.g. buffer=0.25
# Longer buffer results in less choppy audio but at the expense of increased latency.
# Left channel input source and audio summing for two-slot protocols
#left = input.external(buffer=0.25, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -u 23450 -x 2 -s")
#left = audio_to_stereo(left)
#left = stereo.pan(pan=1., left)
# Right channel input source and audio summing for two-slot protocols
#right = input.external(buffer=0.25, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -u 23460 -x 2 -s")
#right = audio_to_stereo(right)
#right = stereo.pan(pan=-1., right)
# OPTIONAL AUDIO SIGNAL PROCESSING BLOCKS
# Uncomment to enable
#
# High pass filter mono
input = filter.iir.butterworth.high(frequency = 200.0, order = 4, input)
# High pass filter stereo
#left = filter.iir.butterworth.high(frequency = 200.0, order = 4, left)
#right = filter.iir.butterworth.high(frequency = 200.0, order = 4, right)
# Low pass filter mono
input = filter.iir.butterworth.low(frequency = 3250.0, order = 4, input)
# Low pass filter stereo
#left = filter.iir.butterworth.low(frequency = 3250.0, order = 4, left)
#right = filter.iir.butterworth.low(frequency = 3250.0, order = 4, right)
# Normalization mono
input = normalize(input, gain_max = 3.0, gain_min = -3.0, target = -16.0, threshold = -40.0)
# Normalization stereo -- Note -- Adjust target gains independently to achieve left/right balance
#left = normalize(left, gain_max = 3.0, gain_min = -3.0, target = -16.0, threshold = -40.0)
#right = normalize(right, gain_max = 3.0, gain_min = -3.0, target = -16.0, threshold = -40.0)
# Commnent out the line below for "non-stereo" (mono) output
#input = mksafe(add(normalize=false, [left,right]))
# LOCAL AUDIO OUTPUT
# Uncomment the line below to enable local sound
#output.ao(input)
# ICECAST STREAMING
# Uncomment to enable output to an icecast server
# Change the "host", "password", and "mount" strings appropriately first!
# For metadata to work properly, the host address given here MUST MATCH the address in op25's meta.json file
#
# Mono Stream
output.icecast(%mp3(bitrate=16, samplerate=22050, stereo=false), description="op25", genre="Public Safety", url="", fallible=false, icy_metadata="false", host="localhost", port=8000, mount="op25", password="hackme", mean(input))
#
# Stereo Stream
#output.icecast(%mp3(bitrate=16, samplerate=22050, stereo=true), description="op25", genre="Public Safety", url="", fallible=false, icy_metadata="false", host="localhost", port=8000, mount="op25", password="hackme", input)

View File

@ -0,0 +1,15 @@
#!/usr/bin/liquidsoap
# Example liquidsoap hls streaming from op25 to nginx
# (c) 2019, 2020 gnorbury@bondcar.com, wllmbecks@gmail.com
# (c) 2020 KA1RBI
set("log.stdout", true)
set("log.file", false)
set("log.level", 1)
set("frame.audio.samplerate", 8000)
input = mksafe(input.external(buffer=0.02, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -x 2 -s -S"))
output.external(%wav(stereo=false, channels=1, samplesize=16, header=false, samplerate=8000), fallible=false, flush=true, "./ffmpeg.sh", mean(input))

View File

@ -0,0 +1,42 @@
#! /bin/sh
# Copyright (c) 2020 OP25
#
# This file is part of OP25 and part of GNU Radio
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
#
# this script should not be run directly - run ffmpeg.liq instead
#
# requires ffmpeg configured with --enable-libx264
#
ffmpeg \
-ar 8000 \
-ac 1 \
-acodec pcm_s16le \
-f s16le \
-i pipe:0 \
-f image2 \
-loop 1 \
-i ../www/images/status.png \
-vcodec libx264 \
-pix_fmt yuv420p \
-f flv \
-acodec aac \
-b:a 48k \
rtmp://localhost/live/stream

View File

@ -0,0 +1,806 @@
#!/usr/bin/env python
# Copyright 2011, 2012, 2013, 2014, 2015 Max H. Parke KA1RBI
#
# This file is part of OP25
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
import sys
import os
import time
import subprocess
import json
import threading
import glob
from gnuradio import gr, eng_notation
from gnuradio import blocks, audio
from gnuradio.eng_option import eng_option
import numpy as np
from gnuradio import gr
from math import pi, sin, cos
_def_debug = 0
_def_sps = 10
_def_cpm_mode = 'cpm'
GNUPLOT = '/usr/bin/gnuplot'
FFT_AVG = 0.25
MIX_AVG = 0.15
BAL_AVG = 0.05
FFT_BINS = 512
def degrees(r):
d = 360 * r / (2*pi)
while d <0:
d += 360
while d > 360:
d -= 360
return d
def limit(a,lim):
if a > lim:
return lim
return a
def ensure_str(s): # for python 2/3
if isinstance(s[0], str):
return s
ns = ''
for i in range(len(s)):
ns += chr(s[i])
return ns
PSEQ = 0
class wrap_gp(object):
def __init__(self, sps=_def_sps, logfile=None, title="", color_cfg='plot-colors.json'):
global PSEQ
self.sps = sps
self.center_freq = 0.0
self.relative_freq = 0.0
self.offset_freq = 0.0
self.width = None
self.ffts = ()
self.freqs = ()
self.avg_pwr = np.zeros(FFT_BINS)
self.avg_sum_pwr = 0.0
self.buf = np.array([])
self.plot_count = 0
self.last_plot = 0
self.plot_interval = None
self.sequence = 0
self.output_dir = None
self.filename = None
self.logfile = logfile
self.title = title
self.sequence_id = PSEQ
PSEQ += 1
x = self.sequence_id % 3
y = self.sequence_id // 3
self.position = (x, y)
self.colors = {}
self.colors['label_color'] = ''
self.colors['tic_color'] = ''
self.colors['border_color'] = ''
self.colors['plot_color'] = ''
self.colors['background_color'] = ''
if color_cfg and os.access(color_cfg, os.R_OK):
ccfg = json.loads(open(color_cfg).read())
for color in ccfg:
self.colors[color] = ccfg[color]
self.next_cpmd = time.time()
self.attach_gp()
def attach_gp(self):
args = (GNUPLOT, '-noraise')
exe = GNUPLOT
self.gp = subprocess.Popen(args, executable=exe, stdin=subprocess.PIPE)
def kill(self):
try:
self.gp.stdin.close() # closing pipe should cause subprocess to exit
except IOError:
pass
sleep_count = 0
while True: # wait politely, but only for so long
self.gp.poll()
if self.gp.returncode is not None:
break
time.sleep(0.1)
if self.gp.returncode is not None:
break
sleep_count += 1
if (sleep_count & 1) == 0:
self.gp.kill()
if sleep_count >= 3:
break
def set_interval(self, v):
self.plot_interval = v
def set_output_dir(self, v):
self.output_dir = v
def set_sps(self, sps):
self.sps = sps
def plot(self, buf, bufsz, mode='eye'):
BUFSZ = bufsz
consumed = min(len(buf), BUFSZ-len(self.buf))
if len(self.buf) < BUFSZ:
self.buf = np.concatenate((self.buf, buf[:int(consumed)]))
if len(self.buf) < BUFSZ:
return consumed
self.plot_count += 1
if mode == 'eye' and self.plot_count % 20 != 0:
self.buf = np.array([])
return consumed
plots = []
s = ''
plot_size = (320,240)
if mode == 'eye':
nplots = len(self.buf) // self.sps - 2
for i in range(nplots):
s += '\n'.join(['%f' % self.buf[i*self.sps+j] for j in range(self.sps+1)])
s += '\ne\n'
plots.append('"-" with lines')
elif mode == 'cpm':
nplots = len(self.buf)
ab = np.abs(self.buf)
for i in range(len(ab)):
s += '%f\n' % ab[i]
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'cpmd':
if time.time() < self.next_cpmd:
self.buf = np.array([])
return 0
self.next_cpmd = time.time() + 0.5
ab = np.abs(self.buf)
thresh = np.max(ab) / 2.0
mask1 = np.array(ab > thresh, dtype=np.int)
maskdf = mask1[1:] - mask1[:-1]
nz = np.array(np.nonzero(maskdf), dtype=np.int)[0]
nzd = nz[1:]-nz[:-1]
nzdn = nzd // self.sps
sel = (nzdn > 165) & (nzdn < 185)
valid = np.array(np.nonzero(sel), dtype=np.int)[0]
if len(valid) < 1:
self.buf = np.array([])
return 0
v0 = valid[0]
i0 = nz[v0]
samples = self.buf[i0 : i0+170*self.sps+2]
fmd = np.angle(samples[1:] * np.conj(samples[:-1]))
fmd = fmd[12*self.sps:]
for n in range(0,len(fmd),self.sps):
sl = fmd[n:n+self.sps+1]
if len(sl) != self.sps+1:
break
s += '\n'.join(['%f' % (x*self.sps) for x in sl])
s += '\ne\n'
plots.append('"-" with lines')
elif mode == 'constellation':
plot_size = (240,240)
self.buf = self.buf[:100]
for b in self.buf:
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
s += 'e\n'
plots.append('"-" with points')
for b in self.buf:
#s += '%f\t%f\n' % (b.real, b.imag)
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'symbol':
for b in self.buf:
s += '%f\n' % (b)
s += 'e\n'
plots.append('"-" with points')
elif mode == 'fftf':
self.ffts = np.fft.rfft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ)
#self.ffts = np.fft.fftshift(self.ffts)
self.ffts = np.abs(self.ffts) ** 2.0
self.ffts /= np.max(self.ffts)
for i in range(len(self.ffts)):
s += '%f\n' % (self.ffts[i])
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'fft' or mode == 'mixer':
sum_pwr = 0.0
self.ffts = np.fft.fft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ)
self.ffts = np.fft.fftshift(self.ffts)
self.freqs = np.fft.fftfreq(len(self.ffts))
self.freqs = np.fft.fftshift(self.freqs)
tune_freq = (self.center_freq - self.relative_freq) / 1e6
if self.center_freq and self.width:
self.freqs = ((self.freqs * self.width) + self.center_freq + self.offset_freq) / 1e6
for i in range(len(self.ffts)):
if mode == 'fft':
self.avg_pwr[i] = ((1.0 - FFT_AVG) * self.avg_pwr[i]) + (FFT_AVG * np.abs(self.ffts[i]))
else:
self.avg_pwr[i] = ((1.0 - MIX_AVG) * self.avg_pwr[i]) + (MIX_AVG * np.abs(self.ffts[i]))
s += '%f\t%f\n' % (self.freqs[i], 20 * np.log10(self.avg_pwr[i]))
if (mode == 'mixer') and (self.avg_pwr[i] > 1e-5):
if (self.freqs[i] - self.center_freq) < 0:
sum_pwr -= self.avg_pwr[i]
elif (self.freqs[i] - self.center_freq) > 0:
sum_pwr += self.avg_pwr[i]
self.avg_sum_pwr = ((1.0 - BAL_AVG) * self.avg_sum_pwr) + (BAL_AVG * sum_pwr)
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'float' or mode == 'correlation':
for b in self.buf:
s += '%f\n' % (b)
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'sync':
s_abs = np.abs(self.buf)
sums = np.zeros(self.sps)
for i in range(self.sps):
sums[i] = np.sum(s_abs[range(i, len(self.buf), self.sps)])
am = np.argmax(sums)
samples = self.buf[am:]
a1 = -np.angle(samples[0])
rz = cos(a1) + 1j * sin(a1)
while len(samples) >= self.sps+1:
for i in range(self.sps+1):
z = samples[i] * rz
s += '%f\t%f\n' % (z.real, z.imag)
s += 'e\n'
plots.append('"-" with linespoints')
samples = samples[self.sps:]
self.buf = np.array([])
# FFT processing needs to be completed to maintain the weighted average buckets
# regardless of whether we actually produce a new plot or not.
if self.plot_interval and self.last_plot + self.plot_interval > time.time():
return consumed
self.last_plot = time.time()
filename = None
if self.output_dir:
if self.sequence >= 2:
delete_pathname = '%s/plot-%s%d-%d.png' % (self.output_dir, mode, self.sequence_id, self.sequence-2)
if os.access(delete_pathname, os.W_OK):
os.remove(delete_pathname)
h0= 'set terminal png size %d, %d\n' % (plot_size)
filename = 'plot-%s%d-%d.png' % (mode, self.sequence_id, self.sequence)
h0 += 'set output "%s/%s"\n' % (self.output_dir, filename)
self.sequence += 1
else:
pos = ''
if self.position is not None:
x = self.position[0] * plot_size[0]
y = self.position[1] * plot_size[1]
x += 50
y += 75
pos = ' position %d, %d' % (x, y)
h0= 'set terminal x11 noraise size %d, %d%s title "%s"\n' % (plot_size[0], plot_size[1], pos, self.title)
background = ''
label_color = ''
tic_color = ''
border_color = ''
plot_color = ''
background_color = ''
if self.colors['label_color']:
label_color = 'textcolor rgb"%s"' % self.colors['label_color']
if self.colors['tic_color']:
tic_color = 'textcolor rgb"%s"' % self.colors['tic_color']
if self.colors['border_color']:
border_color = 'linecolor rgb"%s"' % self.colors['border_color']
if self.colors['plot_color']:
plot_color = 'linecolor rgb"%s"' % self.colors['plot_color']
if self.colors['background_color']:
background_color = 'fillcolor rgb"%s"' % self.colors['background_color']
background += 'set object 1 rectangle from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
background += 'set xtics %s\n' % (tic_color)
background += 'set ytics %s\n' % (tic_color)
background += 'set border %s\n' % (border_color)
h = 'set key off\n'
if mode == 'constellation':
#h+= background
plot_color = ''
h+= 'set size square\n'
h+= 'set xrange [-1:1]\n'
h+= 'set yrange [-1:1]\n'
h += 'unset border\n'
h += 'set polar\n'
h += 'set angles degrees\n'
h += 'unset raxis\n'
h += 'set object 1 rectangle from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
h += 'set object 2 circle at 0,0 size 1 fillcolor rgb 0x0f01 fillstyle solid behind\n'
h += 'set object 3 circle at 0,0 size 1 %s\n' % 'linecolor rgb"#0000f0"'
h += 'set style line 10 lt 1 lc rgb 0x404040 lw 0.1\n'
h += 'set grid polar 45\n'
h += 'set grid ls 10\n'
h += 'set xtics axis\n'
h += 'set ytics axis\n'
h += 'set xtics scale 0\n'
h += 'set xtics ("" 0.2, "" 0.4, "" 0.6, "" 0.8, "" 1)\n'
h += 'set ytics 0, 0.2, 1\n'
h += 'set format ""\n'
h += 'set style line 11 lt 1 lw 2 pt 2 ps 2\n'
h+= 'set title "Constellation %s" %s\n' % (self.title, label_color)
elif mode == 'eye':
h+= background
h+= 'set yrange [-4:4]\n'
h+= 'set title "Datascope %s" %s\n' % (self.title, label_color)
plot_color = ''
elif mode == 'cpm':
h+= background
#h+= 'set yrange [-4:4]\n'
h+= 'set title "CPM RSSI %s" %s\n' % (self.title, label_color)
#plot_color = ''
elif mode == 'cpmd':
h+= background
h+= 'set yrange [-4:4]\n'
h+= 'set title "CPM Datascope %s" %s\n' % (self.title, label_color)
plot_color = ''
elif mode == 'sync':
h += 'set object 1 rect from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
h += 'set size square\n'
h += 'set xtics %s\n' % (tic_color)
h += 'set ytics %s\n' % (tic_color)
h += 'set border %s\n' % (border_color)
elif mode == 'symbol':
h+= background
h+= 'set yrange [-4:4]\n'
h+= 'set title "Symbol %s" %s\n' % (self.title, label_color)
elif mode == 'fft' or mode == 'mixer':
h+= background
h+= 'unset arrow; unset title\n'
h+= 'set xrange [%f:%f]\n' % (self.freqs[0], self.freqs[len(self.freqs)-1])
h+= 'set xlabel "Frequency"\n'
h+= 'set ylabel "Power(dB)"\n'
h+= 'set grid\n'
h+= 'set yrange [-100:0]\n'
if mode == 'mixer': # mixer
h+= 'set title "Mixer %s: balance %3.0f (smaller is better)" %s\n' % (self.title, np.abs(self.avg_sum_pwr * 1000), label_color)
else: # fft
h+= 'set title "Spectrum %s" %s\n' % (self.title, label_color)
if self.center_freq:
arrow_pos = (self.center_freq - self.relative_freq) / 1e6
h+= 'set arrow from %f, graph 0 to %f, graph 1 nohead\n' % (arrow_pos, arrow_pos)
h+= 'set title "Spectrum: tuned to %f Mhz" %s\n' % (arrow_pos, label_color)
elif mode == 'fftf':
h+= 'set yrange [-1:1.2]\n'
h+= 'set title "fftf"\n'
elif mode == 'float':
h+= background
h+= 'set yrange [-2:2]\n'
h+= 'set title "Oscilloscope %s" %s\n' % (self.title, label_color)
elif mode == 'correlation':
h+= background
title = 'Correlation'
if self.title:
title = self.title
h+= 'set yrange [-1.1:1.1]\n'
h+= 'set title "%s" %s\n' % (title, label_color)
if self.output_dir:
s += 'set output\n' ## flush output png
dat = '%s%splot %s %s\n%s' % (h0, h, ','.join(plots), plot_color, s)
if self.logfile is not None:
with open(self.logfile, 'a') as fd:
fd.write(dat)
if sys.version[0] != '2':
dat = bytes(dat, 'utf8')
self.gp.poll()
if self.gp.returncode is None: # make sure gnuplot is still running
try:
rc = self.gp.stdin.write(dat)
except (IOError, ValueError):
pass
try:
self.gp.stdin.flush()
except (IOError, ValueError):
pass
if filename:
self.filename = filename
return consumed
def set_center_freq(self, f):
self.center_freq = f
def set_relative_freq(self, f):
self.relative_freq = f
def set_offset(self, f):
self.offset_freq = f
def set_width(self, w):
self.width = w
def set_logfile(self, logfile=None):
self.logfile = logfile
def set_title(self, title):
self.title = title
class eye_sink_f(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug, sps = _def_sps):
gr.sync_block.__init__(self,
name="eye_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.sps = sps
self.gnuplot = wrap_gp(sps=self.sps)
def work(self, input_items, output_items):
in0 = input_items[0]
consumed = self.gnuplot.plot(in0, 100*self.sps, mode='eye')
return consumed ### len(input_items[0])
def set_title(self, title):
self.gnuplot.set_title(title)
def kill(self):
self.gnuplot.kill()
class cpm_sink_c(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug, sps = _def_sps, plot_mode=_def_cpm_mode):
gr.sync_block.__init__(self,
name="cpm_sink_c",
in_sig=[np.complex64],
out_sig=None)
self.debug = debug
self.sps = sps
self.gnuplot = wrap_gp(sps=self.sps)
self.plot_mode=plot_mode
def work(self, input_items, output_items):
in0 = input_items[0]
l = len(in0)
consumed = self.gnuplot.plot(in0, self.sps*180*10, mode=self.plot_mode)
return len(input_items[0])
def set_title(self, title):
self.gnuplot.set_title(title)
def kill(self):
self.gnuplot.kill()
class constellation_sink_c(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="constellation_sink_c",
in_sig=[np.complex64],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
def work(self, input_items, output_items):
in0 = input_items[0]
self.gnuplot.plot(in0, 1000, mode='constellation')
return len(input_items[0])
def set_title(self, title):
self.gnuplot.set_title(title)
def kill(self):
self.gnuplot.kill()
class fft_sink_f(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="fft_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
self.skip = 0
def work(self, input_items, output_items):
self.skip += 1
if self.skip >= 50:
self.skip = 0
in0 = input_items[0]
self.gnuplot.plot(in0, FFT_BINS, mode='fftf')
return len(input_items[0])
def kill(self):
self.gnuplot.kill()
class fft_sink_c(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="fft_sink_c",
in_sig=[np.complex64],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
self.skip = 0
def work(self, input_items, output_items):
self.skip += 1
if self.skip >= 50:
self.skip = 0
in0 = input_items[0]
self.gnuplot.plot(in0, FFT_BINS, mode='fft')
return len(input_items[0])
def set_title(self, title):
self.gnuplot.set_title(title)
def kill(self):
self.gnuplot.kill()
def set_center_freq(self, f):
self.gnuplot.set_center_freq(f)
self.gnuplot.set_relative_freq(0.0)
def set_relative_freq(self, f):
self.gnuplot.set_relative_freq(f)
def set_offset(self, f):
self.gnuplot.set_offset(f)
def set_width(self, w):
self.gnuplot.set_width(w)
class mixer_sink_c(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="mixer_sink_c",
in_sig=[np.complex64],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
self.skip = 0
def work(self, input_items, output_items):
self.skip += 1
if self.skip >= 10:
self.skip = 0
in0 = input_items[0]
self.gnuplot.plot(in0, FFT_BINS, mode='mixer')
return len(input_items[0])
def set_title(self, title):
self.gnuplot.set_title(title)
def kill(self):
self.gnuplot.kill()
class sync_plot(threading.Thread):
"""
"""
def __init__(self, debug = _def_debug, block = None, **kwds):
threading.Thread.__init__ (self, **kwds)
self.setDaemon(1)
self.SLEEP_TIME = 3 ## TODO - make more configurable
self.sleep_until = time.time() + self.SLEEP_TIME
self.last_file_time = time.time()
self.keep_running = True
self.debug = debug
self.warned = False
block.enable_sync_plot(True) # block must refer to a gardner/costas instance
self.blk_id = block.unique_id()
self.gnuplot = wrap_gp(sps = _def_sps)
self.start()
def run(self):
while self.keep_running == True:
curr_time = time.time()
if curr_time < self.sleep_until:
time.sleep(1.0)
if self.keep_running == False:
break
else:
self.sleep_until = time.time() + self.SLEEP_TIME
self.check_update()
def read_raw_file(self, fn):
s = open(fn, 'rb').read()
s_msg = ensure_str(s)
p = s_msg.find('\n')
if p < 1 or p > 24:
return None # error
hdrline = s_msg[:p]
rest = s[p+1:]
params = hdrline.split()
params = [int(p) for p in params] #idx, p1p2, sps, error
idx = params[0]
p1p2 = params[1]
sps = params[2]
error_amt = params[3]
self.gnuplot.set_sps(sps)
if error_amt != 0:
self.set_title("Tuning Error %d" % error_amt)
else:
self.set_title("")
samples = np.frombuffer(rest, dtype=np.complex64)
samples2 = np.concatenate((samples[idx:], samples[:idx]))
needed = sps * 25 if p1p2 == 1 else sps * 21
if len(samples2) < needed:
if not self.warned:
self.warned = True
sys.stderr.write('read_raw_file: insufficient samples %d, needed %d\n' % (needed, len(samples2)))
elif len(samples2) > needed:
trim = len(samples2) - needed
samples2 = samples2[trim:]
return samples2 # return trimmed buf in np.complex64 format
def check_update(self):
patt = 'sample-%d*.dat' % (self.blk_id)
names = glob.glob(patt)
if len(names) < 1: # no files to work with
return
d = {n: os.stat(n).st_mtime for n in names}
ds = sorted(d.items(), key=lambda x:x[1], reverse = True)[0]
if ds[1] <= self.last_file_time:
return
self.last_file_time = ds[1]
dat = self.read_raw_file(ds[0])
self.gnuplot.plot(dat, len(dat), mode='sync')
def kill(self):
self.keep_running = False
def set_title(self, title):
self.gnuplot.set_title(title)
def kill(self):
self.gnuplot.kill()
class symbol_sink_f(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="symbol_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
def work(self, input_items, output_items):
in0 = input_items[0]
self.gnuplot.plot(in0, 2400, mode='symbol')
return len(input_items[0])
def set_title(self, title):
self.gnuplot.set_title(title)
def kill(self):
self.gnuplot.kill()
class float_sink_f(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="float_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
def work(self, input_items, output_items):
in0 = input_items[0]
self.gnuplot.plot(in0, 2000, mode='float')
return len(input_items[0])
def set_title(self, title):
self.gnuplot.set_title(title)
def kill(self):
self.gnuplot.kill()
class correlation_sink_f(gr.sync_block):
"""
"""
def __init__(self, sps=_def_sps, debug = _def_debug):
gr.sync_block.__init__(self,
name="plot_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.sps = sps
self.gnuplot = wrap_gp()
self.fs = []
self.cbuf = np.array([])
self.ignore = 0
self.pktlen = 1024
def set_length(self, l):
self.pktlen = l
def set_title(self, title):
self.gnuplot.set_title(title)
def set_signature(self, fs):
self.fs = []
for s in fs:
for i in range(self.sps):
self.fs.append(s)
self.fs.reverse() # reverse order for np.convolve
self.fs = np.array(self.fs)
def work(self, input_items, output_items):
if len(self.cbuf) == 0 and self.ignore > 0:
self.ignore -= len(input_items[0])
if self.ignore < 0:
self.ignore = 0
return len(input_items[0])
if len(self.fs) == 0:
return len(input_items[0])
in0 = input_items[0]
self.cbuf = np.append(self.cbuf, in0)
if len(self.cbuf) < self.pktlen:
return len(input_items[0])
result = np.convolve(self.cbuf[:self.pktlen], self.fs)
hi = np.max(np.abs(result))
if hi != 0:
result = result / hi
self.cbuf = []
self.ignore = 3000 * self.sps
self.gnuplot.plot(result, len(result), mode='correlation')
return len(input_items[0])
def kill(self):
self.gnuplot.kill()
def setup_correlation(sps, title, connect_bb):
CFG_FILE = 'correlation.json'
if not os.access(CFG_FILE, os.R_OK):
sys.stderr.write('correlation plot ignored, missing config file %s\n' % CFG_FILE)
return []
ccfg = json.loads(open(CFG_FILE).read())
sinks = []
for cfg in ccfg:
sink = correlation_sink_f(sps=sps)
sink.set_title('%s %s' % (title, cfg['name']))
l = cfg['length'] * sps * 4
LENGTH_LIMIT = 10000
if l > LENGTH_LIMIT:
l = LENGTH_LIMIT
sink.set_length(l)
sink.set_signature(cfg['fs'])
connect_bb('baseband_amp', sink)
sinks.append(sink)
return sinks

View File

@ -0,0 +1,350 @@
% P25 H-CPM Demodulator (C) Copyright 2022 Max H. Parke KA1RBI
% % Experimental H-CPM Demodulator - Release 0 %
%
% This file is part of OP25
%
% OP25 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, or (at your option)
% any later version.
%
% OP25 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 OP25; see the file COPYING. If not, write to the Free
% Software Foundation, Inc., 51 Franklin Street, Boston, MA
% 02110-1301, USA.
%
%
%
% Accepts input IF file in GR binary complex64 format;
% file must be at sample rate 24,000 samples/sec.
%
% this implementation has several simplifications, shortcuts,
% and pitfalls, some of which are:
%
% * input sample must be clean and error free, there is
% currently no tree search (viterbi) stage - decode errors
% that should be recoverable are not autocorrected
% * since the first and last one-quarter of the partial-response
% period (in L=4) appear to contribute a negligible delta-phase,
% this correlator ignores these intervals, using only the inner-
% most two periods - accordingly, the value L=2 is assumed
% * the code is too slow to demod in real time and there are no claims
% to efficiency
% * at each symbol period the received waveform vector is derotated.
% this reduces the number of rows in the waveform matrix by a factor of
% six (6); instead of derotating the samples, a correct correlator would
% include these (approx. 80) additional rows
%
pkg load signal;
global WI0 = [
1.000000, 0.999827, 0.998111, 0.963512, 0.819784, 0.500000,
1.000000, 0.958473, 0.800512, 0.483695, 0.022053, -0.475169,
1.000000, 0.978152, 0.913558, 0.808999, 0.669116, 0.500000,
1.000000, 0.966921, 0.819689, 0.504702, 0.047650, -0.424375,
1.000000, 0.984174, 0.926374, 0.822966, 0.687929, 0.548428,
1.000000, 0.918644, 0.736098, 0.572238, 0.506066, 0.548428,
1.000000, 0.995165, 0.986853, 0.986153, 0.994884, 0.999596,
1.000000, 0.947207, 0.867963, 0.865831, 0.946296, 0.999596,
1.000000, 0.869202, 0.540278, 0.146957, -0.204506, -0.475169,
1.000000, 0.884236, 0.567518, 0.170815, -0.179370, -0.424375,
1.000000, 0.827032, 0.340050, -0.286034, -0.793734, -0.998382,
1.000000, 0.905842, 0.713560, 0.552256, 0.483811, 0.500000,
1.000000, 0.936720, 0.851252, 0.853489, 0.937706, 0.999596,
1.000000, 0.998757, 0.999587, 0.969698, 0.834181, 0.548428,
1.000000, 0.844204, 0.370632, -0.262797, -0.777895, -0.993535,
1.000000, 0.991608, 0.981038, 0.981858, 0.991970, 0.999596,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
0.99179 0.98062998 0.95472002 0.92157 0.89235997 0.87256002];
global WQ0 = [
0.000000, -0.018594, 0.061430, 0.267665, 0.572673, 0.866026,
0.000000, 0.285183, 0.599317, 0.875237, 0.999757, 0.879895,
0.000000, 0.207893, 0.406709, 0.587810, 0.743158, 0.866026,
0.000000, -0.255075, -0.572808, -0.863294, -0.998864, -0.905486,
0.000000, -0.177207, -0.376605, -0.568091, -0.725778, -0.836198,
0.000000, -0.395087, -0.676875, -0.820088, -0.862495, -0.836198,
0.000000, -0.098212, -0.161618, -0.165838, -0.101028, 0.028438,
0.000000, -0.320622, -0.496628, -0.500337, -0.323300, 0.028438,
0.000000, 0.494458, 0.841486, 0.989143, 0.978865, 0.879895,
0.000000, -0.467040, -0.823361, -0.985303, -0.983782, -0.905486,
0.000000, 0.562154, 0.940407, 0.958220, 0.608265, 0.056855,
0.000000, 0.423616, 0.700594, 0.833674, 0.875173, 0.866025,
0.000000, 0.350080, 0.524757, 0.521112, 0.347429, 0.028438,
0.000000, 0.049845, -0.028745, -0.244306, -0.551491, -0.836198,
0.000000, -0.536021, -0.928780, -0.964851, -0.628394, -0.113525,
0.000000, 0.129280, 0.193816, 0.189618, 0.126474, 0.028438,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0. 0.13601001 0.26787999 0.37345001 0.44356 0.48087999];
global S_A = zeros(18,"int");
global S_B = zeros(18,"int");
S_A(1) = -1;
S_B(1) = 3;
S_A(2) = 1;
S_B(2) = 3;
S_A(3) = 1;
S_B(3) = 1;
S_A(4) = -1;
S_B(4) = -3;
S_A(5) = -1;
S_B(5) = -1;
S_A(6) = -3;
S_B(6) = 1;
S_A(7) = -1;
S_B(7) = 1;
S_A(8) = -3;
S_B(8) = 3;
S_A(9) = 3;
S_B(9) = 1;
S_A(10) = -3;
S_B(10) = -1;
S_A(11) = 3;
S_B(11) = 3;
S_A(12) = 3;
S_B(12) = -1;
S_A(13) = 3;
S_B(13) = -3;
S_A(14) = 1;
S_B(14) = -3;
S_A(15) = -3;
S_B(15) = -3;
S_A(16) = 1;
S_B(16) = -1;
S_A(17) = 0;
S_B(17) = 0;
S_A(18) = 1;
S_B(18) = 1;
global L = 4; %sps
global M = 10 % interp factor
global LM = L*M;
global Q = 8; % decim amount
global NEWSPS = 5; % after decimation
global K=360.0 / (2 * pi); % radians -> degrees
global k = 6.0 / (2 * pi);
% change name of input data file
fname = 'if-24000-IQ.dat';
function samples = load_text(fname)
fid = fopen(fname, 'r');
nn = 0;
while 1
nn = nn+1;
s = fgetl(fid);
if s == -1
break;
endif
res = sscanf(s, "%f\t%f");
samples(nn) = res(1) + 1j*res(2);
endwhile
g = max(abs(samples));
samples = samples / g;
endfunction
function amt_left = process_dat(dat)
global L
iq = dat(1:2:end-1) .+ 1j * dat(2:2:end);
a = abs(iq);
thresh = max(a) / 2.0;
msk = a > thresh;
m1 = msk(2:end) - msk(1:end-1);
f = find(m1);
m1f = m1(f)(end);
lens = f(2:end) - f(1:end-1);
l1 = (lens/L > 170 & lens/L < 180) | (lens/L > 350 & lens/L < 360);
l2 = m1(f) == 1;
l2 = l2(1:end-1);
found = find(l1 & l2);
if(length(found)) < 1
amt_left = 0;
return;
endif
for n = 1:length(found)
valid = found(n);
start1 = f(valid);
len1 = lens(valid);
demod_frag(iq(start1:start1+len1));
end
validn = found(end);
startn = f(validn);
lenn = lens(validn);
datlen=length(dat);
amt_left = length(dat) - (startn + lenn);
if (amt_left < 0)
amt_left = 0;
endif
endfunction
function process_file(filename)
global L
bufsize = L * 180 * 12;
fid = fopen(filename, 'r');
save = [];
while 1
[dat, l] = fread(fid, bufsize, 'float32', 0);
if l < 1
break
endif
savel=size(save);
datl=size(dat);
concat = [save.' dat.'].';
amt_left = process_dat(concat);
if (amt_left > 0)
save = concat(end-amt_left:end);
else
save = [];
endif
endwhile
endfunction
function large_frag(msg)
msgl = length(msg);
padsz = 360 - msgl;
lenh = round(length(msg) / 2);
fmsg1 = frame_msg(msg(1:lenh),1);
decode_msg(fmsg1);
fmsg2 = frame_msg(msg(lenh:end),2);
decode_msg(fmsg2);
endfunction
function msg=demod_frag(frag)
global L
global NEWSPS
global M
amt_trim = mod(length(frag), L);
frag = frag(1:end-amt_trim);
g = max(abs(frag));
frag = frag / g;
nsyms = length(frag) / L;
intrp0 = interp(frag, M);
resampq=timing_sync(intrp0);
nsyms = length(resampq) / NEWSPS;
resamp1=frequency_sync(resampq);
msg=correlation(resamp1);
if length(msg) > 270
large_frag(msg);
else
fmsg=frame_msg(msg,1);
decode_msg(fmsg);
lfmsg=length(fmsg);
endif
endfunction
function decode_msg(msg)
if length(msg) != 160
return
endif
DMAP = [3 2 0 1];
duid = msg([37,74,123,160]) + 3;
duid = (duid / 2) + 1;
dibits = DMAP(duid);
duidx = dibits(1) * 64 + dibits(2) * 16 + dibits(3) * 4 + dibits(4);
printf ('hex %x\n' , duidx);
endfunction
function resampq=timing_sync(intrp0)
global LM
global NEWSPS
global Q
nsyms = length(intrp0) / LM;
fmd = angle(intrp0(2:end) .* conj(intrp0(1:end-1)));
fmx = mod(LM*6* ([fmd' 0] / (2*pi) + 0.5) + 0.5, 1);
matx= reshape(fmx, LM, nsyms );
res = std(matx');
[m, amin] = min(res);
amin = amin + 0 + (LM/2);
if amin > LM
amin = amin - LM;
endif
resampq = intrp0(amin:Q:end); # decim by Q
amt_trim = mod(length(resampq), NEWSPS);
resampq = resampq(1:end-amt_trim);
endfunction
function resamp1=frequency_sync(resampq)
global NEWSPS
global k
F = 0;
sz1 = length(resampq);
for iter = 1:4
osc = [0:sz1-1] * (F / 30000);
osc = exp(j*2*pi*osc);
resamp1 = resampq .* osc.';
row = resamp1(1:NEWSPS:end);
rowz = mod(k*angle(row)+0.5, 1);
rowz = unwrap((rowz-0.5) * 2*pi);
meanr = mean(rowz(5:15)) - mean(rowz(end-15:end-5));
F = F + meanr;
end
nsyms = length(resamp1) / NEWSPS;
rfm = angle(resamp1(2:end) .* conj(resamp1(1:end-1)));
afm = angle(resamp1);
endfunction
function eye_plot(dat, sps)
hold on
for nn = 1:sps:length(dat)-sps*2
sl = dat(nn:nn+sps);
plot(sl);
end
hold off
pause
endfunction
function msg=correlation(resamp1)
global NEWSPS
global WI0
global WQ0
global S_A
global S_B
global K
nsyms = length(resamp1) / NEWSPS;
for n= 1 : NEWSPS : length(resamp1) - NEWSPS
stepn=(n-1)/NEWSPS;
idx = ((n-1) / NEWSPS) + 1;
sl=resamp1(n:n+NEWSPS);
sl = sl * conj(sl(1));
sl_i = real(sl);
sl_q = imag(sl);
corr2 = sl_i' * WI0' + sl_q' * WQ0';
[m,am] = max(corr2);
msga(idx) = S_A(am);
msgb(idx) = S_B(am);
end
msgok=msga(2:end) == msgb(1:end-1);
ok=sum(msgok) / length(msga);
msg=msgb;
endfunction
function msg=frame_msg(imsg, fcode)
if (fcode == 1)
pilots = [1 -1 -1 1];
else
pilots = [-3 -3 -1 1];
endif
fml = length(imsg);
msg = [];
excess=length(imsg) - 160;
for n=1:excess
if n+163 > length(imsg)
break
endif
slx = [imsg(n:n+1), imsg(n+162:n+163)];
if sum(slx == pilots) == 4
msg = imsg(n+2:n+161);
return;
endif
end
return;
endfunction
process_file(fname);

View File

@ -0,0 +1,589 @@
#!/usr/bin/env python3
# Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
#
# This file is part of OP25
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
import sys
import os
import time
import re
import json
import socket
import traceback
import threading
import glob
import subprocess
import zmq
import op25
from gnuradio import gr
from waitress.server import create_server
from optparse import OptionParser
from multi_rx import byteify
from tsvfile import load_tsv, make_config
import logging
logging.basicConfig()
my_input_q = None
my_output_q = None
my_recv_q = None
my_port = None
my_backend = None
CFG_DIR = '../www/config/'
TSV_DIR = './'
"""
fake http and ajax server module
TODO: make less fake
"""
def ensure_str(s): # for python 2/3
if isinstance(s[0], str):
return s
ns = ''
for i in range(len(s)):
ns += chr(s[i])
return ns
class event_iterator:
def __iter__(self):
return self
def __next__(self):
_jslog_file = None # set to str(filename) to enable json log
msgs = []
while True:
msg = my_input_q.delete_head()
assert msg.type() == -4
d = json.loads(msg.to_string())
msgs.append(d)
if my_input_q.empty_p():
break
js = json.dumps(msgs)
# TODO: json.loads followed by dumps is redundant -
# can this be optimized?
s = 'data:%s\r\n\r\n' % (js)
if _jslog_file:
t = json.dumps(msgs, indent=4, separators=[',',':'], sort_keys=True)
with open(_jslog_file, 'a') as logfd:
logfd.write('%s\n' % t)
if sys.version[0] != '2':
if isinstance(s, str):
s = s.encode()
return s
next = __next__ # for python2
def static_file(environ, start_response):
content_types = {'tsv': 'text/tab-separated-values', 'json': 'application/json', 'png': 'image/png', 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'gif': 'image/gif', 'css': 'text/css', 'js': 'application/javascript', 'html': 'text/html', 'ico': 'image/vnd.microsoft.icon'}
img_types = 'png jpg jpeg gif ico'.split()
data_types = 'tsv txt json db'.split()
if environ['PATH_INFO'] == '/':
filename = 'index.html'
else:
filename = re.sub(r'[^a-zA-Z0-9_.\-/]', '', environ['PATH_INFO'])
suf = filename.split('.')[-1]
pathname = '../www/www-static'
if suf in img_types:
pathname = '../www/images'
elif suf in data_types:
pathname = TSV_DIR
pathname = '%s/%s' % (pathname, filename)
if suf not in content_types.keys() or '..' in filename or not os.access(pathname, os.R_OK):
sys.stderr.write('404 %s\n' % pathname)
status = '404 NOT FOUND - PATHNAME: %s FILENAME: %s CWD: %s' % (pathname, filename, os. getcwd())
content_type = 'text/plain'
output = status
else:
output = open(pathname, 'rb').read()
content_type = content_types[suf]
status = '200 OK'
return status, content_type, output
def valid_tsv(filename):
if not os.access(filename, os.R_OK):
return False
line = open(filename).readline()
for word in 'Sysname Offset NAC Modulation TGID Whitelist Blacklist'.split():
if word not in line:
return False
return True
def tsv_config(filename):
DEFAULT_CFG = '../www/config/default.json'
filename = '%s%s' % (TSV_DIR, filename)
filename = filename.replace('[TSV]', '.tsv')
if not valid_tsv(filename):
return None
cfg = make_config(load_tsv(filename))
default_cfg = json.loads(open(DEFAULT_CFG).read())
result = default_cfg
channels = [ {'active': True,
'blacklist': cfg[nac]['blacklist'],
'whitelist': cfg[nac]['whitelist'],
'cclist': cfg[nac]['cclist'],
'demod_type': 'cqpsk',
'destination': 'udp://127.0.0.1:23456',
'filter_type': 'rc',
'frequency': 500000000,
'if_rate': 24000,
'nac': nac,
'name': cfg[nac]['sysname'],
'phase2_tdma': False,
'plot': "",
'tgids': cfg[nac]['tgid_map'],
'trunked': True
}
for nac in cfg.keys() ]
result['channels'] = channels
return {'json_type':'config_data', 'data': result}
def do_request(d):
global my_backend
if d['command'].startswith('rx-'):
msg = gr.message().make_from_string(json.dumps(d), -2, 0, 0)
if not my_backend.input_q.full_p():
my_backend.input_q.insert_tail(msg)
return None
elif d['command'] == 'config-load':
if '[TSV]' in d['data']:
return tsv_config(d['data'])
filename = '%s%s.json' % (CFG_DIR, d['data'])
if not os.access(filename, os.R_OK):
return None
js_msg = json.loads(open(filename).read())
return {'json_type':'config_data', 'data': js_msg}
elif d['command'] == 'config-list':
files = glob.glob('%s*.json' % CFG_DIR)
files = [x.replace('.json', '') for x in files]
files = [x.replace(CFG_DIR, '') for x in files]
if d['data'] == 'tsv':
tsvfiles = glob.glob('%s*.tsv' % TSV_DIR)
tsvfiles = [x for x in tsvfiles if valid_tsv(x)]
tsvfiles = [x.replace('.tsv', '[TSV]') for x in tsvfiles]
tsvfiles = [x.replace(TSV_DIR, '') for x in tsvfiles]
files += tsvfiles
return {'json_type':'config_list', 'data': files}
elif d['command'] == 'config-save':
name = d['data']['name']
if '..' in name or '.json' in name or '/' in name:
return None
filename = '%s%s.json' % (CFG_DIR, d['data']['name'])
open(filename, 'w').write(json.dumps(d['data']['value'], indent=4, separators=[',',':'], sort_keys=True))
return None
elif d['command'] == 'config-savesettings':
filename = 'ui-settings.json'
open(filename, 'w').write(d['data'])
sys.stderr.write('saved UI settings to %s\n' % filename)
return None
elif d['command'] == 'config-tsvsave':
filename = d['file']
ok = True
if filename.lower().endswith('tsv'):
ok = True
elif filename.lower().endswith('json'):
ok = True
else:
ok = False
if filename.startswith('.'):
ok = False
if '/' in filename:
ok = False
if '..' in filename:
ok = False
if not ok:
sys.stderr.write('cfg-tsvsave: invalid filename %s\n' % filename)
return None
open(filename, 'w').write(d['data'])
sys.stderr.write('saved UI settings to %s\n' % filename)
return None
def post_req(environ, start_response, postdata):
global my_input_q, my_output_q, my_recv_q, my_port
resp_msg = []
data = []
try:
data = json.loads(postdata)
except:
sys.stderr.write('post_req: error processing input: %s:\n' % (postdata))
traceback.print_exc(limit=None, file=sys.stderr)
sys.stderr.write('*** end traceback ***\n')
for d in data:
if type(d) is str:
sys.stderr.write('%f possible json sequence error: len %d type %s value %s\n' % (time.time(), len(d), type(d), d))
continue
elif type(d) is not dict:
sys.stderr.write('%f possible json sequence error: type %s value %s\n' % (time.time(), type(d), d))
continue
if d['command'].startswith('config-') or d['command'].startswith('rx-'):
resp = do_request(d)
if resp:
resp_msg.append(resp)
continue
if d['command'].startswith('settings-'):
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
else:
msg = gr.message().make_from_string(str(d['command']), -2, d['data'], 0)
if my_output_q.full_p():
my_output_q.delete_head_nowait() # ignores result
if not my_output_q.full_p():
my_output_q.insert_tail(msg)
time.sleep(0.2)
status = '200 OK'
content_type = 'application/json'
output = json.dumps(resp_msg)
return status, content_type, output
def http_request(environ, start_response):
if environ['REQUEST_METHOD'] == 'GET' and '/stream' in environ['PATH_INFO']:
status = '200 OK'
content_type = 'text/event-stream'
response_headers = [('Content-type', content_type),
('Access-Control-Allow-Origin', '*')]
start_response(status, response_headers)
return iter(event_iterator())
elif environ['REQUEST_METHOD'] == 'GET':
status, content_type, output = static_file(environ, start_response)
elif environ['REQUEST_METHOD'] == 'POST':
postdata = environ['wsgi.input'].read()
status, content_type, output = post_req(environ, start_response, postdata)
else:
status = '200 OK'
content_type = 'text/plain'
output = status
sys.stderr.write('http_request: unexpected input %s\n' % environ['PATH_INFO'])
response_headers = [('Content-type', content_type),
('Access-Control-Allow-Origin', '*'),
('Content-Length', str(len(output)))]
start_response(status, response_headers)
if sys.version[0] != '2':
if isinstance(output, str):
output = output.encode()
return [output]
def application(environ, start_response):
failed = False
try:
result = http_request(environ, start_response)
except:
failed = True
sys.stderr.write('application: request failed:\n%s\n' % traceback.format_exc())
if failed:
status = '500 Internal Server Error'
response_headers = [ ('Access-Control-Allow-Origin', '*') ]
start_response(status, response_headers)
output = status
if sys.version[0] != '2':
if isinstance(output, str):
output = output.encode()
return [output]
return result
def process_qmsg(msg):
if my_recv_q.full_p():
my_recv_q.delete_head_nowait() # ignores result
if my_recv_q.full_p():
return
my_recv_q.insert_tail(msg)
class http_server(object):
def __init__(self, input_q, output_q, endpoint, **kwds):
global my_input_q, my_output_q, my_recv_q, my_port
host, port = endpoint.split(':')
if my_port is not None:
raise AssertionError('this server is already active on port %s' % my_port)
my_input_q = input_q
my_output_q = output_q
my_port = int(port)
my_recv_q = gr.msg_queue(10)
SEND_BYTES = 1024
NTHREADS = 10 # TODO: make #threads a function of #plots ?
self.server = create_server(application, host=host, port=my_port, send_bytes=SEND_BYTES, expose_tracebacks=True, threads=NTHREADS)
def run(self):
self.server.run()
class queue_watcher(threading.Thread):
def __init__(self, msgq, callback, **kwds):
threading.Thread.__init__ (self, **kwds)
self.setDaemon(1)
self.msgq = msgq
self.callback = callback
self.keep_running = True
self.start()
def run(self):
while(self.keep_running):
msg = self.msgq.delete_head()
self.callback(msg)
class Backend(threading.Thread):
def __init__(self, options, input_q, output_q, init_config=None, **kwds):
threading.Thread.__init__ (self, **kwds)
self.setDaemon(1)
self.keep_running = True
self.rx_options = None
self.input_q = input_q
self.output_q = output_q
self.verbosity = options.verbosity
self.zmq_context = zmq.Context()
self.zmq_port = options.zmq_port
self.zmq_sub = self.zmq_context.socket(zmq.SUB)
self.zmq_sub.connect('tcp://localhost:%d' % self.zmq_port)
self.zmq_sub.setsockopt_string(zmq.SUBSCRIBE, '')
self.zmq_pub = self.zmq_context.socket(zmq.PUB)
self.zmq_pub.sndhwm = 5
self.zmq_pub.bind('tcp://*:%d' % (self.zmq_port+1))
self.start()
self.subproc = None
self.msg = None
self.q_watcher = queue_watcher(self.input_q, self.process_msg)
if init_config:
d = {'command': 'rx-start', 'data': init_config}
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
self.input_q.insert_tail(msg)
def publish(self, msg):
t = msg.type()
s = msg.to_string()
a = msg.arg1()
s = ensure_str(s)
self.zmq_pub.send_string(json.dumps({'command': s, 'data': a, 'msgtype': t}))
def check_subproc(self): # return True if subprocess is active
if not self.subproc:
return False
rc = self.subproc.poll()
if rc is None:
return True
else:
self.subproc.wait()
self.subproc = None
return False
def process_msg(self, msg):
def make_command(options, config_file):
py_exe = 'python'
if sys.version[0] == '3':
py_exe = 'python3'
trunked_ct = [True for x in options._js_config['channels'] if x['trunked']]
total_ct = [True for x in options._js_config['channels']]
if trunked_ct and len(trunked_ct) != len(total_ct):
self.msg = 'no suitable backend found for this configuration'
return None
if not trunked_ct:
self.backend = '%s/%s' % (os.getcwd(), 'multi_rx.py')
opts = [py_exe, self.backend]
filename = '%s%s.json' % (CFG_DIR, config_file)
opts.append('--config-file')
opts.append(filename)
return opts
# TODO: this probably should be external and/or configurable
# these options must match up one for one with the rx.py cli opts
types = {'costas-alpha': 'float',
'trunk-conf-file': 'str',
'demod-type': 'str',
'logfile-workers': 'int',
'decim-amt': 'int',
'wireshark-host': 'str',
'gain-mu': 'float',
'phase2-tdma': 'bool',
'seek': 'int',
'ifile': 'str',
'pause': 'bool',
'antenna': 'str',
'calibration': 'float',
'fine-tune': 'float',
'raw-symbols': 'str',
'audio-output': 'str',
'vocoder': 'bool',
'input': 'str',
'wireshark': 'bool',
'gains': 'str',
'args': 'str',
'sample-rate': 'int',
'terminal-type': 'str',
'gain': 'float',
'excess-bw': 'float',
'offset': 'float',
'audio-input': 'str',
'audio': 'bool',
'plot-mode': 'str',
'audio-if': 'bool',
'tone-detect': 'bool',
'frequency': 'int',
'freq-corr': 'float',
'hamlib-model': 'int',
'udp-player': 'bool',
'verbosity': 'int',
'audio-gain': 'float',
'freq-error-tracking': 'bool',
'nocrypt': 'bool',
'wireshark-port': 'int'
}
self.backend = '%s/%s' % (os.getcwd(), 'rx.py')
opts = [py_exe, self.backend]
for k in [ x for x in dir(options) if not x.startswith('_') ]:
kw = k.replace('_', '-')
val = getattr(options, k)
if kw not in types.keys():
self.msg = 'make_command: unknown option: %s %s type %s' % (k, val, type(val))
return None
elif types[kw] == 'str':
if val:
opts.append('--%s' % kw)
opts.append('%s' % (val))
elif types[kw] == 'float':
opts.append('--%s' % kw)
if val:
opts.append('%f' % (val))
else:
opts.append('%f' % (0))
elif types[kw] == 'int':
opts.append('--%s' % kw)
if val:
opts.append('%d' % (val))
else:
opts.append('%d' % (0))
elif types[kw] == 'bool':
if val:
opts.append('--%s' % kw)
else:
self.msg = 'make_command: unknown2 option: %s %s type %s' % (k, val, type(val))
return None
return opts
msg = json.loads(msg.to_string())
if msg['command'] == 'rx-start':
if self.check_subproc():
self.msg = 'start command failed: subprocess pid %d already active' % self.subproc.pid
return
options = rx_options(msg['data'])
if getattr(options, '_js_config', None) is None:
self.msg = 'start command failed: rx_options: unable to initialize config=%s' % (msg['data'])
return
options.verbosity = self.verbosity
options.terminal_type = 'zmq:tcp:%d' % (self.zmq_port)
cmd = make_command(options, msg['data'])
sys.stderr.write('executing %s\n' % (' '.join(cmd)))
if cmd:
self.subproc = subprocess.Popen(cmd)
elif msg['command'] == 'rx-stop':
if not self.check_subproc():
self.msg = 'stop command failed: subprocess not active'
return
if msg['data'] == 'kill':
self.subproc.kill()
else:
self.subproc.terminate()
elif msg['command'] == 'rx-state':
d = {}
if self.check_subproc():
d['rx-state'] = 'subprocess pid %d active' % self.subproc.pid
else:
d['rx-state'] = 'subprocess not active, last msg: %s' % self.msg
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
if not self.output_q.full_p():
self.output_q.insert_tail(msg)
def run(self):
while self.keep_running:
js = self.zmq_sub.recv()
if not self.keep_running:
break
js = ensure_str(js)
msg = gr.message().make_from_string(js, -4, 0, 0)
if not self.output_q.full_p():
self.output_q.insert_tail(msg)
class rx_options(object):
def __init__(self, name):
def map_name(k):
return k.replace('-', '_')
filename = '%s%s.json' % (CFG_DIR, name)
if not os.access(filename, os.R_OK):
sys.stderr.write('unable to access config file %s\n' % (filename))
return
config = byteify(json.loads(open(filename).read()))
dev = [x for x in config['devices'] if x['active']][0]
if not dev:
return
chan = [x for x in config['channels'] if x['active']][0]
if not chan:
return
options = object()
for k in config['backend-rx'].keys():
setattr(self, map_name(k), config['backend-rx'][k])
for k in 'args frequency gains offset'.split():
setattr(self, k, dev[k])
self.demod_type = chan['demod_type']
self.freq_corr = dev['ppm']
self.sample_rate = dev['rate']
self.plot_mode = chan['plot']
self.phase2_tdma = chan['phase2_tdma']
self.trunk_conf_file = filename
self._js_config = config
def http_main():
global my_backend
# command line argument parsing
parser = OptionParser()
parser.add_option("-c", "--config", type="string", default=None, help="config json name, without prefix/suffix")
parser.add_option("-e", "--endpoint", type="string", default="127.0.0.1:8080", help="address:port to listen on (use addr 0.0.0.0 to enable external clients)")
parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level")
parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup")
parser.add_option("-z", "--zmq-port", type="int", default=25000, help="backend sub port")
(options, args) = parser.parse_args()
# wait for gdb
if options.pause:
print ('Ready for GDB to attach (pid = %d)' % (os.getpid(),))
raw_input("Press 'Enter' to continue...")
input_q = gr.msg_queue(20)
output_q = gr.msg_queue(20)
backend_input_q = gr.msg_queue(20)
backend_output_q = gr.msg_queue(20)
my_backend = Backend(options, backend_input_q, backend_output_q, init_config=options.config)
server = http_server(input_q, output_q, options.endpoint)
q_watcher = queue_watcher(output_q, lambda msg : my_backend.publish(msg))
backend_q_watcher = queue_watcher(backend_output_q, lambda msg : process_qmsg(msg))
server.run()
if __name__ == '__main__':
http_main()

View File

@ -0,0 +1,31 @@
#! /bin/sh
PIP3=`which pip3`
USERDIR=~/.local/bin
sudo apt-get install python3-pip
# PIP3=`which pip3`
PIP3=/usr/bin/pip3
# # # # # # un-comment the following two lines for ubuntu 16.04 # # # # # #
#pip3 install --user pip==10.0.1
#PIP3=$USERDIR/pip3
echo PIP3 now set to $PIP3
# # # $PIP3 --version # # # generates errors -- (?)
$PIP3 install --user sqlalchemy
$PIP3 install --user flask
$PIP3 install --user datatables
$PIP3 install --user flask-sqlalchemy
cd
git clone https://github.com/Pegase745/sqlalchemy-datatables.git
cd sqlalchemy-datatables
$PIP3 install --user .
cd
echo the following line must be added to your .bashrc
echo "export PATH=$USERDIR:\$PATH"

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python
# Copyright 2020 Graham Norbury
#
# This file is part of OP25
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
# Modify TS_FORMAT to control logger timestamp format
# 0 = legacy epoch seconds
# 1 = formatted mm/dd/yy hh:mm:ss.usec
TS_FORMAT = 1
import time
class log_ts(object):
@staticmethod
def get(supplied_ts=None):
if supplied_ts is None:
ts = time.time()
else:
ts = supplied_ts
if TS_FORMAT == 0:
formatted_ts = "{:.6f}".format(ts)
else:
formatted_ts = "{:s}{:s}".format(time.strftime("%m/%d/%y %H:%M:%S",time.localtime(ts)),"{:.6f}".format(ts - int(ts)).lstrip("0"))
return formatted_ts

View File

@ -0,0 +1,864 @@
#!/usr/bin/env python3
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
#
# This file is part of OP25
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
import os
import sys
import threading
import time
import json
import select
import traceback
import osmosdr
from gnuradio import audio, eng_notation, gr, filter, blocks, fft, analog, digital
from gnuradio.eng_option import eng_option
from math import pi
from optparse import OptionParser
import trunking
import op25
import op25_repeater
import p25_demodulator
import p25_decoder
from sockaudio import audio_thread
from sql_dbi import sql_dbi
from gr_gnuplot import constellation_sink_c
from gr_gnuplot import fft_sink_c
from gr_gnuplot import mixer_sink_c
from gr_gnuplot import symbol_sink_f
from gr_gnuplot import eye_sink_f
from gr_gnuplot import setup_correlation
from gr_gnuplot import sync_plot
from gr_gnuplot import cpm_sink_c
from nxdn_trunking import cac_message
from terminal import op25_terminal
sys.path.append('tdma')
import lfsr
os.environ['IMBE'] = 'soft'
_def_symbol_rate = 4800
_def_interval = 3.0 # sec
_def_file_dir = '../www/images'
_def_audio_port = 23456 # udp port for audio thread
_def_audio_output = 'default' # output device name for audio thread
# The P25 receiver
#
def byteify(input): # thx so
if sys.version[0] != '2': # hack, must be a better way
return input
if isinstance(input, dict):
return {byteify(key): byteify(value)
for key, value in input.iteritems()}
elif isinstance(input, list):
return [byteify(element) for element in input]
elif isinstance(input, unicode):
return input.encode('utf-8')
else:
return input
class device(object):
def __init__(self, config, tb):
self.name = config['name']
self.sample_rate = config['rate']
self.args = config['args']
self.tunable = config['tunable']
self.tb = tb
self.frequency = 0
if config['args'].startswith('audio-if:'):
self.init_audio_if(config)
elif config['args'].startswith('audio:'):
self.init_audio(config)
elif config['args'].startswith('file:'):
self.init_file(config)
elif config['args'].startswith('udp:'):
self.init_udp(config)
else:
self.init_osmosdr(config)
def init_file(self, config):
filename = config['args'].replace('file:', '', 1)
src = blocks.file_source(gr.sizeof_gr_complex, filename, repeat = False)
throttle = blocks.throttle(gr.sizeof_gr_complex, config['rate'])
self.tb.connect(src, throttle)
self.src = throttle
self.frequency = config['frequency']
self.offset = config['offset']
def init_audio_if(self, config):
filename = config['args'].replace('audio-if:', '')
self.audio_source = audio.source(config['rate'], filename)
self.null_source = blocks.null_source (gr.sizeof_float)
self.audio_cvt = blocks.float_to_complex()
self.tb.connect(self.audio_source, (self.audio_cvt, 0))
self.tb.connect(self.null_source, (self.audio_cvt, 1))
self.src = self.audio_cvt
self.frequency = config['frequency']
self.offset = config['offset']
def init_audio(self, config):
filename = config['args'].replace('audio:', '')
if filename.startswith('file:'):
filename = filename.replace('file:', '')
repeat = False
s2f = blocks.short_to_float()
K = 1 / 32767.0
src = blocks.multiply_const_ff(K)
throttle = blocks.throttle(gr.sizeof_short, self.sample_rate) # may be redundant in stdin case ?
if filename == '-':
fd = 0 # stdin
fsrc = blocks.file_descriptor_source(gr.sizeof_short, fd, repeat)
else:
fsrc = blocks.file_source(gr.sizeof_short, filename, repeat)
self.tb.connect(fsrc, throttle, s2f, src)
else:
src = audio.source(self.sample_rate, filename)
gain = 1.0
if config['gains'].startswith('audio:'):
gain = float(config['gains'].replace('audio:', ''))
self.src = blocks.multiply_const_ff(gain)
self.tb.connect(src, self.src)
def init_udp(self, config):
hostinfo = config['args'].split(':')
hostname = hostinfo[1]
udp_port = int(hostinfo[2])
bufsize = 32000 # might try enlarging this if packet loss
self.src = blocks.udp_source(gr.sizeof_gr_complex, hostname, udp_port, payload_size = bufsize)
self.ppm = 0
self.frequency = config['frequency']
self.offset = 0
def init_osmosdr(self, config):
speeds = [250000, 1000000, 1024000, 1800000, 1920000, 2000000, 2048000, 2400000, 2560000]
sys.stderr.write('device: %s\n' % config)
if config['args'].startswith('rtl') and config['rate'] not in speeds:
sys.stderr.write('WARNING: requested sample rate %d for device %s may not\n' % (config['rate'], config['name']))
sys.stderr.write("be optimal. You may want to use one of the following rates\n")
sys.stderr.write('%s\n' % speeds)
self.src = osmosdr.source(config['args'])
for tup in config['gains'].split(','):
name, gain = tup.split(':')
self.src.set_gain(int(gain), name)
self.src.set_freq_corr(config['ppm'])
self.ppm = config['ppm']
self.src.set_sample_rate(config['rate'])
self.src.set_center_freq(config['frequency'])
self.frequency = config['frequency']
self.offset = config['offset']
def set_frequency(self, frequency):
if frequency == self.frequency:
return
if not self.tunable:
return
self.frequency = frequency
self.src.set_center_freq(frequency)
class channel(object):
def __init__(self, config, dev, verbosity, msgq = None, process_msg=None, msgq_id=-1, role=''):
sys.stderr.write('channel (dev %s): %s\n' % (dev.name, config))
self.device = dev
self.name = config['name']
self.symbol_rate = _def_symbol_rate
self.process_msg = process_msg
self.role = role
self.dev = ''
self.sysid = []
self.nac = []
if 'symbol_rate' in config.keys():
self.symbol_rate = config['symbol_rate']
self.config = config
self.verbosity = verbosity
self.frequency = config['frequency'] if self.device.args.startswith('audio-if') else 0
self.tdma_state = False
self.xor_cache = {}
self.tuning_error = 0
self.freq_correction = 0
self.error_band = 0
self.last_error_update = 0
self.last_set_freq_at = time.time()
self.warned_frequencies = {}
self.msgq_id = msgq_id
self.next_band_change = time.time()
self.audio_port = _def_audio_port
self.audio_output = _def_audio_output
self.audio_gain = 1.0
if 'audio_gain' in config:
self.audio_gain = float(config['audio_gain'])
if dev.args.startswith('audio:'):
self.demod = p25_demodulator.p25_demod_fb(
input_rate = dev.sample_rate,
filter_type = config['filter_type'],
if_rate = config['if_rate'],
symbol_rate = self.symbol_rate)
else:
self.demod = p25_demodulator.p25_demod_cb(
input_rate = dev.sample_rate,
demod_type = config['demod_type'],
filter_type = config['filter_type'],
excess_bw = config['excess_bw'],
relative_freq = dev.frequency + dev.offset - config['frequency'],
offset = dev.offset,
if_rate = config['if_rate'],
symbol_rate = self.symbol_rate,
use_old_decim = True if self.device.args.startswith('audio-if') else False)
if msgq is not None:
q = msgq
else:
q = gr.msg_queue(20)
if 'decode' in config.keys() and config['decode'].startswith('p25_decoder'):
num_ambe = 1
(proto, wireshark_host, udp_port) = config['destination'].split(':')
assert proto == 'udp'
wireshark_host = wireshark_host.replace('/', '')
udp_port = int(udp_port)
if role == 'vc':
self.audio_port = udp_port
if 'audio_output' in config.keys():
self.audio_output = config['audio_output']
self.decoder = p25_decoder.p25_decoder_sink_b(dest='audio', do_imbe=True, num_ambe=num_ambe, wireshark_host=wireshark_host, udp_port=udp_port, do_msgq = True, msgq=q, audio_output=self.audio_output, debug=verbosity, msgq_id=self.msgq_id)
else:
self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q, self.msgq_id)
if self.symbol_rate == 6000 and role == 'cc':
sps = config['if_rate'] // self.symbol_rate
self.demod.set_symbol_rate(self.symbol_rate) # this and the foll. call should be merged?
self.demod.clock.set_omega(float(sps))
self.demod.clock.set_tdma(True)
sys.stderr.write('initializing TDMA control channel %s channel ID %d\n' % (self.name, self.msgq_id))
if self.process_msg is not None and msgq is None:
self.q_watcher = du_queue_watcher(q, lambda msg: self.process_msg(msg, sender=self))
self.kill_sink = []
if 'blacklist' in config.keys():
for g in config['blacklist'].split(','):
self.decoder.insert_blacklist(int(g))
if 'whitelist' in config.keys():
for g in config['whitelist'].split(','):
self.decoder.insert_whitelist(int(g))
self.sinks = []
if 'plot' not in config.keys():
return
for plot in config['plot'].split(','):
if plot == 'datascope':
assert config['demod_type'] == 'fsk4' ## datascope plot requires fsk4 demod type
sink = eye_sink_f(sps=config['if_rate'] // self.symbol_rate)
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_bb('symbol_filter', sink)
self.kill_sink.append(sink)
elif plot.startswith('cpm'):
if self.symbol_rate != 6000: # fixed rate value for p25p2
sys.stderr.write('warning: symbol rate %d may be incorrect for CPM channel %s\n' % (self.symbol_rate, self.name))
sink = cpm_sink_c(sps=config['if_rate'] // self.symbol_rate, plot_mode=plot)
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_complex('if_out', sink)
self.kill_sink.append(sink)
elif plot == 'symbol':
sink = symbol_sink_f()
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_float(sink)
self.kill_sink.append(sink)
elif plot == 'fft':
i = len(self.sinks)
sink = fft_sink_c()
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_complex('src', self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'mixer':
if config['demod_type'] == 'cqpsk':
blk = 'mixer'
else:
blk = 'cutoff'
i = len(self.sinks)
sink = mixer_sink_c()
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_complex(blk, self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'constellation':
i = len(self.sinks)
assert config['demod_type'] == 'cqpsk' ## constellation plot requires cqpsk demod type
sink = constellation_sink_c()
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_complex('diffdec', self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'correlation':
assert config['demod_type'] == 'fsk4' ## correlation plot requires fsk4 demod type
assert config['symbol_rate'] == 4800 ## 4800 required for correlation plot
sps=config['if_rate'] // self.symbol_rate
sinks = setup_correlation(sps, self.name, self.demod.connect_bb)
self.kill_sink += sinks
self.sinks += sinks
elif plot == 'sync':
assert config['demod_type'] == 'cqpsk' ## sync plot requires cqpsk demod type
i = len(self.sinks)
sink = sync_plot(block = self.demod.clock)
sink.set_title(self.name)
self.sinks.append(sink)
self.kill_sink.append(self.sinks[i])
# does not issue self.connect()
else:
sys.stderr.write('unrecognized plot type %s\n' % plot)
return
def set_frequency(self, frequency):
assert frequency
if self.device.tunable:
self.device.set_frequency(frequency)
f = self.frequency if self.device.args.startswith('audio-if') else frequency
relative_freq = self.device.frequency + self.device.offset + self.tuning_error - f
if (not self.device.tunable) and (not self.device.args.startswith('audio-if')) and abs(relative_freq) > ((self.demod.input_rate / 2) - (self.demod.if1 / 2)):
if frequency not in self.warned_frequencies:
sys.stderr.write('warning: set frequency %f to non-tunable device %s rejected.\n' % (frequency / 1000000.0, self.device.name))
self.warned_frequencies[frequency] = 0
self.warned_frequencies[frequency] += 1
#print 'set_relative_frequency: error, relative frequency %d exceeds limit %d' % (relative_freq, self.demod.input_rate/2)
return False
self.demod.set_relative_frequency(relative_freq)
self.last_set_freq_at = time.time()
if not self.device.args.startswith('audio-if'):
self.frequency = frequency
def error_tracking(self, last_change_freq):
curr_time = time.time()
if self.config['demod_type'] == 'fsk4':
return None # todo: allow tracking in fsk4 demod
UPDATE_TIME = 3
if self.last_error_update + UPDATE_TIME > curr_time:
return None
self.last_error_update = time.time()
if not self.demod.is_muted():
band = self.demod.get_error_band()
freq_error = self.demod.get_freq_error()
if band and curr_time >= self.next_band_change:
self.next_band_change = curr_time + 20.0
self.error_band += band
sys.stderr.write('channel %d set error band %d\n' % (self.msgq_id, self.error_band))
self.freq_correction += freq_error * 0.15
self.freq_correction = int(self.freq_correction)
if self.freq_correction > 600:
self.freq_correction -= 1200
self.error_band += 1
elif self.freq_correction < -600:
self.freq_correction += 1200
self.error_band -= 1
self.error_band = min(self.error_band, 2)
self.error_band = max(self.error_band, -2)
self.tuning_error = int(self.error_band * 1200 + self.freq_correction)
e = 0
if last_change_freq > 0:
e = (self.tuning_error*1e6) / float(last_change_freq)
else:
e = 0
freq_error = 0
band = 0
### self.set_frequency(self.frequency) # adjust relative frequency with updated tuning_error
if self.verbosity >= 10:
sys.stderr.write('%f\terror_tracking\t%s\t%d\t%d\t%d\t%d\t%d\t%f\n' % (curr_time, self.name, self.msgq_id, freq_error, self.error_band, self.tuning_error, self.freq_correction, e))
d = {'time': time.time(), 'json_type': 'freq_error_tracking', 'name': self.name, 'device': self.device.name, 'freq_error': freq_error, 'band': band, 'error_band': self.error_band, 'tuning_error': self.tuning_error, 'freq_correction': self.freq_correction}
if self.frequency:
self.set_frequency(self.frequency)
return d
def configure_tdma(self, params):
set_tdma = False
if params['tdma'] is not None:
set_tdma = True
self.decoder.set_slotid(params['tdma'])
self.demod.clock.set_tdma(set_tdma)
if set_tdma == self.tdma_state:
return # already in desired state
self.tdma_state = set_tdma
if set_tdma:
hash = '%x%x%x' % (params['nac'], params['sysid'], params['wacn'])
if hash not in self.xor_cache:
self.xor_cache[hash] = lfsr.p25p2_lfsr(params['nac'], params['sysid'], params['wacn']).xor_chars
self.decoder.set_xormask(self.xor_cache[hash], hash)
self.decoder.set_nac(params['nac'])
rate = 6000
else:
rate = 4800
sps = self.config['if_rate'] / rate
self.demod.set_symbol_rate(rate) # this and the foll. call should be merged?
self.demod.clock.set_omega(float(sps))
class du_queue_watcher(threading.Thread):
def __init__(self, msgq, callback, **kwds):
threading.Thread.__init__ (self, **kwds)
self.setDaemon(1)
self.msgq = msgq
self.callback = callback
self.keep_running = True
self.start()
def run(self):
while(self.keep_running):
msg = self.msgq.delete_head()
if not self.keep_running:
break
self.callback(msg)
class rx_block (gr.top_block):
# Initialize the receiver
#
def __init__(self, verbosity, config, trunk_conf_file=None, terminal_type=None, track_errors=False, udp_player=None):
self.verbosity = verbosity
gr.top_block.__init__(self)
self.device_id_by_name = {}
self.msg_types = {}
self.terminal_type = terminal_type
self.last_process_update = 0
self.last_freq_params = {'freq' : 0.0, 'tgid' : None, 'tag' : "", 'tdma' : None}
self.trunk_rx = None
self.track_errors = track_errors
self.last_change_freq = 0
self.sql_db = sql_dbi()
self.input_q = gr.msg_queue(20)
self.output_q = gr.msg_queue(20)
self.last_voice_channel_id = 0
self.terminal = op25_terminal(self.input_q, self.output_q, terminal_type)
self.configure_devices(config['devices'])
self.configure_channels(config['channels'])
if trunk_conf_file:
self.trunk_rx = trunking.rx_ctl(frequency_set = self.change_freq, debug = self.verbosity, conf_file = trunk_conf_file, logfile_workers=[], send_event=self.send_event)
self.sinks = []
for chan in self.channels:
if len(chan.sinks):
self.sinks += chan.sinks
if self.is_http_term():
for sink in self.sinks:
sink.gnuplot.set_interval(_def_interval)
sink.gnuplot.set_output_dir(_def_file_dir)
if udp_player:
chan = self.find_audio_channel() # find chan used for audio
self.audio = audio_thread("127.0.0.1", chan.audio_port, chan.audio_output, False, chan.audio_gain)
else:
self.audio = None
def find_channel_uplink(self, params):
channels = []
for chan in self.channels:
if chan.role != 'uplink':
continue
channels.append(chan)
if self.verbosity > 0:
sys.stderr.write('%f find_channel_uplink: selected channel %d (%s) for tuning request type %s frequency %f\n' % (time.time(), chan.msgq_id, chan.name, 'vc', params['uplink'] / 1000000.0))
return channels
def find_channel_cc(self, params):
channels = []
for chan in self.channels:
if chan.role != 'cc':
continue
if len(chan.nac) and params['nac'] not in chan.nac:
continue
if len(chan.sysid) and params['sysid'] not in chan.sysid:
continue
channels.append(chan)
if self.verbosity > 0:
sys.stderr.write('%f find_channel_cc: selected channel %d (%s) for tuning request type %s frequency %f\n' % (time.time(), chan.msgq_id, chan.name, 'cc', params['freq'] / 1000000.0))
return channels
def find_channel_vc(self, params):
channels = []
for chan in self.channels: # pass1 - search for vc on non-tunable dev having frequency within band
if chan.role != 'vc':
continue
if chan.device.tunable:
continue
if abs(params['freq'] - chan.device.frequency) >= chan.demod.relative_limit:
#sys.stderr.write('%f skipping channel %d frequency %f dev freq %f limit %f\n' % (time.time(), chan.msgq_id, params['freq'] / 1000000.0, chan.device.frequency / 1000000.0, chan.demod.relative_limit / 1000000.0))
continue
channels.append(chan)
if self.verbosity > 0:
sys.stderr.write('%f find_channel_vc: selected channel %d (%s) for tuning request type %s frequency %f (1)\n' % (time.time(), chan.msgq_id, chan.name, 'vc', params['freq'] / 1000000.0))
return channels
for chan in self.channels: # pass2 - search for vc on tunable dev
if chan.role != 'vc':
continue
if not chan.device.tunable:
continue
channels.append(chan)
if self.verbosity > 0:
sys.stderr.write('%f find_channel_vc: selected channel %d (%s) for tuning request type %s frequency %f (2)\n' % (time.time(), chan.msgq_id, chan.name, 'vc', params['freq'] / 1000000.0))
return channels
return [] # pass 1 and 2 failed
def do_error_tracking(self):
if not self.track_errors:
return
for chan in self.channels:
d = chan.error_tracking(self.last_change_freq)
if d is not None and not self.input_q.full_p():
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
self.input_q.insert_tail(msg)
def change_freq(self, params):
self.last_freq_params = params
freq = params['freq']
self.last_change_freq = freq
channel_type = params['channel_type'] # vc or cc
self.uplink_change_freq(params)
if channel_type == 'vc':
channels = self.find_channel_vc(params)
elif channel_type == 'cc':
channels = self.find_channel_cc(params)
else:
raise ValueError('change_freq: invalid channel_type: %s' % channel_type)
if len(channels) == 0:
sys.stderr.write('change_freq: no channel(s) found for %s frequency %f\n' % (channel_type, freq/1000000.0))
return
for chan in channels:
chan.device.set_frequency(freq)
chan.set_frequency(freq)
chan.configure_tdma(params)
self.freq_update()
if channel_type == 'vc':
self.last_voice_channel_id = chan.msgq_id
#return
if self.trunk_rx is None:
return
voice_chans = [chan for chan in self.channels if chan.role == 'vc']
voice_state = channel_type == 'vc'
# FIXME: fsk4 case needs work/testing
for chan in voice_chans:
if voice_state and chan.msgq_id == self.last_voice_channel_id:
chan.demod.set_muted(False)
else:
chan.demod.set_muted(True)
def uplink_change_freq(self, params):
channel_type = params['channel_type'] # vc or cc
if 'uplink' not in params:
return
if channel_type != 'vc':
return
uplink = params['uplink']
channels = self.find_channel_uplink(params)
for chan in channels:
chan.device.set_frequency(uplink)
chan.set_frequency(uplink)
chan.configure_tdma(params)
if self.verbosity > 0:
sys.stderr.write('set uplink frequency %f, channel %s\n' % (uplink / 1000000.0, chan.name))
def is_http_term(self):
if self.terminal_type.startswith('http:'):
return True
else:
return False
def process_terminal_msg(self, msg):
# return true = end top block
RX_COMMANDS = 'skip lockout hold'.split()
s = msg.to_string()
t = msg.type()
if t == -4:
d = json.loads(s)
s = d['command']
if type(s) is not str and isinstance(s, bytes):
# should only get here if python3
s = s.decode()
if s == 'quit': return True
elif s == 'update': ## deprecated here: to be removed
pass
# self.process_update()
elif s == 'set_freq':
sys.stderr.write('set_freq not supported\n')
return
#freq = msg.arg1()
#self.last_freq_params['freq'] = freq
#self.set_freq(freq)
elif s == 'adj_tune':
freq = msg.arg1()
elif s == 'dump_tgids':
self.trunk_rx.dump_tgids()
elif s == 'reload_tags':
nac = msg.arg1()
self.trunk_rx.reload_tags(int(nac))
elif s == 'add_default_config':
nac = msg.arg1()
self.trunk_rx.add_default_config(int(nac))
elif s in RX_COMMANDS:
if self.trunk_rx is not None:
self.trunk_rx.process_qmsg(msg)
elif s == 'settings-enable' and self.trunk_rx is not None:
self.trunk_rx.enable_status(d['data'])
return False
def process_ajax(self):
if not self.is_http_term():
return
if self.input_q.full_p():
return
filenames = [sink.gnuplot.filename for sink in self.sinks if sink.gnuplot.filename]
error = []
for chan in self.channels:
if hasattr(chan.demod, 'get_freq_error'):
error.append(chan.demod.get_freq_error())
d = {'json_type': 'rx_update', 'error': error, 'files': filenames, 'time': time.time()}
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
self.input_q.insert_tail(msg)
def process_update(self):
UPDATE_INTERVAL = 1.0 # sec.
now = time.time()
if now < self.last_process_update + UPDATE_INTERVAL:
return
self.last_process_update = now
self.freq_update()
if self.input_q.full_p():
return
if self.trunk_rx is None:
return ## possible race cond - just ignore
js = self.trunk_rx.to_json()
msg = gr.message().make_from_string(js, -4, 0, 0)
self.input_q.insert_tail(msg)
self.process_ajax()
def send_event(self, d): ## called from trunking module to send json msgs / updates to client
if d is not None:
self.sql_db.event(d)
if d and not self.input_q.full_p():
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
self.input_q.insert_tail(msg)
self.process_update()
def freq_update(self):
if self.input_q.full_p():
return
params = self.last_freq_params
params['json_type'] = 'change_freq'
params['current_time'] = time.time()
js = json.dumps(params)
msg = gr.message().make_from_string(js, -4, 0, 0)
self.input_q.insert_tail(msg)
def process_msg(self, msg):
mtype = msg.type()
if mtype == -2 or mtype == -4:
self.process_terminal_msg(msg)
else:
self.process_channel_msg(msg, mtype)
def process_channel_msg(self, msg, mtype):
msgtext = msg.to_string()
aa55 = trunking.get_ordinals(msgtext[:2])
assert aa55 == 0xaa55
msgq_id = trunking.get_ordinals(msgtext[2:4])
msgtext = msgtext[4:]
if mtype == -5:
self.process_nxdn_msg(msgtext)
else:
self.process_trunked_qmsg(msg, msgq_id)
def process_nxdn_msg(self, s):
if isinstance(s[0], str): # for python 2/3
s = [ord(x) for x in s]
msgtype = chr(s[0])
lich = s[1]
if self.verbosity > 2:
sys.stderr.write ('process_nxdn_msg %s lich %x\n' % (msgtype, lich))
if msgtype == 'c': # CAC type
ran = s[2] & 0x3f
msg = cac_message(s[2:])
if msg['msg_type'] == 'CCH_INFO' and self.verbosity:
sys.stderr.write ('%-10s %-10s system %d site %d ran %d\n' % (msg['cc1']/1e6, msg['cc2']/1e6, msg['location_id']['system'], msg['location_id']['site'], ran))
if self.verbosity > 1:
sys.stderr.write('%s\n' % json.dumps(msg))
def filtered(self, msg, msgq_id):
# return True if msg should be suppressed
chan = self.channels[msgq_id-1]
t = msg.type()
if chan.role == 'vc' and t in [7, 12]: ## suppress tsbk/mbt/pdu received over vc
return True
if chan.role == 'uplink' and t in [-1, 7, 12]: ## suppress as above and also timeout
return True
return False
def process_trunked_qmsg(self, msg, msgq_id): # p25 trunked message
if self.trunk_rx is None:
return
if self.filtered(msg, msgq_id):
return
self.trunk_rx.process_qmsg(msg)
self.trunk_rx.parallel_hunt_cc()
self.do_error_tracking()
def configure_devices(self, config):
self.devices = []
for cfg in config:
self.device_id_by_name[cfg['name']] = len(self.devices)
self.devices.append(device(cfg, self))
def find_trunked_device(self, chan, requested_dev):
if len(self.devices) == 1: # single SDR
return self.devices[0]
for dev in self.devices:
if dev.name == requested_dev:
return dev
return None
def find_device(self, chan, requested_dev):
if 'decode' in chan.keys() and chan['decode'].startswith('p25_decoder'):
return self.find_trunked_device(chan, requested_dev)
for dev in self.devices:
if dev.args.startswith('audio:') and chan['demod_type'] == 'fsk4':
return dev
d = abs(chan['frequency'] - dev.frequency)
nf = dev.sample_rate // 2
if d + 6250 <= nf:
return dev
return None
def configure_channels(self, config):
self.channels = []
for cfg in config:
decode_d = {'role': '', 'dev': ''}
if 'decode' in cfg.keys() and cfg['decode'].startswith('p25_decoder'):
decode_p = cfg['decode'].split(':')[1:]
for p in decode_p: # possible keys: dev, role, nac, sysid; valid roles: cc vc
(k, v) = p.split('=')
if k == 'nac' or k == 'sysid':
v = [int(x, base=0) for x in v.split(',')]
decode_d[k] = v
dev = self.find_device(cfg, decode_d['dev'])
if dev is None:
sys.stderr.write('* * * No device found for channel %s- ignoring!\n' % cfg['name'])
continue
msgq_id = len(self.channels) + 1
chan = channel(cfg, dev, self.verbosity, msgq=self.output_q, msgq_id = msgq_id, role=decode_d['role'])
for k in decode_d.keys():
setattr(chan, k, decode_d[k])
self.channels.append(chan)
self.connect(dev.src, chan.demod, chan.decoder)
sys.stderr.write('assigning channel "%s" (channel id %d) to device "%s"\n' % (chan.name, chan.msgq_id, dev.name))
if 'log_if' in cfg.keys():
chan.logfile_if = blocks.file_sink(gr.sizeof_gr_complex, 'if-%d-%s' % (chan.config['if_rate'], cfg['log_if']))
if cfg['demod_type'] == 'cqpsk':
chan.demod.connect_complex('agc', chan.logfile_if)
else:
chan.demod.connect_complex('if_out', chan.logfile_if)
if 'log_symbols' in cfg.keys():
chan.logfile = blocks.file_sink(gr.sizeof_char, cfg['log_symbols'])
self.connect(chan.demod, chan.logfile)
def find_audio_channel(self):
for chan in self.channels: # pass1 - look for 'vc'
if chan.role == 'vc' and chan.audio_port:
return chan
for chan in self.channels: # pass2 - any chan with audio port specified
if chan.audio_port:
return chan
return self.channels[0]
def scan_channels(self):
for chan in self.channels:
sys.stderr.write('scan %s: error %d\n' % (chan.config['frequency'], chan.demod.get_freq_error()))
class rx_main(object):
def __init__(self):
self.keep_running = True
# command line argument parsing
parser = OptionParser(option_class=eng_option)
parser.add_option("-c", "--config-file", type="string", default=None, help="specify config file name")
parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level")
parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup")
parser.add_option("-M", "--monitor-stdin", action="store_false", default=True, help="enable press ENTER to quit")
parser.add_option("-T", "--trunk-conf-file", type="string", default=None, help="trunking config file name")
parser.add_option("-l", "--terminal-type", type="string", default="curses", help="'curses' or udp port or 'http:host:port'")
parser.add_option("-X", "--freq-error-tracking", action="store_true", default=False, help="enable experimental frequency error tracking")
parser.add_option("-U", "--udp-player", action="store_true", default=False, help="enable built-in udp audio player")
(options, args) = parser.parse_args()
self.options = options
# wait for gdb
if options.pause:
print ('Ready for GDB to attach (pid = %d)' % (os.getpid(),))
raw_input("Press 'Enter' to continue...")
if options.config_file == '-':
config = json.loads(sys.stdin.read())
else:
config = json.loads(open(options.config_file).read())
self.tb = rx_block(options.verbosity, config = byteify(config), trunk_conf_file=options.trunk_conf_file, terminal_type=options.terminal_type, track_errors=options.freq_error_tracking, udp_player = options.udp_player)
sys.stderr.write('python version detected: %s\n' % sys.version)
sys.stderr.flush()
def run(self):
self.tb.start()
if self.options.monitor_stdin:
print("Running. press ENTER to quit")
while self.keep_running:
if self.options.monitor_stdin and select.select([sys.stdin,],[],[],0.0)[0]:
c = sys.stdin.read(1)
self.keep_running = False
break
msg = self.tb.output_q.delete_head()
if self.tb.process_msg(msg):
self.keep_running = False
break
print('Quitting - now stopping top block')
self.tb.stop()
if __name__ == "__main__":
rx = rx_main()
try:
rx.run()
except KeyboardInterrupt:
rx.keep_running = False
print('Program ending')
time.sleep(1)

View File

@ -0,0 +1,316 @@
#!/usr/bin/env python
#
# (C) Copyright 2020 Max H. Parke, KA1RBI
#
# This file is part of OP25
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
#
# nxdn trunking:
# - CAC decoding
#
import sys
import os
sys.path.append('tdma')
from bit_utils import *
def locid(id):
save_id = mk_int(id)
cat = mk_int(id[:2])
if cat == 0:
ssize = 10
elif cat == 2:
ssize = 14
elif cat == 1:
ssize = 17
else:
return {'category': -1, 'system': -1, 'site': -1, 'id': '0x%x' % save_id}
id = id[2:]
syscode = mk_int(id[:ssize])
id = id[ssize:]
sitecode = mk_int(id)
return {'category': cat, 'system': syscode, 'site': sitecode, 'id': '0x%x' % save_id}
def mk_freq(f):
### todo: UHF currently untested; may fail at 400 MHz
return int(f * 1250 + 100000000) # frequency in Hz
def cac_message(s):
d = {}
bits = []
for c in s:
for i in range(8):
bits.append((c >> (7-i)) & 1)
d['structure'] = mk_int(bits[:2])
d['ran'] = mk_int(bits[2:8])
bits = bits[8:]
msg_type = mk_int(bits[2:8])
d['msg_typeid'] = msg_type
if msg_type == 0x18: # SITE_INFO
assert len(bits) == 144
d['msg_type'] = 'SITE_INFO'
bits = bits[8:]
d['location_id'] = locid(bits[:24])
bits = bits[24:]
d['channel_info'] = mk_int(bits[:16])
bits = bits[16:]
d['service_info'] = mk_int(bits[:16])
bits = bits[16:]
d['restr_info'] = mk_int(bits[:24])
bits = bits[24:]
d['access_info'] = mk_int(bits[:24])
bits = bits[24:]
d['version_no'] = mk_int(bits[:8])
bits = bits[8:]
d['adjacent_alloc'] = mk_int(bits[:4])
bits = bits[4:]
d['cc1'] = mk_int(bits[:10])
bits = bits[10:]
d['cc2'] = mk_int(bits[:10])
elif msg_type == 0x19: # SRV_INFO
assert len(bits) >= 72
d['msg_type'] = 'SRV_INFO'
bits = bits[8:]
d['location_id'] = locid(bits[:24])
bits = bits[24:]
d['service_info'] = mk_int(bits[:16])
bits = bits[16:]
d['restr_info'] = mk_int(bits[:24])
elif msg_type == 0x1a: # CCH_INFO
assert len(bits) >= 72
d['msg_type'] = 'CCH_INFO'
bits = bits[8:]
d['location_id'] = locid(bits[:24])
bits = bits[24:]
d['flags1'] = mk_int(bits[:8])
bits = bits[8:]
d['cc1'] = mk_freq(mk_int(bits[:16]))
bits = bits[16:]
d['cc2'] = mk_freq(mk_int(bits[:16]))
elif msg_type == 0x1b: # ADJ_SITE_INFO
assert len(bits) >= 72
d['msg_type'] = 'ADJ_SITE_INFO'
d1 = {}
d2 = {}
bits = bits[8:]
d1['location'] = locid(bits[:24])
bits = bits[24:]
d1['option'] = mk_int(bits[:8])
bits = bits[8:]
d1['cc'] = mk_freq(mk_int(bits[:16]))
bits = bits[16:]
d2['location'] = locid(bits[:24])
bits = bits[24:]
d2['option'] = mk_int(bits[:8])
bits = bits[8:]
d2['cc'] = mk_freq(mk_int(bits[:16]))
bits = bits[16:]
d['sites'] = [d1, d2]
#d['location_3'] = locid(bits[:24])
#bits = bits[24:]
#d['option_3'] = mk_int(bits[:6])
#bits = bits[6:]
#d['cc_3'] = mk_int(bits[:10])
elif msg_type == 0x01: # VCALL_RESP
assert len(bits) >= 64
d['msg_type'] = 'VCALL_RESP'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['call_type'] = mk_int(bits[:3])
d['call_option'] = mk_int(bits[3:8])
bits = bits[8:]
d['source_id'] = mk_int(bits[:16])
bits = bits[16:]
d['destination_id'] = mk_int(bits[:16])
bits = bits[16:]
d['cause'] = mk_int(bits[:8])
bits = bits[8:]
elif msg_type == 0x09: # DCALL_RESP
assert len(bits) >= 64
d['msg_type'] = 'DCALL_RESP'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['call_type'] = mk_int(bits[:3])
d['call_option'] = mk_int(bits[3:8])
bits = bits[8:]
d['source_id'] = mk_int(bits[:16])
bits = bits[16:]
d['destination_id'] = mk_int(bits[:16])
bits = bits[16:]
d['cause'] = mk_int(bits[:8])
bits = bits[8:]
elif msg_type == 0x04: # VCALL_ASSGN
assert len(bits) >= 72
bits2 = bits
s = ''
while len(bits2):
s += '%02x' % mk_int(bits2[:8])
bits2 = bits2[8:]
d['hexdata'] = s
d['msg_type'] = 'VCALL_ASSGN'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['call_type'] = mk_int(bits[:3])
d['call_option'] = mk_int(bits[3:8])
bits = bits[8:]
d['source_id'] = mk_int(bits[:16])
bits = bits[16:]
d['group_id'] = mk_int(bits[:16])
bits = bits[16:]
d['timer'] = mk_int(bits[:8])
d['channel'] = mk_int(bits[6:16])
bits = bits[8:]
d['f1'] = mk_freq(mk_int(bits[:16]))
bits = bits[16:]
d['f2'] = mk_freq(mk_int(bits[:16]))
elif msg_type == 0x0e: # DCALL_ASSGN
assert len(bits) >= 104
d['msg_type'] = 'DCALL_ASSGN'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['call_type'] = mk_int(bits[:3])
d['call_option'] = mk_int(bits[3:8])
bits = bits[8:]
d['source_id'] = mk_int(bits[:16])
bits = bits[16:]
d['group_id'] = mk_int(bits[:16])
bits = bits[16:]
d['timer'] = mk_int(bits[:8])
bits = bits[8:]
d['f1'] = mk_freq(mk_int(bits[:16]))
bits = bits[16:]
d['f2'] = mk_freq(mk_int(bits[:16]))
elif msg_type == 0x20: # REG_RESP
assert len(bits) >= 72
d['msg_type'] = 'REG_RESP'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['location id'] = mk_int(bits[:16])
bits = bits[16:]
d['unit_id'] = mk_int(bits[:16])
bits = bits[16:]
d['group_id'] = mk_int(bits[:16])
bits = bits[16:]
d['cause'] = mk_int(bits[:8])
bits = bits[8:]
d['visitor_unit'] = mk_int(bits[:16])
bits = bits[16:]
d['visitor_group'] = mk_int(bits[:16])
elif msg_type == 0x22: # REG_C_RESP
assert len(bits) >= 56
d['msg_type'] = 'REG_C_RESP'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['location id'] = mk_int(bits[:16])
bits = bits[16:]
d['unit_id'] = mk_int(bits[:16])
elif msg_type == 0x24: # GRP_REG_RESP
assert len(bits) >= 72
d['msg_type'] = 'GRP_REG_RESP'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['destination id'] = mk_int(bits[:16])
bits = bits[16:]
d['group_id'] = mk_int(bits[:16])
bits = bits[16:]
d['cause'] = mk_int(bits[:8])
bits = bits[8:]
d['visitor_group_id'] = mk_int(bits[:16])
elif msg_type == 0x32: # STAT_REQ
assert len(bits) >= 72
d['msg_type'] = 'STAT_REQ'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['call_type'] = mk_int(bits[:3])
d['call_option'] = mk_int(bits[3:8])
bits = bits[8:]
d['source id'] = mk_int(bits[:16])
bits = bits[16:]
d['destination_id'] = mk_int(bits[:16])
bits = bits[8:]
d['spare'] = mk_int(bits[:8])
status = bits[8:]
elif msg_type == 0x33: # STAT_RESP
assert len(bits) >= 64
d['msg_type'] = 'STAT_RESP'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['call_type'] = mk_int(bits[:3])
d['call_option'] = mk_int(bits[3:8])
bits = bits[8:]
d['source id'] = mk_int(bits[:16])
bits = bits[16:]
d['destination_id'] = mk_int(bits[:16])
bits = bits[16:]
d['cause'] = mk_int(bits[:8])
elif msg_type == 0x38: # SDCALL_REQ_HEADER
assert len(bits) >= 64
d['msg_type'] = 'SDCALL_REQ_HEADER'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['call_type'] = mk_int(bits[:3])
d['call_option'] = mk_int(bits[3:8])
bits = bits[8:]
d['source id'] = mk_int(bits[:16])
bits = bits[16:]
d['destination_id'] = mk_int(bits[:16])
bits = bits[16:]
d['cipher_type'] = mk_int(bits[:2])
d['key_id'] = mk_int(bits[2:8])
elif msg_type == 0x39: # SDCALL_REQ_USERDATA
assert len(bits) >= 64
d['msg_type'] = 'SDCALL_REQ_USERDATA'
bits = bits[8:]
d['packet_frame'] = mk_int(bits[:4])
d['block_number'] = mk_int(bits[4:8])
bits = bits[8:]
s = ''
while len(bits):
s += '%02x' % mk_int(bits[:8])
bits = bits[8:]
d['hexdata'] = s
elif msg_type == 0x3b: # SDCALL_RESP
assert len(bits) >= 64
d['msg_type'] = 'SDCALL_RESP'
bits = bits[8:]
d['option'] = mk_int(bits[:8])
bits = bits[8:]
d['call_type'] = mk_int(bits[:3])
d['call_option'] = mk_int(bits[3:8])
bits = bits[8:]
d['source id'] = mk_int(bits[:16])
bits = bits[16:]
d['destination_id'] = mk_int(bits[:16])
bits = bits[16:]
d['cause'] = mk_int(bits[:8])
bits = bits[8:]
else: # msg type unhandled
d['msg_type'] = 'UNSUPPORTED 0x%x' % (msg_type)
return d

View File

@ -0,0 +1,15 @@
[Unit]
Description=op25-liq
After=syslog.target network.target nss-lookup.target network-online.target
Requires=network-online.target
[Service]
User=1000
Group=1000
WorkingDirectory=/home/pi/op25/op25/gr-op25_repeater/apps
ExecStart=/usr/bin/liquidsoap op25.liq
RestartSec=5
Restart=on-failure
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,54 @@
#!/usr/bin/liquidsoap
# Example liquidsoap streaming from op25 to icecast
# (c) 2019, 2020 gnorbury@bondcar.com, wllmbecks@gmail.com
#
set("log.stdout", true)
set("log.file", false)
set("log.level", 1)
# Make the native sample rate compatible with op25
set("frame.audio.samplerate", 8000)
input = mksafe(input.external(buffer=0.02, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -x 2 -s"))
# Consider increasing the buffer value on slow systems such as RPi3. e.g. buffer=0.25
# Longer buffer results in less choppy audio but at the expense of increased latency.
# OPTIONAL AUDIO SIGNAL PROCESSING BLOCKS
# Uncomment to enable
#
# High pass filter
#input = filter.iir.butterworth.high(frequency = 200.0, order = 4, input)
# Low pass filter
#input = filter.iir.butterworth.low(frequency = 3250.0, order = 4, input)
# Normalization
#input = normalize(input, gain_max = 3.0, gain_min = -3.0, target = -16.0, threshold = -40.0)
# LOCAL AUDIO OUTPUT
# Uncomment the appropriate line below to enable local sound
#
# Default audio subsystem
out (input)
#
# PulseAudio
#output.pulseaudio(input)
#
# ALSA
#output.alsa(input)
# ICECAST STREAMING
# Uncomment to enable output to an icecast server
# Change the "host", "password", and "mount" strings appropriately first!
# For metadata to work properly, the host address given here MUST MATCH the address in op25's meta.json file
#
#output.icecast(%mp3(bitrate=16, samplerate=22050, stereo=false), description="op25", genre="Public Safety", url="", fallible=false, icy_metadata="false", host="localhost", port=8000, mount="mountpoint", password="hackme", mean(input))

View File

@ -0,0 +1,4 @@
import os
SQLALCHEMY_DATABASE_URI = 'sqlite:///%s/../op25-data.db' % (os.path.dirname(__file__))
SQLALCHEMY_TRACK_MODIFICATIONS = False

View File

@ -0,0 +1,844 @@
#! /usr/bin/env python
# Copyright 2021 Max H. Parke KA1RBI, Michael Rose
#
# This file is part of OP25
#
# OP25 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, or (at your option)
# any later version.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
import time
from time import sleep
from time import gmtime, strftime
import os
from os import listdir
from os.path import isfile, join
import sys
import traceback
import math
import json
import click
import datetime
from datatables import ColumnDT, DataTables
from flask import Flask, jsonify, render_template, request, redirect, session
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func, desc, and_, or_, case, delete, insert, update, exc
from sqlalchemy.orm import Query
from sqlalchemy.exc import OperationalError
import sqlalchemy.types as types
from shutil import copyfile
sys.path.append('..') # for emap
from emap import oplog_map, cc_events, cc_desc
app = Flask(__name__)
app.config.from_pyfile("../app.cfg")
app.config['SQLALCHEMY_ECHO'] = False # set to True to send sql statements to the console
# enables session variables to be used
app.secret_key = b'kH8HT0ucrh' # random bytes - this key not used anywhere else
db = SQLAlchemy(app)
# db.init_app(app)
with app.app_context():
try:
db.reflect()
except OperationalError as e:
raise(e) # database is locked by another process
class MyDateType(types.TypeDecorator):
impl = types.REAL
def process_result_value(self, value, dialect):
return datetime.datetime.fromtimestamp(value).strftime('%Y-%m-%d %H:%M:%S')
class column_helper(object):
# convenience class - enables columns to be referenced as
# for example, Foo.bar instead of Foo['bar']
def __init__(self, table):
self.table_ = db.metadata.tables[table]
cols = self.table_.columns
for k in cols.keys():
setattr(self, k, cols[k])
def dbstate():
database = app.config['SQLALCHEMY_DATABASE_URI'][10:]
if not os.path.isfile(database):
return 1 # db file does not exist
fs = os.path.getsize(database)
if fs < 1024:
return 2 # file size too small
DataStore = column_helper('data_store')
rows = db.session.query(DataStore.id).count()
if rows < 1:
return 4 # no rows present
return 0
# clears the sm (successMessage) after being used in jinja
def clear_sm():
session['sm'] = 0
return '' #must be an empty string or 'None' is displayed in the template
def t_gmt():
t = time.strftime("%a, %d %b %Y %H:%M:%S", time.gmtime())
return t
def t_loc():
t = strftime("%a, %d %b %Y %H:%M:%S %Z")
return t
# make these functions available to jinja
app.jinja_env.globals.update(t_gmt=t_gmt)
app.jinja_env.globals.update(t_loc=t_loc)
app.jinja_env.globals.update(clear_sm=clear_sm)
# for displaying the db file size, shamelessly stolen from SO
def convert_size(size_bytes):
if size_bytes == 0:
return "0 B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return "%s %s" % (s, size_name[i])
def dbStats():
DataStore = column_helper('data_store')
DataStore = column_helper('data_store')
SysIDTags = column_helper('sysid_tags')
DataStore.time.type = MyDateType()
rows = db.session.query(func.count(DataStore.id)).scalar()
if rows == 0:
return(0, 0, 0, 0, 0, 0, 0)
sys_count = db.session.query(DataStore.sysid) \
.distinct(DataStore.sysid) \
.group_by(DataStore.sysid) \
.filter(DataStore.sysid != 0) \
.count()
# TODO: talkgroups and subs should be distinct by system
talkgroups = db.session.query(DataStore.tgid) \
.distinct(DataStore.tgid) \
.group_by(DataStore.tgid) \
.count()
subs = db.session.query(DataStore.suid) \
.distinct(DataStore.suid) \
.group_by(DataStore.suid) \
.count()
firstRec = db.session.query(DataStore.time, func.min(DataStore.time)).scalar()
lastRec = db.session.query(DataStore.time, func.max(DataStore.time)).scalar()
f = app.config['SQLALCHEMY_DATABASE_URI'][10:] # db file name
dbsize = convert_size(os.path.getsize(f))
return(rows, sys_count, talkgroups, subs, firstRec, lastRec, dbsize, f)
def sysList():
DataStore = column_helper('data_store')
rows = db.session.query(func.count(DataStore.id)).scalar()
if rows == 0:
return []
SysIDTags = column_helper('sysid_tags')
sysList = db.session.query(DataStore.sysid, SysIDTags.tag.label('tag')) \
.distinct(DataStore.sysid) \
.outerjoin(SysIDTags.table_, SysIDTags.sysid == DataStore.sysid) \
.filter(DataStore.sysid != 0)
return sysList
def read_tsv(filename): # used by import_tsv and inspect_tsv, careful w/ changes
rows = []
with open(filename, 'r') as f:
lines = f.read().rstrip().split('\n')
for i in range(len(lines)):
a = lines[i].split('\t')
if i == 0: # check hdr
if not a[0].strip().isdigit():
continue
if not a[0].strip().isdigit(): # check each a[0] for wildcards and skip (continue) if wildcards found
continue
rid = int(a[0])
tag = a[1]
priority = 0 if len(a) < 3 else int(a[2])
s = (rid, tag, priority)
rows.append(s)
return rows
def import_tsv(argv):
UnitIDTags = column_helper('unit_id_tags')
TGIDTags = column_helper('tgid_tags')
cmd = argv[1]
filename = argv[2]
sysid = int(argv[3])
if cmd == 'import_tgid':
tbl = TGIDTags
elif cmd == 'import_unit':
tbl = UnitIDTags
else:
print('%s unsupported' % (cmd))
return
rows = read_tsv(filename)
rm = 0 # records matched
nr = 0 # new records
dr = 0 # duplicate records
if len(rows):
for i in rows:
recCount = db.session.query(tbl.table_).where(and_(tbl.rid == i[0], tbl.sysid == argv[3])).count()
if recCount == 1:
# update record
q = update(tbl.table_) \
.where(and_(tbl.rid == i[0], tbl.sysid == argv[3])) \
.values(rid = int(i[0]), sysid = int(argv[3]), tag = i[1], priority = int(i[2]))
db.session.execute(q)
db.session.commit()
rm +=1
elif recCount == 0:
# insert record
q = insert(tbl.table_).values(rid = int(i[0]), sysid = int(argv[3]), tag = i[1], priority = int(i[2]))
db.session.execute(q)
db.session.commit()
nr += 1
else:
# delete all of the duplicates and insert new (duplicates break things)
print('command %s - db error - %s records for %s %s' % (cmd, recCount, i[0], i[1]))
delRec = delete(TGIDTags.table_).where(and_(tbl.rid == i[0], tbl.sysid == argv[3]))
db.session.execute(delRec)
db.session.commit()
q = insert(tbl.table_).values(rid = int(i[0]), sysid = int(argv[3]), tag = i[1], priority = int(i[2]))
db.session.execute(q)
db.session.commit()
dr += 1
return(rm, nr, dr)
@app.route("/")
def home():
ds = dbstate()
if ds != 0:
return redirect('error?code=%s' % ds)
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
params['cc_desc'] = cc_desc
return render_template("home.html", project="op25", params=params, dbstats=dbStats(), sysList=sysList())
@app.route("/about")
def about():
ds = dbstate()
if ds != 0:
return redirect('error?code=%s' % ds)
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
params['cc_desc'] = cc_desc
return render_template("about.html", project="op25", params=params, sysList=sysList())
# error page for database errors
@app.route("/error")
def error_page():
params = request.args.to_dict()
params['file'] = app.config['SQLALCHEMY_DATABASE_URI'][10:]
return render_template("error.html", params=params, file=params['file'], code=int(params['code']))
# Inspect TSV (import) - returns a table of the tsv for display in a div, accessed by ajax
@app.route("/inspect")
def inspect():
params = request.args.to_dict()
f = os.getcwd() + '/../' + params['file']
i = read_tsv(f)
return render_template("inspect.html", i=i)
# edit and import tags
@app.route("/edit_tags")
def edit_tags():
UnitIDTags = column_helper('unit_id_tags')
TGIDTags = column_helper('tgid_tags')
SysIDTags = column_helper('sysid_tags')
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
if 'cmd' not in params.keys(): # render talkgroup by default
params['cmd'] = 'tgid'
cmd = params['cmd']
session['cmd'] = cmd
systems = db.session.query(SysIDTags.sysid, SysIDTags.tag)
p = os.getcwd() + '/..'
tsvs = []
for root, dirs, files in os.walk(p, topdown=True):
for file in files:
if file.endswith(".tsv") and not file.startswith("._"):
print(os.path.join(root, file))
tsvs.append(os.path.join(root, file))
tsvs.sort()
return render_template("edit_tags.html", params=params, systems=systems, sysList=sysList(), p=p, cmd=cmd, tsvs=tsvs)
# data for tags table editor
@app.route("/edittg")
def edittg():
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
cmd = params['cmd']
sysid = int(params['sysid'])
UnitIDTags = column_helper('unit_id_tags')
TGIDTags = column_helper('tgid_tags')
SysIDTags = column_helper('sysid_tags')
if cmd == 'tgid':
tbl = TGIDTags
if cmd == 'unit':
tbl = UnitIDTags
column_d = {
'tgid': [
ColumnDT(TGIDTags.id),
ColumnDT(TGIDTags.sysid),
ColumnDT(TGIDTags.rid),
ColumnDT(TGIDTags.tag),
ColumnDT(TGIDTags.id)
],
'unit': [
ColumnDT(UnitIDTags.id),
ColumnDT(UnitIDTags.sysid),
ColumnDT(UnitIDTags.rid),
ColumnDT(UnitIDTags.tag),
ColumnDT(UnitIDTags.id)
]
}
q = db.session.query(tbl.id, tbl.sysid, tbl.rid, tbl.tag).order_by(tbl.rid)
if sysid != 0:
q = q.filter(tbl.sysid == sysid)
rowTable = DataTables(params, q, column_d[cmd])
js = jsonify(rowTable.output_result())
return js
#dtd = delete tag data
@app.route("/dtd")
def dtd():
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
cmd = params['cmd']
UnitIDTags = column_helper('unit_id_tags')
TGIDTags = column_helper('tgid_tags')
SysIDTags = column_helper('sysid_tags')
recId = params['id']
if cmd == 'tgid':
tbl = TGIDTags
if cmd == 'unit':
tbl = UnitIDTags
delRec = delete(tbl.table_).where(tbl.id == recId)
db.session.execute(delRec)
db.session.commit()
session['sm'] = 2
return redirect('/edit_tags?cmd=' + cmd)
#utd = update tag data
@app.route("/utd")
def utd():
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
cmd = params['cmd']
UnitIDTags = column_helper('unit_id_tags')
TGIDTags = column_helper('tgid_tags')
SysIDTags = column_helper('sysid_tags')
recId = params['id']
tag = params['tag']
if cmd == 'tgid':
tbl = TGIDTags
if cmd == 'unit':
tbl = UnitIDTags
upRec = update(tbl.table_).where(tbl.id == recId).values(tag=tag)
db.session.execute(upRec)
db.session.commit()
session['sm'] = 1
return redirect('/edit_tags?cmd=' + cmd)
# import tags
@app.route("/itt")
def itt():
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
cmd = params['cmd']
argv = [ None, 'import_' + cmd, os.getcwd() + '/../' + params['file'], params['sysid'] ]
session['imp_results'] = import_tsv(argv)
session['sm'] = 3
return redirect('/edit_tags?cmd=' + cmd)
# delete all talkgroup/subscriber tags
@app.route("/delTags")
def delTags():
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
cmd = params['cmd']
UnitIDTags = column_helper('unit_id_tags')
TGIDTags = column_helper('tgid_tags')
SysIDTags = column_helper('sysid_tags')
sysid = params['sysid']
if cmd == 'tgid':
tbl = TGIDTags
if cmd == 'unit':
tbl = UnitIDTags
delRec = delete(tbl.table_).where(tbl.sysid == sysid)
db.session.execute(delRec)
db.session.commit()
db.session.execute("VACUUM") # sqlite3 clean up -- reduces file size
session['sm'] = 4
return redirect('/edit_tags?cmd=' + cmd)
# system tag editor functions (entirely separate from the tags editor above)
@app.route("/editsys")
def editsys():
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
params['cc_desc'] = cc_desc
SysIDTags = column_helper('sysid_tags')
systems = db.session.query(SysIDTags.sysid, SysIDTags.tag)
return render_template("editsys.html", params=params, systems=systems, sysList=sysList())
#dsd = delete system data
@app.route("/dsd")
def dsd():
params = request.args.to_dict()
SysIDTags = column_helper('sysid_tags')
recId = params['id']
delRec = delete(SysIDTags.table_).where(SysIDTags.id == recId)
db.session.execute(delRec)
db.session.commit()
return redirect('/editsys')
#usd = update system data
@app.route("/usd")
def usd():
params = request.args.to_dict()
SysIDTags = column_helper('sysid_tags')
recId = params['id']
tag = params['tag']
upRec = update(SysIDTags.table_).where(SysIDTags.id == recId).values(tag=tag)
db.session.execute(upRec)
db.session.commit()
return redirect('/editsys')
#esd = edit system data (system tags)
@app.route("/esd")
def esd():
params = request.args.to_dict()
SysIDTags = column_helper('sysid_tags')
column_d = {
's': [
ColumnDT(SysIDTags.id),
ColumnDT(SysIDTags.sysid),
ColumnDT(SysIDTags.tag),
ColumnDT(SysIDTags.id)
]
}
q = db.session.query(SysIDTags.id, SysIDTags.sysid, SysIDTags.tag)
rowTable = DataTables(params, q, column_d['s'])
js = jsonify(rowTable.output_result())
return js
#asd = add system data
@app.route("/asd")
def asd():
params = request.args.to_dict()
ns = params['id']
nt = params['tag']
#todo: validate input
SysIDTags = column_helper('sysid_tags')
insRec = insert(SysIDTags.table_).values(sysid=ns, tag=nt)
db.session.execute(insRec)
db.session.commit()
return redirect('/editsys')
# purge database functions
@app.route("/purge")
def purge():
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
DataStore = column_helper('data_store')
destfile = ''
b = False
if 'bu' in params.keys():
if params['bu'] == 'true':
b = True
t = strftime("%Y%m%d_%H%M%S")
destfile = 'op25-backup-%s.db' % t
src = app.config['SQLALCHEMY_DATABASE_URI'][10:]
s = src.split('/')
f = s[-1]
dst = src.replace(f, destfile)
if 'simulate' in params.keys():
simulate = params['simulate']
if 'action' in params.keys():
if params['action'] == 'purge':
sd = params['sd']
ed = params['ed']
sysid = int(params['sysid'])
delRec = delete(DataStore.table_).where(DataStore.time >= int(sd), DataStore.time <= int(ed))
recCount = db.session.query(DataStore.id).filter(and_(DataStore.time >= int(sd), DataStore.time <= int(ed)))
if sysid != 0:
recCount = recCount.filter(DataStore.sysid == sysid)
delRec = delRec.where(DataStore.sysid == sysid)
if 'kv' in params.keys(): # keep voice calls
if params['kv'] == 'true':
recCount = recCount.where(and_(DataStore.opcode != 0, DataStore.opcode != 2))
delRec = delRec.where(and_(DataStore.opcode != 0, DataStore.opcode != 2))
recCount = recCount.count()
dispQuery = delRec.compile(compile_kwargs={"literal_binds": True})
if simulate == 'false':
if b == True:
copyfile(src, dst)
db.session.execute(delRec)
db.session.commit()
db.session.execute("VACUUM") # sqlite3 clean up -- reduces file size
successMessage = 1
else:
successMessage = 2
else:
recCount = 0
successMessage = 0
dispQuery = ''
return render_template("purge.html", \
project="op25", \
params=params, \
dbstats=dbStats(), \
sysList=sysList(), \
successMessage=successMessage, \
recCount=recCount, \
dispQuery=dispQuery, \
destfile=destfile )
# displays all logs w/ datatables
@app.route("/logs")
def logs():
UnitIDTags = column_helper('unit_id_tags')
TGIDTags = column_helper('tgid_tags')
tag = ''
params = request.args.to_dict()
params['ekeys'] = oplog_map.keys()
params['cc_desc'] = cc_desc
t = None if 'q' not in params.keys() else params['q']
sysid = 0 if 'sysid' not in params.keys() else int(params['sysid'])
if sysid != 0:
if t is not None and params['r'] == 'tgid':
q = db.session.query(TGIDTags.tag).where(and_(TGIDTags.rid == t, TGIDTags.sysid == sysid))
if t is not None and params['r'] == 'su':
q = db.session.query(UnitIDTags.tag).where(and_(UnitIDTags.rid == t, UnitIDTags.sysid == sysid))
if q.count() > 0:
tg = (db.session.execute(q).one())
tag = (' - %s' % tg.tag)
if params['r'] == 'cc_event':
mapl = oplog_map[params['p'].strip()]
params['ckeys'] = [s[1] for s in mapl if s[0] != 'opcode' and s[0] != 'cc_event']
return render_template("logs.html", \
project="logs", \
params=params, \
sysList=sysList(), \
tag=tag )
# data for /logs
@app.route("/data")
def data():
"""Return server side data."""
# GET parameters
params = request.args.to_dict()
host_rid = None if 'host_rid' not in params.keys() else params['host_rid']
host_function_type = None if 'host_function_type' not in params.keys() else params['host_function_type']
host_function_param = None if 'host_function_param' not in params.keys() else params['host_function_param'].strip()
filter_tgid = None if 'tgid' not in params.keys() else int(params['tgid'].strip())
filter_suid = None if 'suid' not in params.keys() else int(params['suid'].strip())
start_time = None if 'sdate' not in params.keys() else datetime.datetime.utcfromtimestamp(float(params['sdate']))
end_time = None if 'edate' not in params.keys() else datetime.datetime.utcfromtimestamp(float(params['edate']))
print(params)
sysid = None if 'sysid' not in params.keys() else int(params['sysid'])
stime = int(params['sdate']) #used in the queries
etime = int(params['edate']) #used in the queries
DataStore = column_helper('data_store')
EventKeys = column_helper('event_keys')
SysIDTags = column_helper('sysid_tags')
UnitIDTags = column_helper('unit_id_tags')
TGIDTags = column_helper('tgid_tags')
LocRegResp = column_helper('loc_reg_resp_rv')
DataStore.time.type = MyDateType()
k = 'logs'
if host_function_type:
k = '%s_%s' % (k, host_function_type)
column_d = {
'logs_su': [
ColumnDT(TGIDTags.tag),
ColumnDT(DataStore.tgid),
ColumnDT(DataStore.tgid),
],
'logs_tgid': [
ColumnDT(DataStore.suid),
ColumnDT(UnitIDTags.tag),
ColumnDT(DataStore.suid),
ColumnDT(DataStore.time)
],
'logs_calls': [
ColumnDT(DataStore.time),
ColumnDT(SysIDTags.tag),
ColumnDT(DataStore.tgid),
ColumnDT(TGIDTags.tag),
ColumnDT(DataStore.frequency),
ColumnDT(DataStore.suid)
],
'logs_joins': [
ColumnDT(DataStore.time),
ColumnDT(DataStore.opcode),
ColumnDT(DataStore.sysid),
ColumnDT(SysIDTags.tag),
ColumnDT(LocRegResp.tag),
ColumnDT(DataStore.tgid),
ColumnDT(TGIDTags.tag),
ColumnDT(DataStore.suid),
ColumnDT(UnitIDTags.tag)
],
'logs_total_tgid': [
ColumnDT(DataStore.sysid),
ColumnDT(SysIDTags.tag),
ColumnDT(DataStore.tgid),
ColumnDT(TGIDTags.tag),
ColumnDT(DataStore.tgid)
],
'logs_call_detail': [
ColumnDT(DataStore.time),
ColumnDT(DataStore.opcode),
ColumnDT(SysIDTags.sysid),
ColumnDT(SysIDTags.tag),
ColumnDT(DataStore.tgid),
ColumnDT(TGIDTags.tag),
ColumnDT(DataStore.suid),
ColumnDT(UnitIDTags.tag),
ColumnDT(DataStore.frequency)
]
}
"""or_( EventKeys.tag == 'grp_v_ch_grant', EventKeys.tag == 'grp_v_ch_grant_exp'),"""
query_d = {
'logs_total_tgid': db.session.query(DataStore.sysid, \
SysIDTags.tag, \
DataStore.tgid, \
TGIDTags.tag, \
func.count(DataStore.tgid).label('count'))
.group_by(DataStore.tgid)
.outerjoin(SysIDTags.table_, DataStore.sysid == SysIDTags.sysid)
.outerjoin(TGIDTags.table_, DataStore.tgid == TGIDTags.rid)
.filter(and_(DataStore.tgid != 0), (DataStore.frequency != None) ),
'logs_call_detail': db.session.query(DataStore.time, \
DataStore.opcode, \
DataStore.sysid, \
SysIDTags.tag, \
DataStore.tgid, \
TGIDTags.tag, \
DataStore.suid, \
UnitIDTags.tag, \
DataStore.frequency )
.outerjoin(SysIDTags.table_, DataStore.sysid == SysIDTags.sysid)
.outerjoin(TGIDTags.table_, and_(DataStore.tgid == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid))
.outerjoin(UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid))
.filter(and_(DataStore.tgid != 0), (DataStore.frequency != None) )
.filter(or_(DataStore.opcode == 0, and_(DataStore.opcode == 2, DataStore.mfrid == 144)) ),
'logs_tgid': db.session.query(DataStore.suid, \
UnitIDTags.tag, \
func.count(DataStore.suid).label('count'), func.max(DataStore.time).label('last') )
.outerjoin(UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid)),
'logs_su': db.session.query(TGIDTags.tag, \
DataStore.tgid, \
func.count(DataStore.tgid).label('count') )
.outerjoin(TGIDTags.table_, DataStore.tgid == TGIDTags.rid),
'logs_calls': db.session.query(DataStore.time, \
SysIDTags.tag, \
DataStore.tgid, \
TGIDTags.tag, \
DataStore.frequency, \
DataStore.suid )
.join(EventKeys.table_, and_(or_( EventKeys.tag == 'grp_v_ch_grant', EventKeys.tag == 'grp_v_ch_grant_mbt'),EventKeys.id == DataStore.cc_event))
.outerjoin(TGIDTags.table_, and_(TGIDTags.rid == DataStore.tgid, TGIDTags.sysid == DataStore.sysid))
.outerjoin(SysIDTags.table_, DataStore.sysid == SysIDTags.sysid),
'logs_joins': db.session.query(DataStore.time, \
DataStore.opcode, \
DataStore.sysid, \
SysIDTags.tag, \
LocRegResp.tag, \
DataStore.tgid, \
TGIDTags.tag, \
DataStore.suid, \
UnitIDTags.tag )
.join(LocRegResp.table_, DataStore.p == LocRegResp.rv)
.outerjoin(SysIDTags.table_, DataStore.sysid == SysIDTags.sysid)
.outerjoin(TGIDTags.table_, and_(DataStore.tgid == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid))
.outerjoin(UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid))
.filter(or_(DataStore.opcode == 40, DataStore.opcode == 43)) # joins
} # end query_d
if host_function_type != 'cc_event':
q = query_d[k]
if host_function_type in 'su tgid'.split():
filter_col = {'su': DataStore.suid, 'tgid': DataStore.tgid}
group_col = {'su': DataStore.tgid, 'tgid': DataStore.suid}
if '?' in host_rid:
id_start = int(host_rid.replace('?', '0'))
id_end = int(host_rid.replace('?', '9'))
q = q.filter(filter_col[host_function_type] >= id_start, filter_col[host_function_type] <= id_end)
elif '-' in host_rid:
id_start, id_end = host_rid.split('-')
id_start = int(id_start)
id_end = int(id_end)
q = q.filter(filter_col[host_function_type] >= id_start, filter_col[host_function_type] <= id_end)
else:
q = q.filter(filter_col[host_function_type] == int(host_rid))
q = q.group_by(group_col[host_function_type])
q = q.filter(DataStore.suid != None)
dt_cols = {
'logs_tgid' : [ DataStore.suid, UnitIDTags.tag, 'count' ],
'logs_su' : [ TGIDTags.tag, DataStore.tgid, 'count' ],
'logs_calls' : [ DataStore.time, SysIDTags.tag, DataStore.tgid, TGIDTags.tag, DataStore.frequency, DataStore.suid ],
'logs_joins' : [ DataStore.time, SysIDTags.tag, LocRegResp.tag, TGIDTags.tag, DataStore.suid ],
'logs_total_tgid' : [ DataStore.sysid, SysIDTags.tag, DataStore.tgid, TGIDTags.tag, 'count' ]
}
if host_function_type == 'cc_event':
mapl = oplog_map[host_function_param]
columns = []
for row in mapl:
col = getattr(DataStore, row[0])
if row[0] == 'sysid':
col = SysIDTags.tag
elif row[1] == 'Talkgroup':
col = TGIDTags.tag
elif row[1] == 'Source' or row[1] == 'Target':
col = UnitIDTags.tag
elif row[0] == 'cc_event':
continue
#col = EventKeys.tag
elif row[0] == 'opcode':
continue
elif host_function_param == 'loc_reg_resp' and row[0] == 'p':
col = LocRegResp.tag
columns.append(col)
column_dt = [ColumnDT(s) for s in columns]
q = db.session.query(*columns
).join(
EventKeys.table_, and_( EventKeys.tag == host_function_param, EventKeys.id == DataStore.cc_event)
).outerjoin(
SysIDTags.table_, DataStore.sysid == SysIDTags.sysid
)
if host_function_param == 'grp_aff_resp':
q = q.outerjoin(
TGIDTags.table_, and_(DataStore.tgid2 == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid)
).outerjoin(
UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid)
)
elif host_function_param == 'ack_resp_fne' or host_function_param == 'grp_aff_q' or host_function_param == 'u_reg_cmd':
q = q.outerjoin(
TGIDTags.table_, and_(DataStore.tgid2 == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid)
).outerjoin(
UnitIDTags.table_, and_(DataStore.suid2 == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid)
)
else:
q = q.outerjoin(
TGIDTags.table_, and_(DataStore.tgid == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid)
).outerjoin(
UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid)
)
if host_function_param == 'loc_reg_resp':
q = q.join(LocRegResp.table_, LocRegResp.rv == DataStore.p)
if host_function_type == 'cc_event':
cl = columns
elif k in dt_cols:
cl = dt_cols[k]
else:
cl = None
# apply tgid and suid filters if present
if host_function_type == 'cc_event':
if filter_tgid is not None and int(filter_tgid) != 0:
q = q.filter(DataStore.tgid == filter_tgid)
if filter_suid is not None and int(filter_suid) != 0:
q = q.filter(DataStore.suid == filter_suid)
if cl:
c = int(params['order[0][column]'])
d = params['order[0][dir]'] # asc or desc
if d == 'asc':
q = q.order_by(cl[c])
else:
q = q.order_by(desc(cl[c]))
q = q.filter(and_(DataStore.time >= int(stime), DataStore.time <= int(etime)))
if sysid != 0:
q = q.filter(DataStore.sysid == sysid)
if host_function_type == 'cc_event':
rowTable = DataTables(params, q, column_dt)
else:
rowTable = DataTables(params, q, column_d[k])
js = jsonify(rowTable.output_result())
# j= 'skipped' # json.dumps(rowTable.output_result(), indent=4, separators=[',', ':'], sort_keys=True)
# with open('data-log', 'a') as logf:
# s = '\n\t'.join(['%s:%s' % (k, params[k]) for k in params.keys()])
# logf.write('keys: %s\n' % (' '.join(params.keys())))
# logf.write('params:\n\t%s\nrequest: %s\n' % (s, function_req))
# logf.write('%s\n' % j)
return js
# switch and backup database file
@app.route("/switch_db")
def switch_db():
params = request.args.to_dict()
params['ekeys'] = sorted(oplog_map.keys())
p = os.getcwd() + '/..'
files = [f for f in listdir(p) if isfile(join(p, f))]
files.sort()
if 'cmd' not in params.keys():
curr_file = app.config['SQLALCHEMY_DATABASE_URI'].split('/')[-1]
return render_template("switch_db.html", params=params, files=files, curr_file=curr_file)
if params['cmd'] == 'backup':
t = strftime("%Y-%m-%d_%H%M%S")
destfile = 'op25-backup-%s.db' % t
src = app.config['SQLALCHEMY_DATABASE_URI'][10:]
s = src.split('/')
curr_file = s[-1]
dst = src.replace(curr_file, destfile)
copyfile(src, dst)
return render_template("switch_db.html", params=params, destfile=destfile, curr_file=curr_file, files=files, sm=1)
if params['cmd'] == 'switch':
new_f = params['file']
database = app.config['SQLALCHEMY_DATABASE_URI']
f = database.split('/')[-1]
new_db = database.replace(f, new_f)
print('switching database to: %s' % new_db)
app.config['SQLALCHEMY_DATABASE_URI'] = new_db
return redirect('/')

File diff suppressed because it is too large Load Diff

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