Compare commits

..

3 Commits
master ... max

215 changed files with 3580 additions and 96262 deletions

3
.gitignore vendored
View File

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

View File

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

23
README
View File

@ -1,23 +0,0 @@
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.

View File

@ -1,10 +0,0 @@
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.

View File

@ -1,32 +0,0 @@
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/'.

4
README.max-branch Normal file
View File

@ -0,0 +1,4 @@
Notice: The max branch of the git repo is deprecated, and will eventually be deleted.
Look in branch 'master' for the latest commits.

View File

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

View File

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

View File

@ -0,0 +1,46 @@
# Copyright 2011 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# GNU Radio is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Radio; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
if(DEFINED __INCLUDED_GR_PLATFORM_CMAKE)
return()
endif()
set(__INCLUDED_GR_PLATFORM_CMAKE TRUE)
########################################################################
# Setup additional defines for OS types
########################################################################
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LINUX TRUE)
endif()
if(LINUX AND EXISTS "/etc/debian_version")
set(DEBIAN TRUE)
endif()
if(LINUX AND EXISTS "/etc/redhat-release")
set(REDHAT TRUE)
endif()
########################################################################
# when the library suffix should be 64 (applies to redhat linux family)
########################################################################
if(NOT DEFINED LIB_SUFFIX AND REDHAT AND CMAKE_SYSTEM_PROCESSOR MATCHES "64$")
set(LIB_SUFFIX 64)
endif()
set(LIB_SUFFIX ${LIB_SUFFIX} CACHE STRING "lib directory suffix")

View File

@ -0,0 +1,227 @@
# 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)
")

229
cmake/Modules/GrSwig.cmake Normal file
View File

@ -0,0 +1,229 @@
# 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)))
")

133
cmake/Modules/GrTest.cmake Normal file
View File

@ -0,0 +1,133 @@
# 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)

View File

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

View File

@ -1,72 +0,0 @@
#! /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 ======

View File

@ -10,12 +10,9 @@ 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 python3-numpy python3-waitress python3-requests
sudo apt-get install liborc-dev
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
mkdir build
cd build
@ -36,10 +33,3 @@ echo ====== you must install gnuplot, e.g., manually as follows:
echo ======
echo ====== sudo apt-get install gnuplot-x11
echo ======
echo ======
echo ====== Separately, we suggest you set device and driver permissions:
echo ====== \$ cd scripts
echo ====== \$ ./udev_rules.sh
echo ====== It is only necessary to do this once. Currently this script
echo ====== handles the rtl-sdr and airspy only.
echo ======

View File

@ -63,32 +63,6 @@ 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
########################################################################
@ -106,6 +80,28 @@ 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
########################################################################

View File

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

View File

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

View File

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

View File

@ -63,7 +63,7 @@ list(APPEND op25_sources
)
add_library(gnuradio-op25 SHARED ${op25_sources})
target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} gnuradio::gnuradio-runtime itpp pcap)
target_link_libraries(gnuradio-op25 ${Boost_LIBRARIES} ${GNURADIO_RUNTIME_LIBRARIES} itpp pcap)
set_target_properties(gnuradio-op25 PROPERTIES DEFINE_SYMBOL "gnuradio_op25_EXPORTS")
########################################################################

View File

@ -12,7 +12,7 @@ class CryptoState
{
public:
CryptoState() :
mi(MESSAGE_INDICATOR_LENGTH), kid(0), algid(0)
kid(0), algid(0), mi(MESSAGE_INDICATOR_LENGTH)
{ }
public:
std::vector<uint8_t> mi;

View File

@ -261,13 +261,13 @@ namespace gr {
}
void
decoder_bf_impl::set_key(const crypto_algorithm::key_type& key)
decoder_bf_impl::set_key(const key_type& key)
{
d_crypto_module->set_key(key);
}
void
decoder_bf_impl::set_key_map(const crypto_algorithm::key_map_type& keys)
decoder_bf_impl::set_key_map(const key_map_type& keys)
{
d_crypto_module->set_key_map(keys);
}

View File

@ -160,9 +160,9 @@ namespace gr {
void set_logging(bool verbose = true);
void set_key(const crypto_algorithm::key_type& key);
void set_key(const key_type& key);
void set_key_map(const crypto_algorithm::key_map_type& keys);
void set_key_map(const key_map_type& keys);
};
} // namespace op25
} // namespace gr

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
/*
* Copyright 2008 Steve Glass
* Copyright 2022 Matt Ames
* Copyright 2018 Matt Ames
*
* This file is part of OP25.
*
@ -72,7 +72,7 @@ const value_string ALGIDS[] = {
{ 0x9F, "Motorola DES-XL 56-bit key" },
{ 0xA0, "Motorola DVI-XL" },
{ 0xA1, "Motorola DVP-XL" },
{ 0xA2, "Motorola DVI-XL-SPFL"},
{ 0xA2, "Motorola DVI-SPFL"},
{ 0xA3, "Motorola HAYSTACK" },
{ 0xA4, "Motorola Assigned - Unknown" },
{ 0xA5, "Motorola Assigned - Unknown" },
@ -82,12 +82,11 @@ const value_string ALGIDS[] = {
{ 0xA9, "Motorola Assigned - Unknown" },
{ 0xAA, "Motorola ADP (40 bit RC4)" },
{ 0xAB, "Motorola CFX-256" },
{ 0xAC, "Motorola GOST 28147-89 (RFC 5830)" },
{ 0xAD, "Motorola Assigned - LOCALIZED" },
{ 0xAC, "Motorola Assigned - Unknown" },
{ 0xAD, "Motorola Assigned - Unknown" },
{ 0xAE, "Motorola Assigned - Unknown" },
{ 0xAF, "Motorola AES+" },
{ 0xAF, "Motorola AES-256-GCM (possibly)" },
{ 0xB0, "Motorola DVP"},
{ 0xD0, "Motorola LOCAL_BR"}
};
const size_t ALGIDS_SZ = sizeof(ALGIDS) / sizeof(ALGIDS[0]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,31 +63,6 @@ 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
########################################################################
@ -105,6 +80,27 @@ 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
########################################################################

View File

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

View File

@ -37,7 +37,6 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python
# Copyright 2017, 2018 Graham Norbury
#
@ -29,8 +29,7 @@ from optparse import OptionParser
from sockaudio import socket_audio
def signal_handler(signal, frame):
sys.stderr.write("audio.py shutting down\n")
audio_handler.stop()
audiothread.stop()
sys.exit(0)
parser = OptionParser()
@ -39,17 +38,16 @@ 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)
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)
audiothread = socket_audio(options.host_ip, options.wireshark_port, options.audio_output, options.two_channel, options.audio_gain)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
audio_handler.run()
while True:
time.sleep(1)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,57 +0,0 @@
#!/usr/bin/env python
#
# (c) Copyright 2020, OP25
#
# This file is part of OP25 and part of GNU Radio
#
# OP25 is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# OP25 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
""" generate named image file consisting of multi-line text """
from PIL import Image, ImageDraw, ImageFont
import os
_TTF_FILE = '/usr/share/fonts/truetype/freefont/FreeSerif.ttf'
def create_image(textlist=["Blank"], imgfile="test.png", bgcolor='red', fgcolor='black', windowsize=(400,300)):
global _TTF_FILE
width=windowsize[0]
height=windowsize[1]
margin = 4
if not os.access(_TTF_FILE, os.R_OK):
font = ImageFont.load_default()
else:
font = ImageFont.truetype(_TTF_FILE, 16)
img = Image.new('RGB', (width, height), bgcolor)
draw = ImageDraw.Draw(img)
cursor = 0
for line in textlist:
w,h = draw.textsize(line, font)
# TODO: overwidth check needed?
if cursor+h >= height:
break
draw.text((margin, cursor), line,'black',font)
cursor += h + margin // 2
img.save(imgfile)
if __name__ == '__main__':
s = []
s.append('Starting...')
create_image(textlist=s, bgcolor='#c0c0c0')

View File

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

View File

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

View File

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

View File

@ -1,42 +0,0 @@
#! /bin/sh
# Copyright (c) 2020 OP25
#
# This file is part of OP25 and part of GNU Radio
#
# OP25 is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# OP25 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
#
# this script should not be run directly - run ffmpeg.liq instead
#
# requires ffmpeg configured with --enable-libx264
#
ffmpeg \
-ar 8000 \
-ac 1 \
-acodec pcm_s16le \
-f s16le \
-i pipe:0 \
-f image2 \
-loop 1 \
-i ../www/images/status.png \
-vcodec libx264 \
-pix_fmt yuv420p \
-f flv \
-acodec aac \
-b:a 48k \
rtmp://localhost/live/stream

View File

@ -23,20 +23,16 @@ import sys
import os
import time
import subprocess
import json
import threading
import glob
from gnuradio import gr, eng_notation
from gnuradio import gr, gru, eng_notation
from gnuradio import blocks, audio
from gnuradio.eng_option import eng_option
import numpy as np
from gnuradio import gr
from math import pi, sin, cos
from math import pi
_def_debug = 0
_def_sps = 10
_def_cpm_mode = 'cpm'
GNUPLOT = '/usr/bin/gnuplot'
@ -58,19 +54,8 @@ 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, title="", color_cfg='plot-colors.json'):
global PSEQ
def __init__(self, sps=_def_sps, logfile=None):
self.sps = sps
self.center_freq = 0.0
self.relative_freq = 0.0
@ -80,7 +65,7 @@ class wrap_gp(object):
self.freqs = ()
self.avg_pwr = np.zeros(FFT_BINS)
self.avg_sum_pwr = 0.0
self.buf = np.array([])
self.buf = []
self.plot_count = 0
self.last_plot = 0
self.plot_interval = None
@ -88,25 +73,6 @@ 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()
@ -140,141 +106,81 @@ 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 = np.concatenate((self.buf, buf[:int(consumed)]))
self.buf.extend(buf[:consumed])
if len(self.buf) < BUFSZ:
return consumed
self.plot_count += 1
if mode == 'eye' and self.plot_count % 20 != 0:
self.buf = np.array([])
self.buf = []
return consumed
plots = []
s = ''
plot_size = (320,240)
if mode == 'eye':
nplots = len(self.buf) // self.sps - 2
for i in range(nplots):
s += '\n'.join(['%f' % self.buf[i*self.sps+j] for j in range(self.sps+1)])
s += '\ne\n'
plots.append('"-" with lines')
elif mode == 'cpm':
nplots = len(self.buf)
ab = np.abs(self.buf)
for i in range(len(ab)):
s += '%f\n' % ab[i]
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'cpmd':
if time.time() < self.next_cpmd:
self.buf = np.array([])
return 0
self.next_cpmd = time.time() + 0.5
ab = np.abs(self.buf)
thresh = np.max(ab) / 2.0
mask1 = np.array(ab > thresh, dtype=np.int)
maskdf = mask1[1:] - mask1[:-1]
nz = np.array(np.nonzero(maskdf), dtype=np.int)[0]
nzd = nz[1:]-nz[:-1]
nzdn = nzd // self.sps
sel = (nzdn > 165) & (nzdn < 185)
valid = np.array(np.nonzero(sel), dtype=np.int)[0]
if len(valid) < 1:
self.buf = np.array([])
return 0
v0 = valid[0]
i0 = nz[v0]
samples = self.buf[i0 : i0+170*self.sps+2]
fmd = np.angle(samples[1:] * np.conj(samples[:-1]))
fmd = fmd[12*self.sps:]
for n in range(0,len(fmd),self.sps):
sl = fmd[n:n+self.sps+1]
if len(sl) != self.sps+1:
while(len(self.buf)):
if mode == 'eye':
if len(self.buf) < self.sps:
break
s += '\n'.join(['%f' % (x*self.sps) for x in sl])
s += '\ne\n'
plots.append('"-" with lines')
elif mode == 'constellation':
plot_size = (240,240)
self.buf = self.buf[:100]
for b in self.buf:
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
s += 'e\n'
plots.append('"-" with points')
for b in self.buf:
#s += '%f\t%f\n' % (b.real, b.imag)
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'symbol':
for b in self.buf:
s += '%f\n' % (b)
s += 'e\n'
plots.append('"-" with points')
elif mode == 'fftf':
self.ffts = np.fft.rfft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ)
#self.ffts = np.fft.fftshift(self.ffts)
self.ffts = np.abs(self.ffts) ** 2.0
self.ffts /= np.max(self.ffts)
for i in range(len(self.ffts)):
s += '%f\n' % (self.ffts[i])
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'fft' or mode == 'mixer':
sum_pwr = 0.0
self.ffts = np.fft.fft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ)
self.ffts = np.fft.fftshift(self.ffts)
self.freqs = np.fft.fftfreq(len(self.ffts))
self.freqs = np.fft.fftshift(self.freqs)
tune_freq = (self.center_freq - self.relative_freq) / 1e6
if self.center_freq and self.width:
self.freqs = ((self.freqs * self.width) + self.center_freq + self.offset_freq) / 1e6
for i in range(len(self.ffts)):
if mode == 'fft':
self.avg_pwr[i] = ((1.0 - FFT_AVG) * self.avg_pwr[i]) + (FFT_AVG * np.abs(self.ffts[i]))
else:
self.avg_pwr[i] = ((1.0 - MIX_AVG) * self.avg_pwr[i]) + (MIX_AVG * np.abs(self.ffts[i]))
s += '%f\t%f\n' % (self.freqs[i], 20 * np.log10(self.avg_pwr[i]))
if (mode == 'mixer') and (self.avg_pwr[i] > 1e-5):
if (self.freqs[i] - self.center_freq) < 0:
sum_pwr -= self.avg_pwr[i]
elif (self.freqs[i] - self.center_freq) > 0:
sum_pwr += self.avg_pwr[i]
self.avg_sum_pwr = ((1.0 - BAL_AVG) * self.avg_sum_pwr) + (BAL_AVG * sum_pwr)
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'float' or mode == 'correlation':
for b in self.buf:
s += '%f\n' % (b)
s += 'e\n'
plots.append('"-" with lines')
elif mode == 'sync':
s_abs = np.abs(self.buf)
sums = np.zeros(self.sps)
for i in range(self.sps):
sums[i] = np.sum(s_abs[range(i, len(self.buf), self.sps)])
am = np.argmax(sums)
samples = self.buf[am:]
a1 = -np.angle(samples[0])
rz = cos(a1) + 1j * sin(a1)
while len(samples) >= self.sps+1:
for i in range(self.sps+1):
z = samples[i] * rz
s += '%f\t%f\n' % (z.real, z.imag)
for i in range(self.sps):
s += '%f\n' % self.buf[i]
s += 'e\n'
plots.append('"-" with linespoints')
samples = samples[self.sps:]
self.buf = np.array([])
self.buf=self.buf[self.sps:]
plots.append('"-" with lines')
elif mode == 'constellation':
plot_size = (240,240)
self.buf = self.buf[:100]
for b in self.buf:
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
s += 'e\n'
plots.append('"-" with points')
for b in self.buf:
#s += '%f\t%f\n' % (b.real, b.imag)
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
s += 'e\n'
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 = []
# FFT processing needs to be completed to maintain the weighted average buckets
# regardless of whether we actually produce a new plot or not.
@ -285,50 +191,19 @@ class wrap_gp(object):
filename = None
if self.output_dir:
if self.sequence >= 2:
delete_pathname = '%s/plot-%s%d-%d.png' % (self.output_dir, mode, self.sequence_id, self.sequence-2)
delete_pathname = '%s/plot-%s-%d.png' % (self.output_dir, mode, self.sequence-2)
if os.access(delete_pathname, os.W_OK):
os.remove(delete_pathname)
h0= 'set terminal png size %d, %d\n' % (plot_size)
filename = 'plot-%s%d-%d.png' % (mode, self.sequence_id, self.sequence)
filename = 'plot-%s-%d.png' % (mode, self.sequence)
h0 += 'set output "%s/%s"\n' % (self.output_dir, filename)
self.sequence += 1
else:
pos = ''
if self.position is not None:
x = self.position[0] * plot_size[0]
y = self.position[1] * plot_size[1]
x += 50
y += 75
pos = ' position %d, %d' % (x, y)
h0= 'set terminal x11 noraise size %d, %d%s title "%s"\n' % (plot_size[0], plot_size[1], pos, self.title)
h0= 'set terminal x11 noraise\n'
background = ''
label_color = ''
tic_color = ''
border_color = ''
plot_color = ''
background_color = ''
if self.colors['label_color']:
label_color = 'textcolor rgb"%s"' % self.colors['label_color']
if self.colors['tic_color']:
tic_color = 'textcolor rgb"%s"' % self.colors['tic_color']
if self.colors['border_color']:
border_color = 'linecolor rgb"%s"' % self.colors['border_color']
if self.colors['plot_color']:
plot_color = 'linecolor rgb"%s"' % self.colors['plot_color']
if self.colors['background_color']:
background_color = 'fillcolor rgb"%s"' % self.colors['background_color']
background += 'set object 1 rectangle from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
background += 'set xtics %s\n' % (tic_color)
background += 'set ytics %s\n' % (tic_color)
background += 'set border %s\n' % (border_color)
h = 'set key off\n'
if mode == 'constellation':
#h+= background
plot_color = ''
h+= background
h+= 'set size square\n'
h+= 'set xrange [-1:1]\n'
h+= 'set yrange [-1:1]\n'
@ -336,9 +211,7 @@ class wrap_gp(object):
h += 'set polar\n'
h += 'set angles degrees\n'
h += 'unset raxis\n'
h += 'set object 1 rectangle from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
h += 'set object 2 circle at 0,0 size 1 fillcolor rgb 0x0f01 fillstyle solid behind\n'
h += 'set object 3 circle at 0,0 size 1 %s\n' % 'linecolor rgb"#0000f0"'
h += 'set object circle at 0,0 size 1 fillcolor rgb 0x0f01 fillstyle solid behind\n'
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'
@ -350,78 +223,41 @@ class wrap_gp(object):
h += 'set format ""\n'
h += 'set style line 11 lt 1 lw 2 pt 2 ps 2\n'
h+= 'set title "Constellation %s" %s\n' % (self.title, label_color)
h+= 'set title "Constellation"\n'
elif mode == 'eye':
h+= background
h+= 'set yrange [-4:4]\n'
h+= 'set title "Datascope %s" %s\n' % (self.title, label_color)
plot_color = ''
elif mode == 'cpm':
h+= background
#h+= 'set yrange [-4:4]\n'
h+= 'set title "CPM RSSI %s" %s\n' % (self.title, label_color)
#plot_color = ''
elif mode == 'cpmd':
h+= background
h+= 'set yrange [-4:4]\n'
h+= 'set title "CPM Datascope %s" %s\n' % (self.title, label_color)
plot_color = ''
elif mode == 'sync':
h += 'set object 1 rect from screen 0,0 to screen 1,1 %s behind\n' % (background_color)
h += 'set size square\n'
h += 'set xtics %s\n' % (tic_color)
h += 'set ytics %s\n' % (tic_color)
h += 'set border %s\n' % (border_color)
h+= 'set title "Datascope"\n'
elif mode == 'symbol':
h+= background
h+= 'set yrange [-4:4]\n'
h+= 'set title "Symbol %s" %s\n' % (self.title, label_color)
h+= 'set title "Symbol"\n'
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 %s: balance %3.0f (smaller is better)" %s\n' % (self.title, np.abs(self.avg_sum_pwr * 1000), label_color)
h+= 'set title "Mixer: balance %3.0f (smaller is better)"\n' % (np.abs(self.avg_sum_pwr * 1000))
else: # fft
h+= 'set title "Spectrum %s" %s\n' % (self.title, label_color)
h+= 'set title "Spectrum"\n'
if self.center_freq:
arrow_pos = (self.center_freq - self.relative_freq) / 1e6
h+= 'set arrow from %f, graph 0 to %f, graph 1 nohead\n' % (arrow_pos, arrow_pos)
h+= 'set title "Spectrum: tuned to %f Mhz" %s\n' % (arrow_pos, label_color)
elif mode == 'fftf':
h+= 'set yrange [-1:1.2]\n'
h+= 'set title "fftf"\n'
h+= 'set title "Spectrum: tuned to %f Mhz"\n' % arrow_pos
elif mode == 'float':
h+= background
h+= 'set yrange [-2:2]\n'
h+= 'set title "Oscilloscope %s" %s\n' % (self.title, label_color)
elif mode == 'correlation':
h+= background
title = 'Correlation'
if self.title:
title = self.title
h+= 'set yrange [-1.1:1.1]\n'
h+= 'set title "%s" %s\n' % (title, label_color)
if self.output_dir:
s += 'set output\n' ## flush output png
dat = '%s%splot %s %s\n%s' % (h0, h, ','.join(plots), plot_color, s)
if self.logfile is not None:
with open(self.logfile, 'a') as fd:
fd.write(dat)
if sys.version[0] != '2':
dat = bytes(dat, 'utf8')
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)
self.gp.poll()
if self.gp.returncode is None: # make sure gnuplot is still running
try:
rc = self.gp.stdin.write(dat)
except (IOError, ValueError):
pass
try:
self.gp.stdin.flush()
self.gp.stdin.write(dat)
except (IOError, ValueError):
pass
if filename:
@ -443,9 +279,6 @@ 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):
"""
"""
@ -460,37 +293,9 @@ 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()
@ -507,33 +312,7 @@ 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')
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')
self.gnuplot.plot(in0, 1000, mode='constellation')
return len(input_items[0])
def kill(self):
@ -556,18 +335,15 @@ 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)
@ -598,92 +374,6 @@ 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()
@ -700,12 +390,9 @@ 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()
@ -725,82 +412,5 @@ 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

View File

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

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
#! /usr/bin/python
# Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
# Copyright 2017, 2018 Max H. Parke KA1RBI
#
# This file is part of OP25
#
@ -30,7 +30,6 @@ import threading
import glob
import subprocess
import zmq
import op25
from gnuradio import gr
from waitress.server import create_server
@ -38,9 +37,6 @@ 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
@ -53,67 +49,26 @@ TSV_DIR = './'
fake http and ajax server module
TODO: make less fake
"""
def ensure_str(s): # for python 2/3
if isinstance(s[0], str):
return s
ns = ''
for i in range(len(s)):
ns += chr(s[i])
return ns
class event_iterator:
def __iter__(self):
return self
def __next__(self):
_jslog_file = None # set to str(filename) to enable json log
msgs = []
while True:
msg = my_input_q.delete_head()
assert msg.type() == -4
d = json.loads(msg.to_string())
msgs.append(d)
if my_input_q.empty_p():
break
js = json.dumps(msgs)
# TODO: json.loads followed by dumps is redundant -
# can this be optimized?
s = 'data:%s\r\n\r\n' % (js)
if _jslog_file:
t = json.dumps(msgs, indent=4, separators=[',',':'], sort_keys=True)
with open(_jslog_file, 'a') as logfd:
logfd.write('%s\n' % t)
if sys.version[0] != '2':
if isinstance(s, str):
s = s.encode()
return s
next = __next__ # for python2
def static_file(environ, start_response):
content_types = {'tsv': 'text/tab-separated-values', 'json': 'application/json', 'png': 'image/png', 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'gif': 'image/gif', 'css': 'text/css', 'js': 'application/javascript', 'html': 'text/html', 'ico': 'image/vnd.microsoft.icon'}
img_types = 'png jpg jpeg gif ico'.split()
data_types = 'tsv txt json db'.split()
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()
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 - PATHNAME: %s FILENAME: %s CWD: %s' % (pathname, filename, os. getcwd())
status = '404 NOT FOUND'
content_type = 'text/plain'
output = status
else:
output = open(pathname, 'rb').read()
output = open(pathname).read()
content_type = content_types[suf]
status = '200 OK'
return status, content_type, output
@ -190,37 +145,10 @@ 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:
@ -228,12 +156,6 @@ 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:
@ -249,20 +171,17 @@ 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' 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':
if environ['REQUEST_METHOD'] == 'GET':
status, content_type, output = static_file(environ, start_response)
elif environ['REQUEST_METHOD'] == 'POST':
postdata = environ['wsgi.input'].read()
@ -274,13 +193,9 @@ 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):
@ -290,16 +205,7 @@ def application(environ, start_response):
except:
failed = True
sys.stderr.write('application: request failed:\n%s\n' % traceback.format_exc())
if failed:
status = '500 Internal Server Error'
response_headers = [ ('Access-Control-Allow-Origin', '*') ]
start_response(status, response_headers)
output = status
if sys.version[0] != '2':
if isinstance(output, str):
output = output.encode()
return [output]
sys.exit(1)
return result
def process_qmsg(msg):
@ -320,10 +226,9 @@ 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)
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)
self.server = create_server(application, host=host, port=my_port)
def run(self):
self.server.run()
@ -357,7 +262,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_string(zmq.SUBSCRIBE, '')
self.zmq_sub.setsockopt(zmq.SUBSCRIBE, '')
self.zmq_pub = self.zmq_context.socket(zmq.PUB)
self.zmq_pub.sndhwm = 5
@ -378,8 +283,7 @@ class Backend(threading.Thread):
t = msg.type()
s = msg.to_string()
a = msg.arg1()
s = ensure_str(s)
self.zmq_pub.send_string(json.dumps({'command': s, 'data': a, 'msgtype': t}))
self.zmq_pub.send(json.dumps({'command': s, 'data': a, 'msgtype': t}))
def check_subproc(self): # return True if subprocess is active
if not self.subproc:
@ -394,9 +298,6 @@ 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):
@ -404,57 +305,15 @@ class Backend(threading.Thread):
return None
if not trunked_ct:
self.backend = '%s/%s' % (os.getcwd(), 'multi_rx.py')
opts = [py_exe, self.backend]
opts = [self.backend]
filename = '%s%s.json' % (CFG_DIR, config_file)
opts.append('--config-file')
opts.append(filename)
return opts
# TODO: this probably should be external and/or configurable
# these options must match up one for one with the rx.py cli opts
types = {'costas-alpha': 'float',
'trunk-conf-file': 'str',
'demod-type': 'str',
'logfile-workers': 'int',
'decim-amt': 'int',
'wireshark-host': 'str',
'gain-mu': 'float',
'phase2-tdma': 'bool',
'seek': 'int',
'ifile': 'str',
'pause': 'bool',
'antenna': 'str',
'calibration': 'float',
'fine-tune': 'float',
'raw-symbols': 'str',
'audio-output': 'str',
'vocoder': 'bool',
'input': 'str',
'wireshark': 'bool',
'gains': 'str',
'args': 'str',
'sample-rate': 'int',
'terminal-type': 'str',
'gain': 'float',
'excess-bw': 'float',
'offset': 'float',
'audio-input': 'str',
'audio': 'bool',
'plot-mode': 'str',
'audio-if': 'bool',
'tone-detect': 'bool',
'frequency': 'int',
'freq-corr': 'float',
'hamlib-model': 'int',
'udp-player': 'bool',
'verbosity': 'int',
'audio-gain': 'float',
'freq-error-tracking': 'bool',
'nocrypt': 'bool',
'wireshark-port': 'int'
}
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'}
self.backend = '%s/%s' % (os.getcwd(), 'rx.py')
opts = [py_exe, self.backend]
opts = [self.backend]
for k in [ x for x in dir(options) if not x.startswith('_') ]:
kw = k.replace('_', '-')
val = getattr(options, k)
@ -497,7 +356,6 @@ 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':
@ -523,7 +381,6 @@ 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)
@ -539,10 +396,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():
@ -570,7 +427,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)

View File

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

View File

@ -1,42 +0,0 @@
#!/usr/bin/env python
# Copyright 2020 Graham Norbury
#
# This file is part of OP25
#
# OP25 is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# OP25 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
# Modify TS_FORMAT to control logger timestamp format
# 0 = legacy epoch seconds
# 1 = formatted mm/dd/yy hh:mm:ss.usec
TS_FORMAT = 1
import time
class log_ts(object):
@staticmethod
def get(supplied_ts=None):
if supplied_ts is None:
ts = time.time()
else:
ts = supplied_ts
if TS_FORMAT == 0:
formatted_ts = "{:.6f}".format(ts)
else:
formatted_ts = "{:s}{:s}".format(time.strftime("%m/%d/%y %H:%M:%S",time.localtime(ts)),"{:.6f}".format(ts - int(ts)).lstrip("0"))
return formatted_ts

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
#!/usr/bin/env python
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI
#
# This file is part of OP25
#
@ -24,55 +24,33 @@ import sys
import threading
import time
import json
import select
import traceback
import osmosdr
from gnuradio import audio, eng_notation, gr, filter, blocks, fft, analog, digital
from gnuradio import audio, eng_notation, gr, gru, 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()}
@ -88,18 +66,12 @@ 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:'):
self.init_audio_if(config)
elif config['args'].startswith('audio:'):
if 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)
@ -112,50 +84,15 @@ 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:', '')
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)
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]
@ -180,53 +117,19 @@ class device(object):
self.offset = config['offset']
def set_frequency(self, frequency):
if frequency == self.frequency:
return
if not self.tunable:
return
self.frequency = frequency
self.src.set_center_freq(frequency)
class channel(object):
def __init__(self, config, dev, verbosity, msgq = None, process_msg=None, msgq_id=-1, role=''):
def __init__(self, config, dev, verbosity):
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(
@ -237,504 +140,58 @@ class channel(object):
relative_freq = dev.frequency + dev.offset - config['frequency'],
offset = dev.offset,
if_rate = config['if_rate'],
symbol_rate = self.symbol_rate,
use_old_decim = True if self.device.args.startswith('audio-if') else False)
if msgq is not None:
q = msgq
else:
q = gr.msg_queue(20)
if 'decode' in config.keys() and config['decode'].startswith('p25_decoder'):
num_ambe = 1
(proto, wireshark_host, udp_port) = config['destination'].split(':')
assert proto == 'udp'
wireshark_host = wireshark_host.replace('/', '')
udp_port = int(udp_port)
if role == 'vc':
self.audio_port = udp_port
if 'audio_output' in config.keys():
self.audio_output = config['audio_output']
self.decoder = p25_decoder.p25_decoder_sink_b(dest='audio', do_imbe=True, num_ambe=num_ambe, wireshark_host=wireshark_host, udp_port=udp_port, do_msgq = True, msgq=q, audio_output=self.audio_output, debug=verbosity, msgq_id=self.msgq_id)
else:
self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q, self.msgq_id)
if self.symbol_rate == 6000 and role == 'cc':
sps = config['if_rate'] // self.symbol_rate
self.demod.set_symbol_rate(self.symbol_rate) # this and the foll. call should be merged?
self.demod.clock.set_omega(float(sps))
self.demod.clock.set_tdma(True)
sys.stderr.write('initializing TDMA control channel %s channel ID %d\n' % (self.name, self.msgq_id))
if self.process_msg is not None and msgq is None:
self.q_watcher = du_queue_watcher(q, lambda msg: self.process_msg(msg, sender=self))
symbol_rate = self.symbol_rate)
q = gr.msg_queue(1)
self.decoder = op25_repeater.frame_assembler(config['destination'], verbosity, q)
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.set_title(self.name)
self.sinks.append(sink)
sink = eye_sink_f(sps=config['if_rate'] / self.symbol_rate)
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)
sink = fft_sink_c()
sink.set_title(self.name)
self.sinks.append(sink)
self.sinks.append(fft_sink_c())
self.demod.connect_complex('src', self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'mixer':
if config['demod_type'] == 'cqpsk':
blk = 'mixer'
else:
blk = 'cutoff'
assert config['demod_type'] == 'cqpsk' ## mixer plot requires cqpsk demod type
i = len(self.sinks)
sink = mixer_sink_c()
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_complex(blk, self.sinks[i])
self.sinks.append(mixer_sink_c())
self.demod.connect_complex('mixer', self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'constellation':
i = len(self.sinks)
assert config['demod_type'] == 'cqpsk' ## constellation plot requires cqpsk demod type
sink = constellation_sink_c()
sink.set_title(self.name)
self.sinks.append(sink)
self.sinks.append(constellation_sink_c())
self.demod.connect_complex('diffdec', self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'correlation':
assert config['demod_type'] == 'fsk4' ## correlation plot requires fsk4 demod type
assert config['symbol_rate'] == 4800 ## 4800 required for correlation plot
sps=config['if_rate'] // self.symbol_rate
sinks = setup_correlation(sps, self.name, self.demod.connect_bb)
self.kill_sink += sinks
self.sinks += sinks
elif plot == 'sync':
assert config['demod_type'] == 'cqpsk' ## sync plot requires cqpsk demod type
i = len(self.sinks)
sink = sync_plot(block = self.demod.clock)
sink.set_title(self.name)
self.sinks.append(sink)
self.kill_sink.append(self.sinks[i])
# does not issue self.connect()
else:
sys.stderr.write('unrecognized plot type %s\n' % plot)
return
def set_frequency(self, frequency):
assert frequency
if self.device.tunable:
self.device.set_frequency(frequency)
f = self.frequency if self.device.args.startswith('audio-if') else frequency
relative_freq = self.device.frequency + self.device.offset + self.tuning_error - f
if (not self.device.tunable) and (not self.device.args.startswith('audio-if')) and abs(relative_freq) > ((self.demod.input_rate / 2) - (self.demod.if1 / 2)):
if frequency not in self.warned_frequencies:
sys.stderr.write('warning: set frequency %f to non-tunable device %s rejected.\n' % (frequency / 1000000.0, self.device.name))
self.warned_frequencies[frequency] = 0
self.warned_frequencies[frequency] += 1
#print 'set_relative_frequency: error, relative frequency %d exceeds limit %d' % (relative_freq, self.demod.input_rate/2)
return False
self.demod.set_relative_frequency(relative_freq)
self.last_set_freq_at = time.time()
if not self.device.args.startswith('audio-if'):
self.frequency = frequency
def error_tracking(self, last_change_freq):
curr_time = time.time()
if self.config['demod_type'] == 'fsk4':
return None # todo: allow tracking in fsk4 demod
UPDATE_TIME = 3
if self.last_error_update + UPDATE_TIME > curr_time:
return None
self.last_error_update = time.time()
if not self.demod.is_muted():
band = self.demod.get_error_band()
freq_error = self.demod.get_freq_error()
if band and curr_time >= self.next_band_change:
self.next_band_change = curr_time + 20.0
self.error_band += band
sys.stderr.write('channel %d set error band %d\n' % (self.msgq_id, self.error_band))
self.freq_correction += freq_error * 0.15
self.freq_correction = int(self.freq_correction)
if self.freq_correction > 600:
self.freq_correction -= 1200
self.error_band += 1
elif self.freq_correction < -600:
self.freq_correction += 1200
self.error_band -= 1
self.error_band = min(self.error_band, 2)
self.error_band = max(self.error_band, -2)
self.tuning_error = int(self.error_band * 1200 + self.freq_correction)
e = 0
if last_change_freq > 0:
e = (self.tuning_error*1e6) / float(last_change_freq)
else:
e = 0
freq_error = 0
band = 0
### self.set_frequency(self.frequency) # adjust relative frequency with updated tuning_error
if self.verbosity >= 10:
sys.stderr.write('%f\terror_tracking\t%s\t%d\t%d\t%d\t%d\t%d\t%f\n' % (curr_time, self.name, self.msgq_id, freq_error, self.error_band, self.tuning_error, self.freq_correction, e))
d = {'time': time.time(), 'json_type': 'freq_error_tracking', 'name': self.name, 'device': self.device.name, 'freq_error': freq_error, 'band': band, 'error_band': self.error_band, 'tuning_error': self.tuning_error, 'freq_correction': self.freq_correction}
if self.frequency:
self.set_frequency(self.frequency)
return d
def configure_tdma(self, params):
set_tdma = False
if params['tdma'] is not None:
set_tdma = True
self.decoder.set_slotid(params['tdma'])
self.demod.clock.set_tdma(set_tdma)
if set_tdma == self.tdma_state:
return # already in desired state
self.tdma_state = set_tdma
if set_tdma:
hash = '%x%x%x' % (params['nac'], params['sysid'], params['wacn'])
if hash not in self.xor_cache:
self.xor_cache[hash] = lfsr.p25p2_lfsr(params['nac'], params['sysid'], params['wacn']).xor_chars
self.decoder.set_xormask(self.xor_cache[hash], hash)
self.decoder.set_nac(params['nac'])
rate = 6000
else:
rate = 4800
sps = self.config['if_rate'] / rate
self.demod.set_symbol_rate(rate) # this and the foll. call should be merged?
self.demod.clock.set_omega(float(sps))
class du_queue_watcher(threading.Thread):
def __init__(self, msgq, callback, **kwds):
threading.Thread.__init__ (self, **kwds)
self.setDaemon(1)
self.msgq = msgq
self.callback = callback
self.keep_running = True
self.start()
def run(self):
while(self.keep_running):
msg = self.msgq.delete_head()
if not self.keep_running:
break
self.callback(msg)
class rx_block (gr.top_block):
# Initialize the receiver
#
def __init__(self, verbosity, config, trunk_conf_file=None, terminal_type=None, track_errors=False, udp_player=None):
def __init__(self, verbosity, config):
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 = []
@ -742,22 +199,12 @@ class rx_block (gr.top_block):
self.device_id_by_name[cfg['name']] = len(self.devices)
self.devices.append(device(cfg, self))
def find_trunked_device(self, chan, requested_dev):
if len(self.devices) == 1: # single SDR
return self.devices[0]
for dev in self.devices:
if dev.name == requested_dev:
return dev
return None
def find_device(self, chan, requested_dev):
if 'decode' in chan.keys() and chan['decode'].startswith('p25_decoder'):
return self.find_trunked_device(chan, requested_dev)
def find_device(self, chan):
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
@ -765,43 +212,13 @@ class rx_block (gr.top_block):
def configure_channels(self, config):
self.channels = []
for cfg in config:
decode_d = {'role': '', 'dev': ''}
if 'decode' in cfg.keys() and cfg['decode'].startswith('p25_decoder'):
decode_p = cfg['decode'].split(':')[1:]
for p in decode_p: # possible keys: dev, role, nac, sysid; valid roles: cc vc
(k, v) = p.split('=')
if k == 'nac' or k == 'sysid':
v = [int(x, base=0) for x in v.split(',')]
decode_d[k] = v
dev = self.find_device(cfg, decode_d['dev'])
dev = self.find_device(cfg)
if dev is None:
sys.stderr.write('* * * No device found for channel %s- ignoring!\n' % cfg['name'])
sys.stderr.write('* * * Frequency %d not within spectrum band of any device - ignoring!\n' % cfg['frequency'])
continue
msgq_id = len(self.channels) + 1
chan = channel(cfg, dev, self.verbosity, msgq=self.output_q, msgq_id = msgq_id, role=decode_d['role'])
for k in decode_d.keys():
setattr(chan, k, decode_d[k])
chan = channel(cfg, dev, self.verbosity)
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:
@ -816,49 +233,28 @@ 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), 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()
self.tb = rx_block(options.verbosity, config = byteify(config))
def run(self):
self.tb.start()
if self.options.monitor_stdin:
print("Running. press ENTER to quit")
while self.keep_running:
if self.options.monitor_stdin and select.select([sys.stdin,],[],[],0.0)[0]:
c = sys.stdin.read(1)
self.keep_running = False
break
msg = self.tb.output_q.delete_head()
if self.tb.process_msg(msg):
self.keep_running = False
break
print('Quitting - now stopping top block')
self.tb.stop()
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())
if __name__ == "__main__":
rx = rx_main()
try:
rx.run()
except KeyboardInterrupt:
rx.keep_running = False
print('Program ending')
time.sleep(1)
rx.run()

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,461 +0,0 @@
/*
* 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;
}
}

View File

@ -1,453 +0,0 @@
/*
* 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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,568 +0,0 @@
.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 */
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,227 +0,0 @@
// 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);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,144 +0,0 @@
<!--
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">
&nbsp;
</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 &copy; 2020, 2021 Max H. Parke KA1RBI<br>
Copyright &copy; 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">
&nbsp;
</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>&nbsp;\
<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 %}

