');
$list.append(bookmarks.map(function(b) {
- var modulation = b.modulation;
- if (modulation in modes) {
- modulation = modes[modulation];
- }
var row = $(
'
' +
- '
' +
+ '
' +
'
' + b.name + '
' +
'
' + renderFrequency(b.frequency) + '
' +
- '
' + modulation + '
' +
+ '
' + renderModulation(b.modulation, modes) + '
' +
+// '
' + renderModulation(b.underlying, modes) + '
' +
'
'
);
row.data('bookmark', b);
@@ -407,31 +438,30 @@ $.fn.bookmarktable = function() {
data: JSON.stringify(selected),
contentType: 'application/json',
method: 'POST'
- }).done(function(data){
+ }).fail(function(data) {
+ // import failed
+ $table.find('.emptytext').remove();
+ }).done(function(data) {
$table.find('.emptytext').remove();
var modes = $table.data('modes');
if (data.length && data.length == selected.length) {
$table.append(data.map(function(obj, index) {
- var bookmark = selected[index];
- var modulation_name = bookmark.modulation;
- if (modulation_name in modes) {
- modulation_name = modes[modulation_name];
- }
- // provide reasonable default for missing fields
- if (!('description' in bookmark)) {
- bookmark.description = '';
- }
- if (!('scannable' in bookmark)) {
+ var b = selected[index];
+ // provide reasonable defaults for missing fields
+ if (!('underlying' in b)) b.underlying = '';
+ if (!('description' in b)) b.description = '';
+ if (!('scannable' in b)) {
var modesToScan = ['lsb', 'usb', 'cw', 'am', 'sam', 'nfm'];
- bookmark.scannable = modesToScan.indexOf(bookmark.modulation) >= 0;
+ b.scannable = modesToScan.indexOf(b.modulation) >= 0;
}
return $(
'
' +
- '
' + bookmark.name + '
' +
- '
' + renderFrequency(bookmark.frequency) +'
' +
- '
' + modulation_name + '
' +
- '
' + bookmark.description + '
' +
- '
' + (bookmark.scannable? '✓':'') + '
' +
+ '
' + b.name + '
' +
+ '
' + renderFrequency(b.frequency) +'
' +
+ '
' + renderModulation(b.modulation, modes) + '
' +
+ '
' + renderModulation(b.underlying, modes) + '
' +
+ '
' + b.description + '
' +
+ '
' + (b.scannable? '✓':'') + '
' +
'
' +
'' +
'
' +
diff --git a/owrx/bookmarks.py b/owrx/bookmarks.py
index b7b3aeea..81f7b93a 100644
--- a/owrx/bookmarks.py
+++ b/owrx/bookmarks.py
@@ -16,6 +16,7 @@ class Bookmark(object):
self.name = j["name"]
self.frequency = j["frequency"]
self.modulation = j["modulation"]
+ self.underlying = j["underlying"] if "underlying" in j else ""
self.description = j["description"] if "description" in j else ""
self.srcFile = srcFile
# By default, only scan modulations that make sense to scan
@@ -33,6 +34,9 @@ class Bookmark(object):
def getModulation(self):
return self.modulation
+ def getUnderlying(self):
+ return self.underlying
+
def getDescription(self):
return self.description
@@ -47,6 +51,7 @@ class Bookmark(object):
"name": self.getName(),
"frequency": self.getFrequency(),
"modulation": self.getModulation(),
+ "underlying": self.getUnderlying(),
"description": self.getDescription(),
"scannable": self.isScannable(),
}
diff --git a/owrx/controllers/settings/bookmarks.py b/owrx/controllers/settings/bookmarks.py
index 1c9c9a09..f2e6499c 100644
--- a/owrx/controllers/settings/bookmarks.py
+++ b/owrx/controllers/settings/bookmarks.py
@@ -2,7 +2,7 @@ from owrx.controllers.template import WebpageController
from owrx.controllers.admin import AuthorizationMixin
from owrx.controllers.settings import SettingsBreadcrumb
from owrx.bookmarks import Bookmark, Bookmarks
-from owrx.modes import Modes
+from owrx.modes import Modes, AnalogMode
from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin
import json
import math
@@ -24,7 +24,7 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
def render_table(self):
bookmarks = Bookmarks.getSharedInstance().getEditableBookmarks()
emptyText = """
-
+
No bookmarks in storage. You can add new bookmarks using the buttons below.
"""
@@ -35,6 +35,7 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
Name
Frequency
Modulation
+
Underlying
Description
Scan
Actions
@@ -43,7 +44,11 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
""".format(
bookmarks="".join(self.render_bookmark(b) for b in bookmarks) if bookmarks else emptyText,
- modes=json.dumps({m.modulation: m.name for m in Modes.getAvailableModes()}),
+ modes=json.dumps({m.modulation: {
+ "name" : m.name,
+ "analog" : isinstance(m, AnalogMode),
+ "underlying" : m.underlying if hasattr(m, "underlying") else []
+ } for m in Modes.getAvailableModes()}),
)
def render_bookmark(self, bookmark: Bookmark):
@@ -65,13 +70,18 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
suffix = suffixes[exp]
return "{num:g} {suffix}Hz".format(num=num, suffix=suffix)
- mode = Modes.findByModulation(bookmark.getModulation())
- scan = bookmark.isScannable()
+ scan = bookmark.isScannable()
+ name1 = bookmark.getModulation()
+ name2 = bookmark.getUnderlying()
+ mode1 = Modes.findByModulation(name1)
+ mode2 = Modes.findByModulation(name2)
+
return """
{name}
{rendered_frequency}
{modulation_name}
+
{underlying_name}
{description}
{scannable_check}
@@ -84,8 +94,10 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
# TODO render frequency in si units
frequency=bookmark.getFrequency(),
rendered_frequency=render_frequency(bookmark.getFrequency()),
- modulation=bookmark.getModulation() if mode is None else mode.modulation,
- modulation_name=bookmark.getModulation() if mode is None else mode.name,
+ modulation=name1 if mode1 is None else mode1.modulation,
+ underlying=name2 if mode2 is None else mode2.modulation,
+ modulation_name=name1 if mode1 is None else mode1.name,
+ underlying_name="None" if not name2 else name2 if mode2 is None else mode2.name,
description=bookmark.getDescription(),
scannable="true" if scan else "false",
scannable_check="✓" if scan else "",
@@ -98,6 +110,45 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
except StopIteration:
return None
+ def _sanitizeBookmark(self, data):
+ try:
+ # Must have name, frequency, modulation
+ if "name" not in data or "frequency" not in data or "modulation" not in data:
+ return "Bookmark missing required fields"
+ # Name must not be empty
+ data["name"] = data["name"].strip()
+ if len(data["name"]) == 0:
+ return "Empty bookmark name"
+ # Frequency must be integer
+ if not isinstance(data["frequency"], int):
+ data["frequency"] = int(data["frequency"])
+ # Frequency must be >0
+ if data["frequency"] <= 0:
+ return "Frequency must be positive"
+ # Get both modes
+ mode1 = Modes.findByModulation(data["modulation"]) if "modulation" in data else None
+ mode2 = Modes.findByModulation(data["underlying"]) if "underlying" in data else None
+ # Unknown main mode
+ if mode1 is None:
+ return "Invalid modulation"
+ # No underlying mode
+ if mode2 is None:
+ data["underlying"] = ""
+ else:
+ # Main mode has no underlying mode or underlying mode incorrect
+ if not hasattr(mode1, "underlying") or mode2.modulation not in mode1.underlying:
+ return "Incorrect underlying modulation"
+ # Underlying mode is at the default value
+ #if mode2.modulation == mode1.underlying[0]:
+ # data["underlying"] = ""
+
+ except Exception as e:
+ # Something else went horribly wrong
+ return str(e)
+
+ # Everything ok
+ return None
+
def update(self):
bookmark_id = int(self.request.matches.group(1))
bookmark = self._findBookmark(bookmark_id)
@@ -105,18 +156,26 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
self.send_response("{}", content_type="application/json", code=404)
return
try:
+ newd = {}
data = json.loads(self.get_body().decode("utf-8"))
- for key in ["name", "frequency", "modulation", "description", "scannable"]:
+ for key in ["name", "frequency", "modulation", "underlying", "description", "scannable"]:
if key in data:
- value = data[key]
- if key == "frequency":
- value = int(value)
- setattr(bookmark, key, value)
+ newd[key] = data[key]
+ elif hasattr(bookmark, key):
+ newd[key] = getattr(bookmark, key)
+ # Make sure everything is correct
+ error = self._sanitizeBookmark(newd)
+ if error is not None:
+ raise ValueError(error)
+ # Update and store bookmark
+ for key in newd:
+ setattr(bookmark, key, newd[key])
Bookmarks.getSharedInstance().store()
# TODO this should not be called explicitly... bookmarks don't have any event capability right now, though
Bookmarks.getSharedInstance().notifySubscriptions(bookmark)
self.send_response("{}", content_type="application/json", code=200)
- except json.JSONDecodeError:
+ except (json.JSONDecodeError, ValueError) as e:
+ logger.warning("Failed updating bookmark: " + str(e))
self.send_response("{}", content_type="application/json", code=400)
def new(self):
@@ -125,12 +184,12 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
def create(bookmark_data):
# sanitize
data = {}
- for key in ["name", "frequency", "modulation", "description", "scannable"]:
+ for key in ["name", "frequency", "modulation", "underlying", "description", "scannable"]:
if key in bookmark_data:
- if key == "frequency":
- data[key] = int(bookmark_data[key])
- else:
- data[key] = bookmark_data[key]
+ data[key] = bookmark_data[key]
+ error = self._sanitizeBookmark(data)
+ if error is not None:
+ raise ValueError(error)
bookmark = Bookmark(data)
bookmarks.addBookmark(bookmark)
return {"bookmark_id": id(bookmark)}
@@ -140,7 +199,8 @@ class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController
result = [create(b) for b in data]
bookmarks.store()
self.send_response(json.dumps(result), content_type="application/json", code=200)
- except json.JSONDecodeError:
+ except (json.JSONDecodeError, ValueError) as e:
+ logger.warning("Failed creating bookmark: " + str(e))
self.send_response("{}", content_type="application/json", code=400)
def delete(self):