Compare commits
158 Commits
Author | SHA1 | Date |
---|---|---|
Max | 6f1b25d2d1 | |
Max | 1d56794504 | |
Max | a47bb712d2 | |
Max | 2feb62e8cf | |
Max | 310ad30a21 | |
Max | eef44be741 | |
Max | b05a4bcc4a | |
Max | c10dde6065 | |
Max | fbe4b881bb | |
Max | e63c106ce2 | |
Max | 8a3bbae9bc | |
Max | dc6856d26c | |
Max | ac9cfa58b7 | |
Max | cbb6462106 | |
Max | 7b31ebd34e | |
Max | 7bc0fb2c1b | |
Max | 874b72042c | |
Max | ebc980c205 | |
Max | 7dc6d61f75 | |
Max | d90ecd7c89 | |
Max | de370340cf | |
Max | 245ff05489 | |
Max | 1cd344a838 | |
Max | 16e8b53fbf | |
Max | 4028ec6a4e | |
Max | e7fc1c6c45 | |
Max | f010f3f5dc | |
Matt Ames | dc90a85c02 | |
Max | dd830a1463 | |
Max | 1e7b8b43a2 | |
Max | f51999c49f | |
Max | 72ccc09ee0 | |
Max | 86a609d743 | |
Max | b2ec8b376e | |
Max | bc0ba19e9b | |
Max | c6b3799b96 | |
Max | 3af1dbe0db | |
Max | 387cf57662 | |
Max | ff96eb549b | |
Max | 6966b5fbe3 | |
Max | 48b0de1986 | |
Max | e965205121 | |
Max | 713e632e6c | |
Max | f8a9b6128a | |
Max | 23cd0111b7 | |
Max | cca08a1694 | |
Max | 702370af8c | |
Max | d1ee843822 | |
Max | 3b687098d0 | |
Max | ecc0c6f6b1 | |
Max | 84042249fc | |
Max | 61144fa477 | |
Max | b9752d54bf | |
Max | 53490d0c8c | |
Max | f31302bff7 | |
Max | 5e87fdc889 | |
Max | e540ade02b | |
Max | eb9a8be83d | |
Max | a148cf1fe5 | |
Max | 3492bd70b1 | |
Max | fbae3bcfde | |
Max | 2ee08e9c24 | |
Max | 5818d58dba | |
Max | 8a948a3bee | |
Max | 8b77120005 | |
Max | d9794b5123 | |
Max | e40f2c53da | |
Max | ddd70bdd07 | |
Max | 13a3bccb9e | |
Max | 7f37fada79 | |
Max | c06a318222 | |
Max | f14bf6deb8 | |
Max | 676409131d | |
Max | 5ef6bd7862 | |
Max | 0ecddbb1bf | |
Matt Ames | c2cebde6d5 | |
Max | 56daa99b94 | |
Max | f93460a0bd | |
Max | e080f32fc6 | |
Max | 65b5c318b0 | |
Max | a48ba912ea | |
Max | a4c7d3852f | |
Max | 233f810d3b | |
Max | 855b590f2d | |
Max | 3e2aece2e6 | |
Max | a8d8dbc882 | |
Max | 3a3509fbf0 | |
Max | ecc8db384f | |
Max | 5c53f3f643 | |
Max | 70143fa2f7 | |
Max | ced242a072 | |
Max | b1e7d81f5e | |
Max | a169f37c3a | |
Max | d964703e83 | |
Max | b57db0d599 | |
Max | 6fb7a6b1ca | |
Max | 7373d10e1c | |
Max | 7290f13486 | |
Max | 47052abc1a | |
Max | db9e7db946 | |
Max | f212d1b6ff | |
Max | 806d7c44ca | |
Max | 51042858e8 | |
Max | 180a4dec97 | |
Max | aec5a8012c | |
Max | ccc0001872 | |
Max | dced944480 | |
Max | 91dead3b5b | |
Max | 3aefbf332b | |
Max | c3010f3265 | |
Max | 18c1cc0a54 | |
Max | ed3334ca6c | |
Max | 2dcbbbcdc2 | |
Max | e971078c55 | |
Max | 59a631b700 | |
Max | ccf9a9f20b | |
Max | 76ca14e5e8 | |
Max | e540ac9c54 | |
Max | 5c5bd11a0c | |
Max | 27ea95ee38 | |
Matt Ames | d8c01956ba | |
Matt Ames | bdd6707dc5 | |
Max | 3612767021 | |
Matt Ames | 679336d55d | |
Max | 19b720715c | |
Max | 856ed97419 | |
Max | a6bd25163a | |
Max | 28ef6960cc | |
Max | 1180f9d8b6 | |
Max | a11ef18339 | |
Max | 478d2cba70 | |
Max | 3a0901e646 | |
Max | 62e2ef92f0 | |
Max | a5df0ec044 | |
Max | 94dc7536e6 | |
Max | 9d16effff3 | |
Max | 0421a55968 | |
Max | 602cb0659f | |
Max | 1da0714da5 | |
Max | 5e5566f0f3 | |
Max | ffb2035c67 | |
Max | 82364672ed | |
Max | 921a1d8ea0 | |
Max | 5e01df4883 | |
Max | 3b69f5e335 | |
Max | 0686f2de14 | |
Max | 733e891d22 | |
Max | 57a682d5f0 | |
Max | c26b51e0c1 | |
Max | 18042e1368 | |
Max | 8b00b8459a | |
Max | 92ba1df2c0 | |
Max | 6f02fc63cc | |
Max | b91039ecd0 | |
Max | 3de2d3e440 | |
Max | e25e7be488 | |
Max | b6b0eeb7b8 | |
Max | a77c18d745 |
|
@ -0,0 +1,4 @@
|
|||
# Ignore build artifacts
|
||||
build/
|
||||
# Ignore .pyc compiled python bytecode files
|
||||
*.pyc
|
|
@ -4,6 +4,24 @@ project(gr-op25 CXX C)
|
|||
set(CMAKE_BUILD_TYPE Debug)
|
||||
set(CMAKE_CXX_FLAGS "-std=c++11")
|
||||
|
||||
execute_process(COMMAND python3 -c "
|
||||
import os
|
||||
import sys
|
||||
from distutils import sysconfig
|
||||
pfx = '/usr/local'
|
||||
m1 = os.path.join('lib', 'python' + sys.version[:3], 'dist-packages')
|
||||
m2 = sysconfig.get_python_lib(plat_specific=True, prefix='')
|
||||
f1 = os.path.join(pfx, m1)
|
||||
f2 = os.path.join(pfx, m2)
|
||||
ok2 = f2 in sys.path
|
||||
if ok2:
|
||||
print(m2)
|
||||
else:
|
||||
print(m1)
|
||||
" OUTPUT_VARIABLE OP25_PYTHON_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
MESSAGE(STATUS "OP25_PYTHON_DIR has been set to \"${OP25_PYTHON_DIR}\".")
|
||||
|
||||
add_subdirectory(op25/gr-op25)
|
||||
add_subdirectory(op25/gr-op25_repeater)
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
As of this writing (Sept. 2022) OP25 builds for python3 and GNU Radio 3.8.
|
||||
|
||||
The full list of supported versions is as follows:
|
||||
|
||||
PYTHON 2 AND GNU RADIO 3.7
|
||||
==========================
|
||||
It should still be possible to use the file gr3.8.patch (in reverse) to
|
||||
downgrade the source tree to build against Python 2 and GNU Radio 3.7,
|
||||
although this has not been tested.
|
||||
$ cat gr3.8.patch | patch -p1 -R
|
||||
Once this has been done, proceed by running the install.sh script.
|
||||
|
||||
PYTHON 3 AND GNU RADIO 3.8
|
||||
==========================
|
||||
It is no longer necessary to apply the gr3.8 patch to the op25 source tree,
|
||||
as Python3/GNU Radio 3.8 is now the default. You can proceed directly to
|
||||
running the install.sh script.
|
||||
|
||||
PYTHON 3 AND GNU RADIO 3.9 / 3.10
|
||||
=================================
|
||||
It is no longer necessary to apply the gr3.8 patch to the op25 source tree,
|
||||
See the file README-gr3.9 for procedures for both GNU Radio 3.9 and 3.10.
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
Updated Sept. 2022
|
||||
|
||||
By default, OP25 now builds for Python3 and GNU Radio3.8.
|
||||
Accordingly, it is no longer necessary to apply the op25 patch
|
||||
for gr3.8.
|
||||
|
||||
It should be possible to use the gr3.8.patch in reverse to downgrade
|
||||
the source tree to build for Python 2 and GNU Radio 3.7. See the
|
||||
file README in this directory for details.
|
|
@ -0,0 +1,32 @@
|
|||
|
||||
Running OP25 in Gnuradio 3.9 and 3.10 Date: May 31, 2022
|
||||
==============================================================
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
0. First remove any existing OP25 installation
|
||||
cd ...path-to-existing/op25/build
|
||||
sudo make uninstall
|
||||
|
||||
1. Step 1 is removed.
|
||||
|
||||
2. Run the command
|
||||
./install-gr3.9.sh
|
||||
(Note, do not use the standard install.sh script for gr3.9 and/or gr3.10).
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
||||
1. The location for WIN_HANN, WIN_HAMMING, and so forth has moved to
|
||||
fft.window (formerly filter.firdes). The python has not yet been
|
||||
updated to reflect.
|
||||
|
||||
2. A strange hang condition has been observed in rx.py - the bug is not
|
||||
fully understood. The strace output shows many futex calls ending
|
||||
with a "timeout" - not clear if some missing event might be causing this
|
||||
stall condition... User feedback is requested...
|
||||
|
||||
3. Currently the cmake process is not centralized; 'make uninstall' must
|
||||
therefore be run once from each of the 'build' and 'build_repeater'
|
||||
directories under 'src/'.
|
|
@ -0,0 +1,231 @@
|
|||
#! /usr/bin/python3
|
||||
|
||||
# Copyright 2022, Max H. Parke KA1RBI
|
||||
#
|
||||
# This file is part of GNU Radio and part of OP25
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
|
||||
import sys
|
||||
import os
|
||||
import glob
|
||||
import shutil
|
||||
from gnuradio.modtool.core.newmod import ModToolNewModule
|
||||
from gnuradio.modtool.core.add import ModToolAdd
|
||||
from gnuradio.modtool.core.bind import ModToolGenBindings
|
||||
|
||||
msg = """
|
||||
This is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
"""
|
||||
|
||||
print('\n%s Copyright 2022, Max H. Parke KA1RBI\nhttps://osmocom.org/projects/op25\n%s' % (sys.argv[0], msg))
|
||||
|
||||
TLD = 'op25'
|
||||
|
||||
MODS={
|
||||
'op25': 'decoder_bf decoder_ff fsk4_demod_ff fsk4_slicer_fb pcap_source_b message msg_queue msg_handler'.split(),
|
||||
'op25_repeater': 'ambe_encoder_sb dmr_bs_tx_bb dstar_tx_sb frame_assembler fsk4_slicer_fb gardner_costas_cc nxdn_tx_sb p25_frame_assembler vocoder ysf_tx_sb'.split()
|
||||
}
|
||||
|
||||
SKIP_CC = 'd2460.cc qa_op25.cc test_op25.cc qa_op25_repeater.cc test_op25_repeater.cc message.cc msg_queue.cc msg_handler.cc'.split()
|
||||
|
||||
SRC_DIR = sys.argv[1]
|
||||
DEST_DIR = sys.argv[2]
|
||||
|
||||
if '..' in SRC_DIR or not SRC_DIR.startswith('/'):
|
||||
sys.stderr.write('error, %s must be an absolute path\n' % SRC_DIR)
|
||||
sys.exit(1)
|
||||
|
||||
if not os.access(SRC_DIR, os.R_OK):
|
||||
sys.stderr.write('error, unable to access %s\n' % SRC_DIR)
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isdir(SRC_DIR + '/op25/gr-op25_repeater'):
|
||||
sys.stderr.write('error, op25 package not found in %s\n' % SRC_DIR)
|
||||
sys.exit(3)
|
||||
|
||||
if os.access(DEST_DIR, os.F_OK) or os.path.isdir(DEST_DIR):
|
||||
sys.stderr.write('error, destination path %s must not exist\n' % DEST_DIR)
|
||||
sys.exit(4)
|
||||
|
||||
os.mkdir(DEST_DIR)
|
||||
op25_dir = DEST_DIR + '/op25'
|
||||
os.mkdir(op25_dir)
|
||||
os.chdir(op25_dir)
|
||||
|
||||
SCRIPTS = SRC_DIR + '/scripts'
|
||||
|
||||
TXT = """add_library(op25-message SHARED message.cc msg_queue.cc msg_handler.cc)
|
||||
install(TARGETS op25-message EXPORT op25-message-export DESTINATION lib)
|
||||
install(EXPORT op25-message-export DESTINATION ${GR_CMAKE_DIR})
|
||||
target_include_directories(op25-message
|
||||
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
|
||||
PUBLIC $<INSTALL_INTERFACE:include>
|
||||
)
|
||||
"""
|
||||
|
||||
def edit_cmake(filename, mod, srcfiles):
|
||||
lines = open(filename).read().rstrip().split('\n')
|
||||
srcdefs = []
|
||||
state = 0
|
||||
end_mark = 0
|
||||
add_library = 0
|
||||
tll = 0 # target_link_library
|
||||
srcfiles = [s.split('/')[-1] for s in srcfiles if s.endswith('.cc') or s.endswith('.c') or s.endswith('.cpp')]
|
||||
lines = [l for l in lines if l.strip() not in SKIP_CC]
|
||||
for i in range(len(lines)):
|
||||
if 'add_library' in lines[i] and 'gnuradio-op25' in lines[i]:
|
||||
add_library = i
|
||||
if lines[i].startswith('list(APPEND op25_') and ('_sources' in lines[i] or '_python_files' in lines[i]):
|
||||
state = 1
|
||||
continue
|
||||
elif ')' in lines[i] and state:
|
||||
state = 0
|
||||
end_mark = i
|
||||
continue
|
||||
elif lines[i].startswith('target_link_libraries(gnuradio-op25'):
|
||||
assert lines[i].endswith(')')
|
||||
tll = i
|
||||
continue
|
||||
if state:
|
||||
srcdefs.append(lines[i].strip())
|
||||
srcfiles = [" %s" % s for s in srcfiles if s not in srcdefs and s not in SKIP_CC]
|
||||
tlls = {
|
||||
'op25': 'target_link_libraries(gnuradio-op25 gnuradio::gnuradio-runtime Boost::system Boost::program_options Boost::filesystem Boost::thread itpp pcap op25-message)',
|
||||
'op25_repeater': 'target_link_libraries(gnuradio-op25_repeater PUBLIC gnuradio::gnuradio-runtime gnuradio::gnuradio-filter op25-message PRIVATE imbe_vocoder)'
|
||||
}
|
||||
assert tll # fail if target_link_libraries line not found
|
||||
lines[tll] = tlls[mod]
|
||||
if mod == 'op25_repeater':
|
||||
lines = lines[:tll] + ['\n' + 'add_subdirectory(imbe_vocoder)\n'] + lines[tll:]
|
||||
elif mod == 'op25':
|
||||
assert add_library > 0
|
||||
lines = lines[:add_library] + [s for s in TXT.split('\n')] + lines[add_library:]
|
||||
|
||||
new_lines = lines[:end_mark] + srcfiles + lines[end_mark:]
|
||||
s = '\n'.join(new_lines)
|
||||
s += '\n'
|
||||
with open(filename, 'w') as fp:
|
||||
fp.write(s)
|
||||
|
||||
def get_args_from_h(mod):
|
||||
lines = open(mod).read().rstrip().split('\n')
|
||||
|
||||
lines = [line for line in lines if 'make' in line]
|
||||
|
||||
answer = []
|
||||
for s in lines:
|
||||
s = s.rstrip()
|
||||
if s[-1] != ';':
|
||||
continue
|
||||
s = s[:-1]
|
||||
s = s.rstrip()
|
||||
if s[-1] != ')':
|
||||
continue
|
||||
s = s[:-1]
|
||||
lp = s.find('(')
|
||||
if lp > 0:
|
||||
s =s[lp+1:]
|
||||
else:
|
||||
continue
|
||||
for arg in s.split(','):
|
||||
eq = arg.find('=')
|
||||
if eq > 0:
|
||||
arg = arg[:eq]
|
||||
answer.append(arg)
|
||||
return ','.join(answer)
|
||||
return ''
|
||||
|
||||
for mod in sorted(MODS.keys()):
|
||||
m = ModToolNewModule(module_name=mod, srcdir=None)
|
||||
m.run()
|
||||
print('gr_modtool newmod %s getcwd now %s' % (mod, os.getcwd()))
|
||||
pfx = '%s/op25/gr-%s' % (SRC_DIR, mod)
|
||||
lib = '%s/lib' % pfx
|
||||
s_py = '%s/python/op25/bindings' % pfx
|
||||
incl = '%s/include/%s' % (pfx, mod)
|
||||
d_pfx = '%s/op25/gr-%s' % (DEST_DIR, mod)
|
||||
d_lib = '%s/lib' % d_pfx
|
||||
d_py = '%s/python/op25/bindings' % d_pfx
|
||||
d_incl_alt1 = '%s/include/%s' % (d_pfx, mod)
|
||||
d_incl_alt2 = '%s/include/gnuradio/%s' % (d_pfx, mod)
|
||||
if os.path.isdir(d_incl_alt1):
|
||||
d_incl = d_incl_alt1
|
||||
elif os.path.isdir(d_incl_alt2):
|
||||
d_incl = d_incl_alt2
|
||||
sl = 'gnuradio/%s' % mod
|
||||
os.symlink(sl, '%s/include/%s' % (d_pfx, mod))
|
||||
if mod == 'op25_repeater':
|
||||
p_pfx = '%s/op25/gr-%s' % (DEST_DIR, 'op25')
|
||||
p_incl = '%s/include/%s' % (p_pfx, 'op25')
|
||||
d = '/'.join(d_incl.split('/')[:-1])
|
||||
os.symlink(p_incl, '%s/include/%s' % (d_pfx, 'op25'))
|
||||
|
||||
else:
|
||||
sys.stderr.write('neither %s nor %s found, aborting\n' % (d_incl_alt1, d_incl_alt2))
|
||||
sys.exit(1)
|
||||
|
||||
for block in MODS[mod]:
|
||||
include = '%s/%s.h' % (incl, block)
|
||||
args = get_args_from_h(include)
|
||||
t = 'sync' if block == 'fsk4_slicer_fb' or block == 'pcap_source_b' else 'general'
|
||||
if block == 'message' or block == 'msg_queue' or block == 'msg_handler':
|
||||
t = 'noblock'
|
||||
print ('add %s %s type %s directory %s args %s' % (mod, block, t, os.getcwd(), args))
|
||||
m = ModToolAdd(blockname=block,block_type=t,lang='cpp',copyright='Steve Glass, OP25 Group', argument_list=args)
|
||||
m.run()
|
||||
|
||||
srcfiles = []
|
||||
srcfiles += glob.glob('%s/lib/*.cc' % pfx)
|
||||
srcfiles += glob.glob('%s/lib/*.cpp' % pfx)
|
||||
srcfiles += glob.glob('%s/lib/*.c' % pfx)
|
||||
srcfiles += glob.glob('%s/lib/*.h' % pfx)
|
||||
hfiles = glob.glob('%s/*.h' % incl)
|
||||
|
||||
assert os.path.isdir(d_lib)
|
||||
assert os.path.isdir(d_incl)
|
||||
|
||||
for f in srcfiles:
|
||||
shutil.copy(f, d_lib)
|
||||
|
||||
for f in hfiles:
|
||||
shutil.copy(f, d_incl)
|
||||
|
||||
os.system('/bin/bash %s/%s %s' % (SCRIPTS, 'do_sedm.sh', d_incl))
|
||||
|
||||
if mod == 'op25_repeater':
|
||||
for d in 'imbe_vocoder ezpwd'.split():
|
||||
os.mkdir('%s/%s' % (d_lib, d))
|
||||
imbefiles = []
|
||||
imbefiles += glob.glob('%s/%s/*' % (lib, d))
|
||||
dest = '%s/%s' % (d_lib, d)
|
||||
for f in imbefiles:
|
||||
shutil.copy(f, dest)
|
||||
|
||||
edit_cmake('%s/CMakeLists.txt' % d_lib, mod, srcfiles)
|
||||
|
||||
os.system('/bin/bash %s/do_sed.sh' % (SCRIPTS))
|
||||
f = '%s/CMakeLists.txt' % (d_pfx)
|
||||
if mod == 'op25':
|
||||
exe = '%s/do_sedb.sh %s' % (SCRIPTS, f)
|
||||
elif mod == 'op25_repeater':
|
||||
exe = '%s/do_sedc.sh %s' % (SCRIPTS, f)
|
||||
os.system('/bin/bash %s %s' % (exe, f))
|
||||
os.system('/bin/bash %s/do_sedp.sh %s' % (SCRIPTS, f))
|
||||
os.system('/bin/bash %s/do_sedp2.sh %s' % (SCRIPTS, d_pfx))
|
||||
|
||||
for block in MODS[mod]:
|
||||
print ('bind %s %s' % (mod, block))
|
||||
m = ModToolGenBindings(block, addl_includes='', define_symbols='', update_hash_only=False)
|
||||
m.run()
|
||||
|
||||
if mod == 'op25':
|
||||
py_cc_srcfiles = 'message_python.cc msg_handler_python.cc msg_queue_python.cc'.split()
|
||||
for f in py_cc_srcfiles:
|
||||
shutil.copy('%s/%s' % (s_py, f), d_py)
|
||||
|
||||
os.chdir(op25_dir)
|
|
@ -1,210 +0,0 @@
|
|||
# Copyright 2010-2011 Free Software Foundation, Inc.
|
||||
#
|
||||
# This file is part of GNU Radio
|
||||
#
|
||||
# GNU Radio is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# GNU Radio is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with GNU Radio; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
if(DEFINED __INCLUDED_GR_MISC_UTILS_CMAKE)
|
||||
return()
|
||||
endif()
|
||||
set(__INCLUDED_GR_MISC_UTILS_CMAKE TRUE)
|
||||
|
||||
########################################################################
|
||||
# Set global variable macro.
|
||||
# Used for subdirectories to export settings.
|
||||
# Example: include and library paths.
|
||||
########################################################################
|
||||
function(GR_SET_GLOBAL var)
|
||||
set(${var} ${ARGN} CACHE INTERNAL "" FORCE)
|
||||
endfunction(GR_SET_GLOBAL)
|
||||
|
||||
########################################################################
|
||||
# Set the pre-processor definition if the condition is true.
|
||||
# - def the pre-processor definition to set and condition name
|
||||
########################################################################
|
||||
function(GR_ADD_COND_DEF def)
|
||||
if(${def})
|
||||
add_definitions(-D${def})
|
||||
endif(${def})
|
||||
endfunction(GR_ADD_COND_DEF)
|
||||
|
||||
########################################################################
|
||||
# Check for a header and conditionally set a compile define.
|
||||
# - hdr the relative path to the header file
|
||||
# - def the pre-processor definition to set
|
||||
########################################################################
|
||||
function(GR_CHECK_HDR_N_DEF hdr def)
|
||||
include(CheckIncludeFileCXX)
|
||||
CHECK_INCLUDE_FILE_CXX(${hdr} ${def})
|
||||
GR_ADD_COND_DEF(${def})
|
||||
endfunction(GR_CHECK_HDR_N_DEF)
|
||||
|
||||
########################################################################
|
||||
# Include subdirectory macro.
|
||||
# Sets the CMake directory variables,
|
||||
# includes the subdirectory CMakeLists.txt,
|
||||
# resets the CMake directory variables.
|
||||
#
|
||||
# This macro includes subdirectories rather than adding them
|
||||
# so that the subdirectory can affect variables in the level above.
|
||||
# This provides a work-around for the lack of convenience libraries.
|
||||
# This way a subdirectory can append to the list of library sources.
|
||||
########################################################################
|
||||
macro(GR_INCLUDE_SUBDIRECTORY subdir)
|
||||
#insert the current directories on the front of the list
|
||||
list(INSERT _cmake_source_dirs 0 ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
list(INSERT _cmake_binary_dirs 0 ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
#set the current directories to the names of the subdirs
|
||||
set(CMAKE_CURRENT_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${subdir})
|
||||
set(CMAKE_CURRENT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${subdir})
|
||||
|
||||
#include the subdirectory CMakeLists to run it
|
||||
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt)
|
||||
|
||||
#reset the value of the current directories
|
||||
list(GET _cmake_source_dirs 0 CMAKE_CURRENT_SOURCE_DIR)
|
||||
list(GET _cmake_binary_dirs 0 CMAKE_CURRENT_BINARY_DIR)
|
||||
|
||||
#pop the subdir names of the front of the list
|
||||
list(REMOVE_AT _cmake_source_dirs 0)
|
||||
list(REMOVE_AT _cmake_binary_dirs 0)
|
||||
endmacro(GR_INCLUDE_SUBDIRECTORY)
|
||||
|
||||
########################################################################
|
||||
# Check if a compiler flag works and conditionally set a compile define.
|
||||
# - flag the compiler flag to check for
|
||||
# - have the variable to set with result
|
||||
########################################################################
|
||||
macro(GR_ADD_CXX_COMPILER_FLAG_IF_AVAILABLE flag have)
|
||||
include(CheckCXXCompilerFlag)
|
||||
CHECK_CXX_COMPILER_FLAG(${flag} ${have})
|
||||
if(${have})
|
||||
add_definitions(${flag})
|
||||
endif(${have})
|
||||
endmacro(GR_ADD_CXX_COMPILER_FLAG_IF_AVAILABLE)
|
||||
|
||||
########################################################################
|
||||
# Generates the .la libtool file
|
||||
# This appears to generate libtool files that cannot be used by auto*.
|
||||
# Usage GR_LIBTOOL(TARGET [target] DESTINATION [dest])
|
||||
# Notice: there is not COMPONENT option, these will not get distributed.
|
||||
########################################################################
|
||||
function(GR_LIBTOOL)
|
||||
if(NOT DEFINED GENERATE_LIBTOOL)
|
||||
set(GENERATE_LIBTOOL OFF) #disabled by default
|
||||
endif()
|
||||
|
||||
if(GENERATE_LIBTOOL)
|
||||
include(CMakeParseArgumentsCopy)
|
||||
CMAKE_PARSE_ARGUMENTS(GR_LIBTOOL "" "TARGET;DESTINATION" "" ${ARGN})
|
||||
|
||||
find_program(LIBTOOL libtool)
|
||||
if(LIBTOOL)
|
||||
include(CMakeMacroLibtoolFile)
|
||||
CREATE_LIBTOOL_FILE(${GR_LIBTOOL_TARGET} /${GR_LIBTOOL_DESTINATION})
|
||||
endif(LIBTOOL)
|
||||
endif(GENERATE_LIBTOOL)
|
||||
|
||||
endfunction(GR_LIBTOOL)
|
||||
|
||||
########################################################################
|
||||
# Do standard things to the library target
|
||||
# - set target properties
|
||||
# - make install rules
|
||||
# Also handle gnuradio custom naming conventions w/ extras mode.
|
||||
########################################################################
|
||||
function(GR_LIBRARY_FOO target)
|
||||
#parse the arguments for component names
|
||||
include(CMakeParseArgumentsCopy)
|
||||
CMAKE_PARSE_ARGUMENTS(GR_LIBRARY "" "RUNTIME_COMPONENT;DEVEL_COMPONENT" "" ${ARGN})
|
||||
|
||||
#set additional target properties
|
||||
set_target_properties(${target} PROPERTIES SOVERSION ${LIBVER})
|
||||
|
||||
#install the generated files like so...
|
||||
install(TARGETS ${target}
|
||||
LIBRARY DESTINATION ${GR_LIBRARY_DIR} COMPONENT ${GR_LIBRARY_RUNTIME_COMPONENT} # .so/.dylib file
|
||||
ARCHIVE DESTINATION ${GR_LIBRARY_DIR} COMPONENT ${GR_LIBRARY_DEVEL_COMPONENT} # .lib file
|
||||
RUNTIME DESTINATION ${GR_RUNTIME_DIR} COMPONENT ${GR_LIBRARY_RUNTIME_COMPONENT} # .dll file
|
||||
)
|
||||
|
||||
#extras mode enabled automatically on linux
|
||||
if(NOT DEFINED LIBRARY_EXTRAS)
|
||||
set(LIBRARY_EXTRAS ${LINUX})
|
||||
endif()
|
||||
|
||||
#special extras mode to enable alternative naming conventions
|
||||
if(LIBRARY_EXTRAS)
|
||||
|
||||
#create .la file before changing props
|
||||
GR_LIBTOOL(TARGET ${target} DESTINATION ${GR_LIBRARY_DIR})
|
||||
|
||||
#give the library a special name with ultra-zero soversion
|
||||
set_target_properties(${target} PROPERTIES LIBRARY_OUTPUT_NAME ${target}-${LIBVER} SOVERSION "0.0.0")
|
||||
set(target_name lib${target}-${LIBVER}.so.0.0.0)
|
||||
|
||||
#custom command to generate symlinks
|
||||
add_custom_command(
|
||||
TARGET ${target}
|
||||
POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink ${target_name} ${CMAKE_CURRENT_BINARY_DIR}/lib${target}.so
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink ${target_name} ${CMAKE_CURRENT_BINARY_DIR}/lib${target}-${LIBVER}.so.0
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${target_name} #so the symlinks point to something valid so cmake 2.6 will install
|
||||
)
|
||||
|
||||
#and install the extra symlinks
|
||||
install(
|
||||
FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/lib${target}.so
|
||||
${CMAKE_CURRENT_BINARY_DIR}/lib${target}-${LIBVER}.so.0
|
||||
DESTINATION ${GR_LIBRARY_DIR} COMPONENT ${GR_LIBRARY_RUNTIME_COMPONENT}
|
||||
)
|
||||
|
||||
endif(LIBRARY_EXTRAS)
|
||||
endfunction(GR_LIBRARY_FOO)
|
||||
|
||||
########################################################################
|
||||
# Create a dummy custom command that depends on other targets.
|
||||
# Usage:
|
||||
# GR_GEN_TARGET_DEPS(unique_name target_deps <target1> <target2> ...)
|
||||
# ADD_CUSTOM_COMMAND(<the usual args> ${target_deps})
|
||||
#
|
||||
# Custom command cant depend on targets, but can depend on executables,
|
||||
# and executables can depend on targets. So this is the process:
|
||||
########################################################################
|
||||
function(GR_GEN_TARGET_DEPS name var)
|
||||
file(
|
||||
WRITE ${CMAKE_CURRENT_BINARY_DIR}/${name}.cpp.in
|
||||
"int main(void){return 0;}\n"
|
||||
)
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${name}.cpp.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${name}.cpp
|
||||
)
|
||||
add_executable(${name} ${CMAKE_CURRENT_BINARY_DIR}/${name}.cpp)
|
||||
if(ARGN)
|
||||
add_dependencies(${name} ${ARGN})
|
||||
endif(ARGN)
|
||||
|
||||
if(CMAKE_CROSSCOMPILING)
|
||||
set(${var} "DEPENDS;${name}" PARENT_SCOPE) #cant call command when cross
|
||||
else()
|
||||
set(${var} "DEPENDS;${name};COMMAND;${name}" PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction(GR_GEN_TARGET_DEPS)
|
|
@ -1,46 +0,0 @@
|
|||
# Copyright 2011 Free Software Foundation, Inc.
|
||||
#
|
||||
# This file is part of GNU Radio
|
||||
#
|
||||
# GNU Radio is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# GNU Radio is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with GNU Radio; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
if(DEFINED __INCLUDED_GR_PLATFORM_CMAKE)
|
||||
return()
|
||||
endif()
|
||||
set(__INCLUDED_GR_PLATFORM_CMAKE TRUE)
|
||||
|
||||
########################################################################
|
||||
# Setup additional defines for OS types
|
||||
########################################################################
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(LINUX TRUE)
|
||||
endif()
|
||||
|
||||
if(LINUX AND EXISTS "/etc/debian_version")
|
||||
set(DEBIAN TRUE)
|
||||
endif()
|
||||
|
||||
if(LINUX AND EXISTS "/etc/redhat-release")
|
||||
set(REDHAT TRUE)
|
||||
endif()
|
||||
|
||||
########################################################################
|
||||
# when the library suffix should be 64 (applies to redhat linux family)
|
||||
########################################################################
|
||||
if(NOT DEFINED LIB_SUFFIX AND REDHAT AND CMAKE_SYSTEM_PROCESSOR MATCHES "64$")
|
||||
set(LIB_SUFFIX 64)
|
||||
endif()
|
||||
set(LIB_SUFFIX ${LIB_SUFFIX} CACHE STRING "lib directory suffix")
|
|
@ -1,227 +0,0 @@
|
|||
# Copyright 2010-2011 Free Software Foundation, Inc.
|
||||
#
|
||||
# This file is part of GNU Radio
|
||||
#
|
||||
# GNU Radio is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# GNU Radio is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with GNU Radio; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
if(DEFINED __INCLUDED_GR_PYTHON_CMAKE)
|
||||
return()
|
||||
endif()
|
||||
set(__INCLUDED_GR_PYTHON_CMAKE TRUE)
|
||||
|
||||
########################################################################
|
||||
# Setup the python interpreter:
|
||||
# This allows the user to specify a specific interpreter,
|
||||
# or finds the interpreter via the built-in cmake module.
|
||||
########################################################################
|
||||
#this allows the user to override PYTHON_EXECUTABLE
|
||||
if(PYTHON_EXECUTABLE)
|
||||
|
||||
set(PYTHONINTERP_FOUND TRUE)
|
||||
|
||||
#otherwise if not set, try to automatically find it
|
||||
else(PYTHON_EXECUTABLE)
|
||||
|
||||
#use the built-in find script
|
||||
find_package(PythonInterp 2)
|
||||
|
||||
#and if that fails use the find program routine
|
||||
if(NOT PYTHONINTERP_FOUND)
|
||||
find_program(PYTHON_EXECUTABLE NAMES python python2 python2.7 python2.6 python2.5)
|
||||
if(PYTHON_EXECUTABLE)
|
||||
set(PYTHONINTERP_FOUND TRUE)
|
||||
endif(PYTHON_EXECUTABLE)
|
||||
endif(NOT PYTHONINTERP_FOUND)
|
||||
|
||||
endif(PYTHON_EXECUTABLE)
|
||||
|
||||
#make the path to the executable appear in the cmake gui
|
||||
set(PYTHON_EXECUTABLE ${PYTHON_EXECUTABLE} CACHE FILEPATH "python interpreter")
|
||||
|
||||
#make sure we can use -B with python (introduced in 2.6)
|
||||
if(PYTHON_EXECUTABLE)
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} -B -c ""
|
||||
OUTPUT_QUIET ERROR_QUIET
|
||||
RESULT_VARIABLE PYTHON_HAS_DASH_B_RESULT
|
||||
)
|
||||
if(PYTHON_HAS_DASH_B_RESULT EQUAL 0)
|
||||
set(PYTHON_DASH_B "-B")
|
||||
endif()
|
||||
endif(PYTHON_EXECUTABLE)
|
||||
|
||||
########################################################################
|
||||
# Check for the existence of a python module:
|
||||
# - desc a string description of the check
|
||||
# - mod the name of the module to import
|
||||
# - cmd an additional command to run
|
||||
# - have the result variable to set
|
||||
########################################################################
|
||||
macro(GR_PYTHON_CHECK_MODULE desc mod cmd have)
|
||||
message(STATUS "")
|
||||
message(STATUS "Python checking for ${desc}")
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE} -c "
|
||||
#########################################
|
||||
try: import ${mod}
|
||||
except: exit(-1)
|
||||
try: assert ${cmd}
|
||||
except: exit(-1)
|
||||
#########################################"
|
||||
RESULT_VARIABLE ${have}
|
||||
)
|
||||
if(${have} EQUAL 0)
|
||||
message(STATUS "Python checking for ${desc} - found")
|
||||
set(${have} TRUE)
|
||||
else(${have} EQUAL 0)
|
||||
message(STATUS "Python checking for ${desc} - not found")
|
||||
set(${have} FALSE)
|
||||
endif(${have} EQUAL 0)
|
||||
endmacro(GR_PYTHON_CHECK_MODULE)
|
||||
|
||||
########################################################################
|
||||
# Sets the python installation directory GR_PYTHON_DIR
|
||||
########################################################################
|
||||
execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "
|
||||
from distutils import sysconfig
|
||||
print sysconfig.get_python_lib(plat_specific=True, prefix='')
|
||||
" OUTPUT_VARIABLE GR_PYTHON_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
file(TO_CMAKE_PATH ${GR_PYTHON_DIR} GR_PYTHON_DIR)
|
||||
|
||||
########################################################################
|
||||
# Create an always-built target with a unique name
|
||||
# Usage: GR_UNIQUE_TARGET(<description> <dependencies list>)
|
||||
########################################################################
|
||||
function(GR_UNIQUE_TARGET desc)
|
||||
file(RELATIVE_PATH reldir ${CMAKE_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR})
|
||||
execute_process(COMMAND ${PYTHON_EXECUTABLE} -c "import re, hashlib
|
||||
unique = hashlib.md5('${reldir}${ARGN}').hexdigest()[:5]
|
||||
print(re.sub('\\W', '_', '${desc} ${reldir} ' + unique))"
|
||||
OUTPUT_VARIABLE _target OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
add_custom_target(${_target} ALL DEPENDS ${ARGN})
|
||||
endfunction(GR_UNIQUE_TARGET)
|
||||
|
||||
########################################################################
|
||||
# Install python sources (also builds and installs byte-compiled python)
|
||||
########################################################################
|
||||
function(GR_PYTHON_INSTALL)
|
||||
include(CMakeParseArgumentsCopy)
|
||||
CMAKE_PARSE_ARGUMENTS(GR_PYTHON_INSTALL "" "DESTINATION;COMPONENT" "FILES;PROGRAMS" ${ARGN})
|
||||
|
||||
####################################################################
|
||||
if(GR_PYTHON_INSTALL_FILES)
|
||||
####################################################################
|
||||
install(${ARGN}) #installs regular python files
|
||||
|
||||
#create a list of all generated files
|
||||
unset(pysrcfiles)
|
||||
unset(pycfiles)
|
||||
unset(pyofiles)
|
||||
foreach(pyfile ${GR_PYTHON_INSTALL_FILES})
|
||||
get_filename_component(pyfile ${pyfile} ABSOLUTE)
|
||||
list(APPEND pysrcfiles ${pyfile})
|
||||
|
||||
#determine if this file is in the source or binary directory
|
||||
file(RELATIVE_PATH source_rel_path ${CMAKE_CURRENT_SOURCE_DIR} ${pyfile})
|
||||
string(LENGTH "${source_rel_path}" source_rel_path_len)
|
||||
file(RELATIVE_PATH binary_rel_path ${CMAKE_CURRENT_BINARY_DIR} ${pyfile})
|
||||
string(LENGTH "${binary_rel_path}" binary_rel_path_len)
|
||||
|
||||
#and set the generated path appropriately
|
||||
if(${source_rel_path_len} GREATER ${binary_rel_path_len})
|
||||
set(pygenfile ${CMAKE_CURRENT_BINARY_DIR}/${binary_rel_path})
|
||||
else()
|
||||
set(pygenfile ${CMAKE_CURRENT_BINARY_DIR}/${source_rel_path})
|
||||
endif()
|
||||
list(APPEND pycfiles ${pygenfile}c)
|
||||
list(APPEND pyofiles ${pygenfile}o)
|
||||
|
||||
#ensure generation path exists
|
||||
get_filename_component(pygen_path ${pygenfile} PATH)
|
||||
file(MAKE_DIRECTORY ${pygen_path})
|
||||
|
||||
endforeach(pyfile)
|
||||
|
||||
#the command to generate the pyc files
|
||||
add_custom_command(
|
||||
DEPENDS ${pysrcfiles} OUTPUT ${pycfiles}
|
||||
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_BINARY_DIR}/python_compile_helper.py ${pysrcfiles} ${pycfiles}
|
||||
)
|
||||
|
||||
#the command to generate the pyo files
|
||||
add_custom_command(
|
||||
DEPENDS ${pysrcfiles} OUTPUT ${pyofiles}
|
||||
COMMAND ${PYTHON_EXECUTABLE} -O ${CMAKE_BINARY_DIR}/python_compile_helper.py ${pysrcfiles} ${pyofiles}
|
||||
)
|
||||
|
||||
#create install rule and add generated files to target list
|
||||
set(python_install_gen_targets ${pycfiles} ${pyofiles})
|
||||
install(FILES ${python_install_gen_targets}
|
||||
DESTINATION ${GR_PYTHON_INSTALL_DESTINATION}
|
||||
COMPONENT ${GR_PYTHON_INSTALL_COMPONENT}
|
||||
)
|
||||
|
||||
|
||||
####################################################################
|
||||
elseif(GR_PYTHON_INSTALL_PROGRAMS)
|
||||
####################################################################
|
||||
file(TO_NATIVE_PATH ${PYTHON_EXECUTABLE} pyexe_native)
|
||||
|
||||
foreach(pyfile ${GR_PYTHON_INSTALL_PROGRAMS})
|
||||
get_filename_component(pyfile_name ${pyfile} NAME)
|
||||
get_filename_component(pyfile ${pyfile} ABSOLUTE)
|
||||
string(REPLACE "${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}" pyexefile "${pyfile}.exe")
|
||||
list(APPEND python_install_gen_targets ${pyexefile})
|
||||
|
||||
get_filename_component(pyexefile_path ${pyexefile} PATH)
|
||||
file(MAKE_DIRECTORY ${pyexefile_path})
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${pyexefile} DEPENDS ${pyfile}
|
||||
COMMAND ${PYTHON_EXECUTABLE} -c
|
||||
\"open('${pyexefile}', 'w').write('\#!${pyexe_native}\\n'+open('${pyfile}').read())\"
|
||||
COMMENT "Shebangin ${pyfile_name}"
|
||||
)
|
||||
|
||||
#on windows, python files need an extension to execute
|
||||
get_filename_component(pyfile_ext ${pyfile} EXT)
|
||||
if(WIN32 AND NOT pyfile_ext)
|
||||
set(pyfile_name "${pyfile_name}.py")
|
||||
endif()
|
||||
|
||||
install(PROGRAMS ${pyexefile} RENAME ${pyfile_name}
|
||||
DESTINATION ${GR_PYTHON_INSTALL_DESTINATION}
|
||||
COMPONENT ${GR_PYTHON_INSTALL_COMPONENT}
|
||||
)
|
||||
endforeach(pyfile)
|
||||
|
||||
endif()
|
||||
|
||||
GR_UNIQUE_TARGET("pygen" ${python_install_gen_targets})
|
||||
|
||||
endfunction(GR_PYTHON_INSTALL)
|
||||
|
||||
########################################################################
|
||||
# Write the python helper script that generates byte code files
|
||||
########################################################################
|
||||
file(WRITE ${CMAKE_BINARY_DIR}/python_compile_helper.py "
|
||||
import sys, py_compile
|
||||
files = sys.argv[1:]
|
||||
srcs, gens = files[:len(files)/2], files[len(files)/2:]
|
||||
for src, gen in zip(srcs, gens):
|
||||
py_compile.compile(file=src, cfile=gen, doraise=True)
|
||||
")
|
|
@ -1,229 +0,0 @@
|
|||
# Copyright 2010-2011 Free Software Foundation, Inc.
|
||||
#
|
||||
# This file is part of GNU Radio
|
||||
#
|
||||
# GNU Radio is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# GNU Radio is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with GNU Radio; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
if(DEFINED __INCLUDED_GR_SWIG_CMAKE)
|
||||
return()
|
||||
endif()
|
||||
set(__INCLUDED_GR_SWIG_CMAKE TRUE)
|
||||
|
||||
include(GrPython)
|
||||
|
||||
########################################################################
|
||||
# Builds a swig documentation file to be generated into python docstrings
|
||||
# Usage: GR_SWIG_MAKE_DOCS(output_file input_path input_path....)
|
||||
#
|
||||
# Set the following variable to specify extra dependent targets:
|
||||
# - GR_SWIG_DOCS_SOURCE_DEPS
|
||||
# - GR_SWIG_DOCS_TARGET_DEPS
|
||||
########################################################################
|
||||
function(GR_SWIG_MAKE_DOCS output_file)
|
||||
find_package(Doxygen)
|
||||
if(DOXYGEN_FOUND)
|
||||
|
||||
#setup the input files variable list, quote formated
|
||||
set(input_files)
|
||||
unset(INPUT_PATHS)
|
||||
foreach(input_path ${ARGN})
|
||||
if (IS_DIRECTORY ${input_path}) #when input path is a directory
|
||||
file(GLOB input_path_h_files ${input_path}/*.h)
|
||||
else() #otherwise its just a file, no glob
|
||||
set(input_path_h_files ${input_path})
|
||||
endif()
|
||||
list(APPEND input_files ${input_path_h_files})
|
||||
set(INPUT_PATHS "${INPUT_PATHS} \"${input_path}\"")
|
||||
endforeach(input_path)
|
||||
|
||||
#determine the output directory
|
||||
get_filename_component(name ${output_file} NAME_WE)
|
||||
get_filename_component(OUTPUT_DIRECTORY ${output_file} PATH)
|
||||
set(OUTPUT_DIRECTORY ${OUTPUT_DIRECTORY}/${name}_swig_docs)
|
||||
make_directory(${OUTPUT_DIRECTORY})
|
||||
|
||||
#generate the Doxyfile used by doxygen
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/docs/doxygen/Doxyfile.swig_doc.in
|
||||
${OUTPUT_DIRECTORY}/Doxyfile
|
||||
@ONLY)
|
||||
|
||||
#Create a dummy custom command that depends on other targets
|
||||
include(GrMiscUtils)
|
||||
GR_GEN_TARGET_DEPS(_${name}_tag tag_deps ${GR_SWIG_DOCS_TARGET_DEPS})
|
||||
|
||||
#call doxygen on the Doxyfile + input headers
|
||||
add_custom_command(
|
||||
OUTPUT ${OUTPUT_DIRECTORY}/xml/index.xml
|
||||
DEPENDS ${input_files} ${GR_SWIG_DOCS_SOURCE_DEPS} ${tag_deps}
|
||||
COMMAND ${DOXYGEN_EXECUTABLE} ${OUTPUT_DIRECTORY}/Doxyfile
|
||||
COMMENT "Generating doxygen xml for ${name} docs"
|
||||
)
|
||||
|
||||
#call the swig_doc script on the xml files
|
||||
add_custom_command(
|
||||
OUTPUT ${output_file}
|
||||
DEPENDS ${input_files} ${OUTPUT_DIRECTORY}/xml/index.xml
|
||||
COMMAND ${PYTHON_EXECUTABLE} ${PYTHON_DASH_B}
|
||||
${CMAKE_SOURCE_DIR}/docs/doxygen/swig_doc.py
|
||||
${OUTPUT_DIRECTORY}/xml
|
||||
${output_file}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/docs/doxygen
|
||||
)
|
||||
|
||||
else(DOXYGEN_FOUND)
|
||||
file(WRITE ${output_file} "\n") #no doxygen -> empty file
|
||||
endif(DOXYGEN_FOUND)
|
||||
endfunction(GR_SWIG_MAKE_DOCS)
|
||||
|
||||
########################################################################
|
||||
# Build a swig target for the common gnuradio use case. Usage:
|
||||
# GR_SWIG_MAKE(target ifile ifile ifile...)
|
||||
#
|
||||
# Set the following variables before calling:
|
||||
# - GR_SWIG_FLAGS
|
||||
# - GR_SWIG_INCLUDE_DIRS
|
||||
# - GR_SWIG_LIBRARIES
|
||||
# - GR_SWIG_SOURCE_DEPS
|
||||
# - GR_SWIG_TARGET_DEPS
|
||||
# - GR_SWIG_DOC_FILE
|
||||
# - GR_SWIG_DOC_DIRS
|
||||
########################################################################
|
||||
macro(GR_SWIG_MAKE name)
|
||||
set(ifiles ${ARGN})
|
||||
|
||||
#do swig doc generation if specified
|
||||
if (GR_SWIG_DOC_FILE)
|
||||
set(GR_SWIG_DOCS_SOURCE_DEPS ${GR_SWIG_SOURCE_DEPS})
|
||||
set(GR_SWIG_DOCS_TAREGT_DEPS ${GR_SWIG_TARGET_DEPS})
|
||||
GR_SWIG_MAKE_DOCS(${GR_SWIG_DOC_FILE} ${GR_SWIG_DOC_DIRS})
|
||||
list(APPEND GR_SWIG_SOURCE_DEPS ${GR_SWIG_DOC_FILE})
|
||||
endif()
|
||||
|
||||
#append additional include directories
|
||||
find_package(PythonLibs 2)
|
||||
list(APPEND GR_SWIG_INCLUDE_DIRS ${PYTHON_INCLUDE_PATH}) #deprecated name (now dirs)
|
||||
list(APPEND GR_SWIG_INCLUDE_DIRS ${PYTHON_INCLUDE_DIRS})
|
||||
list(APPEND GR_SWIG_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
list(APPEND GR_SWIG_INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
#determine include dependencies for swig file
|
||||
execute_process(
|
||||
COMMAND ${PYTHON_EXECUTABLE}
|
||||
${CMAKE_BINARY_DIR}/get_swig_deps.py
|
||||
"${ifiles}" "${GR_SWIG_INCLUDE_DIRS}"
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
OUTPUT_VARIABLE SWIG_MODULE_${name}_EXTRA_DEPS
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
#Create a dummy custom command that depends on other targets
|
||||
include(GrMiscUtils)
|
||||
GR_GEN_TARGET_DEPS(_${name}_swig_tag tag_deps ${GR_SWIG_TARGET_DEPS})
|
||||
set(tag_file ${CMAKE_CURRENT_BINARY_DIR}/${name}.tag)
|
||||
add_custom_command(
|
||||
OUTPUT ${tag_file}
|
||||
DEPENDS ${GR_SWIG_SOURCE_DEPS} ${tag_deps}
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${tag_file}
|
||||
)
|
||||
|
||||
#append the specified include directories
|
||||
include_directories(${GR_SWIG_INCLUDE_DIRS})
|
||||
list(APPEND SWIG_MODULE_${name}_EXTRA_DEPS ${tag_file})
|
||||
|
||||
#setup the swig flags with flags and include directories
|
||||
set(CMAKE_SWIG_FLAGS -fvirtual -modern -keyword -w511 -module ${name} ${GR_SWIG_FLAGS})
|
||||
foreach(dir ${GR_SWIG_INCLUDE_DIRS})
|
||||
list(APPEND CMAKE_SWIG_FLAGS "-I${dir}")
|
||||
endforeach(dir)
|
||||
|
||||
#set the C++ property on the swig .i file so it builds
|
||||
set_source_files_properties(${ifiles} PROPERTIES CPLUSPLUS ON)
|
||||
|
||||
#setup the actual swig library target to be built
|
||||
include(UseSWIG)
|
||||
SWIG_ADD_MODULE(${name} python ${ifiles})
|
||||
SWIG_LINK_LIBRARIES(${name} ${PYTHON_LIBRARIES} ${GR_SWIG_LIBRARIES})
|
||||
|
||||
endmacro(GR_SWIG_MAKE)
|
||||
|
||||
########################################################################
|
||||
# Install swig targets generated by GR_SWIG_MAKE. Usage:
|
||||
# GR_SWIG_INSTALL(
|
||||
# TARGETS target target target...
|
||||
# [DESTINATION destination]
|
||||
# [COMPONENT component]
|
||||
# )
|
||||
########################################################################
|
||||
macro(GR_SWIG_INSTALL)
|
||||
|
||||
include(CMakeParseArgumentsCopy)
|
||||
CMAKE_PARSE_ARGUMENTS(GR_SWIG_INSTALL "" "DESTINATION;COMPONENT" "TARGETS" ${ARGN})
|
||||
|
||||
foreach(name ${GR_SWIG_INSTALL_TARGETS})
|
||||
install(TARGETS ${SWIG_MODULE_${name}_REAL_NAME}
|
||||
DESTINATION ${GR_SWIG_INSTALL_DESTINATION}
|
||||
COMPONENT ${GR_SWIG_INSTALL_COMPONENT}
|
||||
)
|
||||
|
||||
include(GrPython)
|
||||
GR_PYTHON_INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/${name}.py
|
||||
DESTINATION ${GR_SWIG_INSTALL_DESTINATION}
|
||||
COMPONENT ${GR_SWIG_INSTALL_COMPONENT}
|
||||
)
|
||||
|
||||
GR_LIBTOOL(
|
||||
TARGET ${SWIG_MODULE_${name}_REAL_NAME}
|
||||
DESTINATION ${GR_SWIG_INSTALL_DESTINATION}
|
||||
)
|
||||
|
||||
endforeach(name)
|
||||
|
||||
endmacro(GR_SWIG_INSTALL)
|
||||
|
||||
########################################################################
|
||||
# Generate a python file that can determine swig dependencies.
|
||||
# Used by the make macro above to determine extra dependencies.
|
||||
# When you build C++, CMake figures out the header dependencies.
|
||||
# This code essentially performs that logic for swig includes.
|
||||
########################################################################
|
||||
file(WRITE ${CMAKE_BINARY_DIR}/get_swig_deps.py "
|
||||
|
||||
import os, sys, re
|
||||
|
||||
include_matcher = re.compile('[#|%]include\\s*[<|\"](.*)[>|\"]')
|
||||
include_dirs = sys.argv[2].split(';')
|
||||
|
||||
def get_swig_incs(file_path):
|
||||
file_contents = open(file_path, 'r').read()
|
||||
return include_matcher.findall(file_contents, re.MULTILINE)
|
||||
|
||||
def get_swig_deps(file_path, level):
|
||||
deps = [file_path]
|
||||
if level == 0: return deps
|
||||
for inc_file in get_swig_incs(file_path):
|
||||
for inc_dir in include_dirs:
|
||||
inc_path = os.path.join(inc_dir, inc_file)
|
||||
if not os.path.exists(inc_path): continue
|
||||
deps.extend(get_swig_deps(inc_path, level-1))
|
||||
return deps
|
||||
|
||||
if __name__ == '__main__':
|
||||
ifiles = sys.argv[1].split(';')
|
||||
deps = sum([get_swig_deps(ifile, 3) for ifile in ifiles], [])
|
||||
#sys.stderr.write(';'.join(set(deps)) + '\\n\\n')
|
||||
print(';'.join(set(deps)))
|
||||
")
|
|
@ -1,133 +0,0 @@
|
|||
# Copyright 2010-2011 Free Software Foundation, Inc.
|
||||
#
|
||||
# This file is part of GNU Radio
|
||||
#
|
||||
# GNU Radio is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# GNU Radio is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with GNU Radio; see the file COPYING. If not, write to
|
||||
# the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
# Boston, MA 02110-1301, USA.
|
||||
|
||||
if(DEFINED __INCLUDED_GR_TEST_CMAKE)
|
||||
return()
|
||||
endif()
|
||||
set(__INCLUDED_GR_TEST_CMAKE TRUE)
|
||||
|
||||
########################################################################
|
||||
# Add a unit test and setup the environment for a unit test.
|
||||
# Takes the same arguments as the ADD_TEST function.
|
||||
#
|
||||
# Before calling set the following variables:
|
||||
# GR_TEST_TARGET_DEPS - built targets for the library path
|
||||
# GR_TEST_LIBRARY_DIRS - directories for the library path
|
||||
# GR_TEST_PYTHON_DIRS - directories for the python path
|
||||
########################################################################
|
||||
function(GR_ADD_TEST test_name)
|
||||
|
||||
if(WIN32)
|
||||
#Ensure that the build exe also appears in the PATH.
|
||||
list(APPEND GR_TEST_TARGET_DEPS ${ARGN})
|
||||
|
||||
#In the land of windows, all libraries must be in the PATH.
|
||||
#Since the dependent libraries are not yet installed,
|
||||
#we must manually set them in the PATH to run tests.
|
||||
#The following appends the path of a target dependency.
|
||||
foreach(target ${GR_TEST_TARGET_DEPS})
|
||||
get_target_property(location ${target} LOCATION)
|
||||
if(location)
|
||||
get_filename_component(path ${location} PATH)
|
||||
string(REGEX REPLACE "\\$\\(.*\\)" ${CMAKE_BUILD_TYPE} path ${path})
|
||||
list(APPEND GR_TEST_LIBRARY_DIRS ${path})
|
||||
endif(location)
|
||||
endforeach(target)
|
||||
|
||||
#SWIG generates the python library files into a subdirectory.
|
||||
#Therefore, we must append this subdirectory into PYTHONPATH.
|
||||
#Only do this for the python directories matching the following:
|
||||
foreach(pydir ${GR_TEST_PYTHON_DIRS})
|
||||
get_filename_component(name ${pydir} NAME)
|
||||
if(name MATCHES "^(swig|lib|src)$")
|
||||
list(APPEND GR_TEST_PYTHON_DIRS ${pydir}/${CMAKE_BUILD_TYPE})
|
||||
endif()
|
||||
endforeach(pydir)
|
||||
endif(WIN32)
|
||||
|
||||
file(TO_NATIVE_PATH ${CMAKE_CURRENT_SOURCE_DIR} srcdir)
|
||||
file(TO_NATIVE_PATH "${GR_TEST_LIBRARY_DIRS}" libpath) #ok to use on dir list?
|
||||
file(TO_NATIVE_PATH "${GR_TEST_PYTHON_DIRS}" pypath) #ok to use on dir list?
|
||||
|
||||
set(environs "GR_DONT_LOAD_PREFS=1" "srcdir=${srcdir}")
|
||||
|
||||
#http://www.cmake.org/pipermail/cmake/2009-May/029464.html
|
||||
#Replaced this add test + set environs code with the shell script generation.
|
||||
#Its nicer to be able to manually run the shell script to diagnose problems.
|
||||
#ADD_TEST(${ARGV})
|
||||
#SET_TESTS_PROPERTIES(${test_name} PROPERTIES ENVIRONMENT "${environs}")
|
||||
|
||||
if(UNIX)
|
||||
set(binpath "${CMAKE_CURRENT_BINARY_DIR}:$PATH")
|
||||
#set both LD and DYLD paths to cover multiple UNIX OS library paths
|
||||
list(APPEND libpath "$LD_LIBRARY_PATH" "$DYLD_LIBRARY_PATH")
|
||||
list(APPEND pypath "$PYTHONPATH")
|
||||
|
||||
#replace list separator with the path separator
|
||||
string(REPLACE ";" ":" libpath "${libpath}")
|
||||
string(REPLACE ";" ":" pypath "${pypath}")
|
||||
list(APPEND environs "PATH=${binpath}" "LD_LIBRARY_PATH=${libpath}" "DYLD_LIBRARY_PATH=${libpath}" "PYTHONPATH=${pypath}")
|
||||
|
||||
#generate a bat file that sets the environment and runs the test
|
||||
find_program(SHELL sh)
|
||||
set(sh_file ${CMAKE_CURRENT_BINARY_DIR}/${test_name}_test.sh)
|
||||
file(WRITE ${sh_file} "#!${SHELL}\n")
|
||||
#each line sets an environment variable
|
||||
foreach(environ ${environs})
|
||||
file(APPEND ${sh_file} "export ${environ}\n")
|
||||
endforeach(environ)
|
||||
#load the command to run with its arguments
|
||||
foreach(arg ${ARGN})
|
||||
file(APPEND ${sh_file} "${arg} ")
|
||||
endforeach(arg)
|
||||
file(APPEND ${sh_file} "\n")
|
||||
|
||||
#make the shell file executable
|
||||
execute_process(COMMAND chmod +x ${sh_file})
|
||||
|
||||
add_test(${test_name} ${SHELL} ${sh_file})
|
||||
|
||||
endif(UNIX)
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND libpath ${DLL_PATHS} "%PATH%")
|
||||
list(APPEND pypath "%PYTHONPATH%")
|
||||
|
||||
#replace list separator with the path separator (escaped)
|
||||
string(REPLACE ";" "\\;" libpath "${libpath}")
|
||||
string(REPLACE ";" "\\;" pypath "${pypath}")
|
||||
list(APPEND environs "PATH=${libpath}" "PYTHONPATH=${pypath}")
|
||||
|
||||
#generate a bat file that sets the environment and runs the test
|
||||
set(bat_file ${CMAKE_CURRENT_BINARY_DIR}/${test_name}_test.bat)
|
||||
file(WRITE ${bat_file} "@echo off\n")
|
||||
#each line sets an environment variable
|
||||
foreach(environ ${environs})
|
||||
file(APPEND ${bat_file} "SET ${environ}\n")
|
||||
endforeach(environ)
|
||||
#load the command to run with its arguments
|
||||
foreach(arg ${ARGN})
|
||||
file(APPEND ${bat_file} "${arg} ")
|
||||
endforeach(arg)
|
||||
file(APPEND ${bat_file} "\n")
|
||||
|
||||
add_test(${test_name} ${bat_file})
|
||||
endif(WIN32)
|
||||
|
||||
endfunction(GR_ADD_TEST)
|
|
@ -0,0 +1,367 @@
|
|||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 56f95f4..c43483a 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -4,6 +4,24 @@ project(gr-op25 CXX C)
|
||||
set(CMAKE_BUILD_TYPE Debug)
|
||||
set(CMAKE_CXX_FLAGS "-std=c++11")
|
||||
|
||||
+execute_process(COMMAND python3 -c "
|
||||
+import os
|
||||
+import sys
|
||||
+from distutils import sysconfig
|
||||
+pfx = '/usr/local'
|
||||
+m1 = os.path.join('lib', 'python' + sys.version[:3], 'dist-packages')
|
||||
+m2 = sysconfig.get_python_lib(plat_specific=True, prefix='')
|
||||
+f1 = os.path.join(pfx, m1)
|
||||
+f2 = os.path.join(pfx, m2)
|
||||
+ok2 = f2 in sys.path
|
||||
+if ok2:
|
||||
+ print(m2)
|
||||
+else:
|
||||
+ print(m1)
|
||||
+" OUTPUT_VARIABLE OP25_PYTHON_DIR OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
+)
|
||||
+MESSAGE(STATUS "OP25_PYTHON_DIR has been set to \"${OP25_PYTHON_DIR}\".")
|
||||
+
|
||||
add_subdirectory(op25/gr-op25)
|
||||
add_subdirectory(op25/gr-op25_repeater)
|
||||
|
||||
diff --git a/install.sh b/install.sh
|
||||
index 10fbeb5..4246447 100755
|
||||
--- a/install.sh
|
||||
+++ b/install.sh
|
||||
@@ -10,9 +10,12 @@ if [ ! -d op25/gr-op25 ]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
+sudo sed -i -- 's/^# *deb-src/deb-src/' /etc/apt/sources.list
|
||||
+
|
||||
sudo apt-get update
|
||||
sudo apt-get build-dep gnuradio
|
||||
-sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python-numpy python-waitress python-requests
|
||||
+sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python3-numpy python3-waitress python3-requests
|
||||
+sudo apt-get install liborc-dev
|
||||
|
||||
mkdir build
|
||||
cd build
|
||||
diff --git a/op25/gr-op25/CMakeLists.txt b/op25/gr-op25/CMakeLists.txt
|
||||
index 6c05df5..1c6fa23 100644
|
||||
--- a/op25/gr-op25/CMakeLists.txt
|
||||
+++ b/op25/gr-op25/CMakeLists.txt
|
||||
@@ -68,6 +68,9 @@ endif()
|
||||
########################################################################
|
||||
find_package(CppUnit)
|
||||
|
||||
+set(ENABLE_PYTHON "TRUE" CACHE BOOL "enable python")
|
||||
+cmake_policy(SET CMP0012 NEW)
|
||||
+
|
||||
# To run a more advanced search for GNU Radio and it's components and
|
||||
# versions, use the following. Add any components required to the list
|
||||
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
|
||||
@@ -76,11 +79,12 @@ find_package(CppUnit)
|
||||
# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...)
|
||||
# find_package(Gnuradio "version")
|
||||
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
|
||||
-find_package(Gnuradio)
|
||||
-
|
||||
-if(NOT GNURADIO_RUNTIME_FOUND)
|
||||
- message(FATAL_ERROR "GnuRadio Runtime required to compile op25")
|
||||
+SET(MIN_GR_VERSION "3.8.0")
|
||||
+find_package(Gnuradio REQUIRED)
|
||||
+if("${Gnuradio_VERSION}" VERSION_LESS MIN_GR_VERSION)
|
||||
+ MESSAGE(FATAL_ERROR "GnuRadio version required: >=\"" ${MIN_GR_VERSION} "\" found: \"" ${Gnuradio_VERSION} "\"")
|
||||
endif()
|
||||
+
|
||||
if(NOT CPPUNIT_FOUND)
|
||||
message(FATAL_ERROR "CppUnit required to compile op25")
|
||||
endif()
|
||||
diff --git a/op25/gr-op25/lib/CMakeLists.txt b/op25/gr-op25/lib/CMakeLists.txt
|
||||
index 1befdd9..609fa84 100644
|
||||
--- a/op25/gr-op25/lib/CMakeLists.txt
|
||||
+++ b/op25/gr-op25/lib/CMakeLists.txt
|
||||
@@ -63,7 +63,7 @@ list(APPEND op25_sources
|
||||
)
|
||||
|
||||
add_library(gnuradio-op25 SHARED ${op25_sources})
|
||||
-target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} itpp pcap)
|
||||
+target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} gnuradio::gnuradio-runtime itpp pcap)
|
||||
set_target_properties(gnuradio-op25 PROPERTIES DEFINE_SYMBOL "gnuradio_op25_EXPORTS")
|
||||
|
||||
########################################################################
|
||||
diff --git a/op25/gr-op25/python/CMakeLists.txt b/op25/gr-op25/python/CMakeLists.txt
|
||||
index 03361a2..e497faa 100644
|
||||
--- a/op25/gr-op25/python/CMakeLists.txt
|
||||
+++ b/op25/gr-op25/python/CMakeLists.txt
|
||||
@@ -31,7 +31,7 @@ endif()
|
||||
GR_PYTHON_INSTALL(
|
||||
FILES
|
||||
__init__.py
|
||||
- DESTINATION ${GR_PYTHON_DIR}/op25
|
||||
+ DESTINATION ${OP25_PYTHON_DIR}/op25
|
||||
)
|
||||
|
||||
########################################################################
|
||||
diff --git a/op25/gr-op25/swig/CMakeLists.txt b/op25/gr-op25/swig/CMakeLists.txt
|
||||
index e99226f..fd7bd85 100644
|
||||
--- a/op25/gr-op25/swig/CMakeLists.txt
|
||||
+++ b/op25/gr-op25/swig/CMakeLists.txt
|
||||
@@ -21,7 +21,7 @@
|
||||
# Include swig generation macros
|
||||
########################################################################
|
||||
find_package(SWIG)
|
||||
-find_package(PythonLibs 2)
|
||||
+find_package(PythonLibs 3)
|
||||
if(NOT SWIG_FOUND OR NOT PYTHONLIBS_FOUND)
|
||||
return()
|
||||
endif()
|
||||
@@ -31,9 +31,7 @@ include(GrPython)
|
||||
########################################################################
|
||||
# Setup swig generation
|
||||
########################################################################
|
||||
-foreach(incdir ${GNURADIO_RUNTIME_INCLUDE_DIRS})
|
||||
- list(APPEND GR_SWIG_INCLUDE_DIRS ${incdir}/gnuradio/swig)
|
||||
-endforeach(incdir)
|
||||
+set(GR_SWIG_INCLUDE_DIRS $<TARGET_PROPERTY:gnuradio::runtime_swig,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
set(GR_SWIG_LIBRARIES gnuradio-op25)
|
||||
set(GR_SWIG_DOC_FILE ${CMAKE_CURRENT_BINARY_DIR}/op25_swig_doc.i)
|
||||
@@ -44,7 +42,7 @@ GR_SWIG_MAKE(op25_swig op25_swig.i)
|
||||
########################################################################
|
||||
# Install the build swig module
|
||||
########################################################################
|
||||
-GR_SWIG_INSTALL(TARGETS op25_swig DESTINATION ${GR_PYTHON_DIR}/op25)
|
||||
+GR_SWIG_INSTALL(TARGETS op25_swig DESTINATION ${OP25_PYTHON_DIR}/op25)
|
||||
|
||||
########################################################################
|
||||
# Install swig .i files for development
|
||||
diff --git a/op25/gr-op25_repeater/CMakeLists.txt b/op25/gr-op25_repeater/CMakeLists.txt
|
||||
index fa29b9e..dc1d8e7 100644
|
||||
--- a/op25/gr-op25_repeater/CMakeLists.txt
|
||||
+++ b/op25/gr-op25_repeater/CMakeLists.txt
|
||||
@@ -68,6 +68,9 @@ endif()
|
||||
########################################################################
|
||||
find_package(CppUnit)
|
||||
|
||||
+set(ENABLE_PYTHON "TRUE" CACHE BOOL "enable python")
|
||||
+cmake_policy(SET CMP0012 NEW)
|
||||
+
|
||||
# To run a more advanced search for GNU Radio and it's components and
|
||||
# versions, use the following. Add any components required to the list
|
||||
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
|
||||
@@ -75,11 +78,12 @@ find_package(CppUnit)
|
||||
#
|
||||
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
|
||||
# find_package(Gnuradio "version")
|
||||
-find_package(Gnuradio)
|
||||
-
|
||||
-if(NOT GNURADIO_RUNTIME_FOUND)
|
||||
- message(FATAL_ERROR "GnuRadio Runtime required to compile op25_repeater")
|
||||
+set(MIN_GR_VERSION "3.8.0")
|
||||
+find_package(Gnuradio REQUIRED)
|
||||
+if("${Gnuradio_VERSION}" VERSION_LESS MIN_GR_VERSION)
|
||||
+ MESSAGE(FATAL_ERROR "GnuRadio version required: >=\"" ${MIN_GR_VERSION} "\" found: \"" ${Gnuradio_VERSION} "\"")
|
||||
endif()
|
||||
+
|
||||
if(NOT CPPUNIT_FOUND)
|
||||
message(FATAL_ERROR "CppUnit required to compile op25_repeater")
|
||||
endif()
|
||||
diff --git a/op25/gr-op25_repeater/apps/audio.py b/op25/gr-op25_repeater/apps/audio.py
|
||||
index 26cbe4f..255812f 100755
|
||||
--- a/op25/gr-op25_repeater/apps/audio.py
|
||||
+++ b/op25/gr-op25_repeater/apps/audio.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2017, 2018 Graham Norbury
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/http_server.py b/op25/gr-op25_repeater/apps/http_server.py
|
||||
index f402353..f4b047f 100755
|
||||
--- a/op25/gr-op25_repeater/apps/http_server.py
|
||||
+++ b/op25/gr-op25_repeater/apps/http_server.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/multi_rx.py b/op25/gr-op25_repeater/apps/multi_rx.py
|
||||
index e4c71ca..42625f5 100755
|
||||
--- a/op25/gr-op25_repeater/apps/multi_rx.py
|
||||
+++ b/op25/gr-op25_repeater/apps/multi_rx.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py
|
||||
index c671120..b226f8a 100755
|
||||
--- a/op25/gr-op25_repeater/apps/rx.py
|
||||
+++ b/op25/gr-op25_repeater/apps/rx.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2008-2011 Steve Glass
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/sockaudio.py b/op25/gr-op25_repeater/apps/sockaudio.py
|
||||
index 76282ac..68c6adb 100755
|
||||
--- a/op25/gr-op25_repeater/apps/sockaudio.py
|
||||
+++ b/op25/gr-op25_repeater/apps/sockaudio.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2017, 2018 Graham Norbury
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/terminal.py b/op25/gr-op25_repeater/apps/terminal.py
|
||||
index c732a3a..fa73af4 100755
|
||||
--- a/op25/gr-op25_repeater/apps/terminal.py
|
||||
+++ b/op25/gr-op25_repeater/apps/terminal.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2008-2011 Steve Glass
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/tx/dv_tx.py b/op25/gr-op25_repeater/apps/tx/dv_tx.py
|
||||
index 342ba3f..fba399f 100755
|
||||
--- a/op25/gr-op25_repeater/apps/tx/dv_tx.py
|
||||
+++ b/op25/gr-op25_repeater/apps/tx/dv_tx.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
#################################################################################
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/tx/generate-tsbks.py b/op25/gr-op25_repeater/apps/tx/generate-tsbks.py
|
||||
index f4b06d9..de3eb28 100755
|
||||
--- a/op25/gr-op25_repeater/apps/tx/generate-tsbks.py
|
||||
+++ b/op25/gr-op25_repeater/apps/tx/generate-tsbks.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#! /usr/bin/python
|
||||
+#! /usr/bin/python3
|
||||
|
||||
from p25craft import make_fakecc_tsdu
|
||||
|
||||
diff --git a/op25/gr-op25_repeater/apps/tx/multi_tx.py b/op25/gr-op25_repeater/apps/tx/multi_tx.py
|
||||
index a54bc01..1707502 100755
|
||||
--- a/op25/gr-op25_repeater/apps/tx/multi_tx.py
|
||||
+++ b/op25/gr-op25_repeater/apps/tx/multi_tx.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
#################################################################################
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/tx/op25_tx.py b/op25/gr-op25_repeater/apps/tx/op25_tx.py
|
||||
index 3e7afe4..7baa6ae 100755
|
||||
--- a/op25/gr-op25_repeater/apps/tx/op25_tx.py
|
||||
+++ b/op25/gr-op25_repeater/apps/tx/op25_tx.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2005,2006,2007 Free Software Foundation, Inc.
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/tx/p25craft.py b/op25/gr-op25_repeater/apps/tx/p25craft.py
|
||||
index 7fae739..b6e3999 100755
|
||||
--- a/op25/gr-op25_repeater/apps/tx/p25craft.py
|
||||
+++ b/op25/gr-op25_repeater/apps/tx/p25craft.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/python
|
||||
+#!/usr/bin/python3
|
||||
#
|
||||
# p25craft.py - utility for crafting APCO P25 packets
|
||||
#
|
||||
diff --git a/op25/gr-op25_repeater/apps/tx/unpack.py b/op25/gr-op25_repeater/apps/tx/unpack.py
|
||||
index 7cbf05b..73a861a 100755
|
||||
--- a/op25/gr-op25_repeater/apps/tx/unpack.py
|
||||
+++ b/op25/gr-op25_repeater/apps/tx/unpack.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
from gnuradio import gr, audio, eng_notation, blocks
|
||||
from gnuradio.eng_option import eng_option
|
||||
from optparse import OptionParser
|
||||
diff --git a/op25/gr-op25_repeater/apps/util/arb-resample.py b/op25/gr-op25_repeater/apps/util/arb-resample.py
|
||||
index 56a762f..2bf75af 100755
|
||||
--- a/op25/gr-op25_repeater/apps/util/arb-resample.py
|
||||
+++ b/op25/gr-op25_repeater/apps/util/arb-resample.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import math
|
||||
diff --git a/op25/gr-op25_repeater/apps/util/cqpsk-demod-file.py b/op25/gr-op25_repeater/apps/util/cqpsk-demod-file.py
|
||||
index 051ddf0..171878d 100755
|
||||
--- a/op25/gr-op25_repeater/apps/util/cqpsk-demod-file.py
|
||||
+++ b/op25/gr-op25_repeater/apps/util/cqpsk-demod-file.py
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env python
|
||||
+#!/usr/bin/env python3
|
||||
|
||||
#
|
||||
# (C) Copyright 2010, 2014 Max H. Parke, KA1RBI
|
||||
diff --git a/op25/gr-op25_repeater/lib/CMakeLists.txt b/op25/gr-op25_repeater/lib/CMakeLists.txt
|
||||
index da84e14..1464230 100644
|
||||
--- a/op25/gr-op25_repeater/lib/CMakeLists.txt
|
||||
+++ b/op25/gr-op25_repeater/lib/CMakeLists.txt
|
||||
@@ -68,7 +68,7 @@ else(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
find_library(GR_FILTER_LIBRARY libgnuradio-filter.so )
|
||||
endif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
set(GR_FILTER_LIBRARIES ${GR_FILTER_LIBRARY})
|
||||
-target_link_libraries(gnuradio-op25_repeater ${Boost_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} ${GR_FILTER_LIBRARIES} imbe_vocoder)
|
||||
+target_link_libraries(gnuradio-op25_repeater ${Boost_LIBRARIES} gnuradio::gnuradio-runtime ${GR_FILTER_LIBRARIES} imbe_vocoder)
|
||||
set_target_properties(gnuradio-op25_repeater PROPERTIES DEFINE_SYMBOL "gnuradio_op25_repeater_EXPORTS")
|
||||
|
||||
########################################################################
|
||||
diff --git a/op25/gr-op25_repeater/python/CMakeLists.txt b/op25/gr-op25_repeater/python/CMakeLists.txt
|
||||
index 9958577..bb117a0 100644
|
||||
--- a/op25/gr-op25_repeater/python/CMakeLists.txt
|
||||
+++ b/op25/gr-op25_repeater/python/CMakeLists.txt
|
||||
@@ -31,7 +31,7 @@ endif()
|
||||
GR_PYTHON_INSTALL(
|
||||
FILES
|
||||
__init__.py
|
||||
- DESTINATION ${GR_PYTHON_DIR}/op25_repeater
|
||||
+ DESTINATION ${OP25_PYTHON_DIR}/op25_repeater
|
||||
)
|
||||
|
||||
########################################################################
|
||||
diff --git a/op25/gr-op25_repeater/swig/CMakeLists.txt b/op25/gr-op25_repeater/swig/CMakeLists.txt
|
||||
index 1d88bbd..50819d7 100644
|
||||
--- a/op25/gr-op25_repeater/swig/CMakeLists.txt
|
||||
+++ b/op25/gr-op25_repeater/swig/CMakeLists.txt
|
||||
@@ -21,7 +21,7 @@
|
||||
# Include swig generation macros
|
||||
########################################################################
|
||||
find_package(SWIG)
|
||||
-find_package(PythonLibs 2)
|
||||
+find_package(PythonLibs 3)
|
||||
if(NOT SWIG_FOUND OR NOT PYTHONLIBS_FOUND)
|
||||
return()
|
||||
endif()
|
||||
@@ -31,9 +31,7 @@ include(GrPython)
|
||||
########################################################################
|
||||
# Setup swig generation
|
||||
########################################################################
|
||||
-foreach(incdir ${GNURADIO_RUNTIME_INCLUDE_DIRS})
|
||||
- list(APPEND GR_SWIG_INCLUDE_DIRS ${incdir}/gnuradio/swig)
|
||||
-endforeach(incdir)
|
||||
+set(GR_SWIG_INCLUDE_DIRS $<TARGET_PROPERTY:gnuradio::runtime_swig,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
set(GR_SWIG_LIBRARIES gnuradio-op25_repeater)
|
||||
set(GR_SWIG_DOC_FILE ${CMAKE_CURRENT_BINARY_DIR}/op25_repeater_swig_doc.i)
|
||||
@@ -44,7 +42,7 @@ GR_SWIG_MAKE(op25_repeater_swig op25_repeater_swig.i)
|
||||
########################################################################
|
||||
# Install the build swig module
|
||||
########################################################################
|
||||
-GR_SWIG_INSTALL(TARGETS op25_repeater_swig DESTINATION ${GR_PYTHON_DIR}/op25_repeater)
|
||||
+GR_SWIG_INSTALL(TARGETS op25_repeater_swig DESTINATION ${OP25_PYTHON_DIR}/op25_repeater)
|
||||
|
||||
########################################################################
|
||||
# Install swig .i files for development
|
|
@ -0,0 +1,72 @@
|
|||
#! /bin/bash
|
||||
|
||||
# op25 install script for debian based systems
|
||||
# including ubuntu 14/16 and raspbian
|
||||
#
|
||||
# *** this script for gnuradio 3.9 and 3.10 ***
|
||||
|
||||
TREE_DIR="$PWD/src" # directory in which our gr3.9 tree will be built
|
||||
|
||||
if [ ! -d op25/gr-op25 ]; then
|
||||
echo ====== error, op25 top level directories not found
|
||||
echo ====== you must change to the op25 top level directory
|
||||
echo ====== before running this script
|
||||
exit
|
||||
fi
|
||||
|
||||
TOP_DIR=$PWD
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get build-dep gnuradio
|
||||
sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python3-numpy python3-waitress python3-requests python3-pip pybind11-dev clang-format libsndfile1-dev
|
||||
|
||||
# setup and populate gr3.9 src tree
|
||||
echo
|
||||
echo " = = = = = = = generating source tree for gr3.9, this could take a while = = = = = = ="
|
||||
echo
|
||||
python3 add_gr3.9.py $PWD $TREE_DIR
|
||||
if [ ! -d $TREE_DIR ]; then
|
||||
echo ==== Error, directory $TREE_DIR creation failed, exiting
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd $TREE_DIR
|
||||
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ../op25/gr-op25
|
||||
make
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
cd ../
|
||||
|
||||
mkdir build_repeater
|
||||
cd build_repeater
|
||||
cmake ../op25/gr-op25_repeater
|
||||
make
|
||||
sudo make install
|
||||
sudo ldconfig
|
||||
cd ../
|
||||
|
||||
cd $TOP_DIR/op25
|
||||
sh ../scripts/do_sedpy.sh
|
||||
|
||||
echo ======
|
||||
echo ====== NOTICE
|
||||
echo ======
|
||||
echo ====== The gnuplot package is not installed by default here,
|
||||
echo ====== as its installation requires numerous prerequisite packages
|
||||
echo ====== that you may not want to install.
|
||||
echo ======
|
||||
echo ====== In order to do plotting in rx.py using the \-P option
|
||||
echo ====== you must install gnuplot, e.g., manually as follows:
|
||||
echo ======
|
||||
echo ====== sudo apt-get install gnuplot-x11
|
||||
echo ======
|
||||
echo ======
|
||||
echo ====== Separately, we suggest you set device and driver permissions:
|
||||
echo ====== \$ cd scripts
|
||||
echo ====== \$ ./udev_rules.sh
|
||||
echo ====== It is only necessary to do this once. Currently this script
|
||||
echo ====== handles the rtl-sdr and airspy only.
|
||||
echo ======
|
12
install.sh
|
@ -10,9 +10,12 @@ if [ ! -d op25/gr-op25 ]; then
|
|||
exit
|
||||
fi
|
||||
|
||||
sudo sed -i -- 's/^# *deb-src/deb-src/' /etc/apt/sources.list
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get build-dep gnuradio
|
||||
sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python-numpy python-waitress python-requests
|
||||
sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python3-numpy python3-waitress python3-requests
|
||||
sudo apt-get install liborc-dev
|
||||
|
||||
mkdir build
|
||||
cd build
|
||||
|
@ -33,3 +36,10 @@ echo ====== you must install gnuplot, e.g., manually as follows:
|
|||
echo ======
|
||||
echo ====== sudo apt-get install gnuplot-x11
|
||||
echo ======
|
||||
echo ======
|
||||
echo ====== Separately, we suggest you set device and driver permissions:
|
||||
echo ====== \$ cd scripts
|
||||
echo ====== \$ ./udev_rules.sh
|
||||
echo ====== It is only necessary to do this once. Currently this script
|
||||
echo ====== handles the rtl-sdr and airspy only.
|
||||
echo ======
|
||||
|
|
|
@ -63,6 +63,32 @@ if(NOT Boost_FOUND)
|
|||
message(FATAL_ERROR "Boost required to compile op25")
|
||||
endif()
|
||||
|
||||
########################################################################
|
||||
# Find gnuradio build dependencies
|
||||
########################################################################
|
||||
find_package(CppUnit)
|
||||
|
||||
set(ENABLE_PYTHON "TRUE" CACHE BOOL "enable python")
|
||||
cmake_policy(SET CMP0012 NEW)
|
||||
|
||||
# To run a more advanced search for GNU Radio and it's components and
|
||||
# versions, use the following. Add any components required to the list
|
||||
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
|
||||
# minimum API compatible version required.
|
||||
#
|
||||
# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...)
|
||||
# find_package(Gnuradio "version")
|
||||
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
|
||||
SET(MIN_GR_VERSION "3.8.0")
|
||||
find_package(Gnuradio REQUIRED)
|
||||
if("${Gnuradio_VERSION}" VERSION_LESS MIN_GR_VERSION)
|
||||
MESSAGE(FATAL_ERROR "GnuRadio version required: >=\"" ${MIN_GR_VERSION} "\" found: \"" ${Gnuradio_VERSION} "\"")
|
||||
endif()
|
||||
|
||||
if(NOT CPPUNIT_FOUND)
|
||||
message(FATAL_ERROR "CppUnit required to compile op25")
|
||||
endif()
|
||||
|
||||
########################################################################
|
||||
# Install directories
|
||||
########################################################################
|
||||
|
@ -80,28 +106,6 @@ set(GR_LIBEXEC_DIR libexec)
|
|||
set(GR_PKG_LIBEXEC_DIR ${GR_LIBEXEC_DIR}/${CMAKE_PROJECT_NAME})
|
||||
set(GRC_BLOCKS_DIR ${GR_PKG_DATA_DIR}/grc/blocks)
|
||||
|
||||
########################################################################
|
||||
# Find gnuradio build dependencies
|
||||
########################################################################
|
||||
find_package(CppUnit)
|
||||
|
||||
# To run a more advanced search for GNU Radio and it's components and
|
||||
# versions, use the following. Add any components required to the list
|
||||
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
|
||||
# minimum API compatible version required.
|
||||
#
|
||||
# set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER ...)
|
||||
# find_package(Gnuradio "version")
|
||||
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
|
||||
find_package(Gnuradio)
|
||||
|
||||
if(NOT GNURADIO_RUNTIME_FOUND)
|
||||
message(FATAL_ERROR "GnuRadio Runtime required to compile op25")
|
||||
endif()
|
||||
if(NOT CPPUNIT_FOUND)
|
||||
message(FATAL_ERROR "CppUnit required to compile op25")
|
||||
endif()
|
||||
|
||||
########################################################################
|
||||
# Setup the include and linker paths
|
||||
########################################################################
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/* -*- c++ -*- */
|
||||
/*
|
||||
* Copyright 2005,2013 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INCLUDED_OP25_MESSAGE_H
|
||||
#define INCLUDED_OP25_MESSAGE_H
|
||||
|
||||
#include <op25/api.h>
|
||||
#include <gnuradio/types.h>
|
||||
#include <string>
|
||||
|
||||
namespace gr {
|
||||
namespace op25 {
|
||||
|
||||
/*!
|
||||
* \brief Message class.
|
||||
*
|
||||
* \ingroup misc
|
||||
* The ideas and method names for adjustable message length were
|
||||
* lifted from the click modular router "Packet" class.
|
||||
*/
|
||||
class OP25_API message
|
||||
{
|
||||
public:
|
||||
typedef std::shared_ptr<message> sptr;
|
||||
|
||||
private:
|
||||
sptr d_next; // link field for msg queue
|
||||
long d_type; // type of the message
|
||||
double d_arg1; // optional arg1
|
||||
double d_arg2; // optional arg2
|
||||
|
||||
std::vector<unsigned char> d_buf;
|
||||
unsigned char* d_msg_start; // where the msg starts
|
||||
unsigned char* d_msg_end; // one beyond end of msg
|
||||
|
||||
message(long type, double arg1, double arg2, size_t length);
|
||||
|
||||
friend class msg_queue;
|
||||
|
||||
unsigned char* buf_data() { return d_buf.data(); }
|
||||
size_t buf_len() const { return d_buf.size(); }
|
||||
|
||||
public:
|
||||
/*!
|
||||
* \brief public constructor for message
|
||||
*/
|
||||
static sptr make(long type = 0, double arg1 = 0, double arg2 = 0, size_t length = 0);
|
||||
|
||||
static sptr make_from_string(const std::string s,
|
||||
long type = 0,
|
||||
double arg1 = 0,
|
||||
double arg2 = 0);
|
||||
|
||||
|
||||
~message();
|
||||
|
||||
long type() const { return d_type; }
|
||||
double arg1() const { return d_arg1; }
|
||||
double arg2() const { return d_arg2; }
|
||||
|
||||
void set_type(long type) { d_type = type; }
|
||||
void set_arg1(double arg1) { d_arg1 = arg1; }
|
||||
void set_arg2(double arg2) { d_arg2 = arg2; }
|
||||
|
||||
unsigned char* msg() const { return d_msg_start; }
|
||||
size_t length() const { return d_msg_end - d_msg_start; }
|
||||
std::string to_string() const;
|
||||
};
|
||||
|
||||
OP25_API long message_ncurrently_allocated();
|
||||
|
||||
} /* namespace op25 */
|
||||
} /* namespace gr */
|
||||
|
||||
#endif /* INCLUDED_OP25_MESSAGE_H */
|
|
@ -0,0 +1,39 @@
|
|||
/* -*- c++ -*- */
|
||||
/*
|
||||
* Copyright 2005,2013 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INCLUDED_OP25_MSG_HANDLER_H
|
||||
#define INCLUDED_OP25_MSG_HANDLER_H
|
||||
|
||||
#include <op25/api.h>
|
||||
#include <op25/message.h>
|
||||
|
||||
namespace gr {
|
||||
namespace op25 {
|
||||
|
||||
class msg_handler;
|
||||
typedef std::shared_ptr<msg_handler> msg_handler_sptr;
|
||||
|
||||
/*!
|
||||
* \brief abstract class of message handlers
|
||||
* \ingroup base
|
||||
*/
|
||||
class OP25_API msg_handler
|
||||
{
|
||||
public:
|
||||
virtual ~msg_handler();
|
||||
|
||||
//! handle \p msg
|
||||
virtual void handle(message::sptr msg) = 0;
|
||||
};
|
||||
|
||||
} /* namespace op25 */
|
||||
} /* namespace gr */
|
||||
|
||||
#endif /* INCLUDED_OP25_MSG_HANDLER_H */
|
|
@ -0,0 +1,85 @@
|
|||
/* -*- c++ -*- */
|
||||
/*
|
||||
* Copyright 2005,2009 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef INCLUDED_OP25_MSG_QUEUE_H
|
||||
#define INCLUDED_OP25_MSG_QUEUE_H
|
||||
|
||||
#include <op25/api.h>
|
||||
#include <op25/msg_handler.h>
|
||||
#include <gnuradio/thread/thread.h>
|
||||
|
||||
namespace gr {
|
||||
namespace op25 {
|
||||
|
||||
/*!
|
||||
* \brief thread-safe message queue
|
||||
* \ingroup misc
|
||||
*/
|
||||
class OP25_API msg_queue : public msg_handler
|
||||
{
|
||||
gr::thread::mutex d_mutex;
|
||||
gr::thread::condition_variable d_not_empty;
|
||||
gr::thread::condition_variable d_not_full;
|
||||
message::sptr d_head;
|
||||
message::sptr d_tail;
|
||||
unsigned int d_count; // # of messages in queue.
|
||||
unsigned int d_limit; // max # of messages in queue. 0 -> unbounded
|
||||
|
||||
public:
|
||||
typedef std::shared_ptr<msg_queue> sptr;
|
||||
|
||||
static sptr make(unsigned int limit = 0);
|
||||
|
||||
msg_queue(unsigned int limit);
|
||||
~msg_queue() override;
|
||||
|
||||
//! Generic msg_handler method: insert the message.
|
||||
void handle(message::sptr msg) override { insert_tail(msg); }
|
||||
|
||||
/*!
|
||||
* \brief Insert message at tail of queue.
|
||||
* \param msg message
|
||||
*
|
||||
* Block if queue if full.
|
||||
*/
|
||||
void insert_tail(message::sptr msg);
|
||||
|
||||
/*!
|
||||
* \brief Delete message from head of queue and return it.
|
||||
* Block if no message is available.
|
||||
*/
|
||||
message::sptr delete_head();
|
||||
|
||||
/*!
|
||||
* \brief If there's a message in the q, delete it and return it.
|
||||
* If no message is available, return 0.
|
||||
*/
|
||||
message::sptr delete_head_nowait();
|
||||
|
||||
//! Delete all messages from the queue
|
||||
void flush();
|
||||
|
||||
//! is the queue empty?
|
||||
bool empty_p() const { return d_count == 0; }
|
||||
|
||||
//! is the queue full?
|
||||
bool full_p() const { return d_limit != 0 && d_count >= d_limit; }
|
||||
|
||||
//! return number of messages in queue
|
||||
unsigned int count() const { return d_count; }
|
||||
|
||||
//! return limit on number of message in queue. 0 -> unbounded
|
||||
unsigned int limit() const { return d_limit; }
|
||||
};
|
||||
|
||||
} /* namespace op25 */
|
||||
} /* namespace gr */
|
||||
|
||||
#endif /* INCLUDED_OP25_MSG_QUEUE_H */
|
|
@ -63,7 +63,7 @@ list(APPEND op25_sources
|
|||
)
|
||||
|
||||
add_library(gnuradio-op25 SHARED ${op25_sources})
|
||||
target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} itpp pcap)
|
||||
target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} gnuradio::gnuradio-runtime itpp pcap)
|
||||
set_target_properties(gnuradio-op25 PROPERTIES DEFINE_SYMBOL "gnuradio_op25_EXPORTS")
|
||||
|
||||
########################################################################
|
||||
|
|
|
@ -12,7 +12,7 @@ class CryptoState
|
|||
{
|
||||
public:
|
||||
CryptoState() :
|
||||
kid(0), algid(0), mi(MESSAGE_INDICATOR_LENGTH)
|
||||
mi(MESSAGE_INDICATOR_LENGTH), kid(0), algid(0)
|
||||
{ }
|
||||
public:
|
||||
std::vector<uint8_t> mi;
|
||||
|
|
|
@ -261,13 +261,13 @@ namespace gr {
|
|||
}
|
||||
|
||||
void
|
||||
decoder_bf_impl::set_key(const key_type& key)
|
||||
decoder_bf_impl::set_key(const crypto_algorithm::key_type& key)
|
||||
{
|
||||
d_crypto_module->set_key(key);
|
||||
}
|
||||
|
||||
void
|
||||
decoder_bf_impl::set_key_map(const key_map_type& keys)
|
||||
decoder_bf_impl::set_key_map(const crypto_algorithm::key_map_type& keys)
|
||||
{
|
||||
d_crypto_module->set_key_map(keys);
|
||||
}
|
||||
|
|
|
@ -160,9 +160,9 @@ namespace gr {
|
|||
|
||||
void set_logging(bool verbose = true);
|
||||
|
||||
void set_key(const key_type& key);
|
||||
void set_key(const crypto_algorithm::key_type& key);
|
||||
|
||||
void set_key_map(const key_map_type& keys);
|
||||
void set_key_map(const crypto_algorithm::key_map_type& keys);
|
||||
};
|
||||
} // namespace op25
|
||||
} // namespace gr
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
#include <stdio.h>
|
||||
#include <gnuradio/io_signature.h>
|
||||
#include <boost/scoped_array.hpp>
|
||||
#include "fsk4_demod_ff_impl.h"
|
||||
|
||||
/*
|
||||
|
@ -186,7 +187,7 @@ namespace gr {
|
|||
gr::io_signature::make(1, 1, sizeof(float)),
|
||||
gr::io_signature::make(1, 1, sizeof(float))),
|
||||
d_block_rate(sample_rate_Hz / symbol_rate_Hz),
|
||||
d_history(new float[NTAPS]),
|
||||
my_d_history(new float[NTAPS]),
|
||||
d_history_last(0),
|
||||
d_queue(queue),
|
||||
d_symbol_clock(0.0),
|
||||
|
@ -196,7 +197,7 @@ namespace gr {
|
|||
fine_frequency_correction = 0.0;
|
||||
coarse_frequency_correction = 0.0;
|
||||
|
||||
std::fill(&d_history[0], &d_history[NTAPS], 0.0);
|
||||
std::fill(&my_d_history[0], &my_d_history[NTAPS], 0.0);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -269,7 +270,7 @@ namespace gr {
|
|||
{
|
||||
d_symbol_clock += d_symbol_time;
|
||||
|
||||
d_history[d_history_last++] = input;
|
||||
my_d_history[d_history_last++] = input;
|
||||
d_history_last %= NTAPS;
|
||||
|
||||
if(d_symbol_clock > 1.0) {
|
||||
|
@ -296,8 +297,8 @@ namespace gr {
|
|||
double interp = 0.0;
|
||||
double interp_p1 = 0.0;
|
||||
for(size_t i = 0, j = d_history_last; i < NTAPS; ++i) {
|
||||
interp += TAPS[imu][i] * d_history[j];
|
||||
interp_p1 += TAPS[imu_p1][i] * d_history[j];
|
||||
interp += TAPS[imu][i] * my_d_history[j];
|
||||
interp_p1 += TAPS[imu_p1][i] * my_d_history[j];
|
||||
j = (j + 1) % NTAPS;
|
||||
}
|
||||
#else
|
||||
|
@ -306,8 +307,8 @@ namespace gr {
|
|||
double interp_p1 = 0.0;
|
||||
for(int i=0; i<NTAPS; i++)
|
||||
{
|
||||
interp += TAPS[imu ][i] * d_history[j];
|
||||
interp_p1 += TAPS[imu_p1][i] * d_history[j];
|
||||
interp += TAPS[imu ][i] * my_d_history[j];
|
||||
interp_p1 += TAPS[imu_p1][i] * my_d_history[j];
|
||||
j = (j+1) % NTAPS;
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -33,7 +33,7 @@ namespace gr {
|
|||
{
|
||||
private:
|
||||
const float d_block_rate;
|
||||
boost::scoped_array<float> d_history;
|
||||
boost::scoped_array<float> my_d_history;
|
||||
size_t d_history_last;
|
||||
gr::msg_queue::sptr d_queue;
|
||||
double d_symbol_clock;
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/* -*- c++ -*- */
|
||||
/*
|
||||
* Copyright 2005 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <gnuradio/message.h>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
|
||||
namespace gr {
|
||||
namespace op25 {
|
||||
|
||||
static long s_ncurrently_allocated = 0;
|
||||
|
||||
message::sptr message::make(long type, double arg1, double arg2, size_t length)
|
||||
{
|
||||
return message::sptr(new message(type, arg1, arg2, length));
|
||||
}
|
||||
|
||||
message::sptr
|
||||
message::make_from_string(const std::string s, long type, double arg1, double arg2)
|
||||
{
|
||||
message::sptr m = message::make(type, arg1, arg2, s.size());
|
||||
memcpy(m->msg(), s.data(), s.size());
|
||||
return m;
|
||||
}
|
||||
|
||||
message::message(long type, double arg1, double arg2, size_t length)
|
||||
: d_type(type), d_arg1(arg1), d_arg2(arg2), d_buf(length)
|
||||
{
|
||||
if (length == 0)
|
||||
d_msg_start = d_msg_end = nullptr;
|
||||
else {
|
||||
d_msg_start = d_buf.data();
|
||||
d_msg_end = d_msg_start + length;
|
||||
}
|
||||
s_ncurrently_allocated++;
|
||||
}
|
||||
|
||||
message::~message()
|
||||
{
|
||||
assert(d_next == 0);
|
||||
s_ncurrently_allocated--;
|
||||
}
|
||||
|
||||
std::string message::to_string() const
|
||||
{
|
||||
return std::string((char*)d_msg_start, length());
|
||||
}
|
||||
|
||||
long message_ncurrently_allocated() { return s_ncurrently_allocated; }
|
||||
|
||||
} /* namespace op25 */
|
||||
} /* namespace gr */
|
|
@ -0,0 +1,23 @@
|
|||
/* -*- c++ -*- */
|
||||
/*
|
||||
* Copyright 2005,2013 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <op25/msg_handler.h>
|
||||
|
||||
namespace gr {
|
||||
namespace op25 {
|
||||
|
||||
msg_handler::~msg_handler() {}
|
||||
|
||||
} /* namespace op25 */
|
||||
} /* namespace gr */
|
|
@ -0,0 +1,113 @@
|
|||
/* -*- c++ -*- */
|
||||
/*
|
||||
* Copyright 2005,2009,2013 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <gnuradio/msg_queue.h>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace gr {
|
||||
namespace op25 {
|
||||
|
||||
msg_queue::sptr msg_queue::make(unsigned int limit)
|
||||
{
|
||||
return msg_queue::sptr(new msg_queue(limit));
|
||||
}
|
||||
|
||||
msg_queue::msg_queue(unsigned int limit)
|
||||
: d_not_empty(),
|
||||
d_not_full(),
|
||||
/*d_head(0), d_tail(0),*/ d_count(0),
|
||||
d_limit(limit)
|
||||
{
|
||||
}
|
||||
|
||||
msg_queue::~msg_queue() { flush(); }
|
||||
|
||||
void msg_queue::insert_tail(message::sptr msg)
|
||||
{
|
||||
if (msg->d_next)
|
||||
throw std::invalid_argument("gr::msg_queue::insert_tail: msg already in queue");
|
||||
|
||||
gr::thread::scoped_lock guard(d_mutex);
|
||||
|
||||
while (full_p())
|
||||
d_not_full.wait(guard);
|
||||
|
||||
if (d_tail == 0) {
|
||||
d_tail = d_head = msg;
|
||||
// msg->d_next = 0;
|
||||
msg->d_next.reset();
|
||||
} else {
|
||||
d_tail->d_next = msg;
|
||||
d_tail = msg;
|
||||
// msg->d_next = 0;
|
||||
msg->d_next.reset();
|
||||
}
|
||||
d_count++;
|
||||
d_not_empty.notify_one();
|
||||
}
|
||||
|
||||
message::sptr msg_queue::delete_head()
|
||||
{
|
||||
gr::thread::scoped_lock guard(d_mutex);
|
||||
message::sptr m;
|
||||
|
||||
while ((m = d_head) == 0)
|
||||
d_not_empty.wait(guard);
|
||||
|
||||
d_head = m->d_next;
|
||||
if (d_head == 0) {
|
||||
// d_tail = 0;
|
||||
d_tail.reset();
|
||||
}
|
||||
|
||||
d_count--;
|
||||
// m->d_next = 0;
|
||||
m->d_next.reset();
|
||||
d_not_full.notify_one();
|
||||
return m;
|
||||
}
|
||||
|
||||
message::sptr msg_queue::delete_head_nowait()
|
||||
{
|
||||
gr::thread::scoped_lock guard(d_mutex);
|
||||
message::sptr m;
|
||||
|
||||
if ((m = d_head) == 0) {
|
||||
// return 0;
|
||||
return message::sptr();
|
||||
}
|
||||
|
||||
d_head = m->d_next;
|
||||
if (d_head == 0) {
|
||||
// d_tail = 0;
|
||||
d_tail.reset();
|
||||
}
|
||||
|
||||
d_count--;
|
||||
// m->d_next = 0;
|
||||
m->d_next.reset();
|
||||
d_not_full.notify_one();
|
||||
return m;
|
||||
}
|
||||
|
||||
void msg_queue::flush()
|
||||
{
|
||||
message::sptr m;
|
||||
|
||||
while ((m = delete_head_nowait()) != 0)
|
||||
;
|
||||
}
|
||||
|
||||
} /* namespace op25 */
|
||||
} /* namespace gr */
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
/*
|
||||
* Copyright 2008 Steve Glass
|
||||
* Copyright 2018 Matt Ames
|
||||
* Copyright 2022 Matt Ames
|
||||
*
|
||||
* This file is part of OP25.
|
||||
*
|
||||
|
@ -63,29 +63,31 @@ const value_string ALGIDS[] = {
|
|||
{ 0x84, "AES-256-OFB" },
|
||||
{ 0x85, "AES-128-ECB"},
|
||||
{ 0x88, "AES-CBC"},
|
||||
{ 0x89, "AES-128-OFB"},
|
||||
/* Motorola proprietary - some of these have been observed over the air,
|
||||
some have been taken from firmware dumps on various devices, others
|
||||
have come from the TIA's FTP website while it was still public,
|
||||
from document "ALGID Guide 2015-04-15.pdf", and others have been
|
||||
have been worked out with a little bit of guesswork */
|
||||
have been worked out with a little bit of "guesswork" ;) */
|
||||
{ 0x9F, "Motorola DES-XL 56-bit key" },
|
||||
{ 0xA0, "Motorola DVI-XL" },
|
||||
{ 0xA1, "Motorola DVP-XL" },
|
||||
{ 0xA2, "Motorola DVI-SPFL"},
|
||||
{ 0xA3, "Motorola Assigned - Unknown" },
|
||||
{ 0xA2, "Motorola DVI-XL-SPFL"},
|
||||
{ 0xA3, "Motorola HAYSTACK" },
|
||||
{ 0xA4, "Motorola Assigned - Unknown" },
|
||||
{ 0xA5, "Motorola Assigned - Unknown" },
|
||||
{ 0xA6, "Motorola Assigned - Unknown" },
|
||||
{ 0xA7, "Motorola Assigned - Unknown" },
|
||||
{ 0xA8, "Motorola Assigned - Unknown" },
|
||||
{ 0xA9, "Motorola Assigned - Unknown" },
|
||||
{ 0xAA, "Motorola ADP 40 bit RC4" },
|
||||
{ 0xAA, "Motorola ADP (40 bit RC4)" },
|
||||
{ 0xAB, "Motorola CFX-256" },
|
||||
{ 0xAC, "Motorola Assigned - Unknown" },
|
||||
{ 0xAD, "Motorola Assigned - Unknown" },
|
||||
{ 0xAC, "Motorola GOST 28147-89 (RFC 5830)" },
|
||||
{ 0xAD, "Motorola Assigned - LOCALIZED" },
|
||||
{ 0xAE, "Motorola Assigned - Unknown" },
|
||||
{ 0xAF, "Motorola AES-256-GCM (possibly)" },
|
||||
{ 0xAF, "Motorola AES+" },
|
||||
{ 0xB0, "Motorola DVP"},
|
||||
{ 0xD0, "Motorola LOCAL_BR"}
|
||||
};
|
||||
const size_t ALGIDS_SZ = sizeof(ALGIDS) / sizeof(ALGIDS[0]);
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ endif()
|
|||
GR_PYTHON_INSTALL(
|
||||
FILES
|
||||
__init__.py
|
||||
DESTINATION ${GR_PYTHON_DIR}/op25
|
||||
DESTINATION ${OP25_PYTHON_DIR}/op25
|
||||
)
|
||||
|
||||
########################################################################
|
||||
|
|
|
@ -31,9 +31,9 @@ try:
|
|||
from dl import RTLD_GLOBAL as _RTLD_GLOBAL
|
||||
except ImportError:
|
||||
try:
|
||||
from DLFCN import RTLD_GLOBAL as _RTLD_GLOBAL
|
||||
from DLFCN import RTLD_GLOBAL as _RTLD_GLOBAL
|
||||
except ImportError:
|
||||
pass
|
||||
pass
|
||||
|
||||
if _RTLD_GLOBAL != 0:
|
||||
_dlopenflags = sys.getdlopenflags()
|
||||
|
@ -42,7 +42,7 @@ if _RTLD_GLOBAL != 0:
|
|||
|
||||
|
||||
# import swig generated symbols into the op25 namespace
|
||||
from op25_swig import *
|
||||
from .op25_swig import *
|
||||
|
||||
# import any pure python here
|
||||
#
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2022 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
/***********************************************************************************/
|
||||
/* This file is automatically generated using bindtool and can be manually edited */
|
||||
/* The following lines can be configured to regenerate this file during cmake */
|
||||
/* If manual edits are made, the following tags should be modified accordingly. */
|
||||
/* BINDTOOL_GEN_AUTOMATIC(0) */
|
||||
/* BINDTOOL_USE_PYGCCXML(0) */
|
||||
/* BINDTOOL_HEADER_FILE(message.h) */
|
||||
/* BINDTOOL_HEADER_FILE_HASH(e324acfee988515a91a4759680dbabbf) */
|
||||
/***********************************************************************************/
|
||||
|
||||
#include <pybind11/complex.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
#include <gnuradio/op25/message.h>
|
||||
// pydoc.h is automatically generated in the build directory
|
||||
#include <message_pydoc.h>
|
||||
|
||||
void bind_message(py::module& m)
|
||||
{
|
||||
|
||||
using message = ::gr::op25::message;
|
||||
|
||||
|
||||
py::class_<message,
|
||||
std::shared_ptr<message>>(m, "message", D(message))
|
||||
|
||||
.def(py::init(&message::make),
|
||||
py::arg("type") = 0,
|
||||
py::arg("arg1") = 0,
|
||||
py::arg("arg2") = 0,
|
||||
py::arg("length") = 0,
|
||||
D(message,make)
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.def_static("make_from_string",&message::make_from_string,
|
||||
py::arg("s"),
|
||||
py::arg("type") = 0,
|
||||
py::arg("arg1") = 0,
|
||||
py::arg("arg2") = 0,
|
||||
D(message,make_from_string)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("type",&message::type,
|
||||
D(message,type)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("arg1",&message::arg1,
|
||||
D(message,arg1)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("arg2",&message::arg2,
|
||||
D(message,arg2)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("set_type",&message::set_type,
|
||||
py::arg("type"),
|
||||
D(message,set_type)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("set_arg1",&message::set_arg1,
|
||||
py::arg("arg1"),
|
||||
D(message,set_arg1)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("set_arg2",&message::set_arg2,
|
||||
py::arg("arg2"),
|
||||
D(message,set_arg2)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("msg",&message::msg,
|
||||
D(message,msg)
|
||||
)
|
||||
|
||||
|
||||
|
||||
//.def("to_string",&message::to_string,
|
||||
// D(message,to_string)
|
||||
//)
|
||||
.def("to_string",
|
||||
[](std::shared_ptr<message> msg) {
|
||||
std::string s = msg->to_string();
|
||||
return py::bytes(s); // Return the data without transcoding
|
||||
})
|
||||
|
||||
;
|
||||
|
||||
|
||||
|
||||
m.def("message_ncurrently_allocated",&::gr::op25::message_ncurrently_allocated,
|
||||
D(message_ncurrently_allocated)
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright 2022 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
/***********************************************************************************/
|
||||
/* This file is automatically generated using bindtool and can be manually edited */
|
||||
/* The following lines can be configured to regenerate this file during cmake */
|
||||
/* If manual edits are made, the following tags should be modified accordingly. */
|
||||
/* BINDTOOL_GEN_AUTOMATIC(0) */
|
||||
/* BINDTOOL_USE_PYGCCXML(0) */
|
||||
/* BINDTOOL_HEADER_FILE(msg_handler.h) */
|
||||
/* BINDTOOL_HEADER_FILE_HASH(668ae41ad9b9d463886fcf60a87e9ede) */
|
||||
/***********************************************************************************/
|
||||
|
||||
#include <pybind11/complex.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
#include <gnuradio/op25/msg_handler.h>
|
||||
// pydoc.h is automatically generated in the build directory
|
||||
#include <msg_handler_pydoc.h>
|
||||
|
||||
void bind_msg_handler(py::module& m)
|
||||
{
|
||||
|
||||
using msg_handler = ::gr::op25::msg_handler;
|
||||
|
||||
|
||||
py::class_<msg_handler,
|
||||
std::shared_ptr<msg_handler>>(m, "msg_handler", D(msg_handler))
|
||||
|
||||
// .def(py::init<>(),D(msg_handler,msg_handler,0))
|
||||
// .def(py::init<gr::op25::msg_handler const &>(), py::arg("arg0"),
|
||||
// D(msg_handler,msg_handler,1)
|
||||
// )
|
||||
|
||||
|
||||
|
||||
.def("handle",&msg_handler::handle,
|
||||
py::arg("msg"),
|
||||
D(msg_handler,handle)
|
||||
)
|
||||
|
||||
;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright 2022 Free Software Foundation, Inc.
|
||||
*
|
||||
* This file is part of GNU Radio
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
*/
|
||||
|
||||
/***********************************************************************************/
|
||||
/* This file is automatically generated using bindtool and can be manually edited */
|
||||
/* The following lines can be configured to regenerate this file during cmake */
|
||||
/* If manual edits are made, the following tags should be modified accordingly. */
|
||||
/* BINDTOOL_GEN_AUTOMATIC(0) */
|
||||
/* BINDTOOL_USE_PYGCCXML(0) */
|
||||
/* BINDTOOL_HEADER_FILE(msg_queue.h) */
|
||||
/* BINDTOOL_HEADER_FILE_HASH(3f70adbde5e636fca8dc78e0505b06fd) */
|
||||
/***********************************************************************************/
|
||||
|
||||
#include <pybind11/complex.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
#include <gnuradio/op25/msg_queue.h>
|
||||
// pydoc.h is automatically generated in the build directory
|
||||
#include <msg_queue_pydoc.h>
|
||||
|
||||
void bind_msg_queue(py::module& m)
|
||||
{
|
||||
|
||||
using msg_queue = ::gr::op25::msg_queue;
|
||||
|
||||
|
||||
py::class_<msg_queue,
|
||||
std::shared_ptr<msg_queue>>(m, "msg_queue", D(msg_queue))
|
||||
|
||||
.def(py::init(&msg_queue::make),
|
||||
py::arg("limit") = 0,
|
||||
D(msg_queue,make)
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.def("handle",&msg_queue::handle,
|
||||
py::arg("msg"),
|
||||
D(msg_queue,handle)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("insert_tail",&msg_queue::insert_tail,
|
||||
py::arg("msg"),
|
||||
D(msg_queue,insert_tail)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("delete_head",&msg_queue::delete_head, py::call_guard<py::gil_scoped_release>(),
|
||||
D(msg_queue,delete_head)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("delete_head_nowait",&msg_queue::delete_head_nowait,
|
||||
D(msg_queue,delete_head_nowait)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("flush",&msg_queue::flush,
|
||||
D(msg_queue,flush)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("empty_p",&msg_queue::empty_p,
|
||||
D(msg_queue,empty_p)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("full_p",&msg_queue::full_p,
|
||||
D(msg_queue,full_p)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("count",&msg_queue::count,
|
||||
D(msg_queue,count)
|
||||
)
|
||||
|
||||
|
||||
|
||||
.def("limit",&msg_queue::limit,
|
||||
D(msg_queue,limit)
|
||||
)
|
||||
|
||||
;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
# Include swig generation macros
|
||||
########################################################################
|
||||
find_package(SWIG)
|
||||
find_package(PythonLibs 2)
|
||||
find_package(PythonLibs 3)
|
||||
if(NOT SWIG_FOUND OR NOT PYTHONLIBS_FOUND)
|
||||
return()
|
||||
endif()
|
||||
|
@ -31,9 +31,7 @@ include(GrPython)
|
|||
########################################################################
|
||||
# Setup swig generation
|
||||
########################################################################
|
||||
foreach(incdir ${GNURADIO_RUNTIME_INCLUDE_DIRS})
|
||||
list(APPEND GR_SWIG_INCLUDE_DIRS ${incdir}/gnuradio/swig)
|
||||
endforeach(incdir)
|
||||
set(GR_SWIG_INCLUDE_DIRS $<TARGET_PROPERTY:gnuradio::runtime_swig,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
set(GR_SWIG_LIBRARIES gnuradio-op25)
|
||||
set(GR_SWIG_DOC_FILE ${CMAKE_CURRENT_BINARY_DIR}/op25_swig_doc.i)
|
||||
|
@ -44,7 +42,7 @@ GR_SWIG_MAKE(op25_swig op25_swig.i)
|
|||
########################################################################
|
||||
# Install the build swig module
|
||||
########################################################################
|
||||
GR_SWIG_INSTALL(TARGETS op25_swig DESTINATION ${GR_PYTHON_DIR}/op25)
|
||||
GR_SWIG_INSTALL(TARGETS op25_swig DESTINATION ${OP25_PYTHON_DIR}/op25)
|
||||
|
||||
########################################################################
|
||||
# Install swig .i files for development
|
||||
|
|
|
@ -63,6 +63,31 @@ if(NOT Boost_FOUND)
|
|||
message(FATAL_ERROR "Boost required to compile op25_repeater")
|
||||
endif()
|
||||
|
||||
########################################################################
|
||||
# Find gnuradio build dependencies
|
||||
########################################################################
|
||||
find_package(CppUnit)
|
||||
|
||||
set(ENABLE_PYTHON "TRUE" CACHE BOOL "enable python")
|
||||
cmake_policy(SET CMP0012 NEW)
|
||||
|
||||
# To run a more advanced search for GNU Radio and it's components and
|
||||
# versions, use the following. Add any components required to the list
|
||||
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
|
||||
# minimum API compatible version required.
|
||||
#
|
||||
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
|
||||
# find_package(Gnuradio "version")
|
||||
set(MIN_GR_VERSION "3.8.0")
|
||||
find_package(Gnuradio REQUIRED)
|
||||
if("${Gnuradio_VERSION}" VERSION_LESS MIN_GR_VERSION)
|
||||
MESSAGE(FATAL_ERROR "GnuRadio version required: >=\"" ${MIN_GR_VERSION} "\" found: \"" ${Gnuradio_VERSION} "\"")
|
||||
endif()
|
||||
|
||||
if(NOT CPPUNIT_FOUND)
|
||||
message(FATAL_ERROR "CppUnit required to compile op25_repeater")
|
||||
endif()
|
||||
|
||||
########################################################################
|
||||
# Install directories
|
||||
########################################################################
|
||||
|
@ -80,27 +105,6 @@ set(GR_LIBEXEC_DIR libexec)
|
|||
set(GR_PKG_LIBEXEC_DIR ${GR_LIBEXEC_DIR}/${CMAKE_PROJECT_NAME})
|
||||
set(GRC_BLOCKS_DIR ${GR_PKG_DATA_DIR}/grc/blocks)
|
||||
|
||||
########################################################################
|
||||
# Find gnuradio build dependencies
|
||||
########################################################################
|
||||
find_package(CppUnit)
|
||||
|
||||
# To run a more advanced search for GNU Radio and it's components and
|
||||
# versions, use the following. Add any components required to the list
|
||||
# of GR_REQUIRED_COMPONENTS (in all caps) and change "version" to the
|
||||
# minimum API compatible version required.
|
||||
#
|
||||
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
|
||||
# find_package(Gnuradio "version")
|
||||
find_package(Gnuradio)
|
||||
|
||||
if(NOT GNURADIO_RUNTIME_FOUND)
|
||||
message(FATAL_ERROR "GnuRadio Runtime required to compile op25_repeater")
|
||||
endif()
|
||||
if(NOT CPPUNIT_FOUND)
|
||||
message(FATAL_ERROR "CppUnit required to compile op25_repeater")
|
||||
endif()
|
||||
|
||||
########################################################################
|
||||
# Setup the include and linker paths
|
||||
########################################################################
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
"Sysname" "Control Channel List" "Offset" "NAC" "Modulation" "TGID Tags File" "Whitelist" "Blacklist" "Center Frequency"
|
||||
"Fake" "924.975" "0" "0x293" "FSK4" "" "" "" "924.95"
|
|
|
@ -37,6 +37,7 @@ should open. You must click on the terminal window to restore it to
|
|||
focus, otherwise all keystrokes are consumed by gnuplot. Once in the
|
||||
terminal window there are several keyboard commands:
|
||||
h - hold
|
||||
H - hold/goto the specified tgid
|
||||
l - lockout
|
||||
s - skip
|
||||
q - quit program
|
||||
|
|
|
@ -0,0 +1,291 @@
|
|||
|
||||
New features in this release (June, 2021)
|
||||
=========================================
|
||||
|
||||
1. With thanks to OP25 user Triptolemus, the web client is enhanced to
|
||||
include comprehensive logs of recent control channel signalling and
|
||||
call activity. Many other features are also added:
|
||||
* unit ID (subscriber ID) tagging - similar to the existing TGID
|
||||
tags setup.
|
||||
* tag color coding (for both TGID and SUID tags).
|
||||
* tag ranges and wildcarding - for both the TGID and SUID tag maps,
|
||||
a single definition line may be used to create tags for a range of
|
||||
IDs.
|
||||
* real time system frequency status table
|
||||
* smart colors
|
||||
* user settings (colors, preferences) may be edited and saved via a
|
||||
convenient set of web forms and applications
|
||||
|
||||
2. The multi_rx app adds extensions to include trunked P25 call following
|
||||
concurrent with full-time tracking of one or more P25 control channels.
|
||||
If necessary, additional SDR devices may be configured to allow full
|
||||
coverage of all control channels without loss of CC data even during voice
|
||||
call reception. Several new command line options to multi_rx have been
|
||||
added - -T (trunking TSV file) -l (terminal type) as well as -X and -U,
|
||||
all having the same meaning as in rx.py.
|
||||
|
||||
3. Control channel logging to SQL database is added. For details see the
|
||||
section on the Flask Datatables App, below.
|
||||
|
||||
4. Experimental TDMA Control Channel support
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
First locate and change to your current OP25 install build/ directory and
|
||||
run the command
|
||||
sude make uninstall
|
||||
|
||||
Since this version includes library C++ code updates it requires a full
|
||||
source rebuild via the standard install script (install.sh).
|
||||
|
||||
The installation will include one or more SDR receivers, depending
|
||||
on the the amount of spectrum utilized by the target trunking system, how
|
||||
many control channels are to be monitored concurrently, and whether voice
|
||||
call following is desired.
|
||||
|
||||
* When SQL logging is used, it is most desirable to keep the control channel
|
||||
tuned in 100% of the time. With a single SDR this is not possible when the
|
||||
range of control channel and voice channel frequencies exceed the tuning band
|
||||
of the SDR.
|
||||
* When voice call following is to be used, a separate voice channel must be
|
||||
defined for each device over which voice reception is desired. It is
|
||||
redundant to have more than one voice channel assigned to a given device.
|
||||
* A separate SDR can be dedicated to voice call following if needed. If there
|
||||
is already a frequency-locked ("tunable"=false) device whose tuning band
|
||||
includes all desired voice frequencies, a separate voice SDR is not needed.
|
||||
* This version of OP25 follows the same voice call system as in rx.py.
|
||||
That is, a single call at a time is monitored and a 3-second (nominal)
|
||||
time delay is applied at the end of each call to catch possible replies.
|
||||
* A single device may be shared by multiple channels. When more than one channel
|
||||
is assigned to a device, the device should be tuned to a fixed frequency and
|
||||
"tunable" should be set to "false".
|
||||
|
||||
Simplified example: Of all frequencies (control and voice) in the system,
|
||||
the lowest frequency is 464.05 and the highest is 464.6. An RTL-SDR having
|
||||
a maximum sample rate of 2.56 MHz is to be used. Since the band required is
|
||||
0.55 MHz, a single SDR configuration can be used. The sample rate for
|
||||
this example, 2.56 MHz, could be reduced to 1.0 MHz to conserve CPU.
|
||||
|
||||
NOTE: Proper logging of CC activity requires two things:
|
||||
1) Device and/or channel resources must be allocated so that there
|
||||
is 100% time coverage of the control channel. Voice channel
|
||||
operation on the same SDR can only occur when the entire system
|
||||
fits within the SDR tuning band.
|
||||
2) Control channel reception and demodulation must be 100% error-free.
|
||||
Occasional errors are potentially corrected by the FEC but a better
|
||||
course is to increase the receive SNR and/or decrease the system BER.
|
||||
|
||||
Notes on JSON Configuration/Parameters
|
||||
======================================
|
||||
Example json config files are included in the apps/ directory. You
|
||||
should choose one of these files (as described above) and make edits
|
||||
to a working copy of the file. The name of the resulting JSON config
|
||||
file must be passed to multi_rx.py via the "-c" parameter.
|
||||
cfg-trunk.json - When all system frequencies (CC and VC) will fit
|
||||
within the SDR tuning band (without retuning the SDR),
|
||||
or voice decode is not needed.
|
||||
cfg-trunk2.json - When two SDRs are needed to cover both CC and all VCs.
|
||||
cfg-trunkx.json - Large system example with voice following and several CCs.
|
||||
|
||||
There are several key values to note:
|
||||
"tunable" In the single-SDR configuration where all system frequencies
|
||||
(primary/secondary CCs and VCs) are within the SDR band,
|
||||
you should set this to "false". In this case the SDR is
|
||||
fixed-tuned and remains on a single frequency, the center
|
||||
frequency. You must set the center frequency to a value
|
||||
halfway between the lowest and highest frequencies in the
|
||||
system, via the device "frequency" setting.
|
||||
"frequency" See above. When "tunable" is set to "true" this value must
|
||||
be filled in. Otherwise the value is used to set the device
|
||||
frequency at startup time (must be a valid frequency for the
|
||||
device). The device will most likely be retuned one or more
|
||||
times during execution.
|
||||
"decode" Assists multi_rx in assigning channels to the proper device(s).
|
||||
If the value of "decode" starts with the string "p25_decoder",
|
||||
multi_rx uses the p25 decoder instead of its standard decoder.
|
||||
|
||||
Note that "tunable" is a device-specific parameter, and that "decode" is a
|
||||
channel-specific parameter. Also, while both the device and channel define
|
||||
the "frequency" parameter, the description above is for device entries. A
|
||||
channel entry may also define a frequency, but the channel "frequency" parameter
|
||||
is ignored (in this version).
|
||||
|
||||
When the p25_decoder is used, there is a parameter string consisting of a
|
||||
colon-separated list of parameters with each parameter in the form "key=value",
|
||||
with the parameter string defined as the value of the "decode" parameter.
|
||||
|
||||
Here are two examples:
|
||||
"decode": "p25_decoder:role=cc:dev=rtl11:nac=0x4e1", [control]
|
||||
"decode": "p25_decoder:role=vc:dev=rtl12_vc", [voice]
|
||||
The valid parameter keywords are:
|
||||
"p25_decoder" Required for trunked P25. This keyword introduces the
|
||||
parameter list. There is no value.
|
||||
"role" Must be set to "vc" or "cc".
|
||||
"dev" Must be set to the name of the device. Each channel is
|
||||
assigned to exactly one device.
|
||||
"nac" Comma-separated list of NACs for the channel. Only trunked
|
||||
systems having a NAC in the list can be assigned to this
|
||||
channel.
|
||||
"sysid" Comma-separated list of SYSIDs for the channel. Only trunked
|
||||
systems having a SYSID in the list can be assigned to this
|
||||
channel.
|
||||
|
||||
The "nac" and "sysid" options are only checked for control channels ("role=cc").
|
||||
Values starting with "0x" are hexadecimal; otherwise decimal values are assumed .
|
||||
A blank/default value for "sysid" and/or "nac" indicates that parameter is not
|
||||
checked.
|
||||
|
||||
The following startup messages in the stderr log are typical in a 2-SDR config:
|
||||
assigning channel "p25 control channel" (channel id 1) to device "rtl11_cc"
|
||||
assigning channel "p25 voice channel" (channel id 2) to device "rtl12_vc"
|
||||
Note that the channel ID displayed in the "tuning error +/-1200" messages can be
|
||||
linked to the specific device(s) encountering the error using this ID.
|
||||
|
||||
Experimental TDMA Control Channel Support
|
||||
=========================================
|
||||
|
||||
The following specifics detail the JSON configuration file channel parameters
|
||||
needed to define a TDMA control channel:
|
||||
"demod_type": "cqpsk",
|
||||
"if_rate": 24000,
|
||||
"symbol_rate": 6000,
|
||||
"decode": "p25_decoder:role=cc:dev=<device-name>:nac=0x4e1",
|
||||
The NAC should be changed to match that of the system being received, and
|
||||
<device-name> should refer to the assigned device.
|
||||
|
||||
Colors and Tags for Talkgroup and Radio IDs
|
||||
===========================================
|
||||
Tags and colors are defined in two TSV files, one for TGIDs and one for SUIDs.
|
||||
The TSV file format, compatible with earlier versions of OP25 has the TAB
|
||||
separated columns defined as:
|
||||
column one: decimal TG or SU ID. May contain wildcards (see below)
|
||||
column two: tag text (string)
|
||||
column three(optional): encoded priority/color value, decimal (see below)
|
||||
The color code is directly mapped by client JS into style sheet (CSS) colors.
|
||||
If only two columns are present the third column is defaulted to zero.
|
||||
|
||||
The file names of the two files are specified (comma-separated) in the
|
||||
trunking TSV "TGID Tags File" column (the trunking TSV in turn is the
|
||||
file referred to by the "-T" command option of rx.py or multi_rx.py).
|
||||
The talkgroup tags file name is specified first, followed by a comma,
|
||||
then the SUID tags file. The SUID tags file can't be specified alone.
|
||||
|
||||
Wildcard IDs (column one) may be (for example)
|
||||
* 123-678 [all IDs in range, inclusive, are set to same tag/color]
|
||||
* 444.... [all IDs from 4440000 to 4449999]
|
||||
* 456* [all IDs starting with 456]
|
||||
* 54321 [defines that one ID]
|
||||
|
||||
Column three contains a color value from 0-99 (decimal).
|
||||
In the TGID file (only), the column value also contains a talkgroup
|
||||
priority, encoded as follows:
|
||||
- the low-order two decimal digits (tens and units digits) are the
|
||||
color code
|
||||
- the remaining upper-order decimal digits (hundreds digit and above) are
|
||||
the priority value for talkgroup pre-emption purposes.
|
||||
|
||||
Setup SQL Log Database (Optional)
|
||||
=================================
|
||||
|
||||
This addition provides a permanent server-side log of control channel
|
||||
activity via logging to an SQL database. See the next section for details
|
||||
on installing and using the log viewer.
|
||||
|
||||
1. Make sure that sqlite3 is installed in python
|
||||
|
||||
WARNING: OP25 MUST NOT BE RUNNING DURING THIS STEP
|
||||
2. Initialize DB (any existing DB data will be destroyed)
|
||||
op25/.../apps$ python sql_dbi.py reset_db
|
||||
WARNING: OP25 MUST NOT BE RUNNING DURING THIS STEP
|
||||
|
||||
3. Import talkgroups tags file
|
||||
op25/.../apps$ python sql_dbi.py import_tgid tags.tsv <sysid>
|
||||
also, import the radio ID tags file (optional)
|
||||
op25/.../apps$ python sql_dbi.py import_unit radio-tags.tsv <sysid>
|
||||
import the System ID tags file (see below)
|
||||
op25/.../apps$ python sql_dbi.py import_sysid sysid-tags.tsv 0
|
||||
|
||||
The sysid tags must be a TSV file containing two columns
|
||||
column 1 is the P25 trunked sysid (int, decimal)
|
||||
colunn 2 is the System Name (text)
|
||||
(Note: there is no header row line in this TSV file).
|
||||
|
||||
NOTE: in the various import commands above, the sysid (decimal) must follow
|
||||
as the next argument after the TSV file name. For the sysid tags file, the
|
||||
sysid should be set to zero.
|
||||
|
||||
4. Run op25 as usual. Logfile data should be inserted into DB in real time
|
||||
and you should be able to view activity via the OP25 http console (once
|
||||
the flask/datatables app has been set up; see next section).
|
||||
|
||||
Setup Flask Datatables App
|
||||
==========================
|
||||
|
||||
0. The DB must first be established (see previous section)
|
||||
|
||||
1. Install the necessary libs. If you are running the install in Ubuntu
|
||||
16.04 there are two lines in the script that must be un-commented prior
|
||||
to running; then, in any case do:
|
||||
op25/.../apps$ sh install-sql.sh
|
||||
|
||||
Note: you may need to 'sudo apt install git' prior to running this script.
|
||||
|
||||
2. Update your .bashrc file as instructed, then re-login to pick up the
|
||||
update to PATH. Verify that the updated PATH is correct. You can run
|
||||
the command "echo $PATH" to display its current value. Here is an example
|
||||
response: /home/op25/.local/bin:/usr/local/sbin:/usr/local/bin.....
|
||||
You should confirm that the file "flask" exists and is executable (see
|
||||
warning below).
|
||||
|
||||
$ ls -l ~/.local/bin/flask
|
||||
-rwxr-xr-x 1 op25 op25 212 Apr 29 21:43 /home/op25/.local/bin/flask
|
||||
|
||||
3. First change to the "..../apps/oplog" directory, then run the following
|
||||
commands to start the flask http process (listens on port 5000)
|
||||
|
||||
op25/.../apps/oplog$ export FLASK_APP=op25
|
||||
op25/.../apps/oplog$ FLASK_DEBUG=1 flask run
|
||||
|
||||
WARNING: if you receive the following messages when attempting the "flask run"
|
||||
command
|
||||
-------------------------------------------------------------
|
||||
Command 'flask' not found, but can be installed with:
|
||||
sudo apt install python3-flask
|
||||
-------------------------------------------------------------
|
||||
most likely this indicates the PATH is not properly set up (see step 2).
|
||||
In this case we do NOT recommend that you attempt to install the apt version
|
||||
of flask. The install-sql.sh script (step 1, above) should have installed a
|
||||
version of flask in a directory such as:
|
||||
|
||||
$ ls -l ~/.local/bin/flask
|
||||
-rwxr-xr-x 1 op25 op25 212 Apr 29 21:43 /home/op25/.local/bin/flask
|
||||
|
||||
If install of the apt version of flask is attempted, it may result in an
|
||||
obsolete and/or incompatible flask version being installed.
|
||||
|
||||
NOTE: The following command example can be used when starting the oplog
|
||||
process as a system service:
|
||||
/home/<user>/op25/op25/gr-op25_repeater/apps/oplog/oplog.sh
|
||||
* Change <user> to match the username
|
||||
* Make appropriate corrections if the git repo is cloned into a different
|
||||
directory than in the command shown
|
||||
* Verify the resulting path and filename is correct.
|
||||
============ oplog.sh ============
|
||||
#! /bin/sh
|
||||
|
||||
export FLASK_APP=op25
|
||||
FLASK_DEBUG=1 flask run --host=0.0.0.0
|
||||
==================================
|
||||
|
||||
In lieu of setting the flask PATH (as per step 2, above) you could also
|
||||
specify it explicitly. In that case, replace the last line of oplog.sh as
|
||||
follows:
|
||||
FLASK_DEBUG=1 /home/<user>/.local/bin/flask run --host=0.0.0.0
|
||||
* Change <user> to match the username
|
||||
* Confirm the "flask" file is present in ..../bin/ and is executable
|
||||
* Set "host" to "127.0.0.1" to restrict HTTP connections to the local
|
||||
machine.
|
||||
* WARNING The web server is not security-hardened. It is not
|
||||
designed for exposure to public web-facing applications.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
Linking OP25 TX and RX December 2020
|
||||
===================================================================
|
||||
|
||||
The OP25 TX application can be configured to transmit one or more
|
||||
channels directly to one of the RX apps (rx.py or multi_rx.py) in
|
||||
each case by specifying a device args string of the form
|
||||
udp:host:port
|
||||
where host and port are the IP address and UDP port number,
|
||||
respectively. Typically, the TX is started first followed by
|
||||
the RX, but they can be started in any order. UDP port 25252 is
|
||||
used in the examples.
|
||||
|
||||
The sample RX script is in runudp.sh, and the sample TX json config
|
||||
is in cfg-900.json .
|
||||
|
||||
To start the TX
|
||||
cd .../op25/op25-gr_repeater/apps/tx
|
||||
./multi_tx.py -c ../cfg-900.json
|
||||
|
||||
The RX is started with
|
||||
cd .../op25/op25-gr_repeater/apps
|
||||
./runudp.sh
|
||||
|
||||
There is a 120KHz sized block of spectrum transmitted (SR=120000)
|
||||
which is treated as complex baseband with each side pretending that
|
||||
the spectrum is converted to/from the 900 MHz band. On the RX side
|
||||
any frequency-change requests are ignored; instead, a "center
|
||||
frequency" is defined. There are two channels (P25 trunk control
|
||||
channel and P25 voice channel) spaced at 50 KHz separation.
|
||||
|
||||
The TX reads two data files that must be created beforehand. To
|
||||
create the trunk control channel data symbol file, refer to
|
||||
tx/README-fakecc.txt
|
||||
An audio file (PCM/ rate 8000 / mono / signed int16) must also be
|
||||
created - this file will be real-time encoded with the voice codec
|
||||
and sent on the P25 voice channel. These two files must be defined
|
||||
in the cfg-900.json file (channel "source" keyword).
|
|
@ -0,0 +1,268 @@
|
|||
|
||||
OP25 HTTP live streaming December 2020
|
||||
=====================================================================
|
||||
|
||||
These hacks ("OP25-hls hacks") add a new option for audio reception
|
||||
and playback in OP25; namely, via an HTTP live stream to any remote
|
||||
client using a standard Web browser*. The web server software used
|
||||
(nginx) is industrial-strength and immediately scalable to dozens or
|
||||
hundreds of simultaneous remote users with zero added effort. More
|
||||
than one upstream source (in parallel) can be served simultaneously.
|
||||
|
||||
OP25's liquidsoap script is hacked to pipe output PCM audio data
|
||||
to ffmpeg, which also reads the www/images/status.png image file
|
||||
that makes up the video portion of the encoded live stream. The
|
||||
image png file is kept updated by rx.py.
|
||||
|
||||
The selection of ffmpeg codecs ("libx264" for video and "aac" for
|
||||
audio) allows us directly to send the encoded data stream from
|
||||
ffmpeg to the web server (nginx) utilizing RTMP. Individual
|
||||
MPEG-TS segments are stored as files in nginx web server URL-space,
|
||||
and served to web clients via standard HTTP GET requests. The
|
||||
hls.js package is used at the client.
|
||||
|
||||
The entire effort mostly involved assembling existing off-the-shelf
|
||||
building blocks. The ffmpeg package was built manually from source
|
||||
to enable the "libx264" codec, and a modified nginx config was
|
||||
used.
|
||||
|
||||
*the web browser must support the "MediaSource Extensions" API.
|
||||
All recent broswer versions should qualify.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
1. nginx installation
|
||||
|
||||
The libnginx-mod-rtmp package must be installed (in addition to
|
||||
nginx itself). You can copy the sample nginx configuration at the
|
||||
end of this README file to /etc/nginx/nginx.conf, followed by
|
||||
restarting the web server
|
||||
sudo systemctl stop nginx
|
||||
sudo systemctl start nginx
|
||||
With this configuration the web server should listen on HTTP port
|
||||
8081 and RTMP port 1935.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
2. ffmpeg installation
|
||||
git clone https://code.videolan.org/videolan/x264.git
|
||||
git clone https://git.ffmpeg.org/ffmpeg.git
|
||||
cd x264
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
cd ../ffmpeg
|
||||
./configure --enable-shared --enable-libx264 --enable-gpl
|
||||
make
|
||||
sudo make install
|
||||
|
||||
To confirm the installation run the "ffmpeg" command and
|
||||
verify the presence of "--enable-shared" and "--enable-libx264"
|
||||
in the "configuration:" section of the output.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
3. liquidsoap installation
|
||||
Both packages "liquidsoap" and "liquidsoap-plugin-all" were
|
||||
installed, but not tested whether the plugins are required for
|
||||
this application.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
4. nginx setup
|
||||
with the custom config installed as per step 1, copy the files
|
||||
from op25/gr_op25_repeater/www to /var/www/html as follows:
|
||||
|
||||
live.html
|
||||
live.m3u8
|
||||
hls.js
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
5. liquidsoap setup
|
||||
in the op25/gr_op25_repeater/apps directory, note the ffmpeg.liq
|
||||
script. Overall the filtering and buffering options should be
|
||||
similar to those in op25.liq. The default version of ffmpeg.liq
|
||||
should be OK for most uses.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
6. operation
|
||||
With OP25 rx.py started using the options -V -w (and -2 if using
|
||||
TDMA) and with ffmpeg.liq started (both from the apps directory),
|
||||
you should be able to connect to http://hostip:8081/live.html
|
||||
and click the Play button to begin. NOTE: See note E for more
|
||||
details about how to specify the value for 'hostip' in the above
|
||||
link.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
7. troubleshooting
|
||||
A. with the ffmpeg.liq script running ffmpeg should start sending
|
||||
rtmp data over port 1935 to nginx. You should see files
|
||||
start being populated in /var/www/html/hls/ .
|
||||
B. If /var/www/html/hls is empty, check ffmpeg message output
|
||||
for possible errors, and also check the nginx access and
|
||||
error logs. Note that the /var/www/html/hls directory should
|
||||
start receiving files a few seconds after ffmpeg.liq is started
|
||||
(regardless of whether OP25 is actively receiving a station,
|
||||
or is not receiving).
|
||||
C. js debug can be enabled for hls.js by editing that file as
|
||||
follows; locate the lines of code and change the "debug"
|
||||
setting to "true"
|
||||
|
||||
var hlsDefaultConfig = _objectSpread(_objectSpread({
|
||||
autoStartLoad: true,
|
||||
// used by stream-controller
|
||||
startPosition: -1,
|
||||
// used by stream-controller
|
||||
defaultAudioCodec: void 0,
|
||||
// used by stream-controller
|
||||
debug: true, ///// <<<=== change this line from
|
||||
///// "false" to "true"
|
||||
|
||||
D. after reloading the page and with the web browser js console
|
||||
opened (and with all message types enabled), debug messages
|
||||
should now start appearing in the console. As before another
|
||||
place to look for messages is in the nginx access and error
|
||||
logs.
|
||||
E. if you are doing heavy client-side debugging it may be helpful
|
||||
to obtain a copy of the hls.js distribution and to populate
|
||||
the hls.js.map file (with updated hls.js) in /var/www/html.
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
8. notes
|
||||
A. due to the propagation delay inherent in the streaming
|
||||
process, there is a latency of several seconds from when
|
||||
the transmissions are receieved before they are played in
|
||||
the remote web browser. OP25 attempts to keep the video
|
||||
and audio synchronized but the usual user controls (hold,
|
||||
lockout, etc). are not available (in this release) because
|
||||
the several-second delay could cause the commands to operate
|
||||
on stale talkgroup data (without additional work).
|
||||
B. in keeping with the current OP25 liquidsoap setup, the audio
|
||||
stream is converted to mono prior to streaming. It might be
|
||||
possible to retain the stereo data (in cases where the L and
|
||||
R channels contain separate information), but this has not
|
||||
been tested. The ffmpeg.liq script would need to be changed to
|
||||
use "output" instead of "mean(output)" and the ffmpeg script
|
||||
would need to change "-ac 1" to "-ac 2". In addition the
|
||||
options stereo=true and channels=2 would need to be set in the
|
||||
%wav specification parameters.
|
||||
C. multiple independent streams can be served simultaneously by
|
||||
invoking a separate ffmpeg.sh script for each stream and by
|
||||
changing the last component of the rtmp URL to a unique
|
||||
value; for example:
|
||||
rtmp://localhost/live/stream2
|
||||
A unified (parameterized) version of ffmpeg.sh could also be
|
||||
used.
|
||||
Also, new versions of live.html and live.m3u8 in /var/www/html
|
||||
(reflecting the above modification) would need to be added.
|
||||
D. note that pausing and seeking etc. in the media feed isn't
|
||||
possible when doing live streaming.
|
||||
E. when connecting from the remote client to the nginx server as
|
||||
detailed in step 6 (above) you should specify the value for
|
||||
'hostip' as follows (omitting the quotes):
|
||||
|
||||
'localhost' (default) - use this when the client is on the same
|
||||
machine as the server
|
||||
'host.ip.address' - specify the IP address of the server when
|
||||
the client and server machines are different, and the server
|
||||
does not have a DNS hostname.
|
||||
'hostname' - if the server has a DNS hostname, this name should
|
||||
be used in place of 'hostip'.
|
||||
|
||||
Note that in the second and third cases above you must also
|
||||
change the hostname from 'localhost' to the IP address or DNS
|
||||
hostname, respectively, in the following files
|
||||
live.html
|
||||
live.m3u8
|
||||
in /var/www/html (total three occurrences).
|
||||
|
||||
Similarly, if an IP port other than 8081 is to be used, the same
|
||||
updates as above must be made, and also the nginx conf file must
|
||||
be updated to reflect the changed port number.
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
########################################################################
|
||||
####### tested on ubuntu 18.04 #######
|
||||
####### sample nginx conf file - copy everything below this line #######
|
||||
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 768;
|
||||
# multi_accept on;
|
||||
}
|
||||
|
||||
# RTMP configuration
|
||||
rtmp {
|
||||
server {
|
||||
listen 1935; # Listen on standard RTMP port
|
||||
chunk_size 4000;
|
||||
|
||||
application live {
|
||||
live on;
|
||||
# Turn on HLS
|
||||
hls on;
|
||||
hls_path /var/www/html/hls/;
|
||||
hls_fragment 3;
|
||||
hls_playlist_length 60;
|
||||
# disable consuming the stream from nginx as rtmp
|
||||
deny play all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile off;
|
||||
tcp_nopush on;
|
||||
#aio on;
|
||||
directio 512;
|
||||
default_type application/octet-stream;
|
||||
|
||||
server {
|
||||
listen 8081;
|
||||
|
||||
location / {
|
||||
# Disable cache
|
||||
add_header 'Cache-Control' 'no-cache';
|
||||
|
||||
# CORS setup
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||
|
||||
# allow CORS preflight requests
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
# include /etc/nginx/mime.types;
|
||||
types {
|
||||
text/html html;
|
||||
text/css css;
|
||||
application/javascript js;
|
||||
application/dash+xml mpd;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
video/mp2t ts;
|
||||
}
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
root /var/www/html;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"label_color": "#000000",
|
||||
"tic_color": "#000000",
|
||||
"border_color": "#000000",
|
||||
"plot_color": "#c000ff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2017, 2018 Graham Norbury
|
||||
#
|
||||
|
@ -29,7 +29,8 @@ from optparse import OptionParser
|
|||
from sockaudio import socket_audio
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
audiothread.stop()
|
||||
sys.stderr.write("audio.py shutting down\n")
|
||||
audio_handler.stop()
|
||||
sys.exit(0)
|
||||
|
||||
parser = OptionParser()
|
||||
|
@ -38,16 +39,17 @@ parser.add_option("-H", "--host-ip", type="string", default="0.0.0.0", help="IP
|
|||
parser.add_option("-u", "--wireshark-port", type="int", default=23456, help="Wireshark port")
|
||||
parser.add_option("-2", "--two-channel", action="store_true", default=False, help="single or two channel audio")
|
||||
parser.add_option("-x", "--audio-gain", type="float", default="1.0", help="audio gain (default = 1.0)")
|
||||
parser.add_option("-s", "--stdout", action="store_true", default=False, help="write to stdout instead of audio device")
|
||||
parser.add_option("-S", "--silence", action="store_true", default=False, help="suppress output of zeros after timeout")
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
if len(args) != 0:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
audiothread = socket_audio(options.host_ip, options.wireshark_port, options.audio_output, options.two_channel, options.audio_gain)
|
||||
audio_handler = socket_audio(options.host_ip, options.wireshark_port, options.audio_output, options.two_channel, options.audio_gain, options.stdout, silent_flag=options.silence)
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
while True:
|
||||
time.sleep(1)
|
||||
audio_handler.run()
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"channels": [
|
||||
{
|
||||
"demod_type": "fsk4",
|
||||
"destination": "udp://127.0.0.1:56120",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rc",
|
||||
"frequency": 924975000,
|
||||
"if_rate": 24000,
|
||||
"name": "p25 control channel",
|
||||
"plot": "datascope",
|
||||
"source": "symbols:sym-cc925.dat",
|
||||
"symbol_rate": 4800
|
||||
},
|
||||
{
|
||||
"demod_type": "fsk4",
|
||||
"destination": "udp://127.0.0.1:56124",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rc",
|
||||
"frequency": 924925000,
|
||||
"if_rate": 24000,
|
||||
"name": "p25 voice channel",
|
||||
"plot": "datascope",
|
||||
"source": "/home/mhp/rand4.raw",
|
||||
"symbol_rate": 4800
|
||||
}
|
||||
],
|
||||
"devices": [
|
||||
{
|
||||
"args": "udp:127.0.0.1:25252",
|
||||
"frequency": 924950000,
|
||||
"gains": "",
|
||||
"name": "udp",
|
||||
"offset": 0,
|
||||
"ppm": 0,
|
||||
"rate": 120000,
|
||||
"tunable": false
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"channels": [
|
||||
{
|
||||
"demod_type": "cqpsk",
|
||||
"destination": "udp://127.0.0.1:56124",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rc",
|
||||
"frequency": 0,
|
||||
"if_rate": 24000,
|
||||
"name": "p25 control channel",
|
||||
"plot": "symbol",
|
||||
"decode": "p25_decoder:cc",
|
||||
"symbol_rate": 4800
|
||||
},
|
||||
{
|
||||
"demod_type": "cqpsk",
|
||||
"destination": "udp://127.0.0.1:23456",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rc",
|
||||
"frequency": 0,
|
||||
"if_rate": 24000,
|
||||
"name": "p25 voice channel",
|
||||
"plot": "symbol",
|
||||
"decode": "p25_decoder:vc",
|
||||
"symbol_rate": 4800
|
||||
}
|
||||
],
|
||||
"devices": [
|
||||
{
|
||||
"args": "rtl=1",
|
||||
"frequency": 454300000,
|
||||
"gains": "lna:49",
|
||||
"name": "rtl0",
|
||||
"offset": 0,
|
||||
"ppm": 54,
|
||||
"rate": 1000000,
|
||||
"tunable": false
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"channels": [
|
||||
{
|
||||
"demod_type": "cqpsk",
|
||||
"destination": "udp://127.0.0.1:56124",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rc",
|
||||
"frequency": 0,
|
||||
"if_rate": 24000,
|
||||
"name": "p25 control channel",
|
||||
"plot": "symbol",
|
||||
"decode": "p25_decoder:cc",
|
||||
"symbol_rate": 4800
|
||||
},
|
||||
{
|
||||
"demod_type": "cqpsk",
|
||||
"destination": "udp://127.0.0.1:23456",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rc",
|
||||
"frequency": 0,
|
||||
"if_rate": 24000,
|
||||
"name": "p25 voice channel",
|
||||
"plot": "symbol",
|
||||
"decode": "p25_decoder:vc",
|
||||
"symbol_rate": 4800
|
||||
}
|
||||
],
|
||||
"devices": [
|
||||
{
|
||||
"args": "rtl=00000011",
|
||||
"frequency": 460300000,
|
||||
"gains": "lna:49",
|
||||
"name": "rtl11_cc",
|
||||
"offset": 0,
|
||||
"ppm": 54,
|
||||
"rate": 1000000,
|
||||
"tunable": false
|
||||
},
|
||||
{
|
||||
"args": "rtl=00000012",
|
||||
"frequency": 453000000,
|
||||
"gains": "lna:49",
|
||||
"name": "rtl12_vc",
|
||||
"offset": 0,
|
||||
"ppm": 54,
|
||||
"rate": 1000000,
|
||||
"tunable": false
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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,76 @@
|
|||
{
|
||||
"channels": [
|
||||
{
|
||||
"demod_type": "fsk4",
|
||||
"destination": "udp://127.0.0.1:56124",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "nxdn",
|
||||
"frequency": 442112500,
|
||||
"if_rate": 24000,
|
||||
"name": "nxdn48",
|
||||
"plot": "datascope",
|
||||
"source": "/home/mhp/rand0.raw",
|
||||
"symbol_rate": 2400
|
||||
},
|
||||
{
|
||||
"demod_type": "fsk4",
|
||||
"destination": "udp://127.0.0.1:56128",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rrc",
|
||||
"frequency": 442187500,
|
||||
"if_rate": 24000,
|
||||
"name": "dmr",
|
||||
"plot": "datascope",
|
||||
"source": "/home/mhp/rand1.raw",
|
||||
"symbol_rate": 4800
|
||||
},
|
||||
{
|
||||
"demod_type": "fsk4",
|
||||
"destination": "udp://127.0.0.1:56132",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "gmsk",
|
||||
"frequency": 442262500,
|
||||
"if_rate": 24000,
|
||||
"name": "dstar",
|
||||
"plot": "datascope",
|
||||
"source": "/home/mhp/rand2.raw",
|
||||
"symbol_rate": 4800
|
||||
},
|
||||
{
|
||||
"demod_type": "fsk4",
|
||||
"destination": "udp://127.0.0.1:56136",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rrc",
|
||||
"frequency": 442337500,
|
||||
"if_rate": 24000,
|
||||
"name": "ysf",
|
||||
"plot": "datascope",
|
||||
"source": "/home/mhp/rand3.raw",
|
||||
"symbol_rate": 4800
|
||||
},
|
||||
{
|
||||
"demod_type": "fsk4",
|
||||
"destination": "udp://127.0.0.1:56120",
|
||||
"excess_bw": 0.2,
|
||||
"filter_type": "rc",
|
||||
"frequency": 442412500,
|
||||
"if_rate": 24000,
|
||||
"name": "p25",
|
||||
"plot": "datascope",
|
||||
"source": "/home/mhp/rand4.raw",
|
||||
"symbol_rate": 4800
|
||||
}
|
||||
],
|
||||
"devices": [
|
||||
{
|
||||
"args": "udp:127.0.0.1:25252",
|
||||
"frequency": 442262500,
|
||||
"gains": "",
|
||||
"name": "udp",
|
||||
"offset": 0,
|
||||
"ppm": 0,
|
||||
"rate": 480000,
|
||||
"tunable": false
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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,27 @@
|
|||
[
|
||||
{
|
||||
"fs":[ 1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, -1 ],
|
||||
"length":864,
|
||||
"name":"P25"
|
||||
},
|
||||
{
|
||||
"fs":[ -3, -3, 1, 1, -3, -3, 3, 3, -3, -3, -3, -3, 3, 3, 3, 3, -1, -1, 3, 3 ],
|
||||
"length":384,
|
||||
"name":"NXDN48"
|
||||
},
|
||||
{
|
||||
"fs":[ -3, 1, -3, 3, -3, -3, 3, 3, -1, 3 ],
|
||||
"length":192,
|
||||
"name":"NXDN96"
|
||||
},
|
||||
{
|
||||
"fs":[ 1, -1, 1, 1, 1, 1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, -1, 1, -1 ],
|
||||
"length":576,
|
||||
"name":"DMR"
|
||||
},
|
||||
{
|
||||
"fs":[ -3, 3, 3, 1, 3, -3, 1, 3, -3, 1, -1, 3, 3, -1, 1, -3, 3, 1, -3, 3 ],
|
||||
"length":480,
|
||||
"name":"YSF"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# (c) Copyright 2020, OP25
|
||||
#
|
||||
# This file is part of OP25 and part of GNU Radio
|
||||
#
|
||||
# OP25 is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
# License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OP25; see the file COPYING. If not, write to the Free
|
||||
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
|
||||
""" generate named image file consisting of multi-line text """
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
|
||||
_TTF_FILE = '/usr/share/fonts/truetype/freefont/FreeSerif.ttf'
|
||||
|
||||
def create_image(textlist=["Blank"], imgfile="test.png", bgcolor='red', fgcolor='black', windowsize=(400,300)):
|
||||
global _TTF_FILE
|
||||
width=windowsize[0]
|
||||
height=windowsize[1]
|
||||
|
||||
margin = 4
|
||||
if not os.access(_TTF_FILE, os.R_OK):
|
||||
font = ImageFont.load_default()
|
||||
else:
|
||||
font = ImageFont.truetype(_TTF_FILE, 16)
|
||||
img = Image.new('RGB', (width, height), bgcolor)
|
||||
draw = ImageDraw.Draw(img)
|
||||
cursor = 0
|
||||
for line in textlist:
|
||||
w,h = draw.textsize(line, font)
|
||||
# TODO: overwidth check needed?
|
||||
if cursor+h >= height:
|
||||
break
|
||||
draw.text((margin, cursor), line,'black',font)
|
||||
cursor += h + margin // 2
|
||||
|
||||
img.save(imgfile)
|
||||
|
||||
if __name__ == '__main__':
|
||||
s = []
|
||||
s.append('Starting...')
|
||||
|
||||
create_image(textlist=s, bgcolor='#c0c0c0')
|
|
@ -0,0 +1,383 @@
|
|||
#sql_dbi events map
|
||||
|
||||
events_map = {
|
||||
"grp_v_ch_grant_mbt": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'options'],
|
||||
['frequency', 'frequency'],
|
||||
['tgid', 'group'],
|
||||
['suid', 'srcaddr'],
|
||||
],
|
||||
"grg_exenc_cmd": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'mfrid'],
|
||||
['tgid', 'sg'],
|
||||
['p', 'keyid'],
|
||||
],
|
||||
"grp_v_ch_grant": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'mfrid'],
|
||||
['p', 'options'],
|
||||
['frequency', 'frequency'],
|
||||
['tgid', 'group'],
|
||||
['suid', 'srcaddr'],
|
||||
],
|
||||
"mot_grg_cn_grant": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'mfrid'],
|
||||
['frequency', 'frequency'],
|
||||
['tgid', 'sg'],
|
||||
['suid', 'sa'],
|
||||
],
|
||||
"grp_v_ch_grant_updt": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'mfrid'],
|
||||
['frequency', 'frequency1'],
|
||||
['tgid', 'group1'],
|
||||
],
|
||||
"grp_v_ch_grant_updt_exp": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'mfrid'],
|
||||
['p', 'options'],
|
||||
['frequency', 'frequency'],
|
||||
['tgid', 'group'],
|
||||
],
|
||||
"ack_resp_fne": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'aiv'],
|
||||
['p2', 'ex'],
|
||||
['p3', 'addl'],
|
||||
['wacn', 'wacn'],
|
||||
['suid', 'source'],
|
||||
['suid2', 'target'],
|
||||
],
|
||||
"deny_resp": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'aiv'],
|
||||
['p2', 'reason'],
|
||||
['p3', 'additional'],
|
||||
['suid', 'target'],
|
||||
],
|
||||
"grp_aff_resp": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'affiliation'],
|
||||
['p2', 'group_aff_value'],
|
||||
['tgid', 'announce_group'],
|
||||
['tgid2', 'group'],
|
||||
['suid', 'target'],
|
||||
],
|
||||
"grp_aff_q": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['suid', 'source'],
|
||||
['suid2', 'target'],
|
||||
],
|
||||
"loc_reg_resp": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'rv'],
|
||||
['p2', 'rfss'],
|
||||
['p3', 'siteid'],
|
||||
['tgid', 'group'],
|
||||
['suid', 'target'],
|
||||
],
|
||||
"u_reg_resp": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'rv'],
|
||||
['suid', 'source'],
|
||||
['suid2', 'target'],
|
||||
],
|
||||
"u_reg_cmd": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['suid', 'source'],
|
||||
['suid2', 'target'],
|
||||
],
|
||||
"u_de_reg_ack": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['wacn', 'wacn'],
|
||||
['suid', 'source'],
|
||||
],
|
||||
"ext_fnct_cmd": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'mfrid'],
|
||||
['p', 'efclass'],
|
||||
['p2', 'efoperand'],
|
||||
['suid', 'efargs'],
|
||||
],
|
||||
"end_call": [
|
||||
['time', 'time'],
|
||||
['sysid', 'sysid'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'code'],
|
||||
['suid', 'srcaddr'],
|
||||
['tgid', 'tgid'],
|
||||
['p2', 'duration'],
|
||||
['p3', 'count'],
|
||||
],
|
||||
}
|
||||
|
||||
# cc_event to numerical id (oplog and sql_dbi)
|
||||
cc_events = {
|
||||
"ack_resp_fne": 1,
|
||||
"deny_resp": 2,
|
||||
"end_call": 3,
|
||||
"ext_fnct_cmd": 4,
|
||||
"grg_exenc_cmd": 5,
|
||||
"grp_aff_q": 6,
|
||||
"grp_aff_resp": 7,
|
||||
"grp_v_ch_grant": 8,
|
||||
"grp_v_ch_grant_mbt": 9,
|
||||
"grp_v_ch_grant_updt": 10,
|
||||
"grp_v_ch_grant_updt_exp": 11,
|
||||
"loc_reg_resp": 12,
|
||||
"u_de_reg_ack": 13,
|
||||
"u_reg_cmd": 14,
|
||||
"u_reg_resp": 15,
|
||||
"mot_grg_cn_grant": 16,
|
||||
}
|
||||
|
||||
# sql column names to DataTables (Oplog)
|
||||
oplog_map = {
|
||||
"grp_v_ch_grant_mbt": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'MFRID'],
|
||||
['p', 'Options'],
|
||||
['frequency', 'Frequency'],
|
||||
['tgid', 'Talkgroup ID'],
|
||||
['tgid', 'Talkgroup'],
|
||||
['suid', 'Source ID'],
|
||||
['suid', 'Source'],
|
||||
],
|
||||
"grg_exenc_cmd": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'MFRID'],
|
||||
['tgid', 'SG (tgid)'],
|
||||
['p', 'Key ID'],
|
||||
],
|
||||
"grp_v_ch_grant": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'MFRID'],
|
||||
['p', 'Options'],
|
||||
['frequency', 'Frequency'],
|
||||
['tgid', 'Talkgroup ID'],
|
||||
['tgid', 'Talkgroup'],
|
||||
['suid', 'Source ID'],
|
||||
['suid', 'Source'],
|
||||
],
|
||||
"mot_grg_cn_grant": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'MFRID'],
|
||||
['frequency', 'Frequency'],
|
||||
['tgid', 'Talkgroup ID'],
|
||||
['tgid', 'Talkgroup'],
|
||||
['suid', 'Source ID'],
|
||||
['suid', 'Source'],
|
||||
],
|
||||
"grp_v_ch_grant_updt": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'MFRID'],
|
||||
['frequency', 'Frequency'],
|
||||
['tgid', 'Talkgroup ID'],
|
||||
['tgid', 'Talkgroup'],
|
||||
],
|
||||
"grp_v_ch_grant_updt_exp": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'mfrid'],
|
||||
['p', 'Options'],
|
||||
['frequency', 'Frequency'],
|
||||
['tgid', 'Talkgroup ID'],
|
||||
['tgid', 'Talkgroup'],
|
||||
],
|
||||
"ack_resp_fne": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'aiv'],
|
||||
['p2', 'ex'],
|
||||
['p3', 'Additional'],
|
||||
['wacn', 'wacn'],
|
||||
['suid', 'System Source'],
|
||||
['suid2', 'Target ID'],
|
||||
['suid2', 'Target'],
|
||||
],
|
||||
"deny_resp": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'aiv'],
|
||||
['p2', 'Reason'],
|
||||
['p3', 'Additional'],
|
||||
['suid', 'Target ID'],
|
||||
['suid', 'Target'],
|
||||
],
|
||||
"grp_aff_resp": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'Affiliation'],
|
||||
['p2', 'Group Aff Value'],
|
||||
['tgid', 'Announce Group'],
|
||||
['tgid2', 'Talkgroup ID'],
|
||||
['tgid2', 'Talkgroup'],
|
||||
['suid', 'Target ID'],
|
||||
['suid', 'Target'],
|
||||
],
|
||||
"grp_aff_q": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['suid', 'System Source'],
|
||||
['suid2', 'Target ID'],
|
||||
['suid2', 'Target'],
|
||||
],
|
||||
"loc_reg_resp": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'rv'],
|
||||
['p2', 'RFSS'],
|
||||
['p3', 'Site'],
|
||||
['tgid', 'Talkgroup ID'],
|
||||
['tgid', 'Talkgroup'],
|
||||
['suid', 'Target ID'],
|
||||
['suid', 'Target'],
|
||||
],
|
||||
"u_reg_resp": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'rv'],
|
||||
['suid', 'Source ID'],
|
||||
['suid', 'Source'],
|
||||
['suid2', 'Target'],
|
||||
],
|
||||
"u_reg_cmd": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['suid', 'System Source'],
|
||||
['suid2', 'Target ID'],
|
||||
['suid2', 'Target'],
|
||||
],
|
||||
"u_de_reg_ack": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['wacn', 'WACN'],
|
||||
['suid', 'Source ID'],
|
||||
['suid', 'Source'],
|
||||
],
|
||||
"ext_fnct_cmd": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['mfrid', 'MFRID'],
|
||||
['p', 'Class'],
|
||||
['p2', 'Operand'],
|
||||
['suid', 'System Source'],
|
||||
],
|
||||
"end_call": [
|
||||
['time', 'Time'],
|
||||
['sysid', 'System'],
|
||||
['opcode', 'opcode'],
|
||||
['cc_event', 'cc_event'],
|
||||
['p', 'Code'],
|
||||
['suid', 'Source ID'],
|
||||
['suid', 'Source'],
|
||||
['tgid', 'Talkgroup ID'],
|
||||
['tgid', 'Talkgroup'],
|
||||
['p2', 'Duration (ms)'],
|
||||
['p3', 'Count (p3)'],
|
||||
],
|
||||
}
|
||||
|
||||
# friendly long description strings, used in Oplog
|
||||
cc_desc = {
|
||||
"ack_resp_fne": "Acknowledge Response FNE - 0x20",
|
||||
"deny_resp": "Deny Response - 0x27",
|
||||
"end_call": "End Call (not a naitve control channel event)",
|
||||
"ext_fnct_cmd": "Extended Function Command - 0x24",
|
||||
"grg_exenc_cmd": "Harris Group Regroup Explicit Encryption Command - 0x30",
|
||||
"grp_aff_q": "Group Affiliation Query - 0x2A",
|
||||
"grp_aff_resp": "Group Affiliation Response - 0x28",
|
||||
"grp_v_ch_grant": "Group Voice Channel Grant - 0x00",
|
||||
"grp_v_ch_grant_mbt": "Group Voice Channel Grant, Multiple Block Trunking",
|
||||
"grp_v_ch_grant_updt": "Group Voice Channel Grant Update - 0x02",
|
||||
"grp_v_ch_grant_updt_exp": "Group Voice Channel Grant Update, Explicit - 0x03",
|
||||
"loc_reg_resp": "Location Registration Response 0x2B",
|
||||
"mot_grg_cn_grant": "Motorola Patch Channel Grant - 0x02",
|
||||
"u_de_reg_ack": "De-Registration Acknowledge (Logout) - 0x2F",
|
||||
"u_reg_cmd": "Unit Registration Command (Force Unit Registration) - 0x2D",
|
||||
"u_reg_resp": "Unit Registration Response - 0x2C"
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/liquidsoap
|
||||
|
||||
# Example liquidsoap streaming from op25 to icecast
|
||||
# (c) 2019, 2020 gnorbury@bondcar.com, wllmbecks@gmail.com
|
||||
#
|
||||
|
||||
set("log.stdout", true)
|
||||
set("log.file.path", "/home/pi/op25/op25/gr-op25_repeater/apps/liquidsoap.log")
|
||||
set("log.file", true)
|
||||
set("log.level", 3)
|
||||
|
||||
# Make the native sample rate compatible with op25
|
||||
set("frame.audio.samplerate", 8000)
|
||||
|
||||
# SOURCE INPUT BLOCK OPTIONS
|
||||
# Mono and stereo input sources are mutually exclusive. Choose type.
|
||||
|
||||
# Mono input source
|
||||
input = mksafe(input.external(buffer=0.25, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -x 2 -s"))
|
||||
# Consider increasing the buffer value on slow systems such as RPi3. e.g. buffer=0.25
|
||||
# Longer buffer results in less choppy audio but at the expense of increased latency.
|
||||
|
||||
# Left channel input source and audio summing for two-slot protocols
|
||||
#left = input.external(buffer=0.25, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -u 23450 -x 2 -s")
|
||||
#left = audio_to_stereo(left)
|
||||
#left = stereo.pan(pan=1., left)
|
||||
|
||||
# Right channel input source and audio summing for two-slot protocols
|
||||
#right = input.external(buffer=0.25, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -u 23460 -x 2 -s")
|
||||
#right = audio_to_stereo(right)
|
||||
#right = stereo.pan(pan=-1., right)
|
||||
|
||||
# OPTIONAL AUDIO SIGNAL PROCESSING BLOCKS
|
||||
# Uncomment to enable
|
||||
#
|
||||
# High pass filter mono
|
||||
input = filter.iir.butterworth.high(frequency = 200.0, order = 4, input)
|
||||
|
||||
# High pass filter stereo
|
||||
#left = filter.iir.butterworth.high(frequency = 200.0, order = 4, left)
|
||||
#right = filter.iir.butterworth.high(frequency = 200.0, order = 4, right)
|
||||
|
||||
# Low pass filter mono
|
||||
input = filter.iir.butterworth.low(frequency = 3250.0, order = 4, input)
|
||||
|
||||
# Low pass filter stereo
|
||||
#left = filter.iir.butterworth.low(frequency = 3250.0, order = 4, left)
|
||||
#right = filter.iir.butterworth.low(frequency = 3250.0, order = 4, right)
|
||||
|
||||
# Normalization mono
|
||||
input = normalize(input, gain_max = 3.0, gain_min = -3.0, target = -16.0, threshold = -40.0)
|
||||
|
||||
# Normalization stereo -- Note -- Adjust target gains independently to achieve left/right balance
|
||||
#left = normalize(left, gain_max = 3.0, gain_min = -3.0, target = -16.0, threshold = -40.0)
|
||||
#right = normalize(right, gain_max = 3.0, gain_min = -3.0, target = -16.0, threshold = -40.0)
|
||||
|
||||
# Commnent out the line below for "non-stereo" (mono) output
|
||||
#input = mksafe(add(normalize=false, [left,right]))
|
||||
|
||||
# LOCAL AUDIO OUTPUT
|
||||
# Uncomment the line below to enable local sound
|
||||
#output.ao(input)
|
||||
|
||||
# ICECAST STREAMING
|
||||
# Uncomment to enable output to an icecast server
|
||||
# Change the "host", "password", and "mount" strings appropriately first!
|
||||
# For metadata to work properly, the host address given here MUST MATCH the address in op25's meta.json file
|
||||
#
|
||||
# Mono Stream
|
||||
output.icecast(%mp3(bitrate=16, samplerate=22050, stereo=false), description="op25", genre="Public Safety", url="", fallible=false, icy_metadata="false", host="localhost", port=8000, mount="op25", password="hackme", mean(input))
|
||||
#
|
||||
# Stereo Stream
|
||||
#output.icecast(%mp3(bitrate=16, samplerate=22050, stereo=true), description="op25", genre="Public Safety", url="", fallible=false, icy_metadata="false", host="localhost", port=8000, mount="op25", password="hackme", input)
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/liquidsoap
|
||||
|
||||
# Example liquidsoap hls streaming from op25 to nginx
|
||||
# (c) 2019, 2020 gnorbury@bondcar.com, wllmbecks@gmail.com
|
||||
# (c) 2020 KA1RBI
|
||||
|
||||
set("log.stdout", true)
|
||||
set("log.file", false)
|
||||
set("log.level", 1)
|
||||
|
||||
set("frame.audio.samplerate", 8000)
|
||||
|
||||
input = mksafe(input.external(buffer=0.02, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -x 2 -s -S"))
|
||||
|
||||
output.external(%wav(stereo=false, channels=1, samplesize=16, header=false, samplerate=8000), fallible=false, flush=true, "./ffmpeg.sh", mean(input))
|
|
@ -0,0 +1,42 @@
|
|||
#! /bin/sh
|
||||
|
||||
# Copyright (c) 2020 OP25
|
||||
#
|
||||
# This file is part of OP25 and part of GNU Radio
|
||||
#
|
||||
# OP25 is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
# License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OP25; see the file COPYING. If not, write to the Free
|
||||
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
|
||||
#
|
||||
# this script should not be run directly - run ffmpeg.liq instead
|
||||
#
|
||||
# requires ffmpeg configured with --enable-libx264
|
||||
#
|
||||
|
||||
ffmpeg \
|
||||
-ar 8000 \
|
||||
-ac 1 \
|
||||
-acodec pcm_s16le \
|
||||
-f s16le \
|
||||
-i pipe:0 \
|
||||
-f image2 \
|
||||
-loop 1 \
|
||||
-i ../www/images/status.png \
|
||||
-vcodec libx264 \
|
||||
-pix_fmt yuv420p \
|
||||
-f flv \
|
||||
-acodec aac \
|
||||
-b:a 48k \
|
||||
rtmp://localhost/live/stream
|
|
@ -23,16 +23,20 @@ import sys
|
|||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import json
|
||||
import threading
|
||||
import glob
|
||||
|
||||
from gnuradio import gr, gru, eng_notation
|
||||
from gnuradio import gr, eng_notation
|
||||
from gnuradio import blocks, audio
|
||||
from gnuradio.eng_option import eng_option
|
||||
import numpy as np
|
||||
from gnuradio import gr
|
||||
from math import pi
|
||||
from math import pi, sin, cos
|
||||
|
||||
_def_debug = 0
|
||||
_def_sps = 10
|
||||
_def_cpm_mode = 'cpm'
|
||||
|
||||
GNUPLOT = '/usr/bin/gnuplot'
|
||||
|
||||
|
@ -54,8 +58,19 @@ def limit(a,lim):
|
|||
return lim
|
||||
return a
|
||||
|
||||
def ensure_str(s): # for python 2/3
|
||||
if isinstance(s[0], str):
|
||||
return s
|
||||
ns = ''
|
||||
for i in range(len(s)):
|
||||
ns += chr(s[i])
|
||||
return ns
|
||||
|
||||
PSEQ = 0
|
||||
|
||||
class wrap_gp(object):
|
||||
def __init__(self, sps=_def_sps, logfile=None):
|
||||
def __init__(self, sps=_def_sps, logfile=None, title="", color_cfg='plot-colors.json'):
|
||||
global PSEQ
|
||||
self.sps = sps
|
||||
self.center_freq = 0.0
|
||||
self.relative_freq = 0.0
|
||||
|
@ -65,7 +80,7 @@ class wrap_gp(object):
|
|||
self.freqs = ()
|
||||
self.avg_pwr = np.zeros(FFT_BINS)
|
||||
self.avg_sum_pwr = 0.0
|
||||
self.buf = []
|
||||
self.buf = np.array([])
|
||||
self.plot_count = 0
|
||||
self.last_plot = 0
|
||||
self.plot_interval = None
|
||||
|
@ -73,6 +88,25 @@ class wrap_gp(object):
|
|||
self.output_dir = None
|
||||
self.filename = None
|
||||
self.logfile = logfile
|
||||
self.title = title
|
||||
self.sequence_id = PSEQ
|
||||
PSEQ += 1
|
||||
x = self.sequence_id % 3
|
||||
y = self.sequence_id // 3
|
||||
self.position = (x, y)
|
||||
|
||||
self.colors = {}
|
||||
self.colors['label_color'] = ''
|
||||
self.colors['tic_color'] = ''
|
||||
self.colors['border_color'] = ''
|
||||
self.colors['plot_color'] = ''
|
||||
self.colors['background_color'] = ''
|
||||
if color_cfg and os.access(color_cfg, os.R_OK):
|
||||
ccfg = json.loads(open(color_cfg).read())
|
||||
for color in ccfg:
|
||||
self.colors[color] = ccfg[color]
|
||||
|
||||
self.next_cpmd = time.time()
|
||||
|
||||
self.attach_gp()
|
||||
|
||||
|
@ -106,81 +140,141 @@ class wrap_gp(object):
|
|||
def set_output_dir(self, v):
|
||||
self.output_dir = v
|
||||
|
||||
def set_sps(self, sps):
|
||||
self.sps = sps
|
||||
|
||||
def plot(self, buf, bufsz, mode='eye'):
|
||||
BUFSZ = bufsz
|
||||
consumed = min(len(buf), BUFSZ-len(self.buf))
|
||||
if len(self.buf) < BUFSZ:
|
||||
self.buf.extend(buf[:consumed])
|
||||
self.buf = np.concatenate((self.buf, buf[:int(consumed)]))
|
||||
if len(self.buf) < BUFSZ:
|
||||
return consumed
|
||||
|
||||
self.plot_count += 1
|
||||
if mode == 'eye' and self.plot_count % 20 != 0:
|
||||
self.buf = []
|
||||
self.buf = np.array([])
|
||||
return consumed
|
||||
|
||||
plots = []
|
||||
s = ''
|
||||
plot_size = (320,240)
|
||||
while(len(self.buf)):
|
||||
if mode == 'eye':
|
||||
if len(self.buf) < self.sps:
|
||||
if mode == 'eye':
|
||||
nplots = len(self.buf) // self.sps - 2
|
||||
for i in range(nplots):
|
||||
s += '\n'.join(['%f' % self.buf[i*self.sps+j] for j in range(self.sps+1)])
|
||||
s += '\ne\n'
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'cpm':
|
||||
nplots = len(self.buf)
|
||||
ab = np.abs(self.buf)
|
||||
for i in range(len(ab)):
|
||||
s += '%f\n' % ab[i]
|
||||
s += 'e\n'
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'cpmd':
|
||||
if time.time() < self.next_cpmd:
|
||||
self.buf = np.array([])
|
||||
return 0
|
||||
self.next_cpmd = time.time() + 0.5
|
||||
ab = np.abs(self.buf)
|
||||
thresh = np.max(ab) / 2.0
|
||||
mask1 = np.array(ab > thresh, dtype=np.int)
|
||||
maskdf = mask1[1:] - mask1[:-1]
|
||||
nz = np.array(np.nonzero(maskdf), dtype=np.int)[0]
|
||||
nzd = nz[1:]-nz[:-1]
|
||||
nzdn = nzd // self.sps
|
||||
sel = (nzdn > 165) & (nzdn < 185)
|
||||
valid = np.array(np.nonzero(sel), dtype=np.int)[0]
|
||||
if len(valid) < 1:
|
||||
self.buf = np.array([])
|
||||
return 0
|
||||
v0 = valid[0]
|
||||
i0 = nz[v0]
|
||||
samples = self.buf[i0 : i0+170*self.sps+2]
|
||||
fmd = np.angle(samples[1:] * np.conj(samples[:-1]))
|
||||
fmd = fmd[12*self.sps:]
|
||||
for n in range(0,len(fmd),self.sps):
|
||||
sl = fmd[n:n+self.sps+1]
|
||||
if len(sl) != self.sps+1:
|
||||
break
|
||||
for i in range(self.sps):
|
||||
s += '%f\n' % self.buf[i]
|
||||
s += 'e\n'
|
||||
self.buf=self.buf[self.sps:]
|
||||
s += '\n'.join(['%f' % (x*self.sps) for x in sl])
|
||||
s += '\ne\n'
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'constellation':
|
||||
plot_size = (240,240)
|
||||
self.buf = self.buf[:100]
|
||||
for b in self.buf:
|
||||
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
|
||||
s += 'e\n'
|
||||
plots.append('"-" with points')
|
||||
for b in self.buf:
|
||||
#s += '%f\t%f\n' % (b.real, b.imag)
|
||||
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
|
||||
elif mode == 'constellation':
|
||||
plot_size = (240,240)
|
||||
self.buf = self.buf[:100]
|
||||
for b in self.buf:
|
||||
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
|
||||
s += 'e\n'
|
||||
plots.append('"-" with points')
|
||||
for b in self.buf:
|
||||
#s += '%f\t%f\n' % (b.real, b.imag)
|
||||
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
|
||||
s += 'e\n'
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'symbol':
|
||||
for b in self.buf:
|
||||
s += '%f\n' % (b)
|
||||
s += 'e\n'
|
||||
plots.append('"-" with points')
|
||||
elif mode == 'fftf':
|
||||
self.ffts = np.fft.rfft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ)
|
||||
#self.ffts = np.fft.fftshift(self.ffts)
|
||||
self.ffts = np.abs(self.ffts) ** 2.0
|
||||
self.ffts /= np.max(self.ffts)
|
||||
for i in range(len(self.ffts)):
|
||||
s += '%f\n' % (self.ffts[i])
|
||||
s += 'e\n'
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'fft' or mode == 'mixer':
|
||||
sum_pwr = 0.0
|
||||
self.ffts = np.fft.fft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ)
|
||||
self.ffts = np.fft.fftshift(self.ffts)
|
||||
self.freqs = np.fft.fftfreq(len(self.ffts))
|
||||
self.freqs = np.fft.fftshift(self.freqs)
|
||||
tune_freq = (self.center_freq - self.relative_freq) / 1e6
|
||||
if self.center_freq and self.width:
|
||||
self.freqs = ((self.freqs * self.width) + self.center_freq + self.offset_freq) / 1e6
|
||||
for i in range(len(self.ffts)):
|
||||
if mode == 'fft':
|
||||
self.avg_pwr[i] = ((1.0 - FFT_AVG) * self.avg_pwr[i]) + (FFT_AVG * np.abs(self.ffts[i]))
|
||||
else:
|
||||
self.avg_pwr[i] = ((1.0 - MIX_AVG) * self.avg_pwr[i]) + (MIX_AVG * np.abs(self.ffts[i]))
|
||||
s += '%f\t%f\n' % (self.freqs[i], 20 * np.log10(self.avg_pwr[i]))
|
||||
if (mode == 'mixer') and (self.avg_pwr[i] > 1e-5):
|
||||
if (self.freqs[i] - self.center_freq) < 0:
|
||||
sum_pwr -= self.avg_pwr[i]
|
||||
elif (self.freqs[i] - self.center_freq) > 0:
|
||||
sum_pwr += self.avg_pwr[i]
|
||||
self.avg_sum_pwr = ((1.0 - BAL_AVG) * self.avg_sum_pwr) + (BAL_AVG * sum_pwr)
|
||||
s += 'e\n'
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'float' or mode == 'correlation':
|
||||
for b in self.buf:
|
||||
s += '%f\n' % (b)
|
||||
s += 'e\n'
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'sync':
|
||||
s_abs = np.abs(self.buf)
|
||||
sums = np.zeros(self.sps)
|
||||
for i in range(self.sps):
|
||||
sums[i] = np.sum(s_abs[range(i, len(self.buf), self.sps)])
|
||||
am = np.argmax(sums)
|
||||
samples = self.buf[am:]
|
||||
|
||||
a1 = -np.angle(samples[0])
|
||||
rz = cos(a1) + 1j * sin(a1)
|
||||
|
||||
while len(samples) >= self.sps+1:
|
||||
for i in range(self.sps+1):
|
||||
z = samples[i] * rz
|
||||
s += '%f\t%f\n' % (z.real, z.imag)
|
||||
s += 'e\n'
|
||||
self.buf = []
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'symbol':
|
||||
for b in self.buf:
|
||||
s += '%f\n' % (b)
|
||||
s += 'e\n'
|
||||
self.buf = []
|
||||
plots.append('"-" with points')
|
||||
elif mode == 'fft' or mode == 'mixer':
|
||||
sum_pwr = 0.0
|
||||
self.ffts = np.fft.fft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ)
|
||||
self.ffts = np.fft.fftshift(self.ffts)
|
||||
self.freqs = np.fft.fftfreq(len(self.ffts))
|
||||
self.freqs = np.fft.fftshift(self.freqs)
|
||||
tune_freq = (self.center_freq - self.relative_freq) / 1e6
|
||||
if self.center_freq and self.width:
|
||||
self.freqs = ((self.freqs * self.width) + self.center_freq + self.offset_freq) / 1e6
|
||||
for i in xrange(len(self.ffts)):
|
||||
if mode == 'fft':
|
||||
self.avg_pwr[i] = ((1.0 - FFT_AVG) * self.avg_pwr[i]) + (FFT_AVG * np.abs(self.ffts[i]))
|
||||
else:
|
||||
self.avg_pwr[i] = ((1.0 - MIX_AVG) * self.avg_pwr[i]) + (MIX_AVG * np.abs(self.ffts[i]))
|
||||
s += '%f\t%f\n' % (self.freqs[i], 20 * np.log10(self.avg_pwr[i]))
|
||||
if (mode == 'mixer') and (self.avg_pwr[i] > 1e-5):
|
||||
if (self.freqs[i] - self.center_freq) < 0:
|
||||
sum_pwr -= self.avg_pwr[i]
|
||||
elif (self.freqs[i] - self.center_freq) > 0:
|
||||
sum_pwr += self.avg_pwr[i]
|
||||
self.avg_sum_pwr = ((1.0 - BAL_AVG) * self.avg_sum_pwr) + (BAL_AVG * sum_pwr)
|
||||
s += 'e\n'
|
||||
self.buf = []
|
||||
plots.append('"-" with lines')
|
||||
elif mode == 'float':
|
||||
for b in self.buf:
|
||||
s += '%f\n' % (b)
|
||||
s += 'e\n'
|
||||
self.buf = []
|
||||
plots.append('"-" with lines')
|
||||
self.buf = []
|
||||
plots.append('"-" with linespoints')
|
||||
samples = samples[self.sps:]
|
||||
|
||||
self.buf = np.array([])
|
||||
|
||||
# FFT processing needs to be completed to maintain the weighted average buckets
|
||||
# regardless of whether we actually produce a new plot or not.
|
||||
|
@ -191,19 +285,50 @@ class wrap_gp(object):
|
|||
filename = None
|
||||
if self.output_dir:
|
||||
if self.sequence >= 2:
|
||||
delete_pathname = '%s/plot-%s-%d.png' % (self.output_dir, mode, self.sequence-2)
|
||||
delete_pathname = '%s/plot-%s%d-%d.png' % (self.output_dir, mode, self.sequence_id, self.sequence-2)
|
||||
if os.access(delete_pathname, os.W_OK):
|
||||
os.remove(delete_pathname)
|
||||
h0= 'set terminal png size %d, %d\n' % (plot_size)
|
||||
filename = 'plot-%s-%d.png' % (mode, self.sequence)
|
||||
filename = 'plot-%s%d-%d.png' % (mode, self.sequence_id, self.sequence)
|
||||
h0 += 'set output "%s/%s"\n' % (self.output_dir, filename)
|
||||
self.sequence += 1
|
||||
else:
|
||||
h0= 'set terminal x11 noraise\n'
|
||||
pos = ''
|
||||
if self.position is not None:
|
||||
x = self.position[0] * plot_size[0]
|
||||
y = self.position[1] * plot_size[1]
|
||||
x += 50
|
||||
y += 75
|
||||
pos = ' position %d, %d' % (x, y)
|
||||
h0= 'set terminal x11 noraise size %d, %d%s title "%s"\n' % (plot_size[0], plot_size[1], pos, self.title)
|
||||
background = ''
|
||||
|
||||
label_color = ''
|
||||
tic_color = ''
|
||||
border_color = ''
|
||||
plot_color = ''
|
||||
background_color = ''
|
||||
|
||||
if self.colors['label_color']:
|
||||
label_color = 'textcolor rgb"%s"' % self.colors['label_color']
|
||||
if self.colors['tic_color']:
|
||||
tic_color = 'textcolor rgb"%s"' % self.colors['tic_color']
|
||||
if self.colors['border_color']:
|
||||
border_color = 'linecolor rgb"%s"' % self.colors['border_color']
|
||||
if self.colors['plot_color']:
|
||||
plot_color = 'linecolor rgb"%s"' % self.colors['plot_color']
|
||||
if self.colors['background_color']:
|
||||
background_color = 'fillcolor rgb"%s"' % self.colors['background_color']
|
||||
|
||||
background += 'set object 1 rectangle from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
|
||||
background += 'set xtics %s\n' % (tic_color)
|
||||
background += 'set ytics %s\n' % (tic_color)
|
||||
background += 'set border %s\n' % (border_color)
|
||||
|
||||
h = 'set key off\n'
|
||||
if mode == 'constellation':
|
||||
h+= background
|
||||
#h+= background
|
||||
plot_color = ''
|
||||
h+= 'set size square\n'
|
||||
h+= 'set xrange [-1:1]\n'
|
||||
h+= 'set yrange [-1:1]\n'
|
||||
|
@ -211,7 +336,9 @@ class wrap_gp(object):
|
|||
h += 'set polar\n'
|
||||
h += 'set angles degrees\n'
|
||||
h += 'unset raxis\n'
|
||||
h += 'set object circle at 0,0 size 1 fillcolor rgb 0x0f01 fillstyle solid behind\n'
|
||||
h += 'set object 1 rectangle from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
|
||||
h += 'set object 2 circle at 0,0 size 1 fillcolor rgb 0x0f01 fillstyle solid behind\n'
|
||||
h += 'set object 3 circle at 0,0 size 1 %s\n' % 'linecolor rgb"#0000f0"'
|
||||
h += 'set style line 10 lt 1 lc rgb 0x404040 lw 0.1\n'
|
||||
h += 'set grid polar 45\n'
|
||||
h += 'set grid ls 10\n'
|
||||
|
@ -223,41 +350,78 @@ 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"\n'
|
||||
h+= 'set title "Constellation %s" %s\n' % (self.title, label_color)
|
||||
elif mode == 'eye':
|
||||
h+= background
|
||||
h+= 'set yrange [-4:4]\n'
|
||||
h+= 'set title "Datascope"\n'
|
||||
h+= 'set title "Datascope %s" %s\n' % (self.title, label_color)
|
||||
plot_color = ''
|
||||
elif mode == 'cpm':
|
||||
h+= background
|
||||
#h+= 'set yrange [-4:4]\n'
|
||||
h+= 'set title "CPM RSSI %s" %s\n' % (self.title, label_color)
|
||||
#plot_color = ''
|
||||
elif mode == 'cpmd':
|
||||
h+= background
|
||||
h+= 'set yrange [-4:4]\n'
|
||||
h+= 'set title "CPM Datascope %s" %s\n' % (self.title, label_color)
|
||||
plot_color = ''
|
||||
elif mode == 'sync':
|
||||
h += 'set object 1 rect from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
|
||||
h += 'set size square\n'
|
||||
h += 'set xtics %s\n' % (tic_color)
|
||||
h += 'set ytics %s\n' % (tic_color)
|
||||
h += 'set border %s\n' % (border_color)
|
||||
elif mode == 'symbol':
|
||||
h+= background
|
||||
h+= 'set yrange [-4:4]\n'
|
||||
h+= 'set title "Symbol"\n'
|
||||
h+= 'set title "Symbol %s" %s\n' % (self.title, label_color)
|
||||
elif mode == 'fft' or mode == 'mixer':
|
||||
h+= background
|
||||
h+= 'unset arrow; unset title\n'
|
||||
h+= 'set xrange [%f:%f]\n' % (self.freqs[0], self.freqs[len(self.freqs)-1])
|
||||
h+= 'set xlabel "Frequency"\n'
|
||||
h+= 'set ylabel "Power(dB)"\n'
|
||||
h+= 'set grid\n'
|
||||
h+= 'set xlabel "Frequency"\n'
|
||||
h+= 'set ylabel "Power(dB)"\n'
|
||||
h+= 'set grid\n'
|
||||
h+= 'set yrange [-100:0]\n'
|
||||
if mode == 'mixer': # mixer
|
||||
h+= 'set title "Mixer: balance %3.0f (smaller is better)"\n' % (np.abs(self.avg_sum_pwr * 1000))
|
||||
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"\n'
|
||||
h+= 'set title "Spectrum %s" %s\n' % (self.title, label_color)
|
||||
if self.center_freq:
|
||||
arrow_pos = (self.center_freq - self.relative_freq) / 1e6
|
||||
h+= 'set arrow from %f, graph 0 to %f, graph 1 nohead\n' % (arrow_pos, arrow_pos)
|
||||
h+= 'set title "Spectrum: tuned to %f Mhz"\n' % arrow_pos
|
||||
h+= 'set title "Spectrum: tuned to %f Mhz" %s\n' % (arrow_pos, label_color)
|
||||
elif mode == 'fftf':
|
||||
h+= 'set yrange [-1:1.2]\n'
|
||||
h+= 'set title "fftf"\n'
|
||||
elif mode == 'float':
|
||||
h+= background
|
||||
h+= 'set yrange [-2:2]\n'
|
||||
h+= 'set title "Oscilloscope"\n'
|
||||
dat = '%s%splot %s\n%s' % (h0, h, ','.join(plots), s)
|
||||
if self.logfile is not None:
|
||||
with open(self.logfile, 'a') as fd:
|
||||
fd.write(dat)
|
||||
h+= 'set title "Oscilloscope %s" %s\n' % (self.title, label_color)
|
||||
elif mode == 'correlation':
|
||||
h+= background
|
||||
title = 'Correlation'
|
||||
if self.title:
|
||||
title = self.title
|
||||
h+= 'set yrange [-1.1:1.1]\n'
|
||||
h+= 'set title "%s" %s\n' % (title, label_color)
|
||||
if self.output_dir:
|
||||
s += 'set output\n' ## flush output png
|
||||
dat = '%s%splot %s %s\n%s' % (h0, h, ','.join(plots), plot_color, s)
|
||||
if self.logfile is not None:
|
||||
with open(self.logfile, 'a') as fd:
|
||||
fd.write(dat)
|
||||
if sys.version[0] != '2':
|
||||
dat = bytes(dat, 'utf8')
|
||||
self.gp.poll()
|
||||
if self.gp.returncode is None: # make sure gnuplot is still running
|
||||
try:
|
||||
self.gp.stdin.write(dat)
|
||||
rc = self.gp.stdin.write(dat)
|
||||
except (IOError, ValueError):
|
||||
pass
|
||||
try:
|
||||
self.gp.stdin.flush()
|
||||
except (IOError, ValueError):
|
||||
pass
|
||||
if filename:
|
||||
|
@ -279,6 +443,9 @@ class wrap_gp(object):
|
|||
def set_logfile(self, logfile=None):
|
||||
self.logfile = logfile
|
||||
|
||||
def set_title(self, title):
|
||||
self.title = title
|
||||
|
||||
class eye_sink_f(gr.sync_block):
|
||||
"""
|
||||
"""
|
||||
|
@ -293,9 +460,37 @@ class eye_sink_f(gr.sync_block):
|
|||
|
||||
def work(self, input_items, output_items):
|
||||
in0 = input_items[0]
|
||||
consumed = self.gnuplot.plot(in0, 100 * self.sps, mode='eye')
|
||||
consumed = self.gnuplot.plot(in0, 100*self.sps, mode='eye')
|
||||
return consumed ### len(input_items[0])
|
||||
|
||||
def set_title(self, title):
|
||||
self.gnuplot.set_title(title)
|
||||
|
||||
def kill(self):
|
||||
self.gnuplot.kill()
|
||||
|
||||
class cpm_sink_c(gr.sync_block):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, debug = _def_debug, sps = _def_sps, plot_mode=_def_cpm_mode):
|
||||
gr.sync_block.__init__(self,
|
||||
name="cpm_sink_c",
|
||||
in_sig=[np.complex64],
|
||||
out_sig=None)
|
||||
self.debug = debug
|
||||
self.sps = sps
|
||||
self.gnuplot = wrap_gp(sps=self.sps)
|
||||
self.plot_mode=plot_mode
|
||||
|
||||
def work(self, input_items, output_items):
|
||||
in0 = input_items[0]
|
||||
l = len(in0)
|
||||
consumed = self.gnuplot.plot(in0, self.sps*180*10, mode=self.plot_mode)
|
||||
return len(input_items[0])
|
||||
|
||||
def set_title(self, title):
|
||||
self.gnuplot.set_title(title)
|
||||
|
||||
def kill(self):
|
||||
self.gnuplot.kill()
|
||||
|
||||
|
@ -312,7 +507,33 @@ class constellation_sink_c(gr.sync_block):
|
|||
|
||||
def work(self, input_items, output_items):
|
||||
in0 = input_items[0]
|
||||
self.gnuplot.plot(in0, 1000, mode='constellation')
|
||||
self.gnuplot.plot(in0, 1000, mode='constellation')
|
||||
return len(input_items[0])
|
||||
|
||||
def set_title(self, title):
|
||||
self.gnuplot.set_title(title)
|
||||
|
||||
def kill(self):
|
||||
self.gnuplot.kill()
|
||||
|
||||
class fft_sink_f(gr.sync_block):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, debug = _def_debug):
|
||||
gr.sync_block.__init__(self,
|
||||
name="fft_sink_f",
|
||||
in_sig=[np.float32],
|
||||
out_sig=None)
|
||||
self.debug = debug
|
||||
self.gnuplot = wrap_gp()
|
||||
self.skip = 0
|
||||
|
||||
def work(self, input_items, output_items):
|
||||
self.skip += 1
|
||||
if self.skip >= 50:
|
||||
self.skip = 0
|
||||
in0 = input_items[0]
|
||||
self.gnuplot.plot(in0, FFT_BINS, mode='fftf')
|
||||
return len(input_items[0])
|
||||
|
||||
def kill(self):
|
||||
|
@ -335,15 +556,18 @@ class fft_sink_c(gr.sync_block):
|
|||
if self.skip >= 50:
|
||||
self.skip = 0
|
||||
in0 = input_items[0]
|
||||
self.gnuplot.plot(in0, FFT_BINS, mode='fft')
|
||||
self.gnuplot.plot(in0, FFT_BINS, mode='fft')
|
||||
return len(input_items[0])
|
||||
|
||||
def set_title(self, title):
|
||||
self.gnuplot.set_title(title)
|
||||
|
||||
def kill(self):
|
||||
self.gnuplot.kill()
|
||||
|
||||
def set_center_freq(self, f):
|
||||
self.gnuplot.set_center_freq(f)
|
||||
self.gnuplot.set_relative_freq(0.0)
|
||||
self.gnuplot.set_relative_freq(0.0)
|
||||
|
||||
def set_relative_freq(self, f):
|
||||
self.gnuplot.set_relative_freq(f)
|
||||
|
@ -374,6 +598,92 @@ 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()
|
||||
|
||||
class sync_plot(threading.Thread):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, debug = _def_debug, block = None, **kwds):
|
||||
threading.Thread.__init__ (self, **kwds)
|
||||
self.setDaemon(1)
|
||||
self.SLEEP_TIME = 3 ## TODO - make more configurable
|
||||
self.sleep_until = time.time() + self.SLEEP_TIME
|
||||
self.last_file_time = time.time()
|
||||
self.keep_running = True
|
||||
self.debug = debug
|
||||
self.warned = False
|
||||
|
||||
block.enable_sync_plot(True) # block must refer to a gardner/costas instance
|
||||
self.blk_id = block.unique_id()
|
||||
|
||||
self.gnuplot = wrap_gp(sps = _def_sps)
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
while self.keep_running == True:
|
||||
curr_time = time.time()
|
||||
if curr_time < self.sleep_until:
|
||||
time.sleep(1.0)
|
||||
if self.keep_running == False:
|
||||
break
|
||||
else:
|
||||
self.sleep_until = time.time() + self.SLEEP_TIME
|
||||
self.check_update()
|
||||
|
||||
def read_raw_file(self, fn):
|
||||
s = open(fn, 'rb').read()
|
||||
s_msg = ensure_str(s)
|
||||
p = s_msg.find('\n')
|
||||
if p < 1 or p > 24:
|
||||
return None # error
|
||||
hdrline = s_msg[:p]
|
||||
rest = s[p+1:]
|
||||
params = hdrline.split()
|
||||
params = [int(p) for p in params] #idx, p1p2, sps, error
|
||||
idx = params[0]
|
||||
p1p2 = params[1]
|
||||
sps = params[2]
|
||||
error_amt = params[3]
|
||||
self.gnuplot.set_sps(sps)
|
||||
if error_amt != 0:
|
||||
self.set_title("Tuning Error %d" % error_amt)
|
||||
else:
|
||||
self.set_title("")
|
||||
samples = np.frombuffer(rest, dtype=np.complex64)
|
||||
samples2 = np.concatenate((samples[idx:], samples[:idx]))
|
||||
needed = sps * 25 if p1p2 == 1 else sps * 21
|
||||
if len(samples2) < needed:
|
||||
if not self.warned:
|
||||
self.warned = True
|
||||
sys.stderr.write('read_raw_file: insufficient samples %d, needed %d\n' % (needed, len(samples2)))
|
||||
elif len(samples2) > needed:
|
||||
trim = len(samples2) - needed
|
||||
samples2 = samples2[trim:]
|
||||
return samples2 # return trimmed buf in np.complex64 format
|
||||
|
||||
def check_update(self):
|
||||
patt = 'sample-%d*.dat' % (self.blk_id)
|
||||
names = glob.glob(patt)
|
||||
if len(names) < 1: # no files to work with
|
||||
return
|
||||
d = {n: os.stat(n).st_mtime for n in names}
|
||||
ds = sorted(d.items(), key=lambda x:x[1], reverse = True)[0]
|
||||
if ds[1] <= self.last_file_time:
|
||||
return
|
||||
self.last_file_time = ds[1]
|
||||
dat = self.read_raw_file(ds[0])
|
||||
self.gnuplot.plot(dat, len(dat), mode='sync')
|
||||
|
||||
def kill(self):
|
||||
self.keep_running = False
|
||||
|
||||
def set_title(self, title):
|
||||
self.gnuplot.set_title(title)
|
||||
|
||||
def kill(self):
|
||||
self.gnuplot.kill()
|
||||
|
||||
|
@ -390,9 +700,12 @@ class symbol_sink_f(gr.sync_block):
|
|||
|
||||
def work(self, input_items, output_items):
|
||||
in0 = input_items[0]
|
||||
self.gnuplot.plot(in0, 2400, mode='symbol')
|
||||
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()
|
||||
|
||||
|
@ -412,5 +725,82 @@ 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()
|
||||
|
||||
class correlation_sink_f(gr.sync_block):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, sps=_def_sps, debug = _def_debug):
|
||||
gr.sync_block.__init__(self,
|
||||
name="plot_sink_f",
|
||||
in_sig=[np.float32],
|
||||
out_sig=None)
|
||||
self.debug = debug
|
||||
self.sps = sps
|
||||
self.gnuplot = wrap_gp()
|
||||
self.fs = []
|
||||
self.cbuf = np.array([])
|
||||
self.ignore = 0
|
||||
self.pktlen = 1024
|
||||
|
||||
def set_length(self, l):
|
||||
self.pktlen = l
|
||||
|
||||
def set_title(self, title):
|
||||
self.gnuplot.set_title(title)
|
||||
|
||||
def set_signature(self, fs):
|
||||
self.fs = []
|
||||
for s in fs:
|
||||
for i in range(self.sps):
|
||||
self.fs.append(s)
|
||||
self.fs.reverse() # reverse order for np.convolve
|
||||
self.fs = np.array(self.fs)
|
||||
|
||||
def work(self, input_items, output_items):
|
||||
if len(self.cbuf) == 0 and self.ignore > 0:
|
||||
self.ignore -= len(input_items[0])
|
||||
if self.ignore < 0:
|
||||
self.ignore = 0
|
||||
return len(input_items[0])
|
||||
if len(self.fs) == 0:
|
||||
return len(input_items[0])
|
||||
in0 = input_items[0]
|
||||
self.cbuf = np.append(self.cbuf, in0)
|
||||
if len(self.cbuf) < self.pktlen:
|
||||
return len(input_items[0])
|
||||
result = np.convolve(self.cbuf[:self.pktlen], self.fs)
|
||||
hi = np.max(np.abs(result))
|
||||
if hi != 0:
|
||||
result = result / hi
|
||||
self.cbuf = []
|
||||
self.ignore = 3000 * self.sps
|
||||
self.gnuplot.plot(result, len(result), mode='correlation')
|
||||
return len(input_items[0])
|
||||
|
||||
def kill(self):
|
||||
self.gnuplot.kill()
|
||||
|
||||
def setup_correlation(sps, title, connect_bb):
|
||||
CFG_FILE = 'correlation.json'
|
||||
if not os.access(CFG_FILE, os.R_OK):
|
||||
sys.stderr.write('correlation plot ignored, missing config file %s\n' % CFG_FILE)
|
||||
return []
|
||||
ccfg = json.loads(open(CFG_FILE).read())
|
||||
sinks = []
|
||||
for cfg in ccfg:
|
||||
sink = correlation_sink_f(sps=sps)
|
||||
sink.set_title('%s %s' % (title, cfg['name']))
|
||||
l = cfg['length'] * sps * 4
|
||||
LENGTH_LIMIT = 10000
|
||||
if l > LENGTH_LIMIT:
|
||||
l = LENGTH_LIMIT
|
||||
sink.set_length(l)
|
||||
sink.set_signature(cfg['fs'])
|
||||
connect_bb('baseband_amp', sink)
|
||||
sinks.append(sink)
|
||||
return sinks
|
||||
|
|
|
@ -0,0 +1,350 @@
|
|||
|
||||
% P25 H-CPM Demodulator (C) Copyright 2022 Max H. Parke KA1RBI
|
||||
% % Experimental H-CPM Demodulator - Release 0 %
|
||||
%
|
||||
% This file is part of OP25
|
||||
%
|
||||
% OP25 is free software; you can redistribute it and/or modify it
|
||||
% under the terms of the GNU General Public License as published by
|
||||
% the Free Software Foundation; either version 3, or (at your option)
|
||||
% any later version.
|
||||
%
|
||||
% OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
% or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
% License for more details.
|
||||
%
|
||||
% You should have received a copy of the GNU General Public License
|
||||
% along with OP25; see the file COPYING. If not, write to the Free
|
||||
% Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
% 02110-1301, USA.
|
||||
%
|
||||
%
|
||||
%
|
||||
% Accepts input IF file in GR binary complex64 format;
|
||||
% file must be at sample rate 24,000 samples/sec.
|
||||
%
|
||||
% this implementation has several simplifications, shortcuts,
|
||||
% and pitfalls, some of which are:
|
||||
%
|
||||
% * input sample must be clean and error free, there is
|
||||
% currently no tree search (viterbi) stage - decode errors
|
||||
% that should be recoverable are not autocorrected
|
||||
% * since the first and last one-quarter of the partial-response
|
||||
% period (in L=4) appear to contribute a negligible delta-phase,
|
||||
% this correlator ignores these intervals, using only the inner-
|
||||
% most two periods - accordingly, the value L=2 is assumed
|
||||
% * the code is too slow to demod in real time and there are no claims
|
||||
% to efficiency
|
||||
% * at each symbol period the received waveform vector is derotated.
|
||||
% this reduces the number of rows in the waveform matrix by a factor of
|
||||
% six (6); instead of derotating the samples, a correct correlator would
|
||||
% include these (approx. 80) additional rows
|
||||
%
|
||||
|
||||
pkg load signal;
|
||||
|
||||
global WI0 = [
|
||||
1.000000, 0.999827, 0.998111, 0.963512, 0.819784, 0.500000,
|
||||
1.000000, 0.958473, 0.800512, 0.483695, 0.022053, -0.475169,
|
||||
1.000000, 0.978152, 0.913558, 0.808999, 0.669116, 0.500000,
|
||||
1.000000, 0.966921, 0.819689, 0.504702, 0.047650, -0.424375,
|
||||
1.000000, 0.984174, 0.926374, 0.822966, 0.687929, 0.548428,
|
||||
1.000000, 0.918644, 0.736098, 0.572238, 0.506066, 0.548428,
|
||||
1.000000, 0.995165, 0.986853, 0.986153, 0.994884, 0.999596,
|
||||
1.000000, 0.947207, 0.867963, 0.865831, 0.946296, 0.999596,
|
||||
1.000000, 0.869202, 0.540278, 0.146957, -0.204506, -0.475169,
|
||||
1.000000, 0.884236, 0.567518, 0.170815, -0.179370, -0.424375,
|
||||
1.000000, 0.827032, 0.340050, -0.286034, -0.793734, -0.998382,
|
||||
1.000000, 0.905842, 0.713560, 0.552256, 0.483811, 0.500000,
|
||||
1.000000, 0.936720, 0.851252, 0.853489, 0.937706, 0.999596,
|
||||
1.000000, 0.998757, 0.999587, 0.969698, 0.834181, 0.548428,
|
||||
1.000000, 0.844204, 0.370632, -0.262797, -0.777895, -0.993535,
|
||||
1.000000, 0.991608, 0.981038, 0.981858, 0.991970, 0.999596,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
0.99179 0.98062998 0.95472002 0.92157 0.89235997 0.87256002];
|
||||
|
||||
global WQ0 = [
|
||||
0.000000, -0.018594, 0.061430, 0.267665, 0.572673, 0.866026,
|
||||
0.000000, 0.285183, 0.599317, 0.875237, 0.999757, 0.879895,
|
||||
0.000000, 0.207893, 0.406709, 0.587810, 0.743158, 0.866026,
|
||||
0.000000, -0.255075, -0.572808, -0.863294, -0.998864, -0.905486,
|
||||
0.000000, -0.177207, -0.376605, -0.568091, -0.725778, -0.836198,
|
||||
0.000000, -0.395087, -0.676875, -0.820088, -0.862495, -0.836198,
|
||||
0.000000, -0.098212, -0.161618, -0.165838, -0.101028, 0.028438,
|
||||
0.000000, -0.320622, -0.496628, -0.500337, -0.323300, 0.028438,
|
||||
0.000000, 0.494458, 0.841486, 0.989143, 0.978865, 0.879895,
|
||||
0.000000, -0.467040, -0.823361, -0.985303, -0.983782, -0.905486,
|
||||
0.000000, 0.562154, 0.940407, 0.958220, 0.608265, 0.056855,
|
||||
0.000000, 0.423616, 0.700594, 0.833674, 0.875173, 0.866025,
|
||||
0.000000, 0.350080, 0.524757, 0.521112, 0.347429, 0.028438,
|
||||
0.000000, 0.049845, -0.028745, -0.244306, -0.551491, -0.836198,
|
||||
0.000000, -0.536021, -0.928780, -0.964851, -0.628394, -0.113525,
|
||||
0.000000, 0.129280, 0.193816, 0.189618, 0.126474, 0.028438,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0. 0.13601001 0.26787999 0.37345001 0.44356 0.48087999];
|
||||
|
||||
global S_A = zeros(18,"int");
|
||||
global S_B = zeros(18,"int");
|
||||
S_A(1) = -1;
|
||||
S_B(1) = 3;
|
||||
S_A(2) = 1;
|
||||
S_B(2) = 3;
|
||||
S_A(3) = 1;
|
||||
S_B(3) = 1;
|
||||
S_A(4) = -1;
|
||||
S_B(4) = -3;
|
||||
S_A(5) = -1;
|
||||
S_B(5) = -1;
|
||||
S_A(6) = -3;
|
||||
S_B(6) = 1;
|
||||
S_A(7) = -1;
|
||||
S_B(7) = 1;
|
||||
S_A(8) = -3;
|
||||
S_B(8) = 3;
|
||||
S_A(9) = 3;
|
||||
S_B(9) = 1;
|
||||
S_A(10) = -3;
|
||||
S_B(10) = -1;
|
||||
S_A(11) = 3;
|
||||
S_B(11) = 3;
|
||||
S_A(12) = 3;
|
||||
S_B(12) = -1;
|
||||
S_A(13) = 3;
|
||||
S_B(13) = -3;
|
||||
S_A(14) = 1;
|
||||
S_B(14) = -3;
|
||||
S_A(15) = -3;
|
||||
S_B(15) = -3;
|
||||
S_A(16) = 1;
|
||||
S_B(16) = -1;
|
||||
S_A(17) = 0;
|
||||
S_B(17) = 0;
|
||||
S_A(18) = 1;
|
||||
S_B(18) = 1;
|
||||
|
||||
global L = 4; %sps
|
||||
global M = 10 % interp factor
|
||||
global LM = L*M;
|
||||
global Q = 8; % decim amount
|
||||
global NEWSPS = 5; % after decimation
|
||||
global K=360.0 / (2 * pi); % radians -> degrees
|
||||
global k = 6.0 / (2 * pi);
|
||||
|
||||
% change name of input data file
|
||||
fname = 'if-24000-IQ.dat';
|
||||
|
||||
function samples = load_text(fname)
|
||||
fid = fopen(fname, 'r');
|
||||
nn = 0;
|
||||
while 1
|
||||
nn = nn+1;
|
||||
s = fgetl(fid);
|
||||
if s == -1
|
||||
break;
|
||||
endif
|
||||
res = sscanf(s, "%f\t%f");
|
||||
samples(nn) = res(1) + 1j*res(2);
|
||||
endwhile
|
||||
g = max(abs(samples));
|
||||
samples = samples / g;
|
||||
endfunction
|
||||
|
||||
function amt_left = process_dat(dat)
|
||||
global L
|
||||
iq = dat(1:2:end-1) .+ 1j * dat(2:2:end);
|
||||
a = abs(iq);
|
||||
thresh = max(a) / 2.0;
|
||||
msk = a > thresh;
|
||||
m1 = msk(2:end) - msk(1:end-1);
|
||||
f = find(m1);
|
||||
m1f = m1(f)(end);
|
||||
lens = f(2:end) - f(1:end-1);
|
||||
l1 = (lens/L > 170 & lens/L < 180) | (lens/L > 350 & lens/L < 360);
|
||||
l2 = m1(f) == 1;
|
||||
l2 = l2(1:end-1);
|
||||
found = find(l1 & l2);
|
||||
if(length(found)) < 1
|
||||
amt_left = 0;
|
||||
return;
|
||||
endif
|
||||
for n = 1:length(found)
|
||||
valid = found(n);
|
||||
start1 = f(valid);
|
||||
len1 = lens(valid);
|
||||
demod_frag(iq(start1:start1+len1));
|
||||
end
|
||||
validn = found(end);
|
||||
startn = f(validn);
|
||||
lenn = lens(validn);
|
||||
datlen=length(dat);
|
||||
amt_left = length(dat) - (startn + lenn);
|
||||
if (amt_left < 0)
|
||||
amt_left = 0;
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function process_file(filename)
|
||||
global L
|
||||
bufsize = L * 180 * 12;
|
||||
fid = fopen(filename, 'r');
|
||||
save = [];
|
||||
while 1
|
||||
[dat, l] = fread(fid, bufsize, 'float32', 0);
|
||||
if l < 1
|
||||
break
|
||||
endif
|
||||
savel=size(save);
|
||||
datl=size(dat);
|
||||
concat = [save.' dat.'].';
|
||||
amt_left = process_dat(concat);
|
||||
if (amt_left > 0)
|
||||
save = concat(end-amt_left:end);
|
||||
else
|
||||
save = [];
|
||||
endif
|
||||
endwhile
|
||||
endfunction
|
||||
|
||||
function large_frag(msg)
|
||||
msgl = length(msg);
|
||||
padsz = 360 - msgl;
|
||||
lenh = round(length(msg) / 2);
|
||||
fmsg1 = frame_msg(msg(1:lenh),1);
|
||||
decode_msg(fmsg1);
|
||||
fmsg2 = frame_msg(msg(lenh:end),2);
|
||||
decode_msg(fmsg2);
|
||||
endfunction
|
||||
|
||||
function msg=demod_frag(frag)
|
||||
global L
|
||||
global NEWSPS
|
||||
global M
|
||||
amt_trim = mod(length(frag), L);
|
||||
frag = frag(1:end-amt_trim);
|
||||
g = max(abs(frag));
|
||||
frag = frag / g;
|
||||
nsyms = length(frag) / L;
|
||||
intrp0 = interp(frag, M);
|
||||
resampq=timing_sync(intrp0);
|
||||
nsyms = length(resampq) / NEWSPS;
|
||||
resamp1=frequency_sync(resampq);
|
||||
msg=correlation(resamp1);
|
||||
if length(msg) > 270
|
||||
large_frag(msg);
|
||||
else
|
||||
fmsg=frame_msg(msg,1);
|
||||
decode_msg(fmsg);
|
||||
lfmsg=length(fmsg);
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function decode_msg(msg)
|
||||
if length(msg) != 160
|
||||
return
|
||||
endif
|
||||
DMAP = [3 2 0 1];
|
||||
duid = msg([37,74,123,160]) + 3;
|
||||
duid = (duid / 2) + 1;
|
||||
dibits = DMAP(duid);
|
||||
duidx = dibits(1) * 64 + dibits(2) * 16 + dibits(3) * 4 + dibits(4);
|
||||
printf ('hex %x\n' , duidx);
|
||||
endfunction
|
||||
|
||||
function resampq=timing_sync(intrp0)
|
||||
global LM
|
||||
global NEWSPS
|
||||
global Q
|
||||
nsyms = length(intrp0) / LM;
|
||||
fmd = angle(intrp0(2:end) .* conj(intrp0(1:end-1)));
|
||||
fmx = mod(LM*6* ([fmd' 0] / (2*pi) + 0.5) + 0.5, 1);
|
||||
matx= reshape(fmx, LM, nsyms );
|
||||
res = std(matx');
|
||||
[m, amin] = min(res);
|
||||
amin = amin + 0 + (LM/2);
|
||||
if amin > LM
|
||||
amin = amin - LM;
|
||||
endif
|
||||
resampq = intrp0(amin:Q:end); # decim by Q
|
||||
amt_trim = mod(length(resampq), NEWSPS);
|
||||
resampq = resampq(1:end-amt_trim);
|
||||
endfunction
|
||||
|
||||
function resamp1=frequency_sync(resampq)
|
||||
global NEWSPS
|
||||
global k
|
||||
F = 0;
|
||||
sz1 = length(resampq);
|
||||
for iter = 1:4
|
||||
osc = [0:sz1-1] * (F / 30000);
|
||||
osc = exp(j*2*pi*osc);
|
||||
resamp1 = resampq .* osc.';
|
||||
row = resamp1(1:NEWSPS:end);
|
||||
rowz = mod(k*angle(row)+0.5, 1);
|
||||
rowz = unwrap((rowz-0.5) * 2*pi);
|
||||
meanr = mean(rowz(5:15)) - mean(rowz(end-15:end-5));
|
||||
F = F + meanr;
|
||||
end
|
||||
nsyms = length(resamp1) / NEWSPS;
|
||||
rfm = angle(resamp1(2:end) .* conj(resamp1(1:end-1)));
|
||||
afm = angle(resamp1);
|
||||
endfunction
|
||||
|
||||
function eye_plot(dat, sps)
|
||||
hold on
|
||||
for nn = 1:sps:length(dat)-sps*2
|
||||
sl = dat(nn:nn+sps);
|
||||
plot(sl);
|
||||
end
|
||||
hold off
|
||||
pause
|
||||
endfunction
|
||||
|
||||
function msg=correlation(resamp1)
|
||||
global NEWSPS
|
||||
global WI0
|
||||
global WQ0
|
||||
global S_A
|
||||
global S_B
|
||||
global K
|
||||
nsyms = length(resamp1) / NEWSPS;
|
||||
for n= 1 : NEWSPS : length(resamp1) - NEWSPS
|
||||
stepn=(n-1)/NEWSPS;
|
||||
idx = ((n-1) / NEWSPS) + 1;
|
||||
sl=resamp1(n:n+NEWSPS);
|
||||
sl = sl * conj(sl(1));
|
||||
sl_i = real(sl);
|
||||
sl_q = imag(sl);
|
||||
corr2 = sl_i' * WI0' + sl_q' * WQ0';
|
||||
[m,am] = max(corr2);
|
||||
msga(idx) = S_A(am);
|
||||
msgb(idx) = S_B(am);
|
||||
end
|
||||
msgok=msga(2:end) == msgb(1:end-1);
|
||||
ok=sum(msgok) / length(msga);
|
||||
msg=msgb;
|
||||
endfunction
|
||||
|
||||
function msg=frame_msg(imsg, fcode)
|
||||
if (fcode == 1)
|
||||
pilots = [1 -1 -1 1];
|
||||
else
|
||||
pilots = [-3 -3 -1 1];
|
||||
endif
|
||||
fml = length(imsg);
|
||||
msg = [];
|
||||
excess=length(imsg) - 160;
|
||||
for n=1:excess
|
||||
if n+163 > length(imsg)
|
||||
break
|
||||
endif
|
||||
slx = [imsg(n:n+1), imsg(n+162:n+163)];
|
||||
if sum(slx == pilots) == 4
|
||||
msg = imsg(n+2:n+161);
|
||||
return;
|
||||
endif
|
||||
end
|
||||
return;
|
||||
endfunction
|
||||
|
||||
process_file(fname);
|
|
@ -1,6 +1,6 @@
|
|||
#! /usr/bin/python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
# Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
|
||||
#
|
||||
# This file is part of OP25
|
||||
#
|
||||
|
@ -30,6 +30,7 @@ import threading
|
|||
import glob
|
||||
import subprocess
|
||||
import zmq
|
||||
import op25
|
||||
|
||||
from gnuradio import gr
|
||||
from waitress.server import create_server
|
||||
|
@ -37,6 +38,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
|
||||
|
@ -49,26 +53,67 @@ TSV_DIR = './'
|
|||
fake http and ajax server module
|
||||
TODO: make less fake
|
||||
"""
|
||||
def ensure_str(s): # for python 2/3
|
||||
if isinstance(s[0], str):
|
||||
return s
|
||||
ns = ''
|
||||
for i in range(len(s)):
|
||||
ns += chr(s[i])
|
||||
return ns
|
||||
|
||||
class event_iterator:
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
_jslog_file = None # set to str(filename) to enable json log
|
||||
msgs = []
|
||||
while True:
|
||||
msg = my_input_q.delete_head()
|
||||
assert msg.type() == -4
|
||||
d = json.loads(msg.to_string())
|
||||
msgs.append(d)
|
||||
if my_input_q.empty_p():
|
||||
break
|
||||
js = json.dumps(msgs)
|
||||
# TODO: json.loads followed by dumps is redundant -
|
||||
# can this be optimized?
|
||||
s = 'data:%s\r\n\r\n' % (js)
|
||||
|
||||
if _jslog_file:
|
||||
t = json.dumps(msgs, indent=4, separators=[',',':'], sort_keys=True)
|
||||
with open(_jslog_file, 'a') as logfd:
|
||||
logfd.write('%s\n' % t)
|
||||
|
||||
if sys.version[0] != '2':
|
||||
if isinstance(s, str):
|
||||
s = s.encode()
|
||||
return s
|
||||
|
||||
next = __next__ # for python2
|
||||
|
||||
def static_file(environ, start_response):
|
||||
content_types = { 'png': 'image/png', 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'gif': 'image/gif', 'css': 'text/css', 'js': 'application/javascript', 'html': 'text/html'}
|
||||
img_types = 'png jpg jpeg gif'.split()
|
||||
content_types = {'tsv': 'text/tab-separated-values', 'json': 'application/json', 'png': 'image/png', 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'gif': 'image/gif', 'css': 'text/css', 'js': 'application/javascript', 'html': 'text/html', 'ico': 'image/vnd.microsoft.icon'}
|
||||
img_types = 'png jpg jpeg gif ico'.split()
|
||||
data_types = 'tsv txt json db'.split()
|
||||
if environ['PATH_INFO'] == '/':
|
||||
filename = 'index.html'
|
||||
else:
|
||||
filename = re.sub(r'[^a-zA-Z0-9_.\-]', '', environ['PATH_INFO'])
|
||||
filename = re.sub(r'[^a-zA-Z0-9_.\-/]', '', environ['PATH_INFO'])
|
||||
suf = filename.split('.')[-1]
|
||||
pathname = '../www/www-static'
|
||||
if suf in img_types:
|
||||
pathname = '../www/images'
|
||||
elif suf in data_types:
|
||||
pathname = TSV_DIR
|
||||
pathname = '%s/%s' % (pathname, filename)
|
||||
if suf not in content_types.keys() or '..' in filename or not os.access(pathname, os.R_OK):
|
||||
sys.stderr.write('404 %s\n' % pathname)
|
||||
status = '404 NOT FOUND'
|
||||
status = '404 NOT FOUND - PATHNAME: %s FILENAME: %s CWD: %s' % (pathname, filename, os. getcwd())
|
||||
content_type = 'text/plain'
|
||||
output = status
|
||||
else:
|
||||
output = open(pathname).read()
|
||||
output = open(pathname, 'rb').read()
|
||||
content_type = content_types[suf]
|
||||
status = '200 OK'
|
||||
return status, content_type, output
|
||||
|
@ -145,10 +190,37 @@ 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
|
||||
resp_msg = []
|
||||
data = []
|
||||
try:
|
||||
data = json.loads(postdata)
|
||||
except:
|
||||
|
@ -156,6 +228,12 @@ def post_req(environ, start_response, postdata):
|
|||
traceback.print_exc(limit=None, file=sys.stderr)
|
||||
sys.stderr.write('*** end traceback ***\n')
|
||||
for d in data:
|
||||
if type(d) is str:
|
||||
sys.stderr.write('%f possible json sequence error: len %d type %s value %s\n' % (time.time(), len(d), type(d), d))
|
||||
continue
|
||||
elif type(d) is not dict:
|
||||
sys.stderr.write('%f possible json sequence error: type %s value %s\n' % (time.time(), type(d), d))
|
||||
continue
|
||||
if d['command'].startswith('config-') or d['command'].startswith('rx-'):
|
||||
resp = do_request(d)
|
||||
if resp:
|
||||
|
@ -171,17 +249,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()
|
||||
|
@ -193,9 +274,13 @@ 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)
|
||||
|
||||
if sys.version[0] != '2':
|
||||
if isinstance(output, str):
|
||||
output = output.encode()
|
||||
return [output]
|
||||
|
||||
def application(environ, start_response):
|
||||
|
@ -205,7 +290,16 @@ def application(environ, start_response):
|
|||
except:
|
||||
failed = True
|
||||
sys.stderr.write('application: request failed:\n%s\n' % traceback.format_exc())
|
||||
sys.exit(1)
|
||||
if failed:
|
||||
status = '500 Internal Server Error'
|
||||
response_headers = [ ('Access-Control-Allow-Origin', '*') ]
|
||||
start_response(status, response_headers)
|
||||
output = status
|
||||
if sys.version[0] != '2':
|
||||
if isinstance(output, str):
|
||||
output = output.encode()
|
||||
return [output]
|
||||
|
||||
return result
|
||||
|
||||
def process_qmsg(msg):
|
||||
|
@ -226,9 +320,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()
|
||||
|
@ -262,7 +357,7 @@ class Backend(threading.Thread):
|
|||
|
||||
self.zmq_sub = self.zmq_context.socket(zmq.SUB)
|
||||
self.zmq_sub.connect('tcp://localhost:%d' % self.zmq_port)
|
||||
self.zmq_sub.setsockopt(zmq.SUBSCRIBE, '')
|
||||
self.zmq_sub.setsockopt_string(zmq.SUBSCRIBE, '')
|
||||
|
||||
self.zmq_pub = self.zmq_context.socket(zmq.PUB)
|
||||
self.zmq_pub.sndhwm = 5
|
||||
|
@ -283,7 +378,8 @@ class Backend(threading.Thread):
|
|||
t = msg.type()
|
||||
s = msg.to_string()
|
||||
a = msg.arg1()
|
||||
self.zmq_pub.send(json.dumps({'command': s, 'data': a, 'msgtype': t}))
|
||||
s = ensure_str(s)
|
||||
self.zmq_pub.send_string(json.dumps({'command': s, 'data': a, 'msgtype': t}))
|
||||
|
||||
def check_subproc(self): # return True if subprocess is active
|
||||
if not self.subproc:
|
||||
|
@ -298,6 +394,9 @@ class Backend(threading.Thread):
|
|||
|
||||
def process_msg(self, msg):
|
||||
def make_command(options, config_file):
|
||||
py_exe = 'python'
|
||||
if sys.version[0] == '3':
|
||||
py_exe = 'python3'
|
||||
trunked_ct = [True for x in options._js_config['channels'] if x['trunked']]
|
||||
total_ct = [True for x in options._js_config['channels']]
|
||||
if trunked_ct and len(trunked_ct) != len(total_ct):
|
||||
|
@ -305,15 +404,57 @@ class Backend(threading.Thread):
|
|||
return None
|
||||
if not trunked_ct:
|
||||
self.backend = '%s/%s' % (os.getcwd(), 'multi_rx.py')
|
||||
opts = [self.backend]
|
||||
opts = [py_exe, self.backend]
|
||||
filename = '%s%s.json' % (CFG_DIR, config_file)
|
||||
opts.append('--config-file')
|
||||
opts.append(filename)
|
||||
return opts
|
||||
|
||||
types = {'costas-alpha': 'float', 'trunk-conf-file': 'str', 'demod-type': 'str', 'logfile-workers': 'int', 'decim-amt': 'int', 'wireshark-host': 'str', 'gain-mu': 'float', 'phase2-tdma': 'bool', 'seek': 'int', 'ifile': 'str', 'pause': 'bool', 'antenna': 'str', 'calibration': 'float', 'fine-tune': 'float', 'raw-symbols': 'str', 'audio-output': 'str', 'vocoder': 'bool', 'input': 'str', 'wireshark': 'bool', 'gains': 'str', 'args': 'str', 'sample-rate': 'int', 'terminal-type': 'str', 'gain': 'float', 'excess-bw': 'float', 'offset': 'float', 'audio-input': 'str', 'audio': 'bool', 'plot-mode': 'str', 'audio-if': 'bool', 'tone-detect': 'bool', 'frequency': 'int', 'freq-corr': 'float', 'hamlib-model': 'int', 'udp-player': 'bool', 'verbosity': 'int'}
|
||||
# TODO: this probably should be external and/or configurable
|
||||
# these options must match up one for one with the rx.py cli opts
|
||||
types = {'costas-alpha': 'float',
|
||||
'trunk-conf-file': 'str',
|
||||
'demod-type': 'str',
|
||||
'logfile-workers': 'int',
|
||||
'decim-amt': 'int',
|
||||
'wireshark-host': 'str',
|
||||
'gain-mu': 'float',
|
||||
'phase2-tdma': 'bool',
|
||||
'seek': 'int',
|
||||
'ifile': 'str',
|
||||
'pause': 'bool',
|
||||
'antenna': 'str',
|
||||
'calibration': 'float',
|
||||
'fine-tune': 'float',
|
||||
'raw-symbols': 'str',
|
||||
'audio-output': 'str',
|
||||
'vocoder': 'bool',
|
||||
'input': 'str',
|
||||
'wireshark': 'bool',
|
||||
'gains': 'str',
|
||||
'args': 'str',
|
||||
'sample-rate': 'int',
|
||||
'terminal-type': 'str',
|
||||
'gain': 'float',
|
||||
'excess-bw': 'float',
|
||||
'offset': 'float',
|
||||
'audio-input': 'str',
|
||||
'audio': 'bool',
|
||||
'plot-mode': 'str',
|
||||
'audio-if': 'bool',
|
||||
'tone-detect': 'bool',
|
||||
'frequency': 'int',
|
||||
'freq-corr': 'float',
|
||||
'hamlib-model': 'int',
|
||||
'udp-player': 'bool',
|
||||
'verbosity': 'int',
|
||||
'audio-gain': 'float',
|
||||
'freq-error-tracking': 'bool',
|
||||
'nocrypt': 'bool',
|
||||
'wireshark-port': 'int'
|
||||
}
|
||||
self.backend = '%s/%s' % (os.getcwd(), 'rx.py')
|
||||
opts = [self.backend]
|
||||
opts = [py_exe, self.backend]
|
||||
for k in [ x for x in dir(options) if not x.startswith('_') ]:
|
||||
kw = k.replace('_', '-')
|
||||
val = getattr(options, k)
|
||||
|
@ -356,6 +497,7 @@ class Backend(threading.Thread):
|
|||
options.verbosity = self.verbosity
|
||||
options.terminal_type = 'zmq:tcp:%d' % (self.zmq_port)
|
||||
cmd = make_command(options, msg['data'])
|
||||
sys.stderr.write('executing %s\n' % (' '.join(cmd)))
|
||||
if cmd:
|
||||
self.subproc = subprocess.Popen(cmd)
|
||||
elif msg['command'] == 'rx-stop':
|
||||
|
@ -381,6 +523,7 @@ class Backend(threading.Thread):
|
|||
js = self.zmq_sub.recv()
|
||||
if not self.keep_running:
|
||||
break
|
||||
js = ensure_str(js)
|
||||
msg = gr.message().make_from_string(js, -4, 0, 0)
|
||||
if not self.output_q.full_p():
|
||||
self.output_q.insert_tail(msg)
|
||||
|
@ -396,10 +539,10 @@ class rx_options(object):
|
|||
return
|
||||
config = byteify(json.loads(open(filename).read()))
|
||||
dev = [x for x in config['devices'] if x['active']][0]
|
||||
if not dev:
|
||||
if not dev:
|
||||
return
|
||||
chan = [x for x in config['channels'] if x['active']][0]
|
||||
if not chan:
|
||||
if not chan:
|
||||
return
|
||||
options = object()
|
||||
for k in config['backend-rx'].keys():
|
||||
|
@ -427,7 +570,7 @@ def http_main():
|
|||
|
||||
# wait for gdb
|
||||
if options.pause:
|
||||
print 'Ready for GDB to attach (pid = %d)' % (os.getpid(),)
|
||||
print ('Ready for GDB to attach (pid = %d)' % (os.getpid(),))
|
||||
raw_input("Press 'Enter' to continue...")
|
||||
|
||||
input_q = gr.msg_queue(20)
|
|
@ -0,0 +1,31 @@
|
|||
#! /bin/sh
|
||||
|
||||
PIP3=`which pip3`
|
||||
USERDIR=~/.local/bin
|
||||
|
||||
sudo apt-get install python3-pip
|
||||
|
||||
# PIP3=`which pip3`
|
||||
PIP3=/usr/bin/pip3
|
||||
|
||||
# # # # # # un-comment the following two lines for ubuntu 16.04 # # # # # #
|
||||
#pip3 install --user pip==10.0.1
|
||||
#PIP3=$USERDIR/pip3
|
||||
|
||||
echo PIP3 now set to $PIP3
|
||||
# # # $PIP3 --version # # # generates errors -- (?)
|
||||
|
||||
$PIP3 install --user sqlalchemy
|
||||
$PIP3 install --user flask
|
||||
$PIP3 install --user datatables
|
||||
$PIP3 install --user flask-sqlalchemy
|
||||
|
||||
cd
|
||||
git clone https://github.com/Pegase745/sqlalchemy-datatables.git
|
||||
cd sqlalchemy-datatables
|
||||
$PIP3 install --user .
|
||||
cd
|
||||
|
||||
echo the following line must be added to your .bashrc
|
||||
echo "export PATH=$USERDIR:\$PATH"
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2020 Graham Norbury
|
||||
#
|
||||
# This file is part of OP25
|
||||
#
|
||||
# OP25 is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
# License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OP25; see the file COPYING. If not, write to the Free
|
||||
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
|
||||
# Modify TS_FORMAT to control logger timestamp format
|
||||
# 0 = legacy epoch seconds
|
||||
# 1 = formatted mm/dd/yy hh:mm:ss.usec
|
||||
TS_FORMAT = 1
|
||||
|
||||
import time
|
||||
class log_ts(object):
|
||||
@staticmethod
|
||||
def get(supplied_ts=None):
|
||||
if supplied_ts is None:
|
||||
ts = time.time()
|
||||
else:
|
||||
ts = supplied_ts
|
||||
|
||||
if TS_FORMAT == 0:
|
||||
formatted_ts = "{:.6f}".format(ts)
|
||||
else:
|
||||
formatted_ts = "{:s}{:s}".format(time.strftime("%m/%d/%y %H:%M:%S",time.localtime(ts)),"{:.6f}".format(ts - int(ts)).lstrip("0"))
|
||||
|
||||
return formatted_ts
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI
|
||||
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
|
||||
#
|
||||
# This file is part of OP25
|
||||
#
|
||||
|
@ -24,33 +24,55 @@ import sys
|
|||
import threading
|
||||
import time
|
||||
import json
|
||||
import select
|
||||
import traceback
|
||||
import osmosdr
|
||||
|
||||
from gnuradio import audio, eng_notation, gr, gru, filter, blocks, fft, analog, digital
|
||||
from gnuradio import audio, eng_notation, gr, filter, blocks, fft, analog, digital
|
||||
from gnuradio.eng_option import eng_option
|
||||
from math import pi
|
||||
from optparse import OptionParser
|
||||
|
||||
import trunking
|
||||
|
||||
import op25
|
||||
import op25_repeater
|
||||
import p25_demodulator
|
||||
import p25_decoder
|
||||
from sockaudio import audio_thread
|
||||
|
||||
from sql_dbi import sql_dbi
|
||||
|
||||
from gr_gnuplot import constellation_sink_c
|
||||
from gr_gnuplot import fft_sink_c
|
||||
from gr_gnuplot import mixer_sink_c
|
||||
from gr_gnuplot import symbol_sink_f
|
||||
from gr_gnuplot import eye_sink_f
|
||||
from gr_gnuplot import setup_correlation
|
||||
from gr_gnuplot import sync_plot
|
||||
from gr_gnuplot import cpm_sink_c
|
||||
|
||||
from nxdn_trunking import cac_message
|
||||
|
||||
from terminal import op25_terminal
|
||||
|
||||
sys.path.append('tdma')
|
||||
import lfsr
|
||||
|
||||
os.environ['IMBE'] = 'soft'
|
||||
|
||||
_def_symbol_rate = 4800
|
||||
_def_interval = 3.0 # sec
|
||||
_def_file_dir = '../www/images'
|
||||
_def_audio_port = 23456 # udp port for audio thread
|
||||
_def_audio_output = 'default' # output device name for audio thread
|
||||
|
||||
# The P25 receiver
|
||||
#
|
||||
|
||||
def byteify(input): # thx so
|
||||
if sys.version[0] != '2': # hack, must be a better way
|
||||
return input
|
||||
if isinstance(input, dict):
|
||||
return {byteify(key): byteify(value)
|
||||
for key, value in input.iteritems()}
|
||||
|
@ -66,12 +88,18 @@ 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:'):
|
||||
if config['args'].startswith('audio-if:'):
|
||||
self.init_audio_if(config)
|
||||
elif config['args'].startswith('audio:'):
|
||||
self.init_audio(config)
|
||||
elif config['args'].startswith('file:'):
|
||||
self.init_file(config)
|
||||
elif config['args'].startswith('udp:'):
|
||||
self.init_udp(config)
|
||||
else:
|
||||
self.init_osmosdr(config)
|
||||
|
||||
|
@ -84,15 +112,50 @@ class device(object):
|
|||
self.frequency = config['frequency']
|
||||
self.offset = config['offset']
|
||||
|
||||
def init_audio_if(self, config):
|
||||
filename = config['args'].replace('audio-if:', '')
|
||||
self.audio_source = audio.source(config['rate'], filename)
|
||||
self.null_source = blocks.null_source (gr.sizeof_float)
|
||||
self.audio_cvt = blocks.float_to_complex()
|
||||
self.tb.connect(self.audio_source, (self.audio_cvt, 0))
|
||||
self.tb.connect(self.null_source, (self.audio_cvt, 1))
|
||||
self.src = self.audio_cvt
|
||||
self.frequency = config['frequency']
|
||||
self.offset = config['offset']
|
||||
|
||||
def init_audio(self, config):
|
||||
filename = config['args'].replace('audio:', '')
|
||||
src = audio.source(self.sample_rate, filename)
|
||||
if filename.startswith('file:'):
|
||||
filename = filename.replace('file:', '')
|
||||
repeat = False
|
||||
s2f = blocks.short_to_float()
|
||||
K = 1 / 32767.0
|
||||
src = blocks.multiply_const_ff(K)
|
||||
throttle = blocks.throttle(gr.sizeof_short, self.sample_rate) # may be redundant in stdin case ?
|
||||
if filename == '-':
|
||||
fd = 0 # stdin
|
||||
fsrc = blocks.file_descriptor_source(gr.sizeof_short, fd, repeat)
|
||||
else:
|
||||
fsrc = blocks.file_source(gr.sizeof_short, filename, repeat)
|
||||
self.tb.connect(fsrc, throttle, s2f, src)
|
||||
else:
|
||||
src = audio.source(self.sample_rate, filename)
|
||||
gain = 1.0
|
||||
if config['gains'].startswith('audio:'):
|
||||
gain = float(config['gains'].replace('audio:', ''))
|
||||
self.src = blocks.multiply_const_ff(gain)
|
||||
self.tb.connect(src, self.src)
|
||||
|
||||
def init_udp(self, config):
|
||||
hostinfo = config['args'].split(':')
|
||||
hostname = hostinfo[1]
|
||||
udp_port = int(hostinfo[2])
|
||||
bufsize = 32000 # might try enlarging this if packet loss
|
||||
self.src = blocks.udp_source(gr.sizeof_gr_complex, hostname, udp_port, payload_size = bufsize)
|
||||
self.ppm = 0
|
||||
self.frequency = config['frequency']
|
||||
self.offset = 0
|
||||
|
||||
def init_osmosdr(self, config):
|
||||
speeds = [250000, 1000000, 1024000, 1800000, 1920000, 2000000, 2048000, 2400000, 2560000]
|
||||
|
||||
|
@ -117,19 +180,53 @@ 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):
|
||||
def __init__(self, config, dev, verbosity, msgq = None, process_msg=None, msgq_id=-1, role=''):
|
||||
sys.stderr.write('channel (dev %s): %s\n' % (dev.name, config))
|
||||
self.device = dev
|
||||
self.name = config['name']
|
||||
self.symbol_rate = _def_symbol_rate
|
||||
self.process_msg = process_msg
|
||||
self.role = role
|
||||
self.dev = ''
|
||||
self.sysid = []
|
||||
self.nac = []
|
||||
if 'symbol_rate' in config.keys():
|
||||
self.symbol_rate = config['symbol_rate']
|
||||
self.config = config
|
||||
self.verbosity = verbosity
|
||||
self.frequency = config['frequency'] if self.device.args.startswith('audio-if') else 0
|
||||
self.tdma_state = False
|
||||
self.xor_cache = {}
|
||||
|
||||
self.tuning_error = 0
|
||||
self.freq_correction = 0
|
||||
self.error_band = 0
|
||||
self.last_error_update = 0
|
||||
self.last_set_freq_at = time.time()
|
||||
self.warned_frequencies = {}
|
||||
self.msgq_id = msgq_id
|
||||
self.next_band_change = time.time()
|
||||
|
||||
self.audio_port = _def_audio_port
|
||||
self.audio_output = _def_audio_output
|
||||
self.audio_gain = 1.0
|
||||
if 'audio_gain' in config:
|
||||
self.audio_gain = float(config['audio_gain'])
|
||||
|
||||
if dev.args.startswith('audio:'):
|
||||
self.demod = p25_demodulator.p25_demod_fb(
|
||||
input_rate = dev.sample_rate,
|
||||
filter_type = config['filter_type'],
|
||||
if_rate = config['if_rate'],
|
||||
symbol_rate = self.symbol_rate)
|
||||
else:
|
||||
self.demod = p25_demodulator.p25_demod_cb(
|
||||
|
@ -140,58 +237,504 @@ class channel(object):
|
|||
relative_freq = dev.frequency + dev.offset - config['frequency'],
|
||||
offset = dev.offset,
|
||||
if_rate = config['if_rate'],
|
||||
symbol_rate = self.symbol_rate)
|
||||
q = gr.msg_queue(1)
|
||||
self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q)
|
||||
symbol_rate = self.symbol_rate,
|
||||
use_old_decim = True if self.device.args.startswith('audio-if') else False)
|
||||
if msgq is not None:
|
||||
q = msgq
|
||||
else:
|
||||
q = gr.msg_queue(20)
|
||||
if 'decode' in config.keys() and config['decode'].startswith('p25_decoder'):
|
||||
num_ambe = 1
|
||||
(proto, wireshark_host, udp_port) = config['destination'].split(':')
|
||||
assert proto == 'udp'
|
||||
wireshark_host = wireshark_host.replace('/', '')
|
||||
udp_port = int(udp_port)
|
||||
if role == 'vc':
|
||||
self.audio_port = udp_port
|
||||
if 'audio_output' in config.keys():
|
||||
self.audio_output = config['audio_output']
|
||||
|
||||
self.decoder = p25_decoder.p25_decoder_sink_b(dest='audio', do_imbe=True, num_ambe=num_ambe, wireshark_host=wireshark_host, udp_port=udp_port, do_msgq = True, msgq=q, audio_output=self.audio_output, debug=verbosity, msgq_id=self.msgq_id)
|
||||
else:
|
||||
self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q, self.msgq_id)
|
||||
|
||||
if self.symbol_rate == 6000 and role == 'cc':
|
||||
sps = config['if_rate'] // self.symbol_rate
|
||||
self.demod.set_symbol_rate(self.symbol_rate) # this and the foll. call should be merged?
|
||||
self.demod.clock.set_omega(float(sps))
|
||||
self.demod.clock.set_tdma(True)
|
||||
sys.stderr.write('initializing TDMA control channel %s channel ID %d\n' % (self.name, self.msgq_id))
|
||||
|
||||
if self.process_msg is not None and msgq is None:
|
||||
self.q_watcher = du_queue_watcher(q, lambda msg: self.process_msg(msg, sender=self))
|
||||
|
||||
self.kill_sink = []
|
||||
|
||||
if 'blacklist' in config.keys():
|
||||
for g in config['blacklist'].split(','):
|
||||
self.decoder.insert_blacklist(int(g))
|
||||
|
||||
if 'whitelist' in config.keys():
|
||||
for g in config['whitelist'].split(','):
|
||||
self.decoder.insert_whitelist(int(g))
|
||||
|
||||
self.sinks = []
|
||||
if 'plot' not in config.keys():
|
||||
return
|
||||
|
||||
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 = eye_sink_f(sps=config['if_rate'] // self.symbol_rate)
|
||||
sink.set_title(self.name)
|
||||
self.sinks.append(sink)
|
||||
self.demod.connect_bb('symbol_filter', sink)
|
||||
self.kill_sink.append(sink)
|
||||
elif plot.startswith('cpm'):
|
||||
if self.symbol_rate != 6000: # fixed rate value for p25p2
|
||||
sys.stderr.write('warning: symbol rate %d may be incorrect for CPM channel %s\n' % (self.symbol_rate, self.name))
|
||||
sink = cpm_sink_c(sps=config['if_rate'] // self.symbol_rate, plot_mode=plot)
|
||||
sink.set_title(self.name)
|
||||
self.sinks.append(sink)
|
||||
self.demod.connect_complex('if_out', sink)
|
||||
self.kill_sink.append(sink)
|
||||
elif plot == 'symbol':
|
||||
sink = symbol_sink_f()
|
||||
sink.set_title(self.name)
|
||||
self.sinks.append(sink)
|
||||
self.demod.connect_float(sink)
|
||||
self.kill_sink.append(sink)
|
||||
elif plot == 'fft':
|
||||
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
|
||||
if config['demod_type'] == 'cqpsk':
|
||||
blk = 'mixer'
|
||||
else:
|
||||
blk = 'cutoff'
|
||||
i = len(self.sinks)
|
||||
self.sinks.append(mixer_sink_c())
|
||||
self.demod.connect_complex('mixer', self.sinks[i])
|
||||
sink = mixer_sink_c()
|
||||
sink.set_title(self.name)
|
||||
self.sinks.append(sink)
|
||||
self.demod.connect_complex(blk, self.sinks[i])
|
||||
self.kill_sink.append(self.sinks[i])
|
||||
elif plot == 'constellation':
|
||||
i = len(self.sinks)
|
||||
assert config['demod_type'] == 'cqpsk' ## constellation plot requires cqpsk demod type
|
||||
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':
|
||||
assert config['demod_type'] == 'fsk4' ## correlation plot requires fsk4 demod type
|
||||
assert config['symbol_rate'] == 4800 ## 4800 required for correlation plot
|
||||
sps=config['if_rate'] // self.symbol_rate
|
||||
sinks = setup_correlation(sps, self.name, self.demod.connect_bb)
|
||||
self.kill_sink += sinks
|
||||
self.sinks += sinks
|
||||
elif plot == 'sync':
|
||||
assert config['demod_type'] == 'cqpsk' ## sync plot requires cqpsk demod type
|
||||
i = len(self.sinks)
|
||||
sink = sync_plot(block = self.demod.clock)
|
||||
sink.set_title(self.name)
|
||||
self.sinks.append(sink)
|
||||
self.kill_sink.append(self.sinks[i])
|
||||
# does not issue self.connect()
|
||||
else:
|
||||
sys.stderr.write('unrecognized plot type %s\n' % plot)
|
||||
return
|
||||
|
||||
def set_frequency(self, frequency):
|
||||
assert frequency
|
||||
if self.device.tunable:
|
||||
self.device.set_frequency(frequency)
|
||||
f = self.frequency if self.device.args.startswith('audio-if') else frequency
|
||||
relative_freq = self.device.frequency + self.device.offset + self.tuning_error - f
|
||||
if (not self.device.tunable) and (not self.device.args.startswith('audio-if')) and abs(relative_freq) > ((self.demod.input_rate / 2) - (self.demod.if1 / 2)):
|
||||
if frequency not in self.warned_frequencies:
|
||||
sys.stderr.write('warning: set frequency %f to non-tunable device %s rejected.\n' % (frequency / 1000000.0, self.device.name))
|
||||
self.warned_frequencies[frequency] = 0
|
||||
self.warned_frequencies[frequency] += 1
|
||||
#print 'set_relative_frequency: error, relative frequency %d exceeds limit %d' % (relative_freq, self.demod.input_rate/2)
|
||||
return False
|
||||
self.demod.set_relative_frequency(relative_freq)
|
||||
self.last_set_freq_at = time.time()
|
||||
if not self.device.args.startswith('audio-if'):
|
||||
self.frequency = frequency
|
||||
|
||||
def error_tracking(self, last_change_freq):
|
||||
curr_time = time.time()
|
||||
if self.config['demod_type'] == 'fsk4':
|
||||
return None # todo: allow tracking in fsk4 demod
|
||||
UPDATE_TIME = 3
|
||||
if self.last_error_update + UPDATE_TIME > curr_time:
|
||||
return None
|
||||
self.last_error_update = time.time()
|
||||
if not self.demod.is_muted():
|
||||
band = self.demod.get_error_band()
|
||||
freq_error = self.demod.get_freq_error()
|
||||
if band and curr_time >= self.next_band_change:
|
||||
self.next_band_change = curr_time + 20.0
|
||||
self.error_band += band
|
||||
sys.stderr.write('channel %d set error band %d\n' % (self.msgq_id, self.error_band))
|
||||
self.freq_correction += freq_error * 0.15
|
||||
self.freq_correction = int(self.freq_correction)
|
||||
if self.freq_correction > 600:
|
||||
self.freq_correction -= 1200
|
||||
self.error_band += 1
|
||||
elif self.freq_correction < -600:
|
||||
self.freq_correction += 1200
|
||||
self.error_band -= 1
|
||||
self.error_band = min(self.error_band, 2)
|
||||
self.error_band = max(self.error_band, -2)
|
||||
self.tuning_error = int(self.error_band * 1200 + self.freq_correction)
|
||||
e = 0
|
||||
if last_change_freq > 0:
|
||||
e = (self.tuning_error*1e6) / float(last_change_freq)
|
||||
else:
|
||||
e = 0
|
||||
freq_error = 0
|
||||
band = 0
|
||||
### self.set_frequency(self.frequency) # adjust relative frequency with updated tuning_error
|
||||
if self.verbosity >= 10:
|
||||
sys.stderr.write('%f\terror_tracking\t%s\t%d\t%d\t%d\t%d\t%d\t%f\n' % (curr_time, self.name, self.msgq_id, freq_error, self.error_band, self.tuning_error, self.freq_correction, e))
|
||||
d = {'time': time.time(), 'json_type': 'freq_error_tracking', 'name': self.name, 'device': self.device.name, 'freq_error': freq_error, 'band': band, 'error_band': self.error_band, 'tuning_error': self.tuning_error, 'freq_correction': self.freq_correction}
|
||||
if self.frequency:
|
||||
self.set_frequency(self.frequency)
|
||||
return d
|
||||
|
||||
def configure_tdma(self, params):
|
||||
set_tdma = False
|
||||
if params['tdma'] is not None:
|
||||
set_tdma = True
|
||||
self.decoder.set_slotid(params['tdma'])
|
||||
self.demod.clock.set_tdma(set_tdma)
|
||||
if set_tdma == self.tdma_state:
|
||||
return # already in desired state
|
||||
self.tdma_state = set_tdma
|
||||
if set_tdma:
|
||||
hash = '%x%x%x' % (params['nac'], params['sysid'], params['wacn'])
|
||||
if hash not in self.xor_cache:
|
||||
self.xor_cache[hash] = lfsr.p25p2_lfsr(params['nac'], params['sysid'], params['wacn']).xor_chars
|
||||
self.decoder.set_xormask(self.xor_cache[hash], hash)
|
||||
self.decoder.set_nac(params['nac'])
|
||||
rate = 6000
|
||||
else:
|
||||
rate = 4800
|
||||
sps = self.config['if_rate'] / rate
|
||||
self.demod.set_symbol_rate(rate) # this and the foll. call should be merged?
|
||||
self.demod.clock.set_omega(float(sps))
|
||||
|
||||
class du_queue_watcher(threading.Thread):
|
||||
def __init__(self, msgq, callback, **kwds):
|
||||
threading.Thread.__init__ (self, **kwds)
|
||||
self.setDaemon(1)
|
||||
self.msgq = msgq
|
||||
self.callback = callback
|
||||
self.keep_running = True
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
while(self.keep_running):
|
||||
msg = self.msgq.delete_head()
|
||||
if not self.keep_running:
|
||||
break
|
||||
self.callback(msg)
|
||||
|
||||
class rx_block (gr.top_block):
|
||||
|
||||
# Initialize the receiver
|
||||
#
|
||||
def __init__(self, verbosity, config):
|
||||
def __init__(self, verbosity, config, trunk_conf_file=None, terminal_type=None, track_errors=False, udp_player=None):
|
||||
self.verbosity = verbosity
|
||||
gr.top_block.__init__(self)
|
||||
self.device_id_by_name = {}
|
||||
self.msg_types = {}
|
||||
self.terminal_type = terminal_type
|
||||
self.last_process_update = 0
|
||||
self.last_freq_params = {'freq' : 0.0, 'tgid' : None, 'tag' : "", 'tdma' : None}
|
||||
self.trunk_rx = None
|
||||
self.track_errors = track_errors
|
||||
self.last_change_freq = 0
|
||||
self.sql_db = sql_dbi()
|
||||
self.input_q = gr.msg_queue(20)
|
||||
self.output_q = gr.msg_queue(20)
|
||||
self.last_voice_channel_id = 0
|
||||
self.terminal = op25_terminal(self.input_q, self.output_q, terminal_type)
|
||||
self.configure_devices(config['devices'])
|
||||
self.configure_channels(config['channels'])
|
||||
if trunk_conf_file:
|
||||
self.trunk_rx = trunking.rx_ctl(frequency_set = self.change_freq, debug = self.verbosity, conf_file = trunk_conf_file, logfile_workers=[], send_event=self.send_event)
|
||||
self.sinks = []
|
||||
for chan in self.channels:
|
||||
if len(chan.sinks):
|
||||
self.sinks += chan.sinks
|
||||
if self.is_http_term():
|
||||
for sink in self.sinks:
|
||||
sink.gnuplot.set_interval(_def_interval)
|
||||
sink.gnuplot.set_output_dir(_def_file_dir)
|
||||
|
||||
if udp_player:
|
||||
chan = self.find_audio_channel() # find chan used for audio
|
||||
self.audio = audio_thread("127.0.0.1", chan.audio_port, chan.audio_output, False, chan.audio_gain)
|
||||
else:
|
||||
self.audio = None
|
||||
|
||||
def find_channel_uplink(self, params):
|
||||
channels = []
|
||||
for chan in self.channels:
|
||||
if chan.role != 'uplink':
|
||||
continue
|
||||
channels.append(chan)
|
||||
if self.verbosity > 0:
|
||||
sys.stderr.write('%f find_channel_uplink: selected channel %d (%s) for tuning request type %s frequency %f\n' % (time.time(), chan.msgq_id, chan.name, 'vc', params['uplink'] / 1000000.0))
|
||||
return channels
|
||||
|
||||
def find_channel_cc(self, params):
|
||||
channels = []
|
||||
for chan in self.channels:
|
||||
if chan.role != 'cc':
|
||||
continue
|
||||
if len(chan.nac) and params['nac'] not in chan.nac:
|
||||
continue
|
||||
if len(chan.sysid) and params['sysid'] not in chan.sysid:
|
||||
continue
|
||||
channels.append(chan)
|
||||
if self.verbosity > 0:
|
||||
sys.stderr.write('%f find_channel_cc: selected channel %d (%s) for tuning request type %s frequency %f\n' % (time.time(), chan.msgq_id, chan.name, 'cc', params['freq'] / 1000000.0))
|
||||
return channels
|
||||
|
||||
def find_channel_vc(self, params):
|
||||
channels = []
|
||||
for chan in self.channels: # pass1 - search for vc on non-tunable dev having frequency within band
|
||||
if chan.role != 'vc':
|
||||
continue
|
||||
if chan.device.tunable:
|
||||
continue
|
||||
if abs(params['freq'] - chan.device.frequency) >= chan.demod.relative_limit:
|
||||
#sys.stderr.write('%f skipping channel %d frequency %f dev freq %f limit %f\n' % (time.time(), chan.msgq_id, params['freq'] / 1000000.0, chan.device.frequency / 1000000.0, chan.demod.relative_limit / 1000000.0))
|
||||
continue
|
||||
channels.append(chan)
|
||||
if self.verbosity > 0:
|
||||
sys.stderr.write('%f find_channel_vc: selected channel %d (%s) for tuning request type %s frequency %f (1)\n' % (time.time(), chan.msgq_id, chan.name, 'vc', params['freq'] / 1000000.0))
|
||||
return channels
|
||||
for chan in self.channels: # pass2 - search for vc on tunable dev
|
||||
if chan.role != 'vc':
|
||||
continue
|
||||
if not chan.device.tunable:
|
||||
continue
|
||||
channels.append(chan)
|
||||
if self.verbosity > 0:
|
||||
sys.stderr.write('%f find_channel_vc: selected channel %d (%s) for tuning request type %s frequency %f (2)\n' % (time.time(), chan.msgq_id, chan.name, 'vc', params['freq'] / 1000000.0))
|
||||
return channels
|
||||
return [] # pass 1 and 2 failed
|
||||
|
||||
def do_error_tracking(self):
|
||||
if not self.track_errors:
|
||||
return
|
||||
for chan in self.channels:
|
||||
d = chan.error_tracking(self.last_change_freq)
|
||||
if d is not None and not self.input_q.full_p():
|
||||
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
|
||||
self.input_q.insert_tail(msg)
|
||||
|
||||
def change_freq(self, params):
|
||||
self.last_freq_params = params
|
||||
freq = params['freq']
|
||||
self.last_change_freq = freq
|
||||
channel_type = params['channel_type'] # vc or cc
|
||||
self.uplink_change_freq(params)
|
||||
if channel_type == 'vc':
|
||||
channels = self.find_channel_vc(params)
|
||||
elif channel_type == 'cc':
|
||||
channels = self.find_channel_cc(params)
|
||||
else:
|
||||
raise ValueError('change_freq: invalid channel_type: %s' % channel_type)
|
||||
if len(channels) == 0:
|
||||
sys.stderr.write('change_freq: no channel(s) found for %s frequency %f\n' % (channel_type, freq/1000000.0))
|
||||
return
|
||||
for chan in channels:
|
||||
chan.device.set_frequency(freq)
|
||||
chan.set_frequency(freq)
|
||||
chan.configure_tdma(params)
|
||||
self.freq_update()
|
||||
if channel_type == 'vc':
|
||||
self.last_voice_channel_id = chan.msgq_id
|
||||
#return
|
||||
if self.trunk_rx is None:
|
||||
return
|
||||
voice_chans = [chan for chan in self.channels if chan.role == 'vc']
|
||||
voice_state = channel_type == 'vc'
|
||||
# FIXME: fsk4 case needs work/testing
|
||||
for chan in voice_chans:
|
||||
if voice_state and chan.msgq_id == self.last_voice_channel_id:
|
||||
chan.demod.set_muted(False)
|
||||
else:
|
||||
chan.demod.set_muted(True)
|
||||
|
||||
def uplink_change_freq(self, params):
|
||||
channel_type = params['channel_type'] # vc or cc
|
||||
if 'uplink' not in params:
|
||||
return
|
||||
if channel_type != 'vc':
|
||||
return
|
||||
uplink = params['uplink']
|
||||
channels = self.find_channel_uplink(params)
|
||||
for chan in channels:
|
||||
chan.device.set_frequency(uplink)
|
||||
chan.set_frequency(uplink)
|
||||
chan.configure_tdma(params)
|
||||
if self.verbosity > 0:
|
||||
sys.stderr.write('set uplink frequency %f, channel %s\n' % (uplink / 1000000.0, chan.name))
|
||||
|
||||
def is_http_term(self):
|
||||
if self.terminal_type.startswith('http:'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def process_terminal_msg(self, msg):
|
||||
# return true = end top block
|
||||
RX_COMMANDS = 'skip lockout hold'.split()
|
||||
s = msg.to_string()
|
||||
t = msg.type()
|
||||
if t == -4:
|
||||
d = json.loads(s)
|
||||
s = d['command']
|
||||
if type(s) is not str and isinstance(s, bytes):
|
||||
# should only get here if python3
|
||||
s = s.decode()
|
||||
if s == 'quit': return True
|
||||
elif s == 'update': ## deprecated here: to be removed
|
||||
pass
|
||||
# self.process_update()
|
||||
elif s == 'set_freq':
|
||||
sys.stderr.write('set_freq not supported\n')
|
||||
return
|
||||
#freq = msg.arg1()
|
||||
#self.last_freq_params['freq'] = freq
|
||||
#self.set_freq(freq)
|
||||
elif s == 'adj_tune':
|
||||
freq = msg.arg1()
|
||||
elif s == 'dump_tgids':
|
||||
self.trunk_rx.dump_tgids()
|
||||
elif s == 'reload_tags':
|
||||
nac = msg.arg1()
|
||||
self.trunk_rx.reload_tags(int(nac))
|
||||
elif s == 'add_default_config':
|
||||
nac = msg.arg1()
|
||||
self.trunk_rx.add_default_config(int(nac))
|
||||
elif s in RX_COMMANDS:
|
||||
if self.trunk_rx is not None:
|
||||
self.trunk_rx.process_qmsg(msg)
|
||||
elif s == 'settings-enable' and self.trunk_rx is not None:
|
||||
self.trunk_rx.enable_status(d['data'])
|
||||
return False
|
||||
|
||||
def process_ajax(self):
|
||||
if not self.is_http_term():
|
||||
return
|
||||
if self.input_q.full_p():
|
||||
return
|
||||
filenames = [sink.gnuplot.filename for sink in self.sinks if sink.gnuplot.filename]
|
||||
error = []
|
||||
for chan in self.channels:
|
||||
if hasattr(chan.demod, 'get_freq_error'):
|
||||
error.append(chan.demod.get_freq_error())
|
||||
d = {'json_type': 'rx_update', 'error': error, 'files': filenames, 'time': time.time()}
|
||||
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
|
||||
self.input_q.insert_tail(msg)
|
||||
|
||||
def process_update(self):
|
||||
UPDATE_INTERVAL = 1.0 # sec.
|
||||
now = time.time()
|
||||
if now < self.last_process_update + UPDATE_INTERVAL:
|
||||
return
|
||||
self.last_process_update = now
|
||||
self.freq_update()
|
||||
if self.input_q.full_p():
|
||||
return
|
||||
if self.trunk_rx is None:
|
||||
return ## possible race cond - just ignore
|
||||
js = self.trunk_rx.to_json()
|
||||
msg = gr.message().make_from_string(js, -4, 0, 0)
|
||||
self.input_q.insert_tail(msg)
|
||||
self.process_ajax()
|
||||
|
||||
def send_event(self, d): ## called from trunking module to send json msgs / updates to client
|
||||
if d is not None:
|
||||
self.sql_db.event(d)
|
||||
if d and not self.input_q.full_p():
|
||||
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
|
||||
self.input_q.insert_tail(msg)
|
||||
self.process_update()
|
||||
|
||||
def freq_update(self):
|
||||
if self.input_q.full_p():
|
||||
return
|
||||
params = self.last_freq_params
|
||||
params['json_type'] = 'change_freq'
|
||||
params['current_time'] = time.time()
|
||||
js = json.dumps(params)
|
||||
msg = gr.message().make_from_string(js, -4, 0, 0)
|
||||
self.input_q.insert_tail(msg)
|
||||
|
||||
def process_msg(self, msg):
|
||||
mtype = msg.type()
|
||||
if mtype == -2 or mtype == -4:
|
||||
self.process_terminal_msg(msg)
|
||||
else:
|
||||
self.process_channel_msg(msg, mtype)
|
||||
|
||||
def process_channel_msg(self, msg, mtype):
|
||||
msgtext = msg.to_string()
|
||||
aa55 = trunking.get_ordinals(msgtext[:2])
|
||||
assert aa55 == 0xaa55
|
||||
msgq_id = trunking.get_ordinals(msgtext[2:4])
|
||||
msgtext = msgtext[4:]
|
||||
if mtype == -5:
|
||||
self.process_nxdn_msg(msgtext)
|
||||
else:
|
||||
self.process_trunked_qmsg(msg, msgq_id)
|
||||
|
||||
def process_nxdn_msg(self, s):
|
||||
if isinstance(s[0], str): # for python 2/3
|
||||
s = [ord(x) for x in s]
|
||||
msgtype = chr(s[0])
|
||||
lich = s[1]
|
||||
if self.verbosity > 2:
|
||||
sys.stderr.write ('process_nxdn_msg %s lich %x\n' % (msgtype, lich))
|
||||
if msgtype == 'c': # CAC type
|
||||
ran = s[2] & 0x3f
|
||||
msg = cac_message(s[2:])
|
||||
if msg['msg_type'] == 'CCH_INFO' and self.verbosity:
|
||||
sys.stderr.write ('%-10s %-10s system %d site %d ran %d\n' % (msg['cc1']/1e6, msg['cc2']/1e6, msg['location_id']['system'], msg['location_id']['site'], ran))
|
||||
if self.verbosity > 1:
|
||||
sys.stderr.write('%s\n' % json.dumps(msg))
|
||||
|
||||
def filtered(self, msg, msgq_id):
|
||||
# return True if msg should be suppressed
|
||||
chan = self.channels[msgq_id-1]
|
||||
t = msg.type()
|
||||
if chan.role == 'vc' and t in [7, 12]: ## suppress tsbk/mbt/pdu received over vc
|
||||
return True
|
||||
if chan.role == 'uplink' and t in [-1, 7, 12]: ## suppress as above and also timeout
|
||||
return True
|
||||
return False
|
||||
|
||||
def process_trunked_qmsg(self, msg, msgq_id): # p25 trunked message
|
||||
if self.trunk_rx is None:
|
||||
return
|
||||
if self.filtered(msg, msgq_id):
|
||||
return
|
||||
self.trunk_rx.process_qmsg(msg)
|
||||
self.trunk_rx.parallel_hunt_cc()
|
||||
self.do_error_tracking()
|
||||
|
||||
def configure_devices(self, config):
|
||||
self.devices = []
|
||||
|
@ -199,12 +742,22 @@ class rx_block (gr.top_block):
|
|||
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
|
||||
d = abs(chan['frequency'] - dev.frequency)
|
||||
nf = dev.sample_rate / 2
|
||||
nf = dev.sample_rate // 2
|
||||
if d + 6250 <= nf:
|
||||
return dev
|
||||
return None
|
||||
|
@ -212,13 +765,43 @@ 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_id = len(self.channels) + 1
|
||||
chan = channel(cfg, dev, self.verbosity, msgq=self.output_q, msgq_id = msgq_id, role=decode_d['role'])
|
||||
for k in decode_d.keys():
|
||||
setattr(chan, k, decode_d[k])
|
||||
self.channels.append(chan)
|
||||
self.connect(dev.src, chan.demod, chan.decoder)
|
||||
sys.stderr.write('assigning channel "%s" (channel id %d) to device "%s"\n' % (chan.name, chan.msgq_id, dev.name))
|
||||
if 'log_if' in cfg.keys():
|
||||
chan.logfile_if = blocks.file_sink(gr.sizeof_gr_complex, 'if-%d-%s' % (chan.config['if_rate'], cfg['log_if']))
|
||||
if cfg['demod_type'] == 'cqpsk':
|
||||
chan.demod.connect_complex('agc', chan.logfile_if)
|
||||
else:
|
||||
chan.demod.connect_complex('if_out', chan.logfile_if)
|
||||
if 'log_symbols' in cfg.keys():
|
||||
chan.logfile = blocks.file_sink(gr.sizeof_char, cfg['log_symbols'])
|
||||
self.connect(chan.demod, chan.logfile)
|
||||
|
||||
def find_audio_channel(self):
|
||||
for chan in self.channels: # pass1 - look for 'vc'
|
||||
if chan.role == 'vc' and chan.audio_port:
|
||||
return chan
|
||||
for chan in self.channels: # pass2 - any chan with audio port specified
|
||||
if chan.audio_port:
|
||||
return chan
|
||||
return self.channels[0]
|
||||
|
||||
def scan_channels(self):
|
||||
for chan in self.channels:
|
||||
|
@ -233,28 +816,49 @@ 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(),)
|
||||
print ('Ready for GDB to attach (pid = %d)' % (os.getpid(),))
|
||||
raw_input("Press 'Enter' to continue...")
|
||||
|
||||
if options.config_file == '-':
|
||||
config = json.loads(sys.stdin.read())
|
||||
else:
|
||||
config = json.loads(open(options.config_file).read())
|
||||
self.tb = rx_block(options.verbosity, config = byteify(config))
|
||||
self.tb = rx_block(options.verbosity, config = byteify(config), trunk_conf_file=options.trunk_conf_file, terminal_type=options.terminal_type, track_errors=options.freq_error_tracking, udp_player = options.udp_player)
|
||||
sys.stderr.write('python version detected: %s\n' % sys.version)
|
||||
sys.stderr.flush()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.tb.start()
|
||||
while self.keep_running:
|
||||
time.sleep(1)
|
||||
except:
|
||||
sys.stderr.write('main: exception occurred\n')
|
||||
sys.stderr.write('main: exception:\n%s\n' % traceback.format_exc())
|
||||
self.tb.start()
|
||||
if self.options.monitor_stdin:
|
||||
print("Running. press ENTER to quit")
|
||||
while self.keep_running:
|
||||
if self.options.monitor_stdin and select.select([sys.stdin,],[],[],0.0)[0]:
|
||||
c = sys.stdin.read(1)
|
||||
self.keep_running = False
|
||||
break
|
||||
msg = self.tb.output_q.delete_head()
|
||||
if self.tb.process_msg(msg):
|
||||
self.keep_running = False
|
||||
break
|
||||
print('Quitting - now stopping top block')
|
||||
self.tb.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
rx = rx_main()
|
||||
rx.run()
|
||||
try:
|
||||
rx.run()
|
||||
except KeyboardInterrupt:
|
||||
rx.keep_running = False
|
||||
print('Program ending')
|
||||
time.sleep(1)
|
||||
|
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# (C) Copyright 2020 Max H. Parke, KA1RBI
|
||||
#
|
||||
# This file is part of OP25
|
||||
#
|
||||
# OP25 is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
# License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OP25; see the file COPYING. If not, write to the Free
|
||||
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
|
||||
#
|
||||
# nxdn trunking:
|
||||
# - CAC decoding
|
||||
#
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.append('tdma')
|
||||
from bit_utils import *
|
||||
|
||||
def locid(id):
|
||||
save_id = mk_int(id)
|
||||
cat = mk_int(id[:2])
|
||||
if cat == 0:
|
||||
ssize = 10
|
||||
elif cat == 2:
|
||||
ssize = 14
|
||||
elif cat == 1:
|
||||
ssize = 17
|
||||
else:
|
||||
return {'category': -1, 'system': -1, 'site': -1, 'id': '0x%x' % save_id}
|
||||
id = id[2:]
|
||||
syscode = mk_int(id[:ssize])
|
||||
id = id[ssize:]
|
||||
sitecode = mk_int(id)
|
||||
return {'category': cat, 'system': syscode, 'site': sitecode, 'id': '0x%x' % save_id}
|
||||
|
||||
def mk_freq(f):
|
||||
### todo: UHF currently untested; may fail at 400 MHz
|
||||
return int(f * 1250 + 100000000) # frequency in Hz
|
||||
|
||||
def cac_message(s):
|
||||
d = {}
|
||||
bits = []
|
||||
for c in s:
|
||||
for i in range(8):
|
||||
bits.append((c >> (7-i)) & 1)
|
||||
d['structure'] = mk_int(bits[:2])
|
||||
d['ran'] = mk_int(bits[2:8])
|
||||
bits = bits[8:]
|
||||
msg_type = mk_int(bits[2:8])
|
||||
d['msg_typeid'] = msg_type
|
||||
if msg_type == 0x18: # SITE_INFO
|
||||
assert len(bits) == 144
|
||||
d['msg_type'] = 'SITE_INFO'
|
||||
bits = bits[8:]
|
||||
d['location_id'] = locid(bits[:24])
|
||||
bits = bits[24:]
|
||||
d['channel_info'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['service_info'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['restr_info'] = mk_int(bits[:24])
|
||||
bits = bits[24:]
|
||||
d['access_info'] = mk_int(bits[:24])
|
||||
bits = bits[24:]
|
||||
d['version_no'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['adjacent_alloc'] = mk_int(bits[:4])
|
||||
bits = bits[4:]
|
||||
d['cc1'] = mk_int(bits[:10])
|
||||
bits = bits[10:]
|
||||
d['cc2'] = mk_int(bits[:10])
|
||||
elif msg_type == 0x19: # SRV_INFO
|
||||
assert len(bits) >= 72
|
||||
d['msg_type'] = 'SRV_INFO'
|
||||
bits = bits[8:]
|
||||
d['location_id'] = locid(bits[:24])
|
||||
bits = bits[24:]
|
||||
d['service_info'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['restr_info'] = mk_int(bits[:24])
|
||||
elif msg_type == 0x1a: # CCH_INFO
|
||||
assert len(bits) >= 72
|
||||
d['msg_type'] = 'CCH_INFO'
|
||||
bits = bits[8:]
|
||||
d['location_id'] = locid(bits[:24])
|
||||
bits = bits[24:]
|
||||
d['flags1'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['cc1'] = mk_freq(mk_int(bits[:16]))
|
||||
bits = bits[16:]
|
||||
d['cc2'] = mk_freq(mk_int(bits[:16]))
|
||||
elif msg_type == 0x1b: # ADJ_SITE_INFO
|
||||
assert len(bits) >= 72
|
||||
d['msg_type'] = 'ADJ_SITE_INFO'
|
||||
d1 = {}
|
||||
d2 = {}
|
||||
bits = bits[8:]
|
||||
d1['location'] = locid(bits[:24])
|
||||
bits = bits[24:]
|
||||
d1['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d1['cc'] = mk_freq(mk_int(bits[:16]))
|
||||
bits = bits[16:]
|
||||
d2['location'] = locid(bits[:24])
|
||||
bits = bits[24:]
|
||||
d2['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d2['cc'] = mk_freq(mk_int(bits[:16]))
|
||||
bits = bits[16:]
|
||||
d['sites'] = [d1, d2]
|
||||
#d['location_3'] = locid(bits[:24])
|
||||
#bits = bits[24:]
|
||||
#d['option_3'] = mk_int(bits[:6])
|
||||
#bits = bits[6:]
|
||||
#d['cc_3'] = mk_int(bits[:10])
|
||||
elif msg_type == 0x01: # VCALL_RESP
|
||||
assert len(bits) >= 64
|
||||
d['msg_type'] = 'VCALL_RESP'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['call_type'] = mk_int(bits[:3])
|
||||
d['call_option'] = mk_int(bits[3:8])
|
||||
bits = bits[8:]
|
||||
d['source_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['destination_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['cause'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
elif msg_type == 0x09: # DCALL_RESP
|
||||
assert len(bits) >= 64
|
||||
d['msg_type'] = 'DCALL_RESP'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['call_type'] = mk_int(bits[:3])
|
||||
d['call_option'] = mk_int(bits[3:8])
|
||||
bits = bits[8:]
|
||||
d['source_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['destination_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['cause'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
elif msg_type == 0x04: # VCALL_ASSGN
|
||||
assert len(bits) >= 72
|
||||
bits2 = bits
|
||||
s = ''
|
||||
while len(bits2):
|
||||
s += '%02x' % mk_int(bits2[:8])
|
||||
bits2 = bits2[8:]
|
||||
d['hexdata'] = s
|
||||
d['msg_type'] = 'VCALL_ASSGN'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['call_type'] = mk_int(bits[:3])
|
||||
d['call_option'] = mk_int(bits[3:8])
|
||||
bits = bits[8:]
|
||||
d['source_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['group_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['timer'] = mk_int(bits[:8])
|
||||
d['channel'] = mk_int(bits[6:16])
|
||||
bits = bits[8:]
|
||||
d['f1'] = mk_freq(mk_int(bits[:16]))
|
||||
bits = bits[16:]
|
||||
d['f2'] = mk_freq(mk_int(bits[:16]))
|
||||
elif msg_type == 0x0e: # DCALL_ASSGN
|
||||
assert len(bits) >= 104
|
||||
d['msg_type'] = 'DCALL_ASSGN'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['call_type'] = mk_int(bits[:3])
|
||||
d['call_option'] = mk_int(bits[3:8])
|
||||
bits = bits[8:]
|
||||
d['source_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['group_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['timer'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['f1'] = mk_freq(mk_int(bits[:16]))
|
||||
bits = bits[16:]
|
||||
d['f2'] = mk_freq(mk_int(bits[:16]))
|
||||
elif msg_type == 0x20: # REG_RESP
|
||||
assert len(bits) >= 72
|
||||
d['msg_type'] = 'REG_RESP'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['location id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['unit_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['group_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['cause'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['visitor_unit'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['visitor_group'] = mk_int(bits[:16])
|
||||
elif msg_type == 0x22: # REG_C_RESP
|
||||
assert len(bits) >= 56
|
||||
d['msg_type'] = 'REG_C_RESP'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['location id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['unit_id'] = mk_int(bits[:16])
|
||||
elif msg_type == 0x24: # GRP_REG_RESP
|
||||
assert len(bits) >= 72
|
||||
d['msg_type'] = 'GRP_REG_RESP'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['destination id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['group_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['cause'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['visitor_group_id'] = mk_int(bits[:16])
|
||||
elif msg_type == 0x32: # STAT_REQ
|
||||
assert len(bits) >= 72
|
||||
d['msg_type'] = 'STAT_REQ'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['call_type'] = mk_int(bits[:3])
|
||||
d['call_option'] = mk_int(bits[3:8])
|
||||
bits = bits[8:]
|
||||
d['source id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['destination_id'] = mk_int(bits[:16])
|
||||
bits = bits[8:]
|
||||
d['spare'] = mk_int(bits[:8])
|
||||
status = bits[8:]
|
||||
elif msg_type == 0x33: # STAT_RESP
|
||||
assert len(bits) >= 64
|
||||
d['msg_type'] = 'STAT_RESP'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['call_type'] = mk_int(bits[:3])
|
||||
d['call_option'] = mk_int(bits[3:8])
|
||||
bits = bits[8:]
|
||||
d['source id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['destination_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['cause'] = mk_int(bits[:8])
|
||||
elif msg_type == 0x38: # SDCALL_REQ_HEADER
|
||||
assert len(bits) >= 64
|
||||
d['msg_type'] = 'SDCALL_REQ_HEADER'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['call_type'] = mk_int(bits[:3])
|
||||
d['call_option'] = mk_int(bits[3:8])
|
||||
bits = bits[8:]
|
||||
d['source id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['destination_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['cipher_type'] = mk_int(bits[:2])
|
||||
d['key_id'] = mk_int(bits[2:8])
|
||||
elif msg_type == 0x39: # SDCALL_REQ_USERDATA
|
||||
assert len(bits) >= 64
|
||||
d['msg_type'] = 'SDCALL_REQ_USERDATA'
|
||||
bits = bits[8:]
|
||||
d['packet_frame'] = mk_int(bits[:4])
|
||||
d['block_number'] = mk_int(bits[4:8])
|
||||
bits = bits[8:]
|
||||
s = ''
|
||||
while len(bits):
|
||||
s += '%02x' % mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['hexdata'] = s
|
||||
elif msg_type == 0x3b: # SDCALL_RESP
|
||||
assert len(bits) >= 64
|
||||
d['msg_type'] = 'SDCALL_RESP'
|
||||
bits = bits[8:]
|
||||
d['option'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
d['call_type'] = mk_int(bits[:3])
|
||||
d['call_option'] = mk_int(bits[3:8])
|
||||
bits = bits[8:]
|
||||
d['source id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['destination_id'] = mk_int(bits[:16])
|
||||
bits = bits[16:]
|
||||
d['cause'] = mk_int(bits[:8])
|
||||
bits = bits[8:]
|
||||
else: # msg type unhandled
|
||||
d['msg_type'] = 'UNSUPPORTED 0x%x' % (msg_type)
|
||||
return d
|
|
@ -0,0 +1,15 @@
|
|||
[Unit]
|
||||
Description=op25-liq
|
||||
After=syslog.target network.target nss-lookup.target network-online.target
|
||||
Requires=network-online.target
|
||||
|
||||
[Service]
|
||||
User=1000
|
||||
Group=1000
|
||||
WorkingDirectory=/home/pi/op25/op25/gr-op25_repeater/apps
|
||||
ExecStart=/usr/bin/liquidsoap op25.liq
|
||||
RestartSec=5
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/liquidsoap
|
||||
|
||||
# Example liquidsoap streaming from op25 to icecast
|
||||
# (c) 2019, 2020 gnorbury@bondcar.com, wllmbecks@gmail.com
|
||||
#
|
||||
|
||||
set("log.stdout", true)
|
||||
set("log.file", false)
|
||||
set("log.level", 1)
|
||||
|
||||
# Make the native sample rate compatible with op25
|
||||
set("frame.audio.samplerate", 8000)
|
||||
|
||||
input = mksafe(input.external(buffer=0.02, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -x 2 -s"))
|
||||
# Consider increasing the buffer value on slow systems such as RPi3. e.g. buffer=0.25
|
||||
# Longer buffer results in less choppy audio but at the expense of increased latency.
|
||||
|
||||
|
||||
|
||||
# OPTIONAL AUDIO SIGNAL PROCESSING BLOCKS
|
||||
# Uncomment to enable
|
||||
#
|
||||
# High pass filter
|
||||
#input = filter.iir.butterworth.high(frequency = 200.0, order = 4, input)
|
||||
|
||||
# Low pass filter
|
||||
#input = filter.iir.butterworth.low(frequency = 3250.0, order = 4, input)
|
||||
|
||||
# Normalization
|
||||
#input = normalize(input, gain_max = 3.0, gain_min = -3.0, target = -16.0, threshold = -40.0)
|
||||
|
||||
|
||||
|
||||
# LOCAL AUDIO OUTPUT
|
||||
# Uncomment the appropriate line below to enable local sound
|
||||
#
|
||||
# Default audio subsystem
|
||||
out (input)
|
||||
#
|
||||
# PulseAudio
|
||||
#output.pulseaudio(input)
|
||||
#
|
||||
# ALSA
|
||||
#output.alsa(input)
|
||||
|
||||
|
||||
|
||||
# ICECAST STREAMING
|
||||
# Uncomment to enable output to an icecast server
|
||||
# Change the "host", "password", and "mount" strings appropriately first!
|
||||
# For metadata to work properly, the host address given here MUST MATCH the address in op25's meta.json file
|
||||
#
|
||||
#output.icecast(%mp3(bitrate=16, samplerate=22050, stereo=false), description="op25", genre="Public Safety", url="", fallible=false, icy_metadata="false", host="localhost", port=8000, mount="mountpoint", password="hackme", mean(input))
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import os
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///%s/../op25-data.db' % (os.path.dirname(__file__))
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
@ -0,0 +1,844 @@
|
|||
#! /usr/bin/env python
|
||||
|
||||
# Copyright 2021 Max H. Parke KA1RBI, Michael Rose
|
||||
#
|
||||
# This file is part of OP25
|
||||
#
|
||||
# OP25 is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
# License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with OP25; see the file COPYING. If not, write to the Free
|
||||
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
# 02110-1301, USA.
|
||||
|
||||
import time
|
||||
from time import sleep
|
||||
from time import gmtime, strftime
|
||||
import os
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
import sys
|
||||
import traceback
|
||||
import math
|
||||
import json
|
||||
import click
|
||||
import datetime
|
||||
from datatables import ColumnDT, DataTables
|
||||
from flask import Flask, jsonify, render_template, request, redirect, session
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy import func, desc, and_, or_, case, delete, insert, update, exc
|
||||
from sqlalchemy.orm import Query
|
||||
from sqlalchemy.exc import OperationalError
|
||||
import sqlalchemy.types as types
|
||||
from shutil import copyfile
|
||||
|
||||
sys.path.append('..') # for emap
|
||||
from emap import oplog_map, cc_events, cc_desc
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_pyfile("../app.cfg")
|
||||
app.config['SQLALCHEMY_ECHO'] = False # set to True to send sql statements to the console
|
||||
|
||||
# enables session variables to be used
|
||||
app.secret_key = b'kH8HT0ucrh' # random bytes - this key not used anywhere else
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
# db.init_app(app)
|
||||
|
||||
with app.app_context():
|
||||
try:
|
||||
db.reflect()
|
||||
except OperationalError as e:
|
||||
raise(e) # database is locked by another process
|
||||
|
||||
class MyDateType(types.TypeDecorator):
|
||||
impl = types.REAL
|
||||
def process_result_value(self, value, dialect):
|
||||
return datetime.datetime.fromtimestamp(value).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
class column_helper(object):
|
||||
# convenience class - enables columns to be referenced as
|
||||
# for example, Foo.bar instead of Foo['bar']
|
||||
def __init__(self, table):
|
||||
self.table_ = db.metadata.tables[table]
|
||||
cols = self.table_.columns
|
||||
for k in cols.keys():
|
||||
setattr(self, k, cols[k])
|
||||
|
||||
def dbstate():
|
||||
database = app.config['SQLALCHEMY_DATABASE_URI'][10:]
|
||||
if not os.path.isfile(database):
|
||||
return 1 # db file does not exist
|
||||
fs = os.path.getsize(database)
|
||||
if fs < 1024:
|
||||
return 2 # file size too small
|
||||
DataStore = column_helper('data_store')
|
||||
rows = db.session.query(DataStore.id).count()
|
||||
if rows < 1:
|
||||
return 4 # no rows present
|
||||
return 0
|
||||
|
||||
# clears the sm (successMessage) after being used in jinja
|
||||
def clear_sm():
|
||||
session['sm'] = 0
|
||||
return '' #must be an empty string or 'None' is displayed in the template
|
||||
|
||||
def t_gmt():
|
||||
t = time.strftime("%a, %d %b %Y %H:%M:%S", time.gmtime())
|
||||
return t
|
||||
|
||||
def t_loc():
|
||||
t = strftime("%a, %d %b %Y %H:%M:%S %Z")
|
||||
return t
|
||||
|
||||
# make these functions available to jinja
|
||||
app.jinja_env.globals.update(t_gmt=t_gmt)
|
||||
app.jinja_env.globals.update(t_loc=t_loc)
|
||||
app.jinja_env.globals.update(clear_sm=clear_sm)
|
||||
|
||||
# for displaying the db file size, shamelessly stolen from SO
|
||||
def convert_size(size_bytes):
|
||||
if size_bytes == 0:
|
||||
return "0 B"
|
||||
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
i = int(math.floor(math.log(size_bytes, 1024)))
|
||||
p = math.pow(1024, i)
|
||||
s = round(size_bytes / p, 2)
|
||||
return "%s %s" % (s, size_name[i])
|
||||
|
||||
def dbStats():
|
||||
DataStore = column_helper('data_store')
|
||||
DataStore = column_helper('data_store')
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
DataStore.time.type = MyDateType()
|
||||
rows = db.session.query(func.count(DataStore.id)).scalar()
|
||||
if rows == 0:
|
||||
return(0, 0, 0, 0, 0, 0, 0)
|
||||
|
||||
sys_count = db.session.query(DataStore.sysid) \
|
||||
.distinct(DataStore.sysid) \
|
||||
.group_by(DataStore.sysid) \
|
||||
.filter(DataStore.sysid != 0) \
|
||||
.count()
|
||||
|
||||
# TODO: talkgroups and subs should be distinct by system
|
||||
talkgroups = db.session.query(DataStore.tgid) \
|
||||
.distinct(DataStore.tgid) \
|
||||
.group_by(DataStore.tgid) \
|
||||
.count()
|
||||
|
||||
subs = db.session.query(DataStore.suid) \
|
||||
.distinct(DataStore.suid) \
|
||||
.group_by(DataStore.suid) \
|
||||
.count()
|
||||
|
||||
firstRec = db.session.query(DataStore.time, func.min(DataStore.time)).scalar()
|
||||
lastRec = db.session.query(DataStore.time, func.max(DataStore.time)).scalar()
|
||||
f = app.config['SQLALCHEMY_DATABASE_URI'][10:] # db file name
|
||||
dbsize = convert_size(os.path.getsize(f))
|
||||
return(rows, sys_count, talkgroups, subs, firstRec, lastRec, dbsize, f)
|
||||
|
||||
def sysList():
|
||||
DataStore = column_helper('data_store')
|
||||
rows = db.session.query(func.count(DataStore.id)).scalar()
|
||||
if rows == 0:
|
||||
return []
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
sysList = db.session.query(DataStore.sysid, SysIDTags.tag.label('tag')) \
|
||||
.distinct(DataStore.sysid) \
|
||||
.outerjoin(SysIDTags.table_, SysIDTags.sysid == DataStore.sysid) \
|
||||
.filter(DataStore.sysid != 0)
|
||||
return sysList
|
||||
|
||||
def read_tsv(filename): # used by import_tsv and inspect_tsv, careful w/ changes
|
||||
rows = []
|
||||
with open(filename, 'r') as f:
|
||||
lines = f.read().rstrip().split('\n')
|
||||
for i in range(len(lines)):
|
||||
a = lines[i].split('\t')
|
||||
if i == 0: # check hdr
|
||||
if not a[0].strip().isdigit():
|
||||
continue
|
||||
if not a[0].strip().isdigit(): # check each a[0] for wildcards and skip (continue) if wildcards found
|
||||
continue
|
||||
rid = int(a[0])
|
||||
tag = a[1]
|
||||
priority = 0 if len(a) < 3 else int(a[2])
|
||||
s = (rid, tag, priority)
|
||||
rows.append(s)
|
||||
return rows
|
||||
|
||||
def import_tsv(argv):
|
||||
UnitIDTags = column_helper('unit_id_tags')
|
||||
TGIDTags = column_helper('tgid_tags')
|
||||
cmd = argv[1]
|
||||
filename = argv[2]
|
||||
sysid = int(argv[3])
|
||||
if cmd == 'import_tgid':
|
||||
tbl = TGIDTags
|
||||
elif cmd == 'import_unit':
|
||||
tbl = UnitIDTags
|
||||
else:
|
||||
print('%s unsupported' % (cmd))
|
||||
return
|
||||
rows = read_tsv(filename)
|
||||
rm = 0 # records matched
|
||||
nr = 0 # new records
|
||||
dr = 0 # duplicate records
|
||||
if len(rows):
|
||||
for i in rows:
|
||||
recCount = db.session.query(tbl.table_).where(and_(tbl.rid == i[0], tbl.sysid == argv[3])).count()
|
||||
if recCount == 1:
|
||||
# update record
|
||||
q = update(tbl.table_) \
|
||||
.where(and_(tbl.rid == i[0], tbl.sysid == argv[3])) \
|
||||
.values(rid = int(i[0]), sysid = int(argv[3]), tag = i[1], priority = int(i[2]))
|
||||
db.session.execute(q)
|
||||
db.session.commit()
|
||||
rm +=1
|
||||
elif recCount == 0:
|
||||
# insert record
|
||||
q = insert(tbl.table_).values(rid = int(i[0]), sysid = int(argv[3]), tag = i[1], priority = int(i[2]))
|
||||
db.session.execute(q)
|
||||
db.session.commit()
|
||||
nr += 1
|
||||
else:
|
||||
# delete all of the duplicates and insert new (duplicates break things)
|
||||
print('command %s - db error - %s records for %s %s' % (cmd, recCount, i[0], i[1]))
|
||||
delRec = delete(TGIDTags.table_).where(and_(tbl.rid == i[0], tbl.sysid == argv[3]))
|
||||
db.session.execute(delRec)
|
||||
db.session.commit()
|
||||
q = insert(tbl.table_).values(rid = int(i[0]), sysid = int(argv[3]), tag = i[1], priority = int(i[2]))
|
||||
db.session.execute(q)
|
||||
db.session.commit()
|
||||
dr += 1
|
||||
return(rm, nr, dr)
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
ds = dbstate()
|
||||
if ds != 0:
|
||||
return redirect('error?code=%s' % ds)
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
params['cc_desc'] = cc_desc
|
||||
return render_template("home.html", project="op25", params=params, dbstats=dbStats(), sysList=sysList())
|
||||
|
||||
@app.route("/about")
|
||||
def about():
|
||||
ds = dbstate()
|
||||
if ds != 0:
|
||||
return redirect('error?code=%s' % ds)
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
params['cc_desc'] = cc_desc
|
||||
return render_template("about.html", project="op25", params=params, sysList=sysList())
|
||||
|
||||
# error page for database errors
|
||||
@app.route("/error")
|
||||
def error_page():
|
||||
params = request.args.to_dict()
|
||||
params['file'] = app.config['SQLALCHEMY_DATABASE_URI'][10:]
|
||||
return render_template("error.html", params=params, file=params['file'], code=int(params['code']))
|
||||
|
||||
# Inspect TSV (import) - returns a table of the tsv for display in a div, accessed by ajax
|
||||
@app.route("/inspect")
|
||||
def inspect():
|
||||
params = request.args.to_dict()
|
||||
f = os.getcwd() + '/../' + params['file']
|
||||
i = read_tsv(f)
|
||||
return render_template("inspect.html", i=i)
|
||||
|
||||
# edit and import tags
|
||||
@app.route("/edit_tags")
|
||||
def edit_tags():
|
||||
UnitIDTags = column_helper('unit_id_tags')
|
||||
TGIDTags = column_helper('tgid_tags')
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
if 'cmd' not in params.keys(): # render talkgroup by default
|
||||
params['cmd'] = 'tgid'
|
||||
cmd = params['cmd']
|
||||
session['cmd'] = cmd
|
||||
systems = db.session.query(SysIDTags.sysid, SysIDTags.tag)
|
||||
p = os.getcwd() + '/..'
|
||||
tsvs = []
|
||||
for root, dirs, files in os.walk(p, topdown=True):
|
||||
for file in files:
|
||||
if file.endswith(".tsv") and not file.startswith("._"):
|
||||
print(os.path.join(root, file))
|
||||
tsvs.append(os.path.join(root, file))
|
||||
tsvs.sort()
|
||||
return render_template("edit_tags.html", params=params, systems=systems, sysList=sysList(), p=p, cmd=cmd, tsvs=tsvs)
|
||||
|
||||
# data for tags table editor
|
||||
@app.route("/edittg")
|
||||
def edittg():
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
cmd = params['cmd']
|
||||
sysid = int(params['sysid'])
|
||||
UnitIDTags = column_helper('unit_id_tags')
|
||||
TGIDTags = column_helper('tgid_tags')
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
if cmd == 'tgid':
|
||||
tbl = TGIDTags
|
||||
if cmd == 'unit':
|
||||
tbl = UnitIDTags
|
||||
column_d = {
|
||||
'tgid': [
|
||||
ColumnDT(TGIDTags.id),
|
||||
ColumnDT(TGIDTags.sysid),
|
||||
ColumnDT(TGIDTags.rid),
|
||||
ColumnDT(TGIDTags.tag),
|
||||
ColumnDT(TGIDTags.id)
|
||||
],
|
||||
'unit': [
|
||||
ColumnDT(UnitIDTags.id),
|
||||
ColumnDT(UnitIDTags.sysid),
|
||||
ColumnDT(UnitIDTags.rid),
|
||||
ColumnDT(UnitIDTags.tag),
|
||||
ColumnDT(UnitIDTags.id)
|
||||
]
|
||||
}
|
||||
q = db.session.query(tbl.id, tbl.sysid, tbl.rid, tbl.tag).order_by(tbl.rid)
|
||||
if sysid != 0:
|
||||
q = q.filter(tbl.sysid == sysid)
|
||||
rowTable = DataTables(params, q, column_d[cmd])
|
||||
js = jsonify(rowTable.output_result())
|
||||
return js
|
||||
|
||||
#dtd = delete tag data
|
||||
@app.route("/dtd")
|
||||
def dtd():
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
cmd = params['cmd']
|
||||
UnitIDTags = column_helper('unit_id_tags')
|
||||
TGIDTags = column_helper('tgid_tags')
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
recId = params['id']
|
||||
if cmd == 'tgid':
|
||||
tbl = TGIDTags
|
||||
if cmd == 'unit':
|
||||
tbl = UnitIDTags
|
||||
delRec = delete(tbl.table_).where(tbl.id == recId)
|
||||
db.session.execute(delRec)
|
||||
db.session.commit()
|
||||
session['sm'] = 2
|
||||
return redirect('/edit_tags?cmd=' + cmd)
|
||||
|
||||
#utd = update tag data
|
||||
@app.route("/utd")
|
||||
def utd():
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
cmd = params['cmd']
|
||||
UnitIDTags = column_helper('unit_id_tags')
|
||||
TGIDTags = column_helper('tgid_tags')
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
recId = params['id']
|
||||
tag = params['tag']
|
||||
if cmd == 'tgid':
|
||||
tbl = TGIDTags
|
||||
if cmd == 'unit':
|
||||
tbl = UnitIDTags
|
||||
upRec = update(tbl.table_).where(tbl.id == recId).values(tag=tag)
|
||||
db.session.execute(upRec)
|
||||
db.session.commit()
|
||||
session['sm'] = 1
|
||||
return redirect('/edit_tags?cmd=' + cmd)
|
||||
|
||||
# import tags
|
||||
@app.route("/itt")
|
||||
def itt():
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
cmd = params['cmd']
|
||||
argv = [ None, 'import_' + cmd, os.getcwd() + '/../' + params['file'], params['sysid'] ]
|
||||
session['imp_results'] = import_tsv(argv)
|
||||
session['sm'] = 3
|
||||
return redirect('/edit_tags?cmd=' + cmd)
|
||||
|
||||
# delete all talkgroup/subscriber tags
|
||||
@app.route("/delTags")
|
||||
def delTags():
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
cmd = params['cmd']
|
||||
UnitIDTags = column_helper('unit_id_tags')
|
||||
TGIDTags = column_helper('tgid_tags')
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
sysid = params['sysid']
|
||||
if cmd == 'tgid':
|
||||
tbl = TGIDTags
|
||||
if cmd == 'unit':
|
||||
tbl = UnitIDTags
|
||||
delRec = delete(tbl.table_).where(tbl.sysid == sysid)
|
||||
db.session.execute(delRec)
|
||||
db.session.commit()
|
||||
db.session.execute("VACUUM") # sqlite3 clean up -- reduces file size
|
||||
session['sm'] = 4
|
||||
return redirect('/edit_tags?cmd=' + cmd)
|
||||
|
||||
# system tag editor functions (entirely separate from the tags editor above)
|
||||
@app.route("/editsys")
|
||||
def editsys():
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
params['cc_desc'] = cc_desc
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
systems = db.session.query(SysIDTags.sysid, SysIDTags.tag)
|
||||
return render_template("editsys.html", params=params, systems=systems, sysList=sysList())
|
||||
|
||||
#dsd = delete system data
|
||||
@app.route("/dsd")
|
||||
def dsd():
|
||||
params = request.args.to_dict()
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
recId = params['id']
|
||||
delRec = delete(SysIDTags.table_).where(SysIDTags.id == recId)
|
||||
db.session.execute(delRec)
|
||||
db.session.commit()
|
||||
return redirect('/editsys')
|
||||
|
||||
#usd = update system data
|
||||
@app.route("/usd")
|
||||
def usd():
|
||||
params = request.args.to_dict()
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
recId = params['id']
|
||||
tag = params['tag']
|
||||
upRec = update(SysIDTags.table_).where(SysIDTags.id == recId).values(tag=tag)
|
||||
db.session.execute(upRec)
|
||||
db.session.commit()
|
||||
return redirect('/editsys')
|
||||
|
||||
#esd = edit system data (system tags)
|
||||
@app.route("/esd")
|
||||
def esd():
|
||||
params = request.args.to_dict()
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
column_d = {
|
||||
's': [
|
||||
ColumnDT(SysIDTags.id),
|
||||
ColumnDT(SysIDTags.sysid),
|
||||
ColumnDT(SysIDTags.tag),
|
||||
ColumnDT(SysIDTags.id)
|
||||
]
|
||||
}
|
||||
q = db.session.query(SysIDTags.id, SysIDTags.sysid, SysIDTags.tag)
|
||||
rowTable = DataTables(params, q, column_d['s'])
|
||||
js = jsonify(rowTable.output_result())
|
||||
return js
|
||||
|
||||
#asd = add system data
|
||||
@app.route("/asd")
|
||||
def asd():
|
||||
params = request.args.to_dict()
|
||||
ns = params['id']
|
||||
nt = params['tag']
|
||||
#todo: validate input
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
insRec = insert(SysIDTags.table_).values(sysid=ns, tag=nt)
|
||||
db.session.execute(insRec)
|
||||
db.session.commit()
|
||||
return redirect('/editsys')
|
||||
|
||||
# purge database functions
|
||||
@app.route("/purge")
|
||||
def purge():
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
DataStore = column_helper('data_store')
|
||||
destfile = ''
|
||||
b = False
|
||||
if 'bu' in params.keys():
|
||||
if params['bu'] == 'true':
|
||||
b = True
|
||||
t = strftime("%Y%m%d_%H%M%S")
|
||||
destfile = 'op25-backup-%s.db' % t
|
||||
src = app.config['SQLALCHEMY_DATABASE_URI'][10:]
|
||||
s = src.split('/')
|
||||
f = s[-1]
|
||||
dst = src.replace(f, destfile)
|
||||
if 'simulate' in params.keys():
|
||||
simulate = params['simulate']
|
||||
if 'action' in params.keys():
|
||||
if params['action'] == 'purge':
|
||||
sd = params['sd']
|
||||
ed = params['ed']
|
||||
sysid = int(params['sysid'])
|
||||
delRec = delete(DataStore.table_).where(DataStore.time >= int(sd), DataStore.time <= int(ed))
|
||||
recCount = db.session.query(DataStore.id).filter(and_(DataStore.time >= int(sd), DataStore.time <= int(ed)))
|
||||
if sysid != 0:
|
||||
recCount = recCount.filter(DataStore.sysid == sysid)
|
||||
delRec = delRec.where(DataStore.sysid == sysid)
|
||||
if 'kv' in params.keys(): # keep voice calls
|
||||
if params['kv'] == 'true':
|
||||
recCount = recCount.where(and_(DataStore.opcode != 0, DataStore.opcode != 2))
|
||||
delRec = delRec.where(and_(DataStore.opcode != 0, DataStore.opcode != 2))
|
||||
recCount = recCount.count()
|
||||
dispQuery = delRec.compile(compile_kwargs={"literal_binds": True})
|
||||
if simulate == 'false':
|
||||
if b == True:
|
||||
copyfile(src, dst)
|
||||
db.session.execute(delRec)
|
||||
db.session.commit()
|
||||
db.session.execute("VACUUM") # sqlite3 clean up -- reduces file size
|
||||
successMessage = 1
|
||||
else:
|
||||
successMessage = 2
|
||||
else:
|
||||
recCount = 0
|
||||
successMessage = 0
|
||||
dispQuery = ''
|
||||
|
||||
return render_template("purge.html", \
|
||||
project="op25", \
|
||||
params=params, \
|
||||
dbstats=dbStats(), \
|
||||
sysList=sysList(), \
|
||||
successMessage=successMessage, \
|
||||
recCount=recCount, \
|
||||
dispQuery=dispQuery, \
|
||||
destfile=destfile )
|
||||
|
||||
# displays all logs w/ datatables
|
||||
@app.route("/logs")
|
||||
def logs():
|
||||
UnitIDTags = column_helper('unit_id_tags')
|
||||
TGIDTags = column_helper('tgid_tags')
|
||||
tag = ''
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = oplog_map.keys()
|
||||
params['cc_desc'] = cc_desc
|
||||
t = None if 'q' not in params.keys() else params['q']
|
||||
sysid = 0 if 'sysid' not in params.keys() else int(params['sysid'])
|
||||
if sysid != 0:
|
||||
if t is not None and params['r'] == 'tgid':
|
||||
q = db.session.query(TGIDTags.tag).where(and_(TGIDTags.rid == t, TGIDTags.sysid == sysid))
|
||||
if t is not None and params['r'] == 'su':
|
||||
q = db.session.query(UnitIDTags.tag).where(and_(UnitIDTags.rid == t, UnitIDTags.sysid == sysid))
|
||||
if q.count() > 0:
|
||||
tg = (db.session.execute(q).one())
|
||||
tag = (' - %s' % tg.tag)
|
||||
if params['r'] == 'cc_event':
|
||||
mapl = oplog_map[params['p'].strip()]
|
||||
params['ckeys'] = [s[1] for s in mapl if s[0] != 'opcode' and s[0] != 'cc_event']
|
||||
|
||||
return render_template("logs.html", \
|
||||
project="logs", \
|
||||
params=params, \
|
||||
sysList=sysList(), \
|
||||
tag=tag )
|
||||
|
||||
# data for /logs
|
||||
@app.route("/data")
|
||||
def data():
|
||||
"""Return server side data."""
|
||||
# GET parameters
|
||||
params = request.args.to_dict()
|
||||
|
||||
host_rid = None if 'host_rid' not in params.keys() else params['host_rid']
|
||||
host_function_type = None if 'host_function_type' not in params.keys() else params['host_function_type']
|
||||
host_function_param = None if 'host_function_param' not in params.keys() else params['host_function_param'].strip()
|
||||
|
||||
filter_tgid = None if 'tgid' not in params.keys() else int(params['tgid'].strip())
|
||||
filter_suid = None if 'suid' not in params.keys() else int(params['suid'].strip())
|
||||
|
||||
start_time = None if 'sdate' not in params.keys() else datetime.datetime.utcfromtimestamp(float(params['sdate']))
|
||||
end_time = None if 'edate' not in params.keys() else datetime.datetime.utcfromtimestamp(float(params['edate']))
|
||||
print(params)
|
||||
sysid = None if 'sysid' not in params.keys() else int(params['sysid'])
|
||||
|
||||
stime = int(params['sdate']) #used in the queries
|
||||
etime = int(params['edate']) #used in the queries
|
||||
|
||||
DataStore = column_helper('data_store')
|
||||
EventKeys = column_helper('event_keys')
|
||||
SysIDTags = column_helper('sysid_tags')
|
||||
UnitIDTags = column_helper('unit_id_tags')
|
||||
TGIDTags = column_helper('tgid_tags')
|
||||
LocRegResp = column_helper('loc_reg_resp_rv')
|
||||
|
||||
DataStore.time.type = MyDateType()
|
||||
|
||||
k = 'logs'
|
||||
if host_function_type:
|
||||
k = '%s_%s' % (k, host_function_type)
|
||||
|
||||
column_d = {
|
||||
'logs_su': [
|
||||
ColumnDT(TGIDTags.tag),
|
||||
ColumnDT(DataStore.tgid),
|
||||
ColumnDT(DataStore.tgid),
|
||||
],
|
||||
'logs_tgid': [
|
||||
ColumnDT(DataStore.suid),
|
||||
ColumnDT(UnitIDTags.tag),
|
||||
ColumnDT(DataStore.suid),
|
||||
ColumnDT(DataStore.time)
|
||||
],
|
||||
|
||||
'logs_calls': [
|
||||
ColumnDT(DataStore.time),
|
||||
ColumnDT(SysIDTags.tag),
|
||||
ColumnDT(DataStore.tgid),
|
||||
ColumnDT(TGIDTags.tag),
|
||||
ColumnDT(DataStore.frequency),
|
||||
ColumnDT(DataStore.suid)
|
||||
],
|
||||
'logs_joins': [
|
||||
ColumnDT(DataStore.time),
|
||||
ColumnDT(DataStore.opcode),
|
||||
ColumnDT(DataStore.sysid),
|
||||
ColumnDT(SysIDTags.tag),
|
||||
ColumnDT(LocRegResp.tag),
|
||||
ColumnDT(DataStore.tgid),
|
||||
ColumnDT(TGIDTags.tag),
|
||||
ColumnDT(DataStore.suid),
|
||||
ColumnDT(UnitIDTags.tag)
|
||||
],
|
||||
'logs_total_tgid': [
|
||||
ColumnDT(DataStore.sysid),
|
||||
ColumnDT(SysIDTags.tag),
|
||||
ColumnDT(DataStore.tgid),
|
||||
ColumnDT(TGIDTags.tag),
|
||||
ColumnDT(DataStore.tgid)
|
||||
],
|
||||
'logs_call_detail': [
|
||||
ColumnDT(DataStore.time),
|
||||
ColumnDT(DataStore.opcode),
|
||||
ColumnDT(SysIDTags.sysid),
|
||||
ColumnDT(SysIDTags.tag),
|
||||
ColumnDT(DataStore.tgid),
|
||||
ColumnDT(TGIDTags.tag),
|
||||
ColumnDT(DataStore.suid),
|
||||
ColumnDT(UnitIDTags.tag),
|
||||
ColumnDT(DataStore.frequency)
|
||||
]
|
||||
}
|
||||
|
||||
"""or_( EventKeys.tag == 'grp_v_ch_grant', EventKeys.tag == 'grp_v_ch_grant_exp'),"""
|
||||
|
||||
query_d = {
|
||||
'logs_total_tgid': db.session.query(DataStore.sysid, \
|
||||
SysIDTags.tag, \
|
||||
DataStore.tgid, \
|
||||
TGIDTags.tag, \
|
||||
func.count(DataStore.tgid).label('count'))
|
||||
.group_by(DataStore.tgid)
|
||||
.outerjoin(SysIDTags.table_, DataStore.sysid == SysIDTags.sysid)
|
||||
.outerjoin(TGIDTags.table_, DataStore.tgid == TGIDTags.rid)
|
||||
.filter(and_(DataStore.tgid != 0), (DataStore.frequency != None) ),
|
||||
|
||||
'logs_call_detail': db.session.query(DataStore.time, \
|
||||
DataStore.opcode, \
|
||||
DataStore.sysid, \
|
||||
SysIDTags.tag, \
|
||||
DataStore.tgid, \
|
||||
TGIDTags.tag, \
|
||||
DataStore.suid, \
|
||||
UnitIDTags.tag, \
|
||||
DataStore.frequency )
|
||||
.outerjoin(SysIDTags.table_, DataStore.sysid == SysIDTags.sysid)
|
||||
.outerjoin(TGIDTags.table_, and_(DataStore.tgid == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid))
|
||||
.outerjoin(UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid))
|
||||
.filter(and_(DataStore.tgid != 0), (DataStore.frequency != None) )
|
||||
.filter(or_(DataStore.opcode == 0, and_(DataStore.opcode == 2, DataStore.mfrid == 144)) ),
|
||||
|
||||
|
||||
'logs_tgid': db.session.query(DataStore.suid, \
|
||||
UnitIDTags.tag, \
|
||||
func.count(DataStore.suid).label('count'), func.max(DataStore.time).label('last') )
|
||||
.outerjoin(UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid)),
|
||||
|
||||
'logs_su': db.session.query(TGIDTags.tag, \
|
||||
DataStore.tgid, \
|
||||
func.count(DataStore.tgid).label('count') )
|
||||
.outerjoin(TGIDTags.table_, DataStore.tgid == TGIDTags.rid),
|
||||
|
||||
'logs_calls': db.session.query(DataStore.time, \
|
||||
SysIDTags.tag, \
|
||||
DataStore.tgid, \
|
||||
TGIDTags.tag, \
|
||||
DataStore.frequency, \
|
||||
DataStore.suid )
|
||||
.join(EventKeys.table_, and_(or_( EventKeys.tag == 'grp_v_ch_grant', EventKeys.tag == 'grp_v_ch_grant_mbt'),EventKeys.id == DataStore.cc_event))
|
||||
.outerjoin(TGIDTags.table_, and_(TGIDTags.rid == DataStore.tgid, TGIDTags.sysid == DataStore.sysid))
|
||||
.outerjoin(SysIDTags.table_, DataStore.sysid == SysIDTags.sysid),
|
||||
|
||||
'logs_joins': db.session.query(DataStore.time, \
|
||||
DataStore.opcode, \
|
||||
DataStore.sysid, \
|
||||
SysIDTags.tag, \
|
||||
LocRegResp.tag, \
|
||||
DataStore.tgid, \
|
||||
TGIDTags.tag, \
|
||||
DataStore.suid, \
|
||||
UnitIDTags.tag )
|
||||
.join(LocRegResp.table_, DataStore.p == LocRegResp.rv)
|
||||
.outerjoin(SysIDTags.table_, DataStore.sysid == SysIDTags.sysid)
|
||||
.outerjoin(TGIDTags.table_, and_(DataStore.tgid == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid))
|
||||
.outerjoin(UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid))
|
||||
.filter(or_(DataStore.opcode == 40, DataStore.opcode == 43)) # joins
|
||||
} # end query_d
|
||||
|
||||
if host_function_type != 'cc_event':
|
||||
q = query_d[k]
|
||||
|
||||
if host_function_type in 'su tgid'.split():
|
||||
filter_col = {'su': DataStore.suid, 'tgid': DataStore.tgid}
|
||||
group_col = {'su': DataStore.tgid, 'tgid': DataStore.suid}
|
||||
if '?' in host_rid:
|
||||
id_start = int(host_rid.replace('?', '0'))
|
||||
id_end = int(host_rid.replace('?', '9'))
|
||||
q = q.filter(filter_col[host_function_type] >= id_start, filter_col[host_function_type] <= id_end)
|
||||
elif '-' in host_rid:
|
||||
id_start, id_end = host_rid.split('-')
|
||||
id_start = int(id_start)
|
||||
id_end = int(id_end)
|
||||
q = q.filter(filter_col[host_function_type] >= id_start, filter_col[host_function_type] <= id_end)
|
||||
else:
|
||||
q = q.filter(filter_col[host_function_type] == int(host_rid))
|
||||
q = q.group_by(group_col[host_function_type])
|
||||
q = q.filter(DataStore.suid != None)
|
||||
|
||||
dt_cols = {
|
||||
'logs_tgid' : [ DataStore.suid, UnitIDTags.tag, 'count' ],
|
||||
'logs_su' : [ TGIDTags.tag, DataStore.tgid, 'count' ],
|
||||
'logs_calls' : [ DataStore.time, SysIDTags.tag, DataStore.tgid, TGIDTags.tag, DataStore.frequency, DataStore.suid ],
|
||||
'logs_joins' : [ DataStore.time, SysIDTags.tag, LocRegResp.tag, TGIDTags.tag, DataStore.suid ],
|
||||
'logs_total_tgid' : [ DataStore.sysid, SysIDTags.tag, DataStore.tgid, TGIDTags.tag, 'count' ]
|
||||
}
|
||||
|
||||
if host_function_type == 'cc_event':
|
||||
mapl = oplog_map[host_function_param]
|
||||
columns = []
|
||||
for row in mapl:
|
||||
col = getattr(DataStore, row[0])
|
||||
if row[0] == 'sysid':
|
||||
col = SysIDTags.tag
|
||||
elif row[1] == 'Talkgroup':
|
||||
col = TGIDTags.tag
|
||||
elif row[1] == 'Source' or row[1] == 'Target':
|
||||
col = UnitIDTags.tag
|
||||
elif row[0] == 'cc_event':
|
||||
continue
|
||||
#col = EventKeys.tag
|
||||
elif row[0] == 'opcode':
|
||||
continue
|
||||
elif host_function_param == 'loc_reg_resp' and row[0] == 'p':
|
||||
col = LocRegResp.tag
|
||||
columns.append(col)
|
||||
|
||||
column_dt = [ColumnDT(s) for s in columns]
|
||||
|
||||
q = db.session.query(*columns
|
||||
).join(
|
||||
EventKeys.table_, and_( EventKeys.tag == host_function_param, EventKeys.id == DataStore.cc_event)
|
||||
).outerjoin(
|
||||
SysIDTags.table_, DataStore.sysid == SysIDTags.sysid
|
||||
)
|
||||
if host_function_param == 'grp_aff_resp':
|
||||
q = q.outerjoin(
|
||||
TGIDTags.table_, and_(DataStore.tgid2 == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid)
|
||||
).outerjoin(
|
||||
UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid)
|
||||
)
|
||||
|
||||
elif host_function_param == 'ack_resp_fne' or host_function_param == 'grp_aff_q' or host_function_param == 'u_reg_cmd':
|
||||
q = q.outerjoin(
|
||||
TGIDTags.table_, and_(DataStore.tgid2 == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid)
|
||||
).outerjoin(
|
||||
UnitIDTags.table_, and_(DataStore.suid2 == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid)
|
||||
)
|
||||
else:
|
||||
q = q.outerjoin(
|
||||
TGIDTags.table_, and_(DataStore.tgid == TGIDTags.rid, DataStore.sysid == TGIDTags.sysid)
|
||||
).outerjoin(
|
||||
UnitIDTags.table_, and_(DataStore.suid == UnitIDTags.rid, DataStore.sysid == UnitIDTags.sysid)
|
||||
)
|
||||
|
||||
if host_function_param == 'loc_reg_resp':
|
||||
q = q.join(LocRegResp.table_, LocRegResp.rv == DataStore.p)
|
||||
|
||||
if host_function_type == 'cc_event':
|
||||
cl = columns
|
||||
elif k in dt_cols:
|
||||
cl = dt_cols[k]
|
||||
else:
|
||||
cl = None
|
||||
|
||||
# apply tgid and suid filters if present
|
||||
if host_function_type == 'cc_event':
|
||||
if filter_tgid is not None and int(filter_tgid) != 0:
|
||||
q = q.filter(DataStore.tgid == filter_tgid)
|
||||
if filter_suid is not None and int(filter_suid) != 0:
|
||||
q = q.filter(DataStore.suid == filter_suid)
|
||||
|
||||
if cl:
|
||||
c = int(params['order[0][column]'])
|
||||
d = params['order[0][dir]'] # asc or desc
|
||||
if d == 'asc':
|
||||
q = q.order_by(cl[c])
|
||||
else:
|
||||
q = q.order_by(desc(cl[c]))
|
||||
|
||||
q = q.filter(and_(DataStore.time >= int(stime), DataStore.time <= int(etime)))
|
||||
|
||||
if sysid != 0:
|
||||
q = q.filter(DataStore.sysid == sysid)
|
||||
|
||||
if host_function_type == 'cc_event':
|
||||
rowTable = DataTables(params, q, column_dt)
|
||||
else:
|
||||
rowTable = DataTables(params, q, column_d[k])
|
||||
|
||||
js = jsonify(rowTable.output_result())
|
||||
# j= 'skipped' # json.dumps(rowTable.output_result(), indent=4, separators=[',', ':'], sort_keys=True)
|
||||
# with open('data-log', 'a') as logf:
|
||||
# s = '\n\t'.join(['%s:%s' % (k, params[k]) for k in params.keys()])
|
||||
# logf.write('keys: %s\n' % (' '.join(params.keys())))
|
||||
# logf.write('params:\n\t%s\nrequest: %s\n' % (s, function_req))
|
||||
# logf.write('%s\n' % j)
|
||||
return js
|
||||
|
||||
# switch and backup database file
|
||||
@app.route("/switch_db")
|
||||
def switch_db():
|
||||
params = request.args.to_dict()
|
||||
params['ekeys'] = sorted(oplog_map.keys())
|
||||
p = os.getcwd() + '/..'
|
||||
files = [f for f in listdir(p) if isfile(join(p, f))]
|
||||
files.sort()
|
||||
if 'cmd' not in params.keys():
|
||||
curr_file = app.config['SQLALCHEMY_DATABASE_URI'].split('/')[-1]
|
||||
return render_template("switch_db.html", params=params, files=files, curr_file=curr_file)
|
||||
if params['cmd'] == 'backup':
|
||||
t = strftime("%Y-%m-%d_%H%M%S")
|
||||
destfile = 'op25-backup-%s.db' % t
|
||||
src = app.config['SQLALCHEMY_DATABASE_URI'][10:]
|
||||
s = src.split('/')
|
||||
curr_file = s[-1]
|
||||
dst = src.replace(curr_file, destfile)
|
||||
copyfile(src, dst)
|
||||
return render_template("switch_db.html", params=params, destfile=destfile, curr_file=curr_file, files=files, sm=1)
|
||||
if params['cmd'] == 'switch':
|
||||
new_f = params['file']
|
||||
database = app.config['SQLALCHEMY_DATABASE_URI']
|
||||
f = database.split('/')[-1]
|
||||
new_db = database.replace(f, new_f)
|
||||
print('switching database to: %s' % new_db)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = new_db
|
||||
return redirect('/')
|
11038
op25/gr-op25_repeater/apps/oplog/op25/static/css/bootstrap/bootstrap-darkly.css
vendored
Normal file
|
@ -0,0 +1,461 @@
|
|||
/*
|
||||
* Table styles
|
||||
*/
|
||||
table.dataTable {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
clear: both;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
/*
|
||||
* Header and footer styles
|
||||
*/
|
||||
/*
|
||||
* Body styles
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
table.dataTable thead th,
|
||||
table.dataTable tfoot th {
|
||||
font-weight: bold;
|
||||
}
|
||||
table.dataTable thead th,
|
||||
table.dataTable thead td {
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
table.dataTable thead th:active,
|
||||
table.dataTable thead td:active {
|
||||
outline: none;
|
||||
}
|
||||
table.dataTable tfoot th,
|
||||
table.dataTable tfoot td {
|
||||
padding: 10px 18px 6px 18px;
|
||||
border-top: 1px solid #dddddd;
|
||||
}
|
||||
table.dataTable thead .sorting,
|
||||
table.dataTable thead .sorting_asc,
|
||||
table.dataTable thead .sorting_desc,
|
||||
table.dataTable thead .sorting_asc_disabled,
|
||||
table.dataTable thead .sorting_desc_disabled {
|
||||
cursor: pointer;
|
||||
*cursor: hand;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right;
|
||||
}
|
||||
table.dataTable thead .sorting {
|
||||
background-image: url("../images/sort_both.png");
|
||||
}
|
||||
table.dataTable thead .sorting_asc {
|
||||
background-image: url("../images/sort_asc.png") !important;
|
||||
}
|
||||
table.dataTable thead .sorting_desc {
|
||||
background-image: url("../images/sort_desc.png") !important;
|
||||
}
|
||||
table.dataTable thead .sorting_asc_disabled {
|
||||
background-image: url("../images/sort_asc_disabled.png");
|
||||
}
|
||||
table.dataTable thead .sorting_desc_disabled {
|
||||
background-image: url("../images/sort_desc_disabled.png");
|
||||
}
|
||||
table.dataTable tbody tr {
|
||||
background-color: #333333;
|
||||
}
|
||||
table.dataTable tbody tr.selected {
|
||||
background-color: #666666;
|
||||
}
|
||||
table.dataTable tbody th,
|
||||
table.dataTable tbody td {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
|
||||
border-top: 1px solid #111111;
|
||||
}
|
||||
table.dataTable.row-border tbody tr:first-child th,
|
||||
table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
|
||||
table.dataTable.display tbody tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
|
||||
border-top: 1px solid #111111;
|
||||
border-right: 1px solid #111111;
|
||||
}
|
||||
table.dataTable.cell-border tbody tr th:first-child,
|
||||
table.dataTable.cell-border tbody tr td:first-child {
|
||||
border-left: 1px solid #111111;
|
||||
}
|
||||
table.dataTable.cell-border tbody tr:first-child th,
|
||||
table.dataTable.cell-border tbody tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
|
||||
background-color: #323232;
|
||||
}
|
||||
table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
|
||||
background-color: #646464;
|
||||
}
|
||||
table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
|
||||
background-color: #313131;
|
||||
}
|
||||
table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
|
||||
background-color: #626262;
|
||||
}
|
||||
table.dataTable.order-column tbody tr > .sorting_1,
|
||||
table.dataTable.order-column tbody tr > .sorting_2,
|
||||
table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
|
||||
table.dataTable.display tbody tr > .sorting_2,
|
||||
table.dataTable.display tbody tr > .sorting_3 {
|
||||
background-color: #323232;
|
||||
}
|
||||
table.dataTable.order-column tbody tr.selected > .sorting_1,
|
||||
table.dataTable.order-column tbody tr.selected > .sorting_2,
|
||||
table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
|
||||
table.dataTable.display tbody tr.selected > .sorting_2,
|
||||
table.dataTable.display tbody tr.selected > .sorting_3 {
|
||||
background-color: #646464;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
|
||||
background-color: #303030;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
|
||||
background-color: #313131;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
|
||||
background-color: #313131;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
|
||||
background-color: #606060;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
|
||||
background-color: #616161;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
|
||||
background-color: #626262;
|
||||
}
|
||||
table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
|
||||
background-color: #323232;
|
||||
}
|
||||
table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
|
||||
background-color: #323232;
|
||||
}
|
||||
table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
|
||||
background-color: #333333;
|
||||
}
|
||||
table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
|
||||
background-color: #646464;
|
||||
}
|
||||
table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
|
||||
background-color: #656565;
|
||||
}
|
||||
table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
|
||||
background-color: #666666;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
|
||||
background-color: #303030;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
|
||||
background-color: #5e5e5e;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
|
||||
background-color: #5e5e5e;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
|
||||
background-color: #606060;
|
||||
}
|
||||
table.dataTable.no-footer {
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
table.dataTable.nowrap th, table.dataTable.nowrap td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable.compact thead th,
|
||||
table.dataTable.compact thead td {
|
||||
padding: 4px 17px;
|
||||
}
|
||||
table.dataTable.compact tfoot th,
|
||||
table.dataTable.compact tfoot td {
|
||||
padding: 4px;
|
||||
}
|
||||
table.dataTable.compact tbody th,
|
||||
table.dataTable.compact tbody td {
|
||||
padding: 4px;
|
||||
}
|
||||
table.dataTable th.dt-left,
|
||||
table.dataTable td.dt-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable th.dt-center,
|
||||
table.dataTable td.dt-center,
|
||||
table.dataTable td.dataTables_empty {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable th.dt-right,
|
||||
table.dataTable td.dt-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable th.dt-justify,
|
||||
table.dataTable td.dt-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable th.dt-nowrap,
|
||||
table.dataTable td.dt-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable thead th.dt-head-left,
|
||||
table.dataTable thead td.dt-head-left,
|
||||
table.dataTable tfoot th.dt-head-left,
|
||||
table.dataTable tfoot td.dt-head-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable thead th.dt-head-center,
|
||||
table.dataTable thead td.dt-head-center,
|
||||
table.dataTable tfoot th.dt-head-center,
|
||||
table.dataTable tfoot td.dt-head-center {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable thead th.dt-head-right,
|
||||
table.dataTable thead td.dt-head-right,
|
||||
table.dataTable tfoot th.dt-head-right,
|
||||
table.dataTable tfoot td.dt-head-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable thead th.dt-head-justify,
|
||||
table.dataTable thead td.dt-head-justify,
|
||||
table.dataTable tfoot th.dt-head-justify,
|
||||
table.dataTable tfoot td.dt-head-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable thead th.dt-head-nowrap,
|
||||
table.dataTable thead td.dt-head-nowrap,
|
||||
table.dataTable tfoot th.dt-head-nowrap,
|
||||
table.dataTable tfoot td.dt-head-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-left,
|
||||
table.dataTable tbody td.dt-body-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-center,
|
||||
table.dataTable tbody td.dt-body-center {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-right,
|
||||
table.dataTable tbody td.dt-body-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-justify,
|
||||
table.dataTable tbody td.dt-body-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-nowrap,
|
||||
table.dataTable tbody td.dt-body-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.dataTable,
|
||||
table.dataTable th,
|
||||
table.dataTable td {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/*
|
||||
* Control feature layout
|
||||
*/
|
||||
.dataTables_wrapper {
|
||||
position: relative;
|
||||
clear: both;
|
||||
*zoom: 1;
|
||||
zoom: 1;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_length {
|
||||
float: left;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_length select {
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
background-color: transparent;
|
||||
padding: 4px;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_filter input {
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
background-color: transparent;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_info {
|
||||
clear: both;
|
||||
float: left;
|
||||
padding-top: 0.755em;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
float: right;
|
||||
text-align: right;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
min-width: 1.5em;
|
||||
padding: 0.5em 1em;
|
||||
margin-left: 2px;
|
||||
text-align: center;
|
||||
text-decoration: none !important;
|
||||
cursor: pointer;
|
||||
*cursor: hand;
|
||||
color: #aaaaaa !important;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
|
||||
color: #aaaaaa !important;
|
||||
border: 1px solid #979797;
|
||||
background-color: white;
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));
|
||||
/* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%);
|
||||
/* FF3.6+ */
|
||||
background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%);
|
||||
/* IE10+ */
|
||||
background: -o-linear-gradient(top, white 0%, #dcdcdc 100%);
|
||||
/* Opera 11.10+ */
|
||||
background: linear-gradient(to bottom, white 0%, #dcdcdc 100%);
|
||||
/* W3C */
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
|
||||
cursor: default;
|
||||
color: #666 !important;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
|
||||
color: white !important;
|
||||
border: 1px solid #375a7f;
|
||||
background-color: #7ea1c7;
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #7ea1c7), color-stop(100%, #375a7f));
|
||||
/* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, #7ea1c7 0%, #375a7f 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
background: -moz-linear-gradient(top, #7ea1c7 0%, #375a7f 100%);
|
||||
/* FF3.6+ */
|
||||
background: -ms-linear-gradient(top, #7ea1c7 0%, #375a7f 100%);
|
||||
/* IE10+ */
|
||||
background: -o-linear-gradient(top, #7ea1c7 0%, #375a7f 100%);
|
||||
/* Opera 11.10+ */
|
||||
background: linear-gradient(to bottom, #7ea1c7 0%, #375a7f 100%);
|
||||
/* W3C */
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button:active {
|
||||
outline: none;
|
||||
background-color: #4673a3;
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #4673a3), color-stop(100%, #345578));
|
||||
/* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, #4673a3 0%, #345578 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
background: -moz-linear-gradient(top, #4673a3 0%, #345578 100%);
|
||||
/* FF3.6+ */
|
||||
background: -ms-linear-gradient(top, #4673a3 0%, #345578 100%);
|
||||
/* IE10+ */
|
||||
background: -o-linear-gradient(top, #4673a3 0%, #345578 100%);
|
||||
/* Opera 11.10+ */
|
||||
background: linear-gradient(to bottom, #4673a3 0%, #345578 100%);
|
||||
/* W3C */
|
||||
box-shadow: inset 0 0 3px #111;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .ellipsis {
|
||||
padding: 0 1em;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_processing {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 90px;
|
||||
margin-left: -50%;
|
||||
margin-top: -25px;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 2.0em;
|
||||
background-color: white;
|
||||
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(51, 51, 51, 0)), color-stop(25%, rgba(51, 51, 51, 0.9)), color-stop(75%, rgba(51, 51, 51, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
|
||||
background: -webkit-linear-gradient(left, rgba(51, 51, 51, 0) 0%, rgba(51, 51, 51, 0.9) 25%, rgba(51, 51, 51, 0.9) 75%, rgba(51, 51, 51, 0) 100%);
|
||||
background: -moz-linear-gradient(left, rgba(51, 51, 51, 0) 0%, rgba(51, 51, 51, 0.9) 25%, rgba(51, 51, 51, 0.9) 75%, rgba(51, 51, 51, 0) 100%);
|
||||
background: -ms-linear-gradient(left, rgba(51, 51, 51, 0) 0%, rgba(51, 51, 51, 0.9) 25%, rgba(51, 51, 51, 0.9) 75%, rgba(51, 51, 51, 0) 100%);
|
||||
background: -o-linear-gradient(left, rgba(51, 51, 51, 0) 0%, rgba(51, 51, 51, 0.9) 25%, rgba(51, 51, 51, 0.9) 75%, rgba(51, 51, 51, 0) 100%);
|
||||
background: linear-gradient(to right, rgba(51, 51, 51, 0) 0%, rgba(51, 51, 51, 0.9) 25%, rgba(51, 51, 51, 0.9) 75%, rgba(51, 51, 51, 0) 100%);
|
||||
}
|
||||
.dataTables_wrapper .dataTables_length,
|
||||
.dataTables_wrapper .dataTables_filter,
|
||||
.dataTables_wrapper .dataTables_info,
|
||||
.dataTables_wrapper .dataTables_processing,
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_scroll {
|
||||
clear: both;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
|
||||
*margin-top: -1px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing,
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing,
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.dataTables_wrapper.no-footer .dataTables_scrollBody {
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,
|
||||
.dataTables_wrapper.no-footer div.dataTables_scrollBody > table {
|
||||
border-bottom: none;
|
||||
}
|
||||
.dataTables_wrapper:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
content: "";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.dataTables_wrapper .dataTables_info,
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
float: none;
|
||||
text-align: center;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 640px) {
|
||||
.dataTables_wrapper .dataTables_length,
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
float: none;
|
||||
text-align: center;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,453 @@
|
|||
/*
|
||||
* Table styles
|
||||
*/
|
||||
table.dataTable {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
clear: both;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
/*
|
||||
* Header and footer styles
|
||||
*/
|
||||
/*
|
||||
* Body styles
|
||||
*/
|
||||
}
|
||||
table.dataTable thead th,
|
||||
table.dataTable tfoot th {
|
||||
font-weight: bold;
|
||||
}
|
||||
table.dataTable thead th,
|
||||
table.dataTable thead td {
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid #111;
|
||||
}
|
||||
table.dataTable thead th:active,
|
||||
table.dataTable thead td:active {
|
||||
outline: none;
|
||||
}
|
||||
table.dataTable tfoot th,
|
||||
table.dataTable tfoot td {
|
||||
padding: 10px 18px 6px 18px;
|
||||
border-top: 1px solid #111;
|
||||
}
|
||||
table.dataTable thead .sorting,
|
||||
table.dataTable thead .sorting_asc,
|
||||
table.dataTable thead .sorting_desc {
|
||||
cursor: pointer;
|
||||
*cursor: hand;
|
||||
}
|
||||
table.dataTable thead .sorting,
|
||||
table.dataTable thead .sorting_asc,
|
||||
table.dataTable thead .sorting_desc,
|
||||
table.dataTable thead .sorting_asc_disabled,
|
||||
table.dataTable thead .sorting_desc_disabled {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right;
|
||||
}
|
||||
table.dataTable thead .sorting {
|
||||
background-image: url("../images/sort_both.png");
|
||||
}
|
||||
table.dataTable thead .sorting_asc {
|
||||
background-image: url("../images/sort_asc.png");
|
||||
}
|
||||
table.dataTable thead .sorting_desc {
|
||||
background-image: url("../images/sort_desc.png");
|
||||
}
|
||||
table.dataTable thead .sorting_asc_disabled {
|
||||
background-image: url("../images/sort_asc_disabled.png");
|
||||
}
|
||||
table.dataTable thead .sorting_desc_disabled {
|
||||
background-image: url("../images/sort_desc_disabled.png");
|
||||
}
|
||||
table.dataTable tbody tr {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
table.dataTable tbody tr.selected {
|
||||
background-color: #B0BED9;
|
||||
}
|
||||
table.dataTable tbody th,
|
||||
table.dataTable tbody td {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td {
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
table.dataTable.row-border tbody tr:first-child th,
|
||||
table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th,
|
||||
table.dataTable.display tbody tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td {
|
||||
border-top: 1px solid #ddd;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
table.dataTable.cell-border tbody tr th:first-child,
|
||||
table.dataTable.cell-border tbody tr td:first-child {
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
table.dataTable.cell-border tbody tr:first-child th,
|
||||
table.dataTable.cell-border tbody tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected {
|
||||
background-color: #acbad4;
|
||||
}
|
||||
table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected {
|
||||
background-color: #aab7d1;
|
||||
}
|
||||
table.dataTable.order-column tbody tr > .sorting_1,
|
||||
table.dataTable.order-column tbody tr > .sorting_2,
|
||||
table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1,
|
||||
table.dataTable.display tbody tr > .sorting_2,
|
||||
table.dataTable.display tbody tr > .sorting_3 {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
table.dataTable.order-column tbody tr.selected > .sorting_1,
|
||||
table.dataTable.order-column tbody tr.selected > .sorting_2,
|
||||
table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1,
|
||||
table.dataTable.display tbody tr.selected > .sorting_2,
|
||||
table.dataTable.display tbody tr.selected > .sorting_3 {
|
||||
background-color: #acbad5;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 {
|
||||
background-color: #a6b4cd;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 {
|
||||
background-color: #a8b5cf;
|
||||
}
|
||||
table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 {
|
||||
background-color: #a9b7d1;
|
||||
}
|
||||
table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 {
|
||||
background-color: #fcfcfc;
|
||||
}
|
||||
table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 {
|
||||
background-color: #fefefe;
|
||||
}
|
||||
table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 {
|
||||
background-color: #acbad5;
|
||||
}
|
||||
table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 {
|
||||
background-color: #aebcd6;
|
||||
}
|
||||
table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 {
|
||||
background-color: #afbdd8;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 {
|
||||
background-color: #ececec;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 {
|
||||
background-color: #efefef;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 {
|
||||
background-color: #a2aec7;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 {
|
||||
background-color: #a3b0c9;
|
||||
}
|
||||
table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 {
|
||||
background-color: #a5b2cb;
|
||||
}
|
||||
table.dataTable.no-footer {
|
||||
border-bottom: 1px solid #111;
|
||||
}
|
||||
table.dataTable.nowrap th, table.dataTable.nowrap td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable.compact thead th,
|
||||
table.dataTable.compact thead td {
|
||||
padding: 4px 17px 4px 4px;
|
||||
}
|
||||
table.dataTable.compact tfoot th,
|
||||
table.dataTable.compact tfoot td {
|
||||
padding: 4px;
|
||||
}
|
||||
table.dataTable.compact tbody th,
|
||||
table.dataTable.compact tbody td {
|
||||
padding: 4px;
|
||||
}
|
||||
table.dataTable th.dt-left,
|
||||
table.dataTable td.dt-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable th.dt-center,
|
||||
table.dataTable td.dt-center,
|
||||
table.dataTable td.dataTables_empty {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable th.dt-right,
|
||||
table.dataTable td.dt-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable th.dt-justify,
|
||||
table.dataTable td.dt-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable th.dt-nowrap,
|
||||
table.dataTable td.dt-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable thead th.dt-head-left,
|
||||
table.dataTable thead td.dt-head-left,
|
||||
table.dataTable tfoot th.dt-head-left,
|
||||
table.dataTable tfoot td.dt-head-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable thead th.dt-head-center,
|
||||
table.dataTable thead td.dt-head-center,
|
||||
table.dataTable tfoot th.dt-head-center,
|
||||
table.dataTable tfoot td.dt-head-center {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable thead th.dt-head-right,
|
||||
table.dataTable thead td.dt-head-right,
|
||||
table.dataTable tfoot th.dt-head-right,
|
||||
table.dataTable tfoot td.dt-head-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable thead th.dt-head-justify,
|
||||
table.dataTable thead td.dt-head-justify,
|
||||
table.dataTable tfoot th.dt-head-justify,
|
||||
table.dataTable tfoot td.dt-head-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable thead th.dt-head-nowrap,
|
||||
table.dataTable thead td.dt-head-nowrap,
|
||||
table.dataTable tfoot th.dt-head-nowrap,
|
||||
table.dataTable tfoot td.dt-head-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-left,
|
||||
table.dataTable tbody td.dt-body-left {
|
||||
text-align: left;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-center,
|
||||
table.dataTable tbody td.dt-body-center {
|
||||
text-align: center;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-right,
|
||||
table.dataTable tbody td.dt-body-right {
|
||||
text-align: right;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-justify,
|
||||
table.dataTable tbody td.dt-body-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
table.dataTable tbody th.dt-body-nowrap,
|
||||
table.dataTable tbody td.dt-body-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.dataTable,
|
||||
table.dataTable th,
|
||||
table.dataTable td {
|
||||
-webkit-box-sizing: content-box;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/*
|
||||
* Control feature layout
|
||||
*/
|
||||
.dataTables_wrapper {
|
||||
position: relative;
|
||||
clear: both;
|
||||
*zoom: 1;
|
||||
zoom: 1;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_length {
|
||||
float: left;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_filter input {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_info {
|
||||
clear: both;
|
||||
float: left;
|
||||
padding-top: 0.755em;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
float: right;
|
||||
text-align: right;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
min-width: 1.5em;
|
||||
padding: 0.5em 1em;
|
||||
margin-left: 2px;
|
||||
text-align: center;
|
||||
text-decoration: none !important;
|
||||
cursor: pointer;
|
||||
*cursor: hand;
|
||||
color: #333 !important;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
|
||||
color: #333 !important;
|
||||
border: 1px solid #979797;
|
||||
background-color: white;
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc));
|
||||
/* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%);
|
||||
/* FF3.6+ */
|
||||
background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%);
|
||||
/* IE10+ */
|
||||
background: -o-linear-gradient(top, white 0%, #dcdcdc 100%);
|
||||
/* Opera 11.10+ */
|
||||
background: linear-gradient(to bottom, white 0%, #dcdcdc 100%);
|
||||
/* W3C */
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active {
|
||||
cursor: default;
|
||||
color: #666 !important;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
|
||||
color: white !important;
|
||||
border: 1px solid #111;
|
||||
background-color: #585858;
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));
|
||||
/* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, #585858 0%, #111 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
background: -moz-linear-gradient(top, #585858 0%, #111 100%);
|
||||
/* FF3.6+ */
|
||||
background: -ms-linear-gradient(top, #585858 0%, #111 100%);
|
||||
/* IE10+ */
|
||||
background: -o-linear-gradient(top, #585858 0%, #111 100%);
|
||||
/* Opera 11.10+ */
|
||||
background: linear-gradient(to bottom, #585858 0%, #111 100%);
|
||||
/* W3C */
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button:active {
|
||||
outline: none;
|
||||
background-color: #2b2b2b;
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));
|
||||
/* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
|
||||
/* FF3.6+ */
|
||||
background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
|
||||
/* IE10+ */
|
||||
background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);
|
||||
/* Opera 11.10+ */
|
||||
background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);
|
||||
/* W3C */
|
||||
box-shadow: inset 0 0 3px #111;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate .ellipsis {
|
||||
padding: 0 1em;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_processing {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin-left: -50%;
|
||||
margin-top: -25px;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
background-color: white;
|
||||
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0)));
|
||||
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
|
||||
background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
|
||||
background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
|
||||
background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
.dataTables_wrapper .dataTables_length,
|
||||
.dataTables_wrapper .dataTables_filter,
|
||||
.dataTables_wrapper .dataTables_info,
|
||||
.dataTables_wrapper .dataTables_processing,
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
color: #333;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_scroll {
|
||||
clear: both;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody {
|
||||
*margin-top: -1px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th > div.dataTables_sizing,
|
||||
.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td > div.dataTables_sizing {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.dataTables_wrapper.no-footer .dataTables_scrollBody {
|
||||
border-bottom: 1px solid #111;
|
||||
}
|
||||
.dataTables_wrapper.no-footer div.dataTables_scrollHead table,
|
||||
.dataTables_wrapper.no-footer div.dataTables_scrollBody table {
|
||||
border-bottom: none;
|
||||
}
|
||||
.dataTables_wrapper:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
content: "";
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.dataTables_wrapper .dataTables_info,
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
float: none;
|
||||
text-align: center;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_paginate {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 640px) {
|
||||
.dataTables_wrapper .dataTables_length,
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
float: none;
|
||||
text-align: center;
|
||||
}
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 160 B |
After Width: | Height: | Size: 148 B |
After Width: | Height: | Size: 201 B |
After Width: | Height: | Size: 158 B |
After Width: | Height: | Size: 146 B |
|
@ -0,0 +1,237 @@
|
|||
body {
|
||||
/* min-height: 2000px; */
|
||||
/* padding: 0px 25px 0px 25px; */
|
||||
margin: 5px 10px 5px 10px !important;
|
||||
/* background-color: #0f0; */
|
||||
}
|
||||
|
||||
.li-text {
|
||||
padding: 10px 15px 5px 15px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.sel-date {
|
||||
color: black;
|
||||
width: 150px;
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
/* Clear floats after the columns */
|
||||
.row:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
#container {
|
||||
width: 1000px;
|
||||
min-height: 2000px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid;
|
||||
background-color: #f00; }
|
||||
|
||||
#primary {
|
||||
min-height: 2000px; float: left;
|
||||
width: 15%;
|
||||
padding: 5px;
|
||||
background-color: #222;}
|
||||
|
||||
#content {
|
||||
min-height: 2000px; float: left;
|
||||
width: 70%;
|
||||
padding: 5px;
|
||||
background-color: #fff; }
|
||||
|
||||
#secondary {
|
||||
min-height: 2000px; float: left;
|
||||
width: 15%;
|
||||
padding: 5px;
|
||||
background-color: #eee;}
|
||||
*/
|
||||
|
||||
#footer {
|
||||
clear: both;
|
||||
background-color: #375a7f;
|
||||
min-height: 50px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btnMain {
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
width: 210px;
|
||||
}
|
||||
|
||||
.dataTables_length select {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dataTables_length select option {
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.dataTables_filter input[type=search] {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#systemSelect {
|
||||
color: #000;
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
#navSelect {
|
||||
color: #000;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.op-input {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
The Modal (background).modal {
|
||||
display: none; Hidden by default position: fixed; Stay in place z-index: 1; Sit on top left: 0;
|
||||
top: 0;
|
||||
width: 100%; Full width height: 100%; Full height overflow: auto; Enable scroll if needed background-color: rgb(0,0,0); Fallback color background-color: rgba(0,0,0,0.4); Black w/ opacity}
|
||||
|
||||
Modal Content/Box.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 15% auto; 15% from the top and centered padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 80%; Could be more or less, depending on screen size}
|
||||
|
||||
The Close Button.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
*/
|
||||
|
||||
#loading {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
background-color: #fff;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
#loading-image {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
/* New layout stuff */
|
||||
|
||||
/* Style the body */
|
||||
/*
|
||||
body {
|
||||
font-family: Arial;
|
||||
margin: 0;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Header/logo Title */
|
||||
/*
|
||||
.header {
|
||||
padding: 60px;
|
||||
text-align: center;
|
||||
background: #1abc9c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
Style the top navigation bar.navbar {
|
||||
display: flex;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
Style the navigation bar links.navbar a {
|
||||
color: white;
|
||||
padding: 14px 20px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Change color on hover.navbar a:hover {
|
||||
background-color: #ddd;
|
||||
color: black;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Column container */
|
||||
.row-main {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Sidebar/left column */
|
||||
.side {
|
||||
flex: 15%;
|
||||
/* flex: 0 0 275px; */
|
||||
/* width: 200px; */
|
||||
/* background-color: #111; */
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
/* Main column */
|
||||
.main {
|
||||
flex: 70%;
|
||||
/* background-color: white; */
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
/* padding: 20px; */
|
||||
text-align: center;
|
||||
/* background: #ddd; */
|
||||
}
|
||||
|
||||
/* Responsive layout - when the screen is less than 700px wide, make the two columns stack on top of each other instead of next to each other */
|
||||
@media screen and (max-width: 790px) {
|
||||
.row, .navbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,568 @@
|
|||
.xdsoft_datetimepicker {
|
||||
box-shadow: 0 5px 15px -5px rgba(0, 0, 0, 0.506);
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #bbb;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-top: 1px solid #ccc;
|
||||
color: #333;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
padding: 8px;
|
||||
padding-left: 0;
|
||||
padding-top: 2px;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_rtl {
|
||||
padding: 8px 0 8px 8px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker iframe {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 75px;
|
||||
height: 210px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/*For IE8 or lower*/
|
||||
.xdsoft_datetimepicker button {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.xdsoft_noselect {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.xdsoft_noselect::selection { background: transparent }
|
||||
.xdsoft_noselect::-moz-selection { background: transparent }
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_inline {
|
||||
display: inline-block;
|
||||
position: static;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker * {
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_datepicker, .xdsoft_datetimepicker .xdsoft_timepicker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_datepicker.active, .xdsoft_datetimepicker .xdsoft_timepicker.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_datepicker {
|
||||
width: 224px;
|
||||
float: left;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_datepicker {
|
||||
float: right;
|
||||
margin-right: 8px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_datepicker {
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker {
|
||||
width: 58px;
|
||||
float: left;
|
||||
text-align: center;
|
||||
margin-left: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker {
|
||||
float: right;
|
||||
margin-right: 8px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_datepicker.active+.xdsoft_timepicker {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 3px
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_monthpicker {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label i,
|
||||
.xdsoft_datetimepicker .xdsoft_prev,
|
||||
.xdsoft_datetimepicker .xdsoft_next,
|
||||
.xdsoft_datetimepicker .xdsoft_today_button {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Q0NBRjI1NjM0M0UwMTFFNDk4NkFGMzJFQkQzQjEwRUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Q0NBRjI1NjQ0M0UwMTFFNDk4NkFGMzJFQkQzQjEwRUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpDQ0FGMjU2MTQzRTAxMUU0OTg2QUYzMkVCRDNCMTBFQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpDQ0FGMjU2MjQzRTAxMUU0OTg2QUYzMkVCRDNCMTBFQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoNEP54AAAIOSURBVHja7Jq9TsMwEMcxrZD4WpBYeKUCe+kTMCACHZh4BFfHO/AAIHZGFhYkBBsSEqxsLCAgXKhbXYOTxh9pfJVP+qutnZ5s/5Lz2Y5I03QhWji2GIcgAokWgfCxNvcOCCGKqiSqhUp0laHOne05vdEyGMfkdxJDVjgwDlEQgYQBgx+ULJaWSXXS6r/ER5FBVR8VfGftTKcITNs+a1XpcFoExREIDF14AVIFxgQUS+h520cdud6wNkC0UBw6BCO/HoCYwBhD8QCkQ/x1mwDyD4plh4D6DDV0TAGyo4HcawLIBBSLDkHeH0Mg2yVP3l4TQMZQDDsEOl/MgHQqhMNuE0D+oBh0CIr8MAKyazBH9WyBuKxDWgbXfjNf32TZ1KWm/Ap1oSk/R53UtQ5xTh3LUlMmT8gt6g51Q9p+SobxgJQ/qmsfZhWywGFSl0yBjCLJCMgXail3b7+rumdVJ2YRss4cN+r6qAHDkPWjPjdJCF4n9RmAD/V9A/Wp4NQassDjwlB6XBiCxcJQWmZZb8THFilfy/lfrTvLghq2TqTHrRMTKNJ0sIhdo15RT+RpyWwFdY96UZ/LdQKBGjcXpcc1AlSFEfLmouD+1knuxBDUVrvOBmoOC/rEcN7OQxKVeJTCiAdUzUJhA2Oez9QTkp72OTVcxDcXY8iKNkxGAJXmJCOQwOa6dhyXsOa6XwEGAKdeb5ET3rQdAAAAAElFTkSuQmCC);
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label i {
|
||||
opacity: 0.5;
|
||||
background-position: -92px -19px;
|
||||
display: inline-block;
|
||||
width: 9px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_prev {
|
||||
float: left;
|
||||
background-position: -20px 0;
|
||||
}
|
||||
.xdsoft_datetimepicker .xdsoft_today_button {
|
||||
float: left;
|
||||
background-position: -70px 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_next {
|
||||
float: right;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_next,
|
||||
.xdsoft_datetimepicker .xdsoft_prev ,
|
||||
.xdsoft_datetimepicker .xdsoft_today_button {
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
border: 0 none;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 30px;
|
||||
opacity: 0.5;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
|
||||
outline: medium none;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
text-indent: 100%;
|
||||
white-space: nowrap;
|
||||
width: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_prev,
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_next {
|
||||
float: none;
|
||||
background-position: -40px -15px;
|
||||
height: 15px;
|
||||
width: 30px;
|
||||
display: block;
|
||||
margin-left: 14px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker .xdsoft_prev,
|
||||
.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_timepicker .xdsoft_next {
|
||||
float: none;
|
||||
margin-left: 0;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_prev {
|
||||
background-position: -40px 0;
|
||||
margin-bottom: 7px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box {
|
||||
height: 151px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div {
|
||||
background: #f5f5f5;
|
||||
border-top: 1px solid #ddd;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
border-collapse: collapse;
|
||||
cursor: pointer;
|
||||
border-bottom-width: 0;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div > div:first-child {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_today_button:hover,
|
||||
.xdsoft_datetimepicker .xdsoft_next:hover,
|
||||
.xdsoft_datetimepicker .xdsoft_prev:hover {
|
||||
opacity: 1;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label {
|
||||
display: inline;
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
margin: 0;
|
||||
padding: 5px 3px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: bold;
|
||||
background-color: #fff;
|
||||
float: left;
|
||||
width: 182px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label:hover>span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label:hover i {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select {
|
||||
border: 1px solid #ccc;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 30px;
|
||||
z-index: 101;
|
||||
display: none;
|
||||
background: #fff;
|
||||
max-height: 160px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select.xdsoft_monthselect{ right: -7px }
|
||||
.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select.xdsoft_yearselect{ right: 2px }
|
||||
.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option:hover {
|
||||
color: #fff;
|
||||
background: #ff8000;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option {
|
||||
padding: 2px 10px 2px 5px;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_label > .xdsoft_select > div > .xdsoft_option.xdsoft_current {
|
||||
background: #33aaff;
|
||||
box-shadow: #178fe5 0 1px 3px 0 inset;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_month {
|
||||
width: 100px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_year{
|
||||
width: 48px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td > div {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar th {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td,.xdsoft_datetimepicker .xdsoft_calendar th {
|
||||
width: 14.2857142%;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
cursor: pointer;
|
||||
height: 25px;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_calendar td,.xdsoft_datetimepicker.xdsoft_showweeks .xdsoft_calendar th {
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar th {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_today {
|
||||
color: #33aaff;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_highlighted_default {
|
||||
background: #ffe9d2;
|
||||
box-shadow: #ffb871 0 1px 4px 0 inset;
|
||||
color: #000;
|
||||
}
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_highlighted_mint {
|
||||
background: #c1ffc9;
|
||||
box-shadow: #00dd1c 0 1px 4px 0 inset;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_default,
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_current,
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_current {
|
||||
background: #33aaff;
|
||||
box-shadow: #178fe5 0 1px 3px 0 inset;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_other_month,
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_disabled,
|
||||
.xdsoft_datetimepicker .xdsoft_time_box >div >div.xdsoft_disabled {
|
||||
opacity: 0.5;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_other_month.xdsoft_disabled {
|
||||
opacity: 0.2;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td:hover,
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div:hover {
|
||||
color: #fff !important;
|
||||
background: #ff8000 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_current.xdsoft_disabled:hover,
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box>div>div.xdsoft_current.xdsoft_disabled:hover {
|
||||
background: #33aaff !important;
|
||||
box-shadow: #178fe5 0 1px 3px 0 inset !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar td.xdsoft_disabled:hover,
|
||||
.xdsoft_datetimepicker .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_disabled:hover {
|
||||
color: inherit !important;
|
||||
background: inherit !important;
|
||||
box-shadow: inherit !important;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_calendar th {
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_copyright {
|
||||
color: #ccc !important;
|
||||
font-size: 10px;
|
||||
clear: both;
|
||||
float: none;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker .xdsoft_copyright a { color: #eee !important }
|
||||
.xdsoft_datetimepicker .xdsoft_copyright a:hover { color: #aaa !important }
|
||||
|
||||
.xdsoft_time_box {
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.xdsoft_scrollbar >.xdsoft_scroller {
|
||||
background: #ccc !important;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.xdsoft_scrollbar {
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_rtl .xdsoft_scrollbar {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
.xdsoft_scroller_box {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark {
|
||||
box-shadow: 0 5px 15px -5px rgba(255, 255, 255, 0.506);
|
||||
background: #000;
|
||||
border-bottom: 1px solid #444;
|
||||
border-left: 1px solid #333;
|
||||
border-right: 1px solid #333;
|
||||
border-top: 1px solid #333;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box {
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div {
|
||||
background: #0a0a0a;
|
||||
border-top: 1px solid #222;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label {
|
||||
background-color: #000;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select {
|
||||
border: 1px solid #333;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select > div > .xdsoft_option:hover {
|
||||
color: #000;
|
||||
background: #007fff;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label > .xdsoft_select > div > .xdsoft_option.xdsoft_current {
|
||||
background: #cc5500;
|
||||
box-shadow: #b03e00 0 1px 3px 0 inset;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_label i,
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_prev,
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_next,
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_today_button {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAAAeCAYAAADaW7vzAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QUExQUUzOTA0M0UyMTFFNDlBM0FFQTJENTExRDVBODYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QUExQUUzOTE0M0UyMTFFNDlBM0FFQTJENTExRDVBODYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBQTFBRTM4RTQzRTIxMUU0OUEzQUVBMkQ1MTFENUE4NiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBQTFBRTM4RjQzRTIxMUU0OUEzQUVBMkQ1MTFENUE4NiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pp0VxGEAAAIASURBVHja7JrNSgMxEMebtgh+3MSLr1T1Xn2CHoSKB08+QmR8Bx9A8e7RixdB9CKCoNdexIugxFlJa7rNZneTbLIpM/CnNLsdMvNjM8l0mRCiQ9Ye61IKCAgZAUnH+mU3MMZaHYChBnJUDzWOFZdVfc5+ZFLbrWDeXPwbxIqrLLfaeS0hEBVGIRQCEiZoHQwtlGSByCCdYBl8g8egTTAWoKQMRBRBcZxYlhzhKegqMOageErsCHVkk3hXIFooDgHB1KkHIHVgzKB4ADJQ/A1jAFmAYhkQqA5TOBtocrKrgXwQA8gcFIuAIO8sQSA7hidvPwaQGZSaAYHOUWJABhWWw2EMIH9QagQERU4SArJXo0ZZL18uvaxejXt/Em8xjVBXmvFr1KVm/AJ10tRe2XnraNqaJvKE3KHuUbfK1E+VHB0q40/y3sdQSxY4FHWeKJCunP8UyDdqJZenT3ntVV5jIYCAh20vT7ioP8tpf6E2lfEMwERe+whV1MHjwZB7PBiCxcGQWwKZKD62lfGNnP/1poFAA60T7rF1UgcKd2id3KDeUS+oLWV8DfWAepOfq00CgQabi9zjcgJVYVD7PVzQUAUGAQkbNJTBICDhgwYTjDYD6XeW08ZKh+A4pYkzenOxXUbvZcWz7E8ykRMnIHGX1XPl+1m2vPYpL+2qdb8CDAARlKFEz/ZVkAAAAABJRU5ErkJggg==);
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td,
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th {
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #222;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th {
|
||||
background: #0e0e0e;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_today {
|
||||
color: #cc5500;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_highlighted_default {
|
||||
background: #ffe9d2;
|
||||
box-shadow: #ffb871 0 1px 4px 0 inset;
|
||||
color:#000;
|
||||
}
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_highlighted_mint {
|
||||
background: #c1ffc9;
|
||||
box-shadow: #00dd1c 0 1px 4px 0 inset;
|
||||
color:#000;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_default,
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td.xdsoft_current,
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div.xdsoft_current {
|
||||
background: #cc5500;
|
||||
box-shadow: #b03e00 0 1px 3px 0 inset;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar td:hover,
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_timepicker .xdsoft_time_box >div >div:hover {
|
||||
color: #000 !important;
|
||||
background: #007fff !important;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_calendar th {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright { color: #333 !important }
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright a { color: #111 !important }
|
||||
.xdsoft_datetimepicker.xdsoft_dark .xdsoft_copyright a:hover { color: #555 !important }
|
||||
|
||||
.xdsoft_dark .xdsoft_time_box {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.xdsoft_dark .xdsoft_scrollbar >.xdsoft_scroller {
|
||||
background: #333 !important;
|
||||
}
|
||||
.xdsoft_datetimepicker .xdsoft_save_selected {
|
||||
display: block;
|
||||
border: 1px solid #dddddd !important;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
color: #454551;
|
||||
font-size: 13px;
|
||||
}
|
||||
.xdsoft_datetimepicker .blue-gradient-button {
|
||||
font-family: "museo-sans", "Book Antiqua", sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
color: #82878c;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
padding: 4px 17px 4px 33px;
|
||||
border: 1px solid #d7d8da;
|
||||
background: -moz-linear-gradient(top, #fff 0%, #f4f8fa 73%);
|
||||
/* FF3.6+ */
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(73%, #f4f8fa));
|
||||
/* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, #fff 0%, #f4f8fa 73%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
background: -o-linear-gradient(top, #fff 0%, #f4f8fa 73%);
|
||||
/* Opera 11.10+ */
|
||||
background: -ms-linear-gradient(top, #fff 0%, #f4f8fa 73%);
|
||||
/* IE10+ */
|
||||
background: linear-gradient(to bottom, #fff 0%, #f4f8fa 73%);
|
||||
/* W3C */
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff', endColorstr='#f4f8fa',GradientType=0 );
|
||||
/* IE6-9 */
|
||||
}
|
||||
.xdsoft_datetimepicker .blue-gradient-button:hover, .xdsoft_datetimepicker .blue-gradient-button:focus, .xdsoft_datetimepicker .blue-gradient-button:hover span, .xdsoft_datetimepicker .blue-gradient-button:focus span {
|
||||
color: #454551;
|
||||
background: -moz-linear-gradient(top, #f4f8fa 0%, #FFF 73%);
|
||||
/* FF3.6+ */
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f4f8fa), color-stop(73%, #FFF));
|
||||
/* Chrome,Safari4+ */
|
||||
background: -webkit-linear-gradient(top, #f4f8fa 0%, #FFF 73%);
|
||||
/* Chrome10+,Safari5.1+ */
|
||||
background: -o-linear-gradient(top, #f4f8fa 0%, #FFF 73%);
|
||||
/* Opera 11.10+ */
|
||||
background: -ms-linear-gradient(top, #f4f8fa 0%, #FFF 73%);
|
||||
/* IE10+ */
|
||||
background: linear-gradient(to bottom, #f4f8fa 0%, #FFF 73%);
|
||||
/* W3C */
|
||||
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f8fa', endColorstr='#FFF',GradientType=0 );
|
||||
/* IE6-9 */
|
||||
}
|
After Width: | Height: | Size: 1.1 KiB |
7
op25/gr-op25_repeater/apps/oplog/op25/static/js/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
15212
op25/gr-op25_repeater/apps/oplog/op25/static/js/datatables/jquery.dataTables.js
vendored
Normal file
|
@ -0,0 +1,227 @@
|
|||
// Copyright 2017, 2018, 2019, 2020, 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.
|
||||
//
|
||||
// OP25 Logs
|
||||
|
||||
$(window).load(function() {
|
||||
$('#loading').hide();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#startDate').val(localStorage.logStart);
|
||||
$('#endDate').val(localStorage.logEnd);
|
||||
randCss(); // force css reload each time for dev
|
||||
$('#records').text(comma(parseInt(($('#records').text()))));
|
||||
$('#systems').text(comma(parseInt(($('#systems').text()))));
|
||||
$('#talkgroups').text(comma(parseInt(($('#talkgroups').text()))));
|
||||
$('#subs').text(comma(parseInt(($('#subs').text()))));
|
||||
if (localStorage.systemSelect) {
|
||||
$('#systemSelect').val(localStorage.systemSelect);
|
||||
}
|
||||
if (localStorage.systemSelect4) {
|
||||
$('#systemSelect4').val(localStorage.systemSelect4);
|
||||
}
|
||||
});
|
||||
|
||||
$(window).load(function() {
|
||||
$('#loading').hide();
|
||||
});
|
||||
|
||||
function resetDates() {
|
||||
$('#startDate').val('');
|
||||
$('#endDate').val('');
|
||||
$('#systemSelect').val('0');
|
||||
window.localStorage.removeItem('logStart');
|
||||
window.localStorage.removeItem('logEnd');
|
||||
window.localStorage.removeItem('systemSelect');
|
||||
}
|
||||
|
||||
$('#navSelect').change(function(){
|
||||
console.log("shit");
|
||||
var ns = $('#navSelect').val();
|
||||
if (ns == '0')
|
||||
return;
|
||||
console.log(ns);
|
||||
load_new_page1(ns);
|
||||
});
|
||||
|
||||
// forces css to reload - helpful during dev
|
||||
function randCss() {
|
||||
var h, a, f;
|
||||
a = document.getElementsByTagName('link');
|
||||
for (h = 0; h < a.length; h++) {
|
||||
f = a[h];
|
||||
if (f.rel.toLowerCase().match(/stylesheet/) && f.href) {
|
||||
var g = f.href.replace(/(&|\?)rnd=\d+/, '');
|
||||
f.href = g + (g.match(/\?/) ? '&' : '?');
|
||||
f.href += 'rnd=' + (new Date().valueOf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function load_new_page1(request,param) {
|
||||
var v1 = $('#resource_id').val();
|
||||
tgid = $('#cc_filter_tgid').val();
|
||||
suid = $('#cc_filter_suid').val();
|
||||
tgid = (Number.isInteger(parseInt(tgid)) == true) ? parseInt(tgid) : 0;
|
||||
suid = (Number.isInteger(parseInt(suid)) == true) ? parseInt(suid) : 0;
|
||||
load_new_page('/logs', 'q=' + v1 + '&r=' + request + '&p=' + param + '&tgid=' + tgid + '&suid=' + suid);
|
||||
}
|
||||
|
||||
|
||||
//SUID and TGID 'specified' buttons!
|
||||
function load_new_page0(request) {
|
||||
var v1 = $('#resource_id').val();
|
||||
var sysid = $('#systemSelect').val();
|
||||
if (v1 == '') {
|
||||
alert("Subscriber unit ID or talkgroup ID is required!");
|
||||
return;
|
||||
}
|
||||
if (v1.split('-').length > 2) {
|
||||
alert("Too many values for a range.");
|
||||
return;
|
||||
}
|
||||
|
||||
load_new_page('/logs', 'q=' + v1 + '&r=' + request + '&sysid=' + sysid);
|
||||
}
|
||||
|
||||
function load_new_page(url, arg) {
|
||||
var u = url;
|
||||
if (arg)
|
||||
u = u + "?" + arg;
|
||||
window.open(u, "_self", "resizable,location,menubar,toolbar,scrollbars,status")
|
||||
}
|
||||
|
||||
function sdate() {
|
||||
var s = $('#startDate').val() ? new Date($('#startDate').val()) : new Date("2001/01/01 01:00");
|
||||
var stime = s.getTime() / 1000;
|
||||
return stime | 0;
|
||||
}
|
||||
|
||||
function edate() {
|
||||
var e = $('#endDate').val() ? new Date($('#endDate').val()) : new Date();
|
||||
var etime = e.getTime() / 1000;
|
||||
return etime | 0;
|
||||
}
|
||||
|
||||
function comma(x) {
|
||||
// add comma formatting to whatever you give it (xx,xxxx,xxxx)
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
|
||||
function doPurge(sim) {
|
||||
var kv = $('#keepVoice').prop('checked');
|
||||
var bu = $('#createBackup').prop('checked');
|
||||
if ($('#startDate').val() == '' || $('#endDate').val() == '') {
|
||||
alert('Start date and end date are required.');
|
||||
return;
|
||||
}
|
||||
var sd = sdate();
|
||||
var ed = edate();
|
||||
var sysid = $('#systemSelect').val();
|
||||
window.location.href='/purge?action=purge&sd=' + sd + '&ed=' + ed + '&sysid=' + sysid + '&simulate=' + sim + '&kv=' + kv + '&bu=' + bu;
|
||||
|
||||
}
|
||||
|
||||
function addNewSystemTag() {
|
||||
if ($('#newSysId').val() == '' || $('#newSysTag').val() == '') {
|
||||
alert('System ID (dec) and System Tag are required.');
|
||||
return;
|
||||
}
|
||||
var hexId = $('#newSysId').val();
|
||||
var newId = parseInt(dec(hexId));
|
||||
var newTag = $('#newSysTag').val()
|
||||
if (! Number.isInteger(newId)) {
|
||||
alert('Invalid system ID.');
|
||||
return;
|
||||
}
|
||||
window.location.href='/asd?id=' + newId + '&tag=' + newTag;
|
||||
}
|
||||
|
||||
function importTalkgroupTsv(cmd) {
|
||||
$('#impProc').show()
|
||||
if ($('#selTsv').val() == '0' || $('#systemSelect2').val() == '0') {
|
||||
alert('TSV file selection and System selection are required.');
|
||||
$('#impProc').hide()
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#invtsv').length){
|
||||
$('#impProc').hide()
|
||||
alert('The TSV is invalid!');
|
||||
return;
|
||||
}
|
||||
|
||||
var sysid = $('#systemSelect2').val();
|
||||
var tsvfile = $('#selTsv').val();
|
||||
window.location.href='/itt?sysid=' + sysid + '&file=' + tsvfile + '&cmd=' + cmd;
|
||||
}
|
||||
|
||||
function deleteTags(cmd) {
|
||||
if ($('#systemSelect3').val() == '0') {
|
||||
alert('System selection is required.');
|
||||
return;
|
||||
}
|
||||
sysid = $('#systemSelect3').val();
|
||||
window.location.href='/delTags?sysid=' + sysid + '&cmd=' + cmd;
|
||||
}
|
||||
|
||||
function hex(dec) {
|
||||
if (!dec) return;
|
||||
return dec.toString(16);
|
||||
}
|
||||
|
||||
function dec(hex) {
|
||||
if (!hex) return;
|
||||
return parseInt(hex, 16);
|
||||
}
|
||||
|
||||
function csvTable(table_id, separator = ',') { // Quick and simple export target #table_id into a csv
|
||||
var rows = document.querySelectorAll('table#' + table_id + ' tr');
|
||||
// Construct csv
|
||||
var csv = [];
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = [],
|
||||
cols = rows[i].querySelectorAll('td, th');
|
||||
for (var j = 0; j < cols.length; j++) {
|
||||
// Clean innertext to remove multiple spaces and jumpline (break csv)
|
||||
var data = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ');
|
||||
// Escape double-quote with double-double-quote
|
||||
data = data.replace(/"/g, '""');
|
||||
// Push escaped string
|
||||
row.push('"' + data + '"');
|
||||
}
|
||||
csv.push(row.join(separator));
|
||||
}
|
||||
var csv_string = csv.join('\n');
|
||||
// Download it
|
||||
var filename = 'export_' + table_id + '_' + new Date().toLocaleDateString() + '.csv';
|
||||
var link = document.createElement('a');
|
||||
link.style.display = 'none';
|
||||
link.setAttribute('target', '_blank');
|
||||
link.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv_string));
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,144 @@
|
|||
<!--
|
||||
Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
Copyright 2020, 2021 Michael Rose
|
||||
|
||||
This file is part of OP25
|
||||
|
||||
OP25 is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with OP25; see the file COPYING. If not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
02110-1301, USA.
|
||||
|
||||
-->
|
||||
|
||||
{% include 'base.html' %}
|
||||
{% block extra_stylesheets %}
|
||||
<link href="static/css/datatables/jquery.dataTables-dark.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row-main">
|
||||
<div class="side">
|
||||
|
||||
</div> <!-- end side -->
|
||||
|
||||
<div class="main">
|
||||
|
||||
<div class="card mb-3 border-primary">
|
||||
<h4 class="card-header">About OP25 Logs</h4>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
|
||||
<div align="center">
|
||||
<div class="card border-secondary mb-3" style="max-width: 40rem; text-align: left;">
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
OP25 Logs (aka Oplog) is the OP25 sqlite3 logs database viewer.
|
||||
</p>
|
||||
|
||||
<p class="card-text">
|
||||
Copyright © 2020, 2021 Max H. Parke KA1RBI<br>
|
||||
Copyright © 2020, 2021 Michael Rose
|
||||
</p>
|
||||
<p class="card-text">
|
||||
OP25 Logs 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.
|
||||
</p>
|
||||
<p class="card-text">
|
||||
OP25 Logs 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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'footer-links.html' %}
|
||||
</div> <!-- end main -->
|
||||
|
||||
<div class="side">
|
||||
|
||||
</div>
|
||||
</div> <!-- end row -->
|
||||
|
||||
<!-- end secondary -->
|
||||
</div>
|
||||
<!-- end content -->
|
||||
|
||||
|
||||
</div>
|
||||
<br>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<!-- js moved to op25.js -->
|
||||
|
||||
{% block extra_javascripts %}
|
||||
<script src="static/js/datatables/jquery.dataTables.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#startDate').prop('disabled', true );
|
||||
$('#endDate').prop('disabled', true );
|
||||
$('#op25_esd').DataTable({
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
'bFilter': false,
|
||||
'paging': false,
|
||||
"ajax": '/esd',
|
||||
"columns": [
|
||||
null,
|
||||
{
|
||||
"data": [1],
|
||||
"render": function(data, type, row, meta){
|
||||
if(type === 'display'){
|
||||
data = data + ' - ' + hex(data).toUpperCase();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
||||
{
|
||||
"data": [3],
|
||||
"render": function(data, type, row, meta){
|
||||
if(type === 'display'){
|
||||
data = '<button type="button" class="btn btn-primary btn-sm" onclick="this.blur(); editTagName(' + data + ', \'' + row[2] + '\')">Edit Tag</button> \
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="window.location.href=\'\/dsd?id=' + data + '\'">Delete</button>';
|
||||
|
||||
}
|
||||
return data;
|
||||
},
|
||||
"width": "150px"
|
||||
}
|
||||
]
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function editTagName(id, t) {
|
||||
var tag = prompt("Enter new system tag:", t);
|
||||
if (tag == null || tag == '') {
|
||||
return;
|
||||
}
|
||||
window.location.href='/usd?id=' + id + '&tag=' + tag;
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,167 @@
|
|||
<!--
|
||||
Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
Copyright 2020, 2021 Michael Rose
|
||||
|
||||
This file is part of OP25
|
||||
|
||||
OP25 is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with OP25; see the file COPYING. If not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
02110-1301, USA.
|
||||
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ request.locale_name }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="pyramid web application">
|
||||
<meta name="author" content="Pylons Project">
|
||||
<title>OP25 - Logs</title>
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="stylesheet" type="text/css" href="static/css/op25.css">
|
||||
<link href="static/css/bootstrap/bootstrap-darkly.css" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="static/dtpick/jquery.datetimepicker.css">
|
||||
<style>
|
||||
</style>
|
||||
{% block extra_stylesheets %} {% endblock %}
|
||||
<script src="static/jquery/jquery-2.2.4.min.js"></script>
|
||||
<script src="static/js/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<script src="static/dtpick/dtpick2.js"></script>
|
||||
<script src="static/js/op25.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card text-white bg-primary mb-3">
|
||||
<div class="card-body">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<a style="width: 200px;" class="nav-link dropdown-toggle navbar-brand text-white" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><b>OP25 - Logs</b></a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{{ url_for('home') }}">Home</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{{ url_for('logs') }}?r=total_tgid">Total Talkgroup Voice Activity</a>
|
||||
<a class="dropdown-item" href="{{ url_for('logs') }}?r=call_detail">Call Detail</a>
|
||||
<a class="dropdown-item" href="{{ url_for('logs') }}?r=joins">Join Activity</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
{% for s in params['ekeys'] %}
|
||||
<a class="dropdown-item" href="#" onclick="javascript:load_new_page1('cc_event', '{{ s }}');"> {{ s|replace("_", " ") }}</a> {% endfor %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{{ url_for('editsys') }}">Edit System Tags</a>
|
||||
<a class="dropdown-item" href="{{ url_for('edit_tags') }}?cmd=tgid">Update Talkgroup Tags</a>
|
||||
<a class="dropdown-item" href="{{ url_for('edit_tags') }}?cmd=unit">Update Subscriber Tags</a>
|
||||
<a class="dropdown-item" href="{{ url_for('switch_db') }}">Backup & Switch Database</a>
|
||||
<a class="dropdown-item text-danger" href="{{ url_for('purge') }}">Purge Database</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{{ url_for('about') }}">About</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="float: right;">
|
||||
<b>System</b>
|
||||
<select id="systemSelect" style="">
|
||||
<option value="0" selected>All</option>
|
||||
</select>
|
||||
|
||||
<span>Start <input class="sel-date" type="text" id="startDate"></span>
|
||||
<span>End <input class="sel-date" type="text" id="endDate"> </span>
|
||||
<button style="width: 75px;" class="btn btn-info btn-sm" id="btnReset" onclick="location.reload();">Refresh</button>
|
||||
<button style="width: 75px;" class="btn btn-info btn-sm" id="btnClear" onclick="resetDates();">Clear</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container" style="margin-top: 0px; margin-bottom: 15px;">
|
||||
<div align="center">
|
||||
<a href="/"><img src="static/op25-dark-h.png" title="OP25"></a>
|
||||
</div>
|
||||
{% block content %} {% endblock %}
|
||||
</div>
|
||||
|
||||
{% block extra_javascripts %}
|
||||
|
||||
<script>
|
||||
$('#startDate').datetimepicker({
|
||||
inline:false,
|
||||
});
|
||||
|
||||
$('#endDate').datetimepicker({
|
||||
inline:false,
|
||||
});
|
||||
|
||||
$('#startDate').change( function(){
|
||||
localStorage.logStart = $('#startDate').val();
|
||||
});
|
||||
|
||||
$('#endDate').change( function(){
|
||||
localStorage.logEnd = $('#endDate').val();
|
||||
});
|
||||
|
||||
{% if sysList is not none %}
|
||||
{% for i in sysList %}
|
||||
{% if i.tag is not none %}
|
||||
$('#systemSelect').append(new Option('{{ i.tag }} - {{ i.sysid }} - 0x{{ ( '%0x' % i.sysid ).upper() }}', '{{ i.sysid }}'));
|
||||
{% else %}
|
||||
$('#systemSelect').append(new Option('{{ i.sysid }} - 0x{{ ( '%0x' % i.sysid ).upper() }}', '{{ i.sysid }}'));
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
$('#systemSelect').change( function(){
|
||||
localStorage.systemSelect = $('#systemSelect').val();
|
||||
});
|
||||
|
||||
$('#navSelect').append(new Option('Home', '4'));
|
||||
$('#navSelect').append(new Option('Total Talkgroup Voice Activity', '1'));
|
||||
$('#navSelect').append(new Option('Call Detail', '2'));
|
||||
$('#navSelect').append(new Option('Join Activity', '3'));
|
||||
|
||||
{% for s in params['ekeys'] %}
|
||||
$('#navSelect').append(new Option( '{{ s }}', '{{ s }}' ));
|
||||
{% endfor %}
|
||||
|
||||
// pretty sure this is not used anymore 7/2
|
||||
$('#navSelect').change(function(){
|
||||
var ns = $('#navSelect').val();
|
||||
if (ns == '0')
|
||||
return;
|
||||
if (ns == '1') {
|
||||
window.location.href='/logs?r=total_tgid';
|
||||
return;
|
||||
}
|
||||
if (ns == '2'){
|
||||
window.location.href='/logs?r=call_detail';
|
||||
return;
|
||||
}
|
||||
if (ns == '3'){
|
||||
window.location.href='/logs?r=joins';
|
||||
return;
|
||||
}
|
||||
if (ns == '4'){
|
||||
window.location.href='/';
|
||||
return;
|
||||
}
|
||||
load_new_page1('cc_event', ns);
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,53 @@
|
|||
<div class="card mb-3 border-primary">
|
||||
<h4 class="card-header">Database Statistics</h4>
|
||||
<div class="card-body">
|
||||
<div class="card mb-3 bg-dark border-primary">
|
||||
<table border="0" style="width: 100%;" class="border-primary">
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding: 0px;">
|
||||
<div class="card mb-3 bg-dark border-dark">
|
||||
<span class="card-header">Records</span>
|
||||
<div class="card-body">
|
||||
<table style="padding: 5px; width: 100%; border="0">
|
||||
<tr><td>Total Records: <b><span id="records">{{ dbstats[0] }}</span></b>
|
||||
|
||||
Talkgroups: <b><span id="talkgroups">{{ dbstats[2] }}</span></b>
|
||||
|
||||
Subscribers: <b><span id="subs">{{ dbstats[3] }}</span></b>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td>First: <b><span id="firstDate"> {{ dbstats[4] }} </b></span></td>
|
||||
</tr><tr>
|
||||
<td>Last: <b><span id="lastDate"> {{ dbstats[5] }} </b></span></td>
|
||||
</tr><tr>
|
||||
<td>Database Size: <b><span id="dbSize"> {{ dbstats[6] }} </b></span></td>
|
||||
</tr><tr>
|
||||
{% set fn = dbstats[7].split('/') %}
|
||||
<td>Database File: <b><span id="dbFile"> {{ fn|last }} </b> <a href="#" title=" {{ dbstats[7] }}">?</a></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="vertical-align: top; padding: 0px;">
|
||||
<div class="card mb-3 bg-dark border-dark">
|
||||
<span class="card-header">Logged Systems: <b><span id="systems">{{ dbstats[1] }}</span></b><br></span>
|
||||
<div class="card-body">
|
||||
<table style="padding: 5px; width: 100%;" class="table table-hover">
|
||||
{% for i in sysList %}
|
||||
<tr>
|
||||
{% if i.tag is not none %}
|
||||
<td style="padding: 2px;"> {{ i.sysid }} </td><td style="padding: 2px;"> 0x{{ ( '%0x' % i.sysid ).upper() }} </td><td style="padding: 2px;"> {{ i.tag }} </td>
|
||||
{% else %}
|
||||
<td style="padding: 2px;"> {{ i.sysid }}</td><td style="padding: 2px;"> 0x{{ ( '%0x' % i.sysid ).upper() }} </td><td style="padding: 2px;"> — </td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
<!--
|
||||
Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
Copyright 2020, 2021 Michael Rose
|
||||
|
||||
This file is part of OP25
|
||||
|
||||
OP25 is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with OP25; see the file COPYING. If not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
02110-1301, USA.
|
||||
|
||||
-->
|
||||
|
||||
{% include 'base.html' %}
|
||||
{% block extra_stylesheets %}
|
||||
<link href="static/css/datatables/jquery.dataTables-dark.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row-main">
|
||||
<div class="side">
|
||||
|
||||
</div> <!-- end side -->
|
||||
|
||||
<div class="main">
|
||||
<div class="card mb-3 border-primary">
|
||||
{% if cmd == 'tgid' %}
|
||||
<h4 class="card-header">Talkgroup Tags</h4>
|
||||
{% elif cmd == 'unit' %}
|
||||
<h4 class="card-header">Subscriber Tags</h4>
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
|
||||
<div class="card mb-3 border-secondary">
|
||||
<h5 class="card-header">Import Tool</h4>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
This tool imports tags from the selected TSV and associates them with the
|
||||
selected system. Existing System ID and Talkgroup or Subscriber ID combinations will be overwritten by the tag values in the TSV file.
|
||||
To add new tags: Add in OP25 Web UI, then import them here. Wildcard entries are not imported.
|
||||
</p>
|
||||
<label for="selTsv">Choose TSV file:</label>
|
||||
<select name="selTsv" id="selTsv">
|
||||
<option value='0' >Select...</option>
|
||||
{% for i in tsvs %}
|
||||
{% if '.tsv' in i and '._' not in i %}
|
||||
<option value="{{ i.split('/../')[-1] }}">{{ i.split('/../')[-1] }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="systemSelect2">Choose System: </label>
|
||||
<select id="systemSelect2">
|
||||
<option value="0" selected>Select...</option>
|
||||
</select>
|
||||
<br><br>
|
||||
|
||||
<button class="btn btn-primary" onclick="this.blur(); inspectTsv();">Inspect TSV</button>
|
||||
<br>
|
||||
<div id="inspect" style="display: none;">
|
||||
<br>
|
||||
<div id="inspectText" style="width: 100%; height: 225px; overflow: auto;"></div>
|
||||
<br>
|
||||
<button class="btn btn-primary" onclick="this.blur(); importTalkgroupTsv('{{ cmd }}');">Import TSV</button>
|
||||
<img id="impProc" src="static/loading.gif" style="height: 20px; display: none;" alt="loading">
|
||||
</div>
|
||||
|
||||
{% if session['sm'] == 3 %}
|
||||
<br>
|
||||
<div class="alert alert-dismissible alert-success">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<strong>Import completed.</strong><br><br>
|
||||
Existing records updated:<b> {{ session['imp_results'][0] }}</b>
|
||||
New records added:<b> {{ session['imp_results'][1] }} </b>
|
||||
Duplicate records corrected:<b> {{ session['imp_results'][2] }} </b>
|
||||
</div>
|
||||
{{ clear_sm() }}
|
||||
{% endif %}
|
||||
|
||||
{% if session['sm'] == 4 %}
|
||||
<br>
|
||||
<div class="alert alert-dismissible alert-primary">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<strong>Tags deleted.</strong><br><br>
|
||||
</div>
|
||||
{{ clear_sm() }}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="card mb-3 border-secondary">
|
||||
<h5 class="card-header">Editor</h4>
|
||||
<div class="card-body">
|
||||
<div align="right">
|
||||
<label for="systemSelect4">Filter by system: </label>
|
||||
<select id="systemSelect4">
|
||||
<option value="0" selected>All</option>
|
||||
</select><br><br>
|
||||
<button style="width: 75px;" class="btn btn-primary btn-sm" id="applyFilter" onclick="location.reload();">Apply</button>
|
||||
<br><br></div>
|
||||
|
||||
{% if session['sm'] == 1 %}
|
||||
<div class="alert alert-dismissible alert-success">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<strong>Edit completed.</strong>
|
||||
</div>
|
||||
{{ clear_sm() }}
|
||||
{% endif %}
|
||||
|
||||
{% if session['sm'] == 2 %}
|
||||
<div class="alert alert-dismissible alert-warning">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<strong>Record deleted.</strong>
|
||||
</div>
|
||||
{{ clear_sm() }}
|
||||
{% endif %}
|
||||
|
||||
<table id="op25_esd" class="display" cellspacing="0" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if cmd == 'tgid' %}
|
||||
<th>Record ID</th>
|
||||
<th>System ID</th>
|
||||
<th>Talkgroup ID</th>
|
||||
<th>Talkgroup Tag</th>
|
||||
<th>Actions</th>
|
||||
{% elif cmd == 'unit' %}
|
||||
<th>Record ID</th>
|
||||
<th>System ID</th>
|
||||
<th>Subscriber ID</th>
|
||||
<th>Subscriber Tag</th>
|
||||
<th>Actions</th>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 border-danger">
|
||||
<h5 class="card-header">Delete Tags</h4>
|
||||
<div class="card-body">
|
||||
Delete all
|
||||
{% if cmd == 'tgid' %}
|
||||
talkgroup
|
||||
{% elif cmd == 'unit' %}
|
||||
subscriber
|
||||
{% endif %}
|
||||
tags associated with a system. You cannot undo this action!
|
||||
<br><br>
|
||||
<label for="systemSelect3">Choose System: </label>
|
||||
<select id="systemSelect3">
|
||||
<option value="0" selected>Select...</option>
|
||||
</select>
|
||||
<br><br>
|
||||
<div align="center">
|
||||
<button id="btnPurge" class="btn btn-danger" onclick="this.blur(); deleteTags('{{ cmd }}');">Delete
|
||||
{% if cmd == 'tgid' %}
|
||||
Talkgroup
|
||||
{% elif cmd == 'unit' %}
|
||||
Subscriber
|
||||
{% endif %}
|
||||
Tags
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'footer-links.html' %}
|
||||
</div> <!-- end main -->
|
||||
|
||||
<div class="side">
|
||||
|
||||
</div>
|
||||
</div> <!-- end row -->
|
||||
<br>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_javascripts %}
|
||||
|
||||
<script src="static/js/datatables/jquery.dataTables.js"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#startDate').prop('disabled', true );
|
||||
$('#endDate').prop('disabled', true );
|
||||
var sysid = $('#systemSelect4').val();
|
||||
$('#op25_esd').DataTable({
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
'bFilter': true,
|
||||
'paging': true,
|
||||
|
||||
"ajax": {
|
||||
"url": '/edittg',
|
||||
"data": { "sysid": sysid,
|
||||
"cmd": '{{ cmd }}',
|
||||
}
|
||||
},
|
||||
|
||||
"columns": [
|
||||
{ "visible": false },
|
||||
{
|
||||
"data": [1],
|
||||
"render": function(data, type, row, meta){
|
||||
if(type === 'display'){
|
||||
data = data + ' - ' + hex(data).toUpperCase();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"data": [3],
|
||||
"render": function(data, type, row, meta){
|
||||
if(type === 'display'){
|
||||
data = '<button type="button" class="btn btn-primary btn-sm" onclick="this.blur(); editTagName(' + row[0] + ', \'' + row[3] + '\')">Edit Tag</button> \
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="window.location.href=\'\/dtd?cmd={{ cmd }}&id=' + row[0] + '\'">Delete</button>';
|
||||
}
|
||||
return data;
|
||||
},
|
||||
"width": "150px"
|
||||
}
|
||||
]
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function editTagName(id, t) {
|
||||
var tag = prompt("Enter new tag:", t);
|
||||
if (tag == null || tag == '')
|
||||
return;
|
||||
window.location.href='/utd?id=' + id + '&tag=' + tag + '&cmd={{ cmd }}';
|
||||
}
|
||||
|
||||
function inspectTsv() {
|
||||
var file = $('#selTsv').val();
|
||||
if (file == '0')
|
||||
return;
|
||||
|
||||
f = '/inspect?file=' + file;
|
||||
$.ajax({
|
||||
url : f,
|
||||
type : 'GET',
|
||||
success : popInsp,
|
||||
error : function(XMLHttpRequest, textStatus, errorThrown) {alert('Error: \n\nFile:' + f + '\n\n' + errorThrown + '\n\n');}
|
||||
});
|
||||
}
|
||||
|
||||
function popInsp(h) {
|
||||
$('#inspect').show();
|
||||
$('#inspectText').html(h).scrollTop(0);
|
||||
}
|
||||
|
||||
{% if systems is not none %}
|
||||
{% for i in systems %}
|
||||
{% if i.tag is not none %}
|
||||
$('#systemSelect2').append(new Option('{{ i.sysid }} - 0x{{ ( '%0x' % i.sysid ).upper() }} - {{ i.tag }}', '{{ i.sysid }}'));
|
||||
$('#systemSelect3').append(new Option('{{ i.sysid }} - 0x{{ ( '%0x' % i.sysid ).upper() }} - {{ i.tag }}', '{{ i.sysid }}'));
|
||||
$('#systemSelect4').append(new Option('{{ i.sysid }} - 0x{{ ( '%0x' % i.sysid ).upper() }} - {{ i.tag }}', '{{ i.sysid }}'));
|
||||
{% else %}
|
||||
$('#systemSelect2').append(new Option('{{ i.sysid }} - 0x{{ ( '%0x' % i.sysid ).upper() }}', '{{ i.sysid }}'));
|
||||
$('#systemSelect3').append(new Option('{{ i.sysid }} - 0x{{ ( '%0x' % i.sysid ).upper() }}', '{{ i.sysid }}'));
|
||||
$('#systemSelect4').append(new Option('{{ i.sysid }} - 0x{{ ( '%0x' % i.sysid ).upper() }}', '{{ i.sysid }}'));
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
$('#systemSelect4').change( function(){
|
||||
localStorage.systemSelect4 = $('#systemSelect4').val();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
<!--
|
||||
Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
Copyright 2020, 2021 Michael Rose
|
||||
|
||||
This file is part of OP25
|
||||
|
||||
OP25 is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with OP25; see the file COPYING. If not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
02110-1301, USA.
|
||||
|
||||
-->
|
||||
|
||||
{% include 'base.html' %}
|
||||
{% block extra_stylesheets %}
|
||||
<link href="static/css/datatables/jquery.dataTables-dark.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row-main">
|
||||
<div class="side">
|
||||
|
||||
</div> <!-- end side -->
|
||||
|
||||
<div class="main">
|
||||
|
||||
<div class="card mb-3 border-primary">
|
||||
<h4 class="card-header">System Tags</h4>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
</p>
|
||||
|
||||
<table id="op25_esd" class="display" cellspacing="0" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Record ID</th>
|
||||
<th>System ID</th>
|
||||
<th>System Name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<br><br>
|
||||
<table style="width: 65%; padding: 0px;" class="border-primary">
|
||||
<tr>
|
||||
<td style="vertical-align: top;">
|
||||
<div class="form-floating mb-3 primary">
|
||||
<input type="text" width="10" class="form-control" id="newSysId" placeholder="">
|
||||
<label for="floatingInput">System ID (hex)</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" size="25" id="newSysTag" placeholder="">
|
||||
<label for="floatingInput">System Tag</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: left; vertical-align: top;">
|
||||
<button class="btn btn-primary" onclick="this.blur; addNewSystemTag();">Add New</button>
|
||||
</td>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'footer-links.html' %}
|
||||
</div> <!-- end main -->
|
||||
|
||||
<div class="side">
|
||||
|
||||
</div>
|
||||
</div> <!-- end row -->
|
||||
|
||||
<!-- end secondary -->
|
||||
</div>
|
||||
<!-- end content -->
|
||||
|
||||
|
||||
</div>
|
||||
<br>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<!-- js moved to op25.js -->
|
||||
|
||||
{% block extra_javascripts %}
|
||||
<script src="static/js/datatables/jquery.dataTables.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#startDate').prop('disabled', true );
|
||||
$('#endDate').prop('disabled', true );
|
||||
$('#op25_esd').DataTable({
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
'bFilter': false,
|
||||
'paging': false,
|
||||
"ajax": '/esd',
|
||||
"columns": [
|
||||
null,
|
||||
{
|
||||
"data": [1],
|
||||
"render": function(data, type, row, meta){
|
||||
if(type === 'display'){
|
||||
data = data + ' - ' + hex(data).toUpperCase();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
||||
{
|
||||
"data": [3],
|
||||
"render": function(data, type, row, meta){
|
||||
if(type === 'display'){
|
||||
data = '<button type="button" class="btn btn-primary btn-sm" onclick="this.blur(); editTagName(' + data + ', \'' + row[2] + '\')">Edit Tag</button> \
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="window.location.href=\'\/dsd?id=' + data + '\'">Delete</button>';
|
||||
|
||||
}
|
||||
return data;
|
||||
},
|
||||
"width": "150px"
|
||||
}
|
||||
]
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function editTagName(id, t) {
|
||||
var tag = prompt("Enter new system tag:", t);
|
||||
if (tag == null || tag == '') {
|
||||
return;
|
||||
}
|
||||
window.location.href='/usd?id=' + id + '&tag=' + tag;
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,191 @@
|
|||
<!--
|
||||
Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
Copyright 2020, 2021 Michael Rose
|
||||
|
||||
This file is part of OP25
|
||||
|
||||
OP25 is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with OP25; see the file COPYING. If not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
02110-1301, USA.
|
||||
|
||||
-->
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<html lang="{{ request.locale_name }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="pyramid web application">
|
||||
<meta name="author" content="Pylons Project">
|
||||
<title>OP25 - Logs</title>
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="stylesheet" type="text/css" href="static/css/op25.css">
|
||||
<link href="static/css/bootstrap/bootstrap-darkly.css" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="static/dtpick/jquery.datetimepicker.css">
|
||||
<style>
|
||||
</style>
|
||||
{% block extra_stylesheets %} {% endblock %}
|
||||
<script src="static/jquery/jquery-2.2.4.min.js"></script>
|
||||
<script src="static/js/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<script src="static/dtpick/dtpick2.js"></script>
|
||||
<script src="static/js/op25.js"></script>
|
||||
</head>
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<p> </p>
|
||||
</div>
|
||||
<div id="primary">
|
||||
|
||||
</div>
|
||||
<div id="content" align="center">
|
||||
<div class="card mb-3 border-primary" style="max-width: 60rem; text-align: left;">
|
||||
<h4 class="card-header bg-danger">OP25 Logs - Database Error (Code {{ code }})</h4>
|
||||
<div class="card-body">
|
||||
|
||||
{% if code == 1 %}
|
||||
<div class="alert alert-dismissible">
|
||||
<strong>Database file does not exist.</strong> <br><br> {{ file }}</span> <Br><Br>File not found.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if code == 2 %}
|
||||
<div class="alert alert-dismissible">
|
||||
<strong>Database file is too small. </strong> <br><Br> {{ file }} <br><br>Attributes do not conform.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if code == 4 %}
|
||||
<div class="alert alert-dismissible">
|
||||
<strong>Database contains no data. </strong> <br><Br> {{ file }} <br><br> Database structure is good, but no data was found. <br><br>0 rows in table 'data_store'.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if code == 5 %}
|
||||
<div class="alert alert-dismissible">
|
||||
<strong>Database access error. </strong> <br><Br> {{ file }} <br><br> Database might be locked or in use by another process (OP25). <br><br>
|
||||
Source: {{ source }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div align="center">
|
||||
<button class="btnMain btn btn-outline-info" onclick="window.location.href='/'">Try Again</button>
|
||||
<br><br>
|
||||
</div>
|
||||
|
||||
{% if code == 5 %}
|
||||
<div class="card mb-3 border-primary" style="max-width: 60rem; text-align: left;">
|
||||
<h4 class="card-header bg-secondary">Traceback</h4>
|
||||
<div class="card-body">
|
||||
|
||||
{{ e }} <br><br>
|
||||
{{ err }}
|
||||
</div></div>
|
||||
{% endif %}
|
||||
|
||||
{% if code != 5 %}
|
||||
|
||||
<div class="card mb-3 border-primary" style="max-width: 60rem;">
|
||||
<h4 class="card-header bg-primary">../op25/gr-op25_repeater/apps/README</h4>
|
||||
<div class="card-body" style="text-align: left;">
|
||||
|
||||
<h4>Setup SQL Log Database (Optional)</h4>
|
||||
<p>
|
||||
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
1. Make sure that sqlite3 is installed in python
|
||||
</p>
|
||||
<p>
|
||||
2. Initialize DB (any existing DB data will be destroyed)
|
||||
</p>
|
||||
<p>
|
||||
WARNING: OP25 MUST NOT BE RUNNING DURING THIS STEP
|
||||
</p>
|
||||
<p>
|
||||
op25/.../apps$ python sql_dbi.py reset_db
|
||||
</p>
|
||||
<p>
|
||||
3. Import talkgroups tags file
|
||||
</p>
|
||||
<p>
|
||||
op25/.../apps$ python sql_dbi.py import_tgid tags.tsv
|
||||
</p>
|
||||
<p>
|
||||
also, import the radio ID tags file (optional)
|
||||
</p>
|
||||
<p>
|
||||
op25/.../apps$ python sql_dbi.py import_unit radio-tags.tsv
|
||||
</p>
|
||||
<p>
|
||||
import the System ID tags file (see below)
|
||||
</p>
|
||||
<p>
|
||||
op25/.../apps$ python sql_dbi.py import_sysid sysid-tags.tsv
|
||||
</p>
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div id="secondary">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end secondary -->
|
||||
|
||||
</div>
|
||||
<!-- end content -->
|
||||
|
||||
</div>
|
||||
<br>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_javascripts %}
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,17 @@
|
|||
<!-- footer links -->
|
||||
<div class="card border-primary mb-3">
|
||||
<div class="card-body">
|
||||
<div align="center">
|
||||
<a href="{{ url_for('home') }}">Home</a>
|
||||
<a href="{{ url_for('editsys') }}">System Tags</a>
|
||||
<a href="{{ url_for('edit_tags') }}?cmd=tgid">Talkgroup Tags</a>
|
||||
<a href="{{ url_for('edit_tags') }}?cmd=unit">Unit Tags</a>
|
||||
<a href="{{ url_for('purge') }}">Purge</a>
|
||||
<a href="{{ url_for('switch_db') }}">Backup & Switch</a>
|
||||
<a href="{{ url_for('about') }}">About</a>
|
||||
<br>
|
||||
Server time: {{ t_loc() }} <br>
|
||||
08.29.2021
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,129 @@
|
|||
<!--
|
||||
Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
Copyright 2020, 2021 Michael Rose
|
||||
|
||||
This file is part of OP25
|
||||
|
||||
OP25 is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with OP25; see the file COPYING. If not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
02110-1301, USA.
|
||||
|
||||
-->
|
||||
|
||||
{% include 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row-main">
|
||||
<div class="side">
|
||||
|
||||
</div> <!-- end side -->
|
||||
|
||||
<div class="main">
|
||||
|
||||
{% include "dbstats.html" %}
|
||||
|
||||
|
||||
<div class="card mb-3 border-primary">
|
||||
<h4 class="card-header">Activity and Counts by Subscriber or Talkgroup</h4>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<button class="btnMain btn btn-outline-info" onclick="window.location.href='/logs?r=total_tgid'">Total Talkgroup<br>Voice Activity</button>
|
||||
|
||||
<button class="btnMain btn btn-outline-info" onclick="window.location.href='/logs?r=call_detail'">Call<br>Detail</button>
|
||||
|
||||
<button class="btnMain btn btn-outline-info" onclick="window.location.href='/logs?r=joins'">Join<br>Activity</button>
|
||||
<hr style="height: 2px;">
|
||||
<!-- <input class="op-input" style="height: 62px; text-align: center; width: 210px; border: 1px solid orange; background-color: #333; color:#ccc;" placeholder="Enter SU or Talkgroup ID" type="text" id="resource_id"</input> -->
|
||||
|
||||
<div class="form-floating mb-3 primary" style="width: 215px;">
|
||||
<input type="text" width="10" class="form-control" style="height: 62px;" id="resource_id" placeholder="">
|
||||
<label for="floatingInput">TGID or SUID</label>
|
||||
</div>
|
||||
|
||||
<button class="btnMain btn btn-outline-warning" onclick="this.blur(); load_new_page0('tgid');">SU ID Activity for Specified TGID</button>
|
||||
|
||||
<button class="btnMain btn btn-outline-warning" onclick="this.blur(); load_new_page0('su');">Count of Calls by TGID for Specified SU ID</button>
|
||||
<p><br>Note: The ID you enter can define a range of IDs to search, for example:
|
||||
<br>
|
||||
<ul>
|
||||
<li>1234000-1234599 to search specified range</li>
|
||||
</li>
|
||||
<li>1234??? Search for matches between 1234000 and 1234999</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3 border-primary">
|
||||
<h4 class="card-header">Control Channel Events</h4>
|
||||
<div class="card-body">
|
||||
Filter by talkgroup ID, subscriber ID, or both (optional):
|
||||
<table style="width: 400px; padding: 0px;">
|
||||
<tr>
|
||||
<td style="vertical-align: top;">
|
||||
<div class="form-floating mb-3 primary">
|
||||
<input type="text" width="10" class="form-control" id="cc_filter_tgid" style="width: 150px;" placeholder="">
|
||||
<label for="floatingInput">Talkgroup ID</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="form-floating mb-3">
|
||||
<input type="text" class="form-control" size="25" id="cc_filter_suid" style="width: 150px;" placeholder="">
|
||||
<label for="floatingInput">Subscriber ID</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-primary" onclick="this.blur(); clrcc();">Clear</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br>
|
||||
<table>
|
||||
{% for i in params['ekeys'] %}
|
||||
<tr>
|
||||
<td><button class="btnMain btn btn-outline-info" style="height: 38px;" onclick="this.blur(); load_new_page1('cc_event', '{{ i }}');">{{ i|replace("_", " ") }}</button></td>
|
||||
<td> {{ params['cc_desc'][i] }} </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'footer-links.html' %}
|
||||
</div> <!-- end main -->
|
||||
|
||||
<div class="side">
|
||||
|
||||
</div>
|
||||
</div> <!-- end row -->
|
||||
|
||||
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<!-- js moved to op25.js -->
|
||||
|
||||
{% block extra_javascripts %}
|
||||
|
||||
<script>
|
||||
|
||||
function clrcc() {
|
||||
$('#cc_filter_tgid').val('');
|
||||
$('#cc_filter_suid').val('');
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
|||
<!-- Import TSV inspection -->
|
||||
{% if i|length == 0 %}
|
||||
<div class="alert alert-dismissible alert-danger" id="invtsv">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<strong>Invalid TSV</strong><br><br> The TSV is not valid for import.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-warning">Records: <b> {{ i|length }} </b></div><br>
|
||||
<table class="table table-hover">
|
||||
<th>ID</th>
|
||||
<th>Tag</th>
|
||||
<th>Priority/Color</th>
|
||||
{% for s in i %}
|
||||
<tr><td style="padding 2px; width: 150px;"> {{ s[0] }} </td><td style="padding 2px; width: 350px;"> {{ s[1] }} </td><td style="padding 2px;"> {{ s[2] }}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<br>
|
||||
{% endif %}
|
|
@ -0,0 +1,212 @@
|
|||
<!--
|
||||
Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
Copyright 2020, 2021 Michael Rose
|
||||
|
||||
This file is part of OP25
|
||||
|
||||
OP25 is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with OP25; see the file COPYING. If not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
02110-1301, USA.
|
||||
|
||||
-->
|
||||
{% include 'base.html' %}
|
||||
{% block extra_stylesheets %}
|
||||
<link href="static/css/datatables/jquery.dataTables-dark.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row-main">
|
||||
<div class="main">
|
||||
<br>
|
||||
|
||||
<br>
|
||||
{% if params['r'] == 'su' %}
|
||||
<h3>Count by Unit for SU ID {{ params['q'] }} {{ tag }}</h3>
|
||||
{% elif params['r'] == 'joins' %}
|
||||
<h3>Group Join Detail</h3>
|
||||
{% elif params['r'] == 'cc_event' %}
|
||||
<h3>CC Event Type: {{ params['cc_desc'][ params['p']] }}</h3>
|
||||
{% if params['tgid'] != '0' or params['suid'] != '0' %}
|
||||
Filtered by:
|
||||
{% endif %}
|
||||
|
||||
{% if params['tgid'] != '0' %}
|
||||
Talkgroup ID = {{ params['tgid'] }}
|
||||
{% endif %}
|
||||
|
||||
{% if params['suid'] != '0' %}
|
||||
Source ID = {{ params['suid'] }}
|
||||
{% endif %}
|
||||
{% elif params['r'] == 'total_tgid' %}
|
||||
<h3>Total Talkgroup Voice Activity</h3>
|
||||
{% elif params['r'] == 'call_detail' %}
|
||||
<h3>Call Detail</h3>
|
||||
Includes opcodes 0x00 and 0x02
|
||||
{% else %}
|
||||
<h3>Count by Unit for Talkgroup ID {{ params['q'] }} {{ tag }}</h3>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<table id="op25_logs" class="display" cellspacing="0" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if params['r'] == 'tgid' %}
|
||||
<th>Subscriber ID</th>
|
||||
<th>Subscriber Tag</th>
|
||||
<th>Count</th>
|
||||
<th>Last Seen</th>
|
||||
{% elif params['r'] == 'total_tgid' %}
|
||||
<th>System ID</th>
|
||||
<th>System</th>
|
||||
<th>Talkgroup ID</th>
|
||||
<th>Talkgroup</th>
|
||||
<th>Count</th>
|
||||
{% elif params['r'] == 'call_detail' %}
|
||||
<th>Time</th>
|
||||
<th>Opcode</th>
|
||||
<th>System ID</th>
|
||||
<th>System</th>
|
||||
<th>Talkgrou ID</th>
|
||||
<th>Talkgroup</th>
|
||||
<th>Source ID</th>
|
||||
<th>Source</th>
|
||||
<th>Frequency</th>
|
||||
{% elif params['r'] == 'joins' %}
|
||||
<th>Time</th>
|
||||
<th>Opcode</th>
|
||||
<th>System ID</th>
|
||||
<th>System</th>
|
||||
<th>RV</th>
|
||||
<th>Talkgrou ID</th>
|
||||
<th>Talkgroup</th>
|
||||
<th>Source ID</th>
|
||||
<th>Source</th>
|
||||
{% elif params['r'] == 'calls' %}
|
||||
<th>Time</th>
|
||||
<th>Sysid</th>
|
||||
<th>Tgid</th>
|
||||
<th>Talkgroup</th>
|
||||
<th>Frequency</th>
|
||||
<th>SU ID</th>
|
||||
|
||||
{% elif params['r'] == 'cc_event' %}
|
||||
{% for i in params['ckeys'] %}
|
||||
<th> {{ i }} </th>
|
||||
{% endfor %}
|
||||
|
||||
{% else %}
|
||||
<th>Talkgroup</th>
|
||||
<th>TGID</th>
|
||||
<th>Count</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<button onclick="this.blur(); csvTable('op25_logs')" class="btn btn-light btn-sm" title="Export current view to CSV.">Export CSV</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block extra_javascripts %}
|
||||
<!-- <script src="https://cdn.datatables.net/1.10.10/js/jquery.dataTables.min.js"></script> -->
|
||||
<script src="static/js/datatables/jquery.dataTables.js"></script>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(document).ready(function () {
|
||||
var sd = sdate();
|
||||
var ed = edate();
|
||||
var sysid = $('#systemSelect').val();
|
||||
|
||||
var filter_tgid = {% if params['tgid'] is defined %}
|
||||
{{ params['tgid'] }};
|
||||
{% else %}
|
||||
0;
|
||||
{% endif %}
|
||||
var filter_suid = {% if params['suid'] is defined %}
|
||||
{{ params['suid'] }};
|
||||
{% else %}
|
||||
0;
|
||||
{% endif %}
|
||||
|
||||
console.log('filter_tgid=' + filter_tgid);
|
||||
console.log(typeof filter_tgid);
|
||||
console.log('filter_suid=' + filter_suid);
|
||||
console.log(typeof filter_suid);
|
||||
|
||||
var table = $('#op25_logs').DataTable({
|
||||
"lengthMenu": [[10, 25, 50, 100, 500, 1000, 2500], [10, 25, 50, 100, 500, '1,000', '2,500']],
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
|
||||
{% if params['p'] == 'grp_v_ch_grant' %}
|
||||
"columns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ render: function(data){ return data / 1000000; }},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
{% endif %}
|
||||
|
||||
{% if params['p'] == 'mot_grg_cn_grant' %}
|
||||
"columns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ render: function(data){ return data / 1000000; }},
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
{% endif %}
|
||||
|
||||
{% if params['r'] == 'call_detail' %}
|
||||
"columns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ render: function(data){ return data / 1000000; } },
|
||||
],
|
||||
{% endif %}
|
||||
|
||||
|
||||
"ajax": {
|
||||
"url": "{{ url_for('data') }}",
|
||||
"data": {
|
||||
"host_rid": "{{ params['q'] }}",
|
||||
"host_function_type": "{{ params['r'] }}",
|
||||
"host_function_param": "{{ params['p'] }}",
|
||||
"sdate": sd,
|
||||
"edate": ed,
|
||||
"sysid": sysid,
|
||||
"tgid": filter_tgid,
|
||||
"suid": filter_suid
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
<br><br><br>
|
||||
{% include 'footer-links.html' %}
|
|
@ -0,0 +1,123 @@
|
|||
<!--
|
||||
Copyright 2017, 2018 Max H. Parke KA1RBI
|
||||
Copyright 2020, 2021 Michael Rose
|
||||
|
||||
This file is part of OP25
|
||||
|
||||
OP25 is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3, or (at your option)
|
||||
any later version.
|
||||
|
||||
OP25 is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with OP25; see the file COPYING. If not, write to the Free
|
||||
Software Foundation, Inc., 51 Franklin Street, Boston, MA
|
||||
02110-1301, USA.
|
||||
|
||||
-->
|
||||
|
||||
{% include 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="loading">
|
||||
<img id="loading-image" src="static/loading.gif" height="35px" alt="Loading..." />
|
||||
<br>Processing...
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row-main">
|
||||
<div class="side">
|
||||
|
||||
</div> <!-- end side -->
|
||||
|
||||
<div class="main">
|
||||
|
||||
{% include "dbstats.html" %}
|
||||
|
||||
|
||||
<div class="card mb-3 border-primary">
|
||||
<h4 class="card-header bg-danger">Purge Database</h4>
|
||||
<div class="card-body">
|
||||
{% if successMessage == 1 %}
|
||||
<div class="alert alert-dismissible alert-primary">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<strong>Operation completed.</strong> <span id="recCount">{{ recCount }}</span> records have been deleted.<br><br>Executed query:<br><br> {{ dispQuery }}</a>
|
||||
{% if params['bu'] == 'true' %}
|
||||
<br><br>Backup file created: {{ destfile }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if successMessage == 2 %}
|
||||
<div class="alert alert-dismissible alert-info">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<strong>Simulated operation completed.</strong> <span id="recCount">{{ recCount }}</span> records will be deleted.<br><br>Simulated query:<br><br> {{ dispQuery }}</a>
|
||||
{% if params['bu'] == 'true' %}
|
||||
<br><br>Simulated backup file created: {{ destfile }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
To purge records from the database, select the system, start date and end date above, then click Purge Database. Talkgroup and subscriber tags are not affected.
|
||||
<Br><Br>
|
||||
To prevent accidental data loss, a start date and end date are required.
|
||||
<Br><Br>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="keepVoice" checked="checked">
|
||||
<label class="form-check-label" for="keepVoice"><b>Keep Voice Channel Grant Data</b></label>
|
||||
When selected, all voice channel grant data will be saved.
|
||||
<br>
|
||||
<input class="form-check-input" type="checkbox" id="createBackup">
|
||||
<label class="form-check-label" for="createBackup"><b>Create Database Backup File</b></label>
|
||||
Create a backup of the database before purging.
|
||||
</div>
|
||||
<br>
|
||||
<span class="text-danger"><b>FINAL WARNING:</b></span> Once you click "Purge Database", the action cannot be undone.</span></b><br><br>
|
||||
<div align="center">
|
||||
<button id="btnPurge" class="btnMain btn btn-danger" onclick="$('#processing').show(); purgeBtn(); this.blur; doPurge(false);">Purge<br>Database</button>
|
||||
<button class="btnMain btn btn-warning" onclick="this.blur; doPurge(true);">Simulate Purge and<br>Display Query</button>
|
||||
<button class="btnMain btn btn-success" onclick="this.blur; window.location.href='/'">Cancel<br>Purge</button>
|
||||
</div><br>
|
||||
<div align="center" id="processing" style="display: none;">
|
||||
<img src="static/loading.gif" style="height: 20px;" alt="loading">
|
||||
<br>Processing...<br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'footer-links.html' %}
|
||||
</div> <!-- end main -->
|
||||
|
||||
<div class="side">
|
||||
|
||||
</div>
|
||||
</div> <!-- end row -->
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<!-- js moved to op25.js -->
|
||||
|
||||
<script>
|
||||
|
||||
function purgeBtn() {
|
||||
// full page modal while loading, or the little Processing icon below the buttons??
|
||||
// $('#loading').show();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
{% block extra_javascripts %}
|
||||
|
||||
<script>
|
||||
x = $('#recCount').html();
|
||||
$('#recCount').html(comma(x));
|
||||
</script>
|
||||
{% endblock %}
|