├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── bands.json ├── csdr ├── __init__.py ├── chain │ ├── __init__.py │ ├── analog.py │ ├── clientaudio.py │ ├── dablin.py │ ├── demodulator.py │ ├── digiham.py │ ├── digimodes.py │ ├── drm.py │ ├── dummy.py │ ├── dump1090.py │ ├── dumphfdl.py │ ├── dumpvdl2.py │ ├── fft.py │ ├── freedv.py │ ├── m17.py │ ├── redsea.py │ ├── rtl433.py │ └── selector.py └── module │ ├── __init__.py │ ├── drm.py │ ├── freedv.py │ ├── m17.py │ └── msk144.py ├── debian ├── changelog ├── compat ├── control ├── openwebrx.config ├── openwebrx.desktop ├── openwebrx.dirs ├── openwebrx.install ├── openwebrx.postinst ├── openwebrx.postrm ├── openwebrx.svg ├── openwebrx.templates ├── rules └── source │ └── format ├── docker.sh ├── docker ├── Dockerfiles │ ├── Dockerfile-afedri │ ├── Dockerfile-airspy │ ├── Dockerfile-base │ ├── Dockerfile-bladerf │ ├── Dockerfile-fcdpp │ ├── Dockerfile-full │ ├── Dockerfile-hackrf │ ├── Dockerfile-hpsdr │ ├── Dockerfile-limesdr │ ├── Dockerfile-perseus │ ├── Dockerfile-plutosdr │ ├── Dockerfile-radioberry │ ├── Dockerfile-rtlsdr │ ├── Dockerfile-rtlsdr-soapy │ ├── Dockerfile-rtltcp │ ├── Dockerfile-runds │ ├── Dockerfile-sdrplay │ ├── Dockerfile-soapyremote │ ├── Dockerfile-soapysdr │ └── Dockerfile-uhd ├── files │ ├── direwolf │ │ └── direwolf-hamlib.patch │ ├── dream │ │ └── dream.patch │ ├── js8call │ │ └── js8call-hamlib.patch │ ├── sdrplay │ │ ├── install-lib.aarch64.patch │ │ ├── install-lib.armv7l.patch │ │ └── install-lib.x86_64.patch │ ├── services │ │ ├── codecserver │ │ │ └── run │ │ └── sdrplay │ │ │ └── run │ └── wsjtx │ │ ├── wsjtx-hamlib.patch │ │ └── wsjtx.patch └── scripts │ ├── install-connectors.sh │ ├── install-dependencies-afedri.sh │ ├── install-dependencies-airspy.sh │ ├── install-dependencies-bladerf.sh │ ├── install-dependencies-fcdpp.sh │ ├── install-dependencies-hackrf.sh │ ├── install-dependencies-hpsdr.sh │ ├── install-dependencies-limesdr.sh │ ├── install-dependencies-perseus.sh │ ├── install-dependencies-plutosdr.sh │ ├── install-dependencies-radioberry.sh │ ├── install-dependencies-rtlsdr-soapy.sh │ ├── install-dependencies-rtlsdr.sh │ ├── install-dependencies-runds.sh │ ├── install-dependencies-sdrplay.sh │ ├── install-dependencies-soapyremote.sh │ ├── install-dependencies-soapysdr.sh │ ├── install-dependencies-uhd.sh │ ├── install-dependencies.sh │ ├── install-owrx-tools.sh │ └── run.sh ├── htdocs ├── __init__.py ├── apple-touch-icon.png ├── css │ ├── admin.css │ ├── bootstrap.min.css │ ├── login.css │ ├── map.css │ ├── openwebrx-globals.css │ ├── openwebrx-header.css │ └── openwebrx.css ├── favicon.ico ├── features.html ├── features.js ├── fonts │ ├── RobotoMono-Regular.ttf │ ├── RobotoMono-Regular.woff │ └── RobotoMono-Regular.woff2 ├── gfx │ ├── favicon128.png │ ├── favicon32.png │ ├── favicon44.png │ ├── favicon64.png │ ├── favicon96.png │ ├── openwebrx-avatar.png │ ├── openwebrx-background-cool-blue.png │ ├── openwebrx-background-cool-blue.webp │ ├── openwebrx-directcall.svg │ ├── openwebrx-groupcall.svg │ ├── openwebrx-scale-background.png │ ├── openwebrx-top-photo.jpg │ └── svg-defs.svg ├── include │ └── header.include.html ├── index.html ├── lib │ ├── AprsMarker.js │ ├── AudioEngine.js │ ├── AudioProcessor.js │ ├── BookmarkBar.js │ ├── BookmarkDialog.js │ ├── BookmarkLocalStorage.js │ ├── Demodulator.js │ ├── DemodulatorPanel.js │ ├── FrequencyDisplay.js │ ├── Header.js │ ├── Js8Threads.js │ ├── Measurement.js │ ├── MessagePanel.js │ ├── MetaPanel.js │ ├── Modes.js │ ├── PlaneMarker.js │ ├── ProgressBar.js │ ├── bootstrap.bundle.min.js │ ├── chroma.min.js │ ├── jquery-3.2.1.min.js │ ├── jquery.nanoscroller.min.js │ ├── location-picker.min.js │ ├── nanoscroller.css │ ├── nite-overlay.js │ ├── settings │ │ ├── BookmarkTable.js │ │ ├── ExponentialInput.js │ │ ├── GainInput.js │ │ ├── ImageUpload.js │ │ ├── LogMessages.js │ │ ├── MapInput.js │ │ ├── OptionalSection.js │ │ ├── SchedulerInput.js │ │ ├── WaterfallDropdown.js │ │ └── WsjtDecodingDepthsInput.js │ └── wheelDelta.js ├── login.html ├── map.html ├── map.js ├── mstile-144x144.png ├── openwebrx.js ├── pwchange.html ├── settings.html ├── settings.js └── settings │ ├── bookmarks.html │ └── general.html ├── inkscape files ├── favicon.svg ├── google_maps_pin.svg ├── openwebrx-bookmark.svg ├── openwebrx-directcall.svg ├── openwebrx-edit.svg ├── openwebrx-groupcall.svg ├── openwebrx-logo.svg ├── openwebrx-mute.svg ├── openwebrx-panel-log.svg ├── openwebrx-panel-map.svg ├── openwebrx-panel-receiver.svg ├── openwebrx-panel-settings.svg ├── openwebrx-panel-status.svg ├── openwebrx-play-button.svg ├── openwebrx-rx-details-arrow-down.svg ├── openwebrx-rx-details-arrow-up.svg ├── openwebrx-speake-mutedr.svg ├── openwebrx-speaker.svg ├── openwebrx-squelch.svg ├── openwebrx-trashcan.svg ├── openwebrx-waterfall-auto.svg ├── openwebrx-waterfall-continuous.svg ├── openwebrx-waterfall-default.svg ├── openwebrx-zoom-in-total.svg ├── openwebrx-zoom-in.svg ├── openwebrx-zoom-out-total.svg └── openwebrx-zoom-out.svg ├── openwebrx.conf ├── openwebrx.py ├── owrx ├── __main__.py ├── admin │ ├── __init__.py │ └── commands.py ├── adsb │ ├── dump1090.py │ └── modes.py ├── aeronautical.py ├── aprs │ ├── __init__.py │ ├── direwolf.py │ └── kiss.py ├── audio │ ├── __init__.py │ ├── chopper.py │ ├── queue.py │ └── wav.py ├── bands.py ├── bookmarks.py ├── breadcrumb.py ├── client.py ├── command.py ├── config │ ├── __init__.py │ ├── classic.py │ ├── commands.py │ ├── core.py │ ├── defaults.py │ ├── dynamic.py │ ├── error.py │ └── migration.py ├── connection.py ├── controllers │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── assets.py │ ├── feature.py │ ├── imageupload.py │ ├── metrics.py │ ├── profile.py │ ├── receiverid.py │ ├── robots.py │ ├── session.py │ ├── settings │ │ ├── __init__.py │ │ ├── backgrounddecoding.py │ │ ├── bookmarks.py │ │ ├── decoding.py │ │ ├── general.py │ │ ├── reporting.py │ │ └── sdr.py │ ├── status.py │ ├── template.py │ └── websocket.py ├── cpu.py ├── dab │ └── dablin.py ├── details.py ├── dsp.py ├── feature.py ├── fft.py ├── form │ ├── __init__.py │ ├── error.py │ ├── input │ │ ├── __init__.py │ │ ├── aprs.py │ │ ├── converter.py │ │ ├── device.py │ │ ├── gfx.py │ │ ├── location.py │ │ ├── receiverid.py │ │ ├── validator.py │ │ ├── wfm.py │ │ └── wsjt.py │ └── section.py ├── hfdl │ └── dumphfdl.py ├── http.py ├── ism │ └── rtl433.py ├── js8.py ├── jsons.py ├── locator.py ├── log │ └── __init__.py ├── map.py ├── meta.py ├── metrics.py ├── modes.py ├── pocsag.py ├── property │ ├── __init__.py │ ├── filter.py │ └── validators.py ├── rds │ └── redsea.py ├── receiverid.py ├── reporting │ ├── __init__.py │ ├── mqtt.py │ ├── pskreporter.py │ ├── reporter.py │ └── wsprnet.py ├── sdr.py ├── service │ ├── __init__.py │ ├── chain.py │ └── schedule.py ├── soapy.py ├── socket.py ├── source │ ├── __init__.py │ ├── afedri.py │ ├── airspy.py │ ├── airspyhf.py │ ├── bladerf.py │ ├── connector.py │ ├── direct.py │ ├── fcdpp.py │ ├── fifi_sdr.py │ ├── hackrf.py │ ├── hpsdr.py │ ├── lime_sdr.py │ ├── perseussdr.py │ ├── pluto_sdr.py │ ├── radioberry.py │ ├── resampler.py │ ├── rtl_sdr.py │ ├── rtl_sdr_soapy.py │ ├── rtl_tcp.py │ ├── runds.py │ ├── sddc.py │ ├── sdrplay.py │ ├── soapy.py │ ├── soapy_remote.py │ └── uhd.py ├── users.py ├── vdl2 │ └── dumpvdl2.py ├── version.py ├── waterfall.py ├── websocket.py └── wsjt.py ├── setup.py ├── systemd └── openwebrx.service └── test ├── __init__.py └── property ├── __init__.py ├── filter ├── __init__.py ├── test_by_lambda.py └── test_by_property_name.py ├── test_property_carousel.py ├── test_property_deletion.py ├── test_property_filter.py ├── test_property_layer.py ├── test_property_readonly.py ├── test_property_stack.py ├── test_property_validator.py └── validators ├── __init__.py ├── test_bool_validator.py ├── test_float_validator.py ├── test_integer_validator.py ├── test_lambda_validator.py ├── test_number_validator.py ├── test_or_validator.py ├── test_regex_validator.py ├── test_string_validator.py └── test_validator.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .idea 4 | **/*.pyc 5 | **/*.swp 6 | black-env 7 | debian -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Installation method** 20 | How did you install OpenWebRX? (Raspberry Pi SD card image, Debian / Ubuntu packages, Docker image, manually?) 21 | 22 | **Versions** 23 | What version of OpenWebRX are you running? (Check on startup, or see `owrx/version.py`. If a `-dev` version is used, ideally state the commit the issue is appearing on) 24 | 25 | **Log messages** 26 | Are there any relevant messages relating to the bug in the output / log of OpenWebRX? (On most installations, the log should be available using the command `sudo journalctl -u openwebrx`) 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: General support request or other project-relasted question 4 | url: https://groups.io/g/openwebrx 5 | about: Request help on the community mailing list 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | Before posting a new feature request, please check if a similar idea has already been listed 11 | * on the issue tracker 12 | * on the [OpenWebRX github project](https://github.com/users/jketterl/projects/1). 13 | 14 | In the latter case, please only proceed if you have additional information about the feature, and please let us know that there's already a card there. 15 | 16 | **Feature description** 17 | Please describe in plain words what functionality you'd like to see in OpenWebRX, and why you think it's useful. 18 | 19 | **Target audience** 20 | Please let us know if you think that this feature is of particular interest for a particular group of users (e.g. hams, SWLs, DXers, ...) 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | **/*.swp 3 | tags 4 | .idea 5 | packages 6 | -------------------------------------------------------------------------------- /csdr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/csdr/__init__.py -------------------------------------------------------------------------------- /csdr/chain/demodulator.py: -------------------------------------------------------------------------------- 1 | from csdr.chain import Chain 2 | from abc import ABC, ABCMeta, abstractmethod 3 | from pycsdr.modules import Writer 4 | 5 | 6 | class FixedAudioRateChain(ABC): 7 | @abstractmethod 8 | def getFixedAudioRate(self) -> int: 9 | pass 10 | 11 | 12 | class FixedIfSampleRateChain(ABC): 13 | @abstractmethod 14 | def getFixedIfSampleRate(self) -> int: 15 | pass 16 | 17 | 18 | class DialFrequencyReceiver(ABC): 19 | @abstractmethod 20 | def setDialFrequency(self, frequency: int) -> None: 21 | pass 22 | 23 | 24 | # marker interface 25 | class HdAudio: 26 | pass 27 | 28 | 29 | class MetaProvider(ABC): 30 | @abstractmethod 31 | def setMetaWriter(self, writer: Writer) -> None: 32 | pass 33 | 34 | 35 | class SlotFilterChain(ABC): 36 | @abstractmethod 37 | def setSlotFilter(self, filter: int) -> None: 38 | pass 39 | 40 | 41 | class SecondarySelectorChain(ABC): 42 | def getBandwidth(self) -> float: 43 | pass 44 | 45 | 46 | class DeemphasisTauChain(ABC): 47 | @abstractmethod 48 | def setDeemphasisTau(self, tau: float) -> None: 49 | pass 50 | 51 | 52 | class RdsChain(ABC): 53 | @abstractmethod 54 | def setRdsRbds(self, rdsRbds: bool) -> None: 55 | pass 56 | 57 | 58 | class DabServiceSelector(ABC): 59 | @abstractmethod 60 | def setDabServiceId(self, serviceId: int) -> None: 61 | pass 62 | 63 | 64 | class BaseDemodulatorChain(Chain): 65 | def supportsSquelch(self) -> bool: 66 | return True 67 | 68 | def setSampleRate(self, sampleRate: int) -> None: 69 | pass 70 | 71 | 72 | class SecondaryDemodulator(Chain): 73 | def supportsSquelch(self) -> bool: 74 | return True 75 | 76 | def setSampleRate(self, sampleRate: int) -> None: 77 | pass 78 | 79 | def isSecondaryFftShown(self): 80 | return True 81 | 82 | 83 | class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta): 84 | pass 85 | 86 | 87 | class DemodulatorError(Exception): 88 | pass 89 | -------------------------------------------------------------------------------- /csdr/chain/drm.py: -------------------------------------------------------------------------------- 1 | from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain 2 | from pycsdr.modules import Convert, Downmix 3 | from pycsdr.types import Format 4 | from csdr.module.drm import DrmModule 5 | 6 | 7 | class Drm(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain): 8 | def __init__(self): 9 | workers = [ 10 | Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT), 11 | DrmModule(), 12 | Downmix(Format.SHORT), 13 | ] 14 | super().__init__(workers) 15 | 16 | def supportsSquelch(self) -> bool: 17 | return False 18 | 19 | def getFixedIfSampleRate(self) -> int: 20 | return 48000 21 | 22 | def getFixedAudioRate(self) -> int: 23 | return 48000 24 | -------------------------------------------------------------------------------- /csdr/chain/dummy.py: -------------------------------------------------------------------------------- 1 | from pycsdr.types import Format 2 | from csdr.chain import Module 3 | 4 | 5 | class DummyDemodulator(Module): 6 | def __init__(self, outputFormat: Format): 7 | self.outputFormat = outputFormat 8 | super().__init__() 9 | 10 | def getInputFormat(self) -> Format: 11 | return Format.COMPLEX_FLOAT 12 | 13 | def getOutputFormat(self) -> Format: 14 | return self.outputFormat 15 | -------------------------------------------------------------------------------- /csdr/chain/dump1090.py: -------------------------------------------------------------------------------- 1 | from pycsdr.modules import Convert 2 | from pycsdr.types import Format 3 | from csdr.chain.demodulator import ServiceDemodulator 4 | from owrx.adsb.dump1090 import Dump1090Module, RawDeframer 5 | from owrx.adsb.modes import ModeSParser 6 | 7 | 8 | class Dump1090(ServiceDemodulator): 9 | def __init__(self): 10 | workers = [ 11 | Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT), 12 | Dump1090Module(), 13 | RawDeframer(), 14 | ModeSParser(), 15 | ] 16 | 17 | super().__init__(workers) 18 | pass 19 | 20 | def getFixedAudioRate(self) -> int: 21 | return 2400000 22 | 23 | def isSecondaryFftShown(self): 24 | return False 25 | 26 | def supportsSquelch(self) -> bool: 27 | return False 28 | -------------------------------------------------------------------------------- /csdr/chain/dumphfdl.py: -------------------------------------------------------------------------------- 1 | from csdr.chain.demodulator import ServiceDemodulator 2 | from owrx.hfdl.dumphfdl import DumpHFDLModule, HFDLMessageParser 3 | 4 | 5 | class DumpHFDL(ServiceDemodulator): 6 | def __init__(self): 7 | super().__init__([ 8 | DumpHFDLModule(), 9 | HFDLMessageParser(), 10 | ]) 11 | 12 | def getFixedAudioRate(self) -> int: 13 | return 12000 14 | 15 | def supportsSquelch(self) -> bool: 16 | return False 17 | -------------------------------------------------------------------------------- /csdr/chain/dumpvdl2.py: -------------------------------------------------------------------------------- 1 | from csdr.chain.demodulator import ServiceDemodulator 2 | from owrx.vdl2.dumpvdl2 import DumpVDL2Module, VDL2MessageParser 3 | from pycsdr.modules import Convert 4 | from pycsdr.types import Format 5 | 6 | 7 | class DumpVDL2(ServiceDemodulator): 8 | def __init__(self): 9 | super().__init__([ 10 | Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT), 11 | DumpVDL2Module(), 12 | VDL2MessageParser(), 13 | ]) 14 | 15 | def getFixedAudioRate(self) -> int: 16 | return 105000 17 | 18 | def supportsSquelch(self) -> bool: 19 | return False 20 | -------------------------------------------------------------------------------- /csdr/chain/freedv.py: -------------------------------------------------------------------------------- 1 | from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain 2 | from csdr.module.freedv import FreeDVModule 3 | from pycsdr.modules import RealPart, Agc, Convert 4 | from pycsdr.types import Format 5 | 6 | 7 | class FreeDV(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain): 8 | def __init__(self): 9 | agc = Agc(Format.SHORT) 10 | agc.setMaxGain(30) 11 | agc.setInitialGain(3) 12 | workers = [ 13 | RealPart(), 14 | Agc(Format.FLOAT), 15 | Convert(Format.FLOAT, Format.SHORT), 16 | FreeDVModule(), 17 | agc, 18 | ] 19 | super().__init__(workers) 20 | 21 | def getFixedIfSampleRate(self) -> int: 22 | return 8000 23 | 24 | def getFixedAudioRate(self) -> int: 25 | return 8000 26 | 27 | def supportsSquelch(self) -> bool: 28 | return False 29 | -------------------------------------------------------------------------------- /csdr/chain/m17.py: -------------------------------------------------------------------------------- 1 | from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider 2 | from csdr.module.m17 import M17Module 3 | from pycsdr.modules import FmDemod, Limit, Convert, Writer, DcBlock 4 | from pycsdr.types import Format 5 | 6 | 7 | class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider): 8 | def __init__(self): 9 | self.module = M17Module() 10 | workers = [ 11 | FmDemod(), 12 | DcBlock(), 13 | Limit(), 14 | Convert(Format.FLOAT, Format.SHORT), 15 | self.module, 16 | ] 17 | super().__init__(workers) 18 | 19 | def getFixedIfSampleRate(self) -> int: 20 | return 48000 21 | 22 | def getFixedAudioRate(self) -> int: 23 | return 8000 24 | 25 | def supportsSquelch(self) -> bool: 26 | return False 27 | 28 | def setMetaWriter(self, writer: Writer) -> None: 29 | self.module.setMetaWriter(writer) 30 | -------------------------------------------------------------------------------- /csdr/chain/redsea.py: -------------------------------------------------------------------------------- 1 | from csdr.chain import Chain 2 | from pycsdr.modules import Convert 3 | from pycsdr.types import Format 4 | from owrx.rds.redsea import RedseaModule 5 | from csdr.module import JsonParser 6 | 7 | 8 | class Redsea(Chain): 9 | def __init__(self, sampleRate: int, rbds: bool): 10 | super().__init__([ 11 | Convert(Format.FLOAT, Format.SHORT), 12 | RedseaModule(sampleRate, rbds), 13 | JsonParser("WFM"), 14 | ]) 15 | -------------------------------------------------------------------------------- /csdr/chain/rtl433.py: -------------------------------------------------------------------------------- 1 | from owrx.ism.rtl433 import Rtl433Module, IsmParser 2 | from csdr.chain.demodulator import ServiceDemodulator 3 | 4 | 5 | class Rtl433(ServiceDemodulator): 6 | def getFixedAudioRate(self) -> int: 7 | return 1200000 8 | 9 | def __init__(self): 10 | super().__init__( 11 | [ 12 | Rtl433Module(), 13 | IsmParser(), 14 | ] 15 | ) 16 | 17 | def supportsSquelch(self) -> bool: 18 | return False 19 | -------------------------------------------------------------------------------- /csdr/module/drm.py: -------------------------------------------------------------------------------- 1 | from pycsdr.modules import ExecModule 2 | from pycsdr.types import Format 3 | 4 | 5 | class DrmModule(ExecModule): 6 | def __init__(self): 7 | super().__init__( 8 | Format.COMPLEX_SHORT, 9 | Format.SHORT, 10 | ["dream", "-c", "6", "--sigsrate", "48000", "--audsrate", "48000", "-I", "-", "-O", "-"] 11 | ) 12 | -------------------------------------------------------------------------------- /csdr/module/freedv.py: -------------------------------------------------------------------------------- 1 | from pycsdr.types import Format 2 | from pycsdr.modules import ExecModule 3 | 4 | 5 | class FreeDVModule(ExecModule): 6 | def __init__(self): 7 | super().__init__( 8 | Format.SHORT, 9 | Format.SHORT, 10 | ["freedv_rx", "1600", "-", "-"] 11 | ) 12 | -------------------------------------------------------------------------------- /csdr/module/m17.py: -------------------------------------------------------------------------------- 1 | from csdr.module import PopenModule 2 | from pycsdr.types import Format 3 | from pycsdr.modules import Writer 4 | from subprocess import Popen, PIPE 5 | from threading import Thread 6 | 7 | import re 8 | import pickle 9 | 10 | 11 | class M17Module(PopenModule): 12 | lsfRegex = re.compile("SRC: ([a-zA-Z0-9]+), DEST: ([a-zA-Z0-9]+)") 13 | 14 | def __init__(self): 15 | super().__init__() 16 | self.metawriter = None 17 | 18 | def getInputFormat(self) -> Format: 19 | return Format.SHORT 20 | 21 | def getOutputFormat(self) -> Format: 22 | return Format.SHORT 23 | 24 | def getCommand(self): 25 | return ["m17-demod", "-l"] 26 | 27 | def _getProcess(self): 28 | return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE, stderr=PIPE) 29 | 30 | def start(self): 31 | super().start() 32 | Thread(target=self._readOutput).start() 33 | 34 | def _readOutput(self): 35 | while True: 36 | line = self.process.stderr.readline() 37 | if not line: 38 | break 39 | self.parseOutput(line.decode()) 40 | 41 | def parseOutput(self, line): 42 | if self.metawriter is None: 43 | return 44 | matches = self.lsfRegex.match(line) 45 | msg = {"protocol": "M17"} 46 | if matches: 47 | # fake sync 48 | msg["sync"] = "voice" 49 | msg["source"] = matches.group(1) 50 | msg["destination"] = matches.group(2) 51 | elif line.startswith("EOS"): 52 | pass 53 | else: 54 | return 55 | self.metawriter.write(pickle.dumps(msg)) 56 | 57 | def setMetaWriter(self, writer: Writer) -> None: 58 | self.metawriter = writer 59 | -------------------------------------------------------------------------------- /csdr/module/msk144.py: -------------------------------------------------------------------------------- 1 | from pycsdr.types import Format 2 | from pycsdr.modules import ExecModule 3 | from csdr.module import LineBasedModule 4 | from owrx.wsjt import WsjtParser, Msk144Profile 5 | import pickle 6 | 7 | import logging 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Msk144Module(ExecModule): 12 | def __init__(self): 13 | super().__init__( 14 | Format.SHORT, 15 | Format.CHAR, 16 | ["msk144decoder"] 17 | ) 18 | 19 | 20 | class ParserAdapter(LineBasedModule): 21 | def __init__(self): 22 | self.parser = WsjtParser() 23 | self.dialFrequency = 0 24 | self.profile = Msk144Profile() 25 | super().__init__() 26 | 27 | def process(self, line: bytes): 28 | # actual messages from msk144decoder should start with "*** " 29 | if line[0:4] == b"*** ": 30 | return self.parser.parse(self.profile, self.dialFrequency, line[4:]) 31 | 32 | def setDialFrequency(self, frequency: int) -> None: 33 | self.dialFrequency = frequency 34 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 10 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: openwebrx 2 | Maintainer: Jakob Ketterl 3 | Section: hamradio 4 | Priority: optional 5 | Rules-Requires-Root: no 6 | Standards-Version: 4.2.0 7 | Build-Depends: debhelper (>= 11), 8 | dh-python, 9 | python3-all (>= 3.5), 10 | python3-setuptools 11 | Homepage: https://www.openwebrx.de/ 12 | Vcs-Browser: https://github.com/jketterl/openwebrx 13 | Vcs-Git: https://github.com/jketterl/openwebrx.git 14 | 15 | Package: openwebrx 16 | Architecture: all 17 | Depends: adduser, 18 | python3 (>= 3.5), 19 | python3-setuptools, 20 | owrx-connector (>= 0.7), 21 | python3-csdr (>= 0.19), 22 | ${python3:Depends}, 23 | ${misc:Depends} 24 | Recommends: python3-digiham (>= 0.6), 25 | direwolf (>= 1.4), 26 | wsjtx, 27 | js8call, 28 | runds-connector (>= 0.2), 29 | hpsdrconnector, 30 | aprs-symbols, 31 | m17-demod, 32 | js8call, 33 | python3-js8py (>= 0.2), 34 | nmux (>= 0.18), 35 | codecserver (>= 0.1), 36 | msk144decoder, 37 | dump1090-fa-minimal, 38 | dumphfdl, 39 | dumpvdl2, 40 | rtl-433, 41 | extra-sdr-drivers, 42 | perseus-tools, 43 | dream-headless, 44 | codec2, 45 | redsea, 46 | python3-csdr-eti, 47 | dablin, 48 | python3-paho-mqtt 49 | Description: multi-user web sdr 50 | Open source, multi-user SDR receiver with a web interface 51 | -------------------------------------------------------------------------------- /debian/openwebrx.config: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | . /usr/share/debconf/confmodule 3 | 4 | db_get openwebrx/admin_user_configured 5 | if [ "${1:-}" = "reconfigure" ] || [ "${RET}" != true ]; then 6 | db_settitle openwebrx/title 7 | db_input high openwebrx/admin_user_password || true 8 | db_go 9 | fi 10 | -------------------------------------------------------------------------------- /debian/openwebrx.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=OpenWebRX 4 | Type=Application 5 | Comment=Web-based software defined radio receiver 6 | Icon=openwebrx 7 | Exec=xdg-open http://localhost:8073/ 8 | Categories=Network;HamRadio 9 | -------------------------------------------------------------------------------- /debian/openwebrx.dirs: -------------------------------------------------------------------------------- 1 | /etc/openwebrx/openwebrx.conf.d -------------------------------------------------------------------------------- /debian/openwebrx.install: -------------------------------------------------------------------------------- 1 | bands.json etc/openwebrx/ 2 | openwebrx.conf etc/openwebrx/ 3 | systemd/openwebrx.service lib/systemd/system/ 4 | debian/openwebrx.svg usr/share/icons/hicolor/scalable/apps 5 | debian/openwebrx.desktop usr/share/applications -------------------------------------------------------------------------------- /debian/openwebrx.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /usr/share/debconf/confmodule 3 | 4 | set -euo pipefail 5 | 6 | OWRX_USER="openwebrx" 7 | OWRX_DATADIR="/var/lib/openwebrx" 8 | OWRX_USERS_FILE="${OWRX_DATADIR}/users.json" 9 | OWRX_SETTINGS_FILE="${OWRX_DATADIR}/settings.json" 10 | OWRX_BOOKMARKS_FILE="${OWRX_DATADIR}/bookmarks.json" 11 | 12 | case "$1" in 13 | configure|reconfigure) 14 | adduser --system --group --no-create-home --home /nonexistent --quiet "${OWRX_USER}" 15 | usermod -aG plugdev "${OWRX_USER}" 16 | 17 | # ensure group exists first (dependency is optional) 18 | # addgroup will error out if the group exists, but is not a system group. it doesn't matter for the intended purpose, but we need extra protection for this case. 19 | if [ ! $(getent group perseususb) ]; then 20 | addgroup --system --quiet perseususb 21 | fi 22 | usermod -aG perseususb "${OWRX_USER}" 23 | 24 | # create OpenWebRX data directory and set the correct permissions 25 | if [ ! -d "${OWRX_DATADIR}" ] && [ ! -L "${OWRX_DATADIR}" ]; then mkdir "${OWRX_DATADIR}"; fi 26 | chown "${OWRX_USER}": ${OWRX_DATADIR} 27 | 28 | # create empty config files now to avoid permission problems later 29 | if [ ! -e "${OWRX_USERS_FILE}" ]; then 30 | echo "[]" > "${OWRX_USERS_FILE}" 31 | chown "${OWRX_USER}": "${OWRX_USERS_FILE}" 32 | chmod 0600 "${OWRX_USERS_FILE}" 33 | fi 34 | 35 | if [ ! -e "${OWRX_SETTINGS_FILE}" ]; then 36 | echo "{}" > "${OWRX_SETTINGS_FILE}" 37 | chown "${OWRX_USER}": "${OWRX_SETTINGS_FILE}" 38 | fi 39 | 40 | if [ ! -e "${OWRX_BOOKMARKS_FILE}" ]; then 41 | touch "${OWRX_BOOKMARKS_FILE}" 42 | chown "${OWRX_USER}": "${OWRX_BOOKMARKS_FILE}" 43 | fi 44 | 45 | db_get openwebrx/admin_user_password 46 | if [ ! -z "${RET}" ]; then 47 | if ! openwebrx admin --silent hasuser admin; then 48 | # create initial openwebrx user 49 | OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive adduser admin 50 | else 51 | # change existing user's password 52 | OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive resetpassword admin 53 | fi 54 | fi 55 | # remove password from debconf database 56 | db_unregister openwebrx/admin_user_password 57 | # set a marker that admin is configured to avoid future questions 58 | db_set openwebrx/admin_user_configured true 59 | ;; 60 | *) 61 | echo "postinst called with unknown argument '$1'" 1>&2 62 | exit 1 63 | ;; 64 | esac 65 | 66 | #DEBHELPER# 67 | -------------------------------------------------------------------------------- /debian/openwebrx.postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ "$1" = purge ] && [ -e /usr/share/debconf/confmodule ]; then 4 | . /usr/share/debconf/confmodule 5 | db_purge 6 | fi 7 | 8 | #DEBHELPER# 9 | -------------------------------------------------------------------------------- /debian/openwebrx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/openwebrx.templates: -------------------------------------------------------------------------------- 1 | Template: openwebrx/admin_user_password 2 | Type: password 3 | Description: OpenWebRX "admin" user password: 4 | The system can create a user for the OpenWebRX web configuration interface for 5 | you. Using this user, you will be able to log into the "settings" area of 6 | OpenWebRX to configure your receiver conveniently through your browser. 7 | . 8 | The name of the created user will be "admin". 9 | . 10 | If you do not wish to create a web admin user right now, you can leave this 11 | empty for now. You can return to this prompt at a later time by running the 12 | command "sudo dpkg-reconfigure openwebrx". 13 | . 14 | You can also use the "openwebrx admin" command to create, delete or manage 15 | existing users. More information is available in by running the command 16 | "openwebrx admin --help". 17 | 18 | Template: openwebrx/admin_user_configured 19 | Type: boolean 20 | Default: false 21 | Description: OpenWebRX "admin" user previously configured? 22 | Marker used internally by the config scripts to remember if an admin user has 23 | been created. 24 | 25 | Template: openwebrx/title 26 | Type: title 27 | Description: Configuring OpenWebRX -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | export PYBUILD_NAME=openwebrx 3 | 4 | %: 5 | dh $@ --with python3 --buildsystem=pybuild --with systemd 6 | 7 | override_dh_strip_nondeterminism: 8 | dh_strip_nondeterminism -X.png 9 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-afedri: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-afedri.sh / 5 | RUN /install-dependencies-afedri.sh &&\ 6 | rm /install-dependencies-afedri.sh 7 | 8 | ADD . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-airspy: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-airspy.sh / 5 | RUN /install-dependencies-airspy.sh &&\ 6 | rm /install-dependencies-airspy.sh 7 | 8 | ADD . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-base: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | COPY docker/files/js8call/js8call-hamlib.patch \ 4 | docker/files/wsjtx/wsjtx.patch \ 5 | docker/files/wsjtx/wsjtx-hamlib.patch \ 6 | docker/files/dream/dream.patch \ 7 | docker/files/direwolf/direwolf-hamlib.patch \ 8 | docker/scripts/install-dependencies.sh / 9 | RUN /install-dependencies.sh && \ 10 | rm /install-dependencies.sh && \ 11 | rm /*.patch 12 | COPY docker/scripts/install-owrx-tools.sh / 13 | RUN /install-owrx-tools.sh && \ 14 | rm /install-owrx-tools.sh 15 | 16 | COPY docker/files/services/codecserver /etc/services.d/codecserver 17 | 18 | ENTRYPOINT ["/init"] 19 | 20 | WORKDIR /opt/openwebrx 21 | 22 | VOLUME /etc/openwebrx 23 | VOLUME /var/lib/openwebrx 24 | 25 | ENV S6_CMD_ARG0="/opt/openwebrx/docker/scripts/run.sh" 26 | CMD [""] 27 | 28 | EXPOSE 8073 29 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-bladerf: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-bladerf.sh / 5 | RUN /install-dependencies-bladerf.sh &&\ 6 | rm /install-dependencies-bladerf.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-fcdpp: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-fcdpp.sh / 5 | RUN /install-dependencies-fcdpp.sh &&\ 6 | rm /install-dependencies-fcdpp.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-full: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-*.sh \ 5 | docker/files/sdrplay/install-lib.*.patch \ 6 | docker/scripts/install-connectors.sh / 7 | 8 | RUN /install-dependencies-rtlsdr.sh &&\ 9 | /install-dependencies-soapysdr.sh &&\ 10 | /install-dependencies-hackrf.sh &&\ 11 | /install-dependencies-sdrplay.sh &&\ 12 | /install-dependencies-airspy.sh &&\ 13 | /install-dependencies-afedri.sh &&\ 14 | /install-dependencies-rtlsdr-soapy.sh &&\ 15 | /install-dependencies-plutosdr.sh &&\ 16 | /install-dependencies-limesdr.sh &&\ 17 | /install-dependencies-soapyremote.sh &&\ 18 | /install-dependencies-perseus.sh &&\ 19 | /install-dependencies-fcdpp.sh &&\ 20 | /install-dependencies-radioberry.sh &&\ 21 | /install-dependencies-uhd.sh &&\ 22 | /install-dependencies-hpsdr.sh &&\ 23 | /install-dependencies-bladerf.sh &&\ 24 | /install-connectors.sh &&\ 25 | /install-dependencies-runds.sh &&\ 26 | rm /install-dependencies-*.sh &&\ 27 | rm /install-lib.*.patch && \ 28 | rm /install-connectors.sh 29 | 30 | COPY docker/files/services/sdrplay /etc/services.d/sdrplay 31 | 32 | ADD . /opt/openwebrx 33 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-hackrf: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-hackrf.sh / 5 | RUN /install-dependencies-hackrf.sh &&\ 6 | rm /install-dependencies-hackrf.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-hpsdr: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-hpsdr.sh / 5 | 6 | RUN /install-dependencies-hpsdr.sh &&\ 7 | rm /install-dependencies-hpsdr.sh 8 | 9 | COPY . /opt/openwebrx 10 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-limesdr: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-limesdr.sh / 5 | RUN /install-dependencies-limesdr.sh &&\ 6 | rm /install-dependencies-limesdr.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-perseus: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-perseus.sh / 5 | RUN /install-dependencies-perseus.sh &&\ 6 | rm /install-dependencies-perseus.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-plutosdr: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-plutosdr.sh / 5 | RUN /install-dependencies-plutosdr.sh &&\ 6 | rm /install-dependencies-plutosdr.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-radioberry: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-radioberry.sh / 5 | RUN /install-dependencies-radioberry.sh &&\ 6 | rm /install-dependencies-radioberry.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-rtlsdr: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-rtlsdr.sh \ 5 | docker/scripts/install-connectors.sh / 6 | 7 | RUN /install-dependencies-rtlsdr.sh &&\ 8 | rm /install-dependencies-rtlsdr.sh &&\ 9 | /install-connectors.sh &&\ 10 | rm /install-connectors.sh 11 | 12 | COPY . /opt/openwebrx 13 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-rtlsdr-soapy: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-rtlsdr-soapy.sh / 5 | RUN /install-dependencies-rtlsdr-soapy.sh &&\ 6 | rm /install-dependencies-rtlsdr-soapy.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-rtltcp: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-connectors.sh / 5 | 6 | RUN /install-connectors.sh &&\ 7 | rm /install-connectors.sh 8 | 9 | COPY . /opt/openwebrx 10 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-runds: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-connectors.sh \ 5 | docker/scripts/install-dependencies-runds.sh / 6 | 7 | RUN /install-connectors.sh &&\ 8 | rm /install-connectors.sh && \ 9 | /install-dependencies-runds.sh && \ 10 | rm /install-dependencies-runds.sh 11 | 12 | COPY . /opt/openwebrx 13 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-sdrplay: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-sdrplay.sh \ 5 | docker/files/sdrplay/install-lib.*.patch / 6 | RUN /install-dependencies-sdrplay.sh &&\ 7 | rm /install-dependencies-sdrplay.sh &&\ 8 | rm /install-lib.*.patch 9 | 10 | COPY docker/files/services/sdrplay /etc/services.d/sdrplay 11 | 12 | COPY . /opt/openwebrx 13 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-soapyremote: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-soapyremote.sh / 5 | RUN /install-dependencies-soapyremote.sh &&\ 6 | rm /install-dependencies-soapyremote.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-soapysdr: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-soapysdr.sh \ 5 | docker/scripts/install-connectors.sh / 6 | RUN /install-dependencies-soapysdr.sh &&\ 7 | rm /install-dependencies-soapysdr.sh &&\ 8 | /install-connectors.sh &&\ 9 | rm /install-connectors.sh 10 | -------------------------------------------------------------------------------- /docker/Dockerfiles/Dockerfile-uhd: -------------------------------------------------------------------------------- 1 | ARG ARCHTAG 2 | FROM openwebrx-soapysdr-base:$ARCHTAG 3 | 4 | COPY docker/scripts/install-dependencies-uhd.sh / 5 | RUN /install-dependencies-uhd.sh &&\ 6 | rm /install-dependencies-uhd.sh 7 | 8 | COPY . /opt/openwebrx 9 | -------------------------------------------------------------------------------- /docker/files/direwolf/direwolf-hamlib.patch: -------------------------------------------------------------------------------- 1 | diff --git a/CMakeLists.txt b/CMakeLists.txt 2 | index 9e710f5..da90b43 100644 3 | --- a/CMakeLists.txt 4 | +++ b/CMakeLists.txt 5 | @@ -257,13 +257,8 @@ else() 6 | set(GPSD_LIBRARIES "") 7 | endif() 8 | 9 | -find_package(hamlib) 10 | -if(HAMLIB_FOUND) 11 | - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_HAMLIB") 12 | -else() 13 | - set(HAMLIB_INCLUDE_DIRS "") 14 | - set(HAMLIB_LIBRARIES "") 15 | -endif() 16 | +set(HAMLIB_INCLUDE_DIRS "") 17 | +set(HAMLIB_LIBRARIES "") 18 | 19 | if(LINUX) 20 | find_package(ALSA REQUIRED) 21 | -------------------------------------------------------------------------------- /docker/files/sdrplay/install-lib.aarch64.patch: -------------------------------------------------------------------------------- 1 | install-lib.x86_64.patch -------------------------------------------------------------------------------- /docker/files/sdrplay/install-lib.armv7l.patch: -------------------------------------------------------------------------------- 1 | diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh 2 | --- sdrplay-orig/install_lib.sh 2020-05-24 14:13:04.561271707 +0000 3 | +++ sdrplay/install_lib.sh 2020-05-24 14:16:20.068329040 +0000 4 | @@ -4,19 +4,6 @@ 5 | MAJVERS="3" 6 | 7 | echo "Installing SDRplay RSP API library ${VERS}..." 8 | -read -p "Press RETURN to view the license agreement" ret 9 | - 10 | -more sdrplay_license.txt 11 | - 12 | -while true; do 13 | - echo "Press y and RETURN to accept the license agreement and continue with" 14 | - read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn 15 | - case $yn in 16 | - [Yy]* ) break;; 17 | - [Nn]* ) exit;; 18 | - * ) echo "Please answer y or n";; 19 | - esac 20 | -done 21 | 22 | ARCH=`uname -m` 23 | 24 | @@ -141,16 +128,6 @@ 25 | echo "SDRplay API ${VERS} Installation Finished" 26 | echo " " 27 | 28 | -while true; do 29 | - echo "Would you like to add SDRplay USB IDs to the local database for easier 30 | -" 31 | - read -p "identification in applications such as lsusb? [y/n] " yn 32 | - case $yn in 33 | - [Yy]* ) break;; 34 | - [Nn]* ) exit;; 35 | - * ) echo "Please answer y or n";; 36 | - esac 37 | -done 38 | sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/. 39 | sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh 40 | sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/. 41 | -------------------------------------------------------------------------------- /docker/files/services/codecserver/run: -------------------------------------------------------------------------------- 1 | #!/command/execlineb -P 2 | /usr/local/bin/codecserver -------------------------------------------------------------------------------- /docker/files/services/sdrplay/run: -------------------------------------------------------------------------------- 1 | #!/command/execlineb -P 2 | /usr/local/bin/sdrplay_apiService -------------------------------------------------------------------------------- /docker/files/wsjtx/wsjtx-hamlib.patch: -------------------------------------------------------------------------------- 1 | --- CMakeLists.txt.orig 2021-09-28 14:33:14.329598412 +0200 2 | +++ CMakeLists.txt 2021-09-28 14:34:23.052345270 +0200 3 | @@ -106,24 +106,6 @@ 4 | 5 | 6 | # 7 | -# build and install hamlib locally so it can be referenced by the 8 | -# WSJT-X build 9 | -# 10 | -ExternalProject_Add (hamlib 11 | - GIT_REPOSITORY ${hamlib_repo} 12 | - GIT_TAG ${hamlib_TAG} 13 | - GIT_SHALLOW False 14 | - URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}.tar.gz 15 | - URL_HASH MD5=${hamlib_md5sum} 16 | - #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap" 17 | - PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch 18 | - CONFIGURE_COMMAND /configure --prefix= --disable-shared --enable-static --without-cxx-binding ${EXTRA_FLAGS} # LIBUSB_LIBS=${USB_LIBRARY} 19 | - BUILD_COMMAND $(MAKE) all V=1 # $(MAKE) is ExternalProject_Add() magic to do recursive make 20 | - INSTALL_COMMAND $(MAKE) install-strip V=1 DESTDIR="" 21 | - STEP_TARGETS update install 22 | - ) 23 | - 24 | -# 25 | # custom target to make a hamlib source tarball 26 | # 27 | add_custom_target (hamlib_sources 28 | @@ -161,7 +143,6 @@ 29 | # build and optionally install WSJT-X using the hamlib package built 30 | # above 31 | # 32 | -ExternalProject_Get_Property (hamlib INSTALL_DIR) 33 | ExternalProject_Add (wsjtx 34 | GIT_REPOSITORY ${wsjtx_repo} 35 | GIT_TAG ${WSJTX_TAG} 36 | @@ -186,14 +167,8 @@ 37 | DEPENDEES build 38 | ) 39 | 40 | -set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1) 41 | set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1) 42 | 43 | -add_dependencies (wsjtx-configure hamlib-install) 44 | -add_dependencies (wsjtx-build hamlib-install) 45 | -add_dependencies (wsjtx-install hamlib-install) 46 | -add_dependencies (wsjtx-package hamlib-install) 47 | - 48 | # export traditional targets 49 | add_custom_target (build ALL DEPENDS wsjtx-build) 50 | add_custom_target (install DEPENDS wsjtx-install) 51 | -------------------------------------------------------------------------------- /docker/scripts/install-connectors.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libfftw3-single3" 22 | BUILD_PACKAGES="git cmake make gcc g++ libsamplerate-dev libfftw3-dev" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/jketterl/owrx_connector.git 28 | # latest develop as of 2024-01-01 (fixed startup race condition) 29 | cmakebuild owrx_connector 62219d40e180abb539ad61fcd9625b90c34f0e26 30 | 31 | apt-get -y purge --autoremove $BUILD_PACKAGES 32 | apt-get clean 33 | rm -rf /var/lib/apt/lists/* 34 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-afedri.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="" 22 | BUILD_PACKAGES="git cmake make gcc g++" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/alexander-sholohov/SoapyAfedri.git 28 | cmakebuild SoapyAfedri 1.0.1 29 | 30 | apt-get -y purge --autoremove $BUILD_PACKAGES 31 | apt-get clean 32 | rm -rf /var/lib/apt/lists/* 33 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-airspy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libusb-1.0-0" 22 | BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/airspy/airspyone_host.git 28 | # latest from master as of 2020-09-04 29 | cmakebuild airspyone_host 652fd7f1a8f85687641e0bd91f739694d7258ecc 30 | 31 | git clone https://github.com/pothosware/SoapyAirspy.git 32 | cmakebuild SoapyAirspy 10d697b209e7f1acc8b2c8d24851d46170ef77e3 33 | 34 | git clone https://github.com/airspy/airspyhf.git 35 | # latest from master as of 2020-09-04 36 | cmakebuild airspyhf 8891387edddcd185e2949e9814e9ef35f46f0722 37 | 38 | git clone https://github.com/pothosware/SoapyAirspyHF.git 39 | # latest from master as of 2020-09-04 40 | cmakebuild SoapyAirspyHF 5488dac5b44f1432ce67b40b915f7e61d3bd4853 41 | 42 | apt-get -y purge --autoremove $BUILD_PACKAGES 43 | apt-get clean 44 | rm -rf /var/lib/apt/lists/* 45 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-bladerf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libusb-1.0-0" 22 | BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/Nuand/bladeRF.git 28 | cmakebuild bladeRF 2023.02 29 | 30 | git clone https://github.com/pothosware/SoapyBladeRF.git 31 | # latest from master as of 2023-08-30 32 | cmakebuild SoapyBladeRF 85f6dc554ed4c618304d99395b19c4e1523675b0 33 | 34 | apt-get -y purge --autoremove $BUILD_PACKAGES 35 | apt-get clean 36 | rm -rf /var/lib/apt/lists/* 37 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-fcdpp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libhidapi-hidraw0 libhidapi-libusb0 libasound2" 22 | BUILD_PACKAGES="git cmake make gcc g++ libhidapi-dev libasound2-dev" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/pothosware/SoapyFCDPP.git 28 | cmakebuild SoapyFCDPP soapy-fcdpp-0.1.1 29 | 30 | apt-get -y purge --autoremove $BUILD_PACKAGES 31 | apt-get clean 32 | rm -rf /var/lib/apt/lists/* 33 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-hackrf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libusb-1.0-0 libfftw3-single3 udev" 22 | BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/mossmann/hackrf.git 28 | cd hackrf 29 | # latest from master as of 2020-09-04 30 | git checkout 6e5cbda2945c3bab0e6e1510eae418eda60c358e 31 | cmakebuild host 32 | cd .. 33 | rm -rf hackrf 34 | 35 | git clone https://github.com/pothosware/SoapyHackRF.git 36 | # latest from master as of 2020-09-04 37 | cmakebuild SoapyHackRF 7d530872f96c1cbe0ed62617c32c48ce7e103e1d 38 | 39 | SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES 40 | apt-get clean 41 | rm -rf /var/lib/apt/lists/* 42 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-hpsdr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | BUILD_PACKAGES="git wget gcc libc6-dev" 6 | 7 | apt-get update 8 | apt-get -y install --no-install-recommends $BUILD_PACKAGES 9 | 10 | pushd /tmp 11 | 12 | ARCH=$(uname -m) 13 | GOVERSION=1.20.10 14 | 15 | case ${ARCH} in 16 | x86_64) 17 | PACKAGE=go${GOVERSION}.linux-amd64.tar.gz 18 | ;; 19 | armv*) 20 | PACKAGE=go${GOVERSION}.linux-armv6l.tar.gz 21 | ;; 22 | aarch64) 23 | PACKAGE=go${GOVERSION}.linux-arm64.tar.gz 24 | ;; 25 | esac 26 | 27 | wget https://golang.org/dl/${PACKAGE} 28 | tar xfz $PACKAGE 29 | 30 | git clone https://github.com/jancona/hpsdrconnector.git 31 | pushd hpsdrconnector 32 | git checkout v0.6.4 33 | /tmp/go/bin/go build 34 | install -m 0755 hpsdrconnector /usr/local/bin 35 | 36 | popd 37 | 38 | rm -rf hpsdrconnector 39 | rm -rf go 40 | rm $PACKAGE 41 | 42 | popd 43 | 44 | apt-get -y purge --autoremove $BUILD_PACKAGES 45 | apt-get clean 46 | rm -rf /var/lib/apt/lists/* 47 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-limesdr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | cd /tmp 6 | 7 | STATIC_PACKAGES="libusb-1.0-0 libatomic1" 8 | BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++" 9 | 10 | apt-get update 11 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 12 | 13 | SIMD_FLAGS="" 14 | if [[ 'x86_64' == `uname -m` ]] ; then 15 | SIMD_FLAGS="-DDEFAULT_SIMD_FLAGS=SSE3" 16 | fi 17 | 18 | git clone https://github.com/myriadrf/LimeSuite.git 19 | cd LimeSuite 20 | # latest from master as of 2020-09-04 21 | git checkout 9526621f8b4c9e2a7f638b5ef50c45560dcad22a 22 | mkdir builddir 23 | cd builddir 24 | cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" ${SIMD_FLAGS} 25 | make 26 | make install 27 | cd ../.. 28 | rm -rf LimeSuite 29 | 30 | apt-get -y purge --autoremove $BUILD_PACKAGES 31 | apt-get clean 32 | rm -rf /var/lib/apt/lists/* 33 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-perseus.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | cd /tmp 6 | 7 | STATIC_PACKAGES="libusb-1.0-0 libudev1" 8 | BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd" 9 | 10 | apt-get update 11 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 12 | 13 | git clone https://github.com/Microtelecom/libperseus-sdr.git 14 | cd libperseus-sdr 15 | # latest from master as of 2020-09-04 16 | git checkout c2c95daeaa08bf0daed0e8ada970ab17cc264e1b 17 | ./bootstrap.sh 18 | ./configure 19 | make 20 | make install 21 | ldconfig /etc/ld.so.conf.d 22 | cd .. 23 | rm -rf libperseus-sdr 24 | 25 | apt-get -y purge --autoremove $BUILD_PACKAGES 26 | apt-get clean 27 | rm -rf /var/lib/apt/lists/* 28 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-plutosdr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. ${3:-} 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libusb-1.0-0 libxml2" 22 | BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison pkg-config" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/analogdevicesinc/libiio.git 28 | cmakebuild libiio v0.21 -DCMAKE_INSTALL_PREFIX=/usr/local 29 | 30 | git clone https://github.com/analogdevicesinc/libad9361-iio.git 31 | cmakebuild libad9361-iio v0.2 32 | 33 | git clone https://github.com/pothosware/SoapyPlutoSDR.git 34 | # latest from master as of 2020-09-04 35 | cmakebuild SoapyPlutoSDR 93717b32ef052e0dfa717aa2c1a4eb27af16111f 36 | 37 | apt-get -y purge --autoremove $BUILD_PACKAGES 38 | apt-get clean 39 | rm -rf /var/lib/apt/lists/* 40 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-radioberry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="" 22 | BUILD_PACKAGES="git cmake make gcc g++" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/pa3gsb/Radioberry-2.x 28 | cd Radioberry-2.x/SBC/rpi-4 29 | 30 | # latest from master as of 2020-09-04 31 | cmakebuild SoapyRadioberrySDR 8d17de6b4dc076e628900a82f05c7cf0b16cbe24 32 | cd ../../.. 33 | rm -rf Radioberry-2.x 34 | 35 | SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES 36 | apt-get clean 37 | rm -rf /var/lib/apt/lists/* 38 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-rtlsdr-soapy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libusb-1.0-0" 22 | BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/osmocom/rtl-sdr.git 28 | cmakebuild rtl-sdr v2.0.1 29 | 30 | git clone https://github.com/pothosware/SoapyRTLSDR.git 31 | # latest from master as of 2023-09-13 32 | cmakebuild SoapyRTLSDR 068aa77a4c938b239c9d80cd42c4ee7986458e8f 33 | 34 | apt-get -y purge --autoremove $BUILD_PACKAGES 35 | apt-get clean 36 | rm -rf /var/lib/apt/lists/* 37 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-rtlsdr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libusb-1.0.0" 22 | BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/osmocom/rtl-sdr.git 28 | cmakebuild rtl-sdr v2.0.1 29 | 30 | apt-get -y purge --autoremove $BUILD_PACKAGES 31 | apt-get clean 32 | rm -rf /var/lib/apt/lists/* 33 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-runds.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libfftw3-single3" 22 | BUILD_PACKAGES="git cmake make gcc g++ pkg-config libfftw3-dev" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/jketterl/runds_connector.git 28 | # latest develop as of 2023-07-04 (cmake exports) 29 | cmakebuild runds_connector 435364002d756735015707e7f59aa40e8d743585 30 | 31 | apt-get -y purge --autoremove $BUILD_PACKAGES 32 | apt-get clean 33 | rm -rf /var/lib/apt/lists/* 34 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-sdrplay.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libusb-1.0.0 udev" 22 | BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | ARCH=$(uname -m) 28 | 29 | case $ARCH in 30 | x86_64|aarch64) 31 | BINARY=SDRplay_RSP_API-Linux-3.14.0.run 32 | ;; 33 | armv*) 34 | BINARY=SDRplay_RSP_API-ARM32-3.07.2.run 35 | ;; 36 | esac 37 | 38 | wget --no-http-keep-alive https://www.sdrplay.com/software/$BINARY 39 | sh $BINARY --noexec --target sdrplay 40 | patch --verbose -Np0 < /install-lib.$ARCH.patch 41 | 42 | cd sdrplay 43 | ./install_lib.sh 44 | cd .. 45 | rm -rf sdrplay 46 | rm $BINARY 47 | 48 | git clone https://github.com/pothosware/SoapySDRPlay3.git 49 | # latest from master as of 2021-06-19 (reliability fixes) 50 | cmakebuild SoapySDRPlay3 a869f25364a1f0d5b16169ff908aa21a2ace475d 51 | 52 | SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES 53 | apt-get clean 54 | rm -rf /var/lib/apt/lists/* 55 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-soapyremote.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="avahi-daemon libavahi-client3" 22 | BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/pothosware/SoapyRemote.git 28 | cmakebuild SoapyRemote soapy-remote-0.5.2 29 | 30 | apt-get -y purge --autoremove $BUILD_PACKAGES 31 | apt-get clean 32 | rm -rf /var/lib/apt/lists/* 33 | -------------------------------------------------------------------------------- /docker/scripts/install-dependencies-soapysdr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libudev1" 22 | BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" 23 | 24 | apt-get update 25 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 26 | 27 | git clone https://github.com/pothosware/SoapySDR 28 | # latest from master as of 2020-09-04 29 | cmakebuild SoapySDR 580b94f3dad46899f34ec0a060dbb4534e844e57 30 | 31 | SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES 32 | apt-get clean 33 | rm -rf /var/lib/apt/lists/* 34 | -------------------------------------------------------------------------------- /docker/scripts/install-owrx-tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | export MAKEFLAGS="-j4" 4 | 5 | function cmakebuild() { 6 | cd $1 7 | if [[ ! -z "${2:-}" ]]; then 8 | git checkout $2 9 | fi 10 | mkdir build 11 | cd build 12 | cmake ${CMAKE_ARGS:-} .. 13 | make 14 | make install 15 | cd ../.. 16 | rm -rf $1 17 | } 18 | 19 | cd /tmp 20 | 21 | STATIC_PACKAGES="libfftw3-single3 libprotobuf32 libsamplerate0 libicu72 libudev1" 22 | 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" 23 | apt-get update 24 | apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES 25 | 26 | git clone https://github.com/jketterl/js8py.git 27 | pushd js8py 28 | # latest develop as of 2022-11-30 (structured callsign data) 29 | git checkout f7e394b7892d26cbdcce5d43c0b4081a2a6a48f6 30 | python3 setup.py install 31 | popd 32 | rm -rf js8py 33 | 34 | git clone https://github.com/jketterl/csdr.git 35 | # latest develop as of 2024-01-25 (exemodule setargs) 36 | cmakebuild csdr 344179a616cdbadf501479ce9ed1b836543e657b 37 | 38 | git clone https://github.com/jketterl/pycsdr.git 39 | cd pycsdr 40 | # latest develop as of 2024-01-25 (execmodule setargs) 41 | git checkout 9063b8a119e366c31d089596641a24a427e3cbdc 42 | ./setup.py install install_headers 43 | cd .. 44 | rm -rf pycsdr 45 | 46 | git clone https://github.com/jketterl/csdr-eti.git 47 | # latest develop as of 2024-02-13 (fix for aarch64) 48 | cmakebuild csdr-eti e174007f9c247047dba60f092f794800297c594f 49 | 50 | git clone https://github.com/jketterl/pycsdr-eti.git 51 | cd pycsdr-eti 52 | # latest develop as of 2024-02-12 (service id filter) 53 | git checkout 676663b4d796fbadd18dfcae0c3b80eb1b1f9147 54 | ./setup.py install 55 | cd .. 56 | rm -rf pycsdr-eti 57 | 58 | git clone https://github.com/jketterl/codecserver.git 59 | mkdir -p /usr/local/etc/codecserver 60 | cp codecserver/conf/codecserver.conf /usr/local/etc/codecserver 61 | # latest develop as of 2023-07-03 (error handling) 62 | cmakebuild codecserver 0f3703ce285acd85fcd28f6620d7795dc173cb50 63 | 64 | git clone https://github.com/jketterl/digiham.git 65 | # latest develop as of 2023-07-02 (codecserver protocol version) 66 | cmakebuild digiham 262e6dfd9a2c56778bd4b597240756ad0fb9861d 67 | 68 | git clone https://github.com/jketterl/pydigiham.git 69 | cd pydigiham 70 | # latest develop as of 2023-06-30 (csdr cleanup) 71 | git checkout 894aa87ea9a3534d1e7109da86194c7cd5e0b7c7 72 | ./setup.py install 73 | cd .. 74 | rm -rf pydigiham 75 | 76 | apt-get -y purge --autoremove $BUILD_PACKAGES 77 | apt-get clean 78 | rm -rf /var/lib/apt/lists/* 79 | -------------------------------------------------------------------------------- /docker/scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | mkdir -p /etc/openwebrx/openwebrx.conf.d 5 | mkdir -p /var/lib/openwebrx 6 | mkdir -p /tmp/openwebrx/ 7 | if [[ ! -f /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf ]] ; then 8 | cat << EOF > /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf 9 | [core] 10 | temporary_directory = /tmp/openwebrx 11 | EOF 12 | fi 13 | if [[ ! -f /etc/openwebrx/bands.json ]] ; then 14 | cp bands.json /etc/openwebrx/ 15 | fi 16 | if [[ ! -f /etc/openwebrx/openwebrx.conf ]] ; then 17 | cp openwebrx.conf /etc/openwebrx/ 18 | fi 19 | if [[ ! -z "${OPENWEBRX_ADMIN_USER:-}" ]] && [[ ! -z "${OPENWEBRX_ADMIN_PASSWORD:-}" ]] ; then 20 | if ! python3 openwebrx.py admin --silent hasuser "${OPENWEBRX_ADMIN_USER}" ; then 21 | OWRX_PASSWORD="${OPENWEBRX_ADMIN_PASSWORD}" python3 openwebrx.py admin --noninteractive adduser "${OPENWEBRX_ADMIN_USER}" 22 | fi 23 | fi 24 | 25 | 26 | _term() { 27 | echo "Caught signal!" 28 | kill -TERM "$child" 2>/dev/null 29 | } 30 | 31 | trap _term SIGTERM SIGINT 32 | 33 | python3 openwebrx.py $@ & 34 | 35 | child=$! 36 | wait "$child" 37 | -------------------------------------------------------------------------------- /htdocs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/__init__.py -------------------------------------------------------------------------------- /htdocs/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/apple-touch-icon.png -------------------------------------------------------------------------------- /htdocs/css/login.css: -------------------------------------------------------------------------------- 1 | @import url("openwebrx-header.css"); 2 | @import url("openwebrx-globals.css"); 3 | 4 | body { 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | .login-container { 10 | flex: 1; 11 | position: relative; 12 | } 13 | 14 | .login { 15 | position: absolute; 16 | left: 50%; 17 | top: 50%; 18 | transform: translate(-50%, -50%); 19 | 20 | width: 500px; 21 | 22 | padding: 20px; 23 | border-radius: 10px; 24 | border: 1px solid #575757; 25 | box-shadow: 0 0 20px #000; 26 | } 27 | 28 | .login .btn { 29 | width: 100%; 30 | } 31 | 32 | .btn-login { 33 | height: 50px; 34 | } -------------------------------------------------------------------------------- /htdocs/css/map.css: -------------------------------------------------------------------------------- 1 | @import url("openwebrx-header.css"); 2 | @import url("openwebrx-globals.css"); 3 | 4 | body { 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | .openwebrx-map { 10 | flex: 1 1 auto; 11 | } 12 | 13 | h3 { 14 | margin: 10px 0; 15 | text-align: center; 16 | } 17 | 18 | ul { 19 | margin-block-start: 5px; 20 | margin-block-end: 5px; 21 | padding-inline-start: 25px; 22 | } 23 | 24 | /* don't show the filter in it's initial position */ 25 | .openwebrx-map-legend { 26 | display: none; 27 | background-color: #fff; 28 | padding: 10px; 29 | margin: 10px; 30 | user-select: none; 31 | } 32 | 33 | /* show it as soon as google maps has moved it to its container */ 34 | .openwebrx-map .openwebrx-map-legend { 35 | display: block; 36 | } 37 | 38 | .openwebrx-map-legend ul { 39 | list-style-type: none; 40 | padding: 0; 41 | } 42 | 43 | .openwebrx-map-legend ul li { 44 | cursor: pointer; 45 | } 46 | 47 | .openwebrx-map-legend ul li.disabled { 48 | opacity: .3; 49 | filter: grayscale(70%); 50 | } 51 | 52 | .openwebrx-map-legend li.square .illustration { 53 | display: inline-block; 54 | width: 30px; 55 | height: 20px; 56 | margin-right: 10px; 57 | border-width: 2px; 58 | border-style: solid; 59 | } 60 | 61 | .openwebrx-map-legend select { 62 | background-color: #FFF; 63 | border-color: #DDD; 64 | padding: 5px; 65 | } 66 | -------------------------------------------------------------------------------- /htdocs/css/openwebrx-globals.css: -------------------------------------------------------------------------------- 1 | html, body 2 | { 3 | margin: 0; 4 | padding: 0; 5 | height: 100%; 6 | font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /htdocs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/favicon.ico -------------------------------------------------------------------------------- /htdocs/features.html: -------------------------------------------------------------------------------- 1 | 2 | OpenWebRX Feature report 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ${header} 12 |
13 | ${breadcrumb} 14 |

OpenWebRX Feature Report

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
FeatureRequirementDescriptionAvailable
23 | ${breadcrumb} 24 |
25 | -------------------------------------------------------------------------------- /htdocs/features.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var converter = new showdown.Converter({openLinksInNewWindow: true}); 3 | $.ajax('api/features').done(function(data){ 4 | var $table = $('table.features'); 5 | $.each(data, function(name, details) { 6 | var requirements = $.map(details.requirements, function(r, name){ 7 | return '' + 8 | '' + 9 | '' + name + '' + 10 | '' + converter.makeHtml(r.description) + '' + 11 | '' + (r.available ? 'YES' : 'NO') + '' + 12 | ''; 13 | }); 14 | $table.append( 15 | '' + 16 | '' + name + '' + 17 | '' + (details.available ? 'YES' : 'NO') + '' + 18 | '' + 19 | requirements.join("") 20 | ); 21 | }) 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /htdocs/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /htdocs/fonts/RobotoMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/fonts/RobotoMono-Regular.woff -------------------------------------------------------------------------------- /htdocs/fonts/RobotoMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/fonts/RobotoMono-Regular.woff2 -------------------------------------------------------------------------------- /htdocs/gfx/favicon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/favicon128.png -------------------------------------------------------------------------------- /htdocs/gfx/favicon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/favicon32.png -------------------------------------------------------------------------------- /htdocs/gfx/favicon44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/favicon44.png -------------------------------------------------------------------------------- /htdocs/gfx/favicon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/favicon64.png -------------------------------------------------------------------------------- /htdocs/gfx/favicon96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/favicon96.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/openwebrx-avatar.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-background-cool-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/openwebrx-background-cool-blue.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-background-cool-blue.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/openwebrx-background-cool-blue.webp -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-directcall.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-groupcall.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-scale-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/openwebrx-scale-background.png -------------------------------------------------------------------------------- /htdocs/gfx/openwebrx-top-photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/gfx/openwebrx-top-photo.jpg -------------------------------------------------------------------------------- /htdocs/include/header.include.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Receiver avatar 5 |
6 |

${receiver_name}

7 |
${receiver_location} | Loc: ${locator}, ASL: ${receiver_asl} m
8 |
9 |
10 |

Status
11 |

Log
12 |

Receiver
13 |
Map
14 |
Settings
15 |
16 |
17 |
18 |
${photo_title}
19 |
${photo_desc}
20 |
21 | 22 | 23 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /htdocs/lib/BookmarkDialog.js: -------------------------------------------------------------------------------- 1 | $.fn.bookmarkDialog = function() { 2 | var $el = this; 3 | return { 4 | setModes: function(modes) { 5 | $el.find('#modulation').html(modes.filter(function(m){ 6 | return m.isAvailable(); 7 | }).map(function(m) { 8 | return ''; 9 | }).join('')); 10 | return this; 11 | }, 12 | setValues: function(bookmark) { 13 | var $form = $el.find('form'); 14 | ['name', 'frequency', 'modulation'].forEach(function(key){ 15 | $form.find('#' + key).val(bookmark[key]); 16 | }); 17 | $el.data('id', bookmark.id || false); 18 | return this; 19 | }, 20 | getValues: function() { 21 | var bookmark = {}; 22 | var valid = true; 23 | ['name', 'frequency', 'modulation'].forEach(function(key){ 24 | var $input = $el.find('#' + key); 25 | valid = valid && $input[0].checkValidity(); 26 | bookmark[key] = $input.val(); 27 | }); 28 | if (!valid) { 29 | $el.find("form :submit").click(); 30 | return; 31 | } 32 | bookmark.id = $el.data('id'); 33 | return bookmark; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /htdocs/lib/BookmarkLocalStorage.js: -------------------------------------------------------------------------------- 1 | BookmarkLocalStorage = function(){ 2 | }; 3 | 4 | BookmarkLocalStorage.prototype.getBookmarks = function(){ 5 | return JSON.parse(window.localStorage.getItem("bookmarks")) || []; 6 | }; 7 | 8 | BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){ 9 | window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks)); 10 | }; 11 | 12 | BookmarkLocalStorage.prototype.deleteBookmark = function(data) { 13 | if (data.id) data = data.id; 14 | var bookmarks = this.getBookmarks(); 15 | bookmarks = bookmarks.filter(function(b) { return b.id !== data; }); 16 | this.setBookmarks(bookmarks); 17 | }; 18 | -------------------------------------------------------------------------------- /htdocs/lib/Header.js: -------------------------------------------------------------------------------- 1 | function Header(el) { 2 | this.el = el; 3 | 4 | var $buttons = this.el.find('.openwebrx-main-buttons').find('[data-toggle-panel]').filter(function(){ 5 | // ignore buttons when the corresponding panel is not in the DOM 6 | return $('#' + $(this).data('toggle-panel'))[0]; 7 | }); 8 | 9 | $buttons.css({display: 'block'}).click(function () { 10 | toggle_panel($(this).data('toggle-panel')); 11 | }); 12 | 13 | this.init_rx_photo(); 14 | }; 15 | 16 | Header.prototype.setDetails = function(details) { 17 | this.el.find('.webrx-rx-title').html(details['receiver_name']); 18 | this.el.find('.webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m'); 19 | this.el.find('.webrx-rx-photo-title').html(details['photo_title']); 20 | this.el.find('.webrx-rx-photo-desc').html(details['photo_desc']); 21 | }; 22 | 23 | Header.prototype.init_rx_photo = function() { 24 | this.rx_photo_state = 0; 25 | 26 | $.extend($.easing, { 27 | easeOutCubic:function(x) { 28 | return 1 - Math.pow( 1 - x, 3 ); 29 | } 30 | }); 31 | 32 | $('.webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this)); 33 | }; 34 | 35 | Header.prototype.close_rx_photo = function() { 36 | this.rx_photo_state = 0; 37 | this.el.find('.openwebrx-description-container').removeClass('expanded'); 38 | this.el.find(".openwebrx-rx-details-arrow").removeClass('openwebrx-rx-details-arrow--up').addClass('openwebrx-rx-details-arrow--down'); 39 | } 40 | 41 | Header.prototype.open_rx_photo = function() { 42 | this.rx_photo_state = 1; 43 | this.el.find('.openwebrx-description-container').addClass('expanded'); 44 | this.el.find(".openwebrx-rx-details-arrow").removeClass('openwebrx-rx-details-arrow--down').addClass('openwebrx-rx-details-arrow--up'); 45 | } 46 | 47 | Header.prototype.toggle_rx_photo = function(ev) { 48 | if (ev && ev.target && ev.target.tagName == 'A') { 49 | return; 50 | } 51 | if (this.rx_photo_state) { 52 | this.close_rx_photo(); 53 | } else { 54 | this.open_rx_photo(); 55 | } 56 | }; 57 | 58 | $.fn.header = function() { 59 | if (!this.data('header')) { 60 | this.data('header', new Header(this)); 61 | } 62 | return this.data('header'); 63 | }; 64 | 65 | $(function(){ 66 | $('.webrx-top-container').header(); 67 | }); 68 | -------------------------------------------------------------------------------- /htdocs/lib/Measurement.js: -------------------------------------------------------------------------------- 1 | function Measurement() { 2 | this.reporters = []; 3 | this.reset(); 4 | } 5 | 6 | Measurement.prototype.add = function(v) { 7 | this.value += v; 8 | }; 9 | 10 | Measurement.prototype.getValue = function() { 11 | return this.value; 12 | }; 13 | 14 | Measurement.prototype.getElapsed = function() { 15 | return new Date() - this.start; 16 | }; 17 | 18 | Measurement.prototype.getRate = function() { 19 | return this.getValue() / this.getElapsed(); 20 | }; 21 | 22 | Measurement.prototype.reset = function() { 23 | this.value = 0; 24 | this.start = new Date(); 25 | this.reporters.forEach(function(r){ r.reset(); }); 26 | }; 27 | 28 | Measurement.prototype.report = function(range, interval, callback) { 29 | var reporter = new Reporter(this, range, interval, callback); 30 | this.reporters.push(reporter); 31 | return reporter; 32 | }; 33 | 34 | function Reporter(measurement, range, interval, callback) { 35 | this.measurement = measurement; 36 | this.range = range; 37 | this.samples = []; 38 | this.callback = callback; 39 | this.interval = setInterval(this.report.bind(this), interval); 40 | } 41 | 42 | Reporter.prototype.sample = function(){ 43 | this.samples.push({ 44 | timestamp: new Date(), 45 | value: this.measurement.getValue() 46 | }); 47 | }; 48 | 49 | Reporter.prototype.report = function(){ 50 | this.sample(); 51 | var now = new Date(); 52 | var minDate = now.getTime() - this.range; 53 | this.samples = this.samples.filter(function(s) { 54 | return s.timestamp.getTime() > minDate; 55 | }); 56 | this.samples.sort(function(a, b) { 57 | return a.timestamp - b.timestamp; 58 | }); 59 | var oldest = this.samples[0]; 60 | var newest = this.samples[this.samples.length -1]; 61 | var elapsed = newest.timestamp - oldest.timestamp; 62 | if (elapsed <= 0) return; 63 | var accumulated = newest.value - oldest.value; 64 | // we want rate per second, but our time is in milliseconds... compensate by 1000 65 | this.callback(accumulated * 1000 / elapsed); 66 | }; 67 | 68 | Reporter.prototype.reset = function(){ 69 | this.samples = []; 70 | }; -------------------------------------------------------------------------------- /htdocs/lib/Modes.js: -------------------------------------------------------------------------------- 1 | var Modes = { 2 | modes: [], 3 | features: {}, 4 | panels: [], 5 | setModes:function(json){ 6 | this.modes = json.map(function(m){ return new Mode(m); }); 7 | this.updatePanels(); 8 | $('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes); 9 | }, 10 | getModes:function(){ 11 | return this.modes; 12 | }, 13 | setFeatures:function(features){ 14 | this.features = features; 15 | this.updatePanels(); 16 | }, 17 | findByModulation:function(modulation){ 18 | matches = this.modes.filter(function(m) { return m.modulation === modulation; }); 19 | if (matches.length) return matches[0] 20 | }, 21 | registerModePanel: function(el) { 22 | this.panels.push(el); 23 | }, 24 | initComplete: function() { 25 | return this.modes.length && Object.keys(this.features).length; 26 | }, 27 | updatePanels: function() { 28 | this.panels.forEach(function(p) { 29 | p.render(); 30 | p.startDemodulator(); 31 | }); 32 | } 33 | }; 34 | 35 | var Mode = function(json){ 36 | this.modulation = json.modulation; 37 | this.name = json.name; 38 | this.type = json.type; 39 | this.requirements = json.requirements; 40 | this.squelch = json.squelch; 41 | if (json.bandpass) { 42 | this.bandpass = json.bandpass; 43 | } 44 | if (json.ifRate) { 45 | this.ifRate = json.ifRate; 46 | } 47 | if (this.type === 'digimode') { 48 | this.underlying = json.underlying; 49 | this.secondaryFft = json.secondaryFft; 50 | } 51 | }; 52 | 53 | Mode.prototype.isAvailable = function(){ 54 | return this.requirements.map(function(r){ 55 | return Modes.features[r]; 56 | }).reduce(function(a, b){ 57 | return a && b; 58 | }, true); 59 | }; 60 | -------------------------------------------------------------------------------- /htdocs/lib/nanoscroller.css: -------------------------------------------------------------------------------- 1 | /** initial setup **/ 2 | .nano { 3 | position : relative; 4 | width : 100%; 5 | height : 100%; 6 | overflow : hidden; 7 | } 8 | .nano > .nano-content { 9 | position : absolute; 10 | overflow : scroll; 11 | overflow-x : hidden; 12 | top : 0; 13 | right : 0; 14 | bottom : 0; 15 | left : 0; 16 | } 17 | .nano > .nano-content:focus { 18 | outline: thin dotted; 19 | } 20 | .nano > .nano-content::-webkit-scrollbar { 21 | display: none; 22 | } 23 | .has-scrollbar > .nano-content::-webkit-scrollbar { 24 | display: block; 25 | } 26 | .nano > .nano-pane { 27 | background : rgba(0,0,0,.25); 28 | position : absolute; 29 | width : 8px; 30 | right : 0; 31 | top : 0; 32 | bottom : 0; 33 | visibility : hidden\9; /* Target only IE7 and IE8 with this hack */ 34 | opacity : .01; 35 | -webkit-transition : .2s; 36 | -moz-transition : .2s; 37 | -o-transition : .2s; 38 | transition : .2s; 39 | -moz-border-radius : 3px; 40 | -webkit-border-radius : 3px; 41 | border-radius : 3px; 42 | } 43 | .nano > .nano-pane > .nano-slider { 44 | background: #444; 45 | background: rgba(0,0,0,.5); 46 | position : relative; 47 | margin : 0 0px; 48 | -moz-border-radius : 4px; 49 | -webkit-border-radius : 4px; 50 | border-radius : 4px; 51 | } 52 | .nano:hover > .nano-pane, .nano-pane.active, .nano-pane.flashed { 53 | visibility : visible\9; /* Target only IE7 and IE8 with this hack */ 54 | opacity : 0.99; 55 | } 56 | -------------------------------------------------------------------------------- /htdocs/lib/settings/ExponentialInput.js: -------------------------------------------------------------------------------- 1 | $.fn.exponentialInput = function() { 2 | var prefixes = { 3 | 'K': 3, 4 | 'M': 6, 5 | 'G': 9, 6 | 'T': 12 7 | }; 8 | 9 | this.each(function(){ 10 | var $group = $(this); 11 | var currentExponent = 0; 12 | var $input = $group.find('input'); 13 | 14 | var setExponent = function() { 15 | var newExponent = parseInt($exponent.val()); 16 | var delta = currentExponent - newExponent; 17 | if (delta >= 0) { 18 | $input.val(parseFloat($input.val()) * 10 ** delta); 19 | } else { 20 | // should not be necessary to handle this separately, but floating point precision in javascript 21 | // does not handle this well otherwise 22 | $input.val(parseFloat($input.val()) / 10 ** -delta); 23 | } 24 | currentExponent = newExponent; 25 | }; 26 | 27 | $input.on('keydown', function(e) { 28 | var c = String.fromCharCode(e.which); 29 | if (c in prefixes) { 30 | currentExponent = prefixes[c]; 31 | $exponent.val(prefixes[c]); 32 | } 33 | }); 34 | 35 | var $exponent = $group.find('select.exponent'); 36 | $exponent.on('change', setExponent); 37 | 38 | // calculate initial exponent 39 | var value = parseFloat($input.val()); 40 | if (!Number.isNaN(value)) { 41 | $exponent.val(Math.floor(Math.log10(Math.abs(value)) / 3) * 3); 42 | setExponent(); 43 | } 44 | }) 45 | }; -------------------------------------------------------------------------------- /htdocs/lib/settings/GainInput.js: -------------------------------------------------------------------------------- 1 | $.fn.gainInput = function() { 2 | this.each(function() { 3 | var $container = $(this); 4 | 5 | var update = function(value){ 6 | $container.find('.option').hide(); 7 | $container.find('.option.' + value).show(); 8 | } 9 | 10 | var $select = $container.find('select'); 11 | $select.on('change', function(e) { 12 | var value = $(e.target).val(); 13 | update(value); 14 | }); 15 | update($select.val()); 16 | }); 17 | } -------------------------------------------------------------------------------- /htdocs/lib/settings/LogMessages.js: -------------------------------------------------------------------------------- 1 | $.fn.logMessages = function() { 2 | $.each(this, function(){ 3 | $(this).scrollTop(this.scrollHeight); 4 | }); 5 | }; -------------------------------------------------------------------------------- /htdocs/lib/settings/MapInput.js: -------------------------------------------------------------------------------- 1 | $.fn.mapInput = function() { 2 | this.each(function(el) { 3 | var $el = $(this); 4 | var field_id = $el.attr("for"); 5 | var $lat = $('#' + field_id + '-lat'); 6 | var $lon = $('#' + field_id + '-lon'); 7 | $.getScript('https://maps.googleapis.com/maps/api/js?key=' + $el.data('key')).done(function(){ 8 | $el.css('height', '200px'); 9 | var lp = new locationPicker($el.get(0), { 10 | lat: parseFloat($lat.val()), 11 | lng: parseFloat($lon.val()) 12 | }, { 13 | zoom: 7 14 | }); 15 | 16 | google.maps.event.addListener(lp.map, 'idle', function(event){ 17 | var pos = lp.getMarkerPosition(); 18 | $lat.val(pos.lat); 19 | $lon.val(pos.lng); 20 | }); 21 | }); 22 | }); 23 | }; -------------------------------------------------------------------------------- /htdocs/lib/settings/OptionalSection.js: -------------------------------------------------------------------------------- 1 | $.fn.optionalSection = function(){ 2 | this.each(function() { 3 | var $section = $(this); 4 | var $select = $section.find('.optional-select'); 5 | var $optionalInputs = $section.find('.optional-inputs'); 6 | $section.on('click', '.option-add-button', function(e){ 7 | var field = $select.val(); 8 | var group = $optionalInputs.find(".form-group[data-field='" + field + "']"); 9 | group.find('input, select').filter(function(){ 10 | // exclude template inputs 11 | return !$(this).parents('.template').length; 12 | }).prop('disabled', false); 13 | $section.find('hr').before(group); 14 | $select.find('option[value=\'' + field + '\']').remove(); 15 | 16 | return false; 17 | }); 18 | $section.on('click', '.option-remove-button', function(e) { 19 | var group = $(e.target).parents('.form-group') 20 | group.find('input, select').prop('disabled', true); 21 | $optionalInputs.append(group); 22 | var $label = group.find('label'); 23 | var $option = $(''); 24 | $select.append($option); 25 | 26 | return false; 27 | }) 28 | }); 29 | } -------------------------------------------------------------------------------- /htdocs/lib/settings/SchedulerInput.js: -------------------------------------------------------------------------------- 1 | $.fn.schedulerInput = function() { 2 | this.each(function() { 3 | var $container = $(this); 4 | var $template = $container.find('.template'); 5 | $template.find('input, select').prop('disabled', true); 6 | 7 | var update = function(value){ 8 | $container.find('.option').hide(); 9 | $container.find('.option.' + value).show(); 10 | } 11 | 12 | var $select = $container.find('select.mode'); 13 | $select.on('change', function(e) { 14 | var value = $(e.target).val(); 15 | update(value); 16 | }); 17 | update($select.val()); 18 | 19 | $container.find('.add-button').on('click', function() { 20 | var row = $template.clone(); 21 | row.removeClass('template').show(); 22 | row.find('input, select').prop('disabled', false); 23 | $template.before(row); 24 | 25 | return false; 26 | }); 27 | 28 | $container.on('click', '.remove-button', function(e) { 29 | var row = $(e.target).parents('.scheduler-static-time-inputs'); 30 | row.remove(); 31 | }); 32 | }); 33 | } -------------------------------------------------------------------------------- /htdocs/lib/settings/WaterfallDropdown.js: -------------------------------------------------------------------------------- 1 | $.fn.waterfallDropdown = function(){ 2 | this.each(function(){ 3 | var $select = $(this); 4 | var setVisibility = function() { 5 | var show = $select.val() === 'CUSTOM'; 6 | $('#waterfall_colors').parents('.form-group')[show ? 'show' : 'hide'](); 7 | } 8 | $select.on('change', setVisibility); 9 | setVisibility(); 10 | }) 11 | } -------------------------------------------------------------------------------- /htdocs/lib/settings/WsjtDecodingDepthsInput.js: -------------------------------------------------------------------------------- 1 | $.fn.wsjtDecodingDepthsInput = function() { 2 | function WsjtDecodingDepthRow(inputs, mode, value) { 3 | this.el = $(''); 4 | this.modeInput = $(inputs.get(0)).clone(); 5 | this.modeInput.val(mode); 6 | this.valueInput = $(inputs.get(1)).clone(); 7 | this.valueInput.val(value); 8 | this.removeButton = $(''); 9 | this.removeButton.data('row', this); 10 | this.el.append([this.modeInput, this.valueInput, this.removeButton].map(function(i) { 11 | return $('').append(i); 12 | })); 13 | } 14 | 15 | WsjtDecodingDepthRow.prototype.getEl = function() { 16 | return this.el; 17 | } 18 | 19 | WsjtDecodingDepthRow.prototype.getValue = function() { 20 | var value = parseInt(this.valueInput.val()) 21 | if (Number.isNaN(value)) { 22 | return {}; 23 | } 24 | return Object.fromEntries([[this.modeInput.val(), value]]); 25 | } 26 | 27 | this.each(function(){ 28 | var $input = $(this); 29 | var $el = $input.parent(); 30 | var $inputs = $el.find('.inputs') 31 | var inputs = $inputs.find('input, select'); 32 | $inputs.remove(); 33 | var rows = $.map(JSON.parse($input.val()), function(value, mode) { 34 | return new WsjtDecodingDepthRow(inputs, mode, value); 35 | }); 36 | var $table = $(''); 37 | $table.append(rows.map(function(r) { 38 | return r.getEl(); 39 | })); 40 | 41 | var updateValue = function(){ 42 | $input.val(JSON.stringify($.extend.apply({}, rows.map(function(r) { 43 | return r.getValue(); 44 | })))); 45 | }; 46 | 47 | $table.on('change', updateValue); 48 | var $addButton = $(''); 49 | 50 | $addButton.on('click', function() { 51 | var row = new WsjtDecodingDepthRow(inputs) 52 | rows.push(row); 53 | $table.append(row.getEl()); 54 | return false; 55 | }); 56 | $el.on('click', '.btn.remove', function(e){ 57 | var row = $(e.target).data('row'); 58 | var index = rows.indexOf(row); 59 | if (index < 0) return false; 60 | rows.splice(index, 1); 61 | row.getEl().remove(); 62 | updateValue(); 63 | return false; 64 | }); 65 | 66 | $input.after($table, $addButton); 67 | }); 68 | }; -------------------------------------------------------------------------------- /htdocs/lib/wheelDelta.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Normalize scroll wheel events. 3 | * 4 | * It seems like there's no consent as to how mouse wheel events are presented in the javascript API. The standard 5 | * states that a MouseEvent has a deltaY property that contains the scroll distances, together with a deltaMode 6 | * property that state the "unit" that deltaY has been measured in. The deltaMode can be either pixels, lines or 7 | * pages. The latter is seldomly used in practise. 8 | * 9 | * The troublesome part is that there is no standard on how to correlate the two at this point. 10 | * 11 | * The basic idea is that one tick of a mouse wheel results in a total return value of +/- 1 from this method. 12 | * It's important to keep in mind that one tick of a wheel may result in multiple events in the browser. The aim 13 | * of this method is to scale the sum of deltaY over 14 | */ 15 | function wheelDelta(evt) { 16 | if ('deltaMode' in evt && evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { 17 | // chrome and webkit-based browsers seem to correlate one tick of the wheel to 120 pixels. 18 | return evt.deltaY / 120; 19 | } 20 | // firefox seems to scroll at an interval of 6 lines per wheel tick 21 | return evt.deltaY / 6; 22 | } 23 | -------------------------------------------------------------------------------- /htdocs/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenWebRX Login 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ${header} 14 | 29 | -------------------------------------------------------------------------------- /htdocs/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenWebRX Map 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ${header} 16 |
17 |
18 |

Colors

19 | 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /htdocs/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/htdocs/mstile-144x144.png -------------------------------------------------------------------------------- /htdocs/pwchange.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenWebRX Password change 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ${header} 14 | 32 | -------------------------------------------------------------------------------- /htdocs/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenWebRX Settings 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ${header} 13 |
14 |
15 |

Settings

16 |
17 |
18 |
19 | General settings 20 |
21 | 24 |
25 | Bookmark editor 26 |
27 | 30 | 33 | 36 |
37 | Feature report 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /htdocs/settings.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $('.map-input').mapInput(); 3 | $('.imageupload').imageUpload(); 4 | $('.bookmarks').bookmarktable(); 5 | $('.wsjt-decoding-depths').wsjtDecodingDepthsInput(); 6 | $('#waterfall_scheme').waterfallDropdown(); 7 | $('#rf_gain').gainInput(); 8 | $('.optional-section').optionalSection(); 9 | $('#scheduler').schedulerInput(); 10 | $('.exponential-input').exponentialInput(); 11 | $('.device-log-messages').logMessages(); 12 | }); -------------------------------------------------------------------------------- /htdocs/settings/general.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenWebRX Settings 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ${header} 13 |
14 | ${breadcrumb} 15 | ${error} 16 |
17 |

${title}

18 |
19 | ${content} 20 | ${breadcrumb} 21 |
22 | ${modal} 23 | -------------------------------------------------------------------------------- /inkscape files/openwebrx-bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 52 | 56 | 57 | -------------------------------------------------------------------------------- /inkscape files/openwebrx-edit.svg: -------------------------------------------------------------------------------- 1 | 2 | to editimage/svg+xmlShannon E Thomashttp://www.toicon.com/icons/lines-and-angles_editimage/svg+xmlto edit 53 | -------------------------------------------------------------------------------- /inkscape files/openwebrx-play-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 51 | 54 | 62 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /inkscape files/openwebrx-trashcan.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 54 | 58 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /openwebrx.conf: -------------------------------------------------------------------------------- 1 | [core] 2 | data_directory = /var/lib/openwebrx 3 | temporary_directory = /tmp 4 | log_level = INFO 5 | 6 | [web] 7 | port = 8073 8 | ipv6 = true 9 | # Uncomment bind_address to bind OpenWebRX to a specific IP-address. 10 | # By default, OpenWebRX will bind to all interfaces. 11 | # Use ::1 for localhost only, or any other configured address to bind to that address only. 12 | #bind_address = ::1 13 | 14 | [aprs] 15 | # path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols) 16 | symbols_path = /usr/share/aprs-symbols/png 17 | -------------------------------------------------------------------------------- /openwebrx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from owrx.__main__ import main 5 | 6 | if __name__ == "__main__": 7 | sys.exit(main()) 8 | -------------------------------------------------------------------------------- /owrx/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from owrx.admin.commands import NewUser, DeleteUser, ResetPassword, ListUsers, DisableUser, EnableUser, HasUser 2 | import sys 3 | import traceback 4 | 5 | 6 | def add_admin_parser(moduleparser): 7 | subparsers = moduleparser.add_subparsers(title="Commands", dest="command") 8 | 9 | adduser_parser = subparsers.add_parser("adduser", help="Add a new user") 10 | adduser_parser.add_argument("user", help="Username to be added") 11 | adduser_parser.set_defaults(cls=NewUser) 12 | 13 | removeuser_parser = subparsers.add_parser("removeuser", help="Remove an existing user") 14 | removeuser_parser.add_argument("user", help="Username to be remvoed") 15 | removeuser_parser.set_defaults(cls=DeleteUser) 16 | 17 | resetpassword_parser = subparsers.add_parser("resetpassword", help="Reset a user's password") 18 | resetpassword_parser.add_argument("user", help="Username to be remvoed") 19 | resetpassword_parser.set_defaults(cls=ResetPassword) 20 | 21 | listusers_parser = subparsers.add_parser("listusers", help="List enabled users") 22 | listusers_parser.add_argument("-a", "--all", action="store_true", help="Show all users (including disabled ones)") 23 | listusers_parser.set_defaults(cls=ListUsers) 24 | 25 | disableuser_parser = subparsers.add_parser("disableuser", help="Disable a user") 26 | disableuser_parser.add_argument("user", help="Username to be disabled") 27 | disableuser_parser.set_defaults(cls=DisableUser) 28 | 29 | enableuser_parser = subparsers.add_parser("enableuser", help="Enable a user") 30 | enableuser_parser.add_argument("user", help="Username to be enabled") 31 | enableuser_parser.set_defaults(cls=EnableUser) 32 | 33 | hasuser_parser = subparsers.add_parser("hasuser", help="Test if a user exists") 34 | hasuser_parser.add_argument("user", help="Username to be checked") 35 | hasuser_parser.set_defaults(cls=HasUser) 36 | 37 | moduleparser.add_argument( 38 | "--noninteractive", action="store_true", help="Don't ask for any user input (useful for automation)" 39 | ) 40 | moduleparser.add_argument("--silent", action="store_true", help="Ignore errors (useful for automation)") 41 | 42 | 43 | def run_admin_action(parser, args): 44 | if hasattr(args, "cls"): 45 | command = args.cls() 46 | else: 47 | if not hasattr(args, "silent") or not args.silent: 48 | parser.print_help() 49 | return 1 50 | return 0 51 | 52 | try: 53 | return command.run(args) 54 | except Exception: 55 | if not hasattr(args, "silent") or not args.silent: 56 | print("Error running command:") 57 | traceback.print_exc() 58 | return 1 59 | return 0 60 | -------------------------------------------------------------------------------- /owrx/adsb/dump1090.py: -------------------------------------------------------------------------------- 1 | from pycsdr.modules import ExecModule 2 | from pycsdr.types import Format 3 | from csdr.module import LineBasedModule 4 | 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Dump1090Module(ExecModule): 11 | def __init__(self): 12 | super().__init__( 13 | Format.COMPLEX_SHORT, 14 | Format.CHAR, 15 | ["dump1090", "--ifile", "-", "--iformat", "SC16", "--raw"], 16 | # send some data on decoder shutdown since the dump1090 internal reader locks up otherwise 17 | # dump1090 reads chunks of 100ms, which equals to 240k samples at 2.4MS/s 18 | # some extra should not hurt 19 | flushSize=300000 20 | ) 21 | 22 | 23 | class RawDeframer(LineBasedModule): 24 | def process(self, line: bytes): 25 | if line.startswith(b'*') and line.endswith(b';') and len(line) in [16, 30]: 26 | return bytes.fromhex(line[1:-1].decode()) 27 | elif line == b"*0000;": 28 | # heartbeat message. not a valid message, but known. do not log. 29 | return 30 | else: 31 | logger.warning("invalid raw message: %s", line) 32 | -------------------------------------------------------------------------------- /owrx/aprs/kiss.py: -------------------------------------------------------------------------------- 1 | from pycsdr.types import Format 2 | from csdr.module import ThreadModule 3 | import pickle 4 | 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | FEND = 0xC0 10 | FESC = 0xDB 11 | TFEND = 0xDC 12 | TFESC = 0xDD 13 | 14 | 15 | class KissDeframer(ThreadModule): 16 | def __init__(self): 17 | self.escaped = False 18 | self.buf = bytearray() 19 | super().__init__() 20 | 21 | def getInputFormat(self) -> Format: 22 | return Format.CHAR 23 | 24 | def getOutputFormat(self) -> Format: 25 | return Format.CHAR 26 | 27 | def run(self): 28 | while self.doRun: 29 | data = self.reader.read() 30 | if data is None: 31 | self.doRun = False 32 | else: 33 | for frame in self.parse(data): 34 | self.writer.write(pickle.dumps(frame)) 35 | 36 | def parse(self, input): 37 | for b in input: 38 | if b == FESC: 39 | self.escaped = True 40 | elif self.escaped: 41 | if b == TFEND: 42 | self.buf.append(FEND) 43 | elif b == TFESC: 44 | self.buf.append(FESC) 45 | else: 46 | logger.warning("invalid escape char: %s", str(input[0])) 47 | self.escaped = False 48 | elif b == FEND: 49 | # data frames start with 0x00 50 | if len(self.buf) > 1 and self.buf[0] == 0x00: 51 | yield self.buf[1:] 52 | self.buf = bytearray() 53 | else: 54 | self.buf.append(b) 55 | -------------------------------------------------------------------------------- /owrx/audio/__init__.py: -------------------------------------------------------------------------------- 1 | from owrx.config import Config 2 | from abc import ABC, ABCMeta, abstractmethod 3 | from typing import List 4 | 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class AudioChopperProfile(ABC): 11 | @abstractmethod 12 | def getInterval(self): 13 | pass 14 | 15 | @abstractmethod 16 | def getFileTimestampFormat(self): 17 | pass 18 | 19 | @abstractmethod 20 | def decoder_commandline(self, file): 21 | pass 22 | 23 | 24 | class ProfileSourceSubscriber(ABC): 25 | @abstractmethod 26 | def onProfilesChanged(self): 27 | pass 28 | 29 | 30 | class ProfileSource(ABC): 31 | def __init__(self): 32 | self.subscribers = [] 33 | 34 | @abstractmethod 35 | def getProfiles(self) -> List[AudioChopperProfile]: 36 | pass 37 | 38 | def subscribe(self, subscriber: ProfileSourceSubscriber): 39 | if subscriber in self.subscribers: 40 | return 41 | self.subscribers.append(subscriber) 42 | 43 | def unsubscribe(self, subscriber: ProfileSourceSubscriber): 44 | if subscriber not in self.subscribers: 45 | return 46 | self.subscribers.remove(subscriber) 47 | 48 | def fireProfilesChanged(self): 49 | for sub in self.subscribers.copy(): 50 | try: 51 | sub.onProfilesChanged() 52 | except Exception: 53 | logger.exception("Error while notifying profile subscriptions") 54 | 55 | 56 | class ConfigWiredProfileSource(ProfileSource, metaclass=ABCMeta): 57 | def __init__(self): 58 | super().__init__() 59 | self.configSub = None 60 | 61 | @abstractmethod 62 | def getPropertiesToWire(self) -> List[str]: 63 | pass 64 | 65 | def subscribe(self, subscriber: ProfileSourceSubscriber): 66 | super().subscribe(subscriber) 67 | if self.subscribers and self.configSub is None: 68 | self.configSub = Config.get().filter(*self.getPropertiesToWire()).wire(self.fireProfilesChanged) 69 | 70 | def unsubscribe(self, subscriber: ProfileSourceSubscriber): 71 | super().unsubscribe(subscriber) 72 | if not self.subscribers and self.configSub is not None: 73 | self.configSub.cancel() 74 | self.configSub = None 75 | 76 | def fireProfilesChanged(self, *args): 77 | super().fireProfilesChanged() 78 | 79 | 80 | class StaticProfileSource(ProfileSource): 81 | def __init__(self, profiles: List[AudioChopperProfile]): 82 | super().__init__() 83 | self.profiles = profiles 84 | 85 | def getProfiles(self) -> List[AudioChopperProfile]: 86 | return self.profiles 87 | -------------------------------------------------------------------------------- /owrx/breadcrumb.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from abc import ABC, abstractmethod 3 | 4 | 5 | class BreadcrumbItem(object): 6 | def __init__(self, title, href): 7 | self.title = title 8 | self.href = href 9 | 10 | def render(self, documentRoot, active=False): 11 | return ''.format( 12 | documentRoot=documentRoot, href=self.href, title=self.title, active="active" if active else "" 13 | ) 14 | 15 | 16 | class Breadcrumb(object): 17 | def __init__(self, breadcrumbs: List[BreadcrumbItem]): 18 | self.items = breadcrumbs 19 | 20 | def render(self, documentRoot): 21 | return """ 22 | 26 | """.format( 27 | crumbs="".join(item.render(documentRoot) for item in self.items[:-1]), 28 | last_crumb="".join(item.render(documentRoot, True) for item in self.items[-1:]), 29 | ) 30 | 31 | def append(self, crumb: BreadcrumbItem): 32 | self.items.append(crumb) 33 | return self 34 | 35 | 36 | class BreadcrumbMixin(ABC): 37 | def template_variables(self): 38 | variables = super().template_variables() 39 | variables["breadcrumb"] = self.get_breadcrumb().render(self.get_document_root()) 40 | return variables 41 | 42 | @abstractmethod 43 | def get_breadcrumb(self) -> Breadcrumb: 44 | pass 45 | -------------------------------------------------------------------------------- /owrx/client.py: -------------------------------------------------------------------------------- 1 | from owrx.config import Config 2 | import threading 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class TooManyClientsException(Exception): 10 | pass 11 | 12 | 13 | class ClientRegistry(object): 14 | sharedInstance = None 15 | creationLock = threading.Lock() 16 | 17 | @staticmethod 18 | def getSharedInstance(): 19 | with ClientRegistry.creationLock: 20 | if ClientRegistry.sharedInstance is None: 21 | ClientRegistry.sharedInstance = ClientRegistry() 22 | return ClientRegistry.sharedInstance 23 | 24 | def __init__(self): 25 | self.clients = [] 26 | Config.get().wireProperty("max_clients", self._checkClientCount) 27 | super().__init__() 28 | 29 | def broadcast(self): 30 | n = self.clientCount() 31 | for c in self.clients: 32 | c.write_clients(n) 33 | 34 | def addClient(self, client): 35 | pm = Config.get() 36 | if len(self.clients) >= pm["max_clients"]: 37 | raise TooManyClientsException() 38 | self.clients.append(client) 39 | self.broadcast() 40 | 41 | def clientCount(self): 42 | return len(self.clients) 43 | 44 | def removeClient(self, client): 45 | try: 46 | self.clients.remove(client) 47 | except ValueError: 48 | pass 49 | self.broadcast() 50 | 51 | def _checkClientCount(self, new_count): 52 | for client in self.clients[new_count:]: 53 | logger.debug("closing one connection...") 54 | client.close() 55 | -------------------------------------------------------------------------------- /owrx/command.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class CommandMapper(object): 5 | def __init__(self, base=None, mappings=None, static=None): 6 | self.base = base 7 | self.mappings = {} if mappings is None else mappings 8 | self.static = static 9 | 10 | def map(self, values): 11 | args = [self.mappings[k].map(v) for k, v in values.items() if k in self.mappings] 12 | args = [a for a in args if a != ""] 13 | options = " ".join(args) 14 | command = "{0} {1}".format(self.base, options) 15 | if self.static is not None: 16 | command += " " + self.static 17 | return command 18 | 19 | def setMapping(self, key, mapping): 20 | self.mappings[key] = mapping 21 | return self 22 | 23 | def setMappings(self, mappings): 24 | for k, v in mappings.items(): 25 | self.setMapping(k, v) 26 | return self 27 | 28 | def setBase(self, base): 29 | self.base = base 30 | return self 31 | 32 | def setStatic(self, static): 33 | self.static = static 34 | return self 35 | 36 | def keys(self): 37 | return self.mappings.keys() 38 | 39 | 40 | class CommandMapping(ABC): 41 | @abstractmethod 42 | def map(self, value): 43 | pass 44 | 45 | 46 | class Flag(CommandMapping): 47 | def __init__(self, flag): 48 | self.flag = flag 49 | 50 | def map(self, value): 51 | if value is not None and value: 52 | return self.flag 53 | else: 54 | return "" 55 | 56 | 57 | class Option(CommandMapping): 58 | def __init__(self, option): 59 | self.option = option 60 | self.spacer = " " 61 | 62 | def map(self, value): 63 | if value is not None: 64 | if isinstance(value, str) and " " in value: 65 | template = '{option}{spacer}"{value}"' 66 | else: 67 | template = "{option}{spacer}{value}" 68 | return template.format(option=self.option, spacer=self.spacer, value=value) 69 | else: 70 | return "" 71 | 72 | def setSpacer(self, spacer): 73 | self.spacer = spacer 74 | return self 75 | 76 | 77 | class Argument(CommandMapping): 78 | def map(self, value): 79 | return str(value) 80 | -------------------------------------------------------------------------------- /owrx/config/__init__.py: -------------------------------------------------------------------------------- 1 | from owrx.property import PropertyStack 2 | from owrx.config.error import ConfigError 3 | from owrx.config.defaults import defaultConfig 4 | from owrx.config.dynamic import DynamicConfig 5 | from owrx.config.classic import ClassicConfig 6 | 7 | 8 | class Config(PropertyStack): 9 | sharedConfig = None 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.storableConfig = DynamicConfig() 14 | layers = [ 15 | self.storableConfig, 16 | ClassicConfig(), 17 | defaultConfig, 18 | ] 19 | for i, l in enumerate(layers): 20 | self.addLayer(i, l) 21 | 22 | @staticmethod 23 | def get(): 24 | if Config.sharedConfig is None: 25 | Config.sharedConfig = Config() 26 | return Config.sharedConfig 27 | 28 | def store(self): 29 | self.storableConfig.store() 30 | 31 | @staticmethod 32 | def validateConfig(): 33 | # no config checks atm 34 | # just basic loading verification 35 | Config.get() 36 | 37 | def __setitem__(self, key, value): 38 | # in the config, all writes go to the json layer 39 | return self.storableConfig.__setitem__(key, value) 40 | 41 | def __delitem__(self, key): 42 | # all deletes go to the json layer, too 43 | return self.storableConfig.__delitem__(key) 44 | -------------------------------------------------------------------------------- /owrx/config/classic.py: -------------------------------------------------------------------------------- 1 | from owrx.property import PropertyReadOnly, PropertyLayer 2 | from owrx.config.migration import Migrator 3 | import importlib.util 4 | 5 | 6 | class ClassicConfig(PropertyReadOnly): 7 | def __init__(self): 8 | pm = ClassicConfig._loadConfig() 9 | Migrator.migrate(pm) 10 | super().__init__(pm) 11 | 12 | @staticmethod 13 | def _loadConfig(): 14 | for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]: 15 | try: 16 | return ClassicConfig._loadPythonFile(file) 17 | except FileNotFoundError: 18 | pass 19 | return PropertyLayer() 20 | 21 | @staticmethod 22 | def _toLayer(dictionary: dict): 23 | layer = PropertyLayer() 24 | for k, v in dictionary.items(): 25 | if isinstance(v, dict): 26 | layer[k] = ClassicConfig._toLayer(v) 27 | else: 28 | layer[k] = v 29 | return layer 30 | 31 | @staticmethod 32 | def _loadPythonFile(file): 33 | spec = importlib.util.spec_from_file_location("config_webrx", file) 34 | cfg = importlib.util.module_from_spec(spec) 35 | spec.loader.exec_module(cfg) 36 | return ClassicConfig._toLayer({k: v for k, v in cfg.__dict__.items() if not k.startswith("__")}) 37 | -------------------------------------------------------------------------------- /owrx/config/commands.py: -------------------------------------------------------------------------------- 1 | from owrx.admin.commands import Command 2 | from owrx.config import Config 3 | from owrx.bookmarks import Bookmarks 4 | 5 | 6 | class MigrateCommand(Command): 7 | # these keys have been moved to openwebrx.conf 8 | blacklisted_keys = [ 9 | "temporary_directory", 10 | "web_port", 11 | "aprs_symbols_path", 12 | ] 13 | 14 | def run(self, args): 15 | print("Migrating configuration...") 16 | 17 | config = Config.get() 18 | # a key that is set will end up in the DynamicConfig, so this will transfer everything there 19 | for key, value in config.items(): 20 | if key not in MigrateCommand.blacklisted_keys: 21 | config[key] = value 22 | config.store() 23 | 24 | print("Migrating bookmarks...") 25 | # bookmarks just need to be saved 26 | b = Bookmarks.getSharedInstance() 27 | b.getBookmarks() 28 | b.store() 29 | 30 | print("Migration complete!") 31 | -------------------------------------------------------------------------------- /owrx/config/dynamic.py: -------------------------------------------------------------------------------- 1 | from owrx.config.core import CoreConfig 2 | from owrx.config.migration import Migrator 3 | from owrx.property import PropertyLayer, PropertyDeleted 4 | from owrx.jsons import Encoder 5 | import json 6 | 7 | 8 | class DynamicConfig(PropertyLayer): 9 | def __init__(self): 10 | super().__init__() 11 | try: 12 | with open(DynamicConfig._getSettingsFile(), "r") as f: 13 | for k, v in json.load(f).items(): 14 | if isinstance(v, dict): 15 | self[k] = DynamicConfig._toLayer(v) 16 | else: 17 | self[k] = v 18 | except FileNotFoundError: 19 | pass 20 | Migrator.migrate(self) 21 | 22 | @staticmethod 23 | def _toLayer(dictionary: dict): 24 | layer = PropertyLayer() 25 | for k, v in dictionary.items(): 26 | if isinstance(v, dict): 27 | layer[k] = DynamicConfig._toLayer(v) 28 | else: 29 | layer[k] = v 30 | return layer 31 | 32 | @staticmethod 33 | def _getSettingsFile(): 34 | coreConfig = CoreConfig() 35 | return "{data_directory}/settings.json".format(data_directory=coreConfig.get_data_directory()) 36 | 37 | def store(self): 38 | # don't write directly to file to avoid corruption on exceptions 39 | jsonContent = json.dumps(self.__dict__(), indent=4, cls=Encoder) 40 | with open(DynamicConfig._getSettingsFile(), "w") as file: 41 | file.write(jsonContent) 42 | 43 | def __delitem__(self, key): 44 | self.__setitem__(key, PropertyDeleted) 45 | 46 | def __contains__(self, item): 47 | if not super().__contains__(item): 48 | return False 49 | if super().__getitem__(item) is PropertyDeleted: 50 | return False 51 | return True 52 | 53 | def __getitem__(self, item): 54 | if self.__contains__(item): 55 | return super().__getitem__(item) 56 | raise KeyError('Key "{key}" does not exist'.format(key=item)) 57 | 58 | def __dict__(self): 59 | return {k: v for k, v in super().__dict__().items() if v is not PropertyDeleted} 60 | 61 | def keys(self): 62 | return [k for k in super().keys() if self.__contains__(k)] 63 | -------------------------------------------------------------------------------- /owrx/config/error.py: -------------------------------------------------------------------------------- 1 | class ConfigError(Exception): 2 | def __init__(self, key, message): 3 | super().__init__("Configuration Error (key: {0}): {1}".format(key, message)) 4 | -------------------------------------------------------------------------------- /owrx/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | 4 | class BodySizeError(Exception): 5 | pass 6 | 7 | 8 | class Controller(object): 9 | def __init__(self, handler, request, options): 10 | self.handler = handler 11 | self.request = request 12 | self.options = options 13 | self.responseCookies = None 14 | 15 | def send_response( 16 | self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None 17 | ): 18 | self.handler.send_response(code) 19 | if headers is None: 20 | headers = {} 21 | if content_type is not None: 22 | headers["Content-Type"] = content_type 23 | if content_type.startswith("text/"): 24 | headers["Content-Type"] += "; charset=utf-8" 25 | if last_modified is not None: 26 | headers["Last-Modified"] = last_modified.astimezone(tz=timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT") 27 | if max_age is not None: 28 | headers["Cache-Control"] = "max-age={0}".format(max_age) 29 | for key, value in headers.items(): 30 | self.handler.send_header(key, value) 31 | if self.responseCookies is not None: 32 | self.handler.send_header("Set-Cookie", self.responseCookies.output(header="")) 33 | self.handler.end_headers() 34 | if type(content) == str: 35 | content = content.encode() 36 | while len(content): 37 | w = self.handler.wfile.write(content) 38 | content = content[w:] 39 | 40 | def send_redirect(self, location, code=303): 41 | self.handler.send_response(code) 42 | if self.responseCookies is not None: 43 | self.handler.send_header("Set-Cookie", self.responseCookies.output(header="")) 44 | self.handler.send_header("Location", location) 45 | self.handler.end_headers() 46 | 47 | def set_response_cookies(self, cookies): 48 | self.responseCookies = cookies 49 | 50 | def get_body(self, max_size=None): 51 | if "Content-Length" not in self.handler.headers: 52 | return None 53 | length = int(self.handler.headers["Content-Length"]) 54 | if max_size is not None and length > max_size: 55 | raise BodySizeError("HTTP body exceeds maximum allowed size") 56 | return self.handler.rfile.read(length) 57 | 58 | def handle_request(self): 59 | action = "indexAction" 60 | if "action" in self.options: 61 | action = self.options["action"] 62 | getattr(self, action)() 63 | -------------------------------------------------------------------------------- /owrx/controllers/admin.py: -------------------------------------------------------------------------------- 1 | from owrx.controllers.session import SessionStorage 2 | from owrx.users import UserList 3 | from urllib import parse 4 | from http.cookies import SimpleCookie 5 | 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class Authentication(object): 12 | def getUser(self, request): 13 | if "owrx-session" not in request.cookies: 14 | return None 15 | session_id = request.cookies["owrx-session"].value 16 | storage = SessionStorage.getSharedInstance() 17 | session = storage.getSession(session_id) 18 | if session is None: 19 | return None 20 | if "user" not in session: 21 | return None 22 | userList = UserList.getSharedInstance() 23 | user = None 24 | try: 25 | user = userList[session["user"]] 26 | storage.prolongSession(session_id) 27 | except KeyError: 28 | pass 29 | return user 30 | 31 | 32 | class AuthorizationMixin(object): 33 | def __init__(self, handler, request, options): 34 | self.authentication = Authentication() 35 | self.user = self.authentication.getUser(request) 36 | super().__init__(handler, request, options) 37 | 38 | def isAuthorized(self): 39 | return self.user is not None and self.user.is_enabled() and not self.user.must_change_password 40 | 41 | def handle_request(self): 42 | if self.isAuthorized(): 43 | super().handle_request() 44 | else: 45 | cookie = SimpleCookie() 46 | cookie["owrx-session"] = "" 47 | cookie["owrx-session"]["expires"] = "Thu, 01 Jan 1970 00:00:00 GMT" 48 | self.set_response_cookies(cookie) 49 | if ( 50 | "x-requested-with" in self.request.headers 51 | and self.request.headers["x-requested-with"] == "XMLHttpRequest" 52 | ): 53 | self.send_response("{}", code=403) 54 | else: 55 | target = "{}login?{}".format(self.get_document_root(), parse.urlencode({"ref": self.request.path[1:]})) 56 | self.send_redirect(target) 57 | -------------------------------------------------------------------------------- /owrx/controllers/api.py: -------------------------------------------------------------------------------- 1 | from . import Controller 2 | from owrx.feature import FeatureDetector 3 | import json 4 | 5 | 6 | class ApiController(Controller): 7 | def indexAction(self): 8 | data = json.dumps(FeatureDetector().feature_report()) 9 | self.send_response(data, content_type="application/json") 10 | -------------------------------------------------------------------------------- /owrx/controllers/feature.py: -------------------------------------------------------------------------------- 1 | from owrx.controllers.template import WebpageController 2 | from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin 3 | from owrx.controllers.settings import SettingsBreadcrumb 4 | 5 | 6 | class FeatureController(BreadcrumbMixin, WebpageController): 7 | def get_breadcrumb(self) -> Breadcrumb: 8 | return SettingsBreadcrumb().append(BreadcrumbItem("Feature report", "features")) 9 | 10 | def indexAction(self): 11 | self.serve_template("features.html", **self.template_variables()) 12 | -------------------------------------------------------------------------------- /owrx/controllers/metrics.py: -------------------------------------------------------------------------------- 1 | from . import Controller 2 | from owrx.metrics import CounterMetric, DirectMetric, Metrics 3 | import json 4 | import re 5 | 6 | 7 | 8 | class MetricsController(Controller): 9 | def indexAction(self): 10 | data = json.dumps(Metrics.getSharedInstance().getHierarchicalMetrics()) 11 | self.send_response(data, content_type="application/json") 12 | 13 | def prometheusAction(self): 14 | metrics = Metrics.getSharedInstance().getFlatMetrics() 15 | 16 | def prometheusFormat(key, metric): 17 | value = metric.getValue() 18 | if isinstance(metric, CounterMetric): 19 | key += "_total" 20 | value = value["count"] 21 | elif isinstance(metric, DirectMetric): 22 | pass 23 | else: 24 | raise ValueError("Unexpected metric type for metric {}".format(repr(metric))) 25 | 26 | return "{key} {value}".format(key=re.sub('[^a-zA-Z0-9:_]', '_', key), value=value) 27 | 28 | data = ["# https://prometheus.io/docs/instrumenting/exposition_formats/"] + [ 29 | prometheusFormat(k, v) for k, v in metrics.items() 30 | ] 31 | 32 | self.send_response("\n".join(data), content_type="text/plain; version=0.0.4") 33 | -------------------------------------------------------------------------------- /owrx/controllers/profile.py: -------------------------------------------------------------------------------- 1 | from owrx.controllers.template import WebpageController 2 | from owrx.controllers.admin import AuthorizationMixin 3 | from owrx.users import UserList, DefaultPasswordClass 4 | from urllib.parse import parse_qs 5 | 6 | 7 | class ProfileController(AuthorizationMixin, WebpageController): 8 | def isAuthorized(self): 9 | return self.user is not None and self.user.is_enabled() and self.user.must_change_password 10 | 11 | def indexAction(self): 12 | self.serve_template("pwchange.html", **self.template_variables()) 13 | 14 | def processPwChange(self): 15 | data = parse_qs(self.get_body().decode("utf-8")) 16 | data = {k: v[0] for k, v in data.items()} 17 | userlist = UserList.getSharedInstance() 18 | if "password" in data and "confirm" in data and data["password"] == data["confirm"]: 19 | self.user.setPassword(DefaultPasswordClass(data["password"]), must_change_password=False) 20 | userlist.store() 21 | target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings" 22 | else: 23 | target = "/pwchange" 24 | self.send_redirect(target) 25 | -------------------------------------------------------------------------------- /owrx/controllers/receiverid.py: -------------------------------------------------------------------------------- 1 | from owrx.controllers import Controller 2 | from owrx.receiverid import ReceiverId 3 | from datetime import datetime 4 | 5 | 6 | class ReceiverIdController(Controller): 7 | def __init__(self, handler, request, options): 8 | super().__init__(handler, request, options) 9 | self.authHeader = None 10 | 11 | def send_response( 12 | self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None 13 | ): 14 | if self.authHeader is not None: 15 | if headers is None: 16 | headers = {} 17 | headers["Authorization"] = self.authHeader 18 | super().send_response( 19 | content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers 20 | ) 21 | pass 22 | 23 | def handle_request(self): 24 | if "Authorization" in self.request.headers: 25 | self.authHeader = ReceiverId.getResponseHeader(self.request.headers["Authorization"]) 26 | super().handle_request() 27 | -------------------------------------------------------------------------------- /owrx/controllers/robots.py: -------------------------------------------------------------------------------- 1 | from owrx.controllers import Controller 2 | 3 | 4 | class RobotsController(Controller): 5 | def indexAction(self): 6 | # search engines should not be crawling internal / API routes 7 | self.send_response( 8 | """User-agent: * 9 | Disallow: /login 10 | Disallow: /logout 11 | Disallow: /pwchange 12 | Disallow: /settings 13 | Disallow: /imageupload 14 | """, 15 | content_type="text/plain", 16 | ) 17 | -------------------------------------------------------------------------------- /owrx/controllers/settings/backgrounddecoding.py: -------------------------------------------------------------------------------- 1 | from owrx.controllers.settings import SettingsFormController 2 | from owrx.form.section import Section 3 | from owrx.form.input import CheckboxInput, ServicesCheckboxInput 4 | from owrx.breadcrumb import Breadcrumb, BreadcrumbItem 5 | from owrx.controllers.settings import SettingsBreadcrumb 6 | 7 | 8 | class BackgroundDecodingController(SettingsFormController): 9 | def getTitle(self): 10 | return "Background decoding" 11 | 12 | def get_breadcrumb(self) -> Breadcrumb: 13 | return SettingsBreadcrumb().append(BreadcrumbItem("Background decoding", "settings/backgrounddecoding")) 14 | 15 | def getSections(self): 16 | return [ 17 | Section( 18 | "Background decoding", 19 | CheckboxInput( 20 | "services_enabled", 21 | "Enable background decoding services", 22 | ), 23 | ServicesCheckboxInput("services_decoders", "Enabled services"), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /owrx/controllers/status.py: -------------------------------------------------------------------------------- 1 | from .receiverid import ReceiverIdController 2 | from owrx.version import openwebrx_version 3 | from owrx.sdr import SdrService 4 | from owrx.config import Config 5 | from owrx.jsons import Encoder 6 | import json 7 | 8 | import logging 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class StatusController(ReceiverIdController): 14 | def getProfileStats(self, profile): 15 | return { 16 | "name": profile["name"], 17 | "center_freq": profile["center_freq"], 18 | "sample_rate": profile["samp_rate"], 19 | } 20 | 21 | def getReceiverStats(self, receiver): 22 | stats = { 23 | "name": receiver.getName(), 24 | # TODO would be better to have types from the config here 25 | "type": type(receiver).__name__, 26 | "profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()], 27 | } 28 | return stats 29 | 30 | def indexAction(self): 31 | pm = Config.get() 32 | status = { 33 | "receiver": { 34 | "name": pm["receiver_name"], 35 | "admin": pm["receiver_admin"], 36 | "gps": pm["receiver_gps"], 37 | "asl": pm["receiver_asl"], 38 | "location": pm["receiver_location"], 39 | }, 40 | "max_clients": pm["max_clients"], 41 | "version": openwebrx_version, 42 | "sdrs": [self.getReceiverStats(r) for r in SdrService.getActiveSources().values()], 43 | } 44 | self.send_response(json.dumps(status, cls=Encoder), content_type="application/json") 45 | -------------------------------------------------------------------------------- /owrx/controllers/template.py: -------------------------------------------------------------------------------- 1 | from owrx.controllers import Controller 2 | from owrx.details import ReceiverDetails 3 | from string import Template 4 | import pkg_resources 5 | 6 | 7 | class TemplateController(Controller): 8 | def render_template(self, file, **vars): 9 | file_content = pkg_resources.resource_string("htdocs", file).decode("utf-8") 10 | template = Template(file_content) 11 | 12 | return template.safe_substitute(**vars) 13 | 14 | def serve_template(self, file, **vars): 15 | self.send_response(self.render_template(file, **vars), content_type="text/html") 16 | 17 | def default_variables(self): 18 | return {} 19 | 20 | 21 | class WebpageController(TemplateController): 22 | def get_document_root(self): 23 | path_parts = [part for part in self.request.path[1:].split("/")] 24 | levels = max(0, len(path_parts) - 1) 25 | return "../" * levels 26 | 27 | def header_variables(self): 28 | variables = {"document_root": self.get_document_root()} 29 | variables.update(ReceiverDetails().__dict__()) 30 | return variables 31 | 32 | def template_variables(self): 33 | header = self.render_template("include/header.include.html", **self.header_variables()) 34 | return {"header": header, "document_root": self.get_document_root()} 35 | 36 | 37 | class IndexController(WebpageController): 38 | def indexAction(self): 39 | self.serve_template("index.html", **self.template_variables()) 40 | 41 | 42 | class MapController(WebpageController): 43 | def indexAction(self): 44 | # TODO check if we have a google maps api key first? 45 | self.serve_template("map.html", **self.template_variables()) 46 | -------------------------------------------------------------------------------- /owrx/controllers/websocket.py: -------------------------------------------------------------------------------- 1 | from . import Controller 2 | from owrx.websocket import WebSocketConnection 3 | from owrx.connection import HandshakeMessageHandler 4 | 5 | 6 | class WebSocketController(Controller): 7 | def indexAction(self): 8 | conn = WebSocketConnection(self.handler, HandshakeMessageHandler()) 9 | # enter read loop 10 | conn.handle() 11 | -------------------------------------------------------------------------------- /owrx/cpu.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | import logging 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | class CpuUsageThread(threading.Thread): 9 | sharedInstance = None 10 | creationLock = threading.Lock() 11 | 12 | @staticmethod 13 | def getSharedInstance(): 14 | with CpuUsageThread.creationLock: 15 | if CpuUsageThread.sharedInstance is None: 16 | CpuUsageThread.sharedInstance = CpuUsageThread() 17 | return CpuUsageThread.sharedInstance 18 | 19 | def __init__(self): 20 | self.clients = [] 21 | self.doRun = True 22 | self.last_worktime = 0 23 | self.last_idletime = 0 24 | self.endEvent = threading.Event() 25 | self.startLock = threading.Lock() 26 | super().__init__() 27 | 28 | def run(self): 29 | logger.debug("cpu usage thread starting up") 30 | while self.doRun: 31 | try: 32 | cpu_usage = self.get_cpu_usage() 33 | except: 34 | cpu_usage = 0 35 | for c in self.clients: 36 | c.write_cpu_usage(cpu_usage) 37 | self.endEvent.wait(timeout=3) 38 | logger.debug("cpu usage thread shut down") 39 | 40 | def get_cpu_usage(self): 41 | try: 42 | f = open("/proc/stat", "r") 43 | except: 44 | return 0 # Workaround, possibly we're on a Mac 45 | line = "" 46 | while not "cpu " in line: 47 | line = f.readline() 48 | f.close() 49 | spl = line.split(" ") 50 | worktime = int(spl[2]) + int(spl[3]) + int(spl[4]) 51 | idletime = int(spl[5]) 52 | dworktime = worktime - self.last_worktime 53 | didletime = idletime - self.last_idletime 54 | rate = float(dworktime) / (didletime + dworktime) 55 | self.last_worktime = worktime 56 | self.last_idletime = idletime 57 | if self.last_worktime == 0: 58 | return 0 59 | return rate 60 | 61 | def add_client(self, c): 62 | self.clients.append(c) 63 | with self.startLock: 64 | if not self.is_alive(): 65 | self.start() 66 | 67 | def remove_client(self, c): 68 | try: 69 | self.clients.remove(c) 70 | except ValueError: 71 | pass 72 | if not self.clients: 73 | self.shutdown() 74 | 75 | def shutdown(self): 76 | with CpuUsageThread.creationLock: 77 | CpuUsageThread.sharedInstance = None 78 | self.doRun = False 79 | self.endEvent.set() 80 | -------------------------------------------------------------------------------- /owrx/dab/dablin.py: -------------------------------------------------------------------------------- 1 | from pycsdr.modules import ExecModule 2 | from pycsdr.types import Format 3 | 4 | 5 | class DablinModule(ExecModule): 6 | def __init__(self): 7 | self.serviceId = 0 8 | super().__init__( 9 | Format.CHAR, 10 | Format.FLOAT, 11 | self._buildArgs() 12 | ) 13 | 14 | def _buildArgs(self): 15 | return ["dablin", "-p", "-s", "{:#06x}".format(self.serviceId)] 16 | 17 | def setDabServiceId(self, serviceId: int) -> None: 18 | self.serviceId = serviceId 19 | self.setArgs(self._buildArgs()) 20 | self.restart() 21 | -------------------------------------------------------------------------------- /owrx/details.py: -------------------------------------------------------------------------------- 1 | from owrx.config import Config 2 | from owrx.locator import Locator 3 | from owrx.property import PropertyFilter 4 | from owrx.property.filter import ByPropertyName 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class ReceiverDetails(PropertyFilter): 11 | def __init__(self): 12 | super().__init__( 13 | Config.get(), 14 | ByPropertyName( 15 | "receiver_name", 16 | "receiver_location", 17 | "receiver_asl", 18 | "receiver_gps", 19 | "photo_title", 20 | "photo_desc", 21 | ) 22 | ) 23 | 24 | def __dict__(self): 25 | receiver_info = super().__dict__() 26 | try: 27 | receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"]) 28 | except ValueError as e: 29 | logger.error("invalid receiver location, check in settings: %s", str(e)) 30 | return receiver_info 31 | -------------------------------------------------------------------------------- /owrx/form/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/owrx/form/__init__.py -------------------------------------------------------------------------------- /owrx/form/error.py: -------------------------------------------------------------------------------- 1 | class FormError(Exception): 2 | def __init__(self, key, message): 3 | super().__init__("Error processing form data for {}: {}".format(key, message)) 4 | self.key = key 5 | self.message = message 6 | 7 | def getKey(self): 8 | return self.key 9 | 10 | def getMessage(self): 11 | return self.message 12 | 13 | 14 | class ValidationError(FormError): 15 | pass 16 | -------------------------------------------------------------------------------- /owrx/form/input/aprs.py: -------------------------------------------------------------------------------- 1 | from owrx.form.input import DropdownEnum 2 | 3 | 4 | class AprsBeaconSymbols(DropdownEnum): 5 | BEACON_RECEIVE_ONLY = ("R&", "Receive only IGate") 6 | BEACON_HF_GATEWAY = ("/&", "HF Gateway") 7 | BEACON_IGATE_GENERIC = ("I&", "Igate Generic (please use more specific overlay)") 8 | BEACON_PSKMAIL = ("P&", "PSKmail node") 9 | BEACON_TX_1 = ("T&", "TX IGate with path set to 1 hop") 10 | BEACON_WIRES_X = ("W&", "Wires-X") 11 | BEACON_TX_2 = ("2&", "TX IGate with path set to 2 hops") 12 | 13 | def __new__(cls, *args, **kwargs): 14 | value, description = args 15 | obj = object.__new__(cls) 16 | obj._value_ = value 17 | obj.description = description 18 | return obj 19 | 20 | def __str__(self): 21 | return "{description} ({symbol})".format(description=self.description, symbol=self.value) 22 | 23 | 24 | class AprsAntennaDirections(DropdownEnum): 25 | DIRECTION_OMNI = None 26 | DIRECTION_N = "N" 27 | DIRECTION_NE = "NE" 28 | DIRECTION_E = "E" 29 | DIRECTION_SE = "SE" 30 | DIRECTION_S = "S" 31 | DIRECTION_SW = "SW" 32 | DIRECTION_W = "W" 33 | DIRECTION_NW = "NW" 34 | 35 | def __str__(self): 36 | return "omnidirectional" if self.value is None else self.value 37 | -------------------------------------------------------------------------------- /owrx/form/input/gfx.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from owrx.form.input import Input 3 | from datetime import datetime 4 | 5 | 6 | class ImageInput(Input, metaclass=ABCMeta): 7 | def render_input(self, value, errors): 8 | # TODO display errors 9 | return """ 10 |
11 | 12 |
13 | {label} 14 |
15 | 16 | 17 |
18 | """.format( 19 | id=self.id, 20 | label=self.label, 21 | url=self.cachebuster(self.getUrl()), 22 | classes=" ".join(self.getImgClasses()), 23 | maxsize=self.getMaxSize(), 24 | ) 25 | 26 | def cachebuster(self, url: str): 27 | return "{url}{separator}cb={cachebuster}".format( 28 | url=url, 29 | cachebuster=datetime.now().timestamp(), 30 | separator="&" if "?" in url else "?", 31 | ) 32 | 33 | @abstractmethod 34 | def getUrl(self) -> str: 35 | pass 36 | 37 | @abstractmethod 38 | def getImgClasses(self) -> list: 39 | pass 40 | 41 | @abstractmethod 42 | def getMaxSize(self) -> int: 43 | pass 44 | 45 | 46 | class AvatarInput(ImageInput): 47 | def getUrl(self) -> str: 48 | return "../static/gfx/openwebrx-avatar.png" 49 | 50 | def getImgClasses(self) -> list: 51 | return ["webrx-rx-avatar"] 52 | 53 | def getMaxSize(self) -> int: 54 | # 256 kB 55 | return 250 * 1024 56 | 57 | 58 | class TopPhotoInput(ImageInput): 59 | def getUrl(self) -> str: 60 | return "../static/gfx/openwebrx-top-photo.jpg" 61 | 62 | def getImgClasses(self) -> list: 63 | return ["webrx-top-photo"] 64 | 65 | def getMaxSize(self) -> int: 66 | # 2 MB 67 | return 2 * 1024 * 1024 68 | -------------------------------------------------------------------------------- /owrx/form/input/location.py: -------------------------------------------------------------------------------- 1 | from owrx.form.input import Input 2 | from owrx.form.input.validator import Validator 3 | from owrx.form.error import ValidationError 4 | from owrx.config import Config 5 | 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class LocationValidator(Validator): 12 | def validate(self, key, value): 13 | if "lat" in value and not -90 < value["lat"] < 90: 14 | raise ValidationError(key, "Latitude out of range (-90 to 90)") 15 | if "lon" in value and not -180 < value["lon"] < 180: 16 | raise ValidationError(key, "Longitude out of range (-180 to 180)") 17 | pass 18 | 19 | 20 | class LocationInput(Input): 21 | def __init__(self, id, label, validator: Validator = None): 22 | if validator is None: 23 | validator = LocationValidator() 24 | super().__init__(id, label, validator=validator) 25 | 26 | def render_input_group(self, value, errors): 27 | return """ 28 |
29 | {inputs} 30 |
31 | {errors} 32 |
33 |
34 |
35 | """.format( 36 | id=self.id, 37 | rowclass="is-invalid" if errors else "", 38 | inputs=self.render_input(value, errors), 39 | errors=self.render_errors(errors), 40 | key=Config.get()["google_maps_api_key"], 41 | ) 42 | 43 | def render_input(self, value, errors): 44 | return "".join(self.render_sub_input(value, id, errors) for id in ["lat", "lon"]) 45 | 46 | def render_sub_input(self, value, id, errors): 47 | return """ 48 |
49 | 51 |
52 | """.format( 53 | id="{0}-{1}".format(self.id, id), 54 | label=self.label, 55 | classes=self.input_classes(errors), 56 | value=value[id], 57 | disabled="disabled" if self.disabled else "", 58 | ) 59 | 60 | def parse(self, data): 61 | value = {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]} 62 | return {self.id: value} 63 | -------------------------------------------------------------------------------- /owrx/form/input/receiverid.py: -------------------------------------------------------------------------------- 1 | from owrx.form.input import TextAreaInput 2 | from owrx.form.input.converter import Converter 3 | 4 | 5 | class ReceiverKeysConverter(Converter): 6 | def convert_to_form(self, value): 7 | return "" if value is None else "\n".join(value) 8 | 9 | def convert_from_form(self, value): 10 | # \r\n or \n? this should work with both. 11 | stripped = [v.strip("\r ") for v in value.split("\n")] 12 | # omit empty lines 13 | return [v for v in stripped if v] 14 | 15 | 16 | class ReceiverKeysInput(TextAreaInput): 17 | def __init__(self, id, label): 18 | super().__init__( 19 | id, 20 | label, 21 | infotext="Put the keys you receive on listing sites (e.g. " 22 | + 'Receiverbook) here, one per line', 23 | ) 24 | 25 | def input_properties(self, value, errors): 26 | props = super().input_properties(value, errors) 27 | # disable word wrap on the textarea. 28 | # why? keys are longer than the input, and word wrap makes the "one per line" instruction confusing. 29 | props["wrap"] = "off" 30 | return props 31 | 32 | def defaultConverter(self): 33 | return ReceiverKeysConverter() 34 | -------------------------------------------------------------------------------- /owrx/form/input/validator.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from owrx.form.error import ValidationError 3 | from typing import List 4 | 5 | 6 | class Validator(ABC): 7 | @abstractmethod 8 | def validate(self, key, value) -> None: 9 | pass 10 | 11 | 12 | class RequiredValidator(Validator): 13 | def validate(self, key, value) -> None: 14 | if value is None or value == "": 15 | raise ValidationError(key, "Field is required") 16 | 17 | 18 | class Range(object): 19 | def __init__(self, start: int, end: int = None): 20 | self.start = start 21 | self.end = end if end is not None else start 22 | 23 | def isInRange(self, value): 24 | return self.start <= value <= self.end 25 | 26 | def __str__(self): 27 | if self.start == self.end: 28 | return str(self.start) 29 | return "{start}...{end}".format(**vars(self)) 30 | 31 | 32 | class RangeValidator(Validator): 33 | def __init__(self, minValue, maxValue): 34 | self.range = Range(minValue, maxValue) 35 | 36 | def validate(self, key, value) -> None: 37 | if value is None or value == "": 38 | return # Ignore empty values 39 | if not self.range.isInRange(float(value)): 40 | raise ValidationError( 41 | key, "Value must be between {min} and {max}".format(min=self.range.start, max=self.range.end) 42 | ) 43 | 44 | 45 | class RangeListValidator(Validator): 46 | def __init__(self, rangeList: List[Range]): 47 | self.rangeList = rangeList 48 | 49 | def validate(self, key, value) -> None: 50 | if not any(range for range in self.rangeList if range.isInRange(value)): 51 | raise ValidationError( 52 | key, "Value is outside of the allowed range(s) {}".format(self._rangeStr()) 53 | ) 54 | 55 | def _rangeStr(self): 56 | return "[{}]".format(", ".join(str(r) for r in self.rangeList)) 57 | 58 | 59 | class AddressAndOptionalPortValidator(Validator): 60 | def validate(self, key, value) -> None: 61 | parts = value.split(":") 62 | if len(parts) > 2: 63 | raise ValidationError(key, "Value contains too many colons") 64 | 65 | if len(parts) > 1: 66 | try: 67 | port = int(parts[1]) 68 | except ValueError: 69 | raise ValidationError(key, "Port number must be numeric") 70 | if not 0 <= port <= 65535: 71 | raise ValidationError(key, "Port number out of range") 72 | -------------------------------------------------------------------------------- /owrx/form/input/wfm.py: -------------------------------------------------------------------------------- 1 | from owrx.form.input import DropdownEnum 2 | 3 | 4 | class WfmTauValues(DropdownEnum): 5 | TAU_50_MICRO = (50e-6, "most regions") 6 | TAU_75_MICRO = (75e-6, "Americas and South Korea") 7 | 8 | def __new__(cls, *args, **kwargs): 9 | value, description = args 10 | obj = object.__new__(cls) 11 | obj._value_ = value 12 | obj.description = description 13 | return obj 14 | 15 | def __str__(self): 16 | return "{}µs ({})".format(int(self.value * 1e6), self.description) 17 | -------------------------------------------------------------------------------- /owrx/ism/rtl433.py: -------------------------------------------------------------------------------- 1 | from pycsdr.modules import ExecModule 2 | from pycsdr.types import Format 3 | from csdr.module import JsonParser 4 | from owrx.reporting import ReportingEngine 5 | 6 | 7 | class Rtl433Module(ExecModule): 8 | def __init__(self): 9 | super().__init__( 10 | Format.COMPLEX_FLOAT, 11 | Format.CHAR, 12 | ["rtl_433", "-r", "cf32:-", "-F", "json", "-M", "time:unix", "-C", "si", "-s", "1200000"] 13 | ) 14 | 15 | 16 | class IsmParser(JsonParser): 17 | def __init__(self): 18 | super().__init__("ISM") 19 | 20 | def process(self, line): 21 | data = super().process(line) 22 | ReportingEngine.getSharedInstance().spot(data) 23 | return data 24 | -------------------------------------------------------------------------------- /owrx/jsons.py: -------------------------------------------------------------------------------- 1 | from owrx.property import PropertyManager 2 | import json 3 | 4 | 5 | class Encoder(json.JSONEncoder): 6 | def default(self, o): 7 | if isinstance(o, PropertyManager): 8 | return o.__dict__() 9 | return super().default(o) 10 | -------------------------------------------------------------------------------- /owrx/locator.py: -------------------------------------------------------------------------------- 1 | class Locator(object): 2 | @staticmethod 3 | def fromCoordinates(coordinates, depth=3): 4 | 5 | lat = coordinates["lat"] 6 | lon = coordinates["lon"] 7 | 8 | if not -90 < lat < 90: 9 | raise ValueError("invalid latitude: {}".format(lat)) 10 | if not -180 < lon < 180: 11 | raise ValueError("invalid longitude: {}".format(lon)) 12 | 13 | lon = lon + 180 14 | lat = lat + 90 15 | 16 | res = "" 17 | res += chr(65 + int(lon / 20)) 18 | res += chr(65 + int(lat / 10)) 19 | if depth >= 2: 20 | lon = lon % 20 21 | lat = lat % 10 22 | res += str(int(lon / 2)) 23 | res += str(int(lat)) 24 | if depth >= 3: 25 | lon = lon % 2 26 | lat = lat % 1 27 | res += chr(97 + int(lon * 12)) 28 | res += chr(97 + int(lat * 24)) 29 | 30 | return res 31 | -------------------------------------------------------------------------------- /owrx/log/__init__.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import os 3 | from logging import Logger, Handler, LogRecord, Formatter 4 | 5 | 6 | class LogPipe(threading.Thread): 7 | 8 | def __init__(self, level: int, logger: Logger, prefix: str = ""): 9 | threading.Thread.__init__(self) 10 | self.daemon = False 11 | self.level = level 12 | self.logger = logger 13 | self.prefix = prefix 14 | self.fdRead, self.fdWrite = os.pipe() 15 | self.pipeReader = os.fdopen(self.fdRead) 16 | self.start() 17 | 18 | def fileno(self): 19 | return self.fdWrite 20 | 21 | def run(self): 22 | for line in iter(self.pipeReader.readline, ''): 23 | self.logger.log(self.level, "{}: {}".format(self.prefix, line.strip('\n'))) 24 | 25 | self.pipeReader.close() 26 | 27 | def close(self): 28 | os.close(self.fdWrite) 29 | 30 | 31 | class HistoryHandler(Handler): 32 | handlers = {} 33 | 34 | @staticmethod 35 | def getHandler(name: str): 36 | if name not in HistoryHandler.handlers: 37 | HistoryHandler.handlers[name] = HistoryHandler() 38 | return HistoryHandler.handlers[name] 39 | 40 | def __init__(self, maxRecords: int = 200): 41 | super().__init__() 42 | self.history = [] 43 | self.maxRecords = maxRecords 44 | self.setFormatter(Formatter(fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s")) 45 | 46 | def emit(self, record: LogRecord) -> None: 47 | self.history.append(record) 48 | # truncate 49 | self.history = self.history[-self.maxRecords:] 50 | 51 | def getFormattedHistory(self) -> str: 52 | return "\n".join([self.format(r) for r in self.history]) 53 | -------------------------------------------------------------------------------- /owrx/metrics.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from owrx.client import ClientRegistry 3 | 4 | 5 | class Metric(object): 6 | def getValue(self): 7 | return 0 8 | 9 | 10 | class CounterMetric(Metric): 11 | def __init__(self): 12 | self.counter = 0 13 | 14 | def inc(self, increment=1): 15 | self.counter += increment 16 | 17 | def getValue(self): 18 | return {"count": self.counter} 19 | 20 | 21 | class DirectMetric(Metric): 22 | def __init__(self, getter): 23 | self.getter = getter 24 | 25 | def getValue(self): 26 | return self.getter() 27 | 28 | 29 | class Metrics(object): 30 | sharedInstance = None 31 | creationLock = threading.Lock() 32 | 33 | @staticmethod 34 | def getSharedInstance(): 35 | with Metrics.creationLock: 36 | if Metrics.sharedInstance is None: 37 | Metrics.sharedInstance = Metrics() 38 | return Metrics.sharedInstance 39 | 40 | def __init__(self): 41 | self.metrics = {} 42 | self.addMetric("openwebrx.users", DirectMetric(ClientRegistry.getSharedInstance().clientCount)) 43 | 44 | def addMetric(self, name, metric): 45 | self.metrics[name] = metric 46 | 47 | def hasMetric(self, name): 48 | return name in self.metrics 49 | 50 | def getMetric(self, name): 51 | if not self.hasMetric(name): 52 | return None 53 | return self.metrics[name] 54 | 55 | def getFlatMetrics(self): 56 | return self.metrics 57 | 58 | def getHierarchicalMetrics(self): 59 | result = {} 60 | 61 | for (key, metric) in self.metrics.items(): 62 | partial = result 63 | keys = key.split(".") 64 | for keypart in keys[0:-1]: 65 | if not keypart in partial: 66 | partial[keypart] = {} 67 | partial = partial[keypart] 68 | partial[keys[-1]] = metric.getValue() 69 | 70 | return result 71 | -------------------------------------------------------------------------------- /owrx/pocsag.py: -------------------------------------------------------------------------------- 1 | from csdr.module import PickleModule 2 | from owrx.bands import Bandplan 3 | from owrx.metrics import Metrics, CounterMetric 4 | from owrx.reporting import ReportingEngine 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class PocsagParser(PickleModule): 11 | def __init__(self): 12 | self.band = None 13 | super().__init__() 14 | 15 | def process(self, meta): 16 | try: 17 | if "address" in meta: 18 | meta["address"] = int(meta["address"]) 19 | meta["mode"] = "Pocsag" 20 | self.pushDecode() 21 | ReportingEngine.getSharedInstance().spot(meta) 22 | return meta 23 | except Exception: 24 | logger.exception("Exception while parsing Pocsag message") 25 | 26 | def setDialFrequency(self, freq: int) -> None: 27 | self.band = Bandplan.getSharedInstance().findBand(freq) 28 | 29 | def pushDecode(self): 30 | band = "unknown" 31 | if self.band is not None: 32 | band = self.band.getName() 33 | name = "digiham.decodes.{band}.pocsag".format(band=band) 34 | metrics = Metrics.getSharedInstance() 35 | metric = metrics.getMetric(name) 36 | if metric is None: 37 | metric = CounterMetric() 38 | metrics.addMetric(name, metric) 39 | metric.inc() 40 | -------------------------------------------------------------------------------- /owrx/property/filter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Filter(ABC): 5 | @abstractmethod 6 | def apply(self, prop) -> bool: 7 | pass 8 | 9 | 10 | class ByPropertyName(Filter): 11 | def __init__(self, *props): 12 | self.props = props 13 | 14 | def apply(self, prop) -> bool: 15 | return prop in self.props 16 | 17 | 18 | class ByLambda(Filter): 19 | def __init__(self, func): 20 | self.func = func 21 | 22 | def apply(self, prop) -> bool: 23 | return self.func(prop) 24 | -------------------------------------------------------------------------------- /owrx/property/validators.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from functools import reduce 3 | from operator import or_ 4 | 5 | 6 | class ValidatorException(Exception): 7 | pass 8 | 9 | 10 | class Validator(ABC): 11 | @staticmethod 12 | def of(x): 13 | if isinstance(x, Validator): 14 | return x 15 | if callable(x): 16 | return LambdaValidator(x) 17 | if x in validator_types: 18 | return validator_types[x]() 19 | raise ValidatorException("Cannot create validator") 20 | 21 | @abstractmethod 22 | def isValid(self, value): 23 | pass 24 | 25 | 26 | class LambdaValidator(Validator): 27 | def __init__(self, c): 28 | self.callable = c 29 | 30 | def isValid(self, value): 31 | return self.callable(value) 32 | 33 | 34 | class TypeValidator(Validator): 35 | def __init__(self, type): 36 | self.type = type 37 | super().__init__() 38 | 39 | def isValid(self, value): 40 | return isinstance(value, self.type) 41 | 42 | 43 | class IntegerValidator(TypeValidator): 44 | def __init__(self): 45 | super().__init__(int) 46 | 47 | 48 | class FloatValidator(TypeValidator): 49 | def __init__(self): 50 | super().__init__(float) 51 | 52 | 53 | class StringValidator(TypeValidator): 54 | def __init__(self): 55 | super().__init__(str) 56 | 57 | 58 | class BoolValidator(TypeValidator): 59 | def __init__(self): 60 | super().__init__(bool) 61 | 62 | 63 | class OrValidator(Validator): 64 | def __init__(self, *validators): 65 | self.validators = validators 66 | super().__init__() 67 | 68 | def isValid(self, value): 69 | return reduce( 70 | or_, 71 | [v.isValid(value) for v in self.validators], 72 | False 73 | ) 74 | 75 | 76 | class NumberValidator(OrValidator): 77 | def __init__(self): 78 | super().__init__(IntegerValidator(), FloatValidator()) 79 | 80 | 81 | class RegexValidator(StringValidator): 82 | def __init__(self, regex): 83 | self.regex = regex 84 | super().__init__() 85 | 86 | def isValid(self, value): 87 | return super().isValid(value) and self.regex.match(value) is not None 88 | 89 | 90 | validator_types = { 91 | "string": StringValidator, 92 | "str": StringValidator, 93 | "integer": IntegerValidator, 94 | "int": IntegerValidator, 95 | "number": NumberValidator, 96 | "num": NumberValidator, 97 | } 98 | -------------------------------------------------------------------------------- /owrx/rds/redsea.py: -------------------------------------------------------------------------------- 1 | from pycsdr.modules import ExecModule 2 | from pycsdr.types import Format 3 | 4 | 5 | class RedseaModule(ExecModule): 6 | def __init__(self, sampleRate: int, rbds: bool): 7 | args = ["redsea", "--samplerate", str(sampleRate)] 8 | if rbds: 9 | args += ["--rbds"] 10 | super().__init__( 11 | Format.SHORT, 12 | Format.CHAR, 13 | args 14 | ) 15 | -------------------------------------------------------------------------------- /owrx/reporting/mqtt.py: -------------------------------------------------------------------------------- 1 | from paho.mqtt.client import Client 2 | from owrx.reporting.reporter import Reporter 3 | from owrx.config import Config 4 | from owrx.property import PropertyDeleted 5 | import json 6 | import threading 7 | import time 8 | 9 | import logging 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class MqttReporter(Reporter): 15 | DEFAULT_TOPIC = "openwebrx/decodes" 16 | 17 | def __init__(self): 18 | pm = Config.get() 19 | self.topic = self.DEFAULT_TOPIC 20 | self.client = self._getClient() 21 | self.subscriptions = [ 22 | pm.wireProperty("mqtt_topic", self._setTopic), 23 | pm.filter("mqtt_host", "mqtt_user", "mqtt_password", "mqtt_client_id", "mqtt_use_ssl").wire(self._reconnect) 24 | ] 25 | 26 | def _getClient(self): 27 | pm = Config.get() 28 | clientId = pm["mqtt_client_id"] if "mqtt_client_id" in pm else "" 29 | client = Client(clientId) 30 | 31 | if "mqtt_user" in pm and "mqtt_password" in pm: 32 | client.username_pw_set(pm["mqtt_user"], pm["mqtt_password"]) 33 | 34 | port = 1883 35 | if pm["mqtt_use_ssl"]: 36 | client.tls_set() 37 | port = 8883 38 | 39 | parts = pm["mqtt_host"].split(":") 40 | host = parts[0] 41 | if len(parts) > 1: 42 | port = int(parts[1]) 43 | 44 | try: 45 | client.connect(host=host, port=port) 46 | except: 47 | logger.exception("Exception connecting to MQTT server") 48 | 49 | threading.Thread(target=client.loop_forever).start() 50 | 51 | return client 52 | 53 | def _setTopic(self, topic): 54 | if topic is PropertyDeleted: 55 | self.topic = self.DEFAULT_TOPIC 56 | else: 57 | self.topic = topic 58 | 59 | def _reconnect(self, *args, **kwargs): 60 | logger.debug("Reconnecting...") 61 | old = self.client 62 | self.client = self._getClient() 63 | old.disconnect() 64 | 65 | def stop(self): 66 | self.client.disconnect() 67 | while self.subscriptions: 68 | self.subscriptions.pop().cancel() 69 | 70 | def spot(self, spot): 71 | self.client.publish(self.topic, payload=json.dumps(spot)) 72 | -------------------------------------------------------------------------------- /owrx/reporting/reporter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Reporter(ABC): 5 | @abstractmethod 6 | def stop(self): 7 | pass 8 | 9 | @abstractmethod 10 | def spot(self, spot): 11 | pass 12 | 13 | 14 | class FilteredReporter(Reporter): 15 | @abstractmethod 16 | def getSupportedModes(self): 17 | return [] 18 | -------------------------------------------------------------------------------- /owrx/service/chain.py: -------------------------------------------------------------------------------- 1 | from csdr.chain import Chain 2 | from csdr.chain.selector import Selector 3 | from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator 4 | from pycsdr.types import Format 5 | 6 | 7 | class ServiceDemodulatorChain(Chain): 8 | def __init__(self, demod: BaseDemodulatorChain, secondaryDemod: ServiceDemodulator, sampleRate: int, frequencyOffset: int): 9 | self.selector = Selector(sampleRate, secondaryDemod.getFixedAudioRate(), withSquelch=False) 10 | self.selector.setFrequencyOffset(frequencyOffset) 11 | 12 | workers = [self.selector] 13 | 14 | # primary demodulator is only necessary if the secondary does not accept IQ input 15 | if secondaryDemod.getInputFormat() is not Format.COMPLEX_FLOAT: 16 | workers += [demod] 17 | 18 | workers += [secondaryDemod] 19 | 20 | super().__init__(workers) 21 | 22 | def setBandPass(self, lowCut, highCut): 23 | self.selector.setBandpass(lowCut, highCut) 24 | -------------------------------------------------------------------------------- /owrx/soapy.py: -------------------------------------------------------------------------------- 1 | class SoapySettings(object): 2 | @staticmethod 3 | def parse(dstr): 4 | def decodeComponent(c): 5 | kv = c.split("=", 1) 6 | if len(kv) < 2: 7 | return c 8 | else: 9 | return {kv[0]: kv[1]} 10 | 11 | return [decodeComponent(c) for c in dstr.split(",")] 12 | 13 | @staticmethod 14 | def encode(dobj): 15 | def encodeComponent(c): 16 | if isinstance(c, str): 17 | return c 18 | else: 19 | return ",".join(["{0}={1}".format(key, value) for key, value in c.items()]) 20 | 21 | return ",".join([encodeComponent(c) for c in dobj]) 22 | -------------------------------------------------------------------------------- /owrx/socket.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | def getAvailablePort(): 5 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 6 | s.bind(("", 0)) 7 | s.listen(1) 8 | port = s.getsockname()[1] 9 | s.close() 10 | return port 11 | -------------------------------------------------------------------------------- /owrx/source/airspy.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input import Input, CheckboxInput 3 | from owrx.form.input.device import BiasTeeInput 4 | from owrx.form.input.validator import Range 5 | from typing import List 6 | 7 | 8 | class AirspySource(SoapyConnectorSource): 9 | def getSoapySettingsMappings(self): 10 | mappings = super().getSoapySettingsMappings() 11 | mappings.update( 12 | { 13 | "bias_tee": "biastee", 14 | "bitpack": "bitpack", 15 | } 16 | ) 17 | return mappings 18 | 19 | def getDriver(self): 20 | return "airspy" 21 | 22 | 23 | class AirspyDeviceDescription(SoapyConnectorDeviceDescription): 24 | def getName(self): 25 | return "Airspy R2 or Mini" 26 | 27 | def supportsPpm(self): 28 | # not supported by the device API 29 | # frequency calibration can be done with separate tools and will be persisted on the device. 30 | # see discussion here: https://groups.io/g/openwebrx/topic/79360293 31 | return False 32 | 33 | def getInputs(self) -> List[Input]: 34 | return super().getInputs() + [ 35 | BiasTeeInput(), 36 | CheckboxInput( 37 | "bitpack", 38 | "Enable bit-packing", 39 | infotext="Packs two 12-bit samples into 3 bytes." 40 | + " Lowers USB bandwidth consumption, increases CPU load", 41 | ), 42 | ] 43 | 44 | def getDeviceOptionalKeys(self): 45 | return super().getDeviceOptionalKeys() + ["bias_tee", "bitpack"] 46 | 47 | def getProfileOptionalKeys(self): 48 | return super().getProfileOptionalKeys() + ["bias_tee"] 49 | 50 | def getGainStages(self): 51 | return ["LNA", "MIX", "VGA"] 52 | 53 | def getSampleRateRanges(self) -> List[Range]: 54 | # Airspy R2 does 2.5 or 10 MS/s 55 | # Airspy mini does 3 or 6 MS/s 56 | # we don't know what device we're actually dealing with, but we can still clamp it down to a sum of the options. 57 | return [ 58 | Range(2500000), 59 | Range(3000000), 60 | Range(6000000), 61 | Range(10000000), 62 | ] 63 | -------------------------------------------------------------------------------- /owrx/source/airspyhf.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input.validator import Range 3 | from typing import List 4 | 5 | 6 | class AirspyhfSource(SoapyConnectorSource): 7 | def getDriver(self): 8 | return "airspyhf" 9 | 10 | 11 | class AirspyhfDeviceDescription(SoapyConnectorDeviceDescription): 12 | def getName(self): 13 | return "Airspy HF+ or Discovery" 14 | 15 | def supportsPpm(self): 16 | # not currently supported by the SoapySDR module. 17 | return False 18 | 19 | def getSampleRateRanges(self) -> List[Range]: 20 | return [ 21 | Range(192000), 22 | Range(256000), 23 | Range(384000), 24 | Range(456000), 25 | Range(768000), 26 | Range(912000), 27 | ] 28 | -------------------------------------------------------------------------------- /owrx/source/bladerf.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input.validator import Range 3 | from typing import List 4 | 5 | 6 | class BladerfSource(SoapyConnectorSource): 7 | def getDriver(self): 8 | return "bladerf" 9 | 10 | 11 | class BladerfDeviceDescription(SoapyConnectorDeviceDescription): 12 | def getName(self): 13 | return "Blade RF" 14 | 15 | def getSampleRateRanges(self) -> List[Range]: 16 | return [Range(160000, 40000000)] 17 | -------------------------------------------------------------------------------- /owrx/source/fcdpp.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input.validator import Range 3 | from typing import List 4 | 5 | 6 | class FcdppSource(SoapyConnectorSource): 7 | def getDriver(self): 8 | return "fcdpp" 9 | 10 | 11 | class FcdppDeviceDescription(SoapyConnectorDeviceDescription): 12 | def getName(self): 13 | return "FunCube Dongle Pro+" 14 | 15 | def getSampleRateRanges(self) -> List[Range]: 16 | return [ 17 | Range(96000), 18 | Range(192000), 19 | ] 20 | -------------------------------------------------------------------------------- /owrx/source/hackrf.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input import Input 3 | from owrx.form.input.device import BiasTeeInput 4 | from owrx.form.input.validator import Range 5 | from typing import List 6 | 7 | 8 | class HackrfSource(SoapyConnectorSource): 9 | def getSoapySettingsMappings(self): 10 | mappings = super().getSoapySettingsMappings() 11 | mappings.update({"bias_tee": "bias_tx"}) 12 | return mappings 13 | 14 | def getDriver(self): 15 | return "hackrf" 16 | 17 | 18 | class HackrfDeviceDescription(SoapyConnectorDeviceDescription): 19 | def getName(self): 20 | return "HackRF" 21 | 22 | def supportsPpm(self): 23 | # not implemented by the SoapySDR module. 24 | # see discussion here: https://groups.io/g/openwebrx/topic/78339109 25 | return False 26 | 27 | def getInputs(self) -> List[Input]: 28 | return super().getInputs() + [BiasTeeInput()] 29 | 30 | def getDeviceOptionalKeys(self): 31 | return super().getDeviceOptionalKeys() + ["bias_tee"] 32 | 33 | def getProfileOptionalKeys(self): 34 | return super().getProfileOptionalKeys() + ["bias_tee"] 35 | 36 | def getGainStages(self): 37 | return ["LNA", "AMP", "VGA"] 38 | 39 | def getSampleRateRanges(self) -> List[Range]: 40 | return [Range(1000000, 20000000)] 41 | -------------------------------------------------------------------------------- /owrx/source/lime_sdr.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input.validator import Range 3 | from typing import List 4 | 5 | 6 | class LimeSdrSource(SoapyConnectorSource): 7 | def getDriver(self): 8 | return "lime" 9 | 10 | 11 | class LimeSdrDeviceDescription(SoapyConnectorDeviceDescription): 12 | def getName(self): 13 | return "LimeSDR device" 14 | 15 | def getSampleRateRanges(self) -> List[Range]: 16 | return [Range(100000, 65000000)] 17 | -------------------------------------------------------------------------------- /owrx/source/pluto_sdr.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input import Input, TextInput 3 | from owrx.form.input.validator import Range 4 | from typing import List 5 | 6 | 7 | class PlutoSdrSource(SoapyConnectorSource): 8 | def getDriver(self): 9 | return "plutosdr" 10 | 11 | def getEventNames(self): 12 | return super().getEventNames() + ["hostname"] 13 | 14 | def buildSoapyDeviceParameters(self, parsed, values): 15 | params = super().buildSoapyDeviceParameters(parsed, values) 16 | if "hostname" in values: 17 | params = [p for p in params if "hostname" not in p] 18 | params += [{"hostname": values["hostname"]}] 19 | return params 20 | 21 | 22 | class PlutoSdrDeviceDescription(SoapyConnectorDeviceDescription): 23 | def getName(self): 24 | return "PlutoSDR" 25 | 26 | def getInputs(self) -> List[Input]: 27 | return super().getInputs() + [ 28 | TextInput( 29 | "hostname", 30 | "Hostname", 31 | infotext="Use this for PlutoSDR devices attached to the network" 32 | ) 33 | ] 34 | 35 | def getDeviceOptionalKeys(self): 36 | return super().getDeviceOptionalKeys() + ["hostname"] 37 | 38 | def getSampleRateRanges(self) -> List[Range]: 39 | return [Range(520833, 61440000)] 40 | -------------------------------------------------------------------------------- /owrx/source/radioberry.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input.validator import Range 3 | from typing import List 4 | 5 | 6 | class RadioberrySource(SoapyConnectorSource): 7 | def getDriver(self): 8 | return "radioberry" 9 | 10 | 11 | class RadioberryDeviceDescription(SoapyConnectorDeviceDescription): 12 | def getName(self): 13 | return "RadioBerry" 14 | 15 | def getSampleRateRanges(self) -> List[Range]: 16 | return [ 17 | Range(48000), 18 | Range(96000), 19 | Range(192000), 20 | Range(384000), 21 | ] 22 | -------------------------------------------------------------------------------- /owrx/source/resampler.py: -------------------------------------------------------------------------------- 1 | from owrx.source import SdrSource 2 | from pycsdr.modules import Buffer, FirDecimate, Shift 3 | from pycsdr.types import Format 4 | from csdr.chain import Chain 5 | 6 | 7 | class Resampler(SdrSource): 8 | def onPropertyChange(self, changes): 9 | self.logger.warning("Resampler is unable to handle property changes: {0}".format(changes)) 10 | 11 | def __init__(self, props, sdr): 12 | sdrProps = sdr.getProps() 13 | shift = (sdrProps["center_freq"] - props["center_freq"]) / sdrProps["samp_rate"] 14 | decimation = int(float(sdrProps["samp_rate"]) / props["samp_rate"]) 15 | if_samp_rate = sdrProps["samp_rate"] / decimation 16 | transition_bw = 0.15 * (if_samp_rate / float(sdrProps["samp_rate"])) 17 | props["samp_rate"] = if_samp_rate 18 | 19 | self.chain = Chain([ 20 | Shift(shift), 21 | FirDecimate(decimation, transition_bw) 22 | ]) 23 | 24 | self.chain.setReader(sdr.getBuffer().getReader()) 25 | 26 | super().__init__(None, props) 27 | 28 | def getBuffer(self): 29 | if self.buffer is None: 30 | self.buffer = Buffer(Format.COMPLEX_FLOAT) 31 | self.chain.setWriter(self.buffer) 32 | return self.buffer 33 | 34 | def stop(self): 35 | self.chain.stop() 36 | self.chain = None 37 | super().stop() 38 | 39 | def activateProfile(self, profile_id=None): 40 | self.logger.warning("Resampler does not support setting profiles") 41 | pass 42 | 43 | def validateProfiles(self): 44 | # resampler does not support profiles 45 | pass 46 | -------------------------------------------------------------------------------- /owrx/source/rtl_sdr.py: -------------------------------------------------------------------------------- 1 | from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription 2 | from owrx.command import Flag, Option 3 | from typing import List 4 | from owrx.form.input import Input, TextInput 5 | from owrx.form.input.device import BiasTeeInput, DirectSamplingInput 6 | from owrx.form.input.validator import Range 7 | 8 | 9 | class RtlSdrSource(ConnectorSource): 10 | def getCommandMapper(self): 11 | return ( 12 | super() 13 | .getCommandMapper() 14 | .setBase("rtl_connector") 15 | .setMappings({"bias_tee": Flag("-b"), "direct_sampling": Option("-e")}) 16 | ) 17 | 18 | 19 | class RtlSdrDeviceDescription(ConnectorDeviceDescription): 20 | def getName(self): 21 | return "RTL-SDR device" 22 | 23 | def getInputs(self) -> List[Input]: 24 | return super().getInputs() + [ 25 | TextInput( 26 | "device", 27 | "Device identifier", 28 | infotext="Device serial number or index", 29 | ), 30 | BiasTeeInput(), 31 | DirectSamplingInput(), 32 | ] 33 | 34 | def getDeviceOptionalKeys(self): 35 | return super().getDeviceOptionalKeys() + ["device", "bias_tee", "direct_sampling"] 36 | 37 | def getProfileOptionalKeys(self): 38 | return super().getProfileOptionalKeys() + ["bias_tee", "direct_sampling"] 39 | 40 | def getSampleRateRanges(self) -> List[Range]: 41 | return [Range(250000, 3200000)] 42 | -------------------------------------------------------------------------------- /owrx/source/rtl_sdr_soapy.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input import Input 3 | from owrx.form.input.device import BiasTeeInput, DirectSamplingInput 4 | from owrx.form.input.validator import Range 5 | from typing import List 6 | 7 | 8 | class RtlSdrSoapySource(SoapyConnectorSource): 9 | def getSoapySettingsMappings(self): 10 | mappings = super().getSoapySettingsMappings() 11 | mappings.update({"direct_sampling": "direct_samp", "bias_tee": "biastee"}) 12 | return mappings 13 | 14 | def getDriver(self): 15 | return "rtlsdr" 16 | 17 | 18 | class RtlSdrSoapyDeviceDescription(SoapyConnectorDeviceDescription): 19 | def getName(self): 20 | return "RTL-SDR device (via SoapySDR)" 21 | 22 | def getInputs(self) -> List[Input]: 23 | return super().getInputs() + [BiasTeeInput(), DirectSamplingInput()] 24 | 25 | def getDeviceOptionalKeys(self): 26 | return super().getDeviceOptionalKeys() + ["bias_tee", "direct_sampling"] 27 | 28 | def getProfileOptionalKeys(self): 29 | return super().getProfileOptionalKeys() + ["bias_tee", "direct_sampling"] 30 | 31 | def getSampleRateRanges(self) -> List[Range]: 32 | return [Range(250000, 3200000)] 33 | -------------------------------------------------------------------------------- /owrx/source/rtl_tcp.py: -------------------------------------------------------------------------------- 1 | from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription 2 | from owrx.command import Flag, Option, Argument 3 | from owrx.form.input import Input 4 | from owrx.form.input.device import RemoteInput, DirectSamplingInput 5 | from owrx.form.input.validator import Range 6 | from typing import List 7 | 8 | 9 | class RtlTcpSource(ConnectorSource): 10 | def getCommandMapper(self): 11 | return ( 12 | super() 13 | .getCommandMapper() 14 | .setBase("rtl_tcp_connector") 15 | .setMappings( 16 | { 17 | "bias_tee": Flag("-b"), 18 | "direct_sampling": Option("-e"), 19 | "remote": Argument(), 20 | } 21 | ) 22 | ) 23 | 24 | 25 | class RtlTcpDeviceDescription(ConnectorDeviceDescription): 26 | def getName(self): 27 | return "RTL-SDR device (via rtl_tcp)" 28 | 29 | def getInputs(self) -> List[Input]: 30 | return super().getInputs() + [RemoteInput(), DirectSamplingInput()] 31 | 32 | def getDeviceMandatoryKeys(self): 33 | return super().getDeviceMandatoryKeys() + ["remote"] 34 | 35 | def getDeviceOptionalKeys(self): 36 | return super().getDeviceOptionalKeys() + ["direct_sampling"] 37 | 38 | def getProfileOptionalKeys(self): 39 | return super().getProfileOptionalKeys() + ["direct_sampling"] 40 | 41 | def getSampleRateRanges(self) -> List[Range]: 42 | return [Range(250000, 3200000)] 43 | -------------------------------------------------------------------------------- /owrx/source/runds.py: -------------------------------------------------------------------------------- 1 | from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription 2 | from owrx.command import Argument, Flag, Option 3 | from owrx.form.input import Input, DropdownInput, DropdownEnum, CheckboxInput 4 | from owrx.form.input.device import RemoteInput 5 | from owrx.form.input.validator import Range 6 | from typing import List 7 | 8 | 9 | class RundsSource(ConnectorSource): 10 | def getCommandMapper(self): 11 | return ( 12 | super() 13 | .getCommandMapper() 14 | .setBase("runds_connector") 15 | .setMappings( 16 | { 17 | "long": Flag("-l"), 18 | "remote": Argument(), 19 | "protocol": Option("-m"), 20 | } 21 | ) 22 | ) 23 | 24 | 25 | class ProtocolOptions(DropdownEnum): 26 | PROTOCOL_EB200 = ("eb200", "EB200 protocol") 27 | PROTOCOL_AMMOS = ("ammos", "Ammos protocol") 28 | 29 | def __new__(cls, *args, **kwargs): 30 | value, description = args 31 | obj = object.__new__(cls) 32 | obj._value_ = value 33 | obj.description = description 34 | return obj 35 | 36 | def __str__(self): 37 | return self.description 38 | 39 | 40 | class RundsDeviceDescription(ConnectorDeviceDescription): 41 | def getName(self): 42 | return "R&S device using EB200 or Ammos protocol" 43 | 44 | def supportsPpm(self): 45 | # currently not implemented in the connector 46 | return False 47 | 48 | def getInputs(self) -> List[Input]: 49 | return super().getInputs() + [ 50 | RemoteInput(), 51 | DropdownInput("protocol", "Protocol", ProtocolOptions), 52 | CheckboxInput("long", "Use 32-bit sample size (LONG)"), 53 | ] 54 | 55 | def getDeviceMandatoryKeys(self): 56 | return super().getDeviceMandatoryKeys() + ["remote"] 57 | 58 | def getDeviceOptionalKeys(self): 59 | return super().getDeviceOptionalKeys() + ["protocol", "long"] 60 | 61 | def getSampleRateRanges(self) -> List[Range]: 62 | # can't be very specific here due to the wide range of devices, so this is more of a sanity check. 63 | return [Range(0, 20000000)] 64 | -------------------------------------------------------------------------------- /owrx/source/sddc.py: -------------------------------------------------------------------------------- 1 | from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription 2 | from owrx.form.input.validator import Range 3 | from typing import List 4 | 5 | 6 | class SddcSource(ConnectorSource): 7 | def getCommandMapper(self): 8 | return super().getCommandMapper().setBase("sddc_connector") 9 | 10 | 11 | class SddcDeviceDescription(ConnectorDeviceDescription): 12 | def getName(self): 13 | return "BBRF103 / RX666 / RX888 device (libsddc)" 14 | 15 | def hasAgc(self): 16 | return False 17 | 18 | def getSampleRateRanges(self) -> List[Range]: 19 | # resampling is done in software... it can't cover the full range, but it's not finished either. 20 | return [Range(0, 64000000)] 21 | -------------------------------------------------------------------------------- /owrx/source/soapy_remote.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input import Input, TextInput 3 | from owrx.form.input.device import RemoteInput 4 | from owrx.form.input.converter import OptionalConverter 5 | from owrx.form.input.validator import Range 6 | from typing import List 7 | 8 | 9 | class SoapyRemoteSource(SoapyConnectorSource): 10 | def getEventNames(self): 11 | return super().getEventNames() + ["remote", "remote_driver"] 12 | 13 | def getDriver(self): 14 | return "remote" 15 | 16 | def buildSoapyDeviceParameters(self, parsed, values): 17 | params = super().buildSoapyDeviceParameters(parsed, values) 18 | params = [v for v in params if "remote" not in params] 19 | params += [{"remote": values["remote"]}] 20 | if "remote_driver" in values and values["remote_driver"] is not None: 21 | params += [{"remote:driver": values["remote_driver"]}] 22 | return params 23 | 24 | 25 | class SoapyRemoteDeviceDescription(SoapyConnectorDeviceDescription): 26 | def getName(self): 27 | return "Device connected to a SoapyRemote server" 28 | 29 | def getInputs(self) -> List[Input]: 30 | return super().getInputs() + [ 31 | RemoteInput(), 32 | TextInput( 33 | "remote_driver", 34 | "Remote driver", 35 | infotext="SoapySDR driver to be used on the remote SoapySDRServer", 36 | converter=OptionalConverter(), 37 | ), 38 | ] 39 | 40 | def getDeviceMandatoryKeys(self): 41 | return super().getDeviceMandatoryKeys() + ["remote"] 42 | 43 | def getDeviceOptionalKeys(self): 44 | return super().getDeviceOptionalKeys() + ["remote_driver"] 45 | 46 | def getSampleRateRanges(self) -> List[Range]: 47 | return [Range(500000, 20000000)] 48 | -------------------------------------------------------------------------------- /owrx/source/uhd.py: -------------------------------------------------------------------------------- 1 | from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription 2 | from owrx.form.input.validator import Range 3 | from typing import List 4 | 5 | 6 | class UhdSource(SoapyConnectorSource): 7 | def getDriver(self): 8 | return "uhd" 9 | 10 | 11 | class UhdDeviceDescription(SoapyConnectorDeviceDescription): 12 | def getName(self): 13 | return "Ettus Research USRP device" 14 | 15 | def getSampleRateRanges(self) -> List[Range]: 16 | # not sure since this depends of the specific model 17 | return [Range(0, 64000000)] 18 | -------------------------------------------------------------------------------- /owrx/version.py: -------------------------------------------------------------------------------- 1 | from distutils.version import LooseVersion 2 | 3 | _versionstring = "1.3.0-dev" 4 | looseversion = LooseVersion(_versionstring) 5 | openwebrx_version = "v{0}".format(looseversion) 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from glob import glob 2 | from setuptools import setup 3 | from owrx.version import looseversion 4 | 5 | try: 6 | from setuptools import find_namespace_packages 7 | except ImportError: 8 | from setuptools import PEP420PackageFinder 9 | 10 | find_namespace_packages = PEP420PackageFinder.find 11 | 12 | setup( 13 | name="OpenWebRX", 14 | version=str(looseversion), 15 | packages=find_namespace_packages( 16 | include=[ 17 | "owrx*", 18 | "csdr*", 19 | "htdocs", 20 | ] 21 | ), 22 | package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]}, 23 | entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]}, 24 | url="https://www.openwebrx.de/", 25 | author="Jakob Ketterl", 26 | author_email="jakob.ketterl@gmx.de", 27 | maintainer="Jakob Ketterl", 28 | maintainer_email="jakob.ketterl@gmx.de", 29 | license="GAGPL", 30 | python_requires=">=3.5", 31 | ) 32 | -------------------------------------------------------------------------------- /systemd/openwebrx.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenWebRX WebSDR receiver 3 | 4 | [Service] 5 | Type=simple 6 | User=openwebrx 7 | Group=openwebrx 8 | ExecStart=/usr/bin/openwebrx 9 | Restart=always 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/test/__init__.py -------------------------------------------------------------------------------- /test/property/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/test/property/__init__.py -------------------------------------------------------------------------------- /test/property/filter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/test/property/filter/__init__.py -------------------------------------------------------------------------------- /test/property/filter/test_by_lambda.py: -------------------------------------------------------------------------------- 1 | from owrx.property.filter import ByLambda 2 | from unittest import TestCase 3 | from unittest.mock import Mock 4 | 5 | 6 | class TestByLambda(TestCase): 7 | def testPositive(self): 8 | mock = Mock(return_value=True) 9 | filter = ByLambda(mock) 10 | self.assertTrue(filter.apply("test_key")) 11 | mock.assert_called_with("test_key") 12 | 13 | def testNegateive(self): 14 | mock = Mock(return_value=False) 15 | filter = ByLambda(mock) 16 | self.assertFalse(filter.apply("test_key")) 17 | mock.assert_called_with("test_key") 18 | -------------------------------------------------------------------------------- /test/property/filter/test_by_property_name.py: -------------------------------------------------------------------------------- 1 | from owrx.property.filter import ByPropertyName 2 | from unittest import TestCase 3 | 4 | 5 | class ByPropertyNameTest(TestCase): 6 | def testNameIsInList(self): 7 | filter = ByPropertyName("test_key") 8 | self.assertTrue(filter.apply("test_key")) 9 | 10 | def testNameNotInList(self): 11 | filter = ByPropertyName("test_key") 12 | self.assertFalse(filter.apply("other_key")) 13 | -------------------------------------------------------------------------------- /test/property/test_property_deletion.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property import PropertyDeletion 3 | 4 | 5 | class PropertyDeletionTest(TestCase): 6 | def testDeletionEvaluatesToFalse(self): 7 | deletion = PropertyDeletion() 8 | self.assertFalse(deletion) 9 | -------------------------------------------------------------------------------- /test/property/test_property_readonly.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property import PropertyLayer, PropertyReadOnly, PropertyWriteError 3 | 4 | 5 | class PropertyReadOnlyTest(TestCase): 6 | def testPreventsWrites(self): 7 | layer = PropertyLayer() 8 | layer["testkey"] = "initial value" 9 | ro = PropertyReadOnly(layer) 10 | with self.assertRaises(PropertyWriteError): 11 | ro["testkey"] = "new value" 12 | with self.assertRaises(PropertyWriteError): 13 | ro["otherkey"] = "testvalue" 14 | self.assertEqual(ro["testkey"], "initial value") 15 | self.assertNotIn("otherkey", ro) 16 | 17 | def testPreventsDeletes(self): 18 | layer = PropertyLayer(testkey="some value") 19 | ro = PropertyReadOnly(layer) 20 | with self.assertRaises(PropertyWriteError): 21 | del ro["testkey"] 22 | self.assertEqual(ro["testkey"], "some value") 23 | self.assertEqual(layer["testkey"], "some value") 24 | -------------------------------------------------------------------------------- /test/property/test_property_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property import PropertyLayer, PropertyValidator, PropertyValidationError 3 | from owrx.property.validators import NumberValidator, StringValidator 4 | 5 | 6 | class PropertyValidatorTest(TestCase): 7 | def testPassesUnvalidated(self): 8 | pm = PropertyLayer() 9 | pv = PropertyValidator(pm) 10 | pv["testkey"] = "testvalue" 11 | self.assertEqual(pv["testkey"], "testvalue") 12 | self.assertEqual(pm["testkey"], "testvalue") 13 | 14 | def testPassesValidValue(self): 15 | pv = PropertyValidator(PropertyLayer(), {"testkey": NumberValidator()}) 16 | pv["testkey"] = 42 17 | self.assertEqual(pv["testkey"], 42) 18 | 19 | def testThrowsErrorOnInvalidValue(self): 20 | pv = PropertyValidator(PropertyLayer(), {"testkey": NumberValidator()}) 21 | with self.assertRaises(PropertyValidationError): 22 | pv["testkey"] = "text" 23 | 24 | def testSetValidator(self): 25 | pv = PropertyValidator(PropertyLayer()) 26 | pv.setValidator("testkey", NumberValidator()) 27 | with self.assertRaises(PropertyValidationError): 28 | pv["testkey"] = "text" 29 | 30 | def testUpdateValidator(self): 31 | pv = PropertyValidator(PropertyLayer(), {"testkey": StringValidator()}) 32 | # this should pass 33 | pv["testkey"] = "text" 34 | pv.setValidator("testkey", NumberValidator()) 35 | # this should raise 36 | with self.assertRaises(PropertyValidationError): 37 | pv["testkey"] = "text" 38 | -------------------------------------------------------------------------------- /test/property/validators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jketterl/openwebrx/640c5b0b3e19b10d823a7ee703f2a683ec848eac/test/property/validators/__init__.py -------------------------------------------------------------------------------- /test/property/validators/test_bool_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property.validators import BoolValidator 3 | 4 | 5 | class NumberValidatorTest(TestCase): 6 | def testPassesNumbers(self): 7 | validator = BoolValidator() 8 | self.assertTrue(validator.isValid(True)) 9 | self.assertTrue(validator.isValid(False)) 10 | 11 | def testDoesntPassOthers(self): 12 | validator = BoolValidator() 13 | self.assertFalse(validator.isValid(123)) 14 | self.assertFalse(validator.isValid(-2)) 15 | self.assertFalse(validator.isValid(.5)) 16 | self.assertFalse(validator.isValid("text")) 17 | self.assertFalse(validator.isValid(object())) 18 | -------------------------------------------------------------------------------- /test/property/validators/test_float_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property.validators import FloatValidator 3 | 4 | 5 | class FloatValidatorTest(TestCase): 6 | def testPassesNumbers(self): 7 | validator = FloatValidator() 8 | self.assertTrue(validator.isValid(.5)) 9 | 10 | def testDoesntPassOthers(self): 11 | validator = FloatValidator() 12 | self.assertFalse(validator.isValid(123)) 13 | self.assertFalse(validator.isValid(-2)) 14 | self.assertFalse(validator.isValid("text")) 15 | self.assertFalse(validator.isValid(object())) 16 | -------------------------------------------------------------------------------- /test/property/validators/test_integer_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property.validators import IntegerValidator 3 | 4 | 5 | class IntegerValidatorTest(TestCase): 6 | def testPassesIntegers(self): 7 | validator = IntegerValidator() 8 | self.assertTrue(validator.isValid(123)) 9 | self.assertTrue(validator.isValid(-2)) 10 | 11 | def testDoesntPassOthers(self): 12 | validator = IntegerValidator() 13 | self.assertFalse(validator.isValid(.5)) 14 | self.assertFalse(validator.isValid("text")) 15 | self.assertFalse(validator.isValid(object())) 16 | -------------------------------------------------------------------------------- /test/property/validators/test_lambda_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import Mock 3 | from owrx.property.validators import LambdaValidator 4 | 5 | 6 | class LambdaValidatorTest(TestCase): 7 | def testPassesValue(self): 8 | mock = Mock() 9 | validator = LambdaValidator(mock.method) 10 | validator.isValid("test") 11 | mock.method.assert_called_once_with("test") 12 | 13 | def testReturnsTrue(self): 14 | validator = LambdaValidator(lambda x: True) 15 | self.assertTrue(validator.isValid("any value")) 16 | self.assertTrue(validator.isValid(3.1415926)) 17 | 18 | def testReturnsFalse(self): 19 | validator = LambdaValidator(lambda x: False) 20 | self.assertFalse(validator.isValid("any value")) 21 | self.assertFalse(validator.isValid(42)) 22 | -------------------------------------------------------------------------------- /test/property/validators/test_number_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property.validators import NumberValidator 3 | 4 | 5 | class NumberValidatorTest(TestCase): 6 | def testPassesNumbers(self): 7 | validator = NumberValidator() 8 | self.assertTrue(validator.isValid(123)) 9 | self.assertTrue(validator.isValid(-2)) 10 | self.assertTrue(validator.isValid(.5)) 11 | 12 | def testDoesntPassOthers(self): 13 | validator = NumberValidator() 14 | # bool is a subclass of int, so it passes this test. 15 | # not sure if we need to be more specific or if this is alright. 16 | # self.assertFalse(validator.isValid(True)) 17 | self.assertFalse(validator.isValid("text")) 18 | self.assertFalse(validator.isValid(object())) 19 | -------------------------------------------------------------------------------- /test/property/validators/test_or_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property.validators import OrValidator, IntegerValidator, StringValidator 3 | 4 | 5 | class OrValidatorTest(TestCase): 6 | def testPassesAnyValidators(self): 7 | validator = OrValidator(IntegerValidator(), StringValidator()) 8 | self.assertTrue(validator.isValid(42)) 9 | self.assertTrue(validator.isValid("text")) 10 | 11 | def testRejectsOtherTypes(self): 12 | validator = OrValidator(IntegerValidator(), StringValidator()) 13 | self.assertFalse(validator.isValid(.5)) 14 | 15 | def testRejectsIfNoValidator(self): 16 | validator = OrValidator() 17 | self.assertFalse(validator.isValid("any value")) 18 | -------------------------------------------------------------------------------- /test/property/validators/test_regex_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property.validators import RegexValidator 3 | import re 4 | 5 | 6 | class RegexValidatorTest(TestCase): 7 | def testMatchesRegex(self): 8 | validator = RegexValidator(re.compile("abc")) 9 | self.assertTrue(validator.isValid("abc")) 10 | 11 | def testDoesntMatchRegex(self): 12 | validator = RegexValidator(re.compile("abc")) 13 | self.assertFalse(validator.isValid("xyz")) 14 | 15 | def testFailsIfValueIsNoString(self): 16 | validator = RegexValidator(re.compile("abc")) 17 | self.assertFalse(validator.isValid(42)) 18 | -------------------------------------------------------------------------------- /test/property/validators/test_string_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property.validators import StringValidator 3 | 4 | class StringValidatorTest(TestCase): 5 | def testPassesStrings(self): 6 | validator = StringValidator() 7 | self.assertTrue(validator.isValid("text")) 8 | 9 | def testDoesntPassOthers(self): 10 | validator = StringValidator() 11 | self.assertFalse(validator.isValid(123)) 12 | self.assertFalse(validator.isValid(-2)) 13 | self.assertFalse(validator.isValid(.5)) 14 | self.assertFalse(validator.isValid(object())) 15 | -------------------------------------------------------------------------------- /test/property/validators/test_validator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from owrx.property.validators import Validator, NumberValidator, LambdaValidator, StringValidator 3 | 4 | 5 | class ValidatorTest(TestCase): 6 | 7 | def testReturnsValidator(self): 8 | validator = NumberValidator() 9 | self.assertIs(validator, Validator.of(validator)) 10 | 11 | def testTransformsLambda(self): 12 | def my_callable(v): 13 | return True 14 | validator = Validator.of(my_callable) 15 | self.assertIsInstance(validator, LambdaValidator) 16 | self.assertTrue(validator.isValid("test")) 17 | 18 | def testGetsValidatorByKey(self): 19 | validator = Validator.of("str") 20 | self.assertIsInstance(validator, StringValidator) 21 | --------------------------------------------------------------------------------