diff --git a/README.md b/README.md index 6ed54ffe..711164cb 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ It has the following features: - supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices) - Multiple SDR devices can be used simultaneously - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN) -- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4, +- [wsjt-x](https://wsjt.sourceforge.io/) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4, FST4W) - [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets - [JS8Call](http://js8call.com/) support diff --git a/bands.json b/bands.json index f3da83ba..bd8476aa 100644 --- a/bands.json +++ b/bands.json @@ -393,31 +393,40 @@ "upper_bound": 446200000, "tags": ["public"] }, + { + "name": "ADS-B Reporting", + "lower_bound": 960000000, + "upper_bound": 1215000000, + "tags": ["service"], + "frequencies": { + "adsb": 1090000000 + } + }, { "name": "LPD433", "lower_bound": 433050000, "upper_bound": 434790000, + "tags": ["public"], "frequencies": { "ism": 433920000 - }, - "tags": ["public"] + } }, { "name": "VHF Air", "lower_bound": 108000000, "upper_bound": 137000000, + "tags": ["service"], "frequencies": { "vdl2": 136975000 }, - "tags": ["service"] }, { "name": "VHF Marine", "lower_bound": 156000000, "upper_bound": 174000000, + "tags": ["service"], "frequencies": { "ais": [161975000, 162025000] - }, - "tags": ["service"] + } } ] diff --git a/csdr/chain/digimodes.py b/csdr/chain/digimodes.py index b2ddcf5a..bfe6fb0a 100644 --- a/csdr/chain/digimodes.py +++ b/csdr/chain/digimodes.py @@ -5,7 +5,7 @@ from owrx.aprs.kiss import KissDeframer from owrx.aprs import Ax25Parser, AprsParser from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder, JKRttyDecoder, BaudotDecoder, Lowpass, RttyDecoder, CwDecoder, SstvDecoder, FaxDecoder, Shift from pycsdr.types import Format -from owrx.aprs.module import DirewolfModule +from owrx.aprs.direwolf import DirewolfModule from owrx.sstv import SstvParser from owrx.fax import FaxParser from owrx.config import Config diff --git a/owrx/aprs/direwolf.py b/owrx/aprs/direwolf.py index 8237f97a..82836ead 100644 --- a/owrx/aprs/direwolf.py +++ b/owrx/aprs/direwolf.py @@ -1,6 +1,12 @@ -import random +from pycsdr.types import Format +from pycsdr.modules import Writer, TcpSource, ExecModule, CallbackWriter +from csdr.module import LogWriter +from owrx.config.core import CoreConfig from owrx.config import Config from abc import ABC, abstractmethod +import time +import os +import random import socket import logging @@ -136,3 +142,71 @@ IGLOGIN {callsign} {password} ) return config + + +class DirewolfModule(ExecModule, DirewolfConfigSubscriber): + def __init__(self, service: bool = False, ais: bool = False): + self.tcpSource = None + self.writer = None + self.service = service + self.ais = ais + self.direwolfConfigPath = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format( + tmp_dir=CoreConfig().get_temporary_directory(), myid=id(self) + ) + + self.direwolfConfig = DirewolfConfig() + self.direwolfConfig.wire(self) + self.__writeConfig() + + # compose command line + cmdLine = ["direwolf", "-c", self.direwolfConfigPath, "-r", "48000", "-t", "0", "-q", "d", "-q", "h"] + + # for AIS mode, add -B AIS -A + if self.ais: + cmdLine += ["-B", "AIS", "-A"] + + super().__init__(Format.SHORT, Format.CHAR, cmdLine) + # direwolf supplies the data via a socket which we tap into in start() + # the output on its STDOUT is informative, but we still want to log it + super().setWriter(LogWriter(__name__)) + self.start() + + def __writeConfig(self): + file = open(self.direwolfConfigPath, "w") + file.write(self.direwolfConfig.getConfig(self.service)) + file.close() + + def setWriter(self, writer: Writer) -> None: + self.writer = writer + if self.tcpSource is not None: + self.tcpSource.setWriter(writer) + + def start(self): + delay = 0.5 + retries = 0 + while True: + try: + self.tcpSource = TcpSource(self.direwolfConfig.getPort(), Format.CHAR) + if self.writer: + self.tcpSource.setWriter(self.writer) + break + except ConnectionError: + if retries > 20: + logger.error("maximum number of connection attempts reached. did direwolf start up correctly?") + raise + retries += 1 + time.sleep(delay) + + def restart(self): + self.__writeConfig() + super().restart() + self.start() + + def onConfigChanged(self): + self.restart() + + def stop(self) -> None: + super().stop() + os.unlink(self.direwolfConfigPath) + self.direwolfConfig.unwire(self) + self.direwolfConfig = None diff --git a/owrx/bookmarks.py b/owrx/bookmarks.py index accc394e..9aafe449 100644 --- a/owrx/bookmarks.py +++ b/owrx/bookmarks.py @@ -36,7 +36,7 @@ class Bookmark(object): } -class BookmakrSubscription(object): +class BookmarkSubscription(object): def __init__(self, subscriptee, range, subscriber: callable): self.subscriptee = subscriptee self.range = range @@ -67,11 +67,7 @@ class Bookmarks(object): self.bookmarks = [] self.subscriptions = [] # Known bookmark files, starting with the main file - self.fileList = [ - Bookmarks._getBookmarksFile(), - "bookmarks.json", - "/etc/openwebrx/bookmarks.json", - ] + self.fileList = [Bookmarks._getBookmarksFile(), "/etc/openwebrx/bookmarks.json", "bookmarks.json"] # Find additional bookmark files in the bookmarks.d folder try: bookmarksDir = "/etc/openwebrx/bookmarks.d" @@ -166,9 +162,11 @@ class Bookmarks(object): logger.exception("Error while calling bookmark subscriptions") def subscribe(self, range, callback): - self.subscriptions.append(BookmakrSubscription(self, range, callback)) + sub = BookmarkSubscription(self, range, callback) + self.subscriptions.append(BookmarkSubscription(self, range, callback)) + return sub - def unsubscribe(self, subscriptions: BookmakrSubscription): - if subscriptions not in self.subscriptions: + def unsubscribe(self, subscription: BookmarkSubscription): + if subscription not in self.subscriptions: return - self.subscriptions.remove(subscriptions) + self.subscriptions.remove(subscription) diff --git a/owrx/feature.py b/owrx/feature.py index 41f5e77a..8649b69e 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -13,7 +13,6 @@ from datetime import datetime, timedelta import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class UnknownFeatureException(Exception): @@ -76,22 +75,22 @@ class FeatureDetector(object): # optional features and their requirements "digital_voice_digiham": ["digiham", "codecserver_ambe"], "digital_voice_freedv": ["freedv_rx"], - "digital_voice_m17": ["m17_demod", "digiham"], + "digital_voice_m17": ["m17_demod"], "wsjt-x": ["wsjtx"], "wsjt-x-2-3": ["wsjtx_2_3"], "wsjt-x-2-4": ["wsjtx_2_4"], "msk144": ["msk144decoder"], "packet": ["direwolf"], "pocsag": ["digiham"], - "page": ["multimon"], - "selcall": ["multimon"], - "ism": ["rtl433"], - "hfdl": ["dumphfdl"], - "vdl2": ["dumpvdl2"], - "adsb": ["dump1090"], - "acars": ["acarsdec"], "js8call": ["js8", "js8py"], "drm": ["dream"], + "adsb": ["dump1090"], + "ism": ["rtl_433"], + "hfdl": ["dumphfdl"], + "vdl2": ["dumpvdl2"], + "acars": ["acarsdec"], + "page": ["multimon"], + "selcall": ["multimon"], "png": ["imagemagick"], } @@ -145,14 +144,12 @@ class FeatureDetector(object): if cache.has(requirement): return cache.get(requirement) - logger.debug("performing feature check for %s", requirement) method = self._get_requirement_method(requirement) result = False if method is not None: result = method() else: logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) - logger.debug("feature check for %s complete. result: %s", requirement, result) cache.set(requirement, result) return result @@ -175,7 +172,14 @@ class FeatureDetector(object): cwd=tmp_dir, env=env, ) - rc = process.wait() + while True: + try: + rc = process.wait(10) + break + except subprocess.TimeoutExpired: + logger.warning("feature check command \"%s\" did not return after 10 seconds!", command) + process.kill() + if expected_result is None: return rc != 32512 else: @@ -418,7 +422,7 @@ class FeatureDetector(object): You can find more information [here](https://github.com/mobilinkd/m17-cxx-demod) """ - return self.command_is_runnable("m17-demod") + return self.command_is_runnable("m17-demod", 0) def has_direwolf(self): """ @@ -437,7 +441,7 @@ class FeatureDetector(object): def has_wsjtx(self): """ To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the - [WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions + [WSJT-X homepage](https://wsjt.sourceforge.io/) for ready-made packages or instructions on how to build from source. """ return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) @@ -562,6 +566,9 @@ class FeatureDetector(object): Codecserver is used to decode audio data from digital voice modes using the AMBE codec. You can find more information [here](https://github.com/jketterl/codecserver). + + NOTE: this feature flag checks both the availability of codecserver as well as the availability of the AMBE + codec in the configured codecserver instance. """ config = Config.get() @@ -576,6 +583,65 @@ class FeatureDetector(object): return False except ConnectionError: return False + except RuntimeError as e: + logger.exception("Codecserver error while checking for AMBE support:") + return False + + def has_dump1090(self): + """ + To be able to decode Mode-S and ADS-B traffic originating from airplanes, you need to install the dump1090 + decoder. There is a number of forks available, any version that supports the `--ifile` and `--iformat` arguments + should work. + + Recommended fork: [dump1090 by Flightaware](https://github.com/flightaware/dump1090) + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `dump1090-fa-minimal`. + + If you are running a different fork, please make sure that the command `dump1090` (without suffixes) runs the + version you would like to use. You can use symbolic links or the + [Debian alternatives system](https://wiki.debian.org/DebianAlternatives) to achieve this. + """ + return self.command_is_runnable("dump1090 --version") + + def has_rtl_433(self): + """ + OpenWebRX can make use of the `rtl_433` software to decode various signals in the ISM bands. + + You can find more information [here](https://github.com/merbanan/rtl_433). + + Debian and Ubuntu based systems should be able to install the package `rtl-433` from the package manager. + """ + return self.command_is_runnable("rtl_433 -h") + + def has_dumphfdl(self): + """ + OpenWebRX supports decoding HFDL airplane communications using the `dumphfdl` decoder. + + You can find more information [here](https://github.com/szpajder/dumphfdl) + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `dumphfdl`. + """ + return self.command_is_runnable("dumphfdl --version") + + def has_dumpvdl2(self): + """ + OpenWebRX supports decoding VDL Mode 2 airplane communications using the `dumpvdl2` decoder. + + You can find more information [here](https://github.com/szpajder/dumpvdl2) + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `dumpvdl2`. + """ + return self.command_is_runnable("dumpvdl2 --version") + + def has_acarsdec(self): + """ + OpenWebRX uses the [acarsdec](https://github.com/TLeconte/acarsdec) tool to decode ACARS + traffic. You will have to compile it from source. + """ + return self.command_is_runnable("acarsdec --help") def has_imagemagick(self): """ @@ -592,42 +658,3 @@ class FeatureDetector(object): """ return self.command_is_runnable("multimon-ng --help") - def has_rtl433(self): - """ - OpenWebRX uses the [rtl-433](https://github.com/merbanan/rtl_433) decoder suite to decode various - instrumentation signals. Rtl_433 is available from the package manager on many - distributions, or you can compile it from source. - """ - return self.command_is_runnable("rtl_433 --help") - - def has_dumphfdl(self): - """ - OpenWebRX uses the [DumpHFDL](https://github.com/szpajder/dumphfdl) tool to decode HFDL - aircraft communications. The latest DumpHFDL package is available from the OpenWebRX - repository as "dumphfdl", or you can compile it from source. - """ - return self.command_is_runnable("dumphfdl --help") - - def has_dumpvdl2(self): - """ - OpenWebRX uses the [DumpVDL2](https://github.com/szpajder/dumpvdl2) tool to decode VDL2 - aircraft communications. The latest DumpVDL2 package is available from the OpenWebRX - repository as "dumpvdl2", or you can compile it from source. - """ - return self.command_is_runnable("dumpvdl2 --help") - - def has_dump1090(self): - """ - OpenWebRX uses the [dump1090](https://github.com/antirez/dump1090) tool to decode ADSB - traffic. The latest Dump1090 package is available from the OpenWebRX repository as - "dump1090-fa-minimal", or you can compile it from source. - """ - return self.command_is_runnable("dump1090 --help") - - def has_acarsdec(self): - """ - OpenWebRX uses the [acarsdec](https://github.com/TLeconte/acarsdec) tool to decode ACARS - traffic. You will have to compile it from source. - """ - return self.command_is_runnable("acarsdec --help") - diff --git a/owrx/modes.py b/owrx/modes.py index 3bb2ba69..0f2ed256 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -134,7 +134,7 @@ class Modes(object): AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False), DigitalMode("bpsk31", "BPSK31", underlying=["usb"]), DigitalMode("bpsk63", "BPSK63", underlying=["usb"]), -# Using current RTTY decoder for now +# Testing jketterl's RTTY decoder DigitalMode("jkrtty170", "RTTY 45/170", underlying=["usb", "lsb"]), DigitalMode("jkrtty450", "RTTY 50N/450", underlying=["lsb", "usb"]), DigitalMode("jkrtty85", "RTTY 50N/85", underlying=["lsb", "usb"]), diff --git a/owrx/reporting/pskreporter.py b/owrx/reporting/pskreporter.py index bcad4481..c1dfa2f2 100644 --- a/owrx/reporting/pskreporter.py +++ b/owrx/reporting/pskreporter.py @@ -27,7 +27,7 @@ class PskReporter(Reporter): Supports all valid MODE and SUBMODE values from the ADIF standard. Current version at the time of the last change: - https://www.adif.org/312/ADIF_312.htm#Mode_Enumeration + https://www.adif.org/314/ADIF_314.htm#Mode_Enumeration """ return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W", "MSK144"] @@ -105,27 +105,34 @@ class Uploader(object): # filter out any erroneous encodes encoded = [e for e in encoded if e is not None] - def chunks(l, n): - """Yield successive n-sized chunks from l.""" - for i in range(0, len(l), n): - yield l[i : i + n] + def chunks(block, max_size): + size = 0 + current = [] + for r in block: + if size + len(r) > max_size: + yield current + current = [] + size = 0 + size += len(r) + current.append(r) + yield current rHeader = self.getReceiverInformationHeader() rInfo = self.getReceiverInformation() sHeader = self.getSenderInformationHeader() packets = [] - # 50 seems to be a safe bet - for chunk in chunks(encoded, 50): + # 1200 bytes of sender data should keep the packet size below MTU for most cases + for chunk in chunks(encoded, 1200): sInfo = self.getSenderInformation(chunk) length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo) header = self.getHeader(length) packets.append(header + rHeader + sHeader + rInfo + sInfo) + self.sequence = (self.sequence + len(chunk)) % (1 << 32) return packets def getHeader(self, length): - self.sequence += 1 return bytes( # protocol version [0x00, 0x0A] @@ -142,7 +149,7 @@ class Uploader(object): try: return bytes( self.encodeString(spot["callsign"]) - + list(int(spot["freq"]).to_bytes(4, "big")) + + list(int(spot["freq"]).to_bytes(5, "big")) + list(int(spot["db"]).to_bytes(1, "big", signed=True)) + self.encodeString(spot["mode"]) + self.encodeString(spot["locator"]) @@ -208,7 +215,7 @@ class Uploader(object): # senderCallsign + [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] # frequency - + [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F] + + [0x80, 0x05, 0x00, 0x05, 0x00, 0x00, 0x76, 0x8F] # sNR + [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] # mode