2018-04-03 18:40:11 +00:00
#! /usr/bin/python
2018-01-26 00:13:32 +00:00
# Copyright 2017, 2018 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
2018-02-20 01:03:13 +00:00
import threading
2018-03-16 20:26:42 +00:00
import glob
2018-04-03 18:40:11 +00:00
import subprocess
import zmq
2018-01-26 00:13:32 +00:00
from gnuradio import gr
from waitress . server import create_server
2018-03-16 20:26:42 +00:00
from optparse import OptionParser
from multi_rx import byteify
2018-05-18 01:55:02 +00:00
from tsvfile import load_tsv , make_config
2018-01-26 00:13:32 +00:00
my_input_q = None
my_output_q = None
2018-02-20 01:03:13 +00:00
my_recv_q = None
2018-01-26 00:13:32 +00:00
my_port = None
2018-03-16 20:26:42 +00:00
my_backend = None
CFG_DIR = ' ../www/config/ '
2018-05-18 01:55:02 +00:00
TSV_DIR = ' ./ '
2018-01-26 00:13:32 +00:00
"""
fake http and ajax server module
TODO : make less fake
"""
def static_file ( environ , start_response ) :
content_types = { ' png ' : ' image/png ' , ' jpeg ' : ' image/jpeg ' , ' jpg ' : ' image/jpeg ' , ' gif ' : ' image/gif ' , ' css ' : ' text/css ' , ' js ' : ' application/javascript ' , ' html ' : ' text/html ' }
img_types = ' png jpg jpeg gif ' . split ( )
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 '
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 '
content_type = ' text/plain '
output = status
else :
output = open ( pathname ) . read ( )
content_type = content_types [ suf ]
status = ' 200 OK '
return status , content_type , output
2018-03-16 20:26:42 +00:00
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
2018-05-18 01:55:02 +00:00
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 }
2018-03-16 20:26:42 +00:00
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 ' :
2018-05-18 01:55:02 +00:00
if ' [TSV] ' in d [ ' data ' ] :
return tsv_config ( d [ ' data ' ] )
2018-03-16 20:26:42 +00:00
filename = ' %s %s .json ' % ( CFG_DIR , d [ ' data ' ] )
if not os . access ( filename , os . R_OK ) :
2018-05-18 01:55:02 +00:00
return None
2018-03-16 20:26:42 +00:00
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
2018-01-26 00:13:32 +00:00
def post_req ( environ , start_response , postdata ) :
2018-02-20 01:03:13 +00:00
global my_input_q , my_output_q , my_recv_q , my_port
2018-03-16 20:26:42 +00:00
resp_msg = [ ]
2018-01-26 00:13:32 +00:00
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 ' )
2018-03-16 20:26:42 +00:00
for d in data :
if d [ ' command ' ] . startswith ( ' config- ' ) or d [ ' command ' ] . startswith ( ' rx- ' ) :
resp = do_request ( d )
if resp :
resp_msg . append ( resp )
continue
2018-12-26 02:10:06 +00:00
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 )
2018-03-16 20:26:42 +00:00
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 )
2018-01-26 00:13:32 +00:00
2018-02-20 01:03:13 +00:00
while not my_recv_q . empty_p ( ) :
msg = my_recv_q . delete_head ( )
2018-01-26 00:13:32 +00:00
if msg . type ( ) == - 4 :
resp_msg . append ( json . loads ( msg . to_string ( ) ) )
status = ' 200 OK '
content_type = ' application/json '
output = json . dumps ( resp_msg )
return status , content_type , output
def http_request ( environ , start_response ) :
if environ [ ' REQUEST_METHOD ' ] == ' GET ' :
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 ) ,
( ' Content-Length ' , str ( len ( output ) ) ) ]
start_response ( status , response_headers )
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 ( ) )
sys . exit ( 1 )
return result
2018-02-20 01:03:13 +00:00
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 )
2018-01-26 00:13:32 +00:00
class http_server ( object ) :
def __init__ ( self , input_q , output_q , endpoint , * * kwds ) :
2018-02-20 01:03:13 +00:00
global my_input_q , my_output_q , my_recv_q , my_port
2018-04-03 18:40:11 +00:00
host , port = endpoint . split ( ' : ' )
if my_port is not None :
raise AssertionError ( ' this server is already active on port %s ' % my_port )
2018-01-26 00:13:32 +00:00
my_input_q = input_q
my_output_q = output_q
my_port = int ( port )
2018-02-20 01:03:13 +00:00
my_recv_q = gr . msg_queue ( 10 )
self . q_watcher = queue_watcher ( my_input_q , process_qmsg )
2018-01-26 00:13:32 +00:00
self . server = create_server ( application , host = host , port = my_port )
def run ( self ) :
self . server . run ( )
2018-02-20 01:03:13 +00:00
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 )
2018-03-16 20:26:42 +00:00
class Backend ( threading . Thread ) :
2018-12-26 02:10:06 +00:00
def __init__ ( self , options , input_q , output_q , init_config = None , * * kwds ) :
2018-03-16 20:26:42 +00:00
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
2018-04-03 18:40:11 +00:00
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 ( 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 ) )
2018-03-16 20:26:42 +00:00
self . start ( )
2018-04-03 18:40:11 +00:00
self . subproc = None
2018-12-26 02:10:06 +00:00
self . msg = None
2018-04-03 18:40:11 +00:00
2018-06-07 01:46:05 +00:00
self . q_watcher = queue_watcher ( self . input_q , self . process_msg )
2018-12-26 02:10:06 +00:00
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 )
2018-06-07 01:46:05 +00:00
def publish ( self , msg ) :
t = msg . type ( )
s = msg . to_string ( )
a = msg . arg1 ( )
self . zmq_pub . send ( json . dumps ( { ' command ' : s , ' data ' : a , ' msgtype ' : t } ) )
2018-04-03 18:40:11 +00:00
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
2018-03-16 20:26:42 +00:00
def process_msg ( self , msg ) :
2018-12-26 02:10:06 +00:00
def make_command ( options , config_file ) :
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 = [ self . backend ]
filename = ' %s %s .json ' % ( CFG_DIR , config_file )
opts . append ( ' --config-file ' )
opts . append ( filename )
return opts
2018-04-03 18:40:11 +00:00
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 ' }
2018-12-26 02:10:06 +00:00
self . backend = ' %s / %s ' % ( os . getcwd ( ) , ' rx.py ' )
2018-04-03 18:40:11 +00:00
opts = [ 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 ( ) :
2018-12-26 02:10:06 +00:00
self . msg = ' make_command: unknown option: %s %s type %s ' % ( k , val , type ( val ) )
2018-04-03 18:40:11 +00:00
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 :
2018-12-26 02:10:06 +00:00
self . msg = ' make_command: unknown2 option: %s %s type %s ' % ( k , val , type ( val ) )
2018-04-03 18:40:11 +00:00
return None
return opts
2018-03-16 20:26:42 +00:00
msg = json . loads ( msg . to_string ( ) )
if msg [ ' command ' ] == ' rx-start ' :
2018-04-03 18:40:11 +00:00
if self . check_subproc ( ) :
2018-12-26 02:10:06 +00:00
self . msg = ' start command failed: subprocess pid %d already active ' % self . subproc . pid
2018-04-03 18:40:11 +00:00
return
2018-03-16 20:26:42 +00:00
options = rx_options ( msg [ ' data ' ] )
2018-12-26 02:10:06 +00:00
if getattr ( options , ' _js_config ' , None ) is None :
self . msg = ' start command failed: rx_options: unable to initialize config= %s ' % ( msg [ ' data ' ] )
return
2018-03-16 20:26:42 +00:00
options . verbosity = self . verbosity
2018-04-03 18:40:11 +00:00
options . terminal_type = ' zmq:tcp: %d ' % ( self . zmq_port )
2018-12-26 02:10:06 +00:00
cmd = make_command ( options , msg [ ' data ' ] )
if cmd :
self . subproc = subprocess . Popen ( cmd )
2018-04-03 18:40:11 +00:00
elif msg [ ' command ' ] == ' rx-stop ' :
if not self . check_subproc ( ) :
2018-12-26 02:10:06 +00:00
self . msg = ' stop command failed: subprocess not active '
2018-04-03 18:40:11 +00:00
return
if msg [ ' data ' ] == ' kill ' :
self . subproc . kill ( )
else :
self . subproc . terminate ( )
2018-12-26 02:10:06 +00:00
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 )
2018-03-16 20:26:42 +00:00
def run ( self ) :
while self . keep_running :
2018-06-07 01:46:05 +00:00
js = self . zmq_sub . recv ( )
if not self . keep_running :
break
msg = gr . message ( ) . make_from_string ( js , - 4 , 0 , 0 )
if not self . output_q . full_p ( ) :
self . output_q . insert_tail ( msg )
2018-03-16 20:26:42 +00:00
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 ) :
2018-12-26 02:10:06 +00:00
sys . stderr . write ( ' unable to access config file %s \n ' % ( filename ) )
2018-03-16 20:26:42 +00:00
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 ] )
2018-04-03 18:40:11 +00:00
self . demod_type = chan [ ' demod_type ' ]
2018-03-16 20:26:42 +00:00
self . freq_corr = dev [ ' ppm ' ]
self . sample_rate = dev [ ' rate ' ]
self . plot_mode = chan [ ' plot ' ]
self . phase2_tdma = chan [ ' phase2_tdma ' ]
2018-12-26 02:10:06 +00:00
self . trunk_conf_file = filename
2018-03-16 20:26:42 +00:00
self . _js_config = config
def http_main ( ) :
global my_backend
# command line argument parsing
parser = OptionParser ( )
2018-12-26 02:10:06 +00:00
parser . add_option ( " -c " , " --config " , type = " string " , default = None , help = " config json name, without prefix/suffix " )
2018-03-16 20:26:42 +00:00
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 " )
2018-04-03 18:40:11 +00:00
parser . add_option ( " -z " , " --zmq-port " , type = " int " , default = 25000 , help = " backend sub port " )
2018-03-16 20:26:42 +00:00
( 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 )
2018-12-26 02:10:06 +00:00
my_backend = Backend ( options , backend_input_q , backend_output_q , init_config = options . config )
2018-03-16 20:26:42 +00:00
server = http_server ( input_q , output_q , options . endpoint )
2018-06-07 01:46:05 +00:00
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 ) )
2018-03-16 20:26:42 +00:00
server . run ( )
if __name__ == ' __main__ ' :
http_main ( )