Reformatted with black -l 120 -t py35 .

This commit is contained in:
D0han
2019-07-21 19:40:28 +02:00
parent 79062ff3d6
commit e15dc1ce11
17 changed files with 681 additions and 462 deletions

View File

@ -1,6 +1,7 @@
import json
import logging
logger = logging.getLogger(__name__)
@ -16,7 +17,11 @@ class Band(object):
freqs = [freqs]
for f in freqs:
if not self.inBand(f):
logger.warning("Frequency for {mode} on {band} is not within band limits: {frequency}".format(mode = mode, frequency = f, band = self.name))
logger.warning(
"Frequency for {mode} on {band} is not within band limits: {frequency}".format(
mode=mode, frequency=f, band=self.name
)
)
else:
self.frequencies.append({"mode": mode, "frequency": f})
@ -33,6 +38,7 @@ class Band(object):
class Bandplan(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if Bandplan.sharedInstance is None:

View File

@ -1,4 +1,5 @@
import logging
logger = logging.getLogger(__name__)
@ -15,7 +16,7 @@ class Subscription(object):
class Property(object):
def __init__(self, value = None):
def __init__(self, value=None):
self.value = value
self.subscribers = []
@ -23,7 +24,7 @@ class Property(object):
return self.value
def setValue(self, value):
if (self.value == value):
if self.value == value:
return self
self.value = value
for c in self.subscribers:
@ -36,7 +37,8 @@ class Property(object):
def wire(self, callback):
sub = Subscription(self, callback)
self.subscribers.append(sub)
if not self.value is None: sub.call(self.value)
if not self.value is None:
sub.call(self.value)
return sub
def unwire(self, sub):
@ -47,8 +49,10 @@ class Property(object):
pass
return self
class PropertyManager(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if PropertyManager.sharedInstance is None:
@ -56,9 +60,11 @@ class PropertyManager(object):
return PropertyManager.sharedInstance
def collect(self, *props):
return PropertyManager({name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props})
return PropertyManager(
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}
)
def __init__(self, properties = None):
def __init__(self, properties=None):
self.properties = {}
self.subscribers = []
if properties is not None:
@ -67,12 +73,14 @@ class PropertyManager(object):
def add(self, name, prop):
self.properties[name] = prop
def fireCallbacks(value):
for c in self.subscribers:
try:
c.call(name, value)
except Exception as e:
logger.exception(e)
prop.wire(fireCallbacks)
return self
@ -88,7 +96,7 @@ class PropertyManager(object):
self.getProperty(name).setValue(value)
def __dict__(self):
return {k:v.getValue() for k, v in self.properties.items()}
return {k: v.getValue() for k, v in self.properties.items()}
def hasProperty(self, name):
return name in self.properties

View File

@ -7,6 +7,7 @@ import json
from owrx.map import Map
import logging
logger = logging.getLogger(__name__)
@ -29,11 +30,26 @@ class Client(object):
class OpenWebRxReceiverClient(Client):
config_keys = ["waterfall_colors", "waterfall_min_level", "waterfall_max_level",
"waterfall_auto_level_margin", "lfo_offset", "samp_rate", "fft_size", "fft_fps",
"audio_compression", "fft_compression", "max_clients", "start_mod",
"client_audio_buffer_size", "start_freq", "center_freq", "mathbox_waterfall_colors",
"mathbox_waterfall_history_length", "mathbox_waterfall_frequency_resolution"]
config_keys = [
"waterfall_colors",
"waterfall_min_level",
"waterfall_max_level",
"waterfall_auto_level_margin",
"lfo_offset",
"samp_rate",
"fft_size",
"fft_fps",
"audio_compression",
"fft_compression",
"max_clients",
"start_mod",
"client_audio_buffer_size",
"start_freq",
"center_freq",
"mathbox_waterfall_colors",
"mathbox_waterfall_history_length",
"mathbox_waterfall_frequency_resolution",
]
def __init__(self, conn):
super().__init__(conn)
@ -49,12 +65,23 @@ class OpenWebRxReceiverClient(Client):
self.setSdr()
# send receiver info
receiver_keys = ["receiver_name", "receiver_location", "receiver_qra", "receiver_asl", "receiver_gps",
"photo_title", "photo_desc"]
receiver_keys = [
"receiver_name",
"receiver_location",
"receiver_qra",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
]
receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
self.write_receiver_details(receiver_details)
profiles = [{"name": s.getName() + " " + p["name"], "id":sid + "|" + pid} for (sid, s) in SdrService.getSources().items() for (pid, p) in s.getProfiles().items()]
profiles = [
{"name": s.getName() + " " + p["name"], "id": sid + "|" + pid}
for (sid, s) in SdrService.getSources().items()
for (pid, p) in s.getProfiles().items()
]
self.write_profiles(profiles)
features = FeatureDetector().feature_availability()
@ -62,9 +89,9 @@ class OpenWebRxReceiverClient(Client):
CpuUsageThread.getSharedInstance().add_client(self)
def setSdr(self, id = None):
def setSdr(self, id=None):
next = SdrService.getSource(id)
if (next == self.sdr):
if next == self.sdr:
return
self.stopDsp()
@ -76,7 +103,11 @@ class OpenWebRxReceiverClient(Client):
self.sdr = next
# send initial config
configProps = self.sdr.getProps().collect(*OpenWebRxReceiverClient.config_keys).defaults(PropertyManager.getSharedInstance())
configProps = (
self.sdr.getProps()
.collect(*OpenWebRxReceiverClient.config_keys)
.defaults(PropertyManager.getSharedInstance())
)
def sendConfig(key, value):
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
@ -89,7 +120,6 @@ class OpenWebRxReceiverClient(Client):
frequencyRange = (cf - srh, cf + srh)
self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencis(frequencyRange))
self.configSub = configProps.wire(sendConfig)
sendConfig(None, None)
@ -118,8 +148,11 @@ class OpenWebRxReceiverClient(Client):
def setParams(self, params):
# only the keys in the protected property manager can be overridden from the web
protected = self.sdr.getProps().collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain") \
protected = (
self.sdr.getProps()
.collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain")
.defaults(PropertyManager.getSharedInstance())
)
for key, value in params.items():
protected[key] = value
@ -134,13 +167,13 @@ class OpenWebRxReceiverClient(Client):
self.protected_send(bytes([0x02]) + data)
def write_s_meter_level(self, level):
self.protected_send({"type":"smeter","value":level})
self.protected_send({"type": "smeter", "value": level})
def write_cpu_usage(self, usage):
self.protected_send({"type":"cpuusage","value":usage})
self.protected_send({"type": "cpuusage", "value": usage})
def write_clients(self, clients):
self.protected_send({"type":"clients","value":clients})
self.protected_send({"type": "clients", "value": clients})
def write_secondary_fft(self, data):
self.protected_send(bytes([0x03]) + data)
@ -149,22 +182,22 @@ class OpenWebRxReceiverClient(Client):
self.protected_send(bytes([0x04]) + data)
def write_secondary_dsp_config(self, cfg):
self.protected_send({"type":"secondary_config", "value":cfg})
self.protected_send({"type": "secondary_config", "value": cfg})
def write_config(self, cfg):
self.protected_send({"type":"config","value":cfg})
self.protected_send({"type": "config", "value": cfg})
def write_receiver_details(self, details):
self.protected_send({"type":"receiver_details","value":details})
self.protected_send({"type": "receiver_details", "value": details})
def write_profiles(self, profiles):
self.protected_send({"type":"profiles","value":profiles})
self.protected_send({"type": "profiles", "value": profiles})
def write_features(self, features):
self.protected_send({"type":"features","value":features})
self.protected_send({"type": "features", "value": features})
def write_metadata(self, metadata):
self.protected_send({"type":"metadata","value":metadata})
self.protected_send({"type": "metadata", "value": metadata})
def write_wsjt_message(self, message):
self.protected_send({"type": "wsjt_message", "value": message})
@ -187,10 +220,11 @@ class MapConnection(Client):
super().close()
def write_config(self, cfg):
self.protected_send({"type":"config","value":cfg})
self.protected_send({"type": "config", "value": cfg})
def write_update(self, update):
self.protected_send({"type":"update","value":update})
self.protected_send({"type": "update", "value": update})
class WebSocketMessageHandler(object):
def __init__(self):
@ -199,11 +233,11 @@ class WebSocketMessageHandler(object):
self.dsp = None
def handleTextMessage(self, conn, message):
if (message[:16] == "SERVER DE CLIENT"):
if message[:16] == "SERVER DE CLIENT":
meta = message[17:].split(" ")
self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)}
conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version = openwebrx_version))
conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version))
logger.debug("client connection intitialized")
if "type" in self.handshake:

View File

@ -11,13 +11,16 @@ from owrx.version import openwebrx_version
from owrx.feature import FeatureDetector
import logging
logger = logging.getLogger(__name__)
class Controller(object):
def __init__(self, handler, request):
self.handler = handler
self.request = request
def send_response(self, content, code = 200, content_type = "text/html", last_modified: datetime = None, max_age = None):
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None):
self.handler.send_response(code)
if content_type is not None:
self.handler.send_header("Content-Type", content_type)
@ -26,7 +29,7 @@ class Controller(object):
if max_age is not None:
self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
self.handler.end_headers()
if (type(content) == str):
if type(content) == str:
content = content.encode()
self.handler.wfile.write(content)
@ -45,44 +48,49 @@ class StatusController(Controller):
"asl": pm["receiver_asl"],
"loc": pm["receiver_location"],
"sw_version": openwebrx_version,
"avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png")
"avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"),
}
self.send_response("\n".join(["{key}={value}".format(key = key, value = value) for key, value in vars.items()]))
self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()]))
class AssetsController(Controller):
def serve_file(self, file, content_type = None):
def serve_file(self, file, content_type=None):
try:
modified = datetime.fromtimestamp(os.path.getmtime('htdocs/' + file))
modified = datetime.fromtimestamp(os.path.getmtime("htdocs/" + file))
if "If-Modified-Since" in self.handler.headers:
client_modified = datetime.strptime(self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z")
client_modified = datetime.strptime(
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
)
if modified <= client_modified:
self.send_response("", code = 304)
self.send_response("", code=304)
return
f = open('htdocs/' + file, 'rb')
f = open("htdocs/" + file, "rb")
data = f.read()
f.close()
if content_type is None:
(content_type, encoding) = mimetypes.MimeTypes().guess_type(file)
self.send_response(data, content_type = content_type, last_modified = modified, max_age = 3600)
self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600)
except FileNotFoundError:
self.send_response("file not found", code = 404)
self.send_response("file not found", code=404)
def handle_request(self):
filename = self.request.matches.group(1)
self.serve_file(filename)
class TemplateController(Controller):
def render_template(self, file, **vars):
f = open('htdocs/' + file, 'r')
f = open("htdocs/" + file, "r")
template = Template(f.read())
f.close()
return template.safe_substitute(**vars)
def serve_template(self, file, **vars):
self.send_response(self.render_template(file, **vars), content_type = 'text/html')
self.send_response(self.render_template(file, **vars), content_type="text/html")
def default_variables(self):
return {}
@ -90,8 +98,8 @@ class TemplateController(Controller):
class WebpageController(TemplateController):
def template_variables(self):
header = self.render_template('include/header.include.html')
return { "header": header }
header = self.render_template("include/header.include.html")
return {"header": header}
class IndexController(WebpageController):
@ -101,17 +109,20 @@ class IndexController(WebpageController):
class MapController(WebpageController):
def handle_request(self):
#TODO check if we have a google maps api key first?
# TODO check if we have a google maps api key first?
self.serve_template("map.html", **self.template_variables())
class FeatureController(WebpageController):
def handle_request(self):
self.serve_template("features.html", **self.template_variables())
class ApiController(Controller):
def handle_request(self):
data = json.dumps(FeatureDetector().feature_report())
self.send_response(data, content_type = "application/json")
self.send_response(data, content_type="application/json")
class WebSocketController(Controller):
def handle_request(self):

View File

@ -7,6 +7,7 @@ from distutils.version import LooseVersion
import inspect
import logging
logger = logging.getLogger(__name__)
@ -16,14 +17,14 @@ class UnknownFeatureException(Exception):
class FeatureDetector(object):
features = {
"core": [ "csdr", "nmux", "nc" ],
"rtl_sdr": [ "rtl_sdr" ],
"sdrplay": [ "rx_tools" ],
"hackrf": [ "hackrf_transfer" ],
"airspy": [ "airspy_rx" ],
"digital_voice_digiham": [ "digiham", "sox" ],
"digital_voice_dsd": [ "dsd", "sox", "digiham" ],
"wsjt-x": [ "wsjtx", "sox" ]
"core": ["csdr", "nmux", "nc"],
"rtl_sdr": ["rtl_sdr"],
"sdrplay": ["rx_tools"],
"hackrf": ["hackrf_transfer"],
"airspy": ["airspy_rx"],
"digital_voice_digiham": ["digiham", "sox"],
"digital_voice_dsd": ["dsd", "sox", "digiham"],
"wsjt-x": ["wsjtx", "sox"],
}
def feature_availability(self):
@ -36,14 +37,14 @@ class FeatureDetector(object):
"available": available,
# as of now, features are always enabled as soon as they are available. this may change in the future.
"enabled": available,
"description": self.get_requirement_description(name)
"description": self.get_requirement_description(name),
}
def feature_details(name):
return {
"description": "",
"available": self.is_available(name),
"requirements": {name: requirement_details(name) for name in self.get_requirements(name)}
"requirements": {name: requirement_details(name) for name in self.get_requirements(name)},
}
return {name: feature_details(name) for name in FeatureDetector.features}
@ -55,7 +56,7 @@ class FeatureDetector(object):
try:
return FeatureDetector.features[feature]
except KeyError:
raise UnknownFeatureException("Feature \"{0}\" is not known.".format(feature))
raise UnknownFeatureException('Feature "{0}" is not known.'.format(feature))
def has_requirements(self, requirements):
passed = True
@ -102,7 +103,7 @@ class FeatureDetector(object):
Nc is the client used to connect to the nmux multiplexer. It is provided by either the BSD netcat (recommended
for better performance) or GNU netcat packages. Please check your distribution package manager for options.
"""
return self.command_is_runnable('nc --help')
return self.command_is_runnable("nc --help")
def has_rtl_sdr(self):
"""
@ -156,7 +157,8 @@ class FeatureDetector(object):
"""
required_version = LooseVersion("0.2")
digiham_version_regex = re.compile('^digiham version (.*)$')
digiham_version_regex = re.compile("^digiham version (.*)$")
def check_digiham_version(command):
try:
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
@ -165,14 +167,21 @@ class FeatureDetector(object):
return version >= required_version
except FileNotFoundError:
return False
return reduce(
and_,
map(
check_digiham_version,
["rrc_filter", "ysf_decoder", "dmr_decoder", "mbe_synthesizer", "gfsk_demodulator",
"digitalvoice_filter"]
[
"rrc_filter",
"ysf_decoder",
"dmr_decoder",
"mbe_synthesizer",
"gfsk_demodulator",
"digitalvoice_filter",
],
),
True
True,
)
def has_dsd(self):
@ -203,11 +212,4 @@ class FeatureDetector(object):
[WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions
on how to build from source.
"""
return reduce(
and_,
map(
self.command_is_runnable,
["jt9", "wsprd"]
),
True
)
return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)

View File

