126 Commits

Author SHA1 Message Date
57f55bbdd5 Merge pull request #342 from bd5rv/develop
Add --no-http-keep-alive to wget's arguments
2023-03-10 21:11:31 +01:00
e8ba61bb81 Add --no-http-keep-alive to wget's arguments
When downloading SDRPlay API from sdrplay.com'server, it reports 403 forbidden error when the environment variable https_proxy is set. Adding --no-http-keep-live argument to wget will make the download working.
2023-03-11 00:31:19 +08:00
cb5b2e64af change chunking to work with actual byte-sizes 2023-02-22 17:23:11 +01:00
685b9970d2 switch frequency field to 5 bytes to support QO-100 2023-02-22 16:19:18 +01:00
c385a8f6b1 update changelogs 2023-02-22 14:13:01 +01:00
aa60b9d4a7 add msk144decoder to docker build 2023-02-21 19:50:44 +01:00
bff09e3363 add msk144decoder to recommended packages 2023-02-21 17:42:33 +01:00
774f8bd91a remove debugging messages 2023-02-21 17:41:55 +01:00
b1684908a4 remove todo 2023-02-19 16:18:49 +01:00
ed76fd7606 add MSK144 service demodulator 2023-02-19 16:18:08 +01:00
7b3f212ccb improve error handling during service initialization 2023-02-19 16:14:08 +01:00
216a3db45d add MSK144 to list of pskreporter modes 2023-02-16 19:47:46 +01:00
c16de474c6 route msk144 data to the wsjt message panel 2023-02-14 18:45:51 +01:00
afcd8277d1 add MSK144 parsing 2023-02-14 18:36:17 +01:00
525b70d495 add msk144 frequencies 2023-02-14 15:40:20 +01:00
f58023f3e5 add msk144demodulator chain 2023-02-14 15:39:59 +01:00
252edb7a5a add feature detection 2023-02-14 15:38:33 +01:00
2993cc4279 update wsjt-x homepage url 2023-02-14 15:37:37 +01:00
cc4f3c6c1d correctly commit patch? 2023-01-28 19:23:46 +01:00
0de597481c update to wsjt-x 2.6.1 2023-01-28 19:13:10 +01:00
2342bb5d29 update WSJT-X download location 2023-01-28 17:36:45 +01:00
b1ac8caf9b replace with a more robust state engine implementation 2023-01-28 16:37:32 +01:00
d79a1396a6 change name for sdrplay device in default config 2022-12-11 23:54:35 +01:00
1e6e7528b5 add a pseudo-input to display the sdr device type 2022-12-11 23:48:56 +01:00
f6326f8631 use a more generic include instead of manually updating the list 2022-12-11 22:41:10 +01:00
c61986dbcc add log module to the list 2022-12-11 21:06:22 +01:00
bbc9d9e7a8 log names instead of ids for improved transparency 2022-12-11 20:41:35 +01:00
a309af40e9 update dependencies 2022-12-11 20:29:45 +01:00
f3a3b9243c update connectors 2022-12-11 00:58:58 +01:00
edc9009359 fix svg include path 2022-12-10 19:50:47 +01:00
13e323cdd2 show sdr device log messages in the web configuration 2022-12-10 19:50:26 +01:00
ab40a2934f remove old config (no longer used) 2022-12-10 19:44:35 +01:00
322b6a0d52 set loglevels sooner 2022-11-30 18:53:09 +01:00
bba900d8f8 fix config default 2022-11-30 18:51:01 +01:00
64f0510da0 use a dropdown for callsign database setting; add aprs.fi 2022-11-30 16:54:22 +01:00
4050bd7f96 update version in feature check 2022-11-30 01:16:12 +01:00
35abd711ca update dependencies 2022-11-30 01:15:28 +01:00
258e41669e structured callsign data 2022-11-30 01:07:16 +01:00
975f5ffdf0 make loglevel adjustable in config or on CLI 2022-11-29 20:23:39 +01:00
90ed47a115 move pocsag demodulator to digiham to fix import problems 2022-11-10 22:43:08 +01:00
271bd723bc Merge pull request #318 from luarvique/map_distance
Adding distance display to the info windows.
2022-10-04 16:32:09 +02:00
8a7e91be38 update all docker dependencies 2022-09-30 18:51:47 +02:00
9416db5f42 Merge branch 'master' into develop 2022-09-30 18:49:45 +02:00
0127e32ea1 Removing dash from APRS callsigns and showing distance to them. 2022-09-26 15:24:59 -04:00
e20d94e241 update dependencies for docker 2022-09-20 18:51:09 +02:00
6c01d48493 update version 2022-09-20 18:06:03 +02:00
94269e211e fix changelog timestamp 2022-09-20 18:01:43 +02:00
811d95c7bc fifisdr fixes 2022-09-20 18:01:08 +02:00
c07e33d19d update csdr and pycsdr dependencies in docker 2022-09-19 19:07:23 +02:00
c150eca75c fifisdr fixes 2022-09-19 18:46:11 +02:00
7a61f991ad Removed parentheses, added a space before "km". 2022-09-16 00:02:17 -04:00
4423c7f13a Adding distance display to the info windows. 2022-09-15 19:34:39 -04:00
5cd0847362 Merge pull request #306 from luarvique/filter_width
Added filter boundaries display.
2022-07-31 22:44:07 +02:00
1635cbfa42 Merge pull request #307 from luarvique/callsign_lookup
Added an option to add callsign database URL for lookups on a map.
2022-07-31 22:35:19 +02:00
e92b6d657b Addressing comments from jketterl. 2022-07-31 16:10:33 -04:00
d5f7ce9508 Merge pull request #309 from luarvique/waterfall_colors
Now calculating waterfall colors based on what is on the screen (with…
2022-07-31 22:08:01 +02:00
bfd4d5657c Merge pull request #308 from luarvique/zoom_reset
Now resetting zoom when changing to a different profile.
2022-07-31 21:59:15 +02:00
bb625a5f9f Added filter boundaries display. 2022-07-31 15:27:41 -04:00
2ccdc90cc5 Added an option to add callsign database URL for lookups on a map. 2022-07-31 15:25:52 -04:00
ff43555411 Now resetting zoom when changing to a different profile. 2022-07-31 15:16:52 -04:00
339864a572 Now calculating waterfall colors based on what is on the screen (with zoom). 2022-07-31 15:15:38 -04:00
6192978f2f Merge branch 'master' into develop 2022-07-10 03:18:54 +02:00
66d4d88156 update hpsdrconnector to 0.6.1 2022-07-09 18:34:28 +02:00
c87daaabbe fix scroll events on the frequency bar 2022-06-19 22:40:47 +02:00
6f0a209a38 fix deprecation warning 2022-06-18 20:42:11 +02:00
26440d4e24 make waterfall zoom continuous 2022-06-18 15:57:06 +02:00
08188527ce high-res scroll events for the waterfall zoom 2022-06-17 19:42:40 +02:00
8532d9048e process high-resolution scroll events for the frequency display 2022-06-17 19:42:05 +02:00
1771fd55e1 move develop to the next minor version 2022-06-16 23:53:56 +02:00
0145cf5668 update release versions for docker 2022-06-16 22:48:57 +02:00
921fb23c8d prepare release of version 1.2.0 2022-06-15 18:28:50 +02:00
eb3ec5dc36 update m17-cxx-demod to version 2.3 2022-06-15 18:25:26 +02:00
35ad4712bb disable PPM input for devices that don't support it 2022-06-09 20:25:29 +02:00
fe7f2317de add a quick note about HTML being supported 2022-06-09 19:22:45 +02:00
b5bbdae317 fix failed logins for path-routed environemnts 2022-06-09 18:43:54 +02:00
cec4e326c8 prevent "None" showing up in text inputs 2022-06-09 17:24:53 +02:00
eccbdc1655 update libraries in docker 2022-06-01 18:04:54 +02:00
08485f255a add return codes 2022-06-01 17:58:06 +02:00
be8e35cbcf output more descriptive output when dependencies fail 2022-06-01 17:11:45 +02:00
843dde1a68 check for csdr & digiham python bindings 2022-06-01 16:43:18 +02:00
f018ef1d81 turn off debug logging for now 2022-06-01 16:19:24 +02:00
6b43ddf920 add udev dependencies for codecserver 2022-01-24 11:38:06 +01:00
242ec5dfd0 update docker dependencies 2022-01-24 11:03:15 +01:00
b354f38bfb add js8call as a recommended package (available in bullseye now) 2022-01-18 16:55:19 +01:00
983aa8cebc add bladerf docker image build 2022-01-12 18:01:25 +01:00
619f1254fd update wsjt-x to version 2.5.4 2022-01-12 17:59:45 +01:00
b5b52770ee update changelogs 2022-01-12 16:00:16 +01:00
7fd98c8c5c add support for blade rf devices 2022-01-12 15:48:06 +01:00
39bfba673b catch error resulting from monitor race condition 2022-01-11 21:56:16 +01:00
5adb53d990 distinguish between error condition and normal socket close 2022-01-11 19:57:52 +01:00
f3dcf5c320 check closed condition after aquiring the lock to avoid deadlocks 2022-01-05 17:55:46 +01:00
2ce7d943fa fix a client counting bug by deferring client instantiation 2022-01-03 15:19:12 +01:00
60f57bf206 add codecserver to recommended packages 2021-12-29 14:26:30 +01:00
221e0f232b try to avoid "can only be started once" error 2021-12-27 16:37:10 +01:00
46c78f6463 avoid demodulator concurrency
* this frees up resources used by the current demodulator before
  starting a new one
* this addresses an issue where users of single-channel AMBE sticks
  could not seamlessly switch between digital modes
2021-12-23 16:32:51 +01:00
40c68933e1 add preliminary parsing and display of M17 metadata 2021-12-21 21:18:17 +01:00
81b8f183c2 update connector with bias_tee fixes 2021-12-20 16:11:51 +01:00
03f0faf378 update digiham / pydigiham dependencies 2021-12-18 17:54:27 +01:00
f316b2c8ca allow latitude and longitude to be 0 in location-picker 2021-12-14 12:46:25 +01:00
6c3ef7a6ed Merge pull request #281 from chrismrutledge/patch-1
Update bands.json
2021-12-13 14:21:47 +01:00
4ce3816f48 show codecserver errors in the client 2021-12-13 13:26:47 +01:00
397155983d improve handling of failed devices 2021-12-06 15:50:03 +01:00
9c28143dfb add debugging to the feature detection system 2021-12-01 19:22:48 +01:00
ed354cfa6f Update bands.json
80M WSPR frequency change and additional 60M frequency according to wsprnet.org.
2021-11-28 07:02:16 -06:00
dcdfe7969a fix sample rate updates for secondary demods 2021-11-08 17:52:37 +01:00
6d414698e8 update to wsjt-x 2.5.2 2021-11-05 02:20:49 +01:00
70cf4557f7 update to wsjt-x 2.5.1 2021-11-02 17:31:58 +01:00
b0e18286df update connector 2021-11-02 16:11:19 +01:00
85c7a05978 use ImportError for python 3.5 compatibility 2021-10-27 18:33:23 +02:00
33c8e34456 use the resume call before pumping data from a reader 2021-10-26 16:40:38 +02:00
4bc6608e87 update csdr in docker 2021-10-25 14:15:32 +02:00
f967a8d87a catch exceptions while parsing ax25 frames 2021-10-22 15:07:42 +02:00
d757b817b1 make digimodes work in start_mod again 2021-10-15 16:41:07 +02:00
9f89a21cfb remove psk31 character animation since it's killing the client 2021-10-15 15:57:27 +02:00
aaf696e8d7 Merge pull request #273 from doccodyblue/feature/266-normalize-prometheus-metric-names
Feature/266 normalize prometheus metric names
2021-10-04 16:00:34 +02:00
efa305eeec normalize metric label to match prometheus data-model guide 2021-10-03 08:48:40 +02:00
eb43e39a81 normalize metric label to match prometheus data-model guide 2021-10-03 08:39:57 +02:00
c4687816c1 update docker to debian bullseye 2021-10-01 16:23:47 +02:00
8cce5bd889 add metrics for pocsag 2021-10-01 00:52:32 +02:00
66dd4b4581 update list of supported modes for pskreporter 2021-10-01 00:09:20 +02:00
9689ce5202 catch invalid config values for enum dropdowns and reset to default 2021-09-30 23:32:46 +02:00
818b9d87b8 add a validator that prevents invalid locations 2021-09-30 23:26:26 +02:00
0f2aca62f3 code style 2021-09-30 23:09:22 +02:00
1e57fb4609 expect a broken pipe 2021-09-30 23:04:59 +02:00
0b64b4ac97 handle errors when gps coordinates are out of range 2021-09-30 23:03:21 +02:00
460bada88f update owrx libraries 2021-09-30 01:37:10 +02:00
93 changed files with 1708 additions and 1242 deletions

View File

@ -1,6 +1,17 @@
**unreleased** **unreleased**
- SDR device log messages are now available in the web configuration to simplify troubleshooting
- Added support for the MSK144 digimode
**1.2.1**
- FifiSDR support fixed (pipeline formats now line up correctly)
- Added "Device" input for FifiSDR devices for sound card selection
**1.2.0**
- Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator - Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator
modules modules
- Preliminary display of M17 callsign information
- New devices supported:
- Blade RF
**1.1.0** **1.1.0**
- Reworked most graphical elements as SVGs for faster loadtimes and crispier display on hi-dpi displays - Reworked most graphical elements as SVGs for faster loadtimes and crispier display on hi-dpi displays

View File

@ -14,7 +14,7 @@ It has the following features:
- supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices) - supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices)
- Multiple SDR devices can be used simultaneously - Multiple SDR devices can be used simultaneously
- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN) - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN)
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4, - [wsjt-x](https://wsjt.sourceforge.io/) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4,
FST4W) FST4W)
- [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets - [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets
- [JS8Call](http://js8call.com/) support - [JS8Call](http://js8call.com/) support

View File

@ -42,7 +42,7 @@
"frequencies": { "frequencies": {
"bpsk31": 3580000, "bpsk31": 3580000,
"ft8": 3573000, "ft8": 3573000,
"wspr": 3592600, "wspr": 3568600,
"jt65": 3570000, "jt65": 3570000,
"jt9": 3572000, "jt9": 3572000,
"ft4": [3568000, 3575000], "ft4": [3568000, 3575000],
@ -56,7 +56,7 @@
"upper_bound": 5366500, "upper_bound": 5366500,
"frequencies": { "frequencies": {
"ft8": 5357000, "ft8": 5357000,
"wspr": 5364700 "wspr": [5287200, 5364700]
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },
@ -177,7 +177,8 @@
"jt9": 50312000, "jt9": 50312000,
"ft4": 50318000, "ft4": 50318000,
"js8": 50318000, "js8": 50318000,
"q65": [50211000, 50275000] "q65": [50211000, 50275000],
"msk144": 50260000
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },
@ -186,7 +187,8 @@
"lower_bound": 70150000, "lower_bound": 70150000,
"upper_bound": 70200000, "upper_bound": 70200000,
"frequencies": { "frequencies": {
"wspr": 70091000 "wspr": 70091000,
"msk144": 70230000
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },
@ -200,7 +202,8 @@
"ft4": 144170000, "ft4": 144170000,
"jt65": 144120000, "jt65": 144120000,
"packet": 144800000, "packet": 144800000,
"q65": 144116000 "q65": 144116000,
"msk144": 144360000
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },
@ -210,7 +213,8 @@
"upper_bound": 440000000, "upper_bound": 440000000,
"frequencies": { "frequencies": {
"pocsag": 439987500, "pocsag": 439987500,
"q65": 432065000 "q65": 432065000,
"msk144": 432360000
}, },
"tags": ["hamradio"] "tags": ["hamradio"]
}, },

View File

@ -1,386 +0,0 @@
# -*- coding: utf-8 -*-
"""
config_webrx: configuration options for OpenWebRX
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
In addition, as a special exception, the copyright holders
state that config_rtl.py and config_webrx.py are not part of the
Corresponding Source defined in GNU AGPL version 3 section 1.
(It means that you do not have to redistribute config_rtl.py and
config_webrx.py if you make any changes to these two configuration files,
and use them for running your web service with OpenWebRX.)
"""
"""
DEPRECATION notice
As of OpenWebRX 0.21, the configuration system has been completely overhauled.
The configuration of OpenWebRX should now be done in the new web-based
configuration interface exclusively.
Existing configurations can still be used, but their values will be migrated
to the new storage infrastructure as soon as the web configuration is used to
edit them.
The new configuration storage is not intended to be edited manually.
"""
# configuration version. please only modify if you're able to perform the associated migration steps.
version = 7
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
# https://github.com/jketterl/openwebrx/wiki/Configuration-guide
# ==== Server settings ====
#max_clients = 20
# ==== Web GUI configuration ====
#receiver_name = "[Callsign]"
#receiver_location = "Budapest, Hungary"
#receiver_asl = 200
#receiver_admin = "example@example.com"
#receiver_gps = {"lat": 47.000000, "lon": 19.000000}
#photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
# photo_desc allows you to put pretty much any HTML you like into the receiver description.
# The lines below should give you some examples of what's possible.
#photo_desc = """
#You can add your own background photo and receiver information.<br />
#Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
#Device: Receiver Device<br />
#Antenna: Receiver Antenna<br />
#Website: <a href="http://localhost" target="_blank">http://localhost</a>
#"""
# ==== Public receiver listings ====
# You can publish your receiver on online receiver directories, like https://www.receiverbook.de
# You will receive a receiver key from the directory that will authenticate you as the operator of this receiver.
# Please note that you not share your receiver keys publicly since anyone that obtains your receiver key can take over
# your public listing.
# Your receiver keys should be placed into this array:
#receiver_keys = []
# If you list your receiver on multiple sites, you can place all your keys into the array above, or you can append
# keys to the arraylike this:
# receiver_keys += ["my-receiver-key"]
# If you're not sure, simply copy & paste the code you received from your listing site below this line:
# ==== DSP/RX settings ====
#fft_fps = 9
#fft_size = 4096 # Should be power of 2
#fft_voverlap_factor = (
# 0.3 # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
#)
#audio_compression = "adpcm" # valid values: "adpcm", "none"
#fft_compression = "adpcm" # valid values: "adpcm", "none"
# Tau setting for WFM (broadcast FM) deemphasis\
# Quote from wikipedia https://en.wikipedia.org/wiki/FM_broadcasting#Pre-emphasis_and_de-emphasis
# "In most of the world a 50 µs time constant is used. In the Americas and South Korea, 75 µs is used"
# Enable one of the following lines, depending on your location:
# wfm_deemphasis_tau = 75e-6 # for US and South Korea
#wfm_deemphasis_tau = 50e-6 # for the rest of the world
#digimodes_fft_size = 2048
# enables lookup of DMR ids using the radioid api
#digital_voice_dmr_id_lookup = True
"""
Note: if you experience audio underruns while CPU usage is 100%, you can:
- decrease `samp_rate`,
- set `fft_voverlap_factor` to 0,
- decrease `fft_fps` and `fft_size`,
- limit the number of users by decreasing `max_clients`.
"""
# ==== I/Q sources ====
# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.)
###############################################################################
# Is my SDR hardware supported? #
# Check here: https://github.com/jketterl/openwebrx/wiki/Supported-Hardware #
###############################################################################
# Currently supported types of sdr receivers:
# "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr",
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote", "hpsdr", "uhd",
# "radioberry", "fcdpp", "rtl_tcp", "sddc", "runds"
# For more details on specific types, please checkout the wiki:
# https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices
#sdrs = {
# "rtlsdr": {
# "name": "RTL-SDR USB Stick",
# "type": "rtl_sdr",
# "ppm": 0,
# # you can change this if you use an upconverter. formula is:
# # center_freq + lfo_offset = actual frequency on the sdr
# # "lfo_offset": 0,
# "profiles": {
# "70cm": {
# "name": "70cm Relais",
# "center_freq": 438800000,
# "rf_gain": 29,
# "samp_rate": 2400000,
# "start_freq": 439275000,
# "start_mod": "nfm",
# },
# "2m": {
# "name": "2m komplett",
# "center_freq": 145000000,
# "rf_gain": 29,
# "samp_rate": 2048000,
# "start_freq": 145725000,
# "start_mod": "nfm",
# },
# },
# },
# "airspy": {
# "name": "Airspy HF+",
# "type": "airspyhf",
# "ppm": 0,
# "rf_gain": "auto",
# "profiles": {
# "20m": {
# "name": "20m",
# "center_freq": 14150000,
# "samp_rate": 384000,
# "start_freq": 14070000,
# "start_mod": "usb",
# },
# "30m": {
# "name": "30m",
# "center_freq": 10125000,
# "samp_rate": 192000,
# "start_freq": 10142000,
# "start_mod": "usb",
# },
# "40m": {
# "name": "40m",
# "center_freq": 7100000,
# "samp_rate": 256000,
# "start_freq": 7070000,
# "start_mod": "lsb",
# },
# "80m": {
# "name": "80m",
# "center_freq": 3650000,
# "samp_rate": 384000,
# "start_freq": 3570000,
# "start_mod": "lsb",
# },
# "49m": {
# "name": "49m Broadcast",
# "center_freq": 6050000,
# "samp_rate": 384000,
# "start_freq": 6070000,
# "start_mod": "am",
# },
# },
# },
# "sdrplay": {
# "name": "SDRPlay RSP2",
# "type": "sdrplay",
# "ppm": 0,
# "antenna": "Antenna A",
# "profiles": {
# "20m": {
# "name": "20m",
# "center_freq": 14150000,
# "rf_gain": 0,
# "samp_rate": 500000,
# "start_freq": 14070000,
# "start_mod": "usb",
# },
# "30m": {
# "name": "30m",
# "center_freq": 10125000,
# "rf_gain": 0,
# "samp_rate": 250000,
# "start_freq": 10142000,
# "start_mod": "usb",
# },
# "40m": {
# "name": "40m",
# "center_freq": 7100000,
# "rf_gain": 0,
# "samp_rate": 500000,
# "start_freq": 7070000,
# "start_mod": "lsb",
# },
# "80m": {
# "name": "80m",
# "center_freq": 3650000,
# "rf_gain": 0,
# "samp_rate": 500000,
# "start_freq": 3570000,
# "start_mod": "lsb",
# },
# "49m": {
# "name": "49m Broadcast",
# "center_freq": 6000000,
# "rf_gain": 0,
# "samp_rate": 500000,
# "start_freq": 6070000,
# "start_mod": "am",
# },
# },
# },
#}
# ==== Color themes ====
### google turbo colormap (see: https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html)
#waterfall_scheme = "GoogleTurboWaterfall"
### original theme by teejez:
#waterfall_scheme = "TeejeezWaterfall"
### old theme by HA7ILM:
#waterfall_scheme = "Ha7ilmWaterfall"
##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
### custom waterfall schemes can be configured like this:
#waterfall_scheme = "CustomWaterfall"
#waterfall_colors = [0x0000FF, 0x00FF00, 0xFF0000]
### Waterfall calibration
#waterfall_levels = {"min": -88, "max": -20} # in dB
#waterfall_auto_levels = {"min": 3, "max": 10}
#waterfall_auto_min_range = 50
# Note: When the auto waterfall level button is clicked, the following happens:
# [waterfall_levels.min] = [current_min_power_level] - [waterfall_auto_levels["min"]]
# [waterfall_levels.max] = [current_max_power_level] + [waterfall_auto_levels["max"]]
#
# ___|__________________________________|____________________________________|__________________________________|___> signal power
# \_waterfall_auto_levels["min"]_/ |__ current_min_power_level | \_waterfall_auto_levels["max"]_/
# current_max_power_level __|
# This setting allows you to modify the precision of the frequency displays in OpenWebRX.
# Set this to exponent of 10 to select the most precise digit in Hz you'd like to see
# examples:
# a value of 2 selects 10^2 = 100Hz tuning precision (default):
#tuning_precision = 2
# a value of 1 selects 10^1 = 10Hz tuning precision:
#tuning_precision = 1
# This setting tells the auto-squelch the offset to add to the current signal level to use as the new squelch level.
# Lowering this setting will give you a more sensitive squelch, but it may also cause unwanted squelch openings when
# using the auto squelch.
#squelch_auto_margin = 10 # in dB
#google_maps_api_key = ""
# how long should positions be visible on the map?
# they will start fading out after half of that
# in seconds; default: 2 hours
#map_position_retention_time = 2 * 60 * 60
# decoder queue configuration
# due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount
# of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
# to mitigate this, the recordings will be queued and processed in sequence.
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
#decoding_queue_workers = 2
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up
# if you are running background services, make sure this number is high enough to accept the task influx during peaks
# i.e. this should be higher than the number of decoding services running at the same time
#decoding_queue_length = 10
# wsjt decoding depth will allow more results, but will also consume more cpu
#wsjt_decoding_depth = 3
# can also be set for each mode separately
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
#wsjt_decoding_depths = {"jt65": 1}
# FST4 can be transmitted in different intervals. This setting determines which intervals will be decoded.
# available values (in seconds): 15, 30, 60, 120, 300, 900, 1800
#fst4_enabled_intervals = [15, 30]
# FST4W can be transmitted in different intervals. This setting determines which intervals will be decoded.
# available values (in seconds): 120, 300, 900, 1800
#fst4w_enabled_intervals = [120, 300]
# Q65 allows many combinations of intervals and submodes. This setting determines which combinations will be decoded.
# Please use the mode letter followed by the decode interval in seconds to specify the combinations. For example:
#q65_enabled_combinations = ["A30", "E120", "C60"]
# JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled.
#js8_enabled_profiles = ["normal", "slow"]
# JS8 decoding depth; higher value will get more results, but will also consume more cpu
#js8_decoding_depth = 3
# Enable background service for decoding digital data. You can find more information at:
# https://github.com/jketterl/openwebrx/wiki/Background-decoding
#services_enabled = False
#services_decoders = ["ft8", "ft4", "wspr", "packet"]
# === aprs igate settings ===
# If you want to share your APRS decodes with the aprs network, configure these settings accordingly.
# Make sure that you have set services_enabled to true and customize services_decoders to your needs.
#aprs_callsign = "N0CALL"
#aprs_igate_enabled = False
#aprs_igate_server = "euro.aprs2.net"
#aprs_igate_password = ""
# beacon uses the receiver_gps setting, so if you enable this, make sure the location is correct there
#aprs_igate_beacon = False
# Uncomment the following to customize gateway beacon details reported to the aprs network
# Plese see Dire Wolf's documentation on PBEACON configuration for complete details:
# https://github.com/wb2osz/direwolf/raw/master/doc/User-Guide.pdf
# Symbol in its two-character form as specified by the APRS spec at http://www.aprs.org/symbols/symbols-new.txt
# Default: Receive only IGate (do not send msgs back to RF)
# aprs_igate_symbol = "R&"
# Custom comment about igate
# Default: OpenWebRX APRS gateway
# aprs_igate_comment = "OpenWebRX APRS gateway"
# Antenna Height and Gain details
# Unspecified by default
# Antenna height above average terrain (HAAT) in meters
# aprs_igate_height = "5"
# Antenna gain in dBi
# aprs_igate_gain = "0"
# Antenna direction (N, NE, E, SE, S, SW, W, NW). Omnidirectional by default
# aprs_igate_dir = "NE"
# === PSK Reporter settings ===
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
#pskreporter_enabled = False
#pskreporter_callsign = "N0CALL"
# optional antenna information, uncomment to enable
#pskreporter_antenna_information = "Dipole"
# === WSPRNet reporting settings
# enable this if you want to upload WSPR spots to wsprnet.ort
# in addition to these settings also make sure that receiver_gps contains your correct location
#wsprnet_enabled = False
#wsprnet_callsign = "N0CALL"

View File

@ -67,3 +67,7 @@ class SecondaryDemodulator(Chain):
class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta): class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta):
pass pass
class DemodulatorError(Exception):
pass

View File

@ -1,9 +1,10 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedAudioRateChain, FixedIfSampleRateChain, DialFrequencyReceiver, MetaProvider, SlotFilterChain from csdr.chain.demodulator import BaseDemodulatorChain, FixedAudioRateChain, FixedIfSampleRateChain, DialFrequencyReceiver, MetaProvider, SlotFilterChain, DemodulatorError, ServiceDemodulator
from pycsdr.modules import FmDemod, Agc, Writer, Buffer from pycsdr.modules import FmDemod, Agc, Writer, Buffer
from pycsdr.types import Format from pycsdr.types import Format
from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder from digiham.modules import DstarDecoder, DcBlock, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder, PocsagDecoder
from digiham.ambe import Modes from digiham.ambe import Modes, ServerError
from owrx.meta import MetaParser from owrx.meta import MetaParser
from owrx.pocsag import PocsagParser
class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, DialFrequencyReceiver, MetaProvider): class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, DialFrequencyReceiver, MetaProvider):
@ -17,10 +18,16 @@ class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateC
workers = [FmDemod(), DcBlock()] workers = [FmDemod(), DcBlock()]
if filter is not None: if filter is not None:
workers += [filter] workers += [filter]
try:
mbeSynthesizer = MbeSynthesizer(mbeMode, codecserver)
except ConnectionError as ce:
raise DemodulatorError("Connection to codecserver failed: {}".format(ce))
except ServerError as se:
raise DemodulatorError("Codecserver error: {}".format(se))
workers += [ workers += [
fskDemodulator, fskDemodulator,
decoder, decoder,
MbeSynthesizer(mbeMode, codecserver), mbeSynthesizer,
DigitalVoiceFilter(), DigitalVoiceFilter(),
agc agc
] ]
@ -103,3 +110,24 @@ class Ysf(DigihamChain):
filter=WideRrcFilter(), filter=WideRrcFilter(),
codecserver=codecserver codecserver=codecserver
) )
class PocsagDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self):
self.parser = PocsagParser()
workers = [
FmDemod(),
FskDemodulator(samplesPerSymbol=40, invert=True),
PocsagDecoder(),
self.parser,
]
super().__init__(workers)
def supportsSquelch(self) -> bool:
return False
def getFixedAudioRate(self) -> int:
return 48000
def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency)