View File

@ -1,167 +0,0 @@
<!--
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>
&nbsp;&nbsp;&nbsp;&nbsp;
<span>Start <input class="sel-date" type="text" id="startDate"></span>
<span>End <input class="sel-date" type="text" id="endDate">&nbsp;&nbsp;&nbsp;</span>
<button style="width: 75px;" class="btn btn-info btn-sm" id="btnReset" onclick="location.reload();">Refresh</button>&nbsp;&nbsp;
<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>

View File

@ -1,53 +0,0 @@
<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>
&nbsp;&nbsp;&nbsp;&nbsp;
Talkgroups: <b><span id="talkgroups">{{ dbstats[2] }}</span></b>
&nbsp;&nbsp;&nbsp;&nbsp;
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;"> &mdash; </td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
</div>
</td></tr></table>
</div>
</div>
</div>

View File

@ -1,293 +0,0 @@
<!--
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">
&nbsp;
</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>
&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;<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> &nbsp;&nbsp;&nbsp;&nbsp;
New records added:<b> {{ session['imp_results'][1] }} </b> &nbsp;&nbsp;&nbsp;&nbsp;
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">
&nbsp;
</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>&nbsp;\
<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 %}

View File

@ -1,151 +0,0 @@
<!--
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">
&nbsp;
</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">
&nbsp;
</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>&nbsp;\
<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 %}

