switch NXDN to use digiham decoder; add meta panel
This commit is contained in:
parent
34065e455f
commit
f5c2525f22
@ -1,8 +1,8 @@
|
|||||||
**1.1.x - unreleased**
|
**1.1.x - unreleased**
|
||||||
- Reworked most graphical elements as SVGs for faster loadtimes and crispier display on hi-dpi displays
|
- Reworked most graphical elements as SVGs for faster loadtimes and crispier display on hi-dpi displays
|
||||||
- Updated pipelines to match changes in digiham
|
- Updated pipelines to match changes in digiham
|
||||||
- Changed D-Star integration to use new decoder in digiham
|
- Changed D-Star and NXDN integrations to use new decoders from digiham
|
||||||
- Added D-Star metadata display
|
- Added D-Star and NXDN metadata display
|
||||||
|
|
||||||
**1.0.0**
|
**1.0.0**
|
||||||
- Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level
|
- Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level
|
||||||
|
@ -173,7 +173,7 @@ class Dsp(DirewolfConfigSubscriber):
|
|||||||
"m17-demod",
|
"m17-demod",
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
# dsd modes
|
# digiham modes
|
||||||
if which == "dstar":
|
if which == "dstar":
|
||||||
chain += [
|
chain += [
|
||||||
"fsk_demodulator -s 10",
|
"fsk_demodulator -s 10",
|
||||||
@ -181,8 +181,12 @@ class Dsp(DirewolfConfigSubscriber):
|
|||||||
"mbe_synthesizer -d {codecserver_arg}",
|
"mbe_synthesizer -d {codecserver_arg}",
|
||||||
]
|
]
|
||||||
elif which == "nxdn":
|
elif which == "nxdn":
|
||||||
chain += ["csdr limit_ff", "csdr convert_f_s16", "dsd -fi -i - -o - -u {unvoiced_quality} -g -1 "]
|
chain += [
|
||||||
# digiham modes
|
"rrc_filter --narrow",
|
||||||
|
"gfsk_demodulator --samples 20",
|
||||||
|
"nxdn_decoder --fifo {meta_pipe}",
|
||||||
|
"mbe_synthesizer {codecserver_arg}",
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
chain += ["rrc_filter", "gfsk_demodulator"]
|
chain += ["rrc_filter", "gfsk_demodulator"]
|
||||||
if which == "dmr":
|
if which == "dmr":
|
||||||
|
4
debian/changelog
vendored
4
debian/changelog
vendored
@ -3,8 +3,8 @@ openwebrx (1.1.0) UNRELEASED; urgency=low
|
|||||||
* Reworked most graphical elements as SVGs for faster loadtimes and crispier
|
* Reworked most graphical elements as SVGs for faster loadtimes and crispier
|
||||||
display on hi-dpi displays
|
display on hi-dpi displays
|
||||||
* Updated pipelines to match changes in digiham
|
* Updated pipelines to match changes in digiham
|
||||||
* Changed D-Star integration to use new decoder in digiham
|
* Changed D-Star and NXDN integrations to use new decoder from digiham
|
||||||
* Added D-Star metadata display
|
* Added D-Star and NXDN metadata display
|
||||||
|
|
||||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 09 May 2021 14:05:00 +0000
|
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 09 May 2021 14:05:00 +0000
|
||||||
|
|
||||||
|
@ -1051,12 +1051,14 @@ img.openwebrx-mirror-img
|
|||||||
}
|
}
|
||||||
|
|
||||||
.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image .directcall,
|
.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image .directcall,
|
||||||
|
.openwebrx-meta-slot.active.individual .openwebrx-meta-user-image .directcall,
|
||||||
#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
|
#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
|
||||||
#openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall {
|
#openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openwebrx-meta-slot.active.group .openwebrx-meta-user-image .groupcall {
|
.openwebrx-meta-slot.active.group .openwebrx-meta-user-image .groupcall,
|
||||||
|
.openwebrx-meta-slot.active.conference .openwebrx-meta-user-image .groupcall {
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1097,6 +1099,14 @@ img.openwebrx-mirror-img
|
|||||||
content: "RPT2: ";
|
content: "RPT2: ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openwebrx-meta-slot.individual .openwebrx-nxdn-destination:not(:empty):before {
|
||||||
|
content: "Direct: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.openwebrx-meta-slot.conference .openwebrx-nxdn-destination:not(:empty):before {
|
||||||
|
content: "Conference: ";
|
||||||
|
}
|
||||||
|
|
||||||
.openwebrx-maps-pin svg {
|
.openwebrx-maps-pin svg {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
|
@ -97,6 +97,17 @@
|
|||||||
<div class="openwebrx-dstar-destination"></div>
|
<div class="openwebrx-dstar-destination"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-nxdn" style="display: none;" data-panel-name="metadata-nxdn">
|
||||||
|
<div class="openwebrx-meta-slot">
|
||||||
|
<div class="openwebrx-meta-user-image">
|
||||||
|
<img class="directcall" src="static/gfx/openwebrx-directcall.svg">
|
||||||
|
<img class="groupcall" src="static/gfx/openwebrx-groupcall.svg">
|
||||||
|
</div>
|
||||||
|
<div class="openwebrx-nxdn-source"></div>
|
||||||
|
<div class="openwebrx-nxdn-name"></div>
|
||||||
|
<div class="openwebrx-nxdn-destination"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" style="display: none;" data-panel-name="metadata-dmr">
|
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" style="display: none;" data-panel-name="metadata-dmr">
|
||||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||||
<div class="openwebrx-dmr-slot">Timeslot 1</div>
|
<div class="openwebrx-dmr-slot">Timeslot 1</div>
|
||||||
|
@ -237,10 +237,71 @@ DStarMetaPanel.prototype.setLocation = function(lat, lon, callsign) {
|
|||||||
this.el.find('.openwebrx-dstar-ourcall .location').html(html);
|
this.el.find('.openwebrx-dstar-ourcall .location').html(html);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function NxdnMetaPanel(el) {
|
||||||
|
MetaPanel.call(this, el);
|
||||||
|
this.modes = ['NXDN'];
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
NxdnMetaPanel.prototype = new MetaPanel();
|
||||||
|
|
||||||
|
NxdnMetaPanel.prototype.update = function(data) {
|
||||||
|
if (!this.isSupported(data)) return;
|
||||||
|
|
||||||
|
if (data['sync'] && data['sync'] == 'voice') {
|
||||||
|
this.el.find(".openwebrx-meta-slot").addClass("active");
|
||||||
|
this.setSource(data['additional'] && data['additional']['callsign'] || data['source']);
|
||||||
|
this.setName(data['additional'] && data['additional']['fname']);
|
||||||
|
this.setDestination(data['destination']);
|
||||||
|
this.setMode(['conference', 'individual'].includes(data['type']) ? data['type'] : undefined);
|
||||||
|
} else {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
NxdnMetaPanel.prototype.setSource = function(source) {
|
||||||
|
if (this.source === source) return;
|
||||||
|
this.source = source;
|
||||||
|
this.el.find('.openwebrx-nxdn-source').text(source || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
NxdnMetaPanel.prototype.setName = function(name) {
|
||||||
|
if (this.name === name) return;
|
||||||
|
this.name = name;
|
||||||
|
this.el.find('.openwebrx-nxdn-name').text(name || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
NxdnMetaPanel.prototype.setDestination = function(destination) {
|
||||||
|
if (this.destination === destination) return;
|
||||||
|
this.destination = destination;
|
||||||
|
this.el.find('.openwebrx-nxdn-destination').text(destination || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
NxdnMetaPanel.prototype.setMode = function(mode) {
|
||||||
|
if (this.mode === mode) return;
|
||||||
|
var modes = ['individual', 'conference'];
|
||||||
|
if (modes.indexOf(mode) < 0) return;
|
||||||
|
|
||||||
|
this.mode = mode;
|
||||||
|
var classes = modes.filter(function(c){
|
||||||
|
return c !== mode;
|
||||||
|
});
|
||||||
|
this.el.find('.openwebrx-meta-slot').removeClass(classes.join(' ')).addClass(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
NxdnMetaPanel.prototype.clear = function() {
|
||||||
|
MetaPanel.prototype.clear.call(this);
|
||||||
|
this.setMode();
|
||||||
|
this.setSource();
|
||||||
|
this.setName();
|
||||||
|
this.setDestination();
|
||||||
|
};
|
||||||
|
|
||||||
MetaPanel.types = {
|
MetaPanel.types = {
|
||||||
dmr: DmrMetaPanel,
|
dmr: DmrMetaPanel,
|
||||||
ysf: YsfMetaPanel,
|
ysf: YsfMetaPanel,
|
||||||
dstar: DStarMetaPanel,
|
dstar: DStarMetaPanel,
|
||||||
|
nxdn: NxdnMetaPanel,
|
||||||
};
|
};
|
||||||
|
|
||||||
$.fn.metaPanel = function() {
|
$.fn.metaPanel = function() {
|
||||||
|
@ -20,6 +20,7 @@ defaultConfig = PropertyLayer(
|
|||||||
digimodes_fft_size=2048,
|
digimodes_fft_size=2048,
|
||||||
digital_voice_unvoiced_quality=1,
|
digital_voice_unvoiced_quality=1,
|
||||||
digital_voice_dmr_id_lookup=True,
|
digital_voice_dmr_id_lookup=True,
|
||||||
|
digital_voice_nxdn_id_lookup=True,
|
||||||
sdrs=PropertyLayer(
|
sdrs=PropertyLayer(
|
||||||
rtlsdr=PropertyLayer(
|
rtlsdr=PropertyLayer(
|
||||||
name="RTL-SDR USB Stick",
|
name="RTL-SDR USB Stick",
|
||||||
|
@ -53,6 +53,11 @@ class DecodingSettingsController(SettingsFormController):
|
|||||||
'Enable lookup of DMR ids in the <a href="https://www.radioid.net/" target="_blank">'
|
'Enable lookup of DMR ids in the <a href="https://www.radioid.net/" target="_blank">'
|
||||||
+ "radioid</a> database to show callsigns and names",
|
+ "radioid</a> database to show callsigns and names",
|
||||||
),
|
),
|
||||||
|
CheckboxInput(
|
||||||
|
"digital_voice_nxdn_id_lookup",
|
||||||
|
'Enable lookup of NXDN ids in the <a href="https://www.radioid.net/" target="_blank">'
|
||||||
|
+ "radioid</a> database to show callsigns and names",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Section(
|
Section(
|
||||||
"Digimodes",
|
"Digimodes",
|
||||||
|
55
owrx/meta.py
55
owrx/meta.py
@ -21,63 +21,69 @@ class Enricher(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DmrCache(object):
|
class RadioIDCache(object):
|
||||||
sharedInstance = None
|
sharedInstance = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getSharedInstance():
|
def getSharedInstance():
|
||||||
if DmrCache.sharedInstance is None:
|
if RadioIDCache.sharedInstance is None:
|
||||||
DmrCache.sharedInstance = DmrCache()
|
RadioIDCache.sharedInstance = RadioIDCache()
|
||||||
return DmrCache.sharedInstance
|
return RadioIDCache.sharedInstance
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
self.cacheTimeout = timedelta(seconds=86400)
|
self.cacheTimeout = timedelta(seconds=86400)
|
||||||
|
|
||||||
def isValid(self, key):
|
def isValid(self, mode, radio_id):
|
||||||
|
key = self.__key(mode, radio_id)
|
||||||
if key not in self.cache:
|
if key not in self.cache:
|
||||||
return False
|
return False
|
||||||
entry = self.cache[key]
|
entry = self.cache[key]
|
||||||
return entry["timestamp"] + self.cacheTimeout > datetime.now()
|
return entry["timestamp"] + self.cacheTimeout > datetime.now()
|
||||||
|
|
||||||
def put(self, key, value):
|
def __key(self, mode, radio_id):
|
||||||
self.cache[key] = {"timestamp": datetime.now(), "data": value}
|
return "{}-{}".format(mode, radio_id)
|
||||||
|
|
||||||
def get(self, key):
|
def put(self, mode, radio_id, value):
|
||||||
if not self.isValid(key):
|
self.cache[self.__key(mode, radio_id)] = {"timestamp": datetime.now(), "data": value}
|
||||||
|
|
||||||
|
def get(self, mode, radio_id):
|
||||||
|
if not self.isValid(mode, radio_id):
|
||||||
return None
|
return None
|
||||||
return self.cache[key]["data"]
|
return self.cache[self.__key(mode, radio_id)]["data"]
|
||||||
|
|
||||||
|
|
||||||
class DmrMetaEnricher(Enricher):
|
class RadioIDEnricher(Enricher):
|
||||||
def __init__(self, parser):
|
def __init__(self, mode, parser):
|
||||||
super().__init__(parser)
|
super().__init__(parser)
|
||||||
|
self.mode = mode
|
||||||
self.threads = {}
|
self.threads = {}
|
||||||
|
|
||||||
def downloadRadioIdData(self, id):
|
def downloadRadioIdData(self, id):
|
||||||
cache = DmrCache.getSharedInstance()
|
cache = RadioIDCache.getSharedInstance()
|
||||||
try:
|
try:
|
||||||
logger.debug("requesting DMR metadata for id=%s", id)
|
logger.debug("requesting radioid metadata for mode=%s and id=%s", self.mode, id)
|
||||||
res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(id), timeout=30).read()
|
res = request.urlopen("https://www.radioid.net/api/{0}/user/?id={1}".format(self.mode, id), timeout=30).read()
|
||||||
data = json.loads(res.decode("utf-8"))
|
data = json.loads(res.decode("utf-8"))
|
||||||
cache.put(id, data)
|
cache.put(self.mode, id, data)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
cache.put(id, None)
|
cache.put(self.mode, id, None)
|
||||||
del self.threads[id]
|
del self.threads[id]
|
||||||
|
|
||||||
def enrich(self, meta):
|
def enrich(self, meta):
|
||||||
if not Config.get()["digital_voice_dmr_id_lookup"]:
|
config_key = "digital_voice_{}_id_lookup".format(self.mode)
|
||||||
|
if not Config.get()[config_key]:
|
||||||
return meta
|
return meta
|
||||||
if "source" not in meta:
|
if "source" not in meta:
|
||||||
return meta
|
return meta
|
||||||
id = meta["source"]
|
id = meta["source"]
|
||||||
cache = DmrCache.getSharedInstance()
|
cache = RadioIDCache.getSharedInstance()
|
||||||
if not cache.isValid(id):
|
if not cache.isValid(self.mode, id):
|
||||||
if id not in self.threads:
|
if id not in self.threads:
|
||||||
self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id], daemon=True)
|
self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id], daemon=True)
|
||||||
self.threads[id].start()
|
self.threads[id].start()
|
||||||
return meta
|
return meta
|
||||||
data = cache.get(id)
|
data = cache.get(self.mode, id)
|
||||||
if data is not None and "count" in data and data["count"] > 0 and "results" in data:
|
if data is not None and "count" in data and data["count"] > 0 and "results" in data:
|
||||||
meta["additional"] = data["results"][0]
|
meta["additional"] = data["results"][0]
|
||||||
return meta
|
return meta
|
||||||
@ -129,7 +135,12 @@ class DStarEnricher(Enricher):
|
|||||||
class MetaParser(Parser):
|
class MetaParser(Parser):
|
||||||
def __init__(self, handler):
|
def __init__(self, handler):
|
||||||
super().__init__(handler)
|
super().__init__(handler)
|
||||||
self.enrichers = {"DMR": DmrMetaEnricher(self), "YSF": YsfMetaEnricher(self), "DSTAR": DStarEnricher(self)}
|
self.enrichers = {
|
||||||
|
"DMR": RadioIDEnricher("dmr", self),
|
||||||
|
"YSF": YsfMetaEnricher(self),
|
||||||
|
"DSTAR": DStarEnricher(self),
|
||||||
|
"NXDN": RadioIDEnricher("nxdn", self),
|
||||||
|
}
|
||||||
|
|
||||||
def parse(self, meta):
|
def parse(self, meta):
|
||||||
fields = meta.split(";")
|
fields = meta.split(";")
|
||||||
|
Loading…
Reference in New Issue
Block a user