Adding ACARS decoding via AcarsDec tool.
This commit is contained in:
parent
75fdb06997
commit
cb856fa02f
|
|
@ -57,6 +57,27 @@ NOAA-5|||162500|NFM
|
|||
NOAA-6|||162525|NFM
|
||||
NOAA-7|||162550|NFM
|
||||
|
||||
#
|
||||
# ACARS frequencies
|
||||
#
|
||||
USA and Canada Additional|||129125|ACARS
|
||||
USA and Canada Secondary|||130025|ACARS
|
||||
USA Additional|||130425|ACARS
|
||||
USA and Canada Additional|||130450|ACARS
|
||||
USA Additional|||131125|ACARS
|
||||
Japan Primary|||131450|ACARS
|
||||
Air Canada Company Channel|||131475|ACARS
|
||||
European Secondary|||131525|ACARS
|
||||
World Primary|||131550|ACARS
|
||||
Europe Primary|||131725|ACARS
|
||||
New European Channel|||131.850|ACARS
|
||||
USA Additional|||136700|ACARS
|
||||
USA Additional|||136750|ACARS
|
||||
USA Additional|||136800|ACARS
|
||||
SITA|North America||136850|ACARS
|
||||
SITA|Australia & NZ||131550|ACARS
|
||||
ARINC|Australia & NZ||131450|ACARS
|
||||
|
||||
#
|
||||
# VDL2 frequencies
|
||||
#
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from csdr.chain.demodulator import ServiceDemodulator, DialFrequencyReceiver, FixedIfSampleRateChain
|
||||
from csdr.module.toolbox import Rtl433Module, MultimonModule, DumpHfdlModule, DumpVdl2Module, Dump1090Module
|
||||
from csdr.module.toolbox import Rtl433Module, MultimonModule, DumpHfdlModule, DumpVdl2Module, Dump1090Module, AcarsDecModule
|
||||
from pycsdr.modules import FmDemod, AudioResampler, Convert, Agc, Squelch
|
||||
from pycsdr.types import Format
|
||||
from owrx.toolbox import TextParser, PageParser, SelCallParser, IsmParser
|
||||
from owrx.aircraft import HfdlParser, Vdl2Parser, AdsbParser
|
||||
from owrx.aircraft import HfdlParser, Vdl2Parser, AdsbParser, AcarsParser
|
||||
|
||||
|
||||
class IsmDemodulator(ServiceDemodulator, DialFrequencyReceiver):
|
||||
|
|
@ -143,7 +143,6 @@ class Vdl2Demodulator(ServiceDemodulator, DialFrequencyReceiver):
|
|||
|
||||
def setDialFrequency(self, frequency: int) -> None:
|
||||
self.parser.setDialFrequency(frequency)
|
||||
pass
|
||||
|
||||
|
||||
class AdsbDemodulator(ServiceDemodulator, DialFrequencyReceiver):
|
||||
|
|
@ -166,3 +165,25 @@ class AdsbDemodulator(ServiceDemodulator, DialFrequencyReceiver):
|
|||
|
||||
def setDialFrequency(self, frequency: int) -> None:
|
||||
self.parser.setDialFrequency(frequency)
|
||||
|
||||
|
||||
class AcarsDemodulator(ServiceDemodulator, DialFrequencyReceiver):
|
||||
def __init__(self, service: bool = False):
|
||||
self.sampleRate = 12500
|
||||
self.parser = AcarsParser(service=service)
|
||||
workers = [
|
||||
Convert(Format.FLOAT, Format.SHORT),
|
||||
AcarsDecModule(self.sampleRate, jsonOutput = not service),
|
||||
self.parser,
|
||||
]
|
||||
# Connect all the workers
|
||||
super().__init__(workers)
|
||||
|
||||
def getFixedAudioRate(self) -> int:
|
||||
return self.sampleRate
|
||||
|
||||
def supportsSquelch(self) -> bool:
|
||||
return False
|
||||
|
||||
def setDialFrequency(self, frequency: int) -> None:
|
||||
self.parser.setDialFrequency(frequency)
|
||||
|
|
|
|||
|
|
@ -97,3 +97,50 @@ class Dump1090Module(ExecModule):
|
|||
if rawOutput:
|
||||
cmd += [ "--raw" ]
|
||||
super().__init__(Format.COMPLEX_SHORT, Format.CHAR, cmd)
|
||||
|
||||
|
||||
class AcarsDecModule(PopenModule):
|
||||
def __init__(self, sampleRate: int = 12500, jsonOutput: bool = False):
|
||||
self.sampleRate = sampleRate
|
||||
self.jsonOutput = jsonOutput
|
||||
super().__init__()
|
||||
|
||||
def getCommand(self):
|
||||
return [
|
||||
"acarsdec", "-f", "/dev/stdin",
|
||||
"-o", str(4 if self.jsonOutput else 1)
|
||||
]
|
||||
|
||||
def getInputFormat(self) -> Format:
|
||||
return Format.SHORT
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.CHAR
|
||||
|
||||
def start(self):
|
||||
# Create process and pumps
|
||||
super().start()
|
||||
# Created simulated .WAV file header
|
||||
byteRate = (self.sampleRate * 16 * 1) >> 3
|
||||
header = bytearray(44)
|
||||
header[0:3] = b"RIFF"
|
||||
header[4:7] = bytes([36, 0xFF, 0xFF, 0xFF])
|
||||
header[8:11] = b"WAVE"
|
||||
header[12:15] = b"fmt "
|
||||
header[16] = 16 # Chunk size
|
||||
header[20] = 1 # Format (PCM)
|
||||
header[22] = 1 # Number of channels (1)
|
||||
header[24] = self.sampleRate & 0xFF
|
||||
header[25] = (self.sampleRate >> 8) & 0xFF
|
||||
header[26] = (self.sampleRate >> 16) & 0xFF
|
||||
header[27] = (self.sampleRate >> 24) & 0xFF
|
||||
header[28] = byteRate & 0xFF
|
||||
header[29] = (byteRate >> 8) & 0xFF
|
||||
header[30] = (byteRate >> 16) & 0xFF
|
||||
header[31] = (byteRate >> 24) & 0xFF
|
||||
header[32] = 2 # Block alignment (2 bytes)
|
||||
header[34] = 16 # Bits per sample (16)
|
||||
header[36:39] = b"data"
|
||||
header[40:43] = bytes([0, 0xFF, 0xFF, 0xFF])
|
||||
# Send .WAV file header to the process
|
||||
self.process.stdin.write(header)
|
||||
|
|
|
|||
|
|
@ -1445,6 +1445,7 @@ img.openwebrx-mirror-img
|
|||
#openwebrx-panel-digimodes[data-mode="hfdl"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="vdl2"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="adsb"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="acars"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container,
|
||||
|
|
@ -1465,6 +1466,7 @@ img.openwebrx-mirror-img
|
|||
#openwebrx-panel-digimodes[data-mode="hfdl"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="vdl2"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="adsb"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="acars"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel,
|
||||
|
|
@ -1491,6 +1493,7 @@ img.openwebrx-mirror-img
|
|||
#openwebrx-panel-digimodes[data-mode="hfdl"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="vdl2"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="adsb"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="acars"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container,
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ DemodulatorPanel.prototype.updatePanels = function() {
|
|||
toggle_panel("openwebrx-panel-packet-message", ["packet", "ais"].indexOf(modulation) >= 0);
|
||||
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
|
||||
toggle_panel("openwebrx-panel-page-message", modulation === "page");
|
||||
toggle_panel("openwebrx-panel-hfdl-message", ["hfdl", "vdl2", "adsb"].indexOf(modulation) >= 0);
|
||||
toggle_panel("openwebrx-panel-hfdl-message", ["hfdl", "vdl2", "adsb", "acars"].indexOf(modulation) >= 0);
|
||||
toggle_panel("openwebrx-panel-sstv-message", modulation === "sstv");
|
||||
toggle_panel("openwebrx-panel-fax-message", modulation === "fax");
|
||||
toggle_panel("openwebrx-panel-ism-message", modulation === "ism");
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ function MarkerManager() {
|
|||
'OpenWebRX' : '#004000',
|
||||
'HFDL' : '#004000',
|
||||
'VDL2' : '#000080',
|
||||
'ADSB' : '#800000'
|
||||
'ADSB' : '#800000',
|
||||
'ACARS' : '#000000'
|
||||
};
|
||||
|
||||
// Symbols used for marker types
|
||||
|
|
@ -30,7 +31,8 @@ function MarkerManager() {
|
|||
'AIS' : '⩯',
|
||||
'HFDL' : '✈',
|
||||
'VDL2' : '✈',
|
||||
'ADSB' : '✈'
|
||||
'ADSB' : '✈',
|
||||
'ACARS' : '✈'
|
||||
};
|
||||
|
||||
// Marker type shown/hidden status
|
||||
|
|
@ -647,6 +649,7 @@ AprsMarker.prototype.getInfoHTML = function(name, receiverMarker = null) {
|
|||
case 'HFDL':
|
||||
case 'VDL2':
|
||||
case 'ADSB':
|
||||
case 'ACARS':
|
||||
if (this.flight) {
|
||||
name = this.flight;
|
||||
url = flight_url;
|
||||
|
|
|
|||
|
|
@ -335,6 +335,7 @@ $.fn.pageMessagePanel = function() {
|
|||
HfdlMessagePanel = function(el) {
|
||||
MessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
this.modes = ['HFDL', 'VDL2', 'ADSB', 'ACARS'];
|
||||
this.flight_url = null;
|
||||
this.modes_url = null;
|
||||
}
|
||||
|
|
@ -342,7 +343,7 @@ HfdlMessagePanel = function(el) {
|
|||
HfdlMessagePanel.prototype = new MessagePanel();
|
||||
|
||||
HfdlMessagePanel.prototype.supportsMessage = function(message) {
|
||||
return (message['mode'] === 'HFDL') || (message['mode'] === 'VDL2') || (message['mode'] === 'ADSB');
|
||||
return this.modes.indexOf(message['mode']) >= 0;
|
||||
};
|
||||
|
||||
HfdlMessagePanel.prototype.setFlightUrl = function(url) {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class AircraftManager(object):
|
|||
# If both positions exist, compute course
|
||||
if "course" not in data and pos0 and pos1 and pos1 != pos0:
|
||||
item["course"] = round(self.bearing(pos0, pos1))
|
||||
logger.debug("Updated %s course to %d degrees" % (id, item["course"]))
|
||||
#logger.debug("Updated %s course to %d degrees" % (id, item["course"]))
|
||||
# Update timme-to-live, if missing, assume HFDL longevity
|
||||
if "ts" not in data:
|
||||
pm = Config.get()
|
||||
|
|
@ -411,7 +411,7 @@ class AdsbParser(AircraftParser):
|
|||
if msg.startswith("*") and msg.endswith(";") and len(msg) in [16, 30]:
|
||||
# Parse Mode-S message
|
||||
out = self.smode_parser.process(bytes.fromhex(msg[1:-1]))
|
||||
logger.debug("@@@ PARSE OUT: {0}".format(out))
|
||||
#logger.debug("@@@ PARSE OUT: {0}".format(out))
|
||||
# Only consider position and identification reports for now
|
||||
if "identification" in out or "groundspeed" in out or ("lat" in out and "lon" in out):
|
||||
# Add fields for compatibility with other aircraft parsers
|
||||
|
|
@ -456,3 +456,35 @@ class AdsbParser(AircraftParser):
|
|||
|
||||
# No data parsed
|
||||
return {}
|
||||
|
||||
|
||||
#
|
||||
# Parser for ACARS messages coming from AcarsDec in JSON format.
|
||||
#
|
||||
class AcarsParser(AircraftParser):
|
||||
def __init__(self, service: bool = False):
|
||||
super().__init__(filePrefix="ACARS", service=service)
|
||||
|
||||
def parse(self, msg: str):
|
||||
# Expect JSON data in text form
|
||||
data = json.loads(msg)
|
||||
pm = Config.get()
|
||||
ts = data["timestamp"]
|
||||
logger.debug("@@@ ACARS: {0}".format(data))
|
||||
# Collect basic data first
|
||||
out = {
|
||||
"mode" : "ACARS",
|
||||
"time" : datetime.utcfromtimestamp(ts).strftime("%H:%M:%S"),
|
||||
"ts" : ts,
|
||||
"ttl" : ts + pm["acars_ttl"]
|
||||
}
|
||||
# Fetch other data
|
||||
if "tail" in data:
|
||||
out["aircraft"] = data["tail"]
|
||||
if "flight" in data:
|
||||
out["flight"] = data["flight"]
|
||||
if "text" in data:
|
||||
out["message"] = data["text"]
|
||||
# Update aircraft database with the new data
|
||||
AircraftManager.getSharedInstance().update(out)
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -205,6 +205,7 @@ defaultConfig = PropertyLayer(
|
|||
adsb_ttl=900,
|
||||
vdl2_ttl=1800,
|
||||
hfdl_ttl=1800,
|
||||
acars_ttl=1800,
|
||||
fax_postprocess=True,
|
||||
fax_color=False,
|
||||
fax_am=False
|
||||
|
|
|
|||
|
|
@ -85,6 +85,12 @@ class DecodingSettingsController(SettingsFormController):
|
|||
validator=RangeValidator(30, 3600),
|
||||
append="s",
|
||||
),
|
||||
NumberInput(
|
||||
"acars_ttl",
|
||||
"ACARS reports expiration time",
|
||||
validator=RangeValidator(30, 3600),
|
||||
append="s",
|
||||
),
|
||||
),
|
||||
Section(
|
||||
"Fax transmissions",
|
||||
|
|
|
|||
|
|
@ -666,6 +666,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
elif mod == "vdl2":
|
||||
from csdr.chain.toolbox import Vdl2Demodulator
|
||||
return Vdl2Demodulator()
|
||||
elif mod == "acars":
|
||||
from csdr.chain.toolbox import AcarsDemodulator
|
||||
return AcarsDemodulator()
|
||||
elif mod == "adsb":
|
||||
from csdr.chain.toolbox import AdsbDemodulator
|
||||
return AdsbDemodulator()
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ class FeatureDetector(object):
|
|||
"hfdl": ["dumphfdl"],
|
||||
"vdl2": ["dumpvdl2"],
|
||||
"adsb": ["dump1090"],
|
||||
"acars": ["acarsdec"],
|
||||
"js8call": ["js8", "js8py"],
|
||||
"drm": ["dream"],
|
||||
"png": ["imagemagick"],
|
||||
|
|
@ -623,3 +624,10 @@ class FeatureDetector(object):
|
|||
"""
|
||||
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")
|
||||
|
||||
|
|
|
|||
|
|
@ -221,6 +221,15 @@ class Modes(object):
|
|||
service=True,
|
||||
squelch=False
|
||||
),
|
||||
DigitalMode(
|
||||
"acars",
|
||||
"ACARS",
|
||||
underlying=["am"],
|
||||
bandpass=Bandpass(-6250, 6250),
|
||||
requirements=["acars"],
|
||||
service=True,
|
||||
squelch=False
|
||||
),
|
||||
DigitalMode(
|
||||
"adsb",
|
||||
"ADSB",
|
||||
|
|
|
|||
|
|
@ -330,6 +330,9 @@ class ServiceHandler(SdrSourceEventClient):
|
|||
elif mod == "vdl2":
|
||||
from csdr.chain.toolbox import Vdl2Demodulator
|
||||
return Vdl2Demodulator(service=True)
|
||||
elif mod == "acars":
|
||||
from csdr.chain.toolbox import AcarsDemodulator
|
||||
return AcarsDemodulator(service=True)
|
||||
elif mod == "adsb":
|
||||
from csdr.chain.toolbox import AdsbDemodulator
|
||||
return AdsbDemodulator(service=True)
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ class TextParser(ThreadModule):
|
|||
if eol>=0:
|
||||
try:
|
||||
msg = self.data[0:eol].decode(encoding="utf-8", errors="replace")
|
||||
logger.debug("%s: %s" % (self.myName(), msg))
|
||||
#logger.debug("%s: %s" % (self.myName(), msg))
|
||||
# If running as a service...
|
||||
if self.service:
|
||||
# Write message into open log file, including end-of-line
|
||||
|
|
|
|||
Loading…
Reference in New Issue