829 lines
30 KiB
Plaintext
829 lines
30 KiB
Plaintext
|
#!/usr/bin/env python3
|
||
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
import os
|
||
|
import tornado.httpserver
|
||
|
import tornado.ioloop
|
||
|
import tornado.web
|
||
|
import tornado.websocket
|
||
|
import alsaaudio
|
||
|
import threading
|
||
|
import time
|
||
|
import numpy
|
||
|
import gc
|
||
|
from opus.decoder import Decoder as OpusDecoder
|
||
|
import datetime
|
||
|
import configparser
|
||
|
import sys
|
||
|
import Hamlib
|
||
|
from rtlsdr import RtlSdr
|
||
|
import numpy as np
|
||
|
import math
|
||
|
|
||
|
############ Global variables ##################################
|
||
|
CTRX=None
|
||
|
config = configparser.ConfigParser()
|
||
|
config.read('UHRR.conf')
|
||
|
e="No"
|
||
|
|
||
|
############ Global functions ##################################
|
||
|
def writte_log(logmsg):
|
||
|
logfile = open(config['SERVER']['log_file'],"w")
|
||
|
msg = str(datetime.datetime.now())+":"+str(logmsg)
|
||
|
logfile.write(msg)
|
||
|
print(msg)
|
||
|
logfile.close()
|
||
|
|
||
|
############ BaseHandler tornado ##############
|
||
|
class BaseHandler(tornado.web.RequestHandler):
|
||
|
def get_current_user(self):
|
||
|
return self.get_secure_cookie("user")
|
||
|
|
||
|
############ Generate and send FFT from RTLSDR ##############
|
||
|
is_rtlsdr_present = True
|
||
|
|
||
|
try:
|
||
|
FFTSIZE=4096
|
||
|
nbBuffer=24
|
||
|
nbsamples=nbBuffer/2*FFTSIZE
|
||
|
ptime=nbsamples/int(config['PANADAPTER']['sample_rate'])
|
||
|
sdr_windows = eval("np."+config['PANADAPTER']['fft_window']+ "(FFTSIZE)")
|
||
|
fftpaquetlen=int(FFTSIZE*8/2048)
|
||
|
sdr = RtlSdr()
|
||
|
sdr.sample_rate = int(config['PANADAPTER']['sample_rate']) # Hz
|
||
|
sdr.center_freq = int(config['PANADAPTER']['center_freq']) # Hz
|
||
|
sdr.freq_correction = int(config['PANADAPTER']['freq_correction']) # PPM
|
||
|
sdr.gain = int(config['PANADAPTER']['gain']) #or 'auto'
|
||
|
except:
|
||
|
is_rtlsdr_present = False
|
||
|
|
||
|
|
||
|
|
||
|
AudioPanaHandlerClients = []
|
||
|
|
||
|
class loadFFTdata(threading.Thread):
|
||
|
|
||
|
def __init__(self):
|
||
|
threading.Thread.__init__(self)
|
||
|
self.get_log_power_spectrum_w = np.empty(FFTSIZE)
|
||
|
for i in range(FFTSIZE):
|
||
|
self.get_log_power_spectrum_w[i] = 0.5 * (1. - math.cos((2 * math.pi * i) / (FFTSIZE - 1)))
|
||
|
|
||
|
def run(self):
|
||
|
while True:
|
||
|
time.sleep(ptime)
|
||
|
self.getFFT_data()
|
||
|
|
||
|
|
||
|
def get_log_power_spectrum(self,data):
|
||
|
|
||
|
pulse = 10
|
||
|
rejected_count = 0
|
||
|
power_spectrum = np.zeros(FFTSIZE)
|
||
|
db_adjust = 20. * math.log10(FFTSIZE * 2 ** 15)
|
||
|
|
||
|
# Time-domain analysis: Often we have long normal signals interrupted
|
||
|
# by huge wide-band pulses that degrade our power spectrum average.
|
||
|
# We find the "normal" signal level, by computing the median of the
|
||
|
# absolute value. We only do this for the first buffer of a chunk,
|
||
|
# using the median for the remaining buffers in the chunk.
|
||
|
# A "noise pulse" is a signal level greater than some threshold
|
||
|
# times the median. When such a pulse is found, we skip the current
|
||
|
# buffer. It would be better to blank out just the pulse, but that
|
||
|
# would be more costly in CPU time.
|
||
|
|
||
|
# Find the median abs value of first buffer to use for this chunk.
|
||
|
td_median = np.median(np.abs(data[:FFTSIZE]))
|
||
|
# Calculate our current threshold relative to measured median.
|
||
|
td_threshold = pulse * td_median
|
||
|
nbuf_taken = 0 # Actual number of buffers accumulated
|
||
|
for ic in range(nbBuffer-1):
|
||
|
start=ic * int(FFTSIZE/2)
|
||
|
end=start+FFTSIZE
|
||
|
td_segment = data[start:end]*sdr_windows
|
||
|
|
||
|
# remove the 0hz spike
|
||
|
td_segment = np.subtract(td_segment, np.average(td_segment))
|
||
|
|
||
|
td_max = np.amax(np.abs(td_segment)) # Do we have a noise pulse?
|
||
|
if td_max < td_threshold: # No, get pwr spectrum etc.
|
||
|
# EXPERIMENTAL TAPERfd
|
||
|
td_segment *= self.get_log_power_spectrum_w
|
||
|
|
||
|
fd_spectrum = np.fft.fft(td_segment)
|
||
|
# Frequency-domain:
|
||
|
# Rotate array to place 0 freq. in center. (It was at left.)
|
||
|
fd_spectrum_rot = np.fft.fftshift(fd_spectrum)
|
||
|
# Compute the real-valued squared magnitude (ie power) and
|
||
|
# accumulate into pwr_acc.
|
||
|
# fastest way to sum |z|**2 ??
|
||
|
nbuf_taken += 1
|
||
|
power_spectrum = power_spectrum + \
|
||
|
np.real(fd_spectrum_rot * fd_spectrum_rot.conj())
|
||
|
else: # Yes, abort buffer.
|
||
|
rejected_count += 1
|
||
|
# if DEBUG: print "REJECT! %d" % self.rejected_count
|
||
|
if nbuf_taken > 0:
|
||
|
power_spectrum = power_spectrum / nbuf_taken # normalize the sum.
|
||
|
else:
|
||
|
power_spectrum = np.ones(FFTSIZE) # if no good buffers!
|
||
|
# Convert to dB. Note log(0) = "-inf" in Numpy. It can happen if ADC
|
||
|
# isn't working right. Numpy issues a warning.
|
||
|
log_power_spectrum = 10. * np.log10(power_spectrum)
|
||
|
return log_power_spectrum - db_adjust # max poss. signal = 0 dB
|
||
|
|
||
|
def getFFT_data(self):
|
||
|
samples = sdr.read_samples(nbsamples)
|
||
|
samples = np.imag(samples) + 1j * np.real(samples)
|
||
|
|
||
|
max_pow = -254
|
||
|
min_pow = 0
|
||
|
|
||
|
power = self.get_log_power_spectrum(samples)
|
||
|
|
||
|
# search whole data set for maximum and minimum value
|
||
|
for dat in power:
|
||
|
if dat > max_pow:
|
||
|
max_pow = dat
|
||
|
elif dat < min_pow:
|
||
|
min_pow = dat
|
||
|
|
||
|
byteslist=bytearray()
|
||
|
try:
|
||
|
for dat in power:
|
||
|
try:
|
||
|
byteslist.append(self.FFTmymap(dat, min_pow, max_pow, 0, 255))
|
||
|
except (RuntimeError, TypeError, NameError):
|
||
|
byteslist.append(255)
|
||
|
pass
|
||
|
byteslist+=bytearray((65280+int(min_pow)).to_bytes(2, byteorder="big"))
|
||
|
byteslist+=bytearray((65280+int(max_pow)).to_bytes(2, byteorder="big"))
|
||
|
for c in AudioPanaHandlerClients:
|
||
|
c.fftframes.append(bytes(byteslist))
|
||
|
except:
|
||
|
return None
|
||
|
|
||
|
def FFTmymap(self, x, in_min, in_max, out_min, out_max):
|
||
|
ret=int((x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min)
|
||
|
return ret
|
||
|
|
||
|
|
||
|
|
||
|
class WS_panFFTHandler(tornado.websocket.WebSocketHandler):
|
||
|
|
||
|
@tornado.gen.coroutine
|
||
|
def sendFFT(self):
|
||
|
global ptime, fftpaquetlen
|
||
|
try:
|
||
|
while len(self.fftframes)>0:
|
||
|
yield self.write_message(self.fftframes[0],binary=True)
|
||
|
del self.fftframes[0]
|
||
|
except:
|
||
|
return None
|
||
|
tornado.ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=ptime), self.sendFFT)
|
||
|
|
||
|
def open(self):
|
||
|
global is_rtlsdr_present
|
||
|
print('new connection on FFT socket, is_rtlsdr_present = '+str(is_rtlsdr_present))
|
||
|
if self not in AudioPanaHandlerClients:
|
||
|
AudioPanaHandlerClients.append(self)
|
||
|
self.fftframes = []
|
||
|
|
||
|
def on_message(self, data) :
|
||
|
print(data)
|
||
|
if str(data)=="ready":
|
||
|
self.sendFFT()
|
||
|
elif str(data)=="init":
|
||
|
self.write_message("fftsr:"+str(config['PANADAPTER']['sample_rate']));
|
||
|
self.write_message("fftsz:"+str(FFTSIZE));
|
||
|
self.write_message("fftst");
|
||
|
|
||
|
def on_close(self):
|
||
|
print('connection closed for FFT socket')
|
||
|
|
||
|
############ websocket for send RX audio from TRX ##############
|
||
|
flagWavstart = False
|
||
|
AudioRXHandlerClients = []
|
||
|
|
||
|
class loadWavdata(threading.Thread):
|
||
|
|
||
|
def __init__(self):
|
||
|
global flagWavstart
|
||
|
threading.Thread.__init__(self)
|
||
|
#self.inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NORMAL, channels=1, rate=8000, format=alsaaudio.PCM_FORMAT_FLOAT_LE, periodsize=256, device=config['AUDIO']['inputdevice'])
|
||
|
self.inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NORMAL, device=config['AUDIO']['inputdevice'])
|
||
|
self.inp.setchannels(1)
|
||
|
self.inp.setrate(8000)
|
||
|
self.inp.setformat(alsaaudio.PCM_FORMAT_FLOAT_LE)
|
||
|
self.inp.setperiodsize(256)
|
||
|
print('recording...')
|
||
|
|
||
|
def run(self):
|
||
|
global Wavframes, flagWavstart
|
||
|
ret=b''
|
||
|
while True:
|
||
|
while not flagWavstart:
|
||
|
time.sleep(0.5)
|
||
|
l, ret = self.inp.read()
|
||
|
if l > 0:
|
||
|
for c in AudioRXHandlerClients:
|
||
|
c.Wavframes.append(ret)
|
||
|
else:
|
||
|
print("overrun")
|
||
|
time.sleep(0.01)
|
||
|
|
||
|
|
||
|
class WS_AudioRXHandler(tornado.websocket.WebSocketHandler):
|
||
|
|
||
|
def open(self):
|
||
|
self.set_nodelay(True)
|
||
|
global flagWavstart
|
||
|
if self not in AudioRXHandlerClients:
|
||
|
AudioRXHandlerClients.append(self)
|
||
|
self.Wavframes = []
|
||
|
print('new connection on AudioRXHandler socket.')
|
||
|
flagWavstart = True
|
||
|
self.tailstream()
|
||
|
self.set_nodelay(True)
|
||
|
|
||
|
@tornado.gen.coroutine
|
||
|
def tailstream(self):
|
||
|
while flagWavstart:
|
||
|
while len(self.Wavframes)==0:
|
||
|
yield tornado.gen.sleep(0.1)
|
||
|
yield self.write_message(self.Wavframes[0],binary=True)
|
||
|
del self.Wavframes[0]
|
||
|
|
||
|
def on_close(self):
|
||
|
if self in AudioRXHandlerClients:
|
||
|
AudioRXHandlerClients.remove(self)
|
||
|
global flagWavstart
|
||
|
print('connection closed for audioRX')
|
||
|
if len(AudioRXHandlerClients)<=0:
|
||
|
flagWavstart = False
|
||
|
self.Wavframes = []
|
||
|
gc.collect()
|
||
|
|
||
|
############ websocket for control TX ##############
|
||
|
last_AudioTXHandler_msg_time=0
|
||
|
AudioTXHandlerClients = []
|
||
|
|
||
|
class WS_AudioTXHandler(tornado.websocket.WebSocketHandler):
|
||
|
|
||
|
def stoppttontimeout(self):
|
||
|
global last_AudioTXHandler_msg_time
|
||
|
try:
|
||
|
if time.time() > last_AudioTXHandler_msg_time + 10:
|
||
|
if self.ws_connection and CTRX.infos["PTT"]==True:
|
||
|
CTRX.setPTT("false")
|
||
|
print("stop ptt on timeout")
|
||
|
except:
|
||
|
return None
|
||
|
tornado.ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=1), self.stoppttontimeout)
|
||
|
|
||
|
|
||
|
def TX_init(self, msg) :
|
||
|
|
||
|
itrate, is_encoded, op_rate, op_frm_dur = [int(i) for i in msg.split(',')]
|
||
|
self.is_encoded = is_encoded
|
||
|
self.decoder = OpusDecoder(op_rate, 1)
|
||
|
self.frame_size = op_frm_dur * op_rate
|
||
|
|
||
|
device = config['AUDIO']['outputdevice']
|
||
|
self.inp = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NONBLOCK, channels=1, rate=itrate, format=alsaaudio.PCM_FORMAT_S16_LE, periodsize=2048, device=device)
|
||
|
|
||
|
def open(self):
|
||
|
global last_AudioTXHandler_msg_time, AudioTXHandlerClients
|
||
|
if self not in AudioTXHandlerClients:
|
||
|
AudioTXHandlerClients.append(self)
|
||
|
print('new connection on AudioTXHandler socket.')
|
||
|
last_AudioTXHandler_msg_time=time.time()
|
||
|
self.stoppttontimeout()
|
||
|
self.set_nodelay(True)
|
||
|
|
||
|
def on_message(self, data) :
|
||
|
global last_AudioTXHandler_msg_time
|
||
|
last_AudioTXHandler_msg_time=time.time()
|
||
|
|
||
|
if str(data).startswith('m:') :
|
||
|
self.TX_init(str(data[2:]))
|
||
|
elif str(data).startswith('s:') :
|
||
|
self.inp.close()
|
||
|
else :
|
||
|
if self.is_encoded :
|
||
|
pcm = self.decoder.decode(data, self.frame_size, False)
|
||
|
self.inp.write(pcm)
|
||
|
gc.collect()
|
||
|
|
||
|
else :
|
||
|
self.inp.write(data)
|
||
|
gc.collect()
|
||
|
|
||
|
def on_close(self):
|
||
|
global AudioTXHandlerClients
|
||
|
if(hasattr(self,"inp")):
|
||
|
self.inp.close()
|
||
|
if self in AudioTXHandlerClients:
|
||
|
AudioTXHandlerClients.remove(self)
|
||
|
if (not len(AudioTXHandlerClients)) and (CTRX.infos["PTT"]==True):
|
||
|
CTRX.setPTT("false")
|
||
|
print('connection closed for TX socket')
|
||
|
|
||
|
############ websocket for control TRX ##############
|
||
|
ControlTRXHandlerClients = []
|
||
|
LastPing = time.time()
|
||
|
|
||
|
class TRXRIG:
|
||
|
def __init__(self):
|
||
|
self.spoints = {"0":-54, "1":-48, "2":-42, "3":-36, "4":-30, "5":-24, "6":-18, "7":-12, "8":-6, "9":0, "10":10, "20":20, "30":30, "40":40, "50":50, "60":60}
|
||
|
self.infos = {}
|
||
|
self.infos["PTT"]=False
|
||
|
self.infos["powerstat"]=False
|
||
|
self.serialport = Hamlib.hamlib_port_parm_serial
|
||
|
self.serialport.rate=config['HAMLIB']['rig_rate']
|
||
|
try:
|
||
|
Hamlib.rig_set_debug(Hamlib.RIG_DEBUG_NONE)
|
||
|
self.rig_model = "RIG_MODEL_"+str(config['HAMLIB']['rig_model'])
|
||
|
self.rig_pathname = config['HAMLIB']['rig_pathname']
|
||
|
self.rig = Hamlib.Rig(Hamlib.__dict__[self.rig_model]) # Look up the model's numerical index in Hamlib's symbol dictionary.
|
||
|
self.rig.set_conf("rig_pathname", self.rig_pathname)
|
||
|
if(config['HAMLIB']['rig_rate']!=""):
|
||
|
self.rig.set_conf("serial_speed", str(config['HAMLIB']['rig_rate']))
|
||
|
if(config['HAMLIB']['data_bits']!=""):
|
||
|
self.rig.set_conf("data_bits", str(config['HAMLIB']['data_bits'])) #8 as default
|
||
|
if(config['HAMLIB']['stop_bits']!=""):
|
||
|
self.rig.set_conf("stop_bits", str(config['HAMLIB']['stop_bits'])) #2 as default
|
||
|
if(config['HAMLIB']['serial_parity']!=""):
|
||
|
self.rig.set_conf("serial_parity", str(config['HAMLIB']['serial_parity']))# None as default NONE ODD EVEN MARK SPACE
|
||
|
if(config['HAMLIB']['serial_handshake']!=""):
|
||
|
self.rig.set_conf("serial_handshake", str(config['HAMLIB']['serial_handshake'])) # None as default NONE XONXOFF HARDWARE
|
||
|
if(config['HAMLIB']['dtr_state']!=""):
|
||
|
self.rig.set_conf("dtr_state", str(config['HAMLIB']['dtr_state'])) #ON or OFF
|
||
|
if(config['HAMLIB']['rts_state']!=""):
|
||
|
self.rig.set_conf("rts_state", str(config['HAMLIB']['rts_state'])) #ON or OFF
|
||
|
self.rig.set_conf("retry", config['HAMLIB']['retry'])
|
||
|
self.rig.open()
|
||
|
except:
|
||
|
print("Could not open a communication channel to the rig via Hamlib!")
|
||
|
|
||
|
self.setPower(1)
|
||
|
self.getvfo()
|
||
|
self.getFreq()
|
||
|
self.getMode()
|
||
|
|
||
|
def parsedbtospoint(self,spoint):
|
||
|
for key, value in self.spoints.items():
|
||
|
if (spoint<value):
|
||
|
return key
|
||
|
break
|
||
|
|
||
|
|
||
|
def getvfo(self):
|
||
|
try:
|
||
|
self.infos["VFO"] = (self.rig.get_vfo())
|
||
|
except:
|
||
|
print("Could not obtain the current VFO via Hamlib!")
|
||
|
return self.infos["VFO"]
|
||
|
|
||
|
def setFreq(self,frequency):
|
||
|
try:
|
||
|
self.rig.set_freq(Hamlib.RIG_VFO_CURR, float(frequency))
|
||
|
self.getFreq()
|
||
|
except:
|
||
|
print("Could not set the frequency via Hamlib!")
|
||
|
return self.infos["FREQ"]
|
||
|
|
||
|
def getFreq(self):
|
||
|
try:
|
||
|
self.infos["FREQ"] = (int(self.rig.get_freq()))
|
||
|
except:
|
||
|
print("Could not obtain the current frequency via Hamlib!")
|
||
|
return self.infos["FREQ"]
|
||
|
|
||
|
def setMode(self,MODE):
|
||
|
try:
|
||
|
|
||
|
self.rig.set_mode(Hamlib.rig_parse_mode(MODE))
|
||
|
self.getMode()
|
||
|
except:
|
||
|
print("Could not set the mode via Hamlib!")
|
||
|
return self.infos["MODE"]
|
||
|
|
||
|
def getMode(self):
|
||
|
try:
|
||
|
(mode, width) = self.rig.get_mode()
|
||
|
self.infos["MODE"] = Hamlib.rig_strrmode(mode).upper()
|
||
|
self.infos["WIDTH"] = width
|
||
|
except:
|
||
|
print("Could not obtain the current Mode via Hamlib!")
|
||
|
return self.infos["MODE"]
|
||
|
return ""
|
||
|
|
||
|
def getStrgLVL(self):
|
||
|
try:
|
||
|
self.infos["StrgLVLi"] = self.rig.get_level_i(Hamlib.RIG_LEVEL_STRENGTH)
|
||
|
self.infos["StrgLVL"] = self.parsedbtospoint(self.infos["StrgLVLi"])
|
||
|
except:
|
||
|
print("Could not obtain the current Strength signal RX level via Hamlib!")
|
||
|
return self.infos["StrgLVL"]
|
||
|
|
||
|
def setPTT(self,status):
|
||
|
try:
|
||
|
if status == "true":
|
||
|
self.rig.set_ptt(Hamlib.RIG_VFO_CURR,Hamlib.RIG_PTT_ON)
|
||
|
self.infos["PTT"]=True
|
||
|
else:
|
||
|
self.rig.set_ptt(Hamlib.RIG_VFO_CURR,Hamlib.RIG_PTT_OFF)
|
||
|
self.infos["PTT"]=False
|
||
|
except:
|
||
|
print("Could not set the mode via Hamlib!")
|
||
|
return self.infos["PTT"]
|
||
|
|
||
|
def getPTT(self,status):
|
||
|
return self.infos["PTT"]
|
||
|
|
||
|
def setPower(self,status=1):
|
||
|
try:
|
||
|
if status:
|
||
|
self.rig.set_powerstat(Hamlib.RIG_POWER_ON)
|
||
|
else:
|
||
|
self.rig.set_powerstat(Hamlib.RIG_POWER_OFF)
|
||
|
self.infos["powerstat"] = status
|
||
|
except:
|
||
|
print("Could not set power status via Hamlib!")
|
||
|
return self.infos["powerstat"]
|
||
|
|
||
|
class ticksTRXRIG(threading.Thread):
|
||
|
|
||
|
def __init__(self):
|
||
|
threading.Thread.__init__(self)
|
||
|
|
||
|
def run(self):
|
||
|
while True:
|
||
|
if CTRX.infos["powerstat"]:
|
||
|
CTRX.getStrgLVL()
|
||
|
time.sleep(0.1)
|
||
|
|
||
|
class WS_ControlTRX(tornado.websocket.WebSocketHandler):
|
||
|
|
||
|
def send_to_all_clients(self,msg):
|
||
|
print ("Send to all: "+msg)
|
||
|
for client in ControlTRXHandlerClients:
|
||
|
client.write_message(msg)
|
||
|
|
||
|
def sendPTINFOS(self):
|
||
|
try:
|
||
|
if self.StrgLVL != CTRX.infos["StrgLVL"]:
|
||
|
self.write_message("getSignalLevel:"+str(CTRX.infos["StrgLVL"]))
|
||
|
self.StrgLVL=CTRX.infos["StrgLVL"]
|
||
|
except:
|
||
|
print("error TXMETER")
|
||
|
return None
|
||
|
tornado.ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=float(config['CTRL']['interval_smeter_update'])), self.sendPTINFOS)
|
||
|
|
||
|
def open(self):
|
||
|
if self not in ControlTRXHandlerClients:
|
||
|
ControlTRXHandlerClients.append(self)
|
||
|
self.StrgLVL=0
|
||
|
self.sendPTINFOS()
|
||
|
CTRX.setPower(1)
|
||
|
print('new connection on ControlTRX socket.')
|
||
|
if(is_rtlsdr_present):
|
||
|
self.write_message("panfft")
|
||
|
self.set_nodelay(True)
|
||
|
|
||
|
@tornado.gen.coroutine
|
||
|
def on_message(self, data) :
|
||
|
global LastPing
|
||
|
if bool(config['CTRL']['debug']):
|
||
|
print(data)
|
||
|
|
||
|
try:
|
||
|
(action, datato) = data.split(':')
|
||
|
except ValueError:
|
||
|
action = data
|
||
|
pass
|
||
|
|
||
|
if(action == "PING"):
|
||
|
self.write_message("PONG")
|
||
|
elif(action == "getFreq"):
|
||
|
yield self.send_to_all_clients("getFreq:"+str(CTRX.getFreq()))
|
||
|
elif(action == "setFreq"):
|
||
|
yield self.send_to_all_clients("getFreq:"+str(CTRX.setFreq(datato)))
|
||
|
elif(action == "getMode"):
|
||
|
yield self.send_to_all_clients("getMode:"+str(CTRX.getMode()))
|
||
|
elif(action == "setMode"):
|
||
|
yield self.send_to_all_clients("getMode:"+str(CTRX.setMode(datato)))
|
||
|
elif(action == "setPTT"):
|
||
|
yield self.send_to_all_clients("getPTT:"+str(CTRX.setPTT(datato)))
|
||
|
|
||
|
LastPing = time.time();
|
||
|
|
||
|
def on_close(self):
|
||
|
if self in ControlTRXHandlerClients:
|
||
|
ControlTRXHandlerClients.remove(self)
|
||
|
gc.collect()
|
||
|
|
||
|
def timeoutTRXshutdown():
|
||
|
global LastPing
|
||
|
if(LastPing+300) < time.time():
|
||
|
print("Shutdown TRX")
|
||
|
CTRX.setPower(0)
|
||
|
|
||
|
class threadtimeoutTRXshutdown(threading.Thread):
|
||
|
|
||
|
def __init__(self):
|
||
|
threading.Thread.__init__(self)
|
||
|
|
||
|
def run(self):
|
||
|
while True:
|
||
|
time.sleep(60)
|
||
|
timeoutTRXshutdown()
|
||
|
|
||
|
############ Config ##############
|
||
|
class ConfigHandler(BaseHandler):
|
||
|
def get(self):
|
||
|
|
||
|
if bool(config['SERVER']['auth']) and not self.current_user:
|
||
|
self.redirect("/login")
|
||
|
return
|
||
|
|
||
|
self.application.settings.get("compiled_template_cache", False)
|
||
|
self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||
|
try:
|
||
|
from serial.tools.list_ports import comports
|
||
|
except ImportError:
|
||
|
return None
|
||
|
audiodevicesoutput=[s for s in alsaaudio.pcms(0) if "plughw" in s]
|
||
|
audiodevicesinput=[s for s in alsaaudio.pcms(1) if "plughw" in s]
|
||
|
comports=list(comports())
|
||
|
rig_models=[s[10:] for s in dir(Hamlib) if "RIG_MODEL_" in s]
|
||
|
self.write("""<html><form method="POST" action="/CONFIG">""")
|
||
|
self.write("""[SERVER]<br/><br/>""")
|
||
|
self.write("""SERVER TCP/IP port:<input type="text" name="SERVER.port" value="""+config['SERVER']['port']+""">Defautl:<b>8888</b>.The server port<br/><br/>""")
|
||
|
self.write("""SERVER Authentification type:<input type="text" name="SERVER.auth" value="""+config['SERVER']['auth']+"""> Defautl:<b>leave blank</b>. Else you can use "FILE" or/and "PAM".<br/><br/>""")
|
||
|
self.write("""SERVER database users file:<input type="text" name="SERVER.db_users_file" value="""+config['SERVER']['db_users_file']+"""> Defautl:<b>UHRR_users.db</b> Only if you use Authentification type "FILE".<br/><br/>""")
|
||
|
self.write("""You can change database users file in UHRR.conf.<br/> To add a user in FILE type, add it in UHRR_users.db (default file name).<br/>Add one account per line as login password.<br/>""")
|
||
|
self.write("""If you plan to use PAM you can add account in command line: adduser --no-create-home --system thecallsign.<br/><br/>""")
|
||
|
self.write("""If you want to change certfile and keyfile, replace "UHRH.crt" and "UHRH.key" in the boot folder, and when the pi boot, it will use those files to start http ssl.<br/><br/>""")
|
||
|
|
||
|
self.write("""[AUDIO]<br/><br/>""")
|
||
|
self.write("""AUDIO outputdevice:<select name="AUDIO.outputdevice">""")
|
||
|
if(config['AUDIO']['outputdevice']!=""):
|
||
|
self.write("""<option value="""+config['AUDIO']['outputdevice']+""" selected>"""+config['AUDIO']['outputdevice']+"""</option>""")
|
||
|
for c in audiodevicesoutput:
|
||
|
self.write("""<option value="""+c+""">"""+c+"""</option>""")
|
||
|
self.write("""</select> Output from audio soundcard to the mic input of TRX.<br/><br/>""")
|
||
|
|
||
|
self.write("""AUDIO inputdevice:<select name="AUDIO.inputdevice">""")
|
||
|
if(config['AUDIO']['inputdevice']!=""):
|
||
|
self.write("""<option value="""+config['AUDIO']['inputdevice']+""" selected>"""+config['AUDIO']['inputdevice']+"""</option>""")
|
||
|
for c in audiodevicesinput:
|
||
|
self.write("""<option value="""+c+""">"""+c+"""</option>""")
|
||
|
self.write("""</select> Input from audio soundcard from the speaker output of TRX.<br/><br/>""")
|
||
|
|
||
|
self.write("""[HAMLIB]<br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB radio model:<select name="HAMLIB.rig_model">""")
|
||
|
if(config['HAMLIB']['rig_model']!=""):
|
||
|
self.write("""<option value="""+config['HAMLIB']['rig_model']+""" selected>"""+config['HAMLIB']['rig_model']+"""</option>""")
|
||
|
for c in rig_models:
|
||
|
self.write("""<option value="""+c+""">"""+c+"""</option>""")
|
||
|
self.write("""</select> Hamlib trx model.<br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB serial port:<select name="HAMLIB.rig_pathname">""")
|
||
|
if(config['HAMLIB']['rig_pathname']!=""):
|
||
|
self.write("""<option value="""+config['HAMLIB']['rig_pathname']+""" selected>"""+config['HAMLIB']['rig_pathname']+"""</option>""")
|
||
|
for c in comports:
|
||
|
self.write("""<option value="""+str(c.device)+""">"""+str(c.device)+"""</option>""")
|
||
|
self.write("""</select> Serial port of the CAT interface.<br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB radio rate:<select name="HAMLIB.rig_rate">""")
|
||
|
if(config['HAMLIB']['rig_rate']!=""):
|
||
|
self.write("""<option value="""+config['HAMLIB']['rig_rate']+""" selected>"""+config['HAMLIB']['rig_rate']+"""</option>""")
|
||
|
self.write("""<option value=230400>230400</option>""")
|
||
|
self.write("""<option value=115200>115200</option>""")
|
||
|
self.write("""<option value=57600>57600</option>""")
|
||
|
self.write("""<option value=38400>38400</option>""")
|
||
|
self.write("""<option value=19200>19200</option>""")
|
||
|
self.write("""<option value=9600>9600</option>""")
|
||
|
self.write("""<option value=4800>4800</option>""")
|
||
|
self.write("""<option value=2400>2400</option>""")
|
||
|
self.write("""<option value=1200>1200</option>""")
|
||
|
self.write("""<option value=600>600</option>""")
|
||
|
self.write("""<option value=300>300</option>""")
|
||
|
self.write("""<option value=150>150</option>""")
|
||
|
self.write("""</select> Serial port baud rate.<br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB auto tx poweroff:<select name="HAMLIB.trxautopower">""")
|
||
|
if(config['HAMLIB']['trxautopower']!=""):
|
||
|
self.write("""<option value="""+config['HAMLIB']['trxautopower']+""" selected>"""+config['HAMLIB']['trxautopower']+"""</option>""")
|
||
|
self.write("""<option value=\"True\">True</option>""")
|
||
|
self.write("""<option value=\"False\">False</option>""")
|
||
|
self.write("""</select> Set to auto power off the trx when it's not in use<br/><br/>""")
|
||
|
|
||
|
CDVALUE=""
|
||
|
if(config['HAMLIB']['data_bits']!=""):
|
||
|
CDVALUE=config['HAMLIB']['data_bits']
|
||
|
self.write("""HAMLIB serial data bits:<input type="text" name="HAMLIB.data_bits" value="""+CDVALUE+"""> Leave blank to use the HAMIB default value.<br/><br/>""")
|
||
|
|
||
|
CDVALUE=""
|
||
|
if(config['HAMLIB']['stop_bits']!=""):
|
||
|
CDVALUE=config['HAMLIB']['stop_bits']
|
||
|
self.write("""HAMLIB serial stop bits:<input type="text" name="HAMLIB.stop_bits" value="""+CDVALUE+"""> Leave blank to use the HAMIB default value.<br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB serial parity:<select name="HAMLIB.serial_parity">""")
|
||
|
if(config['HAMLIB']['serial_parity']!=""):
|
||
|
self.write("""<option value="""+config['HAMLIB']['serial_parity']+""" selected>"""+config['HAMLIB']['serial_parity']+"""</option>""")
|
||
|
self.write("""<option value=\"\"></option>""")
|
||
|
self.write("""<option value=\"None\">None</option>""")
|
||
|
self.write("""<option value=\"Odd\">Odd</option>""")
|
||
|
self.write("""<option value=\"Even\">Even</option>""")
|
||
|
self.write("""<option value=\"Mark\">Mark</option>""")
|
||
|
self.write("""<option value=\"Space\">Space</option>""")
|
||
|
self.write("""</select> Leave blank to use the HAMIB default value.<br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB serial handshake:<select name="HAMLIB.serial_handshake">""")
|
||
|
if(config['HAMLIB']['serial_handshake']!=""):
|
||
|
self.write("""<option value="""+config['HAMLIB']['serial_handshake']+""" selected>"""+config['HAMLIB']['serial_handshake']+"""</option>""")
|
||
|
self.write("""<option value=\"\"></option>""")
|
||
|
self.write("""<option value=\"None\">None</option>""")
|
||
|
self.write("""<option value=\"XONXOFF\">XONXOFF</option>""")
|
||
|
self.write("""<option value=\"Hardware\">Hardware</option>""")
|
||
|
self.write("""</select> Leave blank to use the HAMIB default value.<br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB dtr state:<select name="HAMLIB.dtr_state">""")
|
||
|
if(config['HAMLIB']['dtr_state']!=""):
|
||
|
self.write("""<option value="""+config['HAMLIB']['dtr_state']+""" selected>"""+config['HAMLIB']['dtr_state']+"""</option>""")
|
||
|
self.write("""<option value=\"\"></option>""")
|
||
|
self.write("""<option value=\"ON\">ON</option>""")
|
||
|
self.write("""<option value=\"OFF\">OFF</option>""")
|
||
|
self.write("""</select> Leave blank to use the HAMIB default value.<br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB rts state:<select name="HAMLIB.rts_state">""")
|
||
|
if(config['HAMLIB']['rts_state']!=""):
|
||
|
self.write("""<option value="""+config['HAMLIB']['rts_state']+""" selected>"""+config['HAMLIB']['rts_state']+"""</option>""")
|
||
|
self.write("""<option value=\"\"></option>""")
|
||
|
self.write("""<option value=\"ON\">ON</option>""")
|
||
|
self.write("""<option value=\"OFF\">OFF</option>""")
|
||
|
self.write("""</select> Leave blank to use the HAMIB default value.<br/><br/>""")
|
||
|
|
||
|
self.write("""[PANADAPTER]<br/><br/>""")
|
||
|
self.write("""PANADAPTER FI frequency (hz):<input type="text" name="PANADAPTER.center_freq" value="""+config['PANADAPTER']['center_freq']+"""><br/><br/>""")
|
||
|
|
||
|
self.write("""HAMLIB radio rate (samples/s):<select name="PANADAPTER.sample_rate">""")
|
||
|
if(config['PANADAPTER']['sample_rate']!=""):
|
||
|
self.write("""<option value="""+config['PANADAPTER']['sample_rate']+""" selected>"""+config['PANADAPTER']['sample_rate']+"""</option>""")
|
||
|
self.write("""<option value=3200000>3200000</option>""")
|
||
|
self.write("""<option value=2880000>2880000</option>""")
|
||
|
self.write("""<option value=2400000>2400000</option>""")
|
||
|
self.write("""<option value=1800000>1800000</option>""")
|
||
|
self.write("""<option value=1440000>1440000</option>""")
|
||
|
self.write("""<option value=1200000>1200000</option>""")
|
||
|
self.write("""<option value=1020000>1020000</option>""")
|
||
|
self.write("""<option value=960000>960000</option>""")
|
||
|
self.write("""</select><br/><br/>""")
|
||
|
|
||
|
self.write("""PANADAPTER frequency correction (ppm):<input type="text" name="PANADAPTER.freq_correction" value="""+config['PANADAPTER']['freq_correction']+"""><br/><br/>""")
|
||
|
|
||
|
self.write("""PANADAPTER initial gain:<input type="text" name="PANADAPTER.gain" value="""+config['PANADAPTER']['gain']+"""><br/><br/>""")
|
||
|
|
||
|
self.write("""PANADAPTER windowing:<select name="PANADAPTER.fft_window">""")
|
||
|
if(config['PANADAPTER']['fft_window']!=""):
|
||
|
self.write("""<option value="""+config['PANADAPTER']['fft_window']+""" selected>"""+config['PANADAPTER']['fft_window']+"""</option>""")
|
||
|
self.write("""<option value="bartlett">bartlett</option>""")
|
||
|
self.write("""<option value="blackman">blackman</option>""")
|
||
|
self.write("""<option value="hamming">hamming</option>""")
|
||
|
self.write("""<option value="hanning">hanning</option>""")
|
||
|
self.write("""</select><br/><br/>""")
|
||
|
|
||
|
self.write("""<input type="submit" value="Save & Restart server"><br/><br/></form>Possible problem:"""+e+"""</html>""")
|
||
|
|
||
|
def post(self):
|
||
|
|
||
|
if bool(config['SERVER']['auth']) and not self.current_user:
|
||
|
self.redirect("/login")
|
||
|
return
|
||
|
|
||
|
for x in self.request.arguments:
|
||
|
(s,o)=x.split(".")
|
||
|
v=self.get_argument(x)
|
||
|
print(s,o,v)
|
||
|
if config.has_option(s,o):
|
||
|
config[s][o]=v
|
||
|
with open('UHRR.conf', 'w') as configfile:
|
||
|
config.write(configfile)
|
||
|
self.write("""<html><head><script>window.setTimeout(function() {window.location.href = 'https://'+window.location.hostname+':'+ '"""+config['SERVER']['port']+"""';}, 10000);</script><head><body>You will be redirected automatically. Please wait...<br><img width="40px" height=40px" src="../img/spinner.gif"></body></html>""")
|
||
|
self.flush()
|
||
|
time.sleep(2)
|
||
|
os.system("sleep 2;./UHRR &")
|
||
|
os._exit(1)
|
||
|
|
||
|
############ Login ##############
|
||
|
class AuthLoginHandler(BaseHandler):
|
||
|
|
||
|
def get(self):
|
||
|
if not bool(config['SERVER']['auth']):
|
||
|
self.redirect("/")
|
||
|
return
|
||
|
self.write('<html><body><form action="/login" method="post">'
|
||
|
'CallSign: <input type="text" name="name"></br>'
|
||
|
'Password: <input type="password" name="passwd"></br>'
|
||
|
'<input type="submit" value="Sign in">'
|
||
|
'</form></body></html>')
|
||
|
|
||
|
def post(self):
|
||
|
if self.get_argument("name") != "" and self.get_argument("passwd") != "":
|
||
|
if self.bind(self.get_argument("name"),self.get_argument("passwd")):
|
||
|
self.set_secure_cookie("user", self.get_argument("name"))
|
||
|
self.set_cookie("callsign", self.get_argument("name"))
|
||
|
self.set_cookie("autha", "1")
|
||
|
else:
|
||
|
writte_log("Auth error for CallSign:"+str(self.get_argument("name")))
|
||
|
self.redirect("/")
|
||
|
|
||
|
def bind(self,user="",password=""):
|
||
|
retval = False
|
||
|
if (user!="" and password!=""):
|
||
|
if config['SERVER']['auth'].find("FILE") != -1: #test with users db file
|
||
|
f = open(config['SERVER']['db_users_file'], "r")
|
||
|
for x in f:
|
||
|
if x[0]!="#":
|
||
|
db=x.strip('\n').split(" ")
|
||
|
if db[0] == user and db[1]== password:
|
||
|
retval = True
|
||
|
break
|
||
|
if not retval and config['SERVER']['auth'].find("PAM") != -1:#test with pam module
|
||
|
if config['SERVER']['pam_account'].find(user) != -1:
|
||
|
import pam
|
||
|
retval = pam.authenticate(user, password)
|
||
|
return retval
|
||
|
|
||
|
class AuthLogoutHandler(BaseHandler):
|
||
|
def get(self):
|
||
|
self.clear_cookie("user")
|
||
|
self.clear_cookie("autha")
|
||
|
self.redirect(self.get_argument("next", "/"))
|
||
|
|
||
|
############ Main ##############
|
||
|
class MainHandler(BaseHandler):
|
||
|
|
||
|
def get(self):
|
||
|
print("Tornado current user:"+str(self.current_user))
|
||
|
if bool(config['SERVER']['auth']) and not self.current_user:
|
||
|
self.redirect("/login")
|
||
|
return
|
||
|
self.application.settings.get("compiled_template_cache", False)
|
||
|
self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||
|
self.render("www/index.html")
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
|
||
|
try:
|
||
|
if is_rtlsdr_present:
|
||
|
threadFFT = loadFFTdata()
|
||
|
threadFFT.start()
|
||
|
|
||
|
threadloadWavdata = loadWavdata()
|
||
|
threadloadWavdata.start()
|
||
|
|
||
|
CTRX = TRXRIG()
|
||
|
|
||
|
threadticksTRXRIG = ticksTRXRIG()
|
||
|
threadticksTRXRIG.start()
|
||
|
|
||
|
if(config['HAMLIB']['trxautopower']=="True"):
|
||
|
threadsurveilTRX = threadtimeoutTRXshutdown()
|
||
|
threadsurveilTRX.start()
|
||
|
|
||
|
|
||
|
app = tornado.web.Application([
|
||
|
(r'/login', AuthLoginHandler),
|
||
|
(r'/logout', AuthLogoutHandler),
|
||
|
(r'/WSaudioRX', WS_AudioRXHandler),
|
||
|
(r'/WSaudioTX', WS_AudioTXHandler),
|
||
|
(r'/WSCTRX', WS_ControlTRX),
|
||
|
(r'/WSpanFFT', WS_panFFTHandler),
|
||
|
(r'/(panfft.*)', tornado.web.StaticFileHandler, { 'path' : './www/panadapter' }),
|
||
|
(r'/CONFIG', ConfigHandler),
|
||
|
(r'/', MainHandler),
|
||
|
(r'/(.*)', tornado.web.StaticFileHandler, { 'path' : './www' })
|
||
|
],debug=bool(config['SERVER']['debug']), websocket_ping_interval=10, cookie_secret=config['SERVER']['cookie_secret'])
|
||
|
except:
|
||
|
e = str(sys.exc_info())
|
||
|
print(e)
|
||
|
app = tornado.web.Application([
|
||
|
(r'/CONFIG', ConfigHandler),
|
||
|
(r'/', ConfigHandler),
|
||
|
(r'/(.*)', tornado.web.StaticFileHandler, { 'path' : './www' })
|
||
|
],debug=bool(config['SERVER']['debug']))
|
||
|
|
||
|
http_server = tornado.httpserver.HTTPServer(app, ssl_options={
|
||
|
"certfile": os.path.join(config['SERVER']['certfile']),
|
||
|
"keyfile": os.path.join(config['SERVER']['keyfile']),
|
||
|
})
|
||
|
http_server.listen(int(config['SERVER']['port']))
|
||
|
print('HTTP server started.')
|
||
|
tornado.ioloop.IOLoop.instance().start()
|
||
|
|