View File

@ -1,12 +1,11 @@
from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain
from csdr.module.msk144 import Msk144Module, ParserAdapter
from owrx.audio.chopper import AudioChopper, AudioChopperParser from owrx.audio.chopper import AudioChopper, AudioChopperParser
from owrx.aprs.kiss import KissDeframer from owrx.aprs.kiss import KissDeframer
from owrx.aprs import Ax25Parser, AprsParser from owrx.aprs import Ax25Parser, AprsParser
from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder
from pycsdr.types import Format from pycsdr.types import Format
from owrx.aprs.module import DirewolfModule from owrx.aprs.module import DirewolfModule
from digiham.modules import FskDemodulator, PocsagDecoder
from owrx.pocsag import PocsagParser
class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver): class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
@ -22,6 +21,23 @@ class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver):
self.chopper.setDialFrequency(frequency) self.chopper.setDialFrequency(frequency)
class Msk144Demodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self):
self.parser = ParserAdapter()
workers = [
Convert(Format.FLOAT, Format.SHORT),
Msk144Module(),
self.parser,
]
super().__init__(workers)
def getFixedAudioRate(self) -> int:
return 12000
def setDialFrequency(self, frequency: int) -> None:
self.parser.setDialFrequency(frequency)
class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver): class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
def __init__(self, service: bool = False): def __init__(self, service: bool = False):
self.parser = AprsParser() self.parser = AprsParser()
@ -45,23 +61,6 @@ class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver):
self.parser.setDialFrequency(frequency) self.parser.setDialFrequency(frequency)
class PocsagDemodulator(ServiceDemodulator):
def __init__(self):
workers = [
FmDemod(),
FskDemodulator(samplesPerSymbol=40, invert=True),
PocsagDecoder(),
PocsagParser(),
]
super().__init__(workers)
def supportsSquelch(self) -> bool:
return False
def getFixedAudioRate(self) -> int:
return 48000
class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain): class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain):
def __init__(self, baudRate: float): def __init__(self, baudRate: float):
self.baudRate = baudRate self.baudRate = baudRate

14
csdr/chain/dummy.py Normal file
View File

@ -0,0 +1,14 @@
from pycsdr.types import Format
from csdr.chain import Module
class DummyDemodulator(Module):
def __init__(self, outputFormat: Format):
self.outputFormat = outputFormat
super().__init__()
def getInputFormat(self) -> Format:
return Format.COMPLEX_FLOAT
def getOutputFormat(self) -> Format:
return self.outputFormat

View File

@ -1,18 +1,19 @@
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider
from csdr.module.m17 import M17Module from csdr.module.m17 import M17Module
from pycsdr.modules import FmDemod, Limit, Convert from pycsdr.modules import FmDemod, Limit, Convert, Writer
from pycsdr.types import Format from pycsdr.types import Format
from digiham.modules import DcBlock from digiham.modules import DcBlock
class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain): class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider):
def __init__(self): def __init__(self):
self.module = M17Module()
workers = [ workers = [
FmDemod(), FmDemod(),
DcBlock(), DcBlock(),
Limit(), Limit(),
Convert(Format.FLOAT, Format.SHORT), Convert(Format.FLOAT, Format.SHORT),
M17Module(), self.module,
] ]
super().__init__(workers) super().__init__(workers)
@ -24,3 +25,6 @@ class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain):
def supportsSquelch(self) -> bool: def supportsSquelch(self) -> bool:
return False return False
def setMetaWriter(self, writer: Writer) -> None:
self.module.setMetaWriter(writer)

View File

@ -37,6 +37,8 @@ class Module(BaseModule, metaclass=ABCMeta):
data = read() data = read()
except ValueError: except ValueError:
pass pass
except BrokenPipeError:
break
if data is None or isinstance(data, bytes) and len(data) == 0: if data is None or isinstance(data, bytes) and len(data) == 0:
break break
write(data) write(data)
@ -116,10 +118,15 @@ class PopenModule(AutoStartModule, metaclass=ABCMeta):
def getCommand(self): def getCommand(self):
pass pass
def _getProcess(self):
return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE)
def start(self): def start(self):
self.process = Popen(self.getCommand(), stdin=PIPE, stdout=PIPE) self.process = self._getProcess()
# resume in case the reader has been stop()ed before
self.reader.resume()
Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start() Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
Thread(target=self.pump(partial(self.process.stdout.read, 1024), self.writer.write)).start() Thread(target=self.pump(partial(self.process.stdout.read1, 1024), self.writer.write)).start()
def stop(self): def stop(self):
if self.process is not None: if self.process is not None:

View File

@ -1,8 +1,20 @@
from csdr.module import PopenModule from csdr.module import PopenModule
from pycsdr.types import Format from pycsdr.types import Format
from pycsdr.modules import Writer
from subprocess import Popen, PIPE
from threading import Thread
import re
import pickle
class M17Module(PopenModule): class M17Module(PopenModule):
lsfRegex = re.compile("SRC: ([a-zA-Z0-9]+), DEST: ([a-zA-Z0-9]+)")
def __init__(self):
super().__init__()
self.metawriter = None
def getInputFormat(self) -> Format: def getInputFormat(self) -> Format:
return Format.SHORT return Format.SHORT
@ -10,4 +22,37 @@ class M17Module(PopenModule):
return Format.SHORT return Format.SHORT
def getCommand(self): def getCommand(self):
return ["m17-demod"] return ["m17-demod", "-l"]
def _getProcess(self):
return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE, stderr=PIPE)
def start(self):
super().start()
Thread(target=self._readOutput).start()
def _readOutput(self):
while True:
line = self.process.stderr.readline()
if not line:
break
self.parseOutput(line.decode())
def parseOutput(self, line):
if self.metawriter is None:
return
matches = self.lsfRegex.match(line)
msg = {"protocol": "M17"}
if matches:
# fake sync
msg["sync"] = "voice"
msg["source"] = matches.group(1)
msg["destination"] = matches.group(2)
elif line.startswith("EOS"):
pass
else:
return
self.metawriter.write(pickle.dumps(msg))
def setMetaWriter(self, writer: Writer) -> None:
self.metawriter = writer

57
csdr/module/msk144.py Normal file
View File

@ -0,0 +1,57 @@
from pycsdr.types import Format
from csdr.module import PopenModule, ThreadModule
from owrx.wsjt import WsjtParser, Msk144Profile
import pickle
import logging
logger = logging.getLogger(__name__)
class Msk144Module(PopenModule):
def getCommand(self):
return ["msk144decoder"]
def getInputFormat(self) -> Format:
return Format.SHORT
def getOutputFormat(self) -> Format:
return Format.CHAR
class ParserAdapter(ThreadModule):
def __init__(self):
self.retained = bytes()
self.parser = WsjtParser()
self.dialFrequency = 0
super().__init__()
def run(self):
profile = Msk144Profile()
while self.doRun:
data = self.reader.read()
if data is None:
self.doRun = False
else:
self.retained += data
lines = self.retained.split(b"\n")
# keep the last line
# this should either be empty if the last char was \n
# or an incomplete line if the read returned early
self.retained = lines[-1]
# parse all completed lines
for line in lines[0:-1]:
# actual messages from msk144decoder should start with "*** "
if line[0:4] == b"*** ":
self.writer.write(pickle.dumps(self.parser.parse(profile, self.dialFrequency, line[4:])))
def getInputFormat(self) -> Format:
return Format.CHAR
def getOutputFormat(self) -> Format:
return Format.CHAR
def setDialFrequency(self, frequency: int) -> None:
self.dialFrequency = frequency

21
debian/changelog vendored
View File

@ -1,9 +1,26 @@
openwebrx (1.2.0) UNRELEASED; urgency=low openwebrx (1.3.0) UNRELEASED; urgency=low
* SDR device log messages are now available in the web configuration to
simplify troubleshooting
* Added support for the MSK144 digimode
-- Jakob Ketterl <jakob.ketterl@gmx.de> Fri, 30 Sep 2022 16:47:00 +0000
openwebrx (1.2.1) bullseye jammy; urgency=low
* FifiSDR support fixed (pipeline formats now line up correctly)
* Added "Device" input for FifiSDR devices for sound card selection
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 20 Sep 2022 16:01:00 +0000
openwebrx (1.2.0) bullseye jammy; urgency=low
* Major rewrite of all demodulation components to make use of the new * Major rewrite of all demodulation components to make use of the new
csdr/pycsdr and digiham/pydigiham demodulator modules csdr/pycsdr and digiham/pydigiham demodulator modules
* Preliminary display of M17 callsign information
* New devices supported:
- Blade RF
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 03 Aug 2021 13:54:00 +0000 -- Jakob Ketterl <jakob.ketterl@gmx.de> Wed, 15 Jun 2022 16:20:00 +0000
openwebrx (1.1.0) buster hirsute; urgency=low openwebrx (1.1.0) buster hirsute; urgency=low

2
debian/control vendored
View File

@ -11,6 +11,6 @@ Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx Package: openwebrx
Architecture: all Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, owrx-connector (>= 0.5), soapysdr-tools, python3-csdr (>= 0.18), ${python3:Depends}, ${misc:Depends} Depends: adduser, python3 (>= 3.5), python3-pkg-resources, owrx-connector (>= 0.5), soapysdr-tools, python3-csdr (>= 0.18), ${python3:Depends}, ${misc:Depends}
Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.1), nmux (>= 0.18) Recommends: python3-digiham (>= 0.6), direwolf (>= 1.4), wsjtx, js8call, runds-connector (>= 0.2), hpsdrconnector, aprs-symbols, m17-demod, js8call, python3-js8py (>= 0.2), nmux (>= 0.18), codecserver (>= 0.1), msk144decoder
Description: multi-user web sdr Description: multi-user web sdr
Open source, multi-user SDR receiver with a web interface Open source, multi-user SDR receiver with a web interface

View File

@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
ARCH=$(uname -m) ARCH=$(uname -m)
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-full openwebrx" IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-bladerf openwebrx-full openwebrx"
ALL_ARCHS="x86_64 armv7l aarch64" ALL_ARCHS="x86_64 armv7l aarch64"
TAG=${TAG:-"latest"} TAG=${TAG:-"latest"}
ARCHTAG="${TAG}-${ARCH}" ARCHTAG="${TAG}-${ARCH}"

View File

@ -1,4 +1,4 @@
FROM debian:buster-slim FROM debian:bullseye-slim
COPY docker/files/js8call/js8call-hamlib.patch \ COPY docker/files/js8call/js8call-hamlib.patch \
docker/files/wsjtx/wsjtx.patch \ docker/files/wsjtx/wsjtx.patch \

View File

@ -0,0 +1,8 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
COPY docker/scripts/install-dependencies-bladerf.sh /
RUN /install-dependencies-bladerf.sh &&\
rm /install-dependencies-bladerf.sh
COPY . /opt/openwebrx

View File

@ -19,6 +19,7 @@ RUN /install-dependencies-rtlsdr.sh &&\
/install-dependencies-radioberry.sh &&\ /install-dependencies-radioberry.sh &&\
/install-dependencies-uhd.sh &&\ /install-dependencies-uhd.sh &&\
/install-dependencies-hpsdr.sh &&\ /install-dependencies-hpsdr.sh &&\
/install-dependencies-bladerf.sh &&\
/install-connectors.sh &&\ /install-connectors.sh &&\
/install-dependencies-runds.sh &&\ /install-dependencies-runds.sh &&\
rm /install-dependencies-*.sh &&\ rm /install-dependencies-*.sh &&\

View File

@ -1,6 +1,6 @@
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
--- wsjtx-orig/CMakeLists.txt 2021-09-28 14:36:01.731488130 +0200 --- wsjtx-orig/CMakeLists.txt 2023-01-28 17:43:05.586124507 +0100
+++ wsjtx/CMakeLists.txt 2021-09-28 15:51:30.136197625 +0200 +++ wsjtx/CMakeLists.txt 2023-01-28 17:56:07.108634912 +0100
@@ -122,7 +122,7 @@ @@ -122,7 +122,7 @@
option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.") option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.")
option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON) option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON)
@ -10,7 +10,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.") option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.")
option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.") option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.")
option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON) option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON)
@@ -169,76 +169,7 @@ @@ -170,77 +170,7 @@
) )
set (wsjt_qt_CXXSRCS set (wsjt_qt_CXXSRCS
@ -34,6 +34,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- widgets/FrequencyDeltaLineEdit.cpp - widgets/FrequencyDeltaLineEdit.cpp
- item_delegates/CandidateKeyFilter.cpp - item_delegates/CandidateKeyFilter.cpp
- item_delegates/ForeignKeyDelegate.cpp - item_delegates/ForeignKeyDelegate.cpp
- item_delegates/MessageItemDelegate.cpp
- validators/LiveFrequencyValidator.cpp - validators/LiveFrequencyValidator.cpp
- GetUserId.cpp - GetUserId.cpp
- Audio/AudioDevice.cpp - Audio/AudioDevice.cpp
@ -87,16 +88,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
) )
set (wsjt_qtmm_CXXSRCS set (wsjt_qtmm_CXXSRCS
@@ -884,8 +815,6 @@ @@ -1089,9 +1019,6 @@
check_type_size (CACHE_ALL HAMLIB_OLD_CACHING)
check_symbol_exists (rig_set_cache_timeout_ms "hamlib/rig.h" HAVE_HAMLIB_CACHING)
-find_package (Portaudio REQUIRED)
-
find_package (Usb REQUIRED)
#
@@ -1081,9 +1010,6 @@
if (WSJT_GENERATE_DOCS) if (WSJT_GENERATE_DOCS)
add_subdirectory (doc) add_subdirectory (doc)
endif (WSJT_GENERATE_DOCS) endif (WSJT_GENERATE_DOCS)
@ -106,7 +98,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# build a library of package functionality (without and optionally with OpenMP support) # build a library of package functionality (without and optionally with OpenMP support)
add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS}) add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS})
@@ -1341,10 +1267,7 @@ @@ -1357,10 +1284,7 @@
add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS}) add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS})
# set wsjtx_udp exports to static variants # set wsjtx_udp exports to static variants
target_compile_definitions (wsjt_qt PUBLIC UDP_STATIC_DEFINE) target_compile_definitions (wsjt_qt PUBLIC UDP_STATIC_DEFINE)
@ -118,7 +110,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# build a library of package Qt functionality used in Fortran utilities # build a library of package Qt functionality used in Fortran utilities
add_library (fort_qt STATIC ${fort_qt_CXXSRCS}) add_library (fort_qt STATIC ${fort_qt_CXXSRCS})
@@ -1408,60 +1331,6 @@ @@ -1425,90 +1349,6 @@
add_subdirectory (map65) add_subdirectory (map65)
endif () endif ()
@ -155,7 +147,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
- ) - )
- -
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS}) -target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
-if (APPLE) -if ((NOT ${OPENMP_FOUND}) OR APPLE)
- target_link_libraries (wsjtx wsjt_fort) - target_link_libraries (wsjtx wsjt_fort)
-else () -else ()
- target_link_libraries (wsjtx wsjt_fort_omp) - target_link_libraries (wsjtx wsjt_fort_omp)
@ -176,10 +168,40 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
-endif () -endif ()
-target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES}) -target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES})
- -
# make a library for WSJT-X UDP servers -# make a library for WSJT-X UDP servers
# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS}) -# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS}) -add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
@@ -1501,47 +1370,9 @@ -#target_include_directories (wsjtx_udp
-# INTERFACE
-# $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/wsjtx>
-# )
-target_include_directories (wsjtx_udp-static
- INTERFACE
- $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/wsjtx>
- )
-#set_target_properties (wsjtx_udp PROPERTIES
-# PUBLIC_HEADER "${UDP_library_HEADERS}"
-# )
-set_target_properties (wsjtx_udp-static PROPERTIES
- OUTPUT_NAME wsjtx_udp
- )
-target_compile_definitions (wsjtx_udp-static PUBLIC UDP_STATIC_DEFINE)
-target_link_libraries (wsjtx_udp-static Qt5::Network Qt5::Gui)
-generate_export_header (wsjtx_udp-static BASE_NAME udp)
-
-generate_version_info (udp_daemon_VERSION_RESOURCES
- NAME udp_daemon
- BUNDLE ${PROJECT_BUNDLE_NAME}
- ICON ${WSJTX_ICON_FILE}
- FILE_DESCRIPTION "Example WSJT-X UDP Message Protocol daemon"
- )
-add_executable (udp_daemon UDPExamples/UDPDaemon.cpp ${udp_daemon_VERSION_RESOURCES})
-target_link_libraries (udp_daemon wsjtx_udp-static)
-
generate_version_info (wsjtx_app_version_VERSION_RESOURCES
NAME wsjtx_app_version
BUNDLE ${PROJECT_BUNDLE_NAME}
@@ -1518,47 +1358,9 @@
add_executable (wsjtx_app_version AppVersion/AppVersion.cpp ${wsjtx_app_version_VERSION_RESOURCES}) add_executable (wsjtx_app_version AppVersion/AppVersion.cpp ${wsjtx_app_version_VERSION_RESOURCES})
target_link_libraries (wsjtx_app_version wsjt_qt) target_link_libraries (wsjtx_app_version wsjt_qt)
@ -227,7 +249,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# install (TARGETS wsjtx_udp EXPORT udp # install (TARGETS wsjtx_udp EXPORT udp
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -1560,12 +1391,7 @@ @@ -1577,12 +1379,7 @@
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx # DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
# ) # )
@ -241,7 +263,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
) )
@@ -1578,38 +1404,6 @@ @@ -1595,38 +1392,6 @@
) )
endif(WSJT_BUILD_UTILS) endif(WSJT_BUILD_UTILS)
@ -280,7 +302,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
install (FILES install (FILES
cty.dat cty.dat
cty.dat_copyright.txt cty.dat_copyright.txt
@@ -1618,13 +1412,6 @@ @@ -1635,13 +1400,6 @@
#COMPONENT runtime #COMPONENT runtime
) )
@ -294,7 +316,7 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
# #
# Mac installer files # Mac installer files
# #
@@ -1676,22 +1463,6 @@ @@ -1693,22 +1451,6 @@
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h" "${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
) )
@ -317,4 +339,3 @@ diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
if (APPLE) if (APPLE)
set (CMAKE_POSTFLIGHT_SCRIPT set (CMAKE_POSTFLIGHT_SCRIPT
"${wsjtx_BINARY_DIR}/postflight.sh") "${wsjtx_BINARY_DIR}/postflight.sh")
Only in wsjtx: .idea

View File

