core rc2-final
This commit is contained in:
parent
180a4dec97
commit
51042858e8
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
]
|
||||
]
|
|
@ -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"
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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('')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"552": {
|
||||
"1": {
|
||||
"1": {
|
||||
"alias": "new1"
|
||||
},
|
||||
"2": {
|
||||
"alias": "new2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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"
|
||||
|
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) ;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) ;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue