Compare commits

..

No commits in common. "master" and "max-skip-test" have entirely different histories.

328 changed files with 5272 additions and 117214 deletions

4
.gitignore vendored
View File

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

View File

@ -1,27 +1,6 @@
cmake_minimum_required(VERSION 2.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/'.

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

@ -1,45 +0,0 @@
#! /bin/sh
# op25 install script for debian based systems
# including ubuntu 14/16 and raspbian
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
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
mkdir build
cd build
cmake ../
make
sudo make install
sudo ldconfig
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

@ -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,31 @@ 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(GnuradioRuntime)
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")
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
find_package(Gnuradio)
endif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
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

@ -4,17 +4,17 @@
<key>op25_decoder_ff</key>
<category>op25</category>
<import>import op25</import>
<make>op25.decoder_ff()</make>
<make>op25.decoder_ff($)</make>
<!-- Make one 'param' node for every Parameter you want settable from the GUI.
Sub-nodes:
* name
* key (makes the value accessible as $keyname, e.g. in the make node)
* type -->
<!--<param>
<param>
<name>...</name>
<key>...</key>
<type>...</type>
</param>-->
</param>
<!-- Make one 'sink' node per input. Sub-nodes:
* name (an identifier for the GUI)
@ -23,7 +23,7 @@
* optional (set to 1 for optional inputs) -->
<sink>
<name>in</name>
<type>float</type>
<type><!-- e.g. int, float, complex, byte, short, xxx_vector, ...--></type>
</sink>
<!-- Make one 'source' node per output. Sub-nodes:
@ -32,7 +32,7 @@
* vlen
* optional (set to 1 for optional inputs) -->
<source>
<name>audio</name>
<type>float</type>
<name>out</name>
<type><!-- e.g. int, float, complex, byte, short, xxx_vector, ...--></type>
</source>
</block>

View File

@ -4,7 +4,7 @@
<key>op25_fsk4_demod_ff</key>
<category>op25</category>
<import>import op25</import>
<make>op25.fsk4_demod_ff($(id)_msgq_out, $sample_rate, $symbol_rate)</make>
<make>op25.fsk4_demod_ff(self.auto_tune_msgq, $sample_rate, $symbol_rate)</make>
<param>
<name>Sample Rate</name>
@ -20,8 +20,7 @@
<type>real</type>
</param>
<!-- Must connect tune message queue as the block expects a queue as first argument! -->
<!--<param>
<param>
<name>Output Auto Tune</name>
<key>tune_out</key>
<value>True</value>
@ -35,7 +34,7 @@
<name>No</name>
<key>False</key>
</option>
</param>-->
</param>
<sink>
<name>in</name>
@ -46,10 +45,4 @@
<name>dibits</name>
<type>float</type>
</source>
<source>
<name>tune</name>
<type>msg</type>
<!--<optional>1</optional>-->
</source>
</block>

View File

@ -51,7 +51,7 @@ namespace gr {
* class. op25::decoder_bf::make is the public interface for
* creating new instances.
*/
static sptr make(bool idle_silence = true, bool verbose = false);
static sptr make();
/**
* Return a pointer to a string identifying the destination of
@ -78,17 +78,6 @@ namespace gr {
* message queue.
*/
virtual void set_msgq(gr::msg_queue::sptr msgq) = 0;
virtual void set_idle_silence(bool idle_silence = true) = 0;
virtual void set_logging(bool verbose = true) = 0;
typedef std::vector<uint8_t> key_type;
typedef std::map<uint16_t, key_type> key_map_type;
virtual void set_key(const key_type& key) = 0;
virtual void set_key_map(const key_map_type& keys) = 0;
};
} // namespace op25

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

@ -53,17 +53,10 @@ list(APPEND op25_sources
value_string.cc
pickle.cc
pcap_source_b_impl.cc
bch.cc
ldu.cc
crypto.cc
crypto_module_du_handler.cc
deskey.c
desport.c
dessp.c
)
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

@ -55,10 +55,10 @@ abstract_data_unit::correct_errors()
}
void
abstract_data_unit::decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod)
abstract_data_unit::decode_audio(imbe_decoder& imbe)
{
if(is_complete()) {
do_decode_audio(d_frame_body, imbe, crypto_mod);
do_decode_audio(d_frame_body, imbe);
} else {
ostringstream msg;
msg << "cannot decode audio - frame is not complete" << endl;
@ -153,8 +153,7 @@ abstract_data_unit::dump(ostream& os) const
}
abstract_data_unit::abstract_data_unit(const_bit_queue& frame_body) :
d_frame_body(frame_body.size()),
d_logging_enabled(false)
d_frame_body(frame_body.size())
{
copy(frame_body.begin(), frame_body.end(), d_frame_body.begin());
}
@ -165,7 +164,7 @@ abstract_data_unit::do_correct_errors(bit_vector& frame_body)
}
void
abstract_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod)
abstract_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe)
{
}
@ -180,15 +179,3 @@ abstract_data_unit::frame_size() const
{
return d_frame_body.size();
}
void
abstract_data_unit::set_logging(bool on)
{
d_logging_enabled = on;
}
bool
abstract_data_unit::logging_enabled() const
{
return d_logging_enabled;
}

View File

@ -26,7 +26,6 @@
#include "data_unit.h"
#include "op25_yank.h"
#include "crypto.h"
#include <string>
#include <vector>
@ -63,7 +62,7 @@ public:
* \precondition is_complete() == true.
* \param imbe The imbe_decoder to use to generate the audio.
*/
virtual void decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod);
virtual void decode_audio(imbe_decoder& imbe);
/**
* Decode the frame into an octet vector.
@ -118,15 +117,6 @@ public:
*/
virtual std::string snapshot() const;
/**
* Returns a string describing the Data Unit ID (DUID).
*
* \return A string identifying the DUID.
*/
virtual std::string duid_str() const = 0;
virtual void set_logging(bool on);
protected:
/**
@ -150,7 +140,7 @@ protected:
* \param frame_body The const_bit_vector to decode.
* \param imbe The imbe_decoder to use.
*/
virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod);
virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe);
/**
* Decode frame_body and write the decoded frame contents to msg.
@ -162,6 +152,13 @@ protected:
*/
virtual size_t decode_frame(const_bit_vector& frame_body, size_t msg_sz, uint8_t *msg);
/**
* Returns a string describing the Data Unit ID (DUID).
*
* \return A string identifying the DUID.
*/
virtual std::string duid_str() const = 0;
/**
* Return a reference to the frame body.
*/
@ -183,8 +180,6 @@ protected:
*/
virtual uint16_t frame_size() const;
virtual bool logging_enabled() const;
private:
/**
@ -192,7 +187,6 @@ private:
*/
bit_vector d_frame_body;
bool d_logging_enabled;
};
#endif /* INCLUDED_ABSTRACT_DATA_UNIT_H */

View File

@ -1,162 +0,0 @@
#include <stdio.h>
#include <vector>
#include "bch.h"
/*
* Copyright 2010, KA1RBI
*/
static const int bchGFexp[64] = {
1, 2, 4, 8, 16, 32, 3, 6, 12, 24, 48, 35, 5, 10, 20, 40,
19, 38, 15, 30, 60, 59, 53, 41, 17, 34, 7, 14, 28, 56, 51, 37,
9, 18, 36, 11, 22, 44, 27, 54, 47, 29, 58, 55, 45, 25, 50, 39,
13, 26, 52, 43, 21, 42, 23, 46, 31, 62, 63, 61, 57, 49, 33, 0
};
static const int bchGFlog[64] = {
-1, 0, 1, 6, 2, 12, 7, 26, 3, 32, 13, 35, 8, 48, 27, 18,
4, 24, 33, 16, 14, 52, 36, 54, 9, 45, 49, 38, 28, 41, 19, 56,
5, 62, 25, 11, 34, 31, 17, 47, 15, 23, 53, 51, 37, 44, 55, 40,
10, 61, 46, 30, 50, 22, 39, 43, 29, 60, 42, 21, 20, 59, 57, 58
};
static const int bchG[48] = {
1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0,
1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0,
1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1
};
int bchDec(bit_vector& Codeword)
{
int elp[24][ 22], S[23];
int D[23], L[24], uLu[24];
int root[11], locn[11], reg[12];
int i,j,U,q,count;
int SynError, CantDecode;
SynError = 0; CantDecode = 0;
for(i = 1; i <= 22; i++) {
S[i] = 0;
// FOR j = 0 TO 62
for(j = 0; j <= 62; j++) {
if( Codeword[j]) { S[i] = S[i] ^ bchGFexp[(i * j) % 63]; }
}
if( S[i]) { SynError = 1; }
S[i] = bchGFlog[S[i]];
// printf("S[%d] %d\n", i, S[i]);
}
if( SynError) { //if there are errors, try to correct them
L[0] = 0; uLu[0] = -1; D[0] = 0; elp[0][ 0] = 0;
L[1] = 0; uLu[1] = 0; D[1] = S[1]; elp[1][ 0] = 1;
//FOR i = 1 TO 21
for(i = 1; i <= 21; i++) {
elp[0][ i] = -1; elp[1][ i] = 0;
}
U = 0;
do {
U = U + 1;
if( D[U] == -1) {
L[U + 1] = L[U];
// FOR i = 0 TO L[U]
for(i = 0; i <= L[U]; i++) {
elp[U + 1][ i] = elp[U][ i]; elp[U][ i] = bchGFlog[elp[U][ i]];
}
} else {
//search for words with greatest uLu(q) for which d(q)!=0
q = U - 1;
while((D[q] == -1) &&(q > 0)) { q = q - 1; }
//have found first non-zero d(q)
if( q > 0) {
j = q;
do { j = j - 1; if((D[j] != -1) &&(uLu[q] < uLu[j])) { q = j; }
} while( j > 0) ;
}
//store degree of new elp polynomial
if( L[U] > L[q] + U - q) {
L[U + 1] = L[U] ;
} else {
L[U + 1] = L[q] + U - q;
}
///* form new elp(x) */
// FOR i = 0 TO 21
for(i = 0; i <= 21; i++) {
elp[U + 1][ i] = 0;
}
// FOR i = 0 TO L(q)
for(i = 0; i <= L[q]; i++) {
if( elp[q][ i] != -1) {
elp[U + 1][ i + U - q] = bchGFexp[(D[U] + 63 - D[q] + elp[q][ i]) % 63];
}
}
// FOR i = 0 TO L(U)
for(i = 0; i <= L[U]; i++) {
elp[U + 1][ i] = elp[U + 1][ i] ^ elp[U][ i];
elp[U][ i] = bchGFlog[elp[U][ i]];
}
}
uLu[U + 1] = U - L[U + 1];
//form(u+1)th discrepancy
if( U < 22) {
//no discrepancy computed on last iteration
if( S[U + 1] != -1) { D[U + 1] = bchGFexp[S[U + 1]]; } else { D[U + 1] = 0; }
// FOR i = 1 TO L(U + 1)
for(i = 1; i <= L[U + 1]; i++) {
if((S[U + 1 - i] != -1) &&(elp[U + 1][ i] != 0)) {
D[U + 1] = D[U + 1] ^ bchGFexp[(S[U + 1 - i] + bchGFlog[elp[U + 1][ i]]) % 63];
}
}
//put d(u+1) into index form */
D[U + 1] = bchGFlog[D[U + 1]];
}
} while((U < 22) &&(L[U + 1] <= 11));
U = U + 1;
if( L[U] <= 11) { // /* Can correct errors */
//put elp into index form
// FOR i = 0 TO L[U]
for(i = 0; i <= L[U]; i++) {
elp[U][ i] = bchGFlog[elp[U][ i]];
}
//Chien search: find roots of the error location polynomial
// FOR i = 1 TO L(U)
for(i = 1; i <= L[U]; i++) {
reg[i] = elp[U][ i];
}
count = 0;
// FOR i = 1 TO 63
for(i = 1; i <= 63; i++) {
q = 1;
//FOR j = 1 TO L(U)
for(j = 1; j <= L[U]; j++) {
if( reg[j] != -1) {
reg[j] =(reg[j] + j) % 63; q = q ^ bchGFexp[reg[j]];
}
}
if( q == 0) { //store root and error location number indices
root[count] = i; locn[count] = 63 - i; count = count + 1;
}
}
if( count == L[U]) {
//no. roots = degree of elp hence <= t errors
//FOR i = 0 TO L[U] - 1
for(i = 0; i <= L[U]-1; i++) {
Codeword[locn[i]] = Codeword[locn[i]] ^ 1;
}
CantDecode = count;
} else { //elp has degree >t hence cannot solve
CantDecode = -1;
}
} else {
CantDecode = -2;
}
}
return CantDecode;
}

View File

@ -1,4 +0,0 @@
#include <vector>
typedef std::vector<bool> bit_vector;
int bchDec(bit_vector& Codeword);

View File

@ -1,286 +0,0 @@
#include "crypto.h"
#include <sstream>
#include <boost/format.hpp>
#include <iostream>
#include <stdio.h>
#include <memory.h>
extern "C" {
#include "des.h"
}
static unsigned long long swap_bytes(uint64_t l)
{
unsigned long long r;
unsigned char* pL = (unsigned char*)&l;
unsigned char* pR = (unsigned char*)&r;
for (int i = 0; i < sizeof(l); ++i)
pR[i] = pL[(sizeof(l) - 1) - i];
return r;
}
///////////////////////////////////////////////////////////////////////////////
/*
class null_algorithm : public crypto_algorithm // This is an algorithm skeleton (can be used for no encryption as pass-through)
{
private:
size_t m_generated_bits;
public:
null_algorithm()
: m_generated_bits(0)
{
}
const type_id id() const
{
return crypto_algorithm::NONE;
}
bool update(const struct CryptoState& state)
{
fprintf(stderr, "NULL:\t%d bits generated\n", m_generated_bits);
m_generated_bits = 0;
return true;
}
bool set_key(const crypto_algorithm::key_type& key)
{
return true;
}
uint64_t generate(size_t n)
{
m_generated_bits += n;
return 0;
}
};
*/
///////////////////////////////////////////////////////////////////////////////
class des_ofb : public crypto_algorithm
{
public:
unsigned long long m_key_des, m_next_iv, m_ks;
int m_ks_idx;
DES_KS m_ksDES;
int m_iterations;
uint16_t m_current_kid;
key_type m_default_key;
key_map_type m_key_map;
bool m_verbose;
public:
des_ofb()
: m_current_kid(-1)
{
memset(&m_ksDES, 0, sizeof(m_ksDES));
m_key_des = 0;
m_next_iv = 0;
m_ks_idx = 0;
m_ks = 0;
m_iterations = 0;
}
void set_logging(bool on)
{
m_verbose = on;
}
const type_id id() const
{
return crypto_algorithm::DES_OFB;
}
bool update(const struct CryptoState& state)
{
if (m_current_kid != state.kid)
{
if (m_key_map.empty())
{
// Nothing to do
}
else
{
key_map_type::iterator it = m_key_map.find(state.kid);
if (it != m_key_map.end())
{
set_key(it->second);
}
else if (!m_default_key.empty())
{
/*if (m_verbose) */fprintf(stderr, "Key 0x%04x not found in key map - using default key\n", state.kid);
set_key(m_default_key);
}
else
{
/*if (m_verbose) */fprintf(stderr, "Key 0x%04x not found in key map and no default key\n", state.kid);
}
}
m_current_kid = state.kid;
}
uint64_t iv = 0;
size_t n = std::min(sizeof(iv), state.mi.size());
memcpy(&iv, &state.mi[0], n);
set_iv(iv);
return (n == 8);
}
void set_key_map(const key_map_type& key_map)
{
m_key_map = key_map;
m_current_kid = -1; // To refresh on next update if it has changed
}
bool set_key(const crypto_algorithm::key_type& key)
{
const size_t valid_key_length = 8;
if (key.size() != valid_key_length)
{
if (m_verbose) fprintf(stderr, "DES:\tIncorrect key length of %lu (should be %lu)\n", key.size(), valid_key_length);
return false;
}
m_default_key = key;
memcpy(&m_key_des, &key[0], std::min(key.size(), sizeof(m_key_des)));
if (m_verbose)
{
std::stringstream ss;
for (int i = 0; i < valid_key_length; ++i)
ss << boost::format("%02X") % (int)key[i];
std::cerr << "DES:\tKey: " << ss.str() << std::endl;
}
deskey(m_ksDES, (unsigned char*)&m_key_des, 0); // 0: encrypt (for OFB mode)
return true;
}
void set_iv(uint64_t iv)
{
if (m_iterations > 0)
{
if (m_verbose) fprintf(stderr, "DES:\t%i bits used from %i iterations\n", m_ks_idx, m_iterations);
}
m_next_iv = iv;
m_ks_idx = 0;
m_iterations = 0;
m_ks = m_next_iv;
des(m_ksDES, (unsigned char*)&m_ks); // First initialisation
++m_iterations;
des(m_ksDES, (unsigned char*)&m_ks); // Throw out first iteration & prepare for second
++m_iterations;
generate(64); // Reserved 3 + first 5 of LC (3 left)
generate(3 * 8); // Use remaining 3 bytes for LC
}
uint64_t generate(size_t count) // 1..64
{
unsigned long long ullCurrent = swap_bytes(m_ks);
const int max_len = 64;
int pos = m_ks_idx % max_len;
m_ks_idx += count;
if ((pos + count) <= max_len) // Up to 64
{
if ((m_ks_idx % max_len) == 0)
{
des(m_ksDES, (unsigned char*)&m_ks); // Prepare for next iteration
++m_iterations;
}
unsigned long long result = (ullCurrent >> (((max_len - 1) - pos) - (count-1))) & ((count == max_len) ? (unsigned long long)-1 : ((1ULL << count) - 1));
return result;
}
// Over-flow 64-bit boundary (so all of rest of current will be used)
des(m_ksDES, (unsigned char*)&m_ks); // Compute second part
++m_iterations;
unsigned long long first = ullCurrent << pos; // RHS will be zeros
ullCurrent = swap_bytes(m_ks);
int remainder = count - (max_len - pos);
first >>= (((max_len - 1) - remainder) - ((max_len - 1) - pos));
unsigned long long next = (ullCurrent >> (((max_len - 1) - 0) - (remainder-1))) & ((1ULL << remainder) - 1);
return (first | next);
}
};
///////////////////////////////////////////////////////////////////////////////
crypto_module::crypto_module(bool verbose/* = true*/)
: d_verbose(verbose)
{
}
crypto_algorithm::sptr crypto_module::algorithm(crypto_algorithm::type_id algid)
{
if ((!d_current_algorithm && (algid == crypto_algorithm::NONE)) || // This line should be commented out if 'null_algorithm' is to be tested
(d_current_algorithm && (algid == d_current_algorithm->id())))
return d_current_algorithm;
switch (algid)
{
case crypto_algorithm::DES_OFB:
d_current_algorithm = crypto_algorithm::sptr(new des_ofb());
break;
//case crypto_algorithm::NONE:
// d_current_algorithm = crypto_algorithm::sptr(new null_algorithm());
// break;
default:
d_current_algorithm = crypto_algorithm::sptr();
};
if (d_current_algorithm)
{
d_current_algorithm->set_logging(logging_enabled());
if (!d_persistent_key_map.empty())
d_current_algorithm->set_key_map(d_persistent_key_map);
if (!d_persistent_key.empty())
d_current_algorithm->set_key(d_persistent_key);
}
return d_current_algorithm;
}
void crypto_module::set_key(const crypto_algorithm::key_type& key)
{
d_persistent_key = key;
if (d_current_algorithm)
d_current_algorithm->set_key(d_persistent_key);
}
void crypto_module::set_key_map(const crypto_algorithm::key_map_type& keys)
{
d_persistent_key_map = keys;
if (d_current_algorithm)
d_current_algorithm->set_key_map(d_persistent_key_map);
}
void crypto_module::set_logging(bool on/* = true*/)
{
d_verbose = on;
if (d_current_algorithm)
d_current_algorithm->set_logging(on);
}

View File

@ -1,73 +0,0 @@
#ifndef INCLUDED_CRYPTO_H
#define INCLUDED_CRYPTO_H
#include <stdint.h>
#include <vector>
#include <map>
#include <boost/shared_ptr.hpp>
static const int MESSAGE_INDICATOR_LENGTH = 9;
class CryptoState
{
public:
CryptoState() :
mi(MESSAGE_INDICATOR_LENGTH), kid(0), algid(0)
{ }
public:
std::vector<uint8_t> mi;
uint16_t kid;
uint8_t algid;
};
class crypto_state_provider
{
public:
virtual struct CryptoState crypto_state() const=0;
};
class crypto_algorithm
{
public:
typedef boost::shared_ptr<class crypto_algorithm> sptr;
typedef std::vector<uint8_t> key_type;
typedef std::map<uint16_t, key_type > key_map_type;
typedef uint8_t type_id;
enum
{
NONE = 0x80,
DES_OFB = 0x81,
};
public:
virtual const type_id id() const=0;
virtual bool set_key(const key_type& key)=0;
virtual void set_key_map(const key_map_type& key_map)=0;
virtual bool update(const struct CryptoState& state)=0;
virtual uint64_t generate(size_t n_bits)=0; // Can request up to 64 bits of key stream at one time
virtual void set_logging(bool on)=0;
};
class crypto_module
{
public:
typedef boost::shared_ptr<class crypto_module> sptr;
public:
crypto_module(bool verbose = false);
public:
virtual crypto_algorithm::sptr algorithm(crypto_algorithm::type_id algid);
virtual void set_key(const crypto_algorithm::key_type& key);
virtual void set_key_map(const crypto_algorithm::key_map_type& keys);
virtual void set_logging(bool on = true);
protected:
crypto_algorithm::sptr d_current_algorithm;
crypto_algorithm::key_type d_persistent_key;
crypto_algorithm::key_map_type d_persistent_key_map;
bool d_verbose;
public:
virtual crypto_algorithm::sptr current_algorithm() const
{ return d_current_algorithm; }
virtual bool logging_enabled() const
{ return d_verbose; }
};
#endif // INCLUDED_CRYPTO_H

View File

@ -1,64 +0,0 @@
#include "crypto_module_du_handler.h"
#include "abstract_data_unit.h"
#include <boost/format.hpp>
#include <sstream>
#include <stdio.h>
crypto_module_du_handler::crypto_module_du_handler(data_unit_handler_sptr next, crypto_module::sptr crypto_mod)
: data_unit_handler(next)
, d_crypto_mod(crypto_mod)
{
}
void
crypto_module_du_handler::handle(data_unit_sptr du)
{
if (!d_crypto_mod)
{
data_unit_handler::handle(du);
return;
}
crypto_state_provider* p = dynamic_cast<crypto_state_provider*>(du.get());
if (p == NULL)
{
data_unit_handler::handle(du);
return;
}
CryptoState state = p->crypto_state();
///////////////////////////////////
if (d_crypto_mod->logging_enabled())
{
std::string duid_str("?");
abstract_data_unit* adu = dynamic_cast<abstract_data_unit*>(du.get());
if (adu)
duid_str = adu->duid_str();
std::stringstream ss;
for (size_t n = 0; n < state.mi.size(); ++n)
ss << (boost::format("%02x") % (int)state.mi[n]);
fprintf(stderr, "%s:\tAlgID: 0x%02x, KID: 0x%04x, MI: %s\n", duid_str.c_str(), state.algid, state.kid, ss.str().c_str());
}
///////////////////////////////////
crypto_algorithm::sptr algorithm = d_crypto_mod->algorithm(state.algid);
if (!algorithm)
{
data_unit_handler::handle(du);
return;
}
// TODO: Could do key management & selection here with 'state.kid'
// Assuming we're only using one key (ignoring 'kid')
algorithm->update(state);
data_unit_handler::handle(du);
}

View File

@ -1,21 +0,0 @@
#ifndef INCLUDED_CRYPTO_MODULE_DU_HANDLER_H
#define INCLUDED_CRYPTO_MODULE_DU_HANDLER_H
#include <boost/shared_ptr.hpp>
#include "data_unit_handler.h"
#include "crypto.h"
class crypto_module_du_handler : public data_unit_handler
{
public:
crypto_module_du_handler(data_unit_handler_sptr next, crypto_module::sptr crypto_mod);
public:
typedef boost::shared_ptr<class crypto_module_du_handler> sptr;
public:
virtual void handle(data_unit_sptr du);
private:
crypto_module::sptr d_crypto_mod;
};
#endif //INCLUDED_CRYPTO_MODULE_HANDLER_H

View File

@ -32,8 +32,6 @@
#include <iosfwd>
#include <stdint.h>
#include "crypto.h"
typedef std::deque<bool> bit_queue;
typedef const std::deque<bool> const_bit_queue;
@ -78,7 +76,7 @@ public:
* \precondition is_complete() == true.
* \param imbe The imbe_decoder to use to generate the audio.
*/
virtual void decode_audio(imbe_decoder& imbe, crypto_module::sptr crypto_mod) = 0;
virtual void decode_audio(imbe_decoder& imbe) = 0;
/**
* Decode the frame into an octet vector.
@ -134,8 +132,6 @@ public:
*/
virtual std::string snapshot() const = 0;
virtual void set_logging(bool on) = 0;
protected:
/**

View File

@ -34,7 +34,6 @@
#include "offline_imbe_decoder.h"
#include "voice_du_handler.h"
#include "op25_yank.h"
#include "bch.h"
using namespace std;
@ -42,13 +41,13 @@ namespace gr {
namespace op25 {
decoder_bf::sptr
decoder_bf::make(bool idle_silence /*= true*/, bool verbose /*= false*/)
decoder_bf::make()
{
return gnuradio::get_initial_sptr
(new decoder_bf_impl(idle_silence, verbose));
(new decoder_bf_impl());
}
decoder_bf_impl::decoder_bf_impl(bool idle_silence /*= true*/, bool verbose /*= false*/) :
decoder_bf_impl::decoder_bf_impl() :
gr::block("decoder_bf",
gr::io_signature::make(1, 1, sizeof(uint8_t)),
gr::io_signature::make(0, 1, sizeof(float))),
@ -57,25 +56,14 @@ namespace gr {
d_frame_hdr(),
d_imbe(imbe_decoder::make()),
d_state(SYNCHRONIZING),
d_p25cai_du_handler(NULL),
d_idle_silence(idle_silence),
d_verbose(false)
d_p25cai_du_handler(NULL)
{
set_logging(verbose);
d_p25cai_du_handler = new p25cai_du_handler(d_data_unit_handler,
"224.0.0.1", 23456);
d_data_unit_handler = data_unit_handler_sptr(d_p25cai_du_handler);
d_snapshot_du_handler = new snapshot_du_handler(d_data_unit_handler);
d_data_unit_handler = data_unit_handler_sptr(d_snapshot_du_handler);
d_crypto_module = crypto_module::sptr(new crypto_module(verbose));
d_crypto_module_du_handler = crypto_module_du_handler::sptr(new crypto_module_du_handler(d_data_unit_handler, d_crypto_module));
d_data_unit_handler = data_unit_handler_sptr(d_crypto_module_du_handler);
d_data_unit_handler = data_unit_handler_sptr(new voice_du_handler(d_data_unit_handler, d_imbe, d_crypto_module));
d_data_unit_handler = data_unit_handler_sptr(new voice_du_handler(d_data_unit_handler, d_imbe));
}
decoder_bf_impl::~decoder_bf_impl()
@ -116,35 +104,34 @@ namespace gr {
gr_vector_void_star &output_items)
{
try {
gr::thread::scoped_lock lock(d_mutex);
// process input
const uint8_t *in = reinterpret_cast<const uint8_t*>(input_items[0]);
for(int i = 0; i < ninput_items[0]; ++i) {
dibit d = in[i] & 0x3;
receive_symbol(d);
}
consume_each(ninput_items[0]);
// process input
const uint8_t *in = reinterpret_cast<const uint8_t*>(input_items[0]);
for(int i = 0; i < ninput_items[0]; ++i) {
dibit d = in[i] & 0x3;
receive_symbol(d);
}
consume_each(ninput_items[0]);
// produce audio
audio_samples *samples = d_imbe->audio();
float *out = reinterpret_cast<float*>(output_items[0]);
const int n = min(static_cast<int>(samples->size()), noutput_items);
if(0 < n) {
copy(samples->begin(), samples->begin() + n, out);
samples->erase(samples->begin(), samples->begin() + n);
}
if((d_idle_silence) && (n < noutput_items)) {
fill(out + n, out + noutput_items, 0.0);
}
return (d_idle_silence ? noutput_items : n);
// produce audio
audio_samples *samples = d_imbe->audio();
float *out = reinterpret_cast<float*>(output_items[0]);
const int n = min(static_cast<int>(samples->size()), noutput_items);
if(0 < n) {
copy(samples->begin(), samples->begin() + n, out);
samples->erase(samples->begin(), samples->begin() + n);
}
if(n < noutput_items) {
fill(out + n, out + noutput_items, 0.0);
}
return noutput_items;
} catch(const std::exception& x) {
cerr << x.what() << endl;
exit(1);
cerr << x.what() << endl;
exit(1);
} catch(...) {
cerr << "unhandled exception" << endl;
exit(2); }
cerr << "unhandled exception" << endl;
exit(2); }
}
const char*
@ -190,12 +177,14 @@ namespace gr {
};
size_t NID_SZ = sizeof(NID) / sizeof(NID[0]);
bit_vector b(NID_SZ);
itpp::bvec b(63), zeroes(16);
itpp::BCH bch(63, 16, 11, "6 3 3 1 1 4 1 3 6 7 2 3 5 4 5 3", true);
yank(d_frame_hdr, NID, NID_SZ, b, 0);
if(bchDec(b) >= 0) {
b = bch.decode(b);
if(b != zeroes) {
b = bch.encode(b);
yank_back(b, 0, d_frame_hdr, NID, NID_SZ);
d_data_unit = data_unit::make_data_unit(d_frame_hdr);
d_data_unit->set_logging(d_verbose);
} else {
data_unit_sptr null;
d_data_unit = null;
@ -240,36 +229,5 @@ namespace gr {
break;
}
}
void
decoder_bf_impl::set_idle_silence(bool idle_silence/* = true*/)
{
gr::thread::scoped_lock lock(d_mutex);
d_idle_silence = idle_silence;
}
void
decoder_bf_impl::set_logging(bool verbose/* = true*/)
{
if (verbose) fprintf(stderr, "[%s<%lu>] verbose logging enabled\n", name().c_str(), unique_id());
d_verbose = verbose;
if (d_crypto_module)
d_crypto_module->set_logging(verbose);
}
void
decoder_bf_impl::set_key(const crypto_algorithm::key_type& key)
{
d_crypto_module->set_key(key);
}
void
decoder_bf_impl::set_key_map(const crypto_algorithm::key_map_type& keys)
{
d_crypto_module->set_key_map(keys);
}
} /* namespace op25 */
} /* namespace gr */

View File

@ -24,14 +24,11 @@
#define INCLUDED_OP25_DECODER_BF_IMPL_H
#include <op25/decoder_bf.h>
#include <gnuradio/thread/thread.h>
#include "data_unit.h"
#include "data_unit_handler.h"
#include "imbe_decoder.h"
#include "p25cai_du_handler.h"
#include "snapshot_du_handler.h"
#include "crypto.h"
#include "crypto_module_du_handler.h"
namespace gr {
namespace op25 {
@ -105,21 +102,8 @@ namespace gr {
*/
class snapshot_du_handler *d_snapshot_du_handler;
/*
* Whether or not to output silence when no audio is synthesised.
*/
bool d_idle_silence;
bool d_verbose;
crypto_module::sptr d_crypto_module;
crypto_module_du_handler::sptr d_crypto_module_du_handler;
gr::thread::mutex d_mutex;
public:
decoder_bf_impl(bool idle_silence = true, bool verbose = false);
decoder_bf_impl();
~decoder_bf_impl();
// Where all the action really happens
@ -155,14 +139,6 @@ namespace gr {
* message queue.
*/
void set_msgq(gr::msg_queue::sptr msgq);
void set_idle_silence(bool idle_silence = true);
void set_logging(bool verbose = true);
void set_key(const crypto_algorithm::key_type& key);
void set_key_map(const crypto_algorithm::key_map_type& keys);
};
} // namespace op25
} // namespace gr

View File

@ -34,7 +34,6 @@
#include "offline_imbe_decoder.h"
#include "voice_du_handler.h"
#include "op25_yank.h"
#include "bch.h"
using namespace std;
@ -186,9 +185,12 @@ namespace gr {
};
size_t NID_SZ = sizeof(NID) / sizeof(NID[0]);
bit_vector b(NID_SZ);
itpp::bvec b(63), zeroes(16);
itpp::BCH bch(63, 16, 11, "6 3 3 1 1 4 1 3 6 7 2 3 5 4 5 3", true);
yank(d_frame_hdr, NID, NID_SZ, b, 0);
if(bchDec(b) >= 0) {
b = bch.decode(b);
if(b != zeroes) {
b = bch.encode(b);
yank_back(b, 0, d_frame_hdr, NID, NID_SZ);
d_data_unit = data_unit::make_data_unit(d_frame_hdr);
} else {

View File

@ -1,15 +0,0 @@
typedef unsigned long DES_KS[16][2]; /* Single-key DES key schedule */
typedef unsigned long DES3_KS[48][2]; /* Triple-DES key schedule */
/* In deskey.c: */
void deskey(DES_KS,unsigned char *,int);
void des3key(DES3_KS,unsigned char *,int);
/* In desport.c, desborl.cas or desgnu.s: */
void des(DES_KS,unsigned char *);
/* In des3port.c, des3borl.cas or des3gnu.s: */
void des3(DES3_KS,unsigned char *);
extern int Asmversion; /* 1 if we're linked with an asm version, 0 if C */

View File

@ -1,124 +0,0 @@
/* Portable C code to create DES key schedules from user-provided keys
* This doesn't have to be fast unless you're cracking keys or UNIX
* passwords
*/
#include <string.h>
#include "des.h"
/* Key schedule-related tables from FIPS-46 */
/* permuted choice table (key) */
static unsigned char pc1[] = {
57, 49, 41, 33, 25, 17, 9,
1, 58, 50, 42, 34, 26, 18,
10, 2, 59, 51, 43, 35, 27,
19, 11, 3, 60, 52, 44, 36,
63, 55, 47, 39, 31, 23, 15,
7, 62, 54, 46, 38, 30, 22,
14, 6, 61, 53, 45, 37, 29,
21, 13, 5, 28, 20, 12, 4
};
/* number left rotations of pc1 */
static unsigned char totrot[] = {
1,2,4,6,8,10,12,14,15,17,19,21,23,25,27,28
};
/* permuted choice key (table) */
static unsigned char pc2[] = {
14, 17, 11, 24, 1, 5,
3, 28, 15, 6, 21, 10,
23, 19, 12, 4, 26, 8,
16, 7, 27, 20, 13, 2,
41, 52, 31, 37, 47, 55,
30, 40, 51, 45, 33, 48,
44, 49, 39, 56, 34, 53,
46, 42, 50, 36, 29, 32
};
/* End of DES-defined tables */
/* bit 0 is left-most in byte */
static int bytebit[] = {
0200,0100,040,020,010,04,02,01
};
/* Generate key schedule for encryption or decryption
* depending on the value of "decrypt"
*/
void
deskey(DES_KS k,unsigned char *key,int decrypt)
/* Key schedule array */
/* 64 bits (will use only 56) */
/* 0 = encrypt, 1 = decrypt */
{
unsigned char pc1m[56]; /* place to modify pc1 into */
unsigned char pcr[56]; /* place to rotate pc1 into */
register int i,j,l;
int m;
unsigned char ks[8];
for (j=0; j<56; j++) { /* convert pc1 to bits of key */
l=pc1[j]-1; /* integer bit location */
m = l & 07; /* find bit */
pc1m[j]=(key[l>>3] & /* find which key byte l is in */
bytebit[m]) /* and which bit of that byte */
? 1 : 0; /* and store 1-bit result */
}
for (i=0; i<16; i++) { /* key chunk for each iteration */
memset(ks,0,sizeof(ks)); /* Clear key schedule */
for (j=0; j<56; j++) /* rotate pc1 the right amount */
pcr[j] = pc1m[(l=j+totrot[decrypt? 15-i : i])<(j<28? 28 : 56) ? l: l-28];
/* rotate left and right halves independently */
for (j=0; j<48; j++){ /* select bits individually */
/* check bit that goes to ks[j] */
if (pcr[pc2[j]-1]){
/* mask it in if it's there */
l= j % 6;
ks[j/6] |= bytebit[l] >> 2;
}
}
/* Now convert to packed odd/even interleaved form */
k[i][0] = ((long)ks[0] << 24)
| ((long)ks[2] << 16)
| ((long)ks[4] << 8)
| ((long)ks[6]);
k[i][1] = ((long)ks[1] << 24)
| ((long)ks[3] << 16)
| ((long)ks[5] << 8)
| ((long)ks[7]);
if(Asmversion){
/* The assembler versions pre-shift each subkey 2 bits
* so the Spbox indexes are already computed
*/
k[i][0] <<= 2;
k[i][1] <<= 2;
}
}
}
/* Generate key schedule for triple DES in E-D-E (or D-E-D) mode.
*
* The key argument is taken to be 24 bytes. The first 8 bytes are K1
* for the first stage, the second 8 bytes are K2 for the middle stage
* and the third 8 bytes are K3 for the last stage
*/
void
des3key(DES3_KS k,unsigned char *key,int decrypt)
/* 192 bits (will use only 168) */
/* 0 = encrypt, 1 = decrypt */
{
if(!decrypt){
deskey(&k[0],&key[0],0);
deskey(&k[16],&key[8],1);
deskey(&k[32],&key[16],0);
} else {
deskey(&k[32],&key[0],1);
deskey(&k[16],&key[8],0);
deskey(&k[0],&key[16],1);
}
}

View File

@ -1,236 +0,0 @@
/* Portable C version of des() function */
#include <stdint.h>
#include "des.h"
/* Tables defined in the Data Encryption Standard documents
* Three of these tables, the initial permutation, the final
* permutation and the expansion operator, are regular enough that
* for speed, we hard-code them. They're here for reference only.
* Also, the S and P boxes are used by a separate program, gensp.c,
* to build the combined SP box, Spbox[]. They're also here just
* for reference.
*/
#ifdef notdef
/* initial permutation IP */
static unsigned char ip[] = {
58, 50, 42, 34, 26, 18, 10, 2,
60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6,
64, 56, 48, 40, 32, 24, 16, 8,
57, 49, 41, 33, 25, 17, 9, 1,
59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5,
63, 55, 47, 39, 31, 23, 15, 7
};
/* final permutation IP^-1 */
static unsigned char fp[] = {
40, 8, 48, 16, 56, 24, 64, 32,
39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30,
37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28,
35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25
};
/* expansion operation matrix */
static unsigned char ei[] = {
32, 1, 2, 3, 4, 5,
4, 5, 6, 7, 8, 9,
8, 9, 10, 11, 12, 13,
12, 13, 14, 15, 16, 17,
16, 17, 18, 19, 20, 21,
20, 21, 22, 23, 24, 25,
24, 25, 26, 27, 28, 29,
28, 29, 30, 31, 32, 1
};
/* The (in)famous S-boxes */
static unsigned char sbox[8][64] = {
/* S1 */
14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13,
/* S2 */
15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9,
/* S3 */
10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12,
/* S4 */
7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14,
/* S5 */
2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3,
/* S6 */
12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13,
/* S7 */
4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12,
/* S8 */
13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11
};
/* 32-bit permutation function P used on the output of the S-boxes */
static unsigned char p32i[] = {
16, 7, 20, 21,
29, 12, 28, 17,
1, 15, 23, 26,
5, 18, 31, 10,
2, 8, 24, 14,
32, 27, 3, 9,
19, 13, 30, 6,
22, 11, 4, 25
};
#endif
int Asmversion = 0;
/* Combined SP lookup table, linked in
* For best results, ensure that this is aligned on a 32-bit boundary;
* Borland C++ 3.1 doesn't guarantee this!
*/
extern uint64_t Spbox[8][64]; /* Combined S and P boxes */
/* Primitive function F.
* Input is r, subkey array in keys, output is XORed into l.
* Each round consumes eight 6-bit subkeys, one for
* each of the 8 S-boxes, 2 longs for each round.
* Each long contains four 6-bit subkeys, each taking up a byte.
* The first long contains, from high to low end, the subkeys for
* S-boxes 1, 3, 5 & 7; the second contains the subkeys for S-boxes
* 2, 4, 6 & 8 (using the origin-1 S-box numbering in the standard,
* not the origin-0 numbering used elsewhere in this code)
* See comments elsewhere about the pre-rotated values of r and Spbox.
*/
#define F(l,r,key){\
work = ((r >> 4) | (r << 28)) ^ key[0];\
l ^= Spbox[6][work & 0x3f];\
l ^= Spbox[4][(work >> 8) & 0x3f];\
l ^= Spbox[2][(work >> 16) & 0x3f];\
l ^= Spbox[0][(work >> 24) & 0x3f];\
work = r ^ key[1];\
l ^= Spbox[7][work & 0x3f];\
l ^= Spbox[5][(work >> 8) & 0x3f];\
l ^= Spbox[3][(work >> 16) & 0x3f];\
l ^= Spbox[1][(work >> 24) & 0x3f];\
}
/* Encrypt or decrypt a block of data in ECB mode */
void
des(unsigned long ks[16][2],unsigned char block[8])
/* Key schedule */
/* Data block */
{
unsigned long left,right,work;
/* Read input block and place in left/right in big-endian order */
left = ((unsigned long)block[0] << 24)
| ((unsigned long)block[1] << 16)
| ((unsigned long)block[2] << 8)
| (unsigned long)block[3];
right = ((unsigned long)block[4] << 24)
| ((unsigned long)block[5] << 16)
| ((unsigned long)block[6] << 8)
| (unsigned long)block[7];
/* Hoey's clever initial permutation algorithm, from Outerbridge
* (see Schneier p 478)
*
* The convention here is the same as Outerbridge: rotate each
* register left by 1 bit, i.e., so that "left" contains permuted
* input bits 2, 3, 4, ... 1 and "right" contains 33, 34, 35, ... 32
* (using origin-1 numbering as in the FIPS). This allows us to avoid
* one of the two rotates that would otherwise be required in each of
* the 16 rounds.
*/
work = ((left >> 4) ^ right) & 0x0f0f0f0f;
right ^= work;
left ^= work << 4;
work = ((left >> 16) ^ right) & 0xffff;
right ^= work;
left ^= work << 16;
work = ((right >> 2) ^ left) & 0x33333333;
left ^= work;
right ^= (work << 2);
work = ((right >> 8) ^ left) & 0xff00ff;
left ^= work;
right ^= (work << 8);
right = (right << 1) | (right >> 31);
work = (left ^ right) & 0xaaaaaaaa;
left ^= work;
right ^= work;
left = (left << 1) | (left >> 31);
/* Now do the 16 rounds */
F(left,right,ks[0]);
F(right,left,ks[1]);
F(left,right,ks[2]);
F(right,left,ks[3]);
F(left,right,ks[4]);
F(right,left,ks[5]);
F(left,right,ks[6]);
F(right,left,ks[7]);
F(left,right,ks[8]);
F(right,left,ks[9]);
F(left,right,ks[10]);
F(right,left,ks[11]);
F(left,right,ks[12]);
F(right,left,ks[13]);
F(left,right,ks[14]);
F(right,left,ks[15]);
/* Inverse permutation, also from Hoey via Outerbridge and Schneier */
right = (right << 31) | (right >> 1);
work = (left ^ right) & 0xaaaaaaaa;
left ^= work;
right ^= work;
left = (left >> 1) | (left << 31);
work = ((left >> 8) ^ right) & 0xff00ff;
right ^= work;
left ^= work << 8;
work = ((left >> 2) ^ right) & 0x33333333;
right ^= work;
left ^= work << 2;
work = ((right >> 16) ^ left) & 0xffff;
left ^= work;
right ^= work << 16;
work = ((right >> 4) ^ left) & 0x0f0f0f0f;
left ^= work;
right ^= work << 4;
/* Put the block back into the user's buffer with final swap */
block[0] = right >> 24;
block[1] = right >> 16;
block[2] = right >> 8;
block[3] = right;
block[4] = left >> 24;
block[5] = left >> 16;
block[6] = left >> 8;
block[7] = left;
}

View File

@ -1,131 +0,0 @@
#include <stdint.h>
uint64_t Spbox[8][64] = {
0x01010400,0x00000000,0x00010000,0x01010404,
0x01010004,0x00010404,0x00000004,0x00010000,
0x00000400,0x01010400,0x01010404,0x00000400,
0x01000404,0x01010004,0x01000000,0x00000004,
0x00000404,0x01000400,0x01000400,0x00010400,
0x00010400,0x01010000,0x01010000,0x01000404,
0x00010004,0x01000004,0x01000004,0x00010004,
0x00000000,0x00000404,0x00010404,0x01000000,
0x00010000,0x01010404,0x00000004,0x01010000,
0x01010400,0x01000000,0x01000000,0x00000400,
0x01010004,0x00010000,0x00010400,0x01000004,
0x00000400,0x00000004,0x01000404,0x00010404,
0x01010404,0x00010004,0x01010000,0x01000404,
0x01000004,0x00000404,0x00010404,0x01010400,
0x00000404,0x01000400,0x01000400,0x00000000,
0x00010004,0x00010400,0x00000000,0x01010004,
0x80108020,0x80008000,0x00008000,0x00108020,
0x00100000,0x00000020,0x80100020,0x80008020,
0x80000020,0x80108020,0x80108000,0x80000000,
0x80008000,0x00100000,0x00000020,0x80100020,
0x00108000,0x00100020,0x80008020,0x00000000,
0x80000000,0x00008000,0x00108020,0x80100000,
0x00100020,0x80000020,0x00000000,0x00108000,
0x00008020,0x80108000,0x80100000,0x00008020,
0x00000000,0x00108020,0x80100020,0x00100000,
0x80008020,0x80100000,0x80108000,0x00008000,
0x80100000,0x80008000,0x00000020,0x80108020,
0x00108020,0x00000020,0x00008000,0x80000000,
0x00008020,0x80108000,0x00100000,0x80000020,
0x00100020,0x80008020,0x80000020,0x00100020,
0x00108000,0x00000000,0x80008000,0x00008020,
0x80000000,0x80100020,0x80108020,0x00108000,
0x00000208,0x08020200,0x00000000,0x08020008,
0x08000200,0x00000000,0x00020208,0x08000200,
0x00020008,0x08000008,0x08000008,0x00020000,
0x08020208,0x00020008,0x08020000,0x00000208,
0x08000000,0x00000008,0x08020200,0x00000200,
0x00020200,0x08020000,0x08020008,0x00020208,
0x08000208,0x00020200,0x00020000,0x08000208,
0x00000008,0x08020208,0x00000200,0x08000000,
0x08020200,0x08000000,0x00020008,0x00000208,
0x00020000,0x08020200,0x08000200,0x00000000,
0x00000200,0x00020008,0x08020208,0x08000200,
0x08000008,0x00000200,0x00000000,0x08020008,
0x08000208,0x00020000,0x08000000,0x08020208,
0x00000008,0x00020208,0x00020200,0x08000008,
0x08020000,0x08000208,0x00000208,0x08020000,
0x00020208,0x00000008,0x08020008,0x00020200,
0x100802001,0x100002081,0x100002081,0x00000080,
0x00802080,0x100800081,0x100800001,0x100002001,
0x00000000,0x00802000,0x00802000,0x100802081,
0x100000081,0x00000000,0x00800080,0x100800001,
0x100000001,0x00002000,0x00800000,0x100802001,
0x00000080,0x00800000,0x100002001,0x00002080,
0x100800081,0x100000001,0x00002080,0x00800080,
0x00002000,0x00802080,0x100802081,0x100000081,
0x00800080,0x100800001,0x00802000,0x100802081,
0x100000081,0x00000000,0x00000000,0x00802000,
0x00002080,0x00800080,0x100800081,0x100000001,
0x100802001,0x100002081,0x100002081,0x00000080,
0x100802081,0x100000081,0x100000001,0x00002000,
0x100800001,0x100002001,0x00802080,0x100800081,
0x100002001,0x00002080,0x00800000,0x100802001,
0x00000080,0x00800000,0x00002000,0x00802080,
0x00000100,0x02080100,0x02080000,0x42000100,
0x00080000,0x00000100,0x40000000,0x02080000,
0x40080100,0x00080000,0x02000100,0x40080100,
0x42000100,0x42080000,0x00080100,0x40000000,
0x02000000,0x40080000,0x40080000,0x00000000,
0x40000100,0x42080100,0x42080100,0x02000100,
0x42080000,0x40000100,0x00000000,0x42000000,
0x02080100,0x02000000,0x42000000,0x00080100,
0x00080000,0x42000100,0x00000100,0x02000000,
0x40000000,0x02080000,0x42000100,0x40080100,
0x02000100,0x40000000,0x42080000,0x02080100,
0x40080100,0x00000100,0x02000000,0x42080000,
0x42080100,0x00080100,0x42000000,0x42080100,
0x02080000,0x00000000,0x40080000,0x42000000,
0x00080100,0x02000100,0x40000100,0x00080000,
0x00000000,0x40080000,0x02080100,0x40000100,
0x20000010,0x20400000,0x00004000,0x20404010,
0x20400000,0x00000010,0x20404010,0x00400000,
0x20004000,0x00404010,0x00400000,0x20000010,
0x00400010,0x20004000,0x20000000,0x00004010,
0x00000000,0x00400010,0x20004010,0x00004000,
0x00404000,0x20004010,0x00000010,0x20400010,
0x20400010,0x00000000,0x00404010,0x20404000,
0x00004010,0x00404000,0x20404000,0x20000000,
0x20004000,0x00000010,0x20400010,0x00404000,
0x20404010,0x00400000,0x00004010,0x20000010,
0x00400000,0x20004000,0x20000000,0x00004010,
0x20000010,0x20404010,0x00404000,0x20400000,
0x00404010,0x20404000,0x00000000,0x20400010,
0x00000010,0x00004000,0x20400000,0x00404010,
0x00004000,0x00400010,0x20004010,0x00000000,
0x20404000,0x20000000,0x00400010,0x20004010,
0x00200000,0x04200002,0x04000802,0x00000000,
0x00000800,0x04000802,0x00200802,0x04200800,
0x04200802,0x00200000,0x00000000,0x04000002,
0x00000002,0x04000000,0x04200002,0x00000802,
0x04000800,0x00200802,0x00200002,0x04000800,
0x04000002,0x04200000,0x04200800,0x00200002,
0x04200000,0x00000800,0x00000802,0x04200802,
0x00200800,0x00000002,0x04000000,0x00200800,
0x04000000,0x00200800,0x00200000,0x04000802,
0x04000802,0x04200002,0x04200002,0x00000002,
0x00200002,0x04000000,0x04000800,0x00200000,
0x04200800,0x00000802,0x00200802,0x04200800,
0x00000802,0x04000002,0x04200802,0x04200000,
0x00200800,0x00000000,0x00000002,0x04200802,
0x00000000,0x00200802,0x04200000,0x00000800,
0x04000002,0x04000800,0x00000800,0x00200002,
0x10001040,0x00001000,0x00040000,0x10041040,
0x10000000,0x10001040,0x00000040,0x10000000,
0x00040040,0x10040000,0x10041040,0x00041000,
0x10041000,0x00041040,0x00001000,0x00000040,
0x10040000,0x10000040,0x10001000,0x00001040,
0x00041000,0x00040040,0x10040040,0x10041000,
0x00001040,0x00000000,0x00000000,0x10040040,
0x10000040,0x10001000,0x00041040,0x00040000,
0x00041040,0x00040000,0x10041000,0x00001000,
0x00000040,0x10040040,0x00001000,0x00041040,
0x10001000,0x00000040,0x10000040,0x10040000,
0x10040040,0x10000000,0x00040000,0x10001040,
0x00000000,0x10041040,0x00040040,0x10000040,
0x10040000,0x10001000,0x10001040,0x00000000,
0x10041040,0x00041000,0x00041000,0x00001040,
0x00001040,0x00040040,0x10000000,0x10041000,
};

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

@ -28,8 +28,6 @@
#include <iomanip>
#include <sstream>
#include <boost/format.hpp>
#include <stdio.h>
using namespace std;
@ -67,8 +65,6 @@ hdu::do_correct_errors(bit_vector& frame)
{
apply_golay_correction(frame);
apply_rs_correction(frame);
if (logging_enabled()) fprintf(stderr, "\n");
}
void
@ -140,58 +136,33 @@ hdu::frame_size_max() const
return 792;
}
uint8_t
hdu::algid() const
string
hdu::algid_str() const
{
const size_t ALGID_BITS[] = {
356, 357, 360, 361, 374, 375, 376, 377
};
const size_t ALGID_BITS_SZ = sizeof(ALGID_BITS) / sizeof(ALGID_BITS[0]);
return extract(frame_body(), ALGID_BITS, ALGID_BITS_SZ);
uint8_t algid = extract(frame_body(), ALGID_BITS, ALGID_BITS_SZ);
return lookup(algid, ALGIDS, ALGIDS_SZ);
}
string
hdu::algid_str() const
{
uint8_t _algid = algid();
return lookup(_algid, ALGIDS, ALGIDS_SZ);
}
uint16_t
hdu::kid() const
hdu::kid_str() const
{
const size_t KID_BITS[] = {
378, 379, 392, 393, 394, 395, 396, 397,
410, 411, 412, 413, 414, 415, 428, 429
};
const size_t KID_BITS_SZ = sizeof(KID_BITS) / sizeof(KID_BITS[0]);
return extract(frame_body(), KID_BITS, KID_BITS_SZ);
}
string
hdu::kid_str() const
{
uint16_t _kid = kid();
uint16_t kid = extract(frame_body(), KID_BITS, KID_BITS_SZ);
ostringstream os;
os << hex << showbase << setfill('0') << setw(4) << _kid;
os << hex << showbase << setfill('0') << setw(4) << kid;
return os.str();
}
std::string
hdu::mi_str() const
{
std::vector<uint8_t> _mi(mi());
ostringstream os;
os << "0x";
for(size_t i = 0; i < _mi.size(); ++i) {
uint16_t octet = _mi[i];
os << hex << setfill('0') << setw(2) << octet;
}
return os.str();
}
std::vector<uint8_t>
hdu::mi() const
{
const size_t MI_BITS[] = {
114, 115, 116, 117, 118, 119, 132, 133,
@ -206,9 +177,15 @@ hdu::mi() const
};
const size_t MI_BITS_SZ = sizeof(MI_BITS) / sizeof(MI_BITS[0]);
std::vector<uint8_t> _mi(((MI_BITS_SZ + 7) / 8));
extract(frame_body(), MI_BITS, MI_BITS_SZ, &_mi[0]);
return _mi;
uint8_t mi[9];
extract(frame_body(), MI_BITS, MI_BITS_SZ, mi);
ostringstream os;
os << "0x";
for(size_t i = 0; i < (sizeof(mi) / sizeof(mi[0])); ++i) {
uint16_t octet = mi[i];
os << hex << setfill('0') << setw(2) << octet;
}
return os.str();
}
string
@ -242,21 +219,7 @@ hdu::tgid_str() const
};
const size_t TGID_BITS_SZ = sizeof(TGID_BITS) / sizeof(TGID_BITS[0]);
const uint16_t tgid = extract(frame_body(), TGID_BITS, TGID_BITS_SZ);
// Zero fill isn't working properly in original implementation
//ostringstream os;
//os << hex << showbase << setfill('0') << setw(4) << tgid;
//return os.str();
return (boost::format("0x%04x") % tgid).str();
}
struct CryptoState
hdu::crypto_state() const
{
struct CryptoState state;
state.mi = mi();
state.kid = kid();
state.algid = algid();
return state;
ostringstream os;
os << hex << showbase << setfill('0') << setw(4) << tgid;
return os.str();
}

View File

@ -25,12 +25,11 @@
#define INCLUDED_HDU_H
#include "abstract_data_unit.h"
#include "crypto.h"
/**
* P25 header data unit (HDU).
*/
class hdu : public abstract_data_unit, public crypto_state_provider
class hdu : public abstract_data_unit
{
public:
@ -97,9 +96,7 @@ protected:
*/
virtual uint16_t frame_size_max() const;
public:
uint8_t algid() const;
private:
/**
* Return a string describing the encryption algorithm ID (ALGID).
@ -108,8 +105,6 @@ public:
*/
std::string algid_str() const;
virtual uint16_t kid() const;
/**
* Returns a string describing the key id (KID).
*
@ -124,8 +119,6 @@ public:
*/
virtual std::string mfid_str() const;
virtual std::vector<uint8_t> mi() const;
/**
* Returns a string describing the message indicator (MI).
*
@ -146,10 +139,6 @@ public:
* \return A string identifying the TGID.
*/
virtual std::string tgid_str() const;
public:
struct CryptoState crypto_state() const;
};
#endif /* INCLUDED_HDU_H */

View File

@ -1,102 +0,0 @@
#include "ldu.h"
#include <stdio.h>
#include <itpp/base/vec.h>
#include <itpp/base/mat.h>
#include <itpp/base/binary.h>
#include <itpp/base/converters.h>
const static itpp::Mat<int> ham_10_6_3_6("1 1 1 0 0 1 1 0 0 0; 1 1 0 1 0 1 0 1 0 0; 1 0 1 1 1 0 0 0 1 0; 0 1 1 1 1 0 0 0 0 1");
typedef std::vector<itpp::Vec<int> > VecArray;
ldu::ldu(const_bit_queue& frame_body) :
voice_data_unit(frame_body),
m_hamming_error_count(0)
{
}
void
ldu::do_correct_errors(bit_vector& frame_body)
{
voice_data_unit::do_correct_errors(frame_body);
}
bool
ldu::process_meta_data(bit_vector& frame_body)
{
m_hamming_error_count = 0;
//std::vector<uint8_t> lc(30);
//std::vector<uint16_t> ham(24);
int lc_bit_idx = 0;
VecArray arrayVec;
itpp::Vec<int> vecRaw(10); // First 6 bits contain data
for (int i = 400; i < 1360; i += 184)
{
for (int j = 0; j < 40; j++)
{
int x = (i + j) + (((i + j) / 70) * 2); // Adjust bit index for status
unsigned char ch = frame_body[x];
//lc[lc_bit_idx / 8] |= (ch << (7 - (lc_bit_idx % 8)));
//ham[lc_bit_idx / 10] = ((ham[lc_bit_idx / 10]) << 1) | ch;
vecRaw(lc_bit_idx % 10) = ch;
++lc_bit_idx;
if ((lc_bit_idx % 10) == 0)
arrayVec.push_back(vecRaw);
}
}
if (lc_bit_idx != 240) // Not enough bits
{
return false;
}
if (arrayVec.size() != 24) // Not enough vectors
{
return false;
}
itpp::Vec<int> vecZero(4);
vecZero.zeros();
m_raw_meta_data.clear();
for (int i = 0; i < arrayVec.size(); ++i)
{
itpp::Vec<int>& vec = arrayVec[i];
itpp::bvec vB(itpp::to_bvec(vec));
itpp::Vec<int> vS = ham_10_6_3_6 * vec;
for (int i = 0; i < vS.length(); ++i)
vS[i] = vS[i] % 2;
itpp::bvec vb(to_bvec(vS));
if (itpp::bin2dec(vb) != 0)
{
++m_hamming_error_count;
}
m_raw_meta_data = concat(m_raw_meta_data, vB.mid(0, 6)); // Includes RS for last 72 bits
}
if (logging_enabled()) fprintf(stderr, "%s: %lu hamming errors, %s\n", duid_str().c_str(), m_hamming_error_count, (meta_data_valid() ? "valid" : "invalid"));
return meta_data_valid();
}
const itpp::bvec&
ldu::raw_meta_data() const
{
return m_raw_meta_data;
}
bool
ldu::meta_data_valid() const
{
return (m_raw_meta_data.length() == 144); // Not enough bits after Hamming(10,6,3)
}

View File

@ -1,28 +0,0 @@
#ifndef INCLUDED_LDU_H
#define INCLUDED_LDU_H
#include "voice_data_unit.h"
class ldu : public voice_data_unit
{
private:
size_t m_hamming_error_count;
itpp::bvec m_raw_meta_data;
protected:
virtual void do_correct_errors(bit_vector& frame_body);
virtual bool process_meta_data(bit_vector& frame_body);
virtual const itpp::bvec& raw_meta_data() const;
public:
ldu(const_bit_queue& frame_body);
virtual bool meta_data_valid() const;
};
#endif // INCLUDED_LDU_H

View File

@ -23,19 +23,10 @@
#include "ldu1.h"
#include <itpp/base/vec.h>
#include <itpp/base/converters.h>
#include <boost/format.hpp>
#include <iostream>
#include "pickle.h"
#include "value_string.h"
using std::string;
ldu1::ldu1(const_bit_queue& frame_body) :
ldu(frame_body)
voice_data_unit(frame_body)
{
}
@ -43,64 +34,6 @@ ldu1::~ldu1()
{
}
void ldu1::do_correct_errors(bit_vector& frame_body)
{
ldu::do_correct_errors(frame_body);
if (!process_meta_data(frame_body))
return;
const itpp::bvec& data = raw_meta_data();
std::stringstream ss;
m_meta_data.m.lcf = bin2dec(data.mid(0, 8));
m_meta_data.m.mfid = bin2dec(data.mid(8, 8));
ss << (boost::format("%s: LCF: 0x%02x, MFID: 0x%02x") % duid_str() % m_meta_data.m.lcf % m_meta_data.m.mfid);
if (m_meta_data.m.lcf == 0x00)
{
m_meta_data.m0.emergency = data[16];
m_meta_data.m0.reserved = bin2dec(data.mid(17, 15));
m_meta_data.m0.tgid = bin2dec(data.mid(32, 16));
m_meta_data.m0.source = bin2dec(data.mid(48, 24));
ss << (boost::format(", Emergency: 0x%02x, Reserved: 0x%04x, TGID: 0x%04x, Source: 0x%06x") % m_meta_data.m0.emergency % m_meta_data.m0.reserved % m_meta_data.m0.tgid % m_meta_data.m0.source);
}
else if (m_meta_data.m.lcf == 0x03)
{
m_meta_data.m3.reserved = bin2dec(data.mid(16, 8));
m_meta_data.m3.destination = bin2dec(data.mid(24, 24));
m_meta_data.m3.source = bin2dec(data.mid(48, 24));
ss << (boost::format(", Reserved: 0x%02x, Destination: 0x%06x, Source: 0x%06x") % m_meta_data.m3.reserved % m_meta_data.m3.destination % m_meta_data.m3.source);
}
else
{
ss << " (unknown LCF)";
}
if (logging_enabled()) std::cerr << ss.str() << std::endl;
}
std::string
ldu1::snapshot() const
{
pickle p;
p.add("duid", duid_str());
p.add("mfid", lookup(m_meta_data.m.mfid, MFIDS, MFIDS_SZ));
if ((m_meta_data.m.lcf == 0x00) || (m_meta_data.m.lcf == 0x03))
p.add("source", (boost::format("0x%06x") % m_meta_data.m0.source).str());
if (m_meta_data.m.lcf == 0x00)
p.add("tgid", (boost::format("0x%04x") % m_meta_data.m0.tgid).str());
if (m_meta_data.m.lcf == 0x03)
p.add("dest", (boost::format("0x%06x") % m_meta_data.m3.destination).str());
return p.to_string();
}
ldu1::combined_meta_data
ldu1::meta_data() const
{
return m_meta_data;
}
string
ldu1::duid_str() const
{

View File

@ -24,45 +24,13 @@
#ifndef INCLUDED_LDU1_H
#define INCLUDED_LDU1_H
#include "ldu.h"
#include "voice_data_unit.h"
/**
* P25 Logical Data Unit 1.
*/
class ldu1 : public ldu
class ldu1 : public voice_data_unit
{
protected:
void do_correct_errors(bit_vector& frame_body);
public:
struct base_meta_data
{
unsigned char lcf;
unsigned char mfid;
unsigned int source;
unsigned short reserved;
};
struct meta_data_0 : public base_meta_data
{
bool emergency;
unsigned short tgid;
};
struct meta_data_3 : public base_meta_data
{
unsigned int destination;
};
union combined_meta_data
{
base_meta_data m;
meta_data_0 m0;
meta_data_3 m3;
} m_meta_data;
public:
/**
@ -82,10 +50,6 @@ public:
*/
std::string duid_str() const;
virtual std::string snapshot() const;
combined_meta_data meta_data() const;
};
#endif /* INCLUDED_LDU1_H */

View File

@ -23,18 +23,10 @@
#include "ldu2.h"
#include <itpp/base/vec.h>
#include <itpp/base/converters.h>
#include <boost/format.hpp>
#include "pickle.h"
#include "value_string.h"
using std::string;
ldu2::ldu2(const_bit_queue& frame_body) :
ldu(frame_body)
voice_data_unit(frame_body)
{
}
@ -47,43 +39,3 @@ ldu2::duid_str() const
{
return string("LDU2");
}
std::string
ldu2::snapshot() const
{
pickle p;
p.add("duid", duid_str());
std::stringstream ss;
ss << "0x";
for (size_t n = 0; n < m_crypto_state.mi.size(); ++n)
ss << (boost::format("%02x") % (int)m_crypto_state.mi[n]);
p.add("mi", ss.str());
p.add("algid", lookup(m_crypto_state.algid, ALGIDS, ALGIDS_SZ));
p.add("kid", (boost::format("0x%04x") % m_crypto_state.kid).str());
return p.to_string();
}
void
ldu2::do_correct_errors(bit_vector& frame_body)
{
ldu::do_correct_errors(frame_body);
if (!process_meta_data(frame_body))
return;
const itpp::bvec& data = raw_meta_data();
for (int i = 0; i < 72; i += 8)
{
m_crypto_state.mi[i/8] = bin2dec(data.mid(i, 8));
}
m_crypto_state.algid = bin2dec(data.mid(72, 8));
m_crypto_state.kid = bin2dec(data.mid(80, 16));
}
struct CryptoState
ldu2::crypto_state() const
{
return m_crypto_state;
}

View File

@ -24,22 +24,13 @@
#ifndef INCLUDED_LDU2_H
#define INCLUDED_LDU2_H
#include "ldu.h"
#include "crypto.h"
#include "voice_data_unit.h"
/**
* P25 Logical Data Unit 2.
*/
class ldu2 : public ldu, public crypto_state_provider
class ldu2 : public voice_data_unit
{
private:
struct CryptoState m_crypto_state;
protected:
void do_correct_errors(bit_vector& frame_body);
public:
/**
@ -58,10 +49,6 @@ public:
* Returns a string describing the Data Unit ID (DUID).
*/
std::string duid_str() const;
virtual std::string snapshot() const;
struct CryptoState crypto_state() const;
};
#endif /* INCLUDED_LDU2_H */

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 */

113
op25/gr-op25/lib/op25.i Normal file
View File

@ -0,0 +1,113 @@
/* -*- C++ -*- */
%feature("autodoc", "1");
%{
#include <stddef.h>
%}
%include "exception.i"
%import "gnuradio.i"
%{
#include "gnuradio/swig/gnuradio_swig_bug_workaround.h"
#include "op25_fsk4_demod_ff.h"
#include "op25_fsk4_slicer_fb.h"
#include "op25_decoder_bf.h"
#include "op25_pcap_source_b.h"
%}
// ----------------------------------------------------------------
/*
* This does some behind-the-scenes magic so we can
* access fsk4_square_ff from python as fsk4.square_ff
*/
GR_SWIG_BLOCK_MAGIC(op25, fsk4_demod_ff);
/*
* Publicly-accesible default constuctor function for op25_fsk4_demod_bf.
*/
op25_fsk4_demod_ff_sptr op25_make_fsk4_demod_ff(gr::msg_queue::sptr queue, float sample_rate, float symbol_rate);
class op25_fsk4_demod_ff : public gr_block
{
private:
op25_fsk4_demod_ff(gr::msg_queue::sptr queue, float sample_rate, float symbol_rate);
};
// ----------------------------------------------------------------
/*
* This does some behind-the-scenes magic so we can invoke
* op25_make_slicer_fb from python as op25.slicer_fbf.
*/
GR_SWIG_BLOCK_MAGIC(op25, fsk4_slicer_fb);
/*
* Publicly-accesible default constuctor function for op25_decoder_bf.
*/
op25_fsk4_slicer_fb_sptr op25_make_fsk4_slicer_fb(const std::vector<float> &slice_levels);
/*
* The op25_fsk4_slicer block. Takes a series of float samples and
* partitions them into dibit symbols according to the slices_levels
* provided to the constructor.
*/
class op25_fsk4_slicer_fb : public gr_sync_block
{
private:
op25_fsk4_slicer_fb (const std::vector<float> &slice_levels);
};
// ----------------------------------------------------------------
/*
* This does some behind-the-scenes magic so we can invoke
* op25_make_decoder_bsf from python as op25.decoder_bf.
*/
GR_SWIG_BLOCK_MAGIC(op25, decoder_bf);
/*
* Publicly-accesible default constuctor function for op25_decoder_bf.
*/
op25_decoder_bf_sptr op25_make_decoder_bf();
/**
* The op25_decoder_bf block. Accepts a stream of dibit symbols and
* produces an 8KS/s audio stream.
*/
class op25_decoder_bf : public gr_block
{
private:
op25_decoder_bf();
public:
const char *destination() const;
gr::msg_queue::sptr get_msgq() const;
void set_msgq(gr::msg_queue::sptr msgq);
};
// ----------------------------------------------------------------
/*
* This does some behind-the-scenes magic so we can invoke
* op25_make_pcap_source_b from python as op25.pcap_source_b.
*/
GR_SWIG_BLOCK_MAGIC(op25, pcap_source_b);
/*
* Publicly-accesible constuctor function for op25_pcap_source.
*/
op25_pcap_source_b_sptr op25_make_pcap_source_b(const char *path, float delay);
/*
* The op25_pcap_source block. Reads symbols from a tcpdump-formatted
* file and produces a stream of symbols of the appropriate size.
*/
class op25_pcap_source_b : public gr_sync_block
{
private:
op25_pcap_source_b(const char *path, float delay);
};
// ----------------------------------------------------------------

View File

@ -3,7 +3,6 @@
#include <cstddef>
#include <stdint.h>
#include <assert.h>
/*
* APCO Hamming(15,11,3) ecoder.
@ -184,18 +183,4 @@ hamming_15_decode(uint16_t& cw)
return errs;
}
static const uint32_t hmg1063EncTbl[64] = {
0, 12, 3, 15, 7, 11, 4, 8, 11, 7, 8, 4, 12, 0, 15, 3,
13, 1, 14, 2, 10, 6, 9, 5, 6, 10, 5, 9, 1, 13, 2, 14,
14, 2, 13, 1, 9, 5, 10, 6, 5, 9, 6, 10, 2, 14, 1, 13,
3, 15, 0, 12, 4, 8, 7, 11, 8, 4, 11, 7, 15, 3, 12, 0 };
static const uint32_t hmg1063DecTbl[16] = {
0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 8, 1, 16, 32, 0 };
static inline int hmg1063Dec (uint32_t Dat, uint32_t Par) {
assert ((Dat < 64) && (Par < 16));
return Dat ^ hmg1063DecTbl[hmg1063EncTbl[Dat] ^ Par];
}
#endif /* INCLUDED_OP25_HAMMING_H */

View File

@ -10,54 +10,9 @@
#include <vector>
typedef std::vector<bool> voice_codeword;
typedef std::vector<uint8_t> packed_codeword;
typedef const std::vector<bool> const_bit_vector;
typedef std::vector<bool> bit_vector;
static const uint16_t hdu_codeword_bits[658] = { // 329 symbols = 324 + 5 pad
114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 144, 145, 146, 147,
148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163,
164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
212, 213, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245,
246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261,
262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277,
278, 279, 280, 281, 282, 283, 284, 285, 288, 289, 290, 291, 292, 293, 294, 295,
296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311,
312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327,
328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343,
344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 360, 361,
362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377,
378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393,
394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409,
410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425,
426, 427, 428, 429, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443,
444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459,
460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475,
476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491,
492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 504, 505, 506, 507, 508, 509,
510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525,
526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541,
542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557,
558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573,
576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591,
592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607,
608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623,
624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639,
640, 641, 642, 643, 644, 645, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657,
658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673,
674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689,
690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705,
706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 720, 721, 722, 723,
724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739,
740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755,
756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771,
772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787,
788, 789 };
static const size_t nof_voice_codewords = 9, voice_codeword_sz = 144;
static const uint16_t imbe_ldu_NID_bits[] = {
@ -296,8 +251,8 @@ pngen23(uint32_t& Pr)
* \param u0-u7 Result output vectors
*/
static inline size_t
imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_t& u2, uint32_t& u3, uint32_t& u4, uint32_t& u5, uint32_t& u6, uint32_t& u7, uint32_t& E0, uint32_t& ET, bool bot_shift = true)
static inline void
imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_t& u2, uint32_t& u3, uint32_t& u4, uint32_t& u5, uint32_t& u6, uint32_t& u7, uint32_t& E0, uint32_t& ET)
{
ET = 0;
@ -339,30 +294,7 @@ imbe_header_decode(const voice_codeword& cw, uint32_t& u0, uint32_t& u1, uint32_
u6 = v6;
u7 = extract(cw, 137, 144);
if (bot_shift)
u7 <<= 1; /* so that bit0 is free (see note about BOT bit */
return errs;
}
/*
* Pack 88 bit IMBE parameters into uint8_t vector
*/
static inline void
imbe_pack(packed_codeword& cw, uint32_t u0, uint32_t u1, uint32_t u2, uint32_t u3, uint32_t u4, uint32_t u5, uint32_t u6, uint32_t u7)
{
cw.empty();
cw.push_back(u0 >> 4);
cw.push_back(((u0 & 0xf) << 4) + (u1 >> 8));
cw.push_back(u1 & 0xff);
cw.push_back(u2 >> 4);
cw.push_back(((u2 & 0xf) << 4) + (u3 >> 8));
cw.push_back(u3 & 0xff);
cw.push_back(u4 >> 3);
cw.push_back(((u4 & 0x7) << 5) + (u5 >> 6));
cw.push_back(((u5 & 0x3f) << 2) + (u6 >> 9));
cw.push_back(u6 >> 1);
cw.push_back(((u6 & 0x1) << 7) + (u7 >> 1));
u7 <<= 1; /* so that bit0 is free (see note about BOT bit */
}
/* APCO IMBE header encoder.

View File

@ -2,7 +2,6 @@
#define INCLUDED_SWAB_H
#include <stdint.h>
#include <algorithm>
/**
* Yank in[bits[0]..bits[bits_sz]) to out[where,where+bits_sz).

View File

@ -2,7 +2,6 @@
/*
* Copyright 2008 Steve Glass
* Copyright 2022 Matt Ames
*
* This file is part of OP25.
*
@ -54,40 +53,18 @@ const value_string ALGIDS[] = {
{ 0x02, "FIREFLY Type 1" },
{ 0x03, "MAYFLY Type 1" },
{ 0x04, "SAVILLE" },
{ 0x05, "Motorola Assigned - PADSTONE" },
{ 0x41, "BATON (Auto Odd)" },
/* Type III */
{ 0x80, "Unencrypted" },
{ 0x81, "DES-OFB, 56 bit key" },
{ 0x83, "3 key Triple DES, 168 bit key" },
{ 0x84, "AES-256-OFB" },
{ 0x85, "AES-128-ECB"},
{ 0x88, "AES-CBC"},
{ 0x89, "AES-128-OFB"},
/* Motorola proprietary - some of these have been observed over the air,
some have been taken from firmware dumps on various devices, others
have come from the TIA's FTP website while it was still public,
from document "ALGID Guide 2015-04-15.pdf", and others have been
have been worked out with a little bit of "guesswork" ;) */
{ 0x9F, "Motorola DES-XL 56-bit key" },
{ 0x80, "Plain" },
{ 0x81, "DES-OFB" },
{ 0x82, "2 key Triple DES" },
{ 0x83, "3 key Triple DES" },
{ 0x84, "AES-256" },
/* Motorola proprietary */
{ 0x9F, "Motorola DES-XL" },
{ 0xA0, "Motorola DVI-XL" },
{ 0xA1, "Motorola DVP-XL" },
{ 0xA2, "Motorola DVI-XL-SPFL"},
{ 0xA3, "Motorola HAYSTACK" },
{ 0xA4, "Motorola Assigned - Unknown" },
{ 0xA5, "Motorola Assigned - Unknown" },
{ 0xA6, "Motorola Assigned - Unknown" },
{ 0xA7, "Motorola Assigned - Unknown" },
{ 0xA8, "Motorola Assigned - Unknown" },
{ 0xA9, "Motorola Assigned - Unknown" },
{ 0xAA, "Motorola ADP (40 bit RC4)" },
{ 0xAB, "Motorola CFX-256" },
{ 0xAC, "Motorola GOST 28147-89 (RFC 5830)" },
{ 0xAD, "Motorola Assigned - LOCALIZED" },
{ 0xAE, "Motorola Assigned - Unknown" },
{ 0xAF, "Motorola AES+" },
{ 0xB0, "Motorola DVP"},
{ 0xD0, "Motorola LOCAL_BR"}
{ 0xAA, "Motorola ADP" },
};
const size_t ALGIDS_SZ = sizeof(ALGIDS) / sizeof(ALGIDS[0]);

View File

@ -24,325 +24,32 @@
#include "voice_data_unit.h"
#include "op25_imbe_frame.h"
#include <map>
#include <iostream>
#include <boost/format.hpp>
#include <stdio.h>
#include <itpp/base/vec.h>
#include <itpp/base/mat.h>
#include <itpp/base/binary.h>
#include <itpp/base/converters.h>
#include <sstream>
using namespace std;
static void vec_mod(itpp::ivec& vec, int modulus = 2)
voice_data_unit::~voice_data_unit()
{
for (int i = 0; i < vec.length(); ++i)
vec[i] = vec[i] % modulus;
}
class cyclic_16_8_5_syndromes
{
public:
typedef map<unsigned char,unsigned short> SyndromeTableMap;
const static itpp::imat cyclic_16_8_5;
private:
SyndromeTableMap m_syndrome_table;
public:
inline const SyndromeTableMap table() const
{
return m_syndrome_table;
}
cyclic_16_8_5_syndromes(bool generate_now = false)
{
if (generate_now)
generate();
}
int generate()
{
if (m_syndrome_table.empty() == false)
return -1;
// n=16, k=8
// E1
itpp::ivec v("1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0");
itpp::ivec r(cyclic_16_8_5 * v);
vec_mod(r);
itpp::bvec b(to_bvec(r));
unsigned char ch = (unsigned char)bin2dec(b);
itpp::bvec bV(to_bvec(v));
unsigned short us = (unsigned short)bin2dec(bV);
m_syndrome_table.insert(make_pair(ch, us));
// E2
for (int i = 0; i <= (16 - 2); ++i)
{
itpp::ivec v2(v);
v2[15-i] = 1;
r = cyclic_16_8_5 * v2;
bV = itpp::to_bvec(v2);
vec_mod(r);
b = itpp::to_bvec(r);
unsigned char ch = (unsigned char)itpp::bin2dec(b);
unsigned short us = (unsigned short)itpp::bin2dec(bV);
m_syndrome_table.insert(make_pair(ch, us));
}
// E3 - disabled: min.d = 5, t=floor(5/2)=2
/*for (int i = 0; i <= (16 - 2); ++i)
{
for (int j = 0; j < i; ++j)
{
ivec v3(v);
v3[15-i] = 1;
v3[15-j] = 1;
r = cyclic_16_8_5 * v3;
bV = to_bvec(v3);
vec_mod(r);
b = to_bvec(r);
unsigned char ch = (unsigned char)bin2dec(b);
unsigned short us = (unsigned short)bin2dec(bV);
m_syndrome_table.insert(make_pair(ch, us));
}
}*/
return m_syndrome_table.size();
}
};
const itpp::imat cyclic_16_8_5_syndromes::cyclic_16_8_5(
"0 0 1 1 1 1 0 0 1 0 0 0 0 0 0 0;"
"1 0 0 1 1 1 1 0 0 1 0 0 0 0 0 0;"
"0 1 0 0 1 1 1 1 0 0 1 0 0 0 0 0;"
"0 0 0 1 1 0 1 1 0 0 0 1 0 0 0 0;"
"1 0 1 1 0 0 0 1 0 0 0 0 1 0 0 0;"
"1 1 1 0 0 1 0 0 0 0 0 0 0 1 0 0;"
"1 1 1 1 0 0 1 0 0 0 0 0 0 0 1 0;"
"0 1 1 1 1 0 0 1 0 0 0 0 0 0 0 1"
);
static cyclic_16_8_5_syndromes g_cyclic_16_8_5_syndromes(true);
static int decode_cyclic_16_8_5(const itpp::ivec& vec, itpp::ivec& out)
{
itpp::ivec vc(cyclic_16_8_5_syndromes::cyclic_16_8_5 * vec);
vec_mod(vc);
itpp::bvec vb(to_bvec(vc));
unsigned char ch = (unsigned char)itpp::bin2dec(vb);
if (ch == 0x00)
return 0;
const cyclic_16_8_5_syndromes::SyndromeTableMap& syndrome_table = g_cyclic_16_8_5_syndromes.table();
cyclic_16_8_5_syndromes::SyndromeTableMap::const_iterator it = syndrome_table.find(ch);
int j = 0;
while (it == syndrome_table.end())
{
++j;
vc = itpp::concat(itpp::ivec("0 0 0 0 0 0 0 0"), vc); // Restore to 16 bits
vc.shift_left(vc[0]); // Rotate (s * x)
vc = cyclic_16_8_5_syndromes::cyclic_16_8_5 * vc;
vec_mod(vc);
vb = itpp::to_bvec(vc);
ch = (unsigned char)itpp::bin2dec(vb);
it = syndrome_table.find(ch);
if (j >= 15)
break;
}
if (it == syndrome_table.end())
{
return -1;
}
unsigned short us = it->second;
itpp::bvec es(itpp::dec2bin(16, us));
if (j > 0)
es.shift_right(es.mid(16-j, j)); // e
vb = itpp::to_bvec(vec);
vb -= es;
out = itpp::to_ivec(vb);
vc = cyclic_16_8_5_syndromes::cyclic_16_8_5 * out;
vec_mod(vc);
vb = itpp::to_bvec(vc);
if (itpp::bin2dec(vb) != 0x00)
{
return -1;
}
return 1;
}
static int decode_cyclic_16_8_5(itpp::ivec& vec)
{
return decode_cyclic_16_8_5(vec, vec);
}
////////////////////////////////////////////////////////////////////////////////////
voice_data_unit::voice_data_unit(const_bit_queue& frame_body) :
abstract_data_unit(frame_body),
d_lsdw(0),
d_lsdw_valid(false)
{
memset(d_lsd_byte_valid, 0x00, sizeof(d_lsd_byte_valid));
}
voice_data_unit::~voice_data_unit()
abstract_data_unit(frame_body)
{
}
void
voice_data_unit::do_correct_errors(bit_vector& frame_body)
{
if (logging_enabled()) fprintf(stderr, "\n");
d_lsd_byte_valid[0] = d_lsd_byte_valid[1] = false;
d_lsdw_valid = false;
itpp::ivec lsd1(16), lsd2(16);
for (int i = 0; i < 32; ++i)
{
int x = 1504 + i;
x = x + ((x / 70) * 2); // Adjust bit index for status
if (i < 16)
lsd1[i] = frame_body[x];
else
lsd2[i-16] = frame_body[x];
}
int iDecode1 = decode_cyclic_16_8_5(lsd1);
if (iDecode1 >= 0)
{
d_lsd_byte_valid[0] = true;
}
else if (iDecode1 == -1)
{
// Error
}
int iDecode2 = decode_cyclic_16_8_5(lsd2);
if (iDecode2 >= 0)
{
d_lsd_byte_valid[1] = true;
}
else
{
// Error
}
d_lsdw = 0;
for (int i = 0; i < 8; ++i)
d_lsdw = d_lsdw | (lsd1[i] << (7 - i)); // Little-endian byte swap
for (int i = 0; i < 8; ++i)
d_lsdw = d_lsdw | (lsd2[i] << (15 - i)); // Little-endian byte swap
if (d_lsd_byte_valid[0] && d_lsd_byte_valid[1])
d_lsdw_valid = true;
}
uint16_t
voice_data_unit::lsdw() const
{
return d_lsdw;
}
bool
voice_data_unit::lsdw_valid() const
{
return d_lsdw_valid;
}
static void extract(unsigned int u, size_t n, std::vector<bool>& out)
{
for (size_t i = 0; i < n; ++i)
out.push_back(((u & (1 << (n-1-i))) != 0));
}
void
voice_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod)
voice_data_unit::do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe)
{
voice_codeword cw(voice_codeword_sz);
for(size_t i = 0; i < nof_voice_codewords; ++i) {
imbe_deinterleave(frame_body, cw, i);
unsigned int u0 = 0;
unsigned int u1,u2,u3,u4,u5,u6,u7;
unsigned int E0 = 0;
unsigned int ET = 0;
// PN/Hamming/Golay - etc.
size_t errs = imbe_header_decode(cw, u0, u1, u2, u3, u4, u5, u6, u7, E0, ET, false); // E0 & ET are not used, and are always returned as 0
crypto_algorithm::sptr algorithm;
if (crypto_mod)
algorithm = crypto_mod->current_algorithm();
if (algorithm)
{
if (i == 8)
{
d_lsdw ^= algorithm->generate(16); // LSDW
}
u0 ^= (int)algorithm->generate(12);
u1 ^= (int)algorithm->generate(12);
u2 ^= (int)algorithm->generate(12);
u3 ^= (int)algorithm->generate(12);
u4 ^= (int)algorithm->generate(11);
u5 ^= (int)algorithm->generate(11);
u6 ^= (int)algorithm->generate(11);
u7 ^= (int)algorithm->generate(7);
imbe_header_encode(cw, u0, u1, u2, u3, u4, u5, u6, (u7 << 1));
}
std::vector<bool> cw_raw;
extract(u0, 12, cw_raw);
extract(u1, 12, cw_raw);
extract(u2, 12, cw_raw);
extract(u3, 12, cw_raw);
extract(u4, 11, cw_raw);
extract(u5, 11, cw_raw);
extract(u6, 11, cw_raw);
extract(u7, 7, cw_raw);
const int cw_octets = 11;
std::vector<uint8_t> cw_vector(cw_octets);
extract(cw_raw, 0, (cw_octets * 8), &cw_vector[0]);
if (logging_enabled())
{
std::stringstream ss;
for (size_t n = 0; n < cw_vector.size(); ++n)
{
ss << (boost::format("%02x") % (int)cw_vector[n]);
if (n < (cw_vector.size() - 1))
ss << " ";
}
if (errs > 0)
ss << (boost::format(" (%llu errors)") % errs);
std:cerr << (boost::format("%s:\t%s") % duid_str() % ss.str()) << std::endl;
}
imbe.decode(cw);
}
if (logging_enabled()) fprintf(stderr, "%s: LSDW: 0x%04x, %s\n", duid_str().c_str(), d_lsdw, (d_lsdw_valid ? "valid" : "invalid"));
}
uint16_t

View File

@ -38,17 +38,6 @@ public:
*/
virtual ~voice_data_unit();
const static int LSD_BYTE_COUNT=2;
private:
union {
uint8_t d_lsd_byte[LSD_BYTE_COUNT];
uint16_t d_lsdw;
};
bool d_lsdw_valid;
bool d_lsd_byte_valid[LSD_BYTE_COUNT];
protected:
/**
@ -74,7 +63,7 @@ protected:
* \param frame_body The const_bit_vector to decode.
* \param imbe The imbe_decoder to use to generate the audio.
*/
virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe, crypto_module::sptr crypto_mod);
virtual void do_decode_audio(const_bit_vector& frame_body, imbe_decoder& imbe);
/**
* Returns the expected size (in bits) of this data_unit. For
@ -85,10 +74,6 @@ protected:
*/
virtual uint16_t frame_size_max() const;
virtual uint16_t lsdw() const;
virtual bool lsdw_valid() const;
};
#endif /* INCLUDED_VOICE_DATA_UNIT_H */

View File

@ -28,10 +28,9 @@
using namespace std;
voice_du_handler::voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder, crypto_module::sptr crypto_mod) :
voice_du_handler::voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder) :
data_unit_handler(next),
d_decoder(decoder),
d_crypto_mod(crypto_mod)
d_decoder(decoder)
{
}
@ -43,6 +42,6 @@ voice_du_handler::~voice_du_handler()
void
voice_du_handler::handle(data_unit_sptr du)
{
du->decode_audio(*d_decoder, d_crypto_mod);
du->decode_audio(*d_decoder);
data_unit_handler::handle(du);
}

View File

@ -26,7 +26,6 @@
#include "data_unit_handler.h"
#include "imbe_decoder.h"
#include "crypto.h"
#include <boost/noncopyable.hpp>
@ -44,7 +43,7 @@ public:
* \param next The next data_unit_handler in the chain.
* \param decoder An imbe_decoder_sptr to the IMBE decoder to use.
*/
voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder, crypto_module::sptr crypto_mod = crypto_module::sptr()); // TODO: Add capability to decoder_ff (remove default argument)
voice_du_handler(data_unit_handler_sptr next, imbe_decoder_sptr decoder);
/**
* voice_du_handler virtual destructor.
@ -65,8 +64,6 @@ private:
*/
imbe_decoder_sptr d_decoder;
crypto_module::sptr d_crypto_mod;
};
#endif /* INCLUDED_VOICE_DU_HANDLER_H */

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

@ -1,7 +1,5 @@
/* -*- c++ -*- */
%include "pycontainer.swg"
#define OP25_API
%include "gnuradio.i" // the common stuff
@ -17,11 +15,6 @@
#include "op25/pcap_source_b.h"
%}
%template(key_type) std::vector<unsigned char>;
// This causes SWIG to segfault
//%template(key_map_type) std::map<uint16_t,key_type >;
%template(key_map_type) std::map<uint16_t,std::vector<unsigned char> >;
%include "op25/fsk4_demod_ff.h"
GR_SWIG_BLOCK_MAGIC2(op25, fsk4_demod_ff);
%include "op25/fsk4_slicer_fb.h"

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,31 @@ 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(GnuradioRuntime)
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")
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(GR_REQUIRED_COMPONENTS RUNTIME BLOCKS FILTER PMT)
find_package(Gnuradio)
endif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
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

@ -1,243 +0,0 @@
April, 2017
===========
This file contains notes on the new version OP25 receiver (rx.py) which
replaces the prior version scope.py. The primary differences are:
* The dependency on WX is completely removed. By default OP25 runs in
a console window in text-only mode.
* An optional real-time plot can be selected when launching rx.py:
contellation, datascope (eye pattern), or symbol trace.
* cqpsk versus fsk4 mode is now selected as a command line parameter.
* most rx.py command-line parameters are compatible with scope.py.
* reduced CPU consumption, as the frame assembler block now runs as a
sink-only GR block.
* dependency on 14.04 is completely removed. Should now run in later
ubuntu and fedora versions with only minor changes (not yet tested).
ADDITIONAL REQUIRED PACKAGES
============================
sudo apt-get install gnuplot-x11
EXAMPLE COMMAND LINE
====================
./rx.py --args 'rtl' --gains 'lna:49' -f 456.7e6 -T tsys.tsv -q -1 -S 1000000 -P symbol -o 50000 -w 2> stderr.2
Running stderr to a file (e.g., "2> stderr.2") is recommended to avoid
screen misprinting.
NOTE: For phase1 voice the "-V" option is not used. Instead the
"-w" option is used (see AUDIO SERVER section, below). For P25 phase 2/TDMA,
the "-2" option is required in addition to the "-w" option.
TERMINAL OPERATION
==================
After starting rx.py if plotting is in use a separate gnuplot window
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
There are also two experimental commands (should not be used in -T mode)
f - manually change frequencies
t - if currently tuned to a CC, autostart scanning talkgroups
The "t" command allows trunk tracking without setting up a trunking TSV
file. However running with the -T <filename> command line option is
preferred as that allows use of white/black lists and talkgroup tags files.
If the terminal window freezes there may have been a crash. Press Ctrl-Z
to suspend the program and examine stderr.2 for error messages. If there
is a traceback please report the full traceback (and the command line) to
the mail list.
REMOTE TERMINAL
===============
Adding (for example) "-l 56111" to the rx.py command starts rx.py but does
not attach a curses terminal. Instead the program runs as normal in the
foreground (hit CTRL-C to end rx.py as desired). To connect to a running
instance of rx.py, (in this example)
./terminal.py 127.0.0.1 56111
NOTE: rx.py and terminal.py need not run on the same machine. The machine
where terminal.py is running need not have an SDR device directly attached;
but GNU Radio (and OP25) must be available.
WARNING: there is no security or encryption on the UDP port.
EXTERNAL UDP AUDIO SERVER
=========================
Starting rx.py with the "-w -W host" options directs udp audio data to
be sent over the network to the specified remote host. It can then be
received and played back with either of the following methods:
1. Execute ./audio.sh on a remote machine equipped with python2.7,
libasound.so.2 and the sockaudio.py file.
-or-
2. Execute the command:
nc -kluvw 1 127.0.0.1 23456 | aplay -c1 -f S16_LE -r 8000
NOTE: audio underruns are to be expected when using nc | aplay as the
pcm stream is interrupted every time a radio transmission ends. The
sockaudio player is designed to handle this more gracefully, and generally
only underruns due to high cpu utilization or reception/decoding errors.
INTERNAL AUDIO SERVER
=====================
Starting rx.py with the "-U" command line option enables an internal udp
audio server which will play received audio through the default ALSA
device. Optionally you may specify which ALSA device to use by setting
the "-O audio_out" option along with "-U".
As of this writing (Aug 2017) it is still necessary to specify the "-w"
(wireshark) option if using either the internal or external audio server.
PLOT MODES
==========
Three types of plotting are currently implemented, via the -P parameter:
* constellation
* datascope
* symbol
The symbol mode is allowed both in fsk4 and cqpsk modes. The datascope
mode works only with fsk4 demod mode (-D fsk4). The constellation mode
only works when the cqpsk demod mode is selected (or defaulted).
A couple of notes specific to plot mode:
1. At program startup time the gnuplot window is given the focus after
it opens. Before you can enter terminal commands you need to click on
the terminal window once to make it the active window.
2. In some cases the gnuplot window is displayed on top of the terminal
window used by OP25. If so it may be necessary to move one or the other
of the two windows.
COMMAND LINE OPTIONS
====================
Usage: rx.py [options]
Options:
-h, --help show this help message and exit
--args=ARGS device args
--antenna=ANTENNA select antenna
-a, --audio use direct audio input
-A, --audio-if soundcard IF mode (use --calibration to set IF freq)
-I AUDIO_INPUT, --audio-input=AUDIO_INPUT
pcm input device name. E.g., hw:0,0 or /dev/dsp
-i INPUT, --input=INPUT
input file name
-b Hz, --excess-bw=Hz
for RRC filter
-c Hz, --calibration=Hz
USRP offset or audio IF frequency
-C Hz, --costas-alpha=Hz
value of alpha for Costas loop
-D DEMOD_TYPE, --demod-type=DEMOD_TYPE
cqpsk | fsk4
-P PLOT_MODE, --plot-mode=PLOT_MODE
constellation | symbol | datascope
-f Hz, --frequency=Hz
USRP center frequency
-F IFILE, --ifile=IFILE
read input from complex capture file
-H HAMLIB_MODEL, --hamlib-model=HAMLIB_MODEL
specify model for hamlib
-s SEEK, --seek=SEEK ifile seek in K
-l TERMINAL_TYPE, --terminal-type=TERMINAL_TYPE
'curses' or udp port or 'http:host:port'
-L LOGFILE_WORKERS, --logfile-workers=LOGFILE_WORKERS
number of demodulators to instantiate
-S SAMPLE_RATE, --sample-rate=SAMPLE_RATE
source samp rate
-t, --tone-detect use experimental tone detect algorithm
-T TRUNK_CONF_FILE, --trunk-conf-file=TRUNK_CONF_FILE
trunking config file name
-v VERBOSITY, --verbosity=VERBOSITY
message debug level
-V, --vocoder voice codec
-o Hz, --offset=Hz tuning offset frequency [to circumvent DC offset]
-p, --pause block on startup
-w, --wireshark output data to Wireshark
-W WIRESHARK_HOST, --wireshark-host=WIRESHARK_HOST
Wireshark host
-r RAW_SYMBOLS, --raw-symbols=RAW_SYMBOLS
dump decoded symbols to file
-R RX_SUBDEV_SPEC, --rx-subdev-spec=RX_SUBDEV_SPEC
select USRP Rx side A or B (default=A)
-g GAIN, --gain=GAIN set USRP gain in dB (default is midpoint) or set audio
gain
-G GAIN_MU, --gain-mu=GAIN_MU
gardner gain
-N GAINS, --gains=GAINS
gain settings
-O AUDIO_OUTPUT, --audio-output=AUDIO_OUTPUT
audio output device name
-q FREQ_CORR, --freq-corr=FREQ_CORR
frequency correction
-2, --phase2-tdma enable phase2 tdma decode
-Z DECIM_AMT, --decim-amt=DECIM_AMT
spectrum decimation
HTTP CONSOLE
============
New as of Jan. 2018, the OP25 dashboard is accessible to any Web browser over
HTTP. Include the option "-l http:<host>:<port>" when starting the rx.py app,
where <host> is either "127.0.0.1" to limit access from only this host, or
"0.0.0.0" if HTTP access from anywhere is to be allowed*. After rx.py has
started it begins listening on the specified port for incoming connections.
Once connected the status page should automatically update to show trunking
system status, frequency list, adjacent sites, and other data.
Example: you have started rx.py with the option "-l http:127.0.0.1:8080".
To connect, set your web browser URL to "http://127.0.0.1:8080".
If one or more plot modes has been selected using the "-P" option you may
view them by clicking the "PLOT" button. The plots are updated approx.
every five seconds. Click "STATUS" to return to the main status page.
*WARNING*: there is no security or encryption. Be careful when using "0.0.0.0"
as the listening address since anyone with access to the network can connect.
NOTE: the python-pyramid package is required when using this option. It can
be installed by running
sudo apt-get install python-pyramid
MULTI-RECEIVER
==============
The multi_rx.py app allows an arbitrary number of SDR devices and channels
to be defined. Each channel may have one or more plot windows attached.
Configuration is achieved via a json file (see cfg.json for an example).
In this version, channels are automatically assigned to the first device
found whose frequency span includes the selected frequency.
As of this writing (winter, 2017), neither trunking nor P25 P2/TDMA are
supported in multi_rx.py. The rx.py app should be used for P25 trunking,
for either P1/FDMA or P2/TDMA.
Below is a summary of the major config file keys:
demod_type: 'cqpsk' for qpsk p25 only; 'fsk4' for ysf/dstar/dmr/fsk4 p25
filter_type: 'rc' for p25; 'rrc' for dmr and ysf; 'gmsk' for d-star
plot: 'fft', 'constellation', 'datascope', 'symbol'
[if more than one plot desired, provide a comma-separated list]
destination: 'udp://host:port' or 'file://<filename>'
name: arbitrary string used to identify channels and devices
bug: 'fft' and 'constellation' currently mutually exclusive
bug: 'gmsk' needs work
Note: DMR audio for the second time slot is sent on the specified port number
plus two. In the example 'udp://127.0.0.1:56122', audio for the first slot
would use 56122; and 56124 for the second.
The command line options for multi_rx:
Usage: multi_rx.py [options]
Options:
-h, --help show this help message and exit
-c CONFIG_FILE, --config-file=CONFIG_FILE
specify config file name
-v VERBOSITY, --verbosity=VERBOSITY
message debug level
-p, --pause block on startup

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,55 +0,0 @@
#!/usr/bin/env python3
# Copyright 2017, 2018 Graham Norbury
#
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI
#
# 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.
import signal
import sys
import time
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()
sys.exit(0)
parser = OptionParser()
parser.add_option("-O", "--audio-output", type="string", default="default", help="audio output device name")
parser.add_option("-H", "--host-ip", type="string", default="0.0.0.0", help="IP address to bind to")
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)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
audio_handler.run()

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,49 +0,0 @@
{
"channels": [
{
"demod_type": "cqpsk",
"destination": "udp://127.0.0.1:56120",
"excess_bw": 0.2,
"filter_type": "rc",
"frequency": 460412500,
"if_rate": 24000,
"name": "p25",
"plot": "symbol",
"symbol_rate": 4800
},
{
"demod_type": "fsk4",
"destination": "file:///tmp/out1.raw",
"excess_bw": 0.2,
"filter_type": "rrc",
"frequency": 460500000,
"if_rate": 24000,
"name": "ysf",
"plot": "datascope",
"symbol_rate": 4800
},
{
"demod_type": "fsk4",
"destination": "udp://127.0.0.1:56122",
"excess_bw": 0.2,
"filter_type": "rrc",
"frequency": 460050000,
"if_rate": 24000,
"name": "dmr",
"plot": "symbol",
"symbol_rate": 4800
}
],
"devices": [
{
"args": "rtl=0",
"frequency": 460100000,
"gains": "lna:49",
"name": "rtl0",
"offset": 0,
"ppm": 38,
"rate": 1000000,
"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

@ -1,806 +0,0 @@
#!/usr/bin/env python
# Copyright 2011, 2012, 2013, 2014, 2015 Max H. Parke KA1RBI
#
# This file is part of OP25
#
# OP25 is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# OP25 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU General Public License
# along with OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
import sys
import os
import time
import subprocess
import json
import threading
import glob
from gnuradio import gr, eng_notation
from gnuradio import blocks, audio
from gnuradio.eng_option import eng_option
import numpy as np
from gnuradio import gr
from math import pi, sin, cos
_def_debug = 0
_def_sps = 10
_def_cpm_mode = 'cpm'
GNUPLOT = '/usr/bin/gnuplot'
FFT_AVG = 0.25
MIX_AVG = 0.15
BAL_AVG = 0.05
FFT_BINS = 512
def degrees(r):
d = 360 * r / (2*pi)
while d <0:
d += 360
while d > 360:
d -= 360
return d
def limit(a,lim):
if 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
self.sps = sps
self.center_freq = 0.0
self.relative_freq = 0.0
self.offset_freq = 0.0
self.width = None
self.ffts = ()
self.freqs = ()
self.avg_pwr = np.zeros(FFT_BINS)
self.avg_sum_pwr = 0.0
self.buf = np.array([])
self.plot_count = 0
self.last_plot = 0
self.plot_interval = None
self.sequence = 0
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()
def attach_gp(self):
args = (GNUPLOT, '-noraise')
exe = GNUPLOT
self.gp = subprocess.Popen(args, executable=exe, stdin=subprocess.PIPE)
def kill(self):
try:
self.gp.stdin.close() # closing pipe should cause subprocess to exit
except IOError:
pass
sleep_count = 0
while True: # wait politely, but only for so long
self.gp.poll()
if self.gp.returncode is not None:
break
time.sleep(0.1)
if self.gp.returncode is not None:
break
sleep_count += 1
if (sleep_count & 1) == 0:
self.gp.kill()
if sleep_count >= 3:
break
def set_interval(self, v):
self.plot_interval = v
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)]))
if len(self.buf) < BUFSZ:
return consumed
self.plot_count += 1
if mode == 'eye' and self.plot_count % 20 != 0:
self.buf = np.array([])
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:
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)
s += 'e\n'
plots.append('"-" with linespoints')
samples = samples[self.sps:]
self.buf = np.array([])
# FFT processing needs to be completed to maintain the weighted average buckets
# regardless of whether we actually produce a new plot or not.
if self.plot_interval and self.last_plot + self.plot_interval > time.time():
return consumed
self.last_plot = time.time()
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)
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)
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)
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+= 'set size square\n'
h+= 'set xrange [-1:1]\n'
h+= 'set yrange [-1:1]\n'
h += 'unset border\n'
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 style line 10 lt 1 lc rgb 0x404040 lw 0.1\n'
h += 'set grid polar 45\n'
h += 'set grid ls 10\n'
h += 'set xtics axis\n'
h += 'set ytics axis\n'
h += 'set xtics scale 0\n'
h += 'set xtics ("" 0.2, "" 0.4, "" 0.6, "" 0.8, "" 1)\n'
h += 'set ytics 0, 0.2, 1\n'
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)
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)
elif mode == 'symbol':
h+= background
h+= 'set yrange [-4:4]\n'
h+= 'set title "Symbol %s" %s\n' % (self.title, label_color)
elif mode == 'fft' or mode == 'mixer':
h+= background
h+= 'unset arrow; unset title\n'
h+= 'set xrange [%f:%f]\n' % (self.freqs[0], self.freqs[len(self.freqs)-1])
h+= 'set xlabel "Frequency"\n'
h+= 'set ylabel "Power(dB)"\n'
h+= 'set grid\n'
h+= 'set 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)
else: # fft
h+= 'set title "Spectrum %s" %s\n' % (self.title, label_color)
if self.center_freq:
arrow_pos = (self.center_freq - self.relative_freq) / 1e6
h+= 'set arrow from %f, graph 0 to %f, graph 1 nohead\n' % (arrow_pos, arrow_pos)
h+= 'set title "Spectrum: tuned to %f Mhz" %s\n' % (arrow_pos, label_color)
elif mode == 'fftf':
h+= 'set yrange [-1:1.2]\n'
h+= 'set title "fftf"\n'
elif mode == 'float':
h+= background
h+= 'set yrange [-2:2]\n'
h+= 'set title "Oscilloscope %s" %s\n' % (self.title, label_color)
elif mode == 'correlation':
h+= background
title = 'Correlation'
if self.title:
title = self.title
h+= 'set yrange [-1.1:1.1]\n'
h+= 'set title "%s" %s\n' % (title, label_color)
if self.output_dir:
s += 'set output\n' ## flush output png
dat = '%s%splot %s %s\n%s' % (h0, h, ','.join(plots), plot_color, s)
if self.logfile is not None:
with open(self.logfile, 'a') as fd:
fd.write(dat)
if sys.version[0] != '2':
dat = bytes(dat, 'utf8')
self.gp.poll()
if self.gp.returncode is None: # make sure gnuplot is still running
try:
rc = self.gp.stdin.write(dat)
except (IOError, ValueError):
pass
try:
self.gp.stdin.flush()
except (IOError, ValueError):
pass
if filename:
self.filename = filename
return consumed
def set_center_freq(self, f):
self.center_freq = f
def set_relative_freq(self, f):
self.relative_freq = f
def set_offset(self, f):
self.offset_freq = f
def set_width(self, w):
self.width = w
def set_logfile(self, logfile=None):
self.logfile = logfile
def set_title(self, title):
self.title = title
class eye_sink_f(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug, sps = _def_sps):
gr.sync_block.__init__(self,
name="eye_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.sps = sps
self.gnuplot = wrap_gp(sps=self.sps)
def work(self, input_items, output_items):
in0 = input_items[0]
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()
class constellation_sink_c(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="constellation_sink_c",
in_sig=[np.complex64],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
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')
return len(input_items[0])
def kill(self):
self.gnuplot.kill()
class fft_sink_c(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="fft_sink_c",
in_sig=[np.complex64],
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='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)
def set_relative_freq(self, f):
self.gnuplot.set_relative_freq(f)
def set_offset(self, f):
self.gnuplot.set_offset(f)
def set_width(self, w):
self.gnuplot.set_width(w)
class mixer_sink_c(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="mixer_sink_c",
in_sig=[np.complex64],
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 >= 10:
self.skip = 0
in0 = input_items[0]
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()
class symbol_sink_f(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="symbol_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
def work(self, input_items, output_items):
in0 = input_items[0]
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()
class float_sink_f(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="float_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
def work(self, input_items, output_items):
in0 = input_items[0]
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,589 +0,0 @@
#!/usr/bin/env python3
# Copyright 2017, 2018, 2019, 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.
import sys
import os
import time
import re
import json
import socket
import traceback
import threading
import glob
import subprocess
import zmq
import op25
from gnuradio import gr
from waitress.server import create_server
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
my_port = None
my_backend = None
CFG_DIR = '../www/config/'
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()
if environ['PATH_INFO'] == '/':
filename = 'index.html'
else:
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())
content_type = 'text/plain'
output = status
else:
output = open(pathname, 'rb').read()
content_type = content_types[suf]
status = '200 OK'
return status, content_type, output
def valid_tsv(filename):
if not os.access(filename, os.R_OK):
return False
line = open(filename).readline()
for word in 'Sysname Offset NAC Modulation TGID Whitelist Blacklist'.split():
if word not in line:
return False
return True
def tsv_config(filename):
DEFAULT_CFG = '../www/config/default.json'
filename = '%s%s' % (TSV_DIR, filename)
filename = filename.replace('[TSV]', '.tsv')
if not valid_tsv(filename):
return None
cfg = make_config(load_tsv(filename))
default_cfg = json.loads(open(DEFAULT_CFG).read())
result = default_cfg
channels = [ {'active': True,
'blacklist': cfg[nac]['blacklist'],
'whitelist': cfg[nac]['whitelist'],
'cclist': cfg[nac]['cclist'],
'demod_type': 'cqpsk',
'destination': 'udp://127.0.0.1:23456',
'filter_type': 'rc',
'frequency': 500000000,
'if_rate': 24000,
'nac': nac,
'name': cfg[nac]['sysname'],
'phase2_tdma': False,
'plot': "",
'tgids': cfg[nac]['tgid_map'],
'trunked': True
}
for nac in cfg.keys() ]
result['channels'] = channels
return {'json_type':'config_data', 'data': result}
def do_request(d):
global my_backend
if d['command'].startswith('rx-'):
msg = gr.message().make_from_string(json.dumps(d), -2, 0, 0)
if not my_backend.input_q.full_p():
my_backend.input_q.insert_tail(msg)
return None
elif d['command'] == 'config-load':
if '[TSV]' in d['data']:
return tsv_config(d['data'])
filename = '%s%s.json' % (CFG_DIR, d['data'])
if not os.access(filename, os.R_OK):
return None
js_msg = json.loads(open(filename).read())
return {'json_type':'config_data', 'data': js_msg}
elif d['command'] == 'config-list':
files = glob.glob('%s*.json' % CFG_DIR)
files = [x.replace('.json', '') for x in files]
files = [x.replace(CFG_DIR, '') for x in files]
if d['data'] == 'tsv':
tsvfiles = glob.glob('%s*.tsv' % TSV_DIR)
tsvfiles = [x for x in tsvfiles if valid_tsv(x)]
tsvfiles = [x.replace('.tsv', '[TSV]') for x in tsvfiles]
tsvfiles = [x.replace(TSV_DIR, '') for x in tsvfiles]
files += tsvfiles
return {'json_type':'config_list', 'data': files}
elif d['command'] == 'config-save':
name = d['data']['name']
if '..' in name or '.json' in name or '/' in name:
return None
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:
sys.stderr.write('post_req: error processing input: %s:\n' % (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:
resp_msg.append(resp)
continue
if d['command'].startswith('settings-'):
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
else:
msg = gr.message().make_from_string(str(d['command']), -2, d['data'], 0)
if my_output_q.full_p():
my_output_q.delete_head_nowait() # ignores result
if not my_output_q.full_p():
my_output_q.insert_tail(msg)
time.sleep(0.2)
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':
status, content_type, output = static_file(environ, start_response)
elif environ['REQUEST_METHOD'] == 'POST':
postdata = environ['wsgi.input'].read()
status, content_type, output = post_req(environ, start_response, postdata)
else:
status = '200 OK'
content_type = 'text/plain'
output = status
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):
failed = False
try:
result = http_request(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]
return result
def process_qmsg(msg):
if my_recv_q.full_p():
my_recv_q.delete_head_nowait() # ignores result
if my_recv_q.full_p():
return
my_recv_q.insert_tail(msg)
class http_server(object):
def __init__(self, input_q, output_q, endpoint, **kwds):
global my_input_q, my_output_q, my_recv_q, my_port
host, port = endpoint.split(':')
if my_port is not None:
raise AssertionError('this server is already active on port %s' % my_port)
my_input_q = input_q
my_output_q = output_q
my_port = int(port)
my_recv_q = gr.msg_queue(10)
SEND_BYTES = 1024
NTHREADS = 10 # TODO: make #threads a function of #plots ?
self.server = create_server(application, host=host, port=my_port, send_bytes=SEND_BYTES, expose_tracebacks=True, threads=NTHREADS)
def run(self):
self.server.run()
class 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()
self.callback(msg)
class Backend(threading.Thread):
def __init__(self, options, input_q, output_q, init_config=None, **kwds):
threading.Thread.__init__ (self, **kwds)
self.setDaemon(1)
self.keep_running = True
self.rx_options = None
self.input_q = input_q
self.output_q = output_q
self.verbosity = options.verbosity
self.zmq_context = zmq.Context()
self.zmq_port = options.zmq_port
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_pub = self.zmq_context.socket(zmq.PUB)
self.zmq_pub.sndhwm = 5
self.zmq_pub.bind('tcp://*:%d' % (self.zmq_port+1))
self.start()
self.subproc = None
self.msg = None
self.q_watcher = queue_watcher(self.input_q, self.process_msg)
if init_config:
d = {'command': 'rx-start', 'data': init_config}
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
self.input_q.insert_tail(msg)
def publish(self, msg):
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}))
def check_subproc(self): # return True if subprocess is active
if not self.subproc:
return False
rc = self.subproc.poll()
if rc is None:
return True
else:
self.subproc.wait()
self.subproc = None
return False
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):
self.msg = 'no suitable backend found for this configuration'
return None
if not trunked_ct:
self.backend = '%s/%s' % (os.getcwd(), 'multi_rx.py')
opts = [py_exe, 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'
}
self.backend = '%s/%s' % (os.getcwd(), 'rx.py')
opts = [py_exe, self.backend]
for k in [ x for x in dir(options) if not x.startswith('_') ]:
kw = k.replace('_', '-')
val = getattr(options, k)
if kw not in types.keys():
self.msg = 'make_command: unknown option: %s %s type %s' % (k, val, type(val))
return None
elif types[kw] == 'str':
if val:
opts.append('--%s' % kw)
opts.append('%s' % (val))
elif types[kw] == 'float':
opts.append('--%s' % kw)
if val:
opts.append('%f' % (val))
else:
opts.append('%f' % (0))
elif types[kw] == 'int':
opts.append('--%s' % kw)
if val:
opts.append('%d' % (val))
else:
opts.append('%d' % (0))
elif types[kw] == 'bool':
if val:
opts.append('--%s' % kw)
else:
self.msg = 'make_command: unknown2 option: %s %s type %s' % (k, val, type(val))
return None
return opts
msg = json.loads(msg.to_string())
if msg['command'] == 'rx-start':
if self.check_subproc():
self.msg = 'start command failed: subprocess pid %d already active' % self.subproc.pid
return
options = rx_options(msg['data'])
if getattr(options, '_js_config', None) is None:
self.msg = 'start command failed: rx_options: unable to initialize config=%s' % (msg['data'])
return
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':
if not self.check_subproc():
self.msg = 'stop command failed: subprocess not active'
return
if msg['data'] == 'kill':
self.subproc.kill()
else:
self.subproc.terminate()
elif msg['command'] == 'rx-state':
d = {}
if self.check_subproc():
d['rx-state'] = 'subprocess pid %d active' % self.subproc.pid
else:
d['rx-state'] = 'subprocess not active, last msg: %s' % self.msg
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
if not self.output_q.full_p():
self.output_q.insert_tail(msg)
def run(self):
while self.keep_running:
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)
class rx_options(object):
def __init__(self, name):
def map_name(k):
return k.replace('-', '_')
filename = '%s%s.json' % (CFG_DIR, name)
if not os.access(filename, os.R_OK):
sys.stderr.write('unable to access config file %s\n' % (filename))
return
config = byteify(json.loads(open(filename).read()))
dev = [x for x in config['devices'] if x['active']][0]
if not dev:
return
chan = [x for x in config['channels'] if x['active']][0]
if not chan:
return
options = object()
for k in config['backend-rx'].keys():
setattr(self, map_name(k), config['backend-rx'][k])
for k in 'args frequency gains offset'.split():
setattr(self, k, dev[k])
self.demod_type = chan['demod_type']
self.freq_corr = dev['ppm']
self.sample_rate = dev['rate']
self.plot_mode = chan['plot']
self.phase2_tdma = chan['phase2_tdma']
self.trunk_conf_file = filename
self._js_config = config
def http_main():
global my_backend
# command line argument parsing
parser = OptionParser()
parser.add_option("-c", "--config", type="string", default=None, help="config json name, without prefix/suffix")
parser.add_option("-e", "--endpoint", type="string", default="127.0.0.1:8080", help="address:port to listen on (use addr 0.0.0.0 to enable external clients)")
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("-z", "--zmq-port", type="int", default=25000, help="backend sub port")
(options, args) = parser.parse_args()
# wait for gdb
if options.pause:
print ('Ready for GDB to attach (pid = %d)' % (os.getpid(),))
raw_input("Press 'Enter' to continue...")
input_q = gr.msg_queue(20)
output_q = gr.msg_queue(20)
backend_input_q = gr.msg_queue(20)
backend_output_q = gr.msg_queue(20)
my_backend = Backend(options, backend_input_q, backend_output_q, init_config=options.config)
server = http_server(input_q, output_q, options.endpoint)
q_watcher = queue_watcher(output_q, lambda msg : my_backend.publish(msg))
backend_q_watcher = queue_watcher(backend_output_q, lambda msg : process_qmsg(msg))
server.run()
if __name__ == '__main__':
http_main()

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,864 +0,0 @@
#!/usr/bin/env python3
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 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.
import os
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.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()}
elif isinstance(input, list):
return [byteify(element) for element in input]
elif isinstance(input, unicode):
return input.encode('utf-8')
else:
return input
class device(object):
def __init__(self, config, tb):
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:'):
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)
def init_file(self, config):
filename = config['args'].replace('file:', '', 1)
src = blocks.file_source(gr.sizeof_gr_complex, filename, repeat = False)
throttle = blocks.throttle(gr.sizeof_gr_complex, config['rate'])
self.tb.connect(src, throttle)
self.src = throttle
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)
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]
sys.stderr.write('device: %s\n' % config)
if config['args'].startswith('rtl') and config['rate'] not in speeds:
sys.stderr.write('WARNING: requested sample rate %d for device %s may not\n' % (config['rate'], config['name']))
sys.stderr.write("be optimal. You may want to use one of the following rates\n")
sys.stderr.write('%s\n' % speeds)
self.src = osmosdr.source(config['args'])
for tup in config['gains'].split(','):
name, gain = tup.split(':')
self.src.set_gain(int(gain), name)
self.src.set_freq_corr(config['ppm'])
self.ppm = config['ppm']
self.src.set_sample_rate(config['rate'])
self.src.set_center_freq(config['frequency'])
self.frequency = config['frequency']
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=''):
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(
input_rate = dev.sample_rate,
demod_type = config['demod_type'],
filter_type = config['filter_type'],
excess_bw = config['excess_bw'],
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))
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
for plot in config['plot'].split(','):
if plot == 'datascope':
assert config['demod_type'] == 'fsk4' ## datascope plot requires fsk4 demod type
sink = eye_sink_f(sps=config['if_rate'] // self.symbol_rate)
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_bb('symbol_filter', sink)
self.kill_sink.append(sink)
elif plot.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':
i = len(self.sinks)
sink = fft_sink_c()
sink.set_title(self.name)
self.sinks.append(sink)
self.demod.connect_complex('src', self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'mixer':
if config['demod_type'] == 'cqpsk':
blk = 'mixer'
else:
blk = 'cutoff'
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.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.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):
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 = []
for cfg in config:
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)
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
if d + 6250 <= nf:
return dev
return None
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'])
if dev is None:
sys.stderr.write('* * * No device found for channel %s- ignoring!\n' % cfg['name'])
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])
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:
sys.stderr.write('scan %s: error %d\n' % (chan.config['frequency'], chan.demod.get_freq_error()))
class rx_main(object):
def __init__(self):
self.keep_running = True
# command line argument parsing
parser = OptionParser(option_class=eng_option)
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(),))
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()
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()
if __name__ == "__main__":
rx = rx_main()
try:
rx.run()
except KeyboardInterrupt:
rx.keep_running = False
print('Program ending')
time.sleep(1)

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

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