structured callsign data

This commit is contained in:
Jakob Ketterl 2022-11-30 01:07:16 +01:00
parent 975f5ffdf0
commit 258e41669e
9 changed files with 130 additions and 94 deletions

View File

@ -157,13 +157,17 @@ PacketMessagePanel.prototype.pushMessage = function(msg) {
if (msg.type && msg.type === 'thirdparty' && msg.data) { if (msg.type && msg.type === 'thirdparty' && msg.data) {
msg = msg.data; msg = msg.data;
} }
var source = msg.source; var source = msg.source;
if (msg.type) { var callsign;
if (msg.type === 'item') { if ('object' in source) {
source = msg.item; callsign = source.object;
} } else if ('item' in source) {
if (msg.type === 'object') { callsign = source.item;
source = msg.object; } else {
callsign = source.callsign;
if ('ssid' in source) {
callsign += '-' + source.ssid;
} }
} }
@ -202,7 +206,7 @@ PacketMessagePanel.prototype.pushMessage = function(msg) {
'style="' + stylesToString(styles) + '"' 'style="' + stylesToString(styles) + '"'
].join(' '); ].join(' ');
if (msg.lat && msg.lon) { if (msg.lat && msg.lon) {
link = '<a ' + attrs + ' href="map?callsign=' + encodeURIComponent(source) + '" target="openwebrx-map">' + overlay + '</a>'; link = '<a ' + attrs + ' href="map?' + new URLSearchParams(source).toString() + '" target="openwebrx-map">' + overlay + '</a>';
} else { } else {
link = '<div ' + attrs + '>' + overlay + '</div>' link = '<div ' + attrs + '>' + overlay + '</div>'
} }
@ -210,7 +214,7 @@ PacketMessagePanel.prototype.pushMessage = function(msg) {
$b.append($( $b.append($(
'<tr>' + '<tr>' +
'<td>' + timestamp + '</td>' + '<td>' + timestamp + '</td>' +
'<td class="callsign">' + source + '</td>' + '<td class="callsign">' + callsign + '</td>' +
'<td class="coord">' + link + '</td>' + '<td class="coord">' + link + '</td>' +
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' + '<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
'</tr>' '</tr>'

View File

@ -1,17 +1,12 @@
$(function(){ $(function(){
var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){ var query = new URLSearchParams(window.location.search);
var s = v.split('=');
var r = {};
r[s[0]] = s.slice(1).join('=');
return r;
}).reduce(function(a, b){
return a.assign(b);
});
var expectedCallsign; var expectedCallsign;
if (query.callsign) expectedCallsign = decodeURIComponent(query.callsign); if (query.has('callsign')) {
expectedCallsign = Object.fromEntries(query.entries());
}
var expectedLocator; var expectedLocator;
if (query.locator) expectedLocator = query.locator; if (query.has('locator')) expectedLocator = query.get('locator');
var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws'; var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws';
@ -102,6 +97,11 @@ $(function(){
return '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>'; return '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
}); });
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>'); $(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
};
var shallowEquals = function(obj1, obj2) {
// basic shallow object comparison
return Object.entries(obj1).sort().toString() === Object.entries(obj2).sort().toString();
} }
var processUpdates = function(updates) { var processUpdates = function(updates) {
@ -110,6 +110,7 @@ $(function(){
return; return;
} }
updates.forEach(function(update){ updates.forEach(function(update){
var key = sourceToKey(update.source);
switch (update.location.type) { switch (update.location.type) {
case 'latlon': case 'latlon':
@ -123,33 +124,33 @@ $(function(){
aprsOptions.course = update.location.course; aprsOptions.course = update.location.course;
aprsOptions.speed = update.location.speed; aprsOptions.speed = update.location.speed;
} }
if (markers[update.callsign]) { if (markers[key]) {
marker = markers[update.callsign]; marker = markers[key];
} else { } else {
marker = new markerClass(); marker = new markerClass();
marker.addListener('click', function(){ marker.addListener('click', function(){
showMarkerInfoWindow(update.callsign, pos); showMarkerInfoWindow(update.source, pos);
}); });
markers[update.callsign] = marker; markers[key] = marker;
} }
marker.setOptions($.extend({ marker.setOptions($.extend({
position: pos, position: pos,
map: map, map: map,
title: update.callsign title: sourceToString(update.source)
}, aprsOptions, getMarkerOpacityOptions(update.lastseen) )); }, aprsOptions, getMarkerOpacityOptions(update.lastseen) ));
marker.lastseen = update.lastseen; marker.lastseen = update.lastseen;
marker.mode = update.mode; marker.mode = update.mode;
marker.band = update.band; marker.band = update.band;
marker.comment = update.location.comment; marker.comment = update.location.comment;
if (expectedCallsign && expectedCallsign == update.callsign) { if (expectedCallsign && shallowEquals(expectedCallsign, update.source)) {
map.panTo(pos); map.panTo(pos);
showMarkerInfoWindow(update.callsign, pos); showMarkerInfoWindow(update.source, pos);
expectedCallsign = false; expectedCallsign = false;
} }
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) { if (infowindow && infowindow.source && shallowEquals(infowindow.source, update.source)) {
showMarkerInfoWindow(infowindow.callsign, pos); showMarkerInfoWindow(infowindow.source, pos);
} }
break; break;
case 'locator': case 'locator':
@ -160,15 +161,16 @@ $(function(){
var rectangle; var rectangle;
// the accessor is designed to work on the rectangle... but it should work on the update object, too // the accessor is designed to work on the rectangle... but it should work on the update object, too
var color = getColor(colorAccessor(update)); var color = getColor(colorAccessor(update));
if (rectangles[update.callsign]) { if (rectangles[key]) {
rectangle = rectangles[update.callsign]; rectangle = rectangles[key];
} else { } else {
rectangle = new google.maps.Rectangle(); rectangle = new google.maps.Rectangle();
rectangle.addListener('click', function(){ rectangle.addListener('click', function(){
showLocatorInfoWindow(this.locator, this.center); showLocatorInfoWindow(this.locator, this.center);
}); });
rectangles[update.callsign] = rectangle; rectangles[key] = rectangle;
} }
rectangle.source = update.source;
rectangle.lastseen = update.lastseen; rectangle.lastseen = update.lastseen;
rectangle.locator = update.location.locator; rectangle.locator = update.location.locator;
rectangle.mode = update.mode; rectangle.mode = update.mode;
@ -188,13 +190,13 @@ $(function(){
} }
}, getRectangleOpacityOptions(update.lastseen) )); }, getRectangleOpacityOptions(update.lastseen) ));
if (expectedLocator && expectedLocator == update.location.locator) { if (expectedLocator && expectedLocator === update.location.locator) {
map.panTo(center); map.panTo(center);
showLocatorInfoWindow(expectedLocator, center); showLocatorInfoWindow(expectedLocator, center);
expectedLocator = false; expectedLocator = false;
} }
if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) { if (infowindow && infowindow.locator && infowindow.locator === update.location.locator) {
showLocatorInfoWindow(infowindow.locator, center); showLocatorInfoWindow(infowindow.locator, center);
} }
break; break;
@ -203,7 +205,7 @@ $(function(){
}; };
var clearMap = function(){ var clearMap = function(){
var reset = function(callsign, item) { item.setMap(); }; var reset = function(_, item) { item.setMap(); };
$.each(markers, reset); $.each(markers, reset);
$.each(rectangles, reset); $.each(rectangles, reset);
receiverMarker.setMap(); receiverMarker.setMap();
@ -336,21 +338,35 @@ $(function(){
infowindow = new google.maps.InfoWindow(); infowindow = new google.maps.InfoWindow();
google.maps.event.addListener(infowindow, 'closeclick', function() { google.maps.event.addListener(infowindow, 'closeclick', function() {
delete infowindow.locator; delete infowindow.locator;
delete infowindow.callsign; delete infowindow.source;
}); });
} }
delete infowindow.locator; delete infowindow.locator;
delete infowindow.callsign; delete infowindow.source;
return infowindow; return infowindow;
}; };
var linkifyCallsign = function(callsign) { var sourceToKey = function(source) {
if ((callsign_url == null) || (callsign_url == '')) // special treatment for special entities
return callsign; // not just for display but also in key treatment in order not to overlap with other locations sent by the same callsign
if ('item' in source) return source['item'];
if ('object' in source) return source['object'];
var key = source.callsign;
if ('ssid' in source) key += '-' + source.ssid;
return key;
};
// we can reuse the same logic for displaying and indexing
var sourceToString = sourceToKey;
var linkifySource = function(source) {
var callsignString = sourceToString(source);
if (callsign_url == null || callsign_url === '')
return callsignString;
else else
return '<a target="callsign_info" href="' + return '<a target="callsign_info" href="' +
callsign_url.replaceAll('{}', callsign.replace(new RegExp('-.*$'), '')) + callsign_url.replaceAll('{}', source.callsign) +
'">' + callsign + '</a>'; '">' + callsignString + '</a>';
}; };
var distanceKm = function(p1, p2) { var distanceKm = function(p1, p2) {
@ -374,10 +390,8 @@ $(function(){
var showLocatorInfoWindow = function(locator, pos) { var showLocatorInfoWindow = function(locator, pos) {
var infowindow = getInfoWindow(); var infowindow = getInfoWindow();
infowindow.locator = locator; infowindow.locator = locator;
var inLocator = $.map(rectangles, function(r, callsign) { var inLocator = Object.values(rectangles).filter(rectangleFilter).filter(function(d) {
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} return d.locator === locator;
}).filter(rectangleFilter).filter(function(d) {
return d.locator == locator;
}).sort(function(a, b){ }).sort(function(a, b){
return b.lastseen - a.lastseen; return b.lastseen - a.lastseen;
}); });
@ -389,7 +403,7 @@ $(function(){
'<ul>' + '<ul>' +
inLocator.map(function(i){ inLocator.map(function(i){
var timestring = moment(i.lastseen).fromNow(); var timestring = moment(i.lastseen).fromNow();
var message = linkifyCallsign(i.callsign) + ' (' + timestring + ' using ' + i.mode; var message = linkifySource(i.source) + ' (' + timestring + ' using ' + i.mode;
if (i.band) message += ' on ' + i.band; if (i.band) message += ' on ' + i.band;
message += ')'; message += ')';
return '<li>' + message + '</li>' return '<li>' + message + '</li>'
@ -400,10 +414,10 @@ $(function(){
infowindow.open(map); infowindow.open(map);
}; };
var showMarkerInfoWindow = function(callsign, pos) { var showMarkerInfoWindow = function(source, pos) {
var infowindow = getInfoWindow(); var infowindow = getInfoWindow();
infowindow.callsign = callsign; infowindow.source = source;
var marker = markers[callsign]; var marker = markers[sourceToKey(source)];
var timestring = moment(marker.lastseen).fromNow(); var timestring = moment(marker.lastseen).fromNow();
var commentString = ""; var commentString = "";
var distance = ""; var distance = "";
@ -414,7 +428,7 @@ $(function(){
distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km"; distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km";
} }
infowindow.setContent( infowindow.setContent(
'<h3>' + linkifyCallsign(callsign) + distance + '</h3>' + '<h3>' + linkifySource(source) + distance + '</h3>' +
'<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' + '<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' +
commentString commentString
); );
@ -457,19 +471,19 @@ $(function(){
// fade out / remove positions after time // fade out / remove positions after time
setInterval(function(){ setInterval(function(){
var now = new Date().getTime(); var now = new Date().getTime();
$.each(rectangles, function(callsign, m) { Object.values(rectangles).forEach(function(m){
var age = now - m.lastseen; var age = now - m.lastseen;
if (age > retention_time) { if (age > retention_time) {
delete rectangles[callsign]; delete rectangles[sourceToKey(m.source)];
m.setMap(); m.setMap();
return; return;
} }
m.setOptions(getRectangleOpacityOptions(m.lastseen)); m.setOptions(getRectangleOpacityOptions(m.lastseen));
}); });
$.each(markers, function(callsign, m) { Object.values(markers).forEach(function(m) {
var age = now - m.lastseen; var age = now - m.lastseen;
if (age > retention_time) { if (age > retention_time) {
delete markers[callsign]; delete markers[sourceToKey(m.source)];
m.setMap(); m.setMap();
return; return;
} }

View File

@ -33,7 +33,7 @@ thirdpartyeRegex = re.compile("^([a-zA-Z0-9-]+)>((([a-zA-Z0-9-]+\\*?,)*)([a-zA-Z
messageIdRegex = re.compile("^(.*){([0-9]{1,5})$") messageIdRegex = re.compile("^(.*){([0-9]{1,5})$")
# regex to filter pseudo "WIDE" path elements # regex to filter pseudo "WIDE" path elements
widePattern = re.compile("^WIDE[0-9]-[0-9]$") widePattern = re.compile("^WIDE[0-9]$")
def decodeBase91(input): def decodeBase91(input):
@ -67,12 +67,13 @@ class Ax25Parser(PickleModule):
logger.exception("error parsing ax25 frame") logger.exception("error parsing ax25 frame")
def extractCallsign(self, input): def extractCallsign(self, input):
cs = bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip() cs = {
"callsign": bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip(),
}
ssid = (input[6] & 0b00011110) >> 1 ssid = (input[6] & 0b00011110) >> 1
if ssid > 0: if ssid > 0:
return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) cs["ssid"] = ssid
else: return cs
return cs
class WeatherMapping(object): class WeatherMapping(object):
@ -178,7 +179,7 @@ class AprsParser(PickleModule):
def isDirect(self, aprsData): def isDirect(self, aprsData):
if "path" in aprsData and len(aprsData["path"]) > 0: if "path" in aprsData and len(aprsData["path"]) > 0:
hops = [host for host in aprsData["path"] if widePattern.match(host) is None] hops = [host for host in aprsData["path"] if widePattern.match(host["callsign"]) is None]
if len(hops) > 0: if len(hops) > 0:
return False return False
if "type" in aprsData and aprsData["type"] in ["thirdparty", "item", "object"]: if "type" in aprsData and aprsData["type"] in ["thirdparty", "item", "object"]:
@ -207,12 +208,13 @@ class AprsParser(PickleModule):
mapData = mapData["data"] mapData = mapData["data"]
if "lat" in mapData and "lon" in mapData: if "lat" in mapData and "lon" in mapData:
loc = AprsLocation(mapData) loc = AprsLocation(mapData)
source = mapData["source"] source = mapData["source"].copy()
# these are special packets, sent on behalf of other entities
if "type" in mapData: if "type" in mapData:
if mapData["type"] == "item": if mapData["type"] == "item" and "item" in mapData:
source = mapData["item"] source["item"] = mapData["item"]
elif mapData["type"] == "object": elif mapData["type"] == "object" and "object" in mapData:
source = mapData["object"] source["object"] = mapData["object"]
Map.getSharedInstance().updateLocation(source, loc, "APRS", self.band) Map.getSharedInstance().updateLocation(source, loc, "APRS", self.band)
def hasCompressedCoordinates(self, raw): def hasCompressedCoordinates(self, raw):
@ -345,15 +347,24 @@ class AprsParser(PickleModule):
return result return result
def parseThirdpartyAprsData(self, information): def parseThirdpartyAprsData(self, information):
# in thirdparty packets, the callsign is passed as a string with -SSID suffix...
# this seems to be the only case where parsing is necessary, hence this function is inline
def parseCallsign(callsign):
el = callsign.split('-')
result = {"callsign": el[0]}
if len(el) > 1:
result["ssid"] = int(el[1])
return result
matches = thirdpartyeRegex.match(information) matches = thirdpartyeRegex.match(information)
if matches: if matches:
path = matches.group(2).split(",") path = matches.group(2).split(",")
destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None) destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None)
data = self.parseAprsData( data = self.parseAprsData(
{ {
"source": matches.group(1).upper(), "source": parseCallsign(matches.group(1).upper()),
"destination": destination, "destination": parseCallsign(destination),
"path": path, "path": [parseCallsign(c) for c in path],
"data": matches.group(6).encode(encoding), "data": matches.group(6).encode(encoding),
} }
) )
@ -531,7 +542,7 @@ class MicEParser(object):
def parse(self, data): def parse(self, data):
information = data["data"] information = data["data"]
destination = data["destination"] destination = data["destination"]["callsign"]
rawLatitude = [self.extractNumber(c) for c in destination[0:6]] rawLatitude = [self.extractNumber(c) for c in destination[0:6]]
lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000 lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000

View File

@ -103,11 +103,11 @@ class Js8Parser(AudioChopperParser):
if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid: if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid:
Map.getSharedInstance().updateLocation( Map.getSharedInstance().updateLocation(
frame.callsign, LocatorLocation(frame.grid), "JS8", band frame.source, LocatorLocation(frame.grid), "JS8", band
) )
ReportingEngine.getSharedInstance().spot( ReportingEngine.getSharedInstance().spot(
{ {
"callsign": frame.callsign, "source": frame.source,
"mode": "JS8", "mode": "JS8",
"locator": frame.grid, "locator": frame.grid,
"freq": freq + frame.freq, "freq": freq + frame.freq,

View File

@ -61,13 +61,13 @@ class Map(object):
client.write_update( client.write_update(
[ [
{ {
"callsign": callsign, "source": record["source"],
"location": record["location"].__dict__(), "location": record["location"].__dict__(),
"lastseen": record["updated"].timestamp() * 1000, "lastseen": record["updated"].timestamp() * 1000,
"mode": record["mode"], "mode": record["mode"],
"band": record["band"].getName() if record["band"] is not None else None, "band": record["band"].getName() if record["band"] is not None else None,
} }
for (callsign, record) in self.positions.items() for record in self.positions.values()
] ]
) )
@ -77,14 +77,20 @@ class Map(object):
except ValueError: except ValueError:
pass pass
def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): def _sourceToKey(self, source):
if "ssid" in source:
return "{callsign}-{ssid}".format(**source)
return source["callsign"]
def updateLocation(self, source, loc: Location, mode: str, band: Band = None):
ts = datetime.now() ts = datetime.now()
key = self._sourceToKey(source)
with self.positionsLock: with self.positionsLock:
self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band} self.positions[key] = {"source": source, "location": loc, "updated": ts, "mode": mode, "band": band}
self.broadcast( self.broadcast(
[ [
{ {
"callsign": callsign, "source": source,
"location": loc.__dict__(), "location": loc.__dict__(),
"lastseen": ts.timestamp() * 1000, "lastseen": ts.timestamp() * 1000,
"mode": mode, "mode": mode,
@ -93,17 +99,18 @@ class Map(object):
] ]
) )
def touchLocation(self, callsign): def touchLocation(self, source):
# not implemented on the client side yet, so do not use! # not implemented on the client side yet, so do not use!
ts = datetime.now() ts = datetime.now()
key = self._sourceToKey(source)
with self.positionsLock: with self.positionsLock:
if callsign in self.positions: if key in self.positions:
self.positions[callsign]["updated"] = ts self.positions[key]["updated"] = ts
self.broadcast([{"callsign": callsign, "lastseen": ts.timestamp() * 1000}]) self.broadcast([{"source": source, "lastseen": ts.timestamp() * 1000}])
def removeLocation(self, callsign): def removeLocation(self, key):
with self.positionsLock: with self.positionsLock:
del self.positions[callsign] del self.positions[key]
# TODO broadcast removal to clients # TODO broadcast removal to clients
def removeOldPositions(self): def removeOldPositions(self):
@ -111,9 +118,9 @@ class Map(object):
retention = timedelta(seconds=pm["map_position_retention_time"]) retention = timedelta(seconds=pm["map_position_retention_time"])
cutoff = datetime.now() - retention cutoff = datetime.now() - retention
to_be_removed = [callsign for (callsign, pos) in self.positions.items() if pos["updated"] < cutoff] to_be_removed = [key for (key, pos) in self.positions.items() if pos["updated"] < cutoff]
for callsign in to_be_removed: for key in to_be_removed:
self.removeLocation(callsign) self.removeLocation(key)
def rebuildPositions(self): def rebuildPositions(self):
logger.debug("rebuilding map storage; size before: %i", sys.getsizeof(self.positions)) logger.debug("rebuilding map storage; size before: %i", sys.getsizeof(self.positions))

View File

@ -129,7 +129,7 @@ class DigihamEnricher(Enricher, metaclass=ABCMeta):
callsign = self.getCallsign(meta) callsign = self.getCallsign(meta)
if callsign is not None and "lat" in meta and "lon" in meta: if callsign is not None and "lat" in meta and "lon" in meta:
loc = LatLngLocation(meta["lat"], meta["lon"]) loc = LatLngLocation(meta["lat"], meta["lon"])
Map.getSharedInstance().updateLocation(callsign, loc, mode, self.parser.getBand()) Map.getSharedInstance().updateLocation({"callsign": callsign}, loc, mode, self.parser.getBand())
return meta return meta
@abstractmethod @abstractmethod
@ -202,7 +202,7 @@ class DStarEnricher(DigihamEnricher):
if "ourcall" in meta: if "ourcall" in meta:
# send location info to map as well (it will show up with the correct symbol there!) # send location info to map as well (it will show up with the correct symbol there!)
loc = AprsLocation(data) loc = AprsLocation(data)
Map.getSharedInstance().updateLocation(meta["ourcall"], loc, "DPRS", self.parser.getBand()) Map.getSharedInstance().updateLocation({"callsign": meta["ourcall"]}, loc, "DPRS", self.parser.getBand())
except Exception: except Exception:
logger.exception("Error while parsing DPRS data") logger.exception("Error while parsing DPRS data")

View File

@ -56,7 +56,7 @@ class PskReporter(Reporter):
self.timer.start() self.timer.start()
def spotEquals(self, s1, s2): def spotEquals(self, s1, s2):
keys = ["callsign", "timestamp", "locator", "mode", "msg"] keys = ["source", "timestamp", "locator", "mode", "msg"]
return reduce(and_, map(lambda key: s1[key] == s2[key], keys)) return reduce(and_, map(lambda key: s1[key] == s2[key], keys))
@ -141,7 +141,7 @@ class Uploader(object):
def encodeSpot(self, spot): def encodeSpot(self, spot):
try: try:
return bytes( return bytes(
self.encodeString(spot["callsign"]) self.encodeString(spot["source"]["callsign"])
+ list(int(spot["freq"]).to_bytes(4, "big")) + list(int(spot["freq"]).to_bytes(4, "big"))
+ list(int(spot["db"]).to_bytes(1, "big", signed=True)) + list(int(spot["db"]).to_bytes(1, "big", signed=True))
+ self.encodeString(spot["mode"]) + self.encodeString(spot["mode"])

View File

@ -56,7 +56,7 @@ class Worker(threading.Thread):
# FST4W does not have drift # FST4W does not have drift
"drift": spot["drift"] if "drift" in spot else 0, "drift": spot["drift"] if "drift" in spot else 0,
"tqrg": spot["freq"] / 1e6, "tqrg": spot["freq"] / 1e6,
"tcall": spot["callsign"], "tcall": spot["source"]["callsign"],
"tgrid": spot["locator"], "tgrid": spot["locator"],
"dbm": spot["dbm"], "dbm": spot["dbm"],
"version": openwebrx_version, "version": openwebrx_version,

View File

@ -276,9 +276,9 @@ class WsjtParser(AudioChopperParser):
out["interval"] = profile.getInterval() out["interval"] = profile.getInterval()
self.pushDecode(mode, band) self.pushDecode(mode, band)
if "callsign" in out and "locator" in out: if "source" in out and "locator" in out:
Map.getSharedInstance().updateLocation( Map.getSharedInstance().updateLocation(
out["callsign"], LocatorLocation(out["locator"]), mode, band out["source"], LocatorLocation(out["locator"]), mode, band
) )
ReportingEngine.getSharedInstance().spot(out) ReportingEngine.getSharedInstance().spot(out)
@ -342,8 +342,8 @@ class QsoMessageParser(MessageParser):
# this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very
# likely this just means roger roger goodbye. # likely this just means roger roger goodbye.
if m.group(3) == "RR73": if m.group(3) == "RR73":
return {"callsign": m.group(1)} return {"source": {"callsign": m.group(1)}}
return {"callsign": m.group(1), "locator": m.group(3)} return {"source": {"callsign": m.group(1)}, "locator": m.group(3)}
# Used in propagation reporting / beacon modes (WSPR / FST4W) # Used in propagation reporting / beacon modes (WSPR / FST4W)
@ -354,7 +354,7 @@ class BeaconMessageParser(MessageParser):
m = BeaconMessageParser.wspr_splitter_pattern.match(msg) m = BeaconMessageParser.wspr_splitter_pattern.match(msg)
if m is None: if m is None:
return {} return {}
return {"callsign": m.group(1), "locator": m.group(2), "dbm": m.group(3)} return {"source": {"callsign": m.group(1)}, "locator": m.group(2), "dbm": m.group(3)}
class Jt9Decoder(Decoder): class Jt9Decoder(Decoder):