#!/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
""") self.write("""[SERVER]

""") self.write("""SERVER TCP/IP port:Defautl:8888.The server port

""") self.write("""SERVER Authentification type: Defautl:leave blank. Else you can use "FILE" or/and "PAM".

""") self.write("""SERVER database users file: Defautl:UHRR_users.db Only if you use Authentification type "FILE".

""") self.write("""You can change database users file in UHRR.conf.
To add a user in FILE type, add it in UHRR_users.db (default file name).
Add one account per line as login password.
""") self.write("""If you plan to use PAM you can add account in command line: adduser --no-create-home --system thecallsign.

""") 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.

""") self.write("""[AUDIO]

""") self.write("""AUDIO outputdevice: Output from audio soundcard to the mic input of TRX.

""") self.write("""AUDIO inputdevice: Input from audio soundcard from the speaker output of TRX.

""") self.write("""[HAMLIB]

""") self.write("""HAMLIB radio model: Hamlib trx model.

""") self.write("""HAMLIB serial port: Serial port of the CAT interface.

""") self.write("""HAMLIB radio rate: Serial port baud rate.

""") self.write("""HAMLIB auto tx poweroff: Set to auto power off the trx when it's not in use

""") CDVALUE="" if(config['HAMLIB']['data_bits']!=""): CDVALUE=config['HAMLIB']['data_bits'] self.write("""HAMLIB serial data bits: Leave blank to use the HAMIB default value.

""") CDVALUE="" if(config['HAMLIB']['stop_bits']!=""): CDVALUE=config['HAMLIB']['stop_bits'] self.write("""HAMLIB serial stop bits: Leave blank to use the HAMIB default value.

""") self.write("""HAMLIB serial parity: Leave blank to use the HAMIB default value.

""") self.write("""HAMLIB serial handshake: Leave blank to use the HAMIB default value.

""") self.write("""HAMLIB dtr state: Leave blank to use the HAMIB default value.

""") self.write("""HAMLIB rts state: Leave blank to use the HAMIB default value.

""") self.write("""[PANADAPTER]

""") self.write("""PANADAPTER FI frequency (hz):

""") self.write("""HAMLIB radio rate (samples/s):

""") self.write("""PANADAPTER frequency correction (ppm):

""") self.write("""PANADAPTER initial gain:

""") self.write("""PANADAPTER windowing:

""") self.write("""

Possible problem:"""+e+"""""") 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("""You will be redirected automatically. Please wait...
""") 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('
' 'CallSign:
' 'Password:
' '' '
') 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()