openwebrx-clone/owrx/controllers/settings.py

421 lines
16 KiB
Python
Raw Normal View History

from owrx.controllers.template import WebpageController
from owrx.controllers.admin import AuthorizationMixin
2021-02-11 18:31:44 +00:00
from owrx.config.core import CoreConfig
from owrx.config import Config
2020-03-26 22:04:02 +00:00
from urllib.parse import parse_qs
2020-03-29 18:14:34 +00:00
from owrx.form import (
TextInput,
NumberInput,
FloatInput,
LocationInput,
TextAreaInput,
CheckboxInput,
DropdownInput,
Option,
ServicesCheckboxInput,
2020-04-25 15:33:30 +00:00
Js8ProfileCheckboxInput,
2021-02-07 21:49:11 +00:00
MultiCheckboxInput,
2020-03-29 18:14:34 +00:00
)
2021-02-08 19:30:12 +00:00
from owrx.form.converter import OptionalConverter
from owrx.form.receiverid import ReceiverKeysConverter
from owrx.form.aprs import AprsBeaconSymbols, AprsAntennaDirections
from owrx.form.wfm import WfmTauValues
from owrx.form.wsjt import Q65ModeMatrix
from owrx.form.gfx import AvatarInput, TopPhotoInput
2020-05-10 18:18:42 +00:00
from urllib.parse import quote
2021-02-07 21:49:11 +00:00
from owrx.wsjt import Fst4Profile, Fst4wProfile
2020-05-10 18:18:42 +00:00
import json
2020-03-26 22:04:02 +00:00
import logging
import shutil
2021-02-10 20:29:46 +00:00
import os
2021-02-10 21:24:43 +00:00
from glob import glob
2020-03-26 22:04:02 +00:00
logger = logging.getLogger(__name__)
2020-03-26 20:52:34 +00:00
2020-03-27 00:14:38 +00:00
class Section(object):
def __init__(self, title, *inputs):
self.title = title
self.inputs = inputs
def render_inputs(self):
2020-03-26 21:08:24 +00:00
config = Config.get()
2020-03-27 00:14:38 +00:00
return "".join([i.render(config) for i in self.inputs])
def render(self):
2020-03-26 22:04:02 +00:00
return """
2021-02-07 21:09:06 +00:00
<div class="col-12 settings-section">
2020-03-27 00:14:38 +00:00
<h3 class="settings-header">
{title}
</h3>
2020-03-26 22:04:02 +00:00
{inputs}
2020-03-27 00:14:38 +00:00
</div>
2020-03-27 20:11:33 +00:00
""".format(
title=self.title, inputs=self.render_inputs()
)
2020-03-27 00:14:38 +00:00
def parse(self, data):
return {k: v for i in self.inputs for k, v in i.parse(data).items()}
class SettingsController(AuthorizationMixin, WebpageController):
2020-04-25 19:42:00 +00:00
def indexAction(self):
self.serve_template("settings.html", **self.template_variables())
class SdrSettingsController(AuthorizationMixin, WebpageController):
2020-04-26 00:15:19 +00:00
def template_variables(self):
variables = super().template_variables()
variables["devices"] = self.render_devices()
return variables
def render_devices(self):
2020-05-10 18:18:42 +00:00
return "".join(self.render_device(key, value) for key, value in Config.get()["sdrs"].items())
def render_device(self, device_id, config):
return """
<div class="card device bg-dark text-white">
<div class="card-header">
{device_name}
</div>
<div class="card-body">
{form}
2020-04-26 00:15:19 +00:00
</div>
2020-05-10 18:18:42 +00:00
</div>
2021-01-20 16:01:46 +00:00
""".format(
device_name=config["name"], form=self.render_form(device_id, config)
)
2020-05-10 18:18:42 +00:00
def render_form(self, device_id, config):
return """
<form class="sdrdevice" data-config="{formdata}"></form>
2021-01-20 16:01:46 +00:00
""".format(
device_id=device_id, formdata=quote(json.dumps(config))
)
2020-04-26 00:15:19 +00:00
2020-04-25 19:55:52 +00:00
def indexAction(self):
self.serve_template("sdrsettings.html", **self.template_variables())
class GeneralSettingsController(AuthorizationMixin, WebpageController):
2020-03-27 00:14:38 +00:00
sections = [
Section(
2021-02-07 16:36:44 +00:00
"Receiver information",
2020-03-27 00:14:38 +00:00
TextInput("receiver_name", "Receiver name"),
TextInput("receiver_location", "Receiver location"),
2020-03-29 18:14:34 +00:00
NumberInput(
"receiver_asl",
"Receiver elevation",
append="meters above mean sea level",
2020-03-29 18:14:34 +00:00
),
2020-03-27 00:14:38 +00:00
TextInput("receiver_admin", "Receiver admin"),
LocationInput("receiver_gps", "Receiver coordinates"),
TextInput("photo_title", "Photo title"),
TextAreaInput("photo_desc", "Photo description"),
),
2021-02-08 22:29:24 +00:00
Section(
"Receiver images",
AvatarInput(
"receiver_avatar",
"Receiver Avatar",
2021-02-09 17:06:32 +00:00
infotext="For performance reasons, images are cached. "
+ "It can take a few hours until they appear on the site.",
2021-02-08 22:29:24 +00:00
),
TopPhotoInput(
"receiver_top_photo",
"Receiver Panorama",
2021-02-09 17:06:32 +00:00
infotext="For performance reasons, images are cached. "
+ "It can take a few hours until they appear on the site.",
),
2021-02-08 22:29:24 +00:00
),
2021-02-12 23:52:08 +00:00
Section(
"Receiver limits",
NumberInput(
"max_clients",
"Maximum number of clients",
),
),
2021-02-07 17:04:46 +00:00
Section(
"Receiver listings",
2021-02-07 20:45:02 +00:00
TextAreaInput(
2021-02-07 17:04:46 +00:00
"receiver_keys",
"Receiver keys",
converter=ReceiverKeysConverter(),
2021-02-07 17:04:46 +00:00
infotext="Put the keys you receive on listing sites (e.g. "
+ '<a href="https://www.receiverbook.de">Receiverbook</a>) here, one per line',
),
),
2020-03-27 20:11:33 +00:00
Section(
"Waterfall settings",
2020-03-27 21:00:10 +00:00
NumberInput(
2020-03-27 20:11:33 +00:00
"fft_fps",
"FFT speed",
2020-03-27 20:11:33 +00:00
infotext="This setting specifies how many lines are being added to the waterfall per second. "
+ "Higher values will give you a faster waterfall, but will also use more CPU.",
append="frames per second",
2020-03-27 20:11:33 +00:00
),
NumberInput("fft_size", "FFT size", append="bins"),
2020-03-27 21:00:10 +00:00
FloatInput(
2020-03-27 20:11:33 +00:00
"fft_voverlap_factor",
"FFT vertical overlap factor",
infotext="If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the "
+ "diagram.",
),
NumberInput("waterfall_min_level", "Lowest waterfall level", append="dBFS"),
NumberInput("waterfall_max_level", "Highest waterfall level", append="dBFS"),
2020-03-27 20:11:33 +00:00
),
Section(
"Compression",
2020-03-29 18:14:34 +00:00
DropdownInput(
"audio_compression",
"Audio compression",
2021-01-20 16:01:46 +00:00
options=[
Option("adpcm", "ADPCM"),
Option("none", "None"),
],
2020-03-29 18:14:34 +00:00
),
DropdownInput(
"fft_compression",
"Waterfall compression",
2021-01-20 16:01:46 +00:00
options=[
Option("adpcm", "ADPCM"),
Option("none", "None"),
],
2020-03-29 18:14:34 +00:00
),
2020-03-27 20:11:33 +00:00
),
Section(
"Digimodes",
CheckboxInput("digimodes_enable", "", checkboxText="Enable Digimodes"),
NumberInput("digimodes_fft_size", "Digimodes FFT size", append="bins"),
2020-03-27 20:11:33 +00:00
),
Section(
"Demodulator settings",
NumberInput(
"squelch_auto_margin",
"Auto-Squelch threshold",
infotext="Offset to be added to the current signal level when using the auto-squelch",
append="dB",
),
DropdownInput(
"wfm_deemphasis_tau",
"Tau setting for WFM (broadcast FM) deemphasis",
WfmTauValues,
infotext='See <a href="https://en.wikipedia.org/wiki/FM_broadcasting#Pre-emphasis_and_de-emphasis">'
+ "this Wikipedia article</a> for more information",
),
),
Section(
"Display settings",
NumberInput(
"frequency_display_precision",
"Frequency display precision",
2021-02-07 21:49:11 +00:00
infotext="Number of decimal digits to show on the frequency display",
),
),
2020-03-27 20:11:33 +00:00
Section(
"Digital voice",
2020-03-27 21:00:10 +00:00
NumberInput(
2020-03-27 20:11:33 +00:00
"digital_voice_unvoiced_quality",
"Quality of unvoiced sounds in synthesized voice",
infotext="Determines the quality, and thus the cpu usage, for the ambe codec used by digital voice"
2020-03-29 18:14:34 +00:00
+ "modes.<br />If you're running on a Raspi (up to 3B+) you should leave this set at 1",
2020-03-27 20:11:33 +00:00
),
CheckboxInput(
"digital_voice_dmr_id_lookup",
"DMR id lookup",
2020-03-29 18:14:34 +00:00
checkboxText="Enable lookup of DMR ids in the radioid database to show callsigns and names",
2020-03-27 20:11:33 +00:00
),
),
Section(
"Map settings",
TextInput(
"google_maps_api_key",
"Google Maps API key",
infotext="Google Maps requires an API key, check out "
+ '<a href="https://developers.google.com/maps/documentation/embed/get-api-key" target="_blank">'
2020-03-29 18:14:34 +00:00
+ "their documentation</a> on how to obtain one.",
2020-03-27 20:11:33 +00:00
),
2020-03-27 21:00:10 +00:00
NumberInput(
2020-03-27 20:11:33 +00:00
"map_position_retention_time",
"Map retention time",
infotext="Specifies how log markers / grids will remain visible on the map",
append="s",
2020-03-27 20:11:33 +00:00
),
),
Section(
2020-04-25 15:33:30 +00:00
"Decoding settings",
NumberInput("decoding_queue_workers", "Number of decoding workers"),
NumberInput("decoding_queue_length", "Maximum length of decoding job queue"),
2020-03-27 21:00:10 +00:00
NumberInput(
2020-03-27 20:11:33 +00:00
"wsjt_decoding_depth",
2020-04-25 15:33:30 +00:00
"Default WSJT decoding depth",
2020-03-29 18:14:34 +00:00
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
),
2020-04-25 15:33:30 +00:00
NumberInput(
"js8_decoding_depth",
"Js8Call decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
),
2021-01-20 16:01:46 +00:00
Js8ProfileCheckboxInput("js8_enabled_profiles", "Js8Call enabled modes"),
2021-02-07 21:49:11 +00:00
MultiCheckboxInput(
"fst4_enabled_intervals",
"Enabled FST4 intervals",
[Option(v, "{}s".format(v)) for v in Fst4Profile.availableIntervals],
),
MultiCheckboxInput(
"fst4w_enabled_intervals",
"Enabled FST4W intervals",
[Option(v, "{}s".format(v)) for v in Fst4wProfile.availableIntervals],
2021-02-07 22:15:57 +00:00
),
2021-02-08 14:16:04 +00:00
Q65ModeMatrix("q65_enabled_combinations", "Enabled Q65 Mode combinations"),
2020-03-27 20:11:33 +00:00
),
Section(
"Background decoding",
2020-03-29 18:14:34 +00:00
CheckboxInput(
"services_enabled",
"Service",
checkboxText="Enable background decoding services",
),
ServicesCheckboxInput("services_decoders", "Enabled services"),
2020-03-27 20:11:33 +00:00
),
Section(
"APRS settings",
TextInput(
"aprs_callsign",
"APRS callsign",
2020-03-29 18:14:34 +00:00
infotext="This callsign will be used to send data to the APRS-IS network",
2020-03-27 20:11:33 +00:00
),
CheckboxInput(
"aprs_igate_enabled",
"APRS I-Gate",
2021-02-07 22:15:57 +00:00
checkboxText="Send received APRS data to APRS-IS",
2020-03-27 20:11:33 +00:00
),
TextInput("aprs_igate_server", "APRS-IS server"),
TextInput("aprs_igate_password", "APRS-IS network password"),
CheckboxInput(
"aprs_igate_beacon",
"APRS beacon",
checkboxText="Send the receiver position to the APRS-IS network",
2021-02-07 23:43:39 +00:00
infotext="Please check that your receiver location is setup correctly before enabling the beacon",
),
DropdownInput(
"aprs_igate_symbol",
"APRS beacon symbol",
AprsBeaconSymbols,
2020-03-27 20:11:33 +00:00
),
2021-02-07 22:15:57 +00:00
TextInput(
"aprs_igate_comment",
"APRS beacon text",
infotext="This text will be sent as APRS comment along with your beacon",
converter=OptionalConverter(),
),
NumberInput(
"aprs_igate_height",
"Antenna height",
infotext="Antenna height above average terrain (HAAT)",
append="m",
converter=OptionalConverter(),
),
NumberInput(
"aprs_igate_gain",
"Antenna gain",
append="dBi",
converter=OptionalConverter(),
),
DropdownInput("aprs_igate_dir", "Antenna direction", AprsAntennaDirections),
2020-03-27 20:11:33 +00:00
),
Section(
"pskreporter settings",
2020-03-29 18:14:34 +00:00
CheckboxInput(
"pskreporter_enabled",
"Reporting",
checkboxText="Enable sending spots to pskreporter.info",
),
2020-03-27 20:11:33 +00:00
TextInput(
"pskreporter_callsign",
"pskreporter callsign",
2020-03-29 18:14:34 +00:00
infotext="This callsign will be used to send spots to pskreporter.info",
2020-03-27 20:11:33 +00:00
),
2021-02-07 22:15:57 +00:00
TextInput(
"pskreporter_antenna_information",
"Antenna information",
infotext="Antenna description to be sent along with spots to pskreporter",
converter=OptionalConverter(),
),
2020-03-27 20:11:33 +00:00
),
2021-02-06 20:01:59 +00:00
Section(
"WSPRnet settings",
CheckboxInput(
"wsprnet_enabled",
"Reporting",
checkboxText="Enable sending spots to wsprnet.org",
),
TextInput(
"wsprnet_callsign",
"wsprnet callsign",
2021-02-06 22:17:43 +00:00
infotext="This callsign will be used to send spots to wsprnet.org",
2021-02-06 20:01:59 +00:00
),
),
2020-03-27 00:14:38 +00:00
]
def render_sections(self):
2020-04-25 19:42:00 +00:00
sections = "".join(section.render() for section in GeneralSettingsController.sections)
2020-03-27 00:14:38 +00:00
return """
<form class="settings-body" method="POST">
{sections}
2020-03-26 22:04:02 +00:00
<div class="buttons">
<button type="submit" class="btn btn-primary">Apply</button>
</div>
</form>
2020-03-27 20:11:33 +00:00
""".format(
sections=sections
)
2020-03-27 00:14:38 +00:00
def indexAction(self):
2020-04-25 19:42:00 +00:00
self.serve_template("generalsettings.html", **self.template_variables())
2020-03-26 20:52:34 +00:00
def template_variables(self):
variables = super().template_variables()
2020-03-27 00:14:38 +00:00
variables["sections"] = self.render_sections()
2020-03-26 20:52:34 +00:00
return variables
2020-03-26 22:04:02 +00:00
def handle_image(self, data, image_id):
2021-02-10 20:29:46 +00:00
if image_id in data:
2021-02-08 23:38:59 +00:00
config = CoreConfig()
2021-02-10 20:29:46 +00:00
if data[image_id] == "restore":
# remove all possible file extensions
for ext in ["png", "jpg"]:
try:
os.unlink("{}/{}.{}".format(config.get_data_directory(), image_id, ext))
except FileNotFoundError:
pass
2021-02-10 20:29:46 +00:00
elif data[image_id]:
if not data[image_id].startswith(image_id):
logger.warning("invalid file name: %s", data[image_id])
else:
# get file extension (luckily, all options are three characters long)
ext = data[image_id][-3:]
data_file = "{}/{}.{}".format(config.get_data_directory(), image_id, ext)
temporary_file = "{}/{}".format(config.get_temporary_directory(), data[image_id])
shutil.copy(temporary_file, data_file)
2021-02-10 20:29:46 +00:00
del data[image_id]
2021-02-10 21:24:43 +00:00
# remove any accumulated temporary files on save
for file in glob("{}/{}*".format(config.get_temporary_directory(), image_id)):
os.unlink(file)
2020-03-26 22:04:02 +00:00
def processFormData(self):
2021-02-07 22:15:57 +00:00
data = parse_qs(self.get_body().decode("utf-8"), keep_blank_values=True)
2021-01-20 16:01:46 +00:00
data = {k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()}
# Image handling
2021-02-10 21:25:43 +00:00
for img in ["receiver_avatar", "receiver_top_photo"]:
self.handle_image(data, img)
2020-03-26 22:04:02 +00:00
config = Config.get()
for k, v in data.items():
2021-02-08 00:00:00 +00:00
if v is None:
if k in config:
del config[k]
else:
config[k] = v
2021-02-11 18:31:44 +00:00
config.store()
2021-02-07 17:04:46 +00:00
self.send_redirect("/generalsettings")