From 51042858e8658f92fc7e52034975e3f1c983d615 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 25 Jul 2021 20:46:14 -0400 Subject: [PATCH] core rc2-final --- op25/gr-op25_repeater/apps/README-July-2021 | 265 +++++ op25/gr-op25_repeater/apps/cfg-trunkx.json | 98 ++ op25/gr-op25_repeater/apps/color-map.json | 602 +++++++++++ op25/gr-op25_repeater/apps/emap.py | 383 +++++++ op25/gr-op25_repeater/apps/gr_gnuplot.py | 32 +- op25/gr-op25_repeater/apps/http_server.py | 86 +- op25/gr-op25_repeater/apps/install-sql.sh | 28 + op25/gr-op25_repeater/apps/multi_rx.py | 506 ++++++++- op25/gr-op25_repeater/apps/p25_decoder.py | 5 +- op25/gr-op25_repeater/apps/p25_demodulator.py | 11 +- op25/gr-op25_repeater/apps/rx.py | 40 +- op25/gr-op25_repeater/apps/site-alias.json | 12 + op25/gr-op25_repeater/apps/sql_dbi.py | 337 ++++++ op25/gr-op25_repeater/apps/trunk.tsv | 3 +- op25/gr-op25_repeater/apps/trunking.py | 964 ++++++++++++++---- op25/gr-op25_repeater/apps/tsvfile.py | 99 +- op25/gr-op25_repeater/apps/ui-settings.json | 34 + .../include/op25_repeater/frame_assembler.h | 2 +- .../include/op25_repeater/gardner_costas_cc.h | 4 + .../op25_repeater/p25_frame_assembler.h | 2 +- .../lib/frame_assembler_impl.cc | 9 +- .../lib/frame_assembler_impl.h | 3 +- .../lib/gardner_costas_cc_impl.cc | 61 +- .../lib/gardner_costas_cc_impl.h | 17 +- .../lib/p25_frame_assembler_impl.cc | 26 +- .../lib/p25_frame_assembler_impl.h | 3 +- op25/gr-op25_repeater/lib/p25_framer.cc | 16 +- op25/gr-op25_repeater/lib/p25_framer.h | 3 +- op25/gr-op25_repeater/lib/p25p1_fdma.cc | 22 +- op25/gr-op25_repeater/lib/p25p1_fdma.h | 3 +- op25/gr-op25_repeater/lib/p25p2_tdma.cc | 48 +- op25/gr-op25_repeater/lib/p25p2_tdma.h | 6 +- op25/gr-op25_repeater/lib/rx_sync.cc | 22 +- op25/gr-op25_repeater/lib/rx_sync.h | 3 +- 34 files changed, 3407 insertions(+), 348 deletions(-) create mode 100644 op25/gr-op25_repeater/apps/README-July-2021 create mode 100644 op25/gr-op25_repeater/apps/cfg-trunkx.json create mode 100644 op25/gr-op25_repeater/apps/color-map.json create mode 100644 op25/gr-op25_repeater/apps/emap.py create mode 100755 op25/gr-op25_repeater/apps/install-sql.sh mode change 100755 => 100644 op25/gr-op25_repeater/apps/multi_rx.py create mode 100644 op25/gr-op25_repeater/apps/site-alias.json create mode 100755 op25/gr-op25_repeater/apps/sql_dbi.py mode change 100644 => 100755 op25/gr-op25_repeater/apps/tsvfile.py create mode 100644 op25/gr-op25_repeater/apps/ui-settings.json diff --git a/op25/gr-op25_repeater/apps/README-July-2021 b/op25/gr-op25_repeater/apps/README-July-2021 new file mode 100644 index 0000000..615dbb9 --- /dev/null +++ b/op25/gr-op25_repeater/apps/README-July-2021 @@ -0,0 +1,265 @@ + +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 + * Experimental TDMA Control Channel support + +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. + +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=:nac=0x4e1", +The NAC should be changed to match that of the system being received, and + 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 + also, import the radio ID tags file (optional) +op25/.../apps$ python sql_dbi.py import_unit radio-tags.tsv + 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. + diff --git a/op25/gr-op25_repeater/apps/cfg-trunkx.json b/op25/gr-op25_repeater/apps/cfg-trunkx.json new file mode 100644 index 0000000..a004d56 --- /dev/null +++ b/op25/gr-op25_repeater/apps/cfg-trunkx.json @@ -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 + } + ] +} diff --git a/op25/gr-op25_repeater/apps/color-map.json b/op25/gr-op25_repeater/apps/color-map.json new file mode 100644 index 0000000..ad90812 --- /dev/null +++ b/op25/gr-op25_repeater/apps/color-map.json @@ -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 + ] +] \ No newline at end of file diff --git a/op25/gr-op25_repeater/apps/emap.py b/op25/gr-op25_repeater/apps/emap.py new file mode 100644 index 0000000..597c38c --- /dev/null +++ b/op25/gr-op25_repeater/apps/emap.py @@ -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 - 0x2B", + "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" +} diff --git a/op25/gr-op25_repeater/apps/gr_gnuplot.py b/op25/gr-op25_repeater/apps/gr_gnuplot.py index 6160132..3104560 100644 --- a/op25/gr-op25_repeater/apps/gr_gnuplot.py +++ b/op25/gr-op25_repeater/apps/gr_gnuplot.py @@ -58,7 +58,7 @@ def limit(a,lim): PSEQ = 0 class wrap_gp(object): - def __init__(self, sps=_def_sps, logfile=None, title=None, color_cfg='plot-colors.json'): + def __init__(self, sps=_def_sps, logfile=None, title="", color_cfg='plot-colors.json'): global PSEQ self.sps = sps self.center_freq = 0.0 @@ -270,16 +270,16 @@ class wrap_gp(object): h += 'set format ""\n' h += 'set style line 11 lt 1 lw 2 pt 2 ps 2\n' - h+= 'set title "Constellation" %s\n' % (label_color) + 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\n' % (label_color) + h+= 'set title "Datascope %s" %s\n' % (self.title, label_color) plot_color = '' elif mode == 'symbol': h+= background h+= 'set yrange [-4:4]\n' - h+= 'set title "Symbol" %s\n' % (label_color) + 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' @@ -289,9 +289,9 @@ class wrap_gp(object): h+= 'set grid\n' h+= 'set yrange [-100:0]\n' if mode == 'mixer': # mixer - h+= 'set title "Mixer: balance %3.0f (smaller is better)" %s\n' % (np.abs(self.avg_sum_pwr * 1000), label_color) + 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\n' % (label_color) + 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) @@ -299,7 +299,7 @@ class wrap_gp(object): elif mode == 'float': h+= background h+= 'set yrange [-2:2]\n' - h+= 'set title "Oscilloscope" %s\n' % (label_color) + h+= 'set title "Oscilloscope %s" %s\n' % (self.title, label_color) elif mode == 'correlation': h+= background title = 'Correlation' @@ -358,6 +358,9 @@ class eye_sink_f(gr.sync_block): 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() @@ -377,6 +380,9 @@ class constellation_sink_c(gr.sync_block): 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() @@ -400,6 +406,9 @@ class fft_sink_c(gr.sync_block): 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() @@ -436,6 +445,9 @@ class mixer_sink_c(gr.sync_block): 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() @@ -455,6 +467,9 @@ class symbol_sink_f(gr.sync_block): 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() @@ -474,6 +489,9 @@ class float_sink_f(gr.sync_block): 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() diff --git a/op25/gr-op25_repeater/apps/http_server.py b/op25/gr-op25_repeater/apps/http_server.py index abb95ab..2650d70 100755 --- a/op25/gr-op25_repeater/apps/http_server.py +++ b/op25/gr-op25_repeater/apps/http_server.py @@ -37,6 +37,9 @@ 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 @@ -57,9 +60,41 @@ def ensure_str(s): # for python 2/3 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 = { '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'} + 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: @@ -68,10 +103,12 @@ def static_file(environ, start_response): 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' + status = '404 NOT FOUND - PATHNAME: %s FILENAME: %s CWD: %s' % (pathname, filename, os. getcwd()) content_type = 'text/plain' output = status else: @@ -152,6 +189,32 @@ def do_request(d): 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 @@ -178,17 +241,20 @@ def post_req(environ, start_response, postdata): my_output_q.insert_tail(msg) time.sleep(0.2) - while not my_recv_q.empty_p(): - msg = my_recv_q.delete_head() - if msg.type() == -4: - resp_msg.append(json.loads(msg.to_string())) 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': + 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() @@ -200,6 +266,7 @@ def http_request(environ, start_response): 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) @@ -236,9 +303,10 @@ class http_server(object): my_port = int(port) my_recv_q = gr.msg_queue(10) - self.q_watcher = queue_watcher(my_input_q, process_qmsg) - self.server = create_server(application, host=host, port=my_port) + 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() diff --git a/op25/gr-op25_repeater/apps/install-sql.sh b/op25/gr-op25_repeater/apps/install-sql.sh new file mode 100755 index 0000000..17008c1 --- /dev/null +++ b/op25/gr-op25_repeater/apps/install-sql.sh @@ -0,0 +1,28 @@ +#! /bin/sh + +PIP3=`which pip3` +USERDIR=~/.local/bin + +sudo apt-get install python3-pip + +# # # # # # 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, checking version... +$PIP3 --version + +$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" + diff --git a/op25/gr-op25_repeater/apps/multi_rx.py b/op25/gr-op25_repeater/apps/multi_rx.py old mode 100755 new mode 100644 index 308afc6..3b88d40 --- a/op25/gr-op25_repeater/apps/multi_rx.py +++ b/op25/gr-op25_repeater/apps/multi_rx.py @@ -24,6 +24,7 @@ import sys import threading import time import json +import select import traceback import osmosdr @@ -32,10 +33,15 @@ 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 @@ -46,9 +52,18 @@ from gr_gnuplot import setup_correlation 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 # @@ -71,7 +86,9 @@ class device(object): 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:'): self.init_audio(config) @@ -134,15 +151,48 @@ class device(object): 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): + 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 = 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, @@ -159,11 +209,34 @@ class channel(object): offset = dev.offset, if_rate = config['if_rate'], symbol_rate = self.symbol_rate) - if msgq is None: - q = gr.msg_queue(1) - else: + if msgq is not None: q = msgq - self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q) + 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) + + 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 = [] @@ -175,36 +248,46 @@ class channel(object): for g in config['whitelist'].split(','): self.decoder.insert_whitelist(int(g)) + self.sinks = [] if 'plot' not in config.keys(): return - self.sinks = [] 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 == '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': assert config['demod_type'] == 'cqpsk' ## fft plot requires cqpsk demod type i = len(self.sinks) - self.sinks.append(fft_sink_c()) + 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': assert config['demod_type'] == 'cqpsk' ## mixer plot requires cqpsk demod type i = len(self.sinks) - self.sinks.append(mixer_sink_c()) + sink = mixer_sink_c() + sink.set_title(self.name) + self.sinks.append(sink) self.demod.connect_complex('mixer', 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 - self.sinks.append(constellation_sink_c()) + 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': @@ -218,6 +301,85 @@ class channel(object): 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) + relative_freq = self.device.frequency + self.device.offset + self.tuning_error - frequency + if (not self.device.tunable) 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() + 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) @@ -238,26 +400,239 @@ class rx_block (gr.top_block): # Initialize the receiver # - def __init__(self, verbosity, config): + 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.msgq = gr.msg_queue(10) + 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']) - self.du_q = du_queue_watcher(self.msgq, self.process_qmsg) + 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) - def process_qmsg(self, msg): - t = msg.type() - s = msg.to_string() - if t != -5: # verify nxdn type + 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_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 + 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 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_qmsg: nxdn msg %s lich %x\n' % (msgtype, lich)) + 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:]) @@ -266,13 +641,40 @@ class rx_block (gr.top_block): 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 + 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_device(self, chan): + 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 @@ -285,17 +687,41 @@ class rx_block (gr.top_block): def configure_channels(self, config): self.channels = [] for cfg in config: - dev = self.find_device(cfg) + 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('* * * Frequency %d not within spectrum band of any device - ignoring!\n' % cfg['frequency']) + sys.stderr.write('* * * No device found for channel %s- ignoring!\n' % cfg['name']) continue - chan = channel(cfg, dev, self.verbosity, msgq=self.msgq) + 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'])) + chan.demod.connect_complex('agc', 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())) @@ -309,8 +735,15 @@ class rx_main(object): 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(),)) @@ -320,18 +753,31 @@ class rx_main(object): config = json.loads(sys.stdin.read()) else: config = json.loads(open(options.config_file).read()) - self.tb = rx_block(options.verbosity, config = byteify(config)) + 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): - try: - self.tb.start() - while self.keep_running: - time.sleep(1) - except: - sys.stderr.write('main: exception occurred\n') - sys.stderr.write('main: exception:\n%s\n' % traceback.format_exc()) + 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() - rx.run() + try: + rx.run() + except KeyboardInterrupt: + rx.keep_running = False + print('Program ending') + time.sleep(1) diff --git a/op25/gr-op25_repeater/apps/p25_decoder.py b/op25/gr-op25_repeater/apps/p25_decoder.py index 54e843d..be60627 100644 --- a/op25/gr-op25_repeater/apps/p25_decoder.py +++ b/op25/gr-op25_repeater/apps/p25_decoder.py @@ -59,7 +59,8 @@ class p25_decoder_sink_b(gr.hier_block2): do_msgq = False, msgq = None, audio_output = _def_audio_output, - debug = _def_debug): + debug = _def_debug, + msgq_id = 0): """ Hierarchical block for P25 decoding. @@ -100,7 +101,7 @@ class p25_decoder_sink_b(gr.hier_block2): if num_ambe > 1: num_decoders += num_ambe - 1 for slot in range(num_decoders): - self.p25_decoders.append(op25_repeater.p25_frame_assembler(wireshark_host, udp_port, debug, do_imbe, do_output, do_msgq, msgq, do_audio_output, do_phase2_tdma)) + self.p25_decoders.append(op25_repeater.p25_frame_assembler(wireshark_host, udp_port, debug, do_imbe, do_output, do_msgq, msgq, do_audio_output, do_phase2_tdma, msgq_id+slot)) self.p25_decoders[slot].set_slotid(slot) self.xorhash.append('') diff --git a/op25/gr-op25_repeater/apps/p25_demodulator.py b/op25/gr-op25_repeater/apps/p25_demodulator.py index 64c8eb0..65bcd7d 100644 --- a/op25/gr-op25_repeater/apps/p25_demodulator.py +++ b/op25/gr-op25_repeater/apps/p25_demodulator.py @@ -268,6 +268,7 @@ class p25_demod_cb(p25_demod_base): self.bpf = filter.fir_filter_ccc(self.decim, bpf_coeffs) self.lpf = filter.fir_filter_ccf(self.decim2, lpf_coeffs) resampled_rate = self.if2 + self.relative_limit = (input_rate // 2) - (self.if1 // 2) self.bfo = analog.sig_source_c (self.if1, analog.GR_SIN_WAVE, 0, 1.0, 0) self.connect(self, self.bpf, (self.mixer, 0)) self.connect(self.bfo, (self.mixer, 1)) @@ -284,10 +285,12 @@ class p25_demod_cb(p25_demod_base): decimation = int(input_rate / if_rate) self.lpf = filter.fir_filter_ccf(decimation, lpf_coeffs) resampled_rate = float(input_rate) / float(decimation) # rate at output of self.lpf + self.relative_limit = (input_rate // 2) - f1 self.connect(self, (self.mixer, 0)) self.connect(self.lo, (self.mixer, 1)) self.connect(self.mixer, self.lpf) + if self.if_rate != resampled_rate: self.if_out = filter.pfb.arb_resampler_ccf(float(self.if_rate) / resampled_rate) self.connect(self.lpf, self.if_out) @@ -335,6 +338,12 @@ class p25_demod_cb(p25_demod_base): def get_freq_error(self): # get error in Hz (approx). return int(self.clock.get_freq_error() * self.symbol_rate) + def is_muted(self): + return self.clock.is_muted() + + def set_muted(self, v): + self.clock.set_muted(v) + def set_omega(self, omega): sps = self.if_rate / float(omega) if sps == self.sps: @@ -344,7 +353,7 @@ class p25_demod_cb(p25_demod_base): self.clock.set_omega(self.sps) def set_relative_frequency(self, freq): - if abs(freq) > ((self.input_rate / 2) - (self.if1 / 2)): + if abs(freq) > self.relative_limit: #print 'set_relative_frequency: error, relative frequency %d exceeds limit %d' % (freq, self.input_rate/2) return False if freq == self.lo_freq: diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index 59af5fb..300da9b 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -131,6 +131,7 @@ class p25_rx_block (gr.top_block): self.last_change_freq_at = time.time() self.last_freq_params = {'freq' : 0.0, 'tgid' : None, 'tag' : "", 'tdma' : None} self.next_status_png = time.time() + self.last_process_update = 0 self.src = None if (not options.input) and (not options.audio) and (not options.audio_if) and (not options.args.startswith('udp:')): @@ -210,7 +211,7 @@ class p25_rx_block (gr.top_block): print ('Ready for GDB to attach (pid = %d)' % (os.getpid(),)) raw_input("Press 'Enter' to continue...") - self.input_q = gr.msg_queue(10) + self.input_q = gr.msg_queue(20) self.output_q = gr.msg_queue(10) # configure specified data source @@ -346,7 +347,7 @@ class p25_rx_block (gr.top_block): logfile_workers.append({'demod': demod, 'decoder': decoder, 'active': False}) self.connect(source, demod, decoder) - self.trunk_rx = trunking.rx_ctl(frequency_set = self.change_freq, debug = self.options.verbosity, conf_file = self.options.trunk_conf_file, logfile_workers=logfile_workers) + self.trunk_rx = trunking.rx_ctl(frequency_set = self.change_freq, debug = self.options.verbosity, conf_file = self.options.trunk_conf_file, logfile_workers=logfile_workers, send_event=self.send_event) self.du_watcher = du_queue_watcher(self.rx_q, self.trunk_rx.process_qmsg) @@ -388,6 +389,7 @@ class p25_rx_block (gr.top_block): 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 @@ -477,6 +479,7 @@ class p25_rx_block (gr.top_block): params = self.last_freq_params params['json_type'] = 'change_freq' params['fine_tune'] = self.options.fine_tune + 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) @@ -673,10 +676,30 @@ class p25_rx_block (gr.top_block): error = None if self.options.demod_type == 'cqpsk': error = self.demod.get_freq_error() - d = {'json_type': 'rx_update', 'error': error, 'fine_tune': self.options.fine_tune, 'files': filenames} + d = {'json_type': 'rx_update', 'error': error, 'fine_tune': self.options.fine_tune, '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.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 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 process_qmsg(self, msg): # return true = end top block RX_COMMANDS = 'skip lockout hold'.split() @@ -689,14 +712,9 @@ class p25_rx_block (gr.top_block): # should only get here if python3 s = s.decode() if s == 'quit': return True - elif s == 'update': - self.freq_update() - if self.trunk_rx is None: - return False ## 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() + elif s == 'update': ## deprecated here: to be removed + pass + # self.process_update() elif s == 'set_freq': freq = msg.arg1() self.last_freq_params['freq'] = freq diff --git a/op25/gr-op25_repeater/apps/site-alias.json b/op25/gr-op25_repeater/apps/site-alias.json new file mode 100644 index 0000000..63cfb94 --- /dev/null +++ b/op25/gr-op25_repeater/apps/site-alias.json @@ -0,0 +1,12 @@ +{ + "552": { + "1": { + "1": { + "alias": "new1" + }, + "2": { + "alias": "new2" + } + } + } +} \ No newline at end of file diff --git a/op25/gr-op25_repeater/apps/sql_dbi.py b/op25/gr-op25_repeater/apps/sql_dbi.py new file mode 100755 index 0000000..0dc7cdc --- /dev/null +++ b/op25/gr-op25_repeater/apps/sql_dbi.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python + +# Copyright 2021 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 json +import threading +import traceback +import sqlite3 + +from gnuradio import gr + +from emap import events_map, cc_events + +_def_db_file = 'op25-data.db' +_def_msgq_size = 100 +_def_uncommitted = 25 + +class du_queue_runner(threading.Thread): + def __init__(self, msgq, **kwds): + threading.Thread.__init__ (self, **kwds) + self.setDaemon(1) + self.msgq = msgq + self.db_filename = _def_db_file + self.conn = None + self.cursor = None + self.failed = False + self.keep_running = True + self.uncommitted = 0 + self.max_q = 0 + self.start() + self.next_t = time.time() + + def run(self): + self.connect() + while self.keep_running and not self.failed: + self.max_q = max(self.max_q, self.msgq.count()) + if time.time() > self.next_t: + self.next_t = time.time() + 2 + msg = self.msgq.delete_head() + if self.failed or not self.keep_running: + break + self.insert_row(msg) + + def disconnect(self): + self.conn.close() + self.cursor = None + self.conn = None + + def connect(self): + self.conn = sqlite3.connect(self.db_filename) + self.cursor = self.conn.cursor() + + def insert_row(self, msg): + if self.cursor is None or self.conn is None: + return + d = json.loads(msg.to_string()) + try: + self.cursor.execute(d['command'], d['row']) + # optimization: only commit when no more msgs queued (or limit reached) + if self.uncommitted < _def_uncommitted and self.msgq.count(): + self.uncommitted += 1 + else: + self.conn.commit() + self.uncommitted = 0 + except: + self.failed = True + traceback.print_exc(limit=None, file=sys.stdout) + traceback.print_exc(limit=None, file=sys.stderr) + sys.stderr.write('sql_dbi: db logging stopped due to error (or db not initialized)\n') + # TODO - add error recovery? + +class sql_dbi: + def __init__(self, db_filename=_def_db_file): + self.conn = None + self.cursor = None + self.db_filename = db_filename + self.db_msgq = gr.msg_queue(_def_msgq_size) + self.q_runner = du_queue_runner(self.db_msgq) + self.db_msgq_overflow = 0 + + self.sql_commands = { + 'calls': 'INSERT INTO calls(time, sysid, options, tgid, srcid)', + 'joins': 'INSERT INTO joins(time, sysid, rv, tgid, srcid)', + 'create_data_store': '''CREATE TABLE data_store ( + id INTEGER PRIMARY KEY, + time REAL NOT NULL, + cc_event INTEGER NOT NULL, + opcode INTEGER NOT NULL, + sysid INTEGER NOT NULL, + mfrid INTEGER NULL, + p INTEGER NULL, + p2 INTEGER NULL, + p3 INTEGER NULL, + wacn INTEGER NULL, + frequency INTEGER NULL, + tgid INTEGER NULL, + tgid2 INTEGER NULL, + suid INTEGER NULL, + suid2 INTEGER NULL, + tsbk_sysid INTEGER NULL, + FOREIGN KEY(cc_event) REFERENCES event_keys (id))''', + 'create_event_keys': '''CREATE TABLE event_keys ( + id INTEGER PRIMARY KEY, + tag TEXT NOT NULL )''', + 'create_sysid': '''CREATE TABLE sysid_tags ( + id INTEGER PRIMARY KEY, + sysid INTEGER NOT NULL, + tag TEXT)''', + 'create_tgid': '''CREATE TABLE tgid_tags ( + id INTEGER PRIMARY KEY, + rid INTEGER NOT NULL, + sysid INTEGER NOT NULL, + tag TEXT, + priority INTEGER)''', + 'create_unit_id': '''CREATE TABLE unit_id_tags ( + id INTEGER PRIMARY KEY, + rid INTEGER NOT NULL, + sysid INTEGER NOT NULL, + tag TEXT, + priority INTEGER)''', + 'create_2b_rv': '''CREATE TABLE loc_reg_resp_rv ( + rv INTEGER NOT NULL, + tag TEXT NOT NULL)''', + 'populate_2b_rv': '''INSERT INTO loc_reg_resp_rv(rv, tag) VALUES(0, "join") + INSERT INTO loc_reg_resp_rv(rv, tag) VALUES(1, "fail") + INSERT INTO loc_reg_resp_rv(rv, tag) VALUES(2, "deny") + INSERT INTO loc_reg_resp_rv(rv, tag) VALUES(3, "refuse")''', + 'create_index': '''CREATE INDEX tgid_idx ON data_store(tgid) + CREATE INDEX tgid2_idx ON data_store(tgid2) + CREATE INDEX suid_idx ON data_store(suid) + CREATE INDEX suid2_idx ON data_store(suid2) + CREATE INDEX t_tgid_idx ON tgid_tags(rid) + CREATE INDEX t_unit_id_idx ON unit_id_tags(rid)''' + } + + def disconnect(self): + self.conn.close() + self.cursor = None + self.conn = None + + def connect(self): + self.conn = sqlite3.connect(self.db_filename) + self.cursor = self.conn.cursor() + + def reset_db(self): # any data in db will be destroyed! + if os.access(self.db_filename, os.W_OK): + os.remove(self.db_filename) + self.conn = sqlite3.connect(self.db_filename) + self.cursor = self.conn.cursor() + self.execute('create_sysid') + self.execute('create_2b_rv') + self.execute_lines('populate_2b_rv') + self.execute('create_tgid') + self.execute('create_unit_id') + self.execute('create_event_keys') + self.execute('create_data_store') + self.execute_lines('create_index') + self.conn.commit() + self.populate_event_keys() + self.conn.close() + + def execute(self, q): + self.cursor.execute(self.sql_commands[q]) + self.conn.commit() + + def execute_lines(self,q): + for line in self.sql_commands[q].split('\n'): + self.cursor.execute(line) + self.conn.commit() + + def q(self, query): + if query != '-': + return self.cursor.execute(query) + lines = sys.stdin.read().strip().split('\n') + for query in lines: + self.cursor.execute(query) + self.conn.commit() + return None + + def write(self, table_name, row): + # the number of elements in tuple 'row' must be two less than the number of table columns + curr_time = time.time() + row = (curr_time,) + row + qs = ['?'] * len(row) + command = self.sql_commands[table_name] + ' VALUES (' + ','.join(qs) + ')' + self.cursor.execute(command, row) + self.conn.commit() + + def event(self, d): + if d['cc_event'] not in events_map: + return + if not os.access(_def_db_file, os.W_OK): # if DB not (yet) set up or not writable + return + mapl = events_map[d['cc_event']] + row = [] + column_names = [] + for col in mapl: + colname = col[0] + k = col[1] + # special mappings: unwrap tgid and srcid objects + if colname.startswith('tgid') and type(d[k]) is dict: + val = d[k]['tg_id'] + elif colname.startswith('suid') and type(d[k]) is dict: + val = d[k]['unit_id'] + elif type(d[k]) is not dict: + val = d[k] + else: + sys.stderr.write('value retrieval error %s %s %s\n' % (d['cc_event'], type(d[k]) is dict, k)) + val = -1 + # special mappings: map cc_event tag to an int + if colname == 'cc_event': + val = cc_events[d[k]] + # special mappings: map affiliation to int + if k == 'affiliation': + if d[k] == 'global': + val = 1 + elif d[k] == 'local': + val = 0 + else: + val = -1 + # special mappings: map duration to int(msec). + if k == 'duration': + val = int(d[k] * 1000) + row.append(val) + column_names.append(colname) + command = "INSERT INTO data_store(%s) VALUES(%s)" % (','.join(column_names), ','.join(['?'] * len(row))) + js = json.dumps({'command': command, 'row': row}) + if not self.db_msgq.full_p(): + msg = gr.message().make_from_string(js, 0, 0, 0) + self.db_msgq.insert_tail(msg) + else: + self.db_msgq_overflow += 1 + + def import_tsv(self, argv): + cmd = argv[1] + filename = argv[2] + sysid = int(argv[3]) + if cmd == 'import_tgid': + table = 'tgid_tags' + elif cmd == 'import_unit': + table = 'unit_id_tags' + elif cmd == 'import_sysid': + table = 'sysid_tags' + else: + print('%s unsupported' % (cmd)) + return + q = 'INSERT INTO ' + table + '(rid, sysid, tag, priority) VALUES(?,?,?,?)' + if table == 'sysid_tags': + q = 'INSERT INTO ' + table + '(sysid, tag) VALUES(?,?)' + 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 + rid = int(a[0]) + tag = a[1] + priority = 0 if len(a) < 3 else int(a[2]) + s = (rid, sysid, tag, priority) + if table == 'sysid_tags': + s = (rid, tag) + rows.append(s) + if len(rows): + self.cursor.executemany(q, rows) + self.conn.commit() + + def populate_event_keys(self): + d = {cc_events[k]:k for k in cc_events} + query = 'INSERT INTO event_keys(id, tag) VALUES(?, ?)' + for k in sorted(d.keys()): + self.cursor.execute(query, [k, d[k]]) + self.conn.commit() + +def main(): + if len(sys.argv) > 1 and sys.argv[1] == 'reset_db': + sql_dbi().reset_db() + return + + db1 = sql_dbi() + db1.connect() + + if len(sys.argv) > 1 and sys.argv[1] == 'setup': + db1.cursor.execute(db1.sql_commands['create_tgid']) + db1.cursor.execute(db1.sql_commands['create_unit_id']) + db1.conn.commit() + db1.conn.close() + return + + if len(sys.argv) > 1 and sys.argv[1] == 'execute_lines': + db1.execute_lines(sys.argv[2]) + return + + if len(sys.argv) > 1 and sys.argv[1] == 'execute': + db1.execute(sys.argv[2]) + return + + if len(sys.argv) > 1 and sys.argv[1] == 'insert': + db1.write('joins', (555, 5555, 5555555)) + return + + if len(sys.argv) > 3 and sys.argv[1].startswith('import_'): + db1.import_tsv(sys.argv) + return + + if len(sys.argv) < 3 or sys.argv[1] != 'query': + print('nothing done') + return + + result = db1.q(sys.argv[2]) + if result: + for row in result: + print ('%s' % ('\t'.join([str(x) for x in row]))) + +if __name__ == '__main__': + main() diff --git a/op25/gr-op25_repeater/apps/trunk.tsv b/op25/gr-op25_repeater/apps/trunk.tsv index 2bcc560..8305356 100644 --- a/op25/gr-op25_repeater/apps/trunk.tsv +++ b/op25/gr-op25_repeater/apps/trunk.tsv @@ -3,5 +3,6 @@ "Cortland" "454.05" "0" "0x4e1" "CQPSK" "Onondaga" "460.5" "0" "0x2a0" "CQPSK" "onondaga.tsv" "Cayuga" "460.4125" "0" "0x2a8" "CQPSK" "onondaga.tsv" -"Ontario" "769.55625,769.85625,770.10625" "0" "0x47f" "CQPSK" "ontario.tsv" +"Ontario" "769.28125,769.55625,769.85625,770.10625" "0" "0x47f" "CQPSK" "ontario.tsv" +"460.375" "460.375" "0" "0x2a4" "CQPSK" "onondaga.tsv" "NYSEG" "152.0225" "0" "0x260" "C4FM" "nyseg.tsv" diff --git a/op25/gr-op25_repeater/apps/trunking.py b/op25/gr-op25_repeater/apps/trunking.py index d03da65..bcf70bd 100644 --- a/op25/gr-op25_repeater/apps/trunking.py +++ b/op25/gr-op25_repeater/apps/trunking.py @@ -27,9 +27,11 @@ import collections import json sys.path.append('tdma') import lfsr -from tsvfile import make_config, load_tsv +from tsvfile import make_config, load_tsv, id_registry, read_tags_file from create_image import create_image +FILTERED_CC_EVENT = 'mot_grg_add_cmd grp_v_ch_grant_updt grp_v_ch_grant_updt_exp'.split() + def crc16(dat,len): # slow version poly = (1<<12) + (1<<5) + (1<<0) crc = 0 @@ -54,7 +56,7 @@ def get_ordinals(s): return t class trunked_system (object): - def __init__(self, debug=0, config=None): + def __init__(self, debug=0, config=None, send_event=None, nac=None): self.debug = debug self.freq_table = {} self.stats = {} @@ -75,9 +77,12 @@ class trunked_system (object): self.voice_frequencies = {} self.blacklist = {} self.whitelist = None - self.tgid_map = {} + self.tgid_map = None + self.unit_id_map = None self.offset = 0 self.sysname = 0 + self.tgid_tags_tsv = "" + self.unit_id_tags_tsv = "" self.trunk_cc = 0 self.last_trunk_cc = 0 @@ -87,12 +92,19 @@ class trunked_system (object): self.center_frequency = 0 self.last_tsbk = 0 self.cc_timeouts = 0 + self.next_hunt_cc = time.time() + 8.0 + self.last_voice_time = 0.0 self.talkgroups = {} + self.frequency_table = {} + self.CALL_TIMEOUT = 0.7 # call expiration time (sec.) + self.CHECK_INTERVAL = 0.1 # freq tracking check interval + self.next_frequency_tracking_expire = 0 if config: self.blacklist = config['blacklist'] self.whitelist = config['whitelist'] self.tgid_map = config['tgid_map'] + self.unit_id_map = config['unit_id_map'] self.offset = config['offset'] self.sysname = config['sysname'] self.trunk_cc = config['cclist'][0] # TODO: scan thru list @@ -105,8 +117,11 @@ class trunked_system (object): self.current_alg = "" self.current_algid = 128 self.current_keyid = 0 + self.send_event = send_event + self.harris_sgs = {} + self.nac = nac - def to_json(self): + def to_dict(self): d = {} d['syid'] = self.rfss_syid d['sysname'] = self.sysname @@ -132,8 +147,17 @@ class trunked_system (object): d['frequencies'][f] = 'voice frequency %f tgid(s) %s %4.1fs ago count %d' % (f / 1000000.0, tgs, t - self.voice_frequencies[f]['time'], self.voice_frequencies[f]['counter']) d['frequency_data'][f] = {'tgids': self.voice_frequencies[f]['tgid'], 'last_activity': '%7.1f' % (t - self.voice_frequencies[f]['time']), 'counter': self.voice_frequencies[f]['counter']} + for s in 'srcaddr tg_tag tg_color srcaddr_tag srcaddr_color'.split(): + d['frequency_data'][f][s] = self.voice_frequencies[f][s] d['adjacent_data'] = self.adjacent_data - return json.dumps(d) + d['talkgroup_data'] = self.talkgroups + self.frequency_tracking_expire(always=True) + d['frequency_tracking'] = self.frequency_table + d['harris_supergroups'] = self.harris_sgs + return d + + def to_json(self): + return json.dumps(self.to_dict()) def to_string(self): s = [] @@ -157,6 +181,24 @@ class trunked_system (object): s.append('adjacent %f: %s' % (float(f) / 1000000.0, self.adjacent[f])) return '\n'.join(s) + def post_event(self, d): + if self.send_event is None: + return + if d['cc_event'] in FILTERED_CC_EVENT: + return + d['json_type'] = 'cc_event' + d['sysid'] = self.rfss_syid if self.rfss_syid else 0 + d['sysname'] = self.sysname + d['time'] = time.time() + d['nac'] = self.nac + self.send_event(d) + + def mk_tg_dict(self, tgid): + return {'tg_id': tgid, 'tag': self.get_tag(tgid), 'priority': self.get_prio(tgid), 'color': self.get_tag_color(tgid)} + + def mk_src_dict(self, srcaddr): + return {'unit_id': srcaddr, 'tag': self.get_unit_id_tag(srcaddr), 'color': self.get_unit_id_color(srcaddr)} + def get_tdma_slot(self, id): table = (id >> 12) & 0xf channel = id & 0xfff @@ -182,17 +224,132 @@ class trunked_system (object): return "ID-0x%x" % (id) return "%f" % (f / 1000000.0) + def get_unit_id_color(self, unit_id): + if not unit_id: + return 0 + elif self.unit_id_map is None: + return 0 + return self.unit_id_map.get_color(unit_id) + + def get_unit_id_tag(self, unit_id): + if not unit_id: + return "" + elif self.unit_id_map is None: + return "Unit %d" % unit_id + return self.unit_id_map.get_tag(unit_id) + def get_tag(self, tgid): if not tgid: return "" - if tgid not in self.tgid_map: - return "Talkgroup ID %d [0x%x]" % (tgid, tgid) - return self.tgid_map[tgid][0] + elif self.tgid_map is None: + return "Talkgroup %d" % tgid + return self.tgid_map.get_tag(tgid) + def get_tag_color(self, tgid): + if not tgid: + return 0 + elif self.tgid_map is None: + return 0 + return self.tgid_map.get_color(tgid) % 100 + + # the third col in the tsv file performs two separate roles + # low-order 2 decimal digits are taken as the tag color code + # remaining upper digits are priority - this is incompatibile + # with previous tsv files (if priorities are in use). def get_prio(self, tgid): - if (not tgid) or (tgid not in self.tgid_map): - return 3 - return self.tgid_map[tgid][1] + if not tgid: + return 0 + elif self.tgid_map is None: + return 0 + return int(self.tgid_map.get_color(tgid) // 100) + + def end_call(self, call, code): + d = {'cc_event': 'end_call', 'code': code, 'srcaddr': call['srcaddr'], 'tgid': call['tgid'], 'duration': call['end_time'] - call['start_time'], 'count': call['count'], 'opcode': -1 } + self.post_event(d) + return + + def frequency_tracking_expire(self, always=False): + current_time = time.time() + if current_time < self.next_frequency_tracking_expire and not always: + return + self.next_frequency_tracking_expire = current_time + self.CHECK_INTERVAL + for f in self.frequency_table.keys(): + freq = self.frequency_table[f] + for i in range(len(freq['calls'])): + call = freq['calls'][i] + if call is not None and call['end_time'] == 0 and call['last_active'] + self.CALL_TIMEOUT < current_time: + call['end_time'] = current_time + self.end_call(call, 1) + + def frequency_tracking(self, frequency, tgid, tdma_slot, srcaddr, protected): + current_time = time.time() + is_tdma = tdma_slot is not None + slot = tdma_slot if is_tdma else 0 + if frequency not in self.frequency_table.keys(): + self.frequency_table[frequency] = {'counter':0, + 'calls': [None,None], + 'tgids': [None,None], + 'last_active': current_time} + self.frequency_table[frequency]['tdma'] = is_tdma + call = {'srcaddr': self.mk_src_dict(srcaddr), + 'protected': protected, + 'tgid': self.mk_tg_dict(tgid), + 'count': 0, + 'start_time': current_time, + 'last_active': current_time, + 'end_time': 0} + + self.frequency_table[frequency]['tgids'][slot] = tgid + self.frequency_table[frequency]['calls'][slot] = call + return + + self.frequency_table[frequency]['counter'] += 1 + self.frequency_table[frequency]['last_active'] = current_time + found = 0 + for f in self.frequency_table.keys(): + freq = self.frequency_table[f] + for i in range(len(freq['tgids'])): + tg = freq['tgids'][i] + call = freq['calls'][i] + if tg is None or call is None or tg != tgid: + continue + # general housekeeping: expire calls + if call['end_time'] == 0 and call['last_active'] + self.CALL_TIMEOUT < current_time: + call['end_time'] = current_time + self.end_call(call, 2) + if f == frequency and freq['tdma'] == is_tdma and i == slot : + found = 1 + call['last_active'] = current_time + call['end_time'] = 0 + call['count'] += 1 + if srcaddr is not None: + call['srcaddr'] = self.mk_src_dict(srcaddr) + if protected is not None: + call['protected'] = protected + else: # found other entry with matching tgid but freq and/or tdma is wrong + if call['end_time'] == 0: + call['end_time'] = current_time + self.end_call(call, 3) + if found: + return + + call = {'srcaddr': self.mk_src_dict(srcaddr), + 'protected': protected, + 'tgid': self.mk_tg_dict(tgid), + 'count': 0, + 'start_time': current_time, + 'last_active': current_time, + 'end_time': 0} + self.frequency_table[frequency]['tdma'] = is_tdma + self.frequency_table[frequency]['tgids'][slot] = tgid + self.frequency_table[frequency]['calls'][slot] = call + if not is_tdma: + self.frequency_table[frequency]['tgids'][1] = None + call = self.frequency_table[frequency]['calls'][1] + if call and call['end_time'] == 0: + call['end_time'] = current_time + self.end_call(call, 4) + self.frequency_table[frequency]['calls'][1] = None def update_talkgroup(self, frequency, tgid, tdma_slot, srcaddr): if self.debug >= 5: @@ -205,12 +362,22 @@ class trunked_system (object): self.talkgroups[tgid]['time'] = time.time() self.talkgroups[tgid]['frequency'] = frequency self.talkgroups[tgid]['tdma_slot'] = tdma_slot - self.talkgroups[tgid]['srcaddr'] = srcaddr self.talkgroups[tgid]['prio'] = self.get_prio(tgid) + self.talkgroups[tgid]['tag_color'] = self.get_tag_color(tgid) - def update_voice_frequency(self, frequency, tgid=None, tdma_slot=None, srcaddr=0): + if srcaddr is None or not srcaddr: + self.talkgroups[tgid]['srcaddr'] = 0 + self.talkgroups[tgid]['srcaddr_tag'] = "" + self.talkgroups[tgid]['srcaddr_color'] = 0 + else: + self.talkgroups[tgid]['srcaddr'] = srcaddr + self.talkgroups[tgid]['srcaddr_tag'] = self.get_unit_id_tag(srcaddr) + self.talkgroups[tgid]['srcaddr_color'] = self.get_unit_id_color(srcaddr) + + def update_voice_frequency(self, frequency, tgid=None, tdma_slot=None, srcaddr=None, protected=None): if not frequency: # e.g., channel identifier not yet known return + self.frequency_tracking(frequency, tgid, tdma_slot, srcaddr, protected) self.update_talkgroup(frequency, tgid, tdma_slot, srcaddr) if frequency not in self.voice_frequencies: self.voice_frequencies[frequency] = {'counter':0} @@ -221,11 +388,20 @@ class trunked_system (object): if tdma_slot is None: tdma_slot = 0 - if 'tgid' not in self.voice_frequencies[frequency]: - self.voice_frequencies[frequency]['tgid'] = [None, None] + + for s in 'tgid srcaddr tg_tag tg_color srcaddr_tag srcaddr_color'.split(): + if s not in self.voice_frequencies[frequency].keys(): + self.voice_frequencies[frequency][s] = [None, None] + self.voice_frequencies[frequency]['tgid'][tdma_slot] = tgid self.voice_frequencies[frequency]['counter'] += 1 self.voice_frequencies[frequency]['time'] = time.time() + self.voice_frequencies[frequency]['tg_tag'][tdma_slot] = self.get_tag(tgid) + self.voice_frequencies[frequency]['tg_color'][tdma_slot] = self.get_tag_color(tgid) + if srcaddr is not None: + self.voice_frequencies[frequency]['srcaddr'][tdma_slot] = srcaddr + self.voice_frequencies[frequency]['srcaddr_tag'][tdma_slot] = self.get_unit_id_tag(srcaddr) + self.voice_frequencies[frequency]['srcaddr_color'][tdma_slot] = self.get_unit_id_color(srcaddr) def get_updated_talkgroups(self, start_time): return [tgid for tgid in self.talkgroups if ( @@ -285,11 +461,17 @@ class trunked_system (object): if self.debug > 10: sys.stderr.write('decode_mbt_data: %x %x\n' %(opcode, mbt_data)) if opcode == 0x0: # grp voice channel grant + srcaddr = (header >> 48) & 0xffffff + opts = (header >> 24) & 0xff ch1 = (mbt_data >> 64) & 0xffff ch2 = (mbt_data >> 48) & 0xffff ga = (mbt_data >> 32) & 0xffff f = self.channel_id_to_frequency(ch1) - self.update_voice_frequency(f, tgid=ga, tdma_slot=self.get_tdma_slot(ch1), srcaddr=src) + if self.debug > 0 and src != srcaddr: + sys.stderr.write('decode_mbt_data: grp_v_ch_grant: src %d does not match srcaddr %d\n' % (src, srcaddr)) + d = {'cc_event': 'grp_v_ch_grant_mbt', 'options': opts, 'frequency': f, 'group': self.mk_tg_dict(ga), 'srcaddr': self.mk_src_dict(srcaddr), 'opcode': opcode, 'tdma_slot': self.get_tdma_slot(ch1) } + self.post_event(d) + self.update_voice_frequency(f, tgid=ga, tdma_slot=self.get_tdma_slot(ch1), srcaddr=srcaddr, protected=opts&64 == 64) if f: updated += 1 if self.debug > 10: @@ -340,6 +522,220 @@ class trunked_system (object): sys.stderr.write('decode_mbt_data(): received unsupported mbt opcode %x\n' % opcode) return updated + def decode_tdma_blk(self, blk): + self.stats['tsbks'] += 1 + op = (blk[0] >> 6) & 3 + moc = blk[0] & 0x3f + if self.debug > 1: + sys.stderr.write('tdma_cc: decode_blk: op %x moc %x\n' % (op, moc)) + if op == 1 and moc == 0x3c: # adjacent + msglen = 9 + msg = get_ordinals(blk[:msglen]) + syid = (msg >> 40) & 0xfff + rfid = (msg >> 32) & 0xff + stid = (msg >> 24) & 0xff + ch1 = (msg >> 8) & 0xffff + table = (ch1 >> 12) & 0xf + cls = msg & 0xff + print ('tdma adacent: %d %d %d %x' % (syid, rfid, stid, ch1)) + f1 = self.channel_id_to_frequency(ch1) + if f1 and table in self.freq_table: + self.adjacent[f1] = 'rfid: %d stid:%d uplink:%f tbl:%d sysid:0x%x' % (rfid, stid, (f1 + self.freq_table[table]['offset']) / 1000000.0, table, syid) + self.adjacent_data[f1] = {'rfid': rfid, 'stid':stid, 'uplink': f1 + self.freq_table[table]['offset'], 'table': table, 'sysid':syid} + if self.debug > 10: + sys.stderr.write('tsbk3c adjacent: rfid %x stid %d ch1 %x(%s)\n' %(rfid, stid, ch1, self.channel_id_to_string(ch1))) + if table in self.freq_table: + sys.stderr.write('tsbk3c : %s %s\n' % (self.freq_table[table]['frequency'] , self.freq_table[table]['step'] )) + elif op == 1 and moc == 0x33: # iden up tdma + msglen = 9 + opcode = 0x33 + msg = get_ordinals(blk[:msglen]) + iden = (msg >> 60) & 0xf + channel_type = (msg >> 56) & 0xf + toff0 = (msg >> 42) & 0x3fff + toff_sign = (toff0 >> 13) & 1 + toff = toff0 & 0x1fff + if toff_sign == 0: + toff = 0 - toff + spac = (msg >> 32) & 0x3ff + f1 = (msg) & 0xffffffff + slots_per_carrier = [1,1,1,2,4,2] + self.freq_table[iden] = {} + self.freq_table[iden]['offset'] = toff * spac * 125 + self.freq_table[iden]['step'] = spac * 125 + self.freq_table[iden]['frequency'] = f1 * 5 + self.freq_table[iden]['tdma'] = slots_per_carrier[channel_type] + d = {'cc_event': 'iden_up_tdma', 'iden': iden, 'offset': self.freq_table[iden]['offset'], 'step': self.freq_table[iden]['step'], 'freq': self.freq_table[iden]['frequency'], 'slots': self.freq_table[iden]['tdma'], 'opcode': opcode } + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk33 iden up tdma id %d f %d offset %d spacing %d slots/carrier %d\n' % (iden, self.freq_table[iden]['frequency'], self.freq_table[iden]['offset'], self.freq_table[iden]['step'], self.freq_table[iden]['tdma'])) + elif op == 1 and moc == 0x3b: # network status + msglen = 11 + opcode = 0x3b + msg = get_ordinals(blk[:msglen]) + wacn = (msg >> 52) & 0xfffff + syid = (msg >> 40) & 0xfff + ch1 = (msg >> 24) & 0xffff + color = (msg ) & 0xfff + sys.stderr.write('tsbk3b net stat: color: 0x%x\n' % color) + f1 = self.channel_id_to_frequency(ch1) + if f1: + self.ns_syid = syid + self.ns_wacn = wacn + self.ns_chan = f1 + if self.debug > 10: + sys.stderr.write('tsbk3b net stat: wacn %x syid %x ch1 %x(%s)\n' %(wacn, syid, ch1, self.channel_id_to_string(ch1))) + elif op == 1 and moc == 0x3a: # rfss status + msglen = 9 + opcode = 0x3a + msg = get_ordinals(blk[:msglen]) + syid = (msg >> 40) & 0xfff + rfid = (msg >> 32) & 0xff + stid = (msg >> 24) & 0xff + chan = (msg >> 8) & 0xffff + f1 = self.channel_id_to_frequency(chan) + if f1: + self.rfss_syid = syid + self.rfss_rfid = rfid + self.rfss_stid = stid + self.rfss_chan = f1 + self.rfss_txchan = f1 + self.freq_table[chan >> 12]['offset'] + if self.debug > 10: + sys.stderr.write('tsbk3a rfss status: syid: %x rfid %x stid %d ch1 %x(%s)\n' %(syid, rfid, stid, chan, self.channel_id_to_string(chan))) + elif op == 1 and moc == 0x39: # secondary cc + msglen = 9 + opcode = 0x39 + msg = get_ordinals(blk[:msglen]) + rfid = (msg >> 56) & 0xff + stid = (msg >> 48) & 0xff + ch1 = (msg >> 32) & 0xffff + ch2 = (msg >> 8) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + f2 = self.channel_id_to_frequency(ch2) + if f1 and f2: + self.secondary[ f1 ] = 1 + self.secondary[ f2 ] = 1 + sorted_freqs = collections.OrderedDict(sorted(self.secondary.items())) + self.secondary = sorted_freqs + if self.debug > 10: + sys.stderr.write('tsbk39 secondary cc: rfid %x stid %d ch1 %x(%s) ch2 %x(%s)\n' %(rfid, stid, ch1, self.channel_id_to_string(ch1), ch2, self.channel_id_to_string(ch2))) + elif op == 1 and moc == 0x3d: # iden_up + msglen = 9 + opcode = 0x3d + msg = get_ordinals(blk[:msglen]) + iden = (msg >> 60) & 0xf + bw = (msg >> 51) & 0x1ff + toff0 = (msg >> 42) & 0x1ff + spac = (msg >> 32) & 0x3ff + freq = msg & 0xffffffff + toff_sign = (toff0 >> 8) & 1 + toff = toff0 & 0xff + if toff_sign == 0: + toff = 0 - toff + txt = ["mob xmit < recv", "mob xmit > recv"] + self.freq_table[iden] = {} + self.freq_table[iden]['offset'] = toff * 250000 + self.freq_table[iden]['step'] = spac * 125 + self.freq_table[iden]['frequency'] = freq * 5 + d = {'cc_event': 'iden_up', 'iden': iden, 'offset': self.freq_table[iden]['offset'], 'step': self.freq_table[iden]['step'], 'freq': self.freq_table[iden]['frequency'], 'opcode': opcode } + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk3d iden id %d toff %f spac %f freq %f\n' % (iden, toff * 0.25, spac * 0.125, freq * 0.000005)) + else: + msglen = -1 + if self.debug > 0: + sys.stderr.write ('tdma_cc unknown request: %x %x %02x %02x %02x\n' % (op, moc, blk[0], blk[1], blk[2] )) + return msglen + + def decode_tdma_cc(self, blk): + rc = self.decode_tdma_blk(blk) + # TODO: Attempt to decode remaining half? + + def decode_tsbk_harris(self, tsbk, opcode, mfrid): + HARRIS_SGS_EXPIRES = 5.0 # sec. + updated = 0 + if opcode != 0x30: # GRG_EXENC_CMD + sys.stderr.write('decode_tsbk: unsupported opcode %02x mfrid %02x\n' % (opcode, mfrid)) + return updated + grg_options = (tsbk >> 72) & 0xff + opt_2way = (grg_options & 0x80) == 0 + opt_group = (grg_options & 0x40) != 0 + opt_act = (grg_options & 0x20) != 0 + opt_ssn = grg_options & 0x1f + sg = (tsbk >> 56) & 0xffff + keyid = (tsbk >> 40) & 0xffff + target = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'grg_exenc_cmd', 'mfrid': mfrid, 'opt_2way': opt_2way, 'opt_group': opt_group, 'opt_act': opt_act, 'opt_ssn': opt_ssn, 'sg': self.mk_tg_dict(sg), 'keyid': keyid} + if opt_group: # target is a group + algid = (target >> 16) & 0xff + target = target & 0xffff + d['target_group'] = self.mk_tg_dict(target) + else: # target is a subscriber unit id + algid = 0 + d['target_unit'] = self.mk_src_dict(target) + d['algid'] = algid + if opt_act and opt_group: # only group id type supported + sgkey = '%d-%d' % (sg, target) + self.harris_sgs[sgkey] = {'supergroup': self.mk_tg_dict(sg), 'target_group': self.mk_tg_dict(target), 'algid': algid, 'keyid': keyid, 'expires': time.time() + HARRIS_SGS_EXPIRES} + return updated + + def decode_tsbk_mot(self, tsbk, opcode, mfrid): + updated = 0 + if opcode == 0x00: # MOT_GRG_ADD_CMD + sg = (tsbk >> 64) & 0xffff + ga1 = (tsbk >> 48) & 0xffff + ga2 = (tsbk >> 32) & 0xffff + ga3 = (tsbk >> 16) & 0xffff + d = {'cc_event': 'mot_grg_add_cmd', 'mfrid': mfrid, 'sg': self.mk_tg_dict(sg), 'ga1': self.mk_tg_dict(ga1), 'ga2': self.mk_tg_dict(ga2), 'ga3': self.mk_tg_dict(ga3), 'opcode': opcode } + self.post_event(d) + if self.debug > 10: + sys.stderr.write('MOT_GRG_ADD_CMD(0x00): sg:%d ga1:%d ga2:%d ga3:%d\n' % (sg, ga1, ga2, ga3)) + elif opcode == 0x01: #MOT_GRG_DEL_CMD + sg = (tsbk >> 64) & 0xffff + ga1 = (tsbk >> 48) & 0xffff + ga2 = (tsbk >> 32) & 0xffff + ga3 = (tsbk >> 16) & 0xffff + d = {'cc_event': 'mot_grg_del_cmd', 'mfrid': mfrid, 'sg': self.mk_tg_dict(sg), 'ga1': self.mk_tg_dict(ga1), 'ga2': self.mk_tg_dict(ga2), 'ga3': self.mk_tg_dict(ga3), 'opcode': opcode } + self.post_event(d) + if self.debug > 10: + sys.stderr.write('MOT_GRG_DEL_CMD(0x01): sg:%d ga1:%d ga2:%d ga3:%d\n' % (sg, ga1, ga2, ga3)) + elif opcode == 0x02: # MOT_GRG_CN_CRANT + ch = (tsbk >> 56) & 0xffff + sg = (tsbk >> 40) & 0xffff + sa = (tsbk >> 16) & 0xffffff + f = self.channel_id_to_frequency(ch) + d = {'cc_event': 'mot_grg_cn_grant', 'mfrid': mfrid, 'frequency': f, 'sg': self.mk_tg_dict(sg), 'sa': self.mk_src_dict(sa), 'opcode': opcode } + self.post_event(d) + self.update_voice_frequency(f, tgid=sg, tdma_slot=self.get_tdma_slot(ch), srcaddr=sa) + if f: + updated += 1 + if self.debug > 10: + sys.stderr.write('MOT_GRG_CN_GRANT(0x02): freq %s sg:%d sa:%d\n' % (self.channel_id_to_string(ch), sg, sa)) + elif opcode == 0x03: #MOT_GRG_CN_GRANT_UPDT + ch1 = (tsbk >> 64) & 0xffff + sg1 = (tsbk >> 48) & 0xffff + ch2 = (tsbk >> 32) & 0xffff + sg2 = (tsbk >> 16) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + f2 = self.channel_id_to_frequency(ch2) + d = {'cc_event': 'mot_grg_cn_grant_updt', 'mfrid': mfrid, 'frequency1': f1, 'sg1': self.mk_tg_dict(sg1), 'opcode': opcode } + self.update_voice_frequency(f1, tgid=sg1, tdma_slot=self.get_tdma_slot(ch1)) + if f1 != f2: + self.update_voice_frequency(f2, tgid=sg2, tdma_slot=self.get_tdma_slot(ch2)) + d['sg2'] = self.mk_tg_dict(sg2) + d['frequency2'] = f2 + self.post_event(d) + if f1: + updated += 1 + if f2: + updated += 1 + if self.debug > 10: + sys.stderr.write('MOT_GRG_CN_GRANT_UPDT(0x03): freq %s sg1:%d freq %s sg2:%d\n' % (self.channel_id_to_string(ch1), sg1, self.channel_id_to_string(ch2), sg2)) + else: + if self.debug > 10: + sys.stderr.write('decode_tsbk: unsupported opcode %02x mfrid %02x\n' % (opcode, mfrid)) + return updated + def decode_tsbk(self, tsbk): self.cc_timeouts = 0 self.last_tsbk = time.time() @@ -347,108 +743,70 @@ class trunked_system (object): updated = 0 tsbk = tsbk << 16 # for missing crc opcode = (tsbk >> 88) & 0x3f + mfrid = (tsbk >> 80) & 0xff # mfrid if self.debug > 10: - sys.stderr.write('TSBK: 0x%02x 0x%024x\n' % (opcode, tsbk)) + sys.stderr.write('TSBK: 0x%02x 0x%024x mfrid %02x\n' % (opcode, tsbk, mfrid)) + + if mfrid == 0x90: + return self.decode_tsbk_mot(tsbk, opcode, mfrid) + elif mfrid == 0xa4: + return self.decode_tsbk_harris(tsbk, opcode, mfrid) + elif mfrid != 0: + sys.stderr.write('unsupported tsbk mfrid: 0x%02x opcode 0x%02x\n' % (mfrid, opcode)) + return updated + if opcode == 0x00: # group voice chan grant - mfrid = (tsbk >> 80) & 0xff - if mfrid == 0x90: # MOT_GRG_ADD_CMD - sg = (tsbk >> 64) & 0xffff - ga1 = (tsbk >> 48) & 0xffff - ga2 = (tsbk >> 32) & 0xffff - ga3 = (tsbk >> 16) & 0xffff - if self.debug > 10: - sys.stderr.write('MOT_GRG_ADD_CMD(0x00): sg:%d ga1:%d ga2:%d ga3:%d\n' % (sg, ga1, ga2, ga3)) - else: - opts = (tsbk >> 72) & 0xff - ch = (tsbk >> 56) & 0xffff - ga = (tsbk >> 40) & 0xffff - sa = (tsbk >> 16) & 0xffffff - f = self.channel_id_to_frequency(ch) - self.update_voice_frequency(f, tgid=ga, tdma_slot=self.get_tdma_slot(ch), srcaddr=sa) - if f: - updated += 1 - if self.debug > 10: - sys.stderr.write('tsbk00 grant freq %s ga %d sa %d\n' % (self.channel_id_to_string(ch), ga, sa)) - elif opcode == 0x01: # reserved - mfrid = (tsbk >> 80) & 0xff - if mfrid == 0x90: #MOT_GRG_DEL_CMD - sg = (tsbk >> 64) & 0xffff - ga1 = (tsbk >> 48) & 0xffff - ga2 = (tsbk >> 32) & 0xffff - ga3 = (tsbk >> 16) & 0xffff - if self.debug > 10: - sys.stderr.write('MOT_GRG_DEL_CMD(0x01): sg:%d ga1:%d ga2:%d ga3:%d\n' % (sg, ga1, ga2, ga3)) + opts = (tsbk >> 72) & 0xff + ch = (tsbk >> 56) & 0xffff + ga = (tsbk >> 40) & 0xffff + sa = (tsbk >> 16) & 0xffffff + f = self.channel_id_to_frequency(ch) + d = {'cc_event': 'grp_v_ch_grant', 'mfrid': mfrid, 'options': opts, 'frequency': f, 'group': self.mk_tg_dict(ga), 'srcaddr': self.mk_src_dict(sa), 'opcode': opcode, 'tdma_slot': self.get_tdma_slot(ch) } + self.post_event(d) + self.update_voice_frequency(f, tgid=ga, tdma_slot=self.get_tdma_slot(ch), srcaddr=sa, protected=opts&64 == 64) + if f: + updated += 1 + if self.debug > 10: + sys.stderr.write('tsbk00 grant freq %s ga %d sa %d\n' % (self.channel_id_to_string(ch), ga, sa)) elif opcode == 0x02: # group voice chan grant update - mfrid = (tsbk >> 80) & 0xff - if mfrid == 0x90: - ch = (tsbk >> 56) & 0xffff - sg = (tsbk >> 40) & 0xffff - sa = (tsbk >> 16) & 0xffffff - f = self.channel_id_to_frequency(ch) - self.update_voice_frequency(f, tgid=sg, tdma_slot=self.get_tdma_slot(ch), srcaddr=sa) - if f: - updated += 1 - if self.debug > 10: - sys.stderr.write('MOT_GRG_CN_GRANT(0x02): freq %s sg:%d sa:%d\n' % (self.channel_id_to_string(ch), sg, sa)) - else: - ch1 = (tsbk >> 64) & 0xffff - ga1 = (tsbk >> 48) & 0xffff - ch2 = (tsbk >> 32) & 0xffff - ga2 = (tsbk >> 16) & 0xffff - f1 = self.channel_id_to_frequency(ch1) - f2 = self.channel_id_to_frequency(ch2) - self.update_voice_frequency(f1, tgid=ga1, tdma_slot=self.get_tdma_slot(ch1)) - if f1 != f2: - self.update_voice_frequency(f2, tgid=ga2, tdma_slot=self.get_tdma_slot(ch2)) - if f1: - updated += 1 - if f2: - updated += 1 - if self.debug > 10: - sys.stderr.write('tsbk02 grant update: chan %s %d %s %d\n' %(self.channel_id_to_string(ch1), ga1, self.channel_id_to_string(ch2), ga2)) + ch1 = (tsbk >> 64) & 0xffff + ga1 = (tsbk >> 48) & 0xffff + ch2 = (tsbk >> 32) & 0xffff + ga2 = (tsbk >> 16) & 0xffff + f1 = self.channel_id_to_frequency(ch1) + f2 = self.channel_id_to_frequency(ch2) + d = {'cc_event': 'grp_v_ch_grant_updt', 'mfrid': mfrid, 'frequency1': f1, 'group1': self.mk_tg_dict(ga1), 'opcode': opcode, 'tdma_slot': self.get_tdma_slot(ch1) } + self.update_voice_frequency(f1, tgid=ga1, tdma_slot=self.get_tdma_slot(ch1)) + if f1 != f2: + self.update_voice_frequency(f2, tgid=ga2, tdma_slot=self.get_tdma_slot(ch2)) + d['frequency2'] = f2 + d['group2'] = self.mk_tg_dict(ga2) + if f1: + updated += 1 + if f2: + updated += 1 + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk02 grant update: chan %s %d %s %d\n' %(self.channel_id_to_string(ch1), ga1, self.channel_id_to_string(ch2), ga2)) elif opcode == 0x03: # group voice chan grant update exp : TIA.102-AABC-B-2005 page 56 - mfrid = (tsbk >> 80) & 0xff - if mfrid == 0x90: #MOT_GRG_CN_GRANT_UPDT - ch1 = (tsbk >> 64) & 0xffff - sg1 = (tsbk >> 48) & 0xffff - ch2 = (tsbk >> 32) & 0xffff - sg2 = (tsbk >> 16) & 0xffff - f1 = self.channel_id_to_frequency(ch1) - f2 = self.channel_id_to_frequency(ch2) - self.update_voice_frequency(f1, tgid=sg1, tdma_slot=self.get_tdma_slot(ch1)) - if f1 != f2: - self.update_voice_frequency(f2, tgid=sg2, tdma_slot=self.get_tdma_slot(ch2)) - if f1: - updated += 1 - if f2: - updated += 1 - if self.debug > 10: - sys.stderr.write('MOT_GRG_CN_GRANT_UPDT(0x03): freq %s sg1:%d freq %s sg2:%d\n' % (self.channel_id_to_string(ch1), sg1, self.channel_id_to_string(ch2), sg2)) - elif mfrid == 0: - ch1 = (tsbk >> 48) & 0xffff - ch2 = (tsbk >> 32) & 0xffff - ga = (tsbk >> 16) & 0xffff - f = self.channel_id_to_frequency(ch1) - self.update_voice_frequency(f, tgid=ga, tdma_slot=self.get_tdma_slot(ch1)) - if f: - updated += 1 - if self.debug > 10: - sys.stderr.write('tsbk03: freq-t %s freq-r %s ga:%d\n' % (self.channel_id_to_string(ch1), self.channel_id_to_string(ch2), ga)) + opts = (tsbk >> 72) & 0xff + ch1 = (tsbk >> 48) & 0xffff + ch2 = (tsbk >> 32) & 0xffff + ga = (tsbk >> 16) & 0xffff + f = self.channel_id_to_frequency(ch1) + d = {'cc_event': 'grp_v_ch_grant_updt_exp', 'mfrid': mfrid, 'options': opts, 'frequency': f, 'group': self.mk_tg_dict(ga), 'opcode': opcode, 'tdma_slot': self.get_tdma_slot(ch1) } + self.post_event(d) + self.update_voice_frequency(f, tgid=ga, tdma_slot=self.get_tdma_slot(ch1)) + if f: + updated += 1 + if self.debug > 10: + sys.stderr.write('tsbk03: freq-t %s freq-r %s ga:%d\n' % (self.channel_id_to_string(ch1), self.channel_id_to_string(ch2), ga)) elif opcode == 0x16: # sndcp data ch ch1 = (tsbk >> 48) & 0xffff ch2 = (tsbk >> 32) & 0xffff if self.debug > 10: sys.stderr.write('tsbk16 sndcp data ch: chan %x %x\n' % (ch1, ch2)) - elif opcode == 0x28: # grp_aff_rsp - mfrid = (tsbk >> 80) & 0xff - lg = (tsbk >> 79) & 0x01 - gav = (tsbk >> 72) & 0x03 - aga = (tsbk >> 56) & 0xffff - ga = (tsbk >> 40) & 0xffff - ta = (tsbk >> 16) & 0xffffff - if self.debug > 10: - sys.stderr.write('tsbk28 grp_aff_resp: mfrid: 0x%x, gav: %d, aga: %d, ga: %d, ta: %d\n' % (mfrid, gav, aga, ga, ta)) elif opcode == 0x34: # iden_up vhf uhf iden = (tsbk >> 76) & 0xf bwvu = (tsbk >> 72) & 0xf @@ -464,29 +822,30 @@ class trunked_system (object): self.freq_table[iden]['offset'] = toff * spac * 125 self.freq_table[iden]['step'] = spac * 125 self.freq_table[iden]['frequency'] = freq * 5 + d = {'cc_event': 'iden_up_vu', 'iden': iden, 'bwvu': bwvu, 'offset': self.freq_table[iden]['offset'], 'step': self.freq_table[iden]['step'], 'freq': self.freq_table[iden]['frequency'], 'opcode': opcode } + self.post_event(d) if self.debug > 10: sys.stderr.write('tsbk34 iden vhf/uhf id %d toff %f spac %f freq %f [%s]\n' % (iden, toff * spac * 0.125 * 1e-3, spac * 0.125, freq * 0.000005, txt[toff_sign])) elif opcode == 0x33: # iden_up_tdma - mfrid = (tsbk >> 80) & 0xff - if mfrid == 0: - iden = (tsbk >> 76) & 0xf - channel_type = (tsbk >> 72) & 0xf - toff0 = (tsbk >> 58) & 0x3fff - spac = (tsbk >> 48) & 0x3ff - toff_sign = (toff0 >> 13) & 1 - toff = toff0 & 0x1fff - if toff_sign == 0: - toff = 0 - toff - f1 = (tsbk >> 16) & 0xffffffff - slots_per_carrier = [1,1,1,2,4,2] - self.freq_table[iden] = {} - self.freq_table[iden]['offset'] = toff * spac * 125 - self.freq_table[iden]['step'] = spac * 125 - self.freq_table[iden]['frequency'] = f1 * 5 - self.freq_table[iden]['tdma'] = slots_per_carrier[channel_type] - if self.debug > 10: - sys.stderr.write('tsbk33 iden up tdma id %d f %d offset %d spacing %d slots/carrier %d\n' % (iden, self.freq_table[iden]['frequency'], self.freq_table[iden]['offset'], self.freq_table[iden]['step'], self.freq_table[iden]['tdma'])) - + iden = (tsbk >> 76) & 0xf + channel_type = (tsbk >> 72) & 0xf + toff0 = (tsbk >> 58) & 0x3fff + spac = (tsbk >> 48) & 0x3ff + toff_sign = (toff0 >> 13) & 1 + toff = toff0 & 0x1fff + if toff_sign == 0: + toff = 0 - toff + f1 = (tsbk >> 16) & 0xffffffff + slots_per_carrier = [1,1,1,2,4,2] + self.freq_table[iden] = {} + self.freq_table[iden]['offset'] = toff * spac * 125 + self.freq_table[iden]['step'] = spac * 125 + self.freq_table[iden]['frequency'] = f1 * 5 + self.freq_table[iden]['tdma'] = slots_per_carrier[channel_type] + d = {'cc_event': 'iden_up_tdma', 'iden': iden, 'offset': self.freq_table[iden]['offset'], 'step': self.freq_table[iden]['step'], 'freq': self.freq_table[iden]['frequency'], 'slots': self.freq_table[iden]['tdma'], 'opcode': opcode } + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk33 iden up tdma id %d f %d offset %d spacing %d slots/carrier %d\n' % (iden, self.freq_table[iden]['frequency'], self.freq_table[iden]['offset'], self.freq_table[iden]['step'], self.freq_table[iden]['tdma'])) elif opcode == 0x3d: # iden_up iden = (tsbk >> 76) & 0xf bw = (tsbk >> 67) & 0x1ff @@ -502,6 +861,8 @@ class trunked_system (object): self.freq_table[iden]['offset'] = toff * 250000 self.freq_table[iden]['step'] = spac * 125 self.freq_table[iden]['frequency'] = freq * 5 + d = {'cc_event': 'iden_up', 'iden': iden, 'offset': self.freq_table[iden]['offset'], 'step': self.freq_table[iden]['step'], 'freq': self.freq_table[iden]['frequency'], 'opcode': opcode } + self.post_event(d) if self.debug > 10: sys.stderr.write('tsbk3d iden id %d toff %f spac %f freq %f\n' % (iden, toff * 0.25, spac * 0.125, freq * 0.000005)) elif opcode == 0x3a: # rfss status @@ -554,31 +915,152 @@ class trunked_system (object): self.adjacent[f1] = 'rfid: %d stid:%d uplink:%f tbl:%d' % (rfid, stid, (f1 + self.freq_table[table]['offset']) / 1000000.0, table) self.adjacent_data[f1] = {'rfid': rfid, 'stid':stid, 'uplink': f1 + self.freq_table[table]['offset'], 'table': table, 'sysid':syid} if self.debug > 10: - sys.stderr.write('tsbk3c adjacent: rfid %x stid %d ch1 %x(%s)\n' %(rfid, stid, ch1, self.channel_id_to_string(ch1))) + sys.stderr.write('tsbk3c adjacent: rfid %x stid %d ch1 %x(%s) sysid 0x%x\n' %(rfid, stid, ch1, self.channel_id_to_string(ch1), syid)) if table in self.freq_table: sys.stderr.write('tsbk3c : %s %s\n' % (self.freq_table[table]['frequency'] , self.freq_table[table]['step'] )) - #else: - # sys.stderr.write('tsbk other %x\n' % opcode) + elif opcode == 0x20: # ACK_RESP_FNE + aiv = (tsbk >> 79) & 1 + ex = (tsbk >> 78) & 1 + addl = (tsbk >> 40) & 0xffffffff + wacn = None + sysid = None + srcaddr = None + if ex: + wacn = (addl > 12) & 0xfffff + sysid = addl & 0xfff + else: + srcaddr = addl & 0xffffff + target = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'ack_resp_fne', 'aiv': aiv, 'ex': ex, 'addl': addl, 'wacn': wacn, 'tsbk_sysid': sysid, 'source': self.mk_src_dict(srcaddr), 'target': self.mk_src_dict(target), 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk20 ack_resp_fne: aiv %d ex %d wacn %s sysid %s src %s\n' % (aiv, ex, wacn, sysid, srcaddr)) + elif opcode == 0x27: # DENY_RESP + aiv = (tsbk >> 79) & 1 + reason = (tsbk >> 64) & 0xff + addl = (tsbk >> 40) & 0xffffff + target = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'deny_resp', 'aiv': aiv, 'reason': reason, 'additional': addl, 'target': self.mk_src_dict(target), 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk27 deny_resp: aiv %d reason %02x additional %x target %d\n' % (aiv, reason, addl, target)) + elif opcode == 0x28: # grp_aff_rsp + lg = (tsbk >> 79) & 0x01 + gav = (tsbk >> 72) & 0x03 + aga = (tsbk >> 56) & 0xffff + ga = (tsbk >> 40) & 0xffff + ta = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'grp_aff_resp', 'affiliation': ['local', 'global'][lg], 'group_aff_value': gav, 'announce_group': self.mk_tg_dict(aga), 'group': self.mk_tg_dict(ga), 'target': self.mk_src_dict(ta), 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk28 grp_aff_resp: mfrid: 0x%x, gav: %d, aga: %d, ga: %d, ta: %d\n' % (mfrid, gav, aga, ga, ta)) + elif opcode == 0x2a: # GRP_AFF_Q + target = (tsbk >> 40) & 0xffffff + source = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'grp_aff_q', 'source': self.mk_src_dict(source), 'target': self.mk_src_dict(target), 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk2a grp_aff_q: mfrid: 0x%x, target %d source %d\n' % (mfrid, target, source)) + elif opcode == 0x2b: # LOC_REG_RESP + rv = (tsbk >> 72) & 3 + ga = (tsbk >> 56) & 0xffff + rfss = (tsbk >> 48) & 0xff + siteid = (tsbk >> 40) & 0xff + target = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'loc_reg_resp', 'rv': rv, 'rfss': rfss, 'siteid': siteid, 'group': self.mk_tg_dict(ga), 'target': self.mk_src_dict(target), 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk2b loc_reg_resp: mfrid: 0x%x, rv %d group %d rfss 0x%x siteid 0x%x target %d\n' % (mfrid, rv, ga, rfss, siteid, target)) + elif opcode == 0x2c: # U_REG_RESP + rv = (tsbk >> 76) & 1 + sysid = (tsbk >> 64) & 0xfff + target = (tsbk >> 40) & 0xffffff + source = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'u_reg_resp', 'rv': rv, 'tsbk_sysid': sysid, 'source': self.mk_src_dict(source), 'target': self.mk_src_dict(target), 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk2c u_reg_resp: mfrid: 0x%x, rv %d sysid %x target %d source %d\n' % (mfrid, rv, sysid, target, source)) + elif opcode == 0x2d: # U_REG_CMD + target = (tsbk >> 40) & 0xffffff + source = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'u_reg_cmd', 'source': self.mk_src_dict(source), 'target': self.mk_src_dict(target), 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk2d u_reg_cmd: mfrid: 0x%x, target %d source %d\n' % (mfrid, target, source)) + elif opcode == 0x2f: # U_DE_REG_ACK + wacn = (tsbk >> 52) & 0xfffff + sysid = (tsbk >> 40) & 0xfff + source = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'u_de_reg_ack', 'wacn': wacn, 'tsbk_sysid': sysid, 'source': self.mk_src_dict(source), 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk2f u_de_reg_ack: mfrid: 0x%x, wacn 0x%x sysid 0x%x source %d\n' % (mfrid, wacn, sysid, source)) + + elif opcode == 0x24: # EXT_FNCT_CMD + efclass = (tsbk >> 72) & 0xff + efoperand = (tsbk >> 64) & 0xff + efargs = (tsbk >> 40) & 0xffffff + target = (tsbk >> 16) & 0xffffff + d = {'cc_event': 'ext_fnct_cmd', 'mfrid': mfrid, 'efclass': efclass, 'efoperand': efoperand, 'efargs': self.mk_src_dict(efargs), 'target': target, 'opcode': opcode} + self.post_event(d) + if self.debug > 10: + sys.stderr.write('tsbk24 ext_fnct_cmd: efclass %d efoperand %d efargs %s sysid %s target %s\n' % (efclass, efoperand, efargs, sysid, target)) + + + #else: + # sys.stderr.write('tsbk other %x\n' % opcode) return updated def hunt_cc(self, curr_time): - if self.cc_timeouts < 6: + # return True if a tune request for frequency=self.trunk_cc should be issued + HUNT_HOLD_TIME = 8.0 + #if self.cc_timeouts < 6: + # return False + if self.last_tsbk + HUNT_HOLD_TIME > time.time(): return False + if time.time() < self.next_hunt_cc: + return False + self.next_hunt_cc = time.time() + HUNT_HOLD_TIME self.cc_timeouts = 0 self.cc_list_index += 1 if self.cc_list_index >= len(self.cc_list): self.cc_list_index = 0 self.trunk_cc = self.cc_list[self.cc_list_index] - sys.stderr.write('%f set trunk_cc to %s\n' % (curr_time, self.trunk_cc)) + sys.stderr.write('%f %s: set trunk_cc to %s\n' % (curr_time, self.sysname, self.trunk_cc)) if self.trunk_cc != self.last_trunk_cc: self.last_trunk_cc = self.trunk_cc if self.debug >=5: - sys.stderr.write('%f set control channel: %f\n' % (curr_time, self.trunk_cc / 1000000.0)) + sys.stderr.write('%f %s: control channel change: %f\n' % (curr_time, self.sysname, self.trunk_cc / 1000000.0)) return True - return False + return True + + def frequency_change_params(self, current_tgid, new_frequency, nac, new_slot, new_frequency_type, curr_time): + params = { + 'freq': new_frequency, + 'tgid': current_tgid, + 'offset': self.offset, + 'tag': self.get_tag(current_tgid), + 'nac': nac, + 'system': self.sysname, + 'center_frequency': self.center_frequency, + 'tdma': new_slot, + 'wacn': self.ns_wacn, + 'sysid': self.ns_syid, + 'srcaddr': self.current_srcaddr, + 'grpaddr': self.current_grpaddr, + 'alg': self.current_alg, + 'algid': self.current_algid, + 'channel_type': new_frequency_type, + 'keyid': self.current_keyid, + 'prio': self.get_prio(current_tgid), + 'tag_color': self.get_tag_color(current_tgid), + 'srcaddr_color': self.get_unit_id_color(self.current_srcaddr), + 'srcaddr_tag': self.get_unit_id_tag(self.current_srcaddr), + 'effective_time': curr_time } + return params class rx_ctl (object): - def __init__(self, debug=0, frequency_set=None, conf_file=None, logfile_workers=None): + def __init__(self, debug=0, frequency_set=None, conf_file=None, logfile_workers=None, send_event=None): class _states(object): ACQ = 0 CC = 1 @@ -613,7 +1095,9 @@ class rx_ctl (object): self.input_rate = self.logfile_workers[0]['demod'].input_rate self.enabled_nacs = None self.next_status_png = time.time() - self.last_freq_params = {'freq' : 0.0, 'tgid' : None, 'tag' : "", 'tdma' : None} + self.send_event = send_event + self.status_msg = '' + self.next_hunt_time = time.time() if conf_file: if conf_file.endswith('.tsv'): @@ -634,6 +1118,7 @@ class rx_ctl (object): self.set_frequency({ 'freq': tsys.trunk_cc, + 'channel_type': 'cc', 'tgid': None, 'offset': tsys.offset, 'tag': "", @@ -642,7 +1127,9 @@ class rx_ctl (object): 'center_frequency': tsys.center_frequency, 'tdma': None, 'wacn': None, - 'sysid': None}) + 'sysid': None, + 'prio': 0, + 'tag_color': None }) def build_config_json(self, conf_file): d = json.loads(open(conf_file).read()) @@ -680,7 +1167,7 @@ class rx_ctl (object): cfg = None if nac in self.configs: cfg = self.configs[nac] - self.trunked_systems[nac] = trunked_system(debug = self.debug, config=cfg) + self.trunked_systems[nac] = trunked_system(debug = self.debug, config=cfg, send_event=self.send_event, nac=nac) def build_config_tsv(self, tsv_filename): self.setup_config(load_tsv(tsv_filename)) @@ -700,7 +1187,24 @@ class rx_ctl (object): configs[nac]['sysname'] = section self.setup_config(configs) - def add_default_config(self, nac, cclist=[], offset=0, whitelist=None, blacklist={}, tgid_map={}, sysname=None, center_frequency=None, modulation='cqpsk'): + def reload_tags(self, nac): + if nac not in self.trunked_systems.keys(): + return + tsys = self.trunked_systems[nac] + tgid_tags_file = self.configs[nac]['tgid_tags_file'] + new_reg = id_registry() + read_tags_file(tgid_tags_file, new_reg) + tsys.tgid_map = new_reg + sys.stderr.write('reloaded %s nac 0x%x\n' % (tgid_tags_file, nac)) + unit_id_tags_file = self.configs[nac]['unit_id_tags_file'] + if unit_id_tags_file is None: + return + new_reg = id_registry() + read_tags_file(unit_id_tags_file, new_reg) + tsys.unit_id_map = new_reg + sys.stderr.write('reloaded %s nac 0x%x\n' % (unit_id_tags_file, nac)) + + def add_default_config(self, nac, cclist=[], offset=0, whitelist=None, blacklist={}, tgid_map=None, unit_id_map=None,sysname=None, center_frequency=None, modulation='cqpsk'): if nac in self.configs.keys(): return if nac not in self.trunked_systems.keys(): @@ -720,7 +1224,7 @@ class rx_ctl (object): cclist = [tsys.rfss_chan] cclist.extend(tsys.secondary.keys()) tsys.cc_list = cclist - self.configs[nac] = {'cclist':cclist, 'offset':offset, 'whitelist':whitelist, 'blacklist':blacklist, 'tgid_map':tgid_map, 'sysname': sysname, 'center_frequency': center_frequency, 'modulation':modulation} + self.configs[nac] = {'cclist':cclist, 'offset':offset, 'whitelist':whitelist, 'blacklist':blacklist, 'tgid_map':tgid_map, 'unit_id_map': unit_id_map, 'sysname': sysname, 'center_frequency': center_frequency, 'modulation':modulation} self.current_nac = nac self.current_state = self.states.CC if nac not in self.nacs: @@ -749,12 +1253,17 @@ class rx_ctl (object): current_time = time.time() d = {'json_type': 'trunk_update'} for nac in self.trunked_systems.keys(): - d[nac] = json.loads(self.trunked_systems[nac].to_json()) + d[nac] = self.trunked_systems[nac].to_dict() + if nac in self.configs.keys() and 'tgid_tags_file' in self.configs[nac]: + d[nac]['tgid_tags_file'] = self.configs[nac]['tgid_tags_file'] + if nac in self.configs.keys() and 'unit_id_tags_file' in self.configs[nac]: + d[nac]['unit_id_tags_file'] = self.configs[nac]['unit_id_tags_file'] d['data'] = {'last_command': self.last_command['command'], 'last_command_time': int(self.last_command['time'] - current_time), 'tgid_hold': self.tgid_hold, 'tgid_hold_until': int(self.tgid_hold_until - current_time), 'hold_mode': self.hold_mode} + d['time'] = current_time return json.dumps(d) def make_status_png(self): @@ -765,13 +1274,23 @@ class rx_ctl (object): return self.next_status_png = time.time() + PNG_UPDATE_INTERVAL status_str = 'OP25-hls hacks (c) Copyright 2020, 2021, KA1RBI\n' - status_str += 'F %f TG %s %s at %s\n' % ( self.last_freq_params['freq'] / 1000000.0, self.last_freq_params['tgid'], self.last_freq_params['tag'], time.asctime()) + status_str += self.status_msg status_str += self.to_string() status = status_str.split('\n') status = [s for s in status if not s.startswith('tbl-id')] create_image(status, imgfile=tmp_output_file, bgcolor="#c0c0c0", windowsize=(640,480)) + if not os.access(tmp_output_file, os.R_OK): + return os.rename(tmp_output_file, output_file) + def frequency_tracking_expire(self): + for nac in self.trunked_systems.keys(): + self.trunked_systems[nac].frequency_tracking_expire() + + def in_voice_state(self): + rc = self.current_state == self.states.TO_VC or self.current_state == self.states.VC + return rc + def dump_tgids(self): for nac in self.trunked_systems.keys(): self.trunked_systems[nac].dump_tgids() @@ -784,15 +1303,30 @@ class rx_ctl (object): return s def process_qmsg(self, msg): + self.frequency_tracking_expire() + if self.send_event is not None: + self.send_event(None) # periodically sends general status info mtype = msg.type() updated = 0 curr_time = time.time() + msgtext = msg.to_string() + aa55 = get_ordinals(msgtext[:2]) + if mtype >= 0 or mtype in [-1, -3, -5, -6]: + assert aa55 == 0xaa55 + msgq_id = get_ordinals(msgtext[2:4]) + msgtext = msgtext[4:] + else: + assert aa55 != 0xaa55 + msgq_id = None if mtype == -3: # P25 call signalling data if self.debug > 10: - sys.stderr.write("%f process_qmsg: P25 info: %s\n" % (time.time(), msg.to_string())) - js = json.loads(msg.to_string()) + sys.stderr.write("%f process_qmsg: P25 info: %s\n" % (time.time(), msgtext)) + js = json.loads(msgtext) nac = js['nac'] if nac != self.current_nac: + sys.stderr.write('warning: nac mismatch: nac %x current_nac %x js %s\n' % (nac, self.current_nac, msgtext)) + # return + if nac not in self.trunked_systems.keys(): return tsys = self.trunked_systems[nac] if 'srcaddr' in js.keys(): @@ -807,7 +1341,7 @@ class rx_ctl (object): tsys.current_keyid = js['keyid'] return elif mtype == -2: # request from gui - cmd = msg.to_string() + cmd = msgtext if self.debug > 10: sys.stderr.write('process_qmsg: command: %s\n' % cmd) self.update_state(cmd, curr_time, int(msg.arg1())) # self.update_state(cmd, curr_time) @@ -819,19 +1353,29 @@ class rx_ctl (object): if self.logfile_workers: self.logging_scheduler(curr_time) return + elif mtype == -6: # p25 tdma cc + # nac is always 1st two bytes + nac = get_ordinals(msgtext[:2]) + msgtext = msgtext[2:] + if nac not in self.trunked_systems.keys(): + sys.stderr.write('tdma_cc received from unexpected NAC 0x%x\n' % nac) + return + tsys = self.trunked_systems[nac] + m1 = msgtext[1] + b1 = (m1 >> 7) & 1 + b2 = (m1 >> 6) & 1 + moc = m1 & 0x3f + tsys.decode_tdma_cc(msgtext[1:]) + return elif mtype < 0: sys.stderr.write('unknown message type %d\n' % (mtype)) return - s = msg.to_string() + s = msgtext # nac is always 1st two bytes nac = get_ordinals(s[:2]) - #assert nac != 0xffff # FIXME: uncomment this after any remaining usages of 0xffff removed if nac == 0xffff: - if (mtype != 7) and (mtype != 12): # TDMA duid (end of call etc) - self.update_state('tdma_duid%d' % mtype, curr_time) - return - else: # voice channel derived TSBK or MBT PDU - nac = self.current_nac + sys.stderr.write('received invalid nac 0xffff, mtype %d msgq_id %s\n' % (mtype, msgq_id)) + return s = s[2:] if self.debug > 10: sys.stderr.write('nac %x type %d at %f state %d len %d\n' %(nac, mtype, time.time(), self.current_state, len(s))) @@ -842,9 +1386,15 @@ class rx_ctl (object): else: sys.stderr.write("%f NAC %x not configured\n" % (time.time(), nac)) return - if mtype == 7: # trunk: TSBK + if nac not in self.trunked_systems.keys(): + sys.stderr.write('received unknown nac 0x%x, mtype %d len configs %d msgq_id %d\n' % (nac, mtype, len(self.configs.keys()), msgq_id)) + return + tsys = self.trunked_systems[nac] + if mtype == 0 or mtype == 5 or mtype == 10: # HDU or LDU1 or LDU2 i.e. voice + tsys.last_voice_time = curr_time + elif mtype == 7: # trunk: TSBK t = get_ordinals(s) - updated += self.trunked_systems[nac].decode_tsbk(t) + updated += tsys.decode_tsbk(t) elif mtype == 12: # trunk: MBT s1 = s[:10] # header without crc s2 = s[12:] @@ -860,17 +1410,17 @@ class rx_ctl (object): opcode = (header >> 16) & 0x3f if self.debug > 10: sys.stderr.write('type %d at %f state %d len %d/%d opcode %x [%x/%x]\n' %(mtype, time.time(), self.current_state, len(s1), len(s2), opcode, header,mbt_data)) - updated += self.trunked_systems[nac].decode_mbt_data(opcode, src, header << 16, mbt_data << 32) + updated += tsys.decode_mbt_data(opcode, src, header << 16, mbt_data << 32) self.make_status_png() - if nac != self.current_nac: - if self.debug > 10: # this is occasionally expected if cycling between different tsys - cnac = self.current_nac - if cnac is None: - cnac = 0 - sys.stderr.write("%f received NAC %x does not match expected NAC %x\n" % (time.time(), nac, cnac)) - return + #if nac != self.current_nac: + # if self.debug > 10: # this is occasionally expected if cycling between different tsys + # cnac = self.current_nac + # if cnac is None: + # cnac = 0 + # sys.stderr.write("%f received NAC %x does not match expected NAC %x\n" % (time.time(), nac, cnac)) + # return if self.logfile_workers: self.logging_scheduler(curr_time) @@ -1002,19 +1552,22 @@ class rx_ctl (object): new_state = None new_nac = None new_slot = None + new_frequency_type = None if command == 'timeout': if self.current_state == self.states.CC: if self.debug > 0: sys.stderr.write("%f control channel timeout\n" % time.time()) tsys.cc_timeouts += 1 - elif self.current_state != self.states.CC: + elif self.current_state != self.states.CC and tsys.last_voice_time + 1.0 < curr_time: if self.debug > 1: sys.stderr.write("%f voice timeout\n" % time.time()) if self.hold_mode is False: self.current_tgid = None + tsys.current_srcaddr = 0 new_state = self.states.CC new_frequency = tsys.trunk_cc + new_frequency_type = 'cc' elif command == 'update': if self.current_state == self.states.CC: desired_tgid = None @@ -1026,6 +1579,7 @@ class rx_ctl (object): self.tgid_hold = None new_frequency, new_tgid, tdma_slot, srcaddr = tsys.find_talkgroup(curr_time, tgid=desired_tgid, hold=self.hold_mode) if new_frequency: + new_frequency_type = 'vc' if self.debug > 0: tslot = tdma_slot if tdma_slot is not None else '-' sys.stderr.write("%f voice update: tg(%s), freq(%s), slot(%s), prio(%d)\n" % (time.time(), new_tgid, new_frequency, tslot, tsys.get_prio(new_tgid))) @@ -1036,12 +1590,13 @@ class rx_ctl (object): self.tgid_hold_until = max(curr_time + self.TGID_HOLD_TIME, self.tgid_hold_until) self.wait_until = curr_time + self.TSYS_HOLD_TIME new_slot = tdma_slot - else: # check for priority tgid preemption + elif 0: # # # # # else: # check for priority tgid preemption new_frequency, new_tgid, tdma_slot, srcaddr = tsys.find_talkgroup(tsys.talkgroups[self.current_tgid]['time'], tgid=self.current_tgid, hold=self.hold_mode) if new_tgid != self.current_tgid: if self.debug > 0: tslot = tdma_slot if tdma_slot is not None else '-' sys.stderr.write("%f voice preempt: tg(%s), freq(%s), slot(%s), prio(%d)\n" % (time.time(), new_tgid, new_frequency, tslot, tsys.get_prio(new_tgid))) + new_frequency_type = 'vc' new_state = self.states.TO_VC self.current_tgid = new_tgid tsys.current_srcaddr = srcaddr @@ -1069,6 +1624,7 @@ class rx_ctl (object): self.current_tgid = None new_state = self.states.CC new_frequency = tsys.trunk_cc + new_frequency_type = 'cc' elif command == 'duid0' or command == 'duid5' or command == 'duid10' or command == 'tdma_duid5': if self.current_state == self.states.TO_VC: new_state = self.states.VC @@ -1079,12 +1635,27 @@ class rx_ctl (object): pass elif command == 'hold': self.last_command = {'command': command, 'time': curr_time} - if cmd_data != 0: + if self.hold_mode: + new_hold_mode = False + else: + new_hold_mode = True + if new_hold_mode and (cmd_data is None or cmd_data < 1): + if self.debug > 0: + sys.stderr.write ('%f set hold failed, unable to determine TGID\n' % (time.time())) + new_hold_mode = False + if new_hold_mode is False: # unset hold + self.current_tgid = None + self.tgid_hold = None + self.tgid_hold_until = curr_time + self.hold_mode = new_hold_mode + else: # set hold self.tgid_hold = cmd_data self.tgid_hold_until = curr_time + 86400 * 10000 - self.hold_mode = True + self.hold_mode = new_hold_mode + if self.debug > 0: + sys.stderr.write ('%f set hold tg(%s) until %f\n' % (time.time(), self.tgid_hold, self.tgid_hold_until)) if self.debug > 0: - sys.stderr.write ('%f set hold tg(%s) until %f\n' % (time.time(), self.tgid_hold, self.tgid_hold_until)) + sys.stderr.write ('%f set hold tg(%s) until %f mode %s current tgid %s\n' % (time.time(), self.tgid_hold, self.tgid_hold_until, self.hold_mode, self.current_tgid)) if self.current_tgid != self.tgid_hold: self.current_tgid = self.tgid_hold self.current_srcaddr = 0 @@ -1094,18 +1665,7 @@ class rx_ctl (object): self.current_keyid = 0 new_state = self.states.CC new_frequency = tsys.trunk_cc - elif self.hold_mode is False: - if self.current_tgid: - self.tgid_hold = self.current_tgid - self.tgid_hold_until = curr_time + 86400 * 10000 - self.hold_mode = True - if self.debug > 0: - sys.stderr.write ('%f set hold tg(%s) until %f\n' % (time.time(), self.tgid_hold, self.tgid_hold_until)) - elif self.hold_mode is True: - self.current_tgid = None - self.tgid_hold = None - self.tgid_hold_until = curr_time - self.hold_mode = False + new_frequency_type = 'cc' elif command == 'set_hold': self.last_command = {'command': command, 'time': curr_time} if self.current_tgid: @@ -1133,13 +1693,18 @@ class rx_ctl (object): self.tgid_hold = None self.tgid_hold_until = curr_time self.hold_mode = False + tsys.current_srcaddr = 0 if self.current_state != self.states.CC: new_state = self.states.CC new_frequency = tsys.trunk_cc + new_frequency_type = 'cc' else: sys.stderr.write('update_state: unknown command: %s\n' % command) assert 0 == 1 + if new_frequency is not None and tsys.trunk_cc != tsys.rfss_chan: + sys.stderr.write('warning: trunk control channel frequency %f does not match rfss frequency %f\n' % (tsys.trunk_cc/1000000.0, tsys.rfss_chan/1000000.0)) + hunted_cc = tsys.hunt_cc(curr_time) if self.enabled_nacs is not None and self.current_nac not in self.enabled_nacs: @@ -1156,6 +1721,7 @@ class rx_ctl (object): tsys.current_grpaddr = 0 new_state = self.states.CC new_frequency = tsys.trunk_cc + new_frequency_type = 'cc' elif self.wait_until <= curr_time and self.tgid_hold_until <= curr_time and self.hold_mode is False and new_state is None: self.wait_until = curr_time + self.TSYS_HOLD_TIME tsys.current_srcaddr = 0 @@ -1167,33 +1733,37 @@ class rx_ctl (object): nac = self.current_nac = new_nac tsys = self.trunked_systems[nac] new_frequency = tsys.trunk_cc + new_frequency_type = 'cc' tsys.current_srcaddr = 0 tsys.current_grpaddr = 0 self.current_tgid = None if new_frequency is not None: - params = { - 'freq': new_frequency, - 'tgid': self.current_tgid, - 'offset': tsys.offset, - 'tag': tsys.get_tag(self.current_tgid), - 'nac': nac, - 'system': tsys.sysname, - 'center_frequency': tsys.center_frequency, - 'tdma': new_slot, - 'wacn': tsys.ns_wacn, - 'sysid': tsys.ns_syid, - 'srcaddr': tsys.current_srcaddr, - 'grpaddr': tsys.current_grpaddr, - 'alg': tsys.current_alg, - 'algid': tsys.current_algid, - 'keyid': tsys.current_keyid } - self.last_freq_params = params + params = tsys.frequency_change_params(self.current_tgid, new_frequency, nac, new_slot, new_frequency_type, curr_time) + self.status_msg = 'F %f TG %s %s at %s\n' % (params['freq'] / 1000000.0, params['tgid'], params['tag'], time.asctime()) self.set_frequency(params) if new_state is not None: self.current_state = new_state + def parallel_hunt_cc(self): + curr_time = time.time() + if curr_time < self.next_hunt_time: + return + self.next_hunt_time = curr_time + 1.0 + for nac in self.trunked_systems.keys(): + tsys = self.trunked_systems[nac] + rc = tsys.hunt_cc(curr_time) + if not rc: + continue + tgid = None + freq = tsys.trunk_cc + new_slot = None + new_frequency_type = 'cc' + params = tsys.frequency_change_params(tgid, freq, nac, new_slot, new_frequency_type, curr_time) + self.status_msg = 'F %f TG %s %s at %s\n' % (params['freq'] / 1000000.0, params['tgid'], params['tag'], time.asctime()) + self.set_frequency(params) + def main(): q = 0x3a000012ae01013348704a54 rc = crc16(q,12) diff --git a/op25/gr-op25_repeater/apps/tsvfile.py b/op25/gr-op25_repeater/apps/tsvfile.py old mode 100644 new mode 100755 index 77f74b5..867e331 --- a/op25/gr-op25_repeater/apps/tsvfile.py +++ b/op25/gr-op25_repeater/apps/tsvfile.py @@ -19,6 +19,7 @@ # 02110-1301, USA. import sys +import os import csv def get_frequency(f): # return frequency in Hz @@ -59,6 +60,55 @@ def get_int_dict(s): def utf_ascii(ustr): return (ustr.decode("utf-8")).encode("ascii", "ignore") +class id_registry: + def __init__(self): + self.cache = {} + self.wildcards = {} + + def add(self, id_str, tag, color): + if '-' in id_str: + a = [int(x) for x in id_str.split('-')] + if a[0] >= a[1]: + raise AssertionError('invalid ID range in %s' % id_str) + for i in range(a[0], a[1]+1): + self.cache[i] = {'tag': tag, 'color': color} + elif '.' in id_str: + rc = id_str.find('.') + self.wildcards[id_str] = {'prefix': id_str[:rc], 'len': len(id_str), 'tag': tag, 'color': color, 'type': '.'} + elif '*' in id_str: + rc = id_str.find('*') + self.wildcards[id_str] = {'prefix': id_str[:rc], 'len': 0, 'tag': tag, 'color': color, 'type': '*'} + else: + self.cache[int(id_str)] = {'tag': tag, 'color': color} + + def lookup(self, id): + if id in self.cache.keys(): + return self.cache[id] + id_str = '%d' % id + for w in self.wildcards.keys(): + if not id_str.startswith(self.wildcards[w]['prefix']): + continue + if self.wildcards[w]['type'] == '.' and len(id_str) != self.wildcards[w]['len']: + continue + # wildcard matched + self.cache[id] = {'tag': self.wildcards[w]['tag'], 'color': self.wildcards[w]['color']} + return self.cache[id] + # not found in cache, and no wildcard matched + self.cache[id] = None + return None + + def get_color(self, id): + d = self.lookup(id) + if not d: + return 0 + return d['color'] + + def get_tag(self, id): + d = self.lookup(id) + if not d: + return "" + return d['tag'] + def load_tsv(tsv_filename): hdrmap = [] configs = {} @@ -82,10 +132,26 @@ def load_tsv(tsv_filename): configs[nac] = fields return configs +def read_tags_file(tags_file, result): + import csv + with open(tags_file, 'r') as csvfile: + sreader = csv.reader(csvfile, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL) + for row in sreader: + id_str = row[0] + txt = row[1] + if len(row) >= 3: + try: + color = int(row[2]) + except ValueError as ex: + color = 0 + else: + color = 0 + result.add(id_str, txt, color) + def make_config(configs): result_config = {} for nac in configs: - result_config[nac] = {'cclist':[], 'offset':0, 'whitelist':None, 'blacklist':{}, 'tgid_map':{}, 'sysname': configs[nac]['sysname'], 'center_frequency': None} + result_config[nac] = {'cclist':[], 'offset':0, 'whitelist':None, 'blacklist':{}, 'tgid_map':id_registry(), 'unit_id_map': id_registry(), 'sysname': configs[nac]['sysname'], 'center_frequency': None} for f in configs[nac]['control_channel_list'].split(','): result_config[nac]['cclist'].append(get_frequency(f)) if 'offset' in configs[nac]: @@ -98,23 +164,20 @@ def make_config(configs): if k in configs[nac]: result_config[nac][k] = get_int_dict(configs[nac][k]) if 'tgid_tags_file' in configs[nac]: - import csv - with open(configs[nac]['tgid_tags_file'], 'r') as csvfile: - sreader = csv.reader(csvfile, delimiter='\t', quotechar='"', quoting=csv.QUOTE_ALL) - for row in sreader: - try: - tgid = int(row[0]) - txt = row[1] - except (IndexError, ValueError) as ex: - continue - if len(row) >= 3: - try: - prio = int(row[2]) - except ValueError as ex: - prio = 3 - else: - prio = 3 - result_config[nac]['tgid_map'][tgid] = (txt, prio) + fname = configs[nac]['tgid_tags_file'] + if ',' in fname: + l = fname.split(',') + tgid_tags_file = l[0] + unit_id_tags_file = l[1] + else: + tgid_tags_file = fname + unit_id_tags_file = None + result_config[nac]['tgid_tags_file'] = tgid_tags_file + result_config[nac]['unit_id_tags_file'] = unit_id_tags_file + read_tags_file(tgid_tags_file, result_config[nac]['tgid_map']) + if unit_id_tags_file is not None and os.access(unit_id_tags_file, os.R_OK): + read_tags_file(unit_id_tags_file, result_config[nac]['unit_id_map']) + if 'center_frequency' in configs[nac]: result_config[nac]['center_frequency'] = get_frequency(configs[nac]['center_frequency']) return result_config diff --git a/op25/gr-op25_repeater/apps/ui-settings.json b/op25/gr-op25_repeater/apps/ui-settings.json new file mode 100644 index 0000000..b664cc0 --- /dev/null +++ b/op25/gr-op25_repeater/apps/ui-settings.json @@ -0,0 +1,34 @@ +{ + "valSystemFont": "24", + "valTagFont": "24", + "valFontStyle": "normal", + "sc1": "police pd sheriff so swat law", + "sc2": "fd fire", + "sc3": "ac amr ccco pw", + "sc4": "", + "log_len": "2500", + "color_main_tag": true, + "color_main_sys": false, + "smartcolors": true, + "log_cc": true, + "log_cf": false, + "log_tu": false, + "log_rx": false, + "show_adj": false, + "je_joins": true, + "je_calls": false, + "je_deny": true, + "je_log": true, + "hide_enc": false, + "trailing_zeros": true, + "selDispMode": "dark", + "acc1": "#790000", + "acc2": "#004364", + "sysColor": "#3d81e7", + "valColor": "#00ffff", + "btnColor": "#20ffff", + "unk_default": "99", + "ani_speed": "250", + "showBandPlan": true, + "showSlot": true +} \ No newline at end of file diff --git a/op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h b/op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h index 5e4301c..f53a066 100644 --- a/op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h +++ b/op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h @@ -47,7 +47,7 @@ namespace gr { * class. op25_repeater::frame_assembler::make is the public interface for * creating new instances. */ - static sptr make(const char* options, int debug, gr::msg_queue::sptr queue); + static sptr make(const char* options, int debug, gr::msg_queue::sptr queue, int msgq_id); virtual void set_xormask(const char*p) {} virtual void set_nac(int nac) {} virtual void set_slotid(int slotid) {} diff --git a/op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h b/op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h index d1efaa0..4d37bf9 100644 --- a/op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h +++ b/op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h @@ -50,6 +50,10 @@ namespace gr { virtual void set_omega(float omega) {} virtual float get_freq_error(void) {} virtual int get_error_band(void) {} + virtual void set_muted(bool) {} + virtual bool is_muted(void) {} + virtual void set_tdma(bool) {} + virtual bool is_tdma(void) {} }; } // namespace op25_repeater diff --git a/op25/gr-op25_repeater/include/op25_repeater/p25_frame_assembler.h b/op25/gr-op25_repeater/include/op25_repeater/p25_frame_assembler.h index ef544fc..ff366c7 100644 --- a/op25/gr-op25_repeater/include/op25_repeater/p25_frame_assembler.h +++ b/op25/gr-op25_repeater/include/op25_repeater/p25_frame_assembler.h @@ -47,7 +47,7 @@ namespace gr { * class. op25_repeater::p25_frame_assembler::make is the public interface for * creating new instances. */ - static sptr make(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, bool do_audio_output, bool do_phase2_tdma); + static sptr make(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, bool do_audio_output, bool do_phase2_tdma, int msgq_id); virtual void set_xormask(const char*p) {} virtual void set_nac(int nac) {} virtual void set_slotid(int slotid) {} diff --git a/op25/gr-op25_repeater/lib/frame_assembler_impl.cc b/op25/gr-op25_repeater/lib/frame_assembler_impl.cc index d40c33e..8f0f3ca 100644 --- a/op25/gr-op25_repeater/lib/frame_assembler_impl.cc +++ b/op25/gr-op25_repeater/lib/frame_assembler_impl.cc @@ -54,10 +54,10 @@ namespace gr { } frame_assembler::sptr - frame_assembler::make(const char* options, int debug, gr::msg_queue::sptr queue) + frame_assembler::make(const char* options, int debug, gr::msg_queue::sptr queue, int msgq_id) { return gnuradio::get_initial_sptr - (new frame_assembler_impl(options, debug, queue)); + (new frame_assembler_impl(options, debug, queue, msgq_id)); } /* @@ -77,12 +77,13 @@ static const int MAX_IN = 1; // maximum number of input streams /* * The private constructor */ - frame_assembler_impl::frame_assembler_impl(const char* options, int debug, gr::msg_queue::sptr queue) + frame_assembler_impl::frame_assembler_impl(const char* options, int debug, gr::msg_queue::sptr queue, int msgq_id) : gr::block("frame_assembler", gr::io_signature::make (MIN_IN, MAX_IN, sizeof (char)), gr::io_signature::make (0, 0, 0)), d_msg_queue(queue), - d_sync(options, debug, queue) + d_sync(options, debug, queue, msgq_id), + d_msgq_id(msgq_id) { } diff --git a/op25/gr-op25_repeater/lib/frame_assembler_impl.h b/op25/gr-op25_repeater/lib/frame_assembler_impl.h index 8cb3389..6080f60 100644 --- a/op25/gr-op25_repeater/lib/frame_assembler_impl.h +++ b/op25/gr-op25_repeater/lib/frame_assembler_impl.h @@ -43,6 +43,7 @@ namespace gr { int d_debug; gr::msg_queue::sptr d_msg_queue; rx_sync d_sync; + int d_msgq_id; // internal functions @@ -56,7 +57,7 @@ namespace gr { public: public: - frame_assembler_impl(const char* options, int debug, gr::msg_queue::sptr queue); + frame_assembler_impl(const char* options, int debug, gr::msg_queue::sptr queue, int msgq_id); ~frame_assembler_impl(); // Where all the action really happens diff --git a/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc b/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc index cbc2368..dc36b74 100644 --- a/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc +++ b/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc @@ -90,7 +90,25 @@ uint8_t gardner_costas_cc_impl::slicer(float sym) { // fprintf(stderr, "P25P1 Framing detect\n"); UPDATE_COUNT(' ') } - else if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ 0x001050551155LL, 0, 48)) { + else if(check_frame_sync((nid_accum & P25P2_FRAME_SYNC_MASK) ^ P25P2_FRAME_SYNC_MAGIC, 0, 40)) { +// fprintf(stderr, "P25P2 Framing detect\n"); + UPDATE_COUNT(' ') + } + if (d_is_tdma) { + if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ 0x000104015155LL, 0, 40)) { + fprintf(stderr, "TDMA: channel %d tuning error -1200\n", -1); + UPDATE_COUNT('-') + } + else if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ 0xfefbfeaeaaLL, 0, 40)) { + fprintf(stderr, "TDMA: channel %d tuning error +1200\n", -1); + UPDATE_COUNT('+') + } + else if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ 0xa8a2a80800LL, 0, 40)) { + fprintf(stderr, "TDMA: channel %d tuning error +/- 2400\n", -1); + UPDATE_COUNT('|') + } + } else { + if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ 0x001050551155LL, 0, 48)) { // fprintf(stderr, "tuning error -1200\n"); UPDATE_COUNT('-') } @@ -102,10 +120,7 @@ uint8_t gardner_costas_cc_impl::slicer(float sym) { // fprintf(stderr, "tuning error +/- 2400\n"); UPDATE_COUNT('|') } - else if(check_frame_sync((nid_accum & P25P2_FRAME_SYNC_MASK) ^ P25P2_FRAME_SYNC_MAGIC, 0, 40)) { -// fprintf(stderr, "P25P2 Framing detect\n"); - UPDATE_COUNT(' ') - } + } if (d_event_type == ' ' || d_event_count < 5) { d_update_request = 0; } else { @@ -150,7 +165,7 @@ uint8_t gardner_costas_cc_impl::slicer(float sym) { d_event_count(0), d_event_type(' '), d_symbol_seq(samples_per_symbol * 4800), d_update_request(0), - d_fm(0), d_fm_accum(0), d_fm_count(0) + d_fm(0), d_fm_accum(0), d_fm_count(0), d_muted(false), d_is_tdma(false) { set_omega(samples_per_symbol); set_relative_rate (1.0 / d_omega); @@ -259,6 +274,8 @@ gardner_costas_cc_impl::general_work (int noutput_items, int i=0, o=0; gr_complex symbol, sample, nco; + gr_complex interp_samp, interp_samp_mid, diffdec; + float error_real, error_imag, symbol_error; while((o < noutput_items) && (i < ninput_items[0])) { while((d_mu > 1.0) && (i < ninput_items[0])) { @@ -293,6 +310,9 @@ gardner_costas_cc_impl::general_work (int noutput_items, } if(i < ninput_items[0]) { + // to mitigate tracking drift in the event of no input signal + // skip tracking on muted channel + if (!d_muted) { float half_omega = d_omega / 2.0; int half_sps = (int) floorf(half_omega); float half_mu = d_mu + half_omega - (float) half_sps; @@ -304,18 +324,19 @@ gardner_costas_cc_impl::general_work (int noutput_items, // half_mu the fractional part, of the halfway mark. // locate two points, separated by half of one symbol time // interp_samp is (we hope) at the optimum sampling point - gr_complex interp_samp_mid = d_interp->interpolate(&d_dl[ d_dl_index ], d_mu); - gr_complex interp_samp = d_interp->interpolate(&d_dl[ d_dl_index + half_sps], half_mu); + interp_samp_mid = d_interp->interpolate(&d_dl[ d_dl_index ], d_mu); + interp_samp = d_interp->interpolate(&d_dl[ d_dl_index + half_sps], half_mu); - float error_real = (d_last_sample.real() - interp_samp.real()) * interp_samp_mid.real(); - float error_imag = (d_last_sample.imag() - interp_samp.imag()) * interp_samp_mid.imag(); - gr_complex diffdec = interp_samp * conj(d_last_sample); - /* cpu reduction */ (void)slicer(std::arg(diffdec)); + error_real = (d_last_sample.real() - interp_samp.real()) * interp_samp_mid.real(); + error_imag = (d_last_sample.imag() - interp_samp.imag()) * interp_samp_mid.imag(); + diffdec = interp_samp * conj(d_last_sample); + if (!d_muted) // if muted, assume channel idle (suspend tuning error checks) + (void)slicer(std::arg(diffdec)); d_last_sample = interp_samp; // save for next time #if 1 - float symbol_error = error_real + error_imag; // Gardner loop error + symbol_error = error_real + error_imag; // Gardner loop error #else - float symbol_error = ((sgn(interp_samp) - sgn(d_last_sample)) * conj(interp_samp_mid)).real(); + symbol_error = ((sgn(interp_samp) - sgn(d_last_sample)) * conj(interp_samp_mid)).real(); #endif if (std::isnan(symbol_error)) symbol_error = 0.0; if (symbol_error < -1.0) symbol_error = -1.0; @@ -326,11 +347,19 @@ gardner_costas_cc_impl::general_work (int noutput_items, #if VERBOSE_GARDNER printf("%f\t%f\t%f\t%f\t%f\n", symbol_error, d_mu, d_omega, error_real, error_imag); #endif + } else { + symbol_error = 0; + } /* end of if (!d_muted) */ d_mu += d_omega + d_gain_mu * symbol_error; // update mu based on loop error + if (!d_muted) { phase_error_tracking(diffdec * PT_45); - - out[o++] = interp_samp; + } /* end of if (!d_muted) */ + + if (d_muted) + out[o++] = 0.0; + else + out[o++] = interp_samp; } } diff --git a/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h b/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h index 6614696..c413557 100644 --- a/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h +++ b/op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h @@ -66,9 +66,18 @@ namespace gr { void set_verbose (bool verbose) { d_verbose = verbose; } //! Sets value of omega and its min and max values - void set_omega (float omega); - float get_freq_error(void); - int get_error_band(void); + inline void set_omega (float omega); + inline float get_freq_error(void); + inline int get_error_band(void); + inline void set_muted(bool v) { + if (v == false && d_muted == true) { + d_event_count = 0; // mute state change from muted to unmuted + } + d_muted = v; + } + inline bool is_muted(void) { return d_muted; } + inline void set_tdma(bool v) { d_is_tdma = v; } + inline bool is_tdma(void) { return d_is_tdma; } protected: bool input_sample0(gr_complex, gr_complex& outp); @@ -110,6 +119,8 @@ protected: float d_fm; float d_fm_accum; int d_fm_count; + bool d_muted; + bool d_is_tdma; float phase_error_detector_qpsk(gr_complex sample); void phase_error_tracking(gr_complex sample); diff --git a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc index 4485760..43086c2 100644 --- a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc +++ b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc @@ -39,14 +39,23 @@ namespace gr { void p25_frame_assembler_impl::p25p2_queue_msg(int duid) { - static const unsigned char wbuf[2] = { (unsigned char) ((d_nac >> 8) & 0xff), (unsigned char) (d_nac & 0xff) }; + unsigned char wbuf[8]; + int p=0; + if (!d_do_msgq) return; if (d_msg_queue->full_p()) return; if (!d_nac) return; - gr::message::sptr msg = gr::message::make_from_string(std::string((const char *)wbuf, 2), duid, 0, 0); + + wbuf[p++] = 0xaa; + wbuf[p++] = 0x55; + wbuf[p++] = (d_msgq_id >> 8) & 0xff; + wbuf[p++] = d_msgq_id & 0xff; + wbuf[p++] = (d_nac >> 8) & 0xff; + wbuf[p++] = d_nac & 0xff; + gr::message::sptr msg = gr::message::make_from_string(std::string((const char *)wbuf, p), duid, 0, 0); d_msg_queue->insert_tail(msg); } @@ -64,10 +73,10 @@ namespace gr { } p25_frame_assembler::sptr - p25_frame_assembler::make(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, bool do_audio_output, bool do_phase2_tdma) + p25_frame_assembler::make(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, bool do_audio_output, bool do_phase2_tdma, int msgq_id) { return gnuradio::get_initial_sptr - (new p25_frame_assembler_impl(udp_host, port, debug, do_imbe, do_output, do_msgq, queue, do_audio_output, do_phase2_tdma)); + (new p25_frame_assembler_impl(udp_host, port, debug, do_imbe, do_output, do_msgq, queue, do_audio_output, do_phase2_tdma, msgq_id)); } /* @@ -87,7 +96,7 @@ static const int MAX_IN = 1; // maximum number of input streams /* * The private constructor */ - p25_frame_assembler_impl::p25_frame_assembler_impl(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, bool do_audio_output, bool do_phase2_tdma) + p25_frame_assembler_impl::p25_frame_assembler_impl(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, bool do_audio_output, bool do_phase2_tdma, int msgq_id) : gr::block("p25_frame_assembler", gr::io_signature::make (MIN_IN, MAX_IN, sizeof (char)), gr::io_signature::make ((do_output) ? 1 : 0, (do_output) ? 1 : 0, (do_audio_output && do_output) ? sizeof(int16_t) : ((do_output) ? sizeof(char) : 0 ))), @@ -95,13 +104,14 @@ static const int MAX_IN = 1; // maximum number of input streams d_do_output(do_output), output_queue(), op25audio(udp_host, port, debug), - p1fdma(op25audio, debug, do_imbe, do_output, do_msgq, queue, output_queue, do_audio_output), + p1fdma(op25audio, debug, do_imbe, do_output, do_msgq, queue, output_queue, do_audio_output, msgq_id), d_do_audio_output(do_audio_output), d_do_phase2_tdma(do_phase2_tdma), - p2tdma(op25audio, 0, debug, do_msgq, queue, output_queue, do_audio_output), + p2tdma(op25audio, 0, debug, do_msgq, queue, output_queue, do_audio_output, msgq_id), d_do_msgq(do_msgq), d_msg_queue(queue), - d_nac(0) + d_nac(0), + d_msgq_id(msgq_id) { fprintf(stderr, "p25_frame_assembler_impl: do_imbe[%d], do_output[%d], do_audio_output[%d], do_phase2_tdma[%d]\n", do_imbe, do_output, do_audio_output, do_phase2_tdma); } diff --git a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.h b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.h index 4514956..5c53a77 100644 --- a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.h +++ b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.h @@ -51,6 +51,7 @@ namespace gr { bool d_do_msgq; gr::msg_queue::sptr d_msg_queue; int d_nac; + int d_msgq_id; // internal functions @@ -66,7 +67,7 @@ namespace gr { // Nothing to declare in this block. public: - p25_frame_assembler_impl(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, bool do_audio_output, bool do_phase2_tdma); + p25_frame_assembler_impl(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, bool do_audio_output, bool do_phase2_tdma, int msgq_id); ~p25_frame_assembler_impl(); op25_audio op25audio; diff --git a/op25/gr-op25_repeater/lib/p25_framer.cc b/op25/gr-op25_repeater/lib/p25_framer.cc index 9cbcd0e..bd6eec7 100644 --- a/op25/gr-op25_repeater/lib/p25_framer.cc +++ b/op25/gr-op25_repeater/lib/p25_framer.cc @@ -33,8 +33,9 @@ static const int max_frame_lengths[16] = { }; // constructor -p25_framer::p25_framer(int debug) : +p25_framer::p25_framer(int debug, int msgq_id) : d_debug(debug), + d_msgq_id(msgq_id), reverse_p(0), nid_syms(0), next_bit(0), @@ -61,6 +62,7 @@ p25_framer::~p25_framer () */ bool p25_framer::nid_codeword(uint64_t acc) { bit_vector cw(64); + uint64_t save_acc = acc; // save the parity lsb, not used by BCH` int acc_parity = acc & 1; @@ -109,6 +111,7 @@ bool p25_framer::nid_codeword(uint64_t acc) { * Returns true when complete frame received, else false */ bool p25_framer::rx_sym(uint8_t dibit) { + struct timeval currtime; symbols_received++; bool rc = false; dibit ^= reverse_p; @@ -134,7 +137,7 @@ bool p25_framer::rx_sym(uint8_t dibit) { if (nid_syms > 0) // if nid accumulation in progress nid_syms++; // count symbols in nid - if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ P25_FRAME_SYNC_MAGIC, 6, 48)) { + if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ P25_FRAME_SYNC_MAGIC, 3, 48)) { nid_syms = 1; } if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ P25_FRAME_SYNC_REV_P, 0, 48)) { @@ -143,13 +146,16 @@ bool p25_framer::rx_sym(uint8_t dibit) { fprintf(stderr, "Reversed FS polarity detected - autocorrecting\n"); } if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ 0x001050551155LL, 0, 48)) { - fprintf(stderr, "tuning error -1200\n"); + gettimeofday(&currtime, 0); + fprintf(stderr, "%010lu.%06lu channel %d tuning error -1200\n", currtime.tv_sec, currtime.tv_usec, d_msgq_id); } if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ 0xFFEFAFAAEEAALL, 0, 48)) { - fprintf(stderr, "tuning error +1200\n"); + gettimeofday(&currtime, 0); + fprintf(stderr, "%010lu.%06lu channel %d tuning error +1200\n", currtime.tv_sec, currtime.tv_usec, d_msgq_id); } if(check_frame_sync((nid_accum & P25_FRAME_SYNC_MASK) ^ 0xAA8A0A008800LL, 0, 48)) { - fprintf(stderr, "tuning error +/- 2400\n"); + gettimeofday(&currtime, 0); + fprintf(stderr, "%010lu.%06lu channel %d tuning error +/- 2400\n", currtime.tv_sec, currtime.tv_usec, d_msgq_id); } if (next_bit > 0) { frame_body[next_bit++] = (dibit >> 1) & 1; diff --git a/op25/gr-op25_repeater/lib/p25_framer.h b/op25/gr-op25_repeater/lib/p25_framer.h index 211437d..1a7fe55 100644 --- a/op25/gr-op25_repeater/lib/p25_framer.h +++ b/op25/gr-op25_repeater/lib/p25_framer.h @@ -27,9 +27,10 @@ private: uint32_t frame_size_limit; int d_debug; + int d_msgq_id; public: - p25_framer(int debug = 0); + p25_framer(int debug = 0, int msgq_id=0); ~p25_framer (); // destructor bool rx_sym(uint8_t dibit) ; diff --git a/op25/gr-op25_repeater/lib/p25p1_fdma.cc b/op25/gr-op25_repeater/lib/p25p1_fdma.cc index f18a99a..d1d4d40 100644 --- a/op25/gr-op25_repeater/lib/p25p1_fdma.cc +++ b/op25/gr-op25_repeater/lib/p25p1_fdma.cc @@ -191,7 +191,7 @@ block_deinterleave(bit_vector& bv, unsigned int start, uint8_t* buf) return 0; } -p25p1_fdma::p25p1_fdma(const op25_audio& udp, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, std::deque &output_queue, bool do_audio_output) : +p25p1_fdma::p25p1_fdma(const op25_audio& udp, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, std::deque &output_queue, bool do_audio_output, int msgq_id) : op25audio(udp), write_bufp(0), d_debug(debug), @@ -200,11 +200,12 @@ p25p1_fdma::p25p1_fdma(const op25_audio& udp, int debug, bool do_imbe, bool do_o d_do_msgq(do_msgq), d_msg_queue(queue), output_queue(output_queue), - framer(new p25_framer(debug)), + framer(new p25_framer(debug, msgq_id)), d_do_audio_output(do_audio_output), ess_algid(0x80), ess_keyid(0), vf_tgid(0), + d_msgq_id(msgq_id), p1voice_decode((debug > 0), udp, output_queue) { gettimeofday(&last_qtime, 0); @@ -219,7 +220,11 @@ p25p1_fdma::process_duid(uint32_t const duid, uint32_t const nac, const uint8_t* return; if (d_msg_queue->full_p()) return; - assert (len+2 <= sizeof(wbuf)); + assert (len+6 <= sizeof(wbuf)); + wbuf[p++] = 0xaa; + wbuf[p++] = 0x55; + wbuf[p++] = (d_msgq_id >> 8) & 0xff; + wbuf[p++] = d_msgq_id & 0xff; wbuf[p++] = (nac >> 8) & 0xff; wbuf[p++] = nac & 0xff; if (buf) { @@ -450,8 +455,11 @@ void p25p1_fdma::process_LCW(std::vector& HB) { int ec = rs12.decode(HB); // Reed Solomon (24,12,13) error correction - if (ec < 0) + if (ec < 0) { + if (d_debug >= 10) + fprintf(stderr, "p25p1_fdma::process_LCW: rs decode failure\n"); return; // failed CRC + } int i, j; std::vector lcw(9,0); // Convert hexbits to bytes @@ -660,10 +668,11 @@ p25p1_fdma::reset_timer() void p25p1_fdma::send_msg(const std::string msg_str, long msg_type) { + unsigned char hdr[4] = {0xaa, 0x55, (unsigned char)((d_msgq_id >> 8) & 0xff), (unsigned char) (d_msgq_id & 0xff)}; if (!d_do_msgq || d_msg_queue->full_p()) return; - gr::message::sptr msg = gr::message::make_from_string(msg_str, msg_type, 0, 0); + gr::message::sptr msg = gr::message::make_from_string(std::string((char*)hdr, 4) + msg_str, msg_type, 0, 0); d_msg_queue->insert_tail(msg); } @@ -671,6 +680,7 @@ void p25p1_fdma::rx_sym (const uint8_t *syms, int nsyms) { struct timeval currtime; + unsigned char hdr[4] = {0xaa, 0x55, (unsigned char)((d_msgq_id >> 8) & 0xff), (unsigned char) (d_msgq_id & 0xff)}; for (int i1 = 0; i1 < nsyms; i1++){ if(framer->rx_sym(syms[i1])) { // complete frame was detected @@ -748,7 +758,7 @@ p25p1_fdma::rx_sym (const uint8_t *syms, int nsyms) } gettimeofday(&last_qtime, 0); - gr::message::sptr msg = gr::message::make(-1, 0, 0); + gr::message::sptr msg = gr::message::make_from_string(std::string((char*)hdr, 4), -1, 0, 0); d_msg_queue->insert_tail(msg); } } diff --git a/op25/gr-op25_repeater/lib/p25p1_fdma.h b/op25/gr-op25_repeater/lib/p25p1_fdma.h index e7053ff..e770478 100644 --- a/op25/gr-op25_repeater/lib/p25p1_fdma.h +++ b/op25/gr-op25_repeater/lib/p25p1_fdma.h @@ -84,11 +84,12 @@ namespace gr { uint16_t ess_algid; uint8_t ess_mi[9] = {0}; uint16_t vf_tgid; + int d_msgq_id; public: void reset_timer(); void rx_sym (const uint8_t *syms, int nsyms); - p25p1_fdma(const op25_audio& udp, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, std::deque &output_queue, bool do_audio_output); + p25p1_fdma(const op25_audio& udp, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, std::deque &output_queue, bool do_audio_output, int msgq_id); ~p25p1_fdma(); // Where all the action really happens diff --git a/op25/gr-op25_repeater/lib/p25p2_tdma.cc b/op25/gr-op25_repeater/lib/p25p2_tdma.cc index b4f92be..1d83a14 100644 --- a/op25/gr-op25_repeater/lib/p25p2_tdma.cc +++ b/op25/gr-op25_repeater/lib/p25p2_tdma.cc @@ -34,6 +34,7 @@ #include "mbelib.h" #include "ambe.h" #include "value_string.h" +#include "crc16.h" static const int BURST_SIZE = 180; static const int SUPERFRAME_SIZE = (12*BURST_SIZE); @@ -87,7 +88,7 @@ static const uint8_t mac_msg_len[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 13, 11, 0, 0, 0 }; -p25p2_tdma::p25p2_tdma(const op25_audio& udp, int slotid, int debug, bool do_msgq, gr::msg_queue::sptr queue, std::deque &qptr, bool do_audio_output) : // constructor +p25p2_tdma::p25p2_tdma(const op25_audio& udp, int slotid, int debug, bool do_msgq, gr::msg_queue::sptr queue, std::deque &qptr, bool do_audio_output, int msgq_id) : // constructor op25audio(udp), write_bufp(0), tdma_xormask(new uint8_t[SUPERFRAME_SIZE]), @@ -105,6 +106,7 @@ p25p2_tdma::p25p2_tdma(const op25_audio& udp, int slotid, int debug, bool do_msg ESS_B(16,0), ess_algid(0x80), ess_keyid(0), + d_msgq_id(msgq_id), p2framer() { assert (slotid == 0 || slotid == 1); @@ -145,6 +147,10 @@ int p25p2_tdma::process_mac_pdu(const uint8_t byte_buf[], const unsigned int len switch (opcode) { + case 0: // MAC_SIGNAL + handle_mac_signal(byte_buf, len); + break; + case 1: // MAC_PTT handle_mac_ptt(byte_buf, len); break; @@ -171,6 +177,16 @@ int p25p2_tdma::process_mac_pdu(const uint8_t byte_buf[], const unsigned int len return opcode_map[opcode]; } +void p25p2_tdma::handle_mac_signal(const uint8_t byte_buf[], const unsigned int len) +{ + char nac_color[2]; + int i; + i = (byte_buf[19] << 4) + ((byte_buf[20] >> 4) & 0xf); + nac_color[0] = i >> 8; + nac_color[1] = i & 0xff; + send_msg(std::string(nac_color, 2) + std::string((const char *)byte_buf, len), -6); +} + void p25p2_tdma::handle_mac_ptt(const uint8_t byte_buf[], const unsigned int len) { uint32_t srcaddr = (byte_buf[13] << 16) + (byte_buf[14] << 8) + byte_buf[15]; @@ -372,7 +388,7 @@ void p25p2_tdma::decode_mac_msg(const uint8_t byte_buf[], const unsigned int len } } -int p25p2_tdma::handle_acch_frame(const uint8_t dibits[], bool fast) +int p25p2_tdma::handle_acch_frame(const uint8_t dibits[], bool fast, bool is_lcch) { int i, j, rc; uint8_t bits[512]; @@ -430,6 +446,8 @@ int p25p2_tdma::handle_acch_frame(const uint8_t dibits[], bool fast) j++; } rc = rs28.decode(HB, Erasures); +// if (d_debug >= 10) +// fprintf(stderr, "p25p2_tdma: rc28: rc %d\n", rc); if (rc < 0) return -1; @@ -439,7 +457,7 @@ int p25p2_tdma::handle_acch_frame(const uint8_t dibits[], bool fast) } else { j = 5; - len = 168; + len = (is_lcch) ? 180 : 168; } for (i = 0; i < len; i += 6) { // convert hexbits back to bits bits[i] = (HB[j] & 0x20) >> 5; @@ -451,12 +469,17 @@ int p25p2_tdma::handle_acch_frame(const uint8_t dibits[], bool fast) j++; } + bool crc_ok = (is_lcch) ? (crc16(bits, len) == 0) : crc12_ok(bits, len); + int olen = (is_lcch) ? 23 : len/8; rc = -1; - if (crc12_ok(bits, len)) { // TODO: rewrite crc12 so we don't have to do so much bit manipulation - for (int i=0; i> 8) & 0xff), (unsigned char)(d_msgq_id & 0xff)}; if (!d_do_msgq || d_msg_queue->full_p()) return; - gr::message::sptr msg = gr::message::make_from_string(msg_str, msg_type, 0, 0); + gr::message::sptr msg = gr::message::make_from_string(std::string((char*)hdr, 4) + msg_str, msg_type, 0, 0); d_msg_queue->insert_tail(msg); } diff --git a/op25/gr-op25_repeater/lib/p25p2_tdma.h b/op25/gr-op25_repeater/lib/p25p2_tdma.h index cfdb7fb..4783816 100644 --- a/op25/gr-op25_repeater/lib/p25p2_tdma.h +++ b/op25/gr-op25_repeater/lib/p25p2_tdma.h @@ -40,7 +40,7 @@ class p25p2_tdma { public: - p25p2_tdma(const op25_audio& udp, int slotid, int debug, bool do_msgq, gr::msg_queue::sptr queue, std::deque &qptr, bool do_audio_output) ; // constructor + p25p2_tdma(const op25_audio& udp, int slotid, int debug, bool do_msgq, gr::msg_queue::sptr queue, std::deque &qptr, bool do_audio_output, int msgq_id) ; // constructor int handle_packet(const uint8_t dibits[]) ; void set_slotid(int slotid); uint8_t* tdma_xormask; @@ -83,12 +83,14 @@ private: uint8_t ess_keyid; uint16_t ess_algid; uint8_t ess_mi[9] = {0}; + int d_msgq_id; p25p2_framer p2framer; - int handle_acch_frame(const uint8_t dibits[], bool fast) ; + int handle_acch_frame(const uint8_t dibits[], bool fast, bool is_lcch) ; void handle_voice_frame(const uint8_t dibits[]) ; int process_mac_pdu(const uint8_t byte_buf[], const unsigned int len) ; + void handle_mac_signal(const uint8_t byte_buf[], const unsigned int len) ; void handle_mac_ptt(const uint8_t byte_buf[], const unsigned int len) ; void handle_mac_end_ptt(const uint8_t byte_buf[], const unsigned int len) ; void handle_mac_idle(const uint8_t byte_buf[], const unsigned int len) ; diff --git a/op25/gr-op25_repeater/lib/rx_sync.cc b/op25/gr-op25_repeater/lib/rx_sync.cc index 07e779f..bf6c359 100644 --- a/op25/gr-op25_repeater/lib/rx_sync.cc +++ b/op25/gr-op25_repeater/lib/rx_sync.cc @@ -346,7 +346,7 @@ void rx_sync::dmr_sync(const uint8_t bitbuf[], int& current_slot, bool& unmute) } } -rx_sync::rx_sync(const char * options, int debug, gr::msg_queue::sptr queue) : // constructor +rx_sync::rx_sync(const char * options, int debug, gr::msg_queue::sptr queue, int msgq_id) : // constructor d_symbol_count(0), d_sync_reg(0), d_cbuf_idx(0), @@ -359,7 +359,8 @@ rx_sync::rx_sync(const char * options, int debug, gr::msg_queue::sptr queue) : / d_msg_queue(queue), d_previous_nxdn_sync(0), d_previous_nxdn_sr_structure(-1), - d_previous_nxdn_sr_ran(-1) + d_previous_nxdn_sr_ran(-1), + d_msgq_id(msgq_id) { mbe_initMbeParms (&cur_mp[0], &prev_mp[0], &enh_mp[0]); mbe_initMbeParms (&cur_mp[1], &prev_mp[1], &enh_mp[1]); @@ -597,9 +598,10 @@ void rx_sync::rx_sym(const uint8_t sym) } } -static inline void qmsg(const gr::msg_queue::sptr msg_queue, const uint8_t s[], int len) { +static inline void qmsg(const gr::msg_queue::sptr msg_queue, const uint8_t s[], int len, int msgq_id) { + unsigned char hdr[4] = {0xaa, 0x55, (unsigned char)((msgq_id >> 8) & 0xff), (unsigned char)(msgq_id & 0xff)}; if (!msg_queue->full_p()) { - gr::message::sptr msg = gr::message::make_from_string(std::string((char*)s, len), -5, 0, 0); + gr::message::sptr msg = gr::message::make_from_string(std::string((char*)hdr, 4) + std::string((char*)s, len), -5, 0, 0); msg_queue->insert_tail(msg); } } @@ -730,7 +732,7 @@ void rx_sync::nxdn_frame(const uint8_t symbol_ptr[]) answer[1] = lich; int nbytes = (answer_len + 7) / 8; cfill(answer+2, sacch_answer, nbytes); - qmsg(d_msg_queue, answer, nbytes+2); + qmsg(d_msg_queue, answer, nbytes+2, d_msgq_id); if (d_debug > 2) debug_dump("nxdn: sacch", answer, nbytes+2); } else if (answer_len > 0 && non_superframe == false) { @@ -754,7 +756,7 @@ void rx_sync::nxdn_frame(const uint8_t symbol_ptr[]) answer[2] = sr_ran; int nbytes = 9; cfill(answer+3, d_sacch_buf, nbytes); - qmsg(d_msg_queue, answer, nbytes+3); + qmsg(d_msg_queue, answer, nbytes+3, d_msgq_id); if (d_debug > 2) debug_dump("nxdn: sacch", answer, nbytes+3); d_previous_nxdn_sr_ran = -1; @@ -772,7 +774,7 @@ void rx_sync::nxdn_frame(const uint8_t symbol_ptr[]) if (answer_len > 0) { answer[0] = 'f'; answer[1] = lich; - qmsg(d_msg_queue, answer, answer_len+2); + qmsg(d_msg_queue, answer, answer_len+2, d_msgq_id); if (d_debug > 2) debug_dump("nxdn: facch", answer, answer_len+2); } @@ -787,7 +789,7 @@ void rx_sync::nxdn_frame(const uint8_t symbol_ptr[]) if (answer_len > 0) { answer[0] = 'f'; answer[1] = lich; - qmsg(d_msg_queue, answer, answer_len+2); + qmsg(d_msg_queue, answer, answer_len+2, d_msgq_id); if (d_debug > 2) debug_dump("nxdn: facch", answer, answer_len+2); } @@ -799,7 +801,7 @@ void rx_sync::nxdn_frame(const uint8_t symbol_ptr[]) if (answer_len > 0) { answer[0] = 'u'; answer[1] = lich; - qmsg(d_msg_queue, answer, answer_len+2); + qmsg(d_msg_queue, answer, answer_len+2, d_msgq_id); if (d_debug > 2) debug_dump("nxdn: facch2", answer, answer_len+2); } @@ -810,7 +812,7 @@ void rx_sync::nxdn_frame(const uint8_t symbol_ptr[]) if (answer_len > 0) { answer[0] = 'c'; answer[1] = lich; - qmsg(d_msg_queue, answer, answer_len+2); + qmsg(d_msg_queue, answer, answer_len+2, d_msgq_id); if (d_debug > 2) debug_dump("nxdn: cac", answer, answer_len+2); } diff --git a/op25/gr-op25_repeater/lib/rx_sync.h b/op25/gr-op25_repeater/lib/rx_sync.h index 2a2aa1d..35d9eb5 100644 --- a/op25/gr-op25_repeater/lib/rx_sync.h +++ b/op25/gr-op25_repeater/lib/rx_sync.h @@ -93,7 +93,7 @@ class rx_sync { public: void rx_sym(const uint8_t sym); void sync_reset(void); - rx_sync(const char * options, int debug, gr::msg_queue::sptr queue); + rx_sync(const char * options, int debug, gr::msg_queue::sptr queue, int msgq_id); ~rx_sync(); void insert_whitelist(int grpaddr); void insert_blacklist(int grpaddr); @@ -138,6 +138,7 @@ private: int d_previous_nxdn_sr_structure; int d_previous_nxdn_sr_ran; uint8_t d_sacch_buf[72]; + int d_msgq_id; }; } // end namespace op25_repeater