@ -18,13 +18,14 @@ function cmakebuild() {
cd /tmp cd /tmp
BUILD_PACKAGES="git cmake make gcc g++" BUILD_PACKAGES="git cmake make gcc g++ libsamplerate-dev libfftw3-dev"
apt-get update apt-get update
apt-get -y install --no-install-recommends $BUILD_PACKAGES apt-get -y install --no-install-recommends $BUILD_PACKAGES
git clone https://github.com/jketterl/owrx_connector.git git clone https://github.com/jketterl/owrx_connector.git
cmakebuild owrx_connector 0.5.0 # latest develop as of 2022-12-11 (std::endl implicit flushing)
cmakebuild owrx_connector bca362707131289f91441c8080fd368fdc067b6d
apt-get -y purge --autoremove $BUILD_PACKAGES apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean apt-get clean

View File

@ -0,0 +1,36 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/Nuand/bladeRF.git
cmakebuild bladeRF 2021.10
git clone https://github.com/pothosware/SoapyBladeRF.git
# latest from master as of 2022-01-12
cmakebuild SoapyBladeRF 70505a5cdf8c9deabc4af3eb3384aa82a7b6f021
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -29,7 +29,7 @@ tar xfz $PACKAGE
git clone https://github.com/jancona/hpsdrconnector.git git clone https://github.com/jancona/hpsdrconnector.git
pushd hpsdrconnector pushd hpsdrconnector
git checkout v0.6.0 git checkout v0.6.1
/tmp/go/bin/go build /tmp/go/bin/go build
install -m 0755 hpsdrconnector /usr/local/bin install -m 0755 hpsdrconnector /usr/local/bin

View File

@ -25,7 +25,8 @@ apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/jketterl/runds_connector.git git clone https://github.com/jketterl/runds_connector.git
cmakebuild runds_connector 0.2.0 # latest develop as of 2022-12-11 (std::endl implicit flushing)
cmakebuild runds_connector 06ca993a3c81ddb0a2581b1474895da07752a9e1
apt-get -y purge --autoremove $BUILD_PACKAGES apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean apt-get clean

View File

@ -38,7 +38,7 @@ case $ARCH in
;; ;;
esac esac
wget https://www.sdrplay.com/software/$BINARY wget --no-http-keep-alive https://www.sdrplay.com/software/$BINARY
sh $BINARY --noexec --target sdrplay sh $BINARY --noexec --target sdrplay
patch --verbose -Np0 < /install-lib.$ARCH.patch patch --verbose -Np0 < /install-lib.$ARCH.patch

View File

@ -18,17 +18,16 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb-1.0.0 libboost-chrono1.67.0 libboost-date-time1.67.0 libboost-filesystem1.67.0 libboost-program-options1.67.0 libboost-regex1.67.0 libboost-test1.67.0 libboost-serialization1.67.0 libboost-thread1.67.0 libboost-system1.67.0 python3-numpy python3-mako" STATIC_PACKAGES="libusb-1.0.0 libboost-chrono1.74.0 libboost-date-time1.74.0 libboost-filesystem1.74.0 libboost-program-options1.74.0 libboost-regex1.74.0 libboost-test1.74.0 libboost-serialization1.74.0 libboost-thread1.74.0 libboost-system1.74.0 python3-numpy python3-mako"
BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev libboost-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-test-dev libboost-serialization-dev libboost-thread-dev libboost-system-dev" BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev libboost-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-test-dev libboost-serialization-dev libboost-thread-dev libboost-system-dev"
apt-get update apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/EttusResearch/uhd.git git clone https://github.com/EttusResearch/uhd.git
# 3.15.0.0 Release
mkdir -p uhd/host/build mkdir -p uhd/host/build
cd uhd/host/build cd uhd/host/build
git checkout v3.15.0.0 git checkout v4.1.0.4
# see https://github.com/EttusResearch/uhd/issues/350 # see https://github.com/EttusResearch/uhd/issues/350
case `uname -m` in case `uname -m` in
arm*) arm*)

View File

@ -7,6 +7,9 @@ function cmakebuild() {
if [[ ! -z "${2:-}" ]]; then if [[ ! -z "${2:-}" ]]; then
git checkout $2 git checkout $2
fi fi
if [[ -f ".gitmodules" ]]; then
git submodule update --init
fi
mkdir build mkdir build
cd build cd build
cmake ${CMAKE_ARGS:-} .. cmake ${CMAKE_ARGS:-} ..
@ -18,8 +21,8 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.67.0 libboost-log1.67.0" STATIC_PACKAGES="libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline8 libgfortran5 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.74.0 libboost-log1.74.0 libcurl4"
BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-default libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev" BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-qmake libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev libpulse-dev libcurl4-openssl-dev"
apt-get update apt-get update
apt-get -y install auto-apt-proxy apt-get -y install auto-apt-proxy
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
@ -48,18 +51,22 @@ tar xfz ${JS8CALL_TGZ}
# patch allows us to build against the packaged hamlib # patch allows us to build against the packaged hamlib
patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch
rm /js8call-hamlib.patch rm /js8call-hamlib.patch
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_HAMLIB_THREE" cmakebuild ${JS8CALL_DIR} cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ} rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.5.0 WSJT_DIR=wsjtx-2.6.1
WSJT_TGZ=${WSJT_DIR}.tgz WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ} wget https://downloads.sourceforge.net/project/wsjt/${WSJT_DIR}/${WSJT_TGZ}
tar xfz ${WSJT_TGZ} tar xfz ${WSJT_TGZ}
patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
mv /wsjtx.patch ${WSJT_DIR} mv /wsjtx.patch ${WSJT_DIR}
cmakebuild ${WSJT_DIR} cmakebuild ${WSJT_DIR}
rm ${WSJT_TGZ} rm ${WSJT_TGZ}
git clone https://github.com/alexander-sholohov/msk144decoder.git
# latest from main as of 2023-02-21
MAKEFLAGS="" cmakebuild msk144decoder fe2991681e455636e258e83c29fd4b2a72d16095
git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git
cd direwolf cd direwolf
# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need. # hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need.
@ -102,7 +109,7 @@ rm -rf dream
rm dream-2.1.1-svn808.tar.gz rm dream-2.1.1-svn808.tar.gz
git clone https://github.com/mobilinkd/m17-cxx-demod.git git clone https://github.com/mobilinkd/m17-cxx-demod.git
cmakebuild m17-cxx-demod v2.2 cmakebuild m17-cxx-demod v2.3
git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols
pushd /usr/share/aprs-symbols pushd /usr/share/aprs-symbols

View File

@ -18,26 +18,25 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libfftw3-bin libprotobuf17 libsamplerate0 libicu63" STATIC_PACKAGES="libfftw3-bin libprotobuf23 libsamplerate0 libicu67 libudev1"
BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++ libprotobuf-dev protobuf-compiler libsamplerate-dev libicu-dev libpython3-dev" BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++ libprotobuf-dev protobuf-compiler libsamplerate-dev libicu-dev libpython3-dev libudev-dev"
apt-get update apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/jketterl/js8py.git git clone https://github.com/jketterl/js8py.git
pushd js8py pushd js8py
git checkout 0.1.0 # latest develop as of 2022-11-30 (structured callsign data)
git checkout f7e394b7892d26cbdcce5d43c0b4081a2a6a48f6
python3 setup.py install python3 setup.py install
popd popd
rm -rf js8py rm -rf js8py
git clone https://github.com/jketterl/csdr.git git clone https://github.com/jketterl/csdr.git
# latest develop as of 2021-09-22 (template fixes) cmakebuild csdr 0.18.1
cmakebuild csdr 536f3b9eb7cfe5434e9a9f1e807c96115dc9ac10
git clone https://github.com/jketterl/pycsdr.git git clone https://github.com/jketterl/pycsdr.git
cd pycsdr cd pycsdr
# latest develop as of 2021-09-22 (first version) git checkout 0.18.1
git checkout 52da48a87ef97eb7d337f1b146db66ca453801e4
./setup.py install install_headers ./setup.py install install_headers
cd .. cd ..
rm -rf pycsdr rm -rf pycsdr
@ -45,17 +44,14 @@ rm -rf pycsdr
git clone https://github.com/jketterl/codecserver.git git clone https://github.com/jketterl/codecserver.git
mkdir -p /usr/local/etc/codecserver mkdir -p /usr/local/etc/codecserver
cp codecserver/conf/codecserver.conf /usr/local/etc/codecserver cp codecserver/conf/codecserver.conf /usr/local/etc/codecserver
# latest develop as of 2021-09-24 (new parsing) cmakebuild codecserver 0.2.0
cmakebuild codecserver c51254323b32db5b169cdfc39e043eed6d613a77
git clone https://github.com/jketterl/digiham.git git clone https://github.com/jketterl/digiham.git
# latest develop as of 2021-09-22 (post-merge) cmakebuild digiham 0.6.1
cmakebuild digiham 62d2b4581025568263ae8c90d2450b65561b7ce8
git clone https://github.com/jketterl/pydigiham.git git clone https://github.com/jketterl/pydigiham.git
cd pydigiham cd pydigiham
# latest develop as of 2021-09-22 (split from digiham) git checkout 0.6.1
git checkout b0cc0c35d5ef2ae84c9bb1a02d56161d5bd5bf2f
./setup.py install ./setup.py install
cd .. cd ..
rm -rf pydigiham rm -rf pydigiham

View File

@ -160,3 +160,7 @@ h1 {
.imageupload.is-invalid ~ .invalid-feedback { .imageupload.is-invalid ~ .invalid-feedback {
display: block; display: block;
} }
.device-log-messages {
max-height: 500px;
}

View File

@ -917,33 +917,6 @@ img.openwebrx-mirror-img
z-index: 10; z-index: 10;
} }
#openwebrx-digimode-content .part
{
perspective: 700px;
}
#openwebrx-digimode-content .part
{
animation: new-digimode-data-3d 100ms;
animation-timing-function: linear;
display: inline-block;
perspective-origin: 50% 50%;
transform-origin: 0% 50%;
}
@keyframes new-digimode-data
{
0%{ opacity: 0; }
100%{ opacity: 1; }
}
@keyframes new-digimode-data-3d
{
0%{ transform: rotateX(0deg) rotateY(-90deg) translateX(-5px) scale(1.3); }
100%{ transform: rotateX(0deg) rotateY(0deg) translateX(0) scale(1); }
}
#openwebrx-digimode-select-channel #openwebrx-digimode-select-channel
{ {
transition: all 500ms; transition: all 500ms;
@ -1052,7 +1025,8 @@ img.openwebrx-mirror-img
.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image .directcall, .openwebrx-meta-slot.active.direct .openwebrx-meta-user-image .directcall,
.openwebrx-meta-slot.active.individual .openwebrx-meta-user-image .directcall, .openwebrx-meta-slot.active.individual .openwebrx-meta-user-image .directcall,
#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall, #openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
#openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall { #openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall,
#openwebrx-panel-metadata-m17 .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall {
display: initial; display: initial;
} }
@ -1086,6 +1060,14 @@ img.openwebrx-mirror-img
content: "Down: "; content: "Down: ";
} }
.openwebrx-m17-source:not(:empty):before {
content: "SRC: ";
}
.openwebrx-m17-destination:not(:empty):before {
content: "DEST: ";
}
.openwebrx-dstar-yourcall:not(:empty):before { .openwebrx-dstar-yourcall:not(:empty):before {
content: "UR: "; content: "UR: ";
} }
@ -1283,6 +1265,7 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
@ -1293,7 +1276,8 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel #openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-select-channel
{ {
display: none; display: none;
} }
@ -1308,7 +1292,8 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="fst4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="fst4w"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container #openwebrx-panel-digimodes[data-mode="q65"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="msk144"] #openwebrx-digimode-canvas-container
{ {
height: 200px; height: 200px;
margin: -10px; margin: -10px;

View File

@ -19,7 +19,7 @@
<div class="webrx-rx-photo-desc">${photo_desc}</div> <div class="webrx-rx-photo-desc">${photo_desc}</div>
</div> </div>
<a class="openwebrx-rx-details-arrow openwebrx-rx-details-arrow--down openwebrx-photo-trigger"> <a class="openwebrx-rx-details-arrow openwebrx-rx-details-arrow--down openwebrx-photo-trigger">
<svg class="down" viewBox="0 0 43 12"><use xlink:href="static/gfx/svg-defs.svg#rx-details-arrow-down"></use></svg> <svg class="down" viewBox="0 0 43 12"><use xlink:href="${document_root}static/gfx/svg-defs.svg#rx-details-arrow-down"></use></svg>
<svg class="up" viewBox="0 0 43 12"><use xlink:href="static/gfx/svg-defs.svg#rx-details-arrow-up"></use></svg> <svg class="up" viewBox="0 0 43 12"><use xlink:href="${document_root}static/gfx/svg-defs.svg#rx-details-arrow-up"></use></svg>
</a> </a>
</div> </div>

View File

@ -74,6 +74,15 @@
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message"></div> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"></div>
<div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div> <div class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"></div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-m17" style="display: none;" data-panel-name="metadata-m17">
<div class="openwebrx-meta-slot">
<div class="openwebrx-meta-user-image">
<img class="directcall" src="static/gfx/openwebrx-directcall.svg">
</div>
<div class="openwebrx-m17-source"></div>
<div class="openwebrx-m17-destination"></div>
</div>
</div>
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf"> <div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf">
<div class="openwebrx-meta-slot"> <div class="openwebrx-meta-slot">
<div class="openwebrx-ysf-mode"></div> <div class="openwebrx-ysf-mode"></div>

View File

@ -70,7 +70,7 @@ AprsMarker.prototype.onAdd = function() {
div.appendChild(overlay); div.appendChild(overlay);
var self = this; var self = this;
google.maps.event.addDomListener(div, "click", function(event) { div.addEventListener("click", function(event) {
event.stopPropagation(); event.stopPropagation();
google.maps.event.trigger(self, "click", event); google.maps.event.trigger(self, "click", event);
}); });

View File

@ -331,7 +331,9 @@ ImaAdpcmCodec.prototype.reset = function() {
this.synchronized = 0; this.synchronized = 0;
this.syncWord = "SYNC"; this.syncWord = "SYNC";
this.syncCounter = 0; this.syncCounter = 0;
this.skip = 0; this.phase = 0;
this.syncBuffer = new Uint8Array(4);
this.syncBufferIndex = 0;
}; };
ImaAdpcmCodec.imaIndexTable = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 ]; ImaAdpcmCodec.imaIndexTable = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 ];
@ -359,38 +361,45 @@ ImaAdpcmCodec.prototype.decode = function(data) {
ImaAdpcmCodec.prototype.decodeWithSync = function(data) { ImaAdpcmCodec.prototype.decodeWithSync = function(data) {
var output = new Int16Array(data.length * 2); var output = new Int16Array(data.length * 2);
var index = this.skip;
var oi = 0; var oi = 0;
while (index < data.length) { for (var index = 0; index < data.length; index++) {
while (this.synchronized < 4 && index < data.length) { switch (this.phase) {
if (data[index] === this.syncWord.charCodeAt(this.synchronized)) { case 0:
this.synchronized++; // search for sync word
} else { if (data[index] !== this.syncWord.charCodeAt(this.synchronized++)) {
this.synchronized = 0; // reset if data is unexpected
} this.synchronized = 0;
index++; }
if (this.synchronized === 4) { // if sync word has been found pass on to next phase
if (index + 4 < data.length) { if (this.synchronized === 4) {
var syncData = new Int16Array(data.buffer.slice(index, index + 4)); this.syncBufferIndex = 0;
this.phase = 1;
}
break;
case 1:
// read codec runtime data from stream
this.syncBuffer[this.syncBufferIndex++] = data[index];
// if data is complete, apply and pass on to next phase
if (this.syncBufferIndex === 4) {
var syncData = new Int16Array(this.syncBuffer.buffer);
this.stepIndex = syncData[0]; this.stepIndex = syncData[0];
this.predictor = syncData[1]; this.predictor = syncData[1];
this.syncCounter = 1000;
this.phase = 2;
} }
this.syncCounter = 1000;
index += 4;
break; break;
} case 2:
} // decode actual audio data
while (index < data.length) { output[oi++] = this.decodeNibble(data[index] & 0x0F);
if (this.syncCounter-- < 0) { output[oi++] = this.decodeNibble(data[index] >> 4);
this.synchronized = 0; // if the next sync keyword is due, reset and return to phase 0
if (this.syncCounter-- === 0) {
this.synchronized = 0;
this.phase = 0;
}
break; break;
}
output[oi++] = this.decodeNibble(data[index] & 0x0F);
output[oi++] = this.decodeNibble(data[index] >> 4);
index++;
} }
} }
this.skip = index - data.length;
return output.slice(0, oi); return output.slice(0, oi);
}; };

View File

@ -81,6 +81,12 @@ Envelope.prototype.draw = function(visible_range){
scale_ctx.fill(); scale_ctx.fill();
scale_ctx.globalAlpha = 1; scale_ctx.globalAlpha = 1;
scale_ctx.stroke(); scale_ctx.stroke();
scale_ctx.lineWidth = 1;
scale_ctx.textAlign = "left";
scale_ctx.fillText(this.demodulator.high_cut.toString(), to_px + env_att_w, env_h2);
scale_ctx.textAlign = "right";
scale_ctx.fillText(this.demodulator.low_cut.toString(), from_px - env_att_w, env_h2);
scale_ctx.lineWidth = 3;
} }
if (typeof line !== "undefined") // out of screen? if (typeof line !== "undefined") // out of screen?
{ {

View File

@ -158,8 +158,8 @@ DemodulatorPanel.prototype.updatePanels = function() {
var modulation = this.getDemodulator().get_secondary_demod(); var modulation = this.getDemodulator().get_secondary_demod();
$('#openwebrx-panel-digimodes').attr('data-mode', modulation); $('#openwebrx-panel-digimodes').attr('data-mode', modulation);
toggle_panel("openwebrx-panel-digimodes", !!modulation); toggle_panel("openwebrx-panel-digimodes", !!modulation);
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65"].indexOf(modulation) >= 0); toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65", "msk144"].indexOf(modulation) >= 0);
toggle_panel("openwebrx-panel-js8-message", modulation == "js8"); toggle_panel("openwebrx-panel-js8-message", modulation === "js8");
toggle_panel("openwebrx-panel-packet-message", modulation === "packet"); toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag"); toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");

View File

@ -98,8 +98,13 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
if (index < 0) return; if (index < 0) return;
var delta = 10 ** (Math.floor(Math.max(me.exponent, Math.log10(me.frequency))) - index); var delta = 10 ** (Math.floor(Math.max(me.exponent, Math.log10(me.frequency))) - index);
if (e.originalEvent.deltaY > 0) delta *= -1; var newFrequency;
var newFrequency = me.frequency + delta; if ('deltaMode' in e.originalEvent && e.originalEvent.deltaMode === 0) {
newFrequency = me.frequency - delta * (e.originalEvent.deltaY / 50);
} else {
if (e.originalEvent.deltaY > 0) delta *= -1;
newFrequency = me.frequency + delta;
}
me.element.trigger('frequencychange', newFrequency); me.element.trigger('frequencychange', newFrequency);
}); });

View File

