diff --git a/openwebrx.py b/openwebrx.py index b89d7e5..e41d6c7 100644 --- a/openwebrx.py +++ b/openwebrx.py @@ -1,6 +1,7 @@ from http.server import HTTPServer from owrx.http import RequestHandler -from owrx.config import PropertyManager, FeatureDetector +from owrx.config import PropertyManager +from owrx.feature import FeatureDetector from owrx.source import SdrService from socketserver import ThreadingMixIn from owrx.sdrhu import SdrHuUpdater diff --git a/owrx/config.py b/owrx/config.py index a117228..8fb6513 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -1,8 +1,7 @@ -import os - import logging logger = logging.getLogger(__name__) + class Property(object): def __init__(self, value = None): self.value = value @@ -99,67 +98,3 @@ class PropertyManager(object): continue self[name] = value return self - - -class UnknownFeatureException(Exception): - pass - -class RequirementMissingException(Exception): - pass - -class FeatureDetector(object): - features = { - "core": [ "csdr", "nmux" ], - "rtl_sdr": [ "rtl_sdr" ], - "sdrplay": [ "rx_tools" ], - "hackrf": [ "hackrf_transfer" ] - } - - def is_available(self, feature): - return self.has_requirements(self.get_requirements(feature)) - - def get_requirements(self, feature): - try: - return FeatureDetector.features[feature] - except KeyError: - raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature)) - - def has_requirements(self, requirements): - passed = True - for requirement in requirements: - methodname = "has_" + requirement - if hasattr(self, methodname) and callable(getattr(self, methodname)): - passed = passed and getattr(self, methodname)() - else: - logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) - return passed - - def has_csdr(self): - return os.system("csdr 2> /dev/null") != 32512 - - def has_nmux(self): - return os.system("nmux --help 2> /dev/null") != 32512 - - def has_rtl_sdr(self): - return os.system("rtl_sdr --help 2> /dev/null") != 32512 - - def has_rx_tools(self): - return os.system("rx_sdr --help 2> /dev/null") != 32512 - - """ - To use a HackRF, compile the HackRF host tools from its "stdout" branch: - git clone https://github.com/mossmann/hackrf/ - cd hackrf - git fetch - git checkout origin/stdout - cd host - mkdir build - cd build - cmake .. -DINSTALL_UDEV_RULES=ON - make - sudo make install - """ - def has_hackrf_transfer(self): - # TODO i don't have a hackrf, so somebody doublecheck this. - # TODO also check if it has the stdout feature - return os.system("hackrf_transfer --help 2> /dev/null") != 32512 diff --git a/owrx/connection.py b/owrx/connection.py new file mode 100644 index 0000000..a0442b8 --- /dev/null +++ b/owrx/connection.py @@ -0,0 +1,180 @@ +from owrx.config import PropertyManager +from owrx.source import DspManager, CpuUsageThread, SdrService, ClientReporterThread +import json + +import logging +logger = logging.getLogger(__name__) + +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", "mathbox_waterfall_colors", + "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] + def __init__(self, conn): + self.conn = conn + + ClientReporterThread.getSharedInstance().addClient(self) + + 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) + + profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()] + self.write_profiles(profiles) + + 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): + next = SdrService.getSource(id) + if (next == self.sdr): + return + + self.stopDsp() + + if self.configProps is not None: + self.configProps.unwire(self.sendConfig) + + self.sdr = next + + # 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.addSpectrumClient(self) + + def startDsp(self): + if self.dsp is None: + self.dsp = DspManager(self, self.sdr) + self.dsp.start() + + def close(self): + self.stopDsp() + CpuUsageThread.getSharedInstance().remove_client(self) + try: + ClientReporterThread.getSharedInstance().removeClient(self) + except ValueError: + pass + logger.debug("connection closed") + + def stopDsp(self): + if self.dsp is not None: + self.dsp.stop() + self.dsp = None + if self.sdr is not None: + self.sdr.removeSpectrumClient(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 protected_send(self, data): + try: + self.conn.send(data) + # these exception happen when the socket is closed + except OSError: + self.close() + except ValueError: + self.close() + + def write_spectrum_data(self, data): + self.protected_send(bytes([0x01]) + data) + def write_dsp_data(self, data): + self.protected_send(bytes([0x02]) + data) + def write_s_meter_level(self, level): + self.protected_send({"type":"smeter","value":level}) + def write_cpu_usage(self, usage): + self.protected_send({"type":"cpuusage","value":usage}) + def write_clients(self, clients): + self.protected_send({"type":"clients","value":clients}) + def write_secondary_fft(self, data): + self.protected_send(bytes([0x03]) + data) + def write_secondary_demod(self, data): + self.protected_send(bytes([0x04]) + data) + def write_secondary_dsp_config(self, cfg): + self.protected_send({"type":"secondary_config", "value":cfg}) + def write_config(self, cfg): + self.protected_send({"type":"config","value":cfg}) + def write_receiver_details(self, details): + self.protected_send({"type":"receiver_details","value":details}) + def write_profiles(self, profiles): + self.protected_send({"type":"profiles","value":profiles}) + +class WebSocketMessageHandler(object): + def __init__(self): + self.handshake = None + self.client = None + self.dsp = None + + def handleTextMessage(self, conn, message): + if (message[:16] == "SERVER DE CLIENT"): + # maybe put some more info in there? nothing to store yet. + self.handshake = "completed" + logger.debug("client connection intitialized") + + self.client = OpenWebRxClient(conn) + + return + + if not self.handshake: + logger.warning("not answering client request since handshake is not complete") + return + + try: + message = json.loads(message) + if "type" in message: + if message["type"] == "dspcontrol": + if "action" in message and message["action"] == "start": + self.client.startDsp() + + if "params" in message: + params = message["params"] + self.client.setDspProperties(params) + + 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"]) + if message["type"] == "selectprofile": + if "params" in message and "profile" in message["params"]: + profile = message["params"]["profile"].split("|") + self.client.setSdr(profile[0]) + self.client.sdr.activateProfile(profile[1]) + else: + logger.warning("received message without type: {0}".format(message)) + + except json.JSONDecodeError: + logger.warning("message is not json: {0}".format(message)) + + def handleBinaryMessage(self, conn, data): + logger.error("unsupported binary message, discarding") + + def handleClose(self, conn): + if self.client: + self.client.close() diff --git a/owrx/controllers.py b/owrx/controllers.py index 366eb3a..a41015a 100644 --- a/owrx/controllers.py +++ b/owrx/controllers.py @@ -1,11 +1,11 @@ +import os import mimetypes +from datetime import datetime from owrx.websocket import WebSocketConnection from owrx.config import PropertyManager -from owrx.source import DspManager, CpuUsageThread, SdrService, ClientReporterThread +from owrx.source import ClientReporterThread +from owrx.connection import WebSocketMessageHandler from owrx.version import openwebrx_version -import json -import os -from datetime import datetime import logging logger = logging.getLogger(__name__) @@ -79,180 +79,6 @@ class IndexController(AssetsController): def handle_request(self): 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", "mathbox_waterfall_colors", - "mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"] - def __init__(self, conn): - self.conn = conn - - ClientReporterThread.getSharedInstance().addClient(self) - - 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) - - profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()] - self.write_profiles(profiles) - - 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): - next = SdrService.getSource(id) - if (next == self.sdr): - return - - self.stopDsp() - - if self.configProps is not None: - self.configProps.unwire(self.sendConfig) - - self.sdr = next - - # 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.addSpectrumClient(self) - - def startDsp(self): - if self.dsp is None: - self.dsp = DspManager(self, self.sdr) - self.dsp.start() - - def close(self): - self.stopDsp() - CpuUsageThread.getSharedInstance().remove_client(self) - try: - ClientReporterThread.getSharedInstance().removeClient(self) - except ValueError: - pass - logger.debug("connection closed") - - def stopDsp(self): - if self.dsp is not None: - self.dsp.stop() - self.dsp = None - if self.sdr is not None: - self.sdr.removeSpectrumClient(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 protected_send(self, data): - try: - self.conn.send(data) - # these exception happen when the socket is closed - except OSError: - self.close() - except ValueError: - self.close() - - def write_spectrum_data(self, data): - self.protected_send(bytes([0x01]) + data) - def write_dsp_data(self, data): - self.protected_send(bytes([0x02]) + data) - def write_s_meter_level(self, level): - self.protected_send({"type":"smeter","value":level}) - def write_cpu_usage(self, usage): - self.protected_send({"type":"cpuusage","value":usage}) - def write_clients(self, clients): - self.protected_send({"type":"clients","value":clients}) - def write_secondary_fft(self, data): - self.protected_send(bytes([0x03]) + data) - def write_secondary_demod(self, data): - self.protected_send(bytes([0x04]) + data) - def write_secondary_dsp_config(self, cfg): - self.protected_send({"type":"secondary_config", "value":cfg}) - def write_config(self, cfg): - self.protected_send({"type":"config","value":cfg}) - def write_receiver_details(self, details): - self.protected_send({"type":"receiver_details","value":details}) - def write_profiles(self, profiles): - self.protected_send({"type":"profiles","value":profiles}) - -class WebSocketMessageHandler(object): - def __init__(self): - self.handshake = None - self.client = None - self.dsp = None - - def handleTextMessage(self, conn, message): - if (message[:16] == "SERVER DE CLIENT"): - # maybe put some more info in there? nothing to store yet. - self.handshake = "completed" - logger.debug("client connection intitialized") - - self.client = OpenWebRxClient(conn) - - return - - if not self.handshake: - logger.warning("not answering client request since handshake is not complete") - return - - try: - message = json.loads(message) - if "type" in message: - if message["type"] == "dspcontrol": - if "action" in message and message["action"] == "start": - self.client.startDsp() - - if "params" in message: - params = message["params"] - self.client.setDspProperties(params) - - 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"]) - if message["type"] == "selectprofile": - if "params" in message and "profile" in message["params"]: - profile = message["params"]["profile"].split("|") - self.client.setSdr(profile[0]) - self.client.sdr.activateProfile(profile[1]) - else: - logger.warning("received message without type: {0}".format(message)) - - except json.JSONDecodeError: - logger.warning("message is not json: {0}".format(message)) - - def handleBinaryMessage(self, conn, data): - logger.error("unsupported binary message, discarding") - - def handleClose(self, conn): - if self.client: - self.client.close() - class WebSocketController(Controller): def handle_request(self): conn = WebSocketConnection(self.handler, WebSocketMessageHandler()) diff --git a/owrx/feature.py b/owrx/feature.py new file mode 100644 index 0000000..83f9232 --- /dev/null +++ b/owrx/feature.py @@ -0,0 +1,65 @@ +import os + +import logging +logger = logging.getLogger(__name__) + + +class UnknownFeatureException(Exception): + pass + +class FeatureDetector(object): + features = { + "core": [ "csdr", "nmux" ], + "rtl_sdr": [ "rtl_sdr" ], + "sdrplay": [ "rx_tools" ], + "hackrf": [ "hackrf_transfer" ] + } + + def is_available(self, feature): + return self.has_requirements(self.get_requirements(feature)) + + def get_requirements(self, feature): + try: + return FeatureDetector.features[feature] + except KeyError: + raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature)) + + def has_requirements(self, requirements): + passed = True + for requirement in requirements: + methodname = "has_" + requirement + if hasattr(self, methodname) and callable(getattr(self, methodname)): + passed = passed and getattr(self, methodname)() + else: + logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) + return passed + + def has_csdr(self): + return os.system("csdr 2> /dev/null") != 32512 + + def has_nmux(self): + return os.system("nmux --help 2> /dev/null") != 32512 + + def has_rtl_sdr(self): + return os.system("rtl_sdr --help 2> /dev/null") != 32512 + + def has_rx_tools(self): + return os.system("rx_sdr --help 2> /dev/null") != 32512 + + """ + To use a HackRF, compile the HackRF host tools from its "stdout" branch: + git clone https://github.com/mossmann/hackrf/ + cd hackrf + git fetch + git checkout origin/stdout + cd host + mkdir build + cd build + cmake .. -DINSTALL_UDEV_RULES=ON + make + sudo make install + """ + def has_hackrf_transfer(self): + # TODO i don't have a hackrf, so somebody doublecheck this. + # TODO also check if it has the stdout feature + return os.system("hackrf_transfer --help 2> /dev/null") != 32512 diff --git a/owrx/source.py b/owrx/source.py index f918e54..c45d12f 100644 --- a/owrx/source.py +++ b/owrx/source.py @@ -1,5 +1,6 @@ import subprocess -from owrx.config import PropertyManager, FeatureDetector, UnknownFeatureException +from owrx.config import PropertyManager +from owrx.feature import FeatureDetector, UnknownFeatureException import threading import csdr import time