restructure the code to have the parser sit where all the parsers sit

This commit is contained in:
Jakob Ketterl 2019-08-15 15:45:15 +02:00
parent 7beb773a37
commit 0207374592
7 changed files with 238 additions and 185 deletions

View File

@ -235,7 +235,7 @@ class dsp(object):
chain = secondary_chain_base + "csdr fmdemod_quadri_cf | " chain = secondary_chain_base + "csdr fmdemod_quadri_cf | "
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
chain += "csdr fractional_decimator_ff {last_decimation} | " chain += "csdr fractional_decimator_ff {last_decimation} | "
chain += "csdr convert_f_s16 | direwolf -r {audio_rate} -t 0 -" chain += "csdr convert_f_s16 | direwolf -r {audio_rate} -t 0 - 1>&2"
return chain return chain
def set_secondary_demodulator(self, what): def set_secondary_demodulator(self, what):

View File

@ -290,7 +290,7 @@
} }
infowindow.setContent( infowindow.setContent(
'<h3>' + callsign + '</h3>' + '<h3>' + callsign + '</h3>' +
'<div>' + timestring + ' using ' + marker.mode + '</div>' + '<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' +
commentString commentString
); );
infowindow.open(map, marker); infowindow.open(map, marker);

172
owrx/aprs.py Normal file
View File

@ -0,0 +1,172 @@
from owrx.kiss import KissDeframer
from owrx.map import Map, LatLngLocation
from owrx.bands import Bandplan
import logging
logger = logging.getLogger(__name__)
class Ax25Parser(object):
def parse(self, ax25frame):
control_pid = ax25frame.find(bytes([0x03, 0xf0]))
if control_pid % 7 > 0:
logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data")
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]
return {
"destination": self.extractCallsign(ax25frame[0:7]),
"source": self.extractCallsign(ax25frame[7:14]),
"path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)],
"data": ax25frame[control_pid+2:]
}
def extractCallsign(self, input):
cs = bytes([b >> 1 for b in input[0:6]]).decode().strip()
ssid = (input[6] & 0b00011110) >> 1
if ssid > 0:
return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid)
else:
return cs
class AprsParser(object):
def __init__(self, handler):
self.ax25parser = Ax25Parser()
self.deframer = KissDeframer()
self.dial_freq = None
self.band = None
self.handler = handler
def setDialFrequency(self, freq):
self.dial_freq = freq
self.band = Bandplan.getSharedInstance().findBand(freq)
def parse(self, raw):
for frame in self.deframer.parse(raw):
data = self.ax25parser.parse(frame)
# TODO how can we tell if this is an APRS frame at all?
aprsData = self.parseAprsData(data)
logger.debug(aprsData)
if "lat" in aprsData and "lon" in aprsData:
loc = LatLngLocation(aprsData["lat"], aprsData["lon"], aprsData["comment"] if "comment" in data else None)
Map.getSharedInstance().updateLocation(data["source"], loc, "APRS", self.band)
self.handler.write_aprs_data(aprsData)
def hasCompressedCoordinatesx(self, raw):
return raw[0] == "/" or raw[0] == "\\"
def parseUncompressedCoordinates(self, raw):
lat = int(raw[0:2]) + float(raw[2:7]) / 60
if raw[7] == "S":
lat *= -1
lon = int(raw[9:12]) + float(raw[12:17]) / 60
if raw[17] == "W":
lon *= -1
return {
"lat": lat,
"lon": lon,
"symbol": raw[18]
}
def parseCompressedCoordinates(self, raw):
def decodeBase91(input):
base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0
return base + (ord(input[-1]) - 33)
return {
"lat": 90 - decodeBase91(raw[1:5]) / 380926,
"lon": -180 + decodeBase91(raw[5:9]) / 190463,
"symbol": raw[9]
}
def parseMicEFrame(self, data):
information = data["data"]
destination = data["destination"]
def extractNumber(input):
n = ord(input)
if n >= ord("P"):
return n - ord("P")
if n >= ord("A"):
return n - ord("A")
return n - ord("0")
def listToNumber(input):
base = listToNumber(input[:-1]) * 10 if len(input) > 1 else 0
return base + input[-1]
logger.debug(destination)
rawLatitude = [extractNumber(c) for c in destination[0:6]]
logger.debug(rawLatitude)
lat = listToNumber(rawLatitude[0:2]) + listToNumber(rawLatitude[2:6]) / 6000
if ord(destination[3]) <= ord("9"):
lat *= -1
logger.debug(lat)
logger.debug(information)
lon = information[1] - 28
if ord(destination[4]) >= ord("P"):
lon += 100
if 180 <= lon <= 189:
lon -= 80
if 190 <= lon <= 199:
lon -= 190
minutes = information[2] - 28
if minutes >= 60:
minutes -= 60
lon += minutes / 60 + (information[3] - 28) / 6000
if ord(destination[5]) >= ord("P"):
lon *= -1
return {
"lat": lat,
"lon": lon,
"comment": information[9:].decode()
}
def parseAprsData(self, data):
information = data["data"]
# forward some of the ax25 data
aprsData = {
"source": data["source"],
"destination": data["destination"],
"path": data["path"]
}
if information[0] == 0x1c or information[0] == 0x60:
aprsData.update(self.parseMicEFrame(data))
return aprsData
information = information.decode()
logger.debug(information)
if information[0] == "!" or information[0] == "=":
# position without timestamp
information = information[1:]
elif information[0] == "/" or information[0] == "@":
# position with timestamp
# TODO parse timestamp
information = information[8:]
else:
return {}
if self.hasCompressedCoordinatesx(information):
aprsData.update(self.parseCompressedCoordinates(information[0:10]))
aprsData["comment"] = information[10:]
else:
aprsData.update(self.parseUncompressedCoordinates(information[0:19]))
aprsData["comment"] = information[19:]
return aprsData