@ -50,7 +50,7 @@ MessagePanel.prototype.initClearButton = function() {
function WsjtMessagePanel(el) { function WsjtMessagePanel(el) {
MessagePanel.call(this, el); MessagePanel.call(this, el);
this.initClearTimer(); this.initClearTimer();
this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65']; this.qsoModes = ['FT8', 'JT65', 'JT9', 'FT4', 'FST4', 'Q65', 'MSK144'];
this.beaconModes = ['WSPR', 'FST4W']; this.beaconModes = ['WSPR', 'FST4W'];
this.modes = [].concat(this.qsoModes, this.beaconModes); this.modes = [].concat(this.qsoModes, this.beaconModes);
} }
@ -157,13 +157,17 @@ PacketMessagePanel.prototype.pushMessage = function(msg) {
if (msg.type && msg.type === 'thirdparty' && msg.data) { if (msg.type && msg.type === 'thirdparty' && msg.data) {
msg = msg.data; msg = msg.data;
} }
var source = msg.source; var source = msg.source;
if (msg.type) { var callsign;
if (msg.type === 'item') { if ('object' in source) {
source = msg.item; callsign = source.object;
} } else if ('item' in source) {
if (msg.type === 'object') { callsign = source.item;
source = msg.object; } else {
callsign = source.callsign;
if ('ssid' in source) {
callsign += '-' + source.ssid;
} }
} }
@ -202,7 +206,7 @@ PacketMessagePanel.prototype.pushMessage = function(msg) {
'style="' + stylesToString(styles) + '"' 'style="' + stylesToString(styles) + '"'
].join(' '); ].join(' ');
if (msg.lat && msg.lon) { if (msg.lat && msg.lon) {
link = '<a ' + attrs + ' href="map?callsign=' + encodeURIComponent(source) + '" target="openwebrx-map">' + overlay + '</a>'; link = '<a ' + attrs + ' href="map?' + new URLSearchParams(source).toString() + '" target="openwebrx-map">' + overlay + '</a>';
} else { } else {
link = '<div ' + attrs + '>' + overlay + '</div>' link = '<div ' + attrs + '>' + overlay + '</div>'
} }
@ -210,7 +214,7 @@ PacketMessagePanel.prototype.pushMessage = function(msg) {
$b.append($( $b.append($(
'<tr>' + '<tr>' +
'<td>' + timestamp + '</td>' + '<td>' + timestamp + '</td>' +
'<td class="callsign">' + source + '</td>' + '<td class="callsign">' + callsign + '</td>' +
'<td class="coord">' + link + '</td>' + '<td class="coord">' + link + '</td>' +
'<td class="message">' + (msg.comment || msg.message || '') + '</td>' + '<td class="message">' + (msg.comment || msg.message || '') + '</td>' +
'</tr>' '</tr>'

View File

@ -321,18 +321,57 @@ NxdnMetaPanel.prototype.clear = function() {
this.setDestination(); this.setDestination();
}; };
function M17MetaPanel(el) {
MetaPanel.call(this, el);
this.modes = ['M17'];
this.clear();
}
M17MetaPanel.prototype = new MetaPanel();
M17MetaPanel.prototype.update = function(data) {
if (!this.isSupported(data)) return;
if (data['sync'] && data['sync'] === 'voice') {
this.el.find(".openwebrx-meta-slot").addClass("active");
this.setSource(data['source']);
this.setDestination(data['destination']);
} else {
this.clear();
}
};
M17MetaPanel.prototype.setSource = function(source) {
if (this.source === source) return;
this.source = source;
this.el.find('.openwebrx-m17-source').text(source || '');
};
M17MetaPanel.prototype.setDestination = function(destination) {
if (this.destination === destination) return;
this.destination = destination;
this.el.find('.openwebrx-m17-destination').text(destination || '');
};
M17MetaPanel.prototype.clear = function() {
MetaPanel.prototype.clear.call(this);
this.setSource();
this.setDestination();
};
MetaPanel.types = { MetaPanel.types = {
dmr: DmrMetaPanel, dmr: DmrMetaPanel,
ysf: YsfMetaPanel, ysf: YsfMetaPanel,
dstar: DStarMetaPanel, dstar: DStarMetaPanel,
nxdn: NxdnMetaPanel, nxdn: NxdnMetaPanel,
m17: M17MetaPanel,
}; };
$.fn.metaPanel = function() { $.fn.metaPanel = function() {
return this.map(function() { return this.map(function() {
var $self = $(this); var $self = $(this);
if (!$self.data('metapanel')) { if (!$self.data('metapanel')) {
var matches = /^openwebrx-panel-metadata-([a-z]+)$/.exec($self.prop('id')); var matches = /^openwebrx-panel-metadata-([a-z0-9]+)$/.exec($self.prop('id'));
var constructor = matches && MetaPanel.types[matches[1]] || MetaPanel; var constructor = matches && MetaPanel.types[matches[1]] || MetaPanel;
$self.data('metapanel', new constructor($self)); $self.data('metapanel', new constructor($self));
} }

View File

@ -1,2 +1,4 @@
/* Taken from https://github.com/cyphercodes/location-picker under GPLv3 license */ /* Taken from https://github.com/cyphercodes/location-picker under GPLv3 license */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.locationPicker=t()}(this,function(){"use strict";return function(e,t){void 0===t&&(t={});var n=t.insertAt;if(e&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css","top"===n&&o.firstChild?o.insertBefore(i,o.firstChild):o.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}('.location-picker .centerMarker{position:absolute;background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAABWCAYAAACEsWWHAAAGLElEQVR4AdXcA2xsXReH8f3Ztm3btm2br23btm3btm2rd257b835vVnJStL0sj3Tds6TrOTkzN5r/5925qgorQRPwQewDHbHebgXDQxlNXLfeTlmmZzzlNJOpMyXsA86TJ2O7PGl6DnbQr/CncYx1tNj4JzTde+0la5V/6/x+x+Z85Ov6/jWZ0TFduyL12JMjI05E4iev5pxQXwFV0lGOx7Xf/xRulb5n45vflrH1z4xqYo5MTd6RC9JrvGVmRB6C04dLzR/yw10fP2TGbJyRa/oOVHw1Fh7uqS+jAY0e3uaPXvupOM7n48w01LRO9aItSDX/nKrpf6FYRi85AJzfvy1WHxGKtaKNZPhyNIKoadjZ0nfIfvFYrNSsXYiMz2tilg00Bweas7fZO1YYFYrMkQWKVfl7ScadS7z12jcFhVZxsn9ayoHimHI71RbVWRKhiPrZA7pjZn+TFX4zDWW6lSAU/PoFw3auiJjcuqSryjyPJWH9LauyBhZEXxlcdd+VyFOjDGxFhVZk6vCoUwEv5KXSXlFUYuKrJE5+dXCvlt3Ia7TYkKtKjIndxr/XcOXIMzzgrZWFZkje/Kl8WL7QP/xR7Z0wWs/9X5HvutNdnjdq2z6qpdFxXbsi9daulZkT/YZ/zbsQNwTtWSRuz7/YXu9+bU2ePlLFlcxJsa2ZM3InnSEU8EH5J1v3iRWqhs+9QGbv+plS5TKirExp/K6kT0ckg8ULIO4RW/Jd2q81GTkWvGdC4dkmYLdoXvHLSs3zrffVCrmVl4/HJLdC86r/vmKA8X7ImCVqnxACYfkvIL7EE+OKjWNI11FsehRKUM4JPcVNBCPxSo1jcN4RbHoUSlDOCSNgiHEM79KTeMcVVEselTKEA7JUMEA4pqr9mLhkAwUPAqNX36n9m/Fxi+/K3mk4GaY+/df1/7gEQ7JzQUnwPwN16h8XTjbh/twSE4oWBf6jjio9ifocEjWLfgeDN14be0vqcIh+V7BCzFobKw556ffqO1FcGQPh3AJpxLgROjebrPa3rZE9uTEEgT4IwzffENtbzQje/LH8WLPRQPycXatKjInjXAp48GGMHjx+bUTi8zJhmUieAX60ez8359rIxVZ0czsrygLA9uM+6zVoiJrsnVZFHg55sP8jddqe6nImMyP7GVxYA0Yffyxtn4iHNkiY7J6WRJ4Fu6G3n13a1uxyJbcHZnL0oDvQHNwsJmPDNqqIlNkg8haJgOOgKGrL287sciUHFEmC16FTpi/2bptIxVZks7IWKYC/grN+fOa+bBnVisyRBaIbKUKOBsGzjp11sUiQ3J2qQreij7oWn3ZWZOKtZO+yFRaAVaAsSceb3Z874szLhVrxtoQWUqrwFNxeeWfoVX/2dflkaW0ErwPQ2h2Lvf3GZOKtdDMtd9XpgOsByMP3q/j25+bdqlYI9ZK1ivTBZ6JW6D3kH2nXSzWSG6Jtct0gs+iaWSkOfcvv5g2qehtdLSJqM+WmQC7wND110ybWPROdikzBV6Ih2H+5uu1XCp6Jg/HWmUmwa+gOa+rOedHX22ZVPSKngh+VWYDnAP9Jx3TMrHolZxTZgu8B8PxBHbun39eWSp6RC8MR+8ym2AbGLzo3Mpi0SPZpsw2eCm6oPP/f5myVMxNuqJnaQewKgzdMPXDf8xNVi3tAp6Nh2DemstPWirmJNHj2aWdwDIwfNstkxaLOckypd3Ac/AEdP73T0stFWOTJ6JHaUewBgycecpSi8XYZI3SruBlGGwODy/VT0djTIzFYMwt7QwOhZ69d16iWIxJDi3tDr5CPh/5xqcWKRWvxRgEX6mDWP5GOF0r/muRYl0r/ltyV8wpdQCbQ98xhy1SLF5LNi91AV+A0ccfXaRYvJZ8oU5iT0cXNP7wExOlYl/SFWNLncARMH+L9U0Ui33JEaVuYGXoP/lYE8ViX7JyHcW+uahrx9iXfLOOYq+Bse75JorFvuQ1pY6gE8b/kV1sJ52lruCuib+9GtvJXXUWu2ri72fFdnJVncXOhvgXFykmtpOz6yx2MMzfdB0pJraTg+sstiwM33mbxm9/qPG7H4ntZNk6iz0Hd5hA7ntOqTN4OfbDo1mx/fIyzTwJJedUPgRWtocAAAAASUVORK5CYII=") no-repeat;background-size:100%;top:50%;left:50%;z-index:1;margin-left:-14px;margin-top:-43px;height:44px;width:28px;cursor:pointer}'),function(){function e(e,t,n){void 0===t&&(t={}),void 0===n&&(n={});var o={setCurrentPosition:!0};Object.assign(o,t);var i={center:new google.maps.LatLng(o.lat?o.lat:34.4346,o.lng?o.lng:35.8362),zoom:15};Object.assign(i,n),e instanceof HTMLElement?this.element=e:this.element=document.getElementById(e),this.map=new google.maps.Map(this.element,i);var r=document.createElement("div");r.classList.add("centerMarker"),this.element&&(this.element.classList.add("location-picker"),this.element.children[0].appendChild(r)),!o.setCurrentPosition||o.lat||o.lng||this.setCurrentPosition()}return e.prototype.getMarkerPosition=function(){var e=this.map.getCenter();return{lat:e.lat(),lng:e.lng()}},e.prototype.setLocation=function(e,t){this.map.setCenter(new google.maps.LatLng(e,t))},e.prototype.setCurrentPosition=function(){var e=this;navigator.geolocation?navigator.geolocation.getCurrentPosition(function(t){var n={lat:t.coords.latitude,lng:t.coords.longitude};e.map.setCenter(n)},function(){console.log("Could not determine your location...")}):console.log("Your browser does not support Geolocation.")},e}()}); /* Contains https://github.com/cyphercodes/location-picker/pull/11 to allow latitude and longitude to be 0 */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.locationPicker=t()}(this,function(){"use strict";return function(e,t){void 0===t&&(t={});var n=t.insertAt;if(e&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css","top"===n&&o.firstChild?o.insertBefore(i,o.firstChild):o.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}('.location-picker .centerMarker{position:absolute;background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAABWCAYAAACEsWWHAAAGLElEQVR4AdXcA2xsXReH8f3Ztm3btm2br23btm3btm2rd257b835vVnJStL0sj3Tds6TrOTkzN5r/5925qgorQRPwQewDHbHebgXDQxlNXLfeTlmmZzzlNJOpMyXsA86TJ2O7PGl6DnbQr/CncYx1tNj4JzTde+0la5V/6/x+x+Z85Ov6/jWZ0TFduyL12JMjI05E4iev5pxQXwFV0lGOx7Xf/xRulb5n45vflrH1z4xqYo5MTd6RC9JrvGVmRB6C04dLzR/yw10fP2TGbJyRa/oOVHw1Fh7uqS+jAY0e3uaPXvupOM7n48w01LRO9aItSDX/nKrpf6FYRi85AJzfvy1WHxGKtaKNZPhyNIKoadjZ0nfIfvFYrNSsXYiMz2tilg00Bweas7fZO1YYFYrMkQWKVfl7ScadS7z12jcFhVZxsn9ayoHimHI71RbVWRKhiPrZA7pjZn+TFX4zDWW6lSAU/PoFw3auiJjcuqSryjyPJWH9LauyBhZEXxlcdd+VyFOjDGxFhVZk6vCoUwEv5KXSXlFUYuKrJE5+dXCvlt3Ia7TYkKtKjIndxr/XcOXIMzzgrZWFZkje/Kl8WL7QP/xR7Z0wWs/9X5HvutNdnjdq2z6qpdFxXbsi9daulZkT/YZ/zbsQNwTtWSRuz7/YXu9+bU2ePlLFlcxJsa2ZM3InnSEU8EH5J1v3iRWqhs+9QGbv+plS5TKirExp/K6kT0ckg8ULIO4RW/Jd2q81GTkWvGdC4dkmYLdoXvHLSs3zrffVCrmVl4/HJLdC86r/vmKA8X7ImCVqnxACYfkvIL7EE+OKjWNI11FsehRKUM4JPcVNBCPxSo1jcN4RbHoUSlDOCSNgiHEM79KTeMcVVEselTKEA7JUMEA4pqr9mLhkAwUPAqNX36n9m/Fxi+/K3mk4GaY+/df1/7gEQ7JzQUnwPwN16h8XTjbh/twSE4oWBf6jjio9ifocEjWLfgeDN14be0vqcIh+V7BCzFobKw556ffqO1FcGQPh3AJpxLgROjebrPa3rZE9uTEEgT4IwzffENtbzQje/LH8WLPRQPycXatKjInjXAp48GGMHjx+bUTi8zJhmUieAX60ez8359rIxVZ0czsrygLA9uM+6zVoiJrsnVZFHg55sP8jddqe6nImMyP7GVxYA0Yffyxtn4iHNkiY7J6WRJ4Fu6G3n13a1uxyJbcHZnL0oDvQHNwsJmPDNqqIlNkg8haJgOOgKGrL287sciUHFEmC16FTpi/2bptIxVZks7IWKYC/grN+fOa+bBnVisyRBaIbKUKOBsGzjp11sUiQ3J2qQreij7oWn3ZWZOKtZO+yFRaAVaAsSceb3Z874szLhVrxtoQWUqrwFNxeeWfoVX/2dflkaW0ErwPQ2h2Lvf3GZOKtdDMtd9XpgOsByMP3q/j25+bdqlYI9ZK1ivTBZ6JW6D3kH2nXSzWSG6Jtct0gs+iaWSkOfcvv5g2qehtdLSJqM+WmQC7wND110ybWPROdikzBV6Ih2H+5uu1XCp6Jg/HWmUmwa+gOa+rOedHX22ZVPSKngh+VWYDnAP9Jx3TMrHolZxTZgu8B8PxBHbun39eWSp6RC8MR+8ym2AbGLzo3Mpi0SPZpsw2eCm6oPP/f5myVMxNuqJnaQewKgzdMPXDf8xNVi3tAp6Nh2DemstPWirmJNHj2aWdwDIwfNstkxaLOckypd3Ac/AEdP73T0stFWOTJ6JHaUewBgycecpSi8XYZI3SruBlGGwODy/VT0djTIzFYMwt7QwOhZ69d16iWIxJDi3tDr5CPh/5xqcWKRWvxRgEX6mDWP5GOF0r/muRYl0r/ltyV8wpdQCbQ98xhy1SLF5LNi91AV+A0ccfXaRYvJZ8oU5iT0cXNP7wExOlYl/SFWNLncARMH+L9U0Ui33JEaVuYGXoP/lYE8ViX7JyHcW+uahrx9iXfLOOYq+Bse75JorFvuQ1pY6gE8b/kV1sJ52lruCuib+9GtvJXXUWu2ri72fFdnJVncXOhvgXFykmtpOz6yx2MMzfdB0pJraTg+sstiwM33mbxm9/qPG7H4ntZNk6iz0Hd5hA7ntOqTN4OfbDo1mx/fIyzTwJJedUPgRWtocAAAAASUVORK5CYII=") no-repeat;background-size:100%;top:50%;left:50%;z-index:1;margin-left:-14px;margin-top:-43px;height:44px;width:28px;cursor:pointer}'),function(){function e(e,t,n){void 0===t&&(t={}),void 0===n&&(n={});var o={setCurrentPosition:!0};Object.assign(o,t);var i={center:new google.maps.LatLng("number"==typeof o.lat?o.lat:34.4346,"number"==typeof o.lng?o.lng:35.8362),zoom:15};Object.assign(i,n),e instanceof HTMLElement?this.element=e:this.element=document.getElementById(e),this.map=new google.maps.Map(this.element,i);var r=document.createElement("div");r.classList.add("centerMarker"),this.element&&(this.element.classList.add("location-picker"),this.element.children[0].appendChild(r)),!o.setCurrentPosition||o.lat||o.lng||this.setCurrentPosition()}return e.prototype.getMarkerPosition=function(){var e=this.map.getCenter();return{lat:e.lat(),lng:e.lng()}},e.prototype.setLocation=function(e,t){this.map.setCenter(new google.maps.LatLng(e,t))},e.prototype.setCurrentPosition=function(){var e=this;navigator.geolocation?navigator.geolocation.getCurrentPosition(function(t){var n={lat:t.coords.latitude,lng:t.coords.longitude};e.map.setCenter(n)},function(){console.log("Could not determine your location...")}):console.log("Your browser does not support Geolocation.")},e}()});

View File

@ -0,0 +1,5 @@
$.fn.logMessages = function() {
$.each(this, function(){
$(this).scrollTop(this.scrollHeight);
});
};

View File

@ -1,17 +1,12 @@
$(function(){ $(function(){
var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){ var query = new URLSearchParams(window.location.search);
var s = v.split('=');
var r = {};
r[s[0]] = s.slice(1).join('=');
return r;
}).reduce(function(a, b){
return a.assign(b);
});
var expectedCallsign; var expectedCallsign;
if (query.callsign) expectedCallsign = decodeURIComponent(query.callsign); if (query.has('callsign')) {
expectedCallsign = Object.fromEntries(query.entries());
}
var expectedLocator; var expectedLocator;
if (query.locator) expectedLocator = query.locator; if (query.has('locator')) expectedLocator = query.get('locator');
var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws'; var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws';
@ -37,6 +32,7 @@ $(function(){
var retention_time = 2 * 60 * 60 * 1000; var retention_time = 2 * 60 * 60 * 1000;
var strokeOpacity = 0.8; var strokeOpacity = 0.8;
var fillOpacity = 0.35; var fillOpacity = 0.35;
var callsign_service;
var colorKeys = {}; var colorKeys = {};
var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl'); var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl');
@ -101,6 +97,11 @@ $(function(){
return '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>'; return '<li class="square' + disabled + '" data-selector="' + key + '"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
}); });
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>'); $(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
};
var shallowEquals = function(obj1, obj2) {
// basic shallow object comparison
return Object.entries(obj1).sort().toString() === Object.entries(obj2).sort().toString();
} }
var processUpdates = function(updates) { var processUpdates = function(updates) {
@ -109,6 +110,7 @@ $(function(){
return; return;
} }
updates.forEach(function(update){ updates.forEach(function(update){
var key = sourceToKey(update.source);
switch (update.location.type) { switch (update.location.type) {
case 'latlon': case 'latlon':
@ -122,33 +124,33 @@ $(function(){
aprsOptions.course = update.location.course; aprsOptions.course = update.location.course;
aprsOptions.speed = update.location.speed; aprsOptions.speed = update.location.speed;
} }
if (markers[update.callsign]) { if (markers[key]) {
marker = markers[update.callsign]; marker = markers[key];
} else { } else {
marker = new markerClass(); marker = new markerClass();
marker.addListener('click', function(){ marker.addListener('click', function(){
showMarkerInfoWindow(update.callsign, pos); showMarkerInfoWindow(update.source, pos);
}); });
markers[update.callsign] = marker; markers[key] = marker;
} }
marker.setOptions($.extend({ marker.setOptions($.extend({
position: pos, position: pos,
map: map, map: map,
title: update.callsign title: sourceToString(update.source)
}, aprsOptions, getMarkerOpacityOptions(update.lastseen) )); }, aprsOptions, getMarkerOpacityOptions(update.lastseen) ));
marker.lastseen = update.lastseen; marker.lastseen = update.lastseen;
marker.mode = update.mode; marker.mode = update.mode;
marker.band = update.band; marker.band = update.band;
marker.comment = update.location.comment; marker.comment = update.location.comment;
if (expectedCallsign && expectedCallsign == update.callsign) { if (expectedCallsign && shallowEquals(expectedCallsign, update.source)) {
map.panTo(pos); map.panTo(pos);
showMarkerInfoWindow(update.callsign, pos); showMarkerInfoWindow(update.source, pos);
expectedCallsign = false; expectedCallsign = false;
} }
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign) { if (infowindow && infowindow.source && shallowEquals(infowindow.source, update.source)) {
showMarkerInfoWindow(infowindow.callsign, pos); showMarkerInfoWindow(infowindow.source, pos);
} }
break; break;
case 'locator': case 'locator':
@ -159,15 +161,16 @@ $(function(){
var rectangle; var rectangle;
// the accessor is designed to work on the rectangle... but it should work on the update object, too // the accessor is designed to work on the rectangle... but it should work on the update object, too
var color = getColor(colorAccessor(update)); var color = getColor(colorAccessor(update));
if (rectangles[update.callsign]) { if (rectangles[key]) {
rectangle = rectangles[update.callsign]; rectangle = rectangles[key];
} else { } else {
rectangle = new google.maps.Rectangle(); rectangle = new google.maps.Rectangle();
rectangle.addListener('click', function(){ rectangle.addListener('click', function(){
showLocatorInfoWindow(this.locator, this.center); showLocatorInfoWindow(this.locator, this.center);
}); });
rectangles[update.callsign] = rectangle; rectangles[key] = rectangle;
} }
rectangle.source = update.source;
rectangle.lastseen = update.lastseen; rectangle.lastseen = update.lastseen;
rectangle.locator = update.location.locator; rectangle.locator = update.location.locator;
rectangle.mode = update.mode; rectangle.mode = update.mode;
@ -187,13 +190,13 @@ $(function(){
} }
}, getRectangleOpacityOptions(update.lastseen) )); }, getRectangleOpacityOptions(update.lastseen) ));
if (expectedLocator && expectedLocator == update.location.locator) { if (expectedLocator && expectedLocator === update.location.locator) {
map.panTo(center); map.panTo(center);
showLocatorInfoWindow(expectedLocator, center); showLocatorInfoWindow(expectedLocator, center);
expectedLocator = false; expectedLocator = false;
} }
if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) { if (infowindow && infowindow.locator && infowindow.locator === update.location.locator) {
showLocatorInfoWindow(infowindow.locator, center); showLocatorInfoWindow(infowindow.locator, center);
} }
break; break;
@ -202,7 +205,7 @@ $(function(){
}; };
var clearMap = function(){ var clearMap = function(){
var reset = function(callsign, item) { item.setMap(); }; var reset = function(_, item) { item.setMap(); };
$.each(markers, reset); $.each(markers, reset);
$.each(rectangles, reset); $.each(rectangles, reset);
receiverMarker.setMap(); receiverMarker.setMap();
@ -286,6 +289,9 @@ $(function(){
if ('map_position_retention_time' in config) { if ('map_position_retention_time' in config) {
retention_time = config.map_position_retention_time * 1000; retention_time = config.map_position_retention_time * 1000;
} }
if ('callsign_service' in config) {
callsign_service = config['callsign_service'];
}
break; break;
case "update": case "update":
processUpdates(json.value); processUpdates(json.value);
@ -332,32 +338,77 @@ $(function(){
infowindow = new google.maps.InfoWindow(); infowindow = new google.maps.InfoWindow();
google.maps.event.addListener(infowindow, 'closeclick', function() { google.maps.event.addListener(infowindow, 'closeclick', function() {
delete infowindow.locator; delete infowindow.locator;
delete infowindow.callsign; delete infowindow.source;
}); });
} }
delete infowindow.locator; delete infowindow.locator;
delete infowindow.callsign; delete infowindow.source;
return infowindow; return infowindow;
} };
var sourceToKey = function(source) {
// special treatment for special entities
// not just for display but also in key treatment in order not to overlap with other locations sent by the same callsign
if ('item' in source) return source['item'];
if ('object' in source) return source['object'];
var key = source.callsign;
if ('ssid' in source) key += '-' + source.ssid;
return key;
};
// we can reuse the same logic for displaying and indexing
var sourceToString = sourceToKey;
var linkifySource = function(source) {
var callsignString = sourceToString(source);
switch (callsign_service) {
case "qrzcq":
return '<a target="callsign_info" href="https://www.qrzcq.com/call/' + source.callsign + '">' + callsignString + '</a>';
case "qrz":
return '<a target="callsign_info" href="https://www.qrz.com/db/' + source.callsign + '">' + callsignString + '</a>';
case 'aprsfi':
var callWithSsid = sourceToKey(source);
return '<a target="callsign_info" href="https://aprs.fi/info/a/' + callWithSsid + '">' + callsignString + '</a>';
default:
return callsignString;
}
};
var distanceKm = function(p1, p2) {
// Earth radius in km
var R = 6371.0;
// Convert degrees to radians
var rlat1 = p1.lat() * (Math.PI/180);
var rlat2 = p2.lat() * (Math.PI/180);
// Compute difference in radians
var difflat = rlat2-rlat1;
var difflon = (p2.lng()-p1.lng()) * (Math.PI/180);
// Compute distance
d = 2 * R * Math.asin(Math.sqrt(
Math.sin(difflat/2) * Math.sin(difflat/2) +
Math.cos(rlat1) * Math.cos(rlat2) * Math.sin(difflon/2) * Math.sin(difflon/2)
));
return Math.round(d);
};
var infowindow; var infowindow;
var showLocatorInfoWindow = function(locator, pos) { var showLocatorInfoWindow = function(locator, pos) {
var infowindow = getInfoWindow(); var infowindow = getInfoWindow();
infowindow.locator = locator; infowindow.locator = locator;
var inLocator = $.map(rectangles, function(r, callsign) { var inLocator = Object.values(rectangles).filter(rectangleFilter).filter(function(d) {
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} return d.locator === locator;
}).filter(rectangleFilter).filter(function(d) {
return d.locator == locator;
}).sort(function(a, b){ }).sort(function(a, b){
return b.lastseen - a.lastseen; return b.lastseen - a.lastseen;
}); });
var distance = receiverMarker?
" at " + distanceKm(receiverMarker.position, pos) + " km" : "";
infowindow.setContent( infowindow.setContent(
'<h3>Locator: ' + locator + '</h3>' + '<h3>Locator: ' + locator + distance + '</h3>' +
'<div>Active Callsigns:</div>' + '<div>Active Callsigns:</div>' +
'<ul>' + '<ul>' +
inLocator.map(function(i){ inLocator.map(function(i){
var timestring = moment(i.lastseen).fromNow(); var timestring = moment(i.lastseen).fromNow();
var message = i.callsign + ' (' + timestring + ' using ' + i.mode; var message = linkifySource(i.source) + ' (' + timestring + ' using ' + i.mode;
if (i.band) message += ' on ' + i.band; if (i.band) message += ' on ' + i.band;
message += ')'; message += ')';
return '<li>' + message + '</li>' return '<li>' + message + '</li>'
@ -368,22 +419,26 @@ $(function(){
infowindow.open(map); infowindow.open(map);
}; };
var showMarkerInfoWindow = function(callsign, pos) { var showMarkerInfoWindow = function(source, pos) {
var infowindow = getInfoWindow(); var infowindow = getInfoWindow();
infowindow.callsign = callsign; infowindow.source = source;
var marker = markers[callsign]; var marker = markers[sourceToKey(source)];
var timestring = moment(marker.lastseen).fromNow(); var timestring = moment(marker.lastseen).fromNow();
var commentString = ""; var commentString = "";
var distance = "";
if (marker.comment) { if (marker.comment) {
commentString = '<div>' + marker.comment + '</div>'; commentString = '<div>' + marker.comment + '</div>';
} }
if (receiverMarker) {
distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km";
}
infowindow.setContent( infowindow.setContent(
'<h3>' + callsign + '</h3>' + '<h3>' + linkifySource(source) + distance + '</h3>' +
'<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' + '<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' +
commentString commentString
); );
infowindow.open(map, marker); infowindow.open(map, marker);
} };
var showReceiverInfoWindow = function(marker) { var showReceiverInfoWindow = function(marker) {
var infowindow = getInfoWindow() var infowindow = getInfoWindow()
@ -392,7 +447,7 @@ $(function(){
'<div>Receiver location</div>' '<div>Receiver location</div>'
); );
infowindow.open(map, marker); infowindow.open(map, marker);
} };
var getScale = function(lastseen) { var getScale = function(lastseen) {
var age = new Date().getTime() - lastseen; var age = new Date().getTime() - lastseen;
@ -421,19 +476,19 @@ $(function(){
// fade out / remove positions after time // fade out / remove positions after time
setInterval(function(){ setInterval(function(){
var now = new Date().getTime(); var now = new Date().getTime();
$.each(rectangles, function(callsign, m) { Object.values(rectangles).forEach(function(m){
var age = now - m.lastseen; var age = now - m.lastseen;
if (age > retention_time) { if (age > retention_time) {
delete rectangles[callsign]; delete rectangles[sourceToKey(m.source)];
m.setMap(); m.setMap();
return; return;
} }
m.setOptions(getRectangleOpacityOptions(m.lastseen)); m.setOptions(getRectangleOpacityOptions(m.lastseen));
}); });
$.each(markers, function(callsign, m) { Object.values(markers).forEach(function(m) {
var age = now - m.lastseen; var age = now - m.lastseen;
if (age > retention_time) { if (age > retention_time) {
delete markers[callsign]; delete markers[sourceToKey(m.source)];
m.setMap(); m.setMap();
return; return;
} }

View File

@ -60,7 +60,7 @@ function zoomOutOneStep() {
} }
function zoomInTotal() { function zoomInTotal() {
zoom_set(zoom_levels.length - 1); zoom_set(zoom_levels_count);
} }
function zoomOutTotal() { function zoomOutTotal() {
@ -317,7 +317,7 @@ function scale_px_from_freq(f, range) {
function get_visible_freq_range() { function get_visible_freq_range() {
if (!bandwidth) return false; if (!bandwidth) return false;
var fcalc = function (x) { var fcalc = function (x) {
var canvasWidth = waterfallWidth() * zoom_levels[zoom_level]; var canvasWidth = waterfallWidth() * get_zoom(zoom_level);
return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2); return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2);
}; };
var out = { var out = {
@ -565,7 +565,7 @@ function canvas_mousemove(evt) {
) { ) {
zoom_center_rel += dpx; zoom_center_rel += dpx;
} }
resize_canvases(false); resize_canvases();
canvas_drag_last_x = evt.pageX; canvas_drag_last_x = evt.pageX;
canvas_drag_last_y = evt.pageY; canvas_drag_last_y = evt.pageY;
mkscale(); mkscale();
@ -616,9 +616,14 @@ function get_relative_x(evt) {
function canvas_mousewheel(evt) { function canvas_mousewheel(evt) {
if (!waterfall_setup_done) return; if (!waterfall_setup_done) return;
var delta = -evt.deltaY;
// deltaMode 0 means pixels instead of lines
if ('deltaMode' in evt && evt.deltaMode === 0) {
delta /= 50;
}
var relativeX = get_relative_x(evt); var relativeX = get_relative_x(evt);
var dir = (evt.deltaY / Math.abs(evt.deltaY)) > 0; zoom_step(delta, relativeX, zoom_center_where_calc(evt.pageX));
zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX));
evt.preventDefault(); evt.preventDefault();
} }
@ -631,7 +636,6 @@ function get_zoom_coeff_from_hps(hps) {
return bandwidth / shown_bw; return bandwidth / shown_bw;
} }
var zoom_levels = [1];
var zoom_level = 0; var zoom_level = 0;
var zoom_offset_px = 0; var zoom_offset_px = 0;
var zoom_center_rel = 0; var zoom_center_rel = 0;
@ -639,45 +643,48 @@ var zoom_center_where = 0;
var smeter_level = 0; var smeter_level = 0;
function mkzoomlevels() { function get_zoom(level) {
zoom_levels = [1];
var maxc = get_zoom_coeff_from_hps(zoom_max_level_hps); var maxc = get_zoom_coeff_from_hps(zoom_max_level_hps);
if (maxc < 1) return; if (maxc < 1) return;
// logarithmic interpolation // logarithmic interpolation
var zoom_ratio = Math.pow(maxc, 1 / zoom_levels_count); var zoom_ratio = Math.pow(maxc, 1 / zoom_levels_count);
for (var i = 1; i < zoom_levels_count; i++) return Math.pow(zoom_ratio, level);
zoom_levels.push(Math.pow(zoom_ratio, i));
} }
function zoom_step(out, where, onscreen) { function zoom_step(delta, where, onscreen) {
if ((out && zoom_level === 0) || (!out && zoom_level >= zoom_levels_count - 1)) return; zoom_level += delta;
if (out) --zoom_level; if (zoom_level < 0) {
else ++zoom_level; zoom_level = 0;
} else if (zoom_level > zoom_levels_count) {
zoom_level = zoom_levels_count;
}
zoom_center_rel = canvas_get_freq_offset(where); zoom_center_rel = canvas_get_freq_offset(where);
//console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString());
zoom_center_where = onscreen; zoom_center_where = onscreen;
//console.log(zoom_center_where, zoom_center_rel, where); resize_canvases();
resize_canvases(true);
mkscale(); mkscale();
bookmarks.position(); bookmarks.position();
} }
function zoom_set(level) { function zoom_set(level) {
if (!(level >= 0 && level <= zoom_levels.length - 1)) return; if (level < 0) {
level = parseInt(level); zoom_level = 0;
zoom_level = level; } else if (level > zoom_levels_count) {
zoom_level = zoom_levels_count;
} else {
zoom_level = parseFloat(level);
}
//zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+waterfallWidth()/2); //zoom to screen center instead of demod envelope //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+waterfallWidth()/2); //zoom to screen center instead of demod envelope
zoom_center_rel = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().get_offset_frequency(); zoom_center_rel = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().get_offset_frequency();
zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack
resize_canvases(true); resize_canvases();
mkscale(); mkscale();
bookmarks.position(); bookmarks.position();
} }
function zoom_calc() { function zoom_calc() {
var winsize = waterfallWidth(); var winsize = waterfallWidth();
var canvases_new_width = winsize * zoom_levels[zoom_level]; var canvases_new_width = winsize * get_zoom(zoom_level);
zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where)); zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where));
if (zoom_offset_px > 0) zoom_offset_px = 0; if (zoom_offset_px > 0) zoom_offset_px = 0;
if (zoom_offset_px < winsize - canvases_new_width) if (zoom_offset_px < winsize - canvases_new_width)
@ -771,6 +778,7 @@ function on_ws_recv(evt) {
$('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString()); $('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
waterfall_clear(); waterfall_clear();
zoom_set(0);
} }
if ('tuning_precision' in config) if ('tuning_precision' in config)
@ -838,6 +846,9 @@ function on_ws_recv(evt) {
$overlay.show(); $overlay.show();
$("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator(); $("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator();
break; break;
case "demodulator_error":
divlog(json['value'], true);
break;
case 'secondary_demod': case 'secondary_demod':
var value = json['value']; var value = json['value'];
var panels = [ var panels = [
@ -932,9 +943,15 @@ var waterfall_measure_minmax_now = false;
var waterfall_measure_minmax_continuous = false; var waterfall_measure_minmax_continuous = false;
function waterfall_measure_minmax_do(what) { function waterfall_measure_minmax_do(what) {
// Get visible range
var range = get_visible_freq_range();
var start = center_freq - bandwidth / 2;
// this is based on an oversampling factor of about 1,25 // this is based on an oversampling factor of about 1,25
var ignored = .1 * what.length; range.start = Math.max(0.1, (range.start - start) / bandwidth);
var data = what.slice(ignored, -ignored); range.end = Math.min(0.9, (range.end - start) / bandwidth);
var data = what.slice(range.start * what.length, range.end * what.length);
return { return {
min: Math.min.apply(Math, data), min: Math.min.apply(Math, data),
max: Math.max.apply(Math, data) max: Math.max.apply(Math, data)
@ -1106,8 +1123,9 @@ function init_canvas_container() {
canvas_container.addEventListener("mouseup", canvas_mouseup, false); canvas_container.addEventListener("mouseup", canvas_mouseup, false);
canvas_container.addEventListener("mousedown", canvas_mousedown, false); canvas_container.addEventListener("mousedown", canvas_mousedown, false);
canvas_container.addEventListener("wheel", canvas_mousewheel, false); canvas_container.addEventListener("wheel", canvas_mousewheel, false);
var frequency_container = $("#openwebrx-frequency-container"); $("#openwebrx-frequency-container").each(function(){
frequency_container.on("wheel", canvas_mousewheel, false); this.addEventListener("wheel", canvas_mousewheel, false);
});
} }
canvas_maxshift = 0; canvas_maxshift = 0;
@ -1119,12 +1137,10 @@ function shift_canvases() {
canvas_maxshift++; canvas_maxshift++;
} }
function resize_canvases(zoom) { function resize_canvases() {
if (typeof zoom === "undefined") zoom = false;
if (!zoom) mkzoomlevels();
zoom_calc(); zoom_calc();
$('#webrx-canvas-container').css({ $('#webrx-canvas-container').css({
width: waterfallWidth() * zoom_levels[zoom_level] + 'px', width: waterfallWidth() * get_zoom(zoom_level) + 'px',
left: zoom_offset_px + "px" left: zoom_offset_px + "px"
}); });
} }
@ -1133,7 +1149,6 @@ function waterfall_init() {
init_canvas_container(); init_canvas_container();
resize_canvases(); resize_canvases();
scale_setup(); scale_setup();
mkzoomlevels();
waterfall_setup_done = 1; waterfall_setup_done = 1;
} }
@ -1458,11 +1473,8 @@ function secondary_demod_push_data(x) {
if (y === "<") return "&lt;"; if (y === "<") return "&lt;";
if (y === ">") return "&gt;"; if (y === ">") return "&gt;";
if (y === " ") return "&nbsp;"; if (y === " ") return "&nbsp;";
if (y === "\n") return "<br />";
return y; return y;
}).map(function (y) {
if (y === "\n")
return "<br />";
return "<span class=\"part\">" + y + "</span>";
}).join(""); }).join("");
$("#openwebrx-cursor-blink").before(x); $("#openwebrx-cursor-blink").before(x);
} }

View File

@ -8,4 +8,5 @@ $(function(){
$('.optional-section').optionalSection(); $('.optional-section').optionalSection();
$('#scheduler').schedulerInput(); $('#scheduler').schedulerInput();
$('.exponential-input').exponentialInput(); $('.exponential-input').exponentialInput();
$('.device-log-messages').logMessages();
}); });

View File

@ -1,6 +1,7 @@
[core] [core]
data_directory = /var/lib/openwebrx data_directory = /var/lib/openwebrx
temporary_directory = /tmp temporary_directory = /tmp
log_level = INFO
[web] [web]
port = 8073 port = 8073

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys
from owrx.__main__ import main from owrx.__main__ import main
if __name__ == "__main__": if __name__ == "__main__":
main() sys.exit(main())

View File

@ -1,7 +1,8 @@
import logging import logging
# the linter will complain about this, but the logging must be configured before importing all the other modules # the linter will complain about this, but the logging must be configured before importing all the other modules
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") # loglevel will be adjusted later, INFO is just for the startup
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from http.server import HTTPServer from http.server import HTTPServer
@ -51,21 +52,23 @@ def main():
args = parser.parse_args() args = parser.parse_args()
# set loglevel to info for CLI commands if args.debug:
if args.module is not None and not args.debug: logging.getLogger().setLevel(logging.DEBUG)
logging.getLogger().setLevel(logging.INFO)
if args.version: if args.version:
print("OpenWebRX version {version}".format(version=openwebrx_version)) print("OpenWebRX version {version}".format(version=openwebrx_version))
elif args.module == "admin": return 0
run_admin_action(adminparser, args)
elif args.module == "config": if args.module == "admin":
run_admin_action(configparser, args) return run_admin_action(adminparser, args)
else:
start_receiver() if args.module == "config":
return run_admin_action(configparser, args)
return start_receiver(loglevel=logging.DEBUG if args.debug else None)
def start_receiver(): def start_receiver(loglevel=None):
print( print(
""" """
@ -84,18 +87,27 @@ Support and info: https://groups.io/g/openwebrx
for sig in [signal.SIGINT, signal.SIGTERM]: for sig in [signal.SIGINT, signal.SIGTERM]:
signal.signal(sig, handleSignal) signal.signal(sig, handleSignal)
# config warmup
Config.validateConfig()
coreConfig = CoreConfig() coreConfig = CoreConfig()
# passed loglevel takes priority (used for the --debug argument)
logging.getLogger().setLevel(coreConfig.get_log_level() if loglevel is None else loglevel)
# config warmup
Config.validateConfig()
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
if not featureDetector.is_available("core"): failed = featureDetector.get_failed_requirements("core")
if failed:
logger.error( logger.error(
"you are missing required dependencies to run openwebrx. " "you are missing required dependencies to run openwebrx. "
"please check that the following core requirements are installed and up to date:" "please check that the following core requirements are installed and up to date: %s",
", ".join(failed)
) )
logger.error(", ".join(featureDetector.get_requirements("core"))) for f in failed:
return description = featureDetector.get_requirement_description(f)
if description:
logger.error("description for %s:\n%s", f, description)
return 1
# Get error messages about unknown / unavailable features as soon as possible # Get error messages about unknown / unavailable features as soon as possible
# start up "always-on" sources right away # start up "always-on" sources right away
@ -114,3 +126,5 @@ Support and info: https://groups.io/g/openwebrx
SdrService.stopAllSources() SdrService.stopAllSources()
ReportingEngine.stopAll() ReportingEngine.stopAll()
DecoderQueue.stopAll() DecoderQueue.stopAll()
return 0

View File

@ -46,15 +46,14 @@ def run_admin_action(parser, args):
else: else:
if not hasattr(args, "silent") or not args.silent: if not hasattr(args, "silent") or not args.silent:
parser.print_help() parser.print_help()
sys.exit(1) return 1
sys.exit(0) return 0
try: try:
command.run(args) return command.run(args)
except Exception: except Exception:
if not hasattr(args, "silent") or not args.silent: if not hasattr(args, "silent") or not args.silent:
print("Error running command:") print("Error running command:")
traceback.print_exc() traceback.print_exc()
sys.exit(1) return 1
sys.exit(0) return 0

View File

@ -30,8 +30,7 @@ class UserCommand(Command, metaclass=ABCMeta):
password = getpass("Please enter the new password for {username}: ".format(username=username)) password = getpass("Please enter the new password for {username}: ".format(username=username))
confirm = getpass("Please confirm the new password: ") confirm = getpass("Please confirm the new password: ")
if password != confirm: if password != confirm:
print("ERROR: Password mismatch.") raise ValueError("Password mismatch")
sys.exit(1)
generated = False generated = False
return password, generated return password, generated
@ -108,8 +107,9 @@ class HasUser(Command):
if args.user in userList: if args.user in userList:
if not args.silent: if not args.silent:
print('User "{name}" exists.'.format(name=args.user)) print('User "{name}" exists.'.format(name=args.user))
return 0
else: else:
if not args.silent: if not args.silent:
print('User "{name}" does not exist.'.format(name=args.user)) print('User "{name}" does not exist.'.format(name=args.user))
# in bash, a return code > 0 is interpreted as "false" # in bash, a return code > 0 is interpreted as "false"
sys.exit(1) return 1

View File

@ -33,7 +33,7 @@ thirdpartyeRegex = re.compile("^([a-zA-Z0-9-]+)>((([a-zA-Z0-9-]+\\*?,)*)([a-zA-Z
messageIdRegex = re.compile("^(.*){([0-9]{1,5})$") messageIdRegex = re.compile("^(.*){([0-9]{1,5})$")
# regex to filter pseudo "WIDE" path elements # regex to filter pseudo "WIDE" path elements
widePattern = re.compile("^WIDE[0-9]-[0-9]$") widePattern = re.compile("^WIDE[0-9]$")
def decodeBase91(input): def decodeBase91(input):
@ -56,20 +56,24 @@ class Ax25Parser(PickleModule):
for i in range(0, len(l), n): for i in range(0, len(l), n):
yield l[i:i + n] yield l[i:i + n]
return { try:
"destination": self.extractCallsign(ax25frame[0:7]), return {
"source": self.extractCallsign(ax25frame[7:14]), "destination": self.extractCallsign(ax25frame[0:7]),
"path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)], "source": self.extractCallsign(ax25frame[7:14]),
"data": ax25frame[control_pid + 2 :], "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)],
} "data": ax25frame[control_pid + 2 :],
}
except (ValueError, IndexError):
logger.exception("error parsing ax25 frame")
def extractCallsign(self, input): def extractCallsign(self, input):
cs = bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip() cs = {
"callsign": bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip(),
}
ssid = (input[6] & 0b00011110) >> 1 ssid = (input[6] & 0b00011110) >> 1
if ssid > 0: if ssid > 0:
return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid) cs["ssid"] = ssid
else: return cs
return cs
class WeatherMapping(object): class WeatherMapping(object):
@ -175,7 +179,7 @@ class AprsParser(PickleModule):
def isDirect(self, aprsData): def isDirect(self, aprsData):
if "path" in aprsData and len(aprsData["path"]) > 0: if "path" in aprsData and len(aprsData["path"]) > 0:
hops = [host for host in aprsData["path"] if widePattern.match(host) is None] hops = [host for host in aprsData["path"] if widePattern.match(host["callsign"]) is None]
if len(hops) > 0: if len(hops) > 0:
return False return False
if "type" in aprsData and aprsData["type"] in ["thirdparty", "item", "object"]: if "type" in aprsData and aprsData["type"] in ["thirdparty", "item", "object"]:
@ -204,12 +208,13 @@ class AprsParser(PickleModule):
mapData = mapData["data"] mapData = mapData["data"]
if "lat" in mapData and "lon" in mapData: if "lat" in mapData and "lon" in mapData:
loc = AprsLocation(mapData) loc = AprsLocation(mapData)
source = mapData["source"] source = mapData["source"].copy()
# these are special packets, sent on behalf of other entities
if "type" in mapData: if "type" in mapData:
if mapData["type"] == "item": if mapData["type"] == "item" and "item" in mapData:
source = mapData["item"] source["item"] = mapData["item"]
elif mapData["type"] == "object": elif mapData["type"] == "object" and "object" in mapData:
source = mapData["object"] source["object"] = mapData["object"]
Map.getSharedInstance().updateLocation(source, loc, "APRS", self.band) Map.getSharedInstance().updateLocation(source, loc, "APRS", self.band)
def hasCompressedCoordinates(self, raw): def hasCompressedCoordinates(self, raw):
@ -342,15 +347,24 @@ class AprsParser(PickleModule):
return result return result
def parseThirdpartyAprsData(self, information): def parseThirdpartyAprsData(self, information):
# in thirdparty packets, the callsign is passed as a string with -SSID suffix...
# this seems to be the only case where parsing is necessary, hence this function is inline
def parseCallsign(callsign):
el = callsign.split('-')
result = {"callsign": el[0]}
if len(el) > 1:
result["ssid"] = int(el[1])
return result
matches = thirdpartyeRegex.match(information) matches = thirdpartyeRegex.match(information)
if matches: if matches:
path = matches.group(2).split(",") path = matches.group(2).split(",")
destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None) destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None)
data = self.parseAprsData( data = self.parseAprsData(
{ {
"source": matches.group(1).upper(), "source": parseCallsign(matches.group(1).upper()),
"destination": destination, "destination": parseCallsign(destination),
"path": path, "path": [parseCallsign(c) for c in path],
"data": matches.group(6).encode(encoding), "data": matches.group(6).encode(encoding),
} }
) )
@ -528,7 +542,7 @@ class MicEParser(object):
def parse(self, data): def parse(self, data):
information = data["data"] information = data["data"]
destination = data["destination"] destination = data["destination"]["callsign"]
rawLatitude = [self.extractNumber(c) for c in destination[0:6]] rawLatitude = [self.extractNumber(c) for c in destination[0:6]]
lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000 lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000

View File

@ -49,6 +49,8 @@ class DirewolfModule(AutoStartModule, DirewolfConfigSubscriber):
stdin=PIPE, stdin=PIPE,
) )
# resume in case the reader has been stop()ed before
self.reader.resume()
threading.Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start() threading.Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start()
delay = 0.5 delay = 0.5

View File

@ -9,6 +9,7 @@ class CoreConfig(object):
"core": { "core": {
"data_directory": "/var/lib/openwebrx", "data_directory": "/var/lib/openwebrx",
"temporary_directory": "/tmp", "temporary_directory": "/tmp",
"log_level": "INFO",
}, },
"web": { "web": {
"port": 8073, "port": 8073,
@ -34,6 +35,7 @@ class CoreConfig(object):
CoreConfig.checkDirectory(self.data_directory, "data_directory") CoreConfig.checkDirectory(self.data_directory, "data_directory")
self.temporary_directory = config.get("core", "temporary_directory") self.temporary_directory = config.get("core", "temporary_directory")
CoreConfig.checkDirectory(self.temporary_directory, "temporary_directory") CoreConfig.checkDirectory(self.temporary_directory, "temporary_directory")
self.log_level = config.get("core", "log_level")
self.web_port = config.getint("web", "port") self.web_port = config.getint("web", "port")
self.aprs_symbols_path = config.get("aprs", "symbols_path") self.aprs_symbols_path = config.get("aprs", "symbols_path")
@ -57,3 +59,6 @@ class CoreConfig(object):
def get_aprs_symbols_path(self): def get_aprs_symbols_path(self):
return self.aprs_symbols_path return self.aprs_symbols_path
def get_log_level(self):
return self.log_level

View File

@ -2,7 +2,7 @@ from owrx.property import PropertyLayer
defaultConfig = PropertyLayer( defaultConfig = PropertyLayer(
version=7, version=8,
max_clients=20, max_clients=20,
receiver_name="[Callsign]", receiver_name="[Callsign]",
receiver_location="Budapest, Hungary", receiver_location="Budapest, Hungary",
@ -90,7 +90,7 @@ defaultConfig = PropertyLayer(
), ),
), ),
sdrplay=PropertyLayer( sdrplay=PropertyLayer(
name="SDRPlay RSP2", name="SDRPlay device",
type="sdrplay", type="sdrplay",
antenna="Antenna A", antenna="Antenna A",
profiles=PropertyLayer( profiles=PropertyLayer(

View File

@ -111,8 +111,21 @@ class ConfigMigratorVersion6(ConfigMigrator):
config["version"] = 7 config["version"] = 7
class ConfigMigratorVersion7(ConfigMigrator):
def migrate(self, config):
if "callsign_url" in config:
if "qrzcq.com" in config["callsign_url"]:
config["callsign_service"] = "qrzcq"
elif "qrz.com" in config["callsign_url"]:
config["callsign_service"] = "qrz"
else:
logger.warning("unable to migrate callsign_url! please check settings!")
del config["callsign_url"]
config["version"] = 8
class Migrator(object): class Migrator(object):
currentVersion = 7 currentVersion = 8
migrators = { migrators = {
1: ConfigMigratorVersion1(), 1: ConfigMigratorVersion1(),
2: ConfigMigratorVersion2(), 2: ConfigMigratorVersion2(),
@ -120,6 +133,7 @@ class Migrator(object):
4: ConfigMigratorVersion4(), 4: ConfigMigratorVersion4(),
5: ConfigMigratorVersion5(), 5: ConfigMigratorVersion5(),
6: ConfigMigratorVersion6(), 6: ConfigMigratorVersion6(),
7: ConfigMigratorVersion7(),
} }
@staticmethod @staticmethod

View File

@ -56,9 +56,10 @@ class Client(Handler, metaclass=ABCMeta):
try: try:
self.conn.send(data) self.conn.send(data)
except IOError: except IOError:
self.close() logger.exception("error in Client::send()")
self.close(error=True)
def close(self): def close(self, error: bool = False):
if self.multithreadingQueue is not None: if self.multithreadingQueue is not None:
while True: while True:
try: try:
@ -70,7 +71,7 @@ class Client(Handler, metaclass=ABCMeta):
except Full: except Full:
# this shouldn't happen, we just emptied the queue, but it's not worth risking the exception # this shouldn't happen, we just emptied the queue, but it's not worth risking the exception
logger.exception("impossible queue state: Full after Empty") logger.exception("impossible queue state: Full after Empty")
self.conn.close() self.conn.close(socketError=error)
def mp_send(self, data): def mp_send(self, data):
if self.multithreadingQueue is None: if self.multithreadingQueue is None:
@ -78,7 +79,7 @@ class Client(Handler, metaclass=ABCMeta):
try: try:
self.multithreadingQueue.put(data, block=False) self.multithreadingQueue.put(data, block=False)
except Full: except Full:
self.close() self.close(error=True)
@abstractmethod @abstractmethod
def handleTextMessage(self, conn, message): def handleTextMessage(self, conn, message):
@ -107,9 +108,9 @@ class OpenWebRxClient(Client, metaclass=ABCMeta):
def write_receiver_details(self, details): def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details}) self.send({"type": "receiver_details", "value": details})
def close(self): def close(self, error: bool = False):
self._detailsSubscription.cancel() self._detailsSubscription.cancel()
super().close() super().close(error)
class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient): class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
@ -339,7 +340,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def handleNoSdrsAvailable(self): def handleNoSdrsAvailable(self):
self.write_sdr_error("No SDR Devices available") self.write_sdr_error("No SDR Devices available")
def close(self): def close(self, error: bool = False):
if self.sdr is not None: if self.sdr is not None:
self.sdr.removeClient(self) self.sdr.removeClient(self)
self.stopDsp() self.stopDsp()
@ -350,7 +351,7 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
if self.bookmarkSub is not None: if self.bookmarkSub is not None:
self.bookmarkSub.cancel() self.bookmarkSub.cancel()
self.bookmarkSub = None self.bookmarkSub = None
super().close() super().close(error)
def stopDsp(self): def stopDsp(self):
with self.dspLock: with self.dspLock:
@ -422,6 +423,9 @@ class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient):
def write_sdr_error(self, message): def write_sdr_error(self, message):
self.send({"type": "sdr_error", "value": message}) self.send({"type": "sdr_error", "value": message})
def write_demodulator_error(self, message):
self.send({"type": "demodulator_error", "value": message})
def write_backoff_message(self, reason): def write_backoff_message(self, reason):
self.send({"type": "backoff", "reason": reason}) self.send({"type": "backoff", "reason": reason})
@ -452,6 +456,7 @@ class MapConnection(OpenWebRxClient):
"google_maps_api_key", "google_maps_api_key",
"receiver_gps", "receiver_gps",
"map_position_retention_time", "map_position_retention_time",
"callsign_service",
"receiver_name", "receiver_name",
) )
filtered_config.wire(self.write_config) filtered_config.wire(self.write_config)
@ -463,9 +468,9 @@ class MapConnection(OpenWebRxClient):
def handleTextMessage(self, conn, message): def handleTextMessage(self, conn, message):
pass pass
def close(self): def close(self, error: bool = False):
Map.getSharedInstance().removeClient(self) Map.getSharedInstance().removeClient(self)
super().close() super().close(error)
def write_config(self, cfg): def write_config(self, cfg):
self.send({"type": "config", "value": cfg}) self.send({"type": "config", "value": cfg})
@ -489,17 +494,17 @@ class HandshakeMessageHandler(Handler):
client = None client = None
if "type" in handshake: if "type" in handshake:
if handshake["type"] == "receiver": if handshake["type"] == "receiver":
client = OpenWebRxReceiverClient(conn) client = OpenWebRxReceiverClient
elif handshake["type"] == "map": elif handshake["type"] == "map":
client = MapConnection(conn) client = MapConnection
else: else:
logger.warning("invalid connection type: %s", handshake["type"]) logger.warning("invalid connection type: %s", handshake["type"])
if client is not None: if client is not None:
logger.debug("handshake complete, handing off to %s", type(client).__name__) logger.debug("handshake complete, handing off to %s", client.__name__)
# hand off all further communication to the correspondig connection # hand off all further communication to the correspondig connection
conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version)) conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version))
conn.setMessageHandler(client) conn.setMessageHandler(client(conn))
else: else:
logger.warning('invalid handshake received') logger.warning('invalid handshake received')
else: else:

View File

@ -158,6 +158,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/settings/OptionalSection.js", "lib/settings/OptionalSection.js",
"lib/settings/SchedulerInput.js", "lib/settings/SchedulerInput.js",
"lib/settings/ExponentialInput.js", "lib/settings/ExponentialInput.js",
"lib/settings/LogMessages.js",
"settings.js", "settings.js",
], ],
} }

View File

@ -1,6 +1,8 @@
from . import Controller from . import Controller
from owrx.metrics import CounterMetric, DirectMetric, Metrics from owrx.metrics import CounterMetric, DirectMetric, Metrics
import json import json
import re
class MetricsController(Controller): class MetricsController(Controller):
@ -21,7 +23,7 @@ class MetricsController(Controller):
else: else:
raise ValueError("Unexpected metric type for metric {}".format(repr(metric))) raise ValueError("Unexpected metric type for metric {}".format(repr(metric)))
return "{key} {value}".format(key=key.replace(".", "_"), value=value) return "{key} {value}".format(key=re.sub('[^a-zA-Z0-9:_]', '_', key), value=value)
data = ["# https://prometheus.io/docs/instrumenting/exposition_formats/"] + [ data = ["# https://prometheus.io/docs/instrumenting/exposition_formats/"] + [
prometheusFormat(k, v) for k, v in metrics.items() prometheusFormat(k, v) for k, v in metrics.items()

View File

@ -72,8 +72,8 @@ class SessionController(WebpageController):
self.set_response_cookies(cookie) self.set_response_cookies(cookie)
self.send_redirect(target) self.send_redirect(target)
return return
target = "?{}".format(urlencode({"ref": self.request.query["ref"][0]})) if "ref" in self.request.query else "" target = "{}login?{}".format(self.get_document_root(), urlencode({"ref": self.request.query["ref"][0]}))
self.send_redirect(self.request.path + target) self.send_redirect(target)
def logoutAction(self): def logoutAction(self):
self.send_redirect("logout happening here") self.send_redirect("logout happening here")

View File

@ -5,7 +5,6 @@ from owrx.form.input import (
TextInput, TextInput,
NumberInput, NumberInput,
FloatInput, FloatInput,
LocationInput,
TextAreaInput, TextAreaInput,
DropdownInput, DropdownInput,
Option, Option,
@ -14,6 +13,7 @@ from owrx.form.input.converter import WaterfallColorsConverter, IntConverter
from owrx.form.input.receiverid import ReceiverKeysConverter from owrx.form.input.receiverid import ReceiverKeysConverter
from owrx.form.input.gfx import AvatarInput, TopPhotoInput from owrx.form.input.gfx import AvatarInput, TopPhotoInput
from owrx.form.input.device import WaterfallLevelsInput, WaterfallAutoLevelsInput from owrx.form.input.device import WaterfallLevelsInput, WaterfallAutoLevelsInput
from owrx.form.input.location import LocationInput
from owrx.waterfall import WaterfallOptions from owrx.waterfall import WaterfallOptions
from owrx.breadcrumb import Breadcrumb, BreadcrumbItem from owrx.breadcrumb import Breadcrumb, BreadcrumbItem
from owrx.controllers.settings import SettingsBreadcrumb from owrx.controllers.settings import SettingsBreadcrumb
@ -48,7 +48,7 @@ class GeneralSettingsController(SettingsFormController):
TextInput("receiver_admin", "Receiver admin"), TextInput("receiver_admin", "Receiver admin"),
LocationInput("receiver_gps", "Receiver coordinates"), LocationInput("receiver_gps", "Receiver coordinates"),
TextInput("photo_title", "Photo title"), TextInput("photo_title", "Photo title"),
TextAreaInput("photo_desc", "Photo description"), TextAreaInput("photo_desc", "Photo description", infotext="HTML supported "),
), ),
Section( Section(
"Receiver images", "Receiver images",
@ -168,6 +168,18 @@ class GeneralSettingsController(SettingsFormController):
infotext="Specifies how log markers / grids will remain visible on the map", infotext="Specifies how log markers / grids will remain visible on the map",
append="s", append="s",
), ),
DropdownInput(
"callsign_service",
"Callsign database service",
infotext="Allows users to navigate to an external callsign database service by clicking on "
+ "callsigns",
options=[
Option(None, "disabled"),
Option("qrzcq", "qrzcq.com"),
Option("qrz", "qrz.com"),
Option("aprsfi", "aprs.fi"),
],
),
), ),
] ]

View File

@ -12,6 +12,7 @@ from owrx.form.input import TextInput, DropdownInput, Option
from owrx.form.input.validator import RequiredValidator from owrx.form.input.validator import RequiredValidator
from owrx.property import PropertyLayer from owrx.property import PropertyLayer
from owrx.breadcrumb import BreadcrumbMixin, Breadcrumb, BreadcrumbItem from owrx.breadcrumb import BreadcrumbMixin, Breadcrumb, BreadcrumbItem
from owrx.log import HistoryHandler
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from uuid import uuid4 from uuid import uuid4
@ -279,6 +280,21 @@ class SdrDeviceController(SdrFormControllerWithModal):
config.store() config.store()
return self.send_redirect("{}settings/sdr".format(self.get_document_root())) return self.send_redirect("{}settings/sdr".format(self.get_document_root()))
def render_sections(self):
handler = HistoryHandler.getHandler("owrx.source.{id}".format(id=self.device_id))
return """
{sections}
<div class="card mt-2">
<div class="card-header">Recent device log messages</div>
<div class="card-body">
<pre class="card-text device-log-messages">{messages}</pre>
</div>
</div>
""".format(
sections=super().render_sections(),
messages=handler.getFormattedHistory(),
)
class NewSdrDeviceController(SettingsFormController): class NewSdrDeviceController(SettingsFormController):
def __init__(self, handler, request, options): def __init__(self, handler, request, options):

View File

@ -22,6 +22,7 @@ class CpuUsageThread(threading.Thread):
self.last_worktime = 0 self.last_worktime = 0
self.last_idletime = 0 self.last_idletime = 0
self.endEvent = threading.Event() self.endEvent = threading.Event()
self.startLock = threading.Lock()
super().__init__() super().__init__()
def run(self): def run(self):
@ -59,8 +60,9 @@ class CpuUsageThread(threading.Thread):
def add_client(self, c): def add_client(self, c):
self.clients.append(c) self.clients.append(c)
if not self.is_alive(): with self.startLock:
self.start() if not self.is_alive():
self.start()
def remove_client(self, c): def remove_client(self, c):
try: try:

View File

@ -2,6 +2,9 @@ from owrx.config import Config
from owrx.locator import Locator from owrx.locator import Locator
from owrx.property import PropertyFilter from owrx.property import PropertyFilter
from owrx.property.filter import ByPropertyName from owrx.property.filter import ByPropertyName
import logging
logger = logging.getLogger(__name__)
class ReceiverDetails(PropertyFilter): class ReceiverDetails(PropertyFilter):
@ -20,5 +23,8 @@ class ReceiverDetails(PropertyFilter):
def __dict__(self): def __dict__(self):
receiver_info = super().__dict__() receiver_info = super().__dict__()
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"]) try:
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
except ValueError as e:
logger.error("invalid receiver location, check in settings: %s", str(e))
return receiver_info return receiver_info

View File

@ -1,12 +1,13 @@
from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass
from owrx.property import PropertyStack, PropertyLayer, PropertyValidator from owrx.property import PropertyStack, PropertyLayer, PropertyValidator
from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.property.validators import OrValidator, RegexValidator, BoolValidator
from owrx.modes import Modes from owrx.modes import Modes, DigitalMode
from csdr.chain import Chain from csdr.chain import Chain
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, DeemphasisTauChain from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, DeemphasisTauChain, DemodulatorError
from csdr.chain.selector import Selector, SecondarySelector from csdr.chain.selector import Selector, SecondarySelector
from csdr.chain.clientaudio import ClientAudioChain from csdr.chain.clientaudio import ClientAudioChain
from csdr.chain.fft import FftChain from csdr.chain.fft import FftChain
from csdr.chain.dummy import DummyDemodulator
from pycsdr.modules import Buffer, Writer from pycsdr.modules import Buffer, Writer
from pycsdr.types import Format from pycsdr.types import Format
from typing import Union, Optional from typing import Union, Optional
@ -103,22 +104,11 @@ class ClientDemodulatorChain(Chain):
self.demodulator = demodulator self.demodulator = demodulator
outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate self.selector.setOutputRate(self._getSelectorOutputRate())
if isinstance(self.demodulator, FixedIfSampleRateChain): clientRate = self._getClientAudioInputRate()
self.selector.setOutputRate(self.demodulator.getFixedIfSampleRate()) self.clientAudioChain.setInputRate(clientRate)
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain): self.demodulator.setSampleRate(clientRate)
self.selector.setOutputRate(self.secondaryDemodulator.getFixedAudioRate())
else:
self.selector.setOutputRate(outputRate)
self.demodulator.setSampleRate(outputRate)
if isinstance(self.demodulator, FixedAudioRateChain):
self.clientAudioChain.setInputRate(self.demodulator.getFixedAudioRate())
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
self.clientAudioChain.setInputRate(self.secondaryDemodulator.getFixedAudioRate())
else:
self.clientAudioChain.setInputRate(outputRate)
if isinstance(self.demodulator, DeemphasisTauChain): if isinstance(self.demodulator, DeemphasisTauChain):
self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau) self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau)
@ -126,17 +116,42 @@ class ClientDemodulatorChain(Chain):
self._updateDialFrequency() self._updateDialFrequency()
self._syncSquelch() self._syncSquelch()
outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
self.clientAudioChain.setClientRate(outputRate) self.clientAudioChain.setClientRate(outputRate)
if self.metaWriter is not None and isinstance(demodulator, MetaProvider): if self.metaWriter is not None and isinstance(demodulator, MetaProvider):
demodulator.setMetaWriter(self.metaWriter) demodulator.setMetaWriter(self.metaWriter)
def stopDemodulator(self):
if self.demodulator is None:
return
# we need to get the currrent demodulator out of the chain so that it can be deallocated properly
# so we just replace it with a dummy here
# in order to avoid any client audio chain hassle, the dummy simply imitates the output format of the current
# demodulator
self.replace(1, DummyDemodulator(self.demodulator.getOutputFormat()))
self.demodulator.stop()
self.demodulator = None
def _getSelectorOutputRate(self): def _getSelectorOutputRate(self):
if isinstance(self.secondaryDemodulator, FixedAudioRateChain): if isinstance(self.demodulator, FixedIfSampleRateChain):
return self.demodulator.getFixedIfSampleRate()
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
if isinstance(self.demodulator, FixedAudioRateChain) and self.demodulator.getFixedAudioRate() != self.secondaryDemodulator.getFixedAudioRate(): if isinstance(self.demodulator, FixedAudioRateChain) and self.demodulator.getFixedAudioRate() != self.secondaryDemodulator.getFixedAudioRate():
raise ValueError("secondary and primary demodulator chain audio rates do not match!") raise ValueError("secondary and primary demodulator chain audio rates do not match!")
return self.secondaryDemodulator.getFixedAudioRate() return self.secondaryDemodulator.getFixedAudioRate()
return self.outputRate else:
return self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
def _getClientAudioInputRate(self):
if isinstance(self.demodulator, FixedAudioRateChain):
return self.demodulator.getFixedAudioRate()
elif isinstance(self.secondaryDemodulator, FixedAudioRateChain):
return self.secondaryDemodulator.getFixedAudioRate()
else:
return self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate
def setSecondaryDemodulator(self, demod: Optional[SecondaryDemodulator]): def setSecondaryDemodulator(self, demod: Optional[SecondaryDemodulator]):
if demod is self.secondaryDemodulator: if demod is self.secondaryDemodulator:
@ -149,8 +164,11 @@ class ClientDemodulatorChain(Chain):
rate = self._getSelectorOutputRate() rate = self._getSelectorOutputRate()
self.selector.setOutputRate(rate) self.selector.setOutputRate(rate)
self.clientAudioChain.setInputRate(rate)
self.demodulator.setSampleRate(rate) clientRate = self._getClientAudioInputRate()
self.clientAudioChain.setInputRate(clientRate)
self.demodulator.setSampleRate(clientRate)
self._updateDialFrequency() self._updateDialFrequency()
self._syncSquelch() self._syncSquelch()
@ -433,12 +451,17 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
self.readers = {} self.readers = {}
if "start_mod" in self.props: if "start_mod" in self.props:
self.setDemodulator(self.props["start_mod"])
mode = Modes.findByModulation(self.props["start_mod"]) mode = Modes.findByModulation(self.props["start_mod"])
if mode:
if mode and mode.bandpass: self.setDemodulator(mode.get_modulation())
bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut] if isinstance(mode, DigitalMode):
self.chain.setBandpass(*bpf) self.setSecondaryDemodulator(mode.modulation)
if mode.bandpass:
bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut]
self.chain.setBandpass(*bpf)
else:
# TODO modes should be mandatory
self.setDemodulator(self.props["start_mod"])
if "start_freq" in self.props and "center_freq" in self.props: if "start_freq" in self.props and "center_freq" in self.props:
self.chain.setFrequencyOffset(self.props["start_freq"] - self.props["center_freq"]) self.chain.setFrequencyOffset(self.props["start_freq"] - self.props["center_freq"])
@ -490,8 +513,6 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
self.sdrSource.addClient(self) self.sdrSource.addClient(self)
super().__init__()
def setSecondaryFftSize(self, size): def setSecondaryFftSize(self, size):
self.chain.setSecondaryFftSize(size) self.chain.setSecondaryFftSize(size)
self.handler.write_secondary_dsp_config({"secondary_fft_size": size}) self.handler.write_secondary_dsp_config({"secondary_fft_size": size})
@ -535,19 +556,23 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
return FreeDV() return FreeDV()
def setDemodulator(self, mod): def setDemodulator(self, mod):
demodulator = self._getDemodulator(mod) self.chain.stopDemodulator()
if demodulator is None: try:
raise ValueError("unsupported demodulator: {}".format(mod)) demodulator = self._getDemodulator(mod)
self.chain.setDemodulator(demodulator) if demodulator is None:
raise ValueError("unsupported demodulator: {}".format(mod))
self.chain.setDemodulator(demodulator)
output = "hd_audio" if isinstance(demodulator, HdAudio) else "audio" output = "hd_audio" if isinstance(demodulator, HdAudio) else "audio"
if output != self.audioOutput: if output != self.audioOutput:
self.audioOutput = output self.audioOutput = output
# re-wire the audio to the correct client API # re-wire the audio to the correct client API
buffer = Buffer(self.chain.getOutputFormat()) buffer = Buffer(self.chain.getOutputFormat())
self.chain.setWriter(buffer) self.chain.setWriter(buffer)
self.wireOutput(self.audioOutput, buffer) self.wireOutput(self.audioOutput, buffer)
except DemodulatorError as de:
self.handler.write_demodulator_error(str(de))
def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]: def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]:
if isinstance(mod, SecondaryDemodulator): if isinstance(mod, SecondaryDemodulator):
@ -556,6 +581,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
from csdr.chain.digimodes import AudioChopperDemodulator from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
return AudioChopperDemodulator(mod, WsjtParser()) return AudioChopperDemodulator(mod, WsjtParser())
elif mod == "msk144":
from csdr.chain.digimodes import Msk144Demodulator
return Msk144Demodulator()
elif mod == "js8": elif mod == "js8":
from csdr.chain.digimodes import AudioChopperDemodulator from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.js8 import Js8Parser from owrx.js8 import Js8Parser
@ -564,7 +592,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
from csdr.chain.digimodes import PacketDemodulator from csdr.chain.digimodes import PacketDemodulator
return PacketDemodulator() return PacketDemodulator()
elif mod == "pocsag": elif mod == "pocsag":
from csdr.chain.digimodes import PocsagDemodulator from csdr.chain.digiham import PocsagDemodulator
return PocsagDemodulator() return PocsagDemodulator()
elif mod == "bpsk31": elif mod == "bpsk31":
from csdr.chain.digimodes import PskDemodulator from csdr.chain.digimodes import PskDemodulator

View File

@ -13,6 +13,7 @@ from datetime import datetime, timedelta
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class UnknownFeatureException(Exception): class UnknownFeatureException(Exception):
@ -51,7 +52,7 @@ class FeatureCache(object):
class FeatureDetector(object): class FeatureDetector(object):
features = { features = {
# core features; we won't start without these # core features; we won't start without these
"core": ["csdr", "pycsdr"], "core": ["csdr"],
# different types of sdrs and their requirements # different types of sdrs and their requirements
"rtl_sdr": ["rtl_connector"], "rtl_sdr": ["rtl_connector"],
"rtl_sdr_soapy": ["soapy_connector", "soapy_rtl_sdr"], "rtl_sdr_soapy": ["soapy_connector", "soapy_rtl_sdr"],
@ -68,6 +69,7 @@ class FeatureDetector(object):
"uhd": ["soapy_connector", "soapy_uhd"], "uhd": ["soapy_connector", "soapy_uhd"],
"radioberry": ["soapy_connector", "soapy_radioberry"], "radioberry": ["soapy_connector", "soapy_radioberry"],
"fcdpp": ["soapy_connector", "soapy_fcdpp"], "fcdpp": ["soapy_connector", "soapy_fcdpp"],
"bladerf": ["soapy_connector", "soapy_bladerf"],
"sddc": ["sddc_connector"], "sddc": ["sddc_connector"],
"hpsdr": ["hpsdr_connector"], "hpsdr": ["hpsdr_connector"],
"runds": ["runds_connector"], "runds": ["runds_connector"],
@ -78,6 +80,7 @@ class FeatureDetector(object):
"wsjt-x": ["wsjtx"], "wsjt-x": ["wsjtx"],
"wsjt-x-2-3": ["wsjtx_2_3"], "wsjt-x-2-3": ["wsjtx_2_3"],
"wsjt-x-2-4": ["wsjtx_2_4"], "wsjt-x-2-4": ["wsjtx_2_4"],
"msk144": ["msk144decoder"],
"packet": ["direwolf"], "packet": ["direwolf"],
"pocsag": ["digiham"], "pocsag": ["digiham"],
"js8call": ["js8", "js8py"], "js8call": ["js8", "js8py"],
@ -108,6 +111,9 @@ class FeatureDetector(object):
def is_available(self, feature): def is_available(self, feature):
return self.has_requirements(self.get_requirements(feature)) return self.has_requirements(self.get_requirements(feature))
def get_failed_requirements(self, feature):
return [req for req in self.get_requirements(feature) if not self.has_requirement(req)]
def get_requirements(self, feature): def get_requirements(self, feature):
try: try:
return FeatureDetector.features[feature] return FeatureDetector.features[feature]
@ -131,12 +137,14 @@ class FeatureDetector(object):
if cache.has(requirement): if cache.has(requirement):
return cache.get(requirement) return cache.get(requirement)
logger.debug("performing feature check for %s", requirement)
method = self._get_requirement_method(requirement) method = self._get_requirement_method(requirement)
result = False result = False
if method is not None: if method is not None:
result = method() result = method()
else: else:
logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement))
logger.debug("feature check for %s complete. result: %s", requirement, result)
cache.set(requirement, result) cache.set(requirement, result)
return result return result
@ -167,27 +175,24 @@ class FeatureDetector(object):
except FileNotFoundError: except FileNotFoundError:
return False return False
_required_csdr_version = LooseVersion("0.18.0")
def has_csdr(self): def has_csdr(self):
""" """
OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project
page on github](https://github.com/jketterl/csdr) for further details and installation instructions. page on github](https://github.com/jketterl/csdr) for further details and installation instructions.
In addition, the [pycsdr](https://github.com/jketterl/pycsdr) package must be installed to provide
python bindings for the csdr library.
""" """
required_version = LooseVersion("0.18.0")
try: try:
from pycsdr.modules import csdr_version from pycsdr.modules import csdr_version
return LooseVersion(csdr_version) >= FeatureDetector._required_csdr_version
except ImportError:
return False
def has_pycsdr(self):
"""
OpenWebRX uses the csdr python bindings from the pycsdr package to build its demodulator pipelines.
Please visit [the project page](https://github.com/jketterl/pycsdr) for further details.
"""
try:
from pycsdr.modules import version as pycsdr_version from pycsdr.modules import version as pycsdr_version
return LooseVersion(pycsdr_version) >= FeatureDetector._required_csdr_version
return (
LooseVersion(csdr_version) >= required_version and
LooseVersion(pycsdr_version) >= required_version
)
except ImportError: except ImportError:
return False return False
@ -222,15 +227,23 @@ class FeatureDetector(object):
To use digital voice modes, the digiham package is required. You can find the package and installation To use digital voice modes, the digiham package is required. You can find the package and installation
instructions [here](https://github.com/jketterl/digiham). instructions [here](https://github.com/jketterl/digiham).
In addition, the [pydigiham](https://github.com/jketterl/pydigiham) package must be installed to provide
python bindings for the digiham library.
Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work. Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work.
If you have an older verison of digiham installed, please update it along with openwebrx. If you have an older verison of digiham installed, please update it along with openwebrx.
As of now, we require version 0.3 of digiham. As of now, we require version 0.6 of digiham.
""" """
required_version = LooseVersion("0.5") required_version = LooseVersion("0.6")
try: try:
from digiham.modules import version as digiham_version from digiham.modules import digiham_version as digiham_version
return LooseVersion(digiham_version) >= required_version from digiham.modules import version as pydigiham_version
return (
LooseVersion(digiham_version) >= required_version
and LooseVersion(pydigiham_version) >= required_version
)
except ImportError: except ImportError:
return False return False
@ -383,6 +396,14 @@ class FeatureDetector(object):
""" """
return self._has_soapy_driver("fcdpp") return self._has_soapy_driver("fcdpp")
def has_soapy_bladerf(self):
"""
The SoapyBladeRF module allows the use of Blade RF devices.
You can get it [here](https://github.com/pothosware/SoapyBladeRF).
"""
return self._has_soapy_driver("bladerf")
def has_m17_demod(self): def has_m17_demod(self):
""" """
The `m17-demod` tool is used to demodulate M17 digital voice signals. The `m17-demod` tool is used to demodulate M17 digital voice signals.
@ -408,7 +429,7 @@ class FeatureDetector(object):
def has_wsjtx(self): def has_wsjtx(self):
""" """
To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the
[WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions [WSJT-X homepage](https://wsjt.sourceforge.io/) for ready-made packages or instructions
on how to build from source. on how to build from source.
""" """
return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)
@ -439,6 +460,13 @@ class FeatureDetector(object):
""" """
return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.4")) return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.4"))
def has_msk144decoder(self):
"""
To decode the MSK144 digimode please install the "msk144decoder". See the
[project page](https://github.com/alexander-sholohov/msk144decoder) for more details.
"""
return self.command_is_runnable("msk144decoder")
def has_js8(self): def has_js8(self):
""" """
To decode JS8, you will need to install [JS8Call](http://js8call.com/) To decode JS8, you will need to install [JS8Call](http://js8call.com/)
@ -454,9 +482,10 @@ class FeatureDetector(object):
The js8py library is used to decode binary JS8 messages into readable text. More information is available on The js8py library is used to decode binary JS8 messages into readable text. More information is available on
[its github page](https://github.com/jketterl/js8py). [its github page](https://github.com/jketterl/js8py).
""" """
required_version = StrictVersion("0.1") required_version = StrictVersion("0.2")
try: try:
from js8py.version import strictversion from js8py.version import strictversion
return strictversion >= required_version return strictversion >= required_version
except ImportError: except ImportError:
return False return False
@ -533,6 +562,7 @@ class FeatureDetector(object):
server = config["digital_voice_codecserver"] server = config["digital_voice_codecserver"]
try: try:
from digiham.modules import MbeSynthesizer from digiham.modules import MbeSynthesizer
return MbeSynthesizer.hasAmbe(server) return MbeSynthesizer.hasAmbe(server)
except ImportError: except ImportError:
return False return False

View File

@ -77,8 +77,9 @@ class SpectrumThread(SdrSourceEventClient):
return return
self.dsp.stop() self.dsp.stop()
self.dsp = None self.dsp = None
self.reader.stop() if self.reader:
self.reader = None self.reader.stop()
self.reader = None
self.sdrSource.removeClient(self) self.sdrSource.removeClient(self)
while self.subscriptions: while self.subscriptions:
self.subscriptions.pop().cancel() self.subscriptions.pop().cancel()
@ -92,7 +93,8 @@ class SpectrumThread(SdrSourceEventClient):
def onStateChange(self, state: SdrSourceState): def onStateChange(self, state: SdrSourceState):
if state is SdrSourceState.STOPPING: if state is SdrSourceState.STOPPING:
self.dsp.stop() if self.dsp:
self.dsp.stop()
elif state == SdrSourceState.RUNNING: elif state == SdrSourceState.RUNNING:
if self.dsp is None: if self.dsp is None:
self.start() self.start()

View File

@ -1,8 +1,7 @@
from abc import ABC from abc import ABC
from owrx.modes import Modes from owrx.modes import Modes
from owrx.config import Config
from owrx.form.input.validator import Validator from owrx.form.input.validator import Validator
from owrx.form.input.converter import Converter, NullConverter, IntConverter, FloatConverter, EnumConverter from owrx.form.input.converter import Converter, NullConverter, IntConverter, FloatConverter, EnumConverter, TextConverter
from enum import Enum from enum import Enum
@ -107,6 +106,9 @@ class TextInput(Input):
props["type"] = "text" props["type"] = "text"
return props return props
def defaultConverter(self):
return TextConverter()
class NumberInput(Input): class NumberInput(Input):
def __init__(self, id, label, infotext=None, append="", converter: Converter = None, validator: Validator = None): def __init__(self, id, label, infotext=None, append="", converter: Converter = None, validator: Validator = None):
@ -158,45 +160,6 @@ class FloatInput(NumberInput):
return FloatConverter() return FloatConverter()
class LocationInput(Input):
def render_input_group(self, value, errors):
return """
<div class="row {rowclass}">
{inputs}
</div>
{errors}
<div class="row">
<div class="col map-input" data-key="{key}" for="{id}"></div>
</div>
""".format(
id=self.id,
rowclass="is-invalid" if errors else "",
inputs=self.render_input(value, errors),
errors=self.render_errors(errors),
key=Config.get()["google_maps_api_key"],
)
def render_input(self, value, errors):
return "".join(self.render_sub_input(value, id, errors) for id in ["lat", "lon"])
def render_sub_input(self, value, id, errors):
return """
<div class="col">
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}"
step="any" {disabled}>
</div>
""".format(
id="{0}-{1}".format(self.id, id),
label=self.label,
classes=self.input_classes(errors),
value=value[id],
disabled="disabled" if self.disabled else "",
)
def parse(self, data):
return {self.id: {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}}
class TextAreaInput(Input): class TextAreaInput(Input):
def render_input(self, value, errors): def render_input(self, value, errors):
return """ return """

View File

@ -14,6 +14,10 @@ class Converter(ABC):
class NullConverter(Converter): class NullConverter(Converter):
"""
The default converter class
Does not change the value in any way, just passes them through
"""
def convert_to_form(self, value): def convert_to_form(self, value):
return value return value
@ -21,10 +25,25 @@ class NullConverter(Converter):
return value return value
class TextConverter(Converter):
"""
Converter class for text inputs
Does nothing more than to prevent the special python value "None" from appearing in the form
The string "None" should pass
"""
def convert_to_form(self, value):
if value is None:
return ""
return value
def convert_from_form(self, value):
return value
class OptionalConverter(Converter): class OptionalConverter(Converter):
""" """
Transforms a special form value to None Transforms a special form value to None
The default is look for an empty string, but this can be used to adopt to other types. The default is to look for an empty string, but this can be used to adopt to other types.
If the default is not found, the actual value is passed to the sub_converter for further transformation. If the default is not found, the actual value is passed to the sub_converter for further transformation.
useful for optional fields since None is not stored in the configuration useful for optional fields since None is not stored in the configuration
""" """
@ -61,7 +80,14 @@ class EnumConverter(Converter):
self.enumCls = enumCls self.enumCls = enumCls
def convert_to_form(self, value): def convert_to_form(self, value):
return None if value is None else self.enumCls(value).name if value is None:
return None
try:
return self.enumCls(value).name
# if the current value is not part of the enum, this will happen:
except ValueError:
# and this will restore the default
return None
def convert_from_form(self, value): def convert_from_form(self, value):
return self.enumCls[value].value return self.enumCls[value].value

View File

@ -0,0 +1,64 @@
from owrx.form.input import Input
from owrx.form.input.validator import Validator
from owrx.form.error import ValidationError
from owrx.config import Config
import logging
logger = logging.getLogger(__name__)
class LocationValidator(Validator):
def validate(self, key, value):
if "lat" in value and not -90 < value["lat"] < 90:
raise ValidationError(key, "Latitude out of range (-90 to 90)")
if "lon" in value and not -180 < value["lon"] < 180:
raise ValidationError(key, "Longitude out of range (-180 to 180)")
pass
class LocationInput(Input):
def __init__(self, id, label, validator: Validator = None):
if validator is None:
validator = LocationValidator()
super().__init__(id, label, validator=validator)
def render_input_group(self, value, errors):
return """
<div class="row {rowclass}">
{inputs}
</div>
{errors}
<div class="row">
<div class="col map-input" data-key="{key}" for="{id}"></div>
</div>
""".format(
id=self.id,
rowclass="is-invalid" if errors else "",
inputs=self.render_input(value, errors),
errors=self.render_errors(errors),
key=Config.get()["google_maps_api_key"],
)
def render_input(self, value, errors):
return "".join(self.render_sub_input(value, id, errors) for id in ["lat", "lon"])
def render_sub_input(self, value, id, errors):
return """
<div class="col">
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}"
step="any" {disabled}>
</div>
""".format(
id="{0}-{1}".format(self.id, id),
label=self.label,
classes=self.input_classes(errors),
value=value[id],
disabled="disabled" if self.disabled else "",
)
def parse(self, data):
value = {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}
if self.validator is not None:
self.validator.validate(self.id, value)
return {self.id: value}

View File

@ -4,23 +4,26 @@ from owrx.form.error import ValidationError
class Validator(ABC): class Validator(ABC):
@abstractmethod @abstractmethod
def validate(self, key, value): def validate(self, key, value) -> None:
pass pass
class RequiredValidator(Validator): class RequiredValidator(Validator):
def validate(self, key, value): def validate(self, key, value) -> None:
if value is None or value == "": if value is None or value == "":
raise ValidationError(key, "Field is required") raise ValidationError(key, "Field is required")
class RangeValidator(Validator): class RangeValidator(Validator):
def __init__(self, minValue, maxValue): def __init__(self, minValue, maxValue):
self.minValue = minValue self.minValue = minValue
self.maxValue = maxValue self.maxValue = maxValue
def validate(self, key, value): def validate(self, key, value) -> None:
if value is None or value == "": if value is None or value == "":
return # Ignore empty values return # Ignore empty values
n = float(value) n = float(value)
if n < self.minValue or n > self.maxValue: if n < self.minValue or n > self.maxValue:
raise ValidationError(key, 'Value must be between %s and %s'%(self.minValue, self.maxValue)) raise ValidationError(
key, "Value must be between {min} and {max}".format(min=self.minValue, max=self.maxValue)
)

View File

@ -103,11 +103,11 @@ class Js8Parser(AudioChopperParser):
if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid: if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid:
Map.getSharedInstance().updateLocation( Map.getSharedInstance().updateLocation(
frame.callsign, LocatorLocation(frame.grid), "JS8", band frame.source, LocatorLocation(frame.grid), "JS8", band
) )
ReportingEngine.getSharedInstance().spot( ReportingEngine.getSharedInstance().spot(
{ {
"callsign": frame.callsign, "source": frame.source,
"mode": "JS8", "mode": "JS8",
"locator": frame.grid, "locator": frame.grid,
"freq": freq + frame.freq, "freq": freq + frame.freq,

View File

@ -5,6 +5,11 @@ class Locator(object):
lat = coordinates["lat"] lat = coordinates["lat"]
lon = coordinates["lon"] lon = coordinates["lon"]
if not -90 < lat < 90:
raise ValueError("invalid latitude: {}".format(lat))
if not -180 < lon < 180:
raise ValueError("invalid longitude: {}".format(lon))
lon = lon + 180 lon = lon + 180
lat = lat + 90 lat = lat + 90

52
owrx/log/__init__.py Normal file
View File

@ -0,0 +1,52 @@
import threading
import os
from logging import Logger, Handler, LogRecord, Formatter
class LogPipe(threading.Thread):
def __init__(self, level: int, logger: Logger, prefix: str = ""):
threading.Thread.__init__(self)
self.daemon = False
self.level = level
self.logger = logger
self.prefix = prefix
self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead)
self.start()
def fileno(self):
return self.fdWrite
def run(self):
for line in iter(self.pipeReader.readline, ''):
self.logger.log(self.level, "{}: {}".format(self.prefix, line.strip('\n')))
self.pipeReader.close()
def close(self):
os.close(self.fdWrite)
class HistoryHandler(Handler):
handlers = {}
@staticmethod
def getHandler(name: str):
if name not in HistoryHandler.handlers:
HistoryHandler.handlers[name] = HistoryHandler()
return HistoryHandler.handlers[name]
def __init__(self, maxRecords: int = 200):
super().__init__()
self.history = []
self.maxRecords = maxRecords
self.setFormatter(Formatter(fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
def emit(self, record: LogRecord) -> None:
self.history.append(record)
# truncate
self.history = self.history[-self.maxRecords:]
def getFormattedHistory(self) -> str:
return "\n".join([self.format(r) for r in self.history])

View File

@ -61,13 +61,13 @@ class Map(object):
client.write_update( client.write_update(
[ [
{ {
"callsign": callsign, "source": record["source"],
"location": record["location"].__dict__(), "location": record["location"].__dict__(),
"lastseen": record["updated"].timestamp() * 1000, "lastseen": record["updated"].timestamp() * 1000,
"mode": record["mode"], "mode": record["mode"],
"band": record["band"].getName() if record["band"] is not None else None, "band": record["band"].getName() if record["band"] is not None else None,
} }
for (callsign, record) in self.positions.items() for record in self.positions.values()
] ]
) )
@ -77,14 +77,20 @@ class Map(object):
except ValueError: except ValueError:
pass pass
def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None): def _sourceToKey(self, source):
if "ssid" in source:
return "{callsign}-{ssid}".format(**source)
return source["callsign"]
def updateLocation(self, source, loc: Location, mode: str, band: Band = None):
ts = datetime.now() ts = datetime.now()
key = self._sourceToKey(source)
with self.positionsLock: with self.positionsLock:
self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band} self.positions[key] = {"source": source, "location": loc, "updated": ts, "mode": mode, "band": band}
self.broadcast( self.broadcast(
[ [
{ {
"callsign": callsign, "source": source,
"location": loc.__dict__(), "location": loc.__dict__(),
"lastseen": ts.timestamp() * 1000, "lastseen": ts.timestamp() * 1000,
"mode": mode, "mode": mode,
@ -93,17 +99,18 @@ class Map(object):
] ]
) )
def touchLocation(self, callsign): def touchLocation(self, source):
# not implemented on the client side yet, so do not use! # not implemented on the client side yet, so do not use!
ts = datetime.now() ts = datetime.now()
key = self._sourceToKey(source)
with self.positionsLock: with self.positionsLock:
if callsign in self.positions: if key in self.positions:
self.positions[callsign]["updated"] = ts self.positions[key]["updated"] = ts
self.broadcast([{"callsign": callsign, "lastseen": ts.timestamp() * 1000}]) self.broadcast([{"source": source, "lastseen": ts.timestamp() * 1000}])
def removeLocation(self, callsign): def removeLocation(self, key):
with self.positionsLock: with self.positionsLock:
del self.positions[callsign] del self.positions[key]
# TODO broadcast removal to clients # TODO broadcast removal to clients
def removeOldPositions(self): def removeOldPositions(self):
@ -111,9 +118,9 @@ class Map(object):
retention = timedelta(seconds=pm["map_position_retention_time"]) retention = timedelta(seconds=pm["map_position_retention_time"])
cutoff = datetime.now() - retention cutoff = datetime.now() - retention
to_be_removed = [callsign for (callsign, pos) in self.positions.items() if pos["updated"] < cutoff] to_be_removed = [key for (key, pos) in self.positions.items() if pos["updated"] < cutoff]
for callsign in to_be_removed: for key in to_be_removed:
self.removeLocation(callsign) self.removeLocation(key)
def rebuildPositions(self): def rebuildPositions(self):
logger.debug("rebuilding map storage; size before: %i", sys.getsizeof(self.positions)) logger.debug("rebuilding map storage; size before: %i", sys.getsizeof(self.positions))

View File

@ -129,7 +129,7 @@ class DigihamEnricher(Enricher, metaclass=ABCMeta):
callsign = self.getCallsign(meta) callsign = self.getCallsign(meta)
if callsign is not None and "lat" in meta and "lon" in meta: if callsign is not None and "lat" in meta and "lon" in meta:
loc = LatLngLocation(meta["lat"], meta["lon"]) loc = LatLngLocation(meta["lat"], meta["lon"])
Map.getSharedInstance().updateLocation(callsign, loc, mode, self.parser.getBand()) Map.getSharedInstance().updateLocation({"callsign": callsign}, loc, mode, self.parser.getBand())
return meta return meta
@abstractmethod @abstractmethod
@ -202,7 +202,7 @@ class DStarEnricher(DigihamEnricher):
if "ourcall" in meta: if "ourcall" in meta:
# send location info to map as well (it will show up with the correct symbol there!) # send location info to map as well (it will show up with the correct symbol there!)
loc = AprsLocation(data) loc = AprsLocation(data)
Map.getSharedInstance().updateLocation(meta["ourcall"], loc, "DPRS", self.parser.getBand()) Map.getSharedInstance().updateLocation({"callsign": meta["ourcall"]}, loc, "DPRS", self.parser.getBand())
except Exception: except Exception:
logger.exception("Error while parsing DPRS data") logger.exception("Error while parsing DPRS data")

View File

@ -120,6 +120,7 @@ class Modes(object):
WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]), WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]),
WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]), WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]),
WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]), WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]),
DigitalMode("msk144", "MSK144", requirements=["msk144"], underlying=["usb"], service=True),
Js8Mode("js8", "JS8Call"), Js8Mode("js8", "JS8Call"),
DigitalMode( DigitalMode(
"packet", "packet",

View File

@ -1,15 +1,37 @@
from csdr.module import PickleModule from csdr.module import PickleModule
from owrx.bands import Bandplan
from owrx.metrics import Metrics, CounterMetric
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class PocsagParser(PickleModule): class PocsagParser(PickleModule):
def __init__(self):
self.band = None
super().__init__()
def process(self, meta): def process(self, meta):
try: try:
if "address" in meta: if "address" in meta:
meta["address"] = int(meta["address"]) meta["address"] = int(meta["address"])
meta["mode"] = "Pocsag" meta["mode"] = "Pocsag"
self.pushDecode()
return meta return meta
except Exception: except Exception:
logger.exception("Exception while parsing Pocsag message") logger.exception("Exception while parsing Pocsag message")
def setDialFrequency(self, freq: int) -> None:
self.band = Bandplan.getSharedInstance().findBand(freq)
def pushDecode(self):
band = "unknown"
if self.band is not None:
band = self.band.getName()
name = "digiham.decodes.{band}.pocsag".format(band=band)
metrics = Metrics.getSharedInstance()
metric = metrics.getMetric(name)
if metric is None:
metric = CounterMetric()
metrics.addMetric(name, metric)
metric.inc()

View File

@ -15,10 +15,21 @@ logger = logging.getLogger(__name__)
class PskReporter(Reporter): class PskReporter(Reporter):
"""
This class implements the reporting interface to send received signals to pskreporter.info.
It interfaces with pskreporter as documented here: https://pskreporter.info/pskdev.html
"""
interval = 300 interval = 300
def getSupportedModes(self): def getSupportedModes(self):
return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65"] """
Supports all valid MODE and SUBMODE values from the ADIF standard.
Current version at the time of the last change:
https://www.adif.org/314/ADIF_314.htm#Mode_Enumeration
"""
return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W", "MSK144"]
def stop(self): def stop(self):
self.cancelTimer() self.cancelTimer()
@ -45,7 +56,7 @@ class PskReporter(Reporter):
self.timer.start() self.timer.start()
def spotEquals(self, s1, s2): def spotEquals(self, s1, s2):
keys = ["callsign", "timestamp", "locator", "mode", "msg"] keys = ["source", "timestamp", "locator", "mode", "msg"]
return reduce(and_, map(lambda key: s1[key] == s2[key], keys)) return reduce(and_, map(lambda key: s1[key] == s2[key], keys))
@ -94,27 +105,34 @@ class Uploader(object):
# filter out any erroneous encodes # filter out any erroneous encodes
encoded = [e for e in encoded if e is not None] encoded = [e for e in encoded if e is not None]
def chunks(l, n): def chunks(block, max_size):
"""Yield successive n-sized chunks from l.""" size = 0
for i in range(0, len(l), n): current = []
yield l[i : i + n] for r in block:
if size + len(r) > max_size:
yield current
current = []
size = 0
size += len(r)
current.append(r)
yield current
rHeader = self.getReceiverInformationHeader() rHeader = self.getReceiverInformationHeader()
rInfo = self.getReceiverInformation() rInfo = self.getReceiverInformation()
sHeader = self.getSenderInformationHeader() sHeader = self.getSenderInformationHeader()
packets = [] packets = []
# 50 seems to be a safe bet # 1200 bytes of sender data should keep the packet size below MTU for most cases
for chunk in chunks(encoded, 50): for chunk in chunks(encoded, 1200):
sInfo = self.getSenderInformation(chunk) sInfo = self.getSenderInformation(chunk)
length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo) length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo)
header = self.getHeader(length) header = self.getHeader(length)
packets.append(header + rHeader + sHeader + rInfo + sInfo) packets.append(header + rHeader + sHeader + rInfo + sInfo)
self.sequence = (self.sequence + len(chunk)) % (1 << 32)
return packets return packets
def getHeader(self, length): def getHeader(self, length):
self.sequence += 1
return bytes( return bytes(
# protocol version # protocol version
[0x00, 0x0A] [0x00, 0x0A]
@ -130,8 +148,8 @@ class Uploader(object):
def encodeSpot(self, spot): def encodeSpot(self, spot):
try: try:
return bytes( return bytes(
self.encodeString(spot["callsign"]) self.encodeString(spot["source"]["callsign"])
+ list(int(spot["freq"]).to_bytes(4, "big")) + list(int(spot["freq"]).to_bytes(5, "big"))
+ list(int(spot["db"]).to_bytes(1, "big", signed=True)) + list(int(spot["db"]).to_bytes(1, "big", signed=True))
+ self.encodeString(spot["mode"]) + self.encodeString(spot["mode"])
+ self.encodeString(spot["locator"]) + self.encodeString(spot["locator"])
@ -197,7 +215,7 @@ class Uploader(object):
# senderCallsign # senderCallsign
+ [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# frequency # frequency
+ [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x05, 0x00, 0x05, 0x00, 0x00, 0x76, 0x8F]
# sNR # sNR
+ [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F]
# mode # mode

View File

@ -56,7 +56,7 @@ class Worker(threading.Thread):
# FST4W does not have drift # FST4W does not have drift
"drift": spot["drift"] if "drift" in spot else 0, "drift": spot["drift"] if "drift" in spot else 0,
"tqrg": spot["freq"] / 1e6, "tqrg": spot["freq"] / 1e6,
"tcall": spot["callsign"], "tcall": spot["source"]["callsign"],
"tgrid": spot["locator"], "tgrid": spot["locator"],
"dbm": spot["dbm"], "dbm": spot["dbm"],
"version": openwebrx_version, "version": openwebrx_version,

View File

@ -122,6 +122,15 @@ class ServiceHandler(SdrSourceEventClient):
self.startupTimer.start() self.startupTimer.start()
def updateServices(self): def updateServices(self):
def addService(dial, source):
mode = dial["mode"]
frequency = dial["frequency"]
try:
service = self.setupService(mode, frequency, source)
self.services.append(service)
except Exception:
logger.exception("Error setting up service %s on frequency %d", mode, frequency)
with self.lock: with self.lock:
logger.debug("re-scheduling services due to sdr changes") logger.debug("re-scheduling services due to sdr changes")
self.stopServices() self.stopServices()
@ -146,7 +155,7 @@ class ServiceHandler(SdrSourceEventClient):
groups = self.optimizeResampling(dials, sr) groups = self.optimizeResampling(dials, sr)
if groups is None: if groups is None:
for dial in dials: for dial in dials:
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source)) addService(dial, self.source)
else: else:
for group in groups: for group in groups:
if len(group) > 1: if len(group) > 1:
@ -157,14 +166,14 @@ class ServiceHandler(SdrSourceEventClient):
resampler = Resampler(resampler_props, self.source) resampler = Resampler(resampler_props, self.source)
for dial in group: for dial in group:
self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler)) addService(dial, resampler)
# resampler goes in after the services since it must not be shutdown as long as the services are # resampler goes in after the services since it must not be shutdown as long as the services are
# still running # still running
self.services.append(resampler) self.services.append(resampler)
else: else:
dial = group[0] dial = group[0]
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source)) addService(dial, self.source)
def get_min_max(self, group): def get_min_max(self, group):
frequencies = sorted(group, key=lambda f: f["frequency"]) frequencies = sorted(group, key=lambda f: f["frequency"])
@ -279,11 +288,13 @@ class ServiceHandler(SdrSourceEventClient):
def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]: def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]:
if isinstance(mod, ServiceDemodulatorChain): if isinstance(mod, ServiceDemodulatorChain):
return mod return mod
# TODO add remaining modes
if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]:
from csdr.chain.digimodes import AudioChopperDemodulator from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
return AudioChopperDemodulator(mod, WsjtParser()) return AudioChopperDemodulator(mod, WsjtParser())
elif mod == "msk144":
from csdr.chain.digimodes import Msk144Demodulator
return Msk144Demodulator()
elif mod == "js8": elif mod == "js8":
from csdr.chain.digimodes import AudioChopperDemodulator from csdr.chain.digimodes import AudioChopperDemodulator
from owrx.js8 import Js8Parser from owrx.js8 import Js8Parser
@ -291,7 +302,8 @@ class ServiceHandler(SdrSourceEventClient):
elif mod == "packet": elif mod == "packet":
from csdr.chain.digimodes import PacketDemodulator from csdr.chain.digimodes import PacketDemodulator
return PacketDemodulator(service=True) return PacketDemodulator(service=True)
return None
raise ValueError("unsupported service modulation: {}".format(mod))
class Services(object): class Services(object):

View File

@ -16,8 +16,10 @@ from owrx.form.input import Input, TextInput, NumberInput, CheckboxInput, ModesI
from owrx.form.input.converter import OptionalConverter from owrx.form.input.converter import OptionalConverter
from owrx.form.input.device import GainInput, SchedulerInput, WaterfallLevelsInput from owrx.form.input.device import GainInput, SchedulerInput, WaterfallLevelsInput
from owrx.form.input.validator import RequiredValidator from owrx.form.input.validator import RequiredValidator
from owrx.form.input.converter import Converter
from owrx.form.section import OptionalSection from owrx.form.section import OptionalSection
from owrx.feature import FeatureDetector from owrx.feature import FeatureDetector
from owrx.log import LogPipe, HistoryHandler
from typing import List from typing import List
from enum import Enum from enum import Enum
@ -114,6 +116,10 @@ class SdrSource(ABC):
self.commandMapper = None self.commandMapper = None
self.tcpSource = None self.tcpSource = None
self.buffer = None self.buffer = None
self.logger = logger.getChild(id) if id is not None else logger
self.logger.addHandler(HistoryHandler.getHandler(self.logger.name))
self.stdoutPipe = None
self.stderrPipe = None
self.props = PropertyStack() self.props = PropertyStack()
@ -185,17 +191,17 @@ class SdrSource(ABC):
for id, p in self.props["profiles"].items(): for id, p in self.props["profiles"].items():
props.replaceLayer(0, p) props.replaceLayer(0, p)
if "center_freq" not in props: if "center_freq" not in props:
logger.warning('Profile "%s" does not specify a center_freq', id) self.logger.warning('Profile "%s" does not specify a center_freq', id)
continue continue
if "samp_rate" not in props: if "samp_rate" not in props:
logger.warning('Profile "%s" does not specify a samp_rate', id) self.logger.warning('Profile "%s" does not specify a samp_rate', id)
continue continue
if "start_freq" in props: if "start_freq" in props:
start_freq = props["start_freq"] start_freq = props["start_freq"]
srh = props["samp_rate"] / 2 srh = props["samp_rate"] / 2
center_freq = props["center_freq"] center_freq = props["center_freq"]
if start_freq < center_freq - srh or start_freq > center_freq + srh: if start_freq < center_freq - srh or start_freq > center_freq + srh:
logger.warning('start_freq for profile "%s" is out of range', id) self.logger.warning('start_freq for profile "%s" is out of range', id)
def isAlwaysOn(self): def isAlwaysOn(self):
return "always-on" in self.props and self.props["always-on"] return "always-on" in self.props and self.props["always-on"]
@ -225,11 +231,12 @@ class SdrSource(ABC):
return [self.getCommandMapper().map(self.getCommandValues())] return [self.getCommandMapper().map(self.getCommandValues())]
def activateProfile(self, profile_id): def activateProfile(self, profile_id):
logger.debug("activating profile {0} for {1}".format(profile_id, self.getId()))
try: try:
profile_name = self.getProfiles()[profile_id]["name"]
self.logger.debug("activating profile \"%s\" for \"%s\"", profile_name, self.getName())
self.profileCarousel.switch(profile_id) self.profileCarousel.switch(profile_id)
except KeyError: except KeyError:
logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.getId()) self.logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.getId())
def getId(self): def getId(self):
return self.id return self.id
@ -249,10 +256,13 @@ class SdrSource(ABC):
def getPort(self): def getPort(self):
return self.port return self.port
def _getTcpSourceFormat(self):
return Format.COMPLEX_FLOAT
def _getTcpSource(self): def _getTcpSource(self):
with self.modificationLock: with self.modificationLock:
if self.tcpSource is None: if self.tcpSource is None:
self.tcpSource = TcpSource(self.port, Format.COMPLEX_FLOAT) self.tcpSource = TcpSource(self.port, self._getTcpSourceFormat())
return self.tcpSource return self.tcpSource
def getBuffer(self): def getBuffer(self):
@ -280,23 +290,37 @@ class SdrSource(ABC):
try: try:
self.preStart() self.preStart()
except Exception: except Exception:
logger.exception("Exception during preStart()") self.logger.exception("Exception during preStart()")
cmd = self.getCommand() cmd = self.getCommand()
cmd = [c for c in cmd if c is not None] cmd = [c for c in cmd if c is not None]
self.stdoutPipe = LogPipe(logging.INFO, self.logger, "STDOUT")
self.stderrPipe = LogPipe(logging.WARNING, self.logger, "STDERR")
# don't use shell mode for commands without piping # don't use shell mode for commands without piping
if len(cmd) > 1: if len(cmd) > 1:
# multiple commands with pipes # multiple commands with pipes
cmd = "|".join(cmd) cmd = "|".join(cmd)
self.process = subprocess.Popen(cmd, shell=True, start_new_session=True) self.process = subprocess.Popen(
cmd,
shell=True,
start_new_session=True,
stdout=self.stdoutPipe,
stderr=self.stderrPipe
)
else: else:
# single command # single command
cmd = cmd[0] cmd = cmd[0]
# start_new_session can go as soon as there's no piped commands left # start_new_session can go as soon as there's no piped commands left
# the os.killpg call must be replaced with something more reasonable at the same time # the os.killpg call must be replaced with something more reasonable at the same time
self.process = subprocess.Popen(shlex.split(cmd), start_new_session=True) self.process = subprocess.Popen(
logger.info("Started sdr source: " + cmd) shlex.split(cmd),
start_new_session=True,
stdout=self.stdoutPipe,
stderr=self.stderrPipe
)
self.logger.info("Started sdr source: " + cmd)
available = False available = False
failed = False failed = False
@ -304,9 +328,13 @@ class SdrSource(ABC):
def wait_for_process_to_end(): def wait_for_process_to_end():
nonlocal failed nonlocal failed
rc = self.process.wait() rc = self.process.wait()
logger.debug("shut down with RC={0}".format(rc)) self.logger.debug("shut down with RC={0}".format(rc))
self.process = None self.process = None
self.monitor = None self.monitor = None
self.stdoutPipe.close()
self.stdoutPipe = None
self.stderrPipe.close()
self.stderrPipe = None
if self.getState() is SdrSourceState.RUNNING: if self.getState() is SdrSourceState.RUNNING:
self.fail() self.fail()
else: else:
@ -337,7 +365,7 @@ class SdrSource(ABC):
try: try:
self.postStart() self.postStart()
except Exception: except Exception:
logger.exception("Exception during postStart()") self.logger.exception("Exception during postStart()")
failed = True failed = True
if failed: if failed:
@ -371,11 +399,14 @@ class SdrSource(ABC):
self.monitor.join(10) self.monitor.join(10)
# if the monitor is still running, the process still hasn't ended, so kill it # if the monitor is still running, the process still hasn't ended, so kill it
if self.monitor: if self.monitor:
logger.warning("source has not shut down normally within 10 seconds, sending SIGKILL") self.logger.warning("source has not shut down normally within 10 seconds, sending SIGKILL")
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL) os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
except ProcessLookupError: except ProcessLookupError:
# been killed by something else, ignore # been killed by something else, ignore
pass pass
except AttributeError:
# self.process has been overwritten by the monitor since we checked it, which is fine
pass
if self.monitor: if self.monitor:
self.monitor.join() self.monitor.join()
if self.tcpSource is not None: if self.tcpSource is not None:
@ -476,6 +507,32 @@ class SdrDeviceDescriptionMissing(Exception):
pass pass
class SdrDeviceTypeConverter(Converter):
def convert_to_form(self, value):
# local import due to circular dependendies
types = SdrDeviceDescription.getTypes()
if value in types:
return types[value]
return value
def convert_from_form(self, value):
return None
class SdrDeviceTypeDisplay(Input):
"""
Not an input per se, just an element that can display the SDR device type in the web config
"""
def __init__(self, id, label):
super().__init__(id, label, disabled=True)
def defaultConverter(self):
return SdrDeviceTypeConverter()
def parse(self, data):
return {}
class SdrDeviceDescription(object): class SdrDeviceDescription(object):
@staticmethod @staticmethod
def getByType(sdr_type: str) -> "SdrDeviceDescription": def getByType(sdr_type: str) -> "SdrDeviceDescription":
@ -484,7 +541,7 @@ class SdrDeviceDescription(object):
module = __import__("owrx.source.{0}".format(sdr_type), fromlist=[className]) module = __import__("owrx.source.{0}".format(sdr_type), fromlist=[className])
cls = getattr(module, className) cls = getattr(module, className)
return cls() return cls()
except (ModuleNotFoundError, AttributeError): except (ImportError, AttributeError):
raise SdrDeviceDescriptionMissing("Device description for type {} not available".format(sdr_type)) raise SdrDeviceDescriptionMissing("Device description for type {} not available".format(sdr_type))
@staticmethod @staticmethod
@ -511,6 +568,14 @@ class SdrDeviceDescription(object):
""" """
return None return None
def supportsPpm(self):
"""
can be overridden if the device does not support configuring PPM correction
:return: bool
"""
return True
def getDeviceInputs(self) -> List[Input]: def getDeviceInputs(self) -> List[Input]:
keys = self.getDeviceMandatoryKeys() + self.getDeviceOptionalKeys() keys = self.getDeviceMandatoryKeys() + self.getDeviceOptionalKeys()
return [TextInput("name", "Device name", validator=RequiredValidator())] + [ return [TextInput("name", "Device name", validator=RequiredValidator())] + [
@ -525,6 +590,7 @@ class SdrDeviceDescription(object):
def getInputs(self) -> List[Input]: def getInputs(self) -> List[Input]:
return [ return [
SdrDeviceTypeDisplay("type", "Device type"),
CheckboxInput("enabled", "Enable this device", converter=OptionalConverter(defaultFormValue=True)), CheckboxInput("enabled", "Enable this device", converter=OptionalConverter(defaultFormValue=True)),
GainInput("rf_gain", "Device gain", self.hasAgc()), GainInput("rf_gain", "Device gain", self.hasAgc()),
NumberInput( NumberInput(
@ -562,11 +628,10 @@ class SdrDeviceDescription(object):
return True return True
def getDeviceMandatoryKeys(self): def getDeviceMandatoryKeys(self):
return ["name", "enabled"] return ["name", "type", "enabled"]
def getDeviceOptionalKeys(self): def getDeviceOptionalKeys(self):
return [ keys = [
"ppm",
"always-on", "always-on",
"services", "services",
"rf_gain", "rf_gain",
@ -574,6 +639,9 @@ class SdrDeviceDescription(object):
"waterfall_levels", "waterfall_levels",
"scheduler", "scheduler",
] ]
if self.supportsPpm():
keys += ["ppm"]
return keys
def getProfileMandatoryKeys(self): def getProfileMandatoryKeys(self):
return ["name", "center_freq", "samp_rate", "start_freq", "start_mod"] return ["name", "center_freq", "samp_rate", "start_freq", "start_mod"]

View File

@ -23,6 +23,12 @@ class AirspyDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self): def getName(self):
return "Airspy R2 or Mini" return "Airspy R2 or Mini"
def supportsPpm(self):
# not supported by the device API
# frequency calibration can be done with separate tools and will be persisted on the device.
# see discussion here: https://groups.io/g/openwebrx/topic/79360293
return False
def getInputs(self) -> List[Input]: def getInputs(self) -> List[Input]:
return super().getInputs() + [ return super().getInputs() + [
BiasTeeInput(), BiasTeeInput(),

View File

@ -9,3 +9,7 @@ class AirspyhfSource(SoapyConnectorSource):
class AirspyhfDeviceDescription(SoapyConnectorDeviceDescription): class AirspyhfDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self): def getName(self):
return "Airspy HF+ or Discovery" return "Airspy HF+ or Discovery"
def supportsPpm(self):
# not currently supported by the SoapySDR module.
return False

11
owrx/source/bladerf.py Normal file
View File

@ -0,0 +1,11 @@
from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription
class BladerfSource(SoapyConnectorSource):
def getDriver(self):
return "bladerf"
class BladerfDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self):
return "Blade RF"

View File

@ -6,10 +6,6 @@ from owrx.command import Flag, Option
from typing import List from typing import List
from owrx.form.input import Input, NumberInput, CheckboxInput from owrx.form.input import Input, NumberInput, CheckboxInput
import logging
logger = logging.getLogger(__name__)
class ConnectorSource(SdrSource): class ConnectorSource(SdrSource):
def __init__(self, id, props): def __init__(self, id, props):
@ -40,7 +36,7 @@ class ConnectorSource(SdrSource):
for prop, value in changes.items(): for prop, value in changes.items():
if value is PropertyDeleted: if value is PropertyDeleted:
value = None value = None
logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value)) self.logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value))
self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode()) self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode())
def onPropertyChange(self, changes): def onPropertyChange(self, changes):
@ -56,7 +52,7 @@ class ConnectorSource(SdrSource):
self.sendControlMessage(changes) self.sendControlMessage(changes)
def postStart(self): def postStart(self):
logger.debug("opening control socket...") self.logger.debug("opening control socket...")
self.controlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.controlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.controlSocket.connect(("localhost", self.controlPort)) self.controlSocket.connect(("localhost", self.controlPort))

View File

@ -5,14 +5,14 @@ from typing import Optional
from pycsdr.modules import Buffer from pycsdr.modules import Buffer
from pycsdr.types import Format from pycsdr.types import Format
import logging
logger = logging.getLogger(__name__)
class DirectSource(SdrSource, metaclass=ABCMeta): class DirectSource(SdrSource, metaclass=ABCMeta):
def __init__(self, id, props):
self._conversion = None
super().__init__(id, props)
def onPropertyChange(self, changes): def onPropertyChange(self, changes):
logger.debug("restarting sdr source due to property changes: {0}".format(changes)) self.logger.debug("restarting sdr source due to property changes: {0}".format(changes))
self.stop() self.stop()
self.sleepOnRestart() self.sleepOnRestart()
self.start() self.start()
@ -48,6 +48,10 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
def getFormatConversion(self) -> Optional[Chain]: def getFormatConversion(self) -> Optional[Chain]:
return None return None
def _getTcpSourceFormat(self):
conversion = self.getFormatConversion()
return Format.COMPLEX_FLOAT if conversion is None else conversion.getInputFormat()
# override this in subclasses, if necessary # override this in subclasses, if necessary
def sleepOnRestart(self): def sleepOnRestart(self):
pass pass
@ -57,12 +61,12 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
source = self._getTcpSource() source = self._getTcpSource()
buffer = Buffer(source.getOutputFormat()) buffer = Buffer(source.getOutputFormat())
source.setWriter(buffer) source.setWriter(buffer)
conversion = self.getFormatConversion() self._conversion = self.getFormatConversion()
if conversion is not None: if self._conversion is not None:
conversion.setReader(buffer.getReader()) self._conversion.setReader(buffer.getReader())
# this one must be COMPLEX_FLOAT # this one must be COMPLEX_FLOAT
buffer = Buffer(Format.COMPLEX_FLOAT) buffer = Buffer(Format.COMPLEX_FLOAT)
conversion.setWriter(buffer) self._conversion.setWriter(buffer)
self.buffer = buffer self.buffer = buffer
return self.buffer return self.buffer

View File

@ -1,14 +1,14 @@
from owrx.command import Option from owrx.command import Option
from owrx.source.direct import DirectSource, DirectSourceDeviceDescription from owrx.source.direct import DirectSource, DirectSourceDeviceDescription
from owrx.log import LogPipe
from subprocess import Popen from subprocess import Popen
from csdr.chain import Chain from csdr.chain import Chain
from pycsdr.modules import Convert, Gain from pycsdr.modules import Convert, Gain
from pycsdr.types import Format from pycsdr.types import Format
from typing import List
from owrx.form.input import Input, TextInput
import logging import logging
logger = logging.getLogger(__name__)
class FifiSdrSource(DirectSource): class FifiSdrSource(DirectSource):
def getCommandMapper(self): def getCommandMapper(self):
@ -27,11 +27,19 @@ class FifiSdrSource(DirectSource):
return Chain([Convert(Format.COMPLEX_SHORT, Format.COMPLEX_FLOAT), Gain(Format.COMPLEX_FLOAT, 5.0)]) return Chain([Convert(Format.COMPLEX_SHORT, Format.COMPLEX_FLOAT), Gain(Format.COMPLEX_FLOAT, 5.0)])
def sendRockProgFrequency(self, frequency): def sendRockProgFrequency(self, frequency):
process = Popen(["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1e6)]) stdoutPipe = LogPipe(logging.DEBUG, self.logger, "STDOUT")
stderrPipe = LogPipe(logging.DEBUG, self.logger, "STDERR")
process = Popen(
["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1e6)],
stdout=stdoutPipe,
stderr=stderrPipe
)
process.communicate() process.communicate()
rc = process.wait() rc = process.wait()
if rc != 0: if rc != 0:
logger.warning("rockprog failed to set frequency; rc=%i", rc) self.logger.warning("rockprog failed to set frequency; rc=%i", rc)
stdoutPipe.close()
stderrPipe.close()
def preStart(self): def preStart(self):
values = self.getCommandValues() values = self.getCommandValues()
@ -45,3 +53,19 @@ class FifiSdrSource(DirectSource):
class FifiSdrDeviceDescription(DirectSourceDeviceDescription): class FifiSdrDeviceDescription(DirectSourceDeviceDescription):
def getName(self): def getName(self):
return "FiFi SDR" return "FiFi SDR"
def supportsPpm(self):
# not currently mapped, and it's unclear how this should be sent to the device
return False
def getInputs(self) -> List[Input]:
return super().getInputs() + [
TextInput(
"device",
"Device identifier",
infotext="Alsa audio device identifier",
),
]
def getDeviceOptionalKeys(self):
return super().getDeviceOptionalKeys() + ["device"]

View File

@ -18,6 +18,11 @@ class HackrfDeviceDescription(SoapyConnectorDeviceDescription):
def getName(self): def getName(self):
return "HackRF" return "HackRF"
def supportsPpm(self):
# not implemented by the SoapySDR module.
# see discussion here: https://groups.io/g/openwebrx/topic/78339109
return False
def getInputs(self) -> List[Input]: def getInputs(self) -> List[Input]:
return super().getInputs() + [BiasTeeInput()] return super().getInputs() + [BiasTeeInput()]

View File

@ -53,6 +53,10 @@ class PerseussdrDeviceDescription(DirectSourceDeviceDescription):
def getName(self): def getName(self):
return "Perseus SDR" return "Perseus SDR"
def supportsPpm(self):
# not currently mapped, and not available as an option to "perseustest"
return False
def getInputs(self) -> List[Input]: def getInputs(self) -> List[Input]:
return super().getInputs() + [ return super().getInputs() + [
DropdownInput("attenuator", "Attenuator", options=AttenuatorOptions), DropdownInput("attenuator", "Attenuator", options=AttenuatorOptions),

View File

@ -3,14 +3,10 @@ from pycsdr.modules import Buffer, FirDecimate, Shift
from pycsdr.types import Format from pycsdr.types import Format
from csdr.chain import Chain from csdr.chain import Chain
import logging
logger = logging.getLogger(__name__)
class Resampler(SdrSource): class Resampler(SdrSource):
def onPropertyChange(self, changes): def onPropertyChange(self, changes):
logger.warning("Resampler is unable to handle property changes: {0}".format(changes)) self.logger.warning("Resampler is unable to handle property changes: {0}".format(changes))
def __init__(self, props, sdr): def __init__(self, props, sdr):
sdrProps = sdr.getProps() sdrProps = sdr.getProps()
@ -41,7 +37,7 @@ class Resampler(SdrSource):
super().stop() super().stop()
def activateProfile(self, profile_id=None): def activateProfile(self, profile_id=None):
logger.warning("Resampler does not support setting profiles") self.logger.warning("Resampler does not support setting profiles")
pass pass
def validateProfiles(self): def validateProfiles(self):

View File

@ -40,6 +40,10 @@ class RundsDeviceDescription(ConnectorDeviceDescription):
def getName(self): def getName(self):
return "R&S device using EB200 or Ammos protocol" return "R&S device using EB200 or Ammos protocol"
def supportsPpm(self):
# currently not implemented in the connector
return False
def getInputs(self) -> List[Input]: def getInputs(self) -> List[Input]:
return super().getInputs() + [ return super().getInputs() + [
RemoteInput(), RemoteInput(),

View File

@ -1,5 +1,5 @@
from distutils.version import LooseVersion from distutils.version import LooseVersion
_versionstring = "1.2.0-dev" _versionstring = "1.3.0-dev"
looseversion = LooseVersion(_versionstring) looseversion = LooseVersion(_versionstring)
openwebrx_version = "v{0}".format(looseversion) openwebrx_version = "v{0}".format(looseversion)

View File

@ -30,10 +30,6 @@ class Drained(WebSocketException):
pass pass
class WebSocketClosed(WebSocketException):
pass
class Handler(ABC): class Handler(ABC):
@abstractmethod @abstractmethod
def handleTextMessage(self, connection, message: str): def handleTextMessage(self, connection, message: str):
@ -66,6 +62,7 @@ class WebSocketConnection(object):
self.setMessageHandler(messageHandler) self.setMessageHandler(messageHandler)
(self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False) (self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False)
self.open = True self.open = True
self.socketError = False
self.sendLock = threading.Lock() self.sendLock = threading.Lock()
headers = {key.lower(): value for key, value in self.handler.headers.items()} headers = {key.lower(): value for key, value in self.handler.headers.items()}
@ -118,8 +115,6 @@ class WebSocketConnection(object):
return bytes([ws_first_byte, size]) return bytes([ws_first_byte, size])
def send(self, data): def send(self, data):
if not self.open:
raise WebSocketClosed()
# convenience # convenience
if type(data) == dict: if type(data) == dict:
# allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway. # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway.
@ -142,27 +137,30 @@ class WebSocketConnection(object):
for i in range(0, len(input), n): for i in range(0, len(input), n):
yield input[i: i + n] yield input[i: i + n]
try: with self.sendLock:
with self.sendLock: if self.socketError:
for chunk in chunks(data_to_send, 1024): logger.warning("_sendBytes() after socket error, ignoring")
(_, write, _) = select.select([], [self.handler.wfile], [], 10) else:
if self.handler.wfile in write: try:
written = self.handler.wfile.write(chunk) for chunk in chunks(data_to_send, 1024):
if written != len(chunk): (_, write, _) = select.select([], [self.handler.wfile], [], 10)
logger.error("incomplete write! closing socket!") if self.handler.wfile in write:
self.close() written = self.handler.wfile.write(chunk)
if written != len(chunk):
logger.error("incomplete write! closing socket!")
self.close(socketError=True)
break
else:
logger.debug("socket not returned from select; closing")
self.close(socketError=True)
break break
else: # these exception happen when the socket is closed
logger.debug("socket not returned from select; closing") except OSError:
self.close() logger.exception("OSError while writing data")
break self.close(socketError=True)
# these exception happen when the socket is closed except ValueError:
except OSError: logger.exception("ValueError while writing data")
logger.exception("OSError while writing data") self.close(socketError=True)
self.close()
except ValueError:
logger.exception("ValueError while writing data")
self.close()
def interrupt(self): def interrupt(self):
if self.interruptPipeSend is None: if self.interruptPipeSend is None:
@ -180,10 +178,13 @@ class WebSocketConnection(object):
self.messageHandler.handleClose() self.messageHandler.handleClose()
self.cancelPing() self.cancelPing()
logger.debug("websocket loop ended; sending close frame") if self.socketError:
logger.debug("websocket closed in error, skipping close frame")
else:
logger.debug("websocket loop ended; sending close frame")
header = self.get_header(0, OPCODE_CLOSE) header = self.get_header(0, OPCODE_CLOSE)
self._sendBytes(header) self._sendBytes(header)
try: try:
WebSocketConnection.connections.remove(self) WebSocketConnection.connections.remove(self)
@ -245,9 +246,11 @@ class WebSocketConnection(object):
available = False available = False
except IncompleteRead: except IncompleteRead:
logger.warning("incomplete read on websocket; closing connection") logger.warning("incomplete read on websocket; closing connection")
self.socketError = True
self.open = False self.open = False
except OSError: except OSError:
logger.exception("OSError while reading data; closing connection") logger.exception("OSError while reading data; closing connection")
self.socketError = True
self.open = False self.open = False
self.interruptPipeSend.close() self.interruptPipeSend.close()
@ -262,7 +265,10 @@ class WebSocketConnection(object):
self.interruptPipeRecv.close() self.interruptPipeRecv.close()
self.interruptPipeRecv = None self.interruptPipeRecv = None
def close(self): def close(self, socketError: bool = False):
# only set flag if it is True
if socketError:
self.socketError = True
if not self.open: if not self.open:
return return
self.open = False self.open = False
@ -270,7 +276,9 @@ class WebSocketConnection(object):
def cancelPing(self): def cancelPing(self):
if self.pingTimer: if self.pingTimer:
self.pingTimer.cancel() old = self.pingTimer
self.pingTimer = None
old.cancel()
def resetPing(self): def resetPing(self):
self.cancelPing() self.cancelPing()

View File

@ -245,6 +245,17 @@ class Q65Profile(WsjtProfile):
return ["jt9", "--q65", "-p", str(self.interval), "-b", self.mode.name, "-d", str(self.decoding_depth()), file] return ["jt9", "--q65", "-p", str(self.interval), "-b", self.mode.name, "-d", str(self.decoding_depth()), file]
class Msk144Profile(WsjtProfile):
def getMode(self):
return "MSK144"
def getInterval(self):
return 15
def decoder_commandline(self, file):
return None
class WsjtParser(AudioChopperParser): class WsjtParser(AudioChopperParser):
def parse(self, profile: WsjtProfile, freq: int, raw_msg: bytes): def parse(self, profile: WsjtProfile, freq: int, raw_msg: bytes):
try: try:
@ -276,9 +287,9 @@ class WsjtParser(AudioChopperParser):
out["interval"] = profile.getInterval() out["interval"] = profile.getInterval()
self.pushDecode(mode, band) self.pushDecode(mode, band)
if "callsign" in out and "locator" in out: if "source" in out and "locator" in out:
Map.getSharedInstance().updateLocation( Map.getSharedInstance().updateLocation(
out["callsign"], LocatorLocation(out["locator"]), mode, band out["source"], LocatorLocation(out["locator"]), mode, band
) )
ReportingEngine.getSharedInstance().spot(out) ReportingEngine.getSharedInstance().spot(out)
@ -342,8 +353,8 @@ class QsoMessageParser(MessageParser):
# this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very
# likely this just means roger roger goodbye. # likely this just means roger roger goodbye.
if m.group(3) == "RR73": if m.group(3) == "RR73":
return {"callsign": m.group(1)} return {"source": {"callsign": m.group(1)}}
return {"callsign": m.group(1), "locator": m.group(3)} return {"source": {"callsign": m.group(1)}, "locator": m.group(3)}
# Used in propagation reporting / beacon modes (WSPR / FST4W) # Used in propagation reporting / beacon modes (WSPR / FST4W)
@ -354,7 +365,7 @@ class BeaconMessageParser(MessageParser):
m = BeaconMessageParser.wspr_splitter_pattern.match(msg) m = BeaconMessageParser.wspr_splitter_pattern.match(msg)
if m is None: if m is None:
return {} return {}
return {"callsign": m.group(1), "locator": m.group(2), "dbm": m.group(3)} return {"source": {"callsign": m.group(1)}, "locator": m.group(2), "dbm": m.group(3)}
class Jt9Decoder(Decoder): class Jt9Decoder(Decoder):
@ -366,6 +377,8 @@ class Jt9Decoder(Decoder):
# '0003 -4 0.4 1762 # CQ R2ABM KO85' # '0003 -4 0.4 1762 # CQ R2ABM KO85'
# fst4 sample # fst4 sample
# '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV' # '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV'
# MSK144 sample
# '221602 8 0.4 1488 & K1JT WA4CQG EM72'
msg, timestamp = self.parse_timestamp(msg) msg, timestamp = self.parse_timestamp(msg)
wsjt_msg = msg[17:53].strip() wsjt_msg = msg[17:53].strip()

View File

@ -14,22 +14,8 @@ setup(
version=str(looseversion), version=str(looseversion),
packages=find_namespace_packages( packages=find_namespace_packages(
include=[ include=[
"owrx", "owrx*",
"owrx.source", "csdr*",
"owrx.service",
"owrx.controllers",
"owrx.controllers.settings",
"owrx.property",
"owrx.form",
"owrx.form.input",
"owrx.config",
"owrx.reporting",
"owrx.audio",
"owrx.admin",
"owrx.aprs",
"csdr",
"csdr.chain",
"csdr.module",
"htdocs", "htdocs",
] ]
), ),