@ -1,23 +1,36 @@
from owrx.controllers import StatusController, IndexController, AssetsController, WebSocketController, MapController, FeatureController, ApiController
from owrx.controllers import (
StatusController,
IndexController,
AssetsController,
WebSocketController,
MapController,
FeatureController,
ApiController,
)
from http.server import BaseHTTPRequestHandler
import re
from urllib.parse import urlparse, parse_qs
import logging
logger = logging.getLogger(__name__)
class RequestHandler(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server):
self.router = Router()
super().__init__(request, client_address, server)
def do_GET(self):
self.router.route(self)
class Request(object):
def __init__(self, query = None, matches = None):
def __init__(self, query=None, matches=None):
self.query = query
self.matches = matches
class Router(object):
mappings = [
{"route": "/", "controller": IndexController},
@ -29,8 +42,9 @@ class Router(object):
{"regex": "/(gfx/openwebrx-avatar.png)", "controller": AssetsController},
{"route": "/map", "controller": MapController},
{"route": "/features", "controller": FeatureController},
{"route": "/api/features", "controller": ApiController}
{"route": "/api/features", "controller": ApiController},
]
def find_controller(self, path):
for m in Router.mappings:
if "route" in m:
@ -41,13 +55,16 @@ class Router(object):
matches = regex.match(path)
if matches:
return (m["controller"], matches)
def route(self, handler):
url = urlparse(handler.path)
res = self.find_controller(url.path)
if res is not None:
(controller, matches) = res
query = parse_qs(url.query)
logger.debug("path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches))
logger.debug(
"path: {0}, controller: {1}, query: {2}, matches: {3}".format(handler.path, controller, query, matches)
)
request = Request(query, matches)
controller(handler, request).handle_request()
else:

View File

@ -4,6 +4,7 @@ from owrx.config import PropertyManager
from owrx.bands import Band
import logging
logger = logging.getLogger(__name__)
@ -14,6 +15,7 @@ class Location(object):
class Map(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if Map.sharedInstance is None:
@ -41,16 +43,18 @@ class Map(object):
def addClient(self, client):
self.clients.append(client)
client.write_update([
{
"callsign": callsign,
"location": record["location"].__dict__(),
"lastseen": record["updated"].timestamp() * 1000,
"mode" : record["mode"],
"band" : record["band"].getName() if record["band"] is not None else None
}
for (callsign, record) in self.positions.items()
])
client.write_update(
[
{
"callsign": callsign,
"location": record["location"].__dict__(),
"lastseen": record["updated"].timestamp() * 1000,
"mode": record["mode"],
"band": record["band"].getName() if record["band"] is not None else None,
}
for (callsign, record) in self.positions.items()
]
)
def removeClient(self, client):
try:
@ -61,15 +65,17 @@ class Map(object):
def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None):
ts = datetime.now()
self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band}
self.broadcast([
{
"callsign": callsign,
"location": loc.__dict__(),
"lastseen": ts.timestamp() * 1000,
"mode" : mode,
"band" : band.getName() if band is not None else None
}
])
self.broadcast(
[
{
"callsign": callsign,
"location": loc.__dict__(),
"lastseen": ts.timestamp() * 1000,
"mode": mode,
"band": band.getName() if band is not None else None,
}
]
)
def removeLocation(self, callsign):
self.positions.pop(callsign, None)
@ -84,17 +90,14 @@ class Map(object):
for callsign in to_be_removed:
self.removeLocation(callsign)
class LatLngLocation(Location):
def __init__(self, lat: float, lon: float):
self.lat = lat
self.lon = lon
def __dict__(self):
return {
"type":"latlon",
"lat":self.lat,
"lon":self.lon
}
return {"type": "latlon", "lat": self.lat, "lon": self.lon}
class LocatorLocation(Location):
@ -102,7 +105,4 @@ class LocatorLocation(Location):
self.locator = locator
def __dict__(self):
return {
"type":"locator",
"locator":self.locator
}
return {"type": "locator", "locator": self.locator}

View File

@ -8,8 +8,10 @@ from owrx.map import Map, LatLngLocation
logger = logging.getLogger(__name__)
class DmrCache(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if DmrCache.sharedInstance is None:
@ -18,21 +20,20 @@ class DmrCache(object):
def __init__(self):
self.cache = {}
self.cacheTimeout = timedelta(seconds = 86400)
self.cacheTimeout = timedelta(seconds=86400)
def isValid(self, key):
if not key in self.cache: return False
if not key in self.cache:
return False
entry = self.cache[key]
return entry["timestamp"] + self.cacheTimeout > datetime.now()
def put(self, key, value):
self.cache[key] = {
"timestamp": datetime.now(),
"data": value
}
self.cache[key] = {"timestamp": datetime.now(), "data": value}
def get(self, key):
if not self.isValid(key): return None
if not self.isValid(key):
return None
return self.cache[key]["data"]
@ -52,8 +53,10 @@ class DmrMetaEnricher(object):
del self.threads[id]
def enrich(self, meta):
if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: return None
if not "source" in meta: return None
if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]:
return None
if not "source" in meta:
return None
id = meta["source"]
cache = DmrCache.getSharedInstance()
if not cache.isValid(id):
@ -77,10 +80,7 @@ class YsfMetaEnricher(object):
class MetaParser(object):
enrichers = {
"DMR": DmrMetaEnricher(),
"YSF": YsfMetaEnricher()
}
enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher()}
def __init__(self, handler):
self.handler = handler
@ -93,6 +93,6 @@ class MetaParser(object):
protocol = meta["protocol"]
if protocol in MetaParser.enrichers:
additional_data = MetaParser.enrichers[protocol].enrich(meta)
if additional_data is not None: meta["additional"] = additional_data
if additional_data is not None:
meta["additional"] = additional_data
self.handler.write_metadata(meta)

View File

@ -4,23 +4,26 @@ import time
from owrx.config import PropertyManager
import logging
logger = logging.getLogger(__name__)
class SdrHuUpdater(threading.Thread):
def __init__(self):
self.doRun = True
super().__init__(daemon = True)
super().__init__(daemon=True)
def update(self):
pm = PropertyManager.getSharedInstance()
cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}\" 2>&1".format(**pm.__dict__())
cmd = 'wget --timeout=15 -4qO- https://sdr.hu/update --post-data "url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}" 2>&1'.format(
**pm.__dict__()
)
logger.debug(cmd)
returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
returned=returned[0].decode('utf-8')
returned = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
returned = returned[0].decode("utf-8")
if "UPDATE:" in returned:
retrytime_mins = 20
value=returned.split("UPDATE:")[1].split("\n",1)[0]
value = returned.split("UPDATE:")[1].split("\n", 1)[0]
if value.startswith("SUCCESS"):
logger.info("Update succeeded!")
else:
@ -33,4 +36,4 @@ class SdrHuUpdater(threading.Thread):
def run(self):
while self.doRun:
retrytime_mins = self.update()
time.sleep(60*retrytime_mins)
time.sleep(60 * retrytime_mins)

View File

@ -14,10 +14,12 @@ import logging
logger = logging.getLogger(__name__)
class SdrService(object):
sdrProps = None
sources = {}
lastPort = None
@staticmethod
def getNextPort():
pm = PropertyManager.getSharedInstance()
@ -29,45 +31,61 @@ class SdrService(object):
if SdrService.lastPort > end:
raise IndexError("no more available ports to start more sdrs")
return SdrService.lastPort
@staticmethod
def loadProps():
if SdrService.sdrProps is None:
pm = PropertyManager.getSharedInstance()
featureDetector = FeatureDetector()
def loadIntoPropertyManager(dict: dict):
propertyManager = PropertyManager()
for (name, value) in dict.items():
propertyManager[name] = value
return propertyManager
def sdrTypeAvailable(value):
try:
if not featureDetector.is_available(value["type"]):
logger.error("The RTL source type \"{0}\" is not available. please check requirements.".format(value["type"]))
logger.error(
'The RTL source type "{0}" is not available. please check requirements.'.format(
value["type"]
)
)
return False
return True
except UnknownFeatureException:
logger.error("The RTL source type \"{0}\" is invalid. Please check your configuration".format(value["type"]))
logger.error(
'The RTL source type "{0}" is invalid. Please check your configuration'.format(value["type"])
)
return False
# transform all dictionary items into PropertyManager object, filtering out unavailable ones
SdrService.sdrProps = {
name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value)
}
logger.info("SDR sources loaded. Availables SDRs: {0}".format(", ".join(map(lambda x: x["name"], SdrService.sdrProps.values()))))
logger.info(
"SDR sources loaded. Availables SDRs: {0}".format(
", ".join(map(lambda x: x["name"], SdrService.sdrProps.values()))
)
)
@staticmethod
def getSource(id = None):
def getSource(id=None):
SdrService.loadProps()
if id is None:
# TODO: configure default sdr in config? right now it will pick the first one off the list.
id = list(SdrService.sdrProps.keys())[0]
sources = SdrService.getSources()
return sources[id]
@staticmethod
def getSources():
SdrService.loadProps()
for id in SdrService.sdrProps.keys():
if not id in SdrService.sources:
props = SdrService.sdrProps[id]
className = ''.join(x for x in props["type"].title() if x.isalnum()) + "Source"
className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source"
cls = getattr(sys.modules[__name__], className)
SdrService.sources[id] = cls(props, SdrService.getNextPort())
return SdrService.sources
@ -85,6 +103,7 @@ class SdrSource(object):
logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value))
self.stop()
self.start()
self.rtlProps.wire(restart)
self.port = port
self.monitor = None
@ -102,7 +121,7 @@ class SdrSource(object):
def getFormatConversion(self):
return None
def activateProfile(self, id = None):
def activateProfile(self, id=None):
profiles = self.props["profiles"]
if id is None:
id = list(profiles.keys())[0]
@ -110,7 +129,8 @@ class SdrSource(object):
profile = profiles[id]
for (key, value) in profile.items():
# skip the name, that would overwrite the source name.
if key == "name": continue
if key == "name":
continue
self.props[key] = value
def getProfiles(self):
@ -134,7 +154,9 @@ class SdrSource(object):
props = self.rtlProps
start_sdr_command = self.getCommand().format(
**props.collect("samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain").__dict__()
**props.collect(
"samp_rate", "center_freq", "ppm", "rf_gain", "lna_gain", "rf_amp", "antenna", "if_gain"
).__dict__()
)
format_conversion = self.getFormatConversion()
@ -142,14 +164,22 @@ class SdrSource(object):
start_sdr_command += " | " + format_conversion
nmux_bufcnt = nmux_bufsize = 0
while nmux_bufsize < props["samp_rate"]/4: nmux_bufsize += 4096
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6: nmux_bufcnt += 1
while nmux_bufsize < props["samp_rate"] / 4:
nmux_bufsize += 4096
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
nmux_bufcnt += 1
if nmux_bufcnt == 0 or nmux_bufsize == 0:
logger.error("Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py")
logger.error(
"Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
)
self.modificationLock.release()
return
logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port)
cmd = start_sdr_command + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (
nmux_bufsize,
nmux_bufcnt,
self.port,
)
self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
logger.info("Started rtl source: " + cmd)
@ -158,7 +188,7 @@ class SdrSource(object):
logger.debug("shut down with RC={0}".format(rc))
self.monitor = None
self.monitor = threading.Thread(target = wait_for_process_to_end)
self.monitor = threading.Thread(target=wait_for_process_to_end)
self.monitor.start()
while True:
@ -201,6 +231,7 @@ class SdrSource(object):
def addClient(self, c):
self.clients.append(c)
self.start()
def removeClient(self, c):
try:
self.clients.remove(c)
@ -236,6 +267,7 @@ class RtlSdrSource(SdrSource):
def getFormatConversion(self):
return "csdr convert_u8_f"
class HackrfSource(SdrSource):
def getCommand(self):
return "hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-"
@ -243,39 +275,54 @@ class HackrfSource(SdrSource):
def getFormatConversion(self):
return "csdr convert_s8_f"
class SdrplaySource(SdrSource):
def getCommand(self):
command = "rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm}"
gainMap = { "rf_gain" : "RFGR", "if_gain" : "IFGR"}
gains = [ "{0}={{{1}}}".format(gainMap[name], name) for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items() if value is not None ]
gainMap = {"rf_gain": "RFGR", "if_gain": "IFGR"}
gains = [
"{0}={{{1}}}".format(gainMap[name], name)
for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items()
if value is not None
]
if gains:
command += " -g {gains}".format(gains = ",".join(gains))
command += " -g {gains}".format(gains=",".join(gains))
if self.rtlProps["antenna"] is not None:
command += " -a \"{antenna}\""
command += ' -a "{antenna}"'
command += " -"
return command
def sleepOnRestart(self):
time.sleep(1)
class AirspySource(SdrSource):
def getCommand(self):
frequency = self.props['center_freq'] / 1e6
frequency = self.props["center_freq"] / 1e6
command = "airspy_rx"
command += " -f{0}".format(frequency)
command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}"
return command
def getFormatConversion(self):
return "csdr convert_s16_f"
class SpectrumThread(csdr.output):
def __init__(self, sdrSource):
self.sdrSource = sdrSource
super().__init__()
self.props = props = self.sdrSource.props.collect(
"samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor", "fft_compression",
"csdr_dynamic_bufsize", "csdr_print_bufsizes", "csdr_through", "temporary_directory"
"samp_rate",
"fft_size",
"fft_fps",
"fft_voverlap_factor",
"fft_compression",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"temporary_directory",
).defaults(PropertyManager.getSharedInstance())
self.dsp = dsp = csdr.dsp(self)
@ -288,7 +335,11 @@ class SpectrumThread(csdr.output):
fft_fps = props["fft_fps"]
fft_voverlap_factor = props["fft_voverlap_factor"]
dsp.set_fft_averages(int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor))) if fft_voverlap_factor>0 else 0)
dsp.set_fft_averages(
int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor)))
if fft_voverlap_factor > 0
else 0
)
self.subscriptions = [
props.getProperty("samp_rate").wire(dsp.set_samp_rate),
@ -296,7 +347,7 @@ class SpectrumThread(csdr.output):
props.getProperty("fft_fps").wire(dsp.set_fft_fps),
props.getProperty("fft_compression").wire(dsp.set_fft_compression),
props.getProperty("temporary_directory").wire(dsp.set_temporary_directory),
props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages)
props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages),
]
set_fft_averages(None, None)
@ -317,7 +368,7 @@ class SpectrumThread(csdr.output):
return
if self.props["csdr_dynamic_bufsize"]:
read_fn(8) #dummy read to skip bufsize & preamble
read_fn(8) # dummy read to skip bufsize & preamble
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
def pipe():
@ -329,7 +380,7 @@ class SpectrumThread(csdr.output):
else:
self.sdrSource.writeSpectrumData(data)
threading.Thread(target = pipe).start()
threading.Thread(target=pipe).start()
def stop(self):
self.dsp.stop()
@ -340,9 +391,11 @@ class SpectrumThread(csdr.output):
def onSdrAvailable(self):
self.dsp.start()
def onSdrUnavailable(self):
self.dsp.stop()
class DspManager(csdr.output):
def __init__(self, handler, sdrSource):
self.handler = handler
@ -350,11 +403,24 @@ class DspManager(csdr.output):
self.metaParser = MetaParser(self.handler)
self.wsjtParser = WsjtParser(self.handler)
self.localProps = self.sdrSource.getProps().collect(
"audio_compression", "fft_compression", "digimodes_fft_size", "csdr_dynamic_bufsize",
"csdr_print_bufsizes", "csdr_through", "digimodes_enable", "samp_rate", "digital_voice_unvoiced_quality",
"dmr_filter", "temporary_directory", "center_freq"
).defaults(PropertyManager.getSharedInstance())
self.localProps = (
self.sdrSource.getProps()
.collect(
"audio_compression",
"fft_compression",
"digimodes_fft_size",
"csdr_dynamic_bufsize",
"csdr_print_bufsizes",
"csdr_through",
"digimodes_enable",
"samp_rate",
"digital_voice_unvoiced_quality",
"dmr_filter",
"temporary_directory",
"center_freq",
)
.defaults(PropertyManager.getSharedInstance())
)
self.dsp = csdr.dsp(self)
self.dsp.nc_port = self.sdrSource.getPort()
@ -386,28 +452,33 @@ class DspManager(csdr.output):
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter),
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory),
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq)
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq),
]
self.dsp.set_offset_freq(0)
self.dsp.set_bpf(-4000,4000)
self.dsp.set_bpf(-4000, 4000)
self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"]
self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
self.dsp.csdr_through = self.localProps["csdr_through"]
if (self.localProps["digimodes_enable"]):
if self.localProps["digimodes_enable"]:
def set_secondary_mod(mod):
if mod == False: mod = None
if mod == False:
mod = None
self.dsp.set_secondary_demodulator(mod)
if mod is not None:
self.handler.write_secondary_dsp_config({
"secondary_fft_size":self.localProps["digimodes_fft_size"],
"if_samp_rate":self.dsp.if_samp_rate(),
"secondary_bw":self.dsp.secondary_bw()
})
self.handler.write_secondary_dsp_config(
{
"secondary_fft_size": self.localProps["digimodes_fft_size"],
"if_samp_rate": self.dsp.if_samp_rate(),
"secondary_bw": self.dsp.secondary_bw(),
}
)
self.subscriptions += [
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod),
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq)
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq),
]
self.sdrSource.addClient(self)
@ -426,7 +497,7 @@ class DspManager(csdr.output):
"secondary_fft": self.handler.write_secondary_fft,
"secondary_demod": self.handler.write_secondary_demod,
"meta": self.metaParser.parse,
"wsjt_demod": self.wsjtParser.parse
"wsjt_demod": self.wsjtParser.parse,
}
write = writers[t]
@ -440,6 +511,7 @@ class DspManager(csdr.output):
run = False
else:
write(data)
return copy
threading.Thread(target=pump(read_fn, write)).start()
@ -462,8 +534,10 @@ class DspManager(csdr.output):
logger.debug("received onSdrUnavailable, shutting down DspSource")
self.dsp.stop()
class CpuUsageThread(threading.Thread):
sharedInstance = None
@staticmethod
def getSharedInstance():
if CpuUsageThread.sharedInstance is None:
@ -491,21 +565,23 @@ class CpuUsageThread(threading.Thread):
def get_cpu_usage(self):
try:
f = open("/proc/stat","r")
f = open("/proc/stat", "r")
except:
return 0 #Workaround, possibly we're on a Mac
return 0 # Workaround, possibly we're on a Mac
line = ""
while not "cpu " in line: line=f.readline()
while not "cpu " in line:
line = f.readline()
f.close()
spl = line.split(" ")
worktime = int(spl[2]) + int(spl[3]) + int(spl[4])
idletime = int(spl[5])
dworktime = (worktime - self.last_worktime)
didletime = (idletime - self.last_idletime)
rate = float(dworktime) / (didletime+dworktime)
dworktime = worktime - self.last_worktime
didletime = idletime - self.last_idletime
rate = float(dworktime) / (didletime + dworktime)
self.last_worktime = worktime
self.last_idletime = idletime
if (self.last_worktime==0): return 0
if self.last_worktime == 0:
return 0
return rate
def add_client(self, c):
@ -523,11 +599,14 @@ class CpuUsageThread(threading.Thread):
CpuUsageThread.sharedInstance = None
self.doRun = False
class TooManyClientsException(Exception):
pass
class ClientRegistry(object):
sharedInstance = None
@staticmethod
def getSharedInstance():
if ClientRegistry.sharedInstance is None:
@ -558,4 +637,4 @@ class ClientRegistry(object):
self.clients.remove(client)
except ValueError:
pass
self.broadcast()
self.broadcast()