View File

@ -205,8 +205,8 @@ class OpenWebRxReceiverClient(Client):
def write_dial_frequendies(self, frequencies): def write_dial_frequendies(self, frequencies):
self.protected_send({"type": "dial_frequencies", "value": frequencies}) self.protected_send({"type": "dial_frequencies", "value": frequencies})
def write_packet_data(self, data): def write_aprs_data(self, data):
self.protected_send({"type": "packet_data", "value": data}) self.protected_send({"type": "aprs_data", "value": data})
class MapConnection(Client): class MapConnection(Client):

View File

@ -1,6 +1,5 @@
import socket import socket
import time import time
from owrx.map import Map, LatLngLocation
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -10,23 +9,6 @@ FESC = 0xDB
TFEND = 0xDC TFEND = 0xDC
TFESC = 0XDD TFESC = 0XDD
def group(a, *ns):
for n in ns:
a = [a[i:i+n] for i in range(0, len(a), n)]
return a
def join(a, *cs):
return [cs[0].join(join(t, *cs[1:])) for t in a] if cs else a
def hexdump(data):
toHex = lambda c: '{:02X}'.format(c)
toChr = lambda c: chr(c) if 32 <= c < 127 else '.'
make = lambda f, *cs: join(group(list(map(f, data)), 8, 2), *cs)
hs = make(toHex, ' ', ' ')
cs = make(toChr, ' ', '')
for i, (h, c) in enumerate(zip(hs, cs)):
print ('{:010X}: {:48} {:16}'.format(i * 16, h, c))
class KissClient(object): class KissClient(object):
def __init__(self, port): def __init__(self, port):
@ -35,167 +17,32 @@ class KissClient(object):
self.socket.connect(("localhost", port)) self.socket.connect(("localhost", port))
def read(self): def read(self):
buf = bytes() return self.socket.recv(1)
escaped = False
while True:
input = self.socket.recv(1)
# EOF
if len(input) == 0:
return bytes()
if input[0] == FESC:
escaped = True class KissDeframer(object):
elif escaped: def __init__(self):
if input[0] == TFEND: self.escaped = False
buf += [FEND] self.buf = bytearray()
elif input[0] == TFESC:
buf += [FESC] def parse(self, input):
frames = []
for b in input:
if b == FESC:
self.escaped = True
elif self.escaped:
if b == TFEND:
self.buf.append(FEND)
elif b == TFESC:
self.buf.append(FESC)
else: else:
logger.warning("invalid escape char: %s", str(input[0])) logger.warning("invalid escape char: %s", str(input[0]))
escaped = False self.escaped = False
elif input[0] == FEND: elif input[0] == FEND:
logger.debug("decoded frame: " + str(buf))
if len(buf) > 0:
try:
return self.parseFrame(buf)
except Exception:
logger.exception("failed to decode packet data")
return {}
else:
buf += input
def parseFrame(self, frame):
# data frames start with 0x00 # data frames start with 0x00
if frame[0] != 0x00: if len(self.buf) > 1 and self.buf[0] == 0x00:
return {} frames += [self.buf[1:]]
ax25frame = frame[1:] self.buf = bytearray()
control_pid = ax25frame.find(bytes([0x03, 0xf0]))
if control_pid % 7 > 0:
logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data")
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]
information = ax25frame[control_pid+2:]
data = {
"destination": self.extractCallsign(ax25frame[0:7]),
"source": self.extractCallsign(ax25frame[7:14]),
"path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)]
}
# TODO how can we tell if this is an APRS frame at all?
aprsData = self.parseAprsData(data["destination"], information)
data.update(aprsData)
logger.debug(data)
if "lat" in data and "lon" in data:
loc = LatLngLocation(data["lat"], data["lon"], data["comment"] if "comment" in data else None)
Map.getSharedInstance().updateLocation(data["source"], loc, "APRS")
return data
def hasCompressedCoordinatesx(self, raw):
return raw[0] == "/" or raw[0] == "\\"
def parseUncompressedCoordinates(self, raw):
lat = int(raw[0:2]) + float(raw[2:7]) / 60
if raw[7] == "S":
lat *= -1
lon = int(raw[9:12]) + float(raw[12:17]) / 60
if raw[17] == "W":
lon *= -1
return {
"lat": lat,
"lon": lon,
"symbol": raw[18]
}
def parseCompressedCoordinates(self, raw):
def decodeBase91(input):
base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0
return base + (ord(input[-1]) - 33)
return {
"lat": 90 - decodeBase91(raw[1:5]) / 380926,
"lon": -180 + decodeBase91(raw[5:9]) / 190463,
"symbol": raw[9]
}
def parseMicEFrame(self, destination, information):
def extractNumber(input):
n = ord(input)
if n >= ord("P"):
return n - ord("P")
if n >= ord("A"):
return n - ord("A")
return n - ord("0")
def listToNumber(input):
base = listToNumber(input[:-1]) * 10 if len(input) > 1 else 0
return base + input[-1]
logger.debug(destination)
rawLatitude = [extractNumber(c) for c in destination[0:6]]
logger.debug(rawLatitude)
lat = listToNumber(rawLatitude[0:2]) + listToNumber(rawLatitude[2:6]) / 6000
if ord(destination[3]) <= ord("9"):
lat *= -1
logger.debug(lat)
logger.debug(information)
lon = information[1] - 28
if ord(destination[4]) >= ord("P"):
lon += 100
if 180 <= lon <= 189:
lon -= 80
if 190 <= lon <= 199:
lon -= 190
minutes = information[2] - 28
if minutes >= 60:
minutes -= 60
lon += minutes / 60 + (information[3] - 28) / 6000
if ord(destination[5]) >= ord("P"):
lon *= -1
return {
"lat": lat,
"lon": lon,
"comment": information[9:].decode()
}
def parseAprsData(self, destination, information):
if information[0] == 0x1c or information[0] == 0x60:
return self.parseMicEFrame(destination, information)
information = information.decode()
logger.debug(information)
if information[0] == "!" or information[0] == "=":
# position without timestamp
information = information[1:]
elif information[0] == "/" or information[0] == "@":
# position with timestamp
# TODO parse timestamp
information = information[8:]
else: else:
return {} self.buf.append(b)
return frames
if self.hasCompressedCoordinatesx(information):
coords = self.parseCompressedCoordinates(information[0:10])
coords["comment"] = information[10:]
else:
coords = self.parseUncompressedCoordinates(information[0:19])
coords["comment"] = information[19:]
return coords
def extractCallsign(self, input):
cs = bytes([b >> 1 for b in input[0:6]]).decode().strip()
ssid = (input[6] & 0b00011110) >> 1
if ssid > 0:
return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid)
else:
return cs

View File

@ -3,6 +3,7 @@ from owrx.source import SdrService
from owrx.bands import Bandplan from owrx.bands import Bandplan
from csdr import dsp, output from csdr import dsp, output
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser
from owrx.config import PropertyManager from owrx.config import PropertyManager
import logging import logging
@ -14,16 +15,35 @@ class ServiceOutput(output):
def __init__(self, frequency): def __init__(self, frequency):
self.frequency = frequency self.frequency = frequency
def getParser(self):
# abstract method; implement in subclasses
pass
def receive_output(self, t, read_fn): def receive_output(self, t, read_fn):
parser = WsjtParser(WsjtHandler()) parser = self.getParser()
parser.setDialFrequency(self.frequency) parser.setDialFrequency(self.frequency)
target = self.pump(read_fn, parser.parse) target = self.pump(read_fn, parser.parse)
threading.Thread(target=target).start() threading.Thread(target=target).start()
class WsjtServiceOutput(ServiceOutput):
def getParser(self):
return WsjtParser(WsjtHandler())
def supports_type(self, t): def supports_type(self, t):
return t == "wsjt_demod" return t == "wsjt_demod"
class AprsServiceOutput(ServiceOutput):
def getParser(self):
return AprsParser(AprsHandler())
def supports_type(self, t):
return t == "packet_demod"
class ServiceHandler(object): class ServiceHandler(object):
def __init__(self, source): def __init__(self, source):
self.services = [] self.services = []
@ -77,7 +97,12 @@ class ServiceHandler(object):
def setupService(self, mode, frequency): def setupService(self, mode, frequency):
logger.debug("setting up service {0} on frequency {1}".format(mode, frequency)) logger.debug("setting up service {0} on frequency {1}".format(mode, frequency))
d = dsp(ServiceOutput(frequency)) # TODO selecting outputs will need some more intelligence here
if mode == "packet":
output = AprsServiceOutput(frequency)
else:
output = WsjtServiceOutput(frequency)
d = dsp(output)
d.nc_port = self.source.getPort() d.nc_port = self.source.getPort()
d.set_offset_freq(frequency - self.source.getProps()["center_freq"]) d.set_offset_freq(frequency - self.source.getProps()["center_freq"])
if mode == "packet": if mode == "packet":
@ -98,6 +123,11 @@ class WsjtHandler(object):
pass pass
class AprsHandler(object):
def write_aprs_data(self, data):
pass
class ServiceManager(object): class ServiceManager(object):
sharedInstance = None sharedInstance = None

View File

@ -3,6 +3,7 @@ from owrx.config import PropertyManager
from owrx.feature import FeatureDetector, UnknownFeatureException from owrx.feature import FeatureDetector, UnknownFeatureException
from owrx.meta import MetaParser from owrx.meta import MetaParser
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser
import threading import threading
import csdr import csdr
import time import time
@ -406,6 +407,7 @@ class DspManager(csdr.output):
self.sdrSource = sdrSource self.sdrSource = sdrSource
self.metaParser = MetaParser(self.handler) self.metaParser = MetaParser(self.handler)
self.wsjtParser = WsjtParser(self.handler) self.wsjtParser = WsjtParser(self.handler)
self.aprsParser = AprsParser(self.handler)
self.localProps = ( self.localProps = (
self.sdrSource.getProps() self.sdrSource.getProps()
@ -440,7 +442,9 @@ class DspManager(csdr.output):
self.dsp.set_bpf(*bpf) self.dsp.set_bpf(*bpf)
def set_dial_freq(key, value): def set_dial_freq(key, value):
self.wsjtParser.setDialFrequency(self.localProps["center_freq"] + self.localProps["offset_freq"]) freq = self.localProps["center_freq"] + self.localProps["offset_freq"]
self.wsjtParser.setDialFrequency(freq)
self.aprsParser.setDialFrequency(freq)
self.subscriptions = [ self.subscriptions = [
self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression), self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression),
@ -502,7 +506,7 @@ class DspManager(csdr.output):
"secondary_demod": self.handler.write_secondary_demod, "secondary_demod": self.handler.write_secondary_demod,
"meta": self.metaParser.parse, "meta": self.metaParser.parse,
"wsjt_demod": self.wsjtParser.parse, "wsjt_demod": self.wsjtParser.parse,
"packet_demod": self.handler.write_packet_data, "packet_demod": self.aprsParser.parse,
} }
write = writers[t] write = writers[t]