mirror of
https://github.com/F4HTB/Universal_HamRadio_Remote_HTML5.git
synced 2024-09-21 07:27:10 +00:00
478 lines
15 KiB
Python
Executable File
478 lines
15 KiB
Python
Executable File
#!/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
|
|
|
|
############ Global variables ##################################
|
|
CTRX=None
|
|
config = configparser.ConfigParser()
|
|
config.read('UHRR.conf')
|
|
e="No"
|
|
|
|
############ 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'])
|
|
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 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()
|
|
|
|
@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 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()
|
|
|
|
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)
|
|
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 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.')
|
|
|
|
@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
|
|
print("Shutdown TRX")
|
|
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()
|
|
|
|
############ Main ##############
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
|
|
def get(self):
|
|
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")
|
|
|
|
class ConfigHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
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']+"""><br/><br/>""")
|
|
self.write("""If you whant 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']!="null"):
|
|
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><br/><br/>""")
|
|
|
|
self.write("""AUDIO inputdevice:<select name="AUDIO.inputdevice">""")
|
|
if(config['AUDIO']['inputdevice']!="null"):
|
|
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><br/><br/>""")
|
|
|
|
self.write("""[HAMLIB]<br/><br/>""")
|
|
self.write("""HAMLIB serial port:<select name="HAMLIB.rig_pathname">""")
|
|
if(config['HAMLIB']['rig_pathname']!="null"):
|
|
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><br/><br/>""")
|
|
|
|
self.write("""HAMLIB radio model:<select name="HAMLIB.rig_model">""")
|
|
if(config['HAMLIB']['rig_model']!="null"):
|
|
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><br/><br/>""")
|
|
|
|
self.write("""HAMLIB radio rate:<select name="HAMLIB.rig_rate">""")
|
|
if(config['HAMLIB']['rig_rate']!="null"):
|
|
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><br/><br/>""")
|
|
|
|
self.write("""HAMLIB auto tx poweroff:<select name="HAMLIB.trxautopower">""")
|
|
if(config['HAMLIB']['trxautopower']!="null"):
|
|
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><br/><br/>""")
|
|
|
|
self.write("""<input type="submit" value="Save & Restart server"><br/><br/></form>Possible problem:"""+e+"""</html>""")
|
|
|
|
def post(self):
|
|
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)
|
|
|
|
if __name__ == "__main__":
|
|
|
|
try:
|
|
threadloadWavdata = loadWavdata()
|
|
threadloadWavdata.start()
|
|
|
|
CTRX = TRXRIG()
|
|
|
|
threadticksTRXRIG = ticksTRXRIG()
|
|
threadticksTRXRIG.start()
|
|
|
|
if(config['HAMLIB']['trxautopower']=="True"):
|
|
threadsurveilTRX = threadtimeoutTRXshutdown()
|
|
threadsurveilTRX.start()
|
|
|
|
|
|
app = tornado.web.Application([
|
|
(r'/audioRX', AudioRXHandler),
|
|
(r'/audioTX', AudioTXHandler),
|
|
(r'/CTRX', ControlTRX),
|
|
(r'/CONFIG', ConfigHandler),
|
|
(r'/', MainHandler),
|
|
(r'/(.*)', tornado.web.StaticFileHandler, { 'path' : './www' })
|
|
],debug=bool(config['SERVER']['debug']), websocket_ping_interval=10)
|
|
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()
|