View File

@ -1 +1 @@
openwebrx_version = "v0.18"
openwebrx_version = "v0.18"

View File

@ -3,69 +3,76 @@ import hashlib
import json
import logging
logger = logging.getLogger(__name__)
class WebSocketConnection(object):
def __init__(self, handler, messageHandler):
self.handler = handler
self.messageHandler = messageHandler
my_headers = self.handler.headers.items()
my_header_keys = list(map(lambda x:x[0],my_headers))
h_key_exists = lambda x:my_header_keys.count(x)
h_value = lambda x:my_headers[my_header_keys.index(x)][1]
if (not h_key_exists("Upgrade")) or not (h_value("Upgrade")=="websocket") or (not h_key_exists("Sec-WebSocket-Key")):
my_header_keys = list(map(lambda x: x[0], my_headers))
h_key_exists = lambda x: my_header_keys.count(x)
h_value = lambda x: my_headers[my_header_keys.index(x)][1]
if (
(not h_key_exists("Upgrade"))
or not (h_value("Upgrade") == "websocket")
or (not h_key_exists("Sec-WebSocket-Key"))
):
raise WebSocketException
ws_key = h_value("Sec-WebSocket-Key")
shakey = hashlib.sha1()
shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key = ws_key).encode())
shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode())
ws_key_toreturn = base64.b64encode(shakey.digest())
self.handler.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(ws_key_toreturn.decode()).encode())
self.handler.wfile.write(
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(
ws_key_toreturn.decode()
).encode()
)
def get_header(self, size, opcode):
ws_first_byte = 0b10000000 | (opcode & 0x0F)
if (size > 2**16 - 1):
if size > 2 ** 16 - 1:
# frame size can be increased up to 2^64 by setting the size to 127
# anything beyond that would need to be segmented into frames. i don't really think we'll need more.
return bytes([
ws_first_byte,
127,
(size >> 56) & 0xff,
(size >> 48) & 0xff,
(size >> 40) & 0xff,
(size >> 32) & 0xff,
(size >> 24) & 0xff,
(size >> 16) & 0xff,
(size >> 8) & 0xff,
size & 0xff
])
elif (size > 125):
return bytes(
[
ws_first_byte,
127,
(size >> 56) & 0xFF,
(size >> 48) & 0xFF,
(size >> 40) & 0xFF,
(size >> 32) & 0xFF,
(size >> 24) & 0xFF,
(size >> 16) & 0xFF,
(size >> 8) & 0xFF,
size & 0xFF,
]
)
elif size > 125:
# up to 2^16 can be sent using the extended payload size field by putting the size to 126
return bytes([
ws_first_byte,
126,
(size >> 8) & 0xff,
size & 0xff
])
return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF])
else:
# 125 bytes binary message in a single unmasked frame
return bytes([ws_first_byte, size])
def send(self, data):
# convenience
if (type(data) == dict):
if type(data) == dict:
# allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway.
data = json.dumps(data, allow_nan = False)
data = json.dumps(data, allow_nan=False)
# string-type messages are sent as text frames
if (type(data) == str):
if type(data) == str:
header = self.get_header(len(data), 1)
data_to_send = header + data.encode('utf-8')
data_to_send = header + data.encode("utf-8")
# anything else as binary
else:
header = self.get_header(len(data), 2)
data_to_send = header + data
written = self.handler.wfile.write(data_to_send)
if (written != len(data_to_send)):
if written != len(data_to_send):
logger.error("incomplete write! closing socket!")
self.close()
else:
@ -73,25 +80,25 @@ class WebSocketConnection(object):
def read_loop(self):
open = True
while (open):
while open:
header = self.handler.rfile.read(2)
opcode = header[0] & 0x0F
length = header[1] & 0x7F
mask = (header[1] & 0x80) >> 7
if (length == 126):
if length == 126:
header = self.handler.rfile.read(2)
length = (header[0] << 8) + header[1]
if (mask):
if mask:
masking_key = self.handler.rfile.read(4)
data = self.handler.rfile.read(length)
if (mask):
if mask:
data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)])
if (opcode == 1):
message = data.decode('utf-8')
if opcode == 1:
message = data.decode("utf-8")
self.messageHandler.handleTextMessage(self, message)
elif (opcode == 2):
elif opcode == 2:
self.messageHandler.handleBinaryMessage(self, data)
elif (opcode == 8):
elif opcode == 8:
open = False
self.messageHandler.handleClose(self)
else:

View File

@ -12,6 +12,7 @@ from owrx.config import PropertyManager
from owrx.bands import Bandplan
import logging
logger = logging.getLogger(__name__)
@ -29,9 +30,7 @@ class WsjtChopper(threading.Thread):
def getWaveFile(self):
filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format(
tmp_dir = self.tmp_dir,
id = id(self),
timestamp = datetime.utcnow().strftime(self.fileTimestampFormat)
tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.fileTimestampFormat)
)
wavefile = wave.open(filename, "wb")
wavefile.setnchannels(1)
@ -44,13 +43,13 @@ class WsjtChopper(threading.Thread):
zeroed = t.replace(minute=0, second=0, microsecond=0)
delta = t - zeroed
seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval
t = zeroed + timedelta(seconds = seconds)
t = zeroed + timedelta(seconds=seconds)
logger.debug("scheduling: {0}".format(t))
return t.timestamp()
def startScheduler(self):
self._scheduleNextSwitch()
threading.Thread(target = self.scheduler.run).start()
threading.Thread(target=self.scheduler.run).start()
def emptyScheduler(self):
for event in self.scheduler.queue:
@ -132,7 +131,7 @@ class Ft8Chopper(WsjtChopper):
super().__init__(source)
def decoder_commandline(self, file):
#TODO expose decoding quality parameters through config
# TODO expose decoding quality parameters through config
return ["jt9", "--ft8", "-d", "3", file]
@ -143,7 +142,7 @@ class WsprChopper(WsjtChopper):
super().__init__(source)
def decoder_commandline(self, file):
#TODO expose decoding quality parameters through config
# TODO expose decoding quality parameters through config
return ["wsprd", "-d", file]
@ -154,7 +153,7 @@ class Jt65Chopper(WsjtChopper):
super().__init__(source)
def decoder_commandline(self, file):
#TODO expose decoding quality parameters through config
# TODO expose decoding quality parameters through config
return ["jt9", "--jt65", "-d", "3", file]
@ -165,7 +164,7 @@ class Jt9Chopper(WsjtChopper):
super().__init__(source)
def decoder_commandline(self, file):
#TODO expose decoding quality parameters through config
# TODO expose decoding quality parameters through config
return ["jt9", "--jt9", "-d", "3", file]
@ -176,7 +175,7 @@ class Ft4Chopper(WsjtChopper):
super().__init__(source)
def decoder_commandline(self, file):
#TODO expose decoding quality parameters through config
# TODO expose decoding quality parameters through config
return ["jt9", "--ft4", "-d", "3", file]
@ -189,12 +188,7 @@ class WsjtParser(object):
self.dial_freq = None
self.band = None
modes = {
"~": "FT8",
"#": "JT65",
"@": "JT9",
"+": "FT4"
}
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"}
def parse(self, data):
try:
@ -230,8 +224,8 @@ class WsjtParser(object):
dateformat = "%H%M"
else:
dateformat = "%H%M%S"
timestamp = self.parse_timestamp(msg[0:len(dateformat)], dateformat)
msg = msg[len(dateformat) + 1:]
timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
msg = msg[len(dateformat) + 1 :]
modeChar = msg[14:15]
mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
wsjt_msg = msg[17:53].strip()
@ -242,7 +236,7 @@ class WsjtParser(object):
"dt": float(msg[4:8]),
"freq": int(msg[9:13]),
"mode": mode,
"msg": wsjt_msg
"msg": wsjt_msg,
}
def parseLocator(self, msg, mode):
@ -268,7 +262,7 @@ class WsjtParser(object):
"freq": float(msg[14:24]),
"drift": int(msg[25:28]),
"mode": "WSPR",
"msg": wsjt_msg
"msg": wsjt_msg,
}
def parseWsprMessage(self, msg):