multi-sdr capabilities!

This commit is contained in:
Jakob Ketterl 2019-05-09 22:44:29 +02:00
parent bd627d77b7
commit 56ef86aab6
5 changed files with 228 additions and 114 deletions

View File

@ -71,12 +71,6 @@ fft_fps=9
fft_size=4096 #Should be power of 2 fft_size=4096 #Should be power of 2
fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
# samp_rate = 250000
samp_rate = 2400000
center_freq = 144250000
rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode.
ppm = 0
audio_compression="adpcm" #valid values: "adpcm", "none" audio_compression="adpcm" #valid values: "adpcm", "none"
fft_compression="adpcm" #valid values: "adpcm", "none" fft_compression="adpcm" #valid values: "adpcm", "none"
@ -104,7 +98,30 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR): # You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR):
# valid: "rtl_sdr", "sdrplay", "hackrf" # valid: "rtl_sdr", "sdrplay", "hackrf"
rtl_type = "rtl_sdr" #rtl_type = "sdrplay"
sdrs = {
"rtlsdr": {
"name": "RTL-SDR USB Stick",
"type": "rtl_sdr",
"center_freq": 438800000,
"rf_gain": 30,
"samp_rate": 2400000,
"ppm": 0,
"start_freq": 439275000,
"start_mod": "nfm"
},
"sdrplay": {
"name": "SDRPlay RSP2",
"type": "sdrplay",
"center_freq": 14150000,
"rf_gain": 30,
"samp_rate": 500000,
"ppm": 0,
"start_freq": 14070000,
"start_mod": "usb"
}
}
# >> RTL-SDR via rtl_sdr # >> RTL-SDR via rtl_sdr
#start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) #start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
@ -168,10 +185,7 @@ client_audio_buffer_size = 5
# - also increase the latency # - also increase the latency
# - decrease the chance of audio underruns # - decrease the chance of audio underruns
start_freq = center_freq iq_port_range = [4950, 4960] #TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
start_mod = "nfm" #nfm, am, lsb, usb, cw
iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
#access_log = "~/openwebrx_access.log" #access_log = "~/openwebrx_access.log"

View File

@ -28,7 +28,7 @@ class PropertyManager(object):
return PropertyManager.sharedInstance return PropertyManager.sharedInstance
def collect(self, *props): def collect(self, *props):
return PropertyManager(dict((name, self.getProperty(name)) for name in props)) return PropertyManager(dict((name, self.getProperty(name) if self.hasProperty(name) else Property()) for name in props))
def __init__(self, properties = None): def __init__(self, properties = None):
self.properties = {} self.properties = {}
@ -52,10 +52,15 @@ class PropertyManager(object):
return self.getPropertyValue(name) return self.getPropertyValue(name)
def __setitem__(self, name, value): def __setitem__(self, name, value):
if not self.hasProperty(name):
self.add(name, Property())
self.getProperty(name).setValue(value) self.getProperty(name).setValue(value)
def hasProperty(self, name):
return name in self.properties
def getProperty(self, name): def getProperty(self, name):
if not name in self.properties: if not self.hasProperty(name):
self.add(name, Property()) self.add(name, Property())
return self.properties[name] return self.properties[name]
@ -66,6 +71,16 @@ class PropertyManager(object):
self.callbacks.append(callback) self.callbacks.append(callback)
return self return self
def unwire(self, callback):
self.callbacks.remove(callback)
return self
def defaults(self, other_pm):
for (key, p) in self.properties.items():
if p.getValue() is None:
p.setValue(other_pm[key])
return self
class RequirementMissingException(Exception): class RequirementMissingException(Exception):
pass pass

View File

@ -1,7 +1,7 @@
import mimetypes import mimetypes
from owrx.websocket import WebSocketConnection from owrx.websocket import WebSocketConnection
from owrx.config import PropertyManager from owrx.config import PropertyManager
from owrx.source import SpectrumThread, DspManager, CpuUsageThread from owrx.source import SpectrumThread, DspManager, CpuUsageThread, SdrService
import json import json
import os import os
from datetime import datetime from datetime import datetime
@ -62,8 +62,76 @@ class IndexController(AssetsController):
self.serve_file("index.wrx", "text/html") self.serve_file("index.wrx", "text/html")
class OpenWebRxClient(object): class OpenWebRxClient(object):
config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level",
"waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps",
"audio_compression", "fft_compression", "max_clients", "start_mod",
"client_audio_buffer_size", "start_freq", "center_freq"]
def __init__(self, conn): def __init__(self, conn):
self.conn = conn self.conn = conn
self.dsp = None
self.sdr = None
self.configProps = None
pm = PropertyManager.getSharedInstance()
self.setSdr()
# send receiver info
receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps",
"photo_title", "photo_desc"]
receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
self.write_receiver_details(receiver_details)
CpuUsageThread.getSharedInstance().add_client(self)
def sendConfig(self, key, value):
config = dict((key, self.configProps[key]) for key in OpenWebRxClient.config_keys)
# TODO mathematical properties? hmmmm
config["start_offset_freq"] = self.configProps["start_freq"] - self.configProps["center_freq"]
self.write_config(config)
def setSdr(self, id = None):
self.stopDsp()
if self.configProps is not None:
self.configProps.unwire(self.sendConfig)
self.sdr = SdrService.getSource(id)
self.sdr.start()
# send initial config
self.configProps = self.sdr.getProps().collect(*OpenWebRxClient.config_keys).defaults(PropertyManager.getSharedInstance())
self.configProps.wire(self.sendConfig)
self.sendConfig(None, None)
self.sdr.spectrumThread.add_client(self)
def startDsp(self):
if self.dsp is None:
self.dsp = DspManager(self, self.sdr)
self.dsp.start()
def stopDsp(self):
if self.dsp is not None:
self.dsp.stop()
self.dsp = None
if self.sdr is not None:
self.sdr.spectrumThread.remove_client(self)
# TODO: this should be disabled somehow, just not with the dsp
#CpuUsageThread.getSharedInstance().remove_client(self)
def setParams(self, params):
# only the keys in the protected property manager can be overridden from the web
protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type") \
.defaults(PropertyManager.getSharedInstance())
for key, value in params.items():
protected[key] = value
def setDspProperties(self, params):
for key, value in params.items():
self.dsp.setProperty(key, value)
def write_spectrum_data(self, data): def write_spectrum_data(self, data):
self.conn.send(bytes([0x01]) + data) self.conn.send(bytes([0x01]) + data)
def write_dsp_data(self, data): def write_dsp_data(self, data):
@ -90,8 +158,6 @@ class WebSocketMessageHandler(object):
self.dsp = None self.dsp = None
def handleTextMessage(self, conn, message): def handleTextMessage(self, conn, message):
pm = PropertyManager.getSharedInstance()
if (message[:16] == "SERVER DE CLIENT"): if (message[:16] == "SERVER DE CLIENT"):
# maybe put some more info in there? nothing to store yet. # maybe put some more info in there? nothing to store yet.
self.handshake = "completed" self.handshake = "completed"
@ -99,31 +165,6 @@ class WebSocketMessageHandler(object):
self.client = OpenWebRxClient(conn) self.client = OpenWebRxClient(conn)
config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level",
"waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps",
"audio_compression", "fft_compression", "max_clients", "start_mod",
"client_audio_buffer_size", "start_freq", "center_freq"]
configProps = pm.collect(*config_keys)
def sendConfig(key, value):
config = dict((key, configProps[key]) for key in config_keys)
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
self.client.write_config(config)
configProps.wire(sendConfig)
sendConfig(None, None)
receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps",
"photo_title", "photo_desc"]
receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
self.client.write_receiver_details(receiver_details)
SpectrumThread.getSharedInstance().add_client(self.client)
CpuUsageThread.getSharedInstance().add_client(self.client)
self.dsp = DspManager(self.client)
return return
if not self.handshake: if not self.handshake:
@ -132,20 +173,23 @@ class WebSocketMessageHandler(object):
try: try:
message = json.loads(message) message = json.loads(message)
if message["type"] == "dspcontrol": if "type" in message:
if "params" in message: if message["type"] == "dspcontrol":
params = message["params"] if "action" in message and message["action"] == "start":
for key, value in params.items(): self.client.startDsp()
self.dsp.setProperty(key, value)
if "action" in message and message["action"] == "start": if "params" in message:
self.dsp.start() params = message["params"]
self.client.setDspProperties(params)
if message["type"] == "config": if message["type"] == "config":
for key, value in message["params"].items(): if "params" in message:
# only the keys in the protected property manager can be overridden from the web self.client.setParams(message["params"])
protected = pm.collect("samp_rate", "center_freq", "rf_gain", "rtl_type") if message["type"] == "setsdr":
protected[key] = value if "params" in message:
self.client.setSdr(message["params"]["sdr"])
else:
print("received message without type: {0}".format(message))
except json.JSONDecodeError: except json.JSONDecodeError:
print("message is not json: {0}".format(message)) print("message is not json: {0}".format(message))
@ -155,10 +199,7 @@ class WebSocketMessageHandler(object):
def handleClose(self, conn): def handleClose(self, conn):
if self.client: if self.client:
SpectrumThread.getSharedInstance().remove_client(self.client) self.client.stopDsp()
CpuUsageThread.getSharedInstance().remove_client(self.client)
if self.dsp:
self.dsp.stop()
class WebSocketController(Controller): class WebSocketController(Controller):
def handle_request(self): def handle_request(self):

View File

@ -6,47 +6,87 @@ import time
import os import os
import signal import signal
class RtlNmuxSource(object): class SdrService(object):
types = { sdrProps = None
"rtl_sdr": { sources = {}
"command": "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -", lastPort = None
"format_conversion": "csdr convert_u8_f" @staticmethod
}, def getNextPort():
"hackrf": { pm = PropertyManager.getSharedInstance()
"command": "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-", (start, end) = pm["iq_port_range"]
"format_conversion": "csdr convert_s8_f" if SdrService.lastPort is None:
}, SdrService.lastPort = start
"sdrplay": { else:
"command": "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -", SdrService.lastPort += 1
"format_conversion": None, if SdrService.lastPort > end:
"sleep": 1 raise IndexError("no more available ports to start more sdrs")
} return SdrService.lastPort
} @staticmethod
def getSource(id = None):
if SdrService.sdrProps is None:
pm = PropertyManager.getSharedInstance()
def loadIntoPropertyManager(dict: dict):
propertyManager = PropertyManager()
for (name, value) in dict.items():
propertyManager[name] = value
return propertyManager
SdrService.sdrProps = dict((name, loadIntoPropertyManager(value)) for (name, value) in pm["sdrs"].items())
print(SdrService.sdrProps)
if id is None:
# TODO: configure default sdr in config? right now it will pick the first one off the list.
id = list(SdrService.sdrProps.keys())[0]
if not id in SdrService.sources:
SdrService.sources[id] = SdrSource(SdrService.sdrProps[id], SdrService.getNextPort())
return SdrService.sources[id]
def setup(self): sdr_types = {
self.props = props = PropertyManager.getSharedInstance().collect( "rtl_sdr": {
"rtl_type", "samp_rate", "nmux_memory", "iq_server_port", "center_freq", "ppm", "command": "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -",
"rf_gain", "lna_gain", "rf_amp" "format_conversion": "csdr convert_u8_f"
) },
"hackrf": {
"command": "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-",
"format_conversion": "csdr convert_s8_f"
},
"sdrplay": {
"command": "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -",
"format_conversion": None,
"sleep": 1
}
}
class SdrSource(object):
def __init__(self, props, port):
self.props = props
self.rtlProps = self.props.collect(
"type", "samp_rate", "nmux_memory", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp"
).defaults(PropertyManager.getSharedInstance())
def restart(name, value): def restart(name, value):
print("restarting rtl source due to property change: {0} changed to {1}".format(name, value)) print("restarting sdr source due to property change: {0} changed to {1}".format(name, value))
self.stop() self.stop()
self.start() self.start()
props.wire(restart) self.rtlProps.wire(restart)
self.port = port
self.monitor = None
self.start() def getProps(self):
return self.props
def getPort(self):
return self.port
def start(self): def start(self):
if self.monitor: return
props = self.props props = self.rtlProps
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
if not featureDetector.is_available(props["rtl_type"]): if not featureDetector.is_available(props["type"]):
print("The RTL source type {0} is not available. please check requirements.".format(props["rtl_type"])) print("The RTL source type {0} is not available. please check requirements.".format(props["rtl_type"]))
return return
self.params = RtlNmuxSource.types[props["rtl_type"]] self.params = sdr_types[props["type"]]
start_sdr_command = self.params["command"].format( start_sdr_command = self.params["command"].format(
samp_rate = props["samp_rate"], samp_rate = props["samp_rate"],
@ -67,7 +107,7 @@ class RtlNmuxSource(object):
print("[RtlNmuxSource] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py") print("[RtlNmuxSource] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py")
return return
print("[RtlNmuxSource] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)) print("[RtlNmuxSource] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, props["iq_server_port"]) cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port)
self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp) self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
print("[RtlNmuxSource] Started rtl source: " + cmd) print("[RtlNmuxSource] Started rtl source: " + cmd)
@ -75,38 +115,36 @@ class RtlNmuxSource(object):
def wait_for_process_to_end(): def wait_for_process_to_end():
rc = self.process.wait() rc = self.process.wait()
print("[RtlNmuxSource] shut down with RC={0}".format(rc)) print("[RtlNmuxSource] shut down with RC={0}".format(rc))
self.monitor = None
self.monitor = threading.Thread(target = wait_for_process_to_end) self.monitor = threading.Thread(target = wait_for_process_to_end)
self.monitor.start() self.monitor.start()
self.spectrumThread = SpectrumThread(self)
def stop(self): def stop(self):
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
self.monitor.join() self.monitor.join()
if "sleep" in self.params: if "sleep" in self.params:
time.sleep(self.params["sleep"]) time.sleep(self.params["sleep"])
class SpectrumThread(threading.Thread): class SpectrumThread(object):
sharedInstance = None def __init__(self, sdrSource):
@staticmethod
def getSharedInstance():
if SpectrumThread.sharedInstance is None:
SpectrumThread.sharedInstance = SpectrumThread()
SpectrumThread.sharedInstance.start()
return SpectrumThread.sharedInstance
def __init__(self):
self.clients = [] self.clients = []
self.doRun = True self.doRun = False
super().__init__() self.sdrSource = sdrSource
def start(self):
threading.Thread(target = self.run).start()
def run(self): def run(self):
props = PropertyManager.getSharedInstance().collect( props = self.sdrSource.props.collect(
"samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression", "samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression",
"csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "iq_server_port" "csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through"
) ).defaults(PropertyManager.getSharedInstance())
dsp = csdr.dsp() dsp = csdr.dsp()
dsp.nc_port = props["iq_server_port"] dsp.nc_port = self.sdrSource.getPort()
dsp.set_demodulator("fft") dsp.set_demodulator("fft")
props.getProperty("samp_rate").wire(dsp.set_samp_rate) props.getProperty("samp_rate").wire(dsp.set_samp_rate)
props.getProperty("fft_size").wire(dsp.set_fft_size) props.getProperty("fft_size").wire(dsp.set_fft_size)
@ -146,26 +184,32 @@ class SpectrumThread(threading.Thread):
def add_client(self, c): def add_client(self, c):
self.clients.append(c) self.clients.append(c)
if not self.doRun:
self.doRun = True
self.start()
def remove_client(self, c): def remove_client(self, c):
self.clients.remove(c) try:
self.clients.remove(c)
except ValueError:
pass
if not self.clients: if not self.clients:
self.shutdown() self.shutdown()
def shutdown(self): def shutdown(self):
print("shutting down spectrum thread") print("shutting down spectrum thread")
SpectrumThread.sharedInstance = None
self.doRun = False self.doRun = False
class DspManager(object): class DspManager(object):
def __init__(self, handler): def __init__(self, handler, sdrSource):
self.doRun = True self.doRun = True
self.handler = handler self.handler = handler
self.sdrSource = sdrSource
self.localProps = PropertyManager.getSharedInstance().collect( self.localProps = self.sdrSource.getProps().collect(
"audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize", "audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize",
"csdr_print_bufsizes", "csdr_through", "iq_server_port", "digimodes_enable", "samp_rate" "csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate"
) ).defaults(PropertyManager.getSharedInstance())
self.dsp = csdr.dsp() self.dsp = csdr.dsp()
#dsp_initialized=False #dsp_initialized=False
@ -175,7 +219,7 @@ class DspManager(object):
self.dsp.set_bpf(-4000,4000) self.dsp.set_bpf(-4000,4000)
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size) self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size)
self.dsp.nc_port = self.localProps["iq_server_port"] self.dsp.nc_port = self.sdrSource.getPort()
self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"] self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"]
self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"] self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
self.dsp.csdr_through = self.localProps["csdr_through"] self.dsp.csdr_through = self.localProps["csdr_through"]
@ -322,7 +366,10 @@ class CpuUsageThread(threading.Thread):
self.clients.append(c) self.clients.append(c)
def remove_client(self, c): def remove_client(self, c):
self.clients.remove(c) try:
self.clients.remove(c)
except ValueError:
pass
if not self.clients: if not self.clients:
self.shutdown() self.shutdown()

View File

@ -1,7 +1,7 @@
from http.server import HTTPServer from http.server import HTTPServer
from owrx.http import RequestHandler from owrx.http import RequestHandler
from owrx.config import PropertyManager, FeatureDetector, RequirementMissingException from owrx.config import PropertyManager, FeatureDetector, RequirementMissingException
from owrx.source import RtlNmuxSource from owrx.source import SdrService
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
class ThreadedHttpServer(ThreadingMixIn, HTTPServer): class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
@ -19,7 +19,7 @@ def main():
pm = PropertyManager.getSharedInstance() pm = PropertyManager.getSharedInstance()
for name, value in cfg.__dict__.items(): for name, value in cfg.__dict__.items():
if (name.startswith("__")): continue if (name.startswith("__")): continue
pm.getProperty(name).setValue(value) pm[name] = value
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
if not featureDetector.is_available("core"): if not featureDetector.is_available("core"):
@ -28,9 +28,6 @@ def main():
print(", ".join(featureDetector.get_requirements("core"))) print(", ".join(featureDetector.get_requirements("core")))
return return
if (pm.getPropertyValue("start_rtl_thread")):
RtlNmuxSource().setup()
server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler) server = ThreadedHttpServer(('0.0.0.0', pm.getPropertyValue("web_port")), RequestHandler)
server.serve_forever() server.serve_forever()