Adding ACARS decoding via AcarsDec tool.

This commit is contained in:
Marat Fayzullin 2023-09-08 13:22:23 -04:00
parent 75fdb06997
commit cb856fa02f
15 changed files with 168 additions and 10 deletions

View File

@ -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
#

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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");

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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()

View File

@ -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")

View File

@ -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",

View File

@ -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)

View File

@ -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