core rc2-final

This commit is contained in:
Max 2021-07-25 20:46:14 -04:00
parent 180a4dec97
commit 51042858e8
34 changed files with 3407 additions and 348 deletions

View File

@ -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=<device-name>:nac=0x4e1",
The NAC should be changed to match that of the system being received, and
<device-name> should refer to the assigned device.
Colors and Tags for Talkgroup and Radio IDs
===========================================
Tags and colors are defined in two TSV files, one for TGIDs and one for SUIDs.
The TSV file format, compatible with earlier versions of OP25 has the TAB
separated columns defined as:
column one: decimal TG or SU ID. May contain wildcards (see below)
column two: tag text (string)
column three(optional): encoded priority/color value, decimal (see below)
The color code is directly mapped by client JS into style sheet (CSS) colors.
If only two columns are present the third column is defaulted to zero.
The file names of the two files are specified (comma-separated) in the
trunking TSV "TGID Tags File" column (the trunking TSV in turn is the
file referred to by the "-T" command option of rx.py or multi_rx.py).
The talkgroup tags file name is specified first, followed by a comma,
then the SUID tags file. The SUID tags file can't be specified alone.
Wildcard IDs (column one) may be (for example)
* 123-678 [all IDs in range, inclusive, are set to same tag/color]
* 444.... [all IDs from 4440000 to 4449999]
* 456* [all IDs starting with 456]
* 54321 [defines that one ID]
Column three contains a color value from 0-99 (decimal).
In the TGID file (only), the column value also contains a talkgroup
priority, encoded as follows:
- the low-order two decimal digits (tens and units digits) are the
color code
- the remaining upper-order decimal digits (hundreds digit and above) are
the priority value for talkgroup pre-emption purposes.
Setup SQL Log Database (Optional)
=================================
This addition provides a permanent server-side log of control channel
activity via logging to an SQL database. See the next section for details
on installing and using the log viewer.
1. Make sure that sqlite3 is installed in python
WARNING: OP25 MUST NOT BE RUNNING DURING THIS STEP
2. Initialize DB (any existing DB data will be destroyed)
op25/.../apps$ python sql_dbi.py reset_db
WARNING: OP25 MUST NOT BE RUNNING DURING THIS STEP
3. Import talkgroups tags file
op25/.../apps$ python sql_dbi.py import_tgid tags.tsv <sysid>
also, import the radio ID tags file (optional)
op25/.../apps$ python sql_dbi.py import_unit radio-tags.tsv <sysid>
import the System ID tags file (see below)
op25/.../apps$ python sql_dbi.py import_sysid sysid-tags.tsv 0
The sysid tags must be a TSV file containing two columns
column 1 is the P25 trunked sysid (int, decimal)
colunn 2 is the System Name (text)
(Note: there is no header row line in this TSV file).
NOTE: in the various import commands above, the sysid (decimal) must follow
as the next argument after the TSV file name. For the sysid tags file, the
sysid should be set to zero.
4. Run op25 as usual. Logfile data should be inserted into DB in real time
and you should be able to view activity via the OP25 http console (once
the flask/datatables app has been set up; see next section).
Setup Flask Datatables App
==========================
0. The DB must first be established (see previous section)
1. Install the necessary libs. If you are running the install in Ubuntu
16.04 there are two lines in the script that must be un-commented prior
to running; then, in any case do:
op25/.../apps$ sh install-sql.sh
Note: you may need to 'sudo apt install git' prior to running this script.
2. Update your .bashrc file as instructed, then re-login to pick up the
update to PATH. Verify that the updated PATH is correct. You can run
the command "echo $PATH" to display its current value. Here is an example
response: /home/op25/.local/bin:/usr/local/sbin:/usr/local/bin.....
You should confirm that the file "flask" exists and is executable (see
warning below).
$ ls -l ~/.local/bin/flask
-rwxr-xr-x 1 op25 op25 212 Apr 29 21:43 /home/op25/.local/bin/flask
3. First change to the "..../apps/oplog" directory, then run the following
commands to start the flask http process (listens on port 5000)
op25/.../apps/oplog$ export FLASK_APP=op25
op25/.../apps/oplog$ FLASK_DEBUG=1 flask run
WARNING: if you receive the following messages when attempting the "flask run"
command
-------------------------------------------------------------
Command 'flask' not found, but can be installed with:
sudo apt install python3-flask
-------------------------------------------------------------
most likely this indicates the PATH is not properly set up (see step 2).
In this case we do NOT recommend that you attempt to install the apt version
of flask. The install-sql.sh script (step 1, above) should have installed a
version of flask in a directory such as:
$ ls -l ~/.local/bin/flask
-rwxr-xr-x 1 op25 op25 212 Apr 29 21:43 /home/op25/.local/bin/flask
If install of the apt version of flask is attempted, it may result in an
obsolete and/or incompatible flask version being installed.

View File

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

View File

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

View File

@ -0,0 +1,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"
}

View File

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

View File

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

View File

@ -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"

498
op25/gr-op25_repeater/apps/multi_rx.py Executable file → Normal file
View File

@ -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,12 +209,35 @@ 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
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 = []
if 'blacklist' in config.keys():
@ -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()
if self.options.monitor_stdin:
print("Running. press ENTER to quit")
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())
if self.options.monitor_stdin and select.select([sys.stdin,],[],[],0.0)[0]:
c = sys.stdin.read(1)
self.keep_running = False
break
msg = self.tb.output_q.delete_head()
if self.tb.process_msg(msg):
self.keep_running = False
break
print('Quitting - now stopping top block')
self.tb.stop()
if __name__ == "__main__":
rx = rx_main()
try:
rx.run()
except KeyboardInterrupt:
rx.keep_running = False
print('Program ending')
time.sleep(1)

View File

@ -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('')

View File

@ -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:

View File

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

View File

@ -0,0 +1,12 @@
{
"552": {
"1": {
"1": {
"alias": "new1"
},
"2": {
"alias": "new2"
}
}
}
}

View File

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

View File

@ -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"

1 Sysname Control Channel List Offset NAC Modulation TGID Tags File Whitelist Blacklist Center Frequency
3 Cortland 454.05 0 0x4e1 CQPSK
4 Onondaga 460.5 0 0x2a0 CQPSK onondaga.tsv
5 Cayuga 460.4125 0 0x2a8 CQPSK onondaga.tsv
6 Ontario 769.55625,769.85625,770.10625 769.28125,769.55625,769.85625,770.10625 0 0x47f CQPSK ontario.tsv
7 460.375 460.375 0 0x2a4 CQPSK onondaga.tsv
8 NYSEG 152.0225 0 0x260 C4FM nyseg.tsv

File diff suppressed because it is too large Load Diff

97
op25/gr-op25_repeater/apps/tsvfile.py Normal file → Executable file
View File

@ -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
fname = configs[nac]['tgid_tags_file']
if ',' in fname:
l = fname.split(',')
tgid_tags_file = l[0]
unit_id_tags_file = l[1]
else:
prio = 3
result_config[nac]['tgid_map'][tgid] = (txt, prio)
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

View File

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

View File

@ -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) {}

View File

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

View File

@ -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) {}

View File

@ -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)
{
}

View File

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

View File

@ -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,9 +120,6 @@ 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;
@ -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,10 +347,18 @@ 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);
} /* end of if (!d_muted) */
if (d_muted)
out[o++] = 0.0;
else
out[o++] = interp_samp;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<int16_t> &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<int16_t> &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<uint8_t>& 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<uint8_t> 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);
}
}

View File

@ -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<int16_t> &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<int16_t> &output_queue, bool do_audio_output, int msgq_id);
~p25p1_fdma();
// Where all the action really happens

View File

@ -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<int16_t> &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<int16_t> &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<len/8; i++) {
if (crc_ok) { // TODO: rewrite crc12 so we don't have to do so much bit manipulation
// fprintf(stderr, "crc12 ok\n");
for (int i=0; i<olen; i++) {
byte_buf[i] = (bits[i*8 + 0] << 7) + (bits[i*8 + 1] << 6) + (bits[i*8 + 2] << 5) + (bits[i*8 + 3] << 4) + (bits[i*8 + 4] << 3) + (bits[i*8 + 5] << 2) + (bits[i*8 + 6] << 1) + (bits[i*8 + 7] << 0);
}
rc = process_mac_pdu(byte_buf, len/8);
rc = process_mac_pdu(byte_buf, olen);
} else {
// fprintf(stderr, "crc12 failed\n");
}
return rc;
}
@ -542,13 +565,15 @@ int p25p2_tdma::handle_packet(const uint8_t dibits[])
}
return -1;
} else if (burst_type == 3) { // scrambled sacch
rc = handle_acch_frame(xored_burst, 0);
rc = handle_acch_frame(xored_burst, 0, false);
} else if (burst_type == 9) { // scrambled facch
rc = handle_acch_frame(xored_burst, 1);
rc = handle_acch_frame(xored_burst, 1, false);
} else if (burst_type == 12) { // unscrambled sacch
rc = handle_acch_frame(burstp, 0);
rc = handle_acch_frame(burstp, 0, false);
} else if (burst_type == 13) { // TDMA CC OECI
rc = handle_acch_frame(burstp, 0, true);
} else if (burst_type == 15) { // unscrambled facch
rc = handle_acch_frame(burstp, 1);
rc = handle_acch_frame(burstp, 1, false);
} else {
// unsupported type duid
return -1;
@ -606,9 +631,10 @@ void p25p2_tdma::handle_4V2V_ess(const uint8_t dibits[])
void p25p2_tdma::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);
}

View File

@ -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<int16_t> &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<int16_t> &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) ;

View File

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

View File

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