View File

@ -1,191 +0,0 @@
<!--
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>&nbsp;</p>
</div>
<div id="primary">
&nbsp;
</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">
&nbsp;
</div>
</div>
</div>
<!-- end secondary -->
</div>
<!-- end content -->
</div>
<br>
{% endblock %}
{% block extra_javascripts %}
{% endblock %}

View File

@ -1,17 +0,0 @@
<!-- footer links -->
<div class="card border-primary mb-3">
<div class="card-body">
<div align="center">
<a href="{{ url_for('home') }}">Home</a>&nbsp;&nbsp;&nbsp;
<a href="{{ url_for('editsys') }}">System Tags</a>&nbsp;&nbsp;&nbsp;
<a href="{{ url_for('edit_tags') }}?cmd=tgid">Talkgroup Tags</a>&nbsp;&nbsp;&nbsp;
<a href="{{ url_for('edit_tags') }}?cmd=unit">Unit Tags</a>&nbsp;&nbsp;&nbsp;
<a href="{{ url_for('purge') }}">Purge</a>&nbsp;&nbsp;&nbsp;
<a href="{{ url_for('switch_db') }}">Backup & Switch</a>&nbsp;&nbsp;&nbsp;
<a href="{{ url_for('about') }}">About</a>
<br>
Server time: {{ t_loc() }} <br>
08.29.2021
</div>
</div>
</div>

View File

@ -1,129 +0,0 @@
<!--
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">
&nbsp;
</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>
&nbsp;&nbsp;
<button class="btnMain btn btn-outline-info" onclick="window.location.href='/logs?r=call_detail'">Call<br>Detail</button>
&nbsp;&nbsp;
<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>
&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<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>&nbsp;{{ params['cc_desc'][i] }} </td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% include 'footer-links.html' %}
</div> <!-- end main -->
<div class="side">
&nbsp;
</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 %}

View File

@ -1,18 +0,0 @@
<!-- 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 %}

View File

@ -1,212 +0,0 @@
<!--
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'] }} &nbsp;&nbsp;
{% 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' %}

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