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_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"
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):
# 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
#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
# - decrease the chance of audio underruns
start_freq = center_freq
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.
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.
#access_log = "~/openwebrx_access.log"

View File

@ -28,7 +28,7 @@ class PropertyManager(object):
return PropertyManager.sharedInstance
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):
self.properties = {}
@ -52,10 +52,15 @@ class PropertyManager(object):
return self.getPropertyValue(name)
def __setitem__(self, name, value):
if not self.hasProperty(name):
self.add(name, Property())
self.getProperty(name).setValue(value)
def hasProperty(self, name):
return name in self.properties
def getProperty(self, name):
if not name in self.properties:
if not self.hasProperty(name):
self.add(name, Property())
return self.properties[name]
@ -66,6 +71,16 @@ class PropertyManager(object):
self.callbacks.append(callback)
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):
pass

View File

@ -1,7 +1,7 @@
import mimetypes
from owrx.websocket import WebSocketConnection
from owrx.config import PropertyManager
from owrx.source import SpectrumThread, DspManager, CpuUsageThread
from owrx.source import SpectrumThread, DspManager, CpuUsageThread, SdrService
import json
import os
from datetime import datetime
@ -62,8 +62,76 @@ class IndexController(AssetsController):
self.serve_file("index.wrx", "text/html")
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):
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):
self.conn.send(bytes([0x01]) + data)
def write_dsp_data(self, data):
@ -90,8 +158,6 @@ class WebSocketMessageHandler(object):
self.dsp = None
def handleTextMessage(self, conn, message):
pm = PropertyManager.getSharedInstance()
if (message[:16] == "SERVER DE CLIENT"):
# maybe put some more info in there? nothing to store yet.
self.handshake = "completed"
@ -99,31 +165,6 @@ class WebSocketMessageHandler(object):
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
if not self.handshake:
@ -132,20 +173,23 @@ class WebSocketMessageHandler(object):
try:
message = json.loads(message)
if message["type"] == "dspcontrol":
if "params" in message:
params = message["params"]
for key, value in params.items():
self.dsp.setProperty(key, value)
if "type" in message:
if message["type"] == "dspcontrol":
if "action" in message and message["action"] == "start":
self.client.startDsp()
if "action" in message and message["action"] == "start":
self.dsp.start()
if "params" in message:
params = message["params"]
self.client.setDspProperties(params)
if message["type"] == "config":
for key, value in message["params"].items():
# only the keys in the protected property manager can be overridden from the web
protected = pm.collect("samp_rate", "center_freq", "rf_gain", "rtl_type")
protected[key] = value
if message["type"] == "config":
if "params" in message:
self.client.setParams(message["params"])
if message["type"] == "setsdr":
if "params" in message:
self.client.setSdr(message["params"]["sdr"])
else:
print("received message without type: {0}".format(message))
except json.JSONDecodeError:
print("message is not json: {0}".format(message))
@ -155,10 +199,7 @@ class WebSocketMessageHandler(object):
def handleClose(self, conn):
if self.client:
SpectrumThread.getSharedInstance().remove_client(self.client)
CpuUsageThread.getSharedInstance().remove_client(self.client)
if self.dsp:
self.dsp.stop()
self.client.stopDsp()
class WebSocketController(Controller):
def handle_request(self):

View File

@ -6,47 +6,87 @@ import time
import os
import signal
class RtlNmuxSource(object):
types = {
"rtl_sdr": {
"command": "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -",
"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 SdrService(object):
sdrProps = None
sources = {}
lastPort = None
@staticmethod
def getNextPort():
pm = PropertyManager.getSharedInstance()
(start, end) = pm["iq_port_range"]
if SdrService.lastPort is None:
SdrService.lastPort = start
else:
SdrService.lastPort += 1
if SdrService.lastPort > end:
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):
self.props = props = PropertyManager.getSharedInstance().collect(
"rtl_type", "samp_rate", "nmux_memory", "iq_server_port", "center_freq", "ppm",
"rf_gain", "lna_gain", "rf_amp"
)
sdr_types = {
"rtl_sdr": {
"command": "rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -",
"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):
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.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):
if self.monitor: return
props = self.props
props = self.rtlProps
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"]))
return
self.params = RtlNmuxSource.types[props["rtl_type"]]
self.params = sdr_types[props["type"]]
start_sdr_command = self.params["command"].format(
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")
return
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)
print("[RtlNmuxSource] Started rtl source: " + cmd)
@ -75,38 +115,36 @@ class RtlNmuxSource(object):
def wait_for_process_to_end():
rc = self.process.wait()
print("[RtlNmuxSource] shut down with RC={0}".format(rc))
self.monitor = None
self.monitor = threading.Thread(target = wait_for_process_to_end)
self.monitor.start()
self.spectrumThread = SpectrumThread(self)
def stop(self):
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
self.monitor.join()
if "sleep" in self.params:
time.sleep(self.params["sleep"])
class SpectrumThread(threading.Thread):
sharedInstance = None
@staticmethod
def getSharedInstance():
if SpectrumThread.sharedInstance is None:
SpectrumThread.sharedInstance = SpectrumThread()
SpectrumThread.sharedInstance.start()
return SpectrumThread.sharedInstance
def __init__(self):
class SpectrumThread(object):
def __init__(self, sdrSource):
self.clients = []
self.doRun = True
super().__init__()
self.doRun = False
self.sdrSource = sdrSource
def start(self):
threading.Thread(target = self.run).start()
def run(self):
props = PropertyManager.getSharedInstance().collect(
props = self.sdrSource.props.collect(
"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.nc_port = props["iq_server_port"]
dsp.nc_port = self.sdrSource.getPort()
dsp.set_demodulator("fft")
props.getProperty("samp_rate").wire(dsp.set_samp_rate)
props.getProperty("fft_size").wire(dsp.set_fft_size)
@ -146,26 +184,32 @@ class SpectrumThread(threading.Thread):
def add_client(self, c):
self.clients.append(c)
if not self.doRun:
self.doRun = True
self.start()
def remove_client(self, c):
self.clients.remove(c)
try:
self.clients.remove(c)
except ValueError:
pass
if not self.clients:
self.shutdown()
def shutdown(self):
print("shutting down spectrum thread")
SpectrumThread.sharedInstance = None
self.doRun = False
class DspManager(object):
def __init__(self, handler):
def __init__(self, handler, sdrSource):
self.doRun = True
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",
"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()
#dsp_initialized=False
@ -175,7 +219,7 @@ class DspManager(object):
self.dsp.set_bpf(-4000,4000)
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_print_bufsizes = self.localProps["csdr_print_bufsizes"]
self.dsp.csdr_through = self.localProps["csdr_through"]
@ -322,7 +366,10 @@ class CpuUsageThread(threading.Thread):
self.clients.append(c)
def remove_client(self, c):
self.clients.remove(c)
try:
self.clients.remove(c)
except ValueError:
pass
if not self.clients:
self.shutdown()

View File

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