mirror of
https://github.com/jketterl/openwebrx.git
synced 2026-01-07 09:10:16 +01:00
Merge branch 'develop' into active_arrays
This commit is contained in:
commit
dcc0f404b7
|
|
@ -1,9 +1,17 @@
|
|||
**unreleased**
|
||||
- SDR device log messages are now available in the web configuration to simplify troubleshooting
|
||||
- Added support for the MSK144 digimode
|
||||
- Added support for decoding ADS-B with dump1090
|
||||
- Added support for decoding HFDL and VDL2 aircraft communications
|
||||
- Added decoding of ISM band transmissions using rtl_433
|
||||
- Added IPv6 support
|
||||
- Added profile re-ordering using drag & drop
|
||||
- Added the ability to disable profiles
|
||||
- New devices supported:
|
||||
- Afedri SDR-Net
|
||||
|
||||
**1.2.2**
|
||||
- Fixed an over-the-air code injection vulnerability
|
||||
|
||||
**1.2.1**
|
||||
- FifiSDR support fixed (pipeline formats now line up correctly)
|
||||
|
|
|
|||
18
bands.json
18
bands.json
|
|
@ -367,5 +367,23 @@
|
|||
"lower_bound": 446000000,
|
||||
"upper_bound": 446200000,
|
||||
"tags": ["public"]
|
||||
},
|
||||
{
|
||||
"name": "Aeronautical Radionavigation",
|
||||
"lower_bound": 960000000,
|
||||
"upper_bound": 1215000000,
|
||||
"tags": [],
|
||||
"frequencies": {
|
||||
"adsb": 1090000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ISM-433",
|
||||
"lower_bound": 433050000,
|
||||
"upper_bound": 434790000,
|
||||
"tags": [],
|
||||
"frequencies": {
|
||||
"ism": 433920000
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -94,19 +94,24 @@ class Chain(Module):
|
|||
if self.writer is not None:
|
||||
newWorker.setWriter(self.writer)
|
||||
|
||||
def insert(self, newWorker):
|
||||
def insert(self, index, newWorker):
|
||||
nextWorker = None
|
||||
if self.workers:
|
||||
nextWorker = self.workers[0]
|
||||
previousWorker = None
|
||||
if index < len(self.workers):
|
||||
nextWorker = self.workers[index]
|
||||
if index > 0:
|
||||
previousWorker = self.workers[index - 1]
|
||||
|
||||
self.workers.insert(0, newWorker)
|
||||
self.workers.insert(index, newWorker)
|
||||
|
||||
if nextWorker:
|
||||
self._connect(newWorker, nextWorker)
|
||||
elif self.writer is not None:
|
||||
newWorker.setWriter(self.writer)
|
||||
|
||||
if self.reader is not None:
|
||||
if previousWorker:
|
||||
self._connect(previousWorker, newWorker)
|
||||
elif self.reader is not None:
|
||||
newWorker.setReader(self.reader)
|
||||
|
||||
def remove(self, index):
|
||||
|
|
|
|||
|
|
@ -74,3 +74,14 @@ class Ssb(BaseDemodulatorChain):
|
|||
Agc(Format.FLOAT),
|
||||
]
|
||||
super().__init__(workers)
|
||||
|
||||
|
||||
class Empty(BaseDemodulatorChain):
|
||||
def __init__(self):
|
||||
super().__init__([])
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.FLOAT
|
||||
|
||||
def setWriter(self, writer):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class ClientAudioChain(Chain):
|
|||
if index >= 0:
|
||||
self.replace(index, converter)
|
||||
else:
|
||||
self.insert(converter)
|
||||
self.insert(0, converter)
|
||||
|
||||
def setFormat(self, format: Format) -> None:
|
||||
if format == self.format:
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ class SecondaryDemodulator(Chain):
|
|||
def setSampleRate(self, sampleRate: int) -> None:
|
||||
pass
|
||||
|
||||
def isSecondaryFftShown(self):
|
||||
return True
|
||||
|
||||
|
||||
class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
from csdr.chain.demodulator import BaseDemodulatorChain, FixedAudioRateChain, FixedIfSampleRateChain, DialFrequencyReceiver, MetaProvider, SlotFilterChain, DemodulatorError, ServiceDemodulator
|
||||
from pycsdr.modules import FmDemod, Agc, Writer, Buffer
|
||||
from pycsdr.modules import FmDemod, Agc, Writer, Buffer, DcBlock, Lowpass
|
||||
from pycsdr.types import Format
|
||||
from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder, PocsagDecoder
|
||||
from digiham.modules import DstarDecoder, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder, PocsagDecoder
|
||||
from digiham.ambe import Modes, ServerError
|
||||
from owrx.meta import MetaParser
|
||||
from owrx.pocsag import PocsagParser
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, DialFrequencyReceiver, MetaProvider):
|
||||
def __init__(self, fskDemodulator, decoder, mbeMode, filter=None, codecserver: str = ""):
|
||||
|
|
@ -24,6 +28,9 @@ class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateC
|
|||
raise DemodulatorError("Connection to codecserver failed: {}".format(ce))
|
||||
except ServerError as se:
|
||||
raise DemodulatorError("Codecserver error: {}".format(se))
|
||||
except RuntimeError as re:
|
||||
logger.exception("Codecserver error while instantiating MbeSynthesizer:")
|
||||
raise DemodulatorError("Fatal codecserver error. Please check receiver logs.")
|
||||
workers += [
|
||||
fskDemodulator,
|
||||
decoder,
|
||||
|
|
@ -72,6 +79,7 @@ class Dstar(DigihamChain):
|
|||
fskDemodulator=FskDemodulator(samplesPerSymbol=10),
|
||||
decoder=DstarDecoder(),
|
||||
mbeMode=Modes.DStarMode,
|
||||
filter=WideRrcFilter(),
|
||||
codecserver=codecserver
|
||||
)
|
||||
|
||||
|
|
@ -117,6 +125,8 @@ class PocsagDemodulator(ServiceDemodulator, DialFrequencyReceiver):
|
|||
self.parser = PocsagParser()
|
||||
workers = [
|
||||
FmDemod(),
|
||||
DcBlock(),
|
||||
Lowpass(Format.FLOAT, 1200 / self.getFixedAudioRate()),
|
||||
FskDemodulator(samplesPerSymbol=40, invert=True),
|
||||
PocsagDecoder(),
|
||||
self.parser,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from csdr.module.msk144 import Msk144Module, ParserAdapter
|
|||
from owrx.audio.chopper import AudioChopper, AudioChopperParser
|
||||
from owrx.aprs.kiss import KissDeframer
|
||||
from owrx.aprs import Ax25Parser, AprsParser
|
||||
from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder
|
||||
from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder, RttyDecoder, BaudotDecoder, Lowpass
|
||||
from pycsdr.types import Format
|
||||
from owrx.aprs.module import DirewolfModule
|
||||
from owrx.aprs.direwolf import DirewolfModule
|
||||
|
||||
|
||||
class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
|
||||
|
|
@ -69,7 +69,7 @@ class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain):
|
|||
secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3
|
||||
workers = [
|
||||
Agc(Format.COMPLEX_FLOAT),
|
||||
TimingRecovery(secondary_samples_per_bits, 0.5, 2, useQ=True),
|
||||
TimingRecovery(Format.COMPLEX_FLOAT, secondary_samples_per_bits, 0.5, 2),
|
||||
DBPskDecoder(),
|
||||
VaricodeDecoder(),
|
||||
]
|
||||
|
|
@ -83,4 +83,38 @@ class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain):
|
|||
return
|
||||
self.sampleRate = sampleRate
|
||||
secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3
|
||||
self.replace(1, TimingRecovery(secondary_samples_per_bits, 0.5, 2, useQ=True))
|
||||
self.replace(1, TimingRecovery(Format.COMPLEX_FLOAT, secondary_samples_per_bits, 0.5, 2))
|
||||
|
||||
|
||||
class RttyDemodulator(SecondaryDemodulator, SecondarySelectorChain):
|
||||
def __init__(self, baudRate, bandWidth, invert=False):
|
||||
self.baudRate = baudRate
|
||||
self.bandWidth = bandWidth
|
||||
self.invert = invert
|
||||
# this is an assumption, we will adjust in setSampleRate
|
||||
self.sampleRate = 12000
|
||||
secondary_samples_per_bit = int(round(self.sampleRate / self.baudRate))
|
||||
cutoff = self.baudRate / self.sampleRate
|
||||
loop_gain = self.sampleRate / self.getBandwidth() / 5
|
||||
workers = [
|
||||
Agc(Format.COMPLEX_FLOAT),
|
||||
FmDemod(),
|
||||
Lowpass(Format.FLOAT, cutoff),
|
||||
TimingRecovery(Format.FLOAT, secondary_samples_per_bit, loop_gain, 10),
|
||||
RttyDecoder(invert),
|
||||
BaudotDecoder(),
|
||||
]
|
||||
super().__init__(workers)
|
||||
|
||||
def getBandwidth(self) -> float:
|
||||
return self.bandWidth
|
||||
|
||||
def setSampleRate(self, sampleRate: int) -> None:
|
||||
if sampleRate == self.sampleRate:
|
||||
return
|
||||
self.sampleRate = sampleRate
|
||||
secondary_samples_per_bit = int(round(self.sampleRate / self.baudRate))
|
||||
cutoff = self.baudRate / self.sampleRate
|
||||
loop_gain = self.sampleRate / self.getBandwidth() / 5
|
||||
self.replace(2, Lowpass(Format.FLOAT, cutoff))
|
||||
self.replace(3, TimingRecovery(Format.FLOAT, secondary_samples_per_bit, loop_gain, 10))
|
||||
|
|
|
|||
27
csdr/chain/dump1090.py
Normal file
27
csdr/chain/dump1090.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from pycsdr.modules import Convert
|
||||
from pycsdr.types import Format
|
||||
from csdr.chain.demodulator import ServiceDemodulator
|
||||
from owrx.adsb.dump1090 import Dump1090Module, RawDeframer
|
||||
from owrx.adsb.modes import ModeSParser
|
||||
|
||||
|
||||
class Dump1090(ServiceDemodulator):
|
||||
def __init__(self):
|
||||
workers = [
|
||||
Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
|
||||
Dump1090Module(),
|
||||
RawDeframer(),
|
||||
ModeSParser(),
|
||||
]
|
||||
|
||||
super().__init__(workers)
|
||||
pass
|
||||
|
||||
def getFixedAudioRate(self) -> int:
|
||||
return 2400000
|
||||
|
||||
def isSecondaryFftShown(self):
|
||||
return False
|
||||
|
||||
def supportsSquelch(self) -> bool:
|
||||
return False
|
||||
16
csdr/chain/dumphfdl.py
Normal file
16
csdr/chain/dumphfdl.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from csdr.chain.demodulator import ServiceDemodulator
|
||||
from owrx.hfdl.dumphfdl import DumpHFDLModule, HFDLMessageParser
|
||||
|
||||
|
||||
class DumpHFDL(ServiceDemodulator):
|
||||
def __init__(self):
|
||||
super().__init__([
|
||||
DumpHFDLModule(),
|
||||
HFDLMessageParser(),
|
||||
])
|
||||
|
||||
def getFixedAudioRate(self) -> int:
|
||||
return 12000
|
||||
|
||||
def supportsSquelch(self) -> bool:
|
||||
return False
|
||||
19
csdr/chain/dumpvdl2.py
Normal file
19
csdr/chain/dumpvdl2.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from csdr.chain.demodulator import ServiceDemodulator
|
||||
from owrx.vdl2.dumpvdl2 import DumpVDL2Module, VDL2MessageParser
|
||||
from pycsdr.modules import Convert
|
||||
from pycsdr.types import Format
|
||||
|
||||
|
||||
class DumpVDL2(ServiceDemodulator):
|
||||
def __init__(self):
|
||||
super().__init__([
|
||||
Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
|
||||
DumpVDL2Module(),
|
||||
VDL2MessageParser(),
|
||||
])
|
||||
|
||||
def getFixedAudioRate(self) -> int:
|
||||
return 105000
|
||||
|
||||
def supportsSquelch(self) -> bool:
|
||||
return False
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider
|
||||
from csdr.module.m17 import M17Module
|
||||
from pycsdr.modules import FmDemod, Limit, Convert, Writer
|
||||
from pycsdr.modules import FmDemod, Limit, Convert, Writer, DcBlock
|
||||
from pycsdr.types import Format
|
||||
from digiham.modules import DcBlock
|
||||
|
||||
|
||||
class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider):
|
||||
|
|
|
|||
19
csdr/chain/rtl433.py
Normal file
19
csdr/chain/rtl433.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from csdr.module import JsonParser
|
||||
from owrx.ism.rtl433 import Rtl433Module
|
||||
from csdr.chain.demodulator import ServiceDemodulator
|
||||
|
||||
|
||||
class Rtl433(ServiceDemodulator):
|
||||
def getFixedAudioRate(self) -> int:
|
||||
return 250000
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
[
|
||||
Rtl433Module(),
|
||||
JsonParser("ISM"),
|
||||
]
|
||||
)
|
||||
|
||||
def supportsSquelch(self) -> bool:
|
||||
return False
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from csdr.chain import Chain
|
||||
from pycsdr.modules import Shift, FirDecimate, Bandpass, Squelch, FractionalDecimator, Writer
|
||||
from pycsdr.types import Format
|
||||
from typing import Union
|
||||
import math
|
||||
|
||||
|
||||
|
|
@ -28,6 +29,13 @@ class Decimator(Chain):
|
|||
super().__init__(workers)
|
||||
|
||||
def _getDecimation(self, outputRate: int) -> (int, float):
|
||||
if outputRate > self.inputRate:
|
||||
raise SelectorError(
|
||||
"cannot provide selected output rate {} since it is bigger than input rate {}".format(
|
||||
outputRate,
|
||||
self.inputRate
|
||||
)
|
||||
)
|
||||
d = self.inputRate / outputRate
|
||||
dInt = int(d)
|
||||
dFloat = float(self.inputRate / dInt) / outputRate
|
||||
|
|
@ -72,10 +80,9 @@ class Selector(Chain):
|
|||
self.decimation = Decimator(inputRate, outputRate)
|
||||
|
||||
self.bandpass = self._buildBandpass()
|
||||
self.bandpassCutoffs = None
|
||||
self.setBandpass(-4000, 4000)
|
||||
self.bandpassCutoffs = [None, None]
|
||||
|
||||
workers = [self.shift, self.decimation, self.bandpass]
|
||||
workers = [self.shift, self.decimation]
|
||||
|
||||
if withSquelch:
|
||||
self.readings_per_second = 4
|
||||
|
|
@ -106,16 +113,30 @@ class Selector(Chain):
|
|||
def setSquelchLevel(self, level: float) -> None:
|
||||
self.squelch.setSquelchLevel(self._convertToLinear(level))
|
||||
|
||||
def _enableBandpass(self):
|
||||
index = self.indexOf(lambda x: isinstance(x, Bandpass))
|
||||
if index < 0:
|
||||
self.insert(2, self.bandpass)
|
||||
|
||||
def _disableBandpass(self):
|
||||
index = self.indexOf(lambda x: isinstance(x, Bandpass))
|
||||
if index >= 0:
|
||||
self.remove(index)
|
||||
|
||||
def setBandpass(self, lowCut: float, highCut: float) -> None:
|
||||
self.bandpassCutoffs = [lowCut, highCut]
|
||||
scaled = [x / self.outputRate for x in self.bandpassCutoffs]
|
||||
self.bandpass.setBandpass(*scaled)
|
||||
if None in self.bandpassCutoffs:
|
||||
self._disableBandpass()
|
||||
else:
|
||||
self._enableBandpass()
|
||||
scaled = [x / self.outputRate for x in self.bandpassCutoffs]
|
||||
self.bandpass.setBandpass(*scaled)
|
||||
|
||||
def setLowCut(self, lowCut: float) -> None:
|
||||
def setLowCut(self, lowCut: Union[float, None]) -> None:
|
||||
self.bandpassCutoffs[0] = lowCut
|
||||
self.setBandpass(*self.bandpassCutoffs)
|
||||
|
||||
def setHighCut(self, highCut: float) -> None:
|
||||
def setHighCut(self, highCut: Union[float, None]) -> None:
|
||||
self.bandpassCutoffs[1] = highCut
|
||||
self.setBandpass(*self.bandpassCutoffs)
|
||||
|
||||
|
|
@ -129,9 +150,11 @@ class Selector(Chain):
|
|||
|
||||
self.decimation.setOutputRate(outputRate)
|
||||
self.squelch.setReportInterval(int(outputRate / (self.readings_per_second * 1024)))
|
||||
index = self.indexOf(lambda x: isinstance(x, Bandpass))
|
||||
self.bandpass = self._buildBandpass()
|
||||
self.setBandpass(*self.bandpassCutoffs)
|
||||
self.replace(2, self.bandpass)
|
||||
if index >= 0:
|
||||
self.replace(index, self.bandpass)
|
||||
|
||||
def setInputRate(self, inputRate: int) -> None:
|
||||
if inputRate == self.inputRate:
|
||||
|
|
@ -158,3 +181,7 @@ class SecondarySelector(Chain):
|
|||
if self.frequencyOffset is None:
|
||||
return
|
||||
self.shift.setRate(-offset / self.sampleRate)
|
||||
|
||||
|
||||
class SelectorError(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
from pycsdr.modules import Module as BaseModule
|
||||
from pycsdr.modules import Reader, Writer
|
||||
from pycsdr.modules import Reader, Writer, Buffer
|
||||
from pycsdr.types import Format
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from threading import Thread
|
||||
from io import BytesIO
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import Popen, PIPE, TimeoutExpired
|
||||
from functools import partial
|
||||
import pickle
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Module(BaseModule, metaclass=ABCMeta):
|
||||
|
|
@ -41,7 +45,10 @@ class Module(BaseModule, metaclass=ABCMeta):
|
|||
break
|
||||
if data is None or isinstance(data, bytes) and len(data) == 0:
|
||||
break
|
||||
write(data)
|
||||
try:
|
||||
write(data)
|
||||
except BrokenPipeError:
|
||||
break
|
||||
|
||||
return copy
|
||||
|
||||
|
|
@ -109,6 +116,57 @@ class PickleModule(ThreadModule):
|
|||
pass
|
||||
|
||||
|
||||
class LineBasedModule(ThreadModule, metaclass=ABCMeta):
|
||||
def __init__(self):
|
||||
self.retained = bytes()
|
||||
super().__init__()
|
||||
|
||||
def getInputFormat(self) -> Format:
|
||||
return Format.CHAR
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.CHAR
|
||||
|
||||
def run(self):
|
||||
while self.doRun:
|
||||
data = self.reader.read()
|
||||
if data is None:
|
||||
self.doRun = False
|
||||
else:
|
||||
self.retained += data
|
||||
lines = self.retained.split(b"\n")
|
||||
|
||||
# keep the last line
|
||||
# this should either be empty if the last char was \n
|
||||
# or an incomplete line if the read returned early
|
||||
self.retained = lines[-1]
|
||||
|
||||
# log all completed lines
|
||||
for line in lines[0:-1]:
|
||||
parsed = self.process(line)
|
||||
if parsed is not None:
|
||||
self.writer.write(pickle.dumps(parsed))
|
||||
|
||||
@abstractmethod
|
||||
def process(self, line: bytes) -> any:
|
||||
pass
|
||||
|
||||
|
||||
class JsonParser(LineBasedModule):
|
||||
def __init__(self, mode: str):
|
||||
self.mode = mode
|
||||
super().__init__()
|
||||
|
||||
def process(self, line):
|
||||
try:
|
||||
msg = json.loads(line)
|
||||
msg["mode"] = self.mode
|
||||
logger.debug(msg)
|
||||
return msg
|
||||
except json.JSONDecodeError:
|
||||
logger.exception("error parsing rtl433 json")
|
||||
|
||||
|
||||
class PopenModule(AutoStartModule, metaclass=ABCMeta):
|
||||
def __init__(self):
|
||||
self.process = None
|
||||
|
|
@ -130,7 +188,41 @@ class PopenModule(AutoStartModule, metaclass=ABCMeta):
|
|||
|
||||
def stop(self):
|
||||
if self.process is not None:
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
# Try terminating normally, kill if failed to terminate
|
||||
try:
|
||||
self.process.terminate()
|
||||
self.process.wait(3)
|
||||
except TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process = None
|
||||
self.reader.stop()
|
||||
|
||||
|
||||
class LogReader(Thread):
|
||||
def __init__(self, prefix: str, buffer: Buffer):
|
||||
self.reader = buffer.getReader()
|
||||
self.logger = logging.getLogger(prefix)
|
||||
self.retained = bytes()
|
||||
super().__init__()
|
||||
self.start()
|
||||
|
||||
def run(self) -> None:
|
||||
while True:
|
||||
data = self.reader.read()
|
||||
if data is None:
|
||||
return
|
||||
|
||||
self.retained += data
|
||||
lines = self.retained.split(b"\n")
|
||||
|
||||
# keep the last line
|
||||
# this should either be empty if the last char was \n
|
||||
# or an incomplete line if the read returned early
|
||||
self.retained = lines[-1]
|
||||
|
||||
# log all completed lines
|
||||
for line in lines[0:-1]:
|
||||
self.logger.info("{}: {}".format("STDOUT", line.decode(errors="replace")))
|
||||
|
||||
def stop(self):
|
||||
self.reader.stop()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
from csdr.module import PopenModule
|
||||
from pycsdr.modules import ExecModule
|
||||
from pycsdr.types import Format
|
||||
|
||||
|
||||
class DrmModule(PopenModule):
|
||||
def getInputFormat(self) -> Format:
|
||||
return Format.COMPLEX_FLOAT
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.SHORT
|
||||
|
||||
def getCommand(self):
|
||||
# dream -c 6 --sigsrate 48000 --audsrate 48000 -I - -O -
|
||||
return ["dream", "-c", "6", "--sigsrate", "48000", "--audsrate", "48000", "-I", "-", "-O", "-"]
|
||||
class DrmModule(ExecModule):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
Format.COMPLEX_SHORT,
|
||||
Format.SHORT,
|
||||
["dream", "-c", "6", "--sigsrate", "48000", "--audsrate", "48000", "-I", "-", "-O", "-"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
from pycsdr.types import Format
|
||||
from csdr.module import PopenModule
|
||||
from pycsdr.modules import ExecModule
|
||||
|
||||
|
||||
class FreeDVModule(PopenModule):
|
||||
def getInputFormat(self) -> Format:
|
||||
return Format.SHORT
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.SHORT
|
||||
|
||||
def getCommand(self):
|
||||
return ["freedv_rx", "1600", "-", "-"]
|
||||
class FreeDVModule(ExecModule):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
Format.SHORT,
|
||||
Format.SHORT,
|
||||
["freedv_rx", "1600", "-", "-"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from pycsdr.types import Format
|
||||
from csdr.module import PopenModule, ThreadModule
|
||||
from pycsdr.modules import ExecModule
|
||||
from csdr.module import LineBasedModule
|
||||
from owrx.wsjt import WsjtParser, Msk144Profile
|
||||
import pickle
|
||||
|
||||
|
|
@ -7,51 +8,26 @@ import logging
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Msk144Module(PopenModule):
|
||||
def getCommand(self):
|
||||
return ["msk144decoder"]
|
||||
|
||||
def getInputFormat(self) -> Format:
|
||||
return Format.SHORT
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.CHAR
|
||||
|
||||
|
||||
class ParserAdapter(ThreadModule):
|
||||
class Msk144Module(ExecModule):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
Format.SHORT,
|
||||
Format.CHAR,
|
||||
["msk144decoder"]
|
||||
)
|
||||
|
||||
|
||||
class ParserAdapter(LineBasedModule):
|
||||
def __init__(self):
|
||||
self.retained = bytes()
|
||||
self.parser = WsjtParser()
|
||||
self.dialFrequency = 0
|
||||
self.profile = Msk144Profile()
|
||||
super().__init__()
|
||||
|
||||
def run(self):
|
||||
profile = Msk144Profile()
|
||||
|
||||
while self.doRun:
|
||||
data = self.reader.read()
|
||||
if data is None:
|
||||
self.doRun = False
|
||||
else:
|
||||
self.retained += data
|
||||
lines = self.retained.split(b"\n")
|
||||
|
||||
# keep the last line
|
||||
# this should either be empty if the last char was \n
|
||||
# or an incomplete line if the read returned early
|
||||
self.retained = lines[-1]
|
||||
|
||||
# parse all completed lines
|
||||
for line in lines[0:-1]:
|
||||
# actual messages from msk144decoder should start with "*** "
|
||||
if line[0:4] == b"*** ":
|
||||
self.writer.write(pickle.dumps(self.parser.parse(profile, self.dialFrequency, line[4:])))
|
||||
|
||||
def getInputFormat(self) -> Format:
|
||||
return Format.CHAR
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.CHAR
|
||||
def process(self, line: bytes):
|
||||
# actual messages from msk144decoder should start with "*** "
|
||||
if line[0:4] == b"*** ":
|
||||
return self.parser.parse(self.profile, self.dialFrequency, line[4:])
|
||||
|
||||
def setDialFrequency(self, frequency: int) -> None:
|
||||
self.dialFrequency = frequency
|
||||
|
|
|
|||
11
debian/changelog
vendored
11
debian/changelog
vendored
|
|
@ -2,12 +2,23 @@ openwebrx (1.3.0) UNRELEASED; urgency=low
|
|||
* SDR device log messages are now available in the web configuration to
|
||||
simplify troubleshooting
|
||||
* Added support for the MSK144 digimode
|
||||
* Added support for decoding ADS-B with dump1090
|
||||
* Added support for decoding HFDL and VDL2 aircraft communications
|
||||
* Added decoding of ISM band transmissions using rtl_433
|
||||
* Added IPv6 support
|
||||
* Added profile re-ordering using drag & drop
|
||||
* Added the ability to disable profiles
|
||||
* New devices supported:
|
||||
- Afedri SDR-Net
|
||||
|
||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Fri, 30 Sep 2022 16:47:00 +0000
|
||||
|
||||
openwebrx (1.2.2) bullseye jammy; urgency=high
|
||||
|
||||
* - Fixed an over-the-air code injection vulnerability
|
||||
|
||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 08 Oct 2023 21:29:00 +0000
|
||||
|
||||
openwebrx (1.2.1) bullseye jammy; urgency=low
|
||||
|
||||
* FifiSDR support fixed (pipeline formats now line up correctly)
|
||||
|
|
|
|||
36
debian/control
vendored
36
debian/control
vendored
|
|
@ -2,15 +2,45 @@ Source: openwebrx
|
|||
Maintainer: Jakob Ketterl <jakob.ketterl@gmx.de>
|
||||
Section: hamradio
|
||||
Priority: optional
|
||||
Rules-Requires-Root: no
|
||||
Standards-Version: 4.2.0
|
||||
Build-Depends: debhelper (>= 11), dh-python, python3-all (>= 3.5), python3-setuptools
|
||||
Build-Depends: debhelper (>= 11),
|
||||
dh-python,
|
||||
python3-all (>= 3.5),
|
||||
python3-setuptools
|
||||
Homepage: https://www.openwebrx.de/
|
||||
Vcs-Browser: https://github.com/jketterl/openwebrx
|
||||
Vcs-Git: https://github.com/jketterl/openwebrx.git
|
||||
|
||||
Package: openwebrx
|
||||
Architecture: all
|
||||
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, owrx-connector (>= 0.7), python3-csdr (>= 0.18), ${python3:Depends}, ${misc:Depends}
|
||||
Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, js8call, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.2), nmux (>= 0.18), codecserver (>= 0.1), msk144decoder
|
||||
Depends: adduser,
|
||||
python3 (>= 3.5),
|
||||
python3-pkg-resources,
|
||||
owrx-connector (>= 0.7),
|
||||
python3-csdr (>= 0.18),
|
||||
${python3:Depends},
|
||||
${misc:Depends}
|
||||
Recommends: python3-digiham (>= 0.6),
|
||||
direwolf (>= 1.4),
|
||||
wsjtx,
|
||||
js8call,
|
||||
runds-connector (>= 0.2),
|
||||
hpsdrconnector,
|
||||
aprs-symbols,
|
||||
m17-demod,
|
||||
js8call,
|
||||
python3-js8py (>= 0.2),
|
||||
nmux (>= 0.18),
|
||||
codecserver (>= 0.1),
|
||||
msk144decoder,
|
||||
dump1090-fa-minimal,
|
||||
dumphfdl,
|
||||
dumpvdl2,
|
||||
rtl-433,
|
||||
extra-sdr-drivers,
|
||||
perseus-tools,
|
||||
dream-headless,
|
||||
codec2
|
||||
Description: multi-user web sdr
|
||||
Open source, multi-user SDR receiver with a web interface
|
||||
|
|
|
|||
1
debian/openwebrx.config
vendored
1
debian/openwebrx.config
vendored
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
db_get openwebrx/admin_user_configured
|
||||
if [ "${1:-}" = "reconfigure" ] || [ "${RET}" != true ]; then
|
||||
db_settitle openwebrx/title
|
||||
db_input high openwebrx/admin_user_password || true
|
||||
db_go
|
||||
fi
|
||||
|
|
|
|||
12
debian/openwebrx.postinst
vendored
12
debian/openwebrx.postinst
vendored
|
|
@ -14,25 +14,29 @@ case "$1" in
|
|||
adduser --system --group --no-create-home --home /nonexistent --quiet "${OWRX_USER}"
|
||||
usermod -aG plugdev "${OWRX_USER}"
|
||||
|
||||
# ensure group exists first (dependency is optional)
|
||||
addgroup --system --quiet perseususb
|
||||
usermod -aG perseususb "${OWRX_USER}"
|
||||
|
||||
# create OpenWebRX data directory and set the correct permissions
|
||||
if [ ! -d "${OWRX_DATADIR}" ] && [ ! -L "${OWRX_DATADIR}" ]; then mkdir "${OWRX_DATADIR}"; fi
|
||||
chown "${OWRX_USER}". ${OWRX_DATADIR}
|
||||
chown "${OWRX_USER}": ${OWRX_DATADIR}
|
||||
|
||||
# create empty config files now to avoid permission problems later
|
||||
if [ ! -e "${OWRX_USERS_FILE}" ]; then
|
||||
echo "[]" > "${OWRX_USERS_FILE}"
|
||||
chown "${OWRX_USER}". "${OWRX_USERS_FILE}"
|
||||
chown "${OWRX_USER}": "${OWRX_USERS_FILE}"
|
||||
chmod 0600 "${OWRX_USERS_FILE}"
|
||||
fi
|
||||
|
||||
if [ ! -e "${OWRX_SETTINGS_FILE}" ]; then
|
||||
echo "{}" > "${OWRX_SETTINGS_FILE}"
|
||||
chown "${OWRX_USER}". "${OWRX_SETTINGS_FILE}"
|
||||
chown "${OWRX_USER}": "${OWRX_SETTINGS_FILE}"
|
||||
fi
|
||||
|
||||
if [ ! -e "${OWRX_BOOKMARKS_FILE}" ]; then
|
||||
touch "${OWRX_BOOKMARKS_FILE}"
|
||||
chown "${OWRX_USER}". "${OWRX_BOOKMARKS_FILE}"
|
||||
chown "${OWRX_USER}": "${OWRX_BOOKMARKS_FILE}"
|
||||
fi
|
||||
|
||||
db_get openwebrx/admin_user_password
|
||||
|
|
|
|||
6
debian/openwebrx.templates
vendored
6
debian/openwebrx.templates
vendored
|
|
@ -20,4 +20,8 @@ Type: boolean
|
|||
Default: false
|
||||
Description: OpenWebRX "admin" user previously configured?
|
||||
Marker used internally by the config scripts to remember if an admin user has
|
||||
been created.
|
||||
been created.
|
||||
|
||||
Template: openwebrx/title
|
||||
Type: title
|
||||
Description: Configuring OpenWebRX
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
set -euo pipefail
|
||||
|
||||
ARCH=$(uname -m)
|
||||
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-bladerf openwebrx-full openwebrx"
|
||||
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-afedri openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-bladerf openwebrx-full openwebrx"
|
||||
ALL_ARCHS="x86_64 armv7l aarch64"
|
||||
TAG=${TAG:-"latest"}
|
||||
ARCHTAG="${TAG}-${ARCH}"
|
||||
|
|
|
|||
8
docker/Dockerfiles/Dockerfile-afedri
Normal file
8
docker/Dockerfiles/Dockerfile-afedri
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
ARG ARCHTAG
|
||||
FROM openwebrx-soapysdr-base:$ARCHTAG
|
||||
|
||||
COPY docker/scripts/install-dependencies-afedri.sh /
|
||||
RUN /install-dependencies-afedri.sh &&\
|
||||
rm /install-dependencies-afedri.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM debian:bullseye-slim
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
COPY docker/files/js8call/js8call-hamlib.patch \
|
||||
docker/files/wsjtx/wsjtx.patch \
|
||||
|
|
@ -23,6 +23,6 @@ VOLUME /etc/openwebrx
|
|||
VOLUME /var/lib/openwebrx
|
||||
|
||||
ENV S6_CMD_ARG0="/opt/openwebrx/docker/scripts/run.sh"
|
||||
CMD []
|
||||
CMD [""]
|
||||
|
||||
EXPOSE 8073
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ RUN /install-dependencies-rtlsdr.sh &&\
|
|||
/install-dependencies-hackrf.sh &&\
|
||||
/install-dependencies-sdrplay.sh &&\
|
||||
/install-dependencies-airspy.sh &&\
|
||||
/install-dependencies-afedri.sh &&\
|
||||
/install-dependencies-rtlsdr-soapy.sh &&\
|
||||
/install-dependencies-plutosdr.sh &&\
|
||||
/install-dependencies-limesdr.sh &&\
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
|
||||
--- sdrplay-orig/install_lib.sh 2020-05-24 14:30:06.022483867 +0000
|
||||
+++ sdrplay/install_lib.sh 2020-05-24 14:30:49.093435726 +0000
|
||||
@@ -4,19 +4,6 @@
|
||||
export MAJVERS="3"
|
||||
|
||||
echo "Installing SDRplay RSP API library ${VERS}..."
|
||||
-read -p "Press RETURN to view the license agreement" ret
|
||||
-
|
||||
-more sdrplay_license.txt
|
||||
-
|
||||
-while true; do
|
||||
- echo "Press y and RETURN to accept the license agreement and continue with"
|
||||
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
|
||||
- case $yn in
|
||||
- [Yy]* ) break;;
|
||||
- [Nn]* ) exit;;
|
||||
- * ) echo "Please answer y or n";;
|
||||
- esac
|
||||
-done
|
||||
|
||||
export ARCH=`uname -m`
|
||||
|
||||
1
docker/files/sdrplay/install-lib.aarch64.patch
Symbolic link
1
docker/files/sdrplay/install-lib.aarch64.patch
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
install-lib.x86_64.patch
|
||||
|
|
@ -1,14 +1,11 @@
|
|||
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
|
||||
--- sdrplay-orig/install_lib.sh 2020-05-24 13:56:56.622000041 +0000
|
||||
+++ sdrplay/install_lib.sh 2020-05-24 13:58:51.837801559 +0000
|
||||
@@ -4,19 +4,6 @@
|
||||
MAJVERS="3"
|
||||
--- sdrplay-orig/install_lib.sh 2024-01-01 15:03:53.377291864 +0100
|
||||
+++ sdrplay/install_lib.sh 2024-01-01 16:09:25.948363042 +0100
|
||||
@@ -17,26 +17,7 @@
|
||||
echo "the system files."
|
||||
echo " "
|
||||
|
||||
echo "Installing SDRplay RSP API library ${VERS}..."
|
||||
-read -p "Press RETURN to view the license agreement" ret
|
||||
-
|
||||
-more sdrplay_license.txt
|
||||
-
|
||||
-more -d sdrplay_license.txt
|
||||
-while true; do
|
||||
- echo "Press y and RETURN to accept the license agreement and continue with"
|
||||
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
|
||||
|
|
@ -18,22 +15,133 @@ diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
|
|||
- * ) echo "Please answer y or n";;
|
||||
- esac
|
||||
-done
|
||||
-
|
||||
-echo " "
|
||||
-echo "A copy of the license agreement can be found here: ${HOME}/sdrplay_license.txt"
|
||||
-cp sdrplay_license.txt ${HOME}/.
|
||||
-chmod 644 ${HOME}/sdrplay_license.txt
|
||||
-echo " "
|
||||
-
|
||||
ARCH=$(uname -m|sed -e 's/x86_64/64/' -e 's/aarch64/64/' -e 's/arm64/64/' -e 's/i.86/32/')
|
||||
-INIT=$(file -L /sbin/init|sed -e 's/^.* \(32\|64\)-bit.*$/\1/')
|
||||
COMPILER=$(getconf LONG_BIT)
|
||||
ARCHM=$(uname -m)
|
||||
INSTALLARCH=$(uname -m)
|
||||
@@ -47,12 +28,11 @@
|
||||
|
||||
ARCH=`uname -m`
|
||||
OSDIST="Unknown"
|
||||
@@ -157,15 +144,6 @@
|
||||
echo " "
|
||||
echo "SDRplay API ${VERS} Installation Finished"
|
||||
echo "Architecture reported as being $ARCH bit"
|
||||
-echo "System reports $INIT bit files found"
|
||||
echo "System is also setup to produce $COMPILER bit files"
|
||||
echo "Architecture reports machine as being $ARCHM compliant"
|
||||
echo " "
|
||||
|
||||
-if [ "${ARCH}" != "64" ] || [ "${INIT}" != "64" ] || [ "${COMPILER}" != "64" ]; then
|
||||
+if [ "${ARCH}" != "64" ] || [ "${COMPILER}" != "64" ]; then
|
||||
echo "This installer only supports 64 bit architectures."
|
||||
echo "One of the above indicates that something is not set for"
|
||||
echo "64 bit operation. Please either fix the relevant OS issue or"
|
||||
@@ -193,11 +173,6 @@
|
||||
sudo chmod 644 /etc/udev/hwdb.d/20-sdrplay.hwdb
|
||||
sudo systemd-hwdb update
|
||||
sudo udevadm trigger
|
||||
- if [ "${SRVTYPE}" != "initd" ]; then
|
||||
- sudo systemctl restart udev
|
||||
- else
|
||||
- sudo service udev restart
|
||||
- fi
|
||||
echo "Done"
|
||||
fi
|
||||
fi
|
||||
@@ -227,7 +202,7 @@
|
||||
fi
|
||||
|
||||
echo " "
|
||||
-locservice="/opt/sdrplay_api"
|
||||
+locservice="/usr/local/bin"
|
||||
locheader="/usr/local/include"
|
||||
loclib="/usr/local/lib"
|
||||
locscripts="/etc/systemd/system"
|
||||
@@ -247,45 +222,6 @@
|
||||
echo "Daemon start system : ${DAEMON_SYS}"
|
||||
echo " "
|
||||
|
||||
-# 0--------1---------2---------3---------4---------5---------6---------7---------8
|
||||
-while true; do
|
||||
- echo "Would you like to add SDRplay USB IDs to the local database for easier"
|
||||
- read -p "identification in applications such as lsusb? [y/n] " yn
|
||||
- echo "To continue the installation with these defaults press y and RETURN"
|
||||
- read -p "or press n and RETURN to change them [y/n] " yn
|
||||
- case $yn in
|
||||
- [Yy]* ) break;;
|
||||
- [Nn]* ) exit;;
|
||||
- [Yy]* ) change="n";break;;
|
||||
- [Nn]* ) change="y";break;;
|
||||
- * ) echo "Please answer y or n";;
|
||||
- esac
|
||||
-done
|
||||
sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/.
|
||||
sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh
|
||||
sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/.
|
||||
-
|
||||
-if [ "${change}" == "y" ]; then
|
||||
- echo "Changing default locations..."
|
||||
- read -p "API service location [${locservice}]: " newloc
|
||||
- if [ "${newloc}" != "" ]; then
|
||||
- locservice=${newloc}
|
||||
- fi
|
||||
- read -p "API header files location [${locheader}]: " newloc
|
||||
- if [ "${newloc}" != "" ]; then
|
||||
- locheader=${newloc}
|
||||
- fi
|
||||
- read -p "API shared library location [${loclib}]: " newloc
|
||||
- if [ "${newloc}" != "" ]; then
|
||||
- loclib=${newloc}
|
||||
- fi
|
||||
-
|
||||
- echo "API service : ${locservice}"
|
||||
- echo "API header files : ${locheader}"
|
||||
- echo "API shared library : ${loclib}"
|
||||
- while true; do
|
||||
- read -p "Please confirm these are correct [y/n] " yn
|
||||
- case $yn in
|
||||
- [Yy]* ) break;;
|
||||
- [Nn]* ) echo "paths not confirmed. Exiting...";exit 1;;
|
||||
- * ) echo "Please answer y or n";;
|
||||
- esac
|
||||
- done
|
||||
-fi
|
||||
-
|
||||
sudo mkdir -p -m 755 ${locservice} >> /dev/null 2>&1
|
||||
sudo mkdir -p -m 755 ${locheader} >> /dev/null 2>&1
|
||||
sudo mkdir -p -m 755 ${loclib} >> /dev/null 2>&1
|
||||
@@ -317,10 +253,6 @@
|
||||
echo -n "Installing Service scripts and starting daemon..."
|
||||
if [ -d "/etc/systemd/system" ]; then
|
||||
SRVTYPE="systemd"
|
||||
- if [ -f "/etc/systemd/system/sdrplay.service" ]; then
|
||||
- sudo systemctl stop sdrplay
|
||||
- sudo systemctl disable sdrplay
|
||||
- fi
|
||||
sudo bash -c 'cat > /etc/systemd/system/sdrplay.service' << EOF
|
||||
[Unit]
|
||||
Description=SDRplay API Service
|
||||
@@ -339,8 +271,6 @@
|
||||
EOF
|
||||
|
||||
sudo chmod 644 /etc/systemd/system/sdrplay.service
|
||||
- sudo systemctl enable sdrplay
|
||||
- sudo systemctl start sdrplay
|
||||
else
|
||||
SRVTYPE="initd"
|
||||
if [ -f "/etc/init.d/sdrplayService" ]; then
|
||||
@@ -443,16 +373,6 @@
|
||||
echo "finished, please reboot this device."
|
||||
|
||||
echo " "
|
||||
-echo "To start and stop the API service, use the following commands..."
|
||||
-echo " "
|
||||
-if [ "${SRVTYPE}" != "systemd" ]; then
|
||||
- echo "sudo service sdrplayService start"
|
||||
- echo "sudo service sdrplayService stop"
|
||||
-else
|
||||
- echo "sudo systemctl start sdrplay"
|
||||
- echo "sudo systemctl stop sdrplay"
|
||||
-fi
|
||||
-echo " "
|
||||
echo "If supported on your system, lsusb will now show the RSP name"
|
||||
echo " "
|
||||
echo "SDRplay API ${VERS} Installation Finished"
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
#!/usr/bin/execlineb -P
|
||||
#!/command/execlineb -P
|
||||
/usr/local/bin/codecserver
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
#!/usr/bin/execlineb -P
|
||||
#!/command/execlineb -P
|
||||
/usr/local/bin/sdrplay_apiService
|
||||
|
|
@ -18,14 +18,15 @@ function cmakebuild() {
|
|||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libfftw3-single3"
|
||||
BUILD_PACKAGES="git cmake make gcc g++ libsamplerate-dev libfftw3-dev"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $BUILD_PACKAGES
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/jketterl/owrx_connector.git
|
||||
# latest develop as of 2023-03-18 (added --listdriver option)
|
||||
cmakebuild owrx_connector beda260363c0e77617d4e17ef864e1f5c3fd86b2
|
||||
# latest develop as of 2024-01-01 (fixed startup race condition)
|
||||
cmakebuild owrx_connector 62219d40e180abb539ad61fcd9625b90c34f0e26
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
|
|
|||
33
docker/scripts/install-dependencies-afedri.sh
Executable file
33
docker/scripts/install-dependencies-afedri.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
export MAKEFLAGS="-j4"
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
if [[ ! -z "${2:-}" ]]; then
|
||||
git checkout $2
|
||||
fi
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES=""
|
||||
BUILD_PACKAGES="git cmake make gcc g++"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/alexander-sholohov/SoapyAfedri.git
|
||||
# latest from master as of 2023-11-16
|
||||
cmakebuild SoapyAfedri a7d0d942fe966c2b69c8817dd6f097fc94122660
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
|
@ -25,11 +25,11 @@ apt-get update
|
|||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/Nuand/bladeRF.git
|
||||
cmakebuild bladeRF 2021.10
|
||||
cmakebuild bladeRF 2023.02
|
||||
|
||||
git clone https://github.com/pothosware/SoapyBladeRF.git
|
||||
# latest from master as of 2022-01-12
|
||||
cmakebuild SoapyBladeRF 70505a5cdf8c9deabc4af3eb3384aa82a7b6f021
|
||||
# latest from master as of 2023-08-30
|
||||
cmakebuild SoapyBladeRF 85f6dc554ed4c618304d99395b19c4e1523675b0
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function cmakebuild() {
|
|||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev"
|
||||
STATIC_PACKAGES="libusb-1.0-0 libfftw3-single3 udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
|
||||
|
||||
apt-get update
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ apt-get -y install --no-install-recommends $BUILD_PACKAGES
|
|||
pushd /tmp
|
||||
|
||||
ARCH=$(uname -m)
|
||||
GOVERSION=1.15.5
|
||||
GOVERSION=1.20.10
|
||||
|
||||
case ${ARCH} in
|
||||
x86_64)
|
||||
|
|
@ -29,7 +29,7 @@ tar xfz $PACKAGE
|
|||
|
||||
git clone https://github.com/jancona/hpsdrconnector.git
|
||||
pushd hpsdrconnector
|
||||
git checkout v0.6.1
|
||||
git checkout v0.6.4
|
||||
/tmp/go/bin/go build
|
||||
install -m 0755 hpsdrconnector /usr/local/bin
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ function cmakebuild() {
|
|||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
|
||||
STATIC_PACKAGES=""
|
||||
BUILD_PACKAGES="git cmake make gcc g++"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@ apt-get update
|
|||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/osmocom/rtl-sdr.git
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild rtl-sdr ed0317e6a58c098874ac58b769cf2e609c18d9a5
|
||||
# latest from master as of 2023-09-13 (integration of rtlsdr blog v4 dongle)
|
||||
cmakebuild rtl-sdr 1261fbb285297da08f4620b18871b6d6d9ec2a7b
|
||||
|
||||
git clone https://github.com/pothosware/SoapyRTLSDR.git
|
||||
cmakebuild SoapyRTLSDR soapy-rtl-sdr-0.3.1
|
||||
# latest from master as of 2023-09-13
|
||||
cmakebuild SoapyRTLSDR 068aa77a4c938b239c9d80cd42c4ee7986458e8f
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ apt-get update
|
|||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/osmocom/rtl-sdr.git
|
||||
# latest from master as of 2020-09-04
|
||||
cmakebuild rtl-sdr ed0317e6a58c098874ac58b769cf2e609c18d9a5
|
||||
# latest from master as of 2023-09-13 (integration of rtlsdr blog v4 dongle)
|
||||
cmakebuild rtl-sdr 1261fbb285297da08f4620b18871b6d6d9ec2a7b
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@ function cmakebuild() {
|
|||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES=""
|
||||
BUILD_PACKAGES="git cmake make gcc g++ pkg-config"
|
||||
STATIC_PACKAGES="libfftw3-single3"
|
||||
BUILD_PACKAGES="git cmake make gcc g++ pkg-config libfftw3-dev"
|
||||
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/jketterl/runds_connector.git
|
||||
# latest develop as of 2022-12-11 (std::endl implicit flushing)
|
||||
cmakebuild runds_connector 06ca993a3c81ddb0a2581b1474895da07752a9e1
|
||||
# latest develop as of 2023-07-04 (cmake exports)
|
||||
cmakebuild runds_connector 435364002d756735015707e7f59aa40e8d743585
|
||||
|
||||
apt-get -y purge --autoremove $BUILD_PACKAGES
|
||||
apt-get clean
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ ARCH=$(uname -m)
|
|||
|
||||
case $ARCH in
|
||||
x86_64)
|
||||
BINARY=SDRplay_RSP_API-Linux-3.07.1.run
|
||||
BINARY=SDRplay_RSP_API-Linux-3.12.1.run
|
||||
;;
|
||||
armv*)
|
||||
BINARY=SDRplay_RSP_API-ARM32-3.07.2.run
|
||||
;;
|
||||
aarch64)
|
||||
BINARY=SDRplay_RSP_API-ARM64-3.07.1.run
|
||||
BINARY=SDRplay_RSP_API-Linux-3.12.1.run
|
||||
;;
|
||||
esac
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ function cmakebuild() {
|
|||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline8 libgfortran5 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.74.0 libboost-log1.74.0 libcurl4"
|
||||
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-qmake libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev libpulse-dev libcurl4-openssl-dev"
|
||||
STATIC_PACKAGES="libfftw3-single3 libfftw3-double3 python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline8 libgfortran5 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.74.0 libboost-log1.74.0 libcurl4 libncurses6 libliquid1 libconfig++9v5"
|
||||
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-qmake libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev libpulse-dev libcurl4-openssl-dev libncurses-dev xz-utils libliquid-dev libconfig++-dev"
|
||||
apt-get update
|
||||
apt-get -y install auto-apt-proxy
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
|
@ -35,13 +35,16 @@ case `uname -m` in
|
|||
PLATFORM=aarch64
|
||||
;;
|
||||
x86_64*)
|
||||
PLATFORM=amd64
|
||||
PLATFORM=x86_64
|
||||
;;
|
||||
esac
|
||||
|
||||
wget https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${PLATFORM}.tar.gz
|
||||
tar xzf s6-overlay-${PLATFORM}.tar.gz -C /
|
||||
rm s6-overlay-${PLATFORM}.tar.gz
|
||||
wget https://github.com/just-containers/s6-overlay/releases/download/v3.1.5.0/s6-overlay-noarch.tar.xz
|
||||
tar -Jxpf /tmp/s6-overlay-noarch.tar.xz -C /
|
||||
rm s6-overlay-noarch.tar.xz
|
||||
wget https://github.com/just-containers/s6-overlay/releases/download/v3.1.5.0/s6-overlay-${PLATFORM}.tar.xz
|
||||
tar -Jxpf /tmp/s6-overlay-${PLATFORM}.tar.xz -C /
|
||||
rm s6-overlay-${PLATFORM}.tar.xz
|
||||
|
||||
JS8CALL_VERSION=2.2.0
|
||||
JS8CALL_DIR=js8call
|
||||
|
|
@ -86,8 +89,7 @@ rm -rf /usr/local/share/doc/direwolf/examples/
|
|||
|
||||
git clone https://github.com/drowe67/codec2.git
|
||||
cd codec2
|
||||
# latest commit from master as of 2020-10-04
|
||||
git checkout 55d7bb8d1bddf881bdbfcb971a718b83e6344598
|
||||
git checkout 1.2.0
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
|
|
@ -111,6 +113,26 @@ rm dream-2.1.1-svn808.tar.gz
|
|||
git clone https://github.com/mobilinkd/m17-cxx-demod.git
|
||||
cmakebuild m17-cxx-demod v2.3
|
||||
|
||||
git clone --depth 1 -b v9.0 https://github.com/flightaware/dump1090
|
||||
cd dump1090
|
||||
make
|
||||
install -m 0755 dump1090 /usr/local/bin
|
||||
cd ..
|
||||
rm -rf dump1090
|
||||
|
||||
git clone https://github.com/merbanan/rtl_433.git
|
||||
# latest from master as of 2023-09-06
|
||||
CMAKE_ARGS="-DENABLE_RTLSDR=OFF" cmakebuild rtl_433 70d84d01e1be87b459f7a10825966f3262b7dd34
|
||||
|
||||
git clone https://github.com/szpajder/libacars.git
|
||||
cmakebuild libacars v2.2.0
|
||||
|
||||
git clone https://github.com/szpajder/dumphfdl
|
||||
cmakebuild dumphfdl v1.4.1
|
||||
|
||||
git clone https://github.com/szpajder/dumpvdl2.git
|
||||
cmakebuild dumpvdl2 v2.3.0
|
||||
|
||||
git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols
|
||||
pushd /usr/share/aprs-symbols
|
||||
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function cmakebuild() {
|
|||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libfftw3-bin libprotobuf23 libsamplerate0 libicu67 libudev1"
|
||||
STATIC_PACKAGES="libfftw3-single3 libprotobuf32 libsamplerate0 libicu72 libudev1"
|
||||
BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++ libprotobuf-dev protobuf-compiler libsamplerate-dev libicu-dev libpython3-dev libudev-dev"
|
||||
apt-get update
|
||||
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
|
||||
|
|
@ -32,11 +32,13 @@ popd
|
|||
rm -rf js8py
|
||||
|
||||
git clone https://github.com/jketterl/csdr.git
|
||||
cmakebuild csdr 0.18.1
|
||||
# latest develop as of 2023-09-08 (execmodule improvements)
|
||||
cmakebuild csdr 764767933ed2b242190285f8ff56b11d80c7d530
|
||||
|
||||
git clone https://github.com/jketterl/pycsdr.git
|
||||
cd pycsdr
|
||||
git checkout 0.18.1
|
||||
# latest develop as of 2023-08-21 (death of CallbackWriter))
|
||||
git checkout 77b3709c545f510b52c7cea2e300e2e1613038e2
|
||||
./setup.py install install_headers
|
||||
cd ..
|
||||
rm -rf pycsdr
|
||||
|
|
@ -44,14 +46,17 @@ rm -rf pycsdr
|
|||
git clone https://github.com/jketterl/codecserver.git
|
||||
mkdir -p /usr/local/etc/codecserver
|
||||
cp codecserver/conf/codecserver.conf /usr/local/etc/codecserver
|
||||
cmakebuild codecserver 0.2.0
|
||||
# latest develop as of 2023-07-03 (error handling)
|
||||
cmakebuild codecserver 0f3703ce285acd85fcd28f6620d7795dc173cb50
|
||||
|
||||
git clone https://github.com/jketterl/digiham.git
|
||||
cmakebuild digiham 0.6.1
|
||||
# latest develop as of 2023-07-02 (codecserver protocol version)
|
||||
cmakebuild digiham 262e6dfd9a2c56778bd4b597240756ad0fb9861d
|
||||
|
||||
git clone https://github.com/jketterl/pydigiham.git
|
||||
cd pydigiham
|
||||
git checkout 0.6.1
|
||||
# latest develop as of 2023-06-30 (csdr cleanup)
|
||||
git checkout 894aa87ea9a3534d1e7109da86194c7cd5e0b7c7
|
||||
./setup.py install
|
||||
cd ..
|
||||
rm -rf pydigiham
|
||||
|
|
|
|||
|
|
@ -34,4 +34,3 @@ python3 openwebrx.py $@ &
|
|||
|
||||
child=$!
|
||||
wait "$child"
|
||||
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ h1 {
|
|||
}
|
||||
|
||||
.removable-group.removable .removable-item, .add-group .add-group-select {
|
||||
flex: 1 0 auto;
|
||||
flex: 1 0 0;
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -791,11 +791,10 @@ img.openwebrx-mirror-img
|
|||
|
||||
#openwebrx-digimode-canvas-container
|
||||
{
|
||||
/*margin: -10px -10px 10px -10px;*/
|
||||
margin: -10px -10px 0px -10px;
|
||||
border-radius: 15px;
|
||||
height: 150px;
|
||||
background-color: #333;
|
||||
margin: -10px -10px -10px -10px;
|
||||
border-radius: 15px;
|
||||
height: 200px;
|
||||
background-color: #333;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -902,7 +901,7 @@ img.openwebrx-mirror-img
|
|||
#openwebrx-digimode-content-container
|
||||
{
|
||||
overflow-y: hidden;
|
||||
display: block;
|
||||
display: none;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -921,16 +920,16 @@ img.openwebrx-mirror-img
|
|||
{
|
||||
transition: all 500ms;
|
||||
background-color: Yellow;
|
||||
display: block;
|
||||
display: none;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
height: 100%;
|
||||
width: 0px;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.7;
|
||||
border-style: solid;
|
||||
border-width: 0px;
|
||||
border-width: 0;
|
||||
border-color: Red;
|
||||
}
|
||||
|
||||
|
|
@ -1095,28 +1094,44 @@ img.openwebrx-mirror-img
|
|||
}
|
||||
|
||||
.openwebrx-message-panel {
|
||||
height: 180px;
|
||||
min-height: 180px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.openwebrx-message-panel tbody {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
.openwebrx-message-panel#openwebrx-panel-adsb-message {
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.openwebrx-message-panel thead tr {
|
||||
.openwebrx-message-panel table {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.openwebrx-message-panel th,
|
||||
.openwebrx-message-panel td {
|
||||
width: 50px;
|
||||
min-width: 50px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.openwebrx-message-panel th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #575757;
|
||||
}
|
||||
|
||||
.openwebrx-message-panel h4 {
|
||||
margin: 0 0 .25em;
|
||||
}
|
||||
|
||||
.openwebrx-message-panel .acars-message {
|
||||
white-space: pre;
|
||||
font-family: roboto-mono, monospace;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message .message {
|
||||
width: 380px;
|
||||
}
|
||||
|
|
@ -1254,49 +1269,19 @@ img.openwebrx-mirror-img
|
|||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #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,
|
||||
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #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,
|
||||
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-select-channel
|
||||
#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-select-channel
|
||||
{
|
||||
display: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="pocsag"] #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,
|
||||
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-canvas-container
|
||||
#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-canvas-container
|
||||
{
|
||||
height: 200px;
|
||||
margin: -10px;
|
||||
height: 150px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.openwebrx-zoom-button svg {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
$(function(){
|
||||
var converter = new showdown.Converter();
|
||||
var converter = new showdown.Converter({openLinksInNewWindow: true});
|
||||
$.ajax('api/features').done(function(data){
|
||||
var $table = $('table.features');
|
||||
$.each(data, function(name, details) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@
|
|||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-adsb-message" style="display: none; width: 619px;" data-panel-name="adsb-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-ism-message" style="display: none; width: 619px;" data-panel-name="ism-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-hfdl-message" style="display: none; width: 619px;" data-panel-name="hfdl-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-vdl2-message" style="display: none; width: 619px;" data-panel-name="vdl2-message"></div>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-m17" style="display: none;" data-panel-name="metadata-m17">
|
||||
<div class="openwebrx-meta-slot">
|
||||
<div class="openwebrx-meta-user-image">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,18 @@ function AprsMarker() {}
|
|||
|
||||
AprsMarker.prototype = new google.maps.OverlayView();
|
||||
|
||||
AprsMarker.prototype.isFacingEast = function(symbol) {
|
||||
var candidates = ''
|
||||
if (symbol.table === '/') {
|
||||
// primary table
|
||||
candidates = '(*<=>CFPUXYZabefgjkpsuv[';
|
||||
} else {
|
||||
// alternate table
|
||||
candidates = '(T`efhjktuvw';
|
||||
}
|
||||
return candidates.includes(symbol.symbol);
|
||||
};
|
||||
|
||||
AprsMarker.prototype.draw = function() {
|
||||
var div = this.div;
|
||||
var overlay = this.overlay;
|
||||
|
|
@ -16,9 +28,15 @@ AprsMarker.prototype.draw = function() {
|
|||
}
|
||||
|
||||
if (this.course) {
|
||||
if (this.course > 180) {
|
||||
if (this.symbol && !this.isFacingEast(this.symbol)) {
|
||||
// assume symbol points to the north
|
||||
div.style.transform = 'rotate(' + this.course + ' deg)';
|
||||
} else if (this.course > 180) {
|
||||
// symbol is pointing east
|
||||
// don't rotate more than 180 degrees, rather mirror
|
||||
div.style.transform = 'scalex(-1) rotate(' + (270 - this.course) + 'deg)'
|
||||
} else {
|
||||
// symbol is pointing east
|
||||
div.style.transform = 'rotate(' + (this.course - 90) + 'deg)';
|
||||
}
|
||||
} else {
|
||||
|
|
@ -79,7 +97,7 @@ AprsMarker.prototype.onAdd = function() {
|
|||
panes.overlayImage.appendChild(div);
|
||||
};
|
||||
|
||||
AprsMarker.prototype.remove = function() {
|
||||
AprsMarker.prototype.onRemove = function() {
|
||||
if (this.div) {
|
||||
this.div.parentNode.removeChild(this.div);
|
||||
this.div = null;
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ Envelope.prototype.draw = function(visible_range){
|
|||
from_px -= (env_att_w + env_bounding_line_w);
|
||||
to_px += (env_att_w + env_bounding_line_w);
|
||||
// do drawing:
|
||||
scale_ctx.lineWidth = 3;
|
||||
var color = this.color || '#ffff00'; // yellow
|
||||
scale_ctx.strokeStyle = color;
|
||||
scale_ctx.fillStyle = color;
|
||||
|
|
@ -77,16 +76,22 @@ Envelope.prototype.draw = function(visible_range){
|
|||
scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2);
|
||||
scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1);
|
||||
scale_ctx.lineTo(to_px, env_h1);
|
||||
scale_ctx.lineWidth = 3;
|
||||
scale_ctx.globalAlpha = 0.3;
|
||||
scale_ctx.fill();
|
||||
scale_ctx.globalAlpha = 1;
|
||||
scale_ctx.stroke();
|
||||
scale_ctx.lineWidth = 1;
|
||||
scale_ctx.font = "bold 11px sans-serif";
|
||||
scale_ctx.textBaseline = "top";
|
||||
scale_ctx.textAlign = "left";
|
||||
scale_ctx.fillText(this.demodulator.high_cut.toString(), to_px + env_att_w, env_h2);
|
||||
if (typeof(this.demodulator.high_cut) === 'number') {
|
||||
scale_ctx.fillText(this.demodulator.high_cut.toString(), to_px + env_att_w, env_h2);
|
||||
}
|
||||
scale_ctx.textAlign = "right";
|
||||
scale_ctx.fillText(this.demodulator.low_cut.toString(), from_px - env_att_w, env_h2);
|
||||
scale_ctx.lineWidth = 3;
|
||||
if (typeof(this.demodulator.low_cut) === 'number') {
|
||||
scale_ctx.fillText(this.demodulator.low_cut.toString(), from_px - env_att_w, env_h2);
|
||||
}
|
||||
}
|
||||
if (typeof line !== "undefined") // out of screen?
|
||||
{
|
||||
|
|
@ -96,6 +101,7 @@ Envelope.prototype.draw = function(visible_range){
|
|||
drag_ranges.line_on_screen = true;
|
||||
scale_ctx.moveTo(line_px, env_h1 + env_lineplus);
|
||||
scale_ctx.lineTo(line_px, env_h2 - env_lineplus);
|
||||
scale_ctx.lineWidth = 3;
|
||||
scale_ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
|
@ -332,6 +338,13 @@ Demodulator.prototype.setBandpass = function(bandpass) {
|
|||
this.set();
|
||||
};
|
||||
|
||||
Demodulator.prototype.disableBandpass = function() {
|
||||
delete this.bandpass;
|
||||
this.low_cut = null;
|
||||
this.high_cut = null;
|
||||
this.set()
|
||||
}
|
||||
|
||||
Demodulator.prototype.setLowCut = function(low_cut) {
|
||||
this.low_cut = low_cut;
|
||||
this.set();
|
||||
|
|
|
|||
|
|
@ -135,8 +135,12 @@ DemodulatorPanel.prototype.setMode = function(requestedModulation, underlyingMod
|
|||
|
||||
if (mode.type === 'digimode') {
|
||||
this.demodulator.set_secondary_demod(mode.modulation);
|
||||
if (mode.bandpass) {
|
||||
this.demodulator.setBandpass(mode.bandpass);
|
||||
var uMode = Modes.findByModulation(underlyingModulation);
|
||||
var bandpass = mode.bandpass || (uMode && uMode.bandpass);
|
||||
if (bandpass) {
|
||||
this.demodulator.setBandpass(bandpass);
|
||||
} else {
|
||||
this.demodulator.disableBandpass();
|
||||
}
|
||||
} else {
|
||||
this.demodulator.set_secondary_demod(false);
|
||||
|
|
@ -158,11 +162,14 @@ DemodulatorPanel.prototype.disableDigiMode = function() {
|
|||
DemodulatorPanel.prototype.updatePanels = function() {
|
||||
var modulation = this.getDemodulator().get_secondary_demod();
|
||||
$('#openwebrx-panel-digimodes').attr('data-mode', modulation);
|
||||
toggle_panel("openwebrx-panel-digimodes", !!modulation);
|
||||
var mode = Modes.findByModulation(modulation);
|
||||
toggle_panel("openwebrx-panel-digimodes", modulation && (!mode || mode.secondaryFft));
|
||||
// WSJT-X modes share the same panel
|
||||
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65", "msk144"].indexOf(modulation) >= 0);
|
||||
toggle_panel("openwebrx-panel-js8-message", modulation === "js8");
|
||||
toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
|
||||
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
|
||||
// these modes come with their own
|
||||
['js8', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2'].forEach(function(m) {
|
||||
toggle_panel('openwebrx-panel-' + m + '-message', modulation === m);
|
||||
});
|
||||
|
||||
modulation = this.getDemodulator().get_modulation();
|
||||
var showing = 'openwebrx-panel-metadata-' + modulation;
|
||||
|
|
@ -371,6 +378,6 @@ DemodulatorPanel.prototype.setTuningPrecision = function(precision) {
|
|||
$.fn.demodulatorPanel = function(){
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new DemodulatorPanel(this));
|
||||
};
|
||||
}
|
||||
return this.data('panel');
|
||||
};
|
||||
|
|
@ -98,13 +98,7 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
|||
if (index < 0) return;
|
||||
|
||||
var delta = 10 ** (Math.floor(Math.max(me.exponent, Math.log10(me.frequency))) - index);
|
||||
var newFrequency;
|
||||
if ('deltaMode' in e.originalEvent && e.originalEvent.deltaMode === 0) {
|
||||
newFrequency = me.frequency - delta * (e.originalEvent.deltaY / 50);
|
||||
} else {
|
||||
if (e.originalEvent.deltaY > 0) delta *= -1;
|
||||
newFrequency = me.frequency + delta;
|
||||
}
|
||||
var newFrequency = me.frequency - delta * wheelDelta(e.originalEvent);
|
||||
|
||||
me.element.trigger('frequencychange', newFrequency);
|
||||
});
|
||||
|
|
@ -126,6 +120,7 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
|
|||
submit();
|
||||
});
|
||||
$inputs.on('blur', function(e){
|
||||
if (!me.input.is(':visible')) return;
|
||||
if ($inputs.toArray().indexOf(e.relatedTarget) >= 0) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ MessagePanel.prototype.initClearButton = function() {
|
|||
$(me.el).append(me.clearButton);
|
||||
};
|
||||
|
||||
MessagePanel.prototype.htmlEscape = function(input) {
|
||||
return $('<div/>').text(input).html()
|
||||
};
|
||||
|
||||
MessagePanel.prototype.scrollToBottom = function() {
|
||||
var $t = $(this.el).find('table');
|
||||
$t.scrollTop($t[0].scrollHeight);
|
||||
};
|
||||
|
||||
function WsjtMessagePanel(el) {
|
||||
MessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
|
|
@ -55,7 +64,7 @@ function WsjtMessagePanel(el) {
|
|||
this.modes = [].concat(this.qsoModes, this.beaconModes);
|
||||
}
|
||||
|
||||
WsjtMessagePanel.prototype = new MessagePanel();
|
||||
WsjtMessagePanel.prototype = Object.create(MessagePanel.prototype);
|
||||
|
||||
WsjtMessagePanel.prototype.supportsMessage = function(message) {
|
||||
return this.modes.indexOf(message['mode']) >= 0;
|
||||
|
|
@ -85,23 +94,19 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) {
|
|||
var linkedmsg = msg['msg'];
|
||||
var matches;
|
||||
|
||||
var html_escape = function(input) {
|
||||
return $('<div/>').text(input).html()
|
||||
};
|
||||
|
||||
if (this.qsoModes.indexOf(msg['mode']) >= 0) {
|
||||
matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/);
|
||||
if (matches && matches[2] !== 'RR73') {
|
||||
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
|
||||
linkedmsg = this.htmlEscape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>';
|
||||
} else {
|
||||
linkedmsg = html_escape(linkedmsg);
|
||||
linkedmsg = this.htmlEscape(linkedmsg);
|
||||
}
|
||||
} else if (this.beaconModes.indexOf(msg['mode']) >= 0) {
|
||||
matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/);
|
||||
if (matches) {
|
||||
linkedmsg = html_escape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + html_escape(matches[3]);
|
||||
linkedmsg = this.htmlEscape(matches[1]) + '<a href="map?locator=' + matches[2] + '" target="openwebrx-map">' + matches[2] + '</a>' + this.htmlEscape(matches[3]);
|
||||
} else {
|
||||
linkedmsg = html_escape(linkedmsg);
|
||||
linkedmsg = this.htmlEscape(linkedmsg);
|
||||
}
|
||||
}
|
||||
$b.append($(
|
||||
|
|
@ -113,7 +118,7 @@ WsjtMessagePanel.prototype.pushMessage = function(msg) {
|
|||
'<td class="message">' + linkedmsg + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
$.fn.wsjtMessagePanel = function(){
|
||||
|
|
@ -128,7 +133,7 @@ function PacketMessagePanel(el) {
|
|||
this.initClearTimer();
|
||||
}
|
||||
|
||||
PacketMessagePanel.prototype = new MessagePanel();
|
||||
PacketMessagePanel.prototype = Object.create(MessagePanel.prototype);
|
||||
|
||||
PacketMessagePanel.prototype.supportsMessage = function(message) {
|
||||
return message['mode'] === 'APRS';
|
||||
|
|
@ -216,10 +221,10 @@ PacketMessagePanel.prototype.pushMessage = function(msg) {
|
|||
'<td>' + timestamp + '</td>' +
|
||||
'<td class="callsign">' + callsign + '</td>' +
|
||||
'<td class="coord">' + link + '</td>' +
|
||||
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
|
||||
'<td class="message">' + this.htmlEscape(msg.comment || msg.message || '') + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
$.fn.packetMessagePanel = function() {
|
||||
|
|
@ -234,7 +239,7 @@ PocsagMessagePanel = function(el) {
|
|||
this.initClearTimer();
|
||||
}
|
||||
|
||||
PocsagMessagePanel.prototype = new MessagePanel();
|
||||
PocsagMessagePanel.prototype = Object.create(MessagePanel.prototype);
|
||||
|
||||
PocsagMessagePanel.prototype.supportsMessage = function(message) {
|
||||
return message['mode'] === 'Pocsag';
|
||||
|
|
@ -257,10 +262,10 @@ PocsagMessagePanel.prototype.pushMessage = function(msg) {
|
|||
$b.append($(
|
||||
'<tr>' +
|
||||
'<td class="address">' + msg.address + '</td>' +
|
||||
'<td class="message">' + msg.message + '</td>' +
|
||||
'<td class="message">' + this.htmlEscape(msg.message) + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
$b.scrollTop($b[0].scrollHeight);
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
$.fn.pocsagMessagePanel = function() {
|
||||
|
|
@ -268,4 +273,540 @@ $.fn.pocsagMessagePanel = function() {
|
|||
this.data('panel', new PocsagMessagePanel(this));
|
||||
}
|
||||
return this.data('panel');
|
||||
};
|
||||
|
||||
AdsbMessagePanel = function(el) {
|
||||
MessagePanel.call(this, el);
|
||||
this.aircraft = {}
|
||||
this.aircraftTrackingService = false;
|
||||
this.initClearTimer();
|
||||
}
|
||||
|
||||
AdsbMessagePanel.prototype = Object.create(MessagePanel.prototype);
|
||||
|
||||
AdsbMessagePanel.prototype.supportsMessage = function(message) {
|
||||
return message["mode"] === "ADSB";
|
||||
};
|
||||
|
||||
AdsbMessagePanel.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th class="address">ICAO</th>' +
|
||||
'<th class="callsign">Flight</th>' +
|
||||
'<th class="altitude">Altitude</th>' +
|
||||
'<th class="speed">Speed</th>' +
|
||||
'<th class="track">Track</th>' +
|
||||
'<th class="verticalspeed">V/S</th>' +
|
||||
'<th class="position">Position</th>' +
|
||||
'<th class="messages">Messages</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
};
|
||||
|
||||
|
||||
AdsbMessagePanel.prototype.pushMessage = function(message) {
|
||||
if (!('icao' in message)) return;
|
||||
if (!(message.icao in this.aircraft)) {
|
||||
var el = $("<tr>");
|
||||
$(this.el).find('tbody').append(el);
|
||||
this.aircraft[message.icao] = {
|
||||
el: el,
|
||||
messages: 0
|
||||
}
|
||||
}
|
||||
var state = this.aircraft[message.icao];
|
||||
Object.assign(state, message);
|
||||
state.lastSeen = Date.now();
|
||||
state.messages += 1;
|
||||
|
||||
var ifDefined = function(input, formatter) {
|
||||
if (typeof(input) !== 'undefined') {
|
||||
if (formatter) return formatter(input);
|
||||
return input;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
var coordRound = function(i) {
|
||||
return Math.round(i * 1000) / 1000;
|
||||
}
|
||||
|
||||
var getPosition = function(state) {
|
||||
if (!('lat' in state) || !('lon') in state) return '';
|
||||
return '<a href="map?icao=' + state.icao + '" target="openwebrx-map">' + coordRound(state.lat) + ', ' + coordRound(state.lon) + '</a>';
|
||||
}
|
||||
|
||||
state.el.html(
|
||||
'<td>' + this.linkify(state, state.icao) + '</td>' +
|
||||
'<td>' + this.linkify(state, ifDefined(state.identification)) + '</td>' +
|
||||
'<td>' + ifDefined(state.altitude) + '</td>' +
|
||||
'<td>' + ifDefined(state.groundspeed || state.IAS || state.TAS, Math.round) + '</td>' +
|
||||
'<td>' + ifDefined(state.groundtrack || state.heading, Math.round) + '</td>' +
|
||||
'<td>' + ifDefined(state.verticalspeed) + '</td>' +
|
||||
'<td>' + getPosition(state) + '</td>' +
|
||||
'<td>' + state.messages + '</td>'
|
||||
);
|
||||
};
|
||||
|
||||
AdsbMessagePanel.prototype.clearMessages = function(toRemain) {
|
||||
var now = Date.now();
|
||||
var me = this;
|
||||
Object.entries(this.aircraft).forEach(function(e) {
|
||||
if (now - e[1].lastSeen > toRemain) {
|
||||
delete me.aircraft[e[0]];
|
||||
e[1].el.remove();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
AdsbMessagePanel.prototype.initClearTimer = function() {
|
||||
var me = this;
|
||||
if (me.removalInterval) clearInterval(me.removalInterval);
|
||||
me.removalInterval = setInterval(function () {
|
||||
me.clearMessages(30000);
|
||||
}, 15000);
|
||||
};
|
||||
|
||||
AdsbMessagePanel.prototype.setAircraftTrackingService = function(service) {
|
||||
this.aircraftTrackingService = service;
|
||||
};
|
||||
|
||||
AdsbMessagePanel.prototype.linkify = function(state, text) {
|
||||
var link = false;
|
||||
switch (this.aircraftTrackingService) {
|
||||
case 'flightaware':
|
||||
link = 'https://flightaware.com/live/modes/' + state.icao;
|
||||
if (state.identification) link += "/ident/" + state.identification
|
||||
link += '/redirect';
|
||||
break;
|
||||
case 'planefinder':
|
||||
if (state.identification) link = 'https://planefinder.net/flight/' + state.identification;
|
||||
break;
|
||||
}
|
||||
if (link) {
|
||||
return '<a target="_blank" href="' + link + '">' + text + '</a>';
|
||||
}
|
||||
return text;
|
||||
|
||||
};
|
||||
|
||||
$.fn.adsbMessagePanel = function () {
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new AdsbMessagePanel(this));
|
||||
}
|
||||
return this.data('panel');
|
||||
};
|
||||
|
||||
IsmMessagePanel = function(el) {
|
||||
MessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
};
|
||||
|
||||
IsmMessagePanel.prototype = Object.create(MessagePanel.prototype);
|
||||
|
||||
IsmMessagePanel.prototype.supportsMessage = function(message) {
|
||||
return message['mode'] === 'ISM';
|
||||
};
|
||||
|
||||
IsmMessagePanel.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th class="model">Model</th>' +
|
||||
'<th class="id">ID</th>' +
|
||||
'<th class="channel">Channel</th>' +
|
||||
'<th class="data">Data</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
|
||||
};
|
||||
|
||||
IsmMessagePanel.prototype.pushMessage = function(message) {
|
||||
var $b = $(this.el).find('tbody');
|
||||
|
||||
var ifDefined = function(input, formatter) {
|
||||
if (typeof(input) !== 'undefined') {
|
||||
if (formatter) return formatter(input);
|
||||
return input;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
var mergeRemainingMessage = function(input, exclude) {
|
||||
return Object.entries(input).map(function(entry) {
|
||||
if (exclude.includes(entry[0])) return '';
|
||||
return entry[0] + ': ' + entry[1] + ';';
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
$b.append($(
|
||||
'<tr>' +
|
||||
'<td class="model">' + ifDefined(message.model) + '</td>' +
|
||||
'<td class="id">' + ifDefined(message.id) + '</td>' +
|
||||
'<td class="channel">' + ifDefined(message.channel) + '</td>' +
|
||||
'<td class="data">' + this.htmlEscape(mergeRemainingMessage(message, ['model', 'id', 'channel', 'mode', 'time'])) + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
$.fn.ismMessagePanel = function() {
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new IsmMessagePanel(this));
|
||||
}
|
||||
return this.data('panel');
|
||||
};
|
||||
|
||||
AircraftMessagePanel = function(el) {
|
||||
MessagePanel.call(this, el);
|
||||
}
|
||||
|
||||
AircraftMessagePanel.prototype = Object.create(MessagePanel.prototype);
|
||||
|
||||
AircraftMessagePanel.prototype.renderAcars = function(acars) {
|
||||
if (acars['more']) {
|
||||
return '<h4>Partial ACARS message</h4>';
|
||||
}
|
||||
var details = '<h4>ACARS message</h4>';
|
||||
if ('flight' in acars) {
|
||||
details += '<div>Flight: ' + this.handleFlight(acars['flight']) + '</div>';
|
||||
}
|
||||
details += '<div>Registration: ' + acars['reg'].replace(/^\.+/g, '') + '</div>';
|
||||
if ('media-adv' in acars) {
|
||||
details += '<div>Media advisory</div>';
|
||||
var mediaadv = acars['media-adv'];
|
||||
if ('current_link' in mediaadv) {
|
||||
details += '<div>Current link: ' + mediaadv['current_link']['descr'];
|
||||
}
|
||||
if ('links_avail' in mediaadv) {
|
||||
details += '<div>Available links: ' + mediaadv['links_avail'].map(function (l) {
|
||||
return l['descr'];
|
||||
}).join(', ') + '</div>';
|
||||
}
|
||||
} else if ('arinc622' in acars) {
|
||||
var arinc622 = acars['arinc622'];
|
||||
if ('adsc' in arinc622) {
|
||||
var adsc = arinc622['adsc'];
|
||||
if ('tags' in adsc) {
|
||||
adsc['tags'].forEach(function(tag) {
|
||||
if ('basic_report' in tag) {
|
||||
var basic_report = tag['basic_report'];
|
||||
details += '<div>Basic ADS-C report</div>';
|
||||
details += '<div>Position: ' + basic_report['lat'] + ', ' + basic_report['lon'] + '</div>';
|
||||
details += '<div>Altitude: ' + basic_report['alt'] + '</div>';
|
||||
} else if ('earth_ref_data' in tag) {
|
||||
var earth_ref_data = tag['earth_ref_data'];
|
||||
details += '<div>Track: ' + earth_ref_data['true_trk_deg'] + '</div>';
|
||||
details += '<div>Speed: ' + earth_ref_data['gnd_spd_kts'] + ' kt</div>';
|
||||
details += '<div>Vertical speed: ' + earth_ref_data['vspd_ftmin'] + ' ft/min</div>';
|
||||
} else if ('cancel_all_contracts' in tag) {
|
||||
details += '<div>Cancel all ADS-C contracts</div>';
|
||||
} else if ('cancel_contract' in tag) {
|
||||
details += '<div>Cancel ADS-C contract</div>';
|
||||
} else {
|
||||
details += '<div>Unsupported tag</div>';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
details += '<div>Other ADS-C data</div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// plain text
|
||||
details += '<div>Label: ' + acars['label'] + '</div>';
|
||||
details += '<div class="acars-message">' + acars['msg_text'] + '</div>';
|
||||
}
|
||||
return details;
|
||||
};
|
||||
|
||||
AircraftMessagePanel.prototype.handleFlight = function(raw) {
|
||||
return raw.replace(/^([0-9A-Z]{2})0*([0-9A-Z]+$)/, '$1$2');
|
||||
};
|
||||
|
||||
HfdlMessagePanel = function(el) {
|
||||
AircraftMessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
}
|
||||
|
||||
HfdlMessagePanel.prototype = Object.create(AircraftMessagePanel.prototype);
|
||||
|
||||
HfdlMessagePanel.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th class="source">Source</th>' +
|
||||
'<th class="destination">Destination</th>' +
|
||||
'<th class="details">Details</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
};
|
||||
|
||||
HfdlMessagePanel.prototype.supportsMessage = function(message) {
|
||||
return message['mode'] === 'HFDL';
|
||||
};
|
||||
|
||||
HfdlMessagePanel.prototype.renderPosition = function(hfnpdu) {
|
||||
if ('pos' in hfnpdu) {
|
||||
var pos = hfnpdu['pos'];
|
||||
var lat = pos['lat'] || 180;
|
||||
var lon = pos['lon'] || 180;
|
||||
if (Math.abs(lat) <= 90 && Math.abs(lon) <= 180) {
|
||||
return '<div>Position: ' + pos['lat'] + ', ' + pos['lon'] + '</div>';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
HfdlMessagePanel.prototype.renderLogon = function(lpdu) {
|
||||
var details = ''
|
||||
if (lpdu['ac_info'] && lpdu['ac_info']['icao']) {
|
||||
details += '<div>ICAO: ' + lpdu['ac_info']['icao'] + '</div>';
|
||||
}
|
||||
if (lpdu['hfnpdu']) {
|
||||
var hfnpdu = lpdu['hfnpdu'];
|
||||
if (hfnpdu['flight_id'] && hfnpdu['flight_id'] !== '') {
|
||||
details += '<div>Flight: ' + this.handleFlight(lpdu['hfnpdu']['flight_id']) + '</div>'
|
||||
}
|
||||
details += this.renderPosition(hfnpdu);
|
||||
}
|
||||
return details;
|
||||
};
|
||||
|
||||
HfdlMessagePanel.prototype.pushMessage = function(message) {
|
||||
var $b = $(this.el).find('tbody');
|
||||
|
||||
var src = '';
|
||||
var dst = '';
|
||||
var details = JSON.stringify(message);
|
||||
|
||||
var renderAddress = function(a) {
|
||||
return a['id'];
|
||||
}
|
||||
|
||||
// TODO remove safety net once parsing is complete
|
||||
try {
|
||||
var payload = message['hfdl'];
|
||||
if ('spdu' in payload) {
|
||||
var spdu = payload['spdu'];
|
||||
src = renderAddress(spdu['src']);
|
||||
details = '<h4>HFDL Squitter message</h4>'
|
||||
details += '<div>Systable version: ' + spdu['systable_version'] + '</div>';
|
||||
|
||||
if ('gs_status' in spdu) {
|
||||
details += spdu['gs_status'].map(function(gs){
|
||||
return '<div>Ground station ' + gs['gs']['id'] + ' is operating on frequency ids ' + gs['freqs'].map(function(f) {return f['id']; }).join(', ') + '</div>';
|
||||
}).join('')
|
||||
}
|
||||
} else if ('lpdu' in payload) {
|
||||
var lpdu = payload['lpdu'];
|
||||
src = renderAddress(lpdu['src']);
|
||||
dst = renderAddress(lpdu['dst']);
|
||||
if (lpdu['type']['id'] === 13 || lpdu['type']['id'] === 29) {
|
||||
// unnumbered data
|
||||
var hfnpdu = lpdu['hfnpdu'];
|
||||
if (hfnpdu['type']['id'] === 209) {
|
||||
// performance data
|
||||
details = '<h4>Performance data</h4>';
|
||||
details += '<div>Flight: ' + this.handleFlight(hfnpdu['flight_id']) + '</div>';
|
||||
details += this.renderPosition(hfnpdu);
|
||||
} else if (hfnpdu['type']['id'] === 255) {
|
||||
// enveloped data
|
||||
if ('acars' in hfnpdu) {
|
||||
details = this.renderAcars(hfnpdu['acars']);
|
||||
}
|
||||
}
|
||||
} else if (lpdu['type']['id'] === 47) {
|
||||
// logon denied
|
||||
details = '<h4>Logon denied</h4>';
|
||||
} else if (lpdu['type']['id'] === 63) {
|
||||
details = '<h4>Logoff request</h4>';
|
||||
if (lpdu['ac_info'] && lpdu['ac_info']['icao']) {
|
||||
details += '<div>ICAO: ' + lpdu['ac_info']['icao'] + '</div>';
|
||||
}
|
||||
} else if (lpdu['type']['id'] === 79) {
|
||||
details = '<h4>Logon resume</h4>';
|
||||
details += this.renderLogon(lpdu);
|
||||
} else if (lpdu['type']['id'] === 95) {
|
||||
details = '<h4>Logon resume confirmation</h4>';
|
||||
} else if (lpdu['type']['id'] === 143) {
|
||||
details = '<h4>Logon request</h4>';
|
||||
details += this.renderLogon(lpdu);
|
||||
} else if (lpdu['type']['id'] === 159) {
|
||||
details = '<h4>Logon confirmation</h4>';
|
||||
if (lpdu['ac_info'] && lpdu['ac_info']['icao']) {
|
||||
details += '<div>ICAO: ' + lpdu['ac_info']['icao'] + '</div>';
|
||||
}
|
||||
if (lpdu['assigned_ac_id']) {
|
||||
details += '<div>Assigned aircraft ID: ' + lpdu['assigned_ac_id'] + '</div>';
|
||||
}
|
||||
} else if (lpdu['type']['id'] === 191) {
|
||||
details = '<h4>Logon request (DLS)</h4>';
|
||||
details += this.renderLogon(lpdu);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e, e.stack);
|
||||
}
|
||||
|
||||
$b.append($(
|
||||
'<tr>' +
|
||||
'<td class="source">' + src + '</td>' +
|
||||
'<td class="destination">' + dst + '</td>' +
|
||||
'<td class="details">' + details + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
$.fn.hfdlMessagePanel = function() {
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new HfdlMessagePanel(this));
|
||||
}
|
||||
return this.data('panel');
|
||||
};
|
||||
|
||||
Vdl2MessagePanel = function(el) {
|
||||
AircraftMessagePanel.call(this, el);
|
||||
this.initClearTimer();
|
||||
}
|
||||
|
||||
Vdl2MessagePanel.prototype = Object.create(AircraftMessagePanel.prototype);
|
||||
|
||||
Vdl2MessagePanel.prototype.render = function() {
|
||||
$(this.el).append($(
|
||||
'<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th class="source">Source</th>' +
|
||||
'<th class="destination">Destination</th>' +
|
||||
'<th class="details">Details</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody></tbody>' +
|
||||
'</table>'
|
||||
));
|
||||
};
|
||||
|
||||
Vdl2MessagePanel.prototype.supportsMessage = function(message) {
|
||||
return message['mode'] === 'VDL2';
|
||||
};
|
||||
|
||||
Vdl2MessagePanel.prototype.pushMessage = function(message) {
|
||||
var $b = $(this.el).find('tbody');
|
||||
var src = '';
|
||||
var dst = '';
|
||||
var details = JSON.stringify(message);
|
||||
|
||||
var renderAddress = function(a) {
|
||||
return '<div>' + a['addr'] + '</div><div>' + a['type'] + ( 'status' in a ? ' (' + a['status'] + ')' : '' ) + '</div>'
|
||||
}
|
||||
|
||||
// TODO remove safety net once parsing is complete
|
||||
try {
|
||||
var payload = message['vdl2'];
|
||||
if ('avlc' in payload) {
|
||||
var avlc = payload['avlc'];
|
||||
src = renderAddress(avlc['src']);
|
||||
dst = renderAddress(avlc['dst']);
|
||||
|
||||
if (avlc['frame_type'] === 'S') {
|
||||
details = '<h4>Supervisory frame</h4>';
|
||||
if (avlc['cmd'] === 'Receive Ready') {
|
||||
details = '<h4>Receive Ready</h4>';
|
||||
}
|
||||
} else if (avlc['frame_type'] === 'I') {
|
||||
details = '<h4>Information frame</h4>';
|
||||
if ('acars' in avlc) {
|
||||
details = this.renderAcars(avlc['acars']);
|
||||
} else if ('x25' in avlc) {
|
||||
var x25 = avlc['x25'];
|
||||
if (!('reasm_status' in x25) || ['skipped', 'complete'].includes(x25['reasm_status'])) {
|
||||
details = '<h4>X.25 frame</h4>';
|
||||
if ('clnp' in x25) {
|
||||
var clnp = x25['clnp']
|
||||
if ('cotp' in clnp) {
|
||||
var cotp = clnp['cotp'];
|
||||
if ('cpdlc' in cotp) {
|
||||
var cpdlc = cotp['cpdlc'];
|
||||
details = '<h4>CPDLC</h4>';
|
||||
if ('atc_downlink_message' in cpdlc) {
|
||||
var atc_downlink_message = cpdlc['atc_downlink_message'];
|
||||
if ('msg_data' in atc_downlink_message) {
|
||||
var msg_data = atc_downlink_message['msg_data'];
|
||||
if ('msg_elements' in msg_data) {
|
||||
details += '<div>' + msg_data['msg_elements'].map(function(e) { return e['msg_element']['choice_label']; }).join(', ') + '</div>';
|
||||
}
|
||||
} else {
|
||||
details += '<div>' + JSON.stringify(cpdlc) + '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('adsc_v2' in cotp) {
|
||||
var adsc_v2 = cotp['adsc_v2']
|
||||
details = '<h4>ADS-C v2 Frame</h4>';
|
||||
if ('adsc_report' in adsc_v2) {
|
||||
var adsc_report = adsc_v2['adsc_report'];
|
||||
var data = adsc_report['data'];
|
||||
if ('periodic_report' in data) {
|
||||
details += '<div>Periodic report</div>';
|
||||
details += this.processReport(data['periodic_report']['report_data']);
|
||||
} else if ('event_report' in data) {
|
||||
details += '<div>Event report</div>';
|
||||
details += this.processReport(data['event_report']['report_data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
details = '<h4>Partial X.25 frame</h4>';
|
||||
}
|
||||
}
|
||||
} else if (avlc['frame_type'] === 'U') {
|
||||
details = '<h4>Unnumbered frame</h4>';
|
||||
if ('xid' in avlc) {
|
||||
var xid = avlc['xid'];
|
||||
details = '<h4>' + xid['type_descr'] + '</h4>';
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e, e.stack);
|
||||
}
|
||||
|
||||
$b.append($(
|
||||
'<tr>' +
|
||||
'<td class="source">' + src + '</td>' +
|
||||
'<td class="destination">' + dst + '</td>' +
|
||||
'<td class="details">' + details + '</td>' +
|
||||
'</tr>'
|
||||
));
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
Vdl2MessagePanel.prototype.processReport = function(report) {
|
||||
var details = '';
|
||||
if ('position' in report) {
|
||||
var position = report['position'];
|
||||
var lat = position['lat']
|
||||
var lon = position['lon']
|
||||
details += '<div>Position: ' +
|
||||
lat['deg'] + '° ' + lat['min'] + '\' ' + lat['sec'] + '" ' + lat['dir'] + ', ' +
|
||||
lon['deg'] + '° ' + lon['min'] + '\' ' + lon['sec'] + '" ' + lon['dir'] +
|
||||
'</div>';
|
||||
details += '<div>Altitude: ' + position['alt']['val'] + ' ' + position['alt']['unit'] + '</div>';
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
$.fn.vdl2MessagePanel = function() {
|
||||
if (!this.data('panel')) {
|
||||
this.data('panel', new Vdl2MessagePanel(this));
|
||||
}
|
||||
return this.data('panel');
|
||||
};
|
||||
|
|
@ -43,6 +43,7 @@ var Mode = function(json){
|
|||
}
|
||||
if (this.type === 'digimode') {
|
||||
this.underlying = json.underlying;
|
||||
this.secondaryFft = json.secondaryFft;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
108
htdocs/lib/PlaneMarker.js
Normal file
108
htdocs/lib/PlaneMarker.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
function PlaneMarker(){}
|
||||
|
||||
PlaneMarker.prototype = new google.maps.OverlayView();
|
||||
|
||||
PlaneMarker.prototype.draw = function() {
|
||||
var svg = this.svg;
|
||||
if (!svg) return;
|
||||
|
||||
var angle = this.groundtrack || this.heading;
|
||||
if (angle) {
|
||||
svg.style.transform = 'rotate(' + angle + 'deg)';
|
||||
} else {
|
||||
svg.style.transform = null;
|
||||
}
|
||||
|
||||
if (this.opacity) {
|
||||
svg.style.opacity = this.opacity;
|
||||
} else {
|
||||
svg.style.opacity = null;
|
||||
}
|
||||
|
||||
var point = this.getProjection().fromLatLngToDivPixel(this.position);
|
||||
|
||||
if (point) {
|
||||
svg.style.left = point.x - 15 + 'px';
|
||||
svg.style.top = point.y - 15 + 'px';
|
||||
}
|
||||
|
||||
svg.setAttribute('fill', this.getMarkerColor());
|
||||
};
|
||||
|
||||
PlaneMarker.prototype.setOptions = function(options) {
|
||||
google.maps.OverlayView.prototype.setOptions.apply(this, arguments);
|
||||
this.draw();
|
||||
};
|
||||
|
||||
PlaneMarker.prototype.onAdd = function() {
|
||||
var svg = this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('viewBox', '0 0 65 65');
|
||||
svg.setAttribute('fill', this.getMarkerColor());
|
||||
svg.setAttribute('stroke', 'black');
|
||||
|
||||
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
path.setAttribute('d', 'M 0,0 M 1.9565564,41.694305 C 1.7174505,40.497708 1.6419973,38.448747 1.8096508,37.70494 1.8936398,37.332056 2.0796653,36.88191 2.222907,36.70461 2.4497603,36.423844 4.087816,35.47248 14.917931,29.331528 l 12.434577,-7.050718 -0.04295,-7.613412 c -0.03657,-6.4844888 -0.01164,-7.7625804 0.168134,-8.6194061 0.276129,-1.3160905 0.762276,-2.5869575 1.347875,-3.5235502 l 0.472298,-0.7553719 1.083746,-0.6085497 c 1.194146,-0.67053522 1.399524,-0.71738842 2.146113,-0.48960552 1.077005,0.3285939 2.06344,1.41299352 2.797602,3.07543322 0.462378,1.0469993 0.978731,2.7738408 1.047635,3.5036272 0.02421,0.2570284 0.06357,3.78334 0.08732,7.836246 0.02375,4.052905 0.0658,7.409251 0.09345,7.458546 0.02764,0.04929 5.600384,3.561772 12.38386,7.805502 l 12.333598,7.715871 0.537584,0.959688 c 0.626485,1.118378 0.651686,1.311286 0.459287,3.516442 -0.175469,2.011604 -0.608966,2.863924 -1.590344,3.127136 -0.748529,0.200763 -1.293144,0.03637 -10.184829,-3.07436 C 48.007733,41.72562 44.793806,40.60197 43.35084,40.098045 l -2.623567,-0.916227 -1.981212,-0.06614 c -1.089663,-0.03638 -1.985079,-0.05089 -1.989804,-0.03225 -0.0052,0.01863 -0.02396,2.421278 -0.04267,5.339183 -0.0395,6.147742 -0.143635,7.215456 -0.862956,8.845475 l -0.300457,0.680872 2.91906,1.361455 c 2.929379,1.366269 3.714195,1.835385 4.04589,2.41841 0.368292,0.647353 0.594634,2.901439 0.395779,3.941627 -0.0705,0.368571 -0.106308,0.404853 -0.765159,0.773916 L 41.4545,62.83158 39.259237,62.80426 c -6.030106,-0.07507 -16.19508,-0.495041 -16.870991,-0.697033 -0.359409,-0.107405 -0.523792,-0.227482 -0.741884,-0.541926 -0.250591,-0.361297 -0.28386,-0.522402 -0.315075,-1.52589 -0.06327,-2.03378 0.23288,-3.033615 1.077963,-3.639283 0.307525,-0.2204 4.818478,-2.133627 6.017853,-2.552345 0.247872,-0.08654 0.247455,-0.102501 -0.01855,-0.711959 -0.330395,-0.756986 -0.708622,-2.221756 -0.832676,-3.224748 -0.05031,-0.406952 -0.133825,-3.078805 -0.185533,-5.937448 -0.0517,-2.858644 -0.145909,-5.208974 -0.209316,-5.222958 -0.06341,-0.01399 -0.974464,-0.0493 -2.024551,-0.07845 L 23.247235,38.61921 18.831373,39.8906 C 4.9432155,43.88916 4.2929558,44.057819 3.4954426,43.86823 2.7487826,43.690732 2.2007966,42.916622 1.9565564,41.694305 z')
|
||||
svg.appendChild(path);
|
||||
|
||||
svg.style.position = 'absolute';
|
||||
svg.style.cursor = 'pointer';
|
||||
svg.style.width = '30px';
|
||||
svg.style.height = '30px';
|
||||
|
||||
var self = this;
|
||||
svg.addEventListener("click", function(event) {
|
||||
event.stopPropagation();
|
||||
google.maps.event.trigger(self, "click", event);
|
||||
});
|
||||
|
||||
var panes = this.getPanes();
|
||||
panes.overlayImage.appendChild(svg);
|
||||
};
|
||||
|
||||
PlaneMarker.prototype.onRemove = function() {
|
||||
if (this.svg) {
|
||||
this.svg.parentNode.removeChild(this.svg);
|
||||
this.svg = null;
|
||||
}
|
||||
};
|
||||
|
||||
PlaneMarker.prototype.getMarkerColor = function() {
|
||||
var toHsl = function(input) {
|
||||
return 'hsl(' + input.h + ', ' + input.s + '%, ' + input.l + '%)'
|
||||
};
|
||||
|
||||
if (!this.altitude) {
|
||||
return toHsl({h: 0, s: 0, l: 40});
|
||||
}
|
||||
if (this.altitude === "ground") {
|
||||
return toHsl({h: 120, s: 100, l: 30});
|
||||
}
|
||||
|
||||
// find the pair of points the current altitude lies between,
|
||||
// and interpolate the hue between those points
|
||||
var hpoints = [
|
||||
{ alt: 2000, val: 20 }, // orange
|
||||
{ alt: 10000, val: 140 }, // light green
|
||||
{ alt: 40000, val: 300 }
|
||||
];
|
||||
var h = hpoints[0].val;
|
||||
|
||||
for (var i = hpoints.length-1; i >= 0; --i) {
|
||||
if (this.altitude > hpoints[i].alt) {
|
||||
if (i === hpoints.length - 1) {
|
||||
h = hpoints[i].val;
|
||||
} else {
|
||||
h = hpoints[i].val + (hpoints[i+1].val - hpoints[i].val) * (this.altitude - hpoints[i].alt) / (hpoints[i+1].alt - hpoints[i].alt)
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (h < 0) {
|
||||
h = (h % 360) + 360;
|
||||
} else if (h >= 360) {
|
||||
h = h % 360;
|
||||
}
|
||||
|
||||
return toHsl({h: h, s: 85, l: 50})
|
||||
}
|
||||
21
htdocs/lib/wheelDelta.js
Normal file
21
htdocs/lib/wheelDelta.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Normalize scroll wheel events.
|
||||
*
|
||||
* It seems like there's no consent as to how mouse wheel events are presented in the javascript API. The standard
|
||||
* states that a MouseEvent has a deltaY property that contains the scroll distances, together with a deltaMode
|
||||
* property that state the "unit" that deltaY has been measured in. The deltaMode can be either pixels, lines or
|
||||
* pages. The latter is seldomly used in practise.
|
||||
*
|
||||
* The troublesome part is that there is no standard on how to correlate the two at this point.
|
||||
*
|
||||
* The basic idea is that one tick of a mouse wheel results in a total return value of +/- 1 from this method.
|
||||
* It's important to keep in mind that one tick of a wheel may result in multiple events in the browser. The aim
|
||||
* of this method is to scale the sum of deltaY over
|
||||
*/
|
||||
function wheelDelta(evt) {
|
||||
if ('deltaMode' in evt && evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
||||
// chrome and webkit-based browsers seem to correlate one tick of the wheel to 120 pixels.
|
||||
return evt.deltaY / 120;
|
||||
}
|
||||
return evt.deltaY;
|
||||
}
|
||||
132
htdocs/map.js
132
htdocs/map.js
|
|
@ -7,6 +7,8 @@ $(function(){
|
|||
}
|
||||
var expectedLocator;
|
||||
if (query.has('locator')) expectedLocator = query.get('locator');
|
||||
var expectedIcao;
|
||||
if (query.has('icao')) expectedIcao = query.get('icao');
|
||||
|
||||
var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws';
|
||||
|
||||
|
|
@ -28,11 +30,10 @@ $(function(){
|
|||
var receiverMarker;
|
||||
var updateQueue = [];
|
||||
|
||||
// reasonable default; will be overriden by server
|
||||
var retention_time = 2 * 60 * 60 * 1000;
|
||||
var strokeOpacity = 0.8;
|
||||
var fillOpacity = 0.35;
|
||||
var callsign_service;
|
||||
var aircraft_tracking_service;
|
||||
|
||||
var colorKeys = {};
|
||||
var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl');
|
||||
|
|
@ -114,7 +115,10 @@ $(function(){
|
|||
|
||||
switch (update.location.type) {
|
||||
case 'latlon':
|
||||
var pos = new google.maps.LatLng(update.location.lat, update.location.lon);
|
||||
var pos = false;
|
||||
if (update.location.lat && update.location.lon) {
|
||||
pos = new google.maps.LatLng(update.location.lat, update.location.lon);
|
||||
}
|
||||
var marker;
|
||||
var markerClass = google.maps.Marker;
|
||||
var aprsOptions = {}
|
||||
|
|
@ -123,25 +127,38 @@ $(function(){
|
|||
aprsOptions.symbol = update.location.symbol;
|
||||
aprsOptions.course = update.location.course;
|
||||
aprsOptions.speed = update.location.speed;
|
||||
} else if (update.source.icao || update.source.flight) {
|
||||
markerClass = PlaneMarker;
|
||||
aprsOptions = update.location;
|
||||
}
|
||||
if (markers[key]) {
|
||||
marker = markers[key];
|
||||
if (!pos) {
|
||||
delete markers[key];
|
||||
marker.setMap();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
marker = new markerClass();
|
||||
marker.addListener('click', function(){
|
||||
showMarkerInfoWindow(update.source, pos);
|
||||
});
|
||||
markers[key] = marker;
|
||||
if (pos) {
|
||||
marker = new markerClass();
|
||||
marker.addListener('click', function () {
|
||||
showMarkerInfoWindow(update.source, pos);
|
||||
});
|
||||
marker.setMap(map);
|
||||
markers[key] = marker;
|
||||
}
|
||||
}
|
||||
if (!marker) return;
|
||||
marker.setOptions($.extend({
|
||||
position: pos,
|
||||
map: map,
|
||||
title: sourceToString(update.source)
|
||||
}, aprsOptions, getMarkerOpacityOptions(update.lastseen) ));
|
||||
}, aprsOptions, getMarkerOpacityOptions(update.lastseen, update.location.ttl) ));
|
||||
marker.source = update.source;
|
||||
marker.lastseen = update.lastseen;
|
||||
marker.mode = update.mode;
|
||||
marker.band = update.band;
|
||||
marker.comment = update.location.comment;
|
||||
marker.ttl = update.location.ttl;
|
||||
|
||||
if (expectedCallsign && shallowEquals(expectedCallsign, update.source)) {
|
||||
map.panTo(pos);
|
||||
|
|
@ -149,6 +166,12 @@ $(function(){
|
|||
expectedCallsign = false;
|
||||
}
|
||||
|
||||
if (expectedIcao && expectedIcao === update.source.icao) {
|
||||
map.panTo(pos);
|
||||
showMarkerInfoWindow(update.source, pos);
|
||||
expectedIcao = false;
|
||||
}
|
||||
|
||||
if (infowindow && infowindow.source && shallowEquals(infowindow.source, update.source)) {
|
||||
showMarkerInfoWindow(infowindow.source, pos);
|
||||
}
|
||||
|
|
@ -176,6 +199,7 @@ $(function(){
|
|||
rectangle.mode = update.mode;
|
||||
rectangle.band = update.band;
|
||||
rectangle.center = center;
|
||||
rectangle.ttl = update.location.ttl;
|
||||
|
||||
rectangle.setOptions($.extend({
|
||||
strokeColor: color,
|
||||
|
|
@ -188,7 +212,7 @@ $(function(){
|
|||
west: lon,
|
||||
east: lon + 2
|
||||
}
|
||||
}, getRectangleOpacityOptions(update.lastseen) ));
|
||||
}, getRectangleOpacityOptions(update.lastseen, update.location.ttl) ));
|
||||
|
||||
if (expectedLocator && expectedLocator === update.location.locator) {
|
||||
map.panTo(center);
|
||||
|
|
@ -252,7 +276,10 @@ $(function(){
|
|||
nite.init(map);
|
||||
setInterval(function() { nite.refresh() }, 10000); // every 10s
|
||||
});
|
||||
$.getScript('static/lib/AprsMarker.js').done(function(){
|
||||
$.when(
|
||||
$.getScript('static/lib/AprsMarker.js'),
|
||||
$.getScript('static/lib/PlaneMarker.js')
|
||||
).done(function(){
|
||||
processUpdates(updateQueue);
|
||||
updateQueue = [];
|
||||
});
|
||||
|
|
@ -286,12 +313,12 @@ $(function(){
|
|||
title: config['receiver_name']
|
||||
});
|
||||
}
|
||||
if ('map_position_retention_time' in config) {
|
||||
retention_time = config.map_position_retention_time * 1000;
|
||||
}
|
||||
if ('callsign_service' in config) {
|
||||
callsign_service = config['callsign_service'];
|
||||
}
|
||||
if ('aircraft_tracking_service' in config) {
|
||||
aircraft_tracking_service = config['aircraft_tracking_service'];
|
||||
}
|
||||
break;
|
||||
case "update":
|
||||
processUpdates(json.value);
|
||||
|
|
@ -351,6 +378,8 @@ $(function(){
|
|||
// not just for display but also in key treatment in order not to overlap with other locations sent by the same callsign
|
||||
if ('item' in source) return source['item'];
|
||||
if ('object' in source) return source['object'];
|
||||
if ('icao' in source) return source['icao'];
|
||||
if ('flight' in source) return source['flight'];
|
||||
var key = source.callsign;
|
||||
if ('ssid' in source) key += '-' + source.ssid;
|
||||
return key;
|
||||
|
|
@ -359,7 +388,7 @@ $(function(){
|
|||
// we can reuse the same logic for displaying and indexing
|
||||
var sourceToString = sourceToKey;
|
||||
|
||||
var linkifySource = function(source) {
|
||||
var linkifyCallsign = function(source) {
|
||||
var callsignString = sourceToString(source);
|
||||
switch (callsign_service) {
|
||||
case "qrzcq":
|
||||
|
|
@ -374,6 +403,30 @@ $(function(){
|
|||
}
|
||||
};
|
||||
|
||||
var linkifyAircraft = function(source, identification) {
|
||||
var aircraftString = identification || source.flight || source.icao;
|
||||
var link = false;
|
||||
switch (aircraft_tracking_service) {
|
||||
case 'flightaware':
|
||||
if (source.icao) {
|
||||
link = 'https://flightaware.com/live/modes/' + source.icao;
|
||||
if (identification) link += "/ident/" + identification
|
||||
link += '/redirect';
|
||||
} else if (source.flight) {
|
||||
link = 'https://flightaware.com/live/flight/' + source.flight;
|
||||
}
|
||||
break;
|
||||
case 'planefinder':
|
||||
if (identification) link = 'https://planefinder.net/flight/' + identification;
|
||||
if (source.flight) link = 'https://planefinder.net/flight/' + source.flight;
|
||||
break;
|
||||
}
|
||||
if (link) {
|
||||
return '<a target="_blank" href="' + link + '">' + aircraftString + '</a>';
|
||||
}
|
||||
return aircraftString;
|
||||
}
|
||||
|
||||
var distanceKm = function(p1, p2) {
|
||||
// Earth radius in km
|
||||
var R = 6371.0;
|
||||
|
|
@ -408,7 +461,7 @@ $(function(){
|
|||
'<ul>' +
|
||||
inLocator.map(function(i){
|
||||
var timestring = moment(i.lastseen).fromNow();
|
||||
var message = linkifySource(i.source) + ' (' + timestring + ' using ' + i.mode;
|
||||
var message = linkifyCallsign(i.source) + ' (' + timestring + ' using ' + i.mode;
|
||||
if (i.band) message += ' on ' + i.band;
|
||||
message += ')';
|
||||
return '<li>' + message + '</li>'
|
||||
|
|
@ -432,8 +485,29 @@ $(function(){
|
|||
if (receiverMarker) {
|
||||
distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km";
|
||||
}
|
||||
var title;
|
||||
if (marker.source.icao || marker.source.flight) {
|
||||
title = linkifyAircraft(source, marker.identification);
|
||||
if ('altitude' in marker) {
|
||||
commentString += '<div>Altitude: ' + marker.altitude + ' ft</div>';
|
||||
}
|
||||
if ('groundspeed' in marker) {
|
||||
commentString += '<div>Speed: ' + Math.round(marker.groundspeed) + ' kt</div>';
|
||||
}
|
||||
if ('verticalspeed' in marker) {
|
||||
commentString += '<div>V/S: ' + marker.verticalspeed + ' ft/min</div>';
|
||||
}
|
||||
if ('IAS' in marker) {
|
||||
commentString += '<div>IAS: ' + marker.IAS + ' kt</div>';
|
||||
}
|
||||
if ('TAS' in marker) {
|
||||
commentString += '<div>TAS: ' + marker.TAS + ' kt</div>/';
|
||||
}
|
||||
} else {
|
||||
title = linkifyCallsign(source);
|
||||
}
|
||||
infowindow.setContent(
|
||||
'<h3>' + linkifySource(source) + distance + '</h3>' +
|
||||
'<h3>' + title + distance + '</h3>' +
|
||||
'<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' +
|
||||
commentString
|
||||
);
|
||||
|
|
@ -449,25 +523,25 @@ $(function(){
|
|||
infowindow.open(map, marker);
|
||||
};
|
||||
|
||||
var getScale = function(lastseen) {
|
||||
var getScale = function(lastseen, ttl) {
|
||||
var age = new Date().getTime() - lastseen;
|
||||
var scale = 1;
|
||||
if (age >= retention_time / 2) {
|
||||
scale = (retention_time - age) / (retention_time / 2);
|
||||
if (age >= ttl / 2) {
|
||||
scale = (ttl - age) / (ttl / 2);
|
||||
}
|
||||
return Math.max(0, Math.min(1, scale));
|
||||
};
|
||||
|
||||
var getRectangleOpacityOptions = function(lastseen) {
|
||||
var scale = getScale(lastseen);
|
||||
var getRectangleOpacityOptions = function(lastseen, ttl) {
|
||||
var scale = getScale(lastseen, ttl);
|
||||
return {
|
||||
strokeOpacity: strokeOpacity * scale,
|
||||
fillOpacity: fillOpacity * scale
|
||||
};
|
||||
};
|
||||
|
||||
var getMarkerOpacityOptions = function(lastseen) {
|
||||
var scale = getScale(lastseen);
|
||||
var getMarkerOpacityOptions = function(lastseen, ttl) {
|
||||
var scale = getScale(lastseen, ttl);
|
||||
return {
|
||||
opacity: scale
|
||||
};
|
||||
|
|
@ -478,21 +552,21 @@ $(function(){
|
|||
var now = new Date().getTime();
|
||||
Object.values(rectangles).forEach(function(m){
|
||||
var age = now - m.lastseen;
|
||||
if (age > retention_time) {
|
||||
if (age > m.ttl) {
|
||||
delete rectangles[sourceToKey(m.source)];
|
||||
m.setMap();
|
||||
return;
|
||||
}
|
||||
m.setOptions(getRectangleOpacityOptions(m.lastseen));
|
||||
m.setOptions(getRectangleOpacityOptions(m.lastseen, m.ttl));
|
||||
});
|
||||
Object.values(markers).forEach(function(m) {
|
||||
var age = now - m.lastseen;
|
||||
if (age > retention_time) {
|
||||
if (age > m.ttl) {
|
||||
delete markers[sourceToKey(m.source)];
|
||||
m.setMap();
|
||||
return;
|
||||
}
|
||||
m.setOptions(getMarkerOpacityOptions(m.lastseen));
|
||||
m.setOptions(getMarkerOpacityOptions(m.lastseen, m.ttl));
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
|
|
|
|||
|
|
@ -619,11 +619,7 @@ function get_relative_x(evt) {
|
|||
function canvas_mousewheel(evt) {
|
||||
if (!waterfall_setup_done) return;
|
||||
|
||||
var delta = -evt.deltaY;
|
||||
// deltaMode 0 means pixels instead of lines
|
||||
if ('deltaMode' in evt && evt.deltaMode === 0) {
|
||||
delta /= 50;
|
||||
}
|
||||
var delta = -wheelDelta(evt);
|
||||
var relativeX = get_relative_x(evt);
|
||||
zoom_step(delta, relativeX, zoom_center_where_calc(evt.pageX));
|
||||
evt.preventDefault();
|
||||
|
|
@ -792,6 +788,9 @@ function on_ws_recv(evt) {
|
|||
if ('tuning_precision' in config)
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel().setTuningPrecision(config['tuning_precision']);
|
||||
|
||||
if ('aircraft_tracking_service' in config)
|
||||
$('#openwebrx-panel-adsb-message').adsbMessagePanel().setAircraftTrackingService(config['aircraft_tracking_service']);
|
||||
|
||||
break;
|
||||
case "secondary_config":
|
||||
var s = json['value'];
|
||||
|
|
@ -860,12 +859,10 @@ function on_ws_recv(evt) {
|
|||
break;
|
||||
case 'secondary_demod':
|
||||
var value = json['value'];
|
||||
var panels = [
|
||||
$("#openwebrx-panel-wsjt-message").wsjtMessagePanel(),
|
||||
$('#openwebrx-panel-packet-message').packetMessagePanel(),
|
||||
$('#openwebrx-panel-pocsag-message').pocsagMessagePanel(),
|
||||
$("#openwebrx-panel-js8-message").js8()
|
||||
];
|
||||
var panels = ['wsjt', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2'].map(function(id) {
|
||||
return $('#openwebrx-panel-' + id + '-message')[id + 'MessagePanel']();
|
||||
});
|
||||
panels.push($('#openwebrx-panel-js8-message').js8());
|
||||
if (!panels.some(function(panel) {
|
||||
if (!panel.supportsMessage(value)) return false;
|
||||
panel.pushMessage(value);
|
||||
|
|
@ -1258,11 +1255,14 @@ function initSliders() {
|
|||
var $slider = $(this);
|
||||
if (!$slider.attr('step')) return;
|
||||
var val = Number($slider.val());
|
||||
// restore previous high-resolution mouse wheel delta
|
||||
var mouseDelta = Number($slider.data('mouseDelta'));
|
||||
if (mouseDelta) val += mouseDelta;
|
||||
var step = Number($slider.attr('step'));
|
||||
if (ev.originalEvent.deltaY > 0) {
|
||||
step *= -1;
|
||||
}
|
||||
$slider.val(val + step);
|
||||
var newVal = val + step * -wheelDelta(ev.originalEvent);
|
||||
$slider.val(newVal);
|
||||
// the calculated value can have a higher resolution than the element can store, so we put the delta into the data attributes
|
||||
$slider.data('mouseDelta', newVal - $slider.val());
|
||||
$slider.trigger('change');
|
||||
});
|
||||
|
||||
|
|
@ -1463,9 +1463,9 @@ function secondary_demod_init() {
|
|||
.mousedown(secondary_demod_canvas_container_mousedown)
|
||||
.mouseenter(secondary_demod_canvas_container_mousein)
|
||||
.mouseleave(secondary_demod_canvas_container_mouseleave);
|
||||
$('#openwebrx-panel-wsjt-message').wsjtMessagePanel();
|
||||
$('#openwebrx-panel-packet-message').packetMessagePanel();
|
||||
$('#openwebrx-panel-pocsag-message').pocsagMessagePanel();
|
||||
['wsjt', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl'].forEach(function(id){
|
||||
$('#openwebrx-panel-' + id + '-message')[id + 'MessagePanel']();
|
||||
})
|
||||
$('#openwebrx-panel-js8-message').js8();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from owrx.reporting import ReportingEngine
|
|||
from owrx.version import openwebrx_version
|
||||
from owrx.audio.queue import DecoderQueue
|
||||
from owrx.admin import add_admin_parser, run_admin_action
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import argparse
|
||||
import socket
|
||||
|
|
@ -44,6 +45,14 @@ def handleSignal(sig, frame):
|
|||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="OpenWebRX - Open Source SDR Web App for Everyone!")
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config",
|
||||
action="store",
|
||||
help="Read core configuration from specified file",
|
||||
metavar="configfile",
|
||||
type=Path,
|
||||
)
|
||||
parser.add_argument("-v", "--version", action="store_true", help="Show the software version")
|
||||
parser.add_argument("--debug", action="store_true", help="Set loglevel to DEBUG")
|
||||
|
||||
|
|
@ -66,6 +75,8 @@ def main():
|
|||
print("OpenWebRX version {version}".format(version=openwebrx_version))
|
||||
return 0
|
||||
|
||||
CoreConfig.load(args.config)
|
||||
|
||||
if args.module == "admin":
|
||||
return run_admin_action(adminparser, args)
|
||||
|
||||
|
|
@ -86,7 +97,8 @@ Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
|
|||
Documentation: https://github.com/jketterl/openwebrx/wiki
|
||||
Support and info: https://groups.io/g/openwebrx
|
||||
|
||||
"""
|
||||
""",
|
||||
flush=True
|
||||
)
|
||||
|
||||
logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version))
|
||||
|
|
@ -124,6 +136,7 @@ Support and info: https://groups.io/g/openwebrx
|
|||
|
||||
try:
|
||||
server = ThreadedHttpServer(coreConfig.get_web_port(), RequestHandler, coreConfig.get_web_ipv6())
|
||||
logger.info("Ready to serve requests.")
|
||||
server.serve_forever()
|
||||
except SignalException:
|
||||
pass
|
||||
|
|
|
|||
31
owrx/adsb/dump1090.py
Normal file
31
owrx/adsb/dump1090.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from pycsdr.modules import ExecModule
|
||||
from pycsdr.types import Format
|
||||
from csdr.module import LineBasedModule
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Dump1090Module(ExecModule):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
Format.COMPLEX_SHORT,
|
||||
Format.CHAR,
|
||||
["dump1090", "--ifile", "-", "--iformat", "SC16", "--raw"],
|
||||
# send some data on decoder shutdown since the dump1090 internal reader locks up otherwise
|
||||
# dump1090 reads chunks of 100ms, which equals to 240k samples at 2.4MS/s
|
||||
# some extra should not hurt
|
||||
flushSize=300000
|
||||
)
|
||||
|
||||
|
||||
class RawDeframer(LineBasedModule):
|
||||
def process(self, line: bytes):
|
||||
if line.startswith(b'*') and line.endswith(b';') and len(line) in [16, 30]:
|
||||
return bytes.fromhex(line[1:-1].decode())
|
||||
elif line == b"*0000;":
|
||||
# heartbeat message. not a valid message, but known. do not log.
|
||||
return
|
||||
else:
|
||||
logger.warning("invalid raw message: %s", line)
|
||||
392
owrx/adsb/modes.py
Normal file
392
owrx/adsb/modes.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
from csdr.module import PickleModule
|
||||
from math import sqrt, atan2, pi, floor, acos, cos
|
||||
from owrx.map import IncrementalUpdate, Location, Map, Source
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from owrx.aeronautical import AirplaneLocation, IcaoSource
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
|
||||
FEET_PER_METER = 3.28084
|
||||
|
||||
|
||||
class AdsbLocation(IncrementalUpdate, AirplaneLocation):
|
||||
mapKeys = [
|
||||
"lat",
|
||||
"lon",
|
||||
"altitude",
|
||||
"heading",
|
||||
"groundtrack",
|
||||
"groundspeed",
|
||||
"verticalspeed",
|
||||
"identification",
|
||||
"TAS",
|
||||
"IAS",
|
||||
"heading",
|
||||
]
|
||||
|
||||
def __init__(self, message):
|
||||
self.history = []
|
||||
self.timestamp = datetime.now()
|
||||
super().__init__(message)
|
||||
|
||||
def update(self, previousLocation: Location):
|
||||
if isinstance(previousLocation, AdsbLocation):
|
||||
history = previousLocation.history
|
||||
now = datetime.now()
|
||||
history = [p for p in history if now - p["timestamp"] < self.getTTL()]
|
||||
else:
|
||||
history = []
|
||||
|
||||
history += [{
|
||||
"timestamp": self.timestamp,
|
||||
"props": self.props,
|
||||
}]
|
||||
self.history = history
|
||||
|
||||
merged = {}
|
||||
for p in self.history:
|
||||
merged.update(p["props"])
|
||||
|
||||
self.props = merged
|
||||
if "lat" in merged:
|
||||
self.lat = merged["lat"]
|
||||
if "lon" in merged:
|
||||
self.lon = merged["lon"]
|
||||
|
||||
def getTTL(self) -> timedelta:
|
||||
# fixed ttl for adsb-locations for now
|
||||
return timedelta(seconds=30)
|
||||
|
||||
|
||||
class CprRecordType(Enum):
|
||||
AIR = ("air", 360)
|
||||
GROUND = ("ground", 90)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
name, baseAngle = args
|
||||
obj = object.__new__(cls)
|
||||
obj._value_ = name
|
||||
obj.baseAngle = baseAngle
|
||||
return obj
|
||||
|
||||
|
||||
class CprCache:
|
||||
def __init__(self):
|
||||
self.airRecords = {}
|
||||
self.groundRecords = {}
|
||||
|
||||
def __getRecords(self, cprType: CprRecordType):
|
||||
if cprType is CprRecordType.AIR:
|
||||
return self.airRecords
|
||||
elif cprType is CprRecordType.GROUND:
|
||||
return self.groundRecords
|
||||
|
||||
def getRecentData(self, icao: str, cprType: CprRecordType):
|
||||
records = self.__getRecords(cprType)
|
||||
if icao not in records:
|
||||
return []
|
||||
now = datetime.now()
|
||||
filtered = [r for r in records[icao] if now - r["timestamp"] < timedelta(seconds=10)]
|
||||
records_sorted = sorted(filtered, key=lambda r: r["timestamp"])
|
||||
records[icao] = records_sorted
|
||||
return [r["data"] for r in records_sorted]
|
||||
|
||||
def addRecord(self, icao: str, data: any, cprType: CprRecordType):
|
||||
records = self.__getRecords(cprType)
|
||||
if icao not in records:
|
||||
records[icao] = []
|
||||
records[icao].append({"timestamp": datetime.now(), "data": data})
|
||||
|
||||
|
||||
class ModeSParser(PickleModule):
|
||||
def __init__(self):
|
||||
self.cprCache = CprCache()
|
||||
name = "dump1090.decodes.adsb"
|
||||
self.metrics = Metrics.getSharedInstance().getMetric(name)
|
||||
if self.metrics is None:
|
||||
self.metrics = CounterMetric()
|
||||
Metrics.getSharedInstance().addMetric(name, self.metrics)
|
||||
super().__init__()
|
||||
|
||||
def process(self, input):
|
||||
format = (input[0] & 0b11111000) >> 3
|
||||
message = {
|
||||
"mode": "ADSB",
|
||||
"format": format
|
||||
}
|
||||
if format == 17:
|
||||
message["capability"] = input[0] & 0b111
|
||||
message["icao"] = icao = input[1:4].hex()
|
||||
type = (input[4] & 0b11111000) >> 3
|
||||
message["adsb_type"] = type
|
||||
|
||||
if type in [1, 2, 3, 4]:
|
||||
# identification message
|
||||
id = [
|
||||
(input[5] & 0b11111100) >> 2,
|
||||
((input[5] & 0b00000011) << 4) | ((input[6] & 0b11110000) >> 4),
|
||||
((input[6] & 0b00001111) << 2) | ((input[7] & 0b11000000) >> 6),
|
||||
input[7] & 0b00111111,
|
||||
(input[8] & 0b11111100) >> 2,
|
||||
((input[8] & 0b00000011) << 4) | ((input[9] & 0b11110000) >> 4),
|
||||
((input[9] & 0b00001111) << 2) | ((input[10] & 0b11000000) >> 6),
|
||||
input[10] & 0b00111111
|
||||
]
|
||||
|
||||
message["identification"] = bytes(b + (0x40 if b < 27 else 0) for b in id).decode("ascii").strip()
|
||||
|
||||
elif type in [5, 6, 7, 8]:
|
||||
# surface position
|
||||
# there's no altitude data in this message type, but the type implies the aircraft is on ground
|
||||
message["altitude"] = "ground"
|
||||
|
||||
movement = ((input[4] & 0b00000111) << 4) | ((input[5] & 0b11110000) >> 4)
|
||||
if movement == 1:
|
||||
message["groundspeed"] = 0
|
||||
elif 2 <= movement < 9:
|
||||
message["groundspeed"] = (movement - 1) * .0125
|
||||
elif 9 <= movement < 13:
|
||||
message["groundspeed"] = 1 + (movement - 8) * .25
|
||||
elif 13 <= movement < 39:
|
||||
message["groundspeed"] = 2 + (movement - 12) * .5
|
||||
elif 39 <= movement < 94:
|
||||
message["groundspeed"] = 15 + (movement - 38) # * 1
|
||||
elif 94 <= movement < 109:
|
||||
message["groundspeed"] = 70 + (movement - 108) * 2
|
||||
elif 109 <= movement < 124:
|
||||
message["groundspeeed"] = 100 + (movement - 123) * 5
|
||||
|
||||
if (input[5] & 0b00001000) >> 3:
|
||||
track = ((input[5] & 0b00000111) << 3) | ((input[6] & 0b11110000) >> 4)
|
||||
message["groundtrack"] = (360 * track) / 128
|
||||
|
||||
cpr = self.__getCprData(icao, input, CprRecordType.GROUND)
|
||||
if cpr is not None:
|
||||
lat, lon = cpr
|
||||
message["lat"] = lat
|
||||
message["lon"] = lon
|
||||
|
||||
elif type in [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]:
|
||||
# airborne position (w/ baro altitude)
|
||||
|
||||
cpr = self.__getCprData(icao, input, CprRecordType.AIR)
|
||||
if cpr is not None:
|
||||
lat, lon = cpr
|
||||
message["lat"] = lat
|
||||
message["lon"] = lon
|
||||
|
||||
q = (input[5] & 0b1)
|
||||
altitude = ((input[5] & 0b11111110) << 3) | ((input[6] & 0b11110000) >> 4)
|
||||
if q:
|
||||
message["altitude"] = altitude * 25 - 1000
|
||||
elif altitude > 0:
|
||||
altitude = self._gillhamDecode(altitude)
|
||||
if altitude is not None:
|
||||
message["altitude"] = altitude
|
||||
|
||||
elif type == 19:
|
||||
# airborne velocity
|
||||
subtype = input[4] & 0b111
|
||||
if subtype in [1, 2]:
|
||||
# velocity is reported in an east/west and a north/south component
|
||||
# vew = velocity east / west
|
||||
vew = ((input[5] & 0b00000011) << 8) | input[6]
|
||||
# vns = velocity north / south
|
||||
vns = ((input[7] & 0b01111111) << 3) | ((input[8] & 0b1110000000) >> 5)
|
||||
|
||||
# 0 means no data
|
||||
if vew != 0 and vns != 0:
|
||||
# dew = direction east/west (0 = to east, 1 = to west)
|
||||
dew = (input[5] & 0b00000100) >> 2
|
||||
# dns = direction north/south (0 = to north, 1 = to south)
|
||||
dns = (input[7] & 0b10000000) >> 7
|
||||
|
||||
vx = vew - 1
|
||||
if dew:
|
||||
vx *= -1
|
||||
vy = vns - 1
|
||||
if dns:
|
||||
vy *= -1
|
||||
# supersonic
|
||||
if subtype == 2:
|
||||
vx *= 4
|
||||
vy *= 4
|
||||
message["groundspeed"] = sqrt(vx ** 2 + vy ** 2)
|
||||
message["groundtrack"] = (atan2(vx, vy) * 360 / (2 * pi)) % 360
|
||||
|
||||
# vertical rate
|
||||
vr = ((input[8] & 0b00000111) << 6) | ((input[9] & 0b11111100) >> 2)
|
||||
if vr != 0:
|
||||
# vertical speed sign (1 = negative)
|
||||
svr = ((input[8] & 0b00001000) >> 3)
|
||||
# vertical speed
|
||||
vs = 64 * (vr - 1)
|
||||
if svr:
|
||||
vs *= -1
|
||||
message["verticalspeed"] = vs
|
||||
|
||||
elif subtype in [3, 4]:
|
||||
sh = (input[5] & 0b00000100) >> 2
|
||||
if sh:
|
||||
hdg = ((input[5] & 0b00000011) << 8) | input[6]
|
||||
message["heading"] = hdg * 360 / 1024
|
||||
airspeed = ((input[7] & 0b01111111) << 3) | ((input[8] & 0b11100000) >> 5)
|
||||
if airspeed != 0:
|
||||
airspeed -= 1
|
||||
# supersonic
|
||||
if subtype == 4:
|
||||
airspeed *= 4
|
||||
airspeed_type = (input[7] & 0b10000000) >> 7
|
||||
if airspeed_type:
|
||||
message["TAS"] = airspeed
|
||||
else:
|
||||
message["IAS"] = airspeed
|
||||
|
||||
elif type in [20, 21, 22]:
|
||||
# airborne position (w/GNSS height)
|
||||
|
||||
cpr = self.__getCprData(icao, input, CprRecordType.AIR)
|
||||
if cpr is not None:
|
||||
lat, lon = cpr
|
||||
message["lat"] = lat
|
||||
message["lon"] = lon
|
||||
|
||||
altitude = (input[5] << 4) | ((input[6] & 0b1111) >> 4)
|
||||
message["altitude"] = altitude * FEET_PER_METER
|
||||
|
||||
elif type == 28:
|
||||
# aircraft status
|
||||
pass
|
||||
|
||||
elif type == 29:
|
||||
# target state and status information
|
||||
pass
|
||||
|
||||
elif type == 31:
|
||||
# aircraft operation status
|
||||
pass
|
||||
|
||||
elif format == 11:
|
||||
# Mode-S All-call reply
|
||||
message["icao"] = input[1:4].hex()
|
||||
|
||||
self.metrics.inc()
|
||||
|
||||
if "icao" in message and AdsbLocation.mapKeys & message.keys():
|
||||
data = {k: message[k] for k in AdsbLocation.mapKeys if k in message}
|
||||
loc = AdsbLocation(data)
|
||||
Map.getSharedInstance().updateLocation(IcaoSource(message['icao']), loc, "ADS-B", None)
|
||||
|
||||
return message
|
||||
|
||||
def __getCprData(self, icao: str, input, cprType: CprRecordType):
|
||||
self.cprCache.addRecord(icao, {
|
||||
"cpr_format": (input[6] & 0b00000100) >> 2,
|
||||
"lat_cpr": ((input[6] & 0b00000011) << 15) | (input[7] << 7) | ((input[8] & 0b11111110) >> 1),
|
||||
"lon_cpr": ((input[8] & 0b00000001) << 16) | (input[9] << 8) | (input[10]),
|
||||
}, cprType)
|
||||
|
||||
records = self.cprCache.getRecentData(icao, cprType)
|
||||
|
||||
try:
|
||||
# records are sorted by timestamp, last should be newest
|
||||
odd = next(r for r in reversed(records) if r["cpr_format"])
|
||||
even = next(r for r in reversed(records) if not r["cpr_format"])
|
||||
newest = next(reversed(records))
|
||||
|
||||
lat_cpr_even = even["lat_cpr"] / 2 ** 17
|
||||
lat_cpr_odd = odd["lat_cpr"] / 2 ** 17
|
||||
|
||||
# latitude zone index
|
||||
j = floor(59 * lat_cpr_even - 60 * lat_cpr_odd + .5)
|
||||
|
||||
nz = 15
|
||||
d_lat_even = cprType.baseAngle / (4 * nz)
|
||||
d_lat_odd = cprType.baseAngle / (4 * nz - 1)
|
||||
|
||||
lat_even = d_lat_even * ((j % 60) + lat_cpr_even)
|
||||
lat_odd = d_lat_odd * ((j % 59) + lat_cpr_odd)
|
||||
|
||||
if lat_even >= 270:
|
||||
lat_even -= 360
|
||||
if lat_odd >= 270:
|
||||
lat_odd -= 360
|
||||
|
||||
def nl(lat):
|
||||
if lat == 0:
|
||||
return 59
|
||||
elif lat == 87:
|
||||
return 2
|
||||
elif lat == -87:
|
||||
return 2
|
||||
elif lat > 87:
|
||||
return 1
|
||||
elif lat < -87:
|
||||
return 1
|
||||
else:
|
||||
return floor((2 * pi) / acos(1 - (1 - cos(pi / (2 * nz))) / (cos((pi / 180) * abs(lat)) ** 2)))
|
||||
|
||||
if nl(lat_even) != nl(lat_odd):
|
||||
# latitude zone mismatch.
|
||||
return
|
||||
|
||||
lat = lat_odd if newest["cpr_format"] else lat_even
|
||||
|
||||
lon_cpr_even = even["lon_cpr"] / 2 ** 17
|
||||
lon_cpr_odd = odd["lon_cpr"] / 2 ** 17
|
||||
|
||||
# longitude zone index
|
||||
nl_lat = nl(lat)
|
||||
m = floor(lon_cpr_even * (nl_lat - 1) - lon_cpr_odd * nl_lat + .5)
|
||||
|
||||
n_even = max(nl_lat, 1)
|
||||
n_odd = max(nl_lat - 1, 1)
|
||||
|
||||
d_lon_even = cprType.baseAngle / n_even
|
||||
d_lon_odd = cprType.baseAngle / n_odd
|
||||
|
||||
lon_even = d_lon_even * (m % n_even + lon_cpr_even)
|
||||
lon_odd = d_lon_odd * (m % n_odd + lon_cpr_odd)
|
||||
|
||||
lon = lon_odd if newest["cpr_format"] else lon_even
|
||||
if lon >= 180:
|
||||
lon -= 360
|
||||
|
||||
return lat, lon
|
||||
|
||||
except StopIteration:
|
||||
# we don't have both CPR records. better luck next time.
|
||||
pass
|
||||
|
||||
def _grayDecode(self, input: int):
|
||||
l = input.bit_length()
|
||||
previous_bit = 0
|
||||
output = 0
|
||||
for i in reversed(range(0, l)):
|
||||
bit = (previous_bit ^ ((input >> i) & 1))
|
||||
output |= bit << i
|
||||
previous_bit = bit
|
||||
return output
|
||||
|
||||
gianniTable = [None, -200, 0, -100, 200, None, 100, None]
|
||||
|
||||
def _gillhamDecode(self, input: int):
|
||||
c = ((input & 0b10000000000) >> 8) | ((input & 0b00100000000) >> 7) | ((input & 0b00001000000) >> 6)
|
||||
b = ((input & 0b00000010000) >> 2) | ((input & 0b00000001000) >> 2) | ((input & 0b00000000010) >> 1)
|
||||
a = ((input & 0b01000000000) >> 7) | ((input & 0b00010000000) >> 6) | ((input & 0b00000100000) >> 5)
|
||||
d = ((input & 0b00000000100) >> 1) | (input & 0b00000000001)
|
||||
|
||||
dab = (d << 6) | (a << 3) | b
|
||||
parity = dab.bit_count() % 2
|
||||
|
||||
offset = self.gianniTable[c]
|
||||
|
||||
if offset is None:
|
||||
# invalid decode...
|
||||
return None
|
||||
|
||||
if parity:
|
||||
offset *= -1
|
||||
|
||||
altitude = self._grayDecode(dab) * 500 + offset - 1000
|
||||
return altitude
|
||||
89
owrx/aeronautical.py
Normal file
89
owrx/aeronautical.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
from owrx.map import Map, LatLngLocation, Source
|
||||
from csdr.module import JsonParser
|
||||
from abc import ABCMeta
|
||||
import re
|
||||
|
||||
|
||||
class AirplaneLocation(LatLngLocation):
|
||||
def __init__(self, message):
|
||||
self.props = message
|
||||
if "lat" in message and "lon" in message:
|
||||
super().__init__(message["lat"], message["lon"])
|
||||
else:
|
||||
self.lat = None
|
||||
self.lon = None
|
||||
|
||||
def __dict__(self):
|
||||
res = super().__dict__()
|
||||
res.update(self.props)
|
||||
return res
|
||||
|
||||
|
||||
class IcaoSource(Source):
|
||||
def __init__(self, icao: str, flight: str = None):
|
||||
self.icao = icao.upper()
|
||||
self.flight = flight
|
||||
|
||||
def getKey(self) -> str:
|
||||
return "icao:{}".format(self.icao)
|
||||
|
||||
def __dict__(self):
|
||||
d = {"icao": self.icao}
|
||||
if self.flight is not None:
|
||||
d["flight"] = self.flight
|
||||
return d
|
||||
|
||||
|
||||
class FlightSource(Source):
|
||||
def __init__(self, flight):
|
||||
self.flight = flight
|
||||
|
||||
def getKey(self) -> str:
|
||||
return "flight:{}".format(self.flight)
|
||||
|
||||
def __dict__(self):
|
||||
return {"flight": self.flight}
|
||||
|
||||
|
||||
class AcarsProcessor(JsonParser, metaclass=ABCMeta):
|
||||
flightRegex = re.compile("^([0-9A-Z]{2})0*([0-9A-Z]+$)")
|
||||
|
||||
def processAcars(self, acars: dict, icao: str = None):
|
||||
if "flight" in acars:
|
||||
flight_id = self.processFlight(acars["flight"])
|
||||
elif "reg" in acars:
|
||||
flight_id = acars['reg'].lstrip(".")
|
||||
else:
|
||||
return
|
||||
|
||||
if "arinc622" in acars:
|
||||
arinc622 = acars["arinc622"]
|
||||
if "adsc" in arinc622:
|
||||
adsc = arinc622["adsc"]
|
||||
if "tags" in adsc and adsc["tags"]:
|
||||
msg = {}
|
||||
for tag in adsc["tags"]:
|
||||
if "basic_report" in tag:
|
||||
basic_report = tag["basic_report"]
|
||||
msg.update({
|
||||
"lat": basic_report["lat"],
|
||||
"lon": basic_report["lon"],
|
||||
"altitude": basic_report["alt"],
|
||||
})
|
||||
if "earth_ref_data" in tag:
|
||||
earth_ref_data = tag["earth_ref_data"]
|
||||
msg.update({
|
||||
"groundtrack": earth_ref_data["true_trk_deg"],
|
||||
"groundspeed": earth_ref_data["gnd_spd_kts"],
|
||||
"verticalspeed": earth_ref_data["vspd_ftmin"],
|
||||
})
|
||||
if icao is not None:
|
||||
source = IcaoSource(icao, flight=flight_id)
|
||||
else:
|
||||
source = FlightSource(flight_id)
|
||||
Map.getSharedInstance().updateLocation(
|
||||
source, AirplaneLocation(msg), "ACARS over {}".format(self.mode)
|
||||
)
|
||||
|
||||
def processFlight(self, raw):
|
||||
return self.flightRegex.sub(r"\g<1>\g<2>", raw)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from owrx.map import Map, LatLngLocation
|
||||
from owrx.map import Map, LatLngLocation, Source
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from owrx.bands import Bandplan
|
||||
from datetime import datetime, timezone
|
||||
|
|
@ -155,6 +155,20 @@ class AprsLocation(LatLngLocation):
|
|||
return res
|
||||
|
||||
|
||||
class AprsSource(Source):
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
|
||||
def getKey(self) -> str:
|
||||
callsign = self.source["callsign"]
|
||||
if "ssid" in self.source:
|
||||
callsign += "-{}".format(self.source["ssid"])
|
||||
return "aprs:{}".format(callsign)
|
||||
|
||||
def __dict__(self):
|
||||
return self.source
|
||||
|
||||
|
||||
class AprsParser(PickleModule):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
|
@ -215,7 +229,7 @@ class AprsParser(PickleModule):
|
|||
source["item"] = mapData["item"]
|
||||
elif mapData["type"] == "object" and "object" in mapData:
|
||||
source["object"] = mapData["object"]
|
||||
Map.getSharedInstance().updateLocation(source, loc, "APRS", self.band)
|
||||
Map.getSharedInstance().updateLocation(AprsSource(source), loc, "APRS", self.band)
|
||||
|
||||
def hasCompressedCoordinates(self, raw):
|
||||
return raw[0] == "/" or raw[0] == "\\"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import random
|
||||
from pycsdr.types import Format
|
||||
from pycsdr.modules import Writer, TcpSource, ExecModule, Buffer
|
||||
from csdr.module import LogReader
|
||||
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,67 @@ IGLOGIN {callsign} {password}
|
|||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class DirewolfModule(ExecModule, DirewolfConfigSubscriber):
|
||||
def __init__(self, service: bool = False):
|
||||
self.tcpSource = None
|
||||
self.writer = None
|
||||
self.service = service
|
||||
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()
|
||||
|
||||
super().__init__(Format.SHORT, Format.CHAR, ["direwolf", "-c", self.direwolfConfigPath, "-r", "48000", "-t", "0", "-q", "d", "-q", "h"])
|
||||
# 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
|
||||
buffer = Buffer(Format.CHAR)
|
||||
self.logReader = LogReader(__name__, buffer)
|
||||
super().setWriter(buffer)
|
||||
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()
|
||||
self.logReader.stop()
|
||||
self.logReader = None
|
||||
os.unlink(self.direwolfConfigPath)
|
||||
self.direwolfConfig.unwire(self)
|
||||
self.direwolfConfig = None
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
from csdr.module import AutoStartModule
|
||||
from pycsdr.types import Format
|
||||
from pycsdr.modules import Writer, TcpSource
|
||||
from subprocess import Popen, PIPE
|
||||
from owrx.aprs.direwolf import DirewolfConfig, DirewolfConfigSubscriber
|
||||
from owrx.config.core import CoreConfig
|
||||
import threading
|
||||
import time
|
||||
import os
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DirewolfModule(AutoStartModule, DirewolfConfigSubscriber):
|
||||
def __init__(self, service: bool = False):
|
||||
self.process = None
|
||||
self.tcpSource = None
|
||||
self.service = service
|
||||
self.direwolfConfigPath = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
|
||||
tmp_dir=CoreConfig().get_temporary_directory(), myid=id(self)
|
||||
)
|
||||
self.direwolfConfig = None
|
||||
super().__init__()
|
||||
|
||||
def setWriter(self, writer: Writer) -> None:
|
||||
super().setWriter(writer)
|
||||
if self.tcpSource is not None:
|
||||
self.tcpSource.setWriter(writer)
|
||||
|
||||
def getInputFormat(self) -> Format:
|
||||
return Format.SHORT
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.CHAR
|
||||
|
||||
def start(self):
|
||||
self.direwolfConfig = DirewolfConfig()
|
||||
self.direwolfConfig.wire(self)
|
||||
file = open(self.direwolfConfigPath, "w")
|
||||
file.write(self.direwolfConfig.getConfig(self.service))
|
||||
file.close()
|
||||
|
||||
# direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h 1>&2
|
||||
self.process = Popen(
|
||||
["direwolf", "-c", self.direwolfConfigPath, "-r", "48000", "-t", "0", "-q", "d", "-q", "h"],
|
||||
start_new_session=True,
|
||||
stdin=PIPE,
|
||||
)
|
||||
|
||||
# resume in case the reader has been stop()ed before
|
||||
self.reader.resume()
|
||||
threading.Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
|
||||
|
||||
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 stop(self):
|
||||
if self.process is not None:
|
||||
self.process.terminate()
|
||||
self.process.wait()
|
||||
self.process = None
|
||||
os.unlink(self.direwolfConfigPath)
|
||||
self.direwolfConfig.unwire(self)
|
||||
self.direwolfConfig = None
|
||||
self.reader.stop()
|
||||
|
||||
def onConfigChanged(self):
|
||||
self.stop()
|
||||
self.start()
|
||||
|
|
@ -29,6 +29,10 @@ class WaveFile(object):
|
|||
self.waveFile.setsampwidth(2)
|
||||
self.waveFile.setframerate(12000)
|
||||
|
||||
def __del__(self):
|
||||
if self.waveFile is not None:
|
||||
logger.warning("WaveFile going out of scope but not unlinked!")
|
||||
|
||||
def close(self):
|
||||
self.waveFile.close()
|
||||
|
||||
|
|
@ -77,14 +81,18 @@ class AudioWriter(object):
|
|||
def _scheduleNextSwitch(self):
|
||||
self.cancelTimer()
|
||||
delta = self.getNextDecodingTime() - datetime.utcnow()
|
||||
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
|
||||
self.timer = threading.Timer(delta.total_seconds(), self._switchFiles)
|
||||
self.timer.start()
|
||||
|
||||
def switchFiles(self):
|
||||
def _switchFiles(self):
|
||||
with self.switchingLock:
|
||||
file = self.wavefile
|
||||
self.wavefile = self.getWaveFile()
|
||||
|
||||
if file is None:
|
||||
logger.warning("switchfiles() with no wave file. sequencing problem?")
|
||||
return
|
||||
|
||||
file.close()
|
||||
tmp_dir = CoreConfig().get_temporary_directory()
|
||||
|
||||
|
|
@ -117,6 +125,8 @@ class AudioWriter(object):
|
|||
self._scheduleNextSwitch()
|
||||
|
||||
def start(self):
|
||||
if self.wavefile is not None:
|
||||
logger.warning("wavefile is not none on startup, sequencing problem?")
|
||||
self.wavefile = self.getWaveFile()
|
||||
self._scheduleNextSwitch()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
from owrx.config import ConfigError
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
|
||||
class CoreConfig(object):
|
||||
defaultSearchLocations = ["./openwebrx.conf", "/etc/openwebrx/openwebrx.conf"]
|
||||
|
||||
defaults = {
|
||||
"core": {
|
||||
"data_directory": "/var/lib/openwebrx",
|
||||
|
|
@ -20,18 +22,41 @@ class CoreConfig(object):
|
|||
}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
sharedConfig = None
|
||||
|
||||
@staticmethod
|
||||
def load(file: Path = None):
|
||||
|
||||
def expand_base(base: Path):
|
||||
# check if config exists
|
||||
if not base.exists() or not base.is_file():
|
||||
return []
|
||||
# every location can additionally have a directory containing config overrides
|
||||
# this directory must have the same name, with the ".d" suffix
|
||||
override_dir = Path(str(base) + ".d")
|
||||
# check if override dir exists
|
||||
if not override_dir.exists() or not override_dir.is_dir():
|
||||
return [base]
|
||||
# load all .conf files from the override dir
|
||||
overrides = override_dir.glob("*.conf")
|
||||
return [base] + [o for o in overrides if o.is_file()]
|
||||
|
||||
if file is None:
|
||||
bases = [Path(b) for b in CoreConfig.defaultSearchLocations]
|
||||
else:
|
||||
bases = [file]
|
||||
configFiles = [o for b in bases for o in expand_base(b)]
|
||||
|
||||
config = ConfigParser()
|
||||
# set up config defaults
|
||||
config.read_dict(CoreConfig.defaults)
|
||||
# check for overrides
|
||||
overrides_dir = "/etc/openwebrx/openwebrx.conf.d"
|
||||
if os.path.exists(overrides_dir) and os.path.isdir(overrides_dir):
|
||||
overrides = glob(overrides_dir + "/*.conf")
|
||||
else:
|
||||
overrides = []
|
||||
# sequence things together
|
||||
config.read(["./openwebrx.conf", "/etc/openwebrx/openwebrx.conf"] + overrides)
|
||||
# read the allocated files
|
||||
config.read(configFiles)
|
||||
|
||||
CoreConfig.sharedConfig = config
|
||||
|
||||
def __init__(self):
|
||||
config = CoreConfig.sharedConfig
|
||||
self.data_directory = config.get("core", "data_directory")
|
||||
CoreConfig.checkDirectory(self.data_directory, "data_directory")
|
||||
self.temporary_directory = config.get("core", "temporary_directory")
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
|
|||
"fft_compression",
|
||||
"max_clients",
|
||||
"tuning_precision",
|
||||
"aircraft_tracking_service",
|
||||
]
|
||||
|
||||
def __init__(self, conn):
|
||||
|
|
@ -461,6 +462,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
|
|||
res["bandpass"] = {"low_cut": m.bandpass.low_cut, "high_cut": m.bandpass.high_cut}
|
||||
if isinstance(m, DigitalMode):
|
||||
res["underlying"] = m.underlying
|
||||
res["secondaryFft"] = m.secondaryFft
|
||||
return res
|
||||
|
||||
self.send({"type": "modes", "value": [to_json(m) for m in modes]})
|
||||
|
|
@ -474,11 +476,11 @@ class MapConnection(OpenWebRxClient):
|
|||
filtered_config = pm.filter(
|
||||
"google_maps_api_key",
|
||||
"receiver_gps",
|
||||
"map_position_retention_time",
|
||||
"callsign_service",
|
||||
"aircraft_tracking_service",
|
||||
"receiver_name",
|
||||
)
|
||||
filtered_config.wire(self.write_config)
|
||||
self.configSub = filtered_config.wire(self.write_config)
|
||||
|
||||
self.write_config(filtered_config.__dict__())
|
||||
|
||||
|
|
@ -489,6 +491,7 @@ class MapConnection(OpenWebRxClient):
|
|||
|
||||
def close(self, error: bool = False):
|
||||
Map.getSharedInstance().removeClient(self)
|
||||
self.configSub.cancel()
|
||||
super().close(error)
|
||||
|
||||
def write_config(self, cfg):
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
|
|||
profiles = {
|
||||
"receiver.js": [
|
||||
"lib/chroma.min.js",
|
||||
"lib/wheelDelta.js",
|
||||
"openwebrx.js",
|
||||
"lib/jquery-3.2.1.min.js",
|
||||
"lib/jquery.nanoscroller.min.js",
|
||||
|
|
|
|||
|
|
@ -183,6 +183,17 @@ class GeneralSettingsController(SettingsFormController):
|
|||
Option("aprsfi", "aprs.fi"),
|
||||
],
|
||||
),
|
||||
DropdownInput(
|
||||
"aircraft_tracking_service",
|
||||
"Aircraft tracking service",
|
||||
infotext="Allows users to navigate to an external flight tracking service by clicking on flight "
|
||||
+ "numbers",
|
||||
options=[
|
||||
Option(None, "disabled"),
|
||||
Option("flightaware", "FlightAware"),
|
||||
Option("planefinder", "planefinder"),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
|||
93
owrx/dsp.py
93
owrx/dsp.py
|
|
@ -1,5 +1,5 @@
|
|||
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
|
||||
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
|
||||
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator, PropertyDeleted, PropertyDeletion
|
||||
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
|
||||
from owrx.modes import Modes, DigitalMode
|
||||
from csdr.chain import Chain
|
||||
|
|
@ -40,7 +40,6 @@ class ClientDemodulatorChain(Chain):
|
|||
self.hdOutputRate = hdOutputRate
|
||||
self.secondaryDspEventReceiver = secondaryDspEventReceiver
|
||||
self.selector = Selector(sampleRate, outputRate)
|
||||
self.selector.setBandpass(-4000, 4000)
|
||||
self.selectorBuffer = Buffer(Format.COMPLEX_FLOAT)
|
||||
self.audioBuffer = None
|
||||
self.demodulator = demod
|
||||
|
|
@ -97,8 +96,6 @@ class ClientDemodulatorChain(Chain):
|
|||
# it's expected and should be mended when swapping out the demodulator in the next step
|
||||
pass
|
||||
|
||||
self.replace(1, demodulator)
|
||||
|
||||
if self.demodulator is not None:
|
||||
self.demodulator.stop()
|
||||
|
||||
|
|
@ -107,7 +104,6 @@ class ClientDemodulatorChain(Chain):
|
|||
self.selector.setOutputRate(self._getSelectorOutputRate())
|
||||
|
||||
clientRate = self._getClientAudioInputRate()
|
||||
self.clientAudioChain.setInputRate(clientRate)
|
||||
self.demodulator.setSampleRate(clientRate)
|
||||
|
||||
if isinstance(self.demodulator, DeemphasisTauChain):
|
||||
|
|
@ -116,12 +112,15 @@ class ClientDemodulatorChain(Chain):
|
|||
self._updateDialFrequency()
|
||||
self._syncSquelch()
|
||||
|
||||
outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
|
||||
self.clientAudioChain.setClientRate(outputRate)
|
||||
|
||||
if self.metaWriter is not None and isinstance(demodulator, MetaProvider):
|
||||
demodulator.setMetaWriter(self.metaWriter)
|
||||
|
||||
self.replace(1, demodulator)
|
||||
|
||||
self.clientAudioChain.setInputRate(clientRate)
|
||||
outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
|
||||
self.clientAudioChain.setClientRate(outputRate)
|
||||
|
||||
def stopDemodulator(self):
|
||||
if self.demodulator is None:
|
||||
return
|
||||
|
|
@ -135,6 +134,8 @@ class ClientDemodulatorChain(Chain):
|
|||
self.demodulator.stop()
|
||||
self.demodulator = None
|
||||
|
||||
self.setSecondaryDemodulator(None)
|
||||
|
||||
def _getSelectorOutputRate(self):
|
||||
if isinstance(self.demodulator, FixedIfSampleRateChain):
|
||||
return self.demodulator.getFixedIfSampleRate()
|
||||
|
|
@ -167,7 +168,8 @@ class ClientDemodulatorChain(Chain):
|
|||
|
||||
clientRate = self._getClientAudioInputRate()
|
||||
self.clientAudioChain.setInputRate(clientRate)
|
||||
self.demodulator.setSampleRate(clientRate)
|
||||
if self.demodulator is not None:
|
||||
self.demodulator.setSampleRate(clientRate)
|
||||
|
||||
self._updateDialFrequency()
|
||||
self._syncSquelch()
|
||||
|
|
@ -193,11 +195,11 @@ class ClientDemodulatorChain(Chain):
|
|||
self.secondaryDemodulator.setReader(self.audioBuffer.getReader())
|
||||
self.secondaryDemodulator.setWriter(self.secondaryWriter)
|
||||
|
||||
if self.secondaryDemodulator is None and self.secondaryFftChain is not None:
|
||||
if (self.secondaryDemodulator is None or not self.secondaryDemodulator.isSecondaryFftShown()) and self.secondaryFftChain is not None:
|
||||
self.secondaryFftChain.stop()
|
||||
self.secondaryFftChain = None
|
||||
|
||||
if self.secondaryDemodulator is not None and self.secondaryFftChain is None:
|
||||
if (self.secondaryDemodulator is not None and self.secondaryDemodulator.isSecondaryFftShown()) and self.secondaryFftChain is None:
|
||||
self._createSecondaryFftChain()
|
||||
|
||||
if self.secondaryFftChain is not None:
|
||||
|
|
@ -212,15 +214,15 @@ class ClientDemodulatorChain(Chain):
|
|||
self.secondaryFftChain.setWriter(self.secondaryFftWriter)
|
||||
|
||||
def _syncSquelch(self):
|
||||
if not self.demodulator.supportsSquelch() or (self.secondaryDemodulator is not None and not self.secondaryDemodulator.supportsSquelch()):
|
||||
if self.demodulator is not None and not self.demodulator.supportsSquelch() or (self.secondaryDemodulator is not None and not self.secondaryDemodulator.supportsSquelch()):
|
||||
self.selector.setSquelchLevel(-150)
|
||||
else:
|
||||
self.selector.setSquelchLevel(self.squelchLevel)
|
||||
|
||||
def setLowCut(self, lowCut):
|
||||
def setLowCut(self, lowCut: Union[float, None]):
|
||||
self.selector.setLowCut(lowCut)
|
||||
|
||||
def setHighCut(self, highCut):
|
||||
def setHighCut(self, highCut: Union[float, None]):
|
||||
self.selector.setHighCut(highCut)
|
||||
|
||||
def setBandpass(self, lowCut, highCut):
|
||||
|
|
@ -366,7 +368,7 @@ class ClientDemodulatorChain(Chain):
|
|||
def getSecondaryFftOutputFormat(self) -> Format:
|
||||
if self.secondaryFftCompression == "adpcm":
|
||||
return Format.CHAR
|
||||
return Format.SHORT
|
||||
return Format.FLOAT
|
||||
|
||||
def setWfmDeemphasisTau(self, tau: float) -> None:
|
||||
if tau == self.wfmDeemphasisTau:
|
||||
|
|
@ -459,6 +461,10 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
if mode.bandpass:
|
||||
bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut]
|
||||
self.chain.setBandpass(*bpf)
|
||||
self.props["low_cut"] = mode.bandpass.low_cut
|
||||
self.props["high_cut"] = mode.bandpass.high_cut
|
||||
else:
|
||||
self.chain.setBandpass(None, None)
|
||||
else:
|
||||
# TODO modes should be mandatory
|
||||
self.setDemodulator(self.props["start_mod"])
|
||||
|
|
@ -470,7 +476,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
|
||||
self.subscriptions = [
|
||||
self.props.wireProperty("audio_compression", self.setAudioCompression),
|
||||
self.props.wireProperty("fft_compression", self.chain.setSecondaryFftCompression),
|
||||
self.props.wireProperty("fft_compression", self.setSecondaryFftCompression),
|
||||
self.props.wireProperty("fft_voverlap_factor", self.chain.setSecondaryFftOverlapFactor),
|
||||
self.props.wireProperty("fft_fps", self.chain.setSecondaryFftFps),
|
||||
self.props.wireProperty("digimodes_fft_size", self.setSecondaryFftSize),
|
||||
|
|
@ -480,8 +486,8 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
self.props.wireProperty("offset_freq", self.chain.setFrequencyOffset),
|
||||
self.props.wireProperty("center_freq", self.chain.setCenterFrequency),
|
||||
self.props.wireProperty("squelch_level", self.chain.setSquelchLevel),
|
||||
self.props.wireProperty("low_cut", self.chain.setLowCut),
|
||||
self.props.wireProperty("high_cut", self.chain.setHighCut),
|
||||
self.props.wireProperty("low_cut", self.setLowCut),
|
||||
self.props.wireProperty("high_cut", self.setHighCut),
|
||||
self.props.wireProperty("mod", self.setDemodulator),
|
||||
self.props.wireProperty("dmr_filter", self.chain.setSlotFilter),
|
||||
self.props.wireProperty("wfm_deemphasis_tau", self.chain.setWfmDeemphasisTau),
|
||||
|
|
@ -554,6 +560,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
elif demod == "freedv":
|
||||
from csdr.chain.freedv import FreeDV
|
||||
return FreeDV()
|
||||
elif demod == "empty":
|
||||
from csdr.chain.analog import Empty
|
||||
return Empty()
|
||||
|
||||
def setDemodulator(self, mod):
|
||||
self.chain.stopDemodulator()
|
||||
|
|
@ -600,6 +609,27 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
elif mod == "bpsk63":
|
||||
from csdr.chain.digimodes import PskDemodulator
|
||||
return PskDemodulator(62.5)
|
||||
elif mod == "rtty170":
|
||||
from csdr.chain.digimodes import RttyDemodulator
|
||||
return RttyDemodulator(45.45, 170)
|
||||
elif mod == "rtty450":
|
||||
from csdr.chain.digimodes import RttyDemodulator
|
||||
return RttyDemodulator(50, 450, invert=True)
|
||||
elif mod == "rtty85":
|
||||
from csdr.chain.digimodes import RttyDemodulator
|
||||
return RttyDemodulator(50, 85, invert=True)
|
||||
elif mod == "adsb":
|
||||
from csdr.chain.dump1090 import Dump1090
|
||||
return Dump1090()
|
||||
elif mod == "ism":
|
||||
from csdr.chain.rtl433 import Rtl433
|
||||
return Rtl433()
|
||||
elif mod == "hfdl":
|
||||
from csdr.chain.dumphfdl import DumpHFDL
|
||||
return DumpHFDL()
|
||||
elif mod == "vdl2":
|
||||
from csdr.chain.dumpvdl2 import DumpVDL2
|
||||
return DumpVDL2()
|
||||
|
||||
def setSecondaryDemodulator(self, mod):
|
||||
demodulator = self._getSecondaryDemodulator(mod)
|
||||
|
|
@ -617,6 +647,23 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
self.chain.setWriter(buffer)
|
||||
self.wireOutput(self.audioOutput, buffer)
|
||||
|
||||
def setSecondaryFftCompression(self, compression):
|
||||
try:
|
||||
self.chain.setSecondaryFftCompression(compression)
|
||||
except ValueError:
|
||||
# wrong output format... need to re-wire
|
||||
pass
|
||||
|
||||
buffer = Buffer(self.chain.getSecondaryFftOutputFormat())
|
||||
self.chain.setSecondaryFftWriter(buffer)
|
||||
self.wireOutput("secondary_fft", buffer)
|
||||
|
||||
def setLowCut(self, lowCut: Union[float, PropertyDeletion]):
|
||||
self.chain.setLowCut(None if lowCut is PropertyDeleted else lowCut)
|
||||
|
||||
def setHighCut(self, highCut: Union[float, PropertyDeletion]):
|
||||
self.chain.setHighCut(None if highCut is PropertyDeleted else highCut)
|
||||
|
||||
def start(self):
|
||||
if self.sdrSource.isAvailable():
|
||||
self.chain.setReader(self.sdrSource.getBuffer().getReader())
|
||||
|
|
@ -652,7 +699,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
b = data.tobytes()
|
||||
# If we know it's not pickled, let us not unpickle
|
||||
if len(b) < 2 or b[0] != 0x80 or not 3 <= b[1] <= pickle.HIGHEST_PROTOCOL:
|
||||
callback(b.decode("ascii"))
|
||||
callback(b.decode("ascii", errors="replace"))
|
||||
return
|
||||
|
||||
io = BytesIO(b)
|
||||
|
|
@ -662,7 +709,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
except EOFError:
|
||||
pass
|
||||
except pickle.UnpicklingError:
|
||||
callback(b.decode("ascii"))
|
||||
callback(b.decode("ascii", errors="replace"))
|
||||
|
||||
return unpickler
|
||||
|
||||
|
|
@ -685,7 +732,11 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
self.setProperty(k, v)
|
||||
|
||||
def setProperty(self, prop, value):
|
||||
self.localProps[prop] = value
|
||||
if value is None:
|
||||
if prop in self.localProps:
|
||||
del self.localProps[prop]
|
||||
else:
|
||||
self.localProps[prop] = value
|
||||
|
||||
def getClientClass(self) -> SdrClientClass:
|
||||
return SdrClientClass.USER
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from datetime import datetime, timedelta
|
|||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class UnknownFeatureException(Exception):
|
||||
|
|
@ -62,6 +61,7 @@ class FeatureDetector(object):
|
|||
"perseussdr": ["perseustest", "nmux"],
|
||||
"airspy": ["soapy_connector", "soapy_airspy"],
|
||||
"airspyhf": ["soapy_connector", "soapy_airspyhf"],
|
||||
"afedri": ["soapy_connector", "soapy_afedri"],
|
||||
"lime_sdr": ["soapy_connector", "soapy_lime_sdr"],
|
||||
"fifi_sdr": ["alsa", "rockprog", "nmux"],
|
||||
"pluto_sdr": ["soapy_connector", "soapy_pluto_sdr"],
|
||||
|
|
@ -76,7 +76,7 @@ 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"],
|
||||
|
|
@ -85,6 +85,10 @@ class FeatureDetector(object):
|
|||
"pocsag": ["digiham"],
|
||||
"js8call": ["js8", "js8py"],
|
||||
"drm": ["dream"],
|
||||
"dump1090": ["dump1090"],
|
||||
"ism": ["rtl_433"],
|
||||
"dumphfdl": ["dumphfdl"],
|
||||
"dumpvdl2": ["dumpvdl2"],
|
||||
}
|
||||
|
||||
def feature_availability(self):
|
||||
|
|
@ -137,14 +141,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
|
||||
|
|
@ -167,7 +169,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:
|
||||
|
|
@ -336,6 +345,14 @@ class FeatureDetector(object):
|
|||
"""
|
||||
return self._has_soapy_driver("airspyhf")
|
||||
|
||||
def has_soapy_afedri(self):
|
||||
"""
|
||||
The SoapyAfedri module allows using Afedri SDR-Net devices with SoapySDR.
|
||||
|
||||
You can get it [here](https://github.com/alexander-sholohov/SoapyAfedri).
|
||||
"""
|
||||
return self._has_soapy_driver("afedri")
|
||||
|
||||
def has_soapy_lime_sdr(self):
|
||||
"""
|
||||
The Lime Suite installs - amongst others - a Soapy driver for the LimeSDR device series.
|
||||
|
|
@ -406,7 +423,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):
|
||||
"""
|
||||
|
|
@ -550,6 +567,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 codecserer instance.
|
||||
"""
|
||||
|
||||
config = Config.get()
|
||||
|
|
@ -564,3 +584,55 @@ 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")
|
||||
|
|
|
|||
101
owrx/hfdl/dumphfdl.py
Normal file
101
owrx/hfdl/dumphfdl.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
from pycsdr.modules import ExecModule
|
||||
from pycsdr.types import Format
|
||||
from owrx.aeronautical import AirplaneLocation, AcarsProcessor, IcaoSource, FlightSource
|
||||
from owrx.map import Map
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpHFDLModule(ExecModule):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
Format.COMPLEX_FLOAT,
|
||||
Format.CHAR,
|
||||
[
|
||||
"dumphfdl",
|
||||
"--iq-file", "-",
|
||||
"--sample-format", "CF32",
|
||||
"--sample-rate", "12000",
|
||||
"--output", "decoded:json:file:path=-",
|
||||
"0",
|
||||
],
|
||||
flushSize=50000,
|
||||
)
|
||||
|
||||
|
||||
class HFDLMessageParser(AcarsProcessor):
|
||||
def __init__(self):
|
||||
name = "dumphfdl.decodes.hfdl"
|
||||
self.metrics = Metrics.getSharedInstance().getMetric(name)
|
||||
if self.metrics is None:
|
||||
self.metrics = CounterMetric()
|
||||
Metrics.getSharedInstance().addMetric(name, self.metrics)
|
||||
super().__init__("HFDL")
|
||||
|
||||
def process(self, line):
|
||||
msg = super().process(line)
|
||||
if msg is not None:
|
||||
try:
|
||||
payload = msg["hfdl"]
|
||||
if "lpdu" in payload:
|
||||
lpdu = payload["lpdu"]
|
||||
icao = lpdu["src"]["ac_info"]["icao"] if "ac_info" in lpdu["src"] else None
|
||||
if lpdu["type"]["id"] in [13, 29]:
|
||||
hfnpdu = lpdu["hfnpdu"]
|
||||
if hfnpdu["type"]["id"] == 209:
|
||||
# performance data
|
||||
self.processPosition(hfnpdu, icao)
|
||||
elif hfnpdu["type"]["id"] == 255:
|
||||
# enveloped data
|
||||
if "acars" in hfnpdu:
|
||||
self.processAcars(hfnpdu["acars"], icao)
|
||||
elif lpdu["type"]["id"] in [79, 143, 191]:
|
||||
if "ac_info" in lpdu:
|
||||
icao = lpdu["ac_info"]["icao"]
|
||||
self.processPosition(lpdu["hfnpdu"], icao)
|
||||
except Exception:
|
||||
logger.exception("error processing HFDL data")
|
||||
|
||||
self.metrics.inc()
|
||||
|
||||
return msg
|
||||
|
||||
def processPosition(self, hfnpdu, icao=None):
|
||||
if "pos" in hfnpdu:
|
||||
pos = hfnpdu["pos"]
|
||||
if abs(pos['lat']) <= 90 and abs(pos['lon']) <= 180:
|
||||
flight = self.processFlight(hfnpdu["flight_id"])
|
||||
|
||||
if icao is not None:
|
||||
source = IcaoSource(icao, flight=flight)
|
||||
elif flight:
|
||||
source = FlightSource(flight)
|
||||
else:
|
||||
source = None
|
||||
|
||||
if source:
|
||||
msg = {
|
||||
"lat": pos["lat"],
|
||||
"lon": pos["lon"],
|
||||
"flight": flight
|
||||
}
|
||||
if "utc_time" in hfnpdu:
|
||||
ts = self.processTimestamp(**hfnpdu["utc_time"])
|
||||
elif "time" in hfnpdu:
|
||||
ts = self.processTimestamp(**hfnpdu["time"])
|
||||
else:
|
||||
ts = None
|
||||
Map.getSharedInstance().updateLocation(source, AirplaneLocation(msg), "HFDL", timestamp=ts)
|
||||
|
||||
def processTimestamp(self, hour, min, sec) -> datetime:
|
||||
now = datetime.now(timezone.utc)
|
||||
t = now.replace(hour=hour, minute=min, second=sec, microsecond=0)
|
||||
# if we have moved the time to the future, it's most likely that we're close to midnight and the time
|
||||
# we received actually refers to yesterday
|
||||
if t > now:
|
||||
t -= timedelta(days=1)
|
||||
return t
|
||||
13
owrx/ism/rtl433.py
Normal file
13
owrx/ism/rtl433.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from pycsdr.modules import ExecModule
|
||||
from pycsdr.types import Format
|
||||
|
||||
|
||||
class Rtl433Module(ExecModule):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
Format.COMPLEX_FLOAT,
|
||||
Format.CHAR,
|
||||
["rtl_433", "-r", "cf32:-", "-F", "json", "-M", "time:unix", "-C", "si"]
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ from owrx.audio.chopper import AudioChopperParser
|
|||
import re
|
||||
from js8py import Js8
|
||||
from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound
|
||||
from owrx.map import Map, LocatorLocation
|
||||
from owrx.map import Map, LocatorLocation, CallsignSource
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from owrx.config import Config
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
|
@ -103,7 +103,7 @@ class Js8Parser(AudioChopperParser):
|
|||
|
||||
if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid:
|
||||
Map.getSharedInstance().updateLocation(
|
||||
frame.source, LocatorLocation(frame.grid), "JS8", band
|
||||
CallsignSource(**frame.source), LocatorLocation(frame.grid), "JS8", band
|
||||
)
|
||||
ReportingEngine.getSharedInstance().spot(
|
||||
{
|
||||
|
|
|
|||
90
owrx/map.py
90
owrx/map.py
|
|
@ -1,6 +1,7 @@
|
|||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from owrx.config import Config
|
||||
from owrx.bands import Band
|
||||
from abc import abstractmethod, ABC, ABCMeta
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
|
|
@ -8,10 +9,24 @@ import sys
|
|||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class Location(object):
|
||||
def getTTL(self) -> timedelta:
|
||||
pm = Config.get()
|
||||
return timedelta(seconds=pm["map_position_retention_time"])
|
||||
|
||||
def __dict__(self):
|
||||
return {
|
||||
"ttl": self.getTTL().total_seconds() * 1000
|
||||
}
|
||||
|
||||
|
||||
class Source(ABC):
|
||||
@abstractmethod
|
||||
def getKey(self) -> str:
|
||||
pass
|
||||
|
||||
def __dict__(self):
|
||||
return {}
|
||||
|
||||
|
|
@ -61,7 +76,7 @@ class Map(object):
|
|||
client.write_update(
|
||||
[
|
||||
{
|
||||
"source": record["source"],
|
||||
"source": record["source"].__dict__(),
|
||||
"location": record["location"].__dict__(),
|
||||
"lastseen": record["updated"].timestamp() * 1000,
|
||||
"mode": record["mode"],
|
||||
|
|
@ -77,36 +92,38 @@ class Map(object):
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
def _sourceToKey(self, source):
|
||||
if "ssid" in source:
|
||||
return "{callsign}-{ssid}".format(**source)
|
||||
return source["callsign"]
|
||||
|
||||
def updateLocation(self, source, loc: Location, mode: str, band: Band = None):
|
||||
ts = datetime.now()
|
||||
key = self._sourceToKey(source)
|
||||
def updateLocation(self, source: Source, loc: Location, mode: str, band: Band = None, timestamp: datetime = None):
|
||||
if timestamp is None:
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
else:
|
||||
# if we get an external timestamp, make sure it's not already expired
|
||||
if datetime.now(timezone.utc) - loc.getTTL() > timestamp:
|
||||
return
|
||||
key = source.getKey()
|
||||
with self.positionsLock:
|
||||
self.positions[key] = {"source": source, "location": loc, "updated": ts, "mode": mode, "band": band}
|
||||
if isinstance(loc, IncrementalUpdate) and key in self.positions:
|
||||
loc.update(self.positions[key]["location"])
|
||||
self.positions[key] = {"source": source, "location": loc, "updated": timestamp, "mode": mode, "band": band}
|
||||
self.broadcast(
|
||||
[
|
||||
{
|
||||
"source": source,
|
||||
"source": source.__dict__(),
|
||||
"location": loc.__dict__(),
|
||||
"lastseen": ts.timestamp() * 1000,
|
||||
"lastseen": timestamp.timestamp() * 1000,
|
||||
"mode": mode,
|
||||
"band": band.getName() if band is not None else None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def touchLocation(self, source):
|
||||
def touchLocation(self, source: Source):
|
||||
# not implemented on the client side yet, so do not use!
|
||||
ts = datetime.now()
|
||||
key = self._sourceToKey(source)
|
||||
ts = datetime.now(timezone.utc)
|
||||
key = source.getKey()
|
||||
with self.positionsLock:
|
||||
if key in self.positions:
|
||||
self.positions[key]["updated"] = ts
|
||||
self.broadcast([{"source": source, "lastseen": ts.timestamp() * 1000}])
|
||||
self.broadcast([{"source": source.__dict__(), "lastseen": ts.timestamp() * 1000}])
|
||||
|
||||
def removeLocation(self, key):
|
||||
with self.positionsLock:
|
||||
|
|
@ -114,11 +131,12 @@ class Map(object):
|
|||
# TODO broadcast removal to clients
|
||||
|
||||
def removeOldPositions(self):
|
||||
pm = Config.get()
|
||||
retention = timedelta(seconds=pm["map_position_retention_time"])
|
||||
cutoff = datetime.now() - retention
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
to_be_removed = [key for (key, pos) in self.positions.items() if pos["updated"] < cutoff]
|
||||
with self.positionsLock:
|
||||
to_be_removed = [
|
||||
key for (key, pos) in self.positions.items() if now - pos["location"].getTTL() > pos["updated"]
|
||||
]
|
||||
for key in to_be_removed:
|
||||
self.removeLocation(key)
|
||||
|
||||
|
|
@ -136,7 +154,10 @@ class LatLngLocation(Location):
|
|||
self.lon = lon
|
||||
|
||||
def __dict__(self):
|
||||
res = {"type": "latlon", "lat": self.lat, "lon": self.lon}
|
||||
res = super().__dict__()
|
||||
res.update(
|
||||
{"type": "latlon", "lat": self.lat, "lon": self.lon}
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
|
|
@ -145,4 +166,25 @@ class LocatorLocation(Location):
|
|||
self.locator = locator
|
||||
|
||||
def __dict__(self):
|
||||
return {"type": "locator", "locator": self.locator}
|
||||
res = super().__dict__()
|
||||
res.update(
|
||||
{"type": "locator", "locator": self.locator}
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
class IncrementalUpdate(Location, metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def update(self, previousLocation: Location):
|
||||
pass
|
||||
|
||||
|
||||
class CallsignSource(Source):
|
||||
def __init__(self, callsign: str):
|
||||
self.callsign = callsign
|
||||
|
||||
def getKey(self) -> str:
|
||||
return "callsign:{}".format(self.callsign)
|
||||
|
||||
def __dict__(self):
|
||||
return {"callsign": self.callsign}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from urllib.error import HTTPError
|
|||
from csdr.module import PickleModule
|
||||
from owrx.aprs import AprsParser, AprsLocation
|
||||
from owrx.config import Config
|
||||
from owrx.map import Map, LatLngLocation
|
||||
from owrx.map import Map, LatLngLocation, CallsignSource
|
||||
from owrx.bands import Bandplan
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -129,7 +129,7 @@ class DigihamEnricher(Enricher, metaclass=ABCMeta):
|
|||
callsign = self.getCallsign(meta)
|
||||
if callsign is not None and "lat" in meta and "lon" in meta:
|
||||
loc = LatLngLocation(meta["lat"], meta["lon"])
|
||||
Map.getSharedInstance().updateLocation({"callsign": callsign}, loc, mode, self.parser.getBand())
|
||||
Map.getSharedInstance().updateLocation(CallsignSource(callsign), loc, mode, self.parser.getBand())
|
||||
return meta
|
||||
|
||||
@abstractmethod
|
||||
|
|
@ -202,7 +202,7 @@ class DStarEnricher(DigihamEnricher):
|
|||
if "ourcall" in meta:
|
||||
# send location info to map as well (it will show up with the correct symbol there!)
|
||||
loc = AprsLocation(data)
|
||||
Map.getSharedInstance().updateLocation({"callsign": meta["ourcall"]}, loc, "DPRS", self.parser.getBand())
|
||||
Map.getSharedInstance().updateLocation(CallsignSource(meta["ourcall"]), loc, "DPRS", self.parser.getBand())
|
||||
except Exception:
|
||||
logger.exception("Error while parsing DPRS data")
|
||||
|
||||
|
|
|
|||
|
|
@ -33,19 +33,34 @@ class Mode:
|
|||
return self.modulation
|
||||
|
||||
|
||||
EmptyMode = Mode("empty", "Empty")
|
||||
|
||||
|
||||
class AnalogMode(Mode):
|
||||
pass
|
||||
|
||||
|
||||
class DigitalMode(Mode):
|
||||
def __init__(
|
||||
self, modulation, name, underlying, bandpass: Bandpass = None, requirements=None, service=False, squelch=True
|
||||
self,
|
||||
modulation,
|
||||
name,
|
||||
underlying,
|
||||
bandpass: Bandpass = None,
|
||||
requirements=None,
|
||||
service=False,
|
||||
squelch=True,
|
||||
secondaryFft=True
|
||||
):
|
||||
super().__init__(modulation, name, bandpass, requirements, service, squelch)
|
||||
self.underlying = underlying
|
||||
self.secondaryFft = secondaryFft
|
||||
|
||||
def get_underlying_mode(self):
|
||||
return Modes.findByModulation(self.underlying[0])
|
||||
mode = Modes.findByModulation(self.underlying[0])
|
||||
if mode is None:
|
||||
mode = EmptyMode
|
||||
return mode
|
||||
|
||||
def get_bandpass(self):
|
||||
if self.bandpass is not None:
|
||||
|
|
@ -106,19 +121,22 @@ class Modes(object):
|
|||
AnalogMode("lsb", "LSB", bandpass=Bandpass(-3000, -300)),
|
||||
AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)),
|
||||
AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)),
|
||||
AnalogMode("dmr", "DMR", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
|
||||
AnalogMode("dmr", "DMR", bandpass=Bandpass(-6250, 6250), requirements=["digital_voice_digiham"], squelch=False),
|
||||
AnalogMode(
|
||||
"dstar", "D-Star", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_digiham"], squelch=False
|
||||
),
|
||||
AnalogMode("nxdn", "NXDN", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_digiham"], squelch=False),
|
||||
AnalogMode("ysf", "YSF", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
|
||||
AnalogMode("m17", "M17", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_m17"], squelch=False),
|
||||
AnalogMode("ysf", "YSF", bandpass=Bandpass(-6250, 6250), requirements=["digital_voice_digiham"], squelch=False),
|
||||
AnalogMode("m17", "M17", bandpass=Bandpass(-6250, 6250), requirements=["digital_voice_m17"], squelch=False),
|
||||
AnalogMode(
|
||||
"freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False
|
||||
),
|
||||
AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
|
||||
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
|
||||
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
|
||||
DigitalMode("rtty170", "RTTY 45/170", underlying=["usb", "lsb"]),
|
||||
DigitalMode("rtty450", "RTTY 50N/450", underlying=["lsb", "usb"]),
|
||||
DigitalMode("rtty85", "RTTY 50N/85", underlying=["lsb", "usb"]),
|
||||
WsjtMode("ft8", "FT8"),
|
||||
WsjtMode("ft4", "FT4"),
|
||||
WsjtMode("jt65", "JT65"),
|
||||
|
|
@ -142,10 +160,46 @@ class Modes(object):
|
|||
"pocsag",
|
||||
"Pocsag",
|
||||
underlying=["nfm"],
|
||||
bandpass=Bandpass(-6000, 6000),
|
||||
bandpass=Bandpass(-6250, 6250),
|
||||
requirements=["pocsag"],
|
||||
squelch=False,
|
||||
),
|
||||
DigitalMode(
|
||||
"adsb",
|
||||
"ADS-B",
|
||||
underlying=["empty"],
|
||||
bandpass=None,
|
||||
requirements=["dump1090"],
|
||||
service=True,
|
||||
squelch=False,
|
||||
secondaryFft=False,
|
||||
),
|
||||
DigitalMode(
|
||||
"ism",
|
||||
"ISM",
|
||||
underlying=["empty"],
|
||||
bandpass=None,
|
||||
requirements=["ism"],
|
||||
squelch=False,
|
||||
),
|
||||
DigitalMode(
|
||||
"hfdl",
|
||||
"HFDL",
|
||||
underlying=["empty"],
|
||||
bandpass=Bandpass(0, 3000),
|
||||
requirements=["dumphfdl"],
|
||||
service=True,
|
||||
squelch=False,
|
||||
),
|
||||
DigitalMode(
|
||||
"vdl2",
|
||||
"VDL2",
|
||||
underlying=["empty"],
|
||||
bandpass=Bandpass(-12500, 12500),
|
||||
requirements=["dumpvdl2"],
|
||||
service=True,
|
||||
squelch=False,
|
||||
)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -267,12 +267,15 @@ class ServiceHandler(SdrSourceEventClient):
|
|||
secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation)
|
||||
center_freq = source.getProps()["center_freq"]
|
||||
sampleRate = source.getProps()["samp_rate"]
|
||||
bandpass = modeObject.get_bandpass()
|
||||
if isinstance(secondaryDemod, DialFrequencyReceiver):
|
||||
secondaryDemod.setDialFrequency(dial["frequency"])
|
||||
|
||||
chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, dial["frequency"] - center_freq)
|
||||
chain.setBandPass(bandpass.low_cut, bandpass.high_cut)
|
||||
bandpass = modeObject.get_bandpass()
|
||||
if bandpass:
|
||||
chain.setBandPass(bandpass.low_cut, bandpass.high_cut)
|
||||
else:
|
||||
chain.setBandPass(None, None)
|
||||
chain.setReader(source.getBuffer().getReader())
|
||||
|
||||
# dummy buffer, we don't use the output right now
|
||||
|
|
@ -310,6 +313,15 @@ class ServiceHandler(SdrSourceEventClient):
|
|||
elif mod == "packet":
|
||||
from csdr.chain.digimodes import PacketDemodulator
|
||||
return PacketDemodulator(service=True)
|
||||
elif mod == "adsb":
|
||||
from csdr.chain.dump1090 import Dump1090
|
||||
return Dump1090()
|
||||
elif mod == "hfdl":
|
||||
from csdr.chain.dumphfdl import DumpHFDL
|
||||
return DumpHFDL()
|
||||
elif mod == "vdl2":
|
||||
from csdr.chain.dumpvdl2 import DumpVDL2
|
||||
return DumpVDL2()
|
||||
|
||||
raise ValueError("unsupported service modulation: {}".format(mod))
|
||||
|
||||
|
|
|
|||
125
owrx/source/afedri.py
Normal file
125
owrx/source/afedri.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import re
|
||||
from ipaddress import IPv4Address, AddressValueError
|
||||
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
|
||||
from owrx.form.input import Input, CheckboxInput, DropdownInput, Option
|
||||
from owrx.form.input.device import TextInput
|
||||
from owrx.form.input.validator import Validator, ValidationError
|
||||
from typing import List
|
||||
|
||||
|
||||
AFEDRI_DEVICE_KEYS = ["rx_mode"]
|
||||
AFEDRI_PROFILE_KEYS = ["r820t_lna_agc", "r820t_mixer_agc"]
|
||||
|
||||
|
||||
class IPv4AndPortValidator(Validator):
|
||||
def validate(self, key, value) -> None:
|
||||
parts = value.split(":")
|
||||
if len(parts) != 2:
|
||||
raise ValidationError(key, "Wrong format. Expected IPv4:port")
|
||||
|
||||
try:
|
||||
IPv4Address(parts[0])
|
||||
except AddressValueError as e:
|
||||
raise ValidationError(key, "IP address error: {}".format(str(e)))
|
||||
|
||||
try:
|
||||
port = int(parts[1])
|
||||
except ValueError:
|
||||
raise ValidationError(key, "Port number invalid")
|
||||
if not 0 <= port <= 65535:
|
||||
raise ValidationError(key, "Port number out of range")
|
||||
|
||||
|
||||
class AfedriAddressPortInput(TextInput):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"afedri_adress_port",
|
||||
"Afedri IP and Port",
|
||||
infotext="Afedri IP and port to connect to. Format = IPv4:Port",
|
||||
validator=IPv4AndPortValidator(),
|
||||
)
|
||||
|
||||
|
||||
class AfedriSource(SoapyConnectorSource):
|
||||
def getSoapySettingsMappings(self):
|
||||
mappings = super().getSoapySettingsMappings()
|
||||
mappings.update({x: x for x in AFEDRI_PROFILE_KEYS})
|
||||
return mappings
|
||||
|
||||
def getEventNames(self):
|
||||
return super().getEventNames() + ["afedri_adress_port"] + AFEDRI_DEVICE_KEYS
|
||||
|
||||
def getDriver(self):
|
||||
return "afedri"
|
||||
|
||||
def buildSoapyDeviceParameters(self, parsed, values):
|
||||
params = super().buildSoapyDeviceParameters(parsed, values)
|
||||
|
||||
address, port = values["afedri_adress_port"].split(":")
|
||||
params += [{"address": address, "port": port}]
|
||||
|
||||
can_be_set_at_start = AFEDRI_DEVICE_KEYS
|
||||
for elm in can_be_set_at_start:
|
||||
if elm in values:
|
||||
params += [{elm: values[elm]}]
|
||||
|
||||
return params
|
||||
|
||||
|
||||
class AfedriDeviceDescription(SoapyConnectorDeviceDescription):
|
||||
def getName(self):
|
||||
return "Afedri device"
|
||||
|
||||
def supportsPpm(self):
|
||||
# not currently mapped, and it's unclear how this should be sent to the device
|
||||
return False
|
||||
|
||||
def hasAgc(self):
|
||||
# not currently mapped
|
||||
return False
|
||||
|
||||
def getInputs(self) -> List[Input]:
|
||||
return super().getInputs() + [
|
||||
AfedriAddressPortInput(),
|
||||
CheckboxInput(
|
||||
"r820t_lna_agc",
|
||||
"Enable R820T LNA AGC",
|
||||
),
|
||||
CheckboxInput(
|
||||
"r820t_mixer_agc",
|
||||
"Enable R820T Mixer AGC",
|
||||
),
|
||||
DropdownInput(
|
||||
"rx_mode",
|
||||
"Switch the device to a specific RX mode at start",
|
||||
options=[
|
||||
Option("0", "Single"),
|
||||
Option("1", "DualDiversity"),
|
||||
Option("2", "Dual"),
|
||||
Option("3", "DiversityInternal"),
|
||||
Option("4", "QuadDiversity"),
|
||||
Option("5", "Quad"),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
def getDeviceMandatoryKeys(self):
|
||||
return super().getDeviceMandatoryKeys() + ["afedri_adress_port"]
|
||||
|
||||
def getDeviceOptionalKeys(self):
|
||||
return super().getDeviceOptionalKeys() + AFEDRI_DEVICE_KEYS
|
||||
|
||||
def getProfileOptionalKeys(self):
|
||||
return super().getProfileOptionalKeys() + AFEDRI_PROFILE_KEYS
|
||||
|
||||
def getGainStages(self):
|
||||
return [
|
||||
"RF",
|
||||
"FE",
|
||||
"R820T_LNA_GAIN",
|
||||
"R820T_MIXER_GAIN",
|
||||
"R820T_VGA_GAIN",
|
||||
]
|
||||
|
||||
def getNumberOfChannels(self) -> int:
|
||||
return 4
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
|
||||
from owrx.command import Option
|
||||
from owrx.command import Option, Flag
|
||||
from owrx.form.error import ValidationError
|
||||
from owrx.form.input import Input, NumberInput, TextInput
|
||||
from owrx.form.input import Input, NumberInput, TextInput, CheckboxInput
|
||||
from owrx.form.input.validator import RangeValidator
|
||||
from typing import List
|
||||
|
||||
# In order to use an HPSDR radio, you must install hpsdrconnector from https://github.com/jancona/hpsdrconnector
|
||||
# These are the command line options available:
|
||||
# --frequency uint
|
||||
# Tune to specified frequency in Hz (default 7100000)
|
||||
|
|
@ -17,10 +16,17 @@ from typing import List
|
|||
# Use the specified samplerate: one of 48000, 96000, 192000, 384000 (default 96000)
|
||||
# --debug
|
||||
# Emit debug log messages on stdout
|
||||
# --serverPort uint
|
||||
# Server port for this radio (default 7300)
|
||||
#
|
||||
# If you omit `remote` from config_webrx.py, hpsdrconnector will use the HPSDR discovery protocol
|
||||
# to find radios on your local network and will connect to the first radio it discovered.
|
||||
|
||||
# If a remote IP address is not set, the connector will use the HPSDR discovery protocol
|
||||
# to find radios on the local network and will connect to the first radio it discovers.
|
||||
# If there is more than one HPSDR radio on the network, the IP address of the desired radio
|
||||
# should always be specified.
|
||||
# To use multiple HPSDR radios, each radio should have its IP address and a unique server port
|
||||
# specfied. For example:
|
||||
# Radio 1: (Remote IP: 192.168.1.11, Server port: 7300)
|
||||
# Radio 2: (Remote IP: 192.168.1.22, Server port: 7301)
|
||||
|
||||
class HpsdrSource(ConnectorSource):
|
||||
def getCommandMapper(self):
|
||||
|
|
@ -34,6 +40,8 @@ class HpsdrSource(ConnectorSource):
|
|||
"samp_rate": Option("--samplerate"),
|
||||
"remote": Option("--radio"),
|
||||
"rf_gain": Option("--gain"),
|
||||
"server_port": Option("--serverPort"),
|
||||
"debug": Flag("--debug"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
@ -41,7 +49,12 @@ class HpsdrSource(ConnectorSource):
|
|||
class RemoteInput(TextInput):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"remote", "Remote IP", infotext="Remote IP address to connect to."
|
||||
"remote",
|
||||
"Remote IP",
|
||||
infotext=(
|
||||
"HPSDR radio IP address. If it is not set, the connector will connect to the first radio it discovers. "
|
||||
"If there is more than one HPSDR radio on the network, IP addresses of the desired radios should always be specified."
|
||||
)
|
||||
)
|
||||
|
||||
class HpsdrDeviceDescription(ConnectorDeviceDescription):
|
||||
|
|
@ -51,11 +64,26 @@ class HpsdrDeviceDescription(ConnectorDeviceDescription):
|
|||
def getInputs(self) -> List[Input]:
|
||||
return super().getInputs() + [
|
||||
RemoteInput(),
|
||||
NumberInput("rf_gain", "LNA Gain", "LNA gain between 0 (-12dB) and 60 (48dB)", validator=RangeValidator(0, 60)),
|
||||
]
|
||||
NumberInput(
|
||||
"rf_gain",
|
||||
"LNA Gain",
|
||||
"LNA gain between 0 (-12dB) and 60 (48dB) (default 20)",
|
||||
validator=RangeValidator(0, 60)
|
||||
),
|
||||
CheckboxInput(
|
||||
"debug",
|
||||
"Show connector debugging messages in the log"
|
||||
),
|
||||
NumberInput(
|
||||
"server_port",
|
||||
"Server port",
|
||||
("Radio server port (default 7300). When using multiple radios, each must be on a separate port, "
|
||||
"e.g. 7300 for the first, 7301 for the second.")
|
||||
),
|
||||
]
|
||||
|
||||
def getDeviceOptionalKeys(self):
|
||||
return list(filter(lambda x : x not in ["rtltcp_compat", "iqswap"], super().getDeviceOptionalKeys())) + ["remote"]
|
||||
return list(filter(lambda x : x not in ["rtltcp_compat", "iqswap"], super().getDeviceOptionalKeys())) + ["remote","debug","server_port"]
|
||||
|
||||
def getProfileOptionalKeys(self):
|
||||
return list(filter(lambda x : x != "iqswap", super().getProfileOptionalKeys()))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
|
||||
from owrx.command import Flag, Option, Argument
|
||||
from owrx.form.input import Input
|
||||
from owrx.form.input.device import RemoteInput
|
||||
from owrx.form.input.device import RemoteInput, DirectSamplingInput
|
||||
from typing import List
|
||||
|
||||
|
||||
|
|
@ -26,7 +26,13 @@ class RtlTcpDeviceDescription(ConnectorDeviceDescription):
|
|||
return "RTL-SDR device (via rtl_tcp)"
|
||||
|
||||
def getInputs(self) -> List[Input]:
|
||||
return super().getInputs() + [RemoteInput()]
|
||||
return super().getInputs() + [RemoteInput(), DirectSamplingInput()]
|
||||
|
||||
def getDeviceMandatoryKeys(self):
|
||||
return super().getDeviceMandatoryKeys() + ["remote"]
|
||||
|
||||
def getDeviceOptionalKeys(self):
|
||||
return super().getDeviceOptionalKeys() + ["direct_sampling"]
|
||||
|
||||
def getProfileOptionalKeys(self):
|
||||
return super().getProfileOptionalKeys() + ["direct_sampling"]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ from abc import ABCMeta, abstractmethod
|
|||
from owrx.command import Option
|
||||
from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription
|
||||
from typing import List
|
||||
from owrx.form.input import Input, TextInput
|
||||
from owrx.form.input import Input, NumberInput, TextInput
|
||||
from owrx.form.input.validator import RangeValidator
|
||||
from owrx.form.input.device import GainInput
|
||||
from owrx.soapy import SoapySettings
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
|
|||
{
|
||||
"antenna": Option("-a"),
|
||||
"soapy_settings": Option("-t"),
|
||||
"channel": Option("-n"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
@ -83,7 +85,7 @@ class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
|
|||
|
||||
class SoapyConnectorDeviceDescription(ConnectorDeviceDescription):
|
||||
def getInputs(self) -> List[Input]:
|
||||
return super().getInputs() + [
|
||||
inputs = super().getInputs() + [
|
||||
TextInput(
|
||||
"device",
|
||||
"Device identifier",
|
||||
|
|
@ -97,9 +99,25 @@ class SoapyConnectorDeviceDescription(ConnectorDeviceDescription):
|
|||
),
|
||||
TextInput("antenna", "Antenna"),
|
||||
]
|
||||
if self.getNumberOfChannels() > 1:
|
||||
inputs += [
|
||||
NumberInput(
|
||||
"channel",
|
||||
"Select SoapySDR Channel",
|
||||
validator=RangeValidator(0, self.getNumberOfChannels() - 1)
|
||||
)
|
||||
]
|
||||
return inputs
|
||||
|
||||
def getNumberOfChannels(self) -> int:
|
||||
"""
|
||||
can be overridden for sdr devices that have multiple channels. will allow the user to select a channel from
|
||||
the device selection screen if > 1
|
||||
"""
|
||||
return 1
|
||||
|
||||
def getDeviceOptionalKeys(self):
|
||||
return super().getDeviceOptionalKeys() + ["device", "rf_gain", "antenna"]
|
||||
return super().getDeviceOptionalKeys() + ["device", "rf_gain", "antenna", "channel"]
|
||||
|
||||
def getProfileOptionalKeys(self):
|
||||
return super().getProfileOptionalKeys() + ["antenna"]
|
||||
|
|
|
|||
116
owrx/vdl2/dumpvdl2.py
Normal file
116
owrx/vdl2/dumpvdl2.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from pycsdr.modules import ExecModule
|
||||
from pycsdr.types import Format
|
||||
from owrx.aeronautical import AcarsProcessor
|
||||
from owrx.map import Map
|
||||
from owrx.aeronautical import AirplaneLocation, IcaoSource
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from datetime import datetime, date, time, timezone
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DumpVDL2Module(ExecModule):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
Format.COMPLEX_SHORT,
|
||||
Format.CHAR,
|
||||
[
|
||||
"dumpvdl2",
|
||||
"--iq-file", "-",
|
||||
"--oversample", "1",
|
||||
"--sample-format", "S16_LE",
|
||||
"--output", "decoded:json:file:path=-",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class VDL2MessageParser(AcarsProcessor):
|
||||
def __init__(self):
|
||||
name = "dumpvdl2.decodes.vdl2"
|
||||
self.metrics = Metrics.getSharedInstance().getMetric(name)
|
||||
if self.metrics is None:
|
||||
self.metrics = CounterMetric()
|
||||
Metrics.getSharedInstance().addMetric(name, self.metrics)
|
||||
super().__init__("VDL2")
|
||||
|
||||
def process(self, line):
|
||||
msg = super().process(line)
|
||||
if msg is not None:
|
||||
try:
|
||||
payload = msg["vdl2"]
|
||||
if "avlc" in payload:
|
||||
avlc = payload["avlc"]
|
||||
src = avlc["src"]["addr"]
|
||||
if avlc["frame_type"] == "I":
|
||||
if "acars" in avlc:
|
||||
self.processAcars(avlc["acars"], icao=src)
|
||||
elif "x25" in avlc:
|
||||
x25 = avlc["x25"]
|
||||
if "clnp" in x25:
|
||||
clnp = x25["clnp"]
|
||||
if "cotp" in clnp:
|
||||
cotp = clnp["cotp"]
|
||||
if "adsc_v2" in cotp:
|
||||
adsc_v2 = cotp["adsc_v2"]
|
||||
if "adsc_report" in adsc_v2:
|
||||
adsc_report = adsc_v2["adsc_report"]
|
||||
data = adsc_report["data"]
|
||||
if "periodic_report" in data:
|
||||
report_data = data["periodic_report"]["report_data"]
|
||||
self.processReport(report_data, src)
|
||||
elif "event_report" in data:
|
||||
report_data = data["event_report"]["report_data"]
|
||||
self.processReport(report_data, src)
|
||||
except Exception:
|
||||
logger.exception("error processing VDL2 data")
|
||||
self.metrics.inc()
|
||||
return msg
|
||||
|
||||
def processReport(self, report, icao):
|
||||
if "position" not in report:
|
||||
return
|
||||
msg = {
|
||||
"lat": self.convertLatitude(**report["position"]["lat"]),
|
||||
"lon": self.convertLongitude(**report["position"]["lon"]),
|
||||
"altitude": report["position"]["alt"]["val"],
|
||||
}
|
||||
if "ground_vector" in report:
|
||||
msg.update({
|
||||
"groundtrack": report["ground_vector"]["ground_track"]["val"],
|
||||
"groundspeed": report["ground_vector"]["ground_speed"]["val"],
|
||||
})
|
||||
if "air_vector" in report:
|
||||
msg.update({
|
||||
"verticalspeed": report["air_vector"]["vertical_rate"]["val"],
|
||||
})
|
||||
if "timestamp" in report:
|
||||
timestamp = self.convertTimestamp(**report["timestamp"])
|
||||
else:
|
||||
timestamp = None
|
||||
Map.getSharedInstance().updateLocation(IcaoSource(icao), AirplaneLocation(msg), "VDL2", timestamp=timestamp)
|
||||
|
||||
def convertLatitude(self, dir, **args) -> float:
|
||||
coord = self.convertCoordinate(**args)
|
||||
if dir == "south":
|
||||
coord *= -1
|
||||
return coord
|
||||
|
||||
def convertLongitude(self, dir, **args) -> float:
|
||||
coord = self.convertCoordinate(**args)
|
||||
if dir == "west":
|
||||
coord *= -1
|
||||
return coord
|
||||
|
||||
def convertCoordinate(self, deg, min, sec) -> float:
|
||||
return deg + float(min) / 60 + float(sec) / 3600
|
||||
|
||||
def convertTimestamp(self, date, time):
|
||||
return datetime.combine(self.convertDate(**date), self.convertTime(**time), tzinfo=timezone.utc)
|
||||
|
||||
def convertDate(self, year, month, day):
|
||||
return date(year=year, month=month, day=day)
|
||||
|
||||
def convertTime(self, hour, min, sec):
|
||||
return time(hour=hour, minute=min, second=sec, microsecond=0, tzinfo=timezone.utc)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
from owrx.map import Map, LocatorLocation
|
||||
from owrx.map import Map, LocatorLocation, CallsignSource
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from owrx.reporting import ReportingEngine
|
||||
from owrx.audio import AudioChopperProfile, StaticProfileSource, ConfigWiredProfileSource
|
||||
|
|
@ -289,7 +289,7 @@ class WsjtParser(AudioChopperParser):
|
|||
self.pushDecode(mode, band)
|
||||
if "source" in out and "locator" in out:
|
||||
Map.getSharedInstance().updateLocation(
|
||||
out["source"], LocatorLocation(out["locator"]), mode, band
|
||||
CallsignSource(**out["source"]), LocatorLocation(out["locator"]), mode, band
|
||||
)
|
||||
ReportingEngine.getSharedInstance().spot(out)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue