openwebrx-clone/owrx/controllers/settings/sdr.py

450 lines
17 KiB
Python
Raw Permalink Normal View History

from owrx.controllers.admin import AuthorizationMixin
from owrx.controllers.template import WebpageController
from owrx.controllers.settings import SettingsFormController
2021-02-27 19:48:37 +00:00
from owrx.source import SdrDeviceDescription, SdrDeviceDescriptionMissing, SdrClientClass
from owrx.config import Config
2021-02-27 19:48:37 +00:00
from owrx.connection import OpenWebRxReceiverClient
from owrx.controllers.settings import SettingsBreadcrumb
from owrx.form.section import Section
from urllib.parse import quote, unquote
from owrx.sdr import SdrService
2021-04-29 13:17:21 +00:00
from owrx.form.input import TextInput, DropdownInput, Option
from owrx.form.input.validator import RequiredValidator
2021-04-18 17:17:27 +00:00
from owrx.property import PropertyLayer
from owrx.breadcrumb import BreadcrumbMixin, Breadcrumb, BreadcrumbItem
from owrx.log import HistoryHandler
from abc import ABCMeta, abstractmethod
2021-04-18 17:04:43 +00:00
from uuid import uuid4
2021-02-19 17:18:25 +00:00
class SdrDeviceBreadcrumb(SettingsBreadcrumb):
def __init__(self):
super().__init__()
self.append(BreadcrumbItem("SDR device settings", "settings/sdr"))
class SdrDeviceListController(AuthorizationMixin, BreadcrumbMixin, WebpageController):
def template_variables(self):
variables = super().template_variables()
2021-02-18 22:05:43 +00:00
variables["content"] = self.render_devices()
2021-02-18 21:12:13 +00:00
variables["title"] = "SDR device settings"
2021-03-03 22:07:41 +00:00
variables["modal"] = ""
variables["error"] = ""
return variables
def get_breadcrumb(self):
return SdrDeviceBreadcrumb()
def render_devices(self):
2021-02-18 22:05:43 +00:00
def render_device(device_id, config):
sources = SdrService.getAllSources()
source = sources[device_id] if device_id in sources else None
2021-02-27 19:48:37 +00:00
additional_info = ""
state_info = "Unknown"
2021-02-27 19:48:37 +00:00
if source is not None:
profiles = source.getProfiles()
currentProfile = profiles[source.getProfileId()]
clients = {c: len(source.getClients(c)) for c in SdrClientClass}
clients = {c: v for c, v in clients.items() if v}
connections = len([c for c in source.getClients() if isinstance(c, OpenWebRxReceiverClient)])
additional_info = """
<div>{num_profiles} profile(s)</div>
2021-02-27 19:48:37 +00:00
<div>Current profile: {current_profile}</div>
<div>Clients: {clients}</div>
<div>Connections: {connections}</div>
""".format(
num_profiles=len(config["profiles"]),
2021-02-27 19:48:37 +00:00
current_profile=currentProfile["name"],
clients=", ".join("{cls}: {count}".format(cls=c.name, count=v) for c, v in clients.items()),
connections=connections,
)
state_info = ", ".join(
s
for s in [
str(source.getState()),
None if source.isEnabled() else "Disabled",
"Failed" if source.isFailed() else None,
]
if s is not None
)
2021-02-18 22:05:43 +00:00
return """
<li class="list-group-item">
2021-03-02 19:28:49 +00:00
<div class="row">
<div class="col-6">
<a href="{device_link}">
<h3>{device_name}</h3>
</a>
<div>State: {state}</div>
</div>
2021-03-03 21:33:37 +00:00
<div class="col-6">
{additional_info}
2021-03-03 21:33:37 +00:00
</div>
2021-03-02 19:28:49 +00:00
</div>
</li>
""".format(
device_name=config["name"] if config["name"] else "[Unnamed device]",
device_link="{}settings/sdr/{}".format(self.get_document_root(), quote(device_id)),
state=state_info,
2021-02-27 19:48:37 +00:00
additional_info=additional_info,
2021-02-18 22:05:43 +00:00
)
return """
2021-03-02 19:28:49 +00:00
<ul class="list-group list-group-flush sdr-device-list">
2021-02-18 22:05:43 +00:00
{devices}
</ul>
<div class="buttons container">
<a class="btn btn-success" href="newsdr">Add new device...</a>
</div>
2021-02-18 22:05:43 +00:00
""".format(
devices="".join(render_device(key, value) for key, value in Config.get()["sdrs"].items())
)
def indexAction(self):
2021-02-18 21:12:13 +00:00
self.serve_template("settings/general.html", **self.template_variables())
2021-02-23 17:32:23 +00:00
class SdrFormController(SettingsFormController, metaclass=ABCMeta):
def __init__(self, handler, request, options):
super().__init__(handler, request, options)
2021-02-19 17:45:29 +00:00
self.device_id, self.device = self._get_device()
def getTitle(self):
return self.device["name"]
def render_sections(self):
2021-03-05 16:43:15 +00:00
return """
{tabs}
<div class="tab-body">
{sections}
</div>
""".format(
tabs=self.render_tabs(),
sections=super().render_sections(),
)
def render_tabs(self):
return """
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {device_active}" href="{device_link}">{device_name}</a>
</li>
{profile_tabs}
<li class="nav-item">
2021-03-05 16:51:19 +00:00
<a href="{new_profile_link}" class="nav-link {new_profile_active}">New profile</a>
</li>
</ul>
""".format(
device_link="{}settings/sdr/{}".format(self.get_document_root(), quote(self.device_id)),
device_name=self.device["name"] if self.device["name"] else "[Unnamed device]",
device_active="active" if self.isDeviceActive() else "",
2021-03-05 16:51:19 +00:00
new_profile_active="active" if self.isNewProfileActive() else "",
new_profile_link="{}settings/sdr/{}/newprofile".format(self.get_document_root(), quote(self.device_id)),
profile_tabs="".join(
"""
<li class="nav-item">
<a class="nav-link {profile_active}" href="{profile_link}">{profile_name}</a>
</li>
""".format(
profile_link="{}settings/sdr/{}/profile/{}".format(
self.get_document_root(), quote(self.device_id), quote(profile_id)
),
profile_name=profile["name"] if profile["name"] else "[Unnamed profile]",
profile_active="active" if self.isProfileActive(profile_id) else "",
)
for profile_id, profile in self.device["profiles"].items()
),
)
def isDeviceActive(self) -> bool:
return False
def isProfileActive(self, profile_id) -> bool:
return False
2021-03-05 16:51:19 +00:00
def isNewProfileActive(self) -> bool:
return False
2021-02-19 17:45:29 +00:00
def store(self):
# need to overwrite the existing key in the config since the layering won't capture the changes otherwise
config = Config.get()
sdrs = config["sdrs"]
2021-02-23 17:41:49 +00:00
sdrs[self.device_id] = self.device
2021-02-19 17:45:29 +00:00
config["sdrs"] = sdrs
super().store()
2021-02-19 17:18:25 +00:00
2021-02-23 17:32:23 +00:00
def _get_device(self):
2021-02-23 17:41:49 +00:00
config = Config.get()
2021-02-23 17:32:23 +00:00
device_id = unquote(self.request.matches.group(1))
2021-02-23 17:41:49 +00:00
if device_id not in config["sdrs"]:
return None, None
2021-02-23 17:41:49 +00:00
return device_id, config["sdrs"][device_id]
2021-02-23 17:32:23 +00:00
2021-03-03 21:33:37 +00:00
class SdrFormControllerWithModal(SdrFormController, metaclass=ABCMeta):
2021-03-05 17:32:16 +00:00
def render_remove_button(self):
return ""
def render_buttons(self):
return self.render_remove_button() + super().render_buttons()
def buildModal(self):
return """
<div class="modal" id="deleteModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5>Please confirm</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Do you really want to delete this {object_type}?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<a type="button" class="btn btn-danger" href="{confirm_url}">Delete</a>
</div>
</div>
</div>
</div>
""".format(
object_type=self.getModalObjectType(),
confirm_url=self.getModalConfirmUrl(),
)
@abstractmethod
def getModalObjectType(self):
pass
@abstractmethod
def getModalConfirmUrl(self):
pass
2021-02-23 17:32:23 +00:00
2021-03-03 21:33:37 +00:00
class SdrDeviceController(SdrFormControllerWithModal):
def get_breadcrumb(self) -> Breadcrumb:
return SdrDeviceBreadcrumb().append(
BreadcrumbItem(self.device["name"], "settings/sdr/{}".format(self.device_id))
)
2021-02-23 17:32:23 +00:00
def getData(self):
return self.device
def getSections(self):
try:
description = SdrDeviceDescription.getByType(self.device["type"])
2021-02-23 17:32:23 +00:00
return [description.getDeviceSection()]
except SdrDeviceDescriptionMissing:
# TODO provide a generic interface that allows to switch the type
return []
2021-03-05 17:32:16 +00:00
def render_remove_button(self):
return """
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal">Remove device...</button>
"""
def isDeviceActive(self) -> bool:
return True
def indexAction(self):
if self.device is None:
self.send_response("device not found", code=404)
return
super().indexAction()
2021-02-23 17:32:23 +00:00
def processFormData(self):
if self.device is None:
self.send_response("device not found", code=404)
return
return super().processFormData()
def getModalObjectType(self):
return "SDR device"
def getModalConfirmUrl(self):
return "{}settings/deletesdr/{}".format(self.get_document_root(), quote(self.device_id))
2021-03-03 22:07:41 +00:00
def deleteDevice(self):
if self.device_id is None:
return self.send_response("device not found", code=404)
config = Config.get()
sdrs = config["sdrs"]
del sdrs[self.device_id]
# need to overwrite the existing key in the config since the layering won't capture the changes otherwise
config["sdrs"] = sdrs
2021-03-03 22:07:41 +00:00
config.store()
return self.send_redirect("{}settings/sdr".format(self.get_document_root()))
def render_sections(self):
handler = HistoryHandler.getHandler("owrx.source.{id}".format(id=self.device_id))
return """
{sections}
<div class="card mt-2">
<div class="card-header">Recent device log messages</div>
<div class="card-body">
<pre class="card-text device-log-messages">{messages}</pre>
</div>
</div>
""".format(
sections=super().render_sections(),
messages=handler.getFormattedHistory(),
)
class NewSdrDeviceController(SettingsFormController):
def __init__(self, handler, request, options):
super().__init__(handler, request, options)
self.data_layer = PropertyLayer(name="", type="", profiles=PropertyLayer())
2021-04-18 17:17:27 +00:00
self.device_id = str(uuid4())
def get_breadcrumb(self) -> Breadcrumb:
return SdrDeviceBreadcrumb().append(BreadcrumbItem("New device", "settings/sdr/newsdr"))
def getSections(self):
return [
Section(
"New device settings",
TextInput("name", "Device name", validator=RequiredValidator()),
DropdownInput(
"type",
"Device type",
[Option(sdr_type, name) for sdr_type, name in SdrDeviceDescription.getTypes().items()],
2021-04-17 16:00:13 +00:00
infotext="Note: Switching the type will not be possible after creation since the set of available "
+ "options is different for each type.<br />Note: This dropdown only shows device types that have "
+ "their requirements met. If a type is missing from the list, please check the feature report.",
),
)
]
def getTitle(self):
return "New device"
def getData(self):
2021-04-18 17:04:43 +00:00
return self.data_layer
def store(self):
# need to overwrite the existing key in the config since the layering won't capture the changes otherwise
config = Config.get()
sdrs = config["sdrs"]
2021-04-18 17:04:43 +00:00
# a uuid should be unique, so i'm not sure if there's a point in this check
2021-04-18 17:17:27 +00:00
if self.device_id in sdrs:
raise ValueError("device {} already exists!".format(self.device_id))
sdrs[self.device_id] = self.data_layer
config["sdrs"] = sdrs
super().store()
def getSuccessfulRedirect(self):
2021-04-18 17:17:27 +00:00
return "{}settings/sdr/{}".format(self.get_document_root(), quote(self.device_id))
2021-02-23 17:32:23 +00:00
2021-03-03 21:33:37 +00:00
class SdrProfileController(SdrFormControllerWithModal):
2021-02-23 17:32:23 +00:00
def __init__(self, handler, request, options):
super().__init__(handler, request, options)
self.profile_id, self.profile = self._get_profile()
def get_breadcrumb(self) -> Breadcrumb:
return (
SdrDeviceBreadcrumb()
.append(BreadcrumbItem(self.device["name"], "settings/sdr/{}".format(self.device_id)))
.append(
BreadcrumbItem(
self.profile["name"], "settings/sdr/{}/profile/{}".format(self.device_id, self.profile_id)
)
)
)
2021-02-23 17:32:23 +00:00
def getData(self):
return self.profile
def _get_profile(self):
2021-02-23 17:41:49 +00:00
if self.device is None:
2021-02-23 17:32:23 +00:00
return None
2021-02-23 17:41:49 +00:00
profile_id = unquote(self.request.matches.group(2))
if profile_id not in self.device["profiles"]:
2021-02-23 17:32:23 +00:00
return None
2021-02-23 17:41:49 +00:00
return profile_id, self.device["profiles"][profile_id]
2021-02-23 17:32:23 +00:00
def isProfileActive(self, profile_id) -> bool:
return profile_id == self.profile_id
2021-02-23 17:32:23 +00:00
def getSections(self):
try:
description = SdrDeviceDescription.getByType(self.device["type"])
return [description.getProfileSection()]
except SdrDeviceDescriptionMissing:
# TODO provide a generic interface that allows to switch the type
return []
def indexAction(self):
if self.profile is None:
self.send_response("profile not found", code=404)
return
super().indexAction()
def processFormData(self):
if self.profile is None:
self.send_response("profile not found", code=404)
return
return super().processFormData()
2021-03-05 17:32:16 +00:00
def render_remove_button(self):
return """
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal">Remove profile...</button>
"""
def getModalObjectType(self):
return "profile"
def getModalConfirmUrl(self):
2021-03-03 22:07:41 +00:00
return "{}settings/sdr/{}/deleteprofile/{}".format(
self.get_document_root(), quote(self.device_id), quote(self.profile_id)
)
2021-03-03 21:33:37 +00:00
2021-03-03 22:07:41 +00:00
def deleteProfile(self):
if self.profile_id is None:
return self.send_response("profile not found", code=404)
config = Config.get()
del self.device["profiles"][self.profile_id]
config.store()
2021-03-05 17:32:16 +00:00
return self.send_redirect("{}settings/sdr/{}".format(self.get_document_root(), quote(self.device_id)))
2021-03-03 22:07:41 +00:00
2021-03-03 21:33:37 +00:00
2021-03-05 17:32:16 +00:00
class NewProfileController(SdrProfileController):
2021-03-03 21:33:37 +00:00
def __init__(self, handler, request, options):
self.data_layer = PropertyLayer(name="")
2021-03-05 17:32:16 +00:00
super().__init__(handler, request, options)
def get_breadcrumb(self) -> Breadcrumb:
return (
SdrDeviceBreadcrumb()
.append(BreadcrumbItem(self.device["name"], "settings/sdr/{}".format(self.device_id)))
.append(BreadcrumbItem("New profile", "settings/sdr/{}/newprofile".format(self.device_id)))
)
2021-03-05 17:32:16 +00:00
def _get_profile(self):
2021-04-18 17:17:27 +00:00
return str(uuid4()), self.data_layer
2021-03-03 21:33:37 +00:00
2021-03-05 16:51:19 +00:00
def isNewProfileActive(self) -> bool:
return True
2021-03-03 21:33:37 +00:00
def store(self):
2021-04-18 17:17:27 +00:00
# a uuid should be unique, so i'm not sure if there's a point in this check
if self.profile_id in self.device["profiles"]:
raise ValueError("Profile {} already exists!".format(self.profile_id))
self.device["profiles"][self.profile_id] = self.data_layer
2021-03-03 21:33:37 +00:00
super().store()
def getSuccessfulRedirect(self):
return "{}settings/sdr/{}/profile/{}".format(
2021-04-18 17:17:27 +00:00
self.get_document_root(), quote(self.device_id), quote(self.profile_id)
2021-03-03 21:33:37 +00:00
)
2021-03-05 17:32:16 +00:00
def render_remove_button(self):
# new profile doesn't have a remove